From 362e29eff9e2f770cf50635540aa922c2b2d708f Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sat, 22 Jul 2023 01:03:02 +0200 Subject: [PATCH] Version 6.0.8 --- BeatSaberPlus/BeatSaberPlus.csproj | 356 +- BeatSaberPlus/BeatSaberPlus.csproj.user | Bin 303 -> 636 bytes .../Animation/AnimationControllerInstance.cs | 14 +- .../CP_SDK/Animation/AnimationLoader.cs | 6 +- .../CP_SDK/Animation/WEBP/WEBPDecoder.cs | 2 +- .../{BSPConfig.cs => CP_SDK/CPConfig.cs} | 8 +- .../CP_SDK/Chat/ChatImageProvider.cs | 12 +- BeatSaberPlus/CP_SDK/Chat/ChatModSettings.cs | 18 +- .../CP_SDK/Chat/Interfaces/EBadgeType.cs | 8 + .../CP_SDK/Chat/Interfaces/IChatBadge.cs | 14 +- .../CP_SDK/Chat/Interfaces/IChatChannel.cs | 14 +- .../Chat/Interfaces/IChatChannelPointEvent.cs | 16 +- .../CP_SDK/Chat/Interfaces/IChatEmote.cs | 14 +- .../Chat/Interfaces/IChatMessageHandler.cs | 7 - .../Chat/Interfaces/IChatResourceData.cs | 12 +- .../Chat/Interfaces/IChatResourceProvider.cs | 9 +- .../CP_SDK/Chat/Interfaces/IChatService.cs | 17 + .../IChatServiceResourceProvider.cs | 54 + .../Chat/Interfaces/IChatSubscriptionEvent.cs | 10 +- .../CP_SDK/Chat/Interfaces/IChatUser.cs | 20 +- .../CP_SDK/Chat/Models/ChatResourceData.cs | 8 +- BeatSaberPlus/CP_SDK/Chat/Models/Emoji.cs | 14 +- BeatSaberPlus/CP_SDK/Chat/Models/EmoteType.cs | 8 - BeatSaberPlus/CP_SDK/Chat/Models/ImageRect.cs | 10 - .../CP_SDK/Chat/Models/Twitch/TwitchBadge.cs | 8 +- .../Chat/Models/Twitch/TwitchChannel.cs | 16 +- .../Models/Twitch/TwitchChannelPointEvent.cs | 16 +- .../Chat/Models/Twitch/TwitchCheermoteData.cs | 28 +- .../Chat/Models/Twitch/TwitchCheermoteTier.cs | 15 + .../CP_SDK/Chat/Models/Twitch/TwitchEmote.cs | 19 +- .../CP_SDK/Chat/Models/Twitch/TwitchHelix.cs | 26 +- .../Chat/Models/Twitch/TwitchMessage.cs | 33 +- .../Chat/Models/Twitch/TwitchRoomstate.cs | 21 +- .../Models/Twitch/TwitchSubscriptionEvent.cs | 10 +- .../CP_SDK/Chat/Models/Twitch/TwitchUser.cs | 28 +- .../CP_SDK/Chat/Resources/index.html | 2 + BeatSaberPlus/CP_SDK/Chat/Service.cs | 69 +- .../ChatPlexGradientNamesDataProvider.cs | 210 + .../CP_SDK/Chat/Services/ChatServiceBase.cs | 1 - .../Chat/Services/ChatServiceMultiplexer.cs | 6 + .../CP_SDK/Chat/Services/RelayChatService.cs | 204 + .../Chat/Services/RelayChatServiceProtocol.cs | 16 + .../Chat/Services/Twitch/7TVDataProvider.cs | 221 +- .../Chat/Services/Twitch/BTTVDataProvider.cs | 22 +- .../Chat/Services/Twitch/FFZDataProvider.cs | 22 +- .../Services/Twitch/TwitchBadgeProvider.cs | 110 +- .../Twitch/TwitchCheermoteProvider.cs | 52 +- .../Services/Twitch/TwitchDataProvider.cs | 75 +- .../Chat/Services/Twitch/TwitchHelix.cs | 296 +- .../Chat/Services/Twitch/TwitchJSValidate.js | 4 +- .../Services/Twitch/TwitchMessageParser.cs | 24 +- .../Chat/Services/Twitch/TwitchService.cs | 63 +- .../CP_SDK/Chat/Utilities/7TVUtils.cs | 60 + BeatSaberPlus/CP_SDK/Chat/WebApp.cs | 2 +- BeatSaberPlus/CP_SDK/ChatPlexSDK.cs | 114 +- BeatSaberPlus/CP_SDK/Config/JsonConfig.cs | 5 +- .../JsonConverters/QuaternionConverter.cs | 41 + .../CP_SDK/Logging/MelonLoaderLogger.cs | 43 + .../CP_SDK/Misc/FastCancellationToken.cs | 29 + .../Checksum/Adler32.cs | 163 + .../Checksum/BZip2Crc.cs | 171 + .../ICSharpCode.SharpZipLib/Checksum/Crc32.cs | 173 + .../Checksum/CrcUtilities.cs | 158 + .../Checksum/IChecksum.cs | 51 + .../Core/ByteOrderUtils.cs | 130 + .../ICSharpCode.SharpZipLib/Core/EmptyRefs.cs | 17 + .../Core/Exceptions/SharpZipBaseException.cs | 58 + .../Exceptions/StreamDecodingException.cs | 50 + .../Exceptions/StreamUnsupportedException.cs | 49 + .../UnexpectedEndOfStreamException.cs | 49 + .../Exceptions/ValueOutOfRangeException.cs | 66 + .../Core/FileSystemScanner.cs | 545 ++ .../Core/INameTransform.cs | 22 + .../Core/IScanFilter.cs | 15 + .../Core/InvalidNameException.cs | 53 + .../Core/NameFilter.cs | 284 + .../Core/PathFilter.cs | 318 ++ .../ICSharpCode.SharpZipLib/Core/PathUtils.cs | 57 + .../Core/StreamUtils.cs | 295 + .../Core/StringBuilderPool.cs | 22 + .../Encryption/PkzipClassic.cs | 487 ++ .../Encryption/ZipAESStream.cs | 230 + .../Encryption/ZipAESTransform.cs | 178 + .../Zip/Compression/Deflater.cs | 604 ++ .../Zip/Compression/DeflaterConstants.cs | 146 + .../Zip/Compression/DeflaterEngine.cs | 946 ++++ .../Zip/Compression/DeflaterHuffman.cs | 959 ++++ .../Zip/Compression/DeflaterPending.cs | 17 + .../Zip/Compression/Inflater.cs | 887 +++ .../Zip/Compression/InflaterDynHeader.cs | 151 + .../Zip/Compression/InflaterHuffmanTree.cs | 237 + .../Zip/Compression/PendingBuffer.cs | 268 + .../Streams/DeflaterOutputStream.cs | 560 ++ .../Streams/InflaterInputStream.cs | 713 +++ .../Zip/Compression/Streams/OutputWindow.cs | 220 + .../Compression/Streams/StreamManipulator.cs | 298 + .../ICSharpCode.SharpZipLib/Zip/FastZip.cs | 1002 ++++ .../Zip/IEntryFactory.cs | 67 + .../Zip/WindowsNameTransform.cs | 266 + .../Zip/ZipConstants.cs | 514 ++ .../Zip/ZipEncryptionMethod.cs | 28 + .../ICSharpCode.SharpZipLib/Zip/ZipEntry.cs | 1157 ++++ .../Zip/ZipEntryExtensions.cs | 32 + .../Zip/ZipEntryFactory.cs | 375 ++ .../Zip/ZipException.cs | 54 + .../Zip/ZipExtraData.cs | 974 ++++ .../ICSharpCode.SharpZipLib/Zip/ZipFile.cs | 4947 +++++++++++++++++ .../ICSharpCode.SharpZipLib/Zip/ZipFormat.cs | 598 ++ .../Zip/ZipHelperStream.cs | 0 .../Zip/ZipInputStream.cs | 776 +++ .../Zip/ZipNameTransform.cs | 313 ++ .../Zip/ZipOutputStream.cs | 1028 ++++ .../ICSharpCode.SharpZipLib/Zip/ZipStrings.cs | 260 + BeatSaberPlus/CP_SDK/Misc/Time.cs | 67 +- BeatSaberPlus/CP_SDK/ModuleBase.cs | 109 +- BeatSaberPlus/CP_SDK/Network/APIClient.cs | 335 -- BeatSaberPlus/CP_SDK/Network/APIResponse.cs | 60 - BeatSaberPlus/CP_SDK/Network/IWebClient.cs | 51 + BeatSaberPlus/CP_SDK/Network/JsonRPCClient.cs | 120 + BeatSaberPlus/CP_SDK/Network/JsonRPCResult.cs | 14 + .../CP_SDK/Network/LiteNetLib/NetManager.cs | 4 +- .../CP_SDK/Network/LiteNetLib/NetSocket.cs | 12 +- .../CP_SDK/Network/LiteNetLib/NetUtils.cs | 2 +- BeatSaberPlus/CP_SDK/Network/RateLimitInfo.cs | 202 +- BeatSaberPlus/CP_SDK/Network/WebClient.cs | 290 + BeatSaberPlus/CP_SDK/Network/WebClientEx.cs | 285 + .../CP_SDK/Network/WebClient_Unity.cs | 207 - BeatSaberPlus/CP_SDK/Network/WebResponse.cs | 100 +- .../CP_SDK/Network/WebSocketClient.cs | 33 +- BeatSaberPlus/CP_SDK/Pool/CollectionPool.cs | 38 + BeatSaberPlus/CP_SDK/Pool/ListPool.cs | 12 + BeatSaberPlus/CP_SDK/Pool/MTCollectionPool.cs | 4 +- .../CP_SDK/UI/Components/CColorInput.cs | 58 + .../CP_SDK/UI/Components/CDropdown.cs | 59 + .../CP_SDK/UI/Components/CFLayout.cs | 106 + .../CP_SDK/UI/Components/CFloatingPanel.cs | 519 ++ .../CP_SDK/UI/Components/CGLayout.cs | 120 + .../CP_SDK/UI/Components/CHLayout.cs | 15 + .../CP_SDK/UI/Components/CIconButton.cs | 87 + BeatSaberPlus/CP_SDK/UI/Components/CImage.cs | 130 + .../CP_SDK/UI/Components/CPrimaryButton.cs | 10 + .../CP_SDK/UI/Components/CSecondaryButton.cs | 10 + BeatSaberPlus/CP_SDK/UI/Components/CSlider.cs | 128 + .../CP_SDK/UI/Components/CTabControl.cs | 52 + BeatSaberPlus/CP_SDK/UI/Components/CText.cs | 135 + .../CP_SDK/UI/Components/CTextInput.cs | 64 + .../UI/Components/CTextSegmentedControl.cs | 58 + BeatSaberPlus/CP_SDK/UI/Components/CToggle.cs | 58 + .../CP_SDK/UI/Components/CVLayout.cs | 15 + .../CP_SDK/UI/Components/CVScrollView.cs | 81 + BeatSaberPlus/CP_SDK/UI/Components/CVVList.cs | 10 + .../UI/Components/Generics/CHOrVLayout.cs | 215 + .../UI/Components/Generics/CPOrSButton.cs | 163 + .../CP_SDK/UI/Components/Generics/CVXList.cs | 127 + BeatSaberPlus/CP_SDK/UI/Data/IListCell.cs | 184 + BeatSaberPlus/CP_SDK/UI/Data/IListItem.cs | 60 + .../CP_SDK/UI/Data/ListCellPrefabs.cs | 36 + BeatSaberPlus/CP_SDK/UI/Data/TextListCell.cs | 54 + BeatSaberPlus/CP_SDK/UI/Data/TextListItem.cs | 59 + .../DefaultComponents/DefaultCColorInput.cs | 203 + .../UI/DefaultComponents/DefaultCDropdown.cs | 240 + .../UI/DefaultComponents/DefaultCFLayout.cs | 431 ++ .../DefaultCFloatingPanel.cs | 117 + .../UI/DefaultComponents/DefaultCGLayout.cs | 52 + .../UI/DefaultComponents/DefaultCHLayout.cs | 59 + .../DefaultComponents/DefaultCIconButton.cs | 172 + .../UI/DefaultComponents/DefaultCImage.cs | 49 + .../DefaultCPrimaryButton.cs | 196 + .../DefaultCSecondaryButton.cs | 196 + .../UI/DefaultComponents/DefaultCSlider.cs | 764 +++ .../DefaultComponents/DefaultCTabControl.cs | 152 + .../UI/DefaultComponents/DefaultCText.cs | 56 + .../UI/DefaultComponents/DefaultCTextInput.cs | 240 + .../DefaultCTextSegmentedControl.cs | 220 + .../UI/DefaultComponents/DefaultCToggle.cs | 433 ++ .../UI/DefaultComponents/DefaultCVLayout.cs | 59 + .../DefaultComponents/DefaultCVScrollView.cs | 378 ++ .../UI/DefaultComponents/DefaultCVVList.cs | 415 ++ .../Subs/SubStackLayoutGroup.cs | 126 + .../Subs/SubToggleWithCallbacks.cs | 45 + .../Subs/SubVScrollIndicator.cs | 73 + .../Subs/SubVScrollViewContent.cs | 85 + .../UI/DefaultFactories/DefaultColorInput.cs | 28 + .../DefaultDropdownFactory.cs | 28 + .../DefaultFactories/DefaultFLayoutFactory.cs | 28 + .../DefaultFloatingPanelFactory.cs | 28 + .../DefaultFactories/DefaultGLayoutFactory.cs | 28 + .../DefaultFactories/DefaultHLayoutFactory.cs | 28 + .../DefaultIconButtonFactory.cs | 28 + .../DefaultFactories/DefaultImageFactory.cs | 28 + .../DefaultPrimaryButtonFactory.cs | 28 + .../DefaultSecondaryButtonFactory.cs | 28 + .../DefaultFactories/DefaultSliderFactory.cs | 28 + .../DefaultTabControlFactory.cs | 28 + .../UI/DefaultFactories/DefaultTextFactory.cs | 28 + .../DefaultTextInputFactory.cs | 28 + .../DefaultTextSegmentedControlFactory.cs | 28 + .../DefaultFactories/DefaultToggleFactory.cs | 28 + .../DefaultFactories/DefaultVLayoutFactory.cs | 28 + .../DefaultVScrollViewFactory.cs | 28 + .../DefaultFactories/DefaultVVListFactory.cs | 28 + .../UI/FactoryInterfaces/IColorInput.cs | 18 + .../UI/FactoryInterfaces/IDropDownFactory.cs | 18 + .../UI/FactoryInterfaces/IFLayoutFactory.cs | 18 + .../IFloatingPanelFactory.cs | 19 + .../UI/FactoryInterfaces/IGLayoutFactory.cs | 18 + .../UI/FactoryInterfaces/IHLayoutFactory.cs | 18 + .../FactoryInterfaces/IIconButtonFactory.cs | 18 + .../UI/FactoryInterfaces/IImageFactory.cs | 18 + .../IPrimaryButtonFactory.cs | 18 + .../ISecondaryButtonFactory.cs | 18 + .../UI/FactoryInterfaces/ISliderFactory.cs | 18 + .../FactoryInterfaces/ITabControlFactory.cs | 18 + .../UI/FactoryInterfaces/ITextFactory.cs | 18 + .../UI/FactoryInterfaces/ITextInputFactory.cs | 18 + .../ITextSegmentedControlFactory.cs | 18 + .../UI/FactoryInterfaces/IToggleFactory.cs | 18 + .../UI/FactoryInterfaces/IVLayoutFactory.cs | 18 + .../FactoryInterfaces/IVScrollViewFactory.cs | 18 + .../UI/FactoryInterfaces/IVVListFactory.cs | 18 + BeatSaberPlus/CP_SDK/UI/FlowCoordinator.cs | 47 + .../FlowCoordinators/MainFlowCoordinator.cs | 90 + BeatSaberPlus/CP_SDK/UI/IFlowCoordinator.cs | 190 + BeatSaberPlus/CP_SDK/UI/IModal.cs | 41 + BeatSaberPlus/CP_SDK/UI/IScreen.cs | 34 + BeatSaberPlus/CP_SDK/UI/IViewController.cs | 160 + BeatSaberPlus/CP_SDK/UI/LoadingProgressBar.cs | 15 +- BeatSaberPlus/CP_SDK/UI/ModButton.cs | 69 + BeatSaberPlus/CP_SDK/UI/ModMenu.cs | 172 + BeatSaberPlus/CP_SDK/UI/Modals/ColorPicker.cs | 182 + .../CP_SDK/UI/Modals/Confirmation.cs | 101 + BeatSaberPlus/CP_SDK/UI/Modals/Dropdown.cs | 116 + BeatSaberPlus/CP_SDK/UI/Modals/Keyboard.cs | 363 ++ BeatSaberPlus/CP_SDK/UI/Modals/Loading.cs | 104 + BeatSaberPlus/CP_SDK/UI/Modals/Message.cs | 86 + BeatSaberPlus/CP_SDK/UI/ScreenSystem.cs | 214 + BeatSaberPlus/CP_SDK/UI/Tooltip.cs | 97 + BeatSaberPlus/CP_SDK/UI/UISystem.cs | 216 + BeatSaberPlus/CP_SDK/UI/ValueFormatters.cs | 54 + BeatSaberPlus/CP_SDK/UI/ViewController.cs | 458 ++ BeatSaberPlus/CP_SDK/UI/Views/MainLeftView.cs | 68 + BeatSaberPlus/CP_SDK/UI/Views/MainMainView.cs | 99 + .../CP_SDK/UI/Views/MainRightView.cs | 33 + BeatSaberPlus/CP_SDK/UI/Views/ModMenuView.cs | 109 + .../UI/Views}/SettingsLeftView.cs | 43 +- .../CP_SDK/UI/Views/SettingsMainView.cs | 170 + .../CP_SDK/UI/Views/SettingsRightView.cs | 243 + .../CP_SDK/UI/Views/TopNavigationView.cs | 88 + .../EnhancedImageParticleEmitterGroup.cs | 4 +- .../EnhancedImageParticleEmitterManager.cs | 16 +- BeatSaberPlus/CP_SDK/Unity/EnhancedImage.cs | 57 +- .../EnhancedImageParticleMaterialProvider.cs | 112 +- .../EnhancedImageParticleSystemProvider.cs | 11 + .../CP_SDK/Unity/Extensions/ColorU.cs | 180 +- .../CP_SDK/Unity/Extensions/GameObjectU.cs | 43 + .../CP_SDK/Unity/Extensions/StringU.cs | 62 - BeatSaberPlus/CP_SDK/Unity/FontManager.cs | 399 +- .../CP_SDK/Unity/MTCoroutineStarter.cs | 18 + .../CP_SDK/Unity/MTMainThreadInvoker.cs | 18 + BeatSaberPlus/CP_SDK/Unity/MTThreadInvoker.cs | 9 +- .../CP_SDK/Unity/OpenType/CollectionHeader.cs | 31 +- .../CP_SDK/Unity/OpenType/NameRecord.cs | 68 + .../CP_SDK/Unity/OpenType/NumericHelpers.cs | 41 - .../CP_SDK/Unity/OpenType/OffsetTable.cs | 30 +- .../Unity/OpenType/OpenTypeCollection.cs | 84 +- .../OpenType/OpenTypeCollectionReader.cs | 24 +- .../CP_SDK/Unity/OpenType/OpenTypeFont.cs | 187 +- .../Unity/OpenType/OpenTypeFontReader.cs | 102 +- .../Unity/OpenType/OpenTypeNameTable.cs | 138 + .../CP_SDK/Unity/OpenType/OpenTypeReader.cs | 71 +- .../CP_SDK/Unity/OpenType/OpenTypeTable.cs | 166 +- .../CP_SDK/Unity/OpenType/OpenTypeTag.cs | 90 +- .../CP_SDK/Unity/OpenType/TableRecord.cs | 14 +- .../CP_SDK/Unity/PersistentSingleton.cs | 11 + .../CP_SDK/Unity/PersistentSingletonInput.cs | 90 + BeatSaberPlus/CP_SDK/Unity/RaycastResultU.cs | 63 + BeatSaberPlus/CP_SDK/Unity/SpriteU.cs | 54 +- BeatSaberPlus/CP_SDK/Unity/Texture2DU.cs | 123 +- BeatSaberPlus/CP_SDK/Unity/TextureRaw.cs | 255 + BeatSaberPlus/CP_SDK/VoiceAttack/Service.cs | 2 +- .../XRInput/InputInternals/FakeButtonState.cs | 25 + .../XRInput/InputInternals/FakeMouseState.cs | 95 + .../FrameCachedPhysicsRaycaster.cs | 109 + BeatSaberPlus/CP_SDK/XRInput/XRController.cs | 63 + .../CP_SDK/XRInput/XRGraphicRaycaster.cs | 227 + BeatSaberPlus/CP_SDK/XRInput/XRInputSystem.cs | 597 ++ .../CP_SDK/XRInput/XRLaserPointer.cs | 104 + .../CP_SDK/XUI/Generics/IXUIBindable.cs | 17 + .../CP_SDK/XUI/Generics/IXUIElement.cs | 35 + .../CP_SDK/XUI/Generics/IXUIElementReady.cs | 22 + .../XUI/Generics/IXUIElementWithChilds.cs | 71 + .../CP_SDK/XUI/Generics/XUIHOrVLayout.cs | 200 + .../CP_SDK/XUI/Generics/XUIHOrVSpacer.cs | 106 + .../CP_SDK/XUI/Generics/XUIPOrSButton.cs | 169 + BeatSaberPlus/CP_SDK/XUI/Templates.cs | 159 + BeatSaberPlus/CP_SDK/XUI/XUIColorInput.cs | 130 + BeatSaberPlus/CP_SDK/XUI/XUIDropDown.cs | 137 + BeatSaberPlus/CP_SDK/XUI/XUIFLayout.cs | 148 + BeatSaberPlus/CP_SDK/XUI/XUIGLayout.cs | 167 + BeatSaberPlus/CP_SDK/XUI/XUIHLayout.cs | 28 + BeatSaberPlus/CP_SDK/XUI/XUIHSpacer.cs | 28 + BeatSaberPlus/CP_SDK/XUI/XUIIconButton.cs | 160 + BeatSaberPlus/CP_SDK/XUI/XUIImage.cs | 164 + BeatSaberPlus/CP_SDK/XUI/XUIPrimaryButton.cs | 33 + .../CP_SDK/XUI/XUISecondaryButton.cs | 33 + BeatSaberPlus/CP_SDK/XUI/XUISlider.cs | 176 + BeatSaberPlus/CP_SDK/XUI/XUITabControl.cs | 142 + BeatSaberPlus/CP_SDK/XUI/XUIText.cs | 165 + BeatSaberPlus/CP_SDK/XUI/XUITextInput.cs | 138 + .../CP_SDK/XUI/XUITextSegmentedControl.cs | 126 + BeatSaberPlus/CP_SDK/XUI/XUIToggle.cs | 120 + BeatSaberPlus/CP_SDK/XUI/XUIVLayout.cs | 28 + BeatSaberPlus/CP_SDK/XUI/XUIVScrollView.cs | 100 + BeatSaberPlus/CP_SDK/XUI/XUIVSpacer.cs | 28 + BeatSaberPlus/CP_SDK/XUI/XUIVVList.cs | 153 + .../_Resources/ChatPlexLogoLoading.webp | Bin 0 -> 324094 bytes .../_Resources/ChatPlexLogoTransparent.png | Bin 0 -> 7002 bytes BeatSaberPlus/CP_SDK/_Resources/UIButton.png | Bin 0 -> 970 bytes .../CP_SDK/_Resources/UIColorPickerFBG.png | Bin 0 -> 177 bytes .../CP_SDK/_Resources/UIColorPickerHBG.png | Bin 0 -> 429 bytes .../CP_SDK/_Resources/UIColorPickerSBG.png | Bin 0 -> 341 bytes .../CP_SDK/_Resources/UIColorPickerVBG.png | Bin 0 -> 467 bytes .../CP_SDK/_Resources/UIDownArrow.png | Bin 0 -> 459 bytes .../CP_SDK/_Resources/UIIconGear.png | Bin 0 -> 5127 bytes .../CP_SDK/_Resources/UIIconLocked.png | Bin 0 -> 2342 bytes .../CP_SDK/_Resources/UIIconUnlocked.png | Bin 0 -> 2349 bytes BeatSaberPlus/CP_SDK/_Resources/UIRectBG.png | Bin 0 -> 188 bytes BeatSaberPlus/CP_SDK/_Resources/UIRoundBG.png | Bin 0 -> 363 bytes .../CP_SDK/_Resources/UIRoundRectLeftBG.png | Bin 0 -> 355 bytes .../CP_SDK/_Resources/UIRoundRectRightBG.png | Bin 0 -> 337 bytes .../CP_SDK/_Resources/UIRoundSmoothFrame.png | Bin 0 -> 691 bytes .../CP_SDK/_Resources/UISliderBG.png | Bin 0 -> 172 bytes .../CP_SDK/_Resources/UISliderHandle.png | Bin 0 -> 300 bytes BeatSaberPlus/Plugin.cs | 195 +- BeatSaberPlus/Properties/AssemblyInfo.cs | 4 +- BeatSaberPlus/SDK/BSPModuleBase.cs | 39 - BeatSaberPlus/SDK/Chat/Service.cs | 2 +- BeatSaberPlus/SDK/Game/BeatMaps/MapDetail.cs | 19 +- BeatSaberPlus/SDK/Game/BeatMaps/MapVersion.cs | 10 +- BeatSaberPlus/SDK/Game/BeatMapsClient.cs | 62 +- BeatSaberPlus/SDK/Game/LevelData.cs | 11 + BeatSaberPlus/SDK/Game/LevelSelection.cs | 24 +- BeatSaberPlus/SDK/Game/Levels.cs | 45 +- BeatSaberPlus/SDK/Game/Logic.cs | 65 +- BeatSaberPlus/SDK/Game/PlayerAvatarPicture.cs | 161 + BeatSaberPlus/SDK/Game/Scoring.cs | 39 +- BeatSaberPlus/SDK/Game/UserPlatform.cs | 7 +- BeatSaberPlus/SDK/UI/BSMLSettingFormartter.cs | 105 - BeatSaberPlus/SDK/UI/Backgroundable.cs | 46 - .../SDK/UI/CP_SDK_UI_IViewControllerBridge.cs | 181 + BeatSaberPlus/SDK/UI/ColorSetting.cs | 48 - BeatSaberPlus/SDK/UI/Data/SongListCell.cs | 134 + .../SDK/UI/Data/SongListController.cs | 9 + BeatSaberPlus/SDK/UI/Data/SongListItem.cs | 457 ++ .../SDK/UI/DataSource/SimpleTextList.cs | 119 - BeatSaberPlus/SDK/UI/DataSource/SongList.cs | 600 -- .../SDK/UI/DataSource/SongListCustom.cs | 163 - .../BS_CFloatingPanel.cs | 168 + .../Subs/SubFloatingPanelMover.cs | 143 + .../Subs/SubFloatingPanelMoverHandle.cs | 27 + .../BS_FloatingPanelFactory.cs | 27 + BeatSaberPlus/SDK/UI/DropDownListSetting.cs | 60 - BeatSaberPlus/SDK/UI/GameFont.cs | 43 + ...Control.cs => HMUIIconSegmentedControl.cs} | 32 +- ...Control.cs => HMUITextSegmentedControl.cs} | 10 +- BeatSaberPlus/SDK/UI/HMUIUIUtils.cs | 97 + ...rdinator.cs => HMUIViewFlowCoordinator.cs} | 69 +- BeatSaberPlus/SDK/UI/IHMUIViewController.cs | 146 + BeatSaberPlus/SDK/UI/IncrementSetting.cs | 50 - .../SDK/UI/Internal/BSMLPrimaryButtonTag.cs | 16 - BeatSaberPlus/SDK/UI/LevelDetail.cs | 222 +- BeatSaberPlus/SDK/UI/ListSetting.cs | 44 - BeatSaberPlus/SDK/UI/ModalView.cs | 76 - .../SDK/UI/Patches/BSMLColorSetting.cs | 35 - .../SDK/UI/Patches/PSimpleTextDropdown.cs | 27 - BeatSaberPlus/SDK/UI/Patches/PVRPointer.cs | 33 + BeatSaberPlus/SDK/UI/SliderSetting.cs | 155 - BeatSaberPlus/SDK/UI/ToggleSetting.cs | 90 - .../SDK/UI/VerticalIconSegmentedControl.cs | 42 - BeatSaberPlus/SDK/UI/ViewController.cs | 590 +- BeatSaberPlus/SDK/UI/{ => __OLD__}/Button.cs | 0 BeatSaberPlus/SDK/Unity/MaterialU.cs | 37 - BeatSaberPlus/SDK/Unity/ShaderU.cs | 34 - BeatSaberPlus/UI/InfoView.bsml | 35 - BeatSaberPlus/UI/InfoView.cs | 73 - BeatSaberPlus/UI/MainView.bsml | 15 - BeatSaberPlus/UI/MainView.cs | 86 - BeatSaberPlus/UI/MainViewFlowCoordinator.cs | 88 - BeatSaberPlus/UI/SettingsLeftView.bsml | 13 - BeatSaberPlus/UI/SettingsRightView.bsml | 75 - BeatSaberPlus/UI/SettingsRightView.cs | 290 - BeatSaberPlus/UI/SettingsView.bsml | 11 - BeatSaberPlus/UI/SettingsView.cs | 93 - BeatSaberPlus/manifest.json | 10 +- .../{Plugin.cs => BSIPA.cs} | 9 +- .../BeatSaberPlus_Chat.csproj | 137 +- .../BeatSaberPlus_Chat.csproj.user | Bin 622 -> 636 bytes Modules/BeatSaberPlus_Chat/CConfig.cs | 92 - Modules/BeatSaberPlus_Chat/Chat.cs | 965 ---- .../ChatPlexMod_Chat/CConfig.cs | 127 + .../ChatPlexMod_Chat/Chat.cs | 721 +++ .../Components/ChatImage.cs | 0 .../Components/ChatMessageText.cs | 4 +- .../Components/ChatMessageWidget.cs | 82 +- .../Extensions/EnhancedFontInfo.cs | 11 +- .../{ => ChatPlexMod_Chat}/Logger.cs | 0 .../ChatPlexMod_Chat/Resources/ViewerIcon.png | Bin 0 -> 2746 bytes .../UI/ChatFloatingPanelView.cs | 654 +++ .../UI/Data/ChatUserListItem.cs | 52 + .../UI/HypeTrainFloatingPanelView.cs | 185 + .../UI/ModerationLeftView.cs} | 90 +- .../ChatPlexMod_Chat/UI/ModerationMainView.cs | 120 + .../UI/ModerationRightView.cs | 204 + .../UI/ModerationShortcutsMainView.cs | 153 + .../UI/ModerationViewFlowCoordinator.cs | 82 + .../UI/PollFloatingPanelView.cs} | 254 +- .../UI/PredictionFloatingPanelView.cs} | 298 +- .../ChatPlexMod_Chat/UI/SettingsLeftView.cs | 113 + .../ChatPlexMod_Chat/UI/SettingsMainView.cs | 157 + .../ChatPlexMod_Chat/UI/SettingsRightView.cs | 139 + .../UI/StatusFloatingPanelView.cs | 118 + .../Utils/ChatMessageBuilder.cs | 0 .../Properties/AssemblyInfo.cs | 5 +- .../BeatSaberPlus_Chat/Resources/Locked.png | Bin 3479 -> 0 bytes .../BeatSaberPlus_Chat/Resources/Settings.png | Bin 8938 -> 0 bytes .../UI/ChatFloatingWindow.bsml | 4 - .../UI/ChatFloatingWindow.cs | 189 - .../UI/ChatFloatingWindow_Events.cs | 320 -- .../UI/ChatFloatingWindow_Logic.cs | 325 -- .../UI/HypeTrainFloatingWindow.bsml | 5 - .../UI/HypeTrainFloatingWindow.cs | 181 - .../BeatSaberPlus_Chat/UI/ModerationLeft.bsml | 26 - .../BeatSaberPlus_Chat/UI/ModerationMain.bsml | 14 - .../BeatSaberPlus_Chat/UI/ModerationMain.cs | 181 - .../UI/ModerationRight.bsml | 25 - .../BeatSaberPlus_Chat/UI/ModerationRight.cs | 350 -- .../UI/ModerationShortcut.bsml | 34 - .../UI/ModerationShortcut.cs | 272 - .../UI/ModerationViewFlowCoordinator.cs | 113 - .../UI/PollFloatingWindow.bsml | 44 - .../UI/PredictionFloatingWindow.bsml | 39 - Modules/BeatSaberPlus_Chat/UI/Settings.bsml | 54 - Modules/BeatSaberPlus_Chat/UI/Settings.cs | 132 - .../BeatSaberPlus_Chat/UI/SettingsLeft.bsml | 36 - Modules/BeatSaberPlus_Chat/UI/SettingsLeft.cs | 105 - .../BeatSaberPlus_Chat/UI/SettingsRight.bsml | 54 - .../BeatSaberPlus_Chat/UI/SettingsRight.cs | 128 - Modules/BeatSaberPlus_Chat/manifest.json | 9 +- .../{Plugin.cs => BSIPA.cs} | 9 +- .../BeatSaberPlus_ChatEmoteRain.csproj | 64 +- .../BeatSaberPlus_ChatEmoteRain.csproj.user | Bin 622 -> 636 bytes .../CERConfig.cs | 51 + .../ChatEmoteRain.cs | 152 +- .../{ => ChatPlexMod_ChatEmoteRain}/Logger.cs | 0 .../Resources/PreviewMaterial.bundle | Bin .../UI/SettingsLeftView.cs | 90 + .../UI/SettingsMainView.cs | 515 ++ .../UI/SettingsRightView.cs | 102 + .../UI/Widgets/EmitterWidget.cs | 211 + .../Properties/AssemblyInfo.cs | 4 +- .../UI/EmitterWidget.bsml | 79 - .../UI/EmitterWidget.cs | 173 - .../UI/Settings.bsml | 134 - .../UI/Settings.cs | 597 -- .../UI/SettingsLeft.bsml | 19 - .../UI/SettingsLeft.cs | 90 - .../UI/SettingsRight.bsml | 51 - .../UI/SettingsRight.cs | 137 - .../BeatSaberPlus_ChatEmoteRain/manifest.json | 9 +- .../Actions/Camera2.cs | 291 - .../Actions/Chat.cs | 241 - .../Actions/EmoteRain.cs | 280 - .../Actions/Event.cs | 215 - .../Actions/GamePlay.cs | 1387 ----- .../Actions/Misc.cs | 262 - .../Actions/NoteTweaker.cs | 109 - .../Actions/OBS.cs | 1158 ---- .../Actions/SongChartVisualizer.cs | 89 - .../Actions/Twitch.cs | 220 - .../Actions/Views/Camera2_SwitchToScene.bsml | 9 - .../Actions/Views/Camera2_ToggleCamera.bsml | 10 - .../Actions/Views/Chat_SendMessage.bsml | 21 - .../Actions/Views/Chat_ToggleVisibility.bsml | 6 - .../Actions/Views/EmoteRain_CustomRain.bsml | 24 - .../Views/EmoteRain_EmoteBombRain.bsml | 17 - .../Actions/Views/Event_ExecuteDummy.bsml | 9 - .../Actions/Views/Event_Toggle.bsml | 9 - .../Views/GamePlay_ChangeBombColor.bsml | 9 - .../Views/GamePlay_ChangeBombScale.bsml | 11 - .../Actions/Views/GamePlay_ChangeDebris.bsml | 7 - .../Views/GamePlay_ChangeLightIntensity.bsml | 11 - .../Views/GamePlay_ChangeMusicVolume.bsml | 11 - .../Views/GamePlay_ChangeNoteColors.bsml | 10 - .../Views/GamePlay_ChangeNoteScale.bsml | 11 - .../Actions/Views/GamePlay_Pause.bsml | 7 - .../Views/GamePlay_SpawnBombPatterns.bsml | 68 - .../Views/GamePlay_SpawnSquatWalls.bsml | 8 - .../Actions/Views/GamePlay_ToggleHUD.bsml | 6 - .../Actions/Views/GamePlay_ToggleLights.bsml | 12 - .../Actions/Views/Misc_Delay.bsml | 16 - .../Actions/Views/Misc_PlaySound.bsml | 19 - .../Views/NoteTweaker_SwitchProfile.bsml | 7 - .../Actions/Views/OBS_RenameLastRecord.bsml | 20 - .../Views/OBS_SetRecordFilenameFormat.bsml | 20 - .../Views/OBS_SwitchPreviewToScene.bsml | 11 - .../Actions/Views/OBS_SwitchToScene.bsml | 11 - .../Actions/Views/OBS_ToggleSource.bsml | 17 - .../Actions/Views/OBS_ToggleSourceAudio.bsml | 17 - .../Actions/Views/OBS_ToggleStudioMode.bsml | 8 - .../Actions/Views/OBS_Transition.bsml | 15 - .../SongChartVisualizer_ToggleVisibility.bsml | 6 - .../Actions/Views/Twitch_AddMarker.bsml | 20 - .../{Plugin.cs => BSIPA.cs} | 7 +- .../BeatSaber/Actions/Camera2.cs | 283 + .../BeatSaber/Actions/GamePlay.cs | 1607 ++++++ .../BeatSaber/Actions/NoteTweaker.cs | 98 + .../BeatSaber/Actions/SongChartVisualizer.cs | 70 + .../BeatSaber/Conditions/ChatRequest.cs | 281 + .../BeatSaber/Conditions/GamePlay.cs | 209 + .../BeatSaber/Enums/BeatmapModType.cs | 43 + .../BeatSaber/Enums/LevelType.cs | 43 + .../BeatSaber/Enums/QueueStatus.cs | 37 + .../BeatSaber/Enums/ValueSource.cs | 41 + .../BeatSaber/Events/LevelEnded.cs | 111 + .../BeatSaber/Events/LevelPaused.cs | 96 + .../BeatSaber/Events/LevelResumed.cs | 96 + .../BeatSaber/Events/LevelStarted.cs | 96 + .../BeatSaber/Manager.cs | 230 + .../BeatSaber/ModPresence.cs | 17 + .../BeatSaber/Models/Actions/Camera2.cs | 35 + .../BeatSaber/Models/Actions/GamePlay.cs | 250 + .../BeatSaber/Models/Actions/NoteTweaker.cs | 26 + .../Models/Actions/SongChartVisualizer.cs | 24 + .../Models/Conditions/ChatRequest.cs | 85 + .../BeatSaber/Models/Conditions/GamePlay.cs | 24 + .../BeatSaber/ModulePresence.cs | 40 + .../BeatSaberPlus_ChatIntegrations.csproj | 280 +- ...BeatSaberPlus_ChatIntegrations.csproj.user | Bin 622 -> 636 bytes .../ChatIntegrations.cs | 517 -- .../Actions/Chat.cs | 244 + .../Actions/EmoteRain.cs | 300 + .../Actions/Event.cs | 209 + .../Actions/Misc.cs | 317 ++ .../Actions/OBS.cs | 1326 +++++ .../Actions/Twitch.cs | 171 + .../CIConfig.cs | 6 +- .../ChatIntegrations.cs | 721 +++ .../ChatIntegrations_Database.cs | 61 +- .../ChatIntegrations_Events.cs | 69 +- .../Conditions/Bits.cs | 62 + .../Conditions/Event.cs | 234 + .../Conditions/Misc.cs | 128 + .../Conditions/OBS.cs | 322 ++ .../Conditions/Subscription.cs | 129 + .../Conditions/User.cs | 92 + .../Enums/ChangeType.cs | 39 + .../Enums/Comparison.cs | 68 + .../Enums/EVisibility.cs | 8 + .../Enums/Toggle.cs | 39 + .../Enums/TwitchSubscribtionPlanType.cs | 41 + .../Enums/Visibility.cs | 37 + .../Events/ChatBits.cs | 94 + .../Events/ChatCommand.cs | 159 + .../Events/ChatFollow.cs | 89 + .../Events/ChatPointsReward.cs | 471 ++ .../Events/ChatRaid.cs | 92 + .../Events/ChatSubscription.cs | 100 + .../Events/Dummy.cs | 91 + .../Events/VoiceAttackCommand.cs | 136 + .../Interfaces/ETriggerType.cs} | 4 +- .../Interfaces/EValueType.cs} | 4 +- .../Interfaces/IAction.cs | 96 + .../Interfaces/IActionBase.cs | 53 + .../Interfaces/ICondition.cs | 85 + .../Interfaces/IConditionBase.cs | 53 + .../Interfaces/IEvent.cs | 247 + .../Interfaces/IEventBase.cs | 322 ++ .../Interfaces/IUIConfigurable.cs | 73 + .../Logger.cs | 2 +- .../Models/Action.cs | 32 + .../Models/Actions/Chat.cs | 5 +- .../Models/Actions/EmoteRain.cs | 6 +- .../Models/Actions/Event.cs | 5 +- .../Models/Actions/Misc.cs | 5 +- .../Models/Actions/OBS.cs | 137 + .../Models/Condition.cs | 2 +- .../Models/Conditions/Bits.cs | 29 + .../Models/Conditions/Events.cs | 2 +- .../Models/Conditions/Misc.cs | 2 +- .../Models/Conditions/OBS.cs | 2 +- .../Models/Conditions/Subscription.cs | 36 + .../Models/Conditions/User.cs | 2 +- .../Models/Event.cs | 2 +- .../Models/EventContext.cs | 60 +- .../Models/Events/ChatBits.cs | 2 +- .../Models/Events/ChatCommand.cs | 2 +- .../Models/Events/ChatFollow.cs | 2 +- .../Models/Events/ChatPointsReward.cs | 2 +- .../Models/Events/ChatSubscription.cs | 2 +- .../Models/Events/VoiceAttackCommand.cs | 2 +- .../ModulePresence.cs | 27 + .../UI/Data/ActionListItem.cs | 49 + .../UI/Data/ConditionListItem.cs | 49 + .../UI/Data/EventListItem.cs | 66 + .../UI/Modals/AddXModal.cs | 196 + .../UI/Modals/EventCreateModal.cs | 123 + .../UI/Modals/EventImportModal.cs | 133 + .../UI/Modals/EventTemplateModal.cs | 119 + .../UI/SettingsLeftView.cs | 81 + .../UI/SettingsMainView.cs | 704 +++ .../UI/SettingsRightView.cs | 370 ++ .../Conditions/Bits.cs | 65 - .../Conditions/ChatRequest.cs | 249 - .../Conditions/Event.cs | 220 - .../Conditions/GamePlay.cs | 188 - .../Conditions/Misc.cs | 93 - .../Conditions/OBS.cs | 239 - .../Conditions/Subscription.cs | 94 - .../Conditions/User.cs | 68 - .../Conditions/Views/Bits_Amount.bsml | 10 - .../Views/ChatRequest_QueueDuration.bsml | 11 - .../Views/ChatRequest_QueueSize.bsml | 11 - .../Views/ChatRequest_QueueStatus.bsml | 9 - .../Conditions/Views/Event_Disabled.bsml | 7 - .../Conditions/Views/Event_Enabled.bsml | 7 - .../Views/GamePlay_LevelEndType.bsml | 11 - .../Conditions/Views/GamePlay_PlayingMap.bsml | 6 - .../Conditions/Views/Misc_Cooldown.bsml | 10 - .../Conditions/Views/OBS_IsInScene.bsml | 10 - .../Conditions/Views/OBS_IsNotInScene.bsml | 10 - .../Views/Subscription_PlanType.bsml | 8 - .../Subscription_PurchasedMonthCount.bsml | 8 - .../Conditions/Views/User_Permissions.bsml | 13 - .../Events/ChatBits.cs | 123 - .../Events/ChatCommand.cs | 187 - .../Events/ChatFollow.cs | 122 - .../Events/ChatPointsReward.cs | 530 -- .../Events/ChatRaid.cs | 126 - .../Events/ChatSubscription.cs | 133 - .../Events/Dummy.cs | 122 - .../Events/LevelEnded.cs | 139 - .../Events/LevelPaused.cs | 126 - .../Events/LevelResumed.cs | 126 - .../Events/LevelStarted.cs | 127 - .../Events/Views/ChatBits.bsml | 10 - .../Events/Views/ChatCommand.bsml | 21 - .../Events/Views/ChatFollow.bsml | 10 - .../Events/Views/ChatPointsReward.bsml | 66 - .../Events/Views/ChatRaid.bsml | 10 - .../Events/Views/ChatSubscription.bsml | 10 - .../Events/Views/Dummy.bsml | 17 - .../Events/Views/LevelEnded.bsml | 10 - .../Events/Views/LevelPaused.bsml | 10 - .../Events/Views/LevelResumed.bsml | 10 - .../Events/Views/LevelStarted.bsml | 10 - .../Events/Views/VoiceAttackCommand.bsml | 27 - .../Events/VoiceAttackCommand.cs | 188 - .../Interfaces/IAction.cs | 194 - .../Interfaces/ICondition.cs | 173 - .../Interfaces/IEvent.cs | 709 --- .../ModPresence.cs | 31 - .../Models/Action.cs | 19 - .../Models/Actions/Camera2.cs | 18 - .../Models/Actions/GamePlay.cs | 130 - .../Models/Actions/OBS.cs | 66 - .../Models/Actions/SongChartVisualizer.cs | 10 - .../Models/Conditions/Bits.cs | 12 - .../Models/Conditions/ChatRequest.cs | 32 - .../Models/Conditions/GamePlay.cs | 75 - .../Models/Conditions/Subscription.cs | 16 - .../ModulePresence.cs | 75 - .../Properties/AssemblyInfo.cs | 4 +- .../UI/Settings.bsml | 177 - .../UI/Settings.cs | 1061 ---- .../UI/SettingsLeft.bsml | 18 - .../UI/SettingsLeft.cs | 77 - .../UI/SettingsRight.bsml | 118 - .../UI/SettingsRight.cs | 988 ---- .../UI/Settings_AddActionFrame.cs | 406 -- .../UI/Settings_AddConditionFrame.cs | 399 -- .../manifest.json | 9 +- .../BeatSaberPlus_ChatRequest.csproj | 56 +- .../BeatSaberPlus_ChatRequest.csproj.user | Bin 622 -> 636 bytes .../BeatSaberPlus_ChatRequest/ChatRequest.cs | 155 +- .../ChatRequest_Commands.cs | 52 +- .../ChatRequest_Database.cs | 145 +- .../ChatRequest_Logic.cs | 138 +- .../Data/SongEntry.cs | 90 + .../Properties/AssemblyInfo.cs | 4 +- .../UI/ManagerLeft.bsml | 30 - .../UI/{ManagerLeft.cs => ManagerLeftView.cs} | 94 +- .../UI/ManagerMain.bsml | 17 - .../UI/ManagerMain.cs | 560 -- .../UI/ManagerMainView.cs | 562 ++ .../UI/ManagerRight.bsml | 19 - .../{ManagerRight.cs => ManagerRightView.cs} | 123 +- .../UI/ManagerViewFlowCoordinator.cs | 67 +- .../UI/Settings.bsml | 46 - .../BeatSaberPlus_ChatRequest/UI/Settings.cs | 122 - .../UI/SettingsLeft.bsml | 20 - .../{SettingsLeft.cs => SettingsLeftView.cs} | 69 +- .../UI/SettingsMainView.cs | 162 + .../UI/SettingsRight.bsml | 98 - .../UI/SettingsRight.cs | 186 - .../UI/SettingsRightView.cs | 213 + .../BeatSaberPlus_ChatRequest/manifest.json | 9 +- .../BeatSaberPlus_GameTweaker.csproj | 60 +- .../BeatSaberPlus_GameTweaker.csproj.user | Bin 622 -> 636 bytes .../Components/FPFCEscape.cs | 32 +- .../Components/MusicBandLogoRemover.cs | 9 +- Modules/BeatSaberPlus_GameTweaker/GTConfig.cs | 77 +- .../BeatSaberPlus_GameTweaker/GameTweaker.cs | 69 +- .../Managers/CustomMenuLightManager.cs | 17 +- .../Patches/PLevelListTableCell.cs | 7 +- .../Patches/PPlayerSettingsPanelController.cs | 133 +- .../Patches/PStandardLevelDetailView.cs | 22 +- .../Properties/AssemblyInfo.cs | 4 +- .../UI/Settings.bsml | 240 - .../BeatSaberPlus_GameTweaker/UI/Settings.cs | 389 -- .../UI/SettingsLeft.bsml | 18 - .../UI/SettingsLeft.cs | 80 - .../UI/SettingsLeftView.cs | 76 + .../UI/SettingsMainView.cs | 453 ++ .../BeatSaberPlus_GameTweaker/manifest.json | 9 +- .../{Plugin.cs => BSIPA.cs} | 6 +- .../BeatSaberPlus_MenuMusic.csproj | 109 +- .../BeatSaberPlus_MenuMusic.csproj.user | Bin 622 -> 636 bytes .../Data/CustomMusicProvider.cs | 99 + .../Data/EMusicProviderType.cs | 39 + .../Data/GameMusicProvider.cs | 124 + .../Data/IMusicProvider.cs | 24 + .../ChatPlexMod_MenuMusic/Data/Music.cs | 223 + .../{ => ChatPlexMod_MenuMusic}/Logger.cs | 2 +- .../{ => ChatPlexMod_MenuMusic}/MMConfig.cs | 8 +- .../ChatPlexMod_MenuMusic/MenuMusic.cs | 679 +++ .../Resources/BackgroundMask.png | Bin 0 -> 1399 bytes .../Resources/CoverMask.png | Bin 0 -> 2928 bytes .../Resources/DefaultCover.png | Bin 0 -> 20065 bytes .../ChatPlexMod_MenuMusic/Resources/Glass.png | Bin 0 -> 46494 bytes .../ChatPlexMod_MenuMusic/Resources/Next.png | Bin 0 -> 1048 bytes .../Resources/Originals.rar | Bin 0 -> 314808 bytes .../ChatPlexMod_MenuMusic/Resources/Pause.png | Bin 0 -> 791 bytes .../ChatPlexMod_MenuMusic/Resources/Play.png | Bin 0 -> 818 bytes .../Resources/Playlist.png | Bin 0 -> 1018 bytes .../ChatPlexMod_MenuMusic/Resources/Prev.png | Bin 0 -> 1091 bytes .../ChatPlexMod_MenuMusic/Resources/Rand.png | Bin 0 -> 1485 bytes .../ChatPlexMod_MenuMusic/Resources/Sound.png | Bin 0 -> 1493 bytes .../UI/PlayerFloatingPanel.cs | 331 ++ .../UI/SettingsLeftView.cs | 87 + .../UI/SettingsMainView.cs | 164 + .../Utils/ArtProvider.cs | 122 + Modules/BeatSaberPlus_MenuMusic/MenuMusic.cs | 848 --- .../Properties/AssemblyInfo.cs | 4 +- .../Resources/NextIcon.png | Bin 3282 -> 0 bytes .../Resources/Pause.png | Bin 3462 -> 0 bytes .../Resources/Play.png | Bin 3415 -> 0 bytes .../Resources/PrevIcon.png | Bin 3337 -> 0 bytes .../Resources/RefreshIcon.png | Bin 2015 -> 0 bytes .../Resources/Settings.png | Bin 8938 -> 0 bytes .../BeatSaberPlus_MenuMusic/UI/Player.bsml | 21 - Modules/BeatSaberPlus_MenuMusic/UI/Player.cs | 288 - .../BeatSaberPlus_MenuMusic/UI/Settings.bsml | 57 - .../BeatSaberPlus_MenuMusic/UI/Settings.cs | 146 - .../UI/SettingsLeft.bsml | 35 - .../UI/SettingsLeft.cs | 90 - Modules/BeatSaberPlus_MenuMusic/manifest.json | 9 +- .../BeatSaberPlus_NoteTweaker.csproj | 63 +- .../BeatSaberPlus_NoteTweaker.csproj.user | Bin 622 -> 636 bytes .../BeatSaberPlus_NoteTweaker/NoteTweaker.cs | 96 +- .../Patches/PBombContoller.cs | 20 +- .../Patches/PColorNoteVisuals.cs | 15 +- .../Patches/PSliderController.cs | 5 +- .../Properties/AssemblyInfo.cs | 4 +- .../UI/Modals/ProfileImportModal.cs | 136 + .../UI/Settings.bsml | 244 - .../BeatSaberPlus_NoteTweaker/UI/Settings.cs | 652 --- .../UI/SettingsLeft.bsml | 13 - .../UI/SettingsLeft.cs | 59 - .../UI/SettingsLeftView.cs | 51 + .../UI/SettingsMainView.cs | 556 ++ .../UI/SettingsRight.bsml | 10 - ...{SettingsRight.cs => SettingsRightView.cs} | 67 +- .../BeatSaberPlus_NoteTweaker/manifest.json | 9 +- .../{Plugin.cs => BSIPA.cs} | 2 +- .../BeatSaberPlus_SongChartVisualizer.csproj | 75 +- ...tSaberPlus_SongChartVisualizer.csproj.user | Bin 622 -> 636 bytes .../Data/DataPoint.cs | 11 + .../Data/Graph.cs | 50 + .../Data/GraphBuilder.cs | 322 ++ .../Logger.cs | 2 +- .../SCVConfig.cs | 23 +- .../SongChartVisualizer.cs | 324 ++ .../UI/ChartFloatingPanelView.cs | 325 ++ .../UI/SettingsLeftView.cs} | 64 +- .../UI/SettingsMainView.cs | 178 + .../UI/SettingsRightView.cs | 24 + .../Components/SongChart.cs | 519 -- .../Properties/AssemblyInfo.cs | 4 +- .../Resources/Locked.png | Bin 3479 -> 0 bytes .../SongChartVisualizer.cs | 241 - .../UI/FloatingWindow.bsml | 3 - .../UI/FloatingWindow.cs | 113 - .../UI/Settings.bsml | 89 - .../UI/Settings.cs | 199 - .../UI/SettingsLeft.bsml | 35 - .../UI/SettingsRight.bsml | 6 - .../UI/SettingsRight.cs | 10 - .../manifest.json | 9 +- .../BeatSaberPlus_SongOverlay.csproj | 44 +- .../Network/OverlayServer.cs | 12 +- .../Properties/AssemblyInfo.cs | 4 +- .../BeatSaberPlus_SongOverlay/SongOverlay.cs | 58 +- .../UI/Settings.bsml | 13 - .../BeatSaberPlus_SongOverlay/UI/Settings.cs | 26 - .../UI/SettingsLeftView.cs | 44 + .../UI/SettingsMainView.cs | 35 + .../BeatSaberPlus_SongOverlay/manifest.json | 9 +- .../BeatSaberPlus_ModuleTemplate.csproj | 31 +- .../BeatSaberPlus_ModuleTemplate.csproj.user | Bin 622 -> 636 bytes .../BeatSaberPlus_ModuleTemplate/Logger.cs | 2 +- .../ModuleTemplate.cs | 61 +- .../BeatSaberPlus_ModuleTemplate/Plugin.cs | 37 +- .../Properties/AssemblyInfo.cs | 4 +- .../UI/Settings.bsml | 16 - .../UI/{Settings.cs => SettingsMainView.cs} | 27 +- .../manifest.json | 15 +- 826 files changed, 68809 insertions(+), 32077 deletions(-) rename BeatSaberPlus/{BSPConfig.cs => CP_SDK/CPConfig.cs} (84%) create mode 100644 BeatSaberPlus/CP_SDK/Chat/Interfaces/EBadgeType.cs delete mode 100644 BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatMessageHandler.cs create mode 100644 BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatServiceResourceProvider.cs delete mode 100644 BeatSaberPlus/CP_SDK/Chat/Models/EmoteType.cs delete mode 100644 BeatSaberPlus/CP_SDK/Chat/Models/ImageRect.cs create mode 100644 BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchCheermoteTier.cs create mode 100644 BeatSaberPlus/CP_SDK/Chat/Services/ChatPlexGradientNamesDataProvider.cs create mode 100644 BeatSaberPlus/CP_SDK/Chat/Services/RelayChatService.cs create mode 100644 BeatSaberPlus/CP_SDK/Chat/Services/RelayChatServiceProtocol.cs create mode 100644 BeatSaberPlus/CP_SDK/Chat/Utilities/7TVUtils.cs create mode 100644 BeatSaberPlus/CP_SDK/Config/JsonConverters/QuaternionConverter.cs create mode 100644 BeatSaberPlus/CP_SDK/Logging/MelonLoaderLogger.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/FastCancellationToken.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/Adler32.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/BZip2Crc.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/Crc32.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/CrcUtilities.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/IChecksum.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/EmptyRefs.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/SharpZipBaseException.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/StreamDecodingException.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/StreamUnsupportedException.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/UnexpectedEndOfStreamException.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/ValueOutOfRangeException.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/FileSystemScanner.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/INameTransform.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/IScanFilter.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/InvalidNameException.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/NameFilter.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/PathFilter.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/PathUtils.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/StreamUtils.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/StringBuilderPool.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/PkzipClassic.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/ZipAESTransform.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Deflater.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterConstants.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterEngine.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterHuffman.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterPending.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/InflaterDynHeader.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/PendingBuffer.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/OutputWindow.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/FastZip.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/IEntryFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipConstants.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEncryptionMethod.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntry.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntryExtensions.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntryFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipException.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipFile.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipNameTransform.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs create mode 100644 BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipStrings.cs delete mode 100644 BeatSaberPlus/CP_SDK/Network/APIClient.cs delete mode 100644 BeatSaberPlus/CP_SDK/Network/APIResponse.cs create mode 100644 BeatSaberPlus/CP_SDK/Network/IWebClient.cs create mode 100644 BeatSaberPlus/CP_SDK/Network/JsonRPCClient.cs create mode 100644 BeatSaberPlus/CP_SDK/Network/JsonRPCResult.cs create mode 100644 BeatSaberPlus/CP_SDK/Network/WebClient.cs create mode 100644 BeatSaberPlus/CP_SDK/Network/WebClientEx.cs delete mode 100644 BeatSaberPlus/CP_SDK/Network/WebClient_Unity.cs create mode 100644 BeatSaberPlus/CP_SDK/Pool/CollectionPool.cs create mode 100644 BeatSaberPlus/CP_SDK/Pool/ListPool.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CColorInput.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CDropdown.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CFLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CFloatingPanel.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CGLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CHLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CIconButton.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CImage.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CPrimaryButton.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CSecondaryButton.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CSlider.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CTabControl.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CText.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CTextInput.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CTextSegmentedControl.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CToggle.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CVLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CVScrollView.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/CVVList.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/Generics/CHOrVLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/Generics/CPOrSButton.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Components/Generics/CVXList.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Data/IListCell.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Data/IListItem.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Data/ListCellPrefabs.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Data/TextListCell.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Data/TextListItem.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCColorInput.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCDropdown.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCFLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCFloatingPanel.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCGLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCHLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCIconButton.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCImage.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCPrimaryButton.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCSecondaryButton.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCSlider.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCTabControl.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCText.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCTextInput.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCTextSegmentedControl.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCToggle.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCVLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCVScrollView.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/DefaultCVVList.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/Subs/SubStackLayoutGroup.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/Subs/SubToggleWithCallbacks.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/Subs/SubVScrollIndicator.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultComponents/Subs/SubVScrollViewContent.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultColorInput.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultDropdownFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultFLayoutFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultFloatingPanelFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultGLayoutFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultHLayoutFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultIconButtonFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultImageFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultPrimaryButtonFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultSecondaryButtonFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultSliderFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultTabControlFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultTextFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultTextInputFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultTextSegmentedControlFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultToggleFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultVLayoutFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultVScrollViewFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/DefaultFactories/DefaultVVListFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IColorInput.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IDropDownFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IFLayoutFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IFloatingPanelFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IGLayoutFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IHLayoutFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IIconButtonFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IImageFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IPrimaryButtonFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/ISecondaryButtonFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/ISliderFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/ITabControlFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/ITextFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/ITextInputFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/ITextSegmentedControlFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IToggleFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IVLayoutFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IVScrollViewFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FactoryInterfaces/IVVListFactory.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FlowCoordinator.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/FlowCoordinators/MainFlowCoordinator.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/IFlowCoordinator.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/IModal.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/IScreen.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/IViewController.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/ModButton.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/ModMenu.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Modals/ColorPicker.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Modals/Confirmation.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Modals/Dropdown.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Modals/Keyboard.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Modals/Loading.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Modals/Message.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/ScreenSystem.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Tooltip.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/UISystem.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/ValueFormatters.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/ViewController.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Views/MainLeftView.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Views/MainMainView.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Views/MainRightView.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Views/ModMenuView.cs rename BeatSaberPlus/{UI => CP_SDK/UI/Views}/SettingsLeftView.cs (60%) create mode 100644 BeatSaberPlus/CP_SDK/UI/Views/SettingsMainView.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Views/SettingsRightView.cs create mode 100644 BeatSaberPlus/CP_SDK/UI/Views/TopNavigationView.cs delete mode 100644 BeatSaberPlus/CP_SDK/Unity/Extensions/StringU.cs create mode 100644 BeatSaberPlus/CP_SDK/Unity/OpenType/NameRecord.cs delete mode 100644 BeatSaberPlus/CP_SDK/Unity/OpenType/NumericHelpers.cs create mode 100644 BeatSaberPlus/CP_SDK/Unity/OpenType/OpenTypeNameTable.cs create mode 100644 BeatSaberPlus/CP_SDK/Unity/PersistentSingletonInput.cs create mode 100644 BeatSaberPlus/CP_SDK/Unity/RaycastResultU.cs create mode 100644 BeatSaberPlus/CP_SDK/Unity/TextureRaw.cs create mode 100644 BeatSaberPlus/CP_SDK/XRInput/InputInternals/FakeButtonState.cs create mode 100644 BeatSaberPlus/CP_SDK/XRInput/InputInternals/FakeMouseState.cs create mode 100644 BeatSaberPlus/CP_SDK/XRInput/InputInternals/FrameCachedPhysicsRaycaster.cs create mode 100644 BeatSaberPlus/CP_SDK/XRInput/XRController.cs create mode 100644 BeatSaberPlus/CP_SDK/XRInput/XRGraphicRaycaster.cs create mode 100644 BeatSaberPlus/CP_SDK/XRInput/XRInputSystem.cs create mode 100644 BeatSaberPlus/CP_SDK/XRInput/XRLaserPointer.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/Generics/IXUIBindable.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/Generics/IXUIElement.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/Generics/IXUIElementReady.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/Generics/IXUIElementWithChilds.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/Generics/XUIHOrVLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/Generics/XUIHOrVSpacer.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/Generics/XUIPOrSButton.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/Templates.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIColorInput.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIDropDown.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIFLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIGLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIHLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIHSpacer.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIIconButton.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIImage.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIPrimaryButton.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUISecondaryButton.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUISlider.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUITabControl.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIText.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUITextInput.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUITextSegmentedControl.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIToggle.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIVLayout.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIVScrollView.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIVSpacer.cs create mode 100644 BeatSaberPlus/CP_SDK/XUI/XUIVVList.cs create mode 100644 BeatSaberPlus/CP_SDK/_Resources/ChatPlexLogoLoading.webp create mode 100644 BeatSaberPlus/CP_SDK/_Resources/ChatPlexLogoTransparent.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIButton.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIColorPickerFBG.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIColorPickerHBG.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIColorPickerSBG.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIColorPickerVBG.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIDownArrow.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIIconGear.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIIconLocked.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIIconUnlocked.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIRectBG.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIRoundBG.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIRoundRectLeftBG.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIRoundRectRightBG.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UIRoundSmoothFrame.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UISliderBG.png create mode 100644 BeatSaberPlus/CP_SDK/_Resources/UISliderHandle.png delete mode 100644 BeatSaberPlus/SDK/BSPModuleBase.cs create mode 100644 BeatSaberPlus/SDK/Game/PlayerAvatarPicture.cs delete mode 100644 BeatSaberPlus/SDK/UI/BSMLSettingFormartter.cs delete mode 100644 BeatSaberPlus/SDK/UI/Backgroundable.cs create mode 100644 BeatSaberPlus/SDK/UI/CP_SDK_UI_IViewControllerBridge.cs delete mode 100644 BeatSaberPlus/SDK/UI/ColorSetting.cs create mode 100644 BeatSaberPlus/SDK/UI/Data/SongListCell.cs create mode 100644 BeatSaberPlus/SDK/UI/Data/SongListController.cs create mode 100644 BeatSaberPlus/SDK/UI/Data/SongListItem.cs delete mode 100644 BeatSaberPlus/SDK/UI/DataSource/SimpleTextList.cs delete mode 100644 BeatSaberPlus/SDK/UI/DataSource/SongList.cs delete mode 100644 BeatSaberPlus/SDK/UI/DataSource/SongListCustom.cs create mode 100644 BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/BS_CFloatingPanel.cs create mode 100644 BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMover.cs create mode 100644 BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMoverHandle.cs create mode 100644 BeatSaberPlus/SDK/UI/DefaultFactoriesOverrides/BS_FloatingPanelFactory.cs delete mode 100644 BeatSaberPlus/SDK/UI/DropDownListSetting.cs create mode 100644 BeatSaberPlus/SDK/UI/GameFont.cs rename BeatSaberPlus/SDK/UI/{HorizontalIconSegmentedControl.cs => HMUIIconSegmentedControl.cs} (54%) rename BeatSaberPlus/SDK/UI/{TextSegmentedControl.cs => HMUITextSegmentedControl.cs} (79%) create mode 100644 BeatSaberPlus/SDK/UI/HMUIUIUtils.cs rename BeatSaberPlus/SDK/UI/{ViewFlowCoordinator.cs => HMUIViewFlowCoordinator.cs} (82%) create mode 100644 BeatSaberPlus/SDK/UI/IHMUIViewController.cs delete mode 100644 BeatSaberPlus/SDK/UI/IncrementSetting.cs delete mode 100644 BeatSaberPlus/SDK/UI/Internal/BSMLPrimaryButtonTag.cs delete mode 100644 BeatSaberPlus/SDK/UI/ListSetting.cs delete mode 100644 BeatSaberPlus/SDK/UI/ModalView.cs delete mode 100644 BeatSaberPlus/SDK/UI/Patches/BSMLColorSetting.cs delete mode 100644 BeatSaberPlus/SDK/UI/Patches/PSimpleTextDropdown.cs create mode 100644 BeatSaberPlus/SDK/UI/Patches/PVRPointer.cs delete mode 100644 BeatSaberPlus/SDK/UI/SliderSetting.cs delete mode 100644 BeatSaberPlus/SDK/UI/ToggleSetting.cs delete mode 100644 BeatSaberPlus/SDK/UI/VerticalIconSegmentedControl.cs rename BeatSaberPlus/SDK/UI/{ => __OLD__}/Button.cs (100%) delete mode 100644 BeatSaberPlus/SDK/Unity/MaterialU.cs delete mode 100644 BeatSaberPlus/SDK/Unity/ShaderU.cs delete mode 100644 BeatSaberPlus/UI/InfoView.bsml delete mode 100644 BeatSaberPlus/UI/InfoView.cs delete mode 100644 BeatSaberPlus/UI/MainView.bsml delete mode 100644 BeatSaberPlus/UI/MainView.cs delete mode 100644 BeatSaberPlus/UI/MainViewFlowCoordinator.cs delete mode 100644 BeatSaberPlus/UI/SettingsLeftView.bsml delete mode 100644 BeatSaberPlus/UI/SettingsRightView.bsml delete mode 100644 BeatSaberPlus/UI/SettingsRightView.cs delete mode 100644 BeatSaberPlus/UI/SettingsView.bsml delete mode 100644 BeatSaberPlus/UI/SettingsView.cs rename Modules/BeatSaberPlus_Chat/{Plugin.cs => BSIPA.cs} (83%) delete mode 100644 Modules/BeatSaberPlus_Chat/CConfig.cs delete mode 100644 Modules/BeatSaberPlus_Chat/Chat.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/CConfig.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Chat.cs rename Modules/BeatSaberPlus_Chat/{ => ChatPlexMod_Chat}/Components/ChatImage.cs (100%) rename Modules/BeatSaberPlus_Chat/{ => ChatPlexMod_Chat}/Components/ChatMessageText.cs (98%) rename Modules/BeatSaberPlus_Chat/{ => ChatPlexMod_Chat}/Components/ChatMessageWidget.cs (82%) rename Modules/BeatSaberPlus_Chat/{ => ChatPlexMod_Chat}/Extensions/EnhancedFontInfo.cs (93%) rename Modules/BeatSaberPlus_Chat/{ => ChatPlexMod_Chat}/Logger.cs (100%) create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Resources/ViewerIcon.png create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ChatFloatingPanelView.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/Data/ChatUserListItem.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/HypeTrainFloatingPanelView.cs rename Modules/BeatSaberPlus_Chat/{UI/ModerationLeft.cs => ChatPlexMod_Chat/UI/ModerationLeftView.cs} (62%) create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationMainView.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationRightView.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationShortcutsMainView.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationViewFlowCoordinator.cs rename Modules/BeatSaberPlus_Chat/{UI/PollFloatingWindow.cs => ChatPlexMod_Chat/UI/PollFloatingPanelView.cs} (54%) rename Modules/BeatSaberPlus_Chat/{UI/PredictionFloatingWindow.cs => ChatPlexMod_Chat/UI/PredictionFloatingPanelView.cs} (56%) create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsLeftView.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsMainView.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsRightView.cs create mode 100644 Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/StatusFloatingPanelView.cs rename Modules/BeatSaberPlus_Chat/{ => ChatPlexMod_Chat}/Utils/ChatMessageBuilder.cs (100%) delete mode 100644 Modules/BeatSaberPlus_Chat/Resources/Locked.png delete mode 100644 Modules/BeatSaberPlus_Chat/Resources/Settings.png delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow_Events.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow_Logic.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/HypeTrainFloatingWindow.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/HypeTrainFloatingWindow.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ModerationLeft.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ModerationMain.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ModerationMain.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ModerationRight.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ModerationRight.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ModerationShortcut.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ModerationShortcut.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/ModerationViewFlowCoordinator.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/PollFloatingWindow.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/PredictionFloatingWindow.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/Settings.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/Settings.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/SettingsLeft.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/SettingsLeft.cs delete mode 100644 Modules/BeatSaberPlus_Chat/UI/SettingsRight.bsml delete mode 100644 Modules/BeatSaberPlus_Chat/UI/SettingsRight.cs rename Modules/BeatSaberPlus_ChatEmoteRain/{Plugin.cs => BSIPA.cs} (83%) rename Modules/BeatSaberPlus_ChatEmoteRain/{ => ChatPlexMod_ChatEmoteRain}/CERConfig.cs (71%) rename Modules/BeatSaberPlus_ChatEmoteRain/{ => ChatPlexMod_ChatEmoteRain}/ChatEmoteRain.cs (80%) rename Modules/BeatSaberPlus_ChatEmoteRain/{ => ChatPlexMod_ChatEmoteRain}/Logger.cs (100%) rename Modules/BeatSaberPlus_ChatEmoteRain/{ => ChatPlexMod_ChatEmoteRain}/Resources/PreviewMaterial.bundle (100%) create mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsLeftView.cs create mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsMainView.cs create mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsRightView.cs create mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/Widgets/EmitterWidget.cs delete mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/UI/EmitterWidget.bsml delete mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/UI/EmitterWidget.cs delete mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/UI/Settings.bsml delete mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/UI/Settings.cs delete mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsLeft.bsml delete mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsLeft.cs delete mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsRight.bsml delete mode 100644 Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsRight.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Camera2.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Chat.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/EmoteRain.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Event.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/GamePlay.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Misc.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/NoteTweaker.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/OBS.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/SongChartVisualizer.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Twitch.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Camera2_SwitchToScene.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Camera2_ToggleCamera.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Chat_SendMessage.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Chat_ToggleVisibility.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/EmoteRain_CustomRain.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/EmoteRain_EmoteBombRain.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Event_ExecuteDummy.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Event_Toggle.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeBombColor.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeBombScale.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeDebris.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeLightIntensity.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeMusicVolume.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeNoteColors.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeNoteScale.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_Pause.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_SpawnBombPatterns.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_SpawnSquatWalls.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ToggleHUD.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ToggleLights.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Misc_Delay.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Misc_PlaySound.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/NoteTweaker_SwitchProfile.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_RenameLastRecord.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SetRecordFilenameFormat.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SwitchPreviewToScene.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SwitchToScene.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleSource.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleSourceAudio.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleStudioMode.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_Transition.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/SongChartVisualizer_ToggleVisibility.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Twitch_AddMarker.bsml rename Modules/BeatSaberPlus_ChatIntegrations/{Plugin.cs => BSIPA.cs} (82%) create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/Camera2.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/GamePlay.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/NoteTweaker.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/SongChartVisualizer.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Conditions/ChatRequest.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Conditions/GamePlay.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/BeatmapModType.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/LevelType.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/QueueStatus.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/ValueSource.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelEnded.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelPaused.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelResumed.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelStarted.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Manager.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/ModPresence.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/Camera2.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/GamePlay.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/NoteTweaker.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/SongChartVisualizer.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Conditions/ChatRequest.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Conditions/GamePlay.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/ModulePresence.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Chat.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/EmoteRain.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Event.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Misc.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/OBS.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Twitch.cs rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/CIConfig.cs (77%) create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations.cs rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/ChatIntegrations_Database.cs (64%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/ChatIntegrations_Events.cs (59%) create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Bits.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Event.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Misc.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/OBS.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Subscription.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/User.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/ChangeType.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Comparison.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/EVisibility.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Toggle.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/TwitchSubscribtionPlanType.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Visibility.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatBits.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatCommand.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatFollow.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatPointsReward.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatRaid.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatSubscription.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/Dummy.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/VoiceAttackCommand.cs rename Modules/BeatSaberPlus_ChatIntegrations/{Interfaces/TriggerType.cs => ChatPlexMod_ChatIntegrations/Interfaces/ETriggerType.cs} (80%) rename Modules/BeatSaberPlus_ChatIntegrations/{Interfaces/ValueType.cs => ChatPlexMod_ChatIntegrations/Interfaces/EValueType.cs} (68%) create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IAction.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IActionBase.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/ICondition.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IConditionBase.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IEvent.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IEventBase.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IUIConfigurable.cs rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Logger.cs (84%) create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Action.cs rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Actions/Chat.cs (68%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Actions/EmoteRain.cs (69%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Actions/Event.cs (62%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Actions/Misc.cs (81%) create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/OBS.cs rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Condition.cs (93%) create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Bits.cs rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Conditions/Events.cs (86%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Conditions/Misc.cs (87%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Conditions/OBS.cs (86%) create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Subscription.cs rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Conditions/User.cs (90%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Event.cs (92%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/EventContext.cs (74%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Events/ChatBits.cs (82%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Events/ChatCommand.cs (91%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Events/ChatFollow.cs (82%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Events/ChatPointsReward.cs (96%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Events/ChatSubscription.cs (84%) rename Modules/BeatSaberPlus_ChatIntegrations/{ => ChatPlexMod_ChatIntegrations}/Models/Events/VoiceAttackCommand.cs (87%) create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ModulePresence.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/ActionListItem.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/ConditionListItem.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/EventListItem.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/AddXModal.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventCreateModal.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventImportModal.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventTemplateModal.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsLeftView.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsMainView.cs create mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsRightView.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Bits.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/ChatRequest.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Event.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/GamePlay.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Misc.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/OBS.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Subscription.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/User.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Bits_Amount.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueDuration.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueSize.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueStatus.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Event_Disabled.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Event_Enabled.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/GamePlay_LevelEndType.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/GamePlay_PlayingMap.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Misc_Cooldown.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/OBS_IsInScene.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/OBS_IsNotInScene.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Subscription_PlanType.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Subscription_PurchasedMonthCount.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/User_Permissions.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/ChatBits.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/ChatCommand.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/ChatFollow.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/ChatPointsReward.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/ChatRaid.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/ChatSubscription.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Dummy.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/LevelEnded.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/LevelPaused.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/LevelResumed.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/LevelStarted.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatBits.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatCommand.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatFollow.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatPointsReward.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatRaid.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatSubscription.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/Dummy.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelEnded.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelPaused.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelResumed.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelStarted.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/Views/VoiceAttackCommand.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Events/VoiceAttackCommand.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Interfaces/IAction.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Interfaces/ICondition.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Interfaces/IEvent.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ModPresence.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Models/Action.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Camera2.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/GamePlay.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/OBS.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/SongChartVisualizer.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Bits.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/ChatRequest.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/GamePlay.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Subscription.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/ModulePresence.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/UI/Settings.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/UI/Settings.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsLeft.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsLeft.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsRight.bsml delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsRight.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/UI/Settings_AddActionFrame.cs delete mode 100644 Modules/BeatSaberPlus_ChatIntegrations/UI/Settings_AddConditionFrame.cs create mode 100644 Modules/BeatSaberPlus_ChatRequest/Data/SongEntry.cs delete mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeft.bsml rename Modules/BeatSaberPlus_ChatRequest/UI/{ManagerLeft.cs => ManagerLeftView.cs} (58%) delete mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/ManagerMain.bsml delete mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/ManagerMain.cs create mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/ManagerMainView.cs delete mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/ManagerRight.bsml rename Modules/BeatSaberPlus_ChatRequest/UI/{ManagerRight.cs => ManagerRightView.cs} (51%) delete mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/Settings.bsml delete mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/Settings.cs delete mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeft.bsml rename Modules/BeatSaberPlus_ChatRequest/UI/{SettingsLeft.cs => SettingsLeftView.cs} (75%) create mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/SettingsMainView.cs delete mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/SettingsRight.bsml delete mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/SettingsRight.cs create mode 100644 Modules/BeatSaberPlus_ChatRequest/UI/SettingsRightView.cs delete mode 100644 Modules/BeatSaberPlus_GameTweaker/UI/Settings.bsml delete mode 100644 Modules/BeatSaberPlus_GameTweaker/UI/Settings.cs delete mode 100644 Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeft.bsml delete mode 100644 Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeft.cs create mode 100644 Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeftView.cs create mode 100644 Modules/BeatSaberPlus_GameTweaker/UI/SettingsMainView.cs rename Modules/BeatSaberPlus_MenuMusic/{Plugin.cs => BSIPA.cs} (85%) create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/CustomMusicProvider.cs create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/EMusicProviderType.cs create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/GameMusicProvider.cs create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/IMusicProvider.cs create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/Music.cs rename Modules/BeatSaberPlus_MenuMusic/{ => ChatPlexMod_MenuMusic}/Logger.cs (86%) rename Modules/BeatSaberPlus_MenuMusic/{ => ChatPlexMod_MenuMusic}/MMConfig.cs (85%) create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/MenuMusic.cs create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/BackgroundMask.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/CoverMask.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/DefaultCover.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Glass.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Next.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Originals.rar create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Pause.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Play.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Playlist.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Prev.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Rand.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Sound.png create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.cs create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/SettingsLeftView.cs create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/SettingsMainView.cs create mode 100644 Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Utils/ArtProvider.cs delete mode 100644 Modules/BeatSaberPlus_MenuMusic/MenuMusic.cs delete mode 100644 Modules/BeatSaberPlus_MenuMusic/Resources/NextIcon.png delete mode 100644 Modules/BeatSaberPlus_MenuMusic/Resources/Pause.png delete mode 100644 Modules/BeatSaberPlus_MenuMusic/Resources/Play.png delete mode 100644 Modules/BeatSaberPlus_MenuMusic/Resources/PrevIcon.png delete mode 100644 Modules/BeatSaberPlus_MenuMusic/Resources/RefreshIcon.png delete mode 100644 Modules/BeatSaberPlus_MenuMusic/Resources/Settings.png delete mode 100644 Modules/BeatSaberPlus_MenuMusic/UI/Player.bsml delete mode 100644 Modules/BeatSaberPlus_MenuMusic/UI/Player.cs delete mode 100644 Modules/BeatSaberPlus_MenuMusic/UI/Settings.bsml delete mode 100644 Modules/BeatSaberPlus_MenuMusic/UI/Settings.cs delete mode 100644 Modules/BeatSaberPlus_MenuMusic/UI/SettingsLeft.bsml delete mode 100644 Modules/BeatSaberPlus_MenuMusic/UI/SettingsLeft.cs create mode 100644 Modules/BeatSaberPlus_NoteTweaker/UI/Modals/ProfileImportModal.cs delete mode 100644 Modules/BeatSaberPlus_NoteTweaker/UI/Settings.bsml delete mode 100644 Modules/BeatSaberPlus_NoteTweaker/UI/Settings.cs delete mode 100644 Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeft.bsml delete mode 100644 Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeft.cs create mode 100644 Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeftView.cs create mode 100644 Modules/BeatSaberPlus_NoteTweaker/UI/SettingsMainView.cs delete mode 100644 Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRight.bsml rename Modules/BeatSaberPlus_NoteTweaker/UI/{SettingsRight.cs => SettingsRightView.cs} (91%) rename Modules/BeatSaberPlus_SongChartVisualizer/{Plugin.cs => BSIPA.cs} (91%) create mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/DataPoint.cs create mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/Graph.cs create mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/GraphBuilder.cs rename Modules/BeatSaberPlus_SongChartVisualizer/{ => ChatPlexMod_SongChartVisualizer}/Logger.cs (83%) rename Modules/BeatSaberPlus_SongChartVisualizer/{ => ChatPlexMod_SongChartVisualizer}/SCVConfig.cs (74%) create mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/SongChartVisualizer.cs create mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/ChartFloatingPanelView.cs rename Modules/BeatSaberPlus_SongChartVisualizer/{UI/SettingsLeft.cs => ChatPlexMod_SongChartVisualizer/UI/SettingsLeftView.cs} (52%) create mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsMainView.cs create mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsRightView.cs delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/Components/SongChart.cs delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/Resources/Locked.png delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/SongChartVisualizer.cs delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/UI/FloatingWindow.bsml delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/UI/FloatingWindow.cs delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/UI/Settings.bsml delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/UI/Settings.cs delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsLeft.bsml delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsRight.bsml delete mode 100644 Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsRight.cs delete mode 100644 Modules/BeatSaberPlus_SongOverlay/UI/Settings.bsml delete mode 100644 Modules/BeatSaberPlus_SongOverlay/UI/Settings.cs create mode 100644 Modules/BeatSaberPlus_SongOverlay/UI/SettingsLeftView.cs create mode 100644 Modules/BeatSaberPlus_SongOverlay/UI/SettingsMainView.cs delete mode 100644 Samples/BeatSaberPlus_ModuleTemplate/UI/Settings.bsml rename Samples/BeatSaberPlus_ModuleTemplate/UI/{Settings.cs => SettingsMainView.cs} (56%) diff --git a/BeatSaberPlus/BeatSaberPlus.csproj b/BeatSaberPlus/BeatSaberPlus.csproj index 20a3c37..83e1bee 100644 --- a/BeatSaberPlus/BeatSaberPlus.csproj +++ b/BeatSaberPlus/BeatSaberPlus.csproj @@ -19,21 +19,25 @@ $(MSBuildProjectDirectory)\ $(AppOutputBase)=X:\$(AssemblyName)\ + + true false bin\Debug\ - TRACE;DEBUG;CP_SDK_IPA;CP_SDK_UNITY;UNITY_5_3_OR_NEWER;UNITY_2018_2_OR_NEWER;UNITY_2019_1_OR_NEWER UNITY_STANDALONE_WIN;UNITY_2018_3_OR_NEWER;NETSTANDARD + TRACE;DEBUG;CP_SDK_IPA;CP_SDK_UNITY;BEATSABER;UNITY_5_3_OR_NEWER;UNITY_2018_2_OR_NEWER;UNITY_2019_1_OR_NEWER UNITY_STANDALONE_WIN;UNITY_2018_3_OR_NEWER;NETSTANDARD;BEATSABER_1_29_4_OR_NEWER;BEATSABER_1_31_0_OR_NEWER prompt 4 true bin\Release\ - CP_SDK_IPA;CP_SDK_UNITY;UNITY_5_3_OR_NEWER;UNITY_2018_2_OR_NEWER;UNITY_2019_1_OR_NEWER UNITY_STANDALONE_WIN;UNITY_2018_3_OR_NEWER;NETSTANDARD + CP_SDK_IPA;CP_SDK_UNITY;BEATSABER;UNITY_5_3_OR_NEWER;UNITY_2018_2_OR_NEWER;UNITY_2019_1_OR_NEWER UNITY_STANDALONE_WIN;UNITY_2018_3_OR_NEWER;NETSTANDARD;BEATSABER_1_29_4_OR_NEWER;BEATSABER_1_31_0_OR_NEWER prompt 4 + + True @@ -61,6 +65,12 @@ prompt MinimumRecommendedRules.ruleset + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + False @@ -88,7 +98,7 @@ False False - + $(BeatSaberDir)\Beat Saber_Data\Managed\HMRendering.dll False False @@ -108,14 +118,6 @@ - - False - $(BeatSaberDir)\Beat Saber_Data\Managed\System.IO.Compression.dll - - - False - $(BeatSaberDir)\Beat Saber_Data\Managed\System.IO.Compression.FileSystem.dll - $(BeatSaberDir)\Beat Saber_Data\Managed\System.Runtime.Serialization.dll @@ -131,15 +133,15 @@ - + $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False - + $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll False - + $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll False @@ -172,6 +174,11 @@ False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.ImageConversionModule.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.IMGUIModule.dll + False + False + False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.InputLegacyModule.dll @@ -187,9 +194,10 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.PhysicsModule.dll False - + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextCoreFontEngineModule.dll + False False - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextCoreModule.dll False @@ -224,7 +232,7 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.XRModule.dll False - + False $(BeatSaberDir)\Beat Saber_Data\Managed\VRUI.dll @@ -234,14 +242,78 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + @@ -270,7 +342,8 @@ - + + @@ -351,17 +424,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - @@ -397,7 +631,6 @@ - @@ -438,8 +671,6 @@ - - @@ -497,32 +728,22 @@ - - - - - - - - - - - + + + + + + + - - - - - - - - - + + + - + @@ -533,38 +754,16 @@ - - - - - - - - - - MainView.cs - - - InfoView.cs - - - SettingsView.cs - - - SettingsLeftView.cs - - - SettingsRightView.cs - + @@ -580,6 +779,23 @@ + + + + + + + + + + + + + + + + + diff --git a/BeatSaberPlus/BeatSaberPlus.csproj.user b/BeatSaberPlus/BeatSaberPlus.csproj.user index 6aff7af894c0a367932e6edbd701541e6f32d244..012781eeb9f58b5f0dd22267773d93a2d711944f 100644 GIT binary patch literal 636 zcmZ{hO;3YB7=-6+;(yq9mwtFrQ*EM+UObp+dg=uVwU{mtKGgns_1Q&Ao5m1W-gjna zX7~O1o@lBsS#4zWp+u!BrOK7*MrRr+(2&)DWLnaXK{LI1v{9{<&UFc6s#~4vL=!!u zk?WluOK_uCsvP}F^CSIxHLy#sUVEQ5O9tEduTiK~=vo7w3dJ>CtC0VIHKpU+D&7^$ zz-P2@V|ea78<2vvq-WtpIHq)C>M>^<>H?oOyCH3tzui-7t4Ht9pwhEi<{i2rt}lEe z?=eHWgf}Dcf`088R!{UJkdSu^Qtgls-GX}Ym)w#!qw2KHp1Se#80(62jz~J#v5phC sS-6z@XX<7$CrzSHavsj=!QV_m8mE1T^?N$yp7Z}*^_4r42?s#*$&76%vitsL`iej`_U);tlufX(6=(ew5fzZ;1lW+^5~ z7&I32<@=~gl K;Ihjfp}zpmoNB!Q diff --git a/BeatSaberPlus/CP_SDK/Animation/AnimationControllerInstance.cs b/BeatSaberPlus/CP_SDK/Animation/AnimationControllerInstance.cs index da9df72..a2acede 100644 --- a/BeatSaberPlus/CP_SDK/Animation/AnimationControllerInstance.cs +++ b/BeatSaberPlus/CP_SDK/Animation/AnimationControllerInstance.cs @@ -67,12 +67,16 @@ public AnimationControllerInstance(Texture2D p_Texture, Rect[] p_UVs, ushort[] p m_UVs = p_UVs; Delays = p_Delays; + var l_Width = p_Texture.width; + var l_Height = p_Texture.height; for (int l_Frame = 0; l_Frame < p_UVs.Length; ++l_Frame) { - Frames[l_Frame] = Sprite.Create(p_Texture, - new Rect(p_UVs[l_Frame].x * p_Texture.width, p_UVs[l_Frame].y * p_Texture.height, p_UVs[l_Frame].width * p_Texture.width, p_UVs[l_Frame].height * p_Texture.height), - Vector2.zero, - 100f, + var l_CurrentUV = p_UVs[l_Frame]; + Frames[l_Frame] = Sprite.Create( + p_Texture, + new Rect(l_CurrentUV.x * l_Width, l_CurrentUV.y * l_Height, l_CurrentUV.width * l_Width, l_CurrentUV.height * l_Height), + new Vector2(0.0f, 0.0f), + 100.0f, 0, SpriteMeshType.FullRect ); @@ -86,7 +90,7 @@ public AnimationControllerInstance(Texture2D p_Texture, Rect[] p_UVs, ushort[] p FirstFrame = Frames[0]; - m_LastFrameChange = (long)(Time.realtimeSinceStartup * 1000f); + m_LastFrameChange = (long)(Time.realtimeSinceStartup * 1000.0f); } //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/CP_SDK/Animation/AnimationLoader.cs b/BeatSaberPlus/CP_SDK/Animation/AnimationLoader.cs index 087356c..91d8eb1 100644 --- a/BeatSaberPlus/CP_SDK/Animation/AnimationLoader.cs +++ b/BeatSaberPlus/CP_SDK/Animation/AnimationLoader.cs @@ -73,10 +73,10 @@ public static void Load( EAnimationType p_Ty /// /// Process loaded animation /// - /// - /// + /// Animation infos + /// Callback /// - public static IEnumerator Coroutine_ProcessLoadedAnimation(AnimationInfo p_AnimationInfo, Action p_Callback) + private static IEnumerator Coroutine_ProcessLoadedAnimation(AnimationInfo p_AnimationInfo, Action p_Callback) { if (p_AnimationInfo == null) p_Callback?.Invoke(null, null, null, 0, 0); diff --git a/BeatSaberPlus/CP_SDK/Animation/WEBP/WEBPDecoder.cs b/BeatSaberPlus/CP_SDK/Animation/WEBP/WEBPDecoder.cs index c1faa90..1350fd6 100644 --- a/BeatSaberPlus/CP_SDK/Animation/WEBP/WEBPDecoder.cs +++ b/BeatSaberPlus/CP_SDK/Animation/WEBP/WEBPDecoder.cs @@ -101,7 +101,7 @@ private static async Task ProcessingThread( byte[] p_Raw, try { - p_StaticCallback?.Invoke(Unity.SpriteU.CreateFromTexture(l_Texture)); + p_StaticCallback?.Invoke(Unity.SpriteU.CreateFromTextureWithBorders(l_Texture)); } catch (System.Exception l_Exception) { diff --git a/BeatSaberPlus/BSPConfig.cs b/BeatSaberPlus/CP_SDK/CPConfig.cs similarity index 84% rename from BeatSaberPlus/BSPConfig.cs rename to BeatSaberPlus/CP_SDK/CPConfig.cs index 6ddbeda..2184b77 100644 --- a/BeatSaberPlus/BSPConfig.cs +++ b/BeatSaberPlus/CP_SDK/CPConfig.cs @@ -1,11 +1,11 @@ using Newtonsoft.Json; -namespace BeatSaberPlus +namespace CP_SDK { /// - /// BeatSaberPlus SDK config + /// ChatPlex SDK config /// - internal class BSPConfig : CP_SDK.Config.JsonConfig + internal class CPConfig : Config.JsonConfig { [JsonProperty] internal bool FirstRun = true; [JsonProperty] internal bool FirstChatCoreRun = true; @@ -18,7 +18,7 @@ internal class BSPConfig : CP_SDK.Config.JsonConfig /// /// public override string GetRelativePath() - => "BeatSaberPlus/Config"; + => $"{ChatPlexSDK.ProductName}/Config"; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/CP_SDK/Chat/ChatImageProvider.cs b/BeatSaberPlus/CP_SDK/Chat/ChatImageProvider.cs index aaaedfa..8634e8a 100644 --- a/BeatSaberPlus/CP_SDK/Chat/ChatImageProvider.cs +++ b/BeatSaberPlus/CP_SDK/Chat/ChatImageProvider.cs @@ -83,7 +83,7 @@ public class ChatImageProvider internal static void Init() { m_CacheFolder = Path.Combine(ChatPlexSDK.BasePath, $"UserData/{ChatPlexSDK.ProductName}/Cache/Chat/"); - m_WebClient = new Network.WebClient(); + m_WebClient = new Network.WebClient("", TimeSpan.FromSeconds(10)); m_WebClient.Timeout = 10; try @@ -141,7 +141,7 @@ public static void TryCacheSingleImage(Interfaces.EChatResourceCategory p_Catego if (string.IsNullOrEmpty(p_URL)) { - ChatPlexSDK.Logger.Error($"[CP_SDK.Chat][ImageProvider.DownloadContent] URI is null or empty in request for resource {p_URL}. Aborting!"); + ChatPlexSDK.Logger.Error($"[CP_SDK.Chat][ChatImageProvider.TryCacheSingleImage] URI is null or empty in request for resource {p_URL} ID:{p_ID}. Aborting!"); m_CachedImageInfo[p_URL] = null; p_Finally?.Invoke(null); @@ -239,7 +239,7 @@ private static void DownloadContent(string p_URL, string p_CacheID, Action + m_WebClient.GetAsync(p_URL, System.Threading.CancellationToken.None, (p_Result) => { if (p_Result == null) { @@ -248,18 +248,18 @@ private static void DownloadContent(string p_URL, string p_CacheID, Action 0) + if (m_CacheEnabled && p_Result?.BodyBytes != null && p_Result.BodyBytes.Length > 0) { Unity.MTThreadInvoker.EnqueueOnThread(() => { if (!Directory.Exists(m_CacheFolder)) Directory.CreateDirectory(m_CacheFolder); - File.WriteAllBytes(m_CacheFolder + p_CacheID, p_Result); + File.WriteAllBytes(m_CacheFolder + p_CacheID, p_Result.BodyBytes); }); } - m_ActiveDownloads[p_URL]?.Invoke(p_Result); + m_ActiveDownloads[p_URL]?.Invoke(p_Result.BodyBytes); m_ActiveDownloads.TryRemove(p_URL, out var _); }); } diff --git a/BeatSaberPlus/CP_SDK/Chat/ChatModSettings.cs b/BeatSaberPlus/CP_SDK/Chat/ChatModSettings.cs index 9c7a0a3..47c7c97 100644 --- a/BeatSaberPlus/CP_SDK/Chat/ChatModSettings.cs +++ b/BeatSaberPlus/CP_SDK/Chat/ChatModSettings.cs @@ -11,10 +11,11 @@ public class ChatModSettings : Config.JsonConfig { internal class _Emotes { - [JsonProperty] internal bool ParseBTTVEmotes = true; - [JsonProperty] internal bool ParseFFZEmotes = true; - [JsonProperty] internal bool Parse7TVEmotes = true; - [JsonProperty] internal bool ParseEmojis = true; + [JsonProperty] internal bool ParseBTTVEmotes = true; + [JsonProperty] internal bool ParseFFZEmotes = true; + [JsonProperty] internal bool Parse7TVEmotes = true; + [JsonProperty] internal bool ParseEmojis = true; + [JsonProperty] internal bool ParseTemporaryChannels = true; } [JsonProperty] internal bool LaunchWebAppOnStartup = true; @@ -26,10 +27,11 @@ internal class _Emotes //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - [JsonIgnore] public bool Emotes_ParseBTTVEmotes => Emotes.ParseBTTVEmotes; - [JsonIgnore] public bool Emotes_ParseFFZEmotes => Emotes.ParseFFZEmotes; - [JsonIgnore] public bool Emotes_Parse7TVEmotes => Emotes.Parse7TVEmotes; - [JsonIgnore] public bool Emotes_ParseEmojis => Emotes.ParseEmojis; + [JsonIgnore] public bool Emotes_ParseBTTVEmotes => Emotes.ParseBTTVEmotes; + [JsonIgnore] public bool Emotes_ParseFFZEmotes => Emotes.ParseFFZEmotes; + [JsonIgnore] public bool Emotes_Parse7TVEmotes => Emotes.Parse7TVEmotes; + [JsonIgnore] public bool Emotes_ParseEmojis => Emotes.ParseEmojis; + [JsonIgnore] public bool Emotes_ParseTemporaryChannels => Emotes.ParseTemporaryChannels; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/EBadgeType.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/EBadgeType.cs new file mode 100644 index 0000000..53d9c5d --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/EBadgeType.cs @@ -0,0 +1,8 @@ +namespace CP_SDK.Chat.Interfaces +{ + public enum EBadgeType + { + Image, + Emoji + } +} diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatBadge.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatBadge.cs index 140f5df..c36bb5a 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatBadge.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatBadge.cs @@ -1,16 +1,10 @@ namespace CP_SDK.Chat.Interfaces { - public enum EBadgeType - { - Image, - Emoji - } - public interface IChatBadge { - string Id { get; } - string Name { get; } - EBadgeType Type { get; } - string Content { get; } + string Id { get; } + string Name { get; } + EBadgeType Type { get; } + string Content { get; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatChannel.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatChannel.cs index 8c99ba5..6a90b0f 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatChannel.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatChannel.cs @@ -2,12 +2,12 @@ { public interface IChatChannel { - string Name { get; } - string Id { get; } - bool IsTemp { get; } - string Prefix { get; } - bool CanSendMessages { get; } - bool Live { get; } - int ViewerCount { get; } + string Id { get; } + string Name { get; } + bool IsTemp { get; } + string Prefix { get; } + bool CanSendMessages { get; } + bool Live { get; } + int ViewerCount { get; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatChannelPointEvent.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatChannelPointEvent.cs index 6db97a2..e2d49de 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatChannelPointEvent.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatChannelPointEvent.cs @@ -2,13 +2,13 @@ { public interface IChatChannelPointEvent { - string RewardID { get; } - string TransactionID { get; } - string Title { get; } - string Prompt { get; } - string UserInput { get; } - int Cost { get; } - string Image { get; } - string BackgroundColor { get; } + string RewardID { get; } + string TransactionID { get; } + string Title { get; } + string Prompt { get; } + string UserInput { get; } + int Cost { get; } + string Image { get; } + string BackgroundColor { get; } } } \ No newline at end of file diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatEmote.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatEmote.cs index 78cb56c..d4ed0c8 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatEmote.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatEmote.cs @@ -1,14 +1,14 @@ -using CP_SDK.Chat.Models; +using CP_SDK.Animation; namespace CP_SDK.Chat.Interfaces { public interface IChatEmote { - string Id { get; } - string Name { get; } - string Uri { get; } - int StartIndex { get; } - int EndIndex { get; } - Animation.EAnimationType Animation { get; } + string Id { get; } + string Name { get; } + string Uri { get; } + int StartIndex { get; } + int EndIndex { get; } + EAnimationType Animation { get; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatMessageHandler.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatMessageHandler.cs deleted file mode 100644 index 85ed47f..0000000 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatMessageHandler.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace CP_SDK.Chat.Interfaces -{ - public interface IChatMessageHandler - { - void OnMessageReceived(IChatMessage messasge); - } -} diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatResourceData.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatResourceData.cs index 28b20dd..d7bbeec 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatResourceData.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatResourceData.cs @@ -1,4 +1,6 @@ -namespace CP_SDK.Chat.Interfaces +using CP_SDK.Animation; + +namespace CP_SDK.Chat.Interfaces { public enum EChatResourceCategory { @@ -9,9 +11,9 @@ public enum EChatResourceCategory public interface IChatResourceData { - string Uri { get; } - Animation.EAnimationType Animation { get; } - EChatResourceCategory Category { get; } - string Type { get; } + string Uri { get; } + EAnimationType Animation { get; } + EChatResourceCategory Category { get; } + string Type { get; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatResourceProvider.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatResourceProvider.cs index 5c499f3..3719347 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatResourceProvider.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatResourceProvider.cs @@ -6,7 +6,14 @@ namespace CP_SDK.Chat.Interfaces public interface IChatResourceProvider { ConcurrentDictionary Resources { get; } - Task TryRequestResources(string category, string p_Token); + /// + /// Try request resources from the provider + /// + /// ID of the channel + /// Name of the channel + /// Access token for the API + /// + Task TryRequestResources(string p_ChannelID, string p_ChannelName, string p_AccessToken); bool TryGetResource(string identifier, string category, out T data); } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatService.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatService.cs index 3cc283a..a90f225 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatService.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using UnityEngine; namespace CP_SDK.Chat.Interfaces { @@ -13,12 +14,22 @@ public interface IChatService /// The display name of the service(s) /// string DisplayName { get; } + /// + /// Side handle of each message color + /// + Color AccentColor { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// /// /// Channels /// ReadOnlyCollection<(IChatService, IChatChannel)> Channels { get; } + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + /// /// On system message /// @@ -28,6 +39,9 @@ public interface IChatService /// event Action OnLogin; + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + /// /// Callback that occurs when the user joins a chat channel /// @@ -69,6 +83,9 @@ public interface IChatService /// event Action OnChannelRaid; + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + /// /// Callback that occurs when a text message is received /// diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatServiceResourceProvider.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatServiceResourceProvider.cs new file mode 100644 index 0000000..89876ce --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatServiceResourceProvider.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace CP_SDK.Chat.Interfaces +{ + public interface IChatServiceResourceManager + where t_EmoteType : IChatResourceData + { + bool IsReady { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Request global resources + /// + void TryRequestGlobalResources(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Try request channel resources + /// + /// Channel instance + /// Callback + void TryRequestChannelResources(IChatChannel p_Channel, Action> p_OnChannelResourceDataCached); + /// + /// Release channel + /// + /// Channel instance + void TryReleaseChannelResources(IChatChannel p_Channel); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Try get a custom user display name + /// + /// UserID + /// Default display name + /// Output painted name + /// + bool TryGetUserDisplayName(string p_UserID, string p_Default, out string p_PaintedName); + /// + /// Get third party emote + /// + /// Word + /// ID of the channel + /// + /// + bool TryGetThirdPartyEmote(string p_Word, string p_ChannelID, out t_EmoteType p_Data); + } +} diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatSubscriptionEvent.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatSubscriptionEvent.cs index 89362e4..9505418 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatSubscriptionEvent.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatSubscriptionEvent.cs @@ -2,10 +2,10 @@ { public interface IChatSubscriptionEvent { - string DisplayName { get; } - string SubPlan { get; } - bool IsGift { get; } - string RecipientDisplayName { get; } - int PurchasedMonthCount { get; } + string DisplayName { get; } + string SubPlan { get; } + bool IsGift { get; } + string RecipientDisplayName { get; } + int PurchasedMonthCount { get; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatUser.cs b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatUser.cs index ecfafa4..7d834c8 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatUser.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Interfaces/IChatUser.cs @@ -2,15 +2,15 @@ { public interface IChatUser { - string Id { get; } - string UserName { get; } - string DisplayName { get; } - string PaintedName { get; } - string Color { get; } - bool IsBroadcaster { get; } - bool IsModerator { get; } - bool IsSubscriber { get; } - bool IsVip { get; } - IChatBadge[] Badges { get; } + string Id { get; } + string UserName { get; } + string DisplayName { get; } + string PaintedName { get; } + string Color { get; } + bool IsBroadcaster { get; } + bool IsModerator { get; } + bool IsSubscriber { get; } + bool IsVip { get; } + IChatBadge[] Badges { get; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/ChatResourceData.cs b/BeatSaberPlus/CP_SDK/Chat/Models/ChatResourceData.cs index 4aadeb6..0c17a06 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/ChatResourceData.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/ChatResourceData.cs @@ -4,9 +4,9 @@ namespace CP_SDK.Chat.Models { public class ChatResourceData : IChatResourceData { - public string Uri { get; set; } - public Animation.EAnimationType Animation { get; set; } - public EChatResourceCategory Category { get; set; } - public string Type { get; set; } + public string Uri { get; set; } + public Animation.EAnimationType Animation { get; set; } + public EChatResourceCategory Category { get; set; } + public string Type { get; set; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Emoji.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Emoji.cs index e55319e..2af29a0 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Emoji.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Emoji.cs @@ -4,13 +4,11 @@ namespace CP_SDK.Chat.Models { public class Emoji : IChatEmote { - public string Id { get; internal set; } - public string Name { get; internal set; } - public string Uri { get; internal set; } - public int StartIndex { get; internal set; } - public int EndIndex { get; internal set; } - public Animation.EAnimationType Animation { get; internal set; } - public EmoteType Type { get; internal set; } - public ImageRect UVs { get; internal set; } + public string Id { get; internal set; } + public string Name { get; internal set; } + public string Uri { get; internal set; } + public int StartIndex { get; internal set; } + public int EndIndex { get; internal set; } + public Animation.EAnimationType Animation { get; internal set; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/EmoteType.cs b/BeatSaberPlus/CP_SDK/Chat/Models/EmoteType.cs deleted file mode 100644 index f4bc059..0000000 --- a/BeatSaberPlus/CP_SDK/Chat/Models/EmoteType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CP_SDK.Chat.Models -{ - public enum EmoteType - { - SingleImage = 0, - SpriteSheet = 1 - } -} diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/ImageRect.cs b/BeatSaberPlus/CP_SDK/Chat/Models/ImageRect.cs deleted file mode 100644 index 0e231ba..0000000 --- a/BeatSaberPlus/CP_SDK/Chat/Models/ImageRect.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CP_SDK.Chat.Models -{ - public struct ImageRect - { - public int x; - public int y; - public int width; - public int height; - } -} diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchBadge.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchBadge.cs index 2f80e63..9f27a27 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchBadge.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchBadge.cs @@ -4,9 +4,9 @@ namespace CP_SDK.Chat.Models.Twitch { public struct TwitchBadge : IChatBadge { - public string Id { get; internal set; } - public string Name { get; internal set; } - public EBadgeType Type { get; internal set; } - public string Content { get; internal set; } + public string Id { get; internal set; } + public string Name { get; internal set; } + public EBadgeType Type { get; internal set; } + public string Content { get; internal set; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchChannel.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchChannel.cs index 12e7895..93e58d6 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchChannel.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchChannel.cs @@ -4,13 +4,13 @@ namespace CP_SDK.Chat.Models.Twitch { public class TwitchChannel : IChatChannel { - public string Id { get; internal set; } - public string Name { get; internal set; } - public bool IsTemp { get; internal set; } = false; - public string Prefix { get; internal set; } - public bool CanSendMessages { get; internal set; } = true; - public bool Live { get; internal set; } = false; - public int ViewerCount { get; internal set; } = 0; - public TwitchRoomstate Roomstate { get; internal set; } + public string Id { get; internal set; } + public string Name { get; internal set; } + public bool IsTemp { get; internal set; } = false; + public string Prefix { get; internal set; } + public bool CanSendMessages { get; internal set; } = true; + public bool Live { get; internal set; } = false; + public int ViewerCount { get; internal set; } = 0; + public TwitchRoomState Roomstate { get; internal set; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchChannelPointEvent.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchChannelPointEvent.cs index c5a17be..5b73ea0 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchChannelPointEvent.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchChannelPointEvent.cs @@ -4,20 +4,20 @@ namespace CP_SDK.Chat.Models.Twitch { public class TwitchChannelPointEvent : IChatChannelPointEvent { - public string RewardID { get; internal set; } + public string RewardID { get; internal set; } - public string TransactionID { get; internal set; } + public string TransactionID { get; internal set; } - public string Title { get; internal set; } + public string Title { get; internal set; } - public string Prompt { get; internal set; } + public string Prompt { get; internal set; } - public string UserInput { get; internal set; } + public string UserInput { get; internal set; } - public int Cost { get; internal set; } + public int Cost { get; internal set; } - public string Image { get; internal set; } + public string Image { get; internal set; } - public string BackgroundColor { get; internal set; } + public string BackgroundColor { get; internal set; } } } \ No newline at end of file diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchCheermoteData.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchCheermoteData.cs index 5189a76..6968757 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchCheermoteData.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchCheermoteData.cs @@ -3,29 +3,23 @@ namespace CP_SDK.Chat.Models.Twitch { - public class CheermoteTier : IChatResourceData - { - public string Uri { get; internal set; } - public int MinBits { get; internal set; } - public string Color { get; internal set; } - public Animation.EAnimationType Animation { get; internal set; } = CP_SDK.Animation.EAnimationType.GIF; - public EChatResourceCategory Category { get; internal set; } = EChatResourceCategory.Cheermote; - public string Type { get; internal set; } = "TwitchCheermote"; - } - public class TwitchCheermoteData { - public string Prefix; - public List Tiers = new List(); + public string Prefix; + public List Tiers = new List(); - public CheermoteTier GetTier(int numBits) + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public TwitchCheermoteTier GetTier(int p_BitsAmount) { - for (int i = 1; i < Tiers.Count; i++) + for (int l_I = 1; l_I < Tiers.Count; l_I++) { - if (numBits < Tiers[i].MinBits) - return Tiers[i - 1]; + if (p_BitsAmount < Tiers[l_I].MinBits) + return Tiers[l_I - 1]; } - return Tiers[0]; + + return Tiers.Count > 0 ? Tiers[0] : null; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchCheermoteTier.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchCheermoteTier.cs new file mode 100644 index 0000000..8e63544 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchCheermoteTier.cs @@ -0,0 +1,15 @@ +using CP_SDK.Animation; +using CP_SDK.Chat.Interfaces; + +namespace CP_SDK.Chat.Models.Twitch +{ + public class TwitchCheermoteTier : IChatResourceData + { + public string Uri { get; internal set; } + public int MinBits { get; internal set; } + public string Color { get; internal set; } + public EAnimationType Animation { get; internal set; } = EAnimationType.GIF; + public EChatResourceCategory Category { get; internal set; } = EChatResourceCategory.Cheermote; + public string Type { get; internal set; } = "TwitchCheermote"; + } +} diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchEmote.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchEmote.cs index 5e4c6f6..f0b5f30 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchEmote.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchEmote.cs @@ -1,16 +1,17 @@ -using CP_SDK.Chat.Interfaces; +using CP_SDK.Animation; +using CP_SDK.Chat.Interfaces; namespace CP_SDK.Chat.Models.Twitch { public struct TwitchEmote : IChatEmote { - public string Id { get; internal set; } - public string Name { get; internal set; } - public string Uri { get; internal set; } - public int StartIndex { get; internal set; } - public int EndIndex { get; internal set; } - public Animation.EAnimationType Animation { get; internal set; } - public int Bits { get; internal set; } - public string Color { get; internal set; } + public string Id { get; internal set; } + public string Name { get; internal set; } + public string Uri { get; internal set; } + public int StartIndex { get; internal set; } + public int EndIndex { get; internal set; } + public EAnimationType Animation { get; internal set; } + public int Bits { get; internal set; } + public string Color { get; internal set; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchHelix.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchHelix.cs index eba3aa0..bfaf550 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchHelix.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchHelix.cs @@ -8,16 +8,11 @@ namespace CP_SDK.Chat.Models.Twitch [Serializable] public class Helix_TokenValidate { - [JsonProperty] - public string client_id = ""; - [JsonProperty] - public string login = ""; - [JsonProperty] - public List scopes = new List(); - [JsonProperty] - public string user_id = ""; - [JsonProperty] - public int expires_in = 0; + [JsonProperty] public string client_id = ""; + [JsonProperty] public string login = ""; + [JsonProperty] public List scopes = new List(); + [JsonProperty] public string user_id = ""; + [JsonProperty] public int expires_in = 0; } //////////////////////////////////////////////////////////////////////////// @@ -107,6 +102,17 @@ public class MaxPerStream } */ + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + [Serializable] + public class Helix_CreateClip + { + [JsonProperty] public string edit_url { get; protected set; } + [JsonProperty] public string id { get; protected set; } + } + //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchMessage.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchMessage.cs index 792be7d..a9b8bd4 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchMessage.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchMessage.cs @@ -5,21 +5,24 @@ namespace CP_SDK.Chat.Models.Twitch { public class TwitchMessage : IChatMessage, ICloneable { - public string Id { get; internal set; } - public string Message { get; internal set; } - public bool IsSystemMessage { get; internal set; } - public bool IsActionMessage { get; internal set; } - public bool IsHighlighted { get; internal set; } - public bool IsPing { get; internal set; } - public bool IsRaid { get; internal set; } - public int RaidViewerCount { get; internal set; } - public IChatUser Sender { get; internal set; } - public IChatChannel Channel { get; internal set; } - public IChatEmote[] Emotes { get; internal set; } - public string TargetUserId { get; internal set; } - public string TargetMsgId { get; internal set; } - public string Type { get; internal set; } - public int Bits { get; internal set; } + public string Id { get; internal set; } + public string Message { get; internal set; } + public bool IsSystemMessage { get; internal set; } + public bool IsActionMessage { get; internal set; } + public bool IsHighlighted { get; internal set; } + public bool IsPing { get; internal set; } + public bool IsRaid { get; internal set; } + public int RaidViewerCount { get; internal set; } + public IChatUser Sender { get; internal set; } + public IChatChannel Channel { get; internal set; } + public IChatEmote[] Emotes { get; internal set; } + public string TargetUserId { get; internal set; } + public string TargetMsgId { get; internal set; } + public string Type { get; internal set; } + public int Bits { get; internal set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// public object Clone() { diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchRoomstate.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchRoomstate.cs index 75014ee..8c606d3 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchRoomstate.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchRoomstate.cs @@ -1,17 +1,14 @@ namespace CP_SDK.Chat.Models.Twitch { - /// - /// Twitch room state model - /// - public struct TwitchRoomstate + public struct TwitchRoomState { - public string BroadcasterLang { get; internal set; } - public string RoomId { get; internal set; } - public bool EmoteOnly { get; internal set; } - public bool FollowersOnly { get; internal set; } - public bool SubscribersOnly { get; internal set; } - public bool R9K { get; internal set; } - public int SlowModeInterval { get; internal set; } - public int MinFollowTime { get; internal set; } + public string BroadcasterLang { get; internal set; } + public string RoomId { get; internal set; } + public bool EmoteOnly { get; internal set; } + public bool FollowersOnly { get; internal set; } + public bool SubscribersOnly { get; internal set; } + public bool R9K { get; internal set; } + public int SlowModeInterval { get; internal set; } + public int MinFollowTime { get; internal set; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchSubscriptionEvent.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchSubscriptionEvent.cs index 4439f31..28c3917 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchSubscriptionEvent.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchSubscriptionEvent.cs @@ -7,10 +7,10 @@ namespace CP_SDK.Chat.Models.Twitch /// public class TwitchSubscriptionEvent : IChatSubscriptionEvent { - public string DisplayName { get; internal set; } - public string SubPlan { get; internal set; } - public bool IsGift { get; internal set; } - public string RecipientDisplayName { get; internal set; } - public int PurchasedMonthCount { get; internal set; } + public string DisplayName { get; internal set; } + public string SubPlan { get; internal set; } + public bool IsGift { get; internal set; } + public string RecipientDisplayName { get; internal set; } + public int PurchasedMonthCount { get; internal set; } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchUser.cs b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchUser.cs index 79d3bc3..6fd4212 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchUser.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Models/Twitch/TwitchUser.cs @@ -7,23 +7,23 @@ namespace CP_SDK.Chat.Models.Twitch /// public class TwitchUser : IChatUser { - public string Id { get; internal set; } - public string UserName { get; internal set; } - public string DisplayName { get; internal set; } - public string PaintedName { get; internal set; } - public string Color { get; internal set; } - public bool IsModerator { get; internal set; } - public bool IsBroadcaster { get; internal set; } - public bool IsSubscriber { get; internal set; } - public bool IsTurbo { get; internal set; } - public bool IsVip { get; internal set; } - public IChatBadge[] Badges { get; internal set; } + public string Id { get; internal set; } + public string UserName { get; internal set; } + public string DisplayName { get; internal set; } + public string PaintedName { get; internal set; } + public string Color { get; internal set; } + public bool IsModerator { get; internal set; } + public bool IsBroadcaster { get; internal set; } + public bool IsSubscriber { get; internal set; } + public bool IsTurbo { get; internal set; } + public bool IsVip { get; internal set; } + public IChatBadge[] Badges { get; internal set; } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - internal int _BadgesCache = 0; - internal bool _FancyNameReady = false; - internal bool _HadFollowed = false; + internal int _BadgesCache = 0; + internal bool _FancyNameReady = false; + internal bool _HadFollowed = false; } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Resources/index.html b/BeatSaberPlus/CP_SDK/Chat/Resources/index.html index 6412f84..9d5fd24 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Resources/index.html +++ b/BeatSaberPlus/CP_SDK/Chat/Resources/index.html @@ -84,6 +84,8 @@ window.top.history.replaceState("statedata", "{APPLICATION_NAME} Settings", l_URL.substring(0, l_URL.indexOf("#"))); } + var g_ServicesCount = {_SERVICES_COUNT_}; + {_JS_} document.addEventListener('keypress', function (e) { diff --git a/BeatSaberPlus/CP_SDK/Chat/Service.cs b/BeatSaberPlus/CP_SDK/Chat/Service.cs index 9949470..92f2de4 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Service.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Service.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Runtime.Remoting.Messaging; namespace CP_SDK.Chat { @@ -137,7 +136,7 @@ public static void Release(bool p_OnExit = false) /// /// Open web configurator /// - public static void OpenWebConfigurator() + public static void OpenWebConfiguration() { Process.Start($"http://localhost:{ChatModSettings.Instance.WebAppPort}"); } @@ -187,22 +186,31 @@ private static void Create() if (m_Services != null) return; - /// Init services - m_Services = new List(); - m_Services.Add(new Services.Twitch.TwitchService()); - m_Services.AddRange(m_ExternalServices); - - /// Run all services - m_ChatCoreMutiplixer = new Services.ChatServiceMultiplexer(m_Services); - m_ChatCoreMutiplixer.OnChannelResourceDataCached += ChatCoreMutiplixer_OnChannelResourceDataCached; - m_ChatCoreMutiplixer.OnTextMessageReceived += ChatCoreMutiplixer_OnTextMessageReceived; - m_ChatCoreMutiplixer.OnLiveStatusUpdated += ChatCoreMutiplixer_OnLiveStatusUpdated; - - /// WebApp - WebApp.Start(); - - if (ChatModSettings.Instance.LaunchWebAppOnStartup) - OpenWebConfigurator(); + try + { + /// Init services + m_Services = new List() { + new Services.Twitch.TwitchService() + }; + m_Services.AddRange(m_ExternalServices); + + /// Run all services + m_ChatCoreMutiplixer = new Services.ChatServiceMultiplexer(m_Services); + m_ChatCoreMutiplixer.OnChannelResourceDataCached += ChatCoreMutiplixer_OnChannelResourceDataCached; + m_ChatCoreMutiplixer.OnTextMessageReceived += ChatCoreMutiplixer_OnTextMessageReceived; + m_ChatCoreMutiplixer.OnLiveStatusUpdated += ChatCoreMutiplixer_OnLiveStatusUpdated; + + /// WebApp + WebApp.Start(); + + if (ChatModSettings.Instance.LaunchWebAppOnStartup) + OpenWebConfiguration(); + } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error("[CP_SDK.Chat][Service.Create] Failed to create service:"); + ChatPlexSDK.Logger.Error(l_Exception); + } } /// /// Destroy @@ -228,7 +236,10 @@ private static void Destroy() foreach (var l_Service in m_Services) l_Service.Stop(); - m_Services = null; + m_Services = null; + m_Started = false; + m_LoadingEmotes = 0; + m_LoadedEmotes = 0; } //////////////////////////////////////////////////////////////////////////// @@ -327,14 +338,8 @@ private static void ChatCoreMutiplixer_OnChannelResourceDataCached(IChatService /// Chat message private static void ChatCoreMutiplixer_OnTextMessageReceived(IChatService p_Service, IChatMessage p_Message) { - try - { - Discrete_OnTextMessageReceived?.Invoke(p_Service, p_Message); - } - catch - { - - } + try { Discrete_OnTextMessageReceived?.Invoke(p_Service, p_Message); } + catch { } } /// /// On room video playback updated @@ -345,14 +350,8 @@ private static void ChatCoreMutiplixer_OnTextMessageReceived(IChatService p_Serv /// Viewer count private static void ChatCoreMutiplixer_OnLiveStatusUpdated(IChatService p_Service, IChatChannel p_Channel, bool p_StreamUP, int p_ViewerCount) { - try - { - Discrete_OnLiveStatusUpdated?.Invoke(p_Service, p_Channel, p_StreamUP, p_ViewerCount); - } - catch - { - - } + try { Discrete_OnLiveStatusUpdated?.Invoke(p_Service, p_Channel, p_StreamUP, p_ViewerCount); } + catch { } } } } diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/ChatPlexGradientNamesDataProvider.cs b/BeatSaberPlus/CP_SDK/Chat/Services/ChatPlexGradientNamesDataProvider.cs new file mode 100644 index 0000000..b4a4931 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Chat/Services/ChatPlexGradientNamesDataProvider.cs @@ -0,0 +1,210 @@ +using CP_SDK.Chat.Interfaces; +using CP_SDK.Chat.Models; +using CP_SDK.Unity.Extensions; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using UnityEngine; + +namespace CP_SDK.Chat.Services +{ + public class ChatPlexGradientNamesDataProvider : IChatResourceProvider + { + /// + /// Paint cached stop + /// + private struct CachedPaintStop + { + [JsonProperty] internal float Stop; + [JsonProperty] internal Color32 StopColor; + } + private struct CustomPaint + { +#pragma warning disable CS0649 + [JsonProperty] internal object[][] Stops; + [JsonProperty] internal string[] Users; +#pragma warning restore CS0649 + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private string m_GradientNamesURL = ""; + private HttpClient m_HTTPClient = new HttpClient(); + private ConcurrentDictionary m_Paints = new ConcurrentDictionary(); + private ConcurrentDictionary m_PaintCache = new ConcurrentDictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public ConcurrentDictionary Resources { get; } = new ConcurrentDictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// Gradient names URL + public ChatPlexGradientNamesDataProvider(string p_GradientNamesURL) + { + m_GradientNamesURL = p_GradientNamesURL; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Try request resources from the provider + /// + /// ID of the channel + /// Name of the channel + /// Access token for the API + /// + public async Task TryRequestResources(string p_ChannelID, string p_ChannelName, string p_AccessToken) + { + var l_IsGlobal = string.IsNullOrEmpty(p_ChannelID); + if (!l_IsGlobal) + return; + + try + { + await RequestChatPlexGradientNames(); + } + catch (Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.Chat.Services][ChatPlexGlobalDataProvider.TryRequestResources] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Try get a resource + /// + /// Resource ID + /// Channel / Category + /// Result data + /// + public bool TryGetResource(string p_Identifier, string p_Category, out ChatResourceData p_Data) + { + p_Data = null; + return false; + } + /// + /// Try get a custom user display name + /// + /// UserID + /// Default display name + /// Output painted name + /// + public bool TryGetUserDisplayName(string p_UserID, string p_Default, out string p_PaintedName) + { + p_PaintedName = p_Default; + if (string.IsNullOrEmpty(p_UserID)) + return false; + + if (m_PaintCache.TryGetValue(p_UserID, out var l_Cached)) + { + p_PaintedName = l_Cached; + return true; + } + else if (m_Paints.TryGetValue(p_UserID, out var l_Paint)) + { + var l_PaintedName = ""; + for (int l_C = 0; l_C < p_Default.Length; ++l_C) + { + var l_Progress = (float)l_C / (float)p_Default.Length; + var l_StopA = new CachedPaintStop() { Stop = -1f, StopColor = Color.black }; + var l_StopB = new CachedPaintStop() { Stop = -1f, StopColor = Color.black }; + + for (int l_S = 0; l_S < l_Paint.Length; ++l_S) + { + var l_CurrentStop = l_Paint[l_S]; + if (l_CurrentStop.Stop >= l_StopA.Stop && l_CurrentStop.Stop <= l_Progress) + l_StopA = l_CurrentStop; + else if (l_CurrentStop.Stop >= l_Progress) + { + l_StopB = l_CurrentStop; + break; + } + } + + var l_Color = Color.Lerp(l_StopA.StopColor, l_StopB.StopColor, (l_Progress - l_StopA.Stop) / (l_StopB.Stop - l_StopA.Stop)); + l_PaintedName += "" + p_Default[l_C] + ""; + } + + m_PaintCache.TryAdd(p_UserID, l_PaintedName); + p_PaintedName = l_PaintedName; + + return true; + } + + return false; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Request ChatPlex gradient names + /// + /// + private async Task RequestChatPlexGradientNames() + { + if (string.IsNullOrEmpty(m_GradientNamesURL)) + return; + + ChatPlexSDK.Logger.Debug($"[CP_SDK.Chat.Services][ChatPlexGlobalDataProvider.TryRequestResources] {m_GradientNamesURL}"); + + using (var l_Message = new HttpRequestMessage(HttpMethod.Get, m_GradientNamesURL)) + { + var l_Response = await m_HTTPClient.SendAsync(l_Message).ConfigureAwait(false); + if (l_Response.IsSuccessStatusCode) + { + var l_CustomPaintings = JsonConvert.DeserializeObject(await l_Response.Content.ReadAsStringAsync().ConfigureAwait(false)); + var l_Count = 0; + + for (int l_I = 0; l_I < l_CustomPaintings.Length; ++l_I) + { + var l_CustomPainting = l_CustomPaintings[l_I]; + var l_StopsS = l_CustomPainting.Stops[0]; + var l_StopsC = l_CustomPainting.Stops[1]; + var l_Converted = new List(); + + for (int l_SI = 0; l_SI < l_StopsS.Length; ++l_SI) + { + l_Converted.Add(new CachedPaintStop() + { + Stop = float.Parse(l_StopsS[l_SI].ToString()), + StopColor = ColorU.ToUnityColor(((string)l_StopsC[l_SI])) + }); + } + + var l_AsArray = l_Converted.ToArray(); + for (int l_UI = 0; l_UI < l_CustomPainting.Users.Length; ++l_UI) + { + var l_UserID = l_CustomPainting.Users[l_UI]; + if (m_Paints.ContainsKey(l_UserID)) + m_Paints[l_UserID] = l_AsArray; + else + m_Paints.TryAdd(l_UserID, l_AsArray); + l_Count++; + } + } + + ChatPlexSDK.Logger.Debug($"[CP_SDK.Chat.Services][ChatPlexGlobalDataProvider.TryRequestResources] Success caching {l_Count} gradient names."); + } + else + ChatPlexSDK.Logger.Error($"[CP_SDK.Chat.Services][ChatPlexGlobalDataProvider.TryRequestResources] Unsuccessful status code when requesting gradient names: {l_Response.ReasonPhrase}"); + } + + m_PaintCache.Clear(); + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/ChatServiceBase.cs b/BeatSaberPlus/CP_SDK/Chat/Services/ChatServiceBase.cs index fef970c..29cfb39 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/ChatServiceBase.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/ChatServiceBase.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Reflection; namespace CP_SDK.Chat.Services { diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/ChatServiceMultiplexer.cs b/BeatSaberPlus/CP_SDK/Chat/Services/ChatServiceMultiplexer.cs index a80c48f..9fff889 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/ChatServiceMultiplexer.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/ChatServiceMultiplexer.cs @@ -1,7 +1,9 @@ using CP_SDK.Chat.Interfaces; +using CP_SDK.Unity.Extensions; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text; +using UnityEngine; namespace CP_SDK.Chat.Services { @@ -14,6 +16,10 @@ public class ChatServiceMultiplexer : ChatServiceBase, IChatService /// The display name of the service(s) /// public string DisplayName { get; private set; } = "System"; + /// + /// Side handle of each message color + /// + public Color AccentColor { get; } = ColorU.WithAlpha(Color.gray, 0.75f); /// /// p_Channels diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/RelayChatService.cs b/BeatSaberPlus/CP_SDK/Chat/Services/RelayChatService.cs new file mode 100644 index 0000000..883481f --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Chat/Services/RelayChatService.cs @@ -0,0 +1,204 @@ +using CP_SDK.Chat.Interfaces; +using CP_SDK.Network; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using UnityEngine; + +namespace CP_SDK.Chat.Services +{ + public abstract class RelayChatService + : ChatServiceBase, IChatService + where t_EmoteType : IChatResourceData + where t_ServiceResourcerProvider : class, IChatServiceResourceManager + where t_ChannelType : IChatChannel + where t_UserType : IChatUser + { + public abstract string DisplayName { get; } + public abstract Color AccentColor { get; } + public ReadOnlyCollection<(IChatService, IChatChannel)> Channels => m_Channels.Select(x => (this as IChatService, x.Value as IChatChannel)).ToList().AsReadOnly(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + protected WebSocketClient m_WebSocket = null; + protected t_ServiceResourcerProvider m_DataProvider = null; + protected ConcurrentDictionary m_Channels = new ConcurrentDictionary(); + protected ConcurrentDictionary m_Users = new ConcurrentDictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Start the service + /// + public abstract void Start(); + /// + /// Stop the service + /// + public abstract void Stop(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Recache emotes + /// + public abstract void RecacheEmotes(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Web page HTML content + /// + /// + public abstract string WebPageHTMLForm(); + /// + /// Web page HTML content + /// + /// + public abstract string WebPageHTML(); + /// + /// Web page javascript content + /// + /// + public abstract string WebPageJS(); + /// + /// Web page javascript content + /// + /// + public abstract string WebPageJSValidate(); + /// + /// On web page post data + /// + /// Post data + public abstract void WebPageOnPost(Dictionary p_PostData); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Sends a text message to the specified IChatChannel + /// + /// The chat channel to send the message to + /// The text message to be sent + public void SendTextMessage(IChatChannel p_Channel, string p_Message) + { + if (!(p_Channel is t_ChannelType)) + return; + + RelayChatServiceProtocol.Send_SendMessage(m_WebSocket, p_Channel.Id, p_Message); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Is connected + /// + /// + public abstract bool IsConnectedAndLive(); + /// + /// Get primary channel name + /// + /// + public abstract string PrimaryChannelName(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Join temp channel with group identifier + /// + /// Group identifier + /// Name of the channel + /// Messages prefix + /// Can send message + public abstract void JoinTempChannel(string p_GroupIdentifier, string p_ChannelName, string p_Prefix, bool p_CanSendMessage); + /// + /// Leave temp channel + /// + /// Name of the channel + public abstract void LeaveTempChannel(string p_ChannelName); + /// + /// Is in temp channel + /// + /// Channel name + /// + public abstract bool IsInTempChannel(string p_ChannelName); + /// + /// Leave all temp channel by group identifier + /// + /// + public abstract void LeaveAllTempChannel(string p_GroupIdentifier); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Web socket open + /// + private void WebSocket_OnOpen() + { + m_OnSystemMessageCallbacks?.InvokeAll(this, $"Connected to {DisplayName}"); + + ChatPlexSDK.Logger.Info($"{DisplayName} connection opened"); + ChatPlexSDK.Logger.Info("Trying to login!"); + + //if (ModLicense.IsReady && ModLicense.Raw != null) + // RelayChatServiceProtocol.Send_AuthModLicense(m_WebSocket, ModLicense.Raw); + //else + // m_OnSystemMessageCallbacks?.InvokeAll(this, "Invalid license file!"); + +#if DEBUG + m_OnSystemMessageCallbacks?.InvokeAll(this, "[Debug] WebSocket_OnOpen"); +#endif + } + /// + /// Web socket close + /// + private void WebSocket_OnClose() + { + ChatPlexSDK.Logger.Info($"{DisplayName} connection closed"); + + CleanUpChannels(); + + m_OnSystemMessageCallbacks?.InvokeAll(this, $"Disconnected from {DisplayName}"); +#if DEBUG + m_OnSystemMessageCallbacks?.InvokeAll(this, "[Debug] WebSocket_OnClose"); +#endif + } + /// + /// Web socket error + /// + private void WebSocket_OnError() + { + ChatPlexSDK.Logger.Error($"An error occurred in {DisplayName} connection"); + + m_OnSystemMessageCallbacks?.InvokeAll(this, $"Disconnected from {DisplayName}, error"); +#if DEBUG + m_OnSystemMessageCallbacks?.InvokeAll(this, "[Debug] WebSocket_OnError"); +#endif + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Clean up all connected channels + /// + private void CleanUpChannels() + { + foreach (var l_Channel in m_Channels) + { + m_DataProvider.TryReleaseChannelResources(l_Channel.Value); + ChatPlexSDK.Logger.Info($"Removed channel {l_Channel.Value.Id} from the channel list."); + m_OnLiveStatusUpdatedCallbacks?.InvokeAll(this, l_Channel.Value, false, 0); + m_OnLeaveRoomCallbacks?.InvokeAll(this, l_Channel.Value); + } + m_Channels.Clear(); + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/RelayChatServiceProtocol.cs b/BeatSaberPlus/CP_SDK/Chat/Services/RelayChatServiceProtocol.cs new file mode 100644 index 0000000..aa66a37 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Chat/Services/RelayChatServiceProtocol.cs @@ -0,0 +1,16 @@ +using CP_SDK.Network; +using System; + +namespace CP_SDK.Chat.Services +{ + public static class RelayChatServiceProtocol + { + public const string PROTOCOL_VERSION = "v1.4"; + + public static void Send_AuthModLicense(WebSocketClient p_Socket, byte[] p_ModLicense) + => p_Socket.SendMessage($"AuthModLicense|{PROTOCOL_VERSION}|{(p_ModLicense?.Length > 0 ? Convert.ToBase64String(p_ModLicense) : "null")}"); + + public static void Send_SendMessage(WebSocketClient p_Socket, string p_ChannelID, string p_Message) + => p_Socket.SendMessage("SendMessage|" + p_ChannelID + "|" + p_Message); + } +} diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/7TVDataProvider.cs b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/7TVDataProvider.cs index bd35413..849828b 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/7TVDataProvider.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/7TVDataProvider.cs @@ -10,6 +10,7 @@ using UnityEngine; using System.Collections.Generic; using CP_SDK.Unity.Extensions; +using CP_SDK.Chat.Utilities; namespace CP_SDK.Chat.Services.Twitch { @@ -18,6 +19,12 @@ namespace CP_SDK.Chat.Services.Twitch /// public class _7TVDataProvider : IChatResourceProvider { + private const string s_PLATFORM_NAME = "Twitch"; + private const string s_REQUEST_PLATFORM_NAME = "twitch"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + /// /// Paint cached stop /// @@ -26,13 +33,6 @@ private struct CachedPaintStop [JsonProperty] internal float Stop; [JsonProperty] internal Color32 StopColor; } - private class CustomPaint - { -#pragma warning disable CS0649 - [JsonProperty] internal object[][] Stops; - [JsonProperty] internal string[] Users; -#pragma warning restore CS0649 - } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -62,54 +62,29 @@ private class CustomPaint //////////////////////////////////////////////////////////////////////////// /// - /// Try request resources + /// Try request resources from the provider /// - /// Category / Channel + /// ID of the channel + /// Name of the channel + /// Access token for the API /// - public async Task TryRequestResources(string p_Category, string p_Token) + public async Task TryRequestResources(string p_ChannelID, string p_ChannelName, string p_AccessToken) { - bool l_IsGlobal = string.IsNullOrEmpty(p_Category); + bool l_IsGlobal = string.IsNullOrEmpty(p_ChannelID); try { - ChatPlexSDK.Logger.Debug($"Requesting 7TV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : $" for channel {p_Category}")}. " + (l_IsGlobal ? "https://api.7tv.app/v2/emotes/global" : $"https://api.7tv.app/v2/users/{p_Category}/emotes")); - - using (HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, l_IsGlobal ? "https://api.7tv.app/v2/emotes/global" : $"https://api.7tv.app/v2/users/{p_Category}/emotes")) - { - var l_Response = await m_HTTPClient.SendAsync(msg).ConfigureAwait(false); - if (l_Response.IsSuccessStatusCode) - { - JSONNode l_JSON = JSON.Parse(await l_Response.Content.ReadAsStringAsync().ConfigureAwait(false)); - if (l_JSON.IsArray) - { - int l_Count = 0; - foreach (JSONObject l_Object in l_JSON.AsArray) - { - string l_URI = l_Object["urls"].AsArray.Count >= 2 ? l_Object["urls"].AsArray[2].AsArray[1] : l_Object["urls"].AsArray[0].AsArray[0]; - string l_ID = l_IsGlobal ? l_Object["name"].Value : $"{p_Category}_{l_Object["name"].Value}"; - - Resources.TryAdd(l_ID, new ChatResourceData() - { - Uri = l_URI, - Animation = Animation.EAnimationType.AUTODETECT, - Category = EChatResourceCategory.Emote, - Type = l_IsGlobal ? "7TVGlobalEmote" : "7TVChannelEmote" - }); - l_Count++; - } + int l_Count = 0; + if (l_IsGlobal) + l_Count += await RequestGlobalEmotes(); + else + l_Count += await RequestChannelEmotes(p_ChannelID); - ChatPlexSDK.Logger.Debug($"Success caching {l_Count} 7TV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_Category)}."); - } - else - ChatPlexSDK.Logger.Error("emotes was not an array."); - } - else - ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting 7TV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_Category)}. {l_Response.ReasonPhrase}"); - } + ChatPlexSDK.Logger.Debug($"Success caching {l_Count} 7TV {s_PLATFORM_NAME} {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); } catch (Exception l_Exception) { - ChatPlexSDK.Logger.Error($"An error occurred while requesting 7TV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_Category)}."); + ChatPlexSDK.Logger.Error($"An error occurred while requesting 7TV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); ChatPlexSDK.Logger.Error(l_Exception); } @@ -157,61 +132,6 @@ public async Task TryRequestResources(string p_Category, string p_Token) ChatPlexSDK.Logger.Error($"An error occurred while requesting 7TV cosmetics."); ChatPlexSDK.Logger.Error(l_Exception); } - - try - { - ChatPlexSDK.Logger.Debug($"Requesting 7TV cosmetics"); - - using (var l_Message = new HttpRequestMessage(HttpMethod.Get, "https://data.chatplex.org/twitch_gradient_names.json")) - { - var l_Response = await m_HTTPClient.SendAsync(l_Message).ConfigureAwait(false); - if (l_Response.IsSuccessStatusCode) - { - var l_CustomPaintings = JsonConvert.DeserializeObject(await l_Response.Content.ReadAsStringAsync().ConfigureAwait(false)); - var l_Count = 0; - - for (int l_I = 0; l_I < l_CustomPaintings.Length; ++l_I) - { - var l_CustomPainting = l_CustomPaintings[l_I]; - var l_StopsS = l_CustomPainting.Stops[0]; - var l_StopsC = l_CustomPainting.Stops[1]; - var l_Converted = new List(); - - for (int l_SI = 0; l_SI < l_StopsS.Length; ++l_SI) - { - //ColorUtility.TryParseHtmlString((string)l_StopsC[l_SI], out var l_Color); - l_Converted.Add(new CachedPaintStop() - { - Stop = float.Parse(l_StopsS[l_SI].ToString()), - StopColor = ((string)l_StopsC[l_SI]).ToUnityColor() - }); - } - - var l_AsArray = l_Converted.ToArray(); - for (int l_UI = 0; l_UI < l_CustomPainting.Users.Length; ++l_UI) - { - var l_UserID = l_CustomPainting.Users[l_UI]; - if (m_Paints.ContainsKey(l_UserID)) - m_Paints[l_UserID] = l_AsArray; - else - m_Paints.TryAdd(l_UserID, l_AsArray); - l_Count++; - } - } - - ChatPlexSDK.Logger.Debug($"Success caching custom cosmetics ({l_Count} Paints)."); - } - else - ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting custom cosmetics. {l_Response.ReasonPhrase}"); - } - - m_PaintCache.Clear(); - } - catch (Exception l_Exception) - { - ChatPlexSDK.Logger.Error($"An error occurred while requesting custom cosmetics."); - ChatPlexSDK.Logger.Error(l_Exception); - } } } @@ -236,23 +156,24 @@ public bool TryGetResource(string p_Identifier, string p_Category, out ChatResou p_Data = null; return false; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - /// /// Try get a custom user display name /// - /// Twitch UserID + /// UserID /// Default display name + /// Output painted name /// - public string TryGetUserDisplayName(string p_UserID, string p_Default) + public bool TryGetUserDisplayName(string p_UserID, string p_Default, out string p_PaintedName) { + p_PaintedName = p_Default; if (string.IsNullOrEmpty(p_UserID)) - return p_Default; + return false; if (m_PaintCache.TryGetValue(p_UserID, out var l_Cached)) - return l_Cached; + { + p_PaintedName = l_Cached; + return true; + } else if (m_Paints.TryGetValue(p_UserID, out var l_Paint)) { var l_PaintedName = ""; @@ -275,14 +196,92 @@ public string TryGetUserDisplayName(string p_UserID, string p_Default) } var l_Color = Color.Lerp(l_StopA.StopColor, l_StopB.StopColor, (l_Progress - l_StopA.Stop) / (l_StopB.Stop - l_StopA.Stop)); - l_PaintedName += "" + p_Default[l_C] + ""; + l_PaintedName += "" + p_Default[l_C] + ""; } m_PaintCache.TryAdd(p_UserID, l_PaintedName); - return l_PaintedName; + p_PaintedName = l_PaintedName; + + return true; + } + + return false; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Request global emotes + /// + /// + private async Task RequestGlobalEmotes() + { + var l_URL = "https://7tv.io/v3/emote-sets/global"; + ChatPlexSDK.Logger.Debug($"Requesting 7TV {s_PLATFORM_NAME} global emotes. {l_URL}"); + + try + { + using (HttpRequestMessage l_Request = new HttpRequestMessage(HttpMethod.Get, l_URL)) + { + var l_Response = await m_HTTPClient.SendAsync(l_Request); + if (!l_Response.IsSuccessStatusCode) + { + ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting 7TV {s_PLATFORM_NAME} global emotes. {l_Response.ReasonPhrase}"); + return 0; + } + + var l_JSON = JSON.Parse(await l_Response.Content.ReadAsStringAsync()); + if (l_JSON == null || !l_JSON.IsObject) + return 0; + + return _7TVUtils.ParseEmoteSet(this, l_JSON.AsObject, "7TVGlobalEmote", string.Empty); + } + } + catch (Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"An error occurred while requesting 7TV {s_PLATFORM_NAME} global emotes."); + ChatPlexSDK.Logger.Error(l_Exception); + } + + return 0; + } + /// + /// Request channel emotes + /// + /// Requester ID + /// + private async Task RequestChannelEmotes(string p_RequesterID) + { + var l_URL = $"https://7tv.io/v3/users/{s_REQUEST_PLATFORM_NAME}/{p_RequesterID}"; + ChatPlexSDK.Logger.Debug($"Requesting 7TV {s_PLATFORM_NAME} channel emotes. {l_URL}"); + + try + { + using (HttpRequestMessage l_Request = new HttpRequestMessage(HttpMethod.Get, l_URL)) + { + var l_Response = await m_HTTPClient.SendAsync(l_Request); + if (!l_Response.IsSuccessStatusCode) + { + ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting 7TV {s_PLATFORM_NAME} channel emotes. {l_Response.ReasonPhrase}"); + return 0; + } + + var l_JSON = JSON.Parse(await l_Response.Content.ReadAsStringAsync()); + if (l_JSON == null || !l_JSON.HasKey("emote_set")) + return 0; + + var l_EmoteSet = l_JSON["emote_set"].AsObject; + return _7TVUtils.ParseEmoteSet(this, l_EmoteSet, "7TVChannelEmote", p_RequesterID); + } + } + catch (Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"An error occurred while requesting 7TV {s_PLATFORM_NAME} global emotes."); + ChatPlexSDK.Logger.Error(l_Exception); } - return p_Default; + return 0; } //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/BTTVDataProvider.cs b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/BTTVDataProvider.cs index e220f6d..3beff28 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/BTTVDataProvider.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/BTTVDataProvider.cs @@ -30,24 +30,26 @@ public class BTTVDataProvider : IChatResourceProvider //////////////////////////////////////////////////////////////////////////// /// - /// Try request resources + /// Try request resources from the provider /// - /// Category / Channel + /// ID of the channel + /// Name of the channel + /// Access token for the API /// - public async Task TryRequestResources(string p_Category, string p_Token) + public async Task TryRequestResources(string p_ChannelID, string p_ChannelName, string p_AccessToken) { - bool l_IsGlobal = string.IsNullOrEmpty(p_Category); + bool l_IsGlobal = string.IsNullOrEmpty(p_ChannelID); try { - ChatPlexSDK.Logger.Debug($"Requesting BTTV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : $" for channel {p_Category}")}."); + ChatPlexSDK.Logger.Debug($"Requesting BTTV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : $" for channel {p_ChannelName}")}."); - using (var l_Message = new HttpRequestMessage(HttpMethod.Get, l_IsGlobal ? "https://api.betterttv.net/3/cached/emotes/global" : $"https://api.betterttv.net/3/cached/users/twitch/{p_Category}")) + using (var l_Message = new HttpRequestMessage(HttpMethod.Get, l_IsGlobal ? "https://api.betterttv.net/3/cached/emotes/global" : $"https://api.betterttv.net/3/cached/users/twitch/{p_ChannelID}")) { var l_Response = await m_HTTPClient.SendAsync(l_Message).ConfigureAwait(false); if (!l_Response.IsSuccessStatusCode) { - ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting BTTV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_Category)}. {l_Response.ReasonPhrase}"); + ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting BTTV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}. {l_Response.ReasonPhrase}"); return; } @@ -83,7 +85,7 @@ public async Task TryRequestResources(string p_Category, string p_Token) foreach (JSONObject l_Object in l_JSONEmotes) { string l_URI = $"https://cdn.betterttv.net/emote/{l_Object["id"].Value}/2x"; - string l_Identifier = l_Object["code"].Value; + string l_Identifier = $"{p_ChannelID}_{l_Object["code"].Value}"; Resources.TryAdd(l_Identifier, new ChatResourceData() { @@ -95,13 +97,13 @@ public async Task TryRequestResources(string p_Category, string p_Token) } } - ChatPlexSDK.Logger.Debug($"Success caching {l_Count} BTTV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_Category)}."); + ChatPlexSDK.Logger.Debug($"Success caching {l_Count} BTTV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); return; } } catch (Exception l_Exception) { - ChatPlexSDK.Logger.Error($"An error occurred while requesting BTTV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_Category)}."); + ChatPlexSDK.Logger.Error($"An error occurred while requesting BTTV {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); ChatPlexSDK.Logger.Error(l_Exception); } diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/FFZDataProvider.cs b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/FFZDataProvider.cs index 0894a5e..5a9971c 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/FFZDataProvider.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/FFZDataProvider.cs @@ -30,23 +30,25 @@ public class FFZDataProvider : IChatResourceProvider //////////////////////////////////////////////////////////////////////////// /// - /// Try request resources + /// Try request resources from the provider /// - /// Category / Channel + /// ID of the channel + /// Name of the channel + /// Access token for the API /// - public async Task TryRequestResources(string p_Category, string p_Token) + public async Task TryRequestResources(string p_ChannelID, string p_ChannelName, string p_AccessToken) { - bool l_IsGlobal = string.IsNullOrEmpty(p_Category); + bool l_IsGlobal = string.IsNullOrEmpty(p_ChannelID); try { - ChatPlexSDK.Logger.Debug($"Requesting FFZ {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : $" for channel {p_Category}")}."); - using (HttpRequestMessage l_Query = new HttpRequestMessage(HttpMethod.Get, l_IsGlobal ? "https://api.frankerfacez.com/v1/set/global" : $"https://api.frankerfacez.com/v1/room/{p_Category}")) + ChatPlexSDK.Logger.Debug($"Requesting FFZ {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : $" for channel {p_ChannelName}")}."); + using (HttpRequestMessage l_Query = new HttpRequestMessage(HttpMethod.Get, l_IsGlobal ? "https://api.frankerfacez.com/v1/set/global" : $"https://api.frankerfacez.com/v1/room/{p_ChannelName}")) { var l_Response = await m_HTTPClient.SendAsync(l_Query).ConfigureAwait(false); if (!l_Response.IsSuccessStatusCode) { - ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting FFZ {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_Category)}. {l_Response.ReasonPhrase}"); + ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting FFZ {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}. {l_Response.ReasonPhrase}"); return; } @@ -63,7 +65,7 @@ public async Task TryRequestResources(string p_Category, string p_Token) JSONObject l_URLs = l_Object["urls"].AsObject; string l_URI = l_URLs[l_URLs.Count - 1].Value; - string l_ID = l_IsGlobal ? l_Object["name"].Value : $"{p_Category}_{l_Object["name"].Value}"; + string l_ID = l_IsGlobal ? l_Object["name"].Value : $"{p_ChannelID}_{l_Object["name"].Value}"; if (l_URI.Length > 0 && l_URI[0] == '/') l_URI = "https:" + l_URI; @@ -78,13 +80,13 @@ public async Task TryRequestResources(string p_Category, string p_Token) l_Count++; } - ChatPlexSDK.Logger.Debug($"Success caching {l_Count} FFZ {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_Category)}."); + ChatPlexSDK.Logger.Debug($"Success caching {l_Count} FFZ {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); return; } } catch (Exception l_Exception) { - ChatPlexSDK.Logger.Error($"An error occurred while requesting FFZ {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_Category)}."); + ChatPlexSDK.Logger.Error($"An error occurred while requesting FFZ {(l_IsGlobal ? "global " : "")}emotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); ChatPlexSDK.Logger.Error(l_Exception); } diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchBadgeProvider.cs b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchBadgeProvider.cs index 29c3e09..991a402 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchBadgeProvider.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchBadgeProvider.cs @@ -5,7 +5,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; +using static System.Net.WebRequestMethods; namespace CP_SDK.Chat.Services.Twitch { @@ -31,61 +33,89 @@ public class TwitchBadgeProvider : IChatResourceProvider //////////////////////////////////////////////////////////////////////////// /// - /// Try request resources + /// Try request resources from the provider /// - /// Category / Channel + /// ID of the channel + /// Name of the channel + /// Access token for the API /// - public async Task TryRequestResources(string p_Category, string p_Token) + public async Task TryRequestResources(string p_ChannelID, string p_ChannelName, string p_AccessToken) { - bool l_IsGlobal = string.IsNullOrEmpty(p_Category); + var l_WebClient = new Network.WebClient("https://api.twitch.tv/helix/", TimeSpan.FromSeconds(10), true); + if (!l_WebClient.Headers.ContainsKey("Client-Id")) + l_WebClient.Headers.Remove("Authorization"); + + if (l_WebClient.Headers.ContainsKey("Client-Id")) + l_WebClient.Headers.Remove("Authorization"); + + l_WebClient.Headers.Add("client-id", TwitchService.TWITCH_CLIENT_ID); + l_WebClient.Headers.Add("Authorization", "Bearer " + p_AccessToken.Replace("oauth:", "")); + + bool l_IsGlobal = string.IsNullOrEmpty(p_ChannelID); try { - ChatPlexSDK.Logger.Debug($"Requesting Twitch {(l_IsGlobal ? "global " : "")}badges{(l_IsGlobal ? "." : $" for channel {p_Category}")}."); - using (HttpRequestMessage l_Query = new HttpRequestMessage(HttpMethod.Get, l_IsGlobal ? $"https://badges.twitch.tv/v1/badges/global/display" : $"https://badges.twitch.tv/v1/badges/channels/{p_Category}/display")) //channel.AsTwitchChannel().Roomstate.RoomId + ChatPlexSDK.Logger.Debug($"Requesting Twitch {(l_IsGlobal ? "global " : "")}badges{(l_IsGlobal ? "." : $" for channel {p_ChannelName}")}."); + + var l_URL = "chat/badges/global"; + if (!l_IsGlobal) + l_URL = $"chat/badges?broadcaster_id={p_ChannelID}"; + + var l_Completion = new TaskCompletionSource(); + l_WebClient.GetAsync(l_URL, CancellationToken.None, (p_Response) => { - var l_Response = await m_HTTPClient.SendAsync(l_Query).ConfigureAwait(false); - if (!l_Response.IsSuccessStatusCode) - { - ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting Twitch {(l_IsGlobal ? "global " : "")}badges{(l_IsGlobal ? "." : " for channel " + p_Category)}. {l_Response.ReasonPhrase}"); - return; - } + l_Completion.SetResult(p_Response); + }); + await l_Completion.Task; + var l_Response = l_Completion.Task?.Result; - JSONNode l_JSON = JSON.Parse(await l_Response.Content.ReadAsStringAsync().ConfigureAwait(false)); - if (!l_JSON["badge_sets"].IsObject) - { - ChatPlexSDK.Logger.Error("badge_sets was not an object."); - return; - } + if (l_Response == null || !l_Response.IsSuccessStatusCode) + { + ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting Twitch {(l_IsGlobal ? "global " : "")}badges{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}. {l_Response.ReasonPhrase}"); + return; + } - int l_Count = 0; - foreach (KeyValuePair l_KVP in l_JSON["badge_sets"]) + JSONNode l_JSON = JSON.Parse(l_Response.BodyString); + if (!l_JSON["data"].IsArray) + { + ChatPlexSDK.Logger.Error("data was not an object."); + return; + } + + var l_Data = l_JSON["data"].AsArray; + + int l_Count = 0; + foreach (KeyValuePair l_SetKVP in l_Data) + { + var l_Set = l_SetKVP.Value; + var l_SetID = l_Set["set_id"].Value; + var l_Versions = l_Set["versions"].AsArray; + + foreach (KeyValuePair l_VersionKVP in l_Versions) { - string l_BadgeName = l_KVP.Key; - foreach (KeyValuePair version in l_KVP.Value.AsObject["versions"].AsObject) + var l_Version = l_VersionKVP.Value; + var l_ID = l_Version["id"].Value; + var l_Picture = l_Version["image_url_2x"].Value; + var l_FinalName = $"{l_SetID}{l_ID}"; + + var l_InternalID = l_IsGlobal ? l_FinalName : $"{p_ChannelID}_{l_FinalName}"; + + Resources[l_InternalID] = new ChatResourceData() { - string l_BadgeVersion = version.Key; - string l_FinalName = $"{l_BadgeName}{l_BadgeVersion}"; - string l_URI = version.Value.AsObject["image_url_2x"].Value; - - string l_ID = l_IsGlobal ? l_FinalName : $"{p_Category}_{l_FinalName}"; - Resources[l_ID] = new ChatResourceData() - { - Uri = l_URI, - Animation = Animation.EAnimationType.NONE, - Category = EChatResourceCategory.Badge, - Type = l_IsGlobal ? "TwitchGlobalBadge" : "TwitchChannelBadge" - }; - - l_Count++; - } + Uri = l_Picture, + Animation = Animation.EAnimationType.NONE, + Category = EChatResourceCategory.Badge, + Type = l_IsGlobal ? "TwitchGlobalBadge" : "TwitchChannelBadge" + }; + + l_Count++; } - ChatPlexSDK.Logger.Debug($"Success caching {l_Count} Twitch {(l_IsGlobal ? "global " : "")}badges{(l_IsGlobal ? "." : " for channel " + p_Category)}."); - return; } + + ChatPlexSDK.Logger.Debug($"Success caching {l_Count} Twitch {(l_IsGlobal ? "global " : "")}badges{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); } catch (Exception l_Exception) { - ChatPlexSDK.Logger.Error($"An error occurred while requesting Twitch {(l_IsGlobal ? "global " : "")}badges{(l_IsGlobal ? "." : " for channel " + p_Category)}."); + ChatPlexSDK.Logger.Error($"An error occurred while requesting Twitch {(l_IsGlobal ? "global " : "")}badges{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); ChatPlexSDK.Logger.Error(l_Exception); } diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchCheermoteProvider.cs b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchCheermoteProvider.cs index be36db2..1e373ed 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchCheermoteProvider.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchCheermoteProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Linq; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -24,30 +23,40 @@ public class TwitchCheermoteProvider : IChatResourceProvider - /// Try request resources + /// Try request resources from the provider /// - /// Category / Channel + /// ID of the channel + /// Name of the channel + /// Access token for the API /// - public async Task TryRequestResources(string p_Category, string p_Token) + public async Task TryRequestResources(string p_ChannelID, string p_ChannelName, string p_AccessToken) { - Network.APIClient m_APIClient = new Network.APIClient("https://api.twitch.tv/helix/", TimeSpan.FromSeconds(10), true); - if (!m_APIClient.InternalClient.DefaultRequestHeaders.Contains("client-id")) - m_APIClient.InternalClient.DefaultRequestHeaders.Add("client-id", TwitchService.TWITCH_CLIENT_ID); + var l_WebClient = new Network.WebClient("https://api.twitch.tv/helix/", TimeSpan.FromSeconds(10), true); + if (!l_WebClient.Headers.ContainsKey("Client-Id")) + l_WebClient.Headers.Remove("Authorization"); - if (m_APIClient.InternalClient.DefaultRequestHeaders.Contains("Authorization")) - m_APIClient.InternalClient.DefaultRequestHeaders.Remove("Authorization"); + if (l_WebClient.Headers.ContainsKey("Client-Id")) + l_WebClient.Headers.Remove("Authorization"); - m_APIClient.InternalClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + p_Token.Replace("oauth:", "")); + l_WebClient.Headers.Add("client-id", TwitchService.TWITCH_CLIENT_ID); + l_WebClient.Headers.Add("Authorization", "Bearer " + p_AccessToken.Replace("oauth:", "")); - bool l_IsGlobal = string.IsNullOrEmpty(p_Category); + bool l_IsGlobal = string.IsNullOrEmpty(p_ChannelID); try { - ChatPlexSDK.Logger.Debug($"Requesting Twitch {(l_IsGlobal ? "global " : "")}cheermotes{(l_IsGlobal ? "." : $" for channel {p_Category}")}."); + ChatPlexSDK.Logger.Debug($"Requesting Twitch {(l_IsGlobal ? "global " : "")}cheermotes{(l_IsGlobal ? "." : $" for channel {p_ChannelName}")}."); + + var l_Completion = new TaskCompletionSource(); + l_WebClient.GetAsync("bits/cheermotes" + (l_IsGlobal ? "" : "?broadcaster_id=" + p_ChannelID), CancellationToken.None, (p_Response) => + { + l_Completion.SetResult(p_Response); + }); + await l_Completion.Task; + var l_Response = l_Completion.Task?.Result; - var l_Response = await m_APIClient.GetAsync("bits/cheermotes" + (l_IsGlobal ? "" : "?broadcaster_id=" + p_Category), CancellationToken.None).ConfigureAwait(false); if (l_Response == null || !l_Response.IsSuccessStatusCode) { - ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting Twitch {(l_IsGlobal ? "global " : "")}cheermotes{(l_IsGlobal ? "." : " for channel " + p_Category)}. {l_Response.ReasonPhrase}"); + ChatPlexSDK.Logger.Error($"Unsuccessful status code when requesting Twitch {(l_IsGlobal ? "global " : "")}cheermotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}. {l_Response.ReasonPhrase}"); return; } @@ -63,10 +72,11 @@ public async Task TryRequestResources(string p_Category, string p_Token) { var l_Cheermote = new TwitchCheermoteData(); var l_Prefix = l_Node["prefix"].Value.ToLower(); + var l_Type = l_Node["type"].Value; foreach (JSONNode l_Tier in l_Node["tiers"].Values) { - var l_NewTier = new CheermoteTier(); + var l_NewTier = new TwitchCheermoteTier(); l_NewTier.MinBits = l_Tier["min_bits"].AsInt; l_NewTier.Color = l_Tier["color"].Value; l_NewTier.Uri = l_Tier["images"]["dark"]["animated"]["3"].Value; @@ -74,20 +84,20 @@ public async Task TryRequestResources(string p_Category, string p_Token) l_Cheermote.Tiers.Add(l_NewTier); } - l_Cheermote.Prefix = l_Prefix; - l_Cheermote.Tiers = l_Cheermote.Tiers.OrderBy(t => t.MinBits).ToList(); + l_Cheermote.Prefix = l_Prefix; + l_Cheermote.Tiers = l_Cheermote.Tiers.OrderBy(t => t.MinBits).ToList(); - string l_ID = l_IsGlobal ? l_Prefix : $"{p_Category}_{l_Prefix}"; - Resources[l_ID] = l_Cheermote; + var l_InternalID = l_Type != "channel_custom" ? l_Prefix : $"{p_ChannelName}_{l_Prefix}"; + Resources[l_InternalID] = l_Cheermote; l_Count++; } - ChatPlexSDK.Logger.Debug($"Success caching {l_Count} Twitch {(l_IsGlobal ? "global " : "")}cheermotes{(l_IsGlobal ? "." : " for channel " + p_Category)}."); + ChatPlexSDK.Logger.Debug($"Success caching {l_Count} Twitch {(l_IsGlobal ? "global " : "")}cheermotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); } catch (Exception l_Exception) { - ChatPlexSDK.Logger.Error($"An error occurred while requesting Twitch {(l_IsGlobal ? "global " : "")}cheermotes{(l_IsGlobal ? "." : " for channel " + p_Category)}."); + ChatPlexSDK.Logger.Error($"An error occurred while requesting Twitch {(l_IsGlobal ? "global " : "")}cheermotes{(l_IsGlobal ? "." : " for channel " + p_ChannelName)}."); ChatPlexSDK.Logger.Error(l_Exception); } diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchDataProvider.cs b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchDataProvider.cs index 884ff8b..cff5ac2 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchDataProvider.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchDataProvider.cs @@ -14,40 +14,19 @@ namespace CP_SDK.Chat.Services.Twitch /// public class TwitchDataProvider { - /// - /// Global lock - /// - private SemaphoreSlim m_GlobalLock = new SemaphoreSlim(1, 1), m_ChannelLock = new SemaphoreSlim(1, 1); - /// - /// Cached data hash set - /// + private SemaphoreSlim m_GlobalLock = new SemaphoreSlim(1, 1), m_ChannelLock = new SemaphoreSlim(1, 1); private HashSet m_ChannelDataCached = new HashSet(); - /// - /// Twitch badge provider - /// - private TwitchBadgeProvider m_TwitchBadgeProvider = new TwitchBadgeProvider(); - /// - /// Twitch cheermote provider - /// - private TwitchCheermoteProvider m_TwitchCheermoteProvider = new TwitchCheermoteProvider(); - /// - /// BTTV data provider - /// - private BTTVDataProvider m_BTTVDataProvider = new BTTVDataProvider(); - /// - /// FFZ data provider - /// - private FFZDataProvider m_FFZDataProvider = new FFZDataProvider(); - /// - /// 7TV data provider - /// - private _7TVDataProvider m_7TVDataProvider = new _7TVDataProvider(); + + private ChatPlexGradientNamesDataProvider m_ChatPlexGradientNamesDataProvider = new ChatPlexGradientNamesDataProvider("https://data.chatplex.org/twitch_gradient_names.json"); + private TwitchBadgeProvider m_TwitchBadgeProvider = new TwitchBadgeProvider(); + private TwitchCheermoteProvider m_TwitchCheermoteProvider = new TwitchCheermoteProvider(); + private BTTVDataProvider m_BTTVDataProvider = new BTTVDataProvider(); + private FFZDataProvider m_FFZDataProvider = new FFZDataProvider(); + private _7TVDataProvider m_7TVDataProvider = new _7TVDataProvider(); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - public _7TVDataProvider _7TVDataProvider => m_7TVDataProvider; - public bool IsReady { get; internal set; } = false; //////////////////////////////////////////////////////////////////////////// @@ -64,10 +43,11 @@ public void TryRequestGlobalResources(string p_Token) try { - await m_TwitchBadgeProvider.TryRequestResources(null, p_Token).ConfigureAwait(false); - await m_BTTVDataProvider.TryRequestResources(null, p_Token).ConfigureAwait(false); - await m_FFZDataProvider.TryRequestResources(null, p_Token).ConfigureAwait(false); - await m_7TVDataProvider.TryRequestResources(null, p_Token).ConfigureAwait(false); + await m_ChatPlexGradientNamesDataProvider.TryRequestResources(null, null, p_Token).ConfigureAwait(false); + await m_TwitchBadgeProvider.TryRequestResources(null, null, p_Token).ConfigureAwait(false); + await m_BTTVDataProvider.TryRequestResources(null, null, p_Token).ConfigureAwait(false); + await m_FFZDataProvider.TryRequestResources(null, null, p_Token).ConfigureAwait(false); + await m_7TVDataProvider.TryRequestResources(null, null, p_Token).ConfigureAwait(false); ///ChatPlexSDK.Logger.Information("Finished caching global emotes/badges."); } catch (Exception l_Exception) @@ -100,13 +80,14 @@ public void TryRequestChannelResources(IChatChannel p_Channel, string p_Token, A { if (!m_ChannelDataCached.Contains(p_Channel.Id)) { - string l_RoomId = p_Channel.AsTwitchChannel().Roomstate.RoomId; + var l_ChannelID = p_Channel.AsTwitchChannel().Roomstate.RoomId; + var l_ChannelName = p_Channel.Id; - await m_TwitchBadgeProvider.TryRequestResources(l_RoomId, p_Token).ConfigureAwait(false); - await m_TwitchCheermoteProvider.TryRequestResources(l_RoomId, p_Token).ConfigureAwait(false); - await m_BTTVDataProvider.TryRequestResources(l_RoomId, p_Token).ConfigureAwait(false); - await m_FFZDataProvider.TryRequestResources(p_Channel.Id, p_Token).ConfigureAwait(false); - await m_7TVDataProvider.TryRequestResources(p_Channel.Id, p_Token).ConfigureAwait(false); + await m_TwitchBadgeProvider.TryRequestResources(l_ChannelID, l_ChannelName, p_Token).ConfigureAwait(false); + await m_TwitchCheermoteProvider.TryRequestResources(l_ChannelID, l_ChannelName, p_Token).ConfigureAwait(false); + await m_BTTVDataProvider.TryRequestResources(l_ChannelID, l_ChannelName, p_Token).ConfigureAwait(false); + await m_FFZDataProvider.TryRequestResources(l_ChannelID, l_ChannelName, p_Token).ConfigureAwait(false); + await m_7TVDataProvider.TryRequestResources(l_ChannelID, l_ChannelName, p_Token).ConfigureAwait(false); var l_Result = new Dictionary(); @@ -183,6 +164,22 @@ public async void TryReleaseChannelResources(IChatChannel p_Channel) //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// + /// + /// Try get a custom user display name + /// + /// UserID + /// Default display name + /// Output painted name + /// + internal bool TryGetUserDisplayName(string p_UserID, string p_Default, out string p_PaintedName) + { + if (m_ChatPlexGradientNamesDataProvider.TryGetUserDisplayName(p_UserID, p_Default, out p_PaintedName) + || m_7TVDataProvider.TryGetUserDisplayName(p_UserID, p_Default, out p_PaintedName)) + return true; + + p_PaintedName = p_Default; + return false; + } /// /// Get third party emote /// diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchHelix.cs b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchHelix.cs index 3a66a37..3491c2c 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchHelix.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchHelix.cs @@ -43,9 +43,10 @@ public class TwitchHelix //////////////////////////////////////////////////////////////////////////// /// - /// API Client + /// Web Client /// - private Network.APIClient m_APIClient = new Network.APIClient("https://api.twitch.tv/helix/", TimeSpan.FromSeconds(10), true); + private Network.WebClient m_WebClient = new Network.WebClient ("https://api.twitch.tv/helix/", TimeSpan.FromSeconds(10), true); + private Network.WebClientEx m_WebClientEx = new Network.WebClientEx("https://api.twitch.tv/helix/", TimeSpan.FromSeconds(10), true); /// /// Broadcaster ID /// @@ -93,7 +94,8 @@ public class TwitchHelix /// /// Client /// - public Network.APIClient APIClient => m_APIClient; + public Network.WebClient WebClient => m_WebClient; + public Network.WebClientEx WebClientEx => m_WebClientEx; /// /// Broadcaster ID /// @@ -131,18 +133,29 @@ internal void OnTokenChanged(string p_Token) { try { - if (!m_APIClient.InternalClient.DefaultRequestHeaders.Contains("client-id")) - m_APIClient.InternalClient.DefaultRequestHeaders.Add("client-id", TwitchService.TWITCH_CLIENT_ID); + if (m_WebClient.Headers.ContainsKey("Client-Id")) + m_WebClient.Headers.Remove("Client-Id"); - if (m_APIClient.InternalClient.DefaultRequestHeaders.Contains("Authorization")) - m_APIClient.InternalClient.DefaultRequestHeaders.Remove("Authorization"); + if (m_WebClient.Headers.ContainsKey("Authorization")) + m_WebClient.Headers.Remove("Authorization"); - m_APIClient.InternalClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + p_Token.Replace("oauth:", "")); + m_WebClient.Headers.Add("Client-Id", TwitchService.TWITCH_CLIENT_ID); + m_WebClient.Headers.Add("Authorization", "Bearer " + p_Token.Replace("oauth:", "")); } - catch + catch (System.Exception) { } + + try { + if (!m_WebClientEx.InternalClient.DefaultRequestHeaders.Contains("Client-Id")) + m_WebClientEx.InternalClient.DefaultRequestHeaders.Remove("Client-Id"); + if (m_WebClientEx.InternalClient.DefaultRequestHeaders.Contains("Authorization")) + m_WebClientEx.InternalClient.DefaultRequestHeaders.Remove("Authorization"); + + m_WebClientEx.InternalClient.DefaultRequestHeaders.Add("Client-Id", TwitchService.TWITCH_CLIENT_ID); + m_WebClientEx.InternalClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + p_Token.Replace("oauth:", "")); } + catch (System.Exception) { } m_APITokenScopes = new List(); m_APIToken = p_Token; @@ -255,41 +268,40 @@ internal void Update() /// private void ValidateToken() { - var l_APIClient = new Network.APIClient("", TimeSpan.FromSeconds(10), false); + var l_WebClient = new Network.WebClient("", TimeSpan.FromSeconds(10), true); try { - l_APIClient.InternalClient.DefaultRequestHeaders.Add("Authorization", "OAuth " + m_APIToken.Replace("oauth:", "")); + l_WebClient.Headers.Add("Authorization", "OAuth " + m_APIToken.Replace("oauth:", "")); } catch { } - l_APIClient.GetAsync("https://id.twitch.tv/oauth2/validate", CancellationToken.None, true).ContinueWith((p_Result) => + l_WebClient.GetAsync("https://id.twitch.tv/oauth2/validate", CancellationToken.None, (p_Result) => { #if DEBUG - if (p_Result.Result != null) + if (p_Result != null) { ChatPlexSDK.Logger.Debug("[CP_SDK.Chat.Service.Twitch][TwitchHelix.ValidateToken] Receiving:"); - ChatPlexSDK.Logger.Debug(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Debug(p_Result.BodyString); } #endif - if (p_Result.Result != null && !p_Result.Result.IsSuccessStatusCode) + if (p_Result != null && !p_Result.IsSuccessStatusCode) { ChatPlexSDK.Logger.Error("[CP_SDK.Chat.Service.Twitch][TwitchHelix.ValidateToken] Failed with message:"); - ChatPlexSDK.Logger.Error(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Error(p_Result.BodyString); } - if (p_Result.Result != null && p_Result.Result.IsSuccessStatusCode) + if (p_Result != null && p_Result.IsSuccessStatusCode) { - if (GetObjectFromJsonString(p_Result.Result.BodyString, out var l_Validate)) + if (GetObjectFromJsonString(p_Result.BodyString, out var l_Validate)) { m_APITokenScopes = new List(l_Validate.scopes); m_BroadcasterID = l_Validate.user_id; m_BroadcasterLogin = l_Validate.login; - OnTokenValidate?.Invoke(true, l_Validate, l_Validate.user_id); } else @@ -303,7 +315,7 @@ private void ValidateToken() OnTokenValidate?.Invoke(false, null, string.Empty); } - }).ConfigureAwait(false); + }, true); } /// /// Has API Token scope @@ -342,38 +354,38 @@ public void CreatePoll(Helix_CreatePoll p_Poll, Action + m_WebClient.PostAsync("polls", l_ContentStr, "application/json", CancellationToken.None, (p_Result) => { #if DEBUG - if (p_Result.Result != null) + if (p_Result != null) { ChatPlexSDK.Logger.Debug("[CP_SDK.Chat.Service.Twitch][TwitchHelix.CreatePoll] Receiving:"); - ChatPlexSDK.Logger.Debug(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Debug(p_Result.BodyString); } #endif - if (p_Result.Result != null && !p_Result.Result.IsSuccessStatusCode) + if (p_Result != null && !p_Result.IsSuccessStatusCode) { ChatPlexSDK.Logger.Error("[CP_SDK.Chat.Service.Twitch][TwitchHelix.CreatePoll] Failed with message:"); - ChatPlexSDK.Logger.Error(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Error(p_Result.BodyString); } - if (p_Result.Result == null) + if (p_Result == null) p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.BadRequest) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.BadRequest) p_Callback?.Invoke(TwitchHelixResult.InvalidRequest, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) p_Callback?.Invoke(TwitchHelixResult.AuthorizationFailed, null); - else if (p_Result.Result.IsSuccessStatusCode) + else if (p_Result.IsSuccessStatusCode) { - JObject l_Reply = JObject.Parse(p_Result.Result.BodyString); + JObject l_Reply = JObject.Parse(p_Result.BodyString); Helix_Poll l_HelixResult = null; if (l_Reply.ContainsKey("data") && l_Reply["data"].Type == JTokenType.Array && (l_Reply["data"] as JArray).Count > 0) @@ -386,7 +398,7 @@ public void CreatePoll(Helix_CreatePoll p_Poll, Action /// Get active poll @@ -400,31 +412,31 @@ public void GetLastPoll(Action p_Callback) return; } - m_APIClient.GetAsync("polls?broadcaster_id=" + m_BroadcasterID + "&first=1", CancellationToken.None, true).ContinueWith((p_Result) => + m_WebClient.GetAsync("polls?broadcaster_id=" + m_BroadcasterID + "&first=1", CancellationToken.None, (p_Result) => { #if DEBUG - if (p_Result.Result != null) + if (p_Result != null) { ChatPlexSDK.Logger.Debug("[CP_SDK.Chat.Service.Twitch][TwitchHelix.GetLastPoll] Receiving:"); - ChatPlexSDK.Logger.Debug(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Debug(p_Result.BodyString); } #endif - if (p_Result.Result != null && !p_Result.Result.IsSuccessStatusCode) + if (p_Result != null && !p_Result.IsSuccessStatusCode) { ChatPlexSDK.Logger.Error("[CP_SDK.Chat.Service.Twitch][TwitchHelix.GetLastPoll] Failed with message:"); - ChatPlexSDK.Logger.Error(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Error(p_Result.BodyString); } - if (p_Result.Result == null) + if (p_Result == null) p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.BadRequest) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.BadRequest) p_Callback?.Invoke(TwitchHelixResult.InvalidRequest, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) p_Callback?.Invoke(TwitchHelixResult.AuthorizationFailed, null); - else if (p_Result.Result.IsSuccessStatusCode) + else if (p_Result.IsSuccessStatusCode) { - JObject l_Reply = JObject.Parse(p_Result.Result.BodyString); + JObject l_Reply = JObject.Parse(p_Result.BodyString); Helix_Poll l_HelixResult = null; if (l_Reply.ContainsKey("data") && l_Reply["data"].Type == JTokenType.Array && (l_Reply["data"] as JArray).Count > 0) @@ -434,7 +446,7 @@ public void GetLastPoll(Action p_Callback) } else p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - }).ConfigureAwait(false); + }, true); } /// /// End a poll @@ -457,38 +469,38 @@ public void EndPoll(Helix_Poll p_Poll, Helix_Poll.Status p_EndStatus, Action + m_WebClient.PatchAsync("polls", l_ContentStr, "application/json", CancellationToken.None, (p_Result) => { #if DEBUG - if (p_Result.Result != null) + if (p_Result != null) { ChatPlexSDK.Logger.Debug("[CP_SDK.Chat.Service.Twitch][TwitchHelix.EndPoll] Receiving:"); - ChatPlexSDK.Logger.Debug(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Debug(p_Result.BodyString); } #endif - if (p_Result.Result != null && !p_Result.Result.IsSuccessStatusCode) + if (p_Result != null && !p_Result.IsSuccessStatusCode) { ChatPlexSDK.Logger.Error("[CP_SDK.Chat.Service.Twitch][TwitchHelix.EndPoll] Failed with message:"); - ChatPlexSDK.Logger.Error(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Error(p_Result.BodyString); } - if (p_Result.Result == null) + if (p_Result == null) p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.BadRequest) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.BadRequest) p_Callback?.Invoke(TwitchHelixResult.InvalidRequest, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) p_Callback?.Invoke(TwitchHelixResult.AuthorizationFailed, null); - else if (p_Result.Result.IsSuccessStatusCode) + else if (p_Result.IsSuccessStatusCode) { - JObject l_Reply = JObject.Parse(p_Result.Result.BodyString); + JObject l_Reply = JObject.Parse(p_Result.BodyString); Helix_Poll l_HelixResult = null; if (l_Reply.ContainsKey("data") && l_Reply["data"].Type == JTokenType.Array && (l_Reply["data"] as JArray).Count > 0) @@ -498,7 +510,7 @@ public void EndPoll(Helix_Poll p_Poll, Helix_Poll.Status p_EndStatus, Action p_Callba return; } - m_APIClient.GetAsync("hypetrain/events?broadcaster_id=" + m_BroadcasterID + "&first=1", CancellationToken.None, true).ContinueWith((p_Result) => + m_WebClient.GetAsync("hypetrain/events?broadcaster_id=" + m_BroadcasterID + "&first=1", CancellationToken.None, (p_Result) => { #if DEBUG - if (p_Result.Result != null) + if (p_Result != null) { ChatPlexSDK.Logger.Debug("[CP_SDK.Chat.Service.Twitch][TwitchHelix.GetLastHypeTrain] Receiving:"); - ChatPlexSDK.Logger.Debug(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Debug(p_Result.BodyString); } #endif - if (p_Result.Result != null && !p_Result.Result.IsSuccessStatusCode) + if (p_Result != null && !p_Result.IsSuccessStatusCode) { ChatPlexSDK.Logger.Error("[CP_SDK.Chat.Service.Twitch][TwitchHelix.GetLastHypeTrain] Failed with message:"); - ChatPlexSDK.Logger.Error(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Error(p_Result.BodyString); } - if (p_Result.Result == null) + if (p_Result == null) p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.BadRequest) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.BadRequest) p_Callback?.Invoke(TwitchHelixResult.InvalidRequest, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) p_Callback?.Invoke(TwitchHelixResult.AuthorizationFailed, null); - else if (p_Result.Result.IsSuccessStatusCode) + else if (p_Result.IsSuccessStatusCode) { - JObject l_Reply = JObject.Parse(p_Result.Result.BodyString); + JObject l_Reply = JObject.Parse(p_Result.BodyString); Helix_HypeTrain l_HelixResult = null; if (l_Reply.ContainsKey("data") && l_Reply["data"].Type == JTokenType.Array && (l_Reply["data"] as JArray).Count > 0) @@ -550,7 +562,7 @@ public void GetLastHypeTrain(Action p_Callba } else p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - }).ConfigureAwait(false); + }, true); } //////////////////////////////////////////////////////////////////////////// @@ -568,31 +580,31 @@ public void GetLastPrediction(Action p_Call return; } - m_APIClient.GetAsync("predictions?broadcaster_id=" + m_BroadcasterID + "&first=1", CancellationToken.None, true).ContinueWith((p_Result) => + m_WebClient.GetAsync("predictions?broadcaster_id=" + m_BroadcasterID + "&first=1", CancellationToken.None, (p_Result) => { #if DEBUG - if (p_Result.Result != null) + if (p_Result != null) { ChatPlexSDK.Logger.Debug("[CP_SDK.Chat.Service.Twitch][TwitchHelix.GetLastPrediction] Receiving:"); - ChatPlexSDK.Logger.Debug(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Debug(p_Result.BodyString); } #endif - if (p_Result.Result != null && !p_Result.Result.IsSuccessStatusCode) + if (p_Result != null && !p_Result.IsSuccessStatusCode) { ChatPlexSDK.Logger.Error("[CP_SDK.Chat.Service.Twitch][TwitchHelix.GetLastPrediction] Failed with message:"); - ChatPlexSDK.Logger.Error(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Error(p_Result.BodyString); } - if (p_Result.Result == null) + if (p_Result == null) p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.BadRequest) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.BadRequest) p_Callback?.Invoke(TwitchHelixResult.InvalidRequest, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) p_Callback?.Invoke(TwitchHelixResult.AuthorizationFailed, null); - else if (p_Result.Result.IsSuccessStatusCode) + else if (p_Result.IsSuccessStatusCode) { - JObject l_Reply = JObject.Parse(p_Result.Result.BodyString); + JObject l_Reply = JObject.Parse(p_Result.BodyString); Helix_Prediction l_HelixResult = null; if (l_Reply.ContainsKey("data") && l_Reply["data"].Type == JTokenType.Array && (l_Reply["data"] as JArray).Count > 0) @@ -602,7 +614,7 @@ public void GetLastPrediction(Action p_Call } else p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - }).ConfigureAwait(false); + }, true); } /// /// End a prediction @@ -627,33 +639,33 @@ public void EndPrediction(string p_ID, Helix_Prediction.Status p_Status, string if (p_Status == Helix_Prediction.Status.RESOLVED) l_Body["winning_outcome_id"] = p_WinningOutcomeID; - var l_ContentStr = new StringContent(l_Body.ToString(Formatting.None), Encoding.UTF8, "application/json"); + var l_ContentStr = new StringContent(l_Body.ToString(Formatting.None), Encoding.UTF8); - m_APIClient.PatchAsync("predictions", l_ContentStr, CancellationToken.None, true).ContinueWith((p_Result) => + m_WebClient.PatchAsync("predictions", l_ContentStr, "application/json", CancellationToken.None, (p_Result) => { #if DEBUG - if (p_Result.Result != null) + if (p_Result != null) { ChatPlexSDK.Logger.Debug("[CP_SDK.Chat.Service.Twitch][TwitchHelix.EndPrediction] Receiving:"); - ChatPlexSDK.Logger.Debug(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Debug(p_Result.BodyString); } #endif - if (p_Result.Result != null && !p_Result.Result.IsSuccessStatusCode) + if (p_Result != null && !p_Result.IsSuccessStatusCode) { ChatPlexSDK.Logger.Error("[CP_SDK.Chat.Service.Twitch][TwitchHelix.EndPrediction] Failed with message:"); - ChatPlexSDK.Logger.Error(p_Result.Result.BodyString); + ChatPlexSDK.Logger.Error(p_Result.BodyString); } - if (p_Result.Result == null) + if (p_Result == null) p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.BadRequest) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.BadRequest) p_Callback?.Invoke(TwitchHelixResult.InvalidRequest, null); - else if (p_Result.Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) + else if (p_Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) p_Callback?.Invoke(TwitchHelixResult.AuthorizationFailed, null); - else if (p_Result.Result.IsSuccessStatusCode) + else if (p_Result.IsSuccessStatusCode) { - JObject l_Reply = JObject.Parse(p_Result.Result.BodyString); + JObject l_Reply = JObject.Parse(p_Result.BodyString); Helix_Prediction l_HelixResult = null; if (l_Reply.ContainsKey("data") && l_Reply["data"].Type == JTokenType.Array && (l_Reply["data"] as JArray).Count > 0) @@ -663,7 +675,113 @@ public void EndPrediction(string p_ID, Helix_Prediction.Status p_Status, string } else p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); - }).ConfigureAwait(false); + }, true); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create clip + /// + /// End callback + public void CreateClip(Action p_Callback) + { + if (!HasTokenPermission("clips:edit")) + { + p_Callback?.Invoke(TwitchHelixResult.TokenMissingPermission, null); + return; + } + + var l_ContentStr = new StringContent("{}", Encoding.UTF8); + m_WebClient.PostAsync("clips?broadcaster_id="+ m_BroadcasterID, l_ContentStr, "application/json", CancellationToken.None, (p_Result) => + { +#if DEBUG + if (p_Result != null) + { + ChatPlexSDK.Logger.Debug("[CP_SDK.Chat.Service.Twitch][TwitchHelix.CreateClip] Receiving:"); + ChatPlexSDK.Logger.Debug(p_Result.BodyString); + } +#endif + + if (p_Result != null && !p_Result.IsSuccessStatusCode) + { + ChatPlexSDK.Logger.Error("[CP_SDK.Chat.Service.Twitch][TwitchHelix.CreateClip] Failed with message:"); + ChatPlexSDK.Logger.Error(p_Result.BodyString); + } + + if (p_Result == null) + p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); + else if (p_Result.StatusCode == System.Net.HttpStatusCode.BadRequest) + p_Callback?.Invoke(TwitchHelixResult.InvalidRequest, null); + else if (p_Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) + p_Callback?.Invoke(TwitchHelixResult.AuthorizationFailed, null); + else if (p_Result.IsSuccessStatusCode) + { + JObject l_Reply = JObject.Parse(p_Result.BodyString); + Helix_CreateClip l_HelixResult = null; + + if (l_Reply.ContainsKey("data") && l_Reply["data"].Type == JTokenType.Array && (l_Reply["data"] as JArray).Count > 0) + GetObjectFromJsonString((l_Reply["data"] as JArray)[0].ToString(), out l_HelixResult); + + p_Callback?.Invoke(TwitchHelixResult.OK, l_HelixResult); + } + else + p_Callback?.Invoke(TwitchHelixResult.NetworkError, null); + }, true); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create marker + /// + /// Marker description + /// End callback + public void CreateMarker(string p_MarkerName, Action p_Callback) + { + if (!HasTokenPermission("channel:manage:broadcast")) + { + p_Callback?.Invoke(TwitchHelixResult.TokenMissingPermission); + return; + } + + var l_Content = new JObject() + { + ["user_id"] = m_BroadcasterID, + ["description"] = p_MarkerName.Length > 140 ? p_MarkerName.Substring(0, 140) : p_MarkerName + }; + + var l_ContentStr = new StringContent(JsonConvert.SerializeObject(l_Content), Encoding.UTF8); + + m_WebClient.PostAsync("streams/markers", l_ContentStr, "application/json", CancellationToken.None, (p_Result) => + { +#if DEBUG + if (p_Result != null) + { + ChatPlexSDK.Logger.Debug("[CP_SDK.Chat.Service.Twitch][TwitchHelix.CreateMarker] Receiving:"); + ChatPlexSDK.Logger.Debug(p_Result.BodyString); + } +#endif + + if (p_Result != null && !p_Result.IsSuccessStatusCode) + { + ChatPlexSDK.Logger.Error("[CP_SDK.Chat.Service.Twitch][TwitchHelix.CreateMarker] Failed with message:"); + ChatPlexSDK.Logger.Error(p_Result.BodyString); + } + + if (p_Result == null) + p_Callback?.Invoke(TwitchHelixResult.NetworkError); + else if (p_Result.StatusCode == System.Net.HttpStatusCode.BadRequest) + p_Callback?.Invoke(TwitchHelixResult.InvalidRequest); + else if (p_Result.StatusCode == System.Net.HttpStatusCode.Unauthorized) + p_Callback?.Invoke(TwitchHelixResult.AuthorizationFailed); + else if (p_Result.IsSuccessStatusCode) + p_Callback?.Invoke(TwitchHelixResult.OK); + else + p_Callback?.Invoke(TwitchHelixResult.NetworkError); + }, true); } //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchJSValidate.js b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchJSValidate.js index 4d65a3e..c22c516 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchJSValidate.js +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchJSValidate.js @@ -1,4 +1,4 @@ -if (document.getElementById('twitch_tokenchat').value == "") { +if (g_ServicesCount == 1 && document.getElementById('twitch_tokenchat').value == "") { var l_NoTwitchTokenModal = new bootstrap.Modal(document.getElementById('NoTwitchTokenModal')); l_NoTwitchTokenModal.show(); return false; @@ -10,7 +10,7 @@ var l_TwitchChannel3 = document.getElementById('twitch_channel3'); var l_TwitchChannel4 = document.getElementById('twitch_channel4'); var l_TwitchChannel5 = document.getElementById('twitch_channel5'); -if (l_TwitchChannel1.value == "" && l_TwitchChannel2.value == "" && l_TwitchChannel3.value == "" && l_TwitchChannel4.value == "" && l_TwitchChannel5.value == "") { +if (g_ServicesCount == 1 && l_TwitchChannel1.value == "" && l_TwitchChannel2.value == "" && l_TwitchChannel3.value == "" && l_TwitchChannel4.value == "" && l_TwitchChannel5.value == "") { var l_NoTwitchChannelModal = new bootstrap.Modal(document.getElementById('NoTwitchChannelModal')); l_NoTwitchChannelModal.show(); return false; diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchMessageParser.cs b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchMessageParser.cs index a9edcd0..b95f09f 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchMessageParser.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchMessageParser.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -53,7 +52,6 @@ public bool ParseRawMessage(string p_RawMessages, ConcurrentDictionary.Get(); var l_RawMessages = p_RawMessages.Split(m_SplitToken, StringSplitOptions.RemoveEmptyEntries); var l_Row = 0; @@ -184,7 +182,7 @@ public bool ParseRawMessage(string p_RawMessages, ConcurrentDictionary 0; } - //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -372,7 +369,9 @@ public TwitchUser GetAndUpdateUser(Dictionary p_Tags, string p_P if (m_TwitchDataProvider.IsReady && !l_User._FancyNameReady && !string.IsNullOrEmpty(l_User.Id)) { - l_User.PaintedName = m_TwitchDataProvider._7TVDataProvider.TryGetUserDisplayName(l_User.Id, l_User.DisplayName); + m_TwitchDataProvider.TryGetUserDisplayName(l_User.Id, l_User.DisplayName, out var l_PaintedName); + + l_User.PaintedName = l_PaintedName; l_User._FancyNameReady = true; } @@ -403,7 +402,6 @@ public TwitchUser GetAndUpdateUser(Dictionary p_Tags, string p_P Type = EBadgeType.Image, Content = l_BadgeInfo.Uri }; - l_Badges.Add(l_NewBadge); } else @@ -424,6 +422,7 @@ public TwitchUser GetAndUpdateUser(Dictionary p_Tags, string p_P /// /// Extract emotes /// + /// Channel instance /// IRC tags /// Raw message content /// Channel ID @@ -431,8 +430,11 @@ public TwitchUser GetAndUpdateUser(Dictionary p_Tags, string p_P /// Message bits /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IChatEmote[] GetEmotes(Dictionary p_Tags, string p_MessageText, string p_RoomID, string p_RoomName, int p_MessageBits) + public IChatEmote[] GetEmotes(TwitchChannel p_Channel, Dictionary p_Tags, string p_MessageText, string p_RoomID, string p_RoomName, int p_MessageBits) { + if (p_Channel.IsTemp && !ChatModSettings.Instance.Emotes.ParseTemporaryChannels) + return null; + if (p_MessageText.Length == 0) return null; @@ -492,9 +494,11 @@ public IChatEmote[] GetEmotes(Dictionary p_Tags, string p_Messag if (!l_FoundTwitchEmotes.Contains(l_LastWord)) { /// Make sure we haven't already matched a Twitch emote with the same string, just incase the user has a BTTV/FFZ emote with the same name - if (TwitchSettingsConfig.Instance.ParseCheermotes && p_MessageBits > 0 && m_TwitchDataProvider.TryGetCheermote(l_LastWord, p_RoomID, out var l_CheermoteData, out var l_NumBits) && l_NumBits > 0) + if (TwitchSettingsConfig.Instance.ParseCheermotes + && p_MessageBits > 0 + && m_TwitchDataProvider.TryGetCheermote(l_LastWord, p_RoomID, out var l_CheermoteData, out var l_NumBits) + && l_NumBits > 0) { - ///ChatPlexSDK.Logger.Error($"Got cheermote! Total message bits: {l_NumBits} {l_CheermoteData.Prefix}"); var l_Tier = l_CheermoteData.GetTier(l_NumBits); if (l_Tier != null) { @@ -511,7 +515,7 @@ public IChatEmote[] GetEmotes(Dictionary p_Tags, string p_Messag }); } } - else if (m_TwitchDataProvider.TryGetThirdPartyEmote(l_LastWord, p_RoomName, out var l_EmoteData)) + else if (m_TwitchDataProvider.TryGetThirdPartyEmote(l_LastWord, p_RoomID, out var l_EmoteData)) { if ( l_EmoteData.Type.StartsWith("BTTV") && ChatModSettings.Instance.Emotes.ParseBTTVEmotes || l_EmoteData.Type.StartsWith("FFZ") && ChatModSettings.Instance.Emotes.ParseFFZEmotes diff --git a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchService.cs b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchService.cs index cb3375b..affd778 100644 --- a/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchService.cs +++ b/BeatSaberPlus/CP_SDK/Chat/Services/Twitch/TwitchService.cs @@ -1,15 +1,15 @@ using CP_SDK.Chat.Interfaces; using CP_SDK.Chat.Models.Twitch; using CP_SDK.Chat.SimpleJSON; +using CP_SDK.Unity.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; -using System.Threading; using System.Threading.Tasks; -using System.Web; +using UnityEngine; namespace CP_SDK.Chat.Services.Twitch { @@ -30,12 +30,22 @@ public class TwitchService : ChatServiceBase, IChatService /// The display name of the service(s) /// public string DisplayName { get; } = "Twitch"; + /// + /// Side handle of each message color + /// + public Color AccentColor { get; } = ColorU.WithAlpha("#9147FF", 0.75f); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// /// /// Channels /// public ReadOnlyCollection<(IChatService, IChatChannel)> Channels => m_Channels.Select(x => (this as IChatService, x.Value as IChatChannel)).ToList().AsReadOnly(); + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + /// /// OAuth token /// @@ -44,6 +54,13 @@ public class TwitchService : ChatServiceBase, IChatService /// OAuth token API /// public string OAuthTokenAPI => m_TokenChannel; + /// + /// Helix API instance + /// + public TwitchHelix HelixAPI { get; private set; } = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// /// /// Required token scopes @@ -51,6 +68,7 @@ public class TwitchService : ChatServiceBase, IChatService public IReadOnlyList RequiredTokenScopes = new List() { "bits:read", + "channel:manage:broadcast", "channel:manage:polls", "channel:manage:predictions", "channel:manage:redemptions", @@ -58,20 +76,14 @@ public class TwitchService : ChatServiceBase, IChatService "channel:read:redemptions", "channel:read:hype_train", "channel:read:predictions", - "channel_subscriptions", + "channel:read:subscriptions", "chat:edit", "chat:read", "clips:edit", - "user:edit:broadcast", "whispers:edit", "whispers:read" }.AsReadOnly(); - /// - /// Helix API instance - /// - public TwitchHelix HelixAPI { get; private set; } = null; - //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -94,7 +106,7 @@ public class TwitchService : ChatServiceBase, IChatService /// /// Random generator /// - private Random m_Random; + private System.Random m_Random; /// /// Is the service started /// @@ -189,7 +201,7 @@ public TwitchService() /// Init m_DataProvider = new TwitchDataProvider(); m_MessageParser = new TwitchMessageParser(this, m_DataProvider, new FrwTwemojiParser()); - m_Random = new Random(); + m_Random = new System.Random(); HelixAPI = new TwitchHelix(); HelixAPI.OnTokenValidate += HelixAPI_OnTokenValidate; @@ -360,7 +372,7 @@ public void WebPageOnPost(Dictionary p_PostData) switch (l_Data.Key) { case "twitch_tokenchat": - var l_NewTwitchTokenChat = new string(HttpUtility.UrlDecode(l_Data.Value.Trim()).Where(c => !char.IsControl(c)).ToArray()); + var l_NewTwitchTokenChat = new string(CP_SDK_WebSocketSharp.Net.HttpUtility.UrlDecode(l_Data.Value.Trim()).Where(c => !char.IsControl(c)).ToArray()); TwitchSettingsConfig.Instance.TokenChat = l_NewTwitchTokenChat.StartsWith("oauth:") @@ -376,7 +388,7 @@ public void WebPageOnPost(Dictionary p_PostData) break; case "twitch_tokenchannel": - var l_NewTwitchTokenChannel = new string(HttpUtility.UrlDecode(l_Data.Value.Trim()).Where(c => !char.IsControl(c)).ToArray()); + var l_NewTwitchTokenChannel = new string(CP_SDK_WebSocketSharp.Net.HttpUtility.UrlDecode(l_Data.Value.Trim()).Where(c => !char.IsControl(c)).ToArray()); TwitchSettingsConfig.Instance.TokenChannel = l_NewTwitchTokenChannel.StartsWith("oauth:") @@ -879,13 +891,12 @@ private void IRCSocket_OnOpen() m_OnSystemMessageCallbacks?.InvokeAll(this, "Connected to Twitch"); ChatPlexSDK.Logger.Info("Twitch connection opened"); - m_IRCWebSocket.SendMessage("CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership"); - ChatPlexSDK.Logger.Info("Trying to login!"); if (!string.IsNullOrEmpty(m_TokenChat)) m_IRCWebSocket.SendMessage($"PASS {m_TokenChat}"); m_IRCWebSocket.SendMessage($"NICK {ChatPlexSDK.ProductName}{m_Random.Next(10000, 1000000)}"); + m_IRCWebSocket.SendMessage("CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership"); } /// /// Twitch IRC socket close @@ -1081,7 +1092,9 @@ private void IRCSocket_OnMessageReceived(string p_RawMessage) if (m_DataProvider.IsReady && !string.IsNullOrEmpty(m_LoggedInUser.Id) && !string.IsNullOrEmpty(m_LoggedInUser.DisplayName)) { - m_LoggedInUser.PaintedName = m_DataProvider._7TVDataProvider.TryGetUserDisplayName(m_LoggedInUser.Id, m_LoggedInUser.DisplayName); + m_DataProvider.TryGetUserDisplayName(m_LoggedInUser.Id, m_LoggedInUser.DisplayName, out var l_PaintedName); + + m_LoggedInUser.PaintedName = l_PaintedName; m_LoggedInUser._FancyNameReady = true; } } @@ -1231,7 +1244,7 @@ private void PubSubSocket_OnMessageReceived(string p_RawMessage) l_Channel.ViewerCount = l_VideoPlaybackMessage.Viewers; if (l_Channel != null) - m_OnLiveStatusUpdatedCallbacks?.InvokeAll(this, l_Channel, l_VideoPlaybackMessage.Type == PubSubVideoPlayback.VideoPlaybackType.StreamUp || l_VideoPlaybackMessage.Viewers != 0, l_VideoPlaybackMessage.Viewers); + m_OnLiveStatusUpdatedCallbacks?.InvokeAll(this, l_Channel, l_Channel.Live, l_Channel.ViewerCount); break; @@ -1250,13 +1263,17 @@ private void PubSubSocket_OnMessageReceived(string p_RawMessage) /// /// Username /// - internal TwitchUser GetTwitchUser(string p_UserId, string p_UserName, string p_DisplayName, string p_Color = null) + internal TwitchUser GetTwitchUser(string p_UserID, string p_UserName, string p_DisplayName, string p_Color = null) { if (m_TwitchUsers.TryGetValue(p_UserName, out var l_User)) { + + if (m_DataProvider.IsReady && !l_User._FancyNameReady && !string.IsNullOrEmpty(l_User.Id) && !string.IsNullOrEmpty(l_User.DisplayName)) { - l_User.PaintedName = m_DataProvider._7TVDataProvider.TryGetUserDisplayName(l_User.Id, l_User.DisplayName); + m_DataProvider.TryGetUserDisplayName(l_User.Id, l_User.DisplayName, out var l_PaintedName); + + l_User.PaintedName = l_PaintedName; l_User._FancyNameReady = true; } @@ -1265,16 +1282,18 @@ internal TwitchUser GetTwitchUser(string p_UserId, string p_UserName, string p_D l_User = new TwitchUser() { - Id = p_UserId ?? string.Empty, + Id = p_UserID ?? string.Empty, UserName = p_UserName, DisplayName = p_DisplayName ?? p_UserName, PaintedName = p_DisplayName ?? p_UserName, Color = string.IsNullOrEmpty(p_Color) ? ChatUtils.GetNameColor(p_UserName) : p_Color, }; - if (m_DataProvider.IsReady && !string.IsNullOrEmpty(p_UserId) && !string.IsNullOrEmpty(p_DisplayName)) + if (m_DataProvider.IsReady && !string.IsNullOrEmpty(p_UserID) && !string.IsNullOrEmpty(p_DisplayName)) { - l_User.PaintedName = m_DataProvider._7TVDataProvider.TryGetUserDisplayName(p_UserId, p_DisplayName); + m_DataProvider.TryGetUserDisplayName(p_UserID, p_DisplayName, out var l_PaintedName); + + l_User.PaintedName = l_PaintedName; l_User._FancyNameReady = true; } diff --git a/BeatSaberPlus/CP_SDK/Chat/Utilities/7TVUtils.cs b/BeatSaberPlus/CP_SDK/Chat/Utilities/7TVUtils.cs new file mode 100644 index 0000000..e626678 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Chat/Utilities/7TVUtils.cs @@ -0,0 +1,60 @@ +using CP_SDK.Animation; +using CP_SDK.Chat.Interfaces; +using CP_SDK.Chat.Models; +using CP_SDK.Chat.SimpleJSON; + +namespace CP_SDK.Chat.Utilities +{ + /// + /// 7TV utilities + /// + public static class _7TVUtils + { + /// + /// Parse a 7TV emote set + /// + /// Emote set to parse + /// Emote type + /// Internal ID prefix + /// + public static int ParseEmoteSet(IChatResourceProvider p_ChatResourceProvider, JSONObject p_EmoteSet, string p_EmoteType, string p_InternalIDPrefix) + { + var l_Count = 0; + if (!p_EmoteSet.HasKey("emotes") || !p_EmoteSet["emotes"].IsArray) + return l_Count; + + var l_Emotes = p_EmoteSet["emotes"].AsArray; + foreach (JSONObject l_Object in l_Emotes.AsArray) + { + if (!l_Object.HasKey("data") || !l_Object["data"].IsObject) + continue; + + var l_Name = l_Object["name"].Value; ///< Renamed emotes + var l_Data = l_Object["data"]; + var l_ID = l_Data["id"].Value; + var l_Host = l_Data["host"]; + + if (l_ID == "000000000000000000000000" || !l_Host.IsObject) + continue; + + var l_BaseURL = $"https:{l_Host["url"].Value}/2x.webp"; + var l_InternalID = string.IsNullOrEmpty(p_InternalIDPrefix) ? l_Name : $"{p_InternalIDPrefix}_{l_Name}"; + + if (p_ChatResourceProvider.Resources.ContainsKey(l_InternalID)) + continue; + + p_ChatResourceProvider.Resources.TryAdd(l_InternalID, new ChatResourceData() + { + Uri = l_BaseURL, + Animation = EAnimationType.AUTODETECT, + Category = EChatResourceCategory.Emote, + Type = p_EmoteType + }); + + l_Count++; + } + + return l_Count; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Chat/WebApp.cs b/BeatSaberPlus/CP_SDK/Chat/WebApp.cs index ce54f76..1abe5bc 100644 --- a/BeatSaberPlus/CP_SDK/Chat/WebApp.cs +++ b/BeatSaberPlus/CP_SDK/Chat/WebApp.cs @@ -7,7 +7,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Web; namespace CP_SDK.Chat { @@ -159,6 +158,7 @@ internal static void OnContext(HttpListenerContext p_Context) l_PageBuilder.Replace("{_HTML_FORM_}", l_HTMLForm); l_PageBuilder.Replace("{_HTML_}", l_HTML); + l_PageBuilder.Replace("{_SERVICES_COUNT_}", Service.Multiplexer.Services.Count.ToString()); l_PageBuilder.Replace("{_JS_}", l_JS); l_PageBuilder.Replace("{_JS_VALIDATE_}", l_JS_VALIDATE); diff --git a/BeatSaberPlus/CP_SDK/ChatPlexSDK.cs b/BeatSaberPlus/CP_SDK/ChatPlexSDK.cs index 8633609..d6d9197 100644 --- a/BeatSaberPlus/CP_SDK/ChatPlexSDK.cs +++ b/BeatSaberPlus/CP_SDK/ChatPlexSDK.cs @@ -11,17 +11,11 @@ namespace CP_SDK /// public static class ChatPlexSDK { - /// - /// Render pipeline - /// public enum ERenderPipeline { BuiltIn, URP } - /// - /// Generic scene enum - /// public enum EGenericScene { None, @@ -32,48 +26,22 @@ public enum EGenericScene //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Module list - /// private static List m_Modules = new List(); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Logger instance - /// public static Logging.ILogger Logger { get; private set; } - /// - /// Product name - /// - public static string ProductName { get; private set; } = string.Empty; - /// - /// Product name - /// - public static string BasePath { get; private set; } = string.Empty; - /// - /// Network user agent - /// - public static string NetworkUserAgent { get; private set; } = string.Empty; - /// - /// Render pipeline - /// - public static ERenderPipeline RenderPipeline { get; private set; } = ERenderPipeline.BuiltIn; - /// - /// Active scene type - /// - public static EGenericScene ActiveGenericScene { get; private set; } = EGenericScene.None; + public static string ProductName { get; private set; } = string.Empty; + public static string ProductVersion { get; private set; } = string.Empty; + public static string BasePath { get; private set; } = string.Empty; + public static string NetworkUserAgent { get; private set; } = string.Empty; + public static ERenderPipeline RenderPipeline { get; private set; } = ERenderPipeline.BuiltIn; + public static EGenericScene ActiveGenericScene { get; private set; } = EGenericScene.None; - /// - /// On scene change - /// - public static event Action OnGenericSceneChange; - /// - /// On menu scene loaded - /// - public static event Action OnGenericMenuSceneLoaded; + public static event Action OnGenericSceneChange; + public static event Action OnGenericMenuSceneLoaded; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -85,11 +53,14 @@ public enum EGenericScene /// Base path /// Product name /// Rendering pipeline - internal static void Configure(Logging.ILogger p_Logger, string p_ProductName, string p_BasePath, ERenderPipeline p_RenderPipeline) + public static void Configure(Logging.ILogger p_Logger, string p_ProductName, string p_BasePath, ERenderPipeline p_RenderPipeline) { Logger = p_Logger; + var l_Version = Assembly.GetExecutingAssembly().GetName().Version; + ProductName = p_ProductName; + ProductVersion = $"{l_Version.Major}.{l_Version.Minor}.{l_Version.Build}"; BasePath = p_BasePath; NetworkUserAgent = $"ChatPlexSDK_{p_ProductName}/{Application.version}"; RenderPipeline = p_RenderPipeline; @@ -97,7 +68,7 @@ internal static void Configure(Logging.ILogger p_Logger, string p_ProductName, s /// /// When the assembly is loaded /// - internal static void OnAssemblyLoaded() + public static void OnAssemblyLoaded() { InstallWEBPCodecs(); @@ -109,7 +80,7 @@ internal static void OnAssemblyLoaded() /// /// On assembly exit /// - internal static void OnAssemblyExit() + public static void OnAssemblyExit() { try { @@ -131,7 +102,7 @@ internal static void OnAssemblyExit() /// /// When unity is ready /// - internal static void OnUnityReady() + public static void OnUnityReady() { try { @@ -141,6 +112,12 @@ internal static void OnUnityReady() /// Init fonts Unity.FontManager.Init(); + + /// Init UI + UI.UISystem.Init(); + UI.ModMenu.Register(new UI.ModButton(ProductName, () => { + UI.FlowCoordinators.MainFlowCoordinator.Instance().Present(true); + }, ProductVersion)); } catch (Exception p_Exception) { @@ -151,11 +128,24 @@ internal static void OnUnityReady() /// /// When unity is exiting /// - internal static void OnUnityExit() + public static void OnUnityExit() { try { - Unity.MTThreadInvoker.Stop(); + OnGenericSceneChange = null; + OnGenericMenuSceneLoaded = null; + + UI.UISystem.Destroy(); + UI.LoadingProgressBar.Destroy(); + + Unity.EnhancedImageParticleMaterialProvider.Destroy(); + Unity.EnhancedImageParticleSystemProvider.Destroy(); + + Unity.MTThreadInvoker.Destroy(); + Unity.MTMainThreadInvoker.Destroy(); + Unity.MTCoroutineStarter.Destroy(); + + Animation.AnimationControllerManager.Destroy(); } catch (Exception p_Exception) { @@ -171,7 +161,7 @@ internal static void OnUnityExit() /// /// Init all the available modules /// - internal static void InitModules() + public static void InitModules() { try { @@ -195,9 +185,6 @@ internal static void InitModules() /// Add plugin to the list m_Modules.Add(l_Module); - - try { l_Module.CheckForActivation(EIModuleBaseActivationType.OnStart); } - catch (Exception p_InitException) { Logger.Error("[CP_SDK][ChatPlexSDK.InitModules] Error on module init " + l_Module.Name); Logger.Error(p_InitException); } } } catch (Exception l_Exception) @@ -207,7 +194,17 @@ internal static void InitModules() } } - m_Modules.Sort((x, y) => x.Name.CompareTo(y.Name)); + m_Modules.Sort((x, y) => x.FancyName.CompareTo(y.FancyName)); + + for (int l_I = 0; l_I < m_Modules.Count; l_I++) + { + var l_Module = m_Modules[l_I]; + + try { l_Module.CheckForActivation(EIModuleBaseActivationType.OnStart); } + catch (Exception p_InitException) { Logger.Error("[CP_SDK][ChatPlexSDK.InitModules] Error on module init " + l_Module.Name); Logger.Error(p_InitException); } + } + + Chat.Service.StartServices(); } catch (Exception p_Exception) { @@ -218,7 +215,7 @@ internal static void InitModules() /// /// Stop modules /// - internal static void StopModules() + public static void StopModules() { for (int l_I = 0; l_I < m_Modules.Count; l_I++) { @@ -233,12 +230,14 @@ internal static void StopModules() Logger.Error(p_Exception); } } + + m_Modules.Clear(); } /// /// Get modules /// /// - internal static List GetModules() + public static List GetModules() => new List(m_Modules); //////////////////////////////////////////////////////////////////////////// @@ -247,7 +246,7 @@ internal static List GetModules() /// /// On generic menu scene /// - internal static void Fire_OnGenericMenuSceneLoaded() + public static void Fire_OnGenericMenuSceneLoaded() { try { @@ -272,6 +271,7 @@ internal static void Fire_OnGenericMenuSceneLoaded() try { OnGenericMenuSceneLoaded?.Invoke(); + Chat.Service.StartServices(); } catch (Exception l_Exception) { @@ -282,14 +282,13 @@ internal static void Fire_OnGenericMenuSceneLoaded() /// /// On generic menu scene /// - internal static void Fire_OnGenericMenuScene() + public static void Fire_OnGenericMenuScene() { ActiveGenericScene = EGenericScene.Menu; try { OnGenericSceneChange?.Invoke(EGenericScene.Menu); - Chat.Service.StartServices(); } catch (Exception l_Exception) @@ -301,13 +300,14 @@ internal static void Fire_OnGenericMenuScene() /// /// On generic play scene /// - internal static void Fire_OnGenericPlayingScene() + public static void Fire_OnGenericPlayingScene() { ActiveGenericScene = EGenericScene.Playing; try { OnGenericSceneChange?.Invoke(EGenericScene.Playing); + Chat.Service.StartServices(); } catch (Exception l_Exception) { diff --git a/BeatSaberPlus/CP_SDK/Config/JsonConfig.cs b/BeatSaberPlus/CP_SDK/Config/JsonConfig.cs index 6e0d406..6bb91d2 100644 --- a/BeatSaberPlus/CP_SDK/Config/JsonConfig.cs +++ b/BeatSaberPlus/CP_SDK/Config/JsonConfig.cs @@ -22,10 +22,11 @@ namespace CP_SDK.Config /// protected List m_JsonConverters = new List() { + new JsonConverters.Color32Converter(), + new JsonConverters.ColorConverter(), + new JsonConverters.QuaternionConverter(), new JsonConverters.Vector2Converter(), new JsonConverters.Vector3Converter(), - new JsonConverters.ColorConverter(), - new JsonConverters.Color32Converter() }; /// /// Raw loaded JSON diff --git a/BeatSaberPlus/CP_SDK/Config/JsonConverters/QuaternionConverter.cs b/BeatSaberPlus/CP_SDK/Config/JsonConverters/QuaternionConverter.cs new file mode 100644 index 0000000..4f10872 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Config/JsonConverters/QuaternionConverter.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; +using System; +using UnityEngine; + +namespace CP_SDK.Config.JsonConverters +{ + public class QuaternionConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + if (objectType == typeof(Quaternion)) + { + return true; + } + return false; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var t = serializer.Deserialize(reader); + var iv = JsonConvert.DeserializeObject(t.ToString()); + return iv; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + Quaternion v = (Quaternion)value; + + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(v.x); + writer.WritePropertyName("y"); + writer.WriteValue(v.y); + writer.WritePropertyName("z"); + writer.WriteValue(v.z); + writer.WritePropertyName("w"); + writer.WriteValue(v.w); + writer.WriteEndObject(); + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Logging/MelonLoaderLogger.cs b/BeatSaberPlus/CP_SDK/Logging/MelonLoaderLogger.cs new file mode 100644 index 0000000..01303c3 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Logging/MelonLoaderLogger.cs @@ -0,0 +1,43 @@ +#if CP_SDK_MELON_LOADER +using System; + +namespace CP_SDK.Logging +{ + /// + /// MelonLoader logger implementation + /// + public class MelonLoaderLogger : ILogger + { + /// + /// Log implementation + /// + /// Kind + /// Data + protected override void LogImplementation(ELogType p_Type, string p_Data) + { + switch (p_Type) + { + case ELogType.Error: MelonLoader.MelonLogger.Error(p_Data); break; + case ELogType.Warning: MelonLoader.MelonLogger.Warning(p_Data); break; + case ELogType.Info: MelonLoader.MelonLogger.Msg(p_Data); break; + case ELogType.Debug: MelonLoader.MelonLogger.Msg(p_Data); break; + } + } + /// + /// Log implementation + /// + /// Kind + /// Data + protected override void LogImplementation(ELogType p_Type, Exception p_Data) + { + switch (p_Type) + { + case ELogType.Error: MelonLoader.MelonLogger.Error(p_Data); break; + case ELogType.Warning: MelonLoader.MelonLogger.Warning(p_Data); break; + case ELogType.Info: MelonLoader.MelonLogger.Msg(p_Data); break; + case ELogType.Debug: MelonLoader.MelonLogger.Msg(p_Data); break; + } + } + } +} +#endif \ No newline at end of file diff --git a/BeatSaberPlus/CP_SDK/Misc/FastCancellationToken.cs b/BeatSaberPlus/CP_SDK/Misc/FastCancellationToken.cs new file mode 100644 index 0000000..3f402dc --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/FastCancellationToken.cs @@ -0,0 +1,29 @@ +namespace CP_SDK.Misc +{ + /// + /// Fast cancellation token + /// + public class FastCancellationToken + { + /// + /// Current serial + /// + public int Serial { get; private set; } = 0; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Increment serial + /// + public void Cancel() + => Serial++; + /// + /// Compare serial + /// + /// Old serial to compare to + /// + public bool IsCancelled(int p_OldSerial) + => Serial > p_OldSerial; + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/Adler32.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/Adler32.cs new file mode 100644 index 0000000..7301cc3 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/Adler32.cs @@ -0,0 +1,163 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Checksum +{ + /// + /// Computes Adler32 checksum for a stream of data. An Adler32 + /// checksum is not as reliable as a CRC32 checksum, but a lot faster to + /// compute. + /// + /// The specification for Adler32 may be found in RFC 1950. + /// ZLIB Compressed Data Format Specification version 3.3) + /// + /// + /// From that document: + /// + /// "ADLER32 (Adler-32 checksum) + /// This contains a checksum value of the uncompressed data + /// (excluding any dictionary data) computed according to Adler-32 + /// algorithm. This algorithm is a 32-bit extension and improvement + /// of the Fletcher algorithm, used in the ITU-T X.224 / ISO 8073 + /// standard. + /// + /// Adler-32 is composed of two sums accumulated per byte: s1 is + /// the sum of all bytes, s2 is the sum of all s1 values. Both sums + /// are done modulo 65521. s1 is initialized to 1, s2 to zero. The + /// Adler-32 checksum is stored as s2*65536 + s1 in most- + /// significant-byte first (network) order." + /// + /// "8.2. The Adler-32 algorithm + /// + /// The Adler-32 algorithm is much faster than the CRC32 algorithm yet + /// still provides an extremely low probability of undetected errors. + /// + /// The modulo on unsigned long accumulators can be delayed for 5552 + /// bytes, so the modulo operation time is negligible. If the bytes + /// are a, b, c, the second sum is 3a + 2b + c + 3, and so is position + /// and order sensitive, unlike the first sum, which is just a + /// checksum. That 65521 is prime is important to avoid a possible + /// large class of two-byte errors that leave the check unchanged. + /// (The Fletcher checksum uses 255, which is not prime and which also + /// makes the Fletcher check insensitive to single byte changes 0 - + /// 255.) + /// + /// The sum s1 is initialized to 1 instead of zero to make the length + /// of the sequence part of s2, so that the length does not have to be + /// checked separately. (Any sequence of zeroes has a Fletcher + /// checksum of zero.)" + /// + /// + /// + public sealed class Adler32 : IChecksum + { + #region Instance Fields + + /// + /// largest prime smaller than 65536 + /// + private static readonly uint BASE = 65521; + + /// + /// The CRC data checksum so far. + /// + private uint checkValue; + + #endregion Instance Fields + + /// + /// Initialise a default instance of + /// + public Adler32() + { + Reset(); + } + + /// + /// Resets the Adler32 data checksum as if no update was ever called. + /// + public void Reset() + { + checkValue = 1; + } + + /// + /// Returns the Adler32 data checksum computed so far. + /// + public long Value + { + get + { + return checkValue; + } + } + + /// + /// Updates the checksum with the byte b. + /// + /// + /// The data value to add. The high byte of the int is ignored. + /// + public void Update(int bval) + { + // We could make a length 1 byte array and call update again, but I + // would rather not have that overhead + uint s1 = checkValue & 0xFFFF; + uint s2 = checkValue >> 16; + + s1 = (s1 + ((uint)bval & 0xFF)) % BASE; + s2 = (s1 + s2) % BASE; + + checkValue = (s2 << 16) + s1; + } + + /// + /// Updates the Adler32 data checksum with the bytes taken from + /// a block of data. + /// + /// Contains the data to update the checksum with. + public void Update(byte[] buffer) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + Update(new ArraySegment(buffer, 0, buffer.Length)); + } + + /// + /// Update Adler32 data checksum based on a portion of a block of data + /// + /// + /// The chunk of data to add + /// + public void Update(ArraySegment segment) + { + //(By Per Bothner) + uint s1 = checkValue & 0xFFFF; + uint s2 = checkValue >> 16; + var count = segment.Count; + var offset = segment.Offset; + while (count > 0) + { + // We can defer the modulo operation: + // s1 maximally grows from 65521 to 65521 + 255 * 3800 + // s2 maximally grows by 3800 * median(s1) = 2090079800 < 2^31 + int n = 3800; + if (n > count) + { + n = count; + } + count -= n; + while (--n >= 0) + { + s1 = s1 + (uint)(segment.Array[offset++] & 0xff); + s2 = s2 + s1; + } + s1 %= BASE; + s2 %= BASE; + } + checkValue = (s2 << 16) | s1; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/BZip2Crc.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/BZip2Crc.cs new file mode 100644 index 0000000..7011f58 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/BZip2Crc.cs @@ -0,0 +1,171 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BSP_ICSharpCode.SharpZipLib.Checksum +{ + /// + /// CRC-32 with unreversed data and reversed output + /// + /// + /// Generate a table for a byte-wise 32-bit CRC calculation on the polynomial: + /// x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x^1+x^0. + /// + /// Polynomials over GF(2) are represented in binary, one bit per coefficient, + /// with the lowest powers in the most significant bit. Then adding polynomials + /// is just exclusive-or, and multiplying a polynomial by x is a right shift by + /// one. If we call the above polynomial p, and represent a byte as the + /// polynomial q, also with the lowest power in the most significant bit (so the + /// byte 0xb1 is the polynomial x^7+x^3+x+1), then the CRC is (q*x^32) mod p, + /// where a mod b means the remainder after dividing a by b. + /// + /// This calculation is done using the shift-register method of multiplying and + /// taking the remainder. The register is initialized to zero, and for each + /// incoming bit, x^32 is added mod p to the register if the bit is a one (where + /// x^32 mod p is p+x^32 = x^26+...+1), and the register is multiplied mod p by + /// x (which is shifting right by one and adding x^32 mod p if the bit shifted + /// out is a one). We start with the highest power (least significant bit) of + /// q and repeat for all eight bits of q. + /// + /// This implementation uses sixteen lookup tables stored in one linear array + /// to implement the slicing-by-16 algorithm, a variant of the slicing-by-8 + /// algorithm described in this Intel white paper: + /// + /// https://web.archive.org/web/20120722193753/http://download.intel.com/technology/comms/perfnet/download/slicing-by-8.pdf + /// + /// The first lookup table is simply the CRC of all possible eight bit values. + /// Each successive lookup table is derived from the original table generated + /// by Sarwate's algorithm. Slicing a 16-bit input and XORing the outputs + /// together will produce the same output as a byte-by-byte CRC loop with + /// fewer arithmetic and bit manipulation operations, at the cost of increased + /// memory consumed by the lookup tables. (Slicing-by-16 requires a 16KB table, + /// which is still small enough to fit in most processors' L1 cache.) + /// + public sealed class BZip2Crc : IChecksum + { + #region Instance Fields + + private const uint crcInit = 0xFFFFFFFF; + //const uint crcXor = 0x00000000; + + private static readonly uint[] crcTable = CrcUtilities.GenerateSlicingLookupTable(0x04C11DB7, isReversed: false); + + /// + /// The CRC data checksum so far. + /// + private uint checkValue; + + #endregion Instance Fields + + /// + /// Initialise a default instance of + /// + public BZip2Crc() + { + Reset(); + } + + /// + /// Resets the CRC data checksum as if no update was ever called. + /// + public void Reset() + { + checkValue = crcInit; + } + + /// + /// Returns the CRC data checksum computed so far. + /// + /// Reversed Out = true + public long Value + { + get + { + // Technically, the output should be: + //return (long)(~checkValue ^ crcXor); + // but x ^ 0 = x, so there is no point in adding + // the XOR operation + return (long)(~checkValue); + } + } + + /// + /// Updates the checksum with the int bval. + /// + /// + /// the byte is taken as the lower 8 bits of bval + /// + /// Reversed Data = false + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(int bval) + { + checkValue = unchecked(crcTable[(byte)(((checkValue >> 24) & 0xFF) ^ bval)] ^ (checkValue << 8)); + } + + /// + /// Updates the CRC data checksum with the bytes taken from + /// a block of data. + /// + /// Contains the data to update the CRC with. + public void Update(byte[] buffer) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + Update(buffer, 0, buffer.Length); + } + + /// + /// Update CRC data checksum based on a portion of a block of data + /// + /// + /// The chunk of data to add + /// + public void Update(ArraySegment segment) + { + Update(segment.Array, segment.Offset, segment.Count); + } + + /// + /// Internal helper function for updating a block of data using slicing. + /// + /// The array containing the data to add + /// Range start for (inclusive) + /// The number of bytes to checksum starting from + private void Update(byte[] data, int offset, int count) + { + int remainder = count % CrcUtilities.SlicingDegree; + int end = offset + count - remainder; + + while (offset != end) + { + checkValue = CrcUtilities.UpdateDataForNormalPoly(data, offset, crcTable, checkValue); + offset += CrcUtilities.SlicingDegree; + } + + if (remainder != 0) + { + SlowUpdateLoop(data, offset, end + remainder); + } + } + + /// + /// A non-inlined function for updating data that doesn't fit in a 16-byte + /// block. We don't expect to enter this function most of the time, and when + /// we do we're not here for long, so disabling inlining here improves + /// performance overall. + /// + /// The array containing the data to add + /// Range start for (inclusive) + /// Range end for (exclusive) + [MethodImpl(MethodImplOptions.NoInlining)] + private void SlowUpdateLoop(byte[] data, int offset, int end) + { + while (offset != end) + { + Update(data[offset++]); + } + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/Crc32.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/Crc32.cs new file mode 100644 index 0000000..fe31114 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/Crc32.cs @@ -0,0 +1,173 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BSP_ICSharpCode.SharpZipLib.Checksum +{ + /// + /// CRC-32 with reversed data and unreversed output + /// + /// + /// Generate a table for a byte-wise 32-bit CRC calculation on the polynomial: + /// x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x^1+x^0. + /// + /// Polynomials over GF(2) are represented in binary, one bit per coefficient, + /// with the lowest powers in the most significant bit. Then adding polynomials + /// is just exclusive-or, and multiplying a polynomial by x is a right shift by + /// one. If we call the above polynomial p, and represent a byte as the + /// polynomial q, also with the lowest power in the most significant bit (so the + /// byte 0xb1 is the polynomial x^7+x^3+x+1), then the CRC is (q*x^32) mod p, + /// where a mod b means the remainder after dividing a by b. + /// + /// This calculation is done using the shift-register method of multiplying and + /// taking the remainder. The register is initialized to zero, and for each + /// incoming bit, x^32 is added mod p to the register if the bit is a one (where + /// x^32 mod p is p+x^32 = x^26+...+1), and the register is multiplied mod p by + /// x (which is shifting right by one and adding x^32 mod p if the bit shifted + /// out is a one). We start with the highest power (least significant bit) of + /// q and repeat for all eight bits of q. + /// + /// This implementation uses sixteen lookup tables stored in one linear array + /// to implement the slicing-by-16 algorithm, a variant of the slicing-by-8 + /// algorithm described in this Intel white paper: + /// + /// https://web.archive.org/web/20120722193753/http://download.intel.com/technology/comms/perfnet/download/slicing-by-8.pdf + /// + /// The first lookup table is simply the CRC of all possible eight bit values. + /// Each successive lookup table is derived from the original table generated + /// by Sarwate's algorithm. Slicing a 16-bit input and XORing the outputs + /// together will produce the same output as a byte-by-byte CRC loop with + /// fewer arithmetic and bit manipulation operations, at the cost of increased + /// memory consumed by the lookup tables. (Slicing-by-16 requires a 16KB table, + /// which is still small enough to fit in most processors' L1 cache.) + /// + public sealed class Crc32 : IChecksum + { + #region Instance Fields + + private static readonly uint crcInit = 0xFFFFFFFF; + private static readonly uint crcXor = 0xFFFFFFFF; + + private static readonly uint[] crcTable = CrcUtilities.GenerateSlicingLookupTable(0xEDB88320, isReversed: true); + + /// + /// The CRC data checksum so far. + /// + private uint checkValue; + + #endregion Instance Fields + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static uint ComputeCrc32(uint oldCrc, byte bval) + { + return (uint)(Crc32.crcTable[(oldCrc ^ bval) & 0xFF] ^ (oldCrc >> 8)); + } + + /// + /// Initialise a default instance of + /// + public Crc32() + { + Reset(); + } + + /// + /// Resets the CRC data checksum as if no update was ever called. + /// + public void Reset() + { + checkValue = crcInit; + } + + /// + /// Returns the CRC data checksum computed so far. + /// + /// Reversed Out = false + public long Value + { + get + { + return (long)(checkValue ^ crcXor); + } + } + + /// + /// Updates the checksum with the int bval. + /// + /// + /// the byte is taken as the lower 8 bits of bval + /// + /// Reversed Data = true + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Update(int bval) + { + checkValue = unchecked(crcTable[(checkValue ^ bval) & 0xFF] ^ (checkValue >> 8)); + } + + /// + /// Updates the CRC data checksum with the bytes taken from + /// a block of data. + /// + /// Contains the data to update the CRC with. + public void Update(byte[] buffer) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + Update(buffer, 0, buffer.Length); + } + + /// + /// Update CRC data checksum based on a portion of a block of data + /// + /// + /// The chunk of data to add + /// + public void Update(ArraySegment segment) + { + Update(segment.Array, segment.Offset, segment.Count); + } + + /// + /// Internal helper function for updating a block of data using slicing. + /// + /// The array containing the data to add + /// Range start for (inclusive) + /// The number of bytes to checksum starting from + private void Update(byte[] data, int offset, int count) + { + int remainder = count % CrcUtilities.SlicingDegree; + int end = offset + count - remainder; + + while (offset != end) + { + checkValue = CrcUtilities.UpdateDataForReversedPoly(data, offset, crcTable, checkValue); + offset += CrcUtilities.SlicingDegree; + } + + if (remainder != 0) + { + SlowUpdateLoop(data, offset, end + remainder); + } + } + + /// + /// A non-inlined function for updating data that doesn't fit in a 16-byte + /// block. We don't expect to enter this function most of the time, and when + /// we do we're not here for long, so disabling inlining here improves + /// performance overall. + /// + /// The array containing the data to add + /// Range start for (inclusive) + /// Range end for (exclusive) + [MethodImpl(MethodImplOptions.NoInlining)] + private void SlowUpdateLoop(byte[] data, int offset, int end) + { + while (offset != end) + { + Update(data[offset++]); + } + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/CrcUtilities.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/CrcUtilities.cs new file mode 100644 index 0000000..660cf92 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/CrcUtilities.cs @@ -0,0 +1,158 @@ +using System.Runtime.CompilerServices; + +namespace BSP_ICSharpCode.SharpZipLib.Checksum +{ + internal static class CrcUtilities + { + /// + /// The number of slicing lookup tables to generate. + /// + internal const int SlicingDegree = 16; + + /// + /// Generates multiple CRC lookup tables for a given polynomial, stored + /// in a linear array of uints. The first block (i.e. the first 256 + /// elements) is the same as the byte-by-byte CRC lookup table. + /// + /// The generating CRC polynomial + /// Whether the polynomial is in reversed bit order + /// A linear array of 256 * elements + /// + /// This table could also be generated as a rectangular array, but the + /// JIT compiler generates slower code than if we use a linear array. + /// Known issue, see: https://github.com/dotnet/runtime/issues/30275 + /// + internal static uint[] GenerateSlicingLookupTable(uint polynomial, bool isReversed) + { + var table = new uint[256 * SlicingDegree]; + uint one = isReversed ? 1 : (1U << 31); + + for (int i = 0; i < 256; i++) + { + uint res = (uint)(isReversed ? i : i << 24); + for (int j = 0; j < SlicingDegree; j++) + { + for (int k = 0; k < 8; k++) + { + if (isReversed) + { + res = (res & one) == 1 ? polynomial ^ (res >> 1) : res >> 1; + } + else + { + res = (res & one) != 0 ? polynomial ^ (res << 1) : res << 1; + } + } + + table[(256 * j) + i] = res; + } + } + + return table; + } + + /// + /// Mixes the first four bytes of input with + /// using normal ordering before calling . + /// + /// Array of data to checksum + /// Offset to start reading from + /// The table to use for slicing-by-16 lookup + /// Checksum state before this update call + /// A new unfinalized checksum value + /// + /// + /// Assumes input[offset]..input[offset + 15] are valid array indexes. + /// For performance reasons, this must be checked by the caller. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static uint UpdateDataForNormalPoly(byte[] input, int offset, uint[] crcTable, uint checkValue) + { + byte x1 = (byte)((byte)(checkValue >> 24) ^ input[offset]); + byte x2 = (byte)((byte)(checkValue >> 16) ^ input[offset + 1]); + byte x3 = (byte)((byte)(checkValue >> 8) ^ input[offset + 2]); + byte x4 = (byte)((byte)checkValue ^ input[offset + 3]); + + return UpdateDataCommon(input, offset, crcTable, x1, x2, x3, x4); + } + + /// + /// Mixes the first four bytes of input with + /// using reflected ordering before calling . + /// + /// Array of data to checksum + /// Offset to start reading from + /// The table to use for slicing-by-16 lookup + /// Checksum state before this update call + /// A new unfinalized checksum value + /// + /// + /// Assumes input[offset]..input[offset + 15] are valid array indexes. + /// For performance reasons, this must be checked by the caller. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static uint UpdateDataForReversedPoly(byte[] input, int offset, uint[] crcTable, uint checkValue) + { + byte x1 = (byte)((byte)checkValue ^ input[offset]); + byte x2 = (byte)((byte)(checkValue >>= 8) ^ input[offset + 1]); + byte x3 = (byte)((byte)(checkValue >>= 8) ^ input[offset + 2]); + byte x4 = (byte)((byte)(checkValue >>= 8) ^ input[offset + 3]); + + return UpdateDataCommon(input, offset, crcTable, x1, x2, x3, x4); + } + + /// + /// A shared method for updating an unfinalized CRC checksum using slicing-by-16. + /// + /// Array of data to checksum + /// Offset to start reading from + /// The table to use for slicing-by-16 lookup + /// First byte of input after mixing with the old CRC + /// Second byte of input after mixing with the old CRC + /// Third byte of input after mixing with the old CRC + /// Fourth byte of input after mixing with the old CRC + /// A new unfinalized checksum value + /// + /// + /// Even though the first four bytes of input are fed in as arguments, + /// should be the same value passed to this + /// function's caller (either or + /// ). This method will get inlined + /// into both functions, so using the same offset produces faster code. + /// + /// + /// Because most processors running C# have some kind of instruction-level + /// parallelism, the order of XOR operations can affect performance. This + /// ordering assumes that the assembly code generated by the just-in-time + /// compiler will emit a bunch of arithmetic operations for checking array + /// bounds. Then it opportunistically XORs a1 and a2 to keep the processor + /// busy while those other parts of the pipeline handle the range check + /// calculations. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint UpdateDataCommon(byte[] input, int offset, uint[] crcTable, byte x1, byte x2, byte x3, byte x4) + { + uint result; + uint a1 = crcTable[x1 + 3840] ^ crcTable[x2 + 3584]; + uint a2 = crcTable[x3 + 3328] ^ crcTable[x4 + 3072]; + + result = crcTable[input[offset + 4] + 2816]; + result ^= crcTable[input[offset + 5] + 2560]; + a1 ^= crcTable[input[offset + 9] + 1536]; + result ^= crcTable[input[offset + 6] + 2304]; + result ^= crcTable[input[offset + 7] + 2048]; + result ^= crcTable[input[offset + 8] + 1792]; + a2 ^= crcTable[input[offset + 13] + 512]; + result ^= crcTable[input[offset + 10] + 1280]; + result ^= crcTable[input[offset + 11] + 1024]; + result ^= crcTable[input[offset + 12] + 768]; + result ^= a1; + result ^= crcTable[input[offset + 14] + 256]; + result ^= crcTable[input[offset + 15]]; + result ^= a2; + + return result; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/IChecksum.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/IChecksum.cs new file mode 100644 index 0000000..610dd10 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Checksum/IChecksum.cs @@ -0,0 +1,51 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Checksum +{ + /// + /// Interface to compute a data checksum used by checked input/output streams. + /// A data checksum can be updated by one byte or with a byte array. After each + /// update the value of the current checksum can be returned by calling + /// getValue. The complete checksum object can also be reset + /// so it can be used again with new data. + /// + public interface IChecksum + { + /// + /// Resets the data checksum as if no update was ever called. + /// + void Reset(); + + /// + /// Returns the data checksum computed so far. + /// + long Value + { + get; + } + + /// + /// Adds one byte to the data checksum. + /// + /// + /// the data value to add. The high byte of the int is ignored. + /// + void Update(int bval); + + /// + /// Updates the data checksum with the bytes taken from the array. + /// + /// + /// buffer an array of bytes + /// + void Update(byte[] buffer); + + /// + /// Adds the byte array to the data checksum. + /// + /// + /// The chunk of data to add + /// + void Update(ArraySegment segment); + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs new file mode 100644 index 0000000..e4fc0a1 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/ByteOrderUtils.cs @@ -0,0 +1,130 @@ +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using CT = System.Threading.CancellationToken; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable InconsistentNaming + +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + internal static class ByteOrderStreamExtensions + { + internal static byte[] SwappedBytes(ushort value) => new[] {(byte)value, (byte)(value >> 8)}; + internal static byte[] SwappedBytes(short value) => new[] {(byte)value, (byte)(value >> 8)}; + internal static byte[] SwappedBytes(uint value) => new[] {(byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24)}; + internal static byte[] SwappedBytes(int value) => new[] {(byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24)}; + + internal static byte[] SwappedBytes(long value) => new[] { + (byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24), + (byte)(value >> 32), (byte)(value >> 40), (byte)(value >> 48), (byte)(value >> 56) + }; + + internal static byte[] SwappedBytes(ulong value) => new[] { + (byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24), + (byte)(value >> 32), (byte)(value >> 40), (byte)(value >> 48), (byte)(value >> 56) + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static long SwappedS64(byte[] bytes) => ( + (long)bytes[0] << 0 | (long)bytes[1] << 8 | (long)bytes[2] << 16 | (long)bytes[3] << 24 | + (long)bytes[4] << 32 | (long)bytes[5] << 40 | (long)bytes[6] << 48 | (long)bytes[7] << 56); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong SwappedU64(byte[] bytes) => ( + (ulong)bytes[0] << 0 | (ulong)bytes[1] << 8 | (ulong)bytes[2] << 16 | (ulong)bytes[3] << 24 | + (ulong)bytes[4] << 32 | (ulong)bytes[5] << 40 | (ulong)bytes[6] << 48 | (ulong)bytes[7] << 56); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int SwappedS32(byte[] bytes) => bytes[0] | bytes[1] << 8 | bytes[2] << 16 | bytes[3] << 24; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static uint SwappedU32(byte[] bytes) => (uint) SwappedS32(bytes); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static short SwappedS16(byte[] bytes) => (short)(bytes[0] | bytes[1] << 8); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ushort SwappedU16(byte[] bytes) => (ushort) SwappedS16(bytes); + + internal static byte[] ReadBytes(this Stream stream, int count) + { + var bytes = new byte[count]; + var remaining = count; + while (remaining > 0) + { + var bytesRead = stream.Read(bytes, count - remaining, remaining); + if (bytesRead < 1) throw new EndOfStreamException(); + remaining -= bytesRead; + } + + return bytes; + } + + /// Read an unsigned short in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadLEShort(this Stream stream) => SwappedS16(ReadBytes(stream, 2)); + + /// Read an int in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadLEInt(this Stream stream) => SwappedS32(ReadBytes(stream, 4)); + + /// Read a long in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long ReadLELong(this Stream stream) => SwappedS64(ReadBytes(stream, 8)); + + /// Write an unsigned short in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEShort(this Stream stream, int value) => stream.Write(SwappedBytes(value), 0, 2); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEShortAsync(this Stream stream, int value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 2, ct).ConfigureAwait(false); + + /// Write a ushort in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEUshort(this Stream stream, ushort value) => stream.Write(SwappedBytes(value), 0, 2); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEUshortAsync(this Stream stream, ushort value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 2, ct).ConfigureAwait(false); + + /// Write an int in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEInt(this Stream stream, int value) => stream.Write(SwappedBytes(value), 0, 4); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEIntAsync(this Stream stream, int value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 4, ct).ConfigureAwait(false); + + /// Write a uint in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEUint(this Stream stream, uint value) => stream.Write(SwappedBytes(value), 0, 4); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEUintAsync(this Stream stream, uint value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 4, ct).ConfigureAwait(false); + + /// Write a long in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLELong(this Stream stream, long value) => stream.Write(SwappedBytes(value), 0, 8); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLELongAsync(this Stream stream, long value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 8, ct).ConfigureAwait(false); + + /// Write a ulong in little endian byte order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteLEUlong(this Stream stream, ulong value) => stream.Write(SwappedBytes(value), 0, 8); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task WriteLEUlongAsync(this Stream stream, ulong value, CT ct) + => await stream.WriteAsync(SwappedBytes(value), 0, 8, ct).ConfigureAwait(false); + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/EmptyRefs.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/EmptyRefs.cs new file mode 100644 index 0000000..c4e5a81 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/EmptyRefs.cs @@ -0,0 +1,17 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + internal static class Empty + { +#if NET45 + internal static class EmptyArray + { + public static readonly T[] Value = new T[0]; + } + public static T[] Array() => EmptyArray.Value; +#else + public static T[] Array() => System.Array.Empty(); +#endif + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/SharpZipBaseException.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/SharpZipBaseException.cs new file mode 100644 index 0000000..c852a8d --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/SharpZipBaseException.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.Serialization; + +namespace BSP_ICSharpCode.SharpZipLib +{ + /// + /// SharpZipBaseException is the base exception class for SharpZipLib. + /// All library exceptions are derived from this. + /// + /// NOTE: Not all exceptions thrown will be derived from this class. + /// A variety of other exceptions are possible for example + [Serializable] + public class SharpZipBaseException : Exception + { + /// + /// Initializes a new instance of the SharpZipBaseException class. + /// + public SharpZipBaseException() + { + } + + /// + /// Initializes a new instance of the SharpZipBaseException class with a specified error message. + /// + /// A message describing the exception. + public SharpZipBaseException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the SharpZipBaseException class with a specified + /// error message and a reference to the inner exception that is the cause of this exception. + /// + /// A message describing the exception. + /// The inner exception + public SharpZipBaseException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the SharpZipBaseException class with serialized data. + /// + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the serialized + /// object data about the exception being thrown. + /// + /// + /// The System.Runtime.Serialization.StreamingContext that contains contextual information + /// about the source or destination. + /// + protected SharpZipBaseException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/StreamDecodingException.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/StreamDecodingException.cs new file mode 100644 index 0000000..784d45e --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/StreamDecodingException.cs @@ -0,0 +1,50 @@ +using System; +using System.Runtime.Serialization; + +namespace BSP_ICSharpCode.SharpZipLib +{ + /// + /// Indicates that an error occurred during decoding of a input stream due to corrupt + /// data or (unintentional) library incompatibility. + /// + [Serializable] + public class StreamDecodingException : SharpZipBaseException + { + private const string GenericMessage = "Input stream could not be decoded"; + + /// + /// Initializes a new instance of the StreamDecodingException with a generic message + /// + public StreamDecodingException() : base(GenericMessage) { } + + /// + /// Initializes a new instance of the StreamDecodingException class with a specified error message. + /// + /// A message describing the exception. + public StreamDecodingException(string message) : base(message) { } + + /// + /// Initializes a new instance of the StreamDecodingException class with a specified + /// error message and a reference to the inner exception that is the cause of this exception. + /// + /// A message describing the exception. + /// The inner exception + public StreamDecodingException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Initializes a new instance of the StreamDecodingException class with serialized data. + /// + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the serialized + /// object data about the exception being thrown. + /// + /// + /// The System.Runtime.Serialization.StreamingContext that contains contextual information + /// about the source or destination. + /// + protected StreamDecodingException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/StreamUnsupportedException.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/StreamUnsupportedException.cs new file mode 100644 index 0000000..3fcd923 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/StreamUnsupportedException.cs @@ -0,0 +1,49 @@ +using System; +using System.Runtime.Serialization; + +namespace BSP_ICSharpCode.SharpZipLib +{ + /// + /// Indicates that the input stream could not decoded due to known library incompability or missing features + /// + [Serializable] + public class StreamUnsupportedException : StreamDecodingException + { + private const string GenericMessage = "Input stream is in a unsupported format"; + + /// + /// Initializes a new instance of the StreamUnsupportedException with a generic message + /// + public StreamUnsupportedException() : base(GenericMessage) { } + + /// + /// Initializes a new instance of the StreamUnsupportedException class with a specified error message. + /// + /// A message describing the exception. + public StreamUnsupportedException(string message) : base(message) { } + + /// + /// Initializes a new instance of the StreamUnsupportedException class with a specified + /// error message and a reference to the inner exception that is the cause of this exception. + /// + /// A message describing the exception. + /// The inner exception + public StreamUnsupportedException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Initializes a new instance of the StreamUnsupportedException class with serialized data. + /// + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the serialized + /// object data about the exception being thrown. + /// + /// + /// The System.Runtime.Serialization.StreamingContext that contains contextual information + /// about the source or destination. + /// + protected StreamUnsupportedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/UnexpectedEndOfStreamException.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/UnexpectedEndOfStreamException.cs new file mode 100644 index 0000000..9315676 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/UnexpectedEndOfStreamException.cs @@ -0,0 +1,49 @@ +using System; +using System.Runtime.Serialization; + +namespace BSP_ICSharpCode.SharpZipLib +{ + /// + /// Indicates that the input stream could not decoded due to the stream ending before enough data had been provided + /// + [Serializable] + public class UnexpectedEndOfStreamException : StreamDecodingException + { + private const string GenericMessage = "Input stream ended unexpectedly"; + + /// + /// Initializes a new instance of the UnexpectedEndOfStreamException with a generic message + /// + public UnexpectedEndOfStreamException() : base(GenericMessage) { } + + /// + /// Initializes a new instance of the UnexpectedEndOfStreamException class with a specified error message. + /// + /// A message describing the exception. + public UnexpectedEndOfStreamException(string message) : base(message) { } + + /// + /// Initializes a new instance of the UnexpectedEndOfStreamException class with a specified + /// error message and a reference to the inner exception that is the cause of this exception. + /// + /// A message describing the exception. + /// The inner exception + public UnexpectedEndOfStreamException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Initializes a new instance of the UnexpectedEndOfStreamException class with serialized data. + /// + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the serialized + /// object data about the exception being thrown. + /// + /// + /// The System.Runtime.Serialization.StreamingContext that contains contextual information + /// about the source or destination. + /// + protected UnexpectedEndOfStreamException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/ValueOutOfRangeException.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/ValueOutOfRangeException.cs new file mode 100644 index 0000000..1e3b6f8 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/Exceptions/ValueOutOfRangeException.cs @@ -0,0 +1,66 @@ +using System; +using System.Runtime.Serialization; + +namespace BSP_ICSharpCode.SharpZipLib +{ + /// + /// Indicates that a value was outside of the expected range when decoding an input stream + /// + [Serializable] + public class ValueOutOfRangeException : StreamDecodingException + { + /// + /// Initializes a new instance of the ValueOutOfRangeException class naming the causing variable + /// + /// Name of the variable, use: nameof() + public ValueOutOfRangeException(string nameOfValue) + : base($"{nameOfValue} out of range") { } + + /// + /// Initializes a new instance of the ValueOutOfRangeException class naming the causing variable, + /// it's current value and expected range. + /// + /// Name of the variable, use: nameof() + /// The invalid value + /// Expected maximum value + /// Expected minimum value + public ValueOutOfRangeException(string nameOfValue, long value, long maxValue, long minValue = 0) + : this(nameOfValue, value.ToString(), maxValue.ToString(), minValue.ToString()) { } + + /// + /// Initializes a new instance of the ValueOutOfRangeException class naming the causing variable, + /// it's current value and expected range. + /// + /// Name of the variable, use: nameof() + /// The invalid value + /// Expected maximum value + /// Expected minimum value + public ValueOutOfRangeException(string nameOfValue, string value, string maxValue, string minValue = "0") : + base($"{nameOfValue} out of range: {value}, should be {minValue}..{maxValue}") + { } + + private ValueOutOfRangeException() + { + } + + private ValueOutOfRangeException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the ValueOutOfRangeException class with serialized data. + /// + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the serialized + /// object data about the exception being thrown. + /// + /// + /// The System.Runtime.Serialization.StreamingContext that contains contextual information + /// about the source or destination. + /// + protected ValueOutOfRangeException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/FileSystemScanner.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/FileSystemScanner.cs new file mode 100644 index 0000000..8be2cbb --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/FileSystemScanner.cs @@ -0,0 +1,545 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + #region EventArgs + + /// + /// Event arguments for scanning. + /// + public class ScanEventArgs : EventArgs + { + #region Constructors + + /// + /// Initialise a new instance of + /// + /// The file or directory name. + public ScanEventArgs(string name) + { + name_ = name; + } + + #endregion Constructors + + /// + /// The file or directory name for this event. + /// + public string Name + { + get { return name_; } + } + + /// + /// Get set a value indicating if scanning should continue or not. + /// + public bool ContinueRunning + { + get { return continueRunning_; } + set { continueRunning_ = value; } + } + + #region Instance Fields + + private string name_; + private bool continueRunning_ = true; + + #endregion Instance Fields + } + + /// + /// Event arguments during processing of a single file or directory. + /// + public class ProgressEventArgs : EventArgs + { + #region Constructors + + /// + /// Initialise a new instance of + /// + /// The file or directory name if known. + /// The number of bytes processed so far + /// The total number of bytes to process, 0 if not known + public ProgressEventArgs(string name, long processed, long target) + { + name_ = name; + processed_ = processed; + target_ = target; + } + + #endregion Constructors + + /// + /// The name for this event if known. + /// + public string Name + { + get { return name_; } + } + + /// + /// Get set a value indicating whether scanning should continue or not. + /// + public bool ContinueRunning + { + get { return continueRunning_; } + set { continueRunning_ = value; } + } + + /// + /// Get a percentage representing how much of the has been processed + /// + /// 0.0 to 100.0 percent; 0 if target is not known. + public float PercentComplete + { + get + { + float result; + if (target_ <= 0) + { + result = 0; + } + else + { + result = ((float)processed_ / (float)target_) * 100.0f; + } + return result; + } + } + + /// + /// The number of bytes processed so far + /// + public long Processed + { + get { return processed_; } + } + + /// + /// The number of bytes to process. + /// + /// Target may be 0 or negative if the value isnt known. + public long Target + { + get { return target_; } + } + + #region Instance Fields + + private string name_; + private long processed_; + private long target_; + private bool continueRunning_ = true; + + #endregion Instance Fields + } + + /// + /// Event arguments for directories. + /// + public class DirectoryEventArgs : ScanEventArgs + { + #region Constructors + + /// + /// Initialize an instance of . + /// + /// The name for this directory. + /// Flag value indicating if any matching files are contained in this directory. + public DirectoryEventArgs(string name, bool hasMatchingFiles) + : base(name) + { + hasMatchingFiles_ = hasMatchingFiles; + } + + #endregion Constructors + + /// + /// Get a value indicating if the directory contains any matching files or not. + /// + public bool HasMatchingFiles + { + get { return hasMatchingFiles_; } + } + + private readonly + + #region Instance Fields + + bool hasMatchingFiles_; + + #endregion Instance Fields + } + + /// + /// Arguments passed when scan failures are detected. + /// + public class ScanFailureEventArgs : EventArgs + { + #region Constructors + + /// + /// Initialise a new instance of + /// + /// The name to apply. + /// The exception to use. + public ScanFailureEventArgs(string name, Exception e) + { + name_ = name; + exception_ = e; + continueRunning_ = true; + } + + #endregion Constructors + + /// + /// The applicable name. + /// + public string Name + { + get { return name_; } + } + + /// + /// The applicable exception. + /// + public Exception Exception + { + get { return exception_; } + } + + /// + /// Get / set a value indicating whether scanning should continue. + /// + public bool ContinueRunning + { + get { return continueRunning_; } + set { continueRunning_ = value; } + } + + #region Instance Fields + + private string name_; + private Exception exception_; + private bool continueRunning_; + + #endregion Instance Fields + } + + #endregion EventArgs + + #region Delegates + + /// + /// Delegate invoked before starting to process a file. + /// + /// The source of the event + /// The event arguments. + public delegate void ProcessFileHandler(object sender, ScanEventArgs e); + + /// + /// Delegate invoked during processing of a file or directory + /// + /// The source of the event + /// The event arguments. + public delegate void ProgressHandler(object sender, ProgressEventArgs e); + + /// + /// Delegate invoked when a file has been completely processed. + /// + /// The source of the event + /// The event arguments. + public delegate void CompletedFileHandler(object sender, ScanEventArgs e); + + /// + /// Delegate invoked when a directory failure is detected. + /// + /// The source of the event + /// The event arguments. + public delegate void DirectoryFailureHandler(object sender, ScanFailureEventArgs e); + + /// + /// Delegate invoked when a file failure is detected. + /// + /// The source of the event + /// The event arguments. + public delegate void FileFailureHandler(object sender, ScanFailureEventArgs e); + + #endregion Delegates + + /// + /// FileSystemScanner provides facilities scanning of files and directories. + /// + public class FileSystemScanner + { + #region Constructors + + /// + /// Initialise a new instance of + /// + /// The file filter to apply when scanning. + public FileSystemScanner(string filter) + { + fileFilter_ = new PathFilter(filter); + } + + /// + /// Initialise a new instance of + /// + /// The file filter to apply. + /// The directory filter to apply. + public FileSystemScanner(string fileFilter, string directoryFilter) + { + fileFilter_ = new PathFilter(fileFilter); + directoryFilter_ = new PathFilter(directoryFilter); + } + + /// + /// Initialise a new instance of + /// + /// The file filter to apply. + public FileSystemScanner(IScanFilter fileFilter) + { + fileFilter_ = fileFilter; + } + + /// + /// Initialise a new instance of + /// + /// The file filter to apply. + /// The directory filter to apply. + public FileSystemScanner(IScanFilter fileFilter, IScanFilter directoryFilter) + { + fileFilter_ = fileFilter; + directoryFilter_ = directoryFilter; + } + + #endregion Constructors + + #region Delegates + + /// + /// Delegate to invoke when a directory is processed. + /// + public event EventHandler ProcessDirectory; + + /// + /// Delegate to invoke when a file is processed. + /// + public ProcessFileHandler ProcessFile; + + /// + /// Delegate to invoke when processing for a file has finished. + /// + public CompletedFileHandler CompletedFile; + + /// + /// Delegate to invoke when a directory failure is detected. + /// + public DirectoryFailureHandler DirectoryFailure; + + /// + /// Delegate to invoke when a file failure is detected. + /// + public FileFailureHandler FileFailure; + + #endregion Delegates + + /// + /// Raise the DirectoryFailure event. + /// + /// The directory name. + /// The exception detected. + private bool OnDirectoryFailure(string directory, Exception e) + { + DirectoryFailureHandler handler = DirectoryFailure; + bool result = (handler != null); + if (result) + { + var args = new ScanFailureEventArgs(directory, e); + handler(this, args); + alive_ = args.ContinueRunning; + } + return result; + } + + /// + /// Raise the FileFailure event. + /// + /// The file name. + /// The exception detected. + private bool OnFileFailure(string file, Exception e) + { + FileFailureHandler handler = FileFailure; + + bool result = (handler != null); + + if (result) + { + var args = new ScanFailureEventArgs(file, e); + FileFailure(this, args); + alive_ = args.ContinueRunning; + } + return result; + } + + /// + /// Raise the ProcessFile event. + /// + /// The file name. + private void OnProcessFile(string file) + { + ProcessFileHandler handler = ProcessFile; + + if (handler != null) + { + var args = new ScanEventArgs(file); + handler(this, args); + alive_ = args.ContinueRunning; + } + } + + /// + /// Raise the complete file event + /// + /// The file name + private void OnCompleteFile(string file) + { + CompletedFileHandler handler = CompletedFile; + + if (handler != null) + { + var args = new ScanEventArgs(file); + handler(this, args); + alive_ = args.ContinueRunning; + } + } + + /// + /// Raise the ProcessDirectory event. + /// + /// The directory name. + /// Flag indicating if the directory has matching files. + private void OnProcessDirectory(string directory, bool hasMatchingFiles) + { + EventHandler handler = ProcessDirectory; + + if (handler != null) + { + var args = new DirectoryEventArgs(directory, hasMatchingFiles); + handler(this, args); + alive_ = args.ContinueRunning; + } + } + + /// + /// Scan a directory. + /// + /// The base directory to scan. + /// True to recurse subdirectories, false to scan a single directory. + public void Scan(string directory, bool recurse) + { + alive_ = true; + ScanDir(directory, recurse); + } + + private void ScanDir(string directory, bool recurse) + { + try + { + string[] names = System.IO.Directory.GetFiles(directory); + bool hasMatch = false; + for (int fileIndex = 0; fileIndex < names.Length; ++fileIndex) + { + if (!fileFilter_.IsMatch(names[fileIndex])) + { + names[fileIndex] = null; + } + else + { + hasMatch = true; + } + } + + OnProcessDirectory(directory, hasMatch); + + if (alive_ && hasMatch) + { + foreach (string fileName in names) + { + try + { + if (fileName != null) + { + OnProcessFile(fileName); + if (!alive_) + { + break; + } + } + } + catch (Exception e) + { + if (!OnFileFailure(fileName, e)) + { + throw; + } + } + } + } + } + catch (Exception e) + { + if (!OnDirectoryFailure(directory, e)) + { + throw; + } + } + + if (alive_ && recurse) + { + try + { + string[] names = System.IO.Directory.GetDirectories(directory); + foreach (string fulldir in names) + { + if ((directoryFilter_ == null) || (directoryFilter_.IsMatch(fulldir))) + { + ScanDir(fulldir, true); + if (!alive_) + { + break; + } + } + } + } + catch (Exception e) + { + if (!OnDirectoryFailure(directory, e)) + { + throw; + } + } + } + } + + #region Instance Fields + + /// + /// The file filter currently in use. + /// + private IScanFilter fileFilter_; + + /// + /// The directory filter currently in use. + /// + private IScanFilter directoryFilter_; + + /// + /// Flag indicating if scanning should continue running. + /// + private bool alive_; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/INameTransform.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/INameTransform.cs new file mode 100644 index 0000000..5692b68 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/INameTransform.cs @@ -0,0 +1,22 @@ +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + /// + /// INameTransform defines how file system names are transformed for use with archives, or vice versa. + /// + public interface INameTransform + { + /// + /// Given a file name determine the transformed value. + /// + /// The name to transform. + /// The transformed file name. + string TransformFile(string name); + + /// + /// Given a directory name determine the transformed value. + /// + /// The name to transform. + /// The transformed directory name + string TransformDirectory(string name); + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/IScanFilter.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/IScanFilter.cs new file mode 100644 index 0000000..7cd7b66 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/IScanFilter.cs @@ -0,0 +1,15 @@ +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + /// + /// Scanning filters support filtering of names. + /// + public interface IScanFilter + { + /// + /// Test a name to see if it 'matches' the filter. + /// + /// The name to test. + /// Returns true if the name matches the filter, false if it does not match. + bool IsMatch(string name); + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/InvalidNameException.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/InvalidNameException.cs new file mode 100644 index 0000000..be4e397 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/InvalidNameException.cs @@ -0,0 +1,53 @@ +using System; +using System.Runtime.Serialization; + +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + /// + /// InvalidNameException is thrown for invalid names such as directory traversal paths and names with invalid characters + /// + [Serializable] + public class InvalidNameException : SharpZipBaseException + { + /// + /// Initializes a new instance of the InvalidNameException class with a default error message. + /// + public InvalidNameException() : base("An invalid name was specified") + { + } + + /// + /// Initializes a new instance of the InvalidNameException class with a specified error message. + /// + /// A message describing the exception. + public InvalidNameException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the InvalidNameException class with a specified + /// error message and a reference to the inner exception that is the cause of this exception. + /// + /// A message describing the exception. + /// The inner exception + public InvalidNameException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the InvalidNameException class with serialized data. + /// + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the serialized + /// object data about the exception being thrown. + /// + /// + /// The System.Runtime.Serialization.StreamingContext that contains contextual information + /// about the source or destination. + /// + protected InvalidNameException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/NameFilter.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/NameFilter.cs new file mode 100644 index 0000000..a3ba3fb --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/NameFilter.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + /// + /// NameFilter is a string matching class which allows for both positive and negative + /// matching. + /// A filter is a sequence of independant regular expressions separated by semi-colons ';'. + /// To include a semi-colon it may be quoted as in \;. Each expression can be prefixed by a plus '+' sign or + /// a minus '-' sign to denote the expression is intended to include or exclude names. + /// If neither a plus or minus sign is found include is the default. + /// A given name is tested for inclusion before checking exclusions. Only names matching an include spec + /// and not matching an exclude spec are deemed to match the filter. + /// An empty filter matches any name. + /// + /// The following expression includes all name ending in '.dat' with the exception of 'dummy.dat' + /// "+\.dat$;-^dummy\.dat$" + /// + public class NameFilter : IScanFilter + { + #region Constructors + + /// + /// Construct an instance based on the filter expression passed + /// + /// The filter expression. + public NameFilter(string filter) + { + filter_ = filter; + inclusions_ = new List(); + exclusions_ = new List(); + Compile(); + } + + #endregion Constructors + + /// + /// Test a string to see if it is a valid regular expression. + /// + /// The expression to test. + /// True if expression is a valid false otherwise. + public static bool IsValidExpression(string expression) + { + bool result = true; + try + { + var exp = new Regex(expression, RegexOptions.IgnoreCase | RegexOptions.Singleline); + } + catch (ArgumentException) + { + result = false; + } + return result; + } + + /// + /// Test an expression to see if it is valid as a filter. + /// + /// The filter expression to test. + /// True if the expression is valid, false otherwise. + public static bool IsValidFilterExpression(string toTest) + { + bool result = true; + + try + { + if (toTest != null) + { + string[] items = SplitQuoted(toTest); + for (int i = 0; i < items.Length; ++i) + { + if ((items[i] != null) && (items[i].Length > 0)) + { + string toCompile; + + if (items[i][0] == '+') + { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else if (items[i][0] == '-') + { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else + { + toCompile = items[i]; + } + + var testRegex = new Regex(toCompile, RegexOptions.IgnoreCase | RegexOptions.Singleline); + } + } + } + } + catch (ArgumentException) + { + result = false; + } + + return result; + } + + /// + /// Split a string into its component pieces + /// + /// The original string + /// Returns an array of values containing the individual filter elements. + public static string[] SplitQuoted(string original) + { + char escape = '\\'; + char[] separators = { ';' }; + + var result = new List(); + + if (!string.IsNullOrEmpty(original)) + { + int endIndex = -1; + var b = new StringBuilder(); + + while (endIndex < original.Length) + { + endIndex += 1; + if (endIndex >= original.Length) + { + result.Add(b.ToString()); + } + else if (original[endIndex] == escape) + { + endIndex += 1; + if (endIndex >= original.Length) + { + throw new ArgumentException("Missing terminating escape character", nameof(original)); + } + // include escape if this is not an escaped separator + if (Array.IndexOf(separators, original[endIndex]) < 0) + b.Append(escape); + + b.Append(original[endIndex]); + } + else + { + if (Array.IndexOf(separators, original[endIndex]) >= 0) + { + result.Add(b.ToString()); + b.Length = 0; + } + else + { + b.Append(original[endIndex]); + } + } + } + } + + return result.ToArray(); + } + + /// + /// Convert this filter to its string equivalent. + /// + /// The string equivalent for this filter. + public override string ToString() + { + return filter_; + } + + /// + /// Test a value to see if it is included by the filter. + /// + /// The value to test. + /// True if the value is included, false otherwise. + public bool IsIncluded(string name) + { + bool result = false; + if (inclusions_.Count == 0) + { + result = true; + } + else + { + foreach (Regex r in inclusions_) + { + if (r.IsMatch(name)) + { + result = true; + break; + } + } + } + return result; + } + + /// + /// Test a value to see if it is excluded by the filter. + /// + /// The value to test. + /// True if the value is excluded, false otherwise. + public bool IsExcluded(string name) + { + bool result = false; + foreach (Regex r in exclusions_) + { + if (r.IsMatch(name)) + { + result = true; + break; + } + } + return result; + } + + #region IScanFilter Members + + /// + /// Test a value to see if it matches the filter. + /// + /// The value to test. + /// True if the value matches, false otherwise. + public bool IsMatch(string name) + { + return (IsIncluded(name) && !IsExcluded(name)); + } + + #endregion IScanFilter Members + + /// + /// Compile this filter. + /// + private void Compile() + { + // TODO: Check to see if combining RE's makes it faster/smaller. + // simple scheme would be to have one RE for inclusion and one for exclusion. + if (filter_ == null) + { + return; + } + + string[] items = SplitQuoted(filter_); + for (int i = 0; i < items.Length; ++i) + { + if ((items[i] != null) && (items[i].Length > 0)) + { + bool include = (items[i][0] != '-'); + string toCompile; + + if (items[i][0] == '+') + { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else if (items[i][0] == '-') + { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else + { + toCompile = items[i]; + } + + // NOTE: Regular expressions can fail to compile here for a number of reasons that cause an exception + // these are left unhandled here as the caller is responsible for ensuring all is valid. + // several functions IsValidFilterExpression and IsValidExpression are provided for such checking + if (include) + { + inclusions_.Add(new Regex(toCompile, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)); + } + else + { + exclusions_.Add(new Regex(toCompile, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)); + } + } + } + } + + #region Instance Fields + + private string filter_; + private List inclusions_; + private List exclusions_; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/PathFilter.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/PathFilter.cs new file mode 100644 index 0000000..ab4d38a --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/PathFilter.cs @@ -0,0 +1,318 @@ +using System; +using System.IO; + +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + /// + /// PathFilter filters directories and files using a form of regular expressions + /// by full path name. + /// See NameFilter for more detail on filtering. + /// + public class PathFilter : IScanFilter + { + #region Constructors + + /// + /// Initialise a new instance of . + /// + /// The filter expression to apply. + public PathFilter(string filter) + { + nameFilter_ = new NameFilter(filter); + } + + #endregion Constructors + + #region IScanFilter Members + + /// + /// Test a name to see if it matches the filter. + /// + /// The name to test. + /// True if the name matches, false otherwise. + /// is used to get the full path before matching. + public virtual bool IsMatch(string name) + { + bool result = false; + + if (name != null) + { + string cooked = (name.Length > 0) ? Path.GetFullPath(name) : ""; + result = nameFilter_.IsMatch(cooked); + } + return result; + } + + private readonly + + #endregion IScanFilter Members + + #region Instance Fields + + NameFilter nameFilter_; + + #endregion Instance Fields + } + + /// + /// ExtendedPathFilter filters based on name, file size, and the last write time of the file. + /// + /// Provides an example of how to customise filtering. + public class ExtendedPathFilter : PathFilter + { + #region Constructors + + /// + /// Initialise a new instance of ExtendedPathFilter. + /// + /// The filter to apply. + /// The minimum file size to include. + /// The maximum file size to include. + public ExtendedPathFilter(string filter, + long minSize, long maxSize) + : base(filter) + { + MinSize = minSize; + MaxSize = maxSize; + } + + /// + /// Initialise a new instance of ExtendedPathFilter. + /// + /// The filter to apply. + /// The minimum to include. + /// The maximum to include. + public ExtendedPathFilter(string filter, + DateTime minDate, DateTime maxDate) + : base(filter) + { + MinDate = minDate; + MaxDate = maxDate; + } + + /// + /// Initialise a new instance of ExtendedPathFilter. + /// + /// The filter to apply. + /// The minimum file size to include. + /// The maximum file size to include. + /// The minimum to include. + /// The maximum to include. + public ExtendedPathFilter(string filter, + long minSize, long maxSize, + DateTime minDate, DateTime maxDate) + : base(filter) + { + MinSize = minSize; + MaxSize = maxSize; + MinDate = minDate; + MaxDate = maxDate; + } + + #endregion Constructors + + #region IScanFilter Members + + /// + /// Test a filename to see if it matches the filter. + /// + /// The filename to test. + /// True if the filter matches, false otherwise. + /// The doesnt exist + public override bool IsMatch(string name) + { + bool result = base.IsMatch(name); + + if (result) + { + var fileInfo = new FileInfo(name); + result = + (MinSize <= fileInfo.Length) && + (MaxSize >= fileInfo.Length) && + (MinDate <= fileInfo.LastWriteTime) && + (MaxDate >= fileInfo.LastWriteTime) + ; + } + return result; + } + + #endregion IScanFilter Members + + #region Properties + + /// + /// Get/set the minimum size/length for a file that will match this filter. + /// + /// The default value is zero. + /// value is less than zero; greater than + public long MinSize + { + get { return minSize_; } + set + { + if ((value < 0) || (maxSize_ < value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + minSize_ = value; + } + } + + /// + /// Get/set the maximum size/length for a file that will match this filter. + /// + /// The default value is + /// value is less than zero or less than + public long MaxSize + { + get { return maxSize_; } + set + { + if ((value < 0) || (minSize_ > value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + maxSize_ = value; + } + } + + /// + /// Get/set the minimum value that will match for this filter. + /// + /// Files with a LastWrite time less than this value are excluded by the filter. + public DateTime MinDate + { + get + { + return minDate_; + } + + set + { + if (value > maxDate_) + { + throw new ArgumentOutOfRangeException(nameof(value), "Exceeds MaxDate"); + } + + minDate_ = value; + } + } + + /// + /// Get/set the maximum value that will match for this filter. + /// + /// Files with a LastWrite time greater than this value are excluded by the filter. + public DateTime MaxDate + { + get + { + return maxDate_; + } + + set + { + if (minDate_ > value) + { + throw new ArgumentOutOfRangeException(nameof(value), "Exceeds MinDate"); + } + + maxDate_ = value; + } + } + + #endregion Properties + + #region Instance Fields + + private long minSize_; + private long maxSize_ = long.MaxValue; + private DateTime minDate_ = DateTime.MinValue; + private DateTime maxDate_ = DateTime.MaxValue; + + #endregion Instance Fields + } + + /// + /// NameAndSizeFilter filters based on name and file size. + /// + /// A sample showing how filters might be extended. + [Obsolete("Use ExtendedPathFilter instead")] + public class NameAndSizeFilter : PathFilter + { + /// + /// Initialise a new instance of NameAndSizeFilter. + /// + /// The filter to apply. + /// The minimum file size to include. + /// The maximum file size to include. + public NameAndSizeFilter(string filter, long minSize, long maxSize) + : base(filter) + { + MinSize = minSize; + MaxSize = maxSize; + } + + /// + /// Test a filename to see if it matches the filter. + /// + /// The filename to test. + /// True if the filter matches, false otherwise. + public override bool IsMatch(string name) + { + bool result = base.IsMatch(name); + + if (result) + { + var fileInfo = new FileInfo(name); + long length = fileInfo.Length; + result = + (MinSize <= length) && + (MaxSize >= length); + } + return result; + } + + /// + /// Get/set the minimum size for a file that will match this filter. + /// + public long MinSize + { + get { return minSize_; } + set + { + if ((value < 0) || (maxSize_ < value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + minSize_ = value; + } + } + + /// + /// Get/set the maximum size for a file that will match this filter. + /// + public long MaxSize + { + get { return maxSize_; } + set + { + if ((value < 0) || (minSize_ > value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + maxSize_ = value; + } + } + + #region Instance Fields + + private long minSize_; + private long maxSize_ = long.MaxValue; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/PathUtils.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/PathUtils.cs new file mode 100644 index 0000000..a9209bd --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/PathUtils.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using System.Linq; + +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + /// + /// PathUtils provides simple utilities for handling paths. + /// + public static class PathUtils + { + /// + /// Remove any path root present in the path + /// + /// A containing path information. + /// The path with the root removed if it was present; path otherwise. + public static string DropPathRoot(string path) + { + // No need to drop anything + if (path == string.Empty) return path; + + var invalidChars = Path.GetInvalidPathChars(); + // If the first character after the root is a ':', .NET < 4.6.2 throws + var cleanRootSep = path.Length >= 3 && path[1] == ':' && path[2] == ':'; + + // Replace any invalid path characters with '_' to prevent Path.GetPathRoot from throwing. + // Only pass the first 258 (should be 260, but that still throws for some reason) characters + // as .NET < 4.6.2 throws on longer paths + var cleanPath = new string(path.Take(258) + .Select( (c, i) => invalidChars.Contains(c) || (i == 2 && cleanRootSep) ? '_' : c).ToArray()); + + var stripLength = Path.GetPathRoot(cleanPath)?.Length ?? 0; + while (path.Length > stripLength && (path[stripLength] == '/' || path[stripLength] == '\\')) stripLength++; + return path.Substring(stripLength); + } + + /// + /// Returns a random file name in the users temporary directory, or in directory of if specified + /// + /// If specified, used as the base file name for the temporary file + /// Returns a temporary file name + public static string GetTempFileName(string original = null) + { + string fileName; + var tempPath = Path.GetTempPath(); + + do + { + fileName = original == null + ? Path.Combine(tempPath, Path.GetRandomFileName()) + : $"{original}.{Path.GetRandomFileName()}"; + } while (File.Exists(fileName)); + + return fileName; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/StreamUtils.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/StreamUtils.cs new file mode 100644 index 0000000..72ff7da --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/StreamUtils.cs @@ -0,0 +1,295 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + /// + /// Provides simple " utilities. + /// + public static class StreamUtils + { + /// + /// Read from a ensuring all the required data is read. + /// + /// The stream to read. + /// The buffer to fill. + /// + public static void ReadFully(Stream stream, byte[] buffer) + { + ReadFully(stream, buffer, 0, buffer.Length); + } + + /// + /// Read from a " ensuring all the required data is read. + /// + /// The stream to read data from. + /// The buffer to store data in. + /// The offset at which to begin storing data. + /// The number of bytes of data to store. + /// Required parameter is null + /// and or are invalid. + /// End of stream is encountered before all the data has been read. + public static void ReadFully(Stream stream, byte[] buffer, int offset, int count) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + // Offset can equal length when buffer and count are 0. + if ((offset < 0) || (offset > buffer.Length)) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if ((count < 0) || (offset + count > buffer.Length)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + while (count > 0) + { + int readCount = stream.Read(buffer, offset, count); + if (readCount <= 0) + { + throw new EndOfStreamException(); + } + offset += readCount; + count -= readCount; + } + } + + /// + /// Read as much data as possible from a ", up to the requested number of bytes + /// + /// The stream to read data from. + /// The buffer to store data in. + /// The offset at which to begin storing data. + /// The number of bytes of data to store. + /// Required parameter is null + /// and or are invalid. + public static int ReadRequestedBytes(Stream stream, byte[] buffer, int offset, int count) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + // Offset can equal length when buffer and count are 0. + if ((offset < 0) || (offset > buffer.Length)) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if ((count < 0) || (offset + count > buffer.Length)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + int totalReadCount = 0; + while (count > 0) + { + int readCount = stream.Read(buffer, offset, count); + if (readCount <= 0) + { + break; + } + offset += readCount; + count -= readCount; + totalReadCount += readCount; + } + + return totalReadCount; + } + + /// + /// Copy the contents of one to another. + /// + /// The stream to source data from. + /// The stream to write data to. + /// The buffer to use during copying. + public static void Copy(Stream source, Stream destination, byte[] buffer) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + // Ensure a reasonable size of buffer is used without being prohibitive. + if (buffer.Length < 128) + { + throw new ArgumentException("Buffer is too small", nameof(buffer)); + } + + bool copying = true; + + while (copying) + { + int bytesRead = source.Read(buffer, 0, buffer.Length); + if (bytesRead > 0) + { + destination.Write(buffer, 0, bytesRead); + } + else + { + destination.Flush(); + copying = false; + } + } + } + + /// + /// Copy the contents of one to another. + /// + /// The stream to source data from. + /// The stream to write data to. + /// The buffer to use during copying. + /// The progress handler delegate to use. + /// The minimum between progress updates. + /// The source for this event. + /// The name to use with the event. + /// This form is specialised for use within #Zip to support events during archive operations. + public static void Copy(Stream source, Stream destination, + byte[] buffer, ProgressHandler progressHandler, TimeSpan updateInterval, object sender, string name) + { + Copy(source, destination, buffer, progressHandler, updateInterval, sender, name, -1); + } + + /// + /// Copy the contents of one to another. + /// + /// The stream to source data from. + /// The stream to write data to. + /// The buffer to use during copying. + /// The progress handler delegate to use. + /// The minimum between progress updates. + /// The source for this event. + /// The name to use with the event. + /// A predetermined fixed target value to use with progress updates. + /// If the value is negative the target is calculated by looking at the stream. + /// This form is specialised for use within #Zip to support events during archive operations. + public static void Copy(Stream source, Stream destination, + byte[] buffer, + ProgressHandler progressHandler, TimeSpan updateInterval, + object sender, string name, long fixedTarget) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + // Ensure a reasonable size of buffer is used without being prohibitive. + if (buffer.Length < 128) + { + throw new ArgumentException("Buffer is too small", nameof(buffer)); + } + + if (progressHandler == null) + { + throw new ArgumentNullException(nameof(progressHandler)); + } + + bool copying = true; + + DateTime marker = DateTime.Now; + long processed = 0; + long target = 0; + + if (fixedTarget >= 0) + { + target = fixedTarget; + } + else if (source.CanSeek) + { + target = source.Length - source.Position; + } + + // Always fire 0% progress.. + var args = new ProgressEventArgs(name, processed, target); + progressHandler(sender, args); + + bool progressFired = true; + + while (copying) + { + int bytesRead = source.Read(buffer, 0, buffer.Length); + if (bytesRead > 0) + { + processed += bytesRead; + progressFired = false; + destination.Write(buffer, 0, bytesRead); + } + else + { + destination.Flush(); + copying = false; + } + + if (DateTime.Now - marker > updateInterval) + { + progressFired = true; + marker = DateTime.Now; + args = new ProgressEventArgs(name, processed, target); + progressHandler(sender, args); + + copying = args.ContinueRunning; + } + } + + if (!progressFired) + { + args = new ProgressEventArgs(name, processed, target); + progressHandler(sender, args); + } + } + + internal static async Task WriteProcToStreamAsync(this Stream targetStream, MemoryStream bufferStream, Action writeProc, CancellationToken ct) + { + bufferStream.SetLength(0); + writeProc(bufferStream); + bufferStream.Position = 0; + await bufferStream.CopyToAsync(targetStream, 81920, ct).ConfigureAwait(false); + bufferStream.SetLength(0); + } + + internal static async Task WriteProcToStreamAsync(this Stream targetStream, Action writeProc, CancellationToken ct) + { + using (var ms = new MemoryStream()) + { + await WriteProcToStreamAsync(targetStream, ms, writeProc, ct).ConfigureAwait(false); + } + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/StringBuilderPool.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/StringBuilderPool.cs new file mode 100644 index 0000000..0a5fbcf --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Core/StringBuilderPool.cs @@ -0,0 +1,22 @@ +using System.Collections.Concurrent; +using System.Text; + +namespace BSP_ICSharpCode.SharpZipLib.Core +{ + internal class StringBuilderPool + { + public static StringBuilderPool Instance { get; } = new StringBuilderPool(); + private readonly ConcurrentQueue pool = new ConcurrentQueue(); + + public StringBuilder Rent() + { + return pool.TryDequeue(out var builder) ? builder : new StringBuilder(); + } + + public void Return(StringBuilder builder) + { + builder.Clear(); + pool.Enqueue(builder); + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/PkzipClassic.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/PkzipClassic.cs new file mode 100644 index 0000000..06e26f3 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/PkzipClassic.cs @@ -0,0 +1,487 @@ +using BSP_ICSharpCode.SharpZipLib.Checksum; +using System; +using System.Security.Cryptography; + +namespace BSP_ICSharpCode.SharpZipLib.Encryption +{ + /// + /// PkzipClassic embodies the classic or original encryption facilities used in Pkzip archives. + /// While it has been superseded by more recent and more powerful algorithms, its still in use and + /// is viable for preventing casual snooping + /// + public abstract class PkzipClassic : SymmetricAlgorithm + { + /// + /// Generates new encryption keys based on given seed + /// + /// The seed value to initialise keys with. + /// A new key value. + static public byte[] GenerateKeys(byte[] seed) + { + if (seed == null) + { + throw new ArgumentNullException(nameof(seed)); + } + + if (seed.Length == 0) + { + throw new ArgumentException("Length is zero", nameof(seed)); + } + + uint[] newKeys = { + 0x12345678, + 0x23456789, + 0x34567890 + }; + + for (int i = 0; i < seed.Length; ++i) + { + newKeys[0] = Crc32.ComputeCrc32(newKeys[0], seed[i]); + newKeys[1] = newKeys[1] + (byte)newKeys[0]; + newKeys[1] = newKeys[1] * 134775813 + 1; + newKeys[2] = Crc32.ComputeCrc32(newKeys[2], (byte)(newKeys[1] >> 24)); + } + + byte[] result = new byte[12]; + result[0] = (byte)(newKeys[0] & 0xff); + result[1] = (byte)((newKeys[0] >> 8) & 0xff); + result[2] = (byte)((newKeys[0] >> 16) & 0xff); + result[3] = (byte)((newKeys[0] >> 24) & 0xff); + result[4] = (byte)(newKeys[1] & 0xff); + result[5] = (byte)((newKeys[1] >> 8) & 0xff); + result[6] = (byte)((newKeys[1] >> 16) & 0xff); + result[7] = (byte)((newKeys[1] >> 24) & 0xff); + result[8] = (byte)(newKeys[2] & 0xff); + result[9] = (byte)((newKeys[2] >> 8) & 0xff); + result[10] = (byte)((newKeys[2] >> 16) & 0xff); + result[11] = (byte)((newKeys[2] >> 24) & 0xff); + return result; + } + } + + /// + /// PkzipClassicCryptoBase provides the low level facilities for encryption + /// and decryption using the PkzipClassic algorithm. + /// + internal class PkzipClassicCryptoBase + { + /// + /// Transform a single byte + /// + /// + /// The transformed value + /// + protected byte TransformByte() + { + uint temp = ((keys[2] & 0xFFFF) | 2); + return (byte)((temp * (temp ^ 1)) >> 8); + } + + /// + /// Set the key schedule for encryption/decryption. + /// + /// The data use to set the keys from. + protected void SetKeys(byte[] keyData) + { + if (keyData == null) + { + throw new ArgumentNullException(nameof(keyData)); + } + + if (keyData.Length != 12) + { + throw new InvalidOperationException("Key length is not valid"); + } + + keys = new uint[3]; + keys[0] = (uint)((keyData[3] << 24) | (keyData[2] << 16) | (keyData[1] << 8) | keyData[0]); + keys[1] = (uint)((keyData[7] << 24) | (keyData[6] << 16) | (keyData[5] << 8) | keyData[4]); + keys[2] = (uint)((keyData[11] << 24) | (keyData[10] << 16) | (keyData[9] << 8) | keyData[8]); + } + + /// + /// Update encryption keys + /// + protected void UpdateKeys(byte ch) + { + keys[0] = Crc32.ComputeCrc32(keys[0], ch); + keys[1] = keys[1] + (byte)keys[0]; + keys[1] = keys[1] * 134775813 + 1; + keys[2] = Crc32.ComputeCrc32(keys[2], (byte)(keys[1] >> 24)); + } + + /// + /// Reset the internal state. + /// + protected void Reset() + { + keys[0] = 0; + keys[1] = 0; + keys[2] = 0; + } + + #region Instance Fields + + private uint[] keys; + + #endregion Instance Fields + } + + /// + /// PkzipClassic CryptoTransform for encryption. + /// + internal class PkzipClassicEncryptCryptoTransform : PkzipClassicCryptoBase, ICryptoTransform + { + /// + /// Initialise a new instance of + /// + /// The key block to use. + internal PkzipClassicEncryptCryptoTransform(byte[] keyBlock) + { + SetKeys(keyBlock); + } + + #region ICryptoTransform Members + + /// + /// Transforms the specified region of the specified byte array. + /// + /// The input for which to compute the transform. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the byte array to use as data. + /// The computed transform. + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + byte[] result = new byte[inputCount]; + TransformBlock(inputBuffer, inputOffset, inputCount, result, 0); + return result; + } + + /// + /// Transforms the specified region of the input byte array and copies + /// the resulting transform to the specified region of the output byte array. + /// + /// The input for which to compute the transform. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + /// The output to which to write the transform. + /// The offset into the output byte array from which to begin writing data. + /// The number of bytes written. + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + for (int i = inputOffset; i < inputOffset + inputCount; ++i) + { + byte oldbyte = inputBuffer[i]; + outputBuffer[outputOffset++] = (byte)(inputBuffer[i] ^ TransformByte()); + UpdateKeys(oldbyte); + } + return inputCount; + } + + /// + /// Gets a value indicating whether the current transform can be reused. + /// + public bool CanReuseTransform + { + get + { + return true; + } + } + + /// + /// Gets the size of the input data blocks in bytes. + /// + public int InputBlockSize + { + get + { + return 1; + } + } + + /// + /// Gets the size of the output data blocks in bytes. + /// + public int OutputBlockSize + { + get + { + return 1; + } + } + + /// + /// Gets a value indicating whether multiple blocks can be transformed. + /// + public bool CanTransformMultipleBlocks + { + get + { + return true; + } + } + + #endregion ICryptoTransform Members + + #region IDisposable Members + + /// + /// Cleanup internal state. + /// + public void Dispose() + { + Reset(); + } + + #endregion IDisposable Members + } + + /// + /// PkzipClassic CryptoTransform for decryption. + /// + internal class PkzipClassicDecryptCryptoTransform : PkzipClassicCryptoBase, ICryptoTransform + { + /// + /// Initialise a new instance of . + /// + /// The key block to decrypt with. + internal PkzipClassicDecryptCryptoTransform(byte[] keyBlock) + { + SetKeys(keyBlock); + } + + #region ICryptoTransform Members + + /// + /// Transforms the specified region of the specified byte array. + /// + /// The input for which to compute the transform. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the byte array to use as data. + /// The computed transform. + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + byte[] result = new byte[inputCount]; + TransformBlock(inputBuffer, inputOffset, inputCount, result, 0); + return result; + } + + /// + /// Transforms the specified region of the input byte array and copies + /// the resulting transform to the specified region of the output byte array. + /// + /// The input for which to compute the transform. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + /// The output to which to write the transform. + /// The offset into the output byte array from which to begin writing data. + /// The number of bytes written. + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + for (int i = inputOffset; i < inputOffset + inputCount; ++i) + { + var newByte = (byte)(inputBuffer[i] ^ TransformByte()); + outputBuffer[outputOffset++] = newByte; + UpdateKeys(newByte); + } + return inputCount; + } + + /// + /// Gets a value indicating whether the current transform can be reused. + /// + public bool CanReuseTransform + { + get + { + return true; + } + } + + /// + /// Gets the size of the input data blocks in bytes. + /// + public int InputBlockSize + { + get + { + return 1; + } + } + + /// + /// Gets the size of the output data blocks in bytes. + /// + public int OutputBlockSize + { + get + { + return 1; + } + } + + /// + /// Gets a value indicating whether multiple blocks can be transformed. + /// + public bool CanTransformMultipleBlocks + { + get + { + return true; + } + } + + #endregion ICryptoTransform Members + + #region IDisposable Members + + /// + /// Cleanup internal state. + /// + public void Dispose() + { + Reset(); + } + + #endregion IDisposable Members + } + + /// + /// Defines a wrapper object to access the Pkzip algorithm. + /// This class cannot be inherited. + /// + public sealed class PkzipClassicManaged : PkzipClassic + { + /// + /// Get / set the applicable block size in bits. + /// + /// The only valid block size is 8. + public override int BlockSize + { + get + { + return 8; + } + + set + { + if (value != 8) + { + throw new CryptographicException("Block size is invalid"); + } + } + } + + /// + /// Get an array of legal key sizes. + /// + public override KeySizes[] LegalKeySizes + { + get + { + KeySizes[] keySizes = new KeySizes[1]; + keySizes[0] = new KeySizes(12 * 8, 12 * 8, 0); + return keySizes; + } + } + + /// + /// Generate an initial vector. + /// + public override void GenerateIV() + { + // Do nothing. + } + + /// + /// Get an array of legal block sizes. + /// + public override KeySizes[] LegalBlockSizes + { + get + { + KeySizes[] keySizes = new KeySizes[1]; + keySizes[0] = new KeySizes(1 * 8, 1 * 8, 0); + return keySizes; + } + } + + /// + /// Get / set the key value applicable. + /// + public override byte[] Key + { + get + { + if (key_ == null) + { + GenerateKey(); + } + + return (byte[])key_.Clone(); + } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (value.Length != 12) + { + throw new CryptographicException("Key size is illegal"); + } + + key_ = (byte[])value.Clone(); + } + } + + /// + /// Generate a new random key. + /// + public override void GenerateKey() + { + key_ = new byte[12]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(key_); + } + } + + /// + /// Create an encryptor. + /// + /// The key to use for this encryptor. + /// Initialisation vector for the new encryptor. + /// Returns a new PkzipClassic encryptor + public override ICryptoTransform CreateEncryptor( + byte[] rgbKey, + byte[] rgbIV) + { + key_ = rgbKey; + return new PkzipClassicEncryptCryptoTransform(Key); + } + + /// + /// Create a decryptor. + /// + /// Keys to use for this new decryptor. + /// Initialisation vector for the new decryptor. + /// Returns a new decryptor. + public override ICryptoTransform CreateDecryptor( + byte[] rgbKey, + byte[] rgbIV) + { + key_ = rgbKey; + return new PkzipClassicDecryptCryptoTransform(Key); + } + + #region Instance Fields + + private byte[] key_; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs new file mode 100644 index 0000000..180ab94 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs @@ -0,0 +1,230 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using BSP_ICSharpCode.SharpZipLib.Core; +using BSP_ICSharpCode.SharpZipLib.Zip; + +namespace BSP_ICSharpCode.SharpZipLib.Encryption +{ + /// + /// Encrypts and decrypts AES ZIP + /// + /// + /// Based on information from http://www.winzip.com/aes_info.htm + /// and http://www.gladman.me.uk/cryptography_technology/fileencrypt/ + /// + internal class ZipAESStream : CryptoStream + { + /// + /// Constructor + /// + /// The stream on which to perform the cryptographic transformation. + /// Instance of ZipAESTransform + /// Read or Write + public ZipAESStream(Stream stream, ZipAESTransform transform, CryptoStreamMode mode) + : base(stream, transform, mode) + { + _stream = stream; + _transform = transform; + _slideBuffer = new byte[1024]; + + // mode: + // CryptoStreamMode.Read means we read from "stream" and pass decrypted to our Read() method. + // Write bypasses this stream and uses the Transform directly. + if (mode != CryptoStreamMode.Read) + { + throw new Exception("ZipAESStream only for read"); + } + } + + // The final n bytes of the AES stream contain the Auth Code. + public const int AUTH_CODE_LENGTH = 10; + + // Blocksize is always 16 here, even for AES-256 which has transform.InputBlockSize of 32. + private const int CRYPTO_BLOCK_SIZE = 16; + + // total length of block + auth code + private const int BLOCK_AND_AUTH = CRYPTO_BLOCK_SIZE + AUTH_CODE_LENGTH; + + private Stream _stream; + private ZipAESTransform _transform; + private byte[] _slideBuffer; + private int _slideBufStartPos; + private int _slideBufFreePos; + + // Buffer block transforms to enable partial reads + private byte[] _transformBuffer = null;// new byte[CRYPTO_BLOCK_SIZE]; + private int _transformBufferFreePos; + private int _transformBufferStartPos; + + // Do we have some buffered data available? + private bool HasBufferedData =>_transformBuffer != null && _transformBufferStartPos < _transformBufferFreePos; + + /// + /// Reads a sequence of bytes from the current CryptoStream into buffer, + /// and advances the position within the stream by the number of bytes read. + /// + public override int Read(byte[] buffer, int offset, int count) + { + // Nothing to do + if (count == 0) + return 0; + + // If we have buffered data, read that first + int nBytes = 0; + if (HasBufferedData) + { + nBytes = ReadBufferedData(buffer, offset, count); + + // Read all requested data from the buffer + if (nBytes == count) + return nBytes; + + offset += nBytes; + count -= nBytes; + } + + // Read more data from the input, if available + if (_slideBuffer != null) + nBytes += ReadAndTransform(buffer, offset, count); + + return nBytes; + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var readCount = Read(buffer, offset, count); + return Task.FromResult(readCount); + } + + // Read data from the underlying stream and decrypt it + private int ReadAndTransform(byte[] buffer, int offset, int count) + { + int nBytes = 0; + while (nBytes < count) + { + int bytesLeftToRead = count - nBytes; + + // Calculate buffer quantities vs read-ahead size, and check for sufficient free space + int byteCount = _slideBufFreePos - _slideBufStartPos; + + // Need to handle final block and Auth Code specially, but don't know total data length. + // Maintain a read-ahead equal to the length of (crypto block + Auth Code). + // When that runs out we can detect these final sections. + int lengthToRead = BLOCK_AND_AUTH - byteCount; + if (_slideBuffer.Length - _slideBufFreePos < lengthToRead) + { + // Shift the data to the beginning of the buffer + int iTo = 0; + for (int iFrom = _slideBufStartPos; iFrom < _slideBufFreePos; iFrom++, iTo++) + { + _slideBuffer[iTo] = _slideBuffer[iFrom]; + } + _slideBufFreePos -= _slideBufStartPos; // Note the -= + _slideBufStartPos = 0; + } + int obtained = StreamUtils.ReadRequestedBytes(_stream, _slideBuffer, _slideBufFreePos, lengthToRead); + _slideBufFreePos += obtained; + + // Recalculate how much data we now have + byteCount = _slideBufFreePos - _slideBufStartPos; + if (byteCount >= BLOCK_AND_AUTH) + { + var read = TransformAndBufferBlock(buffer, offset, bytesLeftToRead, CRYPTO_BLOCK_SIZE); + nBytes += read; + offset += read; + } + else + { + // Last round. + if (byteCount > AUTH_CODE_LENGTH) + { + // At least one byte of data plus auth code + int finalBlock = byteCount - AUTH_CODE_LENGTH; + nBytes += TransformAndBufferBlock(buffer, offset, bytesLeftToRead, finalBlock); + } + else if (byteCount < AUTH_CODE_LENGTH) + throw new ZipException("Internal error missed auth code"); // Coding bug + // Final block done. Check Auth code. + byte[] calcAuthCode = _transform.GetAuthCode(); + for (int i = 0; i < AUTH_CODE_LENGTH; i++) + { + if (calcAuthCode[i] != _slideBuffer[_slideBufStartPos + i]) + { + throw new ZipException("AES Authentication Code does not match. This is a super-CRC check on the data in the file after compression and encryption. \r\n" + + "The file may be damaged."); + } + } + + // don't need this any more, so use it as a 'complete' flag + _slideBuffer = null; + + break; // Reached the auth code + } + } + return nBytes; + } + + // read some buffered data + private int ReadBufferedData(byte[] buffer, int offset, int count) + { + int copyCount = Math.Min(count, _transformBufferFreePos - _transformBufferStartPos); + + Array.Copy(_transformBuffer, _transformBufferStartPos, buffer, offset, copyCount); + _transformBufferStartPos += copyCount; + + return copyCount; + } + + // Perform the crypto transform, and buffer the data if less than one block has been requested. + private int TransformAndBufferBlock(byte[] buffer, int offset, int count, int blockSize) + { + // If the requested data is greater than one block, transform it directly into the output + // If it's smaller, do it into a temporary buffer and copy the requested part + bool bufferRequired = (blockSize > count); + + if (bufferRequired && _transformBuffer == null) + _transformBuffer = new byte[CRYPTO_BLOCK_SIZE]; + + var targetBuffer = bufferRequired ? _transformBuffer : buffer; + var targetOffset = bufferRequired ? 0 : offset; + + // Transform the data + _transform.TransformBlock(_slideBuffer, + _slideBufStartPos, + blockSize, + targetBuffer, + targetOffset); + + _slideBufStartPos += blockSize; + + if (!bufferRequired) + { + return blockSize; + } + else + { + Array.Copy(_transformBuffer, 0, buffer, offset, count); + _transformBufferStartPos = count; + _transformBufferFreePos = blockSize; + + return count; + } + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + public override void Write(byte[] buffer, int offset, int count) + { + // ZipAESStream is used for reading but not for writing. Writing uses the ZipAESTransform directly. + throw new NotImplementedException(); + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/ZipAESTransform.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/ZipAESTransform.cs new file mode 100644 index 0000000..b3c82a6 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Encryption/ZipAESTransform.cs @@ -0,0 +1,178 @@ +using System; +using System.Security.Cryptography; + +namespace BSP_ICSharpCode.SharpZipLib.Encryption +{ + /// + /// Transforms stream using AES in CTR mode + /// + internal class ZipAESTransform : ICryptoTransform + { + private const int PWD_VER_LENGTH = 2; + + // WinZip use iteration count of 1000 for PBKDF2 key generation + private const int KEY_ROUNDS = 1000; + + // For 128-bit AES (16 bytes) the encryption is implemented as expected. + // For 256-bit AES (32 bytes) WinZip do full 256 bit AES of the nonce to create the encryption + // block but use only the first 16 bytes of it, and discard the second half. + private const int ENCRYPT_BLOCK = 16; + + private int _blockSize; + private readonly ICryptoTransform _encryptor; + private readonly byte[] _counterNonce; + private byte[] _encryptBuffer; + private int _encrPos; + private byte[] _pwdVerifier; + private IncrementalHash _hmacsha1; + private byte[] _authCode = null; + + private bool _writeMode; + + /// + /// Constructor. + /// + /// Password string + /// Random bytes, length depends on encryption strength. + /// 128 bits = 8 bytes, 192 bits = 12 bytes, 256 bits = 16 bytes. + /// The encryption strength, in bytes eg 16 for 128 bits. + /// True when creating a zip, false when reading. For the AuthCode. + /// + public ZipAESTransform(string key, byte[] saltBytes, int blockSize, bool writeMode) + { + if (blockSize != 16 && blockSize != 32) // 24 valid for AES but not supported by Winzip + throw new Exception("Invalid blocksize " + blockSize + ". Must be 16 or 32."); + if (saltBytes.Length != blockSize / 2) + throw new Exception("Invalid salt len. Must be " + blockSize / 2 + " for blocksize " + blockSize); + // initialise the encryption buffer and buffer pos + _blockSize = blockSize; + _encryptBuffer = new byte[_blockSize]; + _encrPos = ENCRYPT_BLOCK; + + // Performs the equivalent of derive_key in Dr Brian Gladman's pwd2key.c +#if NET472_OR_GREATER || NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_0_OR_GREATER + var pdb = new Rfc2898DeriveBytes(key, saltBytes, KEY_ROUNDS, HashAlgorithmName.SHA1); +#else + var pdb = new Rfc2898DeriveBytes(key, saltBytes, KEY_ROUNDS); +#endif + var rm = Aes.Create(); + rm.Mode = CipherMode.ECB; // No feedback from cipher for CTR mode + _counterNonce = new byte[_blockSize]; + byte[] key1bytes = pdb.GetBytes(_blockSize); + byte[] key2bytes = pdb.GetBytes(_blockSize); + + // Use empty IV for AES + _encryptor = rm.CreateEncryptor(key1bytes, new byte[16]); + _pwdVerifier = pdb.GetBytes(PWD_VER_LENGTH); + // + _hmacsha1 = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA1, key2bytes); + _writeMode = writeMode; + } + + /// + /// Implement the ICryptoTransform method. + /// + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + // Pass the data stream to the hash algorithm for generating the Auth Code. + // This does not change the inputBuffer. Do this before decryption for read mode. + if (!_writeMode) + { + _hmacsha1.AppendData(inputBuffer, inputOffset, inputCount); + } + // Encrypt with AES in CTR mode. Regards to Dr Brian Gladman for this. + int ix = 0; + while (ix < inputCount) + { + if (_encrPos == ENCRYPT_BLOCK) + { + /* increment encryption nonce */ + int j = 0; + while (++_counterNonce[j] == 0) + { + ++j; + } + /* encrypt the nonce to form next xor buffer */ + _encryptor.TransformBlock(_counterNonce, 0, _blockSize, _encryptBuffer, 0); + _encrPos = 0; + } + outputBuffer[ix + outputOffset] = (byte)(inputBuffer[ix + inputOffset] ^ _encryptBuffer[_encrPos++]); + // + ix++; + } + if (_writeMode) + { + // This does not change the buffer. + _hmacsha1.AppendData(outputBuffer, outputOffset, inputCount); + } + return inputCount; + } + + /// + /// Returns the 2 byte password verifier + /// + public byte[] PwdVerifier => _pwdVerifier; + + /// + /// Returns the 10 byte AUTH CODE to be checked or appended immediately following the AES data stream. + /// + public byte[] GetAuthCode() => _authCode ?? (_authCode = _hmacsha1.GetHashAndReset()); + + #region ICryptoTransform Members + + /// + /// Transform final block and read auth code + /// + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + var buffer = Array.Empty(); + + // FIXME: When used together with `ZipAESStream`, the final block handling is done inside of it instead + // This should not be necessary anymore, and the entire `ZipAESStream` class should be replaced with a plain `CryptoStream` + if (inputCount != 0) { + if (inputCount > ZipAESStream.AUTH_CODE_LENGTH) + { + // At least one byte of data is preceeding the auth code + int finalBlock = inputCount - ZipAESStream.AUTH_CODE_LENGTH; + buffer = new byte[finalBlock]; + TransformBlock(inputBuffer, inputOffset, finalBlock, buffer, 0); + } + else if (inputCount < ZipAESStream.AUTH_CODE_LENGTH) + throw new Zip.ZipException("Auth code missing from input stream"); + + // Read the authcode from the last 10 bytes + _authCode = _hmacsha1.GetHashAndReset(); + } + + + return buffer; + } + + /// + /// Gets the size of the input data blocks in bytes. + /// + public int InputBlockSize => _blockSize; + + /// + /// Gets the size of the output data blocks in bytes. + /// + public int OutputBlockSize => _blockSize; + + /// + /// Gets a value indicating whether multiple blocks can be transformed. + /// + public bool CanTransformMultipleBlocks => true; + + /// + /// Gets a value indicating whether the current transform can be reused. + /// + public bool CanReuseTransform => true; + + /// + /// Cleanup internal state. + /// + public void Dispose() => _encryptor.Dispose(); + + #endregion ICryptoTransform Members + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Deflater.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Deflater.cs new file mode 100644 index 0000000..1a38f2f --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Deflater.cs @@ -0,0 +1,604 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// This is the Deflater class. The deflater class compresses input + /// with the deflate algorithm described in RFC 1951. It has several + /// compression levels and three different strategies described below. + /// + /// This class is not thread safe. This is inherent in the API, due + /// to the split of deflate and setInput. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class Deflater + { + #region Deflater Documentation + + /* + * The Deflater can do the following state transitions: + * + * (1) -> INIT_STATE ----> INIT_FINISHING_STATE ---. + * / | (2) (5) | + * / v (5) | + * (3)| SETDICT_STATE ---> SETDICT_FINISHING_STATE |(3) + * \ | (3) | ,--------' + * | | | (3) / + * v v (5) v v + * (1) -> BUSY_STATE ----> FINISHING_STATE + * | (6) + * v + * FINISHED_STATE + * \_____________________________________/ + * | (7) + * v + * CLOSED_STATE + * + * (1) If we should produce a header we start in INIT_STATE, otherwise + * we start in BUSY_STATE. + * (2) A dictionary may be set only when we are in INIT_STATE, then + * we change the state as indicated. + * (3) Whether a dictionary is set or not, on the first call of deflate + * we change to BUSY_STATE. + * (4) -- intentionally left blank -- :) + * (5) FINISHING_STATE is entered, when flush() is called to indicate that + * there is no more INPUT. There are also states indicating, that + * the header wasn't written yet. + * (6) FINISHED_STATE is entered, when everything has been flushed to the + * internal pending output buffer. + * (7) At any time (7) + * + */ + + #endregion Deflater Documentation + + #region Public Constants + + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + public const int BEST_COMPRESSION = 9; + + /// + /// The worst but fastest compression level. + /// + public const int BEST_SPEED = 1; + + /// + /// The default compression level. + /// + public const int DEFAULT_COMPRESSION = -1; + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + public const int NO_COMPRESSION = 0; + + /// + /// The compression method. This is the only method supported so far. + /// There is no need to use this constant at all. + /// + public const int DEFLATED = 8; + + #endregion Public Constants + + #region Public Enum + + /// + /// Compression Level as an enum for safer use + /// + public enum CompressionLevel + { + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + BEST_COMPRESSION = Deflater.BEST_COMPRESSION, + + /// + /// The worst but fastest compression level. + /// + BEST_SPEED = Deflater.BEST_SPEED, + + /// + /// The default compression level. + /// + DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION, + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + NO_COMPRESSION = Deflater.NO_COMPRESSION, + + /// + /// The compression method. This is the only method supported so far. + /// There is no need to use this constant at all. + /// + DEFLATED = Deflater.DEFLATED + } + + #endregion Public Enum + + #region Local Constants + + private const int IS_SETDICT = 0x01; + private const int IS_FLUSHING = 0x04; + private const int IS_FINISHING = 0x08; + + private const int INIT_STATE = 0x00; + private const int SETDICT_STATE = 0x01; + + // private static int INIT_FINISHING_STATE = 0x08; + // private static int SETDICT_FINISHING_STATE = 0x09; + private const int BUSY_STATE = 0x10; + + private const int FLUSHING_STATE = 0x14; + private const int FINISHING_STATE = 0x1c; + private const int FINISHED_STATE = 0x1e; + private const int CLOSED_STATE = 0x7f; + + #endregion Local Constants + + #region Constructors + + /// + /// Creates a new deflater with default compression level. + /// + public Deflater() : this(DEFAULT_COMPRESSION, false) + { + } + + /// + /// Creates a new deflater with given compression level. + /// + /// + /// the compression level, a value between NO_COMPRESSION + /// and BEST_COMPRESSION, or DEFAULT_COMPRESSION. + /// + /// if lvl is out of range. + public Deflater(int level) : this(level, false) + { + } + + /// + /// Creates a new deflater with given compression level. + /// + /// + /// the compression level, a value between NO_COMPRESSION + /// and BEST_COMPRESSION. + /// + /// + /// true, if we should suppress the Zlib/RFC1950 header at the + /// beginning and the adler checksum at the end of the output. This is + /// useful for the GZIP/PKZIP formats. + /// + /// if lvl is out of range. + public Deflater(int level, bool noZlibHeaderOrFooter) + { + if (level == DEFAULT_COMPRESSION) + { + level = 6; + } + else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + pending = new DeflaterPending(); + engine = new DeflaterEngine(pending, noZlibHeaderOrFooter); + this.noZlibHeaderOrFooter = noZlibHeaderOrFooter; + SetStrategy(DeflateStrategy.Default); + SetLevel(level); + Reset(); + } + + #endregion Constructors + + /// + /// Resets the deflater. The deflater acts afterwards as if it was + /// just created with the same compression level and strategy as it + /// had before. + /// + public void Reset() + { + state = (noZlibHeaderOrFooter ? BUSY_STATE : INIT_STATE); + totalOut = 0; + pending.Reset(); + engine.Reset(); + } + + /// + /// Gets the current adler checksum of the data that was processed so far. + /// + public int Adler + { + get + { + return engine.Adler; + } + } + + /// + /// Gets the number of input bytes processed so far. + /// + public long TotalIn + { + get + { + return engine.TotalIn; + } + } + + /// + /// Gets the number of output bytes so far. + /// + public long TotalOut + { + get + { + return totalOut; + } + } + + /// + /// Flushes the current input block. Further calls to deflate() will + /// produce enough output to inflate everything in the current input + /// block. This is not part of Sun's JDK so I have made it package + /// private. It is used by DeflaterOutputStream to implement + /// flush(). + /// + public void Flush() + { + state |= IS_FLUSHING; + } + + /// + /// Finishes the deflater with the current input block. It is an error + /// to give more input after this method was called. This method must + /// be called to force all bytes to be flushed. + /// + public void Finish() + { + state |= (IS_FLUSHING | IS_FINISHING); + } + + /// + /// Returns true if the stream was finished and no more output bytes + /// are available. + /// + public bool IsFinished + { + get + { + return (state == FINISHED_STATE) && pending.IsFlushed; + } + } + + /// + /// Returns true, if the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method can also return true when the stream + /// was finished. + /// + public bool IsNeedingInput + { + get + { + return engine.NeedsInput(); + } + } + + /// + /// Sets the data which should be compressed next. This should be only + /// called when needsInput indicates that more input is needed. + /// If you call setInput when needsInput() returns false, the + /// previous input that is still pending will be thrown away. + /// The given byte array should not be changed, before needsInput() returns + /// true again. + /// This call is equivalent to setInput(input, 0, input.length). + /// + /// + /// the buffer containing the input data. + /// + /// + /// if the buffer was finished() or ended(). + /// + public void SetInput(byte[] input) + { + SetInput(input, 0, input.Length); + } + + /// + /// Sets the data which should be compressed next. This should be + /// only called when needsInput indicates that more input is needed. + /// The given byte array should not be changed, before needsInput() returns + /// true again. + /// + /// + /// the buffer containing the input data. + /// + /// + /// the start of the data. + /// + /// + /// the number of data bytes of input. + /// + /// + /// if the buffer was Finish()ed or if previous input is still pending. + /// + public void SetInput(byte[] input, int offset, int count) + { + if ((state & IS_FINISHING) != 0) + { + throw new InvalidOperationException("Finish() already called"); + } + engine.SetInput(input, offset, count); + } + + /// + /// Sets the compression level. There is no guarantee of the exact + /// position of the change, but if you call this when needsInput is + /// true the change of compression level will occur somewhere near + /// before the end of the so far given input. + /// + /// + /// the new compression level. + /// + public void SetLevel(int level) + { + if (level == DEFAULT_COMPRESSION) + { + level = 6; + } + else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + if (this.level != level) + { + this.level = level; + engine.SetLevel(level); + } + } + + /// + /// Get current compression level + /// + /// Returns the current compression level + public int GetLevel() + { + return level; + } + + /// + /// Sets the compression strategy. Strategy is one of + /// DEFAULT_STRATEGY, HUFFMAN_ONLY and FILTERED. For the exact + /// position where the strategy is changed, the same as for + /// SetLevel() applies. + /// + /// + /// The new compression strategy. + /// + public void SetStrategy(DeflateStrategy strategy) + { + engine.Strategy = strategy; + } + + /// + /// Deflates the current input block with to the given array. + /// + /// + /// The buffer where compressed data is stored + /// + /// + /// The number of compressed bytes added to the output, or 0 if either + /// IsNeedingInput() or IsFinished returns true or length is zero. + /// + public int Deflate(byte[] output) + { + return Deflate(output, 0, output.Length); + } + + /// + /// Deflates the current input block to the given array. + /// + /// + /// Buffer to store the compressed data. + /// + /// + /// Offset into the output array. + /// + /// + /// The maximum number of bytes that may be stored. + /// + /// + /// The number of compressed bytes added to the output, or 0 if either + /// needsInput() or finished() returns true or length is zero. + /// + /// + /// If Finish() was previously called. + /// + /// + /// If offset or length don't match the array length. + /// + public int Deflate(byte[] output, int offset, int length) + { + int origLength = length; + + if (state == CLOSED_STATE) + { + throw new InvalidOperationException("Deflater closed"); + } + + if (state < BUSY_STATE) + { + // output header + int header = (DEFLATED + + ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; + int level_flags = (level - 1) >> 1; + if (level_flags < 0 || level_flags > 3) + { + level_flags = 3; + } + header |= level_flags << 6; + if ((state & IS_SETDICT) != 0) + { + // Dictionary was set + header |= DeflaterConstants.PRESET_DICT; + } + header += 31 - (header % 31); + + pending.WriteShortMSB(header); + if ((state & IS_SETDICT) != 0) + { + int chksum = engine.Adler; + engine.ResetAdler(); + pending.WriteShortMSB(chksum >> 16); + pending.WriteShortMSB(chksum & 0xffff); + } + + state = BUSY_STATE | (state & (IS_FLUSHING | IS_FINISHING)); + } + + for (; ; ) + { + int count = pending.Flush(output, offset, length); + offset += count; + totalOut += count; + length -= count; + + if (length == 0 || state == FINISHED_STATE) + { + break; + } + + if (!engine.Deflate((state & IS_FLUSHING) != 0, (state & IS_FINISHING) != 0)) + { + switch (state) + { + case BUSY_STATE: + // We need more input now + return origLength - length; + + case FLUSHING_STATE: + if (level != NO_COMPRESSION) + { + /* We have to supply some lookahead. 8 bit lookahead + * is needed by the zlib inflater, and we must fill + * the next byte, so that all bits are flushed. + */ + int neededbits = 8 + ((-pending.BitCount) & 7); + while (neededbits > 0) + { + /* write a static tree block consisting solely of + * an EOF: + */ + pending.WriteBits(2, 10); + neededbits -= 10; + } + } + state = BUSY_STATE; + break; + + case FINISHING_STATE: + pending.AlignToByte(); + + // Compressed data is complete. Write footer information if required. + if (!noZlibHeaderOrFooter) + { + int adler = engine.Adler; + pending.WriteShortMSB(adler >> 16); + pending.WriteShortMSB(adler & 0xffff); + } + state = FINISHED_STATE; + break; + } + } + } + return origLength - length; + } + + /// + /// Sets the dictionary which should be used in the deflate process. + /// This call is equivalent to setDictionary(dict, 0, dict.Length). + /// + /// + /// the dictionary. + /// + /// + /// if SetInput () or Deflate () were already called or another dictionary was already set. + /// + public void SetDictionary(byte[] dictionary) + { + SetDictionary(dictionary, 0, dictionary.Length); + } + + /// + /// Sets the dictionary which should be used in the deflate process. + /// The dictionary is a byte array containing strings that are + /// likely to occur in the data which should be compressed. The + /// dictionary is not stored in the compressed output, only a + /// checksum. To decompress the output you need to supply the same + /// dictionary again. + /// + /// + /// The dictionary data + /// + /// + /// The index where dictionary information commences. + /// + /// + /// The number of bytes in the dictionary. + /// + /// + /// If SetInput () or Deflate() were already called or another dictionary was already set. + /// + public void SetDictionary(byte[] dictionary, int index, int count) + { + if (state != INIT_STATE) + { + throw new InvalidOperationException(); + } + + state = SETDICT_STATE; + engine.SetDictionary(dictionary, index, count); + } + + #region Instance Fields + + /// + /// Compression level. + /// + private int level; + + /// + /// If true no Zlib/RFC1950 headers or footers are generated + /// + private bool noZlibHeaderOrFooter; + + /// + /// The current state. + /// + private int state; + + /// + /// The total bytes of output written. + /// + private long totalOut; + + /// + /// The pending output. + /// + private DeflaterPending pending; + + /// + /// The deflater engine. + /// + private DeflaterEngine engine; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterConstants.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterConstants.cs new file mode 100644 index 0000000..35fd0a0 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterConstants.cs @@ -0,0 +1,146 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// This class contains constants used for deflation. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "kept for backwards compatibility")] + public static class DeflaterConstants + { + /// + /// Set to true to enable debugging + /// + public const bool DEBUGGING = false; + + /// + /// Written to Zip file to identify a stored block + /// + public const int STORED_BLOCK = 0; + + /// + /// Identifies static tree in Zip file + /// + public const int STATIC_TREES = 1; + + /// + /// Identifies dynamic tree in Zip file + /// + public const int DYN_TREES = 2; + + /// + /// Header flag indicating a preset dictionary for deflation + /// + public const int PRESET_DICT = 0x20; + + /// + /// Sets internal buffer sizes for Huffman encoding + /// + public const int DEFAULT_MEM_LEVEL = 8; + + /// + /// Internal compression engine constant + /// + public const int MAX_MATCH = 258; + + /// + /// Internal compression engine constant + /// + public const int MIN_MATCH = 3; + + /// + /// Internal compression engine constant + /// + public const int MAX_WBITS = 15; + + /// + /// Internal compression engine constant + /// + public const int WSIZE = 1 << MAX_WBITS; + + /// + /// Internal compression engine constant + /// + public const int WMASK = WSIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7; + + /// + /// Internal compression engine constant + /// + public const int HASH_SIZE = 1 << HASH_BITS; + + /// + /// Internal compression engine constant + /// + public const int HASH_MASK = HASH_SIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; + + /// + /// Internal compression engine constant + /// + public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1; + + /// + /// Internal compression engine constant + /// + public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD; + + /// + /// Internal compression engine constant + /// + public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8); + + /// + /// Internal compression engine constant + /// + public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5); + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_STORED = 0; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_FAST = 1; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_SLOW = 2; + + /// + /// Internal compression engine constant + /// + public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 }; + + /// + /// Internal compression engine constant + /// + public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 }; + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterEngine.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterEngine.cs new file mode 100644 index 0000000..9dc83e2 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterEngine.cs @@ -0,0 +1,946 @@ +using BSP_ICSharpCode.SharpZipLib.Checksum; +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// Strategies for deflater + /// + public enum DeflateStrategy + { + /// + /// The default strategy + /// + Default = 0, + + /// + /// This strategy will only allow longer string repetitions. It is + /// useful for random data with a small character set. + /// + Filtered = 1, + + /// + /// This strategy will not look for string repetitions at all. It + /// only encodes with Huffman trees (which means, that more common + /// characters get a smaller encoding. + /// + HuffmanOnly = 2 + } + + // DEFLATE ALGORITHM: + // + // The uncompressed stream is inserted into the window array. When + // the window array is full the first half is thrown away and the + // second half is copied to the beginning. + // + // The head array is a hash table. Three characters build a hash value + // and they the value points to the corresponding index in window of + // the last string with this hash. The prev array implements a + // linked list of matches with the same hash: prev[index & WMASK] points + // to the previous index with the same hash. + // + + /// + /// Low level compression engine for deflate algorithm which uses a 32K sliding window + /// with secondary compression from Huffman/Shannon-Fano codes. + /// + public class DeflaterEngine + { + #region Constants + + private const int TooFar = 4096; + + #endregion Constants + + #region Constructors + + /// + /// Construct instance with pending buffer + /// Adler calculation will be performed + /// + /// + /// Pending buffer to use + /// + public DeflaterEngine(DeflaterPending pending) + : this (pending, false) + { + } + + + + /// + /// Construct instance with pending buffer + /// + /// + /// Pending buffer to use + /// + /// + /// If no adler calculation should be performed + /// + public DeflaterEngine(DeflaterPending pending, bool noAdlerCalculation) + { + this.pending = pending; + huffman = new DeflaterHuffman(pending); + if (!noAdlerCalculation) + adler = new Adler32(); + + window = new byte[2 * DeflaterConstants.WSIZE]; + head = new short[DeflaterConstants.HASH_SIZE]; + prev = new short[DeflaterConstants.WSIZE]; + + // We start at index 1, to avoid an implementation deficiency, that + // we cannot build a repeat pattern at index 0. + blockStart = strstart = 1; + } + + #endregion Constructors + + /// + /// Deflate drives actual compression of data + /// + /// True to flush input buffers + /// Finish deflation with the current input. + /// Returns true if progress has been made. + public bool Deflate(bool flush, bool finish) + { + bool progress; + do + { + FillWindow(); + bool canFlush = flush && (inputOff == inputEnd); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.WriteLine("window: [" + blockStart + "," + strstart + "," + + lookahead + "], " + compressionFunction + "," + canFlush); + } +#endif + switch (compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + progress = DeflateStored(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_FAST: + progress = DeflateFast(canFlush, finish); + break; + + case DeflaterConstants.DEFLATE_SLOW: + progress = DeflateSlow(canFlush, finish); + break; + + default: + throw new InvalidOperationException("unknown compressionFunction"); + } + } while (pending.IsFlushed && progress); // repeat while we have no pending output and progress was made + return progress; + } + + /// + /// Sets input data to be deflated. Should only be called when NeedsInput() + /// returns true + /// + /// The buffer containing input data. + /// The offset of the first byte of data. + /// The number of bytes of data to use as input. + public void SetInput(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (inputOff < inputEnd) + { + throw new InvalidOperationException("Old input was not completely processed"); + } + + int end = offset + count; + + /* We want to throw an ArrayIndexOutOfBoundsException early. The + * check is very tricky: it also handles integer wrap around. + */ + if ((offset > end) || (end > buffer.Length)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + inputBuf = buffer; + inputOff = offset; + inputEnd = end; + } + + /// + /// Determines if more input is needed. + /// + /// Return true if input is needed via SetInput + public bool NeedsInput() + { + return (inputEnd == inputOff); + } + + /// + /// Set compression dictionary + /// + /// The buffer containing the dictionary data + /// The offset in the buffer for the first byte of data + /// The length of the dictionary data. + public void SetDictionary(byte[] buffer, int offset, int length) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (strstart != 1) ) + { + throw new InvalidOperationException("strstart not 1"); + } +#endif + adler?.Update(new ArraySegment(buffer, offset, length)); + if (length < DeflaterConstants.MIN_MATCH) + { + return; + } + + if (length > DeflaterConstants.MAX_DIST) + { + offset += length - DeflaterConstants.MAX_DIST; + length = DeflaterConstants.MAX_DIST; + } + + System.Array.Copy(buffer, offset, window, strstart, length); + + UpdateHash(); + --length; + while (--length > 0) + { + InsertString(); + strstart++; + } + strstart += 2; + blockStart = strstart; + } + + /// + /// Reset internal state + /// + public void Reset() + { + huffman.Reset(); + adler?.Reset(); + blockStart = strstart = 1; + lookahead = 0; + totalIn = 0; + prevAvailable = false; + matchLen = DeflaterConstants.MIN_MATCH - 1; + + for (int i = 0; i < DeflaterConstants.HASH_SIZE; i++) + { + head[i] = 0; + } + + for (int i = 0; i < DeflaterConstants.WSIZE; i++) + { + prev[i] = 0; + } + } + + /// + /// Reset Adler checksum + /// + public void ResetAdler() + { + adler?.Reset(); + } + + /// + /// Get current value of Adler checksum + /// + public int Adler + { + get + { + return (adler != null) ? unchecked((int)adler.Value) : 0; + } + } + + /// + /// Total data processed + /// + public long TotalIn + { + get + { + return totalIn; + } + } + + /// + /// Get/set the deflate strategy + /// + public DeflateStrategy Strategy + { + get + { + return strategy; + } + set + { + strategy = value; + } + } + + /// + /// Set the deflate level (0-9) + /// + /// The value to set the level to. + public void SetLevel(int level) + { + if ((level < 0) || (level > 9)) + { + throw new ArgumentOutOfRangeException(nameof(level)); + } + + goodLength = DeflaterConstants.GOOD_LENGTH[level]; + max_lazy = DeflaterConstants.MAX_LAZY[level]; + niceLength = DeflaterConstants.NICE_LENGTH[level]; + max_chain = DeflaterConstants.MAX_CHAIN[level]; + + if (DeflaterConstants.COMPR_FUNC[level] != compressionFunction) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.WriteLine("Change from " + compressionFunction + " to " + + DeflaterConstants.COMPR_FUNC[level]); + } +#endif + switch (compressionFunction) + { + case DeflaterConstants.DEFLATE_STORED: + if (strstart > blockStart) + { + huffman.FlushStoredBlock(window, blockStart, + strstart - blockStart, false); + blockStart = strstart; + } + UpdateHash(); + break; + + case DeflaterConstants.DEFLATE_FAST: + if (strstart > blockStart) + { + huffman.FlushBlock(window, blockStart, strstart - blockStart, + false); + blockStart = strstart; + } + break; + + case DeflaterConstants.DEFLATE_SLOW: + if (prevAvailable) + { + huffman.TallyLit(window[strstart - 1] & 0xff); + } + if (strstart > blockStart) + { + huffman.FlushBlock(window, blockStart, strstart - blockStart, false); + blockStart = strstart; + } + prevAvailable = false; + matchLen = DeflaterConstants.MIN_MATCH - 1; + break; + } + compressionFunction = DeflaterConstants.COMPR_FUNC[level]; + } + } + + /// + /// Fill the window + /// + public void FillWindow() + { + /* If the window is almost full and there is insufficient lookahead, + * move the upper half to the lower one to make room in the upper half. + */ + if (strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) + { + SlideWindow(); + } + + /* If there is not enough lookahead, but still some input left, + * read in the input + */ + if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && inputOff < inputEnd) + { + int more = 2 * DeflaterConstants.WSIZE - lookahead - strstart; + + if (more > inputEnd - inputOff) + { + more = inputEnd - inputOff; + } + + System.Array.Copy(inputBuf, inputOff, window, strstart + lookahead, more); + adler?.Update(new ArraySegment(inputBuf, inputOff, more)); + + inputOff += more; + totalIn += more; + lookahead += more; + } + + if (lookahead >= DeflaterConstants.MIN_MATCH) + { + UpdateHash(); + } + } + + private void UpdateHash() + { + /* + if (DEBUGGING) { + Console.WriteLine("updateHash: "+strstart); + } + */ + ins_h = (window[strstart] << DeflaterConstants.HASH_SHIFT) ^ window[strstart + 1]; + } + + /// + /// Inserts the current string in the head hash and returns the previous + /// value for this hash. + /// + /// The previous hash value + private int InsertString() + { + short match; + int hash = ((ins_h << DeflaterConstants.HASH_SHIFT) ^ window[strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + if (hash != (((window[strstart] << (2*HASH_SHIFT)) ^ + (window[strstart + 1] << HASH_SHIFT) ^ + (window[strstart + 2])) & HASH_MASK)) { + throw new SharpZipBaseException("hash inconsistent: " + hash + "/" + +window[strstart] + "," + +window[strstart + 1] + "," + +window[strstart + 2] + "," + HASH_SHIFT); + } + } +#endif + prev[strstart & DeflaterConstants.WMASK] = match = head[hash]; + head[hash] = unchecked((short)strstart); + ins_h = hash; + return match & 0xffff; + } + + private void SlideWindow() + { + Array.Copy(window, DeflaterConstants.WSIZE, window, 0, DeflaterConstants.WSIZE); + matchStart -= DeflaterConstants.WSIZE; + strstart -= DeflaterConstants.WSIZE; + blockStart -= DeflaterConstants.WSIZE; + + // Slide the hash table (could be avoided with 32 bit values + // at the expense of memory usage). + for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i) + { + int m = head[i] & 0xffff; + head[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + } + + // Slide the prev table. + for (int i = 0; i < DeflaterConstants.WSIZE; i++) + { + int m = prev[i] & 0xffff; + prev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); + } + } + + /// + /// Find the best (longest) string in the window matching the + /// string starting at strstart. + /// + /// Preconditions: + /// + /// strstart + DeflaterConstants.MAX_MATCH <= window.length. + /// + /// + /// True if a match greater than the minimum length is found + private bool FindLongestMatch(int curMatch) + { + int match; + int scan = strstart; + // scanMax is the highest position that we can look at + int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, lookahead) - 1; + int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); + + byte[] window = this.window; + short[] prev = this.prev; + int chainLength = this.max_chain; + int niceLength = Math.Min(this.niceLength, lookahead); + + matchLen = Math.Max(matchLen, DeflaterConstants.MIN_MATCH - 1); + + if (scan + matchLen > scanMax) return false; + + byte scan_end1 = window[scan + matchLen - 1]; + byte scan_end = window[scan + matchLen]; + + // Do not waste too much time if we already have a good match: + if (matchLen >= this.goodLength) chainLength >>= 2; + + do + { + match = curMatch; + scan = strstart; + + if (window[match + matchLen] != scan_end + || window[match + matchLen - 1] != scan_end1 + || window[match] != window[scan] + || window[++match] != window[++scan]) + { + continue; + } + + // scan is set to strstart+1 and the comparison passed, so + // scanMax - scan is the maximum number of bytes we can compare. + // below we compare 8 bytes at a time, so first we compare + // (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8 + + switch ((scanMax - scan) % 8) + { + case 1: + if (window[++scan] == window[++match]) break; + break; + + case 2: + if (window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 3: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 4: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 5: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 6: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + + case 7: + if (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]) break; + break; + } + + if (window[scan] == window[match]) + { + /* We check for insufficient lookahead only every 8th comparison; + * the 256th check will be made at strstart + 258 unless lookahead is + * exhausted first. + */ + do + { + if (scan == scanMax) + { + ++scan; // advance to first position not matched + ++match; + + break; + } + } + while (window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match] + && window[++scan] == window[++match]); + } + + if (scan - strstart > matchLen) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (ins_h == 0) ) + Console.Error.WriteLine("Found match: " + curMatch + "-" + (scan - strstart)); +#endif + + matchStart = curMatch; + matchLen = scan - strstart; + + if (matchLen >= niceLength) + break; + + scan_end1 = window[scan - 1]; + scan_end = window[scan]; + } + } while ((curMatch = (prev[curMatch & DeflaterConstants.WMASK] & 0xffff)) > limit && 0 != --chainLength); + + return matchLen >= DeflaterConstants.MIN_MATCH; + } + + private bool DeflateStored(bool flush, bool finish) + { + if (!flush && (lookahead == 0)) + { + return false; + } + + strstart += lookahead; + lookahead = 0; + + int storedLength = strstart - blockStart; + + if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full + (blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window + flush) + { + bool lastBlock = finish; + if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE) + { + storedLength = DeflaterConstants.MAX_BLOCK_SIZE; + lastBlock = false; + } + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + Console.WriteLine("storedBlock[" + storedLength + "," + lastBlock + "]"); + } +#endif + + huffman.FlushStoredBlock(window, blockStart, storedLength, lastBlock); + blockStart += storedLength; + return !(lastBlock || storedLength == 0); + } + return true; + } + + private bool DeflateFast(bool flush, bool finish) + { + if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + while (lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (lookahead == 0) + { + // We are flushing everything + huffman.FlushBlock(window, blockStart, strstart - blockStart, finish); + blockStart = strstart; + return false; + } + + if (strstart > 2 * DeflaterConstants.WSIZE - DeflaterConstants.MIN_LOOKAHEAD) + { + /* slide window, as FindLongestMatch needs this. + * This should only happen when flushing and the window + * is almost full. + */ + SlideWindow(); + } + + int hashHead; + if (lookahead >= DeflaterConstants.MIN_MATCH && + (hashHead = InsertString()) != 0 && + strategy != DeflateStrategy.HuffmanOnly && + strstart - hashHead <= DeflaterConstants.MAX_DIST && + FindLongestMatch(hashHead)) + { + // longestMatch sets matchStart and matchLen +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + for (int i = 0 ; i < matchLen; i++) { + if (window[strstart + i] != window[matchStart + i]) { + throw new SharpZipBaseException("Match failure"); + } + } + } +#endif + + bool full = huffman.TallyDist(strstart - matchStart, matchLen); + + lookahead -= matchLen; + if (matchLen <= max_lazy && lookahead >= DeflaterConstants.MIN_MATCH) + { + while (--matchLen > 0) + { + ++strstart; + InsertString(); + } + ++strstart; + } + else + { + strstart += matchLen; + if (lookahead >= DeflaterConstants.MIN_MATCH - 1) + { + UpdateHash(); + } + } + matchLen = DeflaterConstants.MIN_MATCH - 1; + if (!full) + { + continue; + } + } + else + { + // No match found + huffman.TallyLit(window[strstart] & 0xff); + ++strstart; + --lookahead; + } + + if (huffman.IsFull()) + { + bool lastBlock = finish && (lookahead == 0); + huffman.FlushBlock(window, blockStart, strstart - blockStart, lastBlock); + blockStart = strstart; + return !lastBlock; + } + } + return true; + } + + private bool DeflateSlow(bool flush, bool finish) + { + if (lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) + { + return false; + } + + while (lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) + { + if (lookahead == 0) + { + if (prevAvailable) + { + huffman.TallyLit(window[strstart - 1] & 0xff); + } + prevAvailable = false; + + // We are flushing everything +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && !flush) + { + throw new SharpZipBaseException("Not flushing, but no lookahead"); + } +#endif + huffman.FlushBlock(window, blockStart, strstart - blockStart, + finish); + blockStart = strstart; + return false; + } + + if (strstart >= 2 * DeflaterConstants.WSIZE - DeflaterConstants.MIN_LOOKAHEAD) + { + /* slide window, as FindLongestMatch needs this. + * This should only happen when flushing and the window + * is almost full. + */ + SlideWindow(); + } + + int prevMatch = matchStart; + int prevLen = matchLen; + if (lookahead >= DeflaterConstants.MIN_MATCH) + { + int hashHead = InsertString(); + + if (strategy != DeflateStrategy.HuffmanOnly && + hashHead != 0 && + strstart - hashHead <= DeflaterConstants.MAX_DIST && + FindLongestMatch(hashHead)) + { + // longestMatch sets matchStart and matchLen + + // Discard match if too small and too far away + if (matchLen <= 5 && (strategy == DeflateStrategy.Filtered || (matchLen == DeflaterConstants.MIN_MATCH && strstart - matchStart > TooFar))) + { + matchLen = DeflaterConstants.MIN_MATCH - 1; + } + } + } + + // previous match was better + if ((prevLen >= DeflaterConstants.MIN_MATCH) && (matchLen <= prevLen)) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + for (int i = 0 ; i < matchLen; i++) { + if (window[strstart-1+i] != window[prevMatch + i]) + throw new SharpZipBaseException(); + } + } +#endif + huffman.TallyDist(strstart - 1 - prevMatch, prevLen); + prevLen -= 2; + do + { + strstart++; + lookahead--; + if (lookahead >= DeflaterConstants.MIN_MATCH) + { + InsertString(); + } + } while (--prevLen > 0); + + strstart++; + lookahead--; + prevAvailable = false; + matchLen = DeflaterConstants.MIN_MATCH - 1; + } + else + { + if (prevAvailable) + { + huffman.TallyLit(window[strstart - 1] & 0xff); + } + prevAvailable = true; + strstart++; + lookahead--; + } + + if (huffman.IsFull()) + { + int len = strstart - blockStart; + if (prevAvailable) + { + len--; + } + bool lastBlock = (finish && (lookahead == 0) && !prevAvailable); + huffman.FlushBlock(window, blockStart, len, lastBlock); + blockStart += len; + return !lastBlock; + } + } + return true; + } + + #region Instance Fields + + // Hash index of string to be inserted + private int ins_h; + + /// + /// Hashtable, hashing three characters to an index for window, so + /// that window[index]..window[index+2] have this hash code. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xffff. + /// + private short[] head; + + /// + /// prev[index & WMASK] points to the previous index that has the + /// same hash code as the string starting at index. This way + /// entries with the same hash code are in a linked list. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xffff. + /// + private short[] prev; + + private int matchStart; + + // Length of best match + private int matchLen; + + // Set if previous match exists + private bool prevAvailable; + + private int blockStart; + + /// + /// Points to the current character in the window. + /// + private int strstart; + + /// + /// lookahead is the number of characters starting at strstart in + /// window that are valid. + /// So window[strstart] until window[strstart+lookahead-1] are valid + /// characters. + /// + private int lookahead; + + /// + /// This array contains the part of the uncompressed stream that + /// is of relevance. The current character is indexed by strstart. + /// + private byte[] window; + + private DeflateStrategy strategy; + private int max_chain, max_lazy, niceLength, goodLength; + + /// + /// The current compression function. + /// + private int compressionFunction; + + /// + /// The input data for compression. + /// + private byte[] inputBuf; + + /// + /// The total bytes of input read. + /// + private long totalIn; + + /// + /// The offset into inputBuf, where input data starts. + /// + private int inputOff; + + /// + /// The end offset of the input data. + /// + private int inputEnd; + + private DeflaterPending pending; + private DeflaterHuffman huffman; + + /// + /// The adler checksum + /// + private Adler32 adler; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterHuffman.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterHuffman.cs new file mode 100644 index 0000000..79fed56 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterHuffman.cs @@ -0,0 +1,959 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// This is the DeflaterHuffman class. + /// + /// This class is not thread safe. This is inherent in the API, due + /// to the split of Deflate and SetInput. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class DeflaterHuffman + { + private const int BUFSIZE = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); + private const int LITERAL_NUM = 286; + + // Number of distance codes + private const int DIST_NUM = 30; + + // Number of codes used to transfer bit lengths + private const int BITLEN_NUM = 19; + + // repeat previous bit length 3-6 times (2 bits of repeat count) + private const int REP_3_6 = 16; + + // repeat a zero length 3-10 times (3 bits of repeat count) + private const int REP_3_10 = 17; + + // repeat a zero length 11-138 times (7 bits of repeat count) + private const int REP_11_138 = 18; + + private const int EOF_SYMBOL = 256; + + // The lengths of the bit length codes are sent in order of decreasing + // probability, to avoid transmitting the lengths for unused bit length codes. + private static readonly int[] BL_ORDER = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + private static readonly byte[] bit4Reverse = { + 0, + 8, + 4, + 12, + 2, + 10, + 6, + 14, + 1, + 9, + 5, + 13, + 3, + 11, + 7, + 15 + }; + + private static short[] staticLCodes; + private static byte[] staticLLength; + private static short[] staticDCodes; + private static byte[] staticDLength; + + private class Tree + { + #region Instance Fields + + public short[] freqs; + + public byte[] length; + + public int minNumCodes; + + public int numCodes; + + private short[] codes; + private readonly int[] bl_counts; + private readonly int maxLength; + private DeflaterHuffman dh; + + #endregion Instance Fields + + #region Constructors + + public Tree(DeflaterHuffman dh, int elems, int minCodes, int maxLength) + { + this.dh = dh; + this.minNumCodes = minCodes; + this.maxLength = maxLength; + freqs = new short[elems]; + bl_counts = new int[maxLength]; + } + + #endregion Constructors + + /// + /// Resets the internal state of the tree + /// + public void Reset() + { + for (int i = 0; i < freqs.Length; i++) + { + freqs[i] = 0; + } + codes = null; + length = null; + } + + public void WriteSymbol(int code) + { + // if (DeflaterConstants.DEBUGGING) { + // freqs[code]--; + // // Console.Write("writeSymbol("+freqs.length+","+code+"): "); + // } + dh.pending.WriteBits(codes[code] & 0xffff, length[code]); + } + + /// + /// Check that all frequencies are zero + /// + /// + /// At least one frequency is non-zero + /// + public void CheckEmpty() + { + bool empty = true; + for (int i = 0; i < freqs.Length; i++) + { + empty &= freqs[i] == 0; + } + + if (!empty) + { + throw new SharpZipBaseException("!Empty"); + } + } + + /// + /// Set static codes and length + /// + /// new codes + /// length for new codes + public void SetStaticCodes(short[] staticCodes, byte[] staticLengths) + { + codes = staticCodes; + length = staticLengths; + } + + /// + /// Build dynamic codes and lengths + /// + public void BuildCodes() + { + int numSymbols = freqs.Length; + int[] nextCode = new int[maxLength]; + int code = 0; + + codes = new short[freqs.Length]; + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("buildCodes: "+freqs.Length); + // } + + for (int bits = 0; bits < maxLength; bits++) + { + nextCode[bits] = code; + code += bl_counts[bits] << (15 - bits); + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("bits: " + ( bits + 1) + " count: " + bl_counts[bits] + // +" nextCode: "+code); + // } + } + +#if DebugDeflation + if ( DeflaterConstants.DEBUGGING && (code != 65536) ) + { + throw new SharpZipBaseException("Inconsistent bl_counts!"); + } +#endif + for (int i = 0; i < numCodes; i++) + { + int bits = length[i]; + if (bits > 0) + { + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("codes["+i+"] = rev(" + nextCode[bits-1]+"), + // +bits); + // } + + codes[i] = BitReverse(nextCode[bits - 1]); + nextCode[bits - 1] += 1 << (16 - bits); + } + } + } + + public void BuildTree() + { + int numSymbols = freqs.Length; + + /* heap is a priority queue, sorted by frequency, least frequent + * nodes first. The heap is a binary tree, with the property, that + * the parent node is smaller than both child nodes. This assures + * that the smallest node is the first parent. + * + * The binary tree is encoded in an array: 0 is root node and + * the nodes 2*n+1, 2*n+2 are the child nodes of node n. + */ + int[] heap = new int[numSymbols]; + int heapLen = 0; + int maxCode = 0; + for (int n = 0; n < numSymbols; n++) + { + int freq = freqs[n]; + if (freq != 0) + { + // Insert n into heap + int pos = heapLen++; + int ppos; + while (pos > 0 && freqs[heap[ppos = (pos - 1) / 2]] > freq) + { + heap[pos] = heap[ppos]; + pos = ppos; + } + heap[pos] = n; + + maxCode = n; + } + } + + /* We could encode a single literal with 0 bits but then we + * don't see the literals. Therefore we force at least two + * literals to avoid this case. We don't care about order in + * this case, both literals get a 1 bit code. + */ + while (heapLen < 2) + { + int node = maxCode < 2 ? ++maxCode : 0; + heap[heapLen++] = node; + } + + numCodes = Math.Max(maxCode + 1, minNumCodes); + + int numLeafs = heapLen; + int[] childs = new int[4 * heapLen - 2]; + int[] values = new int[2 * heapLen - 1]; + int numNodes = numLeafs; + for (int i = 0; i < heapLen; i++) + { + int node = heap[i]; + childs[2 * i] = node; + childs[2 * i + 1] = -1; + values[i] = freqs[node] << 8; + heap[i] = i; + } + + /* Construct the Huffman tree by repeatedly combining the least two + * frequent nodes. + */ + do + { + int first = heap[0]; + int last = heap[--heapLen]; + + // Propagate the hole to the leafs of the heap + int ppos = 0; + int path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen && values[heap[path]] > values[heap[path + 1]]) + { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = path * 2 + 1; + } + + /* Now propagate the last element down along path. Normally + * it shouldn't go too deep. + */ + int lastVal = values[last]; + while ((path = ppos) > 0 && values[heap[ppos = (path - 1) / 2]] > lastVal) + { + heap[path] = heap[ppos]; + } + heap[path] = last; + + int second = heap[0]; + + // Create a new node father of first and second + last = numNodes++; + childs[2 * last] = first; + childs[2 * last + 1] = second; + int mindepth = Math.Min(values[first] & 0xff, values[second] & 0xff); + values[last] = lastVal = values[first] + values[second] - mindepth + 1; + + // Again, propagate the hole to the leafs + ppos = 0; + path = 1; + + while (path < heapLen) + { + if (path + 1 < heapLen && values[heap[path]] > values[heap[path + 1]]) + { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = ppos * 2 + 1; + } + + // Now propagate the new element down along path + while ((path = ppos) > 0 && values[heap[ppos = (path - 1) / 2]] > lastVal) + { + heap[path] = heap[ppos]; + } + heap[path] = last; + } while (heapLen > 1); + + if (heap[0] != childs.Length / 2 - 1) + { + throw new SharpZipBaseException("Heap invariant violated"); + } + + BuildLength(childs); + } + + /// + /// Get encoded length + /// + /// Encoded length, the sum of frequencies * lengths + public int GetEncodedLength() + { + int len = 0; + for (int i = 0; i < freqs.Length; i++) + { + len += freqs[i] * length[i]; + } + return len; + } + + /// + /// Scan a literal or distance tree to determine the frequencies of the codes + /// in the bit length tree. + /// + public void CalcBLFreq(Tree blTree) + { + int max_count; /* max repeat count */ + int min_count; /* min repeat count */ + int count; /* repeat count of the current code */ + int curlen = -1; /* length of current code */ + + int i = 0; + while (i < numCodes) + { + count = 1; + int nextlen = length[i]; + if (nextlen == 0) + { + max_count = 138; + min_count = 3; + } + else + { + max_count = 6; + min_count = 3; + if (curlen != nextlen) + { + blTree.freqs[nextlen]++; + count = 0; + } + } + curlen = nextlen; + i++; + + while (i < numCodes && curlen == length[i]) + { + i++; + if (++count >= max_count) + { + break; + } + } + + if (count < min_count) + { + blTree.freqs[curlen] += (short)count; + } + else if (curlen != 0) + { + blTree.freqs[REP_3_6]++; + } + else if (count <= 10) + { + blTree.freqs[REP_3_10]++; + } + else + { + blTree.freqs[REP_11_138]++; + } + } + } + + /// + /// Write tree values + /// + /// Tree to write + public void WriteTree(Tree blTree) + { + int max_count; // max repeat count + int min_count; // min repeat count + int count; // repeat count of the current code + int curlen = -1; // length of current code + + int i = 0; + while (i < numCodes) + { + count = 1; + int nextlen = length[i]; + if (nextlen == 0) + { + max_count = 138; + min_count = 3; + } + else + { + max_count = 6; + min_count = 3; + if (curlen != nextlen) + { + blTree.WriteSymbol(nextlen); + count = 0; + } + } + curlen = nextlen; + i++; + + while (i < numCodes && curlen == length[i]) + { + i++; + if (++count >= max_count) + { + break; + } + } + + if (count < min_count) + { + while (count-- > 0) + { + blTree.WriteSymbol(curlen); + } + } + else if (curlen != 0) + { + blTree.WriteSymbol(REP_3_6); + dh.pending.WriteBits(count - 3, 2); + } + else if (count <= 10) + { + blTree.WriteSymbol(REP_3_10); + dh.pending.WriteBits(count - 3, 3); + } + else + { + blTree.WriteSymbol(REP_11_138); + dh.pending.WriteBits(count - 11, 7); + } + } + } + + private void BuildLength(int[] childs) + { + this.length = new byte[freqs.Length]; + int numNodes = childs.Length / 2; + int numLeafs = (numNodes + 1) / 2; + int overflow = 0; + + for (int i = 0; i < maxLength; i++) + { + bl_counts[i] = 0; + } + + // First calculate optimal bit lengths + int[] lengths = new int[numNodes]; + lengths[numNodes - 1] = 0; + + for (int i = numNodes - 1; i >= 0; i--) + { + if (childs[2 * i + 1] != -1) + { + int bitLength = lengths[i] + 1; + if (bitLength > maxLength) + { + bitLength = maxLength; + overflow++; + } + lengths[childs[2 * i]] = lengths[childs[2 * i + 1]] = bitLength; + } + else + { + // A leaf node + int bitLength = lengths[i]; + bl_counts[bitLength - 1]++; + this.length[childs[2 * i]] = (byte)lengths[i]; + } + } + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Tree "+freqs.Length+" lengths:"); + // for (int i=0; i < numLeafs; i++) { + // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] + // + " len: "+length[childs[2*i]]); + // } + // } + + if (overflow == 0) + { + return; + } + + int incrBitLen = maxLength - 1; + do + { + // Find the first bit length which could increase: + while (bl_counts[--incrBitLen] == 0) + { + } + + // Move this node one down and remove a corresponding + // number of overflow nodes. + do + { + bl_counts[incrBitLen]--; + bl_counts[++incrBitLen]++; + overflow -= 1 << (maxLength - 1 - incrBitLen); + } while (overflow > 0 && incrBitLen < maxLength - 1); + } while (overflow > 0); + + /* We may have overshot above. Move some nodes from maxLength to + * maxLength-1 in that case. + */ + bl_counts[maxLength - 1] += overflow; + bl_counts[maxLength - 2] -= overflow; + + /* Now recompute all bit lengths, scanning in increasing + * frequency. It is simpler to reconstruct all lengths instead of + * fixing only the wrong ones. This idea is taken from 'ar' + * written by Haruhiko Okumura. + * + * The nodes were inserted with decreasing frequency into the childs + * array. + */ + int nodePtr = 2 * numLeafs; + for (int bits = maxLength; bits != 0; bits--) + { + int n = bl_counts[bits - 1]; + while (n > 0) + { + int childPtr = 2 * childs[nodePtr++]; + if (childs[childPtr + 1] == -1) + { + // We found another leaf + length[childs[childPtr]] = (byte)bits; + n--; + } + } + } + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("*** After overflow elimination. ***"); + // for (int i=0; i < numLeafs; i++) { + // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] + // + " len: "+length[childs[2*i]]); + // } + // } + } + } + + #region Instance Fields + + /// + /// Pending buffer to use + /// + public DeflaterPending pending; + + private Tree literalTree; + private Tree distTree; + private Tree blTree; + + // Buffer for distances + private short[] d_buf; + + private byte[] l_buf; + private int last_lit; + private int extra_bits; + + #endregion Instance Fields + + static DeflaterHuffman() + { + // See RFC 1951 3.2.6 + // Literal codes + staticLCodes = new short[LITERAL_NUM]; + staticLLength = new byte[LITERAL_NUM]; + + int i = 0; + while (i < 144) + { + staticLCodes[i] = BitReverse((0x030 + i) << 8); + staticLLength[i++] = 8; + } + + while (i < 256) + { + staticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); + staticLLength[i++] = 9; + } + + while (i < 280) + { + staticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); + staticLLength[i++] = 7; + } + + while (i < LITERAL_NUM) + { + staticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); + staticLLength[i++] = 8; + } + + // Distance codes + staticDCodes = new short[DIST_NUM]; + staticDLength = new byte[DIST_NUM]; + for (i = 0; i < DIST_NUM; i++) + { + staticDCodes[i] = BitReverse(i << 11); + staticDLength[i] = 5; + } + } + + /// + /// Construct instance with pending buffer + /// + /// Pending buffer to use + public DeflaterHuffman(DeflaterPending pending) + { + this.pending = pending; + + literalTree = new Tree(this, LITERAL_NUM, 257, 15); + distTree = new Tree(this, DIST_NUM, 1, 15); + blTree = new Tree(this, BITLEN_NUM, 4, 7); + + d_buf = new short[BUFSIZE]; + l_buf = new byte[BUFSIZE]; + } + + /// + /// Reset internal state + /// + public void Reset() + { + last_lit = 0; + extra_bits = 0; + literalTree.Reset(); + distTree.Reset(); + blTree.Reset(); + } + + /// + /// Write all trees to pending buffer + /// + /// The number/rank of treecodes to send. + public void SendAllTrees(int blTreeCodes) + { + blTree.BuildCodes(); + literalTree.BuildCodes(); + distTree.BuildCodes(); + pending.WriteBits(literalTree.numCodes - 257, 5); + pending.WriteBits(distTree.numCodes - 1, 5); + pending.WriteBits(blTreeCodes - 4, 4); + for (int rank = 0; rank < blTreeCodes; rank++) + { + pending.WriteBits(blTree.length[BL_ORDER[rank]], 3); + } + literalTree.WriteTree(blTree); + distTree.WriteTree(blTree); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + blTree.CheckEmpty(); + } +#endif + } + + /// + /// Compress current buffer writing data to pending buffer + /// + public void CompressBlock() + { + for (int i = 0; i < last_lit; i++) + { + int litlen = l_buf[i] & 0xff; + int dist = d_buf[i]; + if (dist-- != 0) + { + // if (DeflaterConstants.DEBUGGING) { + // Console.Write("["+(dist+1)+","+(litlen+3)+"]: "); + // } + + int lc = Lcode(litlen); + literalTree.WriteSymbol(lc); + + int bits = (lc - 261) / 4; + if (bits > 0 && bits <= 5) + { + pending.WriteBits(litlen & ((1 << bits) - 1), bits); + } + + int dc = Dcode(dist); + distTree.WriteSymbol(dc); + + bits = dc / 2 - 1; + if (bits > 0) + { + pending.WriteBits(dist & ((1 << bits) - 1), bits); + } + } + else + { + // if (DeflaterConstants.DEBUGGING) { + // if (litlen > 32 && litlen < 127) { + // Console.Write("("+(char)litlen+"): "); + // } else { + // Console.Write("{"+litlen+"}: "); + // } + // } + literalTree.WriteSymbol(litlen); + } + } + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.Write("EOF: "); + } +#endif + literalTree.WriteSymbol(EOF_SYMBOL); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + literalTree.CheckEmpty(); + distTree.CheckEmpty(); + } +#endif + } + + /// + /// Flush block to output with no compression + /// + /// Data to write + /// Index of first byte to write + /// Count of bytes to write + /// True if this is the last block + public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { +#if DebugDeflation + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Flushing stored block "+ storedLength); + // } +#endif + pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); + pending.AlignToByte(); + pending.WriteShort(storedLength); + pending.WriteShort(~storedLength); + pending.WriteBlock(stored, storedOffset, storedLength); + Reset(); + } + + /// + /// Flush block to output with compression + /// + /// Data to flush + /// Index of first byte to flush + /// Count of bytes to flush + /// True if this is the last block + public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { + literalTree.freqs[EOF_SYMBOL]++; + + // Build trees + literalTree.BuildTree(); + distTree.BuildTree(); + + // Calculate bitlen frequency + literalTree.CalcBLFreq(blTree); + distTree.CalcBLFreq(blTree); + + // Build bitlen tree + blTree.BuildTree(); + + int blTreeCodes = 4; + for (int i = 18; i > blTreeCodes; i--) + { + if (blTree.length[BL_ORDER[i]] > 0) + { + blTreeCodes = i + 1; + } + } + int opt_len = 14 + blTreeCodes * 3 + blTree.GetEncodedLength() + + literalTree.GetEncodedLength() + distTree.GetEncodedLength() + + extra_bits; + + int static_len = extra_bits; + for (int i = 0; i < LITERAL_NUM; i++) + { + static_len += literalTree.freqs[i] * staticLLength[i]; + } + for (int i = 0; i < DIST_NUM; i++) + { + static_len += distTree.freqs[i] * staticDLength[i]; + } + if (opt_len >= static_len) + { + // Force static trees + opt_len = static_len; + } + + if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) + { + // Store Block + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Storing, since " + storedLength + " < " + opt_len + // + " <= " + static_len); + // } + FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); + } + else if (opt_len == static_len) + { + // Encode with static tree + pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); + literalTree.SetStaticCodes(staticLCodes, staticLLength); + distTree.SetStaticCodes(staticDCodes, staticDLength); + CompressBlock(); + Reset(); + } + else + { + // Encode with dynamic tree + pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); + SendAllTrees(blTreeCodes); + CompressBlock(); + Reset(); + } + } + + /// + /// Get value indicating if internal buffer is full + /// + /// true if buffer is full + public bool IsFull() + { + return last_lit >= BUFSIZE; + } + + /// + /// Add literal to buffer + /// + /// Literal value to add to buffer. + /// Value indicating internal buffer is full + public bool TallyLit(int literal) + { + // if (DeflaterConstants.DEBUGGING) { + // if (lit > 32 && lit < 127) { + // //Console.WriteLine("("+(char)lit+")"); + // } else { + // //Console.WriteLine("{"+lit+"}"); + // } + // } + d_buf[last_lit] = 0; + l_buf[last_lit++] = (byte)literal; + literalTree.freqs[literal]++; + return IsFull(); + } + + /// + /// Add distance code and length to literal and distance trees + /// + /// Distance code + /// Length + /// Value indicating if internal buffer is full + public bool TallyDist(int distance, int length) + { + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("[" + distance + "," + length + "]"); + // } + + d_buf[last_lit] = (short)distance; + l_buf[last_lit++] = (byte)(length - 3); + + int lc = Lcode(length - 3); + literalTree.freqs[lc]++; + if (lc >= 265 && lc < 285) + { + extra_bits += (lc - 261) / 4; + } + + int dc = Dcode(distance - 1); + distTree.freqs[dc]++; + if (dc >= 4) + { + extra_bits += dc / 2 - 1; + } + return IsFull(); + } + + /// + /// Reverse the bits of a 16 bit value. + /// + /// Value to reverse bits + /// Value with bits reversed + public static short BitReverse(int toReverse) + { + return (short)(bit4Reverse[toReverse & 0xF] << 12 | + bit4Reverse[(toReverse >> 4) & 0xF] << 8 | + bit4Reverse[(toReverse >> 8) & 0xF] << 4 | + bit4Reverse[toReverse >> 12]); + } + + private static int Lcode(int length) + { + if (length == 255) + { + return 285; + } + + int code = 257; + while (length >= 8) + { + code += 4; + length >>= 1; + } + return code + length; + } + + private static int Dcode(int distance) + { + int code = 0; + while (distance >= 4) + { + code += 2; + distance >>= 1; + } + return code + distance; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterPending.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterPending.cs new file mode 100644 index 0000000..d812006 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/DeflaterPending.cs @@ -0,0 +1,17 @@ +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// This class stores the pending output of the Deflater. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class DeflaterPending : PendingBuffer + { + /// + /// Construct instance with default buffer size + /// + public DeflaterPending() : base(DeflaterConstants.PENDING_BUF_SIZE) + { + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs new file mode 100644 index 0000000..ba7e0f3 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Inflater.cs @@ -0,0 +1,887 @@ +using BSP_ICSharpCode.SharpZipLib.Checksum; +using BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// Inflater is used to decompress data that has been compressed according + /// to the "deflate" standard described in rfc1951. + /// + /// By default Zlib (rfc1950) headers and footers are expected in the input. + /// You can use constructor public Inflater(bool noHeader) passing true + /// if there is no Zlib header information + /// + /// The usage is as following. First you have to set some input with + /// SetInput(), then Inflate() it. If inflate doesn't + /// inflate any bytes there may be three reasons: + ///
    + ///
  • IsNeedingInput() returns true because the input buffer is empty. + /// You have to provide more input with SetInput(). + /// NOTE: IsNeedingInput() also returns true when, the stream is finished. + ///
  • + ///
  • IsNeedingDictionary() returns true, you have to provide a preset + /// dictionary with SetDictionary().
  • + ///
  • IsFinished returns true, the inflater has finished.
  • + ///
+ /// Once the first output byte is produced, a dictionary will not be + /// needed at a later stage. + /// + /// author of the original java version : John Leuner, Jochen Hoenicke + ///
+ public class Inflater + { + #region Constants/Readonly + + /// + /// Copy lengths for literal codes 257..285 + /// + private static readonly int[] CPLENS = { + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 + }; + + /// + /// Extra bits for literal codes 257..285 + /// + private static readonly int[] CPLEXT = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 + }; + + /// + /// Copy offsets for distance codes 0..29 + /// + private static readonly int[] CPDIST = { + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577 + }; + + /// + /// Extra bits for distance codes + /// + private static readonly int[] CPDEXT = { + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13 + }; + + /// + /// These are the possible states for an inflater + /// + private const int DECODE_HEADER = 0; + + private const int DECODE_DICT = 1; + private const int DECODE_BLOCKS = 2; + private const int DECODE_STORED_LEN1 = 3; + private const int DECODE_STORED_LEN2 = 4; + private const int DECODE_STORED = 5; + private const int DECODE_DYN_HEADER = 6; + private const int DECODE_HUFFMAN = 7; + private const int DECODE_HUFFMAN_LENBITS = 8; + private const int DECODE_HUFFMAN_DIST = 9; + private const int DECODE_HUFFMAN_DISTBITS = 10; + private const int DECODE_CHKSUM = 11; + private const int FINISHED = 12; + + #endregion Constants/Readonly + + #region Instance Fields + + /// + /// This variable contains the current state. + /// + private int mode; + + /// + /// The adler checksum of the dictionary or of the decompressed + /// stream, as it is written in the header resp. footer of the + /// compressed stream. + /// Only valid if mode is DECODE_DICT or DECODE_CHKSUM. + /// + private int readAdler; + + /// + /// The number of bits needed to complete the current state. This + /// is valid, if mode is DECODE_DICT, DECODE_CHKSUM, + /// DECODE_HUFFMAN_LENBITS or DECODE_HUFFMAN_DISTBITS. + /// + private int neededBits; + + private int repLength; + private int repDist; + private int uncomprLen; + + /// + /// True, if the last block flag was set in the last block of the + /// inflated stream. This means that the stream ends after the + /// current block. + /// + private bool isLastBlock; + + /// + /// The total number of inflated bytes. + /// + private long totalOut; + + /// + /// The total number of bytes set with setInput(). This is not the + /// value returned by the TotalIn property, since this also includes the + /// unprocessed input. + /// + private long totalIn; + + /// + /// This variable stores the noHeader flag that was given to the constructor. + /// True means, that the inflated stream doesn't contain a Zlib header or + /// footer. + /// + private bool noHeader; + + private readonly StreamManipulator input; + private OutputWindow outputWindow; + private InflaterDynHeader dynHeader; + private InflaterHuffmanTree litlenTree, distTree; + private Adler32 adler; + + #endregion Instance Fields + + #region Constructors + + /// + /// Creates a new inflater or RFC1951 decompressor + /// RFC1950/Zlib headers and footers will be expected in the input data + /// + public Inflater() : this(false) + { + } + + /// + /// Creates a new inflater. + /// + /// + /// True if no RFC1950/Zlib header and footer fields are expected in the input data + /// + /// This is used for GZIPed/Zipped input. + /// + /// For compatibility with + /// Sun JDK you should provide one byte of input more than needed in + /// this case. + /// + public Inflater(bool noHeader) + { + this.noHeader = noHeader; + if (!noHeader) + this.adler = new Adler32(); + input = new StreamManipulator(); + outputWindow = new OutputWindow(); + mode = noHeader ? DECODE_BLOCKS : DECODE_HEADER; + } + + #endregion Constructors + + /// + /// Resets the inflater so that a new stream can be decompressed. All + /// pending input and output will be discarded. + /// + public void Reset() + { + mode = noHeader ? DECODE_BLOCKS : DECODE_HEADER; + totalIn = 0; + totalOut = 0; + input.Reset(); + outputWindow.Reset(); + dynHeader = null; + litlenTree = null; + distTree = null; + isLastBlock = false; + adler?.Reset(); + } + + /// + /// Decodes a zlib/RFC1950 header. + /// + /// + /// False if more input is needed. + /// + /// + /// The header is invalid. + /// + private bool DecodeHeader() + { + int header = input.PeekBits(16); + if (header < 0) + { + return false; + } + input.DropBits(16); + + // The header is written in "wrong" byte order + header = ((header << 8) | (header >> 8)) & 0xffff; + if (header % 31 != 0) + { + throw new SharpZipBaseException("Header checksum illegal"); + } + + if ((header & 0x0f00) != (Deflater.DEFLATED << 8)) + { + throw new SharpZipBaseException("Compression Method unknown"); + } + + /* Maximum size of the backwards window in bits. + * We currently ignore this, but we could use it to make the + * inflater window more space efficient. On the other hand the + * full window (15 bits) is needed most times, anyway. + int max_wbits = ((header & 0x7000) >> 12) + 8; + */ + + if ((header & 0x0020) == 0) + { // Dictionary flag? + mode = DECODE_BLOCKS; + } + else + { + mode = DECODE_DICT; + neededBits = 32; + } + return true; + } + + /// + /// Decodes the dictionary checksum after the deflate header. + /// + /// + /// False if more input is needed. + /// + private bool DecodeDict() + { + while (neededBits > 0) + { + int dictByte = input.PeekBits(8); + if (dictByte < 0) + { + return false; + } + input.DropBits(8); + readAdler = (readAdler << 8) | dictByte; + neededBits -= 8; + } + return false; + } + + /// + /// Decodes the huffman encoded symbols in the input stream. + /// + /// + /// false if more input is needed, true if output window is + /// full or the current block ends. + /// + /// + /// if deflated stream is invalid. + /// + private bool DecodeHuffman() + { + int free = outputWindow.GetFreeSpace(); + while (free >= 258) + { + int symbol; + switch (mode) + { + case DECODE_HUFFMAN: + // This is the inner loop so it is optimized a bit + while (((symbol = litlenTree.GetSymbol(input)) & ~0xff) == 0) + { + outputWindow.Write(symbol); + if (--free < 258) + { + return true; + } + } + + if (symbol < 257) + { + if (symbol < 0) + { + return false; + } + else + { + // symbol == 256: end of block + distTree = null; + litlenTree = null; + mode = DECODE_BLOCKS; + return true; + } + } + + try + { + repLength = CPLENS[symbol - 257]; + neededBits = CPLEXT[symbol - 257]; + } + catch (Exception) + { + throw new SharpZipBaseException("Illegal rep length code"); + } + goto case DECODE_HUFFMAN_LENBITS; // fall through + + case DECODE_HUFFMAN_LENBITS: + if (neededBits > 0) + { + mode = DECODE_HUFFMAN_LENBITS; + int i = input.PeekBits(neededBits); + if (i < 0) + { + return false; + } + input.DropBits(neededBits); + repLength += i; + } + mode = DECODE_HUFFMAN_DIST; + goto case DECODE_HUFFMAN_DIST; // fall through + + case DECODE_HUFFMAN_DIST: + symbol = distTree.GetSymbol(input); + if (symbol < 0) + { + return false; + } + + try + { + repDist = CPDIST[symbol]; + neededBits = CPDEXT[symbol]; + } + catch (Exception) + { + throw new SharpZipBaseException("Illegal rep dist code"); + } + + goto case DECODE_HUFFMAN_DISTBITS; // fall through + + case DECODE_HUFFMAN_DISTBITS: + if (neededBits > 0) + { + mode = DECODE_HUFFMAN_DISTBITS; + int i = input.PeekBits(neededBits); + if (i < 0) + { + return false; + } + input.DropBits(neededBits); + repDist += i; + } + + outputWindow.Repeat(repLength, repDist); + free -= repLength; + mode = DECODE_HUFFMAN; + break; + + default: + throw new SharpZipBaseException("Inflater unknown mode"); + } + } + return true; + } + + /// + /// Decodes the adler checksum after the deflate stream. + /// + /// + /// false if more input is needed. + /// + /// + /// If checksum doesn't match. + /// + private bool DecodeChksum() + { + while (neededBits > 0) + { + int chkByte = input.PeekBits(8); + if (chkByte < 0) + { + return false; + } + input.DropBits(8); + readAdler = (readAdler << 8) | chkByte; + neededBits -= 8; + } + + if ((int)adler?.Value != readAdler) + { + throw new SharpZipBaseException("Adler chksum doesn't match: " + (int)adler?.Value + " vs. " + readAdler); + } + + mode = FINISHED; + return false; + } + + /// + /// Decodes the deflated stream. + /// + /// + /// false if more input is needed, or if finished. + /// + /// + /// if deflated stream is invalid. + /// + private bool Decode() + { + switch (mode) + { + case DECODE_HEADER: + return DecodeHeader(); + + case DECODE_DICT: + return DecodeDict(); + + case DECODE_CHKSUM: + return DecodeChksum(); + + case DECODE_BLOCKS: + if (isLastBlock) + { + if (noHeader) + { + mode = FINISHED; + return false; + } + else + { + input.SkipToByteBoundary(); + neededBits = 32; + mode = DECODE_CHKSUM; + return true; + } + } + + int type = input.PeekBits(3); + if (type < 0) + { + return false; + } + input.DropBits(3); + + isLastBlock |= (type & 1) != 0; + switch (type >> 1) + { + case DeflaterConstants.STORED_BLOCK: + input.SkipToByteBoundary(); + mode = DECODE_STORED_LEN1; + break; + + case DeflaterConstants.STATIC_TREES: + litlenTree = InflaterHuffmanTree.defLitLenTree; + distTree = InflaterHuffmanTree.defDistTree; + mode = DECODE_HUFFMAN; + break; + + case DeflaterConstants.DYN_TREES: + dynHeader = new InflaterDynHeader(input); + mode = DECODE_DYN_HEADER; + break; + + default: + throw new SharpZipBaseException("Unknown block type " + type); + } + return true; + + case DECODE_STORED_LEN1: + { + if ((uncomprLen = input.PeekBits(16)) < 0) + { + return false; + } + input.DropBits(16); + mode = DECODE_STORED_LEN2; + } + goto case DECODE_STORED_LEN2; // fall through + + case DECODE_STORED_LEN2: + { + int nlen = input.PeekBits(16); + if (nlen < 0) + { + return false; + } + input.DropBits(16); + if (nlen != (uncomprLen ^ 0xffff)) + { + throw new SharpZipBaseException("broken uncompressed block"); + } + mode = DECODE_STORED; + } + goto case DECODE_STORED; // fall through + + case DECODE_STORED: + { + int more = outputWindow.CopyStored(input, uncomprLen); + uncomprLen -= more; + if (uncomprLen == 0) + { + mode = DECODE_BLOCKS; + return true; + } + return !input.IsNeedingInput; + } + + case DECODE_DYN_HEADER: + if (!dynHeader.AttemptRead()) + { + return false; + } + + litlenTree = dynHeader.LiteralLengthTree; + distTree = dynHeader.DistanceTree; + mode = DECODE_HUFFMAN; + goto case DECODE_HUFFMAN; // fall through + + case DECODE_HUFFMAN: + case DECODE_HUFFMAN_LENBITS: + case DECODE_HUFFMAN_DIST: + case DECODE_HUFFMAN_DISTBITS: + return DecodeHuffman(); + + case FINISHED: + return false; + + default: + throw new SharpZipBaseException("Inflater.Decode unknown mode"); + } + } + + /// + /// Sets the preset dictionary. This should only be called, if + /// needsDictionary() returns true and it should set the same + /// dictionary, that was used for deflating. The getAdler() + /// function returns the checksum of the dictionary needed. + /// + /// + /// The dictionary. + /// + public void SetDictionary(byte[] buffer) + { + SetDictionary(buffer, 0, buffer.Length); + } + + /// + /// Sets the preset dictionary. This should only be called, if + /// needsDictionary() returns true and it should set the same + /// dictionary, that was used for deflating. The getAdler() + /// function returns the checksum of the dictionary needed. + /// + /// + /// The dictionary. + /// + /// + /// The index into buffer where the dictionary starts. + /// + /// + /// The number of bytes in the dictionary. + /// + /// + /// No dictionary is needed. + /// + /// + /// The adler checksum for the buffer is invalid + /// + public void SetDictionary(byte[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (!IsNeedingDictionary) + { + throw new InvalidOperationException("Dictionary is not needed"); + } + + adler?.Update(new ArraySegment(buffer, index, count)); + + if (adler != null && (int)adler.Value != readAdler) + { + throw new SharpZipBaseException("Wrong adler checksum"); + } + adler?.Reset(); + outputWindow.CopyDict(buffer, index, count); + mode = DECODE_BLOCKS; + } + + /// + /// Sets the input. This should only be called, if needsInput() + /// returns true. + /// + /// + /// the input. + /// + public void SetInput(byte[] buffer) + { + SetInput(buffer, 0, buffer.Length); + } + + /// + /// Sets the input. This should only be called, if needsInput() + /// returns true. + /// + /// + /// The source of input data + /// + /// + /// The index into buffer where the input starts. + /// + /// + /// The number of bytes of input to use. + /// + /// + /// No input is needed. + /// + /// + /// The index and/or count are wrong. + /// + public void SetInput(byte[] buffer, int index, int count) + { + input.SetInput(buffer, index, count); + totalIn += (long)count; + } + + /// + /// Inflates the compressed stream to the output buffer. If this + /// returns 0, you should check, whether IsNeedingDictionary(), + /// IsNeedingInput() or IsFinished() returns true, to determine why no + /// further output is produced. + /// + /// + /// the output buffer. + /// + /// + /// The number of bytes written to the buffer, 0 if no further + /// output can be produced. + /// + /// + /// if buffer has length 0. + /// + /// + /// if deflated stream is invalid. + /// + public int Inflate(byte[] buffer) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + return Inflate(buffer, 0, buffer.Length); + } + + /// + /// Inflates the compressed stream to the output buffer. If this + /// returns 0, you should check, whether needsDictionary(), + /// needsInput() or finished() returns true, to determine why no + /// further output is produced. + /// + /// + /// the output buffer. + /// + /// + /// the offset in buffer where storing starts. + /// + /// + /// the maximum number of bytes to output. + /// + /// + /// the number of bytes written to the buffer, 0 if no further output can be produced. + /// + /// + /// if count is less than 0. + /// + /// + /// if the index and / or count are wrong. + /// + /// + /// if deflated stream is invalid. + /// + public int Inflate(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "count cannot be negative"); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "offset cannot be negative"); + } + + if (offset + count > buffer.Length) + { + throw new ArgumentException("count exceeds buffer bounds"); + } + + // Special case: count may be zero + if (count == 0) + { + if (!IsFinished) + { // -jr- 08-Nov-2003 INFLATE_BUG fix.. + Decode(); + } + return 0; + } + + int bytesCopied = 0; + + do + { + if (mode != DECODE_CHKSUM) + { + /* Don't give away any output, if we are waiting for the + * checksum in the input stream. + * + * With this trick we have always: + * IsNeedingInput() and not IsFinished() + * implies more output can be produced. + */ + int more = outputWindow.CopyOutput(buffer, offset, count); + if (more > 0) + { + adler?.Update(new ArraySegment(buffer, offset, more)); + offset += more; + bytesCopied += more; + totalOut += (long)more; + count -= more; + if (count == 0) + { + return bytesCopied; + } + } + } + } while (Decode() || ((outputWindow.GetAvailable() > 0) && (mode != DECODE_CHKSUM))); + return bytesCopied; + } + + /// + /// Returns true, if the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method also returns true when the stream is finished. + /// + public bool IsNeedingInput + { + get + { + return input.IsNeedingInput; + } + } + + /// + /// Returns true, if a preset dictionary is needed to inflate the input. + /// + public bool IsNeedingDictionary + { + get + { + return mode == DECODE_DICT && neededBits == 0; + } + } + + /// + /// Returns true, if the inflater has finished. This means, that no + /// input is needed and no output can be produced. + /// + public bool IsFinished + { + get + { + return mode == FINISHED && outputWindow.GetAvailable() == 0; + } + } + + /// + /// Gets the adler checksum. This is either the checksum of all + /// uncompressed bytes returned by inflate(), or if needsDictionary() + /// returns true (and thus no output was yet produced) this is the + /// adler checksum of the expected dictionary. + /// + /// + /// the adler checksum. + /// + public int Adler + { + get + { + if (IsNeedingDictionary) + { + return readAdler; + } + else if (adler != null) + { + return (int)adler.Value; + } + else + { + return 0; + } + } + } + + /// + /// Gets the total number of output bytes returned by Inflate(). + /// + /// + /// the total number of output bytes. + /// + public long TotalOut + { + get + { + return totalOut; + } + } + + /// + /// Gets the total number of processed compressed input bytes. + /// + /// + /// The total number of bytes of processed input bytes. + /// + public long TotalIn + { + get + { + return totalIn - (long)RemainingInput; + } + } + + /// + /// Gets the number of unprocessed input bytes. Useful, if the end of the + /// stream is reached and you want to further process the bytes after + /// the deflate stream. + /// + /// + /// The number of bytes of the input which have not been processed. + /// + public int RemainingInput + { + // TODO: This should be a long? + get + { + return input.AvailableBytes; + } + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/InflaterDynHeader.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/InflaterDynHeader.cs new file mode 100644 index 0000000..802c41f --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/InflaterDynHeader.cs @@ -0,0 +1,151 @@ +using BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using System; +using System.Collections.Generic; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression +{ + internal class InflaterDynHeader + { + #region Constants + + // maximum number of literal/length codes + private const int LITLEN_MAX = 286; + + // maximum number of distance codes + private const int DIST_MAX = 30; + + // maximum data code lengths to read + private const int CODELEN_MAX = LITLEN_MAX + DIST_MAX; + + // maximum meta code length codes to read + private const int META_MAX = 19; + + private static readonly int[] MetaCodeLengthIndex = + { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + #endregion Constants + + /// + /// Continue decoding header from until more bits are needed or decoding has been completed + /// + /// Returns whether decoding could be completed + public bool AttemptRead() + => !state.MoveNext() || state.Current; + + public InflaterDynHeader(StreamManipulator input) + { + this.input = input; + stateMachine = CreateStateMachine(); + state = stateMachine.GetEnumerator(); + } + + private IEnumerable CreateStateMachine() + { + // Read initial code length counts from header + while (!input.TryGetBits(5, ref litLenCodeCount, 257)) yield return false; + while (!input.TryGetBits(5, ref distanceCodeCount, 1)) yield return false; + while (!input.TryGetBits(4, ref metaCodeCount, 4)) yield return false; + var dataCodeCount = litLenCodeCount + distanceCodeCount; + + if (litLenCodeCount > LITLEN_MAX) throw new ValueOutOfRangeException(nameof(litLenCodeCount)); + if (distanceCodeCount > DIST_MAX) throw new ValueOutOfRangeException(nameof(distanceCodeCount)); + if (metaCodeCount > META_MAX) throw new ValueOutOfRangeException(nameof(metaCodeCount)); + + // Load code lengths for the meta tree from the header bits + for (int i = 0; i < metaCodeCount; i++) + { + while (!input.TryGetBits(3, ref codeLengths, MetaCodeLengthIndex[i])) yield return false; + } + + var metaCodeTree = new InflaterHuffmanTree(codeLengths); + + // Decompress the meta tree symbols into the data table code lengths + int index = 0; + while (index < dataCodeCount) + { + byte codeLength; + int symbol; + + while ((symbol = metaCodeTree.GetSymbol(input)) < 0) yield return false; + + if (symbol < 16) + { + // append literal code length + codeLengths[index++] = (byte)symbol; + } + else + { + int repeatCount = 0; + + if (symbol == 16) // Repeat last code length 3..6 times + { + if (index == 0) + throw new StreamDecodingException("Cannot repeat previous code length when no other code length has been read"); + + codeLength = codeLengths[index - 1]; + + // 2 bits + 3, [3..6] + while (!input.TryGetBits(2, ref repeatCount, 3)) yield return false; + } + else if (symbol == 17) // Repeat zero 3..10 times + { + codeLength = 0; + + // 3 bits + 3, [3..10] + while (!input.TryGetBits(3, ref repeatCount, 3)) yield return false; + } + else // (symbol == 18), Repeat zero 11..138 times + { + codeLength = 0; + + // 7 bits + 11, [11..138] + while (!input.TryGetBits(7, ref repeatCount, 11)) yield return false; + } + + if (index + repeatCount > dataCodeCount) + throw new StreamDecodingException("Cannot repeat code lengths past total number of data code lengths"); + + while (repeatCount-- > 0) + codeLengths[index++] = codeLength; + } + } + + if (codeLengths[256] == 0) + throw new StreamDecodingException("Inflater dynamic header end-of-block code missing"); + + litLenTree = new InflaterHuffmanTree(new ArraySegment(codeLengths, 0, litLenCodeCount)); + distTree = new InflaterHuffmanTree(new ArraySegment(codeLengths, litLenCodeCount, distanceCodeCount)); + + yield return true; + } + + /// + /// Get literal/length huffman tree, must not be used before has returned true + /// + /// If hader has not been successfully read by the state machine + public InflaterHuffmanTree LiteralLengthTree + => litLenTree ?? throw new StreamDecodingException("Header properties were accessed before header had been successfully read"); + + /// + /// Get distance huffman tree, must not be used before has returned true + /// + /// If hader has not been successfully read by the state machine + public InflaterHuffmanTree DistanceTree + => distTree ?? throw new StreamDecodingException("Header properties were accessed before header had been successfully read"); + + #region Instance Fields + + private readonly StreamManipulator input; + private readonly IEnumerator state; + private readonly IEnumerable stateMachine; + + private byte[] codeLengths = new byte[CODELEN_MAX]; + + private InflaterHuffmanTree litLenTree; + private InflaterHuffmanTree distTree; + + private int litLenCodeCount, distanceCodeCount, metaCodeCount; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs new file mode 100644 index 0000000..0d3df4a --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs @@ -0,0 +1,237 @@ +using BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using System; +using System.Collections.Generic; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// Huffman tree used for inflation + /// + public class InflaterHuffmanTree + { + #region Constants + + private const int MAX_BITLEN = 15; + + #endregion Constants + + #region Instance Fields + + private short[] tree; + + #endregion Instance Fields + + /// + /// Literal length tree + /// + public static InflaterHuffmanTree defLitLenTree; + + /// + /// Distance tree + /// + public static InflaterHuffmanTree defDistTree; + + static InflaterHuffmanTree() + { + try + { + byte[] codeLengths = new byte[288]; + int i = 0; + while (i < 144) + { + codeLengths[i++] = 8; + } + while (i < 256) + { + codeLengths[i++] = 9; + } + while (i < 280) + { + codeLengths[i++] = 7; + } + while (i < 288) + { + codeLengths[i++] = 8; + } + defLitLenTree = new InflaterHuffmanTree(codeLengths); + + codeLengths = new byte[32]; + i = 0; + while (i < 32) + { + codeLengths[i++] = 5; + } + defDistTree = new InflaterHuffmanTree(codeLengths); + } + catch (Exception) + { + throw new SharpZipBaseException("InflaterHuffmanTree: static tree length illegal"); + } + } + + #region Constructors + + /// + /// Constructs a Huffman tree from the array of code lengths. + /// + /// + /// the array of code lengths + /// + public InflaterHuffmanTree(IList codeLengths) + { + BuildTree(codeLengths); + } + + #endregion Constructors + + private void BuildTree(IList codeLengths) + { + int[] blCount = new int[MAX_BITLEN + 1]; + int[] nextCode = new int[MAX_BITLEN + 1]; + + for (int i = 0; i < codeLengths.Count; i++) + { + int bits = codeLengths[i]; + if (bits > 0) + { + blCount[bits]++; + } + } + + int code = 0; + int treeSize = 512; + for (int bits = 1; bits <= MAX_BITLEN; bits++) + { + nextCode[bits] = code; + code += blCount[bits] << (16 - bits); + if (bits >= 10) + { + /* We need an extra table for bit lengths >= 10. */ + int start = nextCode[bits] & 0x1ff80; + int end = code & 0x1ff80; + treeSize += (end - start) >> (16 - bits); + } + } + + /* -jr comment this out! doesnt work for dynamic trees and pkzip 2.04g + if (code != 65536) + { + throw new SharpZipBaseException("Code lengths don't add up properly."); + } + */ + /* Now create and fill the extra tables from longest to shortest + * bit len. This way the sub trees will be aligned. + */ + tree = new short[treeSize]; + int treePtr = 512; + for (int bits = MAX_BITLEN; bits >= 10; bits--) + { + int end = code & 0x1ff80; + code -= blCount[bits] << (16 - bits); + int start = code & 0x1ff80; + for (int i = start; i < end; i += 1 << 7) + { + tree[DeflaterHuffman.BitReverse(i)] = (short)((-treePtr << 4) | bits); + treePtr += 1 << (bits - 9); + } + } + + for (int i = 0; i < codeLengths.Count; i++) + { + int bits = codeLengths[i]; + if (bits == 0) + { + continue; + } + code = nextCode[bits]; + int revcode = DeflaterHuffman.BitReverse(code); + if (bits <= 9) + { + do + { + tree[revcode] = (short)((i << 4) | bits); + revcode += 1 << bits; + } while (revcode < 512); + } + else + { + int subTree = tree[revcode & 511]; + int treeLen = 1 << (subTree & 15); + subTree = -(subTree >> 4); + do + { + tree[subTree | (revcode >> 9)] = (short)((i << 4) | bits); + revcode += 1 << bits; + } while (revcode < treeLen); + } + nextCode[bits] = code + (1 << (16 - bits)); + } + } + + /// + /// Reads the next symbol from input. The symbol is encoded using the + /// huffman tree. + /// + /// + /// input the input source. + /// + /// + /// the next symbol, or -1 if not enough input is available. + /// + public int GetSymbol(StreamManipulator input) + { + int lookahead, symbol; + if ((lookahead = input.PeekBits(9)) >= 0) + { + symbol = tree[lookahead]; + int bitlen = symbol & 15; + + if (symbol >= 0) + { + if(bitlen == 0){ + throw new SharpZipBaseException("Encountered invalid codelength 0"); + } + input.DropBits(bitlen); + return symbol >> 4; + } + int subtree = -(symbol >> 4); + if ((lookahead = input.PeekBits(bitlen)) >= 0) + { + symbol = tree[subtree | (lookahead >> 9)]; + input.DropBits(symbol & 15); + return symbol >> 4; + } + else + { + int bits = input.AvailableBits; + lookahead = input.PeekBits(bits); + symbol = tree[subtree | (lookahead >> 9)]; + if ((symbol & 15) <= bits) + { + input.DropBits(symbol & 15); + return symbol >> 4; + } + else + { + return -1; + } + } + } + else // Less than 9 bits + { + int bits = input.AvailableBits; + lookahead = input.PeekBits(bits); + symbol = tree[lookahead]; + if (symbol >= 0 && (symbol & 15) <= bits) + { + input.DropBits(symbol & 15); + return symbol >> 4; + } + else + { + return -1; + } + } + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/PendingBuffer.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/PendingBuffer.cs new file mode 100644 index 0000000..4eb77e0 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/PendingBuffer.cs @@ -0,0 +1,268 @@ +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// This class is general purpose class for writing data to a buffer. + /// + /// It allows you to write bits as well as bytes + /// Based on DeflaterPending.java + /// + /// author of the original java version : Jochen Hoenicke + /// + public class PendingBuffer + { + #region Instance Fields + + /// + /// Internal work buffer + /// + private readonly byte[] buffer; + + private int start; + private int end; + + private uint bits; + private int bitCount; + + #endregion Instance Fields + + #region Constructors + + /// + /// construct instance using default buffer size of 4096 + /// + public PendingBuffer() : this(4096) + { + } + + /// + /// construct instance using specified buffer size + /// + /// + /// size to use for internal buffer + /// + public PendingBuffer(int bufferSize) + { + buffer = new byte[bufferSize]; + } + + #endregion Constructors + + /// + /// Clear internal state/buffers + /// + public void Reset() + { + start = end = bitCount = 0; + } + + /// + /// Write a byte to buffer + /// + /// + /// The value to write + /// + public void WriteByte(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer[end++] = unchecked((byte)value); + } + + /// + /// Write a short value to buffer LSB first + /// + /// + /// The value to write. + /// + public void WriteShort(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer[end++] = unchecked((byte)value); + buffer[end++] = unchecked((byte)(value >> 8)); + } + + /// + /// write an integer LSB first + /// + /// The value to write. + public void WriteInt(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer[end++] = unchecked((byte)value); + buffer[end++] = unchecked((byte)(value >> 8)); + buffer[end++] = unchecked((byte)(value >> 16)); + buffer[end++] = unchecked((byte)(value >> 24)); + } + + /// + /// Write a block of data to buffer + /// + /// data to write + /// offset of first byte to write + /// number of bytes to write + public void WriteBlock(byte[] block, int offset, int length) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + System.Array.Copy(block, offset, buffer, end, length); + end += length; + } + + /// + /// The number of bits written to the buffer + /// + public int BitCount + { + get + { + return bitCount; + } + } + + /// + /// Align internal buffer on a byte boundary + /// + public void AlignToByte() + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + if (bitCount > 0) + { + buffer[end++] = unchecked((byte)bits); + if (bitCount > 8) + { + buffer[end++] = unchecked((byte)(bits >> 8)); + } + } + bits = 0; + bitCount = 0; + } + + /// + /// Write bits to internal buffer + /// + /// source of bits + /// number of bits to write + public void WriteBits(int b, int count) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("writeBits("+b+","+count+")"); + // } +#endif + bits |= (uint)(b << bitCount); + bitCount += count; + if (bitCount >= 16) + { + buffer[end++] = unchecked((byte)bits); + buffer[end++] = unchecked((byte)(bits >> 8)); + bits >>= 16; + bitCount -= 16; + } + } + + /// + /// Write a short value to internal buffer most significant byte first + /// + /// value to write + public void WriteShortMSB(int s) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer[end++] = unchecked((byte)(s >> 8)); + buffer[end++] = unchecked((byte)s); + } + + /// + /// Indicates if buffer has been flushed + /// + public bool IsFlushed + { + get + { + return end == 0; + } + } + + /// + /// Flushes the pending buffer into the given output array. If the + /// output array is to small, only a partial flush is done. + /// + /// The output array. + /// The offset into output array. + /// The maximum number of bytes to store. + /// The number of bytes flushed. + public int Flush(byte[] output, int offset, int length) + { + if (bitCount >= 8) + { + buffer[end++] = unchecked((byte)bits); + bits >>= 8; + bitCount -= 8; + } + + if (length > end - start) + { + length = end - start; + System.Array.Copy(buffer, start, output, offset, length); + start = 0; + end = 0; + } + else + { + System.Array.Copy(buffer, start, output, offset, length); + start += length; + } + return length; + } + + /// + /// Convert internal buffer to byte array. + /// Buffer is empty on completion + /// + /// + /// The internal buffer contents converted to a byte array. + /// + public byte[] ToByteArray() + { + AlignToByte(); + + byte[] result = new byte[end - start]; + System.Array.Copy(buffer, start, result, 0, result.Length); + start = 0; + end = 0; + return result; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs new file mode 100644 index 0000000..8911a58 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs @@ -0,0 +1,560 @@ +using BSP_ICSharpCode.SharpZipLib.Encryption; +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + /// + /// A special stream deflating or compressing the bytes that are + /// written to it. It uses a Deflater to perform actual deflating.
+ /// Authors of the original java version : Tom Tromey, Jochen Hoenicke + ///
+ public class DeflaterOutputStream : Stream + { + #region Constructors + + /// + /// Creates a new DeflaterOutputStream with a default Deflater and default buffer size. + /// + /// + /// the output stream where deflated output should be written. + /// + public DeflaterOutputStream(Stream baseOutputStream) + : this(baseOutputStream, new Deflater(), 512) + { + } + + /// + /// Creates a new DeflaterOutputStream with the given Deflater and + /// default buffer size. + /// + /// + /// the output stream where deflated output should be written. + /// + /// + /// the underlying deflater. + /// + public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater) + : this(baseOutputStream, deflater, 512) + { + } + + /// + /// Creates a new DeflaterOutputStream with the given Deflater and + /// buffer size. + /// + /// + /// The output stream where deflated output is written. + /// + /// + /// The underlying deflater to use + /// + /// + /// The buffer size in bytes to use when deflating (minimum value 512) + /// + /// + /// bufsize is less than or equal to zero. + /// + /// + /// baseOutputStream does not support writing + /// + /// + /// deflater instance is null + /// + public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater, int bufferSize) + { + if (baseOutputStream == null) + { + throw new ArgumentNullException(nameof(baseOutputStream)); + } + + if (baseOutputStream.CanWrite == false) + { + throw new ArgumentException("Must support writing", nameof(baseOutputStream)); + } + + if (bufferSize < 512) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + + baseOutputStream_ = baseOutputStream; + buffer_ = new byte[bufferSize]; + deflater_ = deflater ?? throw new ArgumentNullException(nameof(deflater)); + } + + #endregion Constructors + + #region Public API + + /// + /// Finishes the stream by calling finish() on the deflater. + /// + /// + /// Not all input is deflated + /// + public virtual void Finish() + { + deflater_.Finish(); + while (!deflater_.IsFinished) + { + int len = deflater_.Deflate(buffer_, 0, buffer_.Length); + if (len <= 0) + { + break; + } + + EncryptBlock(buffer_, 0, len); + + baseOutputStream_.Write(buffer_, 0, len); + } + + if (!deflater_.IsFinished) + { + throw new SharpZipBaseException("Can't deflate all input?"); + } + + baseOutputStream_.Flush(); + + if (cryptoTransform_ != null) + { + if (cryptoTransform_ is ZipAESTransform) + { + AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + } + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } + } + + /// + /// Finishes the stream by calling finish() on the deflater. + /// + /// The that can be used to cancel the operation. + /// + /// Not all input is deflated + /// + public virtual async Task FinishAsync(CancellationToken ct) + { + deflater_.Finish(); + while (!deflater_.IsFinished) + { + int len = deflater_.Deflate(buffer_, 0, buffer_.Length); + if (len <= 0) + { + break; + } + + EncryptBlock(buffer_, 0, len); + + await baseOutputStream_.WriteAsync(buffer_, 0, len, ct).ConfigureAwait(false); + } + + if (!deflater_.IsFinished) + { + throw new SharpZipBaseException("Can't deflate all input?"); + } + + await baseOutputStream_.FlushAsync(ct).ConfigureAwait(false); + + if (cryptoTransform_ != null) + { + if (cryptoTransform_ is ZipAESTransform) + { + AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + } + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } + } + + /// + /// Gets or sets a flag indicating ownership of underlying stream. + /// When the flag is true will close the underlying stream also. + /// + /// The default value is true. + public bool IsStreamOwner { get; set; } = true; + + /// + /// Allows client to determine if an entry can be patched after its added + /// + public bool CanPatchEntries + { + get + { + return baseOutputStream_.CanSeek; + } + } + + #endregion Public API + + #region Encryption + + /// + /// The CryptoTransform currently being used to encrypt the compressed data. + /// + protected ICryptoTransform cryptoTransform_; + + /// + /// Returns the 10 byte AUTH CODE to be appended immediately following the AES data stream. + /// + protected byte[] AESAuthCode; + + /// + public Encoding ZipCryptoEncoding { + get => _stringCodec.ZipCryptoEncoding; + set { + _stringCodec = _stringCodec.WithZipCryptoEncoding(value); + } + } + + /// + /// Encrypt a block of data + /// + /// + /// Data to encrypt. NOTE the original contents of the buffer are lost + /// + /// + /// Offset of first byte in buffer to encrypt + /// + /// + /// Number of bytes in buffer to encrypt + /// + protected void EncryptBlock(byte[] buffer, int offset, int length) + { + if(cryptoTransform_ is null) return; + cryptoTransform_.TransformBlock(buffer, 0, length, buffer, 0); + } + + #endregion Encryption + + #region Deflation Support + + /// + /// Deflates everything in the input buffers. This will call + /// def.deflate() until all bytes from the input buffers + /// are processed. + /// + protected void Deflate() + => DeflateSyncOrAsync(false, null).GetAwaiter().GetResult(); + + private async Task DeflateSyncOrAsync(bool flushing, CancellationToken? ct) + { + while (flushing || !deflater_.IsNeedingInput) + { + int deflateCount = deflater_.Deflate(buffer_, 0, buffer_.Length); + + if (deflateCount <= 0) + { + break; + } + + EncryptBlock(buffer_, 0, deflateCount); + + if (ct.HasValue) + { + await baseOutputStream_.WriteAsync(buffer_, 0, deflateCount, ct.Value).ConfigureAwait(false); + } + else + { + baseOutputStream_.Write(buffer_, 0, deflateCount); + } + } + + if (!deflater_.IsNeedingInput) + { + throw new SharpZipBaseException("DeflaterOutputStream can't deflate all input?"); + } + } + + #endregion Deflation Support + + #region Stream Overrides + + /// + /// Gets value indicating stream can be read from + /// + public override bool CanRead + { + get + { + return false; + } + } + + /// + /// Gets a value indicating if seeking is supported for this stream + /// This property always returns false + /// + public override bool CanSeek + { + get + { + return false; + } + } + + /// + /// Get value indicating if this stream supports writing + /// + public override bool CanWrite + { + get + { + return baseOutputStream_.CanWrite; + } + } + + /// + /// Get current length of stream + /// + public override long Length + { + get + { + return baseOutputStream_.Length; + } + } + + /// + /// Gets the current position within the stream. + /// + /// Any attempt to set position + public override long Position + { + get + { + return baseOutputStream_.Position; + } + set + { + throw new NotSupportedException("Position property not supported"); + } + } + + /// + /// Sets the current position of this stream to the given value. Not supported by this class! + /// + /// The offset relative to the to seek. + /// The to seek from. + /// The new position in the stream. + /// Any access + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("DeflaterOutputStream Seek not supported"); + } + + /// + /// Sets the length of this stream to the given value. Not supported by this class! + /// + /// The new stream length. + /// Any access + public override void SetLength(long value) + { + throw new NotSupportedException("DeflaterOutputStream SetLength not supported"); + } + + /// + /// Read a byte from stream advancing position by one + /// + /// The byte read cast to an int. THe value is -1 if at the end of the stream. + /// Any access + public override int ReadByte() + { + throw new NotSupportedException("DeflaterOutputStream ReadByte not supported"); + } + + /// + /// Read a block of bytes from stream + /// + /// The buffer to store read data in. + /// The offset to start storing at. + /// The maximum number of bytes to read. + /// The actual number of bytes read. Zero if end of stream is detected. + /// Any access + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("DeflaterOutputStream Read not supported"); + } + + /// + /// Flushes the stream by calling Flush on the deflater and then + /// on the underlying stream. This ensures that all bytes are flushed. + /// + public override void Flush() + { + deflater_.Flush(); + DeflateSyncOrAsync(true, null).GetAwaiter().GetResult(); + baseOutputStream_.Flush(); + } + + /// + /// Asynchronously clears all buffers for this stream, causes any buffered data to be written to the underlying device, and monitors cancellation requests. + /// + /// + /// The token to monitor for cancellation requests. The default value is . + /// + public override async Task FlushAsync(CancellationToken cancellationToken) + { + deflater_.Flush(); + await DeflateSyncOrAsync(true, cancellationToken).ConfigureAwait(false); + await baseOutputStream_.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Calls and closes the underlying + /// stream when is true. + /// + protected override void Dispose(bool disposing) + { + if (!isClosed_) + { + isClosed_ = true; + + try + { + Finish(); + if (cryptoTransform_ != null) + { + GetAuthCodeIfAES(); + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } + } + finally + { + if (IsStreamOwner) + { + baseOutputStream_.Dispose(); + } + } + } + } + +#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER + /// + /// Calls and closes the underlying + /// stream when is true. + /// + public override async ValueTask DisposeAsync() + { + if (!isClosed_) + { + isClosed_ = true; + + try + { + await FinishAsync(CancellationToken.None).ConfigureAwait(false); + if (cryptoTransform_ != null) + { + GetAuthCodeIfAES(); + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } + } + finally + { + if (IsStreamOwner) + { + await baseOutputStream_.DisposeAsync().ConfigureAwait(false); + } + } + } + } +#endif + + /// + /// Get the Auth code for AES encrypted entries + /// + protected void GetAuthCodeIfAES() + { + if (cryptoTransform_ is ZipAESTransform) + { + AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + } + } + + /// + /// Writes a single byte to the compressed output stream. + /// + /// + /// The byte value. + /// + public override void WriteByte(byte value) + { + byte[] b = new byte[1]; + b[0] = value; + Write(b, 0, 1); + } + + /// + /// Writes bytes from an array to the compressed stream. + /// + /// + /// The byte array + /// + /// + /// The offset into the byte array where to start. + /// + /// + /// The number of bytes to write. + /// + public override void Write(byte[] buffer, int offset, int count) + { + deflater_.SetInput(buffer, offset, count); + Deflate(); + } + + /// + /// Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests. + /// + /// + /// The byte array + /// + /// + /// The offset into the byte array where to start. + /// + /// + /// The number of bytes to write. + /// + /// + /// The token to monitor for cancellation requests. The default value is . + /// + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) + { + deflater_.SetInput(buffer, offset, count); + await DeflateSyncOrAsync(false, ct).ConfigureAwait(false); + } + + #endregion Stream Overrides + + #region Instance Fields + + /// + /// This buffer is used temporarily to retrieve the bytes from the + /// deflater and write them to the underlying output stream. + /// + private byte[] buffer_; + + /// + /// The deflater which is used to deflate the stream. + /// + protected Deflater deflater_; + + /// + /// Base stream the deflater depends on. + /// + protected Stream baseOutputStream_; + + private bool isClosed_; + + /// + protected StringCodec _stringCodec = ZipStrings.GetStringCodec(); + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs new file mode 100644 index 0000000..3faebf2 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs @@ -0,0 +1,713 @@ +using System; +using System.IO; +using System.Security.Cryptography; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + /// + /// An input buffer customised for use by + /// + /// + /// The buffer supports decryption of incoming data. + /// + public class InflaterInputBuffer + { + #region Constructors + + /// + /// Initialise a new instance of with a default buffer size + /// + /// The stream to buffer. + public InflaterInputBuffer(Stream stream) : this(stream, 4096) + { + } + + /// + /// Initialise a new instance of + /// + /// The stream to buffer. + /// The size to use for the buffer + /// A minimum buffer size of 1KB is permitted. Lower sizes are treated as 1KB. + public InflaterInputBuffer(Stream stream, int bufferSize) + { + inputStream = stream; + if (bufferSize < 1024) + { + bufferSize = 1024; + } + rawData = new byte[bufferSize]; + clearText = rawData; + } + + #endregion Constructors + + /// + /// Get the length of bytes in the + /// + public int RawLength + { + get + { + return rawLength; + } + } + + /// + /// Get the contents of the raw data buffer. + /// + /// This may contain encrypted data. + public byte[] RawData + { + get + { + return rawData; + } + } + + /// + /// Get the number of useable bytes in + /// + public int ClearTextLength + { + get + { + return clearTextLength; + } + } + + /// + /// Get the contents of the clear text buffer. + /// + public byte[] ClearText + { + get + { + return clearText; + } + } + + /// + /// Get/set the number of bytes available + /// + public int Available + { + get { return available; } + set { available = value; } + } + + /// + /// Call passing the current clear text buffer contents. + /// + /// The inflater to set input for. + public void SetInflaterInput(Inflater inflater) + { + if (available > 0) + { + inflater.SetInput(clearText, clearTextLength - available, available); + available = 0; + } + } + + /// + /// Fill the buffer from the underlying input stream. + /// + public void Fill() + { + rawLength = 0; + int toRead = rawData.Length; + + while (toRead > 0 && inputStream.CanRead) + { + int count = inputStream.Read(rawData, rawLength, toRead); + if (count <= 0) + { + break; + } + rawLength += count; + toRead -= count; + } + + if (cryptoTransform != null) + { + clearTextLength = cryptoTransform.TransformBlock(rawData, 0, rawLength, clearText, 0); + } + else + { + clearTextLength = rawLength; + } + + available = clearTextLength; + } + + /// + /// Read a buffer directly from the input stream + /// + /// The buffer to fill + /// Returns the number of bytes read. + public int ReadRawBuffer(byte[] buffer) + { + return ReadRawBuffer(buffer, 0, buffer.Length); + } + + /// + /// Read a buffer directly from the input stream + /// + /// The buffer to read into + /// The offset to start reading data into. + /// The number of bytes to read. + /// Returns the number of bytes read. + public int ReadRawBuffer(byte[] outBuffer, int offset, int length) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + int currentOffset = offset; + int currentLength = length; + + while (currentLength > 0) + { + if (available <= 0) + { + Fill(); + if (available <= 0) + { + return 0; + } + } + int toCopy = Math.Min(currentLength, available); + System.Array.Copy(rawData, rawLength - (int)available, outBuffer, currentOffset, toCopy); + currentOffset += toCopy; + currentLength -= toCopy; + available -= toCopy; + } + return length; + } + + /// + /// Read clear text data from the input stream. + /// + /// The buffer to add data to. + /// The offset to start adding data at. + /// The number of bytes to read. + /// Returns the number of bytes actually read. + public int ReadClearTextBuffer(byte[] outBuffer, int offset, int length) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + int currentOffset = offset; + int currentLength = length; + + while (currentLength > 0) + { + if (available <= 0) + { + Fill(); + if (available <= 0) + { + return 0; + } + } + + int toCopy = Math.Min(currentLength, available); + Array.Copy(clearText, clearTextLength - (int)available, outBuffer, currentOffset, toCopy); + currentOffset += toCopy; + currentLength -= toCopy; + available -= toCopy; + } + return length; + } + + /// + /// Read a from the input stream. + /// + /// Returns the byte read. + public byte ReadLeByte() + { + if (available <= 0) + { + Fill(); + if (available <= 0) + { + throw new ZipException("EOF in header"); + } + } + byte result = rawData[rawLength - available]; + available -= 1; + return result; + } + + /// + /// Read an in little endian byte order. + /// + /// The short value read case to an int. + public int ReadLeShort() + { + return ReadLeByte() | (ReadLeByte() << 8); + } + + /// + /// Read an in little endian byte order. + /// + /// The int value read. + public int ReadLeInt() + { + return ReadLeShort() | (ReadLeShort() << 16); + } + + /// + /// Read a in little endian byte order. + /// + /// The long value read. + public long ReadLeLong() + { + return (uint)ReadLeInt() | ((long)ReadLeInt() << 32); + } + + /// + /// Get/set the to apply to any data. + /// + /// Set this value to null to have no transform applied. + public ICryptoTransform CryptoTransform + { + set + { + cryptoTransform = value; + if (cryptoTransform != null) + { + if (rawData == clearText) + { + if (internalClearText == null) + { + internalClearText = new byte[rawData.Length]; + } + clearText = internalClearText; + } + clearTextLength = rawLength; + if (available > 0) + { + cryptoTransform.TransformBlock(rawData, rawLength - available, available, clearText, rawLength - available); + } + } + else + { + clearText = rawData; + clearTextLength = rawLength; + } + } + } + + #region Instance Fields + + private int rawLength; + private byte[] rawData; + + private int clearTextLength; + private byte[] clearText; + private byte[] internalClearText; + + private int available; + + private ICryptoTransform cryptoTransform; + private Stream inputStream; + + #endregion Instance Fields + } + + /// + /// This filter stream is used to decompress data compressed using the "deflate" + /// format. The "deflate" format is described in RFC 1951. + /// + /// This stream may form the basis for other decompression filters, such + /// as the GZipInputStream. + /// + /// Author of the original java version : John Leuner. + /// + public class InflaterInputStream : Stream + { + #region Constructors + + /// + /// Create an InflaterInputStream with the default decompressor + /// and a default buffer size of 4KB. + /// + /// + /// The InputStream to read bytes from + /// + public InflaterInputStream(Stream baseInputStream) + : this(baseInputStream, new Inflater(), 4096) + { + } + + /// + /// Create an InflaterInputStream with the specified decompressor + /// and a default buffer size of 4KB. + /// + /// + /// The source of input data + /// + /// + /// The decompressor used to decompress data read from baseInputStream + /// + public InflaterInputStream(Stream baseInputStream, Inflater inf) + : this(baseInputStream, inf, 4096) + { + } + + /// + /// Create an InflaterInputStream with the specified decompressor + /// and the specified buffer size. + /// + /// + /// The InputStream to read bytes from + /// + /// + /// The decompressor to use + /// + /// + /// Size of the buffer to use + /// + public InflaterInputStream(Stream baseInputStream, Inflater inflater, int bufferSize) + { + if (baseInputStream == null) + { + throw new ArgumentNullException(nameof(baseInputStream)); + } + + if (inflater == null) + { + throw new ArgumentNullException(nameof(inflater)); + } + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + + this.baseInputStream = baseInputStream; + this.inf = inflater; + + inputBuffer = new InflaterInputBuffer(baseInputStream, bufferSize); + } + + #endregion Constructors + + /// + /// Gets or sets a flag indicating ownership of underlying stream. + /// When the flag is true will close the underlying stream also. + /// + /// The default value is true. + public bool IsStreamOwner { get; set; } = true; + + /// + /// Skip specified number of bytes of uncompressed data + /// + /// + /// Number of bytes to skip + /// + /// + /// The number of bytes skipped, zero if the end of + /// stream has been reached + /// + /// + /// The number of bytes to skip is less than or equal to zero. + /// + public long Skip(long count) + { + if (count <= 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + // v0.80 Skip by seeking if underlying stream supports it... + if (baseInputStream.CanSeek) + { + baseInputStream.Seek(count, SeekOrigin.Current); + return count; + } + else + { + int length = 2048; + if (count < length) + { + length = (int)count; + } + + byte[] tmp = new byte[length]; + int readCount = 1; + long toSkip = count; + + while ((toSkip > 0) && (readCount > 0)) + { + if (toSkip < length) + { + length = (int)toSkip; + } + + readCount = baseInputStream.Read(tmp, 0, length); + toSkip -= readCount; + } + + return count - toSkip; + } + } + + /// + /// Clear any cryptographic state. + /// + protected void StopDecrypting() + { + inputBuffer.CryptoTransform = null; + } + + /// + /// Returns 0 once the end of the stream (EOF) has been reached. + /// Otherwise returns 1. + /// + public virtual int Available + { + get + { + return inf.IsFinished ? 0 : 1; + } + } + + /// + /// Fills the buffer with more data to decompress. + /// + /// + /// Stream ends early + /// + protected void Fill() + { + // Protect against redundant calls + if (inputBuffer.Available <= 0) + { + inputBuffer.Fill(); + if (inputBuffer.Available <= 0) + { + throw new SharpZipBaseException("Unexpected EOF"); + } + } + inputBuffer.SetInflaterInput(inf); + } + + #region Stream Overrides + + /// + /// Gets a value indicating whether the current stream supports reading + /// + public override bool CanRead + { + get + { + return baseInputStream.CanRead; + } + } + + /// + /// Gets a value of false indicating seeking is not supported for this stream. + /// + public override bool CanSeek + { + get + { + return false; + } + } + + /// + /// Gets a value of false indicating that this stream is not writeable. + /// + public override bool CanWrite + { + get + { + return false; + } + } + + /// + /// A value representing the length of the stream in bytes. + /// + public override long Length + { + get + { + //return inputBuffer.RawLength; + throw new NotSupportedException("InflaterInputStream Length is not supported"); + } + } + + /// + /// The current position within the stream. + /// Throws a NotSupportedException when attempting to set the position + /// + /// Attempting to set the position + public override long Position + { + get + { + return baseInputStream.Position; + } + set + { + throw new NotSupportedException("InflaterInputStream Position not supported"); + } + } + + /// + /// Flushes the baseInputStream + /// + public override void Flush() + { + baseInputStream.Flush(); + } + + /// + /// Sets the position within the current stream + /// Always throws a NotSupportedException + /// + /// The relative offset to seek to. + /// The defining where to seek from. + /// The new position in the stream. + /// Any access + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("Seek not supported"); + } + + /// + /// Set the length of the current stream + /// Always throws a NotSupportedException + /// + /// The new length value for the stream. + /// Any access + public override void SetLength(long value) + { + throw new NotSupportedException("InflaterInputStream SetLength not supported"); + } + + /// + /// Writes a sequence of bytes to stream and advances the current position + /// This method always throws a NotSupportedException + /// + /// The buffer containing data to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// Any access + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("InflaterInputStream Write not supported"); + } + + /// + /// Writes one byte to the current stream and advances the current position + /// Always throws a NotSupportedException + /// + /// The byte to write. + /// Any access + public override void WriteByte(byte value) + { + throw new NotSupportedException("InflaterInputStream WriteByte not supported"); + } + + /// + /// Closes the input stream. When + /// is true the underlying stream is also closed. + /// + protected override void Dispose(bool disposing) + { + if (!isClosed) + { + isClosed = true; + if (IsStreamOwner) + { + baseInputStream.Dispose(); + } + } + } + + /// + /// Reads decompressed data into the provided buffer byte array + /// + /// + /// The array to read and decompress data into + /// + /// + /// The offset indicating where the data should be placed + /// + /// + /// The number of bytes to decompress + /// + /// The number of bytes read. Zero signals the end of stream + /// + /// Inflater needs a dictionary + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (inf.IsNeedingDictionary) + { + throw new SharpZipBaseException("Need a dictionary"); + } + + int remainingBytes = count; + while (true) + { + int bytesRead = inf.Inflate(buffer, offset, remainingBytes); + offset += bytesRead; + remainingBytes -= bytesRead; + + if (remainingBytes == 0 || inf.IsFinished) + { + break; + } + + if (inf.IsNeedingInput) + { + Fill(); + } + else if (bytesRead == 0) + { + throw new ZipException("Invalid input data"); + } + } + return count - remainingBytes; + } + + #endregion Stream Overrides + + #region Instance Fields + + /// + /// Decompressor for this stream + /// + protected Inflater inf; + + /// + /// Input buffer for this stream. + /// + protected InflaterInputBuffer inputBuffer; + + /// + /// Base stream the inflater reads from. + /// + private Stream baseInputStream; + + /// + /// The compressed size + /// + protected long csize; + + /// + /// Flag indicating whether this instance has been closed or not. + /// + private bool isClosed; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/OutputWindow.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/OutputWindow.cs new file mode 100644 index 0000000..5a5ea04 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/OutputWindow.cs @@ -0,0 +1,220 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + /// + /// Contains the output from the Inflation process. + /// We need to have a window so that we can refer backwards into the output stream + /// to repeat stuff.
+ /// Author of the original java version : John Leuner + ///
+ public class OutputWindow + { + #region Constants + + private const int WindowSize = 1 << 15; + private const int WindowMask = WindowSize - 1; + + #endregion Constants + + #region Instance Fields + + private byte[] window = new byte[WindowSize]; //The window is 2^15 bytes + private int windowEnd; + private int windowFilled; + + #endregion Instance Fields + + /// + /// Write a byte to this output window + /// + /// value to write + /// + /// if window is full + /// + public void Write(int value) + { + if (windowFilled++ == WindowSize) + { + throw new InvalidOperationException("Window full"); + } + window[windowEnd++] = (byte)value; + windowEnd &= WindowMask; + } + + private void SlowRepeat(int repStart, int length, int distance) + { + while (length-- > 0) + { + window[windowEnd++] = window[repStart++]; + windowEnd &= WindowMask; + repStart &= WindowMask; + } + } + + /// + /// Append a byte pattern already in the window itself + /// + /// length of pattern to copy + /// distance from end of window pattern occurs + /// + /// If the repeated data overflows the window + /// + public void Repeat(int length, int distance) + { + if ((windowFilled += length) > WindowSize) + { + throw new InvalidOperationException("Window full"); + } + + int repStart = (windowEnd - distance) & WindowMask; + int border = WindowSize - length; + if ((repStart <= border) && (windowEnd < border)) + { + if (length <= distance) + { + System.Array.Copy(window, repStart, window, windowEnd, length); + windowEnd += length; + } + else + { + // We have to copy manually, since the repeat pattern overlaps. + while (length-- > 0) + { + window[windowEnd++] = window[repStart++]; + } + } + } + else + { + SlowRepeat(repStart, length, distance); + } + } + + /// + /// Copy from input manipulator to internal window + /// + /// source of data + /// length of data to copy + /// the number of bytes copied + public int CopyStored(StreamManipulator input, int length) + { + length = Math.Min(Math.Min(length, WindowSize - windowFilled), input.AvailableBytes); + int copied; + + int tailLen = WindowSize - windowEnd; + if (length > tailLen) + { + copied = input.CopyBytes(window, windowEnd, tailLen); + if (copied == tailLen) + { + copied += input.CopyBytes(window, 0, length - tailLen); + } + } + else + { + copied = input.CopyBytes(window, windowEnd, length); + } + + windowEnd = (windowEnd + copied) & WindowMask; + windowFilled += copied; + return copied; + } + + /// + /// Copy dictionary to window + /// + /// source dictionary + /// offset of start in source dictionary + /// length of dictionary + /// + /// If window isnt empty + /// + public void CopyDict(byte[] dictionary, int offset, int length) + { + if (dictionary == null) + { + throw new ArgumentNullException(nameof(dictionary)); + } + + if (windowFilled > 0) + { + throw new InvalidOperationException(); + } + + if (length > WindowSize) + { + offset += length - WindowSize; + length = WindowSize; + } + System.Array.Copy(dictionary, offset, window, 0, length); + windowEnd = length & WindowMask; + } + + /// + /// Get remaining unfilled space in window + /// + /// Number of bytes left in window + public int GetFreeSpace() + { + return WindowSize - windowFilled; + } + + /// + /// Get bytes available for output in window + /// + /// Number of bytes filled + public int GetAvailable() + { + return windowFilled; + } + + /// + /// Copy contents of window to output + /// + /// buffer to copy to + /// offset to start at + /// number of bytes to count + /// The number of bytes copied + /// + /// If a window underflow occurs + /// + public int CopyOutput(byte[] output, int offset, int len) + { + int copyEnd = windowEnd; + if (len > windowFilled) + { + len = windowFilled; + } + else + { + copyEnd = (windowEnd - windowFilled + len) & WindowMask; + } + + int copied = len; + int tailLen = len - copyEnd; + + if (tailLen > 0) + { + System.Array.Copy(window, WindowSize - tailLen, output, offset, tailLen); + offset += tailLen; + len = copyEnd; + } + System.Array.Copy(window, copyEnd - len, output, offset, len); + windowFilled -= copied; + if (windowFilled < 0) + { + throw new InvalidOperationException(); + } + return copied; + } + + /// + /// Reset by clearing window so GetAvailable returns 0 + /// + public void Reset() + { + windowFilled = windowEnd = 0; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs new file mode 100644 index 0000000..00f22b2 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs @@ -0,0 +1,298 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + /// + /// This class allows us to retrieve a specified number of bits from + /// the input buffer, as well as copy big byte blocks. + /// + /// It uses an int buffer to store up to 31 bits for direct + /// manipulation. This guarantees that we can get at least 16 bits, + /// but we only need at most 15, so this is all safe. + /// + /// There are some optimizations in this class, for example, you must + /// never peek more than 8 bits more than needed, and you must first + /// peek bits before you may drop them. This is not a general purpose + /// class but optimized for the behaviour of the Inflater. + /// + /// authors of the original java version : John Leuner, Jochen Hoenicke + /// + public class StreamManipulator + { + /// + /// Get the next sequence of bits but don't increase input pointer. bitCount must be + /// less or equal 16 and if this call succeeds, you must drop + /// at least n - 8 bits in the next call. + /// + /// The number of bits to peek. + /// + /// the value of the bits, or -1 if not enough bits available. */ + /// + public int PeekBits(int bitCount) + { + if (bitsInBuffer_ < bitCount) + { + if (windowStart_ == windowEnd_) + { + return -1; // ok + } + buffer_ |= (uint)((window_[windowStart_++] & 0xff | + (window_[windowStart_++] & 0xff) << 8) << bitsInBuffer_); + bitsInBuffer_ += 16; + } + return (int)(buffer_ & ((1 << bitCount) - 1)); + } + + /// + /// Tries to grab the next bits from the input and + /// sets to the value, adding . + /// + /// true if enough bits could be read, otherwise false + public bool TryGetBits(int bitCount, ref int output, int outputOffset = 0) + { + var bits = PeekBits(bitCount); + if (bits < 0) + { + return false; + } + output = bits + outputOffset; + DropBits(bitCount); + return true; + } + + /// + /// Tries to grab the next bits from the input and + /// sets of to the value. + /// + /// true if enough bits could be read, otherwise false + public bool TryGetBits(int bitCount, ref byte[] array, int index) + { + var bits = PeekBits(bitCount); + if (bits < 0) + { + return false; + } + array[index] = (byte)bits; + DropBits(bitCount); + return true; + } + + /// + /// Drops the next n bits from the input. You should have called PeekBits + /// with a bigger or equal n before, to make sure that enough bits are in + /// the bit buffer. + /// + /// The number of bits to drop. + public void DropBits(int bitCount) + { + buffer_ >>= bitCount; + bitsInBuffer_ -= bitCount; + } + + /// + /// Gets the next n bits and increases input pointer. This is equivalent + /// to followed by , except for correct error handling. + /// + /// The number of bits to retrieve. + /// + /// the value of the bits, or -1 if not enough bits available. + /// + public int GetBits(int bitCount) + { + int bits = PeekBits(bitCount); + if (bits >= 0) + { + DropBits(bitCount); + } + return bits; + } + + /// + /// Gets the number of bits available in the bit buffer. This must be + /// only called when a previous PeekBits() returned -1. + /// + /// + /// the number of bits available. + /// + public int AvailableBits + { + get + { + return bitsInBuffer_; + } + } + + /// + /// Gets the number of bytes available. + /// + /// + /// The number of bytes available. + /// + public int AvailableBytes + { + get + { + return windowEnd_ - windowStart_ + (bitsInBuffer_ >> 3); + } + } + + /// + /// Skips to the next byte boundary. + /// + public void SkipToByteBoundary() + { + buffer_ >>= (bitsInBuffer_ & 7); + bitsInBuffer_ &= ~7; + } + + /// + /// Returns true when SetInput can be called + /// + public bool IsNeedingInput + { + get + { + return windowStart_ == windowEnd_; + } + } + + /// + /// Copies bytes from input buffer to output buffer starting + /// at output[offset]. You have to make sure, that the buffer is + /// byte aligned. If not enough bytes are available, copies fewer + /// bytes. + /// + /// + /// The buffer to copy bytes to. + /// + /// + /// The offset in the buffer at which copying starts + /// + /// + /// The length to copy, 0 is allowed. + /// + /// + /// The number of bytes copied, 0 if no bytes were available. + /// + /// + /// Length is less than zero + /// + /// + /// Bit buffer isnt byte aligned + /// + public int CopyBytes(byte[] output, int offset, int length) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + if ((bitsInBuffer_ & 7) != 0) + { + // bits_in_buffer may only be 0 or a multiple of 8 + throw new InvalidOperationException("Bit buffer is not byte aligned!"); + } + + int count = 0; + while ((bitsInBuffer_ > 0) && (length > 0)) + { + output[offset++] = (byte)buffer_; + buffer_ >>= 8; + bitsInBuffer_ -= 8; + length--; + count++; + } + + if (length == 0) + { + return count; + } + + int avail = windowEnd_ - windowStart_; + if (length > avail) + { + length = avail; + } + System.Array.Copy(window_, windowStart_, output, offset, length); + windowStart_ += length; + + if (((windowStart_ - windowEnd_) & 1) != 0) + { + // We always want an even number of bytes in input, see peekBits + buffer_ = (uint)(window_[windowStart_++] & 0xff); + bitsInBuffer_ = 8; + } + return count + length; + } + + /// + /// Resets state and empties internal buffers + /// + public void Reset() + { + buffer_ = 0; + windowStart_ = windowEnd_ = bitsInBuffer_ = 0; + } + + /// + /// Add more input for consumption. + /// Only call when IsNeedingInput returns true + /// + /// data to be input + /// offset of first byte of input + /// number of bytes of input to add. + public void SetInput(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Cannot be negative"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Cannot be negative"); + } + + if (windowStart_ < windowEnd_) + { + throw new InvalidOperationException("Old input was not completely processed"); + } + + int end = offset + count; + + // We want to throw an ArrayIndexOutOfBoundsException early. + // Note the check also handles integer wrap around. + if ((offset > end) || (end > buffer.Length)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if ((count & 1) != 0) + { + // We always want an even number of bytes in input, see PeekBits + buffer_ |= (uint)((buffer[offset++] & 0xff) << bitsInBuffer_); + bitsInBuffer_ += 8; + } + + window_ = buffer; + windowStart_ = offset; + windowEnd_ = end; + } + + #region Instance Fields + + private byte[] window_; + private int windowStart_; + private int windowEnd_; + + private uint buffer_; + private int bitsInBuffer_; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/FastZip.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/FastZip.cs new file mode 100644 index 0000000..56ea08d --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/FastZip.cs @@ -0,0 +1,1002 @@ +using BSP_ICSharpCode.SharpZipLib.Core; +using BSP_ICSharpCode.SharpZipLib.Zip.Compression; +using System; +using System.IO; +using static BSP_ICSharpCode.SharpZipLib.Zip.Compression.Deflater; +using static BSP_ICSharpCode.SharpZipLib.Zip.ZipEntryFactory; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// FastZipEvents supports all events applicable to FastZip operations. + /// + public class FastZipEvents + { + /// + /// Delegate to invoke when processing directories. + /// + public event EventHandler ProcessDirectory; + + /// + /// Delegate to invoke when processing files. + /// + public ProcessFileHandler ProcessFile; + + /// + /// Delegate to invoke during processing of files. + /// + public ProgressHandler Progress; + + /// + /// Delegate to invoke when processing for a file has been completed. + /// + public CompletedFileHandler CompletedFile; + + /// + /// Delegate to invoke when processing directory failures. + /// + public DirectoryFailureHandler DirectoryFailure; + + /// + /// Delegate to invoke when processing file failures. + /// + public FileFailureHandler FileFailure; + + /// + /// Raise the directory failure event. + /// + /// The directory causing the failure. + /// The exception for this event. + /// A boolean indicating if execution should continue or not. + public bool OnDirectoryFailure(string directory, Exception e) + { + bool result = false; + DirectoryFailureHandler handler = DirectoryFailure; + + if (handler != null) + { + var args = new ScanFailureEventArgs(directory, e); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the file failure handler delegate. + /// + /// The file causing the failure. + /// The exception for this failure. + /// A boolean indicating if execution should continue or not. + public bool OnFileFailure(string file, Exception e) + { + FileFailureHandler handler = FileFailure; + bool result = (handler != null); + + if (result) + { + var args = new ScanFailureEventArgs(file, e); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the ProcessFile delegate. + /// + /// The file being processed. + /// A boolean indicating if execution should continue or not. + public bool OnProcessFile(string file) + { + bool result = true; + ProcessFileHandler handler = ProcessFile; + + if (handler != null) + { + var args = new ScanEventArgs(file); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the delegate + /// + /// The file whose processing has been completed. + /// A boolean indicating if execution should continue or not. + public bool OnCompletedFile(string file) + { + bool result = true; + CompletedFileHandler handler = CompletedFile; + if (handler != null) + { + var args = new ScanEventArgs(file); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the process directory delegate. + /// + /// The directory being processed. + /// Flag indicating if the directory has matching files as determined by the current filter. + /// A of true if the operation should continue; false otherwise. + public bool OnProcessDirectory(string directory, bool hasMatchingFiles) + { + bool result = true; + EventHandler handler = ProcessDirectory; + if (handler != null) + { + var args = new DirectoryEventArgs(directory, hasMatchingFiles); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// The minimum timespan between events. + /// + /// The minimum period of time between events. + /// + /// The default interval is three seconds. + public TimeSpan ProgressInterval + { + get { return progressInterval_; } + set { progressInterval_ = value; } + } + + #region Instance Fields + + private TimeSpan progressInterval_ = TimeSpan.FromSeconds(3); + + #endregion Instance Fields + } + + /// + /// FastZip provides facilities for creating and extracting zip files. + /// + public class FastZip + { + #region Enumerations + + /// + /// Defines the desired handling when overwriting files during extraction. + /// + public enum Overwrite + { + /// + /// Prompt the user to confirm overwriting + /// + Prompt, + + /// + /// Never overwrite files. + /// + Never, + + /// + /// Always overwrite files. + /// + Always + } + + #endregion Enumerations + + #region Constructors + + /// + /// Initialise a default instance of . + /// + public FastZip() + { + } + + /// + /// Initialise a new instance of using the specified + /// + /// The time setting to use when creating or extracting Zip entries. + /// Using TimeSetting.LastAccessTime[Utc] when + /// creating an archive will set the file time to the moment of reading. + /// + public FastZip(TimeSetting timeSetting) + { + entryFactory_ = new ZipEntryFactory(timeSetting); + restoreDateTimeOnExtract_ = true; + } + + /// + /// Initialise a new instance of using the specified + /// + /// The time to set all values for created or extracted Zip Entries. + public FastZip(DateTime time) + { + entryFactory_ = new ZipEntryFactory(time); + restoreDateTimeOnExtract_ = true; + } + + /// + /// Initialise a new instance of + /// + /// The events to use during operations. + public FastZip(FastZipEvents events) + { + events_ = events; + } + + #endregion Constructors + + #region Properties + + /// + /// Get/set a value indicating whether empty directories should be created. + /// + public bool CreateEmptyDirectories + { + get { return createEmptyDirectories_; } + set { createEmptyDirectories_ = value; } + } + + /// + /// Get / set the password value. + /// + public string Password + { + get { return password_; } + set { password_ = value; } + } + + /// + /// Get / set the method of encrypting entries. + /// + /// + /// Only applies when is set. + /// Defaults to ZipCrypto for backwards compatibility purposes. + /// + public ZipEncryptionMethod EntryEncryptionMethod { get; set; } = ZipEncryptionMethod.ZipCrypto; + + /// + /// Get or set the active when creating Zip files. + /// + /// + public INameTransform NameTransform + { + get { return entryFactory_.NameTransform; } + set + { + entryFactory_.NameTransform = value; + } + } + + /// + /// Get or set the active when creating Zip files. + /// + public IEntryFactory EntryFactory + { + get { return entryFactory_; } + set + { + if (value == null) + { + entryFactory_ = new ZipEntryFactory(); + } + else + { + entryFactory_ = value; + } + } + } + + /// + /// Gets or sets the setting for Zip64 handling when writing. + /// + /// + /// The default value is dynamic which is not backwards compatible with old + /// programs and can cause problems with XP's built in compression which cant + /// read Zip64 archives. However it does avoid the situation were a large file + /// is added and cannot be completed correctly. + /// NOTE: Setting the size for entries before they are added is the best solution! + /// By default the EntryFactory used by FastZip will set the file size. + /// + public UseZip64 UseZip64 + { + get { return useZip64_; } + set { useZip64_ = value; } + } + + /// + /// Get/set a value indicating whether file dates and times should + /// be restored when extracting files from an archive. + /// + /// The default value is false. + public bool RestoreDateTimeOnExtract + { + get + { + return restoreDateTimeOnExtract_; + } + set + { + restoreDateTimeOnExtract_ = value; + } + } + + /// + /// Get/set a value indicating whether file attributes should + /// be restored during extract operations + /// + public bool RestoreAttributesOnExtract + { + get { return restoreAttributesOnExtract_; } + set { restoreAttributesOnExtract_ = value; } + } + + /// + /// Get/set the Compression Level that will be used + /// when creating the zip + /// + public Deflater.CompressionLevel CompressionLevel + { + get { return compressionLevel_; } + set { compressionLevel_ = value; } + } + + /// + /// Reflects the opposite of the internal , setting it to false overrides the encoding used for reading and writing zip entries + /// + public bool UseUnicode + { + get => !_stringCodec.ForceZipLegacyEncoding; + set => _stringCodec.ForceZipLegacyEncoding = !value; + } + + /// Gets or sets the code page used for reading/writing zip file entries when unicode is disabled + public int LegacyCodePage + { + get => _stringCodec.CodePage; + set => _stringCodec = StringCodec.FromCodePage(value); + } + + /// + public StringCodec StringCodec + { + get => _stringCodec; + set => _stringCodec = value; + } + + #endregion Properties + + #region Delegates + + /// + /// Delegate called when confirming overwriting of files. + /// + public delegate bool ConfirmOverwriteDelegate(string fileName); + + #endregion Delegates + + #region CreateZip + + /// + /// Create a zip file. + /// + /// The name of the zip file to create. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + /// The directory filter to apply. + public void CreateZip(string zipFileName, string sourceDirectory, + bool recurse, string fileFilter, string directoryFilter) + { + CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, directoryFilter); + } + + /// + /// Create a zip file/archive. + /// + /// The name of the zip file to create. + /// The directory to obtain files and directories from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, string fileFilter) + { + CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, null); + } + + /// + /// Create a zip archive sending output to the passed. + /// + /// The stream to write archive data to. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + /// The directory filter to apply. + /// The is closed after creation. + public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter) + { + CreateZip(outputStream, sourceDirectory, recurse, fileFilter, directoryFilter, false); + } + + /// + /// Create a zip archive sending output to the passed. + /// + /// The stream to write archive data to. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + /// The directory filter to apply. + /// true to leave open after the zip has been created, false to dispose it. + public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter, bool leaveOpen) + { + var scanner = new FileSystemScanner(fileFilter, directoryFilter); + CreateZip(outputStream, sourceDirectory, recurse, scanner, leaveOpen); + } + + /// + /// Create a zip file. + /// + /// The name of the zip file to create. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + /// The directory filter to apply. + public void CreateZip(string zipFileName, string sourceDirectory, + bool recurse, IScanFilter fileFilter, IScanFilter directoryFilter) + { + CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, directoryFilter, false); + } + + /// + /// Create a zip archive sending output to the passed. + /// + /// The stream to write archive data to. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + /// The directory filter to apply. + /// true to leave open after the zip has been created, false to dispose it. + public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, IScanFilter fileFilter, IScanFilter directoryFilter, bool leaveOpen = false) + { + var scanner = new FileSystemScanner(fileFilter, directoryFilter); + CreateZip(outputStream, sourceDirectory, recurse, scanner, leaveOpen); + } + + /// + /// Create a zip archive sending output to the passed. + /// + /// The stream to write archive data to. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// For performing the actual file system scan + /// true to leave open after the zip has been created, false to dispose it. + /// The is closed after creation. + private void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, FileSystemScanner scanner, bool leaveOpen) + { + NameTransform = new ZipNameTransform(sourceDirectory); + sourceDirectory_ = sourceDirectory; + + using (outputStream_ = new ZipOutputStream(outputStream, _stringCodec)) + { + outputStream_.SetLevel((int)CompressionLevel); + outputStream_.IsStreamOwner = !leaveOpen; + outputStream_.NameTransform = null; // all required transforms handled by us + + if (false == string.IsNullOrEmpty(password_) && EntryEncryptionMethod != ZipEncryptionMethod.None) + { + outputStream_.Password = password_; + } + + outputStream_.UseZip64 = UseZip64; + scanner.ProcessFile += ProcessFile; + if (this.CreateEmptyDirectories) + { + scanner.ProcessDirectory += ProcessDirectory; + } + + if (events_ != null) + { + if (events_.FileFailure != null) + { + scanner.FileFailure += events_.FileFailure; + } + + if (events_.DirectoryFailure != null) + { + scanner.DirectoryFailure += events_.DirectoryFailure; + } + } + + scanner.Scan(sourceDirectory, recurse); + } + } + + #endregion CreateZip + + #region ExtractZip + + /// + /// Extract the contents of a zip file. + /// + /// The zip file to extract from. + /// The directory to save extracted information in. + /// A filter to apply to files. + public void ExtractZip(string zipFileName, string targetDirectory, string fileFilter) + { + ExtractZip(zipFileName, targetDirectory, Overwrite.Always, null, fileFilter, null, restoreDateTimeOnExtract_); + } + + /// + /// Extract the contents of a zip file. + /// + /// The zip file to extract from. + /// The directory to save extracted information in. + /// The style of overwriting to apply. + /// A delegate to invoke when confirming overwriting. + /// A filter to apply to files. + /// A filter to apply to directories. + /// Flag indicating whether to restore the date and time for extracted files. + /// Allow parent directory traversal in file paths (e.g. ../file) + public void ExtractZip(string zipFileName, string targetDirectory, + Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate, + string fileFilter, string directoryFilter, bool restoreDateTime, bool allowParentTraversal = false) + { + Stream inputStream = File.Open(zipFileName, FileMode.Open, FileAccess.Read, FileShare.Read); + ExtractZip(inputStream, targetDirectory, overwrite, confirmDelegate, fileFilter, directoryFilter, restoreDateTime, true, allowParentTraversal); + } + + /// + /// Extract the contents of a zip file held in a stream. + /// + /// The seekable input stream containing the zip to extract from. + /// The directory to save extracted information in. + /// The style of overwriting to apply. + /// A delegate to invoke when confirming overwriting. + /// A filter to apply to files. + /// A filter to apply to directories. + /// Flag indicating whether to restore the date and time for extracted files. + /// Flag indicating whether the inputStream will be closed by this method. + /// Allow parent directory traversal in file paths (e.g. ../file) + public void ExtractZip(Stream inputStream, string targetDirectory, + Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate, + string fileFilter, string directoryFilter, bool restoreDateTime, + bool isStreamOwner, bool allowParentTraversal = false) + { + if ((overwrite == Overwrite.Prompt) && (confirmDelegate == null)) + { + throw new ArgumentNullException(nameof(confirmDelegate)); + } + + continueRunning_ = true; + overwrite_ = overwrite; + confirmDelegate_ = confirmDelegate; + extractNameTransform_ = new WindowsNameTransform(targetDirectory, allowParentTraversal); + + fileFilter_ = new NameFilter(fileFilter); + directoryFilter_ = new NameFilter(directoryFilter); + restoreDateTimeOnExtract_ = restoreDateTime; + + using (zipFile_ = new ZipFile(inputStream, !isStreamOwner, _stringCodec)) + { + if (password_ != null) + { + zipFile_.Password = password_; + } + + System.Collections.IEnumerator enumerator = zipFile_.GetEnumerator(); + while (continueRunning_ && enumerator.MoveNext()) + { + var entry = (ZipEntry)enumerator.Current; + if (entry.IsFile) + { + // TODO Path.GetDirectory can fail here on invalid characters. + if (directoryFilter_.IsMatch(Path.GetDirectoryName(entry.Name)) && fileFilter_.IsMatch(entry.Name)) + { + ExtractEntry(entry); + } + } + else if (entry.IsDirectory) + { + if (directoryFilter_.IsMatch(entry.Name) && CreateEmptyDirectories) + { + ExtractEntry(entry); + } + } + else + { + // Do nothing for volume labels etc... + } + } + } + } + + #endregion ExtractZip + + #region Internal Processing + + private void ProcessDirectory(object sender, DirectoryEventArgs e) + { + if (!e.HasMatchingFiles && CreateEmptyDirectories) + { + if (events_ != null) + { + events_.OnProcessDirectory(e.Name, e.HasMatchingFiles); + } + + if (e.ContinueRunning) + { + if (e.Name != sourceDirectory_) + { + ZipEntry entry = entryFactory_.MakeDirectoryEntry(e.Name); + outputStream_.PutNextEntry(entry); + } + } + } + } + + private void ProcessFile(object sender, ScanEventArgs e) + { + if ((events_ != null) && (events_.ProcessFile != null)) + { + events_.ProcessFile(sender, e); + } + + if (e.ContinueRunning) + { + try + { + // The open below is equivalent to OpenRead which guarantees that if opened the + // file will not be changed by subsequent openers, but precludes opening in some cases + // were it could succeed. ie the open may fail as its already open for writing and the share mode should reflect that. + using (FileStream stream = File.Open(e.Name, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + ZipEntry entry = entryFactory_.MakeFileEntry(e.Name); + if (_stringCodec.ForceZipLegacyEncoding) + { + entry.IsUnicodeText = false; + } + + // Set up AES encryption for the entry if required. + ConfigureEntryEncryption(entry); + + outputStream_.PutNextEntry(entry); + AddFileContents(e.Name, stream); + } + } + catch (Exception ex) + { + if (events_ != null) + { + continueRunning_ = events_.OnFileFailure(e.Name, ex); + } + else + { + continueRunning_ = false; + throw; + } + } + } + } + + // Set up the encryption method to use for the specific entry. + private void ConfigureEntryEncryption(ZipEntry entry) + { + // Only alter the entries options if AES isn't already enabled for it + // (it might have been set up by the entry factory, and if so we let that take precedence) + if (!string.IsNullOrEmpty(Password) && entry.AESEncryptionStrength == 0) + { + switch (EntryEncryptionMethod) + { + case ZipEncryptionMethod.AES128: + entry.AESKeySize = 128; + break; + + case ZipEncryptionMethod.AES256: + entry.AESKeySize = 256; + break; + } + } + } + + private void AddFileContents(string name, Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (buffer_ == null) + { + buffer_ = new byte[4096]; + } + + if ((events_ != null) && (events_.Progress != null)) + { + StreamUtils.Copy(stream, outputStream_, buffer_, + events_.Progress, events_.ProgressInterval, this, name); + } + else + { + StreamUtils.Copy(stream, outputStream_, buffer_); + } + + if (events_ != null) + { + continueRunning_ = events_.OnCompletedFile(name); + } + } + + private void ExtractFileEntry(ZipEntry entry, string targetName) + { + bool proceed = true; + if (overwrite_ != Overwrite.Always) + { + if (File.Exists(targetName)) + { + if ((overwrite_ == Overwrite.Prompt) && (confirmDelegate_ != null)) + { + proceed = confirmDelegate_(targetName); + } + else + { + proceed = false; + } + } + } + + if (proceed) + { + if (events_ != null) + { + continueRunning_ = events_.OnProcessFile(entry.Name); + } + + if (continueRunning_) + { + try + { + using (FileStream outputStream = File.Create(targetName)) + { + if (buffer_ == null) + { + buffer_ = new byte[4096]; + } + + using (var inputStream = zipFile_.GetInputStream(entry)) + { + if ((events_ != null) && (events_.Progress != null)) + { + StreamUtils.Copy(inputStream, outputStream, buffer_, + events_.Progress, events_.ProgressInterval, this, entry.Name, entry.Size); + } + else + { + StreamUtils.Copy(inputStream, outputStream, buffer_); + } + } + + if (events_ != null) + { + continueRunning_ = events_.OnCompletedFile(entry.Name); + } + } + + if (restoreDateTimeOnExtract_) + { + switch (entryFactory_.Setting) + { + case TimeSetting.CreateTime: + File.SetCreationTime(targetName, entry.DateTime); + break; + + case TimeSetting.CreateTimeUtc: + File.SetCreationTimeUtc(targetName, entry.DateTime); + break; + + case TimeSetting.LastAccessTime: + File.SetLastAccessTime(targetName, entry.DateTime); + break; + + case TimeSetting.LastAccessTimeUtc: + File.SetLastAccessTimeUtc(targetName, entry.DateTime); + break; + + case TimeSetting.LastWriteTime: + File.SetLastWriteTime(targetName, entry.DateTime); + break; + + case TimeSetting.LastWriteTimeUtc: + File.SetLastWriteTimeUtc(targetName, entry.DateTime); + break; + + case TimeSetting.Fixed: + File.SetLastWriteTime(targetName, entryFactory_.FixedDateTime); + break; + + default: + throw new ZipException("Unhandled time setting in ExtractFileEntry"); + } + } + + if (RestoreAttributesOnExtract && entry.IsDOSEntry && (entry.ExternalFileAttributes != -1)) + { + var fileAttributes = (FileAttributes)entry.ExternalFileAttributes; + // TODO: FastZip - Setting of other file attributes on extraction is a little trickier. + fileAttributes &= (FileAttributes.Archive | FileAttributes.Normal | FileAttributes.ReadOnly | FileAttributes.Hidden); + File.SetAttributes(targetName, fileAttributes); + } + } + catch (Exception ex) + { + if (events_ != null) + { + continueRunning_ = events_.OnFileFailure(targetName, ex); + } + else + { + continueRunning_ = false; + throw; + } + } + } + } + } + + private void ExtractEntry(ZipEntry entry) + { + bool doExtraction = entry.IsCompressionMethodSupported(); + string targetName = entry.Name; + + if (doExtraction) + { + if (entry.IsFile) + { + targetName = extractNameTransform_.TransformFile(targetName); + } + else if (entry.IsDirectory) + { + targetName = extractNameTransform_.TransformDirectory(targetName); + } + + doExtraction = !(string.IsNullOrEmpty(targetName)); + } + + // TODO: Fire delegate/throw exception were compression method not supported, or name is invalid? + + string dirName = string.Empty; + + if (doExtraction) + { + if (entry.IsDirectory) + { + dirName = targetName; + } + else + { + dirName = Path.GetDirectoryName(Path.GetFullPath(targetName)); + } + } + + if (doExtraction && !Directory.Exists(dirName)) + { + if (!entry.IsDirectory || CreateEmptyDirectories) + { + try + { + continueRunning_ = events_?.OnProcessDirectory(dirName, true) ?? true; + if (continueRunning_) + { + Directory.CreateDirectory(dirName); + if (entry.IsDirectory && restoreDateTimeOnExtract_) + { + switch (entryFactory_.Setting) + { + case TimeSetting.CreateTime: + Directory.SetCreationTime(dirName, entry.DateTime); + break; + + case TimeSetting.CreateTimeUtc: + Directory.SetCreationTimeUtc(dirName, entry.DateTime); + break; + + case TimeSetting.LastAccessTime: + Directory.SetLastAccessTime(dirName, entry.DateTime); + break; + + case TimeSetting.LastAccessTimeUtc: + Directory.SetLastAccessTimeUtc(dirName, entry.DateTime); + break; + + case TimeSetting.LastWriteTime: + Directory.SetLastWriteTime(dirName, entry.DateTime); + break; + + case TimeSetting.LastWriteTimeUtc: + Directory.SetLastWriteTimeUtc(dirName, entry.DateTime); + break; + + case TimeSetting.Fixed: + Directory.SetLastWriteTime(dirName, entryFactory_.FixedDateTime); + break; + + default: + throw new ZipException("Unhandled time setting in ExtractEntry"); + } + } + } + else + { + doExtraction = false; + } + } + catch (Exception ex) + { + doExtraction = false; + if (events_ != null) + { + if (entry.IsDirectory) + { + continueRunning_ = events_.OnDirectoryFailure(targetName, ex); + } + else + { + continueRunning_ = events_.OnFileFailure(targetName, ex); + } + } + else + { + continueRunning_ = false; + throw; + } + } + } + } + + if (doExtraction && entry.IsFile) + { + ExtractFileEntry(entry, targetName); + } + } + + private static int MakeExternalAttributes(FileInfo info) + { + return (int)info.Attributes; + } + + private static bool NameIsValid(string name) + { + return !string.IsNullOrEmpty(name) && + (name.IndexOfAny(Path.GetInvalidPathChars()) < 0); + } + + #endregion Internal Processing + + #region Instance Fields + + private bool continueRunning_; + private byte[] buffer_; + private ZipOutputStream outputStream_; + private ZipFile zipFile_; + private string sourceDirectory_; + private NameFilter fileFilter_; + private NameFilter directoryFilter_; + private Overwrite overwrite_; + private ConfirmOverwriteDelegate confirmDelegate_; + + private bool restoreDateTimeOnExtract_; + private bool restoreAttributesOnExtract_; + private bool createEmptyDirectories_; + private FastZipEvents events_; + private IEntryFactory entryFactory_ = new ZipEntryFactory(); + private INameTransform extractNameTransform_; + private UseZip64 useZip64_ = UseZip64.Dynamic; + private CompressionLevel compressionLevel_ = CompressionLevel.DEFAULT_COMPRESSION; + private StringCodec _stringCodec = ZipStrings.GetStringCodec(); + private string password_; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/IEntryFactory.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/IEntryFactory.cs new file mode 100644 index 0000000..2ebd7f6 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/IEntryFactory.cs @@ -0,0 +1,67 @@ +using System; +using BSP_ICSharpCode.SharpZipLib.Core; +using static BSP_ICSharpCode.SharpZipLib.Zip.ZipEntryFactory; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// Defines factory methods for creating new values. + /// + public interface IEntryFactory + { + /// + /// Create a for a file given its name + /// + /// The name of the file to create an entry for. + /// Returns a file entry based on the passed. + ZipEntry MakeFileEntry(string fileName); + + /// + /// Create a for a file given its name + /// + /// The name of the file to create an entry for. + /// If true get details from the file system if the file exists. + /// Returns a file entry based on the passed. + ZipEntry MakeFileEntry(string fileName, bool useFileSystem); + + /// + /// Create a for a file given its actual name and optional override name + /// + /// The name of the file to create an entry for. + /// An alternative name to be used for the new entry. Null if not applicable. + /// If true get details from the file system if the file exists. + /// Returns a file entry based on the passed. + ZipEntry MakeFileEntry(string fileName, string entryName, bool useFileSystem); + + /// + /// Create a for a directory given its name + /// + /// The name of the directory to create an entry for. + /// Returns a directory entry based on the passed. + ZipEntry MakeDirectoryEntry(string directoryName); + + /// + /// Create a for a directory given its name + /// + /// The name of the directory to create an entry for. + /// If true get details from the file system for this directory if it exists. + /// Returns a directory entry based on the passed. + ZipEntry MakeDirectoryEntry(string directoryName, bool useFileSystem); + + /// + /// Get/set the applicable. + /// + INameTransform NameTransform { get; set; } + + /// + /// Get the in use. + /// + TimeSetting Setting { get; } + + /// + /// Get the value to use when is set to , + /// or if not specified, the value of when the class was the initialized + /// + DateTime FixedDateTime { get; } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs new file mode 100644 index 0000000..dfea421 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs @@ -0,0 +1,266 @@ +using BSP_ICSharpCode.SharpZipLib.Core; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// WindowsNameTransform transforms names to windows compatible ones. + /// + public class WindowsNameTransform : INameTransform + { + /// + /// The maximum windows path name permitted. + /// + /// This may not valid for all windows systems - CE?, etc but I cant find the equivalent in the CLR. + private const int MaxPath = 260; + + private string _baseDirectory; + private bool _trimIncomingPaths; + private char _replacementChar = '_'; + private bool _allowParentTraversal; + + /// + /// In this case we need Windows' invalid path characters. + /// Path.GetInvalidPathChars() only returns a subset invalid on all platforms. + /// + private static readonly char[] InvalidEntryChars = new char[] { + '"', '<', '>', '|', '\0', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', + '\u0006', '\a', '\b', '\t', '\n', '\v', '\f', '\r', '\u000e', '\u000f', + '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', + '\u0017', '\u0018', '\u0019', '\u001a', '\u001b', '\u001c', '\u001d', + '\u001e', '\u001f', + // extra characters for masks, etc. + '*', '?', ':' + }; + + /// + /// Initialises a new instance of + /// + /// + /// Allow parent directory traversal in file paths (e.g. ../file) + public WindowsNameTransform(string baseDirectory, bool allowParentTraversal = false) + { + BaseDirectory = baseDirectory ?? throw new ArgumentNullException(nameof(baseDirectory), "Directory name is invalid"); + AllowParentTraversal = allowParentTraversal; + } + + /// + /// Initialise a default instance of + /// + public WindowsNameTransform() + { + // Do nothing. + } + + /// + /// Gets or sets a value containing the target directory to prefix values with. + /// + public string BaseDirectory + { + get { return _baseDirectory; } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _baseDirectory = Path.GetFullPath(value); + } + } + + /// + /// Allow parent directory traversal in file paths (e.g. ../file) + /// + public bool AllowParentTraversal + { + get => _allowParentTraversal; + set => _allowParentTraversal = value; + } + + /// + /// Gets or sets a value indicating whether paths on incoming values should be removed. + /// + public bool TrimIncomingPaths + { + get { return _trimIncomingPaths; } + set { _trimIncomingPaths = value; } + } + + /// + /// Transform a Zip directory name to a windows directory name. + /// + /// The directory name to transform. + /// The transformed name. + public string TransformDirectory(string name) + { + name = TransformFile(name); + if (name.Length > 0) + { + while (name.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)) + { + name = name.Remove(name.Length - 1, 1); + } + } + else + { + throw new InvalidNameException("Cannot have an empty directory name"); + } + return name; + } + + /// + /// Transform a Zip format file name to a windows style one. + /// + /// The file name to transform. + /// The transformed name. + public string TransformFile(string name) + { + if (name != null) + { + name = MakeValidName(name, _replacementChar); + + if (_trimIncomingPaths) + { + name = Path.GetFileName(name); + } + + // This may exceed windows length restrictions. + // Combine will throw a PathTooLongException in that case. + if (_baseDirectory != null) + { + name = Path.Combine(_baseDirectory, name); + + // Ensure base directory ends with directory separator ('/' or '\' depending on OS) + var pathBase = Path.GetFullPath(_baseDirectory); + if (pathBase[pathBase.Length - 1] != Path.DirectorySeparatorChar) + { + pathBase += Path.DirectorySeparatorChar; + } + + if (!_allowParentTraversal && !Path.GetFullPath(name).StartsWith(pathBase, StringComparison.InvariantCultureIgnoreCase)) + { + throw new InvalidNameException("Parent traversal in paths is not allowed"); + } + } + } + else + { + name = string.Empty; + } + return name; + } + + /// + /// Test a name to see if it is a valid name for a windows filename as extracted from a Zip archive. + /// + /// The name to test. + /// Returns true if the name is a valid zip name; false otherwise. + /// The filename isnt a true windows path in some fundamental ways like no absolute paths, no rooted paths etc. + public static bool IsValidName(string name) + { + bool result = + (name != null) && + (name.Length <= MaxPath) && + (string.Compare(name, MakeValidName(name, '_'), StringComparison.Ordinal) == 0) + ; + + return result; + } + + /// + /// Force a name to be valid by replacing invalid characters with a fixed value + /// + /// The name to make valid + /// The replacement character to use for any invalid characters. + /// Returns a valid name + public static string MakeValidName(string name, char replacement) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + name = PathUtils.DropPathRoot(name.Replace("/", Path.DirectorySeparatorChar.ToString())); + + // Drop any leading slashes. + while ((name.Length > 0) && (name[0] == Path.DirectorySeparatorChar)) + { + name = name.Remove(0, 1); + } + + // Drop any trailing slashes. + while ((name.Length > 0) && (name[name.Length - 1] == Path.DirectorySeparatorChar)) + { + name = name.Remove(name.Length - 1, 1); + } + + // Convert consecutive \\ characters to \ + int index = name.IndexOf(string.Format("{0}{0}", Path.DirectorySeparatorChar), StringComparison.Ordinal); + while (index >= 0) + { + name = name.Remove(index, 1); + index = name.IndexOf(string.Format("{0}{0}", Path.DirectorySeparatorChar), StringComparison.Ordinal); + } + + // Convert any invalid characters using the replacement one. + index = name.IndexOfAny(InvalidEntryChars); + if (index >= 0) + { + var builder = new StringBuilder(name); + + while (index >= 0) + { + builder[index] = replacement; + + if (index >= name.Length) + { + index = -1; + } + else + { + index = name.IndexOfAny(InvalidEntryChars, index + 1); + } + } + name = builder.ToString(); + } + + // Check for names greater than MaxPath characters. + // TODO: Were is CLR version of MaxPath defined? Can't find it in Environment. + if (name.Length > MaxPath) + { + throw new PathTooLongException(); + } + + return name; + } + + /// + /// Gets or set the character to replace invalid characters during transformations. + /// + public char Replacement + { + get { return _replacementChar; } + set + { + for (int i = 0; i < InvalidEntryChars.Length; ++i) + { + if (InvalidEntryChars[i] == value) + { + throw new ArgumentException("invalid path character"); + } + } + + if ((value == Path.DirectorySeparatorChar) || (value == Path.AltDirectorySeparatorChar)) + { + throw new ArgumentException("invalid replacement character"); + } + + _replacementChar = value; + } + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipConstants.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipConstants.cs new file mode 100644 index 0000000..2a82e1d --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipConstants.cs @@ -0,0 +1,514 @@ +using System; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + #region Enumerations + + /// + /// Determines how entries are tested to see if they should use Zip64 extensions or not. + /// + public enum UseZip64 + { + /// + /// Zip64 will not be forced on entries during processing. + /// + /// An entry can have this overridden if required + Off, + + /// + /// Zip64 should always be used. + /// + On, + + /// + /// #ZipLib will determine use based on entry values when added to archive. + /// + Dynamic, + } + + /// + /// The kind of compression used for an entry in an archive + /// + public enum CompressionMethod + { + /// + /// A direct copy of the file contents is held in the archive + /// + Stored = 0, + + /// + /// Common Zip compression method using a sliding dictionary + /// of up to 32KB and secondary compression from Huffman/Shannon-Fano trees + /// + Deflated = 8, + + /// + /// An extension to deflate with a 64KB window. Not supported by #Zip currently + /// + Deflate64 = 9, + + /// + /// BZip2 compression. Not supported by #Zip. + /// + BZip2 = 12, + + /// + /// LZMA compression. Not supported by #Zip. + /// + LZMA = 14, + + /// + /// PPMd compression. Not supported by #Zip. + /// + PPMd = 98, + + /// + /// WinZip special for AES encryption, Now supported by #Zip. + /// + WinZipAES = 99, + } + + /// + /// Identifies the encryption algorithm used for an entry + /// + public enum EncryptionAlgorithm + { + /// + /// No encryption has been used. + /// + None = 0, + + /// + /// Encrypted using PKZIP 2.0 or 'classic' encryption. + /// + PkzipClassic = 1, + + /// + /// DES encryption has been used. + /// + Des = 0x6601, + + /// + /// RC2 encryption has been used for encryption. + /// + RC2 = 0x6602, + + /// + /// Triple DES encryption with 168 bit keys has been used for this entry. + /// + TripleDes168 = 0x6603, + + /// + /// Triple DES with 112 bit keys has been used for this entry. + /// + TripleDes112 = 0x6609, + + /// + /// AES 128 has been used for encryption. + /// + Aes128 = 0x660e, + + /// + /// AES 192 has been used for encryption. + /// + Aes192 = 0x660f, + + /// + /// AES 256 has been used for encryption. + /// + Aes256 = 0x6610, + + /// + /// RC2 corrected has been used for encryption. + /// + RC2Corrected = 0x6702, + + /// + /// Blowfish has been used for encryption. + /// + Blowfish = 0x6720, + + /// + /// Twofish has been used for encryption. + /// + Twofish = 0x6721, + + /// + /// RC4 has been used for encryption. + /// + RC4 = 0x6801, + + /// + /// An unknown algorithm has been used for encryption. + /// + Unknown = 0xffff + } + + /// + /// Defines the contents of the general bit flags field for an archive entry. + /// + [Flags] + public enum GeneralBitFlags + { + /// + /// Bit 0 if set indicates that the file is encrypted + /// + Encrypted = 0x0001, + + /// + /// Bits 1 and 2 - Two bits defining the compression method (only for Method 6 Imploding and 8,9 Deflating) + /// + Method = 0x0006, + + /// + /// Bit 3 if set indicates a trailing data descriptor is appended to the entry data + /// + Descriptor = 0x0008, + + /// + /// Bit 4 is reserved for use with method 8 for enhanced deflation + /// + ReservedPKware4 = 0x0010, + + /// + /// Bit 5 if set indicates the file contains Pkzip compressed patched data. + /// Requires version 2.7 or greater. + /// + Patched = 0x0020, + + /// + /// Bit 6 if set indicates strong encryption has been used for this entry. + /// + StrongEncryption = 0x0040, + + /// + /// Bit 7 is currently unused + /// + Unused7 = 0x0080, + + /// + /// Bit 8 is currently unused + /// + Unused8 = 0x0100, + + /// + /// Bit 9 is currently unused + /// + Unused9 = 0x0200, + + /// + /// Bit 10 is currently unused + /// + Unused10 = 0x0400, + + /// + /// Bit 11 if set indicates the filename and + /// comment fields for this file must be encoded using UTF-8. + /// + UnicodeText = 0x0800, + + /// + /// Bit 12 is documented as being reserved by PKware for enhanced compression. + /// + EnhancedCompress = 0x1000, + + /// + /// Bit 13 if set indicates that values in the local header are masked to hide + /// their actual values, and the central directory is encrypted. + /// + /// + /// Used when encrypting the central directory contents. + /// + HeaderMasked = 0x2000, + + /// + /// Bit 14 is documented as being reserved for use by PKware + /// + ReservedPkware14 = 0x4000, + + /// + /// Bit 15 is documented as being reserved for use by PKware + /// + ReservedPkware15 = 0x8000 + } + + /// + /// Helpers for + /// + public static class GeneralBitFlagsExtensions + { + /// + /// This is equivalent of in .NET Core, but since the .NET FW + /// version is really slow (due to un-/boxing and reflection) we use this wrapper. + /// + /// + /// + /// + public static bool Includes(this GeneralBitFlags flagData, GeneralBitFlags flag) => (flag & flagData) != 0; + } + + #endregion Enumerations + + /// + /// This class contains constants used for Zip format files + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "kept for backwards compatibility")] + public static class ZipConstants + { + #region Versions + + /// + /// The version made by field for entries in the central header when created by this library + /// + /// + /// This is also the Zip version for the library when comparing against the version required to extract + /// for an entry. See . + /// + public const int VersionMadeBy = 51; // was 45 before AES + + /// + /// The version made by field for entries in the central header when created by this library + /// + /// + /// This is also the Zip version for the library when comparing against the version required to extract + /// for an entry. See ZipInputStream.CanDecompressEntry. + /// + [Obsolete("Use VersionMadeBy instead")] + public const int VERSION_MADE_BY = 51; + + /// + /// The minimum version required to support strong encryption + /// + public const int VersionStrongEncryption = 50; + + /// + /// The minimum version required to support strong encryption + /// + [Obsolete("Use VersionStrongEncryption instead")] + public const int VERSION_STRONG_ENCRYPTION = 50; + + /// + /// Version indicating AES encryption + /// + public const int VERSION_AES = 51; + + /// + /// The version required for Zip64 extensions (4.5 or higher) + /// + public const int VersionZip64 = 45; + + /// + /// The version required for BZip2 compression (4.6 or higher) + /// + public const int VersionBZip2 = 46; + + #endregion Versions + + #region Header Sizes + + /// + /// Size of local entry header (excluding variable length fields at end) + /// + public const int LocalHeaderBaseSize = 30; + + /// + /// Size of local entry header (excluding variable length fields at end) + /// + [Obsolete("Use LocalHeaderBaseSize instead")] + public const int LOCHDR = 30; + + /// + /// Size of Zip64 data descriptor + /// + public const int Zip64DataDescriptorSize = 24; + + /// + /// Size of data descriptor + /// + public const int DataDescriptorSize = 16; + + /// + /// Size of data descriptor + /// + [Obsolete("Use DataDescriptorSize instead")] + public const int EXTHDR = 16; + + /// + /// Size of central header entry (excluding variable fields) + /// + public const int CentralHeaderBaseSize = 46; + + /// + /// Size of central header entry + /// + [Obsolete("Use CentralHeaderBaseSize instead")] + public const int CENHDR = 46; + + /// + /// Size of end of central record (excluding variable fields) + /// + public const int EndOfCentralRecordBaseSize = 22; + + /// + /// Size of end of central record (excluding variable fields) + /// + [Obsolete("Use EndOfCentralRecordBaseSize instead")] + public const int ENDHDR = 22; + + /// + /// Size of 'classic' cryptographic header stored before any entry data + /// + public const int CryptoHeaderSize = 12; + + /// + /// Size of cryptographic header stored before entry data + /// + [Obsolete("Use CryptoHeaderSize instead")] + public const int CRYPTO_HEADER_SIZE = 12; + + /// + /// The size of the Zip64 central directory locator. + /// + public const int Zip64EndOfCentralDirectoryLocatorSize = 20; + + #endregion Header Sizes + + #region Header Signatures + + /// + /// Signature for local entry header + /// + public const int LocalHeaderSignature = 'P' | ('K' << 8) | (3 << 16) | (4 << 24); + + /// + /// Signature for local entry header + /// + [Obsolete("Use LocalHeaderSignature instead")] + public const int LOCSIG = 'P' | ('K' << 8) | (3 << 16) | (4 << 24); + + /// + /// Signature for spanning entry + /// + public const int SpanningSignature = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for spanning entry + /// + [Obsolete("Use SpanningSignature instead")] + public const int SPANNINGSIG = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for temporary spanning entry + /// + public const int SpanningTempSignature = 'P' | ('K' << 8) | ('0' << 16) | ('0' << 24); + + /// + /// Signature for temporary spanning entry + /// + [Obsolete("Use SpanningTempSignature instead")] + public const int SPANTEMPSIG = 'P' | ('K' << 8) | ('0' << 16) | ('0' << 24); + + /// + /// Signature for data descriptor + /// + /// + /// This is only used where the length, Crc, or compressed size isnt known when the + /// entry is created and the output stream doesnt support seeking. + /// The local entry cannot be 'patched' with the correct values in this case + /// so the values are recorded after the data prefixed by this header, as well as in the central directory. + /// + public const int DataDescriptorSignature = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for data descriptor + /// + /// + /// This is only used where the length, Crc, or compressed size isnt known when the + /// entry is created and the output stream doesnt support seeking. + /// The local entry cannot be 'patched' with the correct values in this case + /// so the values are recorded after the data prefixed by this header, as well as in the central directory. + /// + [Obsolete("Use DataDescriptorSignature instead")] + public const int EXTSIG = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for central header + /// + [Obsolete("Use CentralHeaderSignature instead")] + public const int CENSIG = 'P' | ('K' << 8) | (1 << 16) | (2 << 24); + + /// + /// Signature for central header + /// + public const int CentralHeaderSignature = 'P' | ('K' << 8) | (1 << 16) | (2 << 24); + + /// + /// Signature for Zip64 central file header + /// + public const int Zip64CentralFileHeaderSignature = 'P' | ('K' << 8) | (6 << 16) | (6 << 24); + + /// + /// Signature for Zip64 central file header + /// + [Obsolete("Use Zip64CentralFileHeaderSignature instead")] + public const int CENSIG64 = 'P' | ('K' << 8) | (6 << 16) | (6 << 24); + + /// + /// Signature for Zip64 central directory locator + /// + public const int Zip64CentralDirLocatorSignature = 'P' | ('K' << 8) | (6 << 16) | (7 << 24); + + /// + /// Signature for archive extra data signature (were headers are encrypted). + /// + public const int ArchiveExtraDataSignature = 'P' | ('K' << 8) | (6 << 16) | (7 << 24); + + /// + /// Central header digital signature + /// + public const int CentralHeaderDigitalSignature = 'P' | ('K' << 8) | (5 << 16) | (5 << 24); + + /// + /// Central header digital signature + /// + [Obsolete("Use CentralHeaderDigitalSignaure instead")] + public const int CENDIGITALSIG = 'P' | ('K' << 8) | (5 << 16) | (5 << 24); + + /// + /// End of central directory record signature + /// + public const int EndOfCentralDirectorySignature = 'P' | ('K' << 8) | (5 << 16) | (6 << 24); + + /// + /// End of central directory record signature + /// + [Obsolete("Use EndOfCentralDirectorySignature instead")] + public const int ENDSIG = 'P' | ('K' << 8) | (5 << 16) | (6 << 24); + + #endregion Header Signatures + } + + /// + /// GeneralBitFlags helper extensions + /// + public static class GenericBitFlagsExtensions + { + /// + /// Efficiently check if any of the flags are set without enum un-/boxing + /// + /// + /// + /// Returns whether any of flags are set + public static bool HasAny(this GeneralBitFlags target, GeneralBitFlags flags) + => ((int)target & (int)flags) != 0; + + /// + /// Efficiently check if all the flags are set without enum un-/boxing + /// + /// + /// + /// Returns whether the flags are all set + public static bool HasAll(this GeneralBitFlags target, GeneralBitFlags flags) + => ((int)target & (int)flags) == (int)flags; + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEncryptionMethod.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEncryptionMethod.cs new file mode 100644 index 0000000..186ec1b --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEncryptionMethod.cs @@ -0,0 +1,28 @@ +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// The method of encrypting entries when creating zip archives. + /// + public enum ZipEncryptionMethod + { + /// + /// No encryption will be used. + /// + None, + + /// + /// Encrypt entries with ZipCrypto. + /// + ZipCrypto, + + /// + /// Encrypt entries with AES 128. + /// + AES128, + + /// + /// Encrypt entries with AES 256. + /// + AES256 + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntry.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntry.cs new file mode 100644 index 0000000..077b9e9 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntry.cs @@ -0,0 +1,1157 @@ +using System; +using System.IO; +using System.Text; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// Defines known values for the property. + /// + public enum HostSystemID + { + /// + /// Host system = MSDOS + /// + Msdos = 0, + + /// + /// Host system = Amiga + /// + Amiga = 1, + + /// + /// Host system = Open VMS + /// + OpenVms = 2, + + /// + /// Host system = Unix + /// + Unix = 3, + + /// + /// Host system = VMCms + /// + VMCms = 4, + + /// + /// Host system = Atari ST + /// + AtariST = 5, + + /// + /// Host system = OS2 + /// + OS2 = 6, + + /// + /// Host system = Macintosh + /// + Macintosh = 7, + + /// + /// Host system = ZSystem + /// + ZSystem = 8, + + /// + /// Host system = Cpm + /// + Cpm = 9, + + /// + /// Host system = Windows NT + /// + WindowsNT = 10, + + /// + /// Host system = MVS + /// + MVS = 11, + + /// + /// Host system = VSE + /// + Vse = 12, + + /// + /// Host system = Acorn RISC + /// + AcornRisc = 13, + + /// + /// Host system = VFAT + /// + Vfat = 14, + + /// + /// Host system = Alternate MVS + /// + AlternateMvs = 15, + + /// + /// Host system = BEOS + /// + BeOS = 16, + + /// + /// Host system = Tandem + /// + Tandem = 17, + + /// + /// Host system = OS400 + /// + OS400 = 18, + + /// + /// Host system = OSX + /// + OSX = 19, + + /// + /// Host system = WinZIP AES + /// + WinZipAES = 99, + } + + /// + /// This class represents an entry in a zip archive. This can be a file + /// or a directory + /// ZipFile and ZipInputStream will give you instances of this class as + /// information about the members in an archive. ZipOutputStream + /// uses an instance of this class when creating an entry in a Zip file. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ public class ZipEntry + { + [Flags] + private enum Known : byte + { + None = 0, + Size = 0x01, + CompressedSize = 0x02, + Crc = 0x04, + Time = 0x08, + ExternalAttributes = 0x10, + } + + #region Constructors + + /// + /// Creates a zip entry with the given name. + /// + /// + /// The name for this entry. Can include directory components. + /// The convention for names is 'unix' style paths with relative names only. + /// There are with no device names and path elements are separated by '/' characters. + /// + /// + /// The name passed is null + /// + public ZipEntry(string name) + : this(name, 0, ZipConstants.VersionMadeBy, CompressionMethod.Deflated, true) + { + } + + /// + /// Creates a zip entry with the given name and version required to extract + /// + /// + /// The name for this entry. Can include directory components. + /// The convention for names is 'unix' style paths with no device names and + /// path elements separated by '/' characters. This is not enforced see CleanName + /// on how to ensure names are valid if this is desired. + /// + /// + /// The minimum 'feature version' required this entry + /// + /// + /// The name passed is null + /// + internal ZipEntry(string name, int versionRequiredToExtract) + : this(name, versionRequiredToExtract, ZipConstants.VersionMadeBy, + CompressionMethod.Deflated, true) + { + } + + /// + /// Initializes an entry with the given name and made by information + /// + /// Name for this entry + /// Version and HostSystem Information + /// Minimum required zip feature version required to extract this entry + /// Compression method for this entry. + /// Whether the entry uses unicode for name and comment + /// + /// The name passed is null + /// + /// + /// versionRequiredToExtract should be 0 (auto-calculate) or > 10 + /// + /// + /// This constructor is used by the ZipFile class when reading from the central header + /// It is not generally useful, use the constructor specifying the name only. + /// + internal ZipEntry(string name, int versionRequiredToExtract, int madeByInfo, + CompressionMethod method, bool unicode) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (name.Length > 0xffff) + { + throw new ArgumentException("Name is too long", nameof(name)); + } + + if ((versionRequiredToExtract != 0) && (versionRequiredToExtract < 10)) + { + throw new ArgumentOutOfRangeException(nameof(versionRequiredToExtract)); + } + + this.DateTime = DateTime.Now; + this.name = name; + this.versionMadeBy = (ushort)madeByInfo; + this.versionToExtract = (ushort)versionRequiredToExtract; + this.method = method; + + IsUnicodeText = unicode; + } + + /// + /// Creates a deep copy of the given zip entry. + /// + /// + /// The entry to copy. + /// + [Obsolete("Use Clone instead")] + public ZipEntry(ZipEntry entry) + { + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + known = entry.known; + name = entry.name; + size = entry.size; + compressedSize = entry.compressedSize; + crc = entry.crc; + dateTime = entry.DateTime; + method = entry.method; + comment = entry.comment; + versionToExtract = entry.versionToExtract; + versionMadeBy = entry.versionMadeBy; + externalFileAttributes = entry.externalFileAttributes; + flags = entry.flags; + + zipFileIndex = entry.zipFileIndex; + offset = entry.offset; + + forceZip64_ = entry.forceZip64_; + + if (entry.extra != null) + { + extra = new byte[entry.extra.Length]; + Array.Copy(entry.extra, 0, extra, 0, entry.extra.Length); + } + } + + #endregion Constructors + + /// + /// Get a value indicating whether the entry has a CRC value available. + /// + public bool HasCrc => (known & Known.Crc) != 0; + + /// + /// Get/Set flag indicating if entry is encrypted. + /// A simple helper routine to aid interpretation of flags + /// + /// This is an assistant that interprets the flags property. + public bool IsCrypted + { + get => this.HasFlag(GeneralBitFlags.Encrypted); + set => this.SetFlag(GeneralBitFlags.Encrypted, value); + } + + /// + /// Get / set a flag indicating whether entry name and comment text are + /// encoded in unicode UTF8. + /// + /// This is an assistant that interprets the flags property. + public bool IsUnicodeText + { + get => this.HasFlag(GeneralBitFlags.UnicodeText); + set => this.SetFlag(GeneralBitFlags.UnicodeText, value); + } + + /// + /// Value used during password checking for PKZIP 2.0 / 'classic' encryption. + /// + internal byte CryptoCheckValue + { + get => cryptoCheckValue_; + set => cryptoCheckValue_ = value; + } + + /// + /// Get/Set general purpose bit flag for entry + /// + /// + /// General purpose bit flag
+ ///
+ /// Bit 0: If set, indicates the file is encrypted
+ /// Bit 1-2 Only used for compression type 6 Imploding, and 8, 9 deflating
+ /// Imploding:
+ /// Bit 1 if set indicates an 8K sliding dictionary was used. If clear a 4k dictionary was used
+ /// Bit 2 if set indicates 3 Shannon-Fanno trees were used to encode the sliding dictionary, 2 otherwise
+ ///
+ /// Deflating:
+ /// Bit 2 Bit 1
+ /// 0 0 Normal compression was used
+ /// 0 1 Maximum compression was used
+ /// 1 0 Fast compression was used
+ /// 1 1 Super fast compression was used
+ ///
+ /// Bit 3: If set, the fields crc-32, compressed size + /// and uncompressed size are were not able to be written during zip file creation + /// The correct values are held in a data descriptor immediately following the compressed data.
+ /// Bit 4: Reserved for use by PKZIP for enhanced deflating
+ /// Bit 5: If set indicates the file contains compressed patch data
+ /// Bit 6: If set indicates strong encryption was used.
+ /// Bit 7-10: Unused or reserved
+ /// Bit 11: If set the name and comments for this entry are in unicode.
+ /// Bit 12-15: Unused or reserved
+ ///
+ /// + /// + public int Flags + { + get => flags; + set => flags = value; + } + + /// + /// Get/Set index of this entry in Zip file + /// + /// This is only valid when the entry is part of a + public long ZipFileIndex + { + get => zipFileIndex; + set => zipFileIndex = value; + } + + /// + /// Get/set offset for use in central header + /// + public long Offset + { + get => offset; + set => offset = value; + } + + /// + /// Get/Set external file attributes as an integer. + /// The values of this are operating system dependent see + /// HostSystem for details + /// + public int ExternalFileAttributes + { + get => (known & Known.ExternalAttributes) == 0 ? -1 : externalFileAttributes; + + set + { + externalFileAttributes = value; + known |= Known.ExternalAttributes; + } + } + + /// + /// Get the version made by for this entry or zero if unknown. + /// The value / 10 indicates the major version number, and + /// the value mod 10 is the minor version number + /// + public int VersionMadeBy => versionMadeBy & 0xff; + + /// + /// Get a value indicating this entry is for a DOS/Windows system. + /// + public bool IsDOSEntry + => (HostSystem == (int)HostSystemID.Msdos) + || (HostSystem == (int)HostSystemID.WindowsNT); + + /// + /// Test the external attributes for this to + /// see if the external attributes are Dos based (including WINNT and variants) + /// and match the values + /// + /// The attributes to test. + /// Returns true if the external attributes are known to be DOS/Windows + /// based and have the same attributes set as the value passed. + private bool HasDosAttributes(int attributes) + { + bool result = false; + if ((known & Known.ExternalAttributes) != 0) + { + result |= (((HostSystem == (int)HostSystemID.Msdos) || + (HostSystem == (int)HostSystemID.WindowsNT)) && + (ExternalFileAttributes & attributes) == attributes); + } + return result; + } + + /// + /// Gets the compatibility information for the external file attribute + /// If the external file attributes are compatible with MS-DOS and can be read + /// by PKZIP for DOS version 2.04g then this value will be zero. Otherwise the value + /// will be non-zero and identify the host system on which the attributes are compatible. + /// + /// + /// + /// The values for this as defined in the Zip File format and by others are shown below. The values are somewhat + /// misleading in some cases as they are not all used as shown. You should consult the relevant documentation + /// to obtain up to date and correct information. The modified appnote by the infozip group is + /// particularly helpful as it documents a lot of peculiarities. The document is however a little dated. + /// + /// 0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) + /// 1 - Amiga + /// 2 - OpenVMS + /// 3 - Unix + /// 4 - VM/CMS + /// 5 - Atari ST + /// 6 - OS/2 HPFS + /// 7 - Macintosh + /// 8 - Z-System + /// 9 - CP/M + /// 10 - Windows NTFS + /// 11 - MVS (OS/390 - Z/OS) + /// 12 - VSE + /// 13 - Acorn Risc + /// 14 - VFAT + /// 15 - Alternate MVS + /// 16 - BeOS + /// 17 - Tandem + /// 18 - OS/400 + /// 19 - OS/X (Darwin) + /// 99 - WinZip AES + /// remainder - unused + /// + /// + public int HostSystem + { + get => (versionMadeBy >> 8) & 0xff; + + set + { + versionMadeBy &= 0x00ff; + versionMadeBy |= (ushort)((value & 0xff) << 8); + } + } + + /// + /// Get minimum Zip feature version required to extract this entry + /// + /// + /// Minimum features are defined as:
+ /// 1.0 - Default value
+ /// 1.1 - File is a volume label
+ /// 2.0 - File is a folder/directory
+ /// 2.0 - File is compressed using Deflate compression
+ /// 2.0 - File is encrypted using traditional encryption
+ /// 2.1 - File is compressed using Deflate64
+ /// 2.5 - File is compressed using PKWARE DCL Implode
+ /// 2.7 - File is a patch data set
+ /// 4.5 - File uses Zip64 format extensions
+ /// 4.6 - File is compressed using BZIP2 compression
+ /// 5.0 - File is encrypted using DES
+ /// 5.0 - File is encrypted using 3DES
+ /// 5.0 - File is encrypted using original RC2 encryption
+ /// 5.0 - File is encrypted using RC4 encryption
+ /// 5.1 - File is encrypted using AES encryption
+ /// 5.1 - File is encrypted using corrected RC2 encryption
+ /// 5.1 - File is encrypted using corrected RC2-64 encryption
+ /// 6.1 - File is encrypted using non-OAEP key wrapping
+ /// 6.2 - Central directory encryption (not confirmed yet)
+ /// 6.3 - File is compressed using LZMA
+ /// 6.3 - File is compressed using PPMD+
+ /// 6.3 - File is encrypted using Blowfish
+ /// 6.3 - File is encrypted using Twofish
+ ///
+ /// + public int Version + { + get + { + // Return recorded version if known. + if (versionToExtract != 0) + // Only lower order byte. High order is O/S file system. + return versionToExtract & 0x00ff; + + if (AESKeySize > 0) + // Ver 5.1 = AES + return ZipConstants.VERSION_AES; + + if (CompressionMethod.BZip2 == method) + return ZipConstants.VersionBZip2; + + if (CentralHeaderRequiresZip64) + return ZipConstants.VersionZip64; + + if (CompressionMethod.Deflated == method || IsDirectory || IsCrypted) + return 20; + + if (HasDosAttributes(0x08)) + return 11; + + return 10; + } + } + + /// + /// Get a value indicating whether this entry can be decompressed by the library. + /// + /// This is based on the and + /// whether the compression method is supported. + public bool CanDecompress + => Version <= ZipConstants.VersionMadeBy + && (Version == 10 || Version == 11 || Version == 20 || Version == 45 || Version == 46 || Version == 51) + && IsCompressionMethodSupported(); + + /// + /// Force this entry to be recorded using Zip64 extensions. + /// + public void ForceZip64() => forceZip64_ = true; + + /// + /// Get a value indicating whether Zip64 extensions were forced. + /// + /// A value of true if Zip64 extensions have been forced on; false if not. + public bool IsZip64Forced() => forceZip64_; + + /// + /// Gets a value indicating if the entry requires Zip64 extensions + /// to store the full entry values. + /// + /// A value of true if a local header requires Zip64 extensions; false if not. + public bool LocalHeaderRequiresZip64 + { + get + { + bool result = forceZip64_; + + if (!result) + { + ulong trueCompressedSize = compressedSize; + + if ((versionToExtract == 0) && IsCrypted) + { + trueCompressedSize += (ulong)this.EncryptionOverheadSize; + } + + // TODO: A better estimation of the true limit based on compression overhead should be used + // to determine when an entry should use Zip64. + result = + ((this.size >= uint.MaxValue) || (trueCompressedSize >= uint.MaxValue)) && + ((versionToExtract == 0) || (versionToExtract >= ZipConstants.VersionZip64)); + } + + return result; + } + } + + /// + /// Get a value indicating whether the central directory entry requires Zip64 extensions to be stored. + /// + public bool CentralHeaderRequiresZip64 + => LocalHeaderRequiresZip64 || (offset >= uint.MaxValue); + + /// + /// Get/Set DosTime value. + /// + /// + /// The MS-DOS date format can only represent dates between 1/1/1980 and 12/31/2107. + /// + public long DosTime + { + get + { + if ((known & Known.Time) == 0) + { + return 0; + } + + var year = (uint)DateTime.Year; + var month = (uint)DateTime.Month; + var day = (uint)DateTime.Day; + var hour = (uint)DateTime.Hour; + var minute = (uint)DateTime.Minute; + var second = (uint)DateTime.Second; + + if (year < 1980) + { + year = 1980; + month = 1; + day = 1; + hour = 0; + minute = 0; + second = 0; + } + else if (year > 2107) + { + year = 2107; + month = 12; + day = 31; + hour = 23; + minute = 59; + second = 59; + } + + return ((year - 1980) & 0x7f) << 25 | + (month << 21) | + (day << 16) | + (hour << 11) | + (minute << 5) | + (second >> 1); + } + + set + { + unchecked + { + var dosTime = (uint)value; + uint sec = Math.Min(59, 2 * (dosTime & 0x1f)); + uint min = Math.Min(59, (dosTime >> 5) & 0x3f); + uint hrs = Math.Min(23, (dosTime >> 11) & 0x1f); + uint mon = Math.Max(1, Math.Min(12, ((uint)(value >> 21) & 0xf))); + uint year = ((dosTime >> 25) & 0x7f) + 1980; + int day = Math.Max(1, Math.Min(DateTime.DaysInMonth((int)year, (int)mon), (int)((value >> 16) & 0x1f))); + DateTime = new DateTime((int)year, (int)mon, day, (int)hrs, (int)min, (int)sec, DateTimeKind.Unspecified); + } + } + } + + /// + /// Gets/Sets the time of last modification of the entry. + /// + /// + /// The property is updated to match this as far as possible. + /// + public DateTime DateTime + { + get => dateTime; + + set + { + dateTime = value; + known |= Known.Time; + } + } + + /// + /// Returns the entry name. + /// + /// + /// The unix naming convention is followed. + /// Path components in the entry should always separated by forward slashes ('/'). + /// Dos device names like C: should also be removed. + /// See the class, or + /// + public string Name + { + get => name; + internal set => name = value; + } + + /// + /// Gets/Sets the size of the uncompressed data. + /// + /// + /// The size or -1 if unknown. + /// + /// Setting the size before adding an entry to an archive can help + /// avoid compatibility problems with some archivers which don't understand Zip64 extensions. + public long Size + { + get => (known & Known.Size) != 0 ? (long)size : -1L; + set + { + size = (ulong)value; + known |= Known.Size; + } + } + + /// + /// Gets/Sets the size of the compressed data. + /// + /// + /// The compressed entry size or -1 if unknown. + /// + public long CompressedSize + { + get => (known & Known.CompressedSize) != 0 ? (long)compressedSize : -1L; + set + { + compressedSize = (ulong)value; + known |= Known.CompressedSize; + } + } + + /// + /// Gets/Sets the crc of the uncompressed data. + /// + /// + /// Crc is not in the range 0..0xffffffffL + /// + /// + /// The crc value or -1 if unknown. + /// + public long Crc + { + get => (known & Known.Crc) != 0 ? crc & 0xffffffffL : -1L; + set + { + if ((crc & 0xffffffff00000000L) != 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + this.crc = (uint)value; + this.known |= Known.Crc; + } + } + + /// + /// Gets/Sets the compression method. + /// + /// + /// The compression method for this entry + /// + public CompressionMethod CompressionMethod + { + get => method; + set => method = value; + } + + /// + /// Gets the compression method for outputting to the local or central header. + /// Returns same value as CompressionMethod except when AES encrypting, which + /// places 99 in the method and places the real method in the extra data. + /// + internal CompressionMethod CompressionMethodForHeader + => (AESKeySize > 0) ? CompressionMethod.WinZipAES : method; + + /// + /// Gets/Sets the extra data. + /// + /// + /// Extra data is longer than 64KB (0xffff) bytes. + /// + /// + /// Extra data or null if not set. + /// + public byte[] ExtraData + { + // TODO: This is slightly safer but less efficient. Think about whether it should change. + // return (byte[]) extra.Clone(); + get => extra; + + set + { + if (value == null) + { + extra = null; + } + else + { + if (value.Length > 0xffff) + { + throw new System.ArgumentOutOfRangeException(nameof(value)); + } + + extra = new byte[value.Length]; + Array.Copy(value, 0, extra, 0, value.Length); + } + } + } + + /// + /// For AES encrypted files returns or sets the number of bits of encryption (128, 192 or 256). + /// When setting, only 0 (off), 128 or 256 is supported. + /// + public int AESKeySize + { + get + { + // the strength (1 or 3) is in the entry header + switch (_aesEncryptionStrength) + { + case 0: + return 0; // Not AES + case 1: + return 128; + + case 2: + return 192; // Not used by WinZip + case 3: + return 256; + + default: + throw new ZipException("Invalid AESEncryptionStrength " + _aesEncryptionStrength); + } + } + set + { + switch (value) + { + case 0: + _aesEncryptionStrength = 0; + break; + + case 128: + _aesEncryptionStrength = 1; + break; + + case 256: + _aesEncryptionStrength = 3; + break; + + default: + throw new ZipException("AESKeySize must be 0, 128 or 256: " + value); + } + } + } + + /// + /// AES Encryption strength for storage in extra data in entry header. + /// 1 is 128 bit, 2 is 192 bit, 3 is 256 bit. + /// + internal byte AESEncryptionStrength => (byte)_aesEncryptionStrength; + + /// + /// Returns the length of the salt, in bytes + /// + /// Key size -> Salt length: 128 bits = 8 bytes, 192 bits = 12 bytes, 256 bits = 16 bytes. + internal int AESSaltLen => AESKeySize / 16; + + /// + /// Number of extra bytes required to hold the AES Header fields (Salt, Pwd verify, AuthCode) + /// + /// File format: + /// Bytes | Content + /// ---------+--------------------------- + /// Variable | Salt value + /// 2 | Password verification value + /// Variable | Encrypted file data + /// 10 | Authentication code + internal int AESOverheadSize => 12 + AESSaltLen; + + /// + /// Number of extra bytes required to hold the encryption header fields. + /// + internal int EncryptionOverheadSize => + !IsCrypted + // Entry is not encrypted - no overhead + ? 0 + : _aesEncryptionStrength == 0 + // Entry is encrypted using ZipCrypto + ? ZipConstants.CryptoHeaderSize + // Entry is encrypted using AES + : AESOverheadSize; + + /// + /// Process extra data fields updating the entry based on the contents. + /// + /// True if the extra data fields should be handled + /// for a local header, rather than for a central header. + /// + internal void ProcessExtraData(bool localHeader) + { + var extraData = new ZipExtraData(this.extra); + + if (extraData.Find(0x0001)) + { + // Version required to extract is ignored here as some archivers dont set it correctly + // in theory it should be version 45 or higher + + // The recorded size will change but remember that this is zip64. + forceZip64_ = true; + + if (extraData.ValueLength < 4) + { + throw new ZipException("Extra data extended Zip64 information length is invalid"); + } + + // (localHeader ||) was deleted, because actually there is no specific difference with reading sizes between local header & central directory + // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + // ... + // 4.4 Explanation of fields + // ... + // 4.4.8 compressed size: (4 bytes) + // 4.4.9 uncompressed size: (4 bytes) + // + // The size of the file compressed (4.4.8) and uncompressed, + // (4.4.9) respectively. When a decryption header is present it + // will be placed in front of the file data and the value of the + // compressed file size will include the bytes of the decryption + // header. If bit 3 of the general purpose bit flag is set, + // these fields are set to zero in the local header and the + // correct values are put in the data descriptor and + // in the central directory. If an archive is in ZIP64 format + // and the value in this field is 0xFFFFFFFF, the size will be + // in the corresponding 8 byte ZIP64 extended information + // extra field. When encrypting the central directory, if the + // local header is not in ZIP64 format and general purpose bit + // flag 13 is set indicating masking, the value stored for the + // uncompressed size in the Local Header will be zero. + // + // Otherwise there is problem with minizip implementation + if (size == uint.MaxValue) + { + size = (ulong)extraData.ReadLong(); + } + + if (compressedSize == uint.MaxValue) + { + compressedSize = (ulong)extraData.ReadLong(); + } + + if (!localHeader && (offset == uint.MaxValue)) + { + offset = extraData.ReadLong(); + } + + // Disk number on which file starts is ignored + } + else + { + if ( + ((versionToExtract & 0xff) >= ZipConstants.VersionZip64) && + ((size == uint.MaxValue) || (compressedSize == uint.MaxValue)) + ) + { + throw new ZipException("Zip64 Extended information required but is missing."); + } + } + + DateTime = GetDateTime(extraData) ?? DateTime; + if (method == CompressionMethod.WinZipAES) + { + ProcessAESExtraData(extraData); + } + } + + private static DateTime? GetDateTime(ZipExtraData extraData) + { + // Check for NT timestamp + // NOTE: Disable by default to match behavior of InfoZIP +#if RESPECT_NT_TIMESTAMP + NTTaggedData ntData = extraData.GetData(); + if (ntData != null) + return ntData.LastModificationTime; +#endif + + // Check for Unix timestamp + ExtendedUnixData unixData = extraData.GetData(); + if (unixData != null && unixData.Include.HasFlag(ExtendedUnixData.Flags.ModificationTime)) + return unixData.ModificationTime; + + return null; + } + + // For AES the method in the entry is 99, and the real compression method is in the extradata + private void ProcessAESExtraData(ZipExtraData extraData) + { + if (extraData.Find(0x9901)) + { + // Set version for Zipfile.CreateAndInitDecryptionStream + versionToExtract = ZipConstants.VERSION_AES; // Ver 5.1 = AES see "Version" getter + + // + // Unpack AES extra data field see http://www.winzip.com/aes_info.htm + int length = extraData.ValueLength; // Data size currently 7 + if (length < 7) + throw new ZipException("AES Extra Data Length " + length + " invalid."); + int ver = extraData.ReadShort(); // Version number (1=AE-1 2=AE-2) + int vendorId = extraData.ReadShort(); // 2-character vendor ID 0x4541 = "AE" + int encrStrength = extraData.ReadByte(); // encryption strength 1 = 128 2 = 192 3 = 256 + int actualCompress = extraData.ReadShort(); // The actual compression method used to compress the file + _aesVer = ver; + _aesEncryptionStrength = encrStrength; + method = (CompressionMethod)actualCompress; + } + else + throw new ZipException("AES Extra Data missing"); + } + + /// + /// Gets/Sets the entry comment. + /// + /// + /// If comment is longer than 0xffff. + /// + /// + /// The comment or null if not set. + /// + /// + /// A comment is only available for entries when read via the class. + /// The class doesn't have the comment data available. + /// + public string Comment + { + get => comment; + set + { + // This test is strictly incorrect as the length is in characters + // while the storage limit is in bytes. + // While the test is partially correct in that a comment of this length or greater + // is definitely invalid, shorter comments may also have an invalid length + // where there are multi-byte characters + // The full test is not possible here however as the code page to apply conversions with + // isn't available. + if ((value != null) && (value.Length > 0xffff)) + { + throw new ArgumentOutOfRangeException(nameof(value), "cannot exceed 65535"); + } + + comment = value; + } + } + + /// + /// Gets a value indicating if the entry is a directory. + /// however. + /// + /// + /// A directory is determined by an entry name with a trailing slash '/'. + /// The external file attributes can also indicate an entry is for a directory. + /// Currently only dos/windows attributes are tested in this manner. + /// The trailing slash convention should always be followed. + /// + public bool IsDirectory + => name.Length > 0 + && (name[name.Length - 1] == '/' || name[name.Length - 1] == '\\') || HasDosAttributes(16); + + /// + /// Get a value of true if the entry appears to be a file; false otherwise + /// + /// + /// This only takes account of DOS/Windows attributes. Other operating systems are ignored. + /// For linux and others the result may be incorrect. + /// + public bool IsFile => !IsDirectory && !HasDosAttributes(8); + + /// + /// Test entry to see if data can be extracted. + /// + /// Returns true if data can be extracted for this entry; false otherwise. + public bool IsCompressionMethodSupported() => IsCompressionMethodSupported(CompressionMethod); + + #region ICloneable Members + + /// + /// Creates a copy of this zip entry. + /// + /// An that is a copy of the current instance. + public object Clone() + { + var result = (ZipEntry)this.MemberwiseClone(); + + // Ensure extra data is unique if it exists. + if (extra != null) + { + result.extra = new byte[extra.Length]; + Array.Copy(extra, 0, result.extra, 0, extra.Length); + } + + return result; + } + + #endregion ICloneable Members + + /// + /// Gets a string representation of this ZipEntry. + /// + /// A readable textual representation of this + public override string ToString() => name; + + /// + /// Test a compression method to see if this library + /// supports extracting data compressed with that method + /// + /// The compression method to test. + /// Returns true if the compression method is supported; false otherwise + public static bool IsCompressionMethodSupported(CompressionMethod method) + => method == CompressionMethod.Deflated + || method == CompressionMethod.Stored + || method == CompressionMethod.BZip2; + + /// + /// Cleans a name making it conform to Zip file conventions. + /// Devices names ('c:\') and UNC share names ('\\server\share') are removed + /// and back slashes ('\') are converted to forward slashes ('/'). + /// Names are made relative by trimming leading slashes which is compatible + /// with the ZIP naming convention. + /// + /// The name to clean + /// The 'cleaned' name. + /// + /// The Zip name transform class is more flexible. + /// + public static string CleanName(string name) + { + if (name == null) + { + return string.Empty; + } + + if (Path.IsPathRooted(name)) + { + // NOTE: + // for UNC names... \\machine\share\zoom\beet.txt gives \zoom\beet.txt + name = name.Substring(Path.GetPathRoot(name).Length); + } + + name = name.Replace(@"\", "/"); + + while ((name.Length > 0) && (name[0] == '/')) + { + name = name.Remove(0, 1); + } + return name; + } + + #region Instance Fields + + private Known known; + private int externalFileAttributes = -1; // contains external attributes (O/S dependant) + + private ushort versionMadeBy; // Contains host system and version information + // only relevant for central header entries + + private string name; + private ulong size; + private ulong compressedSize; + private ushort versionToExtract; // Version required to extract (library handles <= 2.0) + private uint crc; + private DateTime dateTime; + + private CompressionMethod method = CompressionMethod.Deflated; + private byte[] extra; + private string comment; + + private int flags; // general purpose bit flags + + private long zipFileIndex = -1; // used by ZipFile + private long offset; // used by ZipFile and ZipOutputStream + + private bool forceZip64_; + private byte cryptoCheckValue_; + private int _aesVer; // Version number (2 = AE-2 ?). Assigned but not used. + private int _aesEncryptionStrength; // Encryption strength 1 = 128 2 = 192 3 = 256 + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntryExtensions.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntryExtensions.cs new file mode 100644 index 0000000..7ae3bf2 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntryExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// General ZipEntry helper extensions + /// + public static class ZipEntryExtensions + { + /// + /// Efficiently check if a flag is set without enum un-/boxing + /// + /// + /// + /// Returns whether the flag was set + public static bool HasFlag(this ZipEntry entry, GeneralBitFlags flag) + => (entry.Flags & (int) flag) != 0; + + /// + /// Efficiently set a flag without enum un-/boxing + /// + /// + /// + /// Whether the passed flag should be set (1) or cleared (0) + public static void SetFlag(this ZipEntry entry, GeneralBitFlags flag, bool enabled = true) + => entry.Flags = enabled + ? entry.Flags | (int) flag + : entry.Flags & ~(int) flag; + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntryFactory.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntryFactory.cs new file mode 100644 index 0000000..bc92b26 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipEntryFactory.cs @@ -0,0 +1,375 @@ +using BSP_ICSharpCode.SharpZipLib.Core; +using System; +using System.IO; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// Basic implementation of + /// + public class ZipEntryFactory : IEntryFactory + { + #region Enumerations + + /// + /// Defines the possible values to be used for the . + /// + public enum TimeSetting + { + /// + /// Use the recorded LastWriteTime value for the file. + /// + LastWriteTime, + + /// + /// Use the recorded LastWriteTimeUtc value for the file + /// + LastWriteTimeUtc, + + /// + /// Use the recorded CreateTime value for the file. + /// + CreateTime, + + /// + /// Use the recorded CreateTimeUtc value for the file. + /// + CreateTimeUtc, + + /// + /// Use the recorded LastAccessTime value for the file. + /// + LastAccessTime, + + /// + /// Use the recorded LastAccessTimeUtc value for the file. + /// + LastAccessTimeUtc, + + /// + /// Use a fixed value. + /// + /// The actual value used can be + /// specified via the constructor or + /// using the with the setting set + /// to which will use the when this class was constructed. + /// The property can also be used to set this value. + Fixed, + } + + #endregion Enumerations + + #region Constructors + + /// + /// Initialise a new instance of the class. + /// + /// A default , and the LastWriteTime for files is used. + public ZipEntryFactory() + { + nameTransform_ = new ZipNameTransform(); + isUnicodeText_ = true; + } + + /// + /// Initialise a new instance of using the specified + /// + /// The time setting to use when creating Zip entries. + public ZipEntryFactory(TimeSetting timeSetting) : this() + { + timeSetting_ = timeSetting; + } + + /// + /// Initialise a new instance of using the specified + /// + /// The time to set all values to. + public ZipEntryFactory(DateTime time) : this() + { + timeSetting_ = TimeSetting.Fixed; + FixedDateTime = time; + } + + #endregion Constructors + + #region Properties + + /// + /// Get / set the to be used when creating new values. + /// + /// + /// Setting this property to null will cause a default name transform to be used. + /// + public INameTransform NameTransform + { + get { return nameTransform_; } + set + { + if (value == null) + { + nameTransform_ = new ZipNameTransform(); + } + else + { + nameTransform_ = value; + } + } + } + + /// + /// Get / set the in use. + /// + public TimeSetting Setting + { + get { return timeSetting_; } + set { timeSetting_ = value; } + } + + /// + /// Get / set the value to use when is set to + /// + public DateTime FixedDateTime + { + get { return fixedDateTime_; } + set + { + if (value.Year < 1970) + { + throw new ArgumentException("Value is too old to be valid", nameof(value)); + } + fixedDateTime_ = value; + } + } + + /// + /// A bitmask defining the attributes to be retrieved from the actual file. + /// + /// The default is to get all possible attributes from the actual file. + public int GetAttributes + { + get { return getAttributes_; } + set { getAttributes_ = value; } + } + + /// + /// A bitmask defining which attributes are to be set on. + /// + /// By default no attributes are set on. + public int SetAttributes + { + get { return setAttributes_; } + set { setAttributes_ = value; } + } + + /// + /// Get set a value indicating whether unicode text should be set on. + /// + public bool IsUnicodeText + { + get { return isUnicodeText_; } + set { isUnicodeText_ = value; } + } + + #endregion Properties + + #region IEntryFactory Members + + /// + /// Make a new for a file. + /// + /// The name of the file to create a new entry for. + /// Returns a new based on the . + public ZipEntry MakeFileEntry(string fileName) + { + return MakeFileEntry(fileName, null, true); + } + + /// + /// Make a new for a file. + /// + /// The name of the file to create a new entry for. + /// If true entry detail is retrieved from the file system if the file exists. + /// Returns a new based on the . + public ZipEntry MakeFileEntry(string fileName, bool useFileSystem) + { + return MakeFileEntry(fileName, null, useFileSystem); + } + + /// + /// Make a new from a name. + /// + /// The name of the file to create a new entry for. + /// An alternative name to be used for the new entry. Null if not applicable. + /// If true entry detail is retrieved from the file system if the file exists. + /// Returns a new based on the . + public ZipEntry MakeFileEntry(string fileName, string entryName, bool useFileSystem) + { + var result = new ZipEntry(nameTransform_.TransformFile(!string.IsNullOrEmpty(entryName) ? entryName : fileName)); + result.IsUnicodeText = isUnicodeText_; + + int externalAttributes = 0; + bool useAttributes = (setAttributes_ != 0); + + FileInfo fi = null; + if (useFileSystem) + { + fi = new FileInfo(fileName); + } + + if ((fi != null) && fi.Exists) + { + switch (timeSetting_) + { + case TimeSetting.CreateTime: + result.DateTime = fi.CreationTime; + break; + + case TimeSetting.CreateTimeUtc: + result.DateTime = fi.CreationTimeUtc; + break; + + case TimeSetting.LastAccessTime: + result.DateTime = fi.LastAccessTime; + break; + + case TimeSetting.LastAccessTimeUtc: + result.DateTime = fi.LastAccessTimeUtc; + break; + + case TimeSetting.LastWriteTime: + result.DateTime = fi.LastWriteTime; + break; + + case TimeSetting.LastWriteTimeUtc: + result.DateTime = fi.LastWriteTimeUtc; + break; + + case TimeSetting.Fixed: + result.DateTime = fixedDateTime_; + break; + + default: + throw new ZipException("Unhandled time setting in MakeFileEntry"); + } + + result.Size = fi.Length; + + useAttributes = true; + externalAttributes = ((int)fi.Attributes & getAttributes_); + } + else + { + if (timeSetting_ == TimeSetting.Fixed) + { + result.DateTime = fixedDateTime_; + } + } + + if (useAttributes) + { + externalAttributes |= setAttributes_; + result.ExternalFileAttributes = externalAttributes; + } + + return result; + } + + /// + /// Make a new for a directory. + /// + /// The raw untransformed name for the new directory + /// Returns a new representing a directory. + public ZipEntry MakeDirectoryEntry(string directoryName) + { + return MakeDirectoryEntry(directoryName, true); + } + + /// + /// Make a new for a directory. + /// + /// The raw untransformed name for the new directory + /// If true entry detail is retrieved from the file system if the file exists. + /// Returns a new representing a directory. + public ZipEntry MakeDirectoryEntry(string directoryName, bool useFileSystem) + { + var result = new ZipEntry(nameTransform_.TransformDirectory(directoryName)); + result.IsUnicodeText = isUnicodeText_; + result.Size = 0; + + int externalAttributes = 0; + + DirectoryInfo di = null; + + if (useFileSystem) + { + di = new DirectoryInfo(directoryName); + } + + if ((di != null) && di.Exists) + { + switch (timeSetting_) + { + case TimeSetting.CreateTime: + result.DateTime = di.CreationTime; + break; + + case TimeSetting.CreateTimeUtc: + result.DateTime = di.CreationTimeUtc; + break; + + case TimeSetting.LastAccessTime: + result.DateTime = di.LastAccessTime; + break; + + case TimeSetting.LastAccessTimeUtc: + result.DateTime = di.LastAccessTimeUtc; + break; + + case TimeSetting.LastWriteTime: + result.DateTime = di.LastWriteTime; + break; + + case TimeSetting.LastWriteTimeUtc: + result.DateTime = di.LastWriteTimeUtc; + break; + + case TimeSetting.Fixed: + result.DateTime = fixedDateTime_; + break; + + default: + throw new ZipException("Unhandled time setting in MakeDirectoryEntry"); + } + + externalAttributes = ((int)di.Attributes & getAttributes_); + } + else + { + if (timeSetting_ == TimeSetting.Fixed) + { + result.DateTime = fixedDateTime_; + } + } + + // Always set directory attribute on. + externalAttributes |= (setAttributes_ | 16); + result.ExternalFileAttributes = externalAttributes; + + return result; + } + + #endregion IEntryFactory Members + + #region Instance Fields + + private INameTransform nameTransform_; + private DateTime fixedDateTime_ = DateTime.Now; + private TimeSetting timeSetting_ = TimeSetting.LastWriteTime; + private bool isUnicodeText_; + + private int getAttributes_ = -1; + private int setAttributes_; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipException.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipException.cs new file mode 100644 index 0000000..207dfcc --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipException.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.Serialization; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// ZipException represents exceptions specific to Zip classes and code. + /// + [Serializable] + public class ZipException : SharpZipBaseException + { + /// + /// Initialise a new instance of . + /// + public ZipException() + { + } + + /// + /// Initialise a new instance of with its message string. + /// + /// A that describes the error. + public ZipException(string message) + : base(message) + { + } + + /// + /// Initialise a new instance of . + /// + /// A that describes the error. + /// The that caused this exception. + public ZipException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the ZipException class with serialized data. + /// + /// + /// The System.Runtime.Serialization.SerializationInfo that holds the serialized + /// object data about the exception being thrown. + /// + /// + /// The System.Runtime.Serialization.StreamingContext that contains contextual information + /// about the source or destination. + /// + protected ZipException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs new file mode 100644 index 0000000..883285e --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipExtraData.cs @@ -0,0 +1,974 @@ +using System; +using System.IO; +using BSP_ICSharpCode.SharpZipLib.Core; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + // TODO: Sort out whether tagged data is useful and what a good implementation might look like. + // Its just a sketch of an idea at the moment. + + /// + /// ExtraData tagged value interface. + /// + public interface ITaggedData + { + /// + /// Get the ID for this tagged data value. + /// + ushort TagID { get; } + + /// + /// Set the contents of this instance from the data passed. + /// + /// The data to extract contents from. + /// The offset to begin extracting data from. + /// The number of bytes to extract. + void SetData(byte[] data, int offset, int count); + + /// + /// Get the data representing this instance. + /// + /// Returns the data for this instance. + byte[] GetData(); + } + + /// + /// A raw binary tagged value + /// + public class RawTaggedData : ITaggedData + { + /// + /// Initialise a new instance. + /// + /// The tag ID. + public RawTaggedData(ushort tag) + { + _tag = tag; + } + + #region ITaggedData Members + + /// + /// Get the ID for this tagged data value. + /// + public ushort TagID + { + get { return _tag; } + set { _tag = value; } + } + + /// + /// Set the data from the raw values provided. + /// + /// The raw data to extract values from. + /// The index to start extracting values from. + /// The number of bytes available. + public void SetData(byte[] data, int offset, int count) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + _data = new byte[count]; + Array.Copy(data, offset, _data, 0, count); + } + + /// + /// Get the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] GetData() + { + return _data; + } + + #endregion ITaggedData Members + + /// + /// Get /set the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] Data + { + get { return _data; } + set { _data = value; } + } + + #region Instance Fields + + /// + /// The tag ID for this instance. + /// + private ushort _tag; + + private byte[] _data; + + #endregion Instance Fields + } + + /// + /// Class representing extended unix date time values. + /// + public class ExtendedUnixData : ITaggedData + { + /// + /// Flags indicate which values are included in this instance. + /// + [Flags] + public enum Flags : byte + { + /// + /// The modification time is included + /// + ModificationTime = 0x01, + + /// + /// The access time is included + /// + AccessTime = 0x02, + + /// + /// The create time is included. + /// + CreateTime = 0x04, + } + + #region ITaggedData Members + + /// + /// Get the ID + /// + public ushort TagID + { + get { return 0x5455; } + } + + /// + /// Set the data from the raw values provided. + /// + /// The raw data to extract values from. + /// The index to start extracting values from. + /// The number of bytes available. + public void SetData(byte[] data, int index, int count) + { + using (MemoryStream ms = new MemoryStream(data, index, count, false)) + { + // bit 0 if set, modification time is present + // bit 1 if set, access time is present + // bit 2 if set, creation time is present + + _flags = (Flags)ms.ReadByte(); + if (((_flags & Flags.ModificationTime) != 0)) + { + int iTime = ms.ReadLEInt(); + + _modificationTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) + + new TimeSpan(0, 0, 0, iTime, 0); + + // Central-header version is truncated after modification time + if (count <= 5) return; + } + + if ((_flags & Flags.AccessTime) != 0) + { + int iTime = ms.ReadLEInt(); + + _lastAccessTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) + + new TimeSpan(0, 0, 0, iTime, 0); + } + + if ((_flags & Flags.CreateTime) != 0) + { + int iTime = ms.ReadLEInt(); + + _createTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) + + new TimeSpan(0, 0, 0, iTime, 0); + } + } + } + + /// + /// Get the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] GetData() + { + using (MemoryStream ms = new MemoryStream()) + { + ms.WriteByte((byte)_flags); // Flags + if ((_flags & Flags.ModificationTime) != 0) + { + TimeSpan span = _modificationTime - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var seconds = (int)span.TotalSeconds; + ms.WriteLEInt(seconds); + } + if ((_flags & Flags.AccessTime) != 0) + { + TimeSpan span = _lastAccessTime - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var seconds = (int)span.TotalSeconds; + ms.WriteLEInt(seconds); + } + if ((_flags & Flags.CreateTime) != 0) + { + TimeSpan span = _createTime - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var seconds = (int)span.TotalSeconds; + ms.WriteLEInt(seconds); + } + return ms.ToArray(); + } + } + + #endregion ITaggedData Members + + /// + /// Test a value to see if is valid and can be represented here. + /// + /// The value to test. + /// Returns true if the value is valid and can be represented; false if not. + /// The standard Unix time is a signed integer data type, directly encoding the Unix time number, + /// which is the number of seconds since 1970-01-01. + /// Being 32 bits means the values here cover a range of about 136 years. + /// The minimum representable time is 1901-12-13 20:45:52, + /// and the maximum representable time is 2038-01-19 03:14:07. + /// + public static bool IsValidValue(DateTime value) + { + return ((value >= new DateTime(1901, 12, 13, 20, 45, 52)) || + (value <= new DateTime(2038, 1, 19, 03, 14, 07))); + } + + /// + /// Get /set the Modification Time + /// + /// + /// + public DateTime ModificationTime + { + get { return _modificationTime; } + set + { + if (!IsValidValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _flags |= Flags.ModificationTime; + _modificationTime = value; + } + } + + /// + /// Get / set the Access Time + /// + /// + /// + public DateTime AccessTime + { + get { return _lastAccessTime; } + set + { + if (!IsValidValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _flags |= Flags.AccessTime; + _lastAccessTime = value; + } + } + + /// + /// Get / Set the Create Time + /// + /// + /// + public DateTime CreateTime + { + get { return _createTime; } + set + { + if (!IsValidValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _flags |= Flags.CreateTime; + _createTime = value; + } + } + + /// + /// Get/set the values to include. + /// + public Flags Include + { + get { return _flags; } + set { _flags = value; } + } + + #region Instance Fields + + private Flags _flags; + private DateTime _modificationTime = new DateTime(1970, 1, 1); + private DateTime _lastAccessTime = new DateTime(1970, 1, 1); + private DateTime _createTime = new DateTime(1970, 1, 1); + + #endregion Instance Fields + } + + /// + /// Class handling NT date time values. + /// + public class NTTaggedData : ITaggedData + { + /// + /// Get the ID for this tagged data value. + /// + public ushort TagID + { + get { return 10; } + } + + /// + /// Set the data from the raw values provided. + /// + /// The raw data to extract values from. + /// The index to start extracting values from. + /// The number of bytes available. + public void SetData(byte[] data, int index, int count) + { + using (MemoryStream ms = new MemoryStream(data, index, count, false)) + { + ms.ReadLEInt(); // Reserved + while (ms.Position < ms.Length) + { + int ntfsTag = ms.ReadLEShort(); + int ntfsLength = ms.ReadLEShort(); + if (ntfsTag == 1) + { + if (ntfsLength >= 24) + { + long lastModificationTicks = ms.ReadLELong(); + _lastModificationTime = DateTime.FromFileTimeUtc(lastModificationTicks); + + long lastAccessTicks = ms.ReadLELong(); + _lastAccessTime = DateTime.FromFileTimeUtc(lastAccessTicks); + + long createTimeTicks = ms.ReadLELong(); + _createTime = DateTime.FromFileTimeUtc(createTimeTicks); + } + break; + } + else + { + // An unknown NTFS tag so simply skip it. + ms.Seek(ntfsLength, SeekOrigin.Current); + } + } + } + } + + /// + /// Get the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] GetData() + { + using (MemoryStream ms = new MemoryStream()) + { + ms.WriteLEInt(0); // Reserved + ms.WriteLEShort(1); // Tag + ms.WriteLEShort(24); // Length = 3 x 8. + ms.WriteLELong(_lastModificationTime.ToFileTimeUtc()); + ms.WriteLELong(_lastAccessTime.ToFileTimeUtc()); + ms.WriteLELong(_createTime.ToFileTimeUtc()); + return ms.ToArray(); + } + } + + /// + /// Test a valuie to see if is valid and can be represented here. + /// + /// The value to test. + /// Returns true if the value is valid and can be represented; false if not. + /// + /// NTFS filetimes are 64-bit unsigned integers, stored in Intel + /// (least significant byte first) byte order. They determine the + /// number of 1.0E-07 seconds (1/10th microseconds!) past WinNT "epoch", + /// which is "01-Jan-1601 00:00:00 UTC". 28 May 60056 is the upper limit + /// + public static bool IsValidValue(DateTime value) + { + bool result = true; + try + { + value.ToFileTimeUtc(); + } + catch + { + result = false; + } + return result; + } + + /// + /// Get/set the last modification time. + /// + public DateTime LastModificationTime + { + get { return _lastModificationTime; } + set + { + if (!IsValidValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _lastModificationTime = value; + } + } + + /// + /// Get /set the create time + /// + public DateTime CreateTime + { + get { return _createTime; } + set + { + if (!IsValidValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _createTime = value; + } + } + + /// + /// Get /set the last access time. + /// + public DateTime LastAccessTime + { + get { return _lastAccessTime; } + set + { + if (!IsValidValue(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _lastAccessTime = value; + } + } + + #region Instance Fields + + private DateTime _lastAccessTime = DateTime.FromFileTimeUtc(0); + private DateTime _lastModificationTime = DateTime.FromFileTimeUtc(0); + private DateTime _createTime = DateTime.FromFileTimeUtc(0); + + #endregion Instance Fields + } + + /// + /// A factory that creates tagged data instances. + /// + internal interface ITaggedDataFactory + { + /// + /// Get data for a specific tag value. + /// + /// The tag ID to find. + /// The data to search. + /// The offset to begin extracting data from. + /// The number of bytes to extract. + /// The located value found, or null if not found. + ITaggedData Create(short tag, byte[] data, int offset, int count); + } + + /// + /// + /// A class to handle the extra data field for Zip entries + /// + /// + /// Extra data contains 0 or more values each prefixed by a header tag and length. + /// They contain zero or more bytes of actual data. + /// The data is held internally using a copy on write strategy. This is more efficient but + /// means that for extra data created by passing in data can have the values modified by the caller + /// in some circumstances. + /// + sealed public class ZipExtraData : IDisposable + { + #region Constructors + + /// + /// Initialise a default instance. + /// + public ZipExtraData() + { + Clear(); + } + + /// + /// Initialise with known extra data. + /// + /// The extra data. + public ZipExtraData(byte[] data) + { + if (data == null) + { + _data = Empty.Array(); + } + else + { + _data = data; + } + } + + #endregion Constructors + + /// + /// Get the raw extra data value + /// + /// Returns the raw byte[] extra data this instance represents. + public byte[] GetEntryData() + { + if (Length > ushort.MaxValue) + { + throw new ZipException("Data exceeds maximum length"); + } + + return (byte[])_data.Clone(); + } + + /// + /// Clear the stored data. + /// + public void Clear() + { + if ((_data == null) || (_data.Length != 0)) + { + _data = Empty.Array(); + } + } + + /// + /// Gets the current extra data length. + /// + public int Length + { + get { return _data.Length; } + } + + /// + /// Get a read-only for the associated tag. + /// + /// The tag to locate data for. + /// Returns a containing tag data or null if no tag was found. + public Stream GetStreamForTag(int tag) + { + Stream result = null; + if (Find(tag)) + { + result = new MemoryStream(_data, _index, _readValueLength, false); + } + return result; + } + + /// + /// Get the tagged data for a tag. + /// + /// The tag to search for. + /// Returns a tagged value or null if none found. + public T GetData() + where T : class, ITaggedData, new() + { + T result = new T(); + if (Find(result.TagID)) + { + result.SetData(_data, _readValueStart, _readValueLength); + return result; + } + else return null; + } + + /// + /// Get the length of the last value found by + /// + /// This is only valid if has previously returned true. + public int ValueLength + { + get { return _readValueLength; } + } + + /// + /// Get the index for the current read value. + /// + /// This is only valid if has previously returned true. + /// Initially the result will be the index of the first byte of actual data. The value is updated after calls to + /// , and . + public int CurrentReadIndex + { + get { return _index; } + } + + /// + /// Get the number of bytes remaining to be read for the current value; + /// + public int UnreadCount + { + get + { + if ((_readValueStart > _data.Length) || + (_readValueStart < 4)) + { + throw new ZipException("Find must be called before calling a Read method"); + } + + return _readValueStart + _readValueLength - _index; + } + } + + /// + /// Find an extra data value + /// + /// The identifier for the value to find. + /// Returns true if the value was found; false otherwise. + public bool Find(int headerID) + { + _readValueStart = _data.Length; + _readValueLength = 0; + _index = 0; + + int localLength = _readValueStart; + int localTag = headerID - 1; + + // Trailing bytes that cant make up an entry (as there arent enough + // bytes for a tag and length) are ignored! + while ((localTag != headerID) && (_index < _data.Length - 3)) + { + localTag = ReadShortInternal(); + localLength = ReadShortInternal(); + if (localTag != headerID) + { + _index += localLength; + } + } + + bool result = (localTag == headerID) && ((_index + localLength) <= _data.Length); + + if (result) + { + _readValueStart = _index; + _readValueLength = localLength; + } + + return result; + } + + /// + /// Add a new entry to extra data. + /// + /// The value to add. + public void AddEntry(ITaggedData taggedData) + { + if (taggedData == null) + { + throw new ArgumentNullException(nameof(taggedData)); + } + AddEntry(taggedData.TagID, taggedData.GetData()); + } + + /// + /// Add a new entry to extra data + /// + /// The ID for this entry. + /// The data to add. + /// If the ID already exists its contents are replaced. + public void AddEntry(int headerID, byte[] fieldData) + { + if ((headerID > ushort.MaxValue) || (headerID < 0)) + { + throw new ArgumentOutOfRangeException(nameof(headerID)); + } + + int addLength = (fieldData == null) ? 0 : fieldData.Length; + + if (addLength > ushort.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(fieldData), "exceeds maximum length"); + } + + // Test for new length before adjusting data. + int newLength = _data.Length + addLength + 4; + + if (Find(headerID)) + { + newLength -= (ValueLength + 4); + } + + if (newLength > ushort.MaxValue) + { + throw new ZipException("Data exceeds maximum length"); + } + + Delete(headerID); + + byte[] newData = new byte[newLength]; + _data.CopyTo(newData, 0); + int index = _data.Length; + _data = newData; + SetShort(ref index, headerID); + SetShort(ref index, addLength); + if (fieldData != null) + { + fieldData.CopyTo(newData, index); + } + } + + /// + /// Start adding a new entry. + /// + /// Add data using , , , or . + /// The new entry is completed and actually added by calling + /// + public void StartNewEntry() + { + _newEntry = new MemoryStream(); + } + + /// + /// Add entry data added since using the ID passed. + /// + /// The identifier to use for this entry. + public void AddNewEntry(int headerID) + { + byte[] newData = _newEntry.ToArray(); + _newEntry = null; + AddEntry(headerID, newData); + } + + /// + /// Add a byte of data to the pending new entry. + /// + /// The byte to add. + /// + public void AddData(byte data) + { + _newEntry.WriteByte(data); + } + + /// + /// Add data to a pending new entry. + /// + /// The data to add. + /// + public void AddData(byte[] data) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + _newEntry.Write(data, 0, data.Length); + } + + /// + /// Add a short value in little endian order to the pending new entry. + /// + /// The data to add. + /// + public void AddLeShort(int toAdd) + { + unchecked + { + _newEntry.WriteByte((byte)toAdd); + _newEntry.WriteByte((byte)(toAdd >> 8)); + } + } + + /// + /// Add an integer value in little endian order to the pending new entry. + /// + /// The data to add. + /// + public void AddLeInt(int toAdd) + { + unchecked + { + AddLeShort((short)toAdd); + AddLeShort((short)(toAdd >> 16)); + } + } + + /// + /// Add a long value in little endian order to the pending new entry. + /// + /// The data to add. + /// + public void AddLeLong(long toAdd) + { + unchecked + { + AddLeInt((int)(toAdd & 0xffffffff)); + AddLeInt((int)(toAdd >> 32)); + } + } + + /// + /// Delete an extra data field. + /// + /// The identifier of the field to delete. + /// Returns true if the field was found and deleted. + public bool Delete(int headerID) + { + bool result = false; + + if (Find(headerID)) + { + result = true; + int trueStart = _readValueStart - 4; + + byte[] newData = new byte[_data.Length - (ValueLength + 4)]; + Array.Copy(_data, 0, newData, 0, trueStart); + + int trueEnd = trueStart + ValueLength + 4; + Array.Copy(_data, trueEnd, newData, trueStart, _data.Length - trueEnd); + _data = newData; + } + return result; + } + + #region Reading Support + + /// + /// Read a long in little endian form from the last found data value + /// + /// Returns the long value read. + public long ReadLong() + { + ReadCheck(8); + return (ReadInt() & 0xffffffff) | (((long)ReadInt()) << 32); + } + + /// + /// Read an integer in little endian form from the last found data value. + /// + /// Returns the integer read. + public int ReadInt() + { + ReadCheck(4); + + int result = _data[_index] + (_data[_index + 1] << 8) + + (_data[_index + 2] << 16) + (_data[_index + 3] << 24); + _index += 4; + return result; + } + + /// + /// Read a short value in little endian form from the last found data value. + /// + /// Returns the short value read. + public int ReadShort() + { + ReadCheck(2); + int result = _data[_index] + (_data[_index + 1] << 8); + _index += 2; + return result; + } + + /// + /// Read a byte from an extra data + /// + /// The byte value read or -1 if the end of data has been reached. + public int ReadByte() + { + int result = -1; + if ((_index < _data.Length) && (_readValueStart + _readValueLength > _index)) + { + result = _data[_index]; + _index += 1; + } + return result; + } + + /// + /// Skip data during reading. + /// + /// The number of bytes to skip. + public void Skip(int amount) + { + ReadCheck(amount); + _index += amount; + } + + private void ReadCheck(int length) + { + if ((_readValueStart > _data.Length) || + (_readValueStart < 4)) + { + throw new ZipException("Find must be called before calling a Read method"); + } + + if (_index > _readValueStart + _readValueLength - length) + { + throw new ZipException("End of extra data"); + } + + if (_index + length < 4) + { + throw new ZipException("Cannot read before start of tag"); + } + } + + /// + /// Internal form of that reads data at any location. + /// + /// Returns the short value read. + private int ReadShortInternal() + { + if (_index > _data.Length - 2) + { + throw new ZipException("End of extra data"); + } + + int result = _data[_index] + (_data[_index + 1] << 8); + _index += 2; + return result; + } + + private void SetShort(ref int index, int source) + { + _data[index] = (byte)source; + _data[index + 1] = (byte)(source >> 8); + index += 2; + } + + #endregion Reading Support + + #region IDisposable Members + + /// + /// Dispose of this instance. + /// + public void Dispose() + { + if (_newEntry != null) + { + _newEntry.Dispose(); + } + } + + #endregion IDisposable Members + + #region Instance Fields + + private int _index; + private int _readValueStart; + private int _readValueLength; + + private MemoryStream _newEntry; + private byte[] _data; + + #endregion Instance Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipFile.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipFile.cs new file mode 100644 index 0000000..db998da --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipFile.cs @@ -0,0 +1,4947 @@ +using BSP_ICSharpCode.SharpZipLib.Checksum; +using BSP_ICSharpCode.SharpZipLib.Core; +using BSP_ICSharpCode.SharpZipLib.Encryption; +using BSP_ICSharpCode.SharpZipLib.Zip.Compression; +using BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + #region Keys Required Event Args + + /// + /// Arguments used with KeysRequiredEvent + /// + public class KeysRequiredEventArgs : EventArgs + { + #region Constructors + + /// + /// Initialise a new instance of + /// + /// The name of the file for which keys are required. + public KeysRequiredEventArgs(string name) + { + fileName = name; + } + + /// + /// Initialise a new instance of + /// + /// The name of the file for which keys are required. + /// The current key value. + public KeysRequiredEventArgs(string name, byte[] keyValue) + { + fileName = name; + key = keyValue; + } + + #endregion Constructors + + #region Properties + + /// + /// Gets the name of the file for which keys are required. + /// + public string FileName + { + get { return fileName; } + } + + /// + /// Gets or sets the key value + /// + public byte[] Key + { + get { return key; } + set { key = value; } + } + + #endregion Properties + + #region Instance Fields + + private readonly string fileName; + private byte[] key; + + #endregion Instance Fields + } + + #endregion Keys Required Event Args + + #region Test Definitions + + /// + /// The strategy to apply to testing. + /// + public enum TestStrategy + { + /// + /// Find the first error only. + /// + FindFirstError, + + /// + /// Find all possible errors. + /// + FindAllErrors, + } + + /// + /// The operation in progress reported by a during testing. + /// + /// TestArchive + public enum TestOperation + { + /// + /// Setting up testing. + /// + Initialising, + + /// + /// Testing an individual entries header + /// + EntryHeader, + + /// + /// Testing an individual entries data + /// + EntryData, + + /// + /// Testing an individual entry has completed. + /// + EntryComplete, + + /// + /// Running miscellaneous tests + /// + MiscellaneousTests, + + /// + /// Testing is complete + /// + Complete, + } + + /// + /// Status returned by during testing. + /// + /// TestArchive + public class TestStatus + { + #region Constructors + + /// + /// Initialise a new instance of + /// + /// The this status applies to. + public TestStatus(ZipFile file) + { + file_ = file; + } + + #endregion Constructors + + #region Properties + + /// + /// Get the current in progress. + /// + public TestOperation Operation + { + get { return operation_; } + } + + /// + /// Get the this status is applicable to. + /// + public ZipFile File + { + get { return file_; } + } + + /// + /// Get the current/last entry tested. + /// + public ZipEntry Entry + { + get { return entry_; } + } + + /// + /// Get the number of errors detected so far. + /// + public int ErrorCount + { + get { return errorCount_; } + } + + /// + /// Get the number of bytes tested so far for the current entry. + /// + public long BytesTested + { + get { return bytesTested_; } + } + + /// + /// Get a value indicating whether the last entry test was valid. + /// + public bool EntryValid + { + get { return entryValid_; } + } + + #endregion Properties + + #region Internal API + + internal void AddError() + { + errorCount_++; + entryValid_ = false; + } + + internal void SetOperation(TestOperation operation) + { + operation_ = operation; + } + + internal void SetEntry(ZipEntry entry) + { + entry_ = entry; + entryValid_ = true; + bytesTested_ = 0; + } + + internal void SetBytesTested(long value) + { + bytesTested_ = value; + } + + #endregion Internal API + + #region Instance Fields + + private readonly ZipFile file_; + private ZipEntry entry_; + private bool entryValid_; + private int errorCount_; + private long bytesTested_; + private TestOperation operation_; + + #endregion Instance Fields + } + + /// + /// Delegate invoked during testing if supplied indicating current progress and status. + /// + /// If the message is non-null an error has occured. If the message is null + /// the operation as found in status has started. + public delegate void ZipTestResultHandler(TestStatus status, string message); + + #endregion Test Definitions + + #region Update Definitions + + /// + /// The possible ways of applying updates to an archive. + /// + public enum FileUpdateMode + { + /// + /// Perform all updates on temporary files ensuring that the original file is saved. + /// + Safe, + + /// + /// Update the archive directly, which is faster but less safe. + /// + Direct, + } + + #endregion Update Definitions + + #region ZipFile Class + + /// + /// This class represents a Zip archive. You can ask for the contained + /// entries, or get an input stream for a file entry. The entry is + /// automatically decompressed. + /// + /// You can also update the archive adding or deleting entries. + /// + /// This class is thread safe for input: You can open input streams for arbitrary + /// entries in different threads. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ /// + /// + /// using System; + /// using System.Text; + /// using System.Collections; + /// using System.IO; + /// + /// using ICSharpCode.SharpZipLib.Zip; + /// + /// class MainClass + /// { + /// static public void Main(string[] args) + /// { + /// using (ZipFile zFile = new ZipFile(args[0])) { + /// Console.WriteLine("Listing of : " + zFile.Name); + /// Console.WriteLine(""); + /// Console.WriteLine("Raw Size Size Date Time Name"); + /// Console.WriteLine("-------- -------- -------- ------ ---------"); + /// foreach (ZipEntry e in zFile) { + /// if ( e.IsFile ) { + /// DateTime d = e.DateTime; + /// Console.WriteLine("{0, -10}{1, -10}{2} {3} {4}", e.Size, e.CompressedSize, + /// d.ToString("dd-MM-yy"), d.ToString("HH:mm"), + /// e.Name); + /// } + /// } + /// } + /// } + /// } + /// + /// + public class ZipFile : IEnumerable, IDisposable + { + #region KeyHandling + + /// + /// Delegate for handling keys/password setting during compression/decompression. + /// + public delegate void KeysRequiredEventHandler( + object sender, + KeysRequiredEventArgs e + ); + + /// + /// Event handler for handling encryption keys. + /// + public KeysRequiredEventHandler KeysRequired; + + /// + /// Handles getting of encryption keys when required. + /// + /// The file for which encryption keys are required. + private void OnKeysRequired(string fileName) + { + if (KeysRequired != null) + { + var krea = new KeysRequiredEventArgs(fileName, key); + KeysRequired(this, krea); + key = krea.Key; + } + } + + /// + /// Get/set the encryption key value. + /// + private byte[] Key + { + get { return key; } + set { key = value; } + } + + /// + /// Password to be used for encrypting/decrypting files. + /// + /// Set to null if no password is required. + public string Password + { + set + { + if (string.IsNullOrEmpty(value)) + { + key = null; + } + else + { + key = PkzipClassic.GenerateKeys(ZipCryptoEncoding.GetBytes(value)); + } + + rawPassword_ = value; + } + } + + /// + /// Get a value indicating whether encryption keys are currently available. + /// + private bool HaveKeys + { + get { return key != null; } + } + + #endregion KeyHandling + + #region Constructors + + /// + /// Opens a Zip file with the given name for reading. + /// + /// The name of the file to open. + /// + /// The argument supplied is null. + /// + /// An i/o error occurs + /// + /// + /// The file doesn't contain a valid zip archive. + /// + public ZipFile(string name, StringCodec stringCodec = null) + { + name_ = name ?? throw new ArgumentNullException(nameof(name)); + + baseStream_ = File.Open(name, FileMode.Open, FileAccess.Read, FileShare.Read); + isStreamOwner = true; + + if (stringCodec != null) + { + _stringCodec = stringCodec; + } + + try + { + ReadEntries(); + } + catch + { + DisposeInternal(true); + throw; + } + } + + /// + /// Opens a Zip file reading the given . + /// + /// The to read archive data from. + /// The supplied argument is null. + /// + /// An i/o error occurs. + /// + /// + /// The file doesn't contain a valid zip archive. + /// + public ZipFile(FileStream file) : + this(file, false) + { + + } + + /// + /// Opens a Zip file reading the given . + /// + /// The to read archive data from. + /// true to leave the file open when the ZipFile is disposed, false to dispose of it + /// The supplied argument is null. + /// + /// An i/o error occurs. + /// + /// + /// The file doesn't contain a valid zip archive. + /// + public ZipFile(FileStream file, bool leaveOpen) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + if (!file.CanSeek) + { + throw new ArgumentException("Stream is not seekable", nameof(file)); + } + + baseStream_ = file; + name_ = file.Name; + isStreamOwner = !leaveOpen; + + try + { + ReadEntries(); + } + catch + { + DisposeInternal(true); + throw; + } + } + + /// + /// Opens a Zip file reading the given . + /// + /// The to read archive data from. + /// + /// An i/o error occurs + /// + /// + /// The stream doesn't contain a valid zip archive.
+ ///
+ /// + /// The stream doesnt support seeking. + /// + /// + /// The stream argument is null. + /// + public ZipFile(Stream stream) : + this(stream, false) + { + + } + + /// + /// Opens a Zip file reading the given . + /// + /// The to read archive data from. + /// true to leave the stream open when the ZipFile is disposed, false to dispose of it + /// + /// + /// An i/o error occurs + /// + /// + /// The stream doesn't contain a valid zip archive.
+ ///
+ /// + /// The stream doesnt support seeking. + /// + /// + /// The stream argument is null. + /// + public ZipFile(Stream stream, bool leaveOpen, StringCodec stringCodec = null) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanSeek) + { + throw new ArgumentException("Stream is not seekable", nameof(stream)); + } + + baseStream_ = stream; + isStreamOwner = !leaveOpen; + + if (stringCodec != null) + { + _stringCodec = stringCodec; + } + + if (baseStream_.Length > 0) + { + try + { + ReadEntries(); + } + catch + { + DisposeInternal(true); + throw; + } + } + else + { + entries_ = Empty.Array(); + isNewArchive_ = true; + } + } + + /// + /// Initialises a default instance with no entries and no file storage. + /// + internal ZipFile() + { + entries_ = Empty.Array(); + isNewArchive_ = true; + } + + #endregion Constructors + + #region Destructors and Closing + + /// + /// Finalize this instance. + /// + ~ZipFile() + { + Dispose(false); + } + + /// + /// Closes the ZipFile. If the stream is owned then this also closes the underlying input stream. + /// Once closed, no further instance methods should be called. + /// + /// + /// An i/o error occurs. + /// + public void Close() + { + DisposeInternal(true); + GC.SuppressFinalize(this); + } + + #endregion Destructors and Closing + + #region Creators + + /// + /// Create a new whose data will be stored in a file. + /// + /// The name of the archive to create. + /// Returns the newly created + /// is null + public static ZipFile Create(string fileName) + { + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + FileStream fs = File.Create(fileName); + + return new ZipFile + { + name_ = fileName, + baseStream_ = fs, + isStreamOwner = true + }; + } + + /// + /// Create a new whose data will be stored on a stream. + /// + /// The stream providing data storage. + /// Returns the newly created + /// is null + /// doesnt support writing. + public static ZipFile Create(Stream outStream) + { + if (outStream == null) + { + throw new ArgumentNullException(nameof(outStream)); + } + + if (!outStream.CanWrite) + { + throw new ArgumentException("Stream is not writeable", nameof(outStream)); + } + + if (!outStream.CanSeek) + { + throw new ArgumentException("Stream is not seekable", nameof(outStream)); + } + + var result = new ZipFile + { + baseStream_ = outStream + }; + return result; + } + + #endregion Creators + + #region Properties + + /// + /// Get/set a flag indicating if the underlying stream is owned by the ZipFile instance. + /// If the flag is true then the stream will be closed when Close is called. + /// + /// + /// The default value is true in all cases. + /// + public bool IsStreamOwner + { + get { return isStreamOwner; } + set { isStreamOwner = value; } + } + + /// + /// Get a value indicating whether + /// this archive is embedded in another file or not. + /// + public bool IsEmbeddedArchive + { + // Not strictly correct in all circumstances currently + get { return offsetOfFirstEntry > 0; } + } + + /// + /// Get a value indicating that this archive is a new one. + /// + public bool IsNewArchive + { + get { return isNewArchive_; } + } + + /// + /// Gets the comment for the zip file. + /// + public string ZipFileComment + { + get { return comment_; } + } + + /// + /// Gets the name of this zip file. + /// + public string Name + { + get { return name_; } + } + + /// + /// Gets the number of entries in this zip file. + /// + /// + /// The Zip file has been closed. + /// + [Obsolete("Use the Count property instead")] + public int Size + { + get + { + return entries_.Length; + } + } + + /// + /// Get the number of entries contained in this . + /// + public long Count + { + get + { + return entries_.Length; + } + } + + /// + /// Indexer property for ZipEntries + /// + [System.Runtime.CompilerServices.IndexerNameAttribute("EntryByIndex")] + public ZipEntry this[int index] + { + get + { + return (ZipEntry)entries_[index].Clone(); + } + } + + + /// + public Encoding ZipCryptoEncoding + { + get => _stringCodec.ZipCryptoEncoding; + set => _stringCodec = _stringCodec.WithZipCryptoEncoding(value); + } + + /// + public StringCodec StringCodec + { + set { + _stringCodec = value; + if (!isNewArchive_) + { + // Since the string codec was changed + ReadEntries(); + } + } + } + + #endregion Properties + + #region Input Handling + + /// + /// Gets an enumerator for the Zip entries in this Zip file. + /// + /// Returns an for this archive. + /// + /// The Zip file has been closed. + /// + public IEnumerator GetEnumerator() + { + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + return new ZipEntryEnumerator(entries_); + } + + /// + /// Return the index of the entry with a matching name + /// + /// Entry name to find + /// If true the comparison is case insensitive + /// The index position of the matching entry or -1 if not found + /// + /// The Zip file has been closed. + /// + public int FindEntry(string name, bool ignoreCase) + { + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + // TODO: This will be slow as the next ice age for huge archives! + for (int i = 0; i < entries_.Length; i++) + { + if (string.Compare(name, entries_[i].Name, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) == 0) + { + return i; + } + } + return -1; + } + + /// + /// Searches for a zip entry in this archive with the given name. + /// String comparisons are case insensitive + /// + /// + /// The name to find. May contain directory components separated by slashes ('/'). + /// + /// + /// A clone of the zip entry, or null if no entry with that name exists. + /// + /// + /// The Zip file has been closed. + /// + public ZipEntry GetEntry(string name) + { + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + int index = FindEntry(name, true); + return (index >= 0) ? (ZipEntry)entries_[index].Clone() : null; + } + + /// + /// Gets an input stream for reading the given zip entry data in an uncompressed form. + /// Normally the should be an entry returned by GetEntry(). + /// + /// The to obtain a data for + /// An input containing data for this + /// + /// The ZipFile has already been closed + /// + /// + /// The compression method for the entry is unknown + /// + /// + /// The entry is not found in the ZipFile + /// + public Stream GetInputStream(ZipEntry entry) + { + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + long index = entry.ZipFileIndex; + if ((index < 0) || (index >= entries_.Length) || (entries_[index].Name != entry.Name)) + { + index = FindEntry(entry.Name, true); + if (index < 0) + { + throw new ZipException("Entry cannot be found"); + } + } + return GetInputStream(index); + } + + /// + /// Creates an input stream reading a zip entry + /// + /// The index of the entry to obtain an input stream for. + /// + /// An input containing data for this + /// + /// + /// The ZipFile has already been closed + /// + /// + /// The compression method for the entry is unknown + /// + /// + /// The entry is not found in the ZipFile + /// + public Stream GetInputStream(long entryIndex) + { + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + long start = LocateEntry(entries_[entryIndex]); + CompressionMethod method = entries_[entryIndex].CompressionMethod; + Stream result = new PartialInputStream(this, start, entries_[entryIndex].CompressedSize); + + if (entries_[entryIndex].IsCrypted == true) + { + result = CreateAndInitDecryptionStream(result, entries_[entryIndex]); + if (result == null) + { + throw new ZipException("Unable to decrypt this entry"); + } + } + + switch (method) + { + case CompressionMethod.Stored: + // read as is. + break; + + case CompressionMethod.Deflated: + // No need to worry about ownership and closing as underlying stream close does nothing. + result = new InflaterInputStream(result, new Inflater(true)); + break; + +// case CompressionMethod.BZip2: +// result = new BZip2.BZip2InputStream(result); +// break; + + default: + throw new ZipException("Unsupported compression method " + method); + } + + return result; + } + + #endregion Input Handling + + #region Archive Testing + + /// + /// Test an archive for integrity/validity + /// + /// Perform low level data Crc check + /// true if all tests pass, false otherwise + /// Testing will terminate on the first error found. + public bool TestArchive(bool testData) + { + return TestArchive(testData, TestStrategy.FindFirstError, null); + } + + /// + /// Test an archive for integrity/validity + /// + /// Perform low level data Crc check + /// The to apply. + /// The handler to call during testing. + /// true if all tests pass, false otherwise + /// The object has already been closed. + public bool TestArchive(bool testData, TestStrategy strategy, ZipTestResultHandler resultHandler) + { + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + var status = new TestStatus(this); + + resultHandler?.Invoke(status, null); + + HeaderTest test = testData ? (HeaderTest.Header | HeaderTest.Extract) : HeaderTest.Header; + + bool testing = true; + + try + { + int entryIndex = 0; + + while (testing && (entryIndex < Count)) + { + if (resultHandler != null) + { + status.SetEntry(this[entryIndex]); + status.SetOperation(TestOperation.EntryHeader); + resultHandler(status, null); + } + + try + { + TestLocalHeader(this[entryIndex], test); + } + catch (ZipException ex) + { + status.AddError(); + + resultHandler?.Invoke(status, $"Exception during test - '{ex.Message}'"); + + testing &= strategy != TestStrategy.FindFirstError; + } + + if (testing && testData && this[entryIndex].IsFile) + { + // Don't check CRC for AES encrypted archives + var checkCRC = this[entryIndex].AESKeySize == 0; + + if (resultHandler != null) + { + status.SetOperation(TestOperation.EntryData); + resultHandler(status, null); + } + + var crc = new Crc32(); + + using (Stream entryStream = this.GetInputStream(this[entryIndex])) + { + byte[] buffer = new byte[4096]; + long totalBytes = 0; + int bytesRead; + while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0) + { + if (checkCRC) + { + crc.Update(new ArraySegment(buffer, 0, bytesRead)); + } + + if (resultHandler != null) + { + totalBytes += bytesRead; + status.SetBytesTested(totalBytes); + resultHandler(status, null); + } + } + } + + if (checkCRC && this[entryIndex].Crc != crc.Value) + { + status.AddError(); + + resultHandler?.Invoke(status, "CRC mismatch"); + + testing &= strategy != TestStrategy.FindFirstError; + } + + if ((this[entryIndex].Flags & (int)GeneralBitFlags.Descriptor) != 0) + { + var data = new DescriptorData(); + ZipFormat.ReadDataDescriptor(baseStream_, this[entryIndex].LocalHeaderRequiresZip64, data); + if (checkCRC && this[entryIndex].Crc != data.Crc) + { + status.AddError(); + resultHandler?.Invoke(status, "Descriptor CRC mismatch"); + } + + if (this[entryIndex].CompressedSize != data.CompressedSize) + { + status.AddError(); + resultHandler?.Invoke(status, "Descriptor compressed size mismatch"); + } + + if (this[entryIndex].Size != data.Size) + { + status.AddError(); + resultHandler?.Invoke(status, "Descriptor size mismatch"); + } + } + } + + if (resultHandler != null) + { + status.SetOperation(TestOperation.EntryComplete); + resultHandler(status, null); + } + + entryIndex += 1; + } + + if (resultHandler != null) + { + status.SetOperation(TestOperation.MiscellaneousTests); + resultHandler(status, null); + } + + // TODO: the 'Corrina Johns' test where local headers are missing from + // the central directory. They are therefore invisible to many archivers. + } + catch (Exception ex) + { + status.AddError(); + + resultHandler?.Invoke(status, $"Exception during test - '{ex.Message}'"); + } + + if (resultHandler != null) + { + status.SetOperation(TestOperation.Complete); + status.SetEntry(null); + resultHandler(status, null); + } + + return (status.ErrorCount == 0); + } + + [Flags] + private enum HeaderTest + { + None = 0x0, + Extract = 0x01, // Check that this header represents an entry whose data can be extracted + Header = 0x02, // Check that this header contents are valid + } + + /// + /// Test a local header against that provided from the central directory + /// + /// + /// The entry to test against + /// + /// The type of tests to carry out. + /// The offset of the entries data in the file + private long TestLocalHeader(ZipEntry entry, HeaderTest tests) + { + lock (baseStream_) + { + bool testHeader = (tests & HeaderTest.Header) != 0; + bool testData = (tests & HeaderTest.Extract) != 0; + + var entryAbsOffset = offsetOfFirstEntry + entry.Offset; + + baseStream_.Seek(entryAbsOffset, SeekOrigin.Begin); + var signature = (int)ReadLEUint(); + + if (signature != ZipConstants.LocalHeaderSignature) + { + throw new ZipException($"Wrong local header signature at 0x{entryAbsOffset:x}, expected 0x{ZipConstants.LocalHeaderSignature:x8}, actual 0x{signature:x8}"); + } + + var extractVersion = (short)(ReadLEUshort() & 0x00ff); + var localFlags = (GeneralBitFlags)ReadLEUshort(); + var compressionMethod = (CompressionMethod)ReadLEUshort(); + var fileTime = (short)ReadLEUshort(); + var fileDate = (short)ReadLEUshort(); + uint crcValue = ReadLEUint(); + long compressedSize = ReadLEUint(); + long size = ReadLEUint(); + int storedNameLength = ReadLEUshort(); + int extraDataLength = ReadLEUshort(); + + byte[] nameData = new byte[storedNameLength]; + StreamUtils.ReadFully(baseStream_, nameData); + + byte[] extraData = new byte[extraDataLength]; + StreamUtils.ReadFully(baseStream_, extraData); + + var localExtraData = new ZipExtraData(extraData); + + // Extra data / zip64 checks + if (localExtraData.Find(headerID: 1)) + { + // 2010-03-04 Forum 10512: removed checks for version >= ZipConstants.VersionZip64 + // and size or compressedSize = MaxValue, due to rogue creators. + + size = localExtraData.ReadLong(); + compressedSize = localExtraData.ReadLong(); + + if (localFlags.HasAny(GeneralBitFlags.Descriptor)) + { + // These may be valid if patched later + if ((size != 0) && (size != entry.Size)) + { + throw new ZipException("Size invalid for descriptor"); + } + + if ((compressedSize != 0) && (compressedSize != entry.CompressedSize)) + { + throw new ZipException("Compressed size invalid for descriptor"); + } + } + } + else + { + // No zip64 extra data but entry requires it. + if ((extractVersion >= ZipConstants.VersionZip64) && + (((uint)size == uint.MaxValue) || ((uint)compressedSize == uint.MaxValue))) + { + throw new ZipException("Required Zip64 extended information missing"); + } + } + + if (testData) + { + if (entry.IsFile) + { + if (!entry.IsCompressionMethodSupported()) + { + throw new ZipException("Compression method not supported"); + } + + if (extractVersion > ZipConstants.VersionMadeBy + || (extractVersion > 20 && extractVersion < ZipConstants.VersionZip64)) + { + throw new ZipException($"Version required to extract this entry not supported ({extractVersion})"); + } + + const GeneralBitFlags notSupportedFlags = GeneralBitFlags.Patched + | GeneralBitFlags.StrongEncryption + | GeneralBitFlags.EnhancedCompress + | GeneralBitFlags.HeaderMasked; + if (localFlags.HasAny(notSupportedFlags)) + { + throw new ZipException($"The library does not support the zip features required to extract this entry ({localFlags & notSupportedFlags:F})"); + } + } + } + + if (testHeader) + { + if ((extractVersion <= 63) && // Ignore later versions as we dont know about them.. + (extractVersion != 10) && + (extractVersion != 11) && + (extractVersion != 20) && + (extractVersion != 21) && + (extractVersion != 25) && + (extractVersion != 27) && + (extractVersion != 45) && + (extractVersion != 46) && + (extractVersion != 50) && + (extractVersion != 51) && + (extractVersion != 52) && + (extractVersion != 61) && + (extractVersion != 62) && + (extractVersion != 63) + ) + { + throw new ZipException($"Version required to extract this entry is invalid ({extractVersion})"); + } + + var localEncoding = _stringCodec.ZipInputEncoding(localFlags); + + // Local entry flags dont have reserved bit set on. + if (localFlags.HasAny(GeneralBitFlags.ReservedPKware4 | GeneralBitFlags.ReservedPkware14 | GeneralBitFlags.ReservedPkware15)) + { + throw new ZipException("Reserved bit flags cannot be set."); + } + + // Encryption requires extract version >= 20 + if (localFlags.HasAny(GeneralBitFlags.Encrypted) && extractVersion < 20) + { + throw new ZipException($"Version required to extract this entry is too low for encryption ({extractVersion})"); + } + + // Strong encryption requires encryption flag to be set and extract version >= 50. + if (localFlags.HasAny(GeneralBitFlags.StrongEncryption)) + { + if (!localFlags.HasAny(GeneralBitFlags.Encrypted)) + { + throw new ZipException("Strong encryption flag set but encryption flag is not set"); + } + + if (extractVersion < 50) + { + throw new ZipException($"Version required to extract this entry is too low for encryption ({extractVersion})"); + } + } + + // Patched entries require extract version >= 27 + if (localFlags.HasAny(GeneralBitFlags.Patched) && extractVersion < 27) + { + throw new ZipException($"Patched data requires higher version than ({extractVersion})"); + } + + // Central header flags match local entry flags. + if ((int)localFlags != entry.Flags) + { + throw new ZipException($"Central header/local header flags mismatch ({(GeneralBitFlags)entry.Flags:F} vs {localFlags:F})"); + } + + // Central header compression method matches local entry + if (entry.CompressionMethodForHeader != compressionMethod) + { + throw new ZipException($"Central header/local header compression method mismatch ({entry.CompressionMethodForHeader:G} vs {compressionMethod:G})"); + } + + if (entry.Version != extractVersion) + { + throw new ZipException("Extract version mismatch"); + } + + // Strong encryption and extract version match + if (localFlags.HasAny(GeneralBitFlags.StrongEncryption)) + { + if (extractVersion < 62) + { + throw new ZipException("Strong encryption flag set but version not high enough"); + } + } + + if (localFlags.HasAny(GeneralBitFlags.HeaderMasked)) + { + if (fileTime != 0 || fileDate != 0) + { + throw new ZipException("Header masked set but date/time values non-zero"); + } + } + + if (!localFlags.HasAny(GeneralBitFlags.Descriptor)) + { + if (crcValue != (uint)entry.Crc) + { + throw new ZipException("Central header/local header crc mismatch"); + } + } + + // Crc valid for empty entry. + // This will also apply to streamed entries where size isn't known and the header cant be patched + if (size == 0 && compressedSize == 0) + { + if (crcValue != 0) + { + throw new ZipException("Invalid CRC for empty entry"); + } + } + + // TODO: make test more correct... can't compare lengths as was done originally as this can fail for MBCS strings + // Assuming a code page at this point is not valid? Best is to store the name length in the ZipEntry probably + if (entry.Name.Length > storedNameLength) + { + throw new ZipException("File name length mismatch"); + } + + // Name data has already been read convert it and compare. + string localName = localEncoding.GetString(nameData); + + // Central directory and local entry name match + if (localName != entry.Name) + { + throw new ZipException("Central header and local header file name mismatch"); + } + + // Directories have zero actual size but can have compressed size + if (entry.IsDirectory) + { + if (size > 0) + { + throw new ZipException("Directory cannot have size"); + } + + // There may be other cases where the compressed size can be greater than this? + // If so until details are known we will be strict. + if (entry.IsCrypted) + { + if (compressedSize > entry.EncryptionOverheadSize + 2) + { + throw new ZipException("Directory compressed size invalid"); + } + } + else if (compressedSize > 2) + { + // When not compressed the directory size can validly be 2 bytes + // if the true size wasn't known when data was originally being written. + // NOTE: Versions of the library 0.85.4 and earlier always added 2 bytes + throw new ZipException("Directory compressed size invalid"); + } + } + + if (!ZipNameTransform.IsValidName(localName, true)) + { + throw new ZipException("Name is invalid"); + } + } + + // Tests that apply to both data and header. + + // Size can be verified only if it is known in the local header. + // it will always be known in the central header. + if (!localFlags.HasAny(GeneralBitFlags.Descriptor) || + ((size > 0 || compressedSize > 0) && entry.Size > 0)) + { + if (size != 0 && size != entry.Size) + { + throw new ZipException($"Size mismatch between central header ({entry.Size}) and local header ({size})"); + } + + if (compressedSize != 0 + && (compressedSize != entry.CompressedSize && compressedSize != 0xFFFFFFFF && compressedSize != -1)) + { + throw new ZipException($"Compressed size mismatch between central header({entry.CompressedSize}) and local header({compressedSize})"); + } + } + + int extraLength = storedNameLength + extraDataLength; + return offsetOfFirstEntry + entry.Offset + ZipConstants.LocalHeaderBaseSize + extraLength; + } + } + + #endregion Archive Testing + + #region Updating + + private const int DefaultBufferSize = 4096; + + /// + /// The kind of update to apply. + /// + private enum UpdateCommand + { + Copy, // Copy original file contents. + Modify, // Change encryption, compression, attributes, name, time etc, of an existing file. + Add, // Add a new file to the archive. + } + + #region Properties + + /// + /// Get / set the to apply to names when updating. + /// + public INameTransform NameTransform + { + get + { + return updateEntryFactory_.NameTransform; + } + + set + { + updateEntryFactory_.NameTransform = value; + } + } + + /// + /// Get/set the used to generate values + /// during updates. + /// + public IEntryFactory EntryFactory + { + get + { + return updateEntryFactory_; + } + + set + { + if (value == null) + { + updateEntryFactory_ = new ZipEntryFactory(); + } + else + { + updateEntryFactory_ = value; + } + } + } + + /// + /// Get /set the buffer size to be used when updating this zip file. + /// + public int BufferSize + { + get { return bufferSize_; } + set + { + if (value < 1024) + { + throw new ArgumentOutOfRangeException(nameof(value), "cannot be below 1024"); + } + + if (bufferSize_ != value) + { + bufferSize_ = value; + copyBuffer_ = null; + } + } + } + + /// + /// Get a value indicating an update has been started. + /// + public bool IsUpdating + { + get { return updates_ != null; } + } + + /// + /// Get / set a value indicating how Zip64 Extension usage is determined when adding entries. + /// + public UseZip64 UseZip64 + { + get { return useZip64_; } + set { useZip64_ = value; } + } + + #endregion Properties + + #region Immediate updating + + // TBD: Direct form of updating + // + // public void Update(IEntryMatcher deleteMatcher) + // { + // } + // + // public void Update(IScanner addScanner) + // { + // } + + #endregion Immediate updating + + #region Deferred Updating + + /// + /// Begin updating this archive. + /// + /// The archive storage for use during the update. + /// The data source to utilise during updating. + /// ZipFile has been closed. + /// One of the arguments provided is null + /// ZipFile has been closed. + public void BeginUpdate(IArchiveStorage archiveStorage, IDynamicDataSource dataSource) + { + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + if (IsEmbeddedArchive) + { + throw new ZipException("Cannot update embedded/SFX archives"); + } + + archiveStorage_ = archiveStorage ?? throw new ArgumentNullException(nameof(archiveStorage)); + updateDataSource_ = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); + + // NOTE: the baseStream_ may not currently support writing or seeking. + + updateIndex_ = new Dictionary(); + + updates_ = new List(entries_.Length); + foreach (ZipEntry entry in entries_) + { + int index = updates_.Count; + updates_.Add(new ZipUpdate(entry)); + updateIndex_.Add(entry.Name, index); + } + + // We must sort by offset before using offset's calculated sizes + updates_.Sort(new UpdateComparer()); + + int idx = 0; + foreach (ZipUpdate update in updates_) + { + //If last entry, there is no next entry offset to use + if (idx == updates_.Count - 1) + break; + + update.OffsetBasedSize = ((ZipUpdate)updates_[idx + 1]).Entry.Offset - update.Entry.Offset; + idx++; + } + updateCount_ = updates_.Count; + + contentsEdited_ = false; + commentEdited_ = false; + newComment_ = null; + } + + /// + /// Begin updating to this archive. + /// + /// The storage to use during the update. + public void BeginUpdate(IArchiveStorage archiveStorage) + { + BeginUpdate(archiveStorage, new DynamicDiskDataSource()); + } + + /// + /// Begin updating this archive. + /// + /// + /// + /// + public void BeginUpdate() + { + if (Name == null) + { + BeginUpdate(new MemoryArchiveStorage(), new DynamicDiskDataSource()); + } + else + { + BeginUpdate(new DiskArchiveStorage(this), new DynamicDiskDataSource()); + } + } + + /// + /// Commit current updates, updating this archive. + /// + /// + /// + /// ZipFile has been closed. + public void CommitUpdate() + { + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + CheckUpdating(); + + try + { + updateIndex_.Clear(); + updateIndex_ = null; + + if (contentsEdited_) + { + RunUpdates(); + } + else if (commentEdited_ && !isNewArchive_) + { + UpdateCommentOnly(); + } + else + { + // Create an empty archive if none existed originally. + if (entries_.Length != 0) return; + byte[] theComment = (newComment_ != null) + ? newComment_.RawComment + : _stringCodec.ZipArchiveCommentEncoding.GetBytes(comment_); + ZipFormat.WriteEndOfCentralDirectory(baseStream_, 0, 0, 0, theComment); + } + } + finally + { + PostUpdateCleanup(); + } + } + + /// + /// Abort updating leaving the archive unchanged. + /// + /// + /// + public void AbortUpdate() + { + PostUpdateCleanup(); + } + + /// + /// Set the file comment to be recorded when the current update is commited. + /// + /// The comment to record. + /// ZipFile has been closed. + public void SetComment(string comment) + { + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + CheckUpdating(); + + newComment_ = new ZipString(comment, _stringCodec.ZipArchiveCommentEncoding); + + if (newComment_.RawLength > 0xffff) + { + newComment_ = null; + throw new ZipException("Comment length exceeds maximum - 65535"); + } + + // We dont take account of the original and current comment appearing to be the same + // as encoding may be different. + commentEdited_ = true; + } + + #endregion Deferred Updating + + #region Adding Entries + + private void AddUpdate(ZipUpdate update) + { + contentsEdited_ = true; + + int index = FindExistingUpdate(update.Entry.Name, isEntryName: true); + + if (index >= 0) + { + if (updates_[index] == null) + { + updateCount_ += 1; + } + + // Direct replacement is faster than delete and add. + updates_[index] = update; + } + else + { + index = updates_.Count; + updates_.Add(update); + updateCount_ += 1; + updateIndex_.Add(update.Entry.Name, index); + } + } + + /// + /// Add a new entry to the archive. + /// + /// The name of the file to add. + /// The compression method to use. + /// Ensure Unicode text is used for name and comment for this entry. + /// Argument supplied is null. + /// ZipFile has been closed. + /// Compression method is not supported for creating entries. + public void Add(string fileName, CompressionMethod compressionMethod, bool useUnicodeText) + { + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + if (isDisposed_) + { + throw new ObjectDisposedException("ZipFile"); + } + + CheckSupportedCompressionMethod(compressionMethod); + CheckUpdating(); + contentsEdited_ = true; + + ZipEntry entry = EntryFactory.MakeFileEntry(fileName); + entry.IsUnicodeText = useUnicodeText; + entry.CompressionMethod = compressionMethod; + + AddUpdate(new ZipUpdate(fileName, entry)); + } + + /// + /// Add a new entry to the archive. + /// + /// The name of the file to add. + /// The compression method to use. + /// ZipFile has been closed. + /// Compression method is not supported for creating entries. + public void Add(string fileName, CompressionMethod compressionMethod) + { + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + CheckSupportedCompressionMethod(compressionMethod); + CheckUpdating(); + contentsEdited_ = true; + + ZipEntry entry = EntryFactory.MakeFileEntry(fileName); + entry.CompressionMethod = compressionMethod; + AddUpdate(new ZipUpdate(fileName, entry)); + } + + /// + /// Add a file to the archive. + /// + /// The name of the file to add. + /// Argument supplied is null. + public void Add(string fileName) + { + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + CheckUpdating(); + AddUpdate(new ZipUpdate(fileName, EntryFactory.MakeFileEntry(fileName))); + } + + /// + /// Add a file to the archive. + /// + /// The name of the file to add. + /// The name to use for the on the Zip file created. + /// Argument supplied is null. + public void Add(string fileName, string entryName) + { + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + if (entryName == null) + { + throw new ArgumentNullException(nameof(entryName)); + } + + CheckUpdating(); + AddUpdate(new ZipUpdate(fileName, EntryFactory.MakeFileEntry(fileName, entryName, true))); + } + + /// + /// Add a file entry with data. + /// + /// The source of the data for this entry. + /// The name to give to the entry. + public void Add(IStaticDataSource dataSource, string entryName) + { + if (dataSource == null) + { + throw new ArgumentNullException(nameof(dataSource)); + } + + if (entryName == null) + { + throw new ArgumentNullException(nameof(entryName)); + } + + CheckUpdating(); + AddUpdate(new ZipUpdate(dataSource, EntryFactory.MakeFileEntry(entryName, false))); + } + + /// + /// Add a file entry with data. + /// + /// The source of the data for this entry. + /// The name to give to the entry. + /// The compression method to use. + /// Compression method is not supported for creating entries. + public void Add(IStaticDataSource dataSource, string entryName, CompressionMethod compressionMethod) + { + if (dataSource == null) + { + throw new ArgumentNullException(nameof(dataSource)); + } + + if (entryName == null) + { + throw new ArgumentNullException(nameof(entryName)); + } + + CheckSupportedCompressionMethod(compressionMethod); + CheckUpdating(); + + ZipEntry entry = EntryFactory.MakeFileEntry(entryName, false); + entry.CompressionMethod = compressionMethod; + + AddUpdate(new ZipUpdate(dataSource, entry)); + } + + /// + /// Add a file entry with data. + /// + /// The source of the data for this entry. + /// The name to give to the entry. + /// The compression method to use. + /// Ensure Unicode text is used for name and comments for this entry. + /// Compression method is not supported for creating entries. + public void Add(IStaticDataSource dataSource, string entryName, CompressionMethod compressionMethod, bool useUnicodeText) + { + if (dataSource == null) + { + throw new ArgumentNullException(nameof(dataSource)); + } + + if (entryName == null) + { + throw new ArgumentNullException(nameof(entryName)); + } + + CheckSupportedCompressionMethod(compressionMethod); + CheckUpdating(); + + ZipEntry entry = EntryFactory.MakeFileEntry(entryName, false); + entry.IsUnicodeText = useUnicodeText; + entry.CompressionMethod = compressionMethod; + + AddUpdate(new ZipUpdate(dataSource, entry)); + } + + /// + /// Add a that contains no data. + /// + /// The entry to add. + /// This can be used to add directories, volume labels, or empty file entries. + public void Add(ZipEntry entry) + { + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + CheckUpdating(); + + if ((entry.Size != 0) || (entry.CompressedSize != 0)) + { + throw new ZipException("Entry cannot have any data"); + } + + AddUpdate(new ZipUpdate(UpdateCommand.Add, entry)); + } + + /// + /// Add a with data. + /// + /// The source of the data for this entry. + /// The entry to add. + /// This can be used to add file entries with a custom data source. + /// + /// The encryption method specified in is unsupported. + /// + /// Compression method is not supported for creating entries. + public void Add(IStaticDataSource dataSource, ZipEntry entry) + { + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + if (dataSource == null) + { + throw new ArgumentNullException(nameof(dataSource)); + } + + // We don't currently support adding entries with AES encryption, so throw + // up front instead of failing or falling back to ZipCrypto later on + if (entry.AESKeySize > 0) + { + throw new NotSupportedException("Creation of AES encrypted entries is not supported"); + } + + CheckSupportedCompressionMethod(entry.CompressionMethod); + CheckUpdating(); + + AddUpdate(new ZipUpdate(dataSource, entry)); + } + + /// + /// Add a directory entry to the archive. + /// + /// The directory to add. + public void AddDirectory(string directoryName) + { + if (directoryName == null) + { + throw new ArgumentNullException(nameof(directoryName)); + } + + CheckUpdating(); + + ZipEntry dirEntry = EntryFactory.MakeDirectoryEntry(directoryName); + AddUpdate(new ZipUpdate(UpdateCommand.Add, dirEntry)); + } + + /// + /// Check if the specified compression method is supported for adding a new entry. + /// + /// The compression method for the new entry. + private static void CheckSupportedCompressionMethod(CompressionMethod compressionMethod) + { + if (compressionMethod != CompressionMethod.Deflated && compressionMethod != CompressionMethod.Stored && compressionMethod != CompressionMethod.BZip2) + { + throw new NotImplementedException("Compression method not supported"); + } + } + + #endregion Adding Entries + + #region Modifying Entries + + /* Modify not yet ready for public consumption. + Direct modification of an entry should not overwrite original data before its read. + Safe mode is trivial in this sense. + public void Modify(ZipEntry original, ZipEntry updated) + { + if ( original == null ) { + throw new ArgumentNullException("original"); + } + if ( updated == null ) { + throw new ArgumentNullException("updated"); + } + CheckUpdating(); + contentsEdited_ = true; + updates_.Add(new ZipUpdate(original, updated)); + } + */ + + #endregion Modifying Entries + + #region Deleting Entries + + /// + /// Delete an entry by name + /// + /// The filename to delete + /// True if the entry was found and deleted; false otherwise. + public bool Delete(string fileName) + { + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + CheckUpdating(); + + bool result = false; + int index = FindExistingUpdate(fileName); + if ((index >= 0) && (updates_[index] != null)) + { + result = true; + contentsEdited_ = true; + updates_[index] = null; + updateCount_ -= 1; + } + else + { + throw new ZipException("Cannot find entry to delete"); + } + return result; + } + + /// + /// Delete a from the archive. + /// + /// The entry to delete. + public void Delete(ZipEntry entry) + { + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + CheckUpdating(); + + int index = FindExistingUpdate(entry); + if (index >= 0) + { + contentsEdited_ = true; + updates_[index] = null; + updateCount_ -= 1; + } + else + { + throw new ZipException("Cannot find entry to delete"); + } + } + + #endregion Deleting Entries + + #region Update Support + + #region Writing Values/Headers + + private void WriteLEShort(int value) + { + baseStream_.WriteByte((byte)(value & 0xff)); + baseStream_.WriteByte((byte)((value >> 8) & 0xff)); + } + + /// + /// Write an unsigned short in little endian byte order. + /// + private void WriteLEUshort(ushort value) + { + baseStream_.WriteByte((byte)(value & 0xff)); + baseStream_.WriteByte((byte)(value >> 8)); + } + + /// + /// Write an int in little endian byte order. + /// + private void WriteLEInt(int value) + { + WriteLEShort(value & 0xffff); + WriteLEShort(value >> 16); + } + + /// + /// Write an unsigned int in little endian byte order. + /// + private void WriteLEUint(uint value) + { + WriteLEUshort((ushort)(value & 0xffff)); + WriteLEUshort((ushort)(value >> 16)); + } + + /// + /// Write a long in little endian byte order. + /// + private void WriteLeLong(long value) + { + WriteLEInt((int)(value & 0xffffffff)); + WriteLEInt((int)(value >> 32)); + } + + private void WriteLEUlong(ulong value) + { + WriteLEUint((uint)(value & 0xffffffff)); + WriteLEUint((uint)(value >> 32)); + } + + private void WriteLocalEntryHeader(ZipUpdate update) + { + ZipEntry entry = update.OutEntry; + + // TODO: Local offset will require adjusting for multi-disk zip files. + entry.Offset = baseStream_.Position; + + // TODO: Need to clear any entry flags that dont make sense or throw an exception here. + if (update.Command != UpdateCommand.Copy) + { + if (entry.CompressionMethod == CompressionMethod.Deflated) + { + if (entry.Size == 0) + { + // No need to compress - no data. + entry.CompressedSize = entry.Size; + entry.Crc = 0; + entry.CompressionMethod = CompressionMethod.Stored; + } + } + else if (entry.CompressionMethod == CompressionMethod.Stored) + { + entry.Flags &= ~(int)GeneralBitFlags.Descriptor; + } + + if (HaveKeys) + { + entry.IsCrypted = true; + if (entry.Crc < 0) + { + entry.Flags |= (int)GeneralBitFlags.Descriptor; + } + } + else + { + entry.IsCrypted = false; + } + + switch (useZip64_) + { + case UseZip64.Dynamic: + if (entry.Size < 0) + { + entry.ForceZip64(); + } + break; + + case UseZip64.On: + entry.ForceZip64(); + break; + + case UseZip64.Off: + // Do nothing. The entry itself may be using Zip64 independently. + break; + } + } + + // Write the local file header + WriteLEInt(ZipConstants.LocalHeaderSignature); + + WriteLEShort(entry.Version); + WriteLEShort(entry.Flags); + + WriteLEShort((byte)entry.CompressionMethodForHeader); + WriteLEInt((int)entry.DosTime); + + if (!entry.HasCrc) + { + // Note patch address for updating CRC later. + update.CrcPatchOffset = baseStream_.Position; + WriteLEInt((int)0); + } + else + { + WriteLEInt(unchecked((int)entry.Crc)); + } + + if (entry.LocalHeaderRequiresZip64) + { + WriteLEInt(-1); + WriteLEInt(-1); + } + else + { + if ((entry.CompressedSize < 0) || (entry.Size < 0)) + { + update.SizePatchOffset = baseStream_.Position; + } + + WriteLEInt((int)entry.CompressedSize); + WriteLEInt((int)entry.Size); + } + + var entryEncoding = _stringCodec.ZipInputEncoding(entry.Flags); + byte[] name = entryEncoding.GetBytes(entry.Name); + + if (name.Length > 0xFFFF) + { + throw new ZipException("Entry name too long."); + } + + var ed = new ZipExtraData(entry.ExtraData); + + if (entry.LocalHeaderRequiresZip64) + { + ed.StartNewEntry(); + + // Local entry header always includes size and compressed size. + // NOTE the order of these fields is reversed when compared to the normal headers! + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize); + ed.AddNewEntry(1); + } + else + { + ed.Delete(1); + } + + entry.ExtraData = ed.GetEntryData(); + + WriteLEShort(name.Length); + WriteLEShort(entry.ExtraData.Length); + + if (name.Length > 0) + { + baseStream_.Write(name, 0, name.Length); + } + + if (entry.LocalHeaderRequiresZip64) + { + if (!ed.Find(1)) + { + throw new ZipException("Internal error cannot find extra data"); + } + + update.SizePatchOffset = baseStream_.Position + ed.CurrentReadIndex; + } + + if (entry.ExtraData.Length > 0) + { + baseStream_.Write(entry.ExtraData, 0, entry.ExtraData.Length); + } + } + + private int WriteCentralDirectoryHeader(ZipEntry entry) + { + if (entry.CompressedSize < 0) + { + throw new ZipException("Attempt to write central directory entry with unknown csize"); + } + + if (entry.Size < 0) + { + throw new ZipException("Attempt to write central directory entry with unknown size"); + } + + if (entry.Crc < 0) + { + throw new ZipException("Attempt to write central directory entry with unknown crc"); + } + + // Write the central file header + WriteLEInt(ZipConstants.CentralHeaderSignature); + + // Version made by + WriteLEShort((entry.HostSystem << 8) | entry.VersionMadeBy); + + // Version required to extract + WriteLEShort(entry.Version); + + WriteLEShort(entry.Flags); + + unchecked + { + WriteLEShort((byte)entry.CompressionMethodForHeader); + WriteLEInt((int)entry.DosTime); + WriteLEInt((int)entry.Crc); + } + + bool useExtraCompressedSize = false; //Do we want to store the compressed size in the extra data? + if ((entry.IsZip64Forced()) || (entry.CompressedSize >= 0xffffffff)) + { + useExtraCompressedSize = true; + WriteLEInt(-1); + } + else + { + WriteLEInt((int)(entry.CompressedSize & 0xffffffff)); + } + + bool useExtraUncompressedSize = false; //Do we want to store the uncompressed size in the extra data? + if ((entry.IsZip64Forced()) || (entry.Size >= 0xffffffff)) + { + useExtraUncompressedSize = true; + WriteLEInt(-1); + } + else + { + WriteLEInt((int)entry.Size); + } + + var entryEncoding = _stringCodec.ZipInputEncoding(entry.Flags); + byte[] name = entryEncoding.GetBytes(entry.Name); + + if (name.Length > 0xFFFF) + { + throw new ZipException("Entry name is too long."); + } + + WriteLEShort(name.Length); + + // Central header extra data is different to local header version so regenerate. + var ed = new ZipExtraData(entry.ExtraData); + + if (entry.CentralHeaderRequiresZip64) + { + ed.StartNewEntry(); + + if (useExtraUncompressedSize) + { + ed.AddLeLong(entry.Size); + } + + if (useExtraCompressedSize) + { + ed.AddLeLong(entry.CompressedSize); + } + + if (entry.Offset >= 0xffffffff) + { + ed.AddLeLong(entry.Offset); + } + + // Number of disk on which this file starts isnt supported and is never written here. + ed.AddNewEntry(1); + } + else + { + // Should have already be done when local header was added. + ed.Delete(1); + } + + byte[] centralExtraData = ed.GetEntryData(); + + WriteLEShort(centralExtraData.Length); + WriteLEShort(entry.Comment != null ? entry.Comment.Length : 0); + + WriteLEShort(0); // disk number + WriteLEShort(0); // internal file attributes + + // External file attributes... + if (entry.ExternalFileAttributes != -1) + { + WriteLEInt(entry.ExternalFileAttributes); + } + else + { + if (entry.IsDirectory) + { + WriteLEUint(16); + } + else + { + WriteLEUint(0); + } + } + + if (entry.Offset >= 0xffffffff) + { + WriteLEUint(0xffffffff); + } + else + { + WriteLEUint((uint)(int)entry.Offset); + } + + if (name.Length > 0) + { + baseStream_.Write(name, 0, name.Length); + } + + if (centralExtraData.Length > 0) + { + baseStream_.Write(centralExtraData, 0, centralExtraData.Length); + } + + byte[] rawComment = (entry.Comment != null) ? Encoding.ASCII.GetBytes(entry.Comment) : Empty.Array(); + + if (rawComment.Length > 0) + { + baseStream_.Write(rawComment, 0, rawComment.Length); + } + + return ZipConstants.CentralHeaderBaseSize + name.Length + centralExtraData.Length + rawComment.Length; + } + + #endregion Writing Values/Headers + + private void PostUpdateCleanup() + { + updateDataSource_ = null; + updates_ = null; + updateIndex_ = null; + + if (archiveStorage_ != null) + { + archiveStorage_.Dispose(); + archiveStorage_ = null; + } + } + + private string GetTransformedFileName(string name) + { + INameTransform transform = NameTransform; + return (transform != null) ? + transform.TransformFile(name) : + name; + } + + private string GetTransformedDirectoryName(string name) + { + INameTransform transform = NameTransform; + return (transform != null) ? + transform.TransformDirectory(name) : + name; + } + + /// + /// Get a raw memory buffer. + /// + /// Returns a raw memory buffer. + private byte[] GetBuffer() + { + if (copyBuffer_ == null) + { + copyBuffer_ = new byte[bufferSize_]; + } + return copyBuffer_; + } + + private void CopyDescriptorBytes(ZipUpdate update, Stream dest, Stream source) + { + // Don't include the signature size to allow copy without seeking + var bytesToCopy = GetDescriptorSize(update, false); + + // Don't touch the source stream if no descriptor is present + if (bytesToCopy == 0) return; + + var buffer = GetBuffer(); + + // Copy the first 4 bytes of the descriptor + source.Read(buffer, 0, sizeof(int)); + dest.Write(buffer, 0, sizeof(int)); + + if (BitConverter.ToUInt32(buffer, 0) != ZipConstants.DataDescriptorSignature) + { + // The initial bytes wasn't the descriptor, reduce the pending byte count + bytesToCopy -= buffer.Length; + } + + while (bytesToCopy > 0) + { + int readSize = Math.Min(buffer.Length, bytesToCopy); + + int bytesRead = source.Read(buffer, 0, readSize); + if (bytesRead > 0) + { + dest.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + } + else + { + throw new ZipException("Unxpected end of stream"); + } + } + } + + private void CopyBytes(ZipUpdate update, Stream destination, Stream source, + long bytesToCopy, bool updateCrc) + { + if (destination == source) + { + throw new InvalidOperationException("Destination and source are the same"); + } + + // NOTE: Compressed size is updated elsewhere. + var crc = new Crc32(); + byte[] buffer = GetBuffer(); + + long targetBytes = bytesToCopy; + long totalBytesRead = 0; + + int bytesRead; + do + { + int readSize = buffer.Length; + + if (bytesToCopy < readSize) + { + readSize = (int)bytesToCopy; + } + + bytesRead = source.Read(buffer, 0, readSize); + if (bytesRead > 0) + { + if (updateCrc) + { + crc.Update(new ArraySegment(buffer, 0, bytesRead)); + } + destination.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + totalBytesRead += bytesRead; + } + } + while ((bytesRead > 0) && (bytesToCopy > 0)); + + if (totalBytesRead != targetBytes) + { + throw new ZipException(string.Format("Failed to copy bytes expected {0} read {1}", targetBytes, totalBytesRead)); + } + + if (updateCrc) + { + update.OutEntry.Crc = crc.Value; + } + } + + /// + /// Get the size of the source descriptor for a . + /// + /// The update to get the size for. + /// Whether to include the signature size + /// The descriptor size, zero if there isn't one. + private static int GetDescriptorSize(ZipUpdate update, bool includingSignature) + { + if (!((GeneralBitFlags)update.Entry.Flags).HasAny(GeneralBitFlags.Descriptor)) + return 0; + + var descriptorWithSignature = update.Entry.LocalHeaderRequiresZip64 + ? ZipConstants.Zip64DataDescriptorSize + : ZipConstants.DataDescriptorSize; + + return includingSignature + ? descriptorWithSignature + : descriptorWithSignature - sizeof(int); + } + + private void CopyDescriptorBytesDirect(ZipUpdate update, Stream stream, ref long destinationPosition, long sourcePosition) + { + var buffer = GetBuffer(); ; + + stream.Position = sourcePosition; + stream.Read(buffer, 0, sizeof(int)); + var sourceHasSignature = BitConverter.ToUInt32(buffer, 0) == ZipConstants.DataDescriptorSignature; + + var bytesToCopy = GetDescriptorSize(update, sourceHasSignature); + + while (bytesToCopy > 0) + { + stream.Position = sourcePosition; + + var bytesRead = stream.Read(buffer, 0, bytesToCopy); + if (bytesRead > 0) + { + stream.Position = destinationPosition; + stream.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + destinationPosition += bytesRead; + sourcePosition += bytesRead; + } + else + { + throw new ZipException("Unexpected end of stream"); + } + } + } + + private void CopyEntryDataDirect(ZipUpdate update, Stream stream, bool updateCrc, ref long destinationPosition, ref long sourcePosition) + { + long bytesToCopy = update.Entry.CompressedSize; + + // NOTE: Compressed size is updated elsewhere. + var crc = new Crc32(); + byte[] buffer = GetBuffer(); + + long targetBytes = bytesToCopy; + long totalBytesRead = 0; + + int bytesRead; + do + { + int readSize = buffer.Length; + + if (bytesToCopy < readSize) + { + readSize = (int)bytesToCopy; + } + + stream.Position = sourcePosition; + bytesRead = stream.Read(buffer, 0, readSize); + if (bytesRead > 0) + { + if (updateCrc) + { + crc.Update(new ArraySegment(buffer, 0, bytesRead)); + } + stream.Position = destinationPosition; + stream.Write(buffer, 0, bytesRead); + + destinationPosition += bytesRead; + sourcePosition += bytesRead; + bytesToCopy -= bytesRead; + totalBytesRead += bytesRead; + } + } + while ((bytesRead > 0) && (bytesToCopy > 0)); + + if (totalBytesRead != targetBytes) + { + throw new ZipException(string.Format("Failed to copy bytes expected {0} read {1}", targetBytes, totalBytesRead)); + } + + if (updateCrc) + { + update.OutEntry.Crc = crc.Value; + } + } + + private int FindExistingUpdate(ZipEntry entry) + { + int result = -1; + if (updateIndex_.ContainsKey(entry.Name)) + { + result = (int)updateIndex_[entry.Name]; + } + /* + // This is slow like the coming of the next ice age but takes less storage and may be useful + // for CF? + for (int index = 0; index < updates_.Count; ++index) + { + ZipUpdate zu = ( ZipUpdate )updates_[index]; + if ( (zu.Entry.ZipFileIndex == entry.ZipFileIndex) && + (string.Compare(convertedName, zu.Entry.Name, true, CultureInfo.InvariantCulture) == 0) ) { + result = index; + break; + } + } + */ + return result; + } + + private int FindExistingUpdate(string fileName, bool isEntryName = false) + { + int result = -1; + + string convertedName = !isEntryName ? GetTransformedFileName(fileName) : fileName; + + if (updateIndex_.ContainsKey(convertedName)) + { + result = (int)updateIndex_[convertedName]; + } + + /* + // This is slow like the coming of the next ice age but takes less storage and may be useful + // for CF? + for ( int index = 0; index < updates_.Count; ++index ) { + if ( string.Compare(convertedName, (( ZipUpdate )updates_[index]).Entry.Name, + true, CultureInfo.InvariantCulture) == 0 ) { + result = index; + break; + } + } + */ + + return result; + } + + /// + /// Get an output stream for the specified + /// + /// The entry to get an output stream for. + /// The output stream obtained for the entry. + private Stream GetOutputStream(ZipEntry entry) + { + Stream result = baseStream_; + + if (entry.IsCrypted == true) + { + result = CreateAndInitEncryptionStream(result, entry); + } + + switch (entry.CompressionMethod) + { + case CompressionMethod.Stored: + if (!entry.IsCrypted) + { + // If there is an encryption stream in use, that can be returned directly + // otherwise, wrap the base stream in an UncompressedStream instead of returning it directly + result = new UncompressedStream(result); + } + break; + + case CompressionMethod.Deflated: + var dos = new DeflaterOutputStream(result, new Deflater(9, true)) + { + // If there is an encryption stream in use, then we want that to be disposed when the deflator stream is disposed + // If not, then we don't want it to dispose the base stream + IsStreamOwner = entry.IsCrypted + }; + result = dos; + break; + +// case CompressionMethod.BZip2: +// var bzos = new BZip2.BZip2OutputStream(result) +// { +// // If there is an encryption stream in use, then we want that to be disposed when the BZip2OutputStream stream is disposed +// // If not, then we don't want it to dispose the base stream +// IsStreamOwner = entry.IsCrypted +// }; +// result = bzos; +// break; + + default: + throw new ZipException("Unknown compression method " + entry.CompressionMethod); + } + return result; + } + + private void AddEntry(ZipFile workFile, ZipUpdate update) + { + Stream source = null; + + if (update.Entry.IsFile) + { + source = update.GetSource(); + + if (source == null) + { + source = updateDataSource_.GetSource(update.Entry, update.Filename); + } + } + + var useCrc = update.Entry.AESKeySize == 0; + + if (source != null) + { + using (source) + { + long sourceStreamLength = source.Length; + if (update.OutEntry.Size < 0) + { + update.OutEntry.Size = sourceStreamLength; + } + else + { + // Check for errant entries. + if (update.OutEntry.Size != sourceStreamLength) + { + throw new ZipException("Entry size/stream size mismatch"); + } + } + + workFile.WriteLocalEntryHeader(update); + + long dataStart = workFile.baseStream_.Position; + + using (Stream output = workFile.GetOutputStream(update.OutEntry)) + { + CopyBytes(update, output, source, sourceStreamLength, useCrc); + } + + long dataEnd = workFile.baseStream_.Position; + update.OutEntry.CompressedSize = dataEnd - dataStart; + + if ((update.OutEntry.Flags & (int)GeneralBitFlags.Descriptor) == (int)GeneralBitFlags.Descriptor) + { + ZipFormat.WriteDataDescriptor(workFile.baseStream_, update.OutEntry); + } + } + } + else + { + workFile.WriteLocalEntryHeader(update); + update.OutEntry.CompressedSize = 0; + } + } + + private void ModifyEntry(ZipFile workFile, ZipUpdate update) + { + workFile.WriteLocalEntryHeader(update); + long dataStart = workFile.baseStream_.Position; + + // TODO: This is slow if the changes don't effect the data!! + if (update.Entry.IsFile && (update.Filename != null)) + { + using (Stream output = workFile.GetOutputStream(update.OutEntry)) + { + using (Stream source = this.GetInputStream(update.Entry)) + { + CopyBytes(update, output, source, source.Length, true); + } + } + } + + long dataEnd = workFile.baseStream_.Position; + update.Entry.CompressedSize = dataEnd - dataStart; + } + + private void CopyEntryDirect(ZipFile workFile, ZipUpdate update, ref long destinationPosition) + { + bool skipOver = false || update.Entry.Offset == destinationPosition; + + if (!skipOver) + { + baseStream_.Position = destinationPosition; + workFile.WriteLocalEntryHeader(update); + destinationPosition = baseStream_.Position; + } + + long sourcePosition = 0; + + const int NameLengthOffset = 26; + + // TODO: Add base for SFX friendly handling + long entryDataOffset = update.Entry.Offset + NameLengthOffset; + + baseStream_.Seek(entryDataOffset, SeekOrigin.Begin); + + // Clumsy way of handling retrieving the original name and extra data length for now. + // TODO: Stop re-reading name and data length in CopyEntryDirect. + + uint nameLength = ReadLEUshort(); + uint extraLength = ReadLEUshort(); + + sourcePosition = baseStream_.Position + nameLength + extraLength; + + if (skipOver) + { + if (update.OffsetBasedSize != -1) + { + destinationPosition += update.OffsetBasedSize; + } + else + { + // Skip entry header + destinationPosition += (sourcePosition - entryDataOffset) + NameLengthOffset; + + // Skip entry compressed data + destinationPosition += update.Entry.CompressedSize; + + // Seek to end of entry to check for descriptor signature + baseStream_.Seek(destinationPosition, SeekOrigin.Begin); + + var descriptorHasSignature = ReadLEUint() == ZipConstants.DataDescriptorSignature; + + // Skip descriptor and it's signature (if present) + destinationPosition += GetDescriptorSize(update, descriptorHasSignature); + } + } + else + { + if (update.Entry.CompressedSize > 0) + { + CopyEntryDataDirect(update, baseStream_, false, ref destinationPosition, ref sourcePosition); + } + CopyDescriptorBytesDirect(update, baseStream_, ref destinationPosition, sourcePosition); + } + } + + private void CopyEntry(ZipFile workFile, ZipUpdate update) + { + workFile.WriteLocalEntryHeader(update); + + if (update.Entry.CompressedSize > 0) + { + const int NameLengthOffset = 26; + + long entryDataOffset = update.Entry.Offset + NameLengthOffset; + + // TODO: This wont work for SFX files! + baseStream_.Seek(entryDataOffset, SeekOrigin.Begin); + + uint nameLength = ReadLEUshort(); + uint extraLength = ReadLEUshort(); + + baseStream_.Seek(nameLength + extraLength, SeekOrigin.Current); + + CopyBytes(update, workFile.baseStream_, baseStream_, update.Entry.CompressedSize, false); + } + CopyDescriptorBytes(update, workFile.baseStream_, baseStream_); + } + + private void Reopen(Stream source) + { + isNewArchive_ = false; + baseStream_ = source ?? throw new ZipException("Failed to reopen archive - no source"); + ReadEntries(); + } + + private void Reopen() + { + if (Name == null) + { + throw new InvalidOperationException("Name is not known cannot Reopen"); + } + + Reopen(File.Open(Name, FileMode.Open, FileAccess.Read, FileShare.Read)); + } + + private void UpdateCommentOnly() + { + long baseLength = baseStream_.Length; + + Stream updateFile; + + if (archiveStorage_.UpdateMode == FileUpdateMode.Safe) + { + updateFile = archiveStorage_.MakeTemporaryCopy(baseStream_); + + baseStream_.Dispose(); + baseStream_ = null; + } + else + { + if (archiveStorage_.UpdateMode == FileUpdateMode.Direct) + { + // TODO: archiveStorage wasnt originally intended for this use. + // Need to revisit this to tidy up handling as archive storage currently doesnt + // handle the original stream well. + // The problem is when using an existing zip archive with an in memory archive storage. + // The open stream wont support writing but the memory storage should open the same file not an in memory one. + + // Need to tidy up the archive storage interface and contract basically. + baseStream_ = archiveStorage_.OpenForDirectUpdate(baseStream_); + updateFile = baseStream_; + } + else + { + baseStream_.Dispose(); + baseStream_ = null; + updateFile = new FileStream(Name, FileMode.Open, FileAccess.ReadWrite); + } + } + + try + { + long locatedCentralDirOffset = + ZipFormat.LocateBlockWithSignature(updateFile, ZipConstants.EndOfCentralDirectorySignature, + baseLength, ZipConstants.EndOfCentralRecordBaseSize, 0xffff); + if (locatedCentralDirOffset < 0) + { + throw new ZipException("Cannot find central directory"); + } + + const int CentralHeaderCommentSizeOffset = 16; + updateFile.Position += CentralHeaderCommentSizeOffset; + + byte[] rawComment = newComment_.RawComment; + + updateFile.WriteLEShort(rawComment.Length); + updateFile.Write(rawComment, 0, rawComment.Length); + updateFile.SetLength(updateFile.Position); + } + finally + { + if(updateFile != baseStream_) + updateFile.Dispose(); + } + + if (archiveStorage_.UpdateMode == FileUpdateMode.Safe) + { + Reopen(archiveStorage_.ConvertTemporaryToFinal()); + } + else + { + ReadEntries(); + } + } + + /// + /// Class used to sort updates. + /// + private class UpdateComparer : IComparer + { + /// + /// Compares two objects and returns a value indicating whether one is + /// less than, equal to or greater than the other. + /// + /// First object to compare + /// Second object to compare. + /// Compare result. + public int Compare(ZipUpdate x, ZipUpdate y) + { + int result; + + if (x == null) + { + if (y == null) + { + result = 0; + } + else + { + result = -1; + } + } + else if (y == null) + { + result = 1; + } + else + { + int xCmdValue = ((x.Command == UpdateCommand.Copy) || (x.Command == UpdateCommand.Modify)) ? 0 : 1; + int yCmdValue = ((y.Command == UpdateCommand.Copy) || (y.Command == UpdateCommand.Modify)) ? 0 : 1; + + result = xCmdValue - yCmdValue; + if (result == 0) + { + long offsetDiff = x.Entry.Offset - y.Entry.Offset; + if (offsetDiff < 0) + { + result = -1; + } + else if (offsetDiff == 0) + { + result = 0; + } + else + { + result = 1; + } + } + } + return result; + } + } + + private void RunUpdates() + { + long sizeEntries = 0; + long endOfStream = 0; + bool directUpdate = false; + long destinationPosition = 0; // NOT SFX friendly + + ZipFile workFile; + + if (IsNewArchive) + { + workFile = this; + workFile.baseStream_.Position = 0; + directUpdate = true; + } + else if (archiveStorage_.UpdateMode == FileUpdateMode.Direct) + { + workFile = this; + workFile.baseStream_.Position = 0; + directUpdate = true; + + // Sort the updates by offset within copies/modifies, then adds. + // This ensures that data required by copies will not be overwritten. + updates_.Sort(new UpdateComparer()); + } + else + { + workFile = ZipFile.Create(archiveStorage_.GetTemporaryOutput()); + workFile.UseZip64 = UseZip64; + + if (key != null) + { + workFile.key = (byte[])key.Clone(); + } + } + + try + { + foreach (ZipUpdate update in updates_) + { + if (update != null) + { + switch (update.Command) + { + case UpdateCommand.Copy: + if (directUpdate) + { + CopyEntryDirect(workFile, update, ref destinationPosition); + } + else + { + CopyEntry(workFile, update); + } + break; + + case UpdateCommand.Modify: + // TODO: Direct modifying of an entry will take some legwork. + ModifyEntry(workFile, update); + break; + + case UpdateCommand.Add: + if (!IsNewArchive && directUpdate) + { + workFile.baseStream_.Position = destinationPosition; + } + + AddEntry(workFile, update); + + if (directUpdate) + { + destinationPosition = workFile.baseStream_.Position; + } + break; + } + } + } + + if (!IsNewArchive && directUpdate) + { + workFile.baseStream_.Position = destinationPosition; + } + + long centralDirOffset = workFile.baseStream_.Position; + + foreach (ZipUpdate update in updates_) + { + if (update != null) + { + sizeEntries += workFile.WriteCentralDirectoryHeader(update.OutEntry); + } + } + + byte[] theComment = newComment_?.RawComment ?? _stringCodec.ZipArchiveCommentEncoding.GetBytes(comment_); + ZipFormat.WriteEndOfCentralDirectory(workFile.baseStream_, updateCount_, + sizeEntries, centralDirOffset, theComment); + + endOfStream = workFile.baseStream_.Position; + + // And now patch entries... + foreach (ZipUpdate update in updates_) + { + if (update != null) + { + // If the size of the entry is zero leave the crc as 0 as well. + // The calculated crc will be all bits on... + if ((update.CrcPatchOffset > 0) && (update.OutEntry.CompressedSize > 0)) + { + workFile.baseStream_.Position = update.CrcPatchOffset; + workFile.WriteLEInt((int)update.OutEntry.Crc); + } + + if (update.SizePatchOffset > 0) + { + workFile.baseStream_.Position = update.SizePatchOffset; + if (update.OutEntry.LocalHeaderRequiresZip64) + { + workFile.WriteLeLong(update.OutEntry.Size); + workFile.WriteLeLong(update.OutEntry.CompressedSize); + } + else + { + workFile.WriteLEInt((int)update.OutEntry.CompressedSize); + workFile.WriteLEInt((int)update.OutEntry.Size); + } + } + } + } + } + catch + { + workFile.Close(); + if (!directUpdate && (workFile.Name != null)) + { + File.Delete(workFile.Name); + } + throw; + } + + if (directUpdate) + { + workFile.baseStream_.SetLength(endOfStream); + workFile.baseStream_.Flush(); + isNewArchive_ = false; + ReadEntries(); + } + else + { + baseStream_.Dispose(); + Reopen(archiveStorage_.ConvertTemporaryToFinal()); + } + } + + private void CheckUpdating() + { + if (updates_ == null) + { + throw new InvalidOperationException("BeginUpdate has not been called"); + } + } + + #endregion Update Support + + #region ZipUpdate class + + /// + /// Represents a pending update to a Zip file. + /// + private class ZipUpdate + { + #region Constructors + + public ZipUpdate(string fileName, ZipEntry entry) + { + command_ = UpdateCommand.Add; + entry_ = entry; + filename_ = fileName; + } + + [Obsolete] + public ZipUpdate(string fileName, string entryName, CompressionMethod compressionMethod) + { + command_ = UpdateCommand.Add; + entry_ = new ZipEntry(entryName) + { + CompressionMethod = compressionMethod + }; + filename_ = fileName; + } + + [Obsolete] + public ZipUpdate(string fileName, string entryName) + : this(fileName, entryName, CompressionMethod.Deflated) + { + // Do nothing. + } + + [Obsolete] + public ZipUpdate(IStaticDataSource dataSource, string entryName, CompressionMethod compressionMethod) + { + command_ = UpdateCommand.Add; + entry_ = new ZipEntry(entryName) + { + CompressionMethod = compressionMethod + }; + dataSource_ = dataSource; + } + + public ZipUpdate(IStaticDataSource dataSource, ZipEntry entry) + { + command_ = UpdateCommand.Add; + entry_ = entry; + dataSource_ = dataSource; + } + + public ZipUpdate(ZipEntry original, ZipEntry updated) + { + throw new ZipException("Modify not currently supported"); + /* + command_ = UpdateCommand.Modify; + entry_ = ( ZipEntry )original.Clone(); + outEntry_ = ( ZipEntry )updated.Clone(); + */ + } + + public ZipUpdate(UpdateCommand command, ZipEntry entry) + { + command_ = command; + entry_ = (ZipEntry)entry.Clone(); + } + + /// + /// Copy an existing entry. + /// + /// The existing entry to copy. + public ZipUpdate(ZipEntry entry) + : this(UpdateCommand.Copy, entry) + { + // Do nothing. + } + + #endregion Constructors + + /// + /// Get the for this update. + /// + /// This is the source or original entry. + public ZipEntry Entry + { + get { return entry_; } + } + + /// + /// Get the that will be written to the updated/new file. + /// + public ZipEntry OutEntry + { + get + { + if (outEntry_ == null) + { + outEntry_ = (ZipEntry)entry_.Clone(); + } + + return outEntry_; + } + } + + /// + /// Get the command for this update. + /// + public UpdateCommand Command + { + get { return command_; } + } + + /// + /// Get the filename if any for this update. Null if none exists. + /// + public string Filename + { + get { return filename_; } + } + + /// + /// Get/set the location of the size patch for this update. + /// + public long SizePatchOffset + { + get { return sizePatchOffset_; } + set { sizePatchOffset_ = value; } + } + + /// + /// Get /set the location of the crc patch for this update. + /// + public long CrcPatchOffset + { + get { return crcPatchOffset_; } + set { crcPatchOffset_ = value; } + } + + /// + /// Get/set the size calculated by offset. + /// Specifically, the difference between this and next entry's starting offset. + /// + public long OffsetBasedSize + { + get { return _offsetBasedSize; } + set { _offsetBasedSize = value; } + } + + public Stream GetSource() + { + Stream result = null; + if (dataSource_ != null) + { + result = dataSource_.GetSource(); + } + + return result; + } + + #region Instance Fields + + private ZipEntry entry_; + private ZipEntry outEntry_; + private readonly UpdateCommand command_; + private IStaticDataSource dataSource_; + private readonly string filename_; + private long sizePatchOffset_ = -1; + private long crcPatchOffset_ = -1; + private long _offsetBasedSize = -1; + + #endregion Instance Fields + } + + #endregion ZipUpdate class + + #endregion Updating + + #region Disposing + + #region IDisposable Members + + void IDisposable.Dispose() + { + Close(); + } + + #endregion IDisposable Members + + private void DisposeInternal(bool disposing) + { + if (!isDisposed_) + { + isDisposed_ = true; + entries_ = Empty.Array(); + + if (IsStreamOwner && (baseStream_ != null)) + { + lock (baseStream_) + { + baseStream_.Dispose(); + } + } + + PostUpdateCleanup(); + } + } + + /// + /// Releases the unmanaged resources used by the this instance and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + DisposeInternal(disposing); + } + + #endregion Disposing + + #region Internal routines + + #region Reading + + /// + /// Read an unsigned short in little endian byte order. + /// + /// Returns the value read. + /// + /// The stream ends prematurely + /// + private ushort ReadLEUshort() + { + int data1 = baseStream_.ReadByte(); + + if (data1 < 0) + { + throw new EndOfStreamException("End of stream"); + } + + int data2 = baseStream_.ReadByte(); + + if (data2 < 0) + { + throw new EndOfStreamException("End of stream"); + } + + return unchecked((ushort)((ushort)data1 | (ushort)(data2 << 8))); + } + + /// + /// Read a uint in little endian byte order. + /// + /// Returns the value read. + /// + /// An i/o error occurs. + /// + /// + /// The file ends prematurely + /// + private uint ReadLEUint() + { + return (uint)(ReadLEUshort() | (ReadLEUshort() << 16)); + } + + private ulong ReadLEUlong() + { + return ReadLEUint() | ((ulong)ReadLEUint() << 32); + } + + #endregion Reading + + // NOTE this returns the offset of the first byte after the signature. + private long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize, int maximumVariableData) + => ZipFormat.LocateBlockWithSignature(baseStream_, signature, endLocation, minimumBlockSize, maximumVariableData); + + /// + /// Search for and read the central directory of a zip file filling the entries array. + /// + /// + /// An i/o error occurs. + /// + /// + /// The central directory is malformed or cannot be found + /// + private void ReadEntries() + { + // Search for the End Of Central Directory. When a zip comment is + // present the directory will start earlier + // + // The search is limited to 64K which is the maximum size of a trailing comment field to aid speed. + // This should be compatible with both SFX and ZIP files but has only been tested for Zip files + // If a SFX file has the Zip data attached as a resource and there are other resources occurring later then + // this could be invalid. + // Could also speed this up by reading memory in larger blocks. + + if (baseStream_.CanSeek == false) + { + throw new ZipException("ZipFile stream must be seekable"); + } + + long locatedEndOfCentralDir = LocateBlockWithSignature(ZipConstants.EndOfCentralDirectorySignature, + baseStream_.Length, ZipConstants.EndOfCentralRecordBaseSize, 0xffff); + + if (locatedEndOfCentralDir < 0) + { + throw new ZipException("Cannot find central directory"); + } + + // Read end of central directory record + ushort thisDiskNumber = ReadLEUshort(); + ushort startCentralDirDisk = ReadLEUshort(); + ulong entriesForThisDisk = ReadLEUshort(); + ulong entriesForWholeCentralDir = ReadLEUshort(); + ulong centralDirSize = ReadLEUint(); + long offsetOfCentralDir = ReadLEUint(); + uint commentSize = ReadLEUshort(); + + if (commentSize > 0) + { + byte[] comment = new byte[commentSize]; + + StreamUtils.ReadFully(baseStream_, comment); + comment_ = _stringCodec.ZipArchiveCommentEncoding.GetString(comment); + } + else + { + comment_ = string.Empty; + } + + bool isZip64 = false; + + // Check if zip64 header information is required. + bool requireZip64 = thisDiskNumber == 0xffff || + startCentralDirDisk == 0xffff || + entriesForThisDisk == 0xffff || + entriesForWholeCentralDir == 0xffff || + centralDirSize == 0xffffffff || + offsetOfCentralDir == 0xffffffff; + + // #357 - always check for the existence of the Zip64 central directory. + // #403 - Take account of the fixed size of the locator when searching. + // Subtract from locatedEndOfCentralDir so that the endLocation is the location of EndOfCentralDirectorySignature, + // rather than the data following the signature. + long locatedZip64EndOfCentralDirLocator = LocateBlockWithSignature( + ZipConstants.Zip64CentralDirLocatorSignature, + locatedEndOfCentralDir - 4, + ZipConstants.Zip64EndOfCentralDirectoryLocatorSize, + 0); + + if (locatedZip64EndOfCentralDirLocator < 0) + { + if (requireZip64) + { + // This is only an error in cases where the Zip64 directory is required. + throw new ZipException("Cannot find Zip64 locator"); + } + } + else + { + isZip64 = true; + + // number of the disk with the start of the zip64 end of central directory 4 bytes + // relative offset of the zip64 end of central directory record 8 bytes + // total number of disks 4 bytes + ReadLEUint(); // startDisk64 is not currently used + ulong offset64 = ReadLEUlong(); + uint totalDisks = ReadLEUint(); + + baseStream_.Position = (long)offset64; + long sig64 = ReadLEUint(); + + if (sig64 != ZipConstants.Zip64CentralFileHeaderSignature) + { + throw new ZipException($"Invalid Zip64 Central directory signature at {offset64:X}"); + } + + // NOTE: Record size = SizeOfFixedFields + SizeOfVariableData - 12. + ulong recordSize = ReadLEUlong(); + int versionMadeBy = ReadLEUshort(); + int versionToExtract = ReadLEUshort(); + uint thisDisk = ReadLEUint(); + uint centralDirDisk = ReadLEUint(); + entriesForThisDisk = ReadLEUlong(); + entriesForWholeCentralDir = ReadLEUlong(); + centralDirSize = ReadLEUlong(); + offsetOfCentralDir = (long)ReadLEUlong(); + + // NOTE: zip64 extensible data sector (variable size) is ignored. + } + + entries_ = new ZipEntry[entriesForThisDisk]; + + // SFX/embedded support, find the offset of the first entry vis the start of the stream + // This applies to Zip files that are appended to the end of an SFX stub. + // Or are appended as a resource to an executable. + // Zip files created by some archivers have the offsets altered to reflect the true offsets + // and so dont require any adjustment here... + // TODO: Difficulty with Zip64 and SFX offset handling needs resolution - maths? + if (!isZip64 && (offsetOfCentralDir < locatedEndOfCentralDir - (4 + (long)centralDirSize))) + { + offsetOfFirstEntry = locatedEndOfCentralDir - (4 + (long)centralDirSize + offsetOfCentralDir); + if (offsetOfFirstEntry <= 0) + { + throw new ZipException("Invalid embedded zip archive"); + } + } + + baseStream_.Seek(offsetOfFirstEntry + offsetOfCentralDir, SeekOrigin.Begin); + + for (ulong i = 0; i < entriesForThisDisk; i++) + { + if (ReadLEUint() != ZipConstants.CentralHeaderSignature) + { + throw new ZipException("Wrong Central Directory signature"); + } + + int versionMadeBy = ReadLEUshort(); + int versionToExtract = ReadLEUshort(); + int bitFlags = ReadLEUshort(); + int method = ReadLEUshort(); + uint dostime = ReadLEUint(); + uint crc = ReadLEUint(); + var csize = (long)ReadLEUint(); + var size = (long)ReadLEUint(); + int nameLen = ReadLEUshort(); + int extraLen = ReadLEUshort(); + int commentLen = ReadLEUshort(); + + + // ReSharper disable once UnusedVariable, Currently unused but needs to be read to offset the stream + int diskStartNo = ReadLEUshort(); + // ReSharper disable once UnusedVariable, Currently unused but needs to be read to offset the stream + int internalAttributes = ReadLEUshort(); + + uint externalAttributes = ReadLEUint(); + long offset = ReadLEUint(); + + byte[] buffer = new byte[Math.Max(nameLen, commentLen)]; + var entryEncoding = _stringCodec.ZipInputEncoding(bitFlags); + + StreamUtils.ReadFully(baseStream_, buffer, 0, nameLen); + string name = entryEncoding.GetString(buffer, 0, nameLen); + var unicode = entryEncoding.IsZipUnicode(); + + var entry = new ZipEntry(name, versionToExtract, versionMadeBy, (CompressionMethod)method, unicode) + { + Crc = crc & 0xffffffffL, + Size = size & 0xffffffffL, + CompressedSize = csize & 0xffffffffL, + Flags = bitFlags, + DosTime = dostime, + ZipFileIndex = (long)i, + Offset = offset, + ExternalFileAttributes = (int)externalAttributes + }; + + if (!entry.HasFlag(GeneralBitFlags.Descriptor)) + { + entry.CryptoCheckValue = (byte)(crc >> 24); + } + else + { + entry.CryptoCheckValue = (byte)((dostime >> 8) & 0xff); + } + + if (extraLen > 0) + { + byte[] extra = new byte[extraLen]; + StreamUtils.ReadFully(baseStream_, extra); + entry.ExtraData = extra; + } + + entry.ProcessExtraData(false); + + if (commentLen > 0) + { + StreamUtils.ReadFully(baseStream_, buffer, 0, commentLen); + entry.Comment = entryEncoding.GetString(buffer, 0, commentLen); + } + + entries_[i] = entry; + } + } + + /// + /// Locate the data for a given entry. + /// + /// + /// The start offset of the data. + /// + /// + /// The stream ends prematurely + /// + /// + /// The local header signature is invalid, the entry and central header file name lengths are different + /// or the local and entry compression methods dont match + /// + private long LocateEntry(ZipEntry entry) + { + return TestLocalHeader(entry, SkipLocalEntryTestsOnLocate ? HeaderTest.None : HeaderTest.Extract); + } + + /// + /// Skip the verification of the local header when reading an archive entry. Set this to attempt to read the + /// entries even if the headers should indicate that doing so would fail or produce an unexpected output. + /// + public bool SkipLocalEntryTestsOnLocate { get; set; } = false; + + private Stream CreateAndInitDecryptionStream(Stream baseStream, ZipEntry entry) + { + CryptoStream result = null; + + if (entry.CompressionMethodForHeader == CompressionMethod.WinZipAES) + { + if (entry.Version >= ZipConstants.VERSION_AES) + { + // Issue #471 - accept an empty string as a password, but reject null. + OnKeysRequired(entry.Name); + if (rawPassword_ == null) + { + throw new ZipException("No password available for AES encrypted stream"); + } + int saltLen = entry.AESSaltLen; + byte[] saltBytes = new byte[saltLen]; + int saltIn = StreamUtils.ReadRequestedBytes(baseStream, saltBytes, offset: 0, saltLen); + + if (saltIn != saltLen) throw new ZipException($"AES Salt expected {saltLen} git {saltIn}"); + + byte[] pwdVerifyRead = new byte[2]; + StreamUtils.ReadFully(baseStream, pwdVerifyRead); + int blockSize = entry.AESKeySize / 8; // bits to bytes + + var decryptor = new ZipAESTransform(rawPassword_, saltBytes, blockSize, writeMode: false); + byte[] pwdVerifyCalc = decryptor.PwdVerifier; + if (pwdVerifyCalc[0] != pwdVerifyRead[0] || pwdVerifyCalc[1] != pwdVerifyRead[1]) + throw new ZipException("Invalid password for AES"); + result = new ZipAESStream(baseStream, decryptor, CryptoStreamMode.Read); + } + else + { + throw new ZipException("Decryption method not supported"); + } + } + else + { + if (entry.Version < ZipConstants.VersionStrongEncryption || !entry.HasFlag(GeneralBitFlags.StrongEncryption)) + { + var classicManaged = new PkzipClassicManaged(); + + OnKeysRequired(entry.Name); + if (HaveKeys == false) + { + throw new ZipException("No password available for encrypted stream"); + } + + result = new CryptoStream(baseStream, classicManaged.CreateDecryptor(key, null), CryptoStreamMode.Read); + CheckClassicPassword(result, entry); + } + else + { + // We don't support PKWare strong encryption + throw new ZipException("Decryption method not supported"); + } + } + + return result; + } + + private Stream CreateAndInitEncryptionStream(Stream baseStream, ZipEntry entry) + { + if (entry.Version >= ZipConstants.VersionStrongEncryption && + entry.HasFlag(GeneralBitFlags.StrongEncryption)) return null; + + var classicManaged = new PkzipClassicManaged(); + + OnKeysRequired(entry.Name); + if (HaveKeys == false) + { + throw new ZipException("No password available for encrypted stream"); + } + + // Closing a CryptoStream will close the base stream as well so wrap it in an UncompressedStream + // which doesnt do this. + var result = new CryptoStream(new UncompressedStream(baseStream), + classicManaged.CreateEncryptor(key, null), CryptoStreamMode.Write); + + if (entry.Crc < 0 || entry.HasFlag(GeneralBitFlags.Descriptor)) + { + WriteEncryptionHeader(result, entry.DosTime << 16); + } + else + { + WriteEncryptionHeader(result, entry.Crc); + } + return result; + } + + private static void CheckClassicPassword(CryptoStream classicCryptoStream, ZipEntry entry) + { + byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize]; + StreamUtils.ReadFully(classicCryptoStream, cryptbuffer); + if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue) + { + throw new ZipException("Invalid password"); + } + } + + private static void WriteEncryptionHeader(Stream stream, long crcValue) + { + byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(cryptBuffer); + } + cryptBuffer[11] = (byte)(crcValue >> 24); + stream.Write(cryptBuffer, offset: 0, cryptBuffer.Length); + } + + #endregion Internal routines + + #region Instance Fields + + private bool isDisposed_; + private string name_; + private string comment_ = string.Empty; + private string rawPassword_; + private Stream baseStream_; + private bool isStreamOwner; + private long offsetOfFirstEntry; + private ZipEntry[] entries_; + private byte[] key; + private bool isNewArchive_; + private StringCodec _stringCodec = ZipStrings.GetStringCodec(); + + // Default is dynamic which is not backwards compatible and can cause problems + // with XP's built in compression which cant read Zip64 archives. + // However it does avoid the situation were a large file is added and cannot be completed correctly. + // Hint: Set always ZipEntry size before they are added to an archive and this setting isnt needed. + private UseZip64 useZip64_ = UseZip64.Dynamic; + + #region Zip Update Instance Fields + + private List updates_; + private long updateCount_; // Count is managed manually as updates_ can contain nulls! + private Dictionary updateIndex_; + private IArchiveStorage archiveStorage_; + private IDynamicDataSource updateDataSource_; + private bool contentsEdited_; + private int bufferSize_ = DefaultBufferSize; + private byte[] copyBuffer_; + private ZipString newComment_; + private bool commentEdited_; + private IEntryFactory updateEntryFactory_ = new ZipEntryFactory(); + + #endregion Zip Update Instance Fields + + #endregion Instance Fields + + #region Support Classes + + /// + /// Represents a string from a which is stored as an array of bytes. + /// + private class ZipString + { + #region Constructors + + /// + /// Initialise a with a string. + /// + /// The textual string form. + /// + public ZipString(string comment, Encoding encoding) + { + comment_ = comment; + isSourceString_ = true; + _encoding = encoding; + } + + /// + /// Initialise a using a string in its binary 'raw' form. + /// + /// + /// + public ZipString(byte[] rawString, Encoding encoding) + { + rawComment_ = rawString; + _encoding = encoding; + } + + #endregion Constructors + + /// + /// Get a value indicating the original source of data for this instance. + /// True if the source was a string; false if the source was binary data. + /// + public bool IsSourceString => isSourceString_; + + /// + /// Get the length of the comment when represented as raw bytes. + /// + public int RawLength + { + get + { + MakeBytesAvailable(); + return rawComment_.Length; + } + } + + /// + /// Get the comment in its 'raw' form as plain bytes. + /// + public byte[] RawComment + { + get + { + MakeBytesAvailable(); + return (byte[])rawComment_.Clone(); + } + } + + /// + /// Reset the comment to its initial state. + /// + public void Reset() + { + if (isSourceString_) + { + rawComment_ = null; + } + else + { + comment_ = null; + } + } + + private void MakeTextAvailable() + { + if (comment_ == null) + { + comment_ = _encoding.GetString(rawComment_); + } + } + + private void MakeBytesAvailable() + { + if (rawComment_ == null) + { + rawComment_ = _encoding.GetBytes(comment_); + } + } + + /// + /// Implicit conversion of comment to a string. + /// + /// The to convert to a string. + /// The textual equivalent for the input value. + public static implicit operator string(ZipString zipString) + { + zipString.MakeTextAvailable(); + return zipString.comment_; + } + + #region Instance Fields + + private string comment_; + private byte[] rawComment_; + private readonly bool isSourceString_; + private readonly Encoding _encoding; + + #endregion Instance Fields + } + + /// + /// An enumerator for Zip entries + /// + private class ZipEntryEnumerator : IEnumerator + { + #region Constructors + + public ZipEntryEnumerator(ZipEntry[] entries) + { + array = entries; + } + + #endregion Constructors + + #region IEnumerator Members + + public object Current + { + get + { + return array[index]; + } + } + + public void Reset() + { + index = -1; + } + + public bool MoveNext() + { + return (++index < array.Length); + } + + #endregion IEnumerator Members + + #region Instance Fields + + private ZipEntry[] array; + private int index = -1; + + #endregion Instance Fields + } + + /// + /// An is a stream that you can write uncompressed data + /// to and flush, but cannot read, seek or do anything else to. + /// + private class UncompressedStream : Stream + { + #region Constructors + + public UncompressedStream(Stream baseStream) + { + baseStream_ = baseStream; + } + + #endregion Constructors + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + public override bool CanRead + { + get + { + return false; + } + } + + /// + /// Write any buffered data to underlying storage. + /// + public override void Flush() + { + baseStream_.Flush(); + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + public override bool CanWrite + { + get + { + return baseStream_.CanWrite; + } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + public override bool CanSeek + { + get + { + return false; + } + } + + /// + /// Get the length in bytes of the stream. + /// + public override long Length + { + get + { + return 0; + } + } + + /// + /// Gets or sets the position within the current stream. + /// + public override long Position + { + get + { + return baseStream_.Position; + } + set + { + throw new NotImplementedException(); + } + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + /// The sum of offset and count is larger than the buffer length. + /// Methods were called after the stream was closed. + /// The stream does not support reading. + /// buffer is null. + /// An I/O error occurs. + /// offset or count is negative. + public override int Read(byte[] buffer, int offset, int count) + { + return 0; + } + + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + /// An I/O error occurs. + /// The stream does not support seeking, such as if the stream is constructed from a pipe or console output. + /// Methods were called after the stream was closed. + public override long Seek(long offset, SeekOrigin origin) + { + return 0; + } + + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. + /// An I/O error occurs. + /// Methods were called after the stream was closed. + public override void SetLength(long value) + { + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// An I/O error occurs. + /// The stream does not support writing. + /// Methods were called after the stream was closed. + /// buffer is null. + /// The sum of offset and count is greater than the buffer length. + /// offset or count is negative. + public override void Write(byte[] buffer, int offset, int count) + { + baseStream_.Write(buffer, offset, count); + } + + private readonly + + #region Instance Fields + + Stream baseStream_; + + #endregion Instance Fields + } + + /// + /// A is an + /// whose data is only a part or subsection of a file. + /// + private class PartialInputStream : Stream + { + #region Constructors + + /// + /// Initialise a new instance of the class. + /// + /// The containing the underlying stream to use for IO. + /// The start of the partial data. + /// The length of the partial data. + public PartialInputStream(ZipFile zipFile, long start, long length) + { + start_ = start; + length_ = length; + + // Although this is the only time the zipfile is used + // keeping a reference here prevents premature closure of + // this zip file and thus the baseStream_. + + // Code like this will cause apparently random failures depending + // on the size of the files and when garbage is collected. + // + // ZipFile z = new ZipFile (stream); + // Stream reader = z.GetInputStream(0); + // uses reader here.... + zipFile_ = zipFile; + baseStream_ = zipFile_.baseStream_; + readPos_ = start; + end_ = start + length; + } + + #endregion Constructors + + /// + /// Read a byte from this stream. + /// + /// Returns the byte read or -1 on end of stream. + public override int ReadByte() + { + if (readPos_ >= end_) + { + // -1 is the correct value at end of stream. + return -1; + } + + lock (baseStream_) + { + baseStream_.Seek(readPos_++, SeekOrigin.Begin); + return baseStream_.ReadByte(); + } + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + /// The sum of offset and count is larger than the buffer length. + /// Methods were called after the stream was closed. + /// The stream does not support reading. + /// buffer is null. + /// An I/O error occurs. + /// offset or count is negative. + public override int Read(byte[] buffer, int offset, int count) + { + lock (baseStream_) + { + if (count > end_ - readPos_) + { + count = (int)(end_ - readPos_); + if (count == 0) + { + return 0; + } + } + // Protect against Stream implementations that throw away their buffer on every Seek + // (for example, Mono FileStream) + if (baseStream_.Position != readPos_) + { + baseStream_.Seek(readPos_, SeekOrigin.Begin); + } + int readCount = baseStream_.Read(buffer, offset, count); + if (readCount > 0) + { + readPos_ += readCount; + } + return readCount; + } + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// An I/O error occurs. + /// The stream does not support writing. + /// Methods were called after the stream was closed. + /// buffer is null. + /// The sum of offset and count is greater than the buffer length. + /// offset or count is negative. + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + /// When overridden in a derived class, sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. + /// An I/O error occurs. + /// Methods were called after the stream was closed. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// When overridden in a derived class, sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + /// An I/O error occurs. + /// The stream does not support seeking, such as if the stream is constructed from a pipe or console output. + /// Methods were called after the stream was closed. + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = readPos_; + + switch (origin) + { + case SeekOrigin.Begin: + newPos = start_ + offset; + break; + + case SeekOrigin.Current: + newPos = readPos_ + offset; + break; + + case SeekOrigin.End: + newPos = end_ + offset; + break; + } + + if (newPos < start_) + { + throw new ArgumentException("Negative position is invalid"); + } + + if (newPos > end_) + { + throw new IOException("Cannot seek past end"); + } + readPos_ = newPos; + return readPos_; + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. + /// + /// An I/O error occurs. + public override void Flush() + { + // Nothing to do. + } + + /// + /// Gets or sets the position within the current stream. + /// + /// + /// The current position within the stream. + /// An I/O error occurs. + /// The stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Position + { + get { return readPos_ - start_; } + set + { + long newPos = start_ + value; + + if (newPos < start_) + { + throw new ArgumentException("Negative position is invalid"); + } + + if (newPos > end_) + { + throw new InvalidOperationException("Cannot seek past end"); + } + readPos_ = newPos; + } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// + /// A long value representing the length of the stream in bytes. + /// A class derived from Stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Length + { + get { return length_; } + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + /// false + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get { return false; } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + /// true + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get { return true; } + } + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + /// true. + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get { return true; } + } + + /// + /// Gets a value that determines whether the current stream can time out. + /// + /// + /// A value that determines whether the current stream can time out. + public override bool CanTimeout + { + get { return baseStream_.CanTimeout; } + } + + #region Instance Fields + + private ZipFile zipFile_; + private Stream baseStream_; + private readonly long start_; + private readonly long length_; + private long readPos_; + private readonly long end_; + + #endregion Instance Fields + } + + #endregion Support Classes + } + + #endregion ZipFile Class + + #region DataSources + + /// + /// Provides a static way to obtain a source of data for an entry. + /// + public interface IStaticDataSource + { + /// + /// Get a source of data by creating a new stream. + /// + /// Returns a to use for compression input. + /// Ideally a new stream is created and opened to achieve this, to avoid locking problems. + Stream GetSource(); + } + + /// + /// Represents a source of data that can dynamically provide + /// multiple data sources based on the parameters passed. + /// + public interface IDynamicDataSource + { + /// + /// Get a data source. + /// + /// The to get a source for. + /// The name for data if known. + /// Returns a to use for compression input. + /// Ideally a new stream is created and opened to achieve this, to avoid locking problems. + Stream GetSource(ZipEntry entry, string name); + } + + /// + /// Default implementation of a for use with files stored on disk. + /// + public class StaticDiskDataSource : IStaticDataSource + { + /// + /// Initialise a new instance of + /// + /// The name of the file to obtain data from. + public StaticDiskDataSource(string fileName) + { + fileName_ = fileName; + } + + #region IDataSource Members + + /// + /// Get a providing data. + /// + /// Returns a providing data. + public Stream GetSource() + { + return File.Open(fileName_, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + private readonly + + #endregion IDataSource Members + + #region Instance Fields + + string fileName_; + + #endregion Instance Fields + } + + /// + /// Default implementation of for files stored on disk. + /// + public class DynamicDiskDataSource : IDynamicDataSource + { + #region IDataSource Members + + /// + /// Get a providing data for an entry. + /// + /// The entry to provide data for. + /// The file name for data if known. + /// Returns a stream providing data; or null if not available + public Stream GetSource(ZipEntry entry, string name) + { + Stream result = null; + + if (name != null) + { + result = File.Open(name, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + return result; + } + + #endregion IDataSource Members + } + + #endregion DataSources + + #region Archive Storage + + /// + /// Defines facilities for data storage when updating Zip Archives. + /// + public interface IArchiveStorage + { + /// + /// Get the to apply during updates. + /// + FileUpdateMode UpdateMode { get; } + + /// + /// Get an empty that can be used for temporary output. + /// + /// Returns a temporary output + /// + Stream GetTemporaryOutput(); + + /// + /// Convert a temporary output stream to a final stream. + /// + /// The resulting final + /// + Stream ConvertTemporaryToFinal(); + + /// + /// Make a temporary copy of the original stream. + /// + /// The to copy. + /// Returns a temporary output that is a copy of the input. + Stream MakeTemporaryCopy(Stream stream); + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The current stream. + /// Returns a stream suitable for direct updating. + /// This may be the current stream passed. + Stream OpenForDirectUpdate(Stream stream); + + /// + /// Dispose of this instance. + /// + void Dispose(); + } + + /// + /// An abstract suitable for extension by inheritance. + /// + abstract public class BaseArchiveStorage : IArchiveStorage + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The update mode. + protected BaseArchiveStorage(FileUpdateMode updateMode) + { + updateMode_ = updateMode; + } + + #endregion Constructors + + #region IArchiveStorage Members + + /// + /// Gets a temporary output + /// + /// Returns the temporary output stream. + /// + public abstract Stream GetTemporaryOutput(); + + /// + /// Converts the temporary to its final form. + /// + /// Returns a that can be used to read + /// the final storage for the archive. + /// + public abstract Stream ConvertTemporaryToFinal(); + + /// + /// Make a temporary copy of a . + /// + /// The to make a copy of. + /// Returns a temporary output that is a copy of the input. + public abstract Stream MakeTemporaryCopy(Stream stream); + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The to open for direct update. + /// Returns a stream suitable for direct updating. + public abstract Stream OpenForDirectUpdate(Stream stream); + + /// + /// Disposes this instance. + /// + public abstract void Dispose(); + + /// + /// Gets the update mode applicable. + /// + /// The update mode. + public FileUpdateMode UpdateMode + { + get + { + return updateMode_; + } + } + + #endregion IArchiveStorage Members + + #region Instance Fields + + private readonly FileUpdateMode updateMode_; + + #endregion Instance Fields + } + + /// + /// An implementation suitable for hard disks. + /// + public class DiskArchiveStorage : BaseArchiveStorage + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The file. + /// The update mode. + public DiskArchiveStorage(ZipFile file, FileUpdateMode updateMode) + : base(updateMode) + { + if (file.Name == null) + { + throw new ZipException("Cant handle non file archives"); + } + + fileName_ = file.Name; + } + + /// + /// Initializes a new instance of the class. + /// + /// The file. + public DiskArchiveStorage(ZipFile file) + : this(file, FileUpdateMode.Safe) + { + } + + #endregion Constructors + + #region IArchiveStorage Members + + /// + /// Gets a temporary output for performing updates on. + /// + /// Returns the temporary output stream. + public override Stream GetTemporaryOutput() + { + temporaryName_ = PathUtils.GetTempFileName(temporaryName_); + temporaryStream_ = File.Open(temporaryName_, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); + + return temporaryStream_; + } + + /// + /// Converts a temporary to its final form. + /// + /// Returns a that can be used to read + /// the final storage for the archive. + public override Stream ConvertTemporaryToFinal() + { + if (temporaryStream_ == null) + { + throw new ZipException("No temporary stream has been created"); + } + + Stream result = null; + + string moveTempName = PathUtils.GetTempFileName(fileName_); + bool newFileCreated = false; + + try + { + temporaryStream_.Dispose(); + File.Move(fileName_, moveTempName); + File.Move(temporaryName_, fileName_); + newFileCreated = true; + File.Delete(moveTempName); + + result = File.Open(fileName_, FileMode.Open, FileAccess.Read, FileShare.Read); + } + catch (Exception) + { + result = null; + + // Try to roll back changes... + if (!newFileCreated) + { + File.Move(moveTempName, fileName_); + File.Delete(temporaryName_); + } + + throw; + } + + return result; + } + + /// + /// Make a temporary copy of a stream. + /// + /// The to copy. + /// Returns a temporary output that is a copy of the input. + public override Stream MakeTemporaryCopy(Stream stream) + { + stream.Dispose(); + + temporaryName_ = PathUtils.GetTempFileName(fileName_); + File.Copy(fileName_, temporaryName_, true); + + temporaryStream_ = new FileStream(temporaryName_, + FileMode.Open, + FileAccess.ReadWrite); + return temporaryStream_; + } + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The current stream. + /// Returns a stream suitable for direct updating. + /// If the is not null this is used as is. + public override Stream OpenForDirectUpdate(Stream stream) + { + Stream result; + if ((stream == null) || !stream.CanWrite) + { + if (stream != null) + { + stream.Dispose(); + } + + result = new FileStream(fileName_, + FileMode.Open, + FileAccess.ReadWrite); + } + else + { + result = stream; + } + + return result; + } + + /// + /// Disposes this instance. + /// + public override void Dispose() + { + if (temporaryStream_ != null) + { + temporaryStream_.Dispose(); + } + } + + #endregion IArchiveStorage Members + + #region Instance Fields + + private Stream temporaryStream_; + private readonly string fileName_; + private string temporaryName_; + + #endregion Instance Fields + } + + /// + /// An implementation suitable for in memory streams. + /// + public class MemoryArchiveStorage : BaseArchiveStorage + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public MemoryArchiveStorage() + : base(FileUpdateMode.Direct) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The to use + /// This constructor is for testing as memory streams dont really require safe mode. + public MemoryArchiveStorage(FileUpdateMode updateMode) + : base(updateMode) + { + } + + #endregion Constructors + + #region Properties + + /// + /// Get the stream returned by if this was in fact called. + /// + public MemoryStream FinalStream + { + get { return finalStream_; } + } + + #endregion Properties + + #region IArchiveStorage Members + + /// + /// Gets the temporary output + /// + /// Returns the temporary output stream. + public override Stream GetTemporaryOutput() + { + temporaryStream_ = new MemoryStream(); + return temporaryStream_; + } + + /// + /// Converts the temporary to its final form. + /// + /// Returns a that can be used to read + /// the final storage for the archive. + public override Stream ConvertTemporaryToFinal() + { + if (temporaryStream_ == null) + { + throw new ZipException("No temporary stream has been created"); + } + + finalStream_ = new MemoryStream(temporaryStream_.ToArray()); + return finalStream_; + } + + /// + /// Make a temporary copy of the original stream. + /// + /// The to copy. + /// Returns a temporary output that is a copy of the input. + public override Stream MakeTemporaryCopy(Stream stream) + { + temporaryStream_ = new MemoryStream(); + stream.Position = 0; + StreamUtils.Copy(stream, temporaryStream_, new byte[4096]); + return temporaryStream_; + } + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The original source stream + /// Returns a stream suitable for direct updating. + /// If the passed is not null this is used; + /// otherwise a new is returned. + public override Stream OpenForDirectUpdate(Stream stream) + { + Stream result; + if ((stream == null) || !stream.CanWrite) + { + result = new MemoryStream(); + + if (stream != null) + { + stream.Position = 0; + StreamUtils.Copy(stream, result, new byte[4096]); + + stream.Dispose(); + } + } + else + { + result = stream; + } + + return result; + } + + /// + /// Disposes this instance. + /// + public override void Dispose() + { + if (temporaryStream_ != null) + { + temporaryStream_.Dispose(); + } + } + + #endregion IArchiveStorage Members + + #region Instance Fields + + private MemoryStream temporaryStream_; + private MemoryStream finalStream_; + + #endregion Instance Fields + } + + #endregion Archive Storage +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs new file mode 100644 index 0000000..694644f --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipFormat.cs @@ -0,0 +1,598 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using BSP_ICSharpCode.SharpZipLib.Core; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// Holds data pertinent to a data descriptor. + /// + public class DescriptorData + { + private long _crc; + + /// + /// Get /set the compressed size of data. + /// + public long CompressedSize { get; set; } + + /// + /// Get / set the uncompressed size of data + /// + public long Size { get; set; } + + /// + /// Get /set the crc value. + /// + public long Crc + { + get => _crc; + set => _crc = (value & 0xffffffff); + } + } + + internal struct EntryPatchData + { + public long SizePatchOffset { get; set; } + + public long CrcPatchOffset { get; set; } + } + + /// + /// This class assists with writing/reading from Zip files. + /// + internal static class ZipFormat + { + // Write the local file header + // TODO: ZipFormat.WriteLocalHeader is not yet used and needs checking for ZipFile and ZipOuptutStream usage + internal static int WriteLocalHeader(Stream stream, ZipEntry entry, out EntryPatchData patchData, + bool headerInfoAvailable, bool patchEntryHeader, long streamOffset, StringCodec stringCodec) + { + patchData = new EntryPatchData(); + + stream.WriteLEInt(ZipConstants.LocalHeaderSignature); + stream.WriteLEShort(entry.Version); + stream.WriteLEShort(entry.Flags); + stream.WriteLEShort((byte)entry.CompressionMethodForHeader); + stream.WriteLEInt((int)entry.DosTime); + + if (headerInfoAvailable) + { + stream.WriteLEInt((int)entry.Crc); + if (entry.LocalHeaderRequiresZip64) + { + stream.WriteLEInt(-1); + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt((int)entry.CompressedSize + entry.EncryptionOverheadSize); + stream.WriteLEInt((int)entry.Size); + } + } + else + { + if (patchEntryHeader) + patchData.CrcPatchOffset = streamOffset + stream.Position; + + stream.WriteLEInt(0); // Crc + + if (patchEntryHeader) + patchData.SizePatchOffset = streamOffset + stream.Position; + + // For local header both sizes appear in Zip64 Extended Information + if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) + { + stream.WriteLEInt(-1); + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt(0); // Compressed size + stream.WriteLEInt(0); // Uncompressed size + } + } + + byte[] name = stringCodec.ZipEncoding(entry.IsUnicodeText).GetBytes(entry.Name); + + if (name.Length > 0xFFFF) + { + throw new ZipException("Entry name too long."); + } + + var ed = new ZipExtraData(entry.ExtraData); + + if (entry.LocalHeaderRequiresZip64) + { + ed.StartNewEntry(); + if (headerInfoAvailable) + { + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize + entry.EncryptionOverheadSize); + } + else + { + // If the sizes are stored in the descriptor, the local Zip64 sizes should be 0 + ed.AddLeLong(0); + ed.AddLeLong(0); + } + ed.AddNewEntry(1); + + if (!ed.Find(1)) + { + throw new ZipException("Internal error cant find extra data"); + } + + patchData.SizePatchOffset = ed.CurrentReadIndex; + } + else + { + ed.Delete(1); + } + + if (entry.AESKeySize > 0) + { + AddExtraDataAES(entry, ed); + } + byte[] extra = ed.GetEntryData(); + + stream.WriteLEShort(name.Length); + stream.WriteLEShort(extra.Length); + + if (name.Length > 0) + { + stream.Write(name, 0, name.Length); + } + + if (entry.LocalHeaderRequiresZip64 && patchEntryHeader) + { + patchData.SizePatchOffset += streamOffset + stream.Position; + } + + if (extra.Length > 0) + { + stream.Write(extra, 0, extra.Length); + } + + return ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; + } + + /// + /// Locates a block with the desired . + /// + /// + /// The signature to find. + /// Location, marking the end of block. + /// Minimum size of the block. + /// The maximum variable data. + /// Returns the offset of the first byte after the signature; -1 if not found + internal static long LocateBlockWithSignature(Stream stream, int signature, long endLocation, int minimumBlockSize, int maximumVariableData) + { + long pos = endLocation - minimumBlockSize; + if (pos < 0) + { + return -1; + } + + long giveUpMarker = Math.Max(pos - maximumVariableData, 0); + + // TODO: This loop could be optimized for speed. + do + { + if (pos < giveUpMarker) + { + return -1; + } + stream.Seek(pos--, SeekOrigin.Begin); + } while (stream.ReadLEInt() != signature); + + return stream.Position; + } + + /// + public static async Task WriteZip64EndOfCentralDirectoryAsync(Stream stream, long noOfEntries, + long sizeEntries, long centralDirOffset, CancellationToken cancellationToken) + { + await stream.WriteProcToStreamAsync(s => WriteZip64EndOfCentralDirectory(s, noOfEntries, sizeEntries, centralDirOffset), cancellationToken).ConfigureAwait(false); + } + + /// + /// Write Zip64 end of central directory records (File header and locator). + /// + /// + /// The number of entries in the central directory. + /// The size of entries in the central directory. + /// The offset of the central directory. + internal static void WriteZip64EndOfCentralDirectory(Stream stream, long noOfEntries, long sizeEntries, long centralDirOffset) + { + long centralSignatureOffset = centralDirOffset + sizeEntries; + stream.WriteLEInt(ZipConstants.Zip64CentralFileHeaderSignature); + stream.WriteLELong(44); // Size of this record (total size of remaining fields in header or full size - 12) + stream.WriteLEShort(ZipConstants.VersionMadeBy); // Version made by + stream.WriteLEShort(ZipConstants.VersionZip64); // Version to extract + stream.WriteLEInt(0); // Number of this disk + stream.WriteLEInt(0); // number of the disk with the start of the central directory + stream.WriteLELong(noOfEntries); // No of entries on this disk + stream.WriteLELong(noOfEntries); // Total No of entries in central directory + stream.WriteLELong(sizeEntries); // Size of the central directory + stream.WriteLELong(centralDirOffset); // offset of start of central directory + // zip64 extensible data sector not catered for here (variable size) + + // Write the Zip64 end of central directory locator + stream.WriteLEInt(ZipConstants.Zip64CentralDirLocatorSignature); + + // no of the disk with the start of the zip64 end of central directory + stream.WriteLEInt(0); + + // relative offset of the zip64 end of central directory record + stream.WriteLELong(centralSignatureOffset); + + // total number of disks + stream.WriteLEInt(1); + } + + /// + public static async Task WriteEndOfCentralDirectoryAsync(Stream stream, long noOfEntries, long sizeEntries, + long start, byte[] comment, CancellationToken cancellationToken) + => await stream.WriteProcToStreamAsync(s + => WriteEndOfCentralDirectory(s, noOfEntries, sizeEntries, start, comment), cancellationToken).ConfigureAwait(false); + + /// + /// Write the required records to end the central directory. + /// + /// + /// The number of entries in the directory. + /// The size of the entries in the directory. + /// The start of the central directory. + /// The archive comment. (This can be null). + + internal static void WriteEndOfCentralDirectory(Stream stream, long noOfEntries, long sizeEntries, long start, byte[] comment) + { + if (noOfEntries >= 0xffff || + start >= 0xffffffff || + sizeEntries >= 0xffffffff) + { + WriteZip64EndOfCentralDirectory(stream, noOfEntries, sizeEntries, start); + } + + stream.WriteLEInt(ZipConstants.EndOfCentralDirectorySignature); + + // TODO: ZipFile Multi disk handling not done + stream.WriteLEShort(0); // number of this disk + stream.WriteLEShort(0); // no of disk with start of central dir + + // Number of entries + if (noOfEntries >= 0xffff) + { + stream.WriteLEUshort(0xffff); // Zip64 marker + stream.WriteLEUshort(0xffff); + } + else + { + stream.WriteLEShort((short)noOfEntries); // entries in central dir for this disk + stream.WriteLEShort((short)noOfEntries); // total entries in central directory + } + + // Size of the central directory + if (sizeEntries >= 0xffffffff) + { + stream.WriteLEUint(0xffffffff); // Zip64 marker + } + else + { + stream.WriteLEInt((int)sizeEntries); + } + + // offset of start of central directory + if (start >= 0xffffffff) + { + stream.WriteLEUint(0xffffffff); // Zip64 marker + } + else + { + stream.WriteLEInt((int)start); + } + + var commentLength = comment?.Length ?? 0; + + if (commentLength > 0xffff) + { + throw new ZipException($"Comment length ({commentLength}) is larger than 64K"); + } + + stream.WriteLEShort(commentLength); + + if (commentLength > 0) + { + stream.Write(comment, 0, commentLength); + } + } + + + + /// + /// Write a data descriptor. + /// + /// + /// The entry to write a descriptor for. + /// Returns the number of descriptor bytes written. + internal static int WriteDataDescriptor(Stream stream, ZipEntry entry) + { + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + int result = 0; + + // Add data descriptor if flagged as required + if ((entry.Flags & (int)GeneralBitFlags.Descriptor) != 0) + { + // The signature is not PKZIP originally but is now described as optional + // in the PKZIP Appnote documenting the format. + stream.WriteLEInt(ZipConstants.DataDescriptorSignature); + stream.WriteLEInt(unchecked((int)(entry.Crc))); + + result += 8; + + if (entry.LocalHeaderRequiresZip64) + { + stream.WriteLELong(entry.CompressedSize); + stream.WriteLELong(entry.Size); + result += 16; + } + else + { + stream.WriteLEInt((int)entry.CompressedSize); + stream.WriteLEInt((int)entry.Size); + result += 8; + } + } + + return result; + } + + /// + /// Read data descriptor at the end of compressed data. + /// + /// + /// if set to true [zip64]. + /// The data to fill in. + /// Returns the number of bytes read in the descriptor. + internal static void ReadDataDescriptor(Stream stream, bool zip64, DescriptorData data) + { + int intValue = stream.ReadLEInt(); + + // In theory this may not be a descriptor according to PKZIP appnote. + // In practice its always there. + if (intValue != ZipConstants.DataDescriptorSignature) + { + throw new ZipException("Data descriptor signature not found"); + } + + data.Crc = stream.ReadLEInt(); + + if (zip64) + { + data.CompressedSize = stream.ReadLELong(); + data.Size = stream.ReadLELong(); + } + else + { + data.CompressedSize = stream.ReadLEInt(); + data.Size = stream.ReadLEInt(); + } + } + + internal static int WriteEndEntry(Stream stream, ZipEntry entry, StringCodec stringCodec) + { + stream.WriteLEInt(ZipConstants.CentralHeaderSignature); + stream.WriteLEShort((entry.HostSystem << 8) | entry.VersionMadeBy); + stream.WriteLEShort(entry.Version); + stream.WriteLEShort(entry.Flags); + stream.WriteLEShort((short)entry.CompressionMethodForHeader); + stream.WriteLEInt((int)entry.DosTime); + stream.WriteLEInt((int)entry.Crc); + + if (entry.IsZip64Forced() || + (entry.CompressedSize >= uint.MaxValue)) + { + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt((int)entry.CompressedSize); + } + + if (entry.IsZip64Forced() || + (entry.Size >= uint.MaxValue)) + { + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt((int)entry.Size); + } + + byte[] name = stringCodec.ZipOutputEncoding.GetBytes(entry.Name); + + if (name.Length > 0xffff) + { + throw new ZipException("Name too long."); + } + + var ed = new ZipExtraData(entry.ExtraData); + + if (entry.CentralHeaderRequiresZip64) + { + ed.StartNewEntry(); + if (entry.IsZip64Forced() || + (entry.Size >= 0xffffffff)) + { + ed.AddLeLong(entry.Size); + } + + if (entry.IsZip64Forced() || + (entry.CompressedSize >= 0xffffffff)) + { + ed.AddLeLong(entry.CompressedSize); + } + + if (entry.Offset >= 0xffffffff) + { + ed.AddLeLong(entry.Offset); + } + + ed.AddNewEntry(1); + } + else + { + ed.Delete(1); + } + + if (entry.AESKeySize > 0) + { + AddExtraDataAES(entry, ed); + } + byte[] extra = ed.GetEntryData(); + + byte[] entryComment = !(entry.Comment is null) + ? stringCodec.ZipOutputEncoding.GetBytes(entry.Comment) + : Empty.Array(); + + if (entryComment.Length > 0xffff) + { + throw new ZipException("Comment too long."); + } + + stream.WriteLEShort(name.Length); + stream.WriteLEShort(extra.Length); + stream.WriteLEShort(entryComment.Length); + stream.WriteLEShort(0); // disk number + stream.WriteLEShort(0); // internal file attributes + // external file attributes + + if (entry.ExternalFileAttributes != -1) + { + stream.WriteLEInt(entry.ExternalFileAttributes); + } + else + { + if (entry.IsDirectory) + { // mark entry as directory (from nikolam.AT.perfectinfo.com) + stream.WriteLEInt(16); + } + else + { + stream.WriteLEInt(0); + } + } + + if (entry.Offset >= uint.MaxValue) + { + stream.WriteLEInt(-1); + } + else + { + stream.WriteLEInt((int)entry.Offset); + } + + if (name.Length > 0) + { + stream.Write(name, 0, name.Length); + } + + if (extra.Length > 0) + { + stream.Write(extra, 0, extra.Length); + } + + if (entryComment.Length > 0) + { + stream.Write(entryComment, 0, entryComment.Length); + } + + return ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; + } + + internal static void AddExtraDataAES(ZipEntry entry, ZipExtraData extraData) + { + // Vendor Version: AE-1 IS 1. AE-2 is 2. With AE-2 no CRC is required and 0 is stored. + const int VENDOR_VERSION = 2; + // Vendor ID is the two ASCII characters "AE". + const int VENDOR_ID = 0x4541; //not 6965; + extraData.StartNewEntry(); + // Pack AES extra data field see http://www.winzip.com/aes_info.htm + //extraData.AddLeShort(7); // Data size (currently 7) + extraData.AddLeShort(VENDOR_VERSION); // 2 = AE-2 + extraData.AddLeShort(VENDOR_ID); // "AE" + extraData.AddData(entry.AESEncryptionStrength); // 1 = 128, 2 = 192, 3 = 256 + extraData.AddLeShort((int)entry.CompressionMethod); // The actual compression method used to compress the file + extraData.AddNewEntry(0x9901); + } + + internal static async Task PatchLocalHeaderAsync(Stream stream, ZipEntry entry, + EntryPatchData patchData, CancellationToken ct) + { + var initialPos = stream.Position; + + // Update CRC + stream.Seek(patchData.CrcPatchOffset, SeekOrigin.Begin); + await stream.WriteLEIntAsync((int)entry.Crc, ct).ConfigureAwait(false); + + // Update Sizes + if (entry.LocalHeaderRequiresZip64) + { + if (patchData.SizePatchOffset == -1) + { + throw new ZipException("Entry requires zip64 but this has been turned off"); + } + // Seek to the Zip64 Extra Data + stream.Seek(patchData.SizePatchOffset, SeekOrigin.Begin); + + // Note: The order of the size fields is reversed when compared to the local header! + await stream.WriteLELongAsync(entry.Size, ct).ConfigureAwait(false); + await stream.WriteLELongAsync(entry.CompressedSize, ct).ConfigureAwait(false); + } + else + { + await stream.WriteLEIntAsync((int)entry.CompressedSize, ct).ConfigureAwait(false); + await stream.WriteLEIntAsync((int)entry.Size, ct).ConfigureAwait(false); + } + + stream.Seek(initialPos, SeekOrigin.Begin); + } + + internal static void PatchLocalHeaderSync(Stream stream, ZipEntry entry, + EntryPatchData patchData) + { + var initialPos = stream.Position; + stream.Seek(patchData.CrcPatchOffset, SeekOrigin.Begin); + stream.WriteLEInt((int)entry.Crc); + + if (entry.LocalHeaderRequiresZip64) + { + if (patchData.SizePatchOffset == -1) + { + throw new ZipException("Entry requires zip64 but this has been turned off"); + } + + // Seek to the Zip64 Extra Data + stream.Seek(patchData.SizePatchOffset, SeekOrigin.Begin); + + // Note: The order of the size fields is reversed when compared to the local header! + stream.WriteLELong(entry.Size); + stream.WriteLELong(entry.CompressedSize); + } + else + { + stream.WriteLEInt((int)entry.CompressedSize); + stream.WriteLEInt((int)entry.Size); + } + + stream.Seek(initialPos, SeekOrigin.Begin); + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipHelperStream.cs new file mode 100644 index 0000000..e69de29 diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs new file mode 100644 index 0000000..6dfb3a5 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs @@ -0,0 +1,776 @@ +using BSP_ICSharpCode.SharpZipLib.Checksum; +using BSP_ICSharpCode.SharpZipLib.Encryption; +using BSP_ICSharpCode.SharpZipLib.Zip.Compression; +using BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using System; +using System.Diagnostics; +using System.IO; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// This is an InflaterInputStream that reads the files baseInputStream an zip archive + /// one after another. It has a special method to get the zip entry of + /// the next file. The zip entry contains information about the file name + /// size, compressed size, Crc, etc. + /// It includes support for Stored and Deflated entries. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ /// + /// This sample shows how to read a zip file + /// + /// using System; + /// using System.Text; + /// using System.IO; + /// + /// using ICSharpCode.SharpZipLib.Zip; + /// + /// class MainClass + /// { + /// public static void Main(string[] args) + /// { + /// using ( ZipInputStream s = new ZipInputStream(File.OpenRead(args[0]))) { + /// + /// ZipEntry theEntry; + /// const int size = 2048; + /// byte[] data = new byte[2048]; + /// + /// while ((theEntry = s.GetNextEntry()) != null) { + /// if ( entry.IsFile ) { + /// Console.Write("Show contents (y/n) ?"); + /// if (Console.ReadLine() == "y") { + /// while (true) { + /// size = s.Read(data, 0, data.Length); + /// if (size > 0) { + /// Console.Write(new ASCIIEncoding().GetString(data, 0, size)); + /// } else { + /// break; + /// } + /// } + /// } + /// } + /// } + /// } + /// } + /// } + /// + /// + public class ZipInputStream : InflaterInputStream + { + #region Instance Fields + + /// + /// Delegate for reading bytes from a stream. + /// + private delegate int ReadDataHandler(byte[] b, int offset, int length); + + /// + /// The current reader this instance. + /// + private ReadDataHandler internalReader; + + private Crc32 crc = new Crc32(); + private ZipEntry entry; + + private long size; + private CompressionMethod method; + private int flags; + private string password; + private readonly StringCodec _stringCodec = ZipStrings.GetStringCodec(); + + #endregion Instance Fields + + #region Constructors + + /// + /// Creates a new Zip input stream, for reading a zip archive. + /// + /// The underlying providing data. + public ZipInputStream(Stream baseInputStream) + : base(baseInputStream, new Inflater(true)) + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + } + + /// + /// Creates a new Zip input stream, for reading a zip archive. + /// + /// The underlying providing data. + /// Size of the buffer. + public ZipInputStream(Stream baseInputStream, int bufferSize) + : base(baseInputStream, new Inflater(true), bufferSize) + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + } + + /// + /// Creates a new Zip input stream, for reading a zip archive. + /// + /// The underlying providing data. + /// + public ZipInputStream(Stream baseInputStream, StringCodec stringCodec) + : base(baseInputStream, new Inflater(true)) + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + if (stringCodec != null) + { + _stringCodec = stringCodec; + } + } + + #endregion Constructors + + /// + /// Optional password used for encryption when non-null + /// + /// A password for all encrypted entries in this + public string Password + { + get + { + return password; + } + set + { + password = value; + } + } + + /// + /// Gets a value indicating if there is a current entry and it can be decompressed + /// + /// + /// The entry can only be decompressed if the library supports the zip features required to extract it. + /// See the ZipEntry Version property for more details. + /// + /// Since uses the local headers for extraction, entries with no compression combined with the + /// flag set, cannot be extracted as the end of the entry data cannot be deduced. + /// + public bool CanDecompressEntry + => entry != null + && IsEntryCompressionMethodSupported(entry) + && entry.CanDecompress + && (!entry.HasFlag(GeneralBitFlags.Descriptor) || entry.CompressionMethod != CompressionMethod.Stored || entry.IsCrypted); + + /// + /// Is the compression method for the specified entry supported? + /// + /// + /// Uses entry.CompressionMethodForHeader so that entries of type WinZipAES will be rejected. + /// + /// the entry to check. + /// true if the compression method is supported, false if not. + private static bool IsEntryCompressionMethodSupported(ZipEntry entry) + { + var entryCompressionMethod = entry.CompressionMethodForHeader; + + return entryCompressionMethod == CompressionMethod.Deflated || + entryCompressionMethod == CompressionMethod.Stored; + } + + /// + /// Advances to the next entry in the archive + /// + /// + /// The next entry in the archive or null if there are no more entries. + /// + /// + /// If the previous entry is still open CloseEntry is called. + /// + /// + /// Input stream is closed + /// + /// + /// Password is not set, password is invalid, compression method is invalid, + /// version required to extract is not supported + /// + public ZipEntry GetNextEntry() + { + if (crc == null) + { + throw new InvalidOperationException("Closed."); + } + + if (entry != null) + { + CloseEntry(); + } + + if (!SkipUntilNextEntry()) + { + Dispose(); + return null; + } + + var versionRequiredToExtract = (short)inputBuffer.ReadLeShort(); + + flags = inputBuffer.ReadLeShort(); + method = (CompressionMethod)inputBuffer.ReadLeShort(); + var dostime = (uint)inputBuffer.ReadLeInt(); + int crc2 = inputBuffer.ReadLeInt(); + csize = inputBuffer.ReadLeInt(); + size = inputBuffer.ReadLeInt(); + int nameLen = inputBuffer.ReadLeShort(); + int extraLen = inputBuffer.ReadLeShort(); + + bool isCrypted = (flags & 1) == 1; + + byte[] buffer = new byte[nameLen]; + inputBuffer.ReadRawBuffer(buffer); + + var entryEncoding = _stringCodec.ZipInputEncoding(flags); + string name = entryEncoding.GetString(buffer); + var unicode = entryEncoding.IsZipUnicode(); + + entry = new ZipEntry(name, versionRequiredToExtract, ZipConstants.VersionMadeBy, method, unicode) + { + Flags = flags, + }; + + if ((flags & 8) == 0) + { + entry.Crc = crc2 & 0xFFFFFFFFL; + entry.Size = size & 0xFFFFFFFFL; + entry.CompressedSize = csize & 0xFFFFFFFFL; + + entry.CryptoCheckValue = (byte)((crc2 >> 24) & 0xff); + } + else + { + // This allows for GNU, WinZip and possibly other archives, the PKZIP spec + // says these values are zero under these circumstances. + if (crc2 != 0) + { + entry.Crc = crc2 & 0xFFFFFFFFL; + } + + if (size != 0) + { + entry.Size = size & 0xFFFFFFFFL; + } + + if (csize != 0) + { + entry.CompressedSize = csize & 0xFFFFFFFFL; + } + + entry.CryptoCheckValue = (byte)((dostime >> 8) & 0xff); + } + + entry.DosTime = dostime; + + // If local header requires Zip64 is true then the extended header should contain + // both values. + + // Handle extra data if present. This can set/alter some fields of the entry. + if (extraLen > 0) + { + byte[] extra = new byte[extraLen]; + inputBuffer.ReadRawBuffer(extra); + entry.ExtraData = extra; + } + + entry.ProcessExtraData(true); + if (entry.CompressedSize >= 0) + { + csize = entry.CompressedSize; + } + + if (entry.Size >= 0) + { + size = entry.Size; + } + + if (method == CompressionMethod.Stored && (!isCrypted && csize != size || (isCrypted && csize - ZipConstants.CryptoHeaderSize != size))) + { + throw new ZipException("Stored, but compressed != uncompressed"); + } + + // Determine how to handle reading of data if this is attempted. + if (IsEntryCompressionMethodSupported(entry)) + { + internalReader = new ReadDataHandler(InitialRead); + } + else + { + internalReader = new ReadDataHandler(ReadingNotSupported); + } + + return entry; + } + + /// + /// Reads bytes from the input stream until either a local file header signature, or another signature + /// indicating that no more entries should be present, is found. + /// + /// Thrown if the end of the input stream is reached without any signatures found + /// Returns whether the found signature is for a local entry header + private bool SkipUntilNextEntry() + { + // First let's skip all null bytes since it's the sane padding to add when updating an entry with smaller size + var paddingSkipped = 0; + while(inputBuffer.ReadLeByte() == 0) { + paddingSkipped++; + } + + // Last byte read was not actually consumed, restore the offset + inputBuffer.Available += 1; + if(paddingSkipped > 0) { + Debug.WriteLine("Skipped {0} null byte(s) before reading signature", paddingSkipped); + } + + var offset = 0; + // Read initial header quad directly after the last entry + var header = (uint)inputBuffer.ReadLeInt(); + do + { + switch (header) + { + case ZipConstants.CentralHeaderSignature: + case ZipConstants.EndOfCentralDirectorySignature: + case ZipConstants.CentralHeaderDigitalSignature: + case ZipConstants.ArchiveExtraDataSignature: + case ZipConstants.Zip64CentralFileHeaderSignature: + Debug.WriteLine("Non-entry signature found at offset {0,2}: 0x{1:x8}", offset, header); + // No more individual entries exist + return false; + + case ZipConstants.LocalHeaderSignature: + Debug.WriteLine("Entry local header signature found at offset {0,2}: 0x{1:x8}", offset, header); + return true; + default: + // Current header quad did not match any signature, shift in another byte + header = (uint) (inputBuffer.ReadLeByte() << 24) | (header >> 8); + offset++; + break; + } + } while (true); // Loop until we either get an EOF exception or we find the next signature + } + + /// + /// Read data descriptor at the end of compressed data. + /// + private void ReadDataDescriptor() + { + if (inputBuffer.ReadLeInt() != ZipConstants.DataDescriptorSignature) + { + throw new ZipException("Data descriptor signature not found"); + } + + entry.Crc = inputBuffer.ReadLeInt() & 0xFFFFFFFFL; + + if (entry.LocalHeaderRequiresZip64) + { + csize = inputBuffer.ReadLeLong(); + size = inputBuffer.ReadLeLong(); + } + else + { + csize = inputBuffer.ReadLeInt(); + size = inputBuffer.ReadLeInt(); + } + entry.CompressedSize = csize; + entry.Size = size; + } + + /// + /// Complete cleanup as the final part of closing. + /// + /// True if the crc value should be tested + private void CompleteCloseEntry(bool testCrc) + { + StopDecrypting(); + + if ((flags & 8) != 0) + { + ReadDataDescriptor(); + } + + size = 0; + + if (testCrc && + ((crc.Value & 0xFFFFFFFFL) != entry.Crc) && (entry.Crc != -1)) + { + throw new ZipException("CRC mismatch"); + } + + crc.Reset(); + + if (method == CompressionMethod.Deflated) + { + inf.Reset(); + } + entry = null; + } + + /// + /// Closes the current zip entry and moves to the next one. + /// + /// + /// The stream is closed + /// + /// + /// The Zip stream ends early + /// + public void CloseEntry() + { + if (crc == null) + { + throw new InvalidOperationException("Closed"); + } + + if (entry == null) + { + return; + } + + if (method == CompressionMethod.Deflated) + { + if ((flags & 8) != 0) + { + // We don't know how much we must skip, read until end. + byte[] tmp = new byte[4096]; + + // Read will close this entry + while (Read(tmp, 0, tmp.Length) > 0) + { + } + return; + } + + csize -= inf.TotalIn; + inputBuffer.Available += inf.RemainingInput; + } + + if ((inputBuffer.Available > csize) && (csize >= 0)) + { + // Buffer can contain entire entry data. Internally offsetting position inside buffer + inputBuffer.Available = (int)((long)inputBuffer.Available - csize); + } + else + { + csize -= inputBuffer.Available; + inputBuffer.Available = 0; + while (csize != 0) + { + long skipped = Skip(csize); + + if (skipped <= 0) + { + throw new ZipException("Zip archive ends early."); + } + + csize -= skipped; + } + } + + CompleteCloseEntry(false); + } + + /// + /// Returns 1 if there is an entry available + /// Otherwise returns 0. + /// + public override int Available + { + get + { + return entry != null ? 1 : 0; + } + } + + /// + /// Returns the current size that can be read from the current entry if available + /// + /// Thrown if the entry size is not known. + /// Thrown if no entry is currently available. + public override long Length + { + get + { + if (entry != null) + { + if (entry.Size >= 0) + { + return entry.Size; + } + else + { + throw new ZipException("Length not available for the current entry"); + } + } + else + { + throw new InvalidOperationException("No current entry"); + } + } + } + + /// + /// Reads a byte from the current zip entry. + /// + /// + /// The byte or -1 if end of stream is reached. + /// + public override int ReadByte() + { + byte[] b = new byte[1]; + if (Read(b, 0, 1) <= 0) + { + return -1; + } + return b[0] & 0xff; + } + + /// + /// Handle attempts to read by throwing an . + /// + /// The destination array to store data in. + /// The offset at which data read should be stored. + /// The maximum number of bytes to read. + /// Returns the number of bytes actually read. + private int ReadingNotAvailable(byte[] destination, int offset, int count) + { + throw new InvalidOperationException("Unable to read from this stream"); + } + + /// + /// Handle attempts to read from this entry by throwing an exception + /// + private int ReadingNotSupported(byte[] destination, int offset, int count) + { + throw new ZipException("The compression method for this entry is not supported"); + } + + /// + /// Handle attempts to read from this entry by throwing an exception + /// + private int StoredDescriptorEntry(byte[] destination, int offset, int count) => + throw new StreamUnsupportedException( + "The combination of Stored compression method and Descriptor flag is not possible to read using ZipInputStream"); + + + /// + /// Perform the initial read on an entry which may include + /// reading encryption headers and setting up inflation. + /// + /// The destination to fill with data read. + /// The offset to start reading at. + /// The maximum number of bytes to read. + /// The actual number of bytes read. + private int InitialRead(byte[] destination, int offset, int count) + { + var usesDescriptor = (entry.Flags & (int)GeneralBitFlags.Descriptor) != 0; + + // Handle encryption if required. + if (entry.IsCrypted) + { + if (password == null) + { + throw new ZipException("No password set."); + } + + // Generate and set crypto transform... + var managed = new PkzipClassicManaged(); + byte[] key = PkzipClassic.GenerateKeys(_stringCodec.ZipCryptoEncoding.GetBytes(password)); + + inputBuffer.CryptoTransform = managed.CreateDecryptor(key, null); + + byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize]; + inputBuffer.ReadClearTextBuffer(cryptbuffer, 0, ZipConstants.CryptoHeaderSize); + + if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue) + { + throw new ZipException("Invalid password"); + } + + if (csize >= ZipConstants.CryptoHeaderSize) + { + csize -= ZipConstants.CryptoHeaderSize; + } + else if (!usesDescriptor) + { + throw new ZipException($"Entry compressed size {csize} too small for encryption"); + } + } + else + { + inputBuffer.CryptoTransform = null; + } + + if (csize > 0 || usesDescriptor) + { + if (method == CompressionMethod.Deflated && inputBuffer.Available > 0) + { + inputBuffer.SetInflaterInput(inf); + } + + // It's not possible to know how many bytes to read when using "Stored" compression (unless using encryption) + if (!entry.IsCrypted && method == CompressionMethod.Stored && usesDescriptor) + { + internalReader = StoredDescriptorEntry; + return StoredDescriptorEntry(destination, offset, count); + } + + if (!CanDecompressEntry) + { + internalReader = ReadingNotSupported; + return ReadingNotSupported(destination, offset, count); + } + + internalReader = BodyRead; + return BodyRead(destination, offset, count); + } + + + internalReader = ReadingNotAvailable; + return 0; + } + + /// + /// Read a block of bytes from the stream. + /// + /// The destination for the bytes. + /// The index to start storing data. + /// The number of bytes to attempt to read. + /// Returns the number of bytes read. + /// Zero bytes read means end of stream. + public override int Read(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Cannot be negative"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Cannot be negative"); + } + + if ((buffer.Length - offset) < count) + { + throw new ArgumentException("Invalid offset/count combination"); + } + + return internalReader(buffer, offset, count); + } + + /// + /// Reads a block of bytes from the current zip entry. + /// + /// + /// The number of bytes read (this may be less than the length requested, even before the end of stream), or 0 on end of stream. + /// + /// + /// An i/o error occurred. + /// + /// + /// The deflated stream is corrupted. + /// + /// + /// The stream is not open. + /// + private int BodyRead(byte[] buffer, int offset, int count) + { + if (crc == null) + { + throw new InvalidOperationException("Closed"); + } + + if ((entry == null) || (count <= 0)) + { + return 0; + } + + if (offset + count > buffer.Length) + { + throw new ArgumentException("Offset + count exceeds buffer size"); + } + + bool finished = false; + + switch (method) + { + case CompressionMethod.Deflated: + count = base.Read(buffer, offset, count); + if (count <= 0) + { + if (!inf.IsFinished) + { + throw new ZipException("Inflater not finished!"); + } + inputBuffer.Available = inf.RemainingInput; + + // A csize of -1 is from an unpatched local header + if ((flags & 8) == 0 && + (inf.TotalIn != csize && csize != 0xFFFFFFFF && csize != -1 || inf.TotalOut != size)) + { + throw new ZipException("Size mismatch: " + csize + ";" + size + " <-> " + inf.TotalIn + ";" + inf.TotalOut); + } + inf.Reset(); + finished = true; + } + break; + + case CompressionMethod.Stored: + if ((count > csize) && (csize >= 0)) + { + count = (int)csize; + } + + if (count > 0) + { + count = inputBuffer.ReadClearTextBuffer(buffer, offset, count); + if (count > 0) + { + csize -= count; + size -= count; + } + } + + if (csize == 0) + { + finished = true; + } + else + { + if (count < 0) + { + throw new ZipException("EOF in stored block"); + } + } + break; + } + + if (count > 0) + { + crc.Update(new ArraySegment(buffer, offset, count)); + } + + if (finished) + { + CompleteCloseEntry(true); + } + + return count; + } + + /// + /// Closes the zip input stream + /// + protected override void Dispose(bool disposing) + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + crc = null; + entry = null; + + base.Dispose(disposing); + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipNameTransform.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipNameTransform.cs new file mode 100644 index 0000000..8d06673 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipNameTransform.cs @@ -0,0 +1,313 @@ +using BSP_ICSharpCode.SharpZipLib.Core; +using System; +using System.IO; +using System.Text; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// ZipNameTransform transforms names as per the Zip file naming convention. + /// + /// The use of absolute names is supported although its use is not valid + /// according to Zip naming conventions, and should not be used if maximum compatability is desired. + public class ZipNameTransform : INameTransform + { + #region Constructors + + /// + /// Initialize a new instance of + /// + public ZipNameTransform() + { + } + + /// + /// Initialize a new instance of + /// + /// The string to trim from the front of paths if found. + public ZipNameTransform(string trimPrefix) + { + TrimPrefix = trimPrefix; + } + + #endregion Constructors + + /// + /// Static constructor. + /// + static ZipNameTransform() + { + char[] invalidPathChars; + invalidPathChars = Path.GetInvalidPathChars(); + int howMany = invalidPathChars.Length + 2; + + InvalidEntryCharsRelaxed = new char[howMany]; + Array.Copy(invalidPathChars, 0, InvalidEntryCharsRelaxed, 0, invalidPathChars.Length); + InvalidEntryCharsRelaxed[howMany - 1] = '*'; + InvalidEntryCharsRelaxed[howMany - 2] = '?'; + + howMany = invalidPathChars.Length + 4; + InvalidEntryChars = new char[howMany]; + Array.Copy(invalidPathChars, 0, InvalidEntryChars, 0, invalidPathChars.Length); + InvalidEntryChars[howMany - 1] = ':'; + InvalidEntryChars[howMany - 2] = '\\'; + InvalidEntryChars[howMany - 3] = '*'; + InvalidEntryChars[howMany - 4] = '?'; + } + + /// + /// Transform a windows directory name according to the Zip file naming conventions. + /// + /// The directory name to transform. + /// The transformed name. + public string TransformDirectory(string name) + { + name = TransformFile(name); + if (name.Length > 0) + { + if (!name.EndsWith("/", StringComparison.Ordinal)) + { + name += "/"; + } + } + else + { + throw new ZipException("Cannot have an empty directory name"); + } + return name; + } + + /// + /// Transform a windows file name according to the Zip file naming conventions. + /// + /// The file name to transform. + /// The transformed name. + public string TransformFile(string name) + { + if (name != null) + { + string lowerName = name.ToLower(); + if ((trimPrefix_ != null) && (lowerName.IndexOf(trimPrefix_, StringComparison.Ordinal) == 0)) + { + name = name.Substring(trimPrefix_.Length); + } + + name = name.Replace(@"\", "/"); + name = PathUtils.DropPathRoot(name); + + // Drop any leading and trailing slashes. + name = name.Trim('/'); + + // Convert consecutive // characters to / + int index = name.IndexOf("//", StringComparison.Ordinal); + while (index >= 0) + { + name = name.Remove(index, 1); + index = name.IndexOf("//", StringComparison.Ordinal); + } + + name = MakeValidName(name, '_'); + } + else + { + name = string.Empty; + } + return name; + } + + /// + /// Get/set the path prefix to be trimmed from paths if present. + /// + /// The prefix is trimmed before any conversion from + /// a windows path is done. + public string TrimPrefix + { + get { return trimPrefix_; } + set + { + trimPrefix_ = value; + if (trimPrefix_ != null) + { + trimPrefix_ = trimPrefix_.ToLower(); + } + } + } + + /// + /// Force a name to be valid by replacing invalid characters with a fixed value + /// + /// The name to force valid + /// The replacement character to use. + /// Returns a valid name + private static string MakeValidName(string name, char replacement) + { + int index = name.IndexOfAny(InvalidEntryChars); + if (index >= 0) + { + var builder = new StringBuilder(name); + + while (index >= 0) + { + builder[index] = replacement; + + if (index >= name.Length) + { + index = -1; + } + else + { + index = name.IndexOfAny(InvalidEntryChars, index + 1); + } + } + name = builder.ToString(); + } + + if (name.Length > 0xffff) + { + throw new PathTooLongException(); + } + + return name; + } + + /// + /// Test a name to see if it is a valid name for a zip entry. + /// + /// The name to test. + /// If true checking is relaxed about windows file names and absolute paths. + /// Returns true if the name is a valid zip name; false otherwise. + /// Zip path names are actually in Unix format, and should only contain relative paths. + /// This means that any path stored should not contain a drive or + /// device letter, or a leading slash. All slashes should forward slashes '/'. + /// An empty name is valid for a file where the input comes from standard input. + /// A null name is not considered valid. + /// + public static bool IsValidName(string name, bool relaxed) + { + bool result = (name != null); + + if (result) + { + if (relaxed) + { + result = name.IndexOfAny(InvalidEntryCharsRelaxed) < 0; + } + else + { + result = + (name.IndexOfAny(InvalidEntryChars) < 0) && + (name.IndexOf('/') != 0); + } + } + + return result; + } + + /// + /// Test a name to see if it is a valid name for a zip entry. + /// + /// The name to test. + /// Returns true if the name is a valid zip name; false otherwise. + /// Zip path names are actually in unix format, + /// and should only contain relative paths if a path is present. + /// This means that the path stored should not contain a drive or + /// device letter, or a leading slash. All slashes should forward slashes '/'. + /// An empty name is valid where the input comes from standard input. + /// A null name is not considered valid. + /// + public static bool IsValidName(string name) + { + bool result = + (name != null) && + (name.IndexOfAny(InvalidEntryChars) < 0) && + (name.IndexOf('/') != 0) + ; + return result; + } + + #region Instance Fields + + private string trimPrefix_; + + #endregion Instance Fields + + #region Class Fields + + private static readonly char[] InvalidEntryChars; + private static readonly char[] InvalidEntryCharsRelaxed; + + #endregion Class Fields + } + + /// + /// An implementation of INameTransform that transforms entry paths as per the Zip file naming convention. + /// Strips path roots and puts directory separators in the correct format ('/') + /// + public class PathTransformer : INameTransform + { + /// + /// Initialize a new instance of + /// + public PathTransformer() + { + } + + /// + /// Transform a windows directory name according to the Zip file naming conventions. + /// + /// The directory name to transform. + /// The transformed name. + public string TransformDirectory(string name) + { + name = TransformFile(name); + + if (name.Length > 0) + { + if (!name.EndsWith("/", StringComparison.Ordinal)) + { + name += "/"; + } + } + else + { + throw new ZipException("Cannot have an empty directory name"); + } + + return name; + } + + /// + /// Transform a windows file name according to the Zip file naming conventions. + /// + /// The file name to transform. + /// The transformed name. + public string TransformFile(string name) + { + if (name != null) + { + // Put separators in the expected format. + name = name.Replace(@"\", "/"); + + // Remove the path root. + name = PathUtils.DropPathRoot(name); + + // Drop any leading and trailing slashes. + name = name.Trim('/'); + + // Convert consecutive // characters to / + int index = name.IndexOf("//", StringComparison.Ordinal); + while (index >= 0) + { + name = name.Remove(index, 1); + index = name.IndexOf("//", StringComparison.Ordinal); + } + } + else + { + name = string.Empty; + } + + return name; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs new file mode 100644 index 0000000..06c34eb --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs @@ -0,0 +1,1028 @@ +using BSP_ICSharpCode.SharpZipLib.Checksum; +using BSP_ICSharpCode.SharpZipLib.Core; +using BSP_ICSharpCode.SharpZipLib.Encryption; +using BSP_ICSharpCode.SharpZipLib.Zip.Compression; +using BSP_ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + /// + /// This is a DeflaterOutputStream that writes the files into a zip + /// archive one after another. It has a special method to start a new + /// zip entry. The zip entries contains information about the file name + /// size, compressed size, CRC, etc. + /// + /// It includes support for Stored and Deflated entries. + /// This class is not thread safe. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ /// This sample shows how to create a zip file + /// + /// using System; + /// using System.IO; + /// + /// using ICSharpCode.SharpZipLib.Core; + /// using ICSharpCode.SharpZipLib.Zip; + /// + /// class MainClass + /// { + /// public static void Main(string[] args) + /// { + /// string[] filenames = Directory.GetFiles(args[0]); + /// byte[] buffer = new byte[4096]; + /// + /// using ( ZipOutputStream s = new ZipOutputStream(File.Create(args[1])) ) { + /// + /// s.SetLevel(9); // 0 - store only to 9 - means best compression + /// + /// foreach (string file in filenames) { + /// ZipEntry entry = new ZipEntry(file); + /// s.PutNextEntry(entry); + /// + /// using (FileStream fs = File.OpenRead(file)) { + /// StreamUtils.Copy(fs, s, buffer); + /// } + /// } + /// } + /// } + /// } + /// + /// + public class ZipOutputStream : DeflaterOutputStream + { + #region Constructors + + /// + /// Creates a new Zip output stream, writing a zip archive. + /// + /// + /// The output stream to which the archive contents are written. + /// + public ZipOutputStream(Stream baseOutputStream) + : base(baseOutputStream, new Deflater(Deflater.DEFAULT_COMPRESSION, true)) + { + } + + /// + /// Creates a new Zip output stream, writing a zip archive. + /// + /// The output stream to which the archive contents are written. + /// Size of the buffer to use. + public ZipOutputStream(Stream baseOutputStream, int bufferSize) + : base(baseOutputStream, new Deflater(Deflater.DEFAULT_COMPRESSION, true), bufferSize) + { + } + + /// + /// Creates a new Zip output stream, writing a zip archive. + /// + /// The output stream to which the archive contents are written. + /// + public ZipOutputStream(Stream baseOutputStream, StringCodec stringCodec) : this(baseOutputStream) + { + _stringCodec = stringCodec; + } + + #endregion Constructors + + /// + /// Gets a flag value of true if the central header has been added for this archive; false if it has not been added. + /// + /// No further entries can be added once this has been done. + public bool IsFinished + { + get + { + return entries == null; + } + } + + /// + /// Set the zip file comment. + /// + /// + /// The comment text for the entire archive. + /// + /// + /// The converted comment is longer than 0xffff bytes. + /// + public void SetComment(string comment) + { + byte[] commentBytes = _stringCodec.ZipArchiveCommentEncoding.GetBytes(comment); + if (commentBytes.Length > 0xffff) + { + throw new ArgumentOutOfRangeException(nameof(comment)); + } + zipComment = commentBytes; + } + + /// + /// Sets the compression level. The new level will be activated + /// immediately. + /// + /// The new compression level (1 to 9). + /// + /// Level specified is not supported. + /// + /// + public void SetLevel(int level) + { + deflater_.SetLevel(level); + defaultCompressionLevel = level; + } + + /// + /// Get the current deflater compression level + /// + /// The current compression level + public int GetLevel() + { + return deflater_.GetLevel(); + } + + /// + /// Get / set a value indicating how Zip64 Extension usage is determined when adding entries. + /// + /// Older archivers may not understand Zip64 extensions. + /// If backwards compatability is an issue be careful when adding entries to an archive. + /// Setting this property to off is workable but less desirable as in those circumstances adding a file + /// larger then 4GB will fail. + public UseZip64 UseZip64 + { + get { return useZip64_; } + set { useZip64_ = value; } + } + + /// + /// Used for transforming the names of entries added by . + /// Defaults to , set to null to disable transforms and use names as supplied. + /// + public INameTransform NameTransform { get; set; } = new PathTransformer(); + + /// + /// Get/set the password used for encryption. + /// + /// When set to null or if the password is empty no encryption is performed + public string Password + { + get + { + return password; + } + set + { + if ((value != null) && (value.Length == 0)) + { + password = null; + } + else + { + password = value; + } + } + } + + /// + /// Write an unsigned short in little endian byte order. + /// + private void WriteLeShort(int value) + { + unchecked + { + baseOutputStream_.WriteByte((byte)(value & 0xff)); + baseOutputStream_.WriteByte((byte)((value >> 8) & 0xff)); + } + } + + /// + /// Write an int in little endian byte order. + /// + private void WriteLeInt(int value) + { + unchecked + { + WriteLeShort(value); + WriteLeShort(value >> 16); + } + } + + /// + /// Write an int in little endian byte order. + /// + private void WriteLeLong(long value) + { + unchecked + { + WriteLeInt((int)value); + WriteLeInt((int)(value >> 32)); + } + } + + // Apply any configured transforms/cleaning to the name of the supplied entry. + private void TransformEntryName(ZipEntry entry) + { + if (NameTransform == null) return; + entry.Name = entry.IsDirectory + ? NameTransform.TransformDirectory(entry.Name) + : NameTransform.TransformFile(entry.Name); + } + + /// + /// Starts a new Zip entry. It automatically closes the previous + /// entry if present. + /// All entry elements bar name are optional, but must be correct if present. + /// If the compression method is stored and the output is not patchable + /// the compression for that entry is automatically changed to deflate level 0 + /// + /// + /// the entry. + /// + /// + /// if entry passed is null. + /// + /// + /// if an I/O error occurred. + /// + /// + /// if stream was finished + /// + /// + /// Too many entries in the Zip file
+ /// Entry name is too long
+ /// Finish has already been called
+ ///
+ /// + /// The Compression method specified for the entry is unsupported. + /// + public void PutNextEntry(ZipEntry entry) + { + if (curEntry != null) + { + CloseEntry(); + } + + PutNextEntry(baseOutputStream_, entry); + + if (entry.IsCrypted) + { + WriteOutput(GetEntryEncryptionHeader(entry)); + } + } + + /// + /// Starts a new passthrough Zip entry. It automatically closes the previous + /// entry if present. + /// Passthrough entry is an entry that is created from compressed data. + /// It is useful to avoid recompression to save CPU resources if compressed data is already disposable. + /// All entry elements bar name, crc, size and compressed size are optional, but must be correct if present. + /// Compression should be set to Deflated. + /// + /// + /// the entry. + /// + /// + /// if entry passed is null. + /// + /// + /// if an I/O error occurred. + /// + /// + /// if stream was finished. + /// + /// + /// Crc is not set
+ /// Size is not set
+ /// CompressedSize is not set
+ /// CompressionMethod is not Deflate
+ /// Too many entries in the Zip file
+ /// Entry name is too long
+ /// Finish has already been called
+ ///
+ /// + /// The Compression method specified for the entry is unsupported
+ /// Entry is encrypted
+ ///
+ public void PutNextPassthroughEntry(ZipEntry entry) + { + if(curEntry != null) + { + CloseEntry(); + } + + if(entry.Crc < 0) + { + throw new ZipException("Crc must be set for passthrough entry"); + } + + if(entry.Size < 0) + { + throw new ZipException("Size must be set for passthrough entry"); + } + + if(entry.CompressedSize < 0) + { + throw new ZipException("CompressedSize must be set for passthrough entry"); + } + + if(entry.CompressionMethod != CompressionMethod.Deflated) + { + throw new NotImplementedException("Only Deflated entries are supported for passthrough"); + } + + if(!string.IsNullOrEmpty(Password)) + { + throw new NotImplementedException("Encrypted passthrough entries are not supported"); + } + + PutNextEntry(baseOutputStream_, entry, 0, true); + } + + + private void WriteOutput(byte[] bytes) + => baseOutputStream_.Write(bytes, 0, bytes.Length); + + private Task WriteOutputAsync(byte[] bytes) + => baseOutputStream_.WriteAsync(bytes, 0, bytes.Length); + + private byte[] GetEntryEncryptionHeader(ZipEntry entry) => + entry.AESKeySize > 0 + ? InitializeAESPassword(entry, Password) + : CreateZipCryptoHeader(entry.Crc < 0 ? entry.DosTime << 16 : entry.Crc); + + internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0, bool passthroughEntry = false) + { + if (entry == null) + { + throw new ArgumentNullException(nameof(entry)); + } + + if (entries == null) + { + throw new InvalidOperationException("ZipOutputStream was finished"); + } + + if (entries.Count == int.MaxValue) + { + throw new ZipException("Too many entries for Zip file"); + } + + CompressionMethod method = entry.CompressionMethod; + + // Check that the compression is one that we support + if (method != CompressionMethod.Deflated && method != CompressionMethod.Stored) + { + throw new NotImplementedException("Compression method not supported"); + } + + // A password must have been set in order to add AES encrypted entries + if (entry.AESKeySize > 0 && string.IsNullOrEmpty(this.Password)) + { + throw new InvalidOperationException("The Password property must be set before AES encrypted entries can be added"); + } + + entryIsPassthrough = passthroughEntry; + + int compressionLevel = defaultCompressionLevel; + + // Clear flags that the library manages internally + entry.Flags &= (int)GeneralBitFlags.UnicodeText; + patchEntryHeader = false; + + bool headerInfoAvailable; + + // No need to compress - definitely no data. + if (entry.Size == 0 && !entryIsPassthrough) + { + entry.CompressedSize = entry.Size; + entry.Crc = 0; + method = CompressionMethod.Stored; + headerInfoAvailable = true; + } + else + { + headerInfoAvailable = (entry.Size >= 0) && entry.HasCrc && entry.CompressedSize >= 0; + + // Switch to deflation if storing isnt possible. + if (method == CompressionMethod.Stored) + { + if (!headerInfoAvailable) + { + if (!CanPatchEntries) + { + // Can't patch entries so storing is not possible. + method = CompressionMethod.Deflated; + compressionLevel = 0; + } + } + else // entry.size must be > 0 + { + entry.CompressedSize = entry.Size; + headerInfoAvailable = entry.HasCrc; + } + } + } + + if (headerInfoAvailable == false) + { + if (CanPatchEntries == false) + { + // Only way to record size and compressed size is to append a data descriptor + // after compressed data. + + // Stored entries of this form have already been converted to deflating. + entry.Flags |= 8; + } + else + { + patchEntryHeader = true; + } + } + + if (Password != null) + { + entry.IsCrypted = true; + if (entry.Crc < 0) + { + // Need to append a data descriptor as the crc isnt available for use + // with encryption, the date is used instead. Setting the flag + // indicates this to the decompressor. + entry.Flags |= 8; + } + } + + entry.Offset = offset; + entry.CompressionMethod = (CompressionMethod)method; + + curMethod = method; + + if ((useZip64_ == UseZip64.On) || ((entry.Size < 0) && (useZip64_ == UseZip64.Dynamic))) + { + entry.ForceZip64(); + } + + // Apply any required transforms to the entry name + TransformEntryName(entry); + + // Write the local file header + offset += ZipFormat.WriteLocalHeader(stream, entry, out var entryPatchData, + headerInfoAvailable, patchEntryHeader, streamOffset, _stringCodec); + + patchData = entryPatchData; + + // Fix offsetOfCentraldir for AES + if (entry.AESKeySize > 0) + offset += entry.AESOverheadSize; + + // Activate the entry. + curEntry = entry; + size = 0; + + if(entryIsPassthrough) + return; + + crc.Reset(); + if (method == CompressionMethod.Deflated) + { + deflater_.Reset(); + deflater_.SetLevel(compressionLevel); + } + } + + /// + /// Starts a new Zip entry. It automatically closes the previous + /// entry if present. + /// All entry elements bar name are optional, but must be correct if present. + /// If the compression method is stored and the output is not patchable + /// the compression for that entry is automatically changed to deflate level 0 + /// + /// + /// the entry. + /// + /// The that can be used to cancel the operation. + /// + /// if entry passed is null. + /// + /// + /// if an I/O error occured. + /// + /// + /// if stream was finished + /// + /// + /// Too many entries in the Zip file
+ /// Entry name is too long
+ /// Finish has already been called
+ ///
+ /// + /// The Compression method specified for the entry is unsupported. + /// + public async Task PutNextEntryAsync(ZipEntry entry, CancellationToken ct = default) + { + if (curEntry != null) await CloseEntryAsync(ct).ConfigureAwait(false); + var position = CanPatchEntries ? baseOutputStream_.Position : -1; + await baseOutputStream_.WriteProcToStreamAsync(s => + { + PutNextEntry(s, entry, position); + }, ct).ConfigureAwait(false); + + if (!entry.IsCrypted) return; + await WriteOutputAsync(GetEntryEncryptionHeader(entry)).ConfigureAwait(false); + } + + /// + /// Closes the current entry, updating header and footer information as required + /// + /// + /// Invalid entry field values. + /// + /// + /// An I/O error occurs. + /// + /// + /// No entry is active. + /// + public void CloseEntry() + { + // Note: This method will run synchronously + FinishCompressionSyncOrAsync(null).GetAwaiter().GetResult(); + WriteEntryFooter(baseOutputStream_); + + // Patch the header if possible + if (patchEntryHeader) + { + patchEntryHeader = false; + ZipFormat.PatchLocalHeaderSync(baseOutputStream_, curEntry, patchData); + } + + entries.Add(curEntry); + curEntry = null; + } + + private async Task FinishCompressionSyncOrAsync(CancellationToken? ct) + { + // Compression handled externally + if (entryIsPassthrough) return; + + // First finish the deflater, if appropriate + if (curMethod == CompressionMethod.Deflated) + { + if (size >= 0) + { + if (ct.HasValue) { + await base.FinishAsync(ct.Value).ConfigureAwait(false); + } else { + base.Finish(); + } + } + else + { + deflater_.Reset(); + } + } + if (curMethod == CompressionMethod.Stored) + { + // This is done by Finish() for Deflated entries, but we need to do it + // ourselves for Stored ones + base.GetAuthCodeIfAES(); + } + + return; + } + + /// + public async Task CloseEntryAsync(CancellationToken ct) + { + await FinishCompressionSyncOrAsync(ct).ConfigureAwait(false); + await baseOutputStream_.WriteProcToStreamAsync(WriteEntryFooter, ct).ConfigureAwait(false); + + // Patch the header if possible + if (patchEntryHeader) + { + patchEntryHeader = false; + await ZipFormat.PatchLocalHeaderAsync(baseOutputStream_, curEntry, patchData, ct).ConfigureAwait(false); + } + + entries.Add(curEntry); + curEntry = null; + } + + internal void WriteEntryFooter(Stream stream) + { + if (curEntry == null) + { + throw new InvalidOperationException("No open entry"); + } + + if(entryIsPassthrough) + { + if(curEntry.CompressedSize != size) + { + throw new ZipException($"compressed size was {size}, but {curEntry.CompressedSize} expected"); + } + + offset += size; + return; + } + + long csize = size; + + if (curMethod == CompressionMethod.Deflated && size >= 0) + { + csize = deflater_.TotalOut; + } + + // Write the AES Authentication Code (a hash of the compressed and encrypted data) + if (curEntry.AESKeySize > 0) + { + stream.Write(AESAuthCode, 0, 10); + // Always use 0 as CRC for AE-2 format + curEntry.Crc = 0; + } + else + { + if (curEntry.Crc < 0) + { + curEntry.Crc = crc.Value; + } + else if (curEntry.Crc != crc.Value) + { + throw new ZipException($"crc was {crc.Value}, but {curEntry.Crc} was expected"); + } + } + + if (curEntry.Size < 0) + { + curEntry.Size = size; + } + else if (curEntry.Size != size) + { + throw new ZipException($"size was {size}, but {curEntry.Size} was expected"); + } + + if (curEntry.CompressedSize < 0) + { + curEntry.CompressedSize = csize; + } + else if (curEntry.CompressedSize != csize) + { + throw new ZipException($"compressed size was {csize}, but {curEntry.CompressedSize} expected"); + } + + offset += csize; + + if (curEntry.IsCrypted) + { + curEntry.CompressedSize += curEntry.EncryptionOverheadSize; + } + + // Add data descriptor if flagged as required + if ((curEntry.Flags & 8) != 0) + { + stream.WriteLEInt(ZipConstants.DataDescriptorSignature); + stream.WriteLEInt(unchecked((int)curEntry.Crc)); + + if (curEntry.LocalHeaderRequiresZip64) + { + stream.WriteLELong(curEntry.CompressedSize); + stream.WriteLELong(curEntry.Size); + offset += ZipConstants.Zip64DataDescriptorSize; + } + else + { + stream.WriteLEInt((int)curEntry.CompressedSize); + stream.WriteLEInt((int)curEntry.Size); + offset += ZipConstants.DataDescriptorSize; + } + } + } + + + + // File format for AES: + // Size (bytes) Content + // ------------ ------- + // Variable Salt value + // 2 Password verification value + // Variable Encrypted file data + // 10 Authentication code + // + // Value in the "compressed size" fields of the local file header and the central directory entry + // is the total size of all the items listed above. In other words, it is the total size of the + // salt value, password verification value, encrypted data, and authentication code. + + /// + /// Initializes encryption keys based on given password. + /// + protected byte[] InitializeAESPassword(ZipEntry entry, string rawPassword) + { + var salt = new byte[entry.AESSaltLen]; + // Salt needs to be cryptographically random, and unique per file + if (_aesRnd == null) + _aesRnd = RandomNumberGenerator.Create(); + _aesRnd.GetBytes(salt); + int blockSize = entry.AESKeySize / 8; // bits to bytes + + cryptoTransform_ = new ZipAESTransform(rawPassword, salt, blockSize, true); + + var headBytes = new byte[salt.Length + 2]; + + Array.Copy(salt, headBytes, salt.Length); + Array.Copy(((ZipAESTransform)cryptoTransform_).PwdVerifier, 0, + headBytes, headBytes.Length - 2, 2); + + return headBytes; + } + + private byte[] CreateZipCryptoHeader(long crcValue) + { + offset += ZipConstants.CryptoHeaderSize; + + InitializeZipCryptoPassword(Password); + + byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(cryptBuffer); + } + + cryptBuffer[11] = (byte)(crcValue >> 24); + + EncryptBlock(cryptBuffer, 0, cryptBuffer.Length); + + return cryptBuffer; + } + + /// + /// Initializes encryption keys based on given . + /// + /// The password. + private void InitializeZipCryptoPassword(string password) + { + var pkManaged = new PkzipClassicManaged(); + byte[] key = PkzipClassic.GenerateKeys(ZipCryptoEncoding.GetBytes(password)); + cryptoTransform_ = pkManaged.CreateEncryptor(key, null); + } + + /// + /// Writes the given buffer to the current entry. + /// + /// The buffer containing data to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// Archive size is invalid + /// No entry is active. + public override void Write(byte[] buffer, int offset, int count) + => WriteSyncOrAsync(buffer, offset, count, null).GetAwaiter().GetResult(); + + /// + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken ct) + => await WriteSyncOrAsync(buffer, offset, count, ct).ConfigureAwait(false); + + private async Task WriteSyncOrAsync(byte[] buffer, int offset, int count, CancellationToken? ct) + { + if (curEntry == null) + { + throw new InvalidOperationException("No open entry."); + } + + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Cannot be negative"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Cannot be negative"); + } + + if ((buffer.Length - offset) < count) + { + throw new ArgumentException("Invalid offset/count combination"); + } + + if (curEntry.AESKeySize == 0 && !entryIsPassthrough) + { + // Only update CRC if AES is not enabled and entry is not a passthrough one + crc.Update(new ArraySegment(buffer, offset, count)); + } + + size += count; + + if (curMethod == CompressionMethod.Stored || entryIsPassthrough) + { + if (Password != null) + { + CopyAndEncrypt(buffer, offset, count); + } + else + { + if (ct.HasValue) + { + await baseOutputStream_.WriteAsync(buffer, offset, count, ct.Value).ConfigureAwait(false); + } + else + { + baseOutputStream_.Write(buffer, offset, count); + } + } + } + else + { + if (ct.HasValue) + { + await base.WriteAsync(buffer, offset, count, ct.Value).ConfigureAwait(false); + } + else + { + base.Write(buffer, offset, count); + } + } + } + + private void CopyAndEncrypt(byte[] buffer, int offset, int count) + { + const int copyBufferSize = 4096; + byte[] localBuffer = new byte[copyBufferSize]; + while (count > 0) + { + int bufferCount = (count < copyBufferSize) ? count : copyBufferSize; + + Array.Copy(buffer, offset, localBuffer, 0, bufferCount); + EncryptBlock(localBuffer, 0, bufferCount); + baseOutputStream_.Write(localBuffer, 0, bufferCount); + count -= bufferCount; + offset += bufferCount; + } + } + + /// + /// Finishes the stream. This will write the central directory at the + /// end of the zip file and flush the stream. + /// + /// + /// This is automatically called when the stream is closed. + /// + /// + /// An I/O error occurs. + /// + /// + /// Comment exceeds the maximum length
+ /// Entry name exceeds the maximum length + ///
+ public override void Finish() + { + if (entries == null) + { + return; + } + + if (curEntry != null) + { + CloseEntry(); + } + + long numEntries = entries.Count; + long sizeEntries = 0; + + foreach (var entry in entries) + { + sizeEntries += ZipFormat.WriteEndEntry(baseOutputStream_, entry, _stringCodec); + } + + ZipFormat.WriteEndOfCentralDirectory(baseOutputStream_, numEntries, sizeEntries, offset, zipComment); + + entries = null; + } + + /// > + public override async Task FinishAsync(CancellationToken ct) + { + using (var ms = new MemoryStream()) + { + if (entries == null) + { + return; + } + + if (curEntry != null) + { + await CloseEntryAsync(ct).ConfigureAwait(false); + } + + long numEntries = entries.Count; + long sizeEntries = 0; + + foreach (var entry in entries) + { + await baseOutputStream_.WriteProcToStreamAsync(ms, s => + { + sizeEntries += ZipFormat.WriteEndEntry(s, entry, _stringCodec); + }, ct).ConfigureAwait(false); + } + + await baseOutputStream_.WriteProcToStreamAsync(ms, s + => ZipFormat.WriteEndOfCentralDirectory(s, numEntries, sizeEntries, offset, zipComment), + ct).ConfigureAwait(false); + + entries = null; + } + } + + /// + /// Flushes the stream by calling Flush on the deflater stream unless + /// the current compression method is . Then it flushes the underlying output stream. + /// + public override void Flush() + { + if(curMethod == CompressionMethod.Stored) + { + baseOutputStream_.Flush(); + } + else + { + base.Flush(); + } + } + + #region Instance Fields + + /// + /// The entries for the archive. + /// + private List entries = new List(); + + /// + /// Used to track the crc of data added to entries. + /// + private Crc32 crc = new Crc32(); + + /// + /// The current entry being added. + /// + private ZipEntry curEntry; + + private bool entryIsPassthrough; + + private int defaultCompressionLevel = Deflater.DEFAULT_COMPRESSION; + + private CompressionMethod curMethod = CompressionMethod.Deflated; + + /// + /// Used to track the size of data for an entry during writing. + /// + private long size; + + /// + /// Offset to be recorded for each entry in the central header. + /// + private long offset; + + /// + /// Comment for the entire archive recorded in central header. + /// + private byte[] zipComment = Empty.Array(); + + /// + /// Flag indicating that header patching is required for the current entry. + /// + private bool patchEntryHeader; + + /// + /// The values to patch in the entry local header + /// + private EntryPatchData patchData; + + // Default is dynamic which is not backwards compatible and can cause problems + // with XP's built in compression which cant read Zip64 archives. + // However it does avoid the situation were a large file is added and cannot be completed correctly. + // NOTE: Setting the size for entries before they are added is the best solution! + private UseZip64 useZip64_ = UseZip64.Dynamic; + + /// + /// The password to use when encrypting archive entries. + /// + private string password; + + #endregion Instance Fields + + #region Static Fields + + // Static to help ensure that multiple files within a zip will get different random salt + private static RandomNumberGenerator _aesRnd = RandomNumberGenerator.Create(); + + #endregion Static Fields + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipStrings.cs b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipStrings.cs new file mode 100644 index 0000000..926dffd --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Misc/ICSharpCode.SharpZipLib/Zip/ZipStrings.cs @@ -0,0 +1,260 @@ +using System; +using System.Text; +using BSP_ICSharpCode.SharpZipLib.Core; + +namespace BSP_ICSharpCode.SharpZipLib.Zip +{ + internal static class EncodingExtensions + { + public static bool IsZipUnicode(this Encoding e) + => e.Equals(StringCodec.UnicodeZipEncoding); + } + + /// + /// Deprecated way of setting zip encoding provided for backwards compability. + /// Use when possible. + /// + /// + /// If any ZipStrings properties are being modified, it will enter a backwards compatibility mode, mimicking the + /// old behaviour where a single instance was shared between all Zip* instances. + /// + public static class ZipStrings + { + static StringCodec CompatCodec = StringCodec.Default; + + private static bool compatibilityMode; + + /// + /// Returns a new instance or the shared backwards compatible instance. + /// + /// + public static StringCodec GetStringCodec() + => compatibilityMode ? CompatCodec : StringCodec.Default; + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + public static int CodePage + { + get => CompatCodec.CodePage; + set + { + CompatCodec = new StringCodec(CompatCodec.ForceZipLegacyEncoding, Encoding.GetEncoding(value)) + { + ZipArchiveCommentEncoding = CompatCodec.ZipArchiveCommentEncoding, + ZipCryptoEncoding = CompatCodec.ZipCryptoEncoding, + }; + compatibilityMode = true; + } + } + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + public static int SystemDefaultCodePage => StringCodec.SystemDefaultCodePage; + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + public static bool UseUnicode + { + get => !CompatCodec.ForceZipLegacyEncoding; + set + { + CompatCodec = new StringCodec(!value, CompatCodec.LegacyEncoding) + { + ZipArchiveCommentEncoding = CompatCodec.ZipArchiveCommentEncoding, + ZipCryptoEncoding = CompatCodec.ZipCryptoEncoding, + }; + compatibilityMode = true; + } + } + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + private static bool HasUnicodeFlag(int flags) + => ((GeneralBitFlags)flags).HasFlag(GeneralBitFlags.UnicodeText); + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + public static string ConvertToString(byte[] data, int count) + => CompatCodec.ZipOutputEncoding.GetString(data, 0, count); + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + public static string ConvertToString(byte[] data) + => CompatCodec.ZipOutputEncoding.GetString(data); + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + public static string ConvertToStringExt(int flags, byte[] data, int count) + => CompatCodec.ZipEncoding(HasUnicodeFlag(flags)).GetString(data, 0, count); + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + public static string ConvertToStringExt(int flags, byte[] data) + => CompatCodec.ZipEncoding(HasUnicodeFlag(flags)).GetString(data); + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + public static byte[] ConvertToArray(string str) + => ConvertToArray(0, str); + + /// + [Obsolete("Use ZipFile/Zip*Stream StringCodec instead")] + public static byte[] ConvertToArray(int flags, string str) + => (string.IsNullOrEmpty(str)) + ? Empty.Array() + : CompatCodec.ZipEncoding(HasUnicodeFlag(flags)).GetBytes(str); + } + + /// + /// Utility class for resolving the encoding used for reading and writing strings + /// + public class StringCodec + { + internal StringCodec(bool forceLegacyEncoding, Encoding legacyEncoding) + { + LegacyEncoding = legacyEncoding; + ForceZipLegacyEncoding = forceLegacyEncoding; + ZipArchiveCommentEncoding = legacyEncoding; + ZipCryptoEncoding = legacyEncoding; + } + + /// + /// Creates a StringCodec that uses the system default encoder or UTF-8 depending on whether the zip entry Unicode flag is set + /// + public static StringCodec Default + => new StringCodec(false, SystemDefaultEncoding); + + /// + /// Creates a StringCodec that uses an encoding from the specified code page except for zip entries with the Unicode flag + /// + public static StringCodec FromCodePage(int codePage) + => new StringCodec(false, Encoding.GetEncoding(codePage)); + + /// + /// Creates a StringCodec that uses an the specified encoding, except for zip entries with the Unicode flag + /// + public static StringCodec FromEncoding(Encoding encoding) + => new StringCodec(false, encoding); + + /// + /// Creates a StringCodec that uses the zip specification encoder or UTF-8 depending on whether the zip entry Unicode flag is set + /// + public static StringCodec WithStrictSpecEncoding() + => new StringCodec(false, Encoding.GetEncoding(ZipSpecCodePage)); + + /// + /// If set, use the encoding set by for zip entries instead of the defaults + /// + public bool ForceZipLegacyEncoding { get; internal set; } + + /// + /// The default encoding used for ZipCrypto passwords in zip files, set to + /// for greatest compability. + /// + public static Encoding DefaultZipCryptoEncoding => SystemDefaultEncoding; + + /// + /// Returns the encoding for an output . + /// Unless overriden by it returns . + /// + public Encoding ZipOutputEncoding => ZipEncoding(!ForceZipLegacyEncoding); + + /// + /// Returns if is set, otherwise it returns the encoding indicated by + /// + public Encoding ZipEncoding(bool unicode) + => unicode ? UnicodeZipEncoding : LegacyEncoding; + + /// + /// Returns the appropriate encoding for an input according to . + /// If overridden by , it always returns the encoding indicated by . + /// + /// + /// + public Encoding ZipInputEncoding(GeneralBitFlags flags) + => ZipEncoding(!ForceZipLegacyEncoding && flags.HasAny(GeneralBitFlags.UnicodeText)); + + /// + public Encoding ZipInputEncoding(int flags) => ZipInputEncoding((GeneralBitFlags)flags); + + /// Code page encoding, used for non-unicode strings + /// + /// The original Zip specification (https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) states + /// that file names should only be encoded with IBM Code Page 437 or UTF-8. + /// In practice, most zip apps use OEM or system encoding (typically cp437 on Windows). + /// + public Encoding LegacyEncoding { get; internal set; } + + /// + /// Returns the UTF-8 code page (65001) used for zip entries with unicode flag set + /// + public static readonly Encoding UnicodeZipEncoding = Encoding.UTF8; + + /// + /// Code page used for non-unicode strings and legacy zip encoding (if is set). + /// Default value is + /// + public int CodePage => LegacyEncoding.CodePage; + + /// + /// The non-unicode code page that should be used according to the zip specification + /// + public const int ZipSpecCodePage = 437; + + /// + /// Operating system default codepage. + /// + public static int SystemDefaultCodePage => SystemDefaultEncoding.CodePage; + + /// + /// The system default encoding. + /// + public static Encoding SystemDefaultEncoding => Encoding.GetEncoding(0); + + /// + /// The encoding used for the zip archive comment. Defaults to the encoding for , since + /// no unicode flag can be set for it in the files. + /// + public Encoding ZipArchiveCommentEncoding { get; internal set; } + + /// + /// The encoding used for the ZipCrypto passwords. Defaults to . + /// + public Encoding ZipCryptoEncoding { get; internal set; } + + /// + /// Create a copy of this StringCodec with the specified zip archive comment encoding + /// + /// + /// + public StringCodec WithZipArchiveCommentEncoding(Encoding commentEncoding) + => new StringCodec(ForceZipLegacyEncoding, LegacyEncoding) + { + ZipArchiveCommentEncoding = commentEncoding, + ZipCryptoEncoding = ZipCryptoEncoding + }; + + /// + /// Create a copy of this StringCodec with the specified zip crypto password encoding + /// + /// + /// + public StringCodec WithZipCryptoEncoding(Encoding cryptoEncoding) + => new StringCodec(ForceZipLegacyEncoding, LegacyEncoding) + { + ZipArchiveCommentEncoding = ZipArchiveCommentEncoding, + ZipCryptoEncoding = cryptoEncoding + }; + + /// + /// Create a copy of this StringCodec that ignores the Unicode flag when reading entries + /// + /// + public StringCodec WithForcedLegacyEncoding() + => new StringCodec(true, LegacyEncoding) + { + ZipArchiveCommentEncoding = ZipArchiveCommentEncoding, + ZipCryptoEncoding = ZipCryptoEncoding + }; + } +} diff --git a/BeatSaberPlus/CP_SDK/Misc/Time.cs b/BeatSaberPlus/CP_SDK/Misc/Time.cs index faaf0fd..3226879 100644 --- a/BeatSaberPlus/CP_SDK/Misc/Time.cs +++ b/BeatSaberPlus/CP_SDK/Misc/Time.cs @@ -6,12 +6,43 @@ namespace CP_SDK.Misc /// /// Time helper /// - public class Time + public static class Time { - /// - /// Unix Epoch - /// private static readonly DateTime s_UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + private static string[] s_Months = new string[] { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + }; + private static string[] s_MonthsShort = new string[] { + "Jan.", + "Feb.", + "Mar.", + "Apr.", + "May", + "Jun.", + "Jul.", + "Aug.", + "Sept.", + "Oct.", + "Nov.", + "Dec." + }; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public static string[] MonthNames => s_Months; + public static string[] MonthNamesShort => s_MonthsShort; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -21,53 +52,41 @@ public class Time ///
/// Unix timestamp public static Int64 UnixTimeNow() - { - return (Int64)(DateTime.UtcNow - s_UnixEpoch).TotalSeconds; - } + => (Int64)(DateTime.UtcNow - s_UnixEpoch).TotalSeconds; /// /// Get UnixTimestamp /// /// Unix timestamp public static Int64 UnixTimeNowMS() - { - return (Int64)(DateTime.UtcNow - s_UnixEpoch).TotalMilliseconds; - } + => (Int64)(DateTime.UtcNow - s_UnixEpoch).TotalMilliseconds; /// /// Convert DateTime to UnixTimestamp /// /// The DateTime to convert /// public static Int64 ToUnixTime(DateTime p_DateTime) - { - return (Int64)p_DateTime.ToUniversalTime().Subtract(s_UnixEpoch).TotalSeconds; - } + => (Int64)p_DateTime.ToUniversalTime().Subtract(s_UnixEpoch).TotalSeconds; /// /// Convert DateTime to UnixTimestamp /// /// The DateTime to convert /// public static Int64 ToUnixTimeMS(DateTime p_DateTime) - { - return (Int64)p_DateTime.ToUniversalTime().Subtract(s_UnixEpoch).TotalMilliseconds; - } + => (Int64)p_DateTime.ToUniversalTime().Subtract(s_UnixEpoch).TotalMilliseconds; /// /// Convert UnixTimestamp to DateTime /// /// /// public static DateTime FromUnixTime(Int64 p_TimeStamp) - { - return s_UnixEpoch.AddSeconds(p_TimeStamp).ToLocalTime(); - } + => s_UnixEpoch.AddSeconds(p_TimeStamp).ToLocalTime(); /// /// Convert UnixTimestamp to DateTime /// /// /// public static DateTime FromUnixTimeMS(Int64 p_TimeStamp) - { - return s_UnixEpoch.AddMilliseconds(p_TimeStamp).ToLocalTime(); - } + => s_UnixEpoch.AddMilliseconds(p_TimeStamp).ToLocalTime(); /// /// Try parse international data /// @@ -75,8 +94,6 @@ public static DateTime FromUnixTimeMS(Int64 p_TimeStamp) /// /// public static bool TryParseInternational(string p_Input, out DateTime p_Result) - { - return DateTime.TryParse(p_Input, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out p_Result); - } + => DateTime.TryParse(p_Input, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out p_Result); } } diff --git a/BeatSaberPlus/CP_SDK/ModuleBase.cs b/BeatSaberPlus/CP_SDK/ModuleBase.cs index 1af385d..a00446f 100644 --- a/BeatSaberPlus/CP_SDK/ModuleBase.cs +++ b/BeatSaberPlus/CP_SDK/ModuleBase.cs @@ -1,4 +1,6 @@ -namespace CP_SDK +using UnityEngine; + +namespace CP_SDK { /// /// Module type @@ -26,34 +28,14 @@ public enum EIModuleBaseActivationType /// public interface IModuleBase { - /// - /// Module type - /// - EIModuleBaseType Type { get; } - /// - /// Name of the Module - /// - string Name { get; } - /// - /// Fancy Name of the Module - /// - string FancyName { get; } - /// - /// Description of the Module - /// - string Description { get; } - /// - /// Is the plugin using chat features - /// - bool UseChatFeatures { get; } - /// - /// Is enabled - /// - bool IsEnabled { get; set; } - /// - /// Activation type - /// - EIModuleBaseActivationType ActivationType { get; } + EIModuleBaseType Type { get; } + string Name { get; } + string FancyName { get; } + string Description { get; } + string DocumentationURL { get; } + bool UseChatFeatures { get; } + bool IsEnabled { get; set; } + EIModuleBaseActivationType ActivationType { get; } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -76,6 +58,14 @@ public interface IModuleBase /// On application exit /// void OnApplicationExit(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get Module settings UI + /// + (UI.IViewController, UI.IViewController, UI.IViewController) GetSettingsViewControllers(); } //////////////////////////////////////////////////////////////////////////// @@ -84,37 +74,17 @@ public interface IModuleBase /// /// Module base interface /// - public abstract class ModuleBase : IModuleBase - where T : ModuleBase, new() + public abstract class ModuleBase : IModuleBase + where t_Type : ModuleBase, new() { - /// - /// Module type - /// - public abstract EIModuleBaseType Type { get; } - /// - /// Name of the Module - /// - public abstract string Name { get; } - /// - /// Fancy Name of the Module - /// - public virtual string FancyName => Name; - /// - /// Description of the Module - /// - public abstract string Description { get; } - /// - /// Is the plugin using chat features - /// - public abstract bool UseChatFeatures { get; } - /// - /// Is enabled - /// - public abstract bool IsEnabled { get; set; } - /// - /// Activation type - /// - public abstract EIModuleBaseActivationType ActivationType { get; } + public abstract EIModuleBaseType Type { get; } + public abstract string Name { get; } + public virtual string FancyName => Name; + public abstract string Description { get; } + public virtual string DocumentationURL => string.Empty; + public abstract bool UseChatFeatures { get; } + public abstract bool IsEnabled { get; set; } + public abstract EIModuleBaseActivationType ActivationType { get; } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -122,7 +92,7 @@ public abstract class ModuleBase : IModuleBase /// /// Singleton /// - public static T Instance { get; private set; } = null; + public static t_Type Instance { get; private set; } = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -180,7 +150,7 @@ internal void Enable() return; m_WasEnabled = true; - Instance = this as T; + Instance = this as t_Type; OnEnable(); } /// @@ -207,5 +177,22 @@ internal void Disable() /// Disable the Module /// protected abstract void OnDisable(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get Module settings UI + /// + public (UI.IViewController, UI.IViewController, UI.IViewController) GetSettingsViewControllers() => GetSettingsViewControllersImplementation(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get Module settings UI + /// + protected virtual (UI.IViewController, UI.IViewController, UI.IViewController) GetSettingsViewControllersImplementation() + => (null, null, null); } } diff --git a/BeatSaberPlus/CP_SDK/Network/APIClient.cs b/BeatSaberPlus/CP_SDK/Network/APIClient.cs deleted file mode 100644 index 0c9084d..0000000 --- a/BeatSaberPlus/CP_SDK/Network/APIClient.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace CP_SDK.Network -{ - /// - /// API client class - /// - public sealed class APIClient - { - /// - /// API client - /// - private HttpClient m_Client = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Maximum retry attempt - /// - public int MaxRetry = 5; - /// - /// Delay between each retry - /// - public int RetryInterval = 5 * 1000; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Client public accesor - /// - public HttpClient InternalClient => m_Client; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - /// Base address - /// Maximum timeout - /// Should keep alive the connection - /// Should force cache discard - public APIClient(string p_BaseAddress, TimeSpan p_TimeOut, bool p_KeepAlive = true, bool p_ForceCacheDiscard = true) - { - HttpClientHandler l_Handler = new HttpClientHandler() - { - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, - }; - - m_Client = new HttpClient(l_Handler) - { - Timeout = p_TimeOut, - }; - - if (!string.IsNullOrEmpty(p_BaseAddress)) - m_Client.BaseAddress = new Uri(p_BaseAddress); - - if (p_ForceCacheDiscard) - { - m_Client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue - { - NoCache = true, - NoStore = false, - MustRevalidate = true, - ProxyRevalidate = true, - MaxAge = TimeSpan.FromSeconds(0), - SharedMaxAge = TimeSpan.FromMilliseconds(0), - MaxStaleLimit = TimeSpan.FromMilliseconds(0) - }; - } - m_Client.DefaultRequestHeaders.ConnectionClose = !p_KeepAlive; - m_Client.DefaultRequestHeaders.Add("User-Agent", ChatPlexSDK.NetworkUserAgent); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Do Async get query - /// - /// Request content - /// HTTPResponse - public async Task GetAsync(string p_URL, CancellationToken p_Token, bool p_DontRetry = false) - { -#if DEBUG - ChatPlexSDK.Logger.Debug("[CP_SDK.Network][APIClient.GetAsync] GET " + p_URL); -#endif - p_Token.ThrowIfCancellationRequested(); - - HttpResponseMessage l_Reply = null; - for (int l_Retry = 0; l_Retry < MaxRetry; l_Retry++) - { - if (p_Token.IsCancellationRequested) - p_Token.ThrowIfCancellationRequested(); - - try - { - l_Reply = await m_Client.GetAsync(p_URL, p_Token).ConfigureAwait(false); - - if (l_Reply != null && l_Reply.StatusCode == (HttpStatusCode)429) - { - var l_Limits = RateLimitInfo.FromHttp(l_Reply); - if (l_Limits != null) - { - int l_TotalMilliseconds = (int)(l_Limits.Reset - DateTime.Now).TotalMilliseconds; - if (l_TotalMilliseconds > 0) - { - await Task.Delay(l_TotalMilliseconds).ConfigureAwait(false); - continue; - } - } - } - - if (p_DontRetry || l_Reply.IsSuccessStatusCode || l_Reply.StatusCode == HttpStatusCode.NotFound || l_Reply.StatusCode == HttpStatusCode.BadRequest) - { - /// Read reply - var l_Buffer = await l_Reply.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - var l_ResponseBuffer = Encoding.UTF8.GetString(l_Buffer, 0, l_Buffer.Length); - - return new APIResponse(l_Reply, l_Buffer, l_ResponseBuffer); - } - } - catch (System.Exception) - { - /// Do nothing here - } - - if (p_Token.IsCancellationRequested) - p_Token.ThrowIfCancellationRequested(); - - if (l_Reply != null) - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][APIClient.GetAsync] Request {SafeURL(p_URL)} failed with code {l_Reply.StatusCode}:\"{l_Reply.ReasonPhrase}\", next try in 5 seconds..."); - else - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][APIClient.GetAsync] Request {SafeURL(p_URL)} failed, next try in 5 seconds..."); - - /// Short exit - if (p_DontRetry) - return null; - - /// Wait 5 seconds - await Task.Delay(RetryInterval).ConfigureAwait(false); - } - - return null; - } - /// - /// Do Async post query - /// - /// Request content - /// HTTPResponse - public async Task PostAsync(string p_URL, HttpContent p_Content, CancellationToken p_Token, bool p_DontRetry = false) - { - p_Token.ThrowIfCancellationRequested(); - - HttpResponseMessage l_Reply = null; - for (int l_Retry = 0; l_Retry < MaxRetry; l_Retry++) - { - if (p_Token.IsCancellationRequested) - p_Token.ThrowIfCancellationRequested(); - - try - { - l_Reply = await m_Client.PostAsync(p_URL, p_Content, p_Token).ConfigureAwait(false); - - if (p_DontRetry || l_Reply.IsSuccessStatusCode || l_Reply.StatusCode == HttpStatusCode.NotFound || l_Reply.StatusCode == HttpStatusCode.BadRequest) - { - /// Read reply - var l_Buffer = await l_Reply.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - var l_ResponseBuffer = Encoding.UTF8.GetString(l_Buffer, 0, l_Buffer.Length); - - return new APIResponse(l_Reply, l_Buffer, l_ResponseBuffer); - } - } - catch (System.Exception) - { - /// Do nothing here - } - - if (p_Token.IsCancellationRequested) - p_Token.ThrowIfCancellationRequested(); - - if (l_Reply != null) - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][APIClient.PostAsync] Request {SafeURL(p_URL)} failed with code {l_Reply.StatusCode}:\"{l_Reply.ReasonPhrase}\", next try in 5 seconds..."); - else - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][APIClient.PostAsync] Request {SafeURL(p_URL)} failed, next try in 5 seconds..."); - - /// Short exit - if (p_DontRetry) - return null; - - /// Wait 5 seconds - await Task.Delay(RetryInterval).ConfigureAwait(false); - } - - return null; - } - /// - /// Do Async patch query - /// - /// Request content - /// HTTPResponse - public async Task PatchAsync(string p_URL, HttpContent p_Content, CancellationToken p_Token, bool p_DontRetry = false) - { - p_Token.ThrowIfCancellationRequested(); - - var l_Request = new HttpRequestMessage(new HttpMethod("PATCH"), p_URL) - { - Content = p_Content - }; - - HttpResponseMessage l_Reply = null; - for (int l_Retry = 0; l_Retry < MaxRetry; l_Retry++) - { - if (p_Token.IsCancellationRequested) - p_Token.ThrowIfCancellationRequested(); - - try - { - l_Reply = await m_Client.SendAsync(l_Request, p_Token).ConfigureAwait(false); - - if (p_DontRetry || l_Reply.IsSuccessStatusCode || l_Reply.StatusCode == HttpStatusCode.NotFound || l_Reply.StatusCode == HttpStatusCode.BadRequest) - { - /// Read reply - var l_Buffer = await l_Reply.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - var l_ResponseBuffer = Encoding.UTF8.GetString(l_Buffer, 0, l_Buffer.Length); - - return new APIResponse(l_Reply, l_Buffer, l_ResponseBuffer); - } - } - catch (System.Exception) - { - /// Do nothing here - } - - if (p_Token.IsCancellationRequested) - p_Token.ThrowIfCancellationRequested(); - - if (l_Reply != null) - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][APIClient.PatchAsync] Request {SafeURL(p_URL)} failed with code {l_Reply.StatusCode}:\"{l_Reply.ReasonPhrase}\", next try in 5 seconds..."); - else - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][APIClient.PatchAsync] Request {SafeURL(p_URL)} failed, next try in 5 seconds..."); - - /// Short exit - if (p_DontRetry) - return null; - - /// Wait 5 seconds - await Task.Delay(RetryInterval).ConfigureAwait(false); - } - - return null; - } - /// - /// Do Async delete query - /// - /// Request content - /// HTTPResponse - public async Task DeleteAsync(string p_URL, CancellationToken p_Token, bool p_DontRetry = false) - { - p_Token.ThrowIfCancellationRequested(); - - HttpResponseMessage l_Reply = null; - for (int l_Retry = 0; l_Retry < MaxRetry; l_Retry++) - { - if (p_Token.IsCancellationRequested) - p_Token.ThrowIfCancellationRequested(); - - try - { - l_Reply = await m_Client.DeleteAsync(p_URL, p_Token).ConfigureAwait(false); - - if (p_DontRetry || l_Reply.IsSuccessStatusCode || l_Reply.StatusCode == HttpStatusCode.NotFound || l_Reply.StatusCode == HttpStatusCode.BadRequest) - { - /// Read reply - var l_Buffer = await l_Reply.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - var l_ResponseBuffer = Encoding.UTF8.GetString(l_Buffer, 0, l_Buffer.Length); - - return new APIResponse(l_Reply, l_Buffer, l_ResponseBuffer); - } - } - catch (System.Exception) - { - /// Do nothing here - } - - if (p_Token.IsCancellationRequested) - p_Token.ThrowIfCancellationRequested(); - - if (l_Reply != null) - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][APIClient.DeleteAsync] Request {SafeURL(p_URL)} failed with code {l_Reply.StatusCode}:\"{l_Reply.ReasonPhrase}\", next try in 5 seconds..."); - else - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][APIClient.DeleteAsync] Request {SafeURL(p_URL)} failed, next try in 5 seconds..."); - - /// Short exit - if (p_DontRetry) - return null; - - /// Wait 5 seconds - await Task.Delay(RetryInterval).ConfigureAwait(false); - } - - return null; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Safe URL parsing - /// - /// - /// - private string SafeURL(string p_URL) - { - var l_Result = p_URL; - - if (!p_URL.ToLower().StartsWith("http")) - l_Result = m_Client.BaseAddress + l_Result; - - if (l_Result.Contains("?")) - l_Result = l_Result.Substring(0, l_Result.IndexOf("?")); - - return l_Result; - } - } -} diff --git a/BeatSaberPlus/CP_SDK/Network/APIResponse.cs b/BeatSaberPlus/CP_SDK/Network/APIResponse.cs deleted file mode 100644 index a24bd0b..0000000 --- a/BeatSaberPlus/CP_SDK/Network/APIResponse.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Net; -using System.Net.Http; - -namespace CP_SDK.Network -{ - /// - /// API Response class - /// - public class APIResponse - { - /// - /// Result code - /// - public readonly HttpStatusCode StatusCode; - /// - /// Reason phrase - /// - public readonly string ReasonPhrase; - /// - /// Is success - /// - public readonly bool IsSuccessStatusCode; - /// - /// Response bytes - /// - public readonly byte[] BodyBytes; - /// - /// Response string - /// - public readonly string BodyString; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - /// Reply status - /// Reply body - public APIResponse(HttpResponseMessage p_Reply, byte[] p_BodyBytes, string p_BodyString) - { - StatusCode = p_Reply.StatusCode; - ReasonPhrase = p_Reply.ReasonPhrase; - IsSuccessStatusCode = p_Reply.IsSuccessStatusCode; - BodyBytes = p_BodyBytes; - BodyString = p_BodyString; -#if DEBUG - ChatPlexSDK.Logger.Debug("[CP_SDK.Network][APIResponse.APIResponse] Result " + p_Reply.RequestMessage.RequestUri.ToString() + " - " + StatusCode); - /*foreach (var l_Header in p_Reply.RequestMessage.Headers) - { - ChatPlexSDK.Logger.Debug(l_Header.Key); - foreach (var l_Value in l_Header.Value) - ChatPlexSDK.Logger.Debug(" " + l_Value); - }*/ -#endif - - p_Reply.Dispose(); - } - } -} diff --git a/BeatSaberPlus/CP_SDK/Network/IWebClient.cs b/BeatSaberPlus/CP_SDK/Network/IWebClient.cs new file mode 100644 index 0000000..e5878b3 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Network/IWebClient.cs @@ -0,0 +1,51 @@ +using System; +using System.Net.Http; +using System.Threading; + +namespace CP_SDK.Network +{ + /// + /// Web Client interface + /// + public interface IWebClient + { + /// + /// Do Async GET query + /// + /// Target URL + /// Cancellation token + /// Callback + /// Should not retry + /// Progress reporter + void GetAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false, IProgress p_Progress = null); + void DownloadAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false, IProgress p_Progress = null); + /// + /// Do Async POST query + /// + /// Target URL + /// Optional content to post + /// Content type + /// Cancellation token + /// Callback + /// Should not retry + void PostAsync(string p_URL, HttpContent p_Content, string p_ContentType, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false); + /// + /// Do Async PATCH query + /// + /// Target URL + /// Optional content to post + /// Content type + /// Cancellation token + /// Callback + /// Should not retry + void PatchAsync(string p_URL, HttpContent p_Content, string p_ContentType, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false); + /// + /// Do Async DELETE query + /// + /// Target URL + /// Cancellation token + /// Callback + /// Should not retry + void DeleteAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false); + } +} diff --git a/BeatSaberPlus/CP_SDK/Network/JsonRPCClient.cs b/BeatSaberPlus/CP_SDK/Network/JsonRPCClient.cs new file mode 100644 index 0000000..734e6cf --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Network/JsonRPCClient.cs @@ -0,0 +1,120 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Net.Http; +using System.Threading; + +namespace CP_SDK.Network +{ + /// + /// JsonRPCClient + /// + public sealed class JsonRPCClient + { + /// + /// WebClient + /// + private IWebClient m_WebClient = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// Web client instance + public JsonRPCClient(IWebClient p_WebClient) + => m_WebClient = p_WebClient; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Do a RPC request + /// + /// Target method + /// Request parameters + /// Cancellation token + /// Callback + /// Should not retry? + /// + public void RequestAsync(string p_Method, object[] p_Parameters, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) + { + var l_Content = new JObject() + { + ["id"] = 1, + ["jsonrpc"] = "2.0", + ["method"] = p_Method, + ["params"] = new JArray(p_Parameters) + + }.ToString(); + + DoRequestAsync(p_Method, l_Content, p_Token, p_Callback, p_DontRetry); + } + /// + /// Do a RPC request + /// + /// Target method + /// Request parameters + /// Cancellation token + /// Callback + /// Should not retry? + /// + public void RequestAsync(string p_Method, JObject p_Parameters, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) + { + var l_Content = new JObject() + { + ["id"] = 1, + ["jsonrpc"] = "2.0", + ["method"] = p_Method, + ["params"] = p_Parameters + + }.ToString(); + + DoRequestAsync(p_Method, l_Content, p_Token, p_Callback, p_DontRetry); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Do a RPC request + /// + /// Target method + /// Query content + /// Cancellation token + /// Callback + /// Should not retry? + /// + private void DoRequestAsync(string p_Method, string p_Content, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) + { + m_WebClient.PostAsync("", new StringContent(p_Content), "application/json", p_Token, (p_WebResponse) => + { + if (p_WebResponse == null) + { + p_Callback?.Invoke(new JsonRPCResult() { RawResponse = p_WebResponse, Result = null }); + return; + } + + try + { + var l_JsonResult = JObject.Parse(p_WebResponse.BodyString); + + p_Callback?.Invoke(new JsonRPCResult() + { + RawResponse = p_WebResponse, + Result = (l_JsonResult.GetValue("result") ?? null) as JObject, + Error = (l_JsonResult.GetValue("error") ?? null) as JObject + }); + } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.Network][JsonRPCClient.DoRequestAsync] Request {p_Method} failed parsing response:"); + ChatPlexSDK.Logger.Error(l_Exception.ToString()); + + p_Callback?.Invoke(null); + } + + }, p_DontRetry); + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Network/JsonRPCResult.cs b/BeatSaberPlus/CP_SDK/Network/JsonRPCResult.cs new file mode 100644 index 0000000..685b616 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Network/JsonRPCResult.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json.Linq; + +namespace CP_SDK.Network +{ + /// + /// JsonRPCResult + /// + public sealed class JsonRPCResult + { + public WebResponse RawResponse; + public JObject Result; + public JObject Error; + } +} diff --git a/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetManager.cs b/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetManager.cs index 5fb0c04..b91479b 100644 --- a/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetManager.cs +++ b/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetManager.cs @@ -828,7 +828,7 @@ internal void OnMessageReceived(NetPacket packet, SocketError errorCode, IPEndPo //ProcessEvents DataReceived(packet, remoteEndPoint); } - catch(Exception e) + catch(Exception) { //protects socket receive thread //NetDebug.WriteError("[NM] SocketReceiveThread error: " + e ); @@ -987,7 +987,7 @@ private void DataReceived(NetPacket packet, IPEndPoint remoteEndPoint) { ntpPacket.ValidateReply(); } - catch (InvalidOperationException ex) + catch (InvalidOperationException) { //NetDebug.Write(NetLogLevel.Trace, "NTP response error: {}", ex.Message); ntpPacket = null; diff --git a/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetSocket.cs b/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetSocket.cs index fc071f4..41b0189 100644 --- a/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetSocket.cs +++ b/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetSocket.cs @@ -352,13 +352,13 @@ private bool BindSocket(Socket socket, IPEndPoint ep, bool reuseAddress, IPv6Mod if(!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) #endif try { socket.DontFragment = true; } - catch (SocketException e) + catch (SocketException) { //NetDebug.WriteError("[B]DontFragment error: {0}", e.SocketErrorCode); } try { socket.EnableBroadcast = true; } - catch (SocketException e) + catch (SocketException) { //NetDebug.WriteError("[B]Broadcast error: {0}", e.SocketErrorCode); } @@ -372,7 +372,7 @@ private bool BindSocket(Socket socket, IPEndPoint ep, bool reuseAddress, IPv6Mod //Disable IPv6 only mode socket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, false); } - catch(Exception e) + catch(Exception) { //NetDebug.WriteError("[B]Bind exception (dualmode setting): {0}", e.ToString()); } @@ -418,7 +418,7 @@ private bool BindSocket(Socket socket, IPEndPoint ep, bool reuseAddress, IPv6Mod socket.Bind(ep); } #if UNITY_2018_3_OR_NEWER - catch (SocketException ex) + catch (SocketException ) { //because its fixed in 2018_3 @@ -467,7 +467,7 @@ public bool SendBroadcast(byte[] data, int offset, int size, int port) new IPEndPoint(MulticastAddressV6, port)) > 0; } } - catch (Exception ex) + catch (Exception) { //NetDebug.WriteError("[S][MCAST]" + ex); return broadcastSuccess; @@ -504,7 +504,7 @@ public int SendTo(byte[] data, int offset, int size, IPEndPoint remoteEndPoint, errorCode = ex.SocketErrorCode; return -1; } - catch (Exception ex) + catch (Exception) { //NetDebug.WriteError("[S]" + ex); return -1; diff --git a/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetUtils.cs b/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetUtils.cs index 0c20a0c..6278608 100644 --- a/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetUtils.cs +++ b/BeatSaberPlus/CP_SDK/Network/LiteNetLib/NetUtils.cs @@ -183,7 +183,7 @@ internal static void PrintInterfaceInfos() } } } - catch (Exception e) + catch (Exception) { //NetDebug.WriteForce(NetLogLevel.Info, "Error while getting interface infos: {0}", e.ToString()); } diff --git a/BeatSaberPlus/CP_SDK/Network/RateLimitInfo.cs b/BeatSaberPlus/CP_SDK/Network/RateLimitInfo.cs index a75cddf..1851033 100644 --- a/BeatSaberPlus/CP_SDK/Network/RateLimitInfo.cs +++ b/BeatSaberPlus/CP_SDK/Network/RateLimitInfo.cs @@ -1,64 +1,194 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; +#if CP_SDK_UNITY +using UnityEngine.Networking; +#endif namespace CP_SDK.Network { /// /// Rate Limit Info /// - public class RateLimitInfo + public sealed class RateLimitInfo { + /// + /// Total allowed requests for a given time window + /// + public int Limit { get; private set; } /// /// Number of requests remaining /// public int Remaining { get; private set; } /// - /// Time at which rate limit bucket resets + /// Time at which rate limit window resets /// public DateTime Reset { get; private set; } - /// - /// Total allowed requests for a given bucket window - /// - public int Total { get; private set; } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - public static RateLimitInfo FromHttp(HttpResponseMessage p_Response) + /// + /// Get RateLimitInfo from HttpResponseMessage + /// + /// Response + /// + /// + public static RateLimitInfo Get(HttpResponseMessage p_Response) { if (p_Response == null) throw new ArgumentNullException(nameof(p_Response)); - if (!p_Response.Headers.TryGetValues("Rate-Limit-Remaining", out var l_Remainings)) - return null; - string l_RemainingStr = l_Remainings.FirstOrDefault(); - if (l_RemainingStr == null) - return null; - if (!int.TryParse(l_RemainingStr, out var l_Remaining)) - return null; - - if (!p_Response.Headers.TryGetValues("Rate-Limit-Total", out var l_Totals)) - return null; - string l_TotalStr = l_Totals.FirstOrDefault(); - if (l_TotalStr == null) - return null; - if (!int.TryParse(l_TotalStr, out var l_Total)) - return null; - - if (!p_Response.Headers.TryGetValues("Rate-Limit-Reset", out var l_Resets)) - return null; - string l_ResetStr = l_Resets.FirstOrDefault(); - if (l_ResetStr == null) - return null; - - DateTime l_Reset = new DateTime(); - if (!ulong.TryParse(l_ResetStr, out var l_ResetVal)) - return null; - l_Reset = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - l_Reset = l_Reset.AddSeconds((double)l_ResetVal).ToLocalTime(); - - return new RateLimitInfo() { Remaining = l_Remaining, Reset = l_Reset, Total = l_Total }; + var l_Headers = GetTransformedHeaders(p_Response); + + return new RateLimitInfo() + { + Limit = GetLimit(l_Headers), + Remaining = GetRemaining(l_Headers), + Reset = GetReset(l_Headers), + }; + } +#if CP_SDK_UNITY + /// + /// Get RateLimitInfo from UnityWebRequest + /// + /// Request + /// + /// + public static RateLimitInfo Get(UnityWebRequest p_Request) + { + if (p_Request == null) + throw new ArgumentNullException(nameof(p_Request)); + + var l_Headers = GetTransformedHeaders(p_Request); + + return new RateLimitInfo() { + Limit = GetLimit(l_Headers), + Remaining = GetRemaining(l_Headers), + Reset = GetReset(l_Headers), + }; + } +#endif + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get transformed headers from a HttpResponseMessage + /// + /// Response + /// + private static Dictionary GetTransformedHeaders(HttpResponseMessage p_Response) + { + var l_Result = new Dictionary(); + + foreach (var l_KVP in p_Response.Headers) + { + if (l_KVP.Value.FirstOrDefault().Contains(",")) + l_Result.Add(l_KVP.Key, l_KVP.Value.FirstOrDefault().Split(',').FirstOrDefault()?.Trim()); + else + l_Result.Add(l_KVP.Key, l_KVP.Value.FirstOrDefault().Trim()); + } + + return l_Result; + } +#if CP_SDK_UNITY + /// + /// Get transformed headers from a UnityWebRequest + /// + /// Request + /// + private static Dictionary GetTransformedHeaders(UnityWebRequest p_Request) + { + var l_Result = new Dictionary(); + var l_BaseHeaders = p_Request.GetResponseHeaders(); + + foreach (var l_KVP in l_BaseHeaders) + { + if (l_KVP.Value.Contains(",")) + l_Result.Add(l_KVP.Key, l_KVP.Value.Split(',').FirstOrDefault()?.Trim()); + else + l_Result.Add(l_KVP.Key, l_KVP.Value.Trim()); + } + + return l_Result; + } +#endif + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get limit value from header + /// + /// Transformed headers + /// + private static int GetLimit(Dictionary p_TransformedHeaders) + { + foreach (var l_KVP in p_TransformedHeaders) + { + var l_Lower = l_KVP.Key.ToLower(); + if ( l_Lower == "x-rate-limit-limit" || l_Lower == "x-ratelimit-limit" + || l_Lower == "rate-limit-limit" || l_Lower == "ratelimit-limit" + || l_Lower == "x-rate-limit-total" || l_Lower == "x-ratelimit-total" + || l_Lower == "rate-limit-total" || l_Lower == "ratelimit-total") + { + if (int.TryParse(l_KVP.Value, out var l_Value)) + return l_Value; + else + return -1; + } + } + + return -1; + } + /// + /// Get remaining value from header + /// + /// Transformed headers + /// + private static int GetRemaining(Dictionary p_TransformedHeaders) + { + foreach (var l_KVP in p_TransformedHeaders) + { + var l_Lower = l_KVP.Key.ToLower(); + if ( l_Lower == "x-rate-limit-remaining" || l_Lower == "x-ratelimit-remaining" + || l_Lower == "rate-limit-remaining" || l_Lower == "ratelimit-remaining") + { + if (int.TryParse(l_KVP.Value, out var l_Value)) + return l_Value; + else + return -1; + } + } + + return -1; + } + /// + /// Get reset time from header + /// + /// Transformed headers + /// + private static DateTime GetReset(Dictionary p_TransformedHeaders) + { + foreach (var l_KVP in p_TransformedHeaders) + { + var l_Lower = l_KVP.Key.ToLower(); + if ( l_Lower == "x-rate-limit-reset" || l_Lower == "x-ratelimit-reset" + || l_Lower == "rate-limit-reset" || l_Lower == "ratelimit-reset") + { + if (!long.TryParse(l_KVP.Value, out var l_Value)) + return DateTime.Now.AddSeconds(2); + + if (l_Value < 1000000000) + return Misc.Time.FromUnixTime(Misc.Time.UnixTimeNow() + l_Value); + + return Misc.Time.FromUnixTime(l_Value); + } + } + + return DateTime.Now.AddSeconds(2); } } } diff --git a/BeatSaberPlus/CP_SDK/Network/WebClient.cs b/BeatSaberPlus/CP_SDK/Network/WebClient.cs new file mode 100644 index 0000000..dfc57be --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Network/WebClient.cs @@ -0,0 +1,290 @@ +#if CP_SDK_UNITY +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using UnityEngine; +using UnityEngine.Networking; + +namespace CP_SDK.Network +{ + /// + /// WebClient using unity web requests + /// + public sealed class WebClient : IWebClient + { + /// + /// Global client instance + /// + public static readonly WebClient GlobalClient = new WebClient("", TimeSpan.FromSeconds(10)); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Base URL + /// + private string m_BaseAddress = string.Empty; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Timeout seconds + /// + public int Timeout = 10; + /// + /// Timeout seconds + /// + public int DownloadTimeout = 2 * 60; + /// + /// Maximum retry attempt + /// + public int MaxRetry = 2; + /// + /// Delay between each retry + /// + public int RetryInterval = 5; + /// + /// Headers + /// + public Dictionary Headers = new Dictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// + /// + /// + public WebClient(string p_BaseAddress, TimeSpan p_TimeOut, bool p_ForceCacheDiscard = false) + { + m_BaseAddress = p_BaseAddress; + + Timeout = (int)p_TimeOut.TotalSeconds; + + if (p_ForceCacheDiscard) + Headers.Add("Cache-Control", "no-cache, must-revalidate, proxy-revalidate, max-age=0, s-maxage=0, max-stale=0"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Do Async GET query + /// + /// Target URL + /// Cancellation token + /// Callback + /// Should not retry + /// Progress reporter + public void GetAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false, IProgress p_Progress = null) + => Unity.MTCoroutineStarter.EnqueueFromThread(Coroutine_DoRequest("GetAsync", "GET", GetURL(p_URL), null, null, p_Token, p_Callback, p_DontRetry, p_Progress)); + public void DownloadAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false, IProgress p_Progress = null) + => Unity.MTCoroutineStarter.EnqueueFromThread(Coroutine_DoRequest("DownloadAsync", "DOWNLOAD", GetURL(p_URL), null, null, p_Token, p_Callback, p_DontRetry, p_Progress)); + /// + /// Do Async POST query + /// + /// Target URL + /// Optional content to post + /// Content type + /// Cancellation token + /// Callback + /// Should not retry + public void PostAsync(string p_URL, HttpContent p_Content, string p_ContentType, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) + => Unity.MTCoroutineStarter.EnqueueFromThread(Coroutine_DoRequest("PostAsync", "POST", GetURL(p_URL), p_Content, p_ContentType, p_Token, p_Callback, p_DontRetry, null)); + /// + /// Do Async PATCH query + /// + /// Target URL + /// Optional content to post + /// Content type + /// Cancellation token + /// Callback + /// Should not retry + public void PatchAsync(string p_URL, HttpContent p_Content, string p_ContentType, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) + => Unity.MTCoroutineStarter.EnqueueFromThread(Coroutine_DoRequest("PatchAsync", "PATCH", GetURL(p_URL), p_Content, p_ContentType, p_Token, p_Callback, p_DontRetry, null)); + /// + /// Do Async DELETE query + /// + /// Target URL + /// Cancellation token + /// Callback + /// Should not retry + public void DeleteAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) + => Unity.MTCoroutineStarter.EnqueueFromThread(Coroutine_DoRequest("DeleteAsync", "DELETE", GetURL(p_URL), null, null, p_Token, p_Callback, p_DontRetry, null)); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get URL + /// + /// Request URL + /// + private string GetURL(string p_URL) + { + if (string.IsNullOrEmpty(m_BaseAddress)) return p_URL; + if (p_URL.Contains("://")) return p_URL; + if (m_BaseAddress.EndsWith("/")) return m_BaseAddress + p_URL; + + return m_BaseAddress + "/" + p_URL; + } + /// + /// Safe URL parsing + /// + /// Source URL + /// + private string SafeURL(string p_URL) + { + var l_Result = p_URL; + if (l_Result.Contains("?")) + l_Result = l_Result.Substring(0, l_Result.IndexOf("?")); + + return l_Result; + } + /// + /// Prepare request + /// + /// Request to prepare + private void PrepareRequest(UnityWebRequest p_Request, bool p_IsDownload) + { + if (p_Request.downloadHandler == null) + p_Request.downloadHandler = new DownloadHandlerBuffer(); + + p_Request.timeout = p_IsDownload ? DownloadTimeout : Timeout; + + foreach (var l_KVP in Headers) + p_Request.SetRequestHeader(l_KVP.Key, l_KVP.Value); + } + /// + /// Do request + /// + /// Method name for logs + /// Http method + /// Target URL + /// Optional content to post + /// Content type + /// Cancellation token + /// Callback + /// Should not retry + /// Progress reporter + /// + private IEnumerator Coroutine_DoRequest(string p_DebugName, + string p_HttpMethod, + string p_URL, + HttpContent p_Content, + string p_ContentType, + CancellationToken p_Token, + Action p_Callback, + bool p_DontRetry, + IProgress p_Progress) + { +#if DEBUG + ChatPlexSDK.Logger.Debug($"[CP_SDK.Network][WebClient.{p_DebugName}] {p_HttpMethod} " + p_URL); +#endif + + var l_Reply = null as WebResponse; + for (int l_RetryI = 1; l_RetryI <= MaxRetry; l_RetryI++) + { + if (p_Token.IsCancellationRequested) + break; + + var l_Request = null as UnityWebRequest; + switch (p_HttpMethod) + { + case "GET": + case "DOWNLOAD": + l_Request = UnityWebRequest.Get(p_URL); + break; + + case "POST": + case "PATCH": + l_Request = new UnityWebRequest(p_URL, p_HttpMethod) + { + uploadHandler = new UploadHandlerRaw(p_Content.ReadAsByteArrayAsync().Result) + { + contentType = p_ContentType + }, + downloadHandler = new DownloadHandlerBuffer() + }; + break; + + case "DELETE": + l_Request = UnityWebRequest.Delete(p_URL); + break; + } + + PrepareRequest(l_Request, p_HttpMethod == "DOWNLOAD"); + + if (p_Progress == null) + yield return l_Request.SendWebRequest(); + else + { + try { p_Progress?.Report(0f); } catch { } + l_Request.SendWebRequest(); + + var l_Waiter = new WaitForSecondsRealtime(0.05f); + do + { + yield return l_Waiter; + try { p_Progress?.Report(l_Request.downloadProgress); } catch { } + + if (p_Token.IsCancellationRequested || l_Request.isDone || l_Request.isHttpError || l_Request.isNetworkError) + break; + } while (true); + } + + if (p_Token.IsCancellationRequested) + break; + + l_Reply = new WebResponse(l_Request); + + if (!l_Reply.IsSuccessStatusCode && l_Reply.StatusCode == (HttpStatusCode)429) + { + var l_Limits = RateLimitInfo.Get(l_Request); + if (l_Limits != null) + { + int l_TotalMilliseconds = (int)(l_Limits.Reset - DateTime.Now).TotalMilliseconds; + if (l_TotalMilliseconds > 0) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClient.{p_DebugName}] Request {SafeURL(p_URL)} was rate limited, retrying in {l_TotalMilliseconds}ms..."); + + yield return new WaitForSecondsRealtime(RetryInterval); + continue; + } + } + } + + if (!l_Reply.IsSuccessStatusCode) + { + if (!l_Reply.ShouldRetry || p_DontRetry) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClient.{p_DebugName}] Request {SafeURL(p_URL)} failed with code {l_Request.responseCode}:\"{l_Request.error}\", not retrying"); + break; + } + + ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClient.{p_DebugName}] Request {SafeURL(p_URL)} failed with code {l_Reply.StatusCode}:\"{l_Reply.ReasonPhrase}\", next try in {RetryInterval} seconds..."); + + yield return new WaitForSecondsRealtime(RetryInterval); + continue; + } + else + { + if (p_Progress != null) + try { p_Progress?.Report(1f); } catch { } + + break; + } + } + + if (!p_Token.IsCancellationRequested) + Unity.MTThreadInvoker.EnqueueOnThread(() => p_Callback?.Invoke(l_Reply)); + } + } +} +#endif \ No newline at end of file diff --git a/BeatSaberPlus/CP_SDK/Network/WebClientEx.cs b/BeatSaberPlus/CP_SDK/Network/WebClientEx.cs new file mode 100644 index 0000000..c544a4f --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Network/WebClientEx.cs @@ -0,0 +1,285 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace CP_SDK.Network +{ + /// + /// WebClient client class + /// + public sealed class WebClientEx : IWebClient + { + /// + /// Global client instance + /// + public static readonly WebClientEx GlobalClient = new WebClientEx("", TimeSpan.FromSeconds(10), true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// API client + /// + private HttpClient m_Client = null; + /// + /// Cookie container + /// + private CookieContainer m_CookieContainer = new CookieContainer(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Maximum retry attempt + /// + public int MaxRetry = 5; + /// + /// Delay between each retry + /// + public int RetryInterval = 5; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Client public accessor + /// + public HttpClient InternalClient => m_Client; + /// + /// Cookies container + /// + public CookieContainer Cookies => m_CookieContainer; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// Base address + /// Maximum timeout + /// Should keep alive the connection + /// Should force cache discard + public WebClientEx(string p_BaseAddress, TimeSpan p_TimeOut, bool p_KeepAlive = true, bool p_ForceCacheDiscard = false) + { + HttpClientHandler l_Handler = new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + CookieContainer = m_CookieContainer + }; + + m_Client = new HttpClient(l_Handler) + { + Timeout = p_TimeOut, + }; + + if (!string.IsNullOrEmpty(p_BaseAddress)) + m_Client.BaseAddress = new Uri(p_BaseAddress); + + if (p_ForceCacheDiscard) + { + m_Client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue + { + NoCache = true, + NoStore = false, + MustRevalidate = true, + ProxyRevalidate = true, + MaxAge = TimeSpan.FromSeconds(0), + SharedMaxAge = TimeSpan.FromMilliseconds(0), + MaxStaleLimit = TimeSpan.FromMilliseconds(0) + }; + } + + m_Client.DefaultRequestHeaders.ConnectionClose = !p_KeepAlive; + m_Client.DefaultRequestHeaders.Add("User-Agent", "ChatPlexAPISDK_ApiClient/6.0.3"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Do Async GET query + /// + /// Target URL + /// Cancellation token + /// Callback + /// Should not retry + /// Progress reporter + public async void GetAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false, IProgress p_Progress = null) + => await DoRequest("GetAsync", "GET", p_URL, null, null, p_Token, p_Callback, p_DontRetry, p_Progress).ConfigureAwait(false); + public async void DownloadAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false, IProgress p_Progress = null) + => await DoRequest("DownloadAsync", "GET", p_URL, null, null, p_Token, p_Callback, p_DontRetry, p_Progress).ConfigureAwait(false); + /// + /// Do Async POST query + /// + /// Target URL + /// Optional content to post + /// Content type + /// Cancellation token + /// Callback + /// Should not retry + public async void PostAsync(string p_URL, HttpContent p_Content, string p_ContentType, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) + => await DoRequest("PostAsync", "POST", p_URL, p_Content, p_ContentType, p_Token, p_Callback, p_DontRetry, null).ConfigureAwait(false); + /// + /// Do Async PATCH query + /// + /// Target URL + /// Optional content to post + /// Content type + /// Cancellation token + /// Callback + /// Should not retry + public async void PatchAsync(string p_URL, HttpContent p_Content, string p_ContentType, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) + => await DoRequest("PatchAsync", "PATCH", p_URL, p_Content, p_ContentType, p_Token, p_Callback, p_DontRetry, null).ConfigureAwait(false); + /// + /// Do Async DELETE query + /// + /// Target URL + /// Cancellation token + /// Callback + /// Should not retry + public async void DeleteAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) + => await DoRequest("DeleteAsync", "DELETE", p_URL, null, null, p_Token, p_Callback, p_DontRetry, null).ConfigureAwait(false); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Safe URL parsing + /// + /// + /// + private string SafeURL(string p_URL) + { + var l_Result = p_URL; + + if (!p_URL.Contains("://")) + l_Result = m_Client.BaseAddress + l_Result; + + if (l_Result.Contains("?")) + l_Result = l_Result.Substring(0, l_Result.IndexOf("?")); + + return l_Result; + } + /// + /// Do request + /// + /// Method name for logs + /// Http method + /// Target URL + /// Optional content to post + /// Content type + /// Cancellation token + /// Callback + /// Should not retry + /// Progress reporter + /// + private async Task DoRequest( string p_DebugName, + string p_HttpMethod, + string p_URL, + HttpContent p_Content, + string p_ContentType, + CancellationToken p_Token, + Action p_Callback, + bool p_DontRetry, + IProgress p_Progress) + { +#if DEBUG + ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClientEx.{p_DebugName}] {p_HttpMethod} " + p_URL); +#endif + + var l_Reply = null as WebResponse; + for (int l_Retry = 0; l_Retry < MaxRetry; l_Retry++) + { + if (p_Token.IsCancellationRequested) + break; + + var l_Response = null as HttpResponseMessage; + try + { + l_Reply = null; + switch (p_HttpMethod) + { + case "GET": + l_Response = await m_Client.GetAsync(p_URL, p_Token).ConfigureAwait(false); + break; + + case "POST": + p_Content.Headers.ContentType.MediaType = p_ContentType; + l_Response = await m_Client.PostAsync(p_URL, p_Content, p_Token).ConfigureAwait(false); + break; + + case "PATCH": + p_Content.Headers.ContentType.MediaType = p_ContentType; + l_Response = await m_Client.SendAsync(new HttpRequestMessage(new HttpMethod("PATCH"), p_URL) { Content = p_Content, }, p_Token).ConfigureAwait(false); + break; + + case "DELETE": + l_Response = await m_Client.DeleteAsync(p_URL, p_Token).ConfigureAwait(false); + break; + } + + if (p_Token.IsCancellationRequested) + break; + + l_Reply = new WebResponse(l_Response); + + if (!l_Reply.IsSuccessStatusCode && l_Reply.StatusCode == (HttpStatusCode)429) + { + var l_Limits = RateLimitInfo.Get(l_Response); + if (l_Limits != null) + { + int l_TotalMilliseconds = (int)(l_Limits.Reset - DateTime.Now).TotalMilliseconds; + if (l_TotalMilliseconds > 0) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClientEx.{p_DebugName}] Request {SafeURL(p_URL)} was rate limited, retrying in {l_TotalMilliseconds}ms..."); + + await Task.Delay(l_TotalMilliseconds).ConfigureAwait(false); + continue; + } + } + } + + if ((l_Reply.IsSuccessStatusCode || (!l_Reply.ShouldRetry || p_DontRetry))) + //&& (l_Response.Content.Headers.ContentLength > 0 || (l_Response.Content.Headers.ContentType != null && l_Response.Content.Headers.ContentLength == null))) + { + l_Reply.Populate(await l_Response.Content.ReadAsByteArrayAsync().ConfigureAwait(false)); + } + + if (!l_Reply.IsSuccessStatusCode) + { + if (!l_Reply.ShouldRetry || p_DontRetry) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClientEx.{p_DebugName}] Request {SafeURL(p_URL)} failed with code {l_Reply.StatusCode}:\"{l_Reply.ReasonPhrase}\", not retrying"); + break; + } + + ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClientEx.{p_DebugName}] Request {SafeURL(p_URL)} failed with code {l_Reply.StatusCode}:\"{l_Reply.ReasonPhrase}\", next try in {RetryInterval} seconds..."); + + await Task.Delay(RetryInterval * 1000).ConfigureAwait(false); + + continue; + } + + break; + } + catch (System.Exception) + { + /// Do nothing here + } + finally + { + l_Response?.Dispose(); + } + } + + if (!p_Token.IsCancellationRequested) + p_Callback?.Invoke(l_Reply); + + return true; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/Network/WebClient_Unity.cs b/BeatSaberPlus/CP_SDK/Network/WebClient_Unity.cs deleted file mode 100644 index 8cf4cd1..0000000 --- a/BeatSaberPlus/CP_SDK/Network/WebClient_Unity.cs +++ /dev/null @@ -1,207 +0,0 @@ -#if CP_SDK_UNITY -using System; -using System.Collections; -using System.Threading; -using UnityEngine; -using UnityEngine.Networking; - -namespace CP_SDK.Network -{ - /// - /// WebClient using unity web requests - /// - public class WebClient - { - /// - /// Global client instance - /// - public static readonly WebClient GlobalClient = new WebClient(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Timeout seconds - /// - public int Timeout = 10; - /// - /// Timeout seconds - /// - public int DownloadTimeout = 2 * 60; - /// - /// Maximum retry attempt - /// - public int MaxRetry = 2; - /// - /// Delay between each retry - /// - public float RetryInterval = 5f; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get async - /// - /// Request URL - /// Cancellation token - /// On result callbacks - /// Should not retry? - public void GetAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry = false) - { -#if DEBUG - ChatPlexSDK.Logger.Debug("[CP_SDK.Network][WebClient.GetAsync] GET " + p_URL); -#endif - - Unity.MTCoroutineStarter.EnqueueFromThread(Coroutine_GetAsync(p_URL, p_Token, p_Callback, p_DontRetry)); - } - /// - /// Download async - /// - /// Request URL - /// Cancellation token - /// On result callbacks - /// Progress reporter - /// Should not retry? - public void DownloadAsync(string p_URL, CancellationToken p_Token, Action p_Callback, IProgress p_Progress = null, bool p_DontRetry = false) - { -#if DEBUG - ChatPlexSDK.Logger.Debug("[CP_SDK.Network][WebClient.DownloadAsync] GET " + p_URL); -#endif - - Unity.MTCoroutineStarter.EnqueueFromThread(Coroutine_DownloadAsync(p_URL, p_Token, p_Callback, p_Progress, p_DontRetry)); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get async - /// - /// Request URL - /// Cancellation token - /// On result callbacks - /// Should not retry? - private IEnumerator Coroutine_GetAsync(string p_URL, CancellationToken p_Token, Action p_Callback, bool p_DontRetry) - { - var l_Reply = null as WebResponse; - for (int l_RetryI = 1; l_RetryI <= MaxRetry; l_RetryI++) - { - if (p_Token.IsCancellationRequested) - break; - - var l_Request = new UnityWebRequest(p_URL) - { - downloadHandler = new DownloadHandlerBuffer(), - timeout = Timeout - }; - - yield return l_Request.SendWebRequest(); - l_Reply = new WebResponse(l_Request); - - if (p_Token.IsCancellationRequested) - break; - - if (!l_Reply.IsSuccessStatusCode) - { - if (!l_Reply.ShouldRetry || p_DontRetry) - { - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClient.DownloadAsync] Request {SafeURL(p_URL)} failed with code {l_Request.responseCode}:\"{l_Request.error}\", not retrying"); - break; - } - - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClient.GetAsync] Request {SafeURL(p_URL)} failed with code {l_Reply.StatusCode}:\"{l_Reply.ReasonPhrase}\", next try in {RetryInterval} seconds..."); - - yield return new WaitForSecondsRealtime(RetryInterval); - - continue; - } - else - break; - } - - Unity.MTThreadInvoker.EnqueueOnThread(() => p_Callback(l_Reply)); - } - /// - /// Download async - /// - /// Request URL - /// Cancellation token - /// On result callbacks - /// Progress reporter - /// Should not retry? - private IEnumerator Coroutine_DownloadAsync(string p_URL, CancellationToken p_Token, Action p_Callback, IProgress p_Progress, bool p_DontRetry) - { - var l_Waiter = new WaitForSecondsRealtime(0.05f); - - for (int l_RetryI = 1; l_RetryI <= MaxRetry; l_RetryI++) - { - if (p_Token.IsCancellationRequested) - break; - - var l_Request = new UnityWebRequest(p_URL) - { - downloadHandler = new DownloadHandlerBuffer(), - timeout = DownloadTimeout - }; - - p_Progress?.Report(0f); - - l_Request.SendWebRequest(); - - do - { - yield return l_Waiter; - p_Progress?.Report(l_Request.downloadProgress); - - if (p_Token.IsCancellationRequested || l_Request.isDone || l_Request.isHttpError || l_Request.isNetworkError) - break; - } while (true); - - if (p_Token.IsCancellationRequested) - break; - - if (l_Request.isHttpError || l_Request.isNetworkError) - { - if (!(l_Request.responseCode < 400 || l_Request.responseCode >= 500) || p_DontRetry) - { - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClient.DownloadAsync] Request {SafeURL(p_URL)} failed with code {l_Request.responseCode}:\"{l_Request.error}\", not retrying"); - break; - } - - ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebClient.DownloadAsync] Request {SafeURL(p_URL)} failed with code {l_Request.responseCode}:\"{l_Request.error}\", next try in {RetryInterval} seconds..."); - - yield return new WaitForSecondsRealtime(RetryInterval); - - continue; - } - else - { - p_Progress?.Report(1f); - Unity.MTThreadInvoker.EnqueueOnThread(() => p_Callback(l_Request.downloadHandler.data)); - yield break; - } - } - - Unity.MTThreadInvoker.EnqueueOnThread(() => p_Callback?.Invoke(null)); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Safe URL parsing - /// - /// Source URL - /// - private string SafeURL(string p_URL) - { - var l_Result = p_URL; - if (l_Result.Contains("?")) - l_Result = l_Result.Substring(0, l_Result.IndexOf("?")); - - return l_Result; - } - } -} -#endif \ No newline at end of file diff --git a/BeatSaberPlus/CP_SDK/Network/WebResponse.cs b/BeatSaberPlus/CP_SDK/Network/WebResponse.cs index 955f3c5..bb7c9ed 100644 --- a/BeatSaberPlus/CP_SDK/Network/WebResponse.cs +++ b/BeatSaberPlus/CP_SDK/Network/WebResponse.cs @@ -1,15 +1,30 @@ -using System.Net; -#if CP_SDK_UNITY +using Newtonsoft.Json.Linq; +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; using UnityEngine.Networking; -#endif namespace CP_SDK.Network { /// /// Web Response class /// - public class WebResponse + public sealed class WebResponse { + /// + /// Response bytes + /// + private byte[] m_BodyBytes; + /// + /// Body string + /// + private string m_BodyString = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + /// /// Result code /// @@ -29,12 +44,35 @@ public class WebResponse /// /// Response bytes /// - public readonly string BodyString; + public byte[] BodyBytes => m_BodyBytes; + /// + /// Response string + /// + public string BodyString + { + get + { + if (m_BodyString == null) + { + if (m_BodyBytes?.Length > 0) + { + var l_Preamble = Encoding.UTF8.GetPreamble(); + if (l_Preamble?.Length > 0 && m_BodyBytes.Length >= l_Preamble.Length && m_BodyBytes.Take(l_Preamble.Length).SequenceEqual(l_Preamble)) + m_BodyString = Encoding.UTF8.GetString(m_BodyBytes, l_Preamble.Length, m_BodyBytes.Length - l_Preamble.Length); + else + m_BodyString = Encoding.UTF8.GetString(m_BodyBytes); + } + else + m_BodyString = string.Empty; + } + + return m_BodyString; + } + } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// -#if CP_SDK_UNITY /// /// Constructor /// @@ -45,8 +83,54 @@ public WebResponse(UnityWebRequest p_Request) ReasonPhrase = p_Request.error; IsSuccessStatusCode = !(p_Request.isHttpError || p_Request.isNetworkError); ShouldRetry = IsSuccessStatusCode ? false : (p_Request.responseCode < 400 || p_Request.responseCode >= 500); - BodyString = p_Request.downloadHandler.text; + + m_BodyBytes = p_Request.downloadHandler.data; + } + /// + /// Constructor + /// + /// Reply status + public WebResponse(HttpResponseMessage p_Response) + { + StatusCode = p_Response.StatusCode; + ReasonPhrase = p_Response.ReasonPhrase; + IsSuccessStatusCode = p_Response.IsSuccessStatusCode; + ShouldRetry = IsSuccessStatusCode ? false : ((int)p_Response.StatusCode < 400 || (int)p_Response.StatusCode >= 500); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Populate data + /// + /// Body bytes + internal void Populate(byte[] p_BodyBytes) + => m_BodyBytes = p_BodyBytes; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get JObject from serialized JSON + /// + /// Input + /// Result object + /// + public bool TryGetObject(out T p_JObject) + where T : class, new() + { + p_JObject = null; + try + { + p_JObject = JObject.Parse(BodyString).ToObject(); + } + catch (Exception) + { + return false; + } + + return p_JObject != null; } -#endif } } diff --git a/BeatSaberPlus/CP_SDK/Network/WebSocketClient.cs b/BeatSaberPlus/CP_SDK/Network/WebSocketClient.cs index 5e72021..7012477 100644 --- a/BeatSaberPlus/CP_SDK/Network/WebSocketClient.cs +++ b/BeatSaberPlus/CP_SDK/Network/WebSocketClient.cs @@ -46,6 +46,10 @@ public class WebSocketClient /// Reconnect lock semaphore /// private SemaphoreSlim m_ReconnectLock = new SemaphoreSlim(1, 1); + /// + /// Is disconnecting? + /// + private bool m_Disconnecting = false; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -93,6 +97,7 @@ public void Dispose() { if (m_Client != null) { + m_Disconnecting = true; if (IsConnected) { m_CancellationToken?.Cancel(); @@ -117,6 +122,8 @@ public void Connect(string p_URI) { Dispose(); + m_Disconnecting = false; + if (m_Client is null) { ChatPlexSDK.Logger.Debug($"[CP_SDK.Network][WebSocketClient.Connect] Connecting to {p_URI}"); @@ -164,15 +171,21 @@ public void Connect(string p_URI) } } } - catch (System.Exception) + catch (System.Exception l_Exception) { - + if (!m_Disconnecting) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.Network][WebSocketClient.Connect] An exception occurred in WebSocket while reading {m_URI}"); + ChatPlexSDK.Logger.Error(l_Exception); + } } Client_OnClose(this); } - else + else if (!m_Disconnecting) Client_OnError(this); + else + Client_OnClose(this); } catch (TaskCanceledException) { @@ -185,7 +198,8 @@ public void Connect(string p_URI) OnError?.Invoke(); - TryHandleReconnect(); + if (!m_Disconnecting) + TryHandleReconnect(); } }, m_CancellationToken.Token).ConfigureAwait(false); } @@ -198,6 +212,7 @@ public void Disconnect() { lock (m_LockObject) { + m_Disconnecting = true; ChatPlexSDK.Logger.Info("[CP_SDK.Network][WebSocketClient.Disconnect] Disconnecting"); Dispose(); } @@ -260,13 +275,15 @@ public async void TryHandleReconnect() m_Client.Dispose(); m_Client = null; - if (AutoReconnect && !m_CancellationToken.IsCancellationRequested) + if (AutoReconnect && !m_CancellationToken.IsCancellationRequested && !m_Disconnecting) { ChatPlexSDK.Logger.Info($"[CP_SDK.Network][WebSocketClient.TryHandleReconnect] Trying to reconnect to {m_URI} in {(int)TimeSpan.FromMilliseconds(ReconnectDelay).TotalSeconds} sec"); try { await Task.Delay(ReconnectDelay, m_CancellationToken.Token).ConfigureAwait(false); + if (m_Disconnecting) + return; Connect(m_URI); ReconnectDelay *= 2; @@ -304,7 +321,8 @@ private void Client_OnClose(object p_Sender) ChatPlexSDK.Logger.Debug($"[CP_SDK.Network][WebSocketClient.Client_OnClose] WebSocket connection to {m_URI} was closed"); OnClose?.Invoke(); - TryHandleReconnect(); + if (!m_Disconnecting) + TryHandleReconnect(); } /// /// On error @@ -316,7 +334,8 @@ private void Client_OnError(object p_Sender) OnError?.Invoke(); - TryHandleReconnect(); + if (!m_Disconnecting) + TryHandleReconnect(); } /// /// On message received diff --git a/BeatSaberPlus/CP_SDK/Pool/CollectionPool.cs b/BeatSaberPlus/CP_SDK/Pool/CollectionPool.cs new file mode 100644 index 0000000..ea3642b --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Pool/CollectionPool.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace CP_SDK.Pool +{ + /// + /// A Collection such as List, HashSet, Dictionary etc can be pooled and reused by using a CollectionPool. + /// + public class MTCollectionPool where TCollection : class, ICollection, new() + { + /// + /// Static collection + /// + internal static readonly MTObjectPool s_Pool = new MTObjectPool(() => new TCollection(), actionOnRelease: (x => x.Clear()), defaultCapacity: 100); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Simple get + /// + /// + public static TCollection Get() + => s_Pool.Get(); + /// + /// Managed object get + /// + /// Result value + /// + public static PooledObject Get(out TCollection p_Element) + => s_Pool.Get(out p_Element); + /// + /// Release an element + /// + /// Element to release + public static void Release(TCollection p_Element) + => s_Pool.Release(p_Element); + } +} \ No newline at end of file diff --git a/BeatSaberPlus/CP_SDK/Pool/ListPool.cs b/BeatSaberPlus/CP_SDK/Pool/ListPool.cs new file mode 100644 index 0000000..595ca10 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/Pool/ListPool.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace CP_SDK.Pool +{ + /// + /// A version of Pool.CollectionPool_2 for Lists. + /// + public class ListPool : CollectionPool, T> + { + + } +} \ No newline at end of file diff --git a/BeatSaberPlus/CP_SDK/Pool/MTCollectionPool.cs b/BeatSaberPlus/CP_SDK/Pool/MTCollectionPool.cs index ea3642b..232dada 100644 --- a/BeatSaberPlus/CP_SDK/Pool/MTCollectionPool.cs +++ b/BeatSaberPlus/CP_SDK/Pool/MTCollectionPool.cs @@ -5,12 +5,12 @@ namespace CP_SDK.Pool /// /// A Collection such as List, HashSet, Dictionary etc can be pooled and reused by using a CollectionPool. /// - public class MTCollectionPool where TCollection : class, ICollection, new() + public class CollectionPool where TCollection : class, ICollection, new() { /// /// Static collection /// - internal static readonly MTObjectPool s_Pool = new MTObjectPool(() => new TCollection(), actionOnRelease: (x => x.Clear()), defaultCapacity: 100); + internal static readonly ObjectPool s_Pool = new ObjectPool(() => new TCollection(), actionOnRelease: (x => x.Clear()), defaultCapacity: 100); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CColorInput.cs b/BeatSaberPlus/CP_SDK/UI/Components/CColorInput.cs new file mode 100644 index 0000000..40b6e9e --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CColorInput.cs @@ -0,0 +1,58 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// CColorInput component + /// + public abstract class CColorInput : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On value changed event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CColorInput OnValueChanged(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get value + /// + /// + public abstract Color GetValue(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set alpha support + /// + /// New state + /// + public abstract CColorInput SetAlphaSupport(bool p_Support); + /// + /// Set interactable state + /// + /// New state + /// + public abstract CColorInput SetInteractable(bool p_Interactable); + /// + /// Set value + /// + /// New value + /// Should notify? + /// + public abstract CColorInput SetValue(Color p_Value, bool p_Notify = true); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CDropdown.cs b/BeatSaberPlus/CP_SDK/UI/Components/CDropdown.cs new file mode 100644 index 0000000..a9a4998 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CDropdown.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// CDropdown component + /// + public abstract class CDropdown : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On value changed event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CDropdown OnValueChanged(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get value + /// + /// + public abstract string GetValue(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set interactable state + /// + /// New state + /// + public abstract CDropdown SetInteractable(bool p_Interactable); + /// + /// Set available options + /// + /// New options list + /// + public abstract CDropdown SetOptions(List p_Options); + /// + /// Set value + /// + /// Select option + /// Should notify? + /// + public abstract CDropdown SetValue(string p_Value, bool p_Notify = true); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CFLayout.cs b/BeatSaberPlus/CP_SDK/UI/Components/CFLayout.cs new file mode 100644 index 0000000..4ff7c94 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CFLayout.cs @@ -0,0 +1,106 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Flow layout group + /// + public abstract class CFLayout : LayoutGroup + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public enum EAxis + { + Horizontal, + Vertical + } + + public abstract EAxis StartAxis { get; set; } + public abstract bool ChildForceExpandWidth { get; set; } + public abstract bool ChildForceExpandHeight { get; set; } + public abstract bool ExpandHorizontalSpacing { get; set; } + public abstract float SpacingX { get; set; } + public abstract float SpacingY { get; set; } + public abstract bool InvertOrder { get; set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set preferred width + /// + /// Width + /// + public virtual CFLayout SetWidth(float p_Width) + { + RTransform.sizeDelta = new Vector2(p_Width, RTransform.sizeDelta.y); + LElement.preferredWidth = p_Width; + return this; + } + /// + /// Set preferred height + /// + /// Height + /// + public virtual CFLayout SetHeight(float p_Height) + { + RTransform.sizeDelta = new Vector2(RTransform.sizeDelta.x, p_Height); + LElement.preferredHeight = p_Height; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set min width + /// + /// Width + /// + public virtual CFLayout SetMinWidth(float p_Width) + { + LElement.minWidth = p_Width; + return this; + } + /// + /// Set min height + /// + /// Height + /// + public virtual CFLayout SetMinHeight(float p_Height) + { + LElement.minHeight = p_Height; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set child alignment + /// + /// New alignment + /// + public virtual CFLayout SetChildAlign(TextAnchor p_ChildAlign) + { + childAlignment = p_ChildAlign; + return this; + } + /// + /// Set spacing between elements + /// + /// New spacing + /// + public virtual CFLayout SetSpacing(Vector2 p_Spacing) + { + SpacingX = p_Spacing.x; + SpacingY = p_Spacing.y; + return this; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CFloatingPanel.cs b/BeatSaberPlus/CP_SDK/UI/Components/CFloatingPanel.cs new file mode 100644 index 0000000..d9ba2de --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CFloatingPanel.cs @@ -0,0 +1,519 @@ +using CP_SDK.Unity.Extensions; +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Floating Panel component + /// + public abstract class CFloatingPanel : IScreen + { + private IViewController m_CurrentViewController = null; + private bool m_AllowMovement = false; + private bool m_AutoLockOnSceneSwitch = true; + private bool m_AlignWithFloor = true; + private Dictionary m_SceneTransforms = new Dictionary(); + private Dictionary> m_OnSceneRelease = new Dictionary>(); + private Image m_Background = null; + private CIconButton m_LockIcon = null; + private CIconButton m_GearIcon = null; + + private event Action m_OnSceneRelocated; + private event Action m_OnSizeChanged; + private event Action m_OnGearIcon; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public enum ECorner + { + None, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IViewController CurrentViewController => m_CurrentViewController; + public abstract RectTransform RTransform { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Replace active view controller + /// + /// New view controller + public override void SetViewController(IViewController p_ViewController) + { + if (p_ViewController && p_ViewController.CurrentScreen == this) + return; + + if (m_CurrentViewController) + m_CurrentViewController.__Deactivate(); + + if (p_ViewController && p_ViewController.CurrentScreen) + p_ViewController.CurrentScreen.SetViewController(null); + + m_CurrentViewController = p_ViewController; + if (p_ViewController) p_ViewController.__Activate(this); + + if (m_Background) m_Background.transform.SetAsFirstSibling(); + if (m_LockIcon) m_LockIcon.transform.SetAsLastSibling(); + if (m_GearIcon) m_GearIcon.transform.SetAsLastSibling(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On grab event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CFloatingPanel OnGrab(Action p_Functor, bool p_Add = true); + /// + /// On release event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CFloatingPanel OnRelease(Action p_Functor, bool p_Add = true); + /// + /// On scene relocated icon event + /// + /// Functor to add/remove + /// Should add + /// + public CFloatingPanel OnSceneRelocated(Action p_Functor, bool p_Add = true) + { + if (p_Add) m_OnSceneRelocated += p_Functor; + else m_OnSceneRelocated -= p_Functor; + + return this; + } + /// + /// On scene relocated icon event + /// + /// Functor to add/remove + /// Should add + /// + public CFloatingPanel OnSizeChanged(Action p_Functor, bool p_Add = true) + { + if (p_Add) m_OnSizeChanged += p_Functor; + else m_OnSizeChanged -= p_Functor; + + return this; + } + /// + /// On gear icon event + /// + /// Functor to add/remove + /// Should add + /// + public CFloatingPanel OnGearIcon(Action p_Functor, bool p_Add = true) + { + if (p_Add) m_OnGearIcon += p_Functor; + else m_OnGearIcon -= p_Functor; + + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get allow movement + /// + /// + public virtual bool GetAllowMovement() => m_AllowMovement; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set align with floor + /// + /// Align + /// + public virtual CFloatingPanel SetAlignWithFloor(bool p_Align) + { + m_AlignWithFloor = p_Align; + return this; + } + /// + /// Set allow movements + /// + /// Is allowed? + /// + public virtual CFloatingPanel SetAllowMovement(bool p_Allow) + { + m_AllowMovement = p_Allow; + if (m_LockIcon) + { + m_LockIcon.SetSprite(p_Allow ? UISystem.GetUIIconUnlockedSprite() : UISystem.GetUIIconLockedSprite()); + m_LockIcon.SetColor(GetAllowMovement() ? ColorU.ToUnityColor("#D0FCB3") : Color.white); + } + return this; + } + /// + /// Set background state + /// + /// Is enabled? + /// Optional color + /// + public virtual CFloatingPanel SetBackground(bool p_Enabled, Color? p_Color = null) + { + if (p_Enabled) + { + if (!m_Background) + { + m_Background = new GameObject("BG", UISystem.Override_UnityComponent_Image).GetComponent(UISystem.Override_UnityComponent_Image) as Image; + m_Background.gameObject.layer = UISystem.UILayer; + m_Background.rectTransform.SetParent(transform, false); + m_Background.rectTransform.SetAsFirstSibling(); + m_Background.rectTransform.localPosition = Vector3.zero; + m_Background.rectTransform.localRotation = Quaternion.identity; + m_Background.rectTransform.localScale = Vector3.one; + m_Background.rectTransform.anchorMin = Vector2.zero; + m_Background.rectTransform.anchorMax = Vector2.one; + m_Background.rectTransform.sizeDelta = Vector2.zero; + m_Background.raycastTarget = true; + m_Background.material = UISystem.Override_GetUIMaterial(); + } + + m_Background.pixelsPerUnitMultiplier = 1; + m_Background.type = Image.Type.Sliced; + m_Background.sprite = UISystem.GetUIRoundBGSprite(); + m_Background.color = (p_Color.HasValue ? p_Color.Value : UISystem.DefaultBGColor); + m_Background.enabled = true; + } + else if (m_Background) + { + Destroy(m_Background.gameObject); + m_Background = null; + } + + return this; + } + /// + /// Set background color + /// + /// New background color + /// + public virtual CFloatingPanel SetBackgroundColor(Color p_Color) + { + if (!m_Background) + return this; + + m_Background.color = p_Color; + + return this; + } + /// + /// Set background sprite + /// + /// New sprite + /// Image type + /// + public virtual CFloatingPanel SetBackgroundSprite(Sprite p_Sprite, Image.Type p_Type = Image.Type.Simple) + { + if (!m_Background) + return this; + + m_Background.type = p_Type; + m_Background.sprite = p_Sprite; + + return this; + } + /// + /// Set lock icon mode + /// + /// Corner or none + /// + public virtual CFloatingPanel SetLockIcon(ECorner p_Corner) + { + if (p_Corner == ECorner.None) + { + if (m_LockIcon) + { + GameObject.Destroy(m_LockIcon.gameObject); + m_LockIcon = null; + } + return this; + } + + float l_Width = 5.0f; + float l_Height = 5.0f; + + if (!m_LockIcon) + { + m_LockIcon = UISystem.IconButtonFactory.Create("LockIcon", transform); + m_LockIcon.RTransform.SetAsLastSibling(); + m_LockIcon.LElement.enabled = false; + m_LockIcon.SetWidth(l_Width).SetHeight(l_Height); + m_LockIcon.SetSprite(UISystem.GetUIIconLockedSprite()); + m_LockIcon.SetColor(GetAllowMovement() ? ColorU.ToUnityColor("#D0FCB3") : Color.white); + m_LockIcon.OnClick(() => SetAllowMovement(!GetAllowMovement())); + } + + if (p_Corner == ECorner.TopLeft) + { + m_LockIcon.RTransform.anchorMin = new Vector2(0.0f, 1.0f); + m_LockIcon.RTransform.anchorMax = new Vector2(0.0f, 1.0f); + m_LockIcon.RTransform.anchoredPosition = new Vector2(l_Width, -l_Height); + } + else if (p_Corner == ECorner.TopRight) + { + m_LockIcon.RTransform.anchorMin = new Vector2(1.0f, 1.0f); + m_LockIcon.RTransform.anchorMax = new Vector2(1.0f, 1.0f); + m_LockIcon.RTransform.anchoredPosition = new Vector2(-l_Width, -l_Height); + } + else if (p_Corner == ECorner.BottomLeft) + { + m_LockIcon.RTransform.anchorMin = new Vector2(0.0f, 0.0f); + m_LockIcon.RTransform.anchorMax = new Vector2(0.0f, 0.0f); + m_LockIcon.RTransform.anchoredPosition = new Vector2(l_Width, l_Height); + } + else if (p_Corner == ECorner.BottomRight) + { + m_LockIcon.RTransform.anchorMin = new Vector2(1.0f, 0.0f); + m_LockIcon.RTransform.anchorMax = new Vector2(1.0f, 0.0f); + m_LockIcon.RTransform.anchoredPosition = new Vector2(-l_Width, l_Height); + } + + return this; + } + /// + /// Set gear icon mode + /// + /// Corner or none + /// + public virtual CFloatingPanel SetGearIcon(ECorner p_Corner) + { + if (p_Corner == ECorner.None) + { + if (m_GearIcon) + { + GameObject.Destroy(m_GearIcon.gameObject); + m_GearIcon = null; + } + return this; + } + + float l_Width = 5.0f; + float l_Height = 5.0f; + + if (!m_GearIcon) + { + m_GearIcon = UISystem.IconButtonFactory.Create("GearIcon", transform); + m_GearIcon.RTransform.SetAsLastSibling(); + m_GearIcon.LElement.enabled = false; + m_GearIcon.SetWidth(l_Width).SetHeight(l_Height); + m_GearIcon.SetSprite(UISystem.GetUIIconGearSprite()); + m_GearIcon.OnClick(() => { + try { m_OnGearIcon?.Invoke(this); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.UI.Components][CFloatingPanel.SetGearIcon] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + }); + } + + if (p_Corner == ECorner.TopLeft) + { + m_GearIcon.RTransform.anchorMin = new Vector2(0.0f, 1.0f); + m_GearIcon.RTransform.anchorMax = new Vector2(0.0f, 1.0f); + m_GearIcon.RTransform.anchoredPosition = new Vector2(l_Width, -l_Height); + } + else if (p_Corner == ECorner.TopRight) + { + m_GearIcon.RTransform.anchorMin = new Vector2(1.0f, 1.0f); + m_GearIcon.RTransform.anchorMax = new Vector2(1.0f, 1.0f); + m_GearIcon.RTransform.anchoredPosition = new Vector2(-l_Width, -l_Height); + } + else if (p_Corner == ECorner.BottomLeft) + { + m_GearIcon.RTransform.anchorMin = new Vector2(0.0f, 0.0f); + m_GearIcon.RTransform.anchorMax = new Vector2(0.0f, 0.0f); + m_GearIcon.RTransform.anchoredPosition = new Vector2(l_Width, l_Height); + } + else if (p_Corner == ECorner.BottomRight) + { + m_GearIcon.RTransform.anchorMin = new Vector2(1.0f, 0.0f); + m_GearIcon.RTransform.anchorMax = new Vector2(1.0f, 0.0f); + m_GearIcon.RTransform.anchoredPosition = new Vector2(-l_Width, l_Height); + } + + return this; + } + /// + /// Set radius on supported games + /// + /// Canvas radius + /// + public virtual CFloatingPanel SetRadius(float p_Radius) + { + return this; + } + /// + /// Set on scene release + /// + /// Target scene + /// Callback + /// + public virtual CFloatingPanel OnSceneRelease(ChatPlexSDK.EGenericScene p_Scene, Action p_Callback) + { + if (m_OnSceneRelease.ContainsKey(p_Scene)) + m_OnSceneRelease[p_Scene] = p_Callback; + else + m_OnSceneRelease.Add(p_Scene, p_Callback); + + return this; + } + /// + /// Set scene transform + /// + /// Target scene + /// Local position + /// Local euler angles + /// + public virtual CFloatingPanel SetSceneTransform(ChatPlexSDK.EGenericScene p_Scene, Vector3 p_LocalPosition, Vector3 p_LocalEulerAngles) + { + if (m_SceneTransforms.ContainsKey(p_Scene)) + m_SceneTransforms[p_Scene] = (p_LocalPosition, p_LocalEulerAngles); + else + m_SceneTransforms.Add(p_Scene, (p_LocalPosition, p_LocalEulerAngles)); + + if (p_Scene == ChatPlexSDK.ActiveGenericScene) + { + RTransform.localPosition = p_LocalPosition; + RTransform.localEulerAngles = p_LocalEulerAngles; + + try { m_OnSceneRelocated?.Invoke(this); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.UI.CFloatingPanel][CFloatingPanel.SetSceneTransform] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + + return this; + } + /// + /// Set size + /// + /// New size + /// + public virtual CFloatingPanel SetSize(Vector2 p_Size) + { + RTransform.sizeDelta = p_Size; + + try { m_OnSizeChanged?.Invoke(this, p_Size); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.UI.CFloatingPanel][CFloatingPanel.SetSize] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + + return this; + } + /// + /// Set transform direct + /// + /// Local position + /// Local euler angles + /// + public virtual CFloatingPanel SetTransformDirect(Vector3 p_LocalPosition, Vector3 p_LocalEulerAngles) + { + RTransform.localPosition = p_LocalPosition; + RTransform.localEulerAngles = p_LocalEulerAngles; + + try { m_OnSceneRelocated?.Invoke(this); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.UI.CFloatingPanel][CFloatingPanel.SetTransformDirect] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On component creation + /// + protected virtual void Awake() + { + /// Bind event + ChatPlexSDK.OnGenericSceneChange -= ChatPlexSDK_OnGenericSceneChange; + ChatPlexSDK.OnGenericSceneChange += ChatPlexSDK_OnGenericSceneChange; + + OnRelease((_) => + { + if (m_AlignWithFloor) + RTransform.localEulerAngles = new Vector3(RTransform.localEulerAngles.x, RTransform.localEulerAngles.y, 0); + + if (m_SceneTransforms.ContainsKey(ChatPlexSDK.ActiveGenericScene)) + SetSceneTransform(ChatPlexSDK.ActiveGenericScene, RTransform.localPosition, RTransform.localEulerAngles); + + if (m_OnSceneRelease.TryGetValue(ChatPlexSDK.ActiveGenericScene, out var l_OnSceneReleaseCallback) && l_OnSceneReleaseCallback != null) + { + try { l_OnSceneReleaseCallback?.Invoke(RTransform.localPosition, RTransform.localEulerAngles); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.UI.CFloatingPanel][CFloatingPanel.OnRelease] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + }); + } + /// + /// On component destruction + /// + protected virtual void OnDestroy() + { + /// Unbind event + ChatPlexSDK.OnGenericSceneChange -= ChatPlexSDK_OnGenericSceneChange; + + /// Discard any view controller + SetViewController(null); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On generic scene change + /// + /// New active scene + private void ChatPlexSDK_OnGenericSceneChange(ChatPlexSDK.EGenericScene p_ActiveScene) + { + if (m_AutoLockOnSceneSwitch) + SetAllowMovement(false); + + if (m_SceneTransforms.TryGetValue(p_ActiveScene, out var l_SceneTransform)) + { + RTransform.localPosition = l_SceneTransform.Item1; + RTransform.localEulerAngles = l_SceneTransform.Item2; + + try { m_OnSceneRelocated?.Invoke(this); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.UI.CFloatingPanel][CFloatingPanel.ChatPlexSDK_OnGenericSceneChange] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + } + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CGLayout.cs b/BeatSaberPlus/CP_SDK/UI/Components/CGLayout.cs new file mode 100644 index 0000000..7f1407f --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CGLayout.cs @@ -0,0 +1,120 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Grid layout group + /// + public abstract class CGLayout : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract ContentSizeFitter CSizeFitter { get; } + public abstract LayoutElement LElement { get; } + public abstract GridLayoutGroup GLayoutGroup { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set preferred width + /// + /// Width + /// + public virtual CGLayout SetWidth(float p_Width) + { + RTransform.sizeDelta = new Vector2(p_Width, RTransform.sizeDelta.y); + LElement.preferredWidth = p_Width; + return this; + } + /// + /// Set preferred height + /// + /// Height + /// + public virtual CGLayout SetHeight(float p_Height) + { + RTransform.sizeDelta = new Vector2(RTransform.sizeDelta.x, p_Height); + LElement.preferredHeight = p_Height; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set min width + /// + /// Width + /// + public virtual CGLayout SetMinWidth(float p_Width) + { + LElement.minWidth = p_Width; + return this; + } + /// + /// Set min height + /// + /// Height + /// + public virtual CGLayout SetMinHeight(float p_Height) + { + LElement.minHeight = p_Height; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set cell size + /// + /// New size + /// + public virtual CGLayout SetCellSize(Vector2 p_CellSize) + { + GLayoutGroup.cellSize = p_CellSize; + return this; + } + /// + /// Set child alignment + /// + /// New alignment + /// + public virtual CGLayout SetChildAlign(TextAnchor p_ChildAlign) + { + GLayoutGroup.childAlignment = p_ChildAlign; + return this; + } + /// + /// Set layout constraint + /// + /// New value + /// + public virtual CGLayout SetConstraint(GridLayoutGroup.Constraint p_Constraint) + { + GLayoutGroup.constraint = p_Constraint; + return this; + } + /// + /// Set layout constraint count + /// + /// New value + /// + public virtual CGLayout SetConstraintCount(int p_ConstraintCount) + { + GLayoutGroup.constraintCount = p_ConstraintCount; + return this; + } + /// + /// Set spacing between elements + /// + /// New spacing + /// + public virtual CGLayout SetSpacing(Vector2 p_Spacing) + { + GLayoutGroup.spacing = p_Spacing; + return this; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CHLayout.cs b/BeatSaberPlus/CP_SDK/UI/Components/CHLayout.cs new file mode 100644 index 0000000..d17edb5 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CHLayout.cs @@ -0,0 +1,15 @@ +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Horizontal layout component + /// + public abstract class CHLayout : CHOrVLayout + { + /// + /// Unity HorizontalLayoutGroup accessor + /// + public abstract HorizontalLayoutGroup HLayoutGroup { get; } + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CIconButton.cs b/BeatSaberPlus/CP_SDK/UI/Components/CIconButton.cs new file mode 100644 index 0000000..8ff2c2d --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CIconButton.cs @@ -0,0 +1,87 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Icon button component + /// + public abstract class CIconButton : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + public abstract Button ButtonC { get; } + public abstract Image IconImageC { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On click event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CIconButton OnClick(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set preferred width + /// + /// Width + /// + public virtual CIconButton SetWidth(float p_Width) + { + RTransform.sizeDelta = new Vector2(p_Width, RTransform.sizeDelta.y); + LElement.preferredWidth = p_Width; + return this; + } + /// + /// Set preferred height + /// + /// Height + /// + public virtual CIconButton SetHeight(float p_Height) + { + RTransform.sizeDelta = new Vector2(RTransform.sizeDelta.x, p_Height); + LElement.preferredHeight = p_Height; + return this; + } + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set sprite color + /// + /// New color + /// + public virtual CIconButton SetColor(Color p_Color) + { + IconImageC.color = p_Color; + return this; + } + /// + /// Set button interactable state + /// + /// New state + /// + public virtual CIconButton SetInteractable(bool p_Interactable) + { + ButtonC.interactable = p_Interactable; + return this; + } + /// + /// Set button sprite + /// + /// New sprite + /// + public virtual CIconButton SetSprite(Sprite p_Sprite) + { + IconImageC.sprite = p_Sprite; + return this; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CImage.cs b/BeatSaberPlus/CP_SDK/UI/Components/CImage.cs new file mode 100644 index 0000000..48d0a04 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CImage.cs @@ -0,0 +1,130 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Image component + /// + public abstract class CImage : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + public abstract Image ImageC { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set preferred width + /// + /// Width + /// + public CImage SetWidth(float p_Width) + { + RTransform.sizeDelta = new Vector2(p_Width, RTransform.sizeDelta.y); + LElement.preferredWidth = p_Width; + return this; + } + /// + /// Set preferred height + /// + /// Height + /// + public CImage SetHeight(float p_Height) + { + RTransform.sizeDelta = new Vector2(RTransform.sizeDelta.x, p_Height); + LElement.preferredHeight = p_Height; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set min width + /// + /// Width + /// + public CImage SetMinWidth(float p_Width) + { + LElement.minWidth = p_Width; + return this; + } + /// + /// Set min height + /// + /// Height + /// + public CImage SetMinHeight(float p_Height) + { + LElement.minHeight = p_Height; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set color + /// + /// New color + /// + public virtual CImage SetColor(Color p_Color) + { + ImageC.color = p_Color; + return this; + } + /// + /// Set enhanced image + /// + /// New enhanced image + /// + public virtual CImage SetEnhancedImage(Unity.EnhancedImage p_EnhancedImage) + { + var l_Updater = GetComponent(); + if (!l_Updater) + l_Updater = gameObject.AddComponent(); + + if (l_Updater && p_EnhancedImage == null) + GameObject.Destroy(l_Updater); + else + { + l_Updater.TargetImage = ImageC; + l_Updater.ControllerDataInstance = p_EnhancedImage.AnimControllerData; + } + + return this; + } + /// + /// Set pixels per unit multiplier + /// + /// New multiplier + /// + public virtual CImage SetPixelsPerUnitMultiplier(float p_Multiplier) + { + ImageC.pixelsPerUnitMultiplier = p_Multiplier; + return this; + } + /// + /// Set sprite + /// + /// New sprite + /// + public virtual CImage SetSprite(Sprite p_Sprite) + { + ImageC.sprite = p_Sprite; + return this; + } + /// + /// Set type + /// + /// New type + /// + public virtual CImage SetType(Image.Type p_Type) + { + ImageC.type = p_Type; + return this; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CPrimaryButton.cs b/BeatSaberPlus/CP_SDK/UI/Components/CPrimaryButton.cs new file mode 100644 index 0000000..a50843e --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CPrimaryButton.cs @@ -0,0 +1,10 @@ +namespace CP_SDK.UI.Components +{ + /// + /// Primary button component + /// + public abstract class CPrimaryButton : CPOrSButton + { + + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CSecondaryButton.cs b/BeatSaberPlus/CP_SDK/UI/Components/CSecondaryButton.cs new file mode 100644 index 0000000..703a243 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CSecondaryButton.cs @@ -0,0 +1,10 @@ +namespace CP_SDK.UI.Components +{ + /// + /// Secondary button component + /// + public abstract class CSecondaryButton : CPOrSButton + { + + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CSlider.cs b/BeatSaberPlus/CP_SDK/UI/Components/CSlider.cs new file mode 100644 index 0000000..dfab460 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CSlider.cs @@ -0,0 +1,128 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// CSlider component + /// + public abstract class CSlider : Selectable + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + public abstract CPOrSButton DecButton { get; } + public abstract CPOrSButton IncButton { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On value changed event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CSlider OnValueChanged(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get min value + /// + /// + public abstract float GetMinValue(); + /// + /// Get max value + /// + /// + public abstract float GetMaxValue(); + /// + /// Get increments + /// + /// + public abstract float GetIncrements(); + /// + /// Get value + /// + /// + public abstract float GetValue(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set theme color + /// + /// New color + /// + public abstract CSlider SetColor(Color p_Color); + /// + /// Set value formatter + /// + /// Custom value formatter + /// + public abstract CSlider SetFormatter(Func p_CustomFormatter); + /// + /// Set integer mode + /// + /// Is integer? + /// + public abstract CSlider SetInteger(bool p_IsInteger); + /// + /// Set button interactable state + /// + /// New state + /// + public virtual CSlider SetInteractable(bool p_Interactable) + { + interactable = p_Interactable; + return this; + } + /// + /// Set min value + /// + /// New value + /// + public abstract CSlider SetMinValue(float p_MinValue); + /// + /// Set max value + /// + /// New value + /// + public abstract CSlider SetMaxValue(float p_MaxValue); + /// + /// Set increments + /// + /// New value + /// + public abstract CSlider SetIncrements(float p_Increments); + /// + /// Set value + /// + /// Value + /// Notify? + /// + public abstract CSlider SetValue(float p_Value, bool p_Notify = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Switch to color mode + /// + /// Is Hue mode? + /// Is saturation mode? + /// Is value mode? + /// Is opacity mode? + /// + public abstract CSlider SwitchToColorMode(bool p_H, bool p_S, bool p_V, bool p_O); + /// + /// Color mode set H + /// + /// Is Hue mode? + /// + public abstract CSlider ColorModeSetHue(float p_H); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CTabControl.cs b/BeatSaberPlus/CP_SDK/UI/Components/CTabControl.cs new file mode 100644 index 0000000..5e09c22 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CTabControl.cs @@ -0,0 +1,52 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Tab control widget + /// + public abstract class CTabControl : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On active tab changed event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CTabControl OnActiveChanged(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get active tab + /// + /// + public abstract int GetActiveTab(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set active tab + /// + /// New active index + /// Should notify? + /// + public abstract CTabControl SetActiveTab(int p_Index, bool p_Notify = true); + /// + /// Set tabs + /// + /// Tabs + /// + public abstract CTabControl SetTabs(params (string, RectTransform)[] p_Tabs); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CText.cs b/BeatSaberPlus/CP_SDK/UI/Components/CText.cs new file mode 100644 index 0000000..d536737 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CText.cs @@ -0,0 +1,135 @@ +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// CText component + /// + public abstract class CText : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + public abstract TextMeshProUGUI TMProUGUI { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get text + /// + /// + public string GetText() + => TMProUGUI.text; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set align + /// + /// New align + /// + public CText SetAlign(TextAlignmentOptions p_Align) + { + TMProUGUI.alignment = p_Align; + return this; + } + /// + /// Set alpha + /// + /// New alpha + /// + public CText SetAlpha(float p_Alpha) + { + TMProUGUI.alpha = p_Alpha; + return this; + } + /// + /// Set color + /// + /// New color + /// + public CText SetColor(Color p_Color) + { + TMProUGUI.color = p_Color; + return this; + } + /// + /// Set font size + /// + /// New size + /// + public CText SetFontSize(float p_Size) + { + TMProUGUI.fontSize = p_Size * UISystem.FontScale; + return this; + } + /// + /// Set font sizes + /// + /// New size + /// New size + /// + public CText SetFontSizes(float p_MinSize, float p_MaxSize) + { + TMProUGUI.fontSizeMin = p_MinSize * UISystem.FontScale; + TMProUGUI.fontSizeMax = p_MaxSize * UISystem.FontScale; + return this; + } + /// + /// Set margins + /// + /// Left margin + /// Top margin + /// Right margin + /// Bottom margin + /// + public CText SetMargins(float p_Left, float p_Top, float p_Right, float p_Bottom) + { + TMProUGUI.margin = new Vector4(p_Left, p_Top, p_Right, p_Bottom); + return this; + } + /// + /// Set overflow mode + /// + /// New overflow mdoe + /// + public CText SetOverflowMode(TextOverflowModes p_OverflowMode) + { + TMProUGUI.overflowMode = p_OverflowMode; + return this; + } + /// + /// Set style + /// + /// New style + /// + public CText SetStyle(FontStyles p_Style) + { + TMProUGUI.fontStyle = p_Style; + return this; + } + /// + /// Set button text + /// + /// New text + /// + public CText SetText(string p_Text) + { + TMProUGUI.text = p_Text; + return this; + } + /// + /// Set wrapping + /// + /// New state + /// + public CText SetWrapping(bool p_Wrapping) + { + TMProUGUI.enableWordWrapping = p_Wrapping; + return this; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CTextInput.cs b/BeatSaberPlus/CP_SDK/UI/Components/CTextInput.cs new file mode 100644 index 0000000..a2fbd6e --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CTextInput.cs @@ -0,0 +1,64 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// CTextInput component + /// + public abstract class CTextInput : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On value changed event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CTextInput OnValueChanged(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get value + /// + /// + public abstract string GetValue(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set interactable state + /// + /// New state + /// + public abstract CTextInput SetInteractable(bool p_Interactable); + /// + /// Set is password + /// + /// Is password? + /// + public abstract CTextInput SetIsPassword(bool p_IsPassword); + /// + /// Set place holder + /// + /// New place holder + /// + public abstract CTextInput SetPlaceHolder(string p_PlaceHolder); + /// + /// Set value + /// + /// New value + /// Should notify? + /// + public abstract CTextInput SetValue(string p_Value, bool p_Notify = true); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CTextSegmentedControl.cs b/BeatSaberPlus/CP_SDK/UI/Components/CTextSegmentedControl.cs new file mode 100644 index 0000000..3f756d9 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CTextSegmentedControl.cs @@ -0,0 +1,58 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// CTextSegmentedControl component + /// + public abstract class CTextSegmentedControl : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract ContentSizeFitter CSizeFitter { get; } + public abstract LayoutElement LElement { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On active text changed event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CTextSegmentedControl OnActiveChanged(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get active text + /// + /// + public abstract int GetActiveText(); + /// + /// Get text count + /// + /// + public abstract int GetTextCount(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set active text + /// + /// New active index + /// Should notify? + /// + public abstract CTextSegmentedControl SetActiveText(int p_Index, bool p_Notify = true); + /// + /// Set texts + /// + /// New texts + /// + public abstract CTextSegmentedControl SetTexts(params string[] p_Texts); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CToggle.cs b/BeatSaberPlus/CP_SDK/UI/Components/CToggle.cs new file mode 100644 index 0000000..3940a3c --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CToggle.cs @@ -0,0 +1,58 @@ +using System; +using System.Xml; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// CToggle component + /// + public abstract class CToggle : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + public abstract Toggle Toggle { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On value changed event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CToggle OnValueChanged(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get toggle value + /// + /// + public abstract bool GetValue(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set interactable state + /// + /// New state + /// + public virtual CToggle SetInteractable(bool p_Interactable) + { + Toggle.interactable = p_Interactable; + return this; + } + /// + /// Set value + /// + /// Value + /// Notify? + /// + public abstract CToggle SetValue(bool p_Value, bool p_Notify = true); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CVLayout.cs b/BeatSaberPlus/CP_SDK/UI/Components/CVLayout.cs new file mode 100644 index 0000000..4bdea9e --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CVLayout.cs @@ -0,0 +1,15 @@ +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Vertical layout component + /// + public abstract class CVLayout : CHOrVLayout + { + /// + /// Unity VerticalLayoutGroup accessor + /// + public abstract VerticalLayoutGroup VLayoutGroup { get; } + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CVScrollView.cs b/BeatSaberPlus/CP_SDK/UI/Components/CVScrollView.cs new file mode 100644 index 0000000..afc0efe --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CVScrollView.cs @@ -0,0 +1,81 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Vertical scroll view component + /// + public abstract class CVScrollView : MonoBehaviour + { + /// + /// Scroll type enum + /// + public enum EScrollType + { + PageSize, + FixedCellSize + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + public abstract RectTransform Container { get; } + + public EScrollType ScrollType = EScrollType.PageSize; + public float FixedCellSize = 10f; + public float PageStepNormalizedSize = 0.7f; + + public abstract float Position { get; } + public abstract float ViewPortWidth { get; } + public abstract float ScrollableSize { get; } + public abstract float ScrollPageSize { get; } + public abstract float ContentSize { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On scroll changed + /// + /// Functor to add/remove + /// Should add + /// + public abstract CVScrollView OnScrollChanged(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Update content size + /// + public abstract CVScrollView UpdateContentSize(); + /// + /// Set content size + /// + /// New content size + public abstract CVScrollView SetContentSize(float p_ContentSize); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Scroll to position + /// + /// New target position + /// Is animated? + public abstract CVScrollView ScrollTo(float p_TargetPosition, bool p_Animated); + /// + /// Scroll to end + /// + /// Is animated? + public abstract CVScrollView ScrollToEnd(bool p_Animated); + /// + /// Refresh scroll buttons + /// + public abstract CVScrollView RefreshScrollButtons(); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/CVVList.cs b/BeatSaberPlus/CP_SDK/UI/Components/CVVList.cs new file mode 100644 index 0000000..a9c7275 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/CVVList.cs @@ -0,0 +1,10 @@ +namespace CP_SDK.UI.Components +{ + /// + /// Virtual Vertical List + /// + public abstract class CVVList : CVXList + { + + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/Generics/CHOrVLayout.cs b/BeatSaberPlus/CP_SDK/UI/Components/Generics/CHOrVLayout.cs new file mode 100644 index 0000000..b4382c1 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/Generics/CHOrVLayout.cs @@ -0,0 +1,215 @@ +using CP_SDK.Unity.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Horizontal or vertical layout base component + /// + public abstract class CHOrVLayout : MonoBehaviour + { + private Image m_Background = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public abstract RectTransform RTransform { get; } + public abstract ContentSizeFitter CSizeFitter { get; } + public abstract LayoutElement LElement { get; } + public abstract HorizontalOrVerticalLayoutGroup HOrVLayoutGroup { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get background fill amount + /// + /// + public float GetBackgroundFillAmount() + => m_Background ? m_Background.fillAmount : 0f; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set background state + /// + /// Is enabled? + /// Optional color, default to black + /// Should raycast target + /// + public CHOrVLayout SetBackground(bool p_Enabled, Color? p_Color = null, bool p_RaycastTarget = false) + { + if (p_Enabled) + { + if (!m_Background) + { + m_Background = gameObject.AddComponent(UISystem.Override_UnityComponent_Image) as Image; + m_Background.material = UISystem.Override_GetUIMaterial(); + m_Background.raycastTarget = p_RaycastTarget; + } + + m_Background.pixelsPerUnitMultiplier = 1; + m_Background.type = Image.Type.Sliced; + m_Background.sprite = UISystem.GetUIRoundBGSprite(); + m_Background.color = p_Color.HasValue ? p_Color.Value : UISystem.DefaultBGColor; + } + else if (m_Background) + { + GameObject.Destroy(m_Background); + m_Background = null; + } + + return this; + } + /// + /// Set background color + /// + /// New background color + /// + public virtual CHOrVLayout SetBackgroundColor(Color p_Color) + { + if (!m_Background) return this; + m_Background.color = p_Color; + return this; + } + /// + /// Set background fill method + /// + /// Fill method + /// + public virtual CHOrVLayout SetBackgroundFillMethod(Image.FillMethod p_FillMethod) + { + if (!m_Background) return this; + m_Background.fillMethod = p_FillMethod; + return this; + } + /// + /// Set background fill amount + /// + /// Fill amount + /// + public virtual CHOrVLayout SetBackgroundFillAmount(float p_FillAmount) + { + if (!m_Background) return this; + m_Background.fillAmount = p_FillAmount; + return this; + } + /// + /// Set background sprite + /// + /// New sprite + /// Image type + /// + public virtual CHOrVLayout SetBackgroundSprite(Sprite p_Sprite, Image.Type p_Type = Image.Type.Simple) + { + if (!m_Background) return this; + m_Background.type = p_Type; + m_Background.sprite = p_Sprite; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set preferred width + /// + /// Width + /// + public CHOrVLayout SetWidth(float p_Width) + { + RTransform.sizeDelta = new Vector2(p_Width, RTransform.sizeDelta.y); + LElement.preferredWidth = p_Width; + return this; + } + /// + /// Set preferred height + /// + /// Height + /// + public CHOrVLayout SetHeight(float p_Height) + { + RTransform.sizeDelta = new Vector2(RTransform.sizeDelta.x, p_Height); + LElement.preferredHeight = p_Height; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set min width + /// + /// Width + /// + public CHOrVLayout SetMinWidth(float p_Width) + { + LElement.minWidth = p_Width; + return this; + } + /// + /// Set min height + /// + /// Height + /// + public CHOrVLayout SetMinHeight(float p_Height) + { + LElement.minHeight = p_Height; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set padding + /// + /// New padding + /// + public CHOrVLayout SetPadding(RectOffset p_Padding) + { + HOrVLayoutGroup.padding = p_Padding; + return this; + } + /// + /// Set padding + /// + /// Top padding + /// Right padding + /// Bottom padding + /// Left padding + /// + public CHOrVLayout SetPadding(int p_Top, int p_Right, int p_Bottom, int p_Left) + { + SetPadding(new RectOffset(p_Left, p_Right, p_Top, p_Bottom)); + return this; + } + /// + /// Set padding + /// + /// New padding + /// + public CHOrVLayout SetPadding(int p_Padding) + { + HOrVLayoutGroup.padding = new RectOffset(p_Padding, p_Padding, p_Padding, p_Padding); + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set spacing between elements + /// + /// New spacing + /// + public CHOrVLayout SetSpacing(float p_Spacing) + { + HOrVLayoutGroup.spacing = p_Spacing; + return this; + } + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/Generics/CPOrSButton.cs b/BeatSaberPlus/CP_SDK/UI/Components/Generics/CPOrSButton.cs new file mode 100644 index 0000000..196d6a5 --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/Generics/CPOrSButton.cs @@ -0,0 +1,163 @@ +using System; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Primary or Secondary button component + /// + public abstract class CPOrSButton : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract ContentSizeFitter CSizeFitter { get; } + public abstract LayoutGroup LayoutGroupC { get; } + public abstract LayoutElement LElement { get; } + public abstract Button ButtonC { get; } + public abstract Image BackgroundImageC { get; } + public abstract Image IconImageC { get; } + public abstract CText TextC { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On click event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CPOrSButton OnClick(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get text + /// + /// + public string GetText() + { + return TextC.GetText(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set preferred width + /// + /// Width + /// + public virtual CPOrSButton SetWidth(float p_Width) + { + RTransform.sizeDelta = new Vector2(p_Width, RTransform.sizeDelta.y); + LElement.preferredWidth = p_Width; + return this; + } + /// + /// Set preferred height + /// + /// Height + /// + public virtual CPOrSButton SetHeight(float p_Height) + { + RTransform.sizeDelta = new Vector2(RTransform.sizeDelta.x, p_Height); + LElement.preferredHeight = p_Height; + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set background color + /// + /// New background color + /// + public virtual CPOrSButton SetBackgroundColor(Color p_Color) + { + BackgroundImageC.color = p_Color; + return this; + } + /// + /// Set background sprite + /// + /// New sprite + /// + public virtual CPOrSButton SetBackgroundSprite(Sprite p_Sprite) + { + BackgroundImageC.sprite = p_Sprite; + BackgroundImageC.gameObject.SetActive(p_Sprite); + return this; + } + /// + /// Set font size + /// + /// New size + /// + public CPOrSButton SetFontSize(float p_Size) + { + TextC.SetFontSize(p_Size); + return this; + } + /// + /// Set theme color + /// + /// New color + /// + public virtual CPOrSButton SetColor(Color p_Color) + { + BackgroundImageC.color = p_Color; + return this; + } + /// + /// Set button icon sprite + /// + /// New sprite + /// + public virtual CPOrSButton SetIconSprite(Sprite p_Sprite) + { + IconImageC.sprite = p_Sprite; + IconImageC.gameObject.SetActive(p_Sprite); + return this; + } + /// + /// Set button interactable state + /// + /// New state + /// + public virtual CPOrSButton SetInteractable(bool p_Interactable) + { + ButtonC.interactable = p_Interactable; + return this; + } + /// + /// Set overflow mode + /// + /// New overflow mdoe + /// + public CPOrSButton SetOverflowMode(TextOverflowModes p_OverflowMode) + { + TextC.SetOverflowMode(p_OverflowMode); + return this; + } + /// + /// Set button text + /// + /// New text + /// + public CPOrSButton SetText(string p_Text) + { + TextC.SetText(p_Text); + return this; + } + /// + /// Set tooltip + /// + /// New tooltip + /// + public abstract CPOrSButton SetTooltip(string p_Tooltip); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Components/Generics/CVXList.cs b/BeatSaberPlus/CP_SDK/UI/Components/Generics/CVXList.cs new file mode 100644 index 0000000..12d310c --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Components/Generics/CVXList.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace CP_SDK.UI.Components +{ + /// + /// Generic virtual list interface + /// + public abstract class CVXList : MonoBehaviour + { + public abstract RectTransform RTransform { get; } + public abstract LayoutElement LElement { get; } + public abstract float ScrollPosition { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On list item selected event + /// + /// Functor to add/remove + /// Should add + /// + public abstract CVXList OnListItemSelected(Action p_Functor, bool p_Add = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get selected item + /// + /// + public abstract Data.IListItem GetSelectedItem(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Scroll to position + /// + /// New target position + /// Is animated? + public abstract CVXList ScrollTo(float p_TargetPosition, bool p_Animated); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set list cell prefab + /// + /// New prefab + public abstract CVXList SetListCellPrefab(Data.IListCell p_Prefab); + /// + /// Set list items + /// + /// New items + /// + public abstract CVXList SetListItems(List p_ListItems); + /// + /// Set list items + /// + /// New items + /// + public CVXList SetListItems(List p_ListItems) + where T : Data.IListItem + { + var l_List = Pool.ListPool.Get(); + try + { + l_List.Clear(); + l_List.Capacity = p_ListItems.Count; + l_List.AddRange(p_ListItems); + SetListItems(l_List); + } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[CP_SDK.UI.Components][CVXList.SetListItems] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + finally + { + Pool.ListPool.Release(l_List); + } + + return this; + } + /// + /// Set selected list item + /// + /// Selected list item + /// Should notify? + /// + public abstract CVXList SetSelectedListItem(Data.IListItem p_ListItem, bool p_Notify = true); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Add a list item + /// + /// Item to add + /// + public abstract CVXList AddListItem(Data.IListItem p_ListItem); + /// + /// Sort list items by a functor + /// + /// + public abstract CVXList SortListItems(Func p_Functor); + /// + /// Remove a list item + /// + /// Item to remove + /// + public abstract CVXList RemoveListItem(Data.IListItem p_ListItem); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On list cell clicked + /// + /// Clicked list cell + public abstract void OnListCellClicked(Data.IListCell p_ListCell); + } +} diff --git a/BeatSaberPlus/CP_SDK/UI/Data/IListCell.cs b/BeatSaberPlus/CP_SDK/UI/Data/IListCell.cs new file mode 100644 index 0000000..aa898dc --- /dev/null +++ b/BeatSaberPlus/CP_SDK/UI/Data/IListCell.cs @@ -0,0 +1,184 @@ +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace CP_SDK.UI.Data +{ + /// + /// Abstract List Cell component + /// + [RequireComponent(typeof(RectTransform))] + public abstract class IListCell : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler + { + private RectTransform m_RTransform; + private Image m_Image; + private Button m_Button; + private Components.CVXList m_OwnerList; + private int m_Index; + private IListItem m_ListItem; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public RectTransform RTransform => m_RTransform; + public Components.CVXList OwnerList => m_OwnerList; + public int Index => m_Index; + public IListItem ListItem => m_ListItem; + public string Tooltip; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create cell instance + /// + /// Parent + /// + public IListCell Create(RectTransform p_Parent) + { + var l_NewCell = AddSelfComponent(new GameObject("ListCell", typeof(RectTransform), UISystem.Override_UnityComponent_Image, typeof(Button))); + l_NewCell.transform.SetParent(p_Parent, false); + l_NewCell.gameObject.SetActive(false); + l_NewCell.Build(); + + return l_NewCell; + } + /// + /// Bind to list + /// + /// Owner list + /// List item index + /// List item instance + public void Bind(Components.CVXList p_OwnerList, int p_Index, IListItem p_ListItem) + { + m_OwnerList = p_OwnerList; + m_Index = p_Index; + m_ListItem = p_ListItem; + + SetState(false); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build cell + /// + public virtual void Build() + { + if (m_RTransform) + return; + + m_RTransform = GetComponent(); + + m_Image = gameObject.GetComponent(UISystem.Override_UnityComponent_Image) as Image; + m_Image.material = UISystem.Override_GetUIMaterial(); + m_Image.type = Image.Type.Sliced; + m_Image.pixelsPerUnitMultiplier = 1; + m_Image.sprite = UISystem.GetUIRoundBGSprite(); + m_Image.preserveAspect = false; + + m_Button = gameObject.GetComponent private void OnModButtonPressed() - => UI.MainViewFlowCoordinator.Instance().Present(true); + => CP_SDK.UI.FlowCoordinators.MainFlowCoordinator.Instance().Present(true); /// /// On text message received /// @@ -176,7 +160,130 @@ private void Service_Discrete_OnTextMessageReceived(CP_SDK.Chat.Interfaces.IChat p_Service.SendTextMessage(p_Message.Channel, l_Message); } + /*else if (l_LMessage.StartsWith("!score") && CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Menu) + { + var l_LevelCompletionData = SDK.Game.Logic.LevelCompletionData; + if (l_LevelCompletionData != null) + { + var l_MaxMultiplied = l_LevelCompletionData.MaxMultipliedScore; + var l_MultipliedScore = l_LevelCompletionData.Results.multipliedScore; + var l_Percentage = SDK.Game.Levels.GetScorePercentage(l_MaxMultiplied, l_MultipliedScore); + + p_Service.SendTextMessage( + p_Message.Channel, + $"! Acc: {(l_Percentage * 100):F2}" + ); + } + }*/ + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Patch UI system + /// + private void PatchUI() + { + CP_SDK.UI.UISystem.FloatingPanelFactory = new SDK.UI.DefaultFactoriesOverrides.BS_FloatingPanelFactory(); + + CP_SDK.UI.UISystem.UILayer = LayerMask.NameToLayer("UI"); + + CP_SDK.UI.UISystem.Override_UnityComponent_Image = typeof(HMUI.ImageView); + CP_SDK.UI.UISystem.Override_UnityComponent_TextMeshProUGUI = typeof(HMUI.CurvedTextMeshPro); + + CP_SDK.UI.UISystem.Override_GetUIMaterial = () => + { + if (m_UINoGlowMaterial == null) + { + m_UINoGlowMaterial = Resources.FindObjectsOfTypeAll().Where(x => x.name == "UINoGlow").FirstOrDefault(); + + if (m_UINoGlowMaterial != null) + m_UINoGlowMaterial = Material.Instantiate(m_UINoGlowMaterial); + } + + return m_UINoGlowMaterial; + }; + CP_SDK.UI.UISystem.Override_OnClick = (p_MonoBehavior) => + { + if (!m_BasicUIAudioManager || CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + m_BasicUIAudioManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + + m_BasicUIAudioManager?.HandleButtonClickEvent(); + }; + + CP_SDK.UI.UISystem.OnScreenCreated = (x) => + { + if (x.GetComponent()) + return; + + if (!m_VRGraphicRaycasterCache) + m_VRGraphicRaycasterCache = Resources.FindObjectsOfTypeAll().FirstOrDefault(y => y._physicsRaycaster != null); + + if (m_VRGraphicRaycasterCache) + x.gameObject.AddComponent().SetField("_physicsRaycaster", m_VRGraphicRaycasterCache._physicsRaycaster); + }; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + CP_SDK.UI.ScreenSystem.OnCreated += () => + { + var l_Instance = CP_SDK.UI.ScreenSystem.Instance; + l_Instance.LeftScreen.SetTransformDirect(new Vector3(-2.47f, 0.00f, -1.30f), new Vector3(0.0f, -55.0f, 0.0f)); + l_Instance.LeftScreen.SetRadius(140.0f); + + l_Instance.TopScreen.SetRadius(140.0f); + l_Instance.MainScreen.SetRadius(140.0f); + + l_Instance.RightScreen.SetTransformDirect(new Vector3(2.47f, 0.00f, -1.30f), new Vector3(0.0f, 55.0f, 0.0f)); + l_Instance.RightScreen.SetRadius(140.0f); + }; + CP_SDK.UI.ScreenSystem.OnPresent += ScreenSystem_OnPresent; + CP_SDK.UI.ScreenSystem.OnDismiss += ScreenSystem_OnDismiss; + } + /// + /// Screen system on present + /// + private void ScreenSystem_OnPresent() + { + void DeactivateScreenSafe(HMUI.Screen p_HMUIScreen) + { + if (!p_HMUIScreen.gameObject.activeSelf) + return; + + m_HMUIDeactivatedScreens.Add(p_HMUIScreen.gameObject); + p_HMUIScreen.gameObject.SetActive(false); + } + + if (m_HMUIScreenSystem == null || !m_HMUIScreenSystem) + { + m_HMUIDeactivatedScreens.Clear(); + m_HMUIScreenSystem = Resources.FindObjectsOfTypeAll().FirstOrDefault(); } + + if (!m_HMUIScreenSystem) + return; + + m_HMUIDeactivatedScreens.Clear(); + DeactivateScreenSafe(m_HMUIScreenSystem.leftScreen); + DeactivateScreenSafe(m_HMUIScreenSystem.mainScreen); + DeactivateScreenSafe(m_HMUIScreenSystem.rightScreen); + DeactivateScreenSafe(m_HMUIScreenSystem.bottomScreen); + DeactivateScreenSafe(m_HMUIScreenSystem.topScreen); + + CP_SDK.UI.ScreenSystem.Instance.transform.localScale = m_HMUIScreenSystem.transform.localScale; + } + /// + /// Screen system on dismiss + /// + private void ScreenSystem_OnDismiss() + { + for (var l_I = 0; l_I < m_HMUIDeactivatedScreens.Count; ++l_I) + m_HMUIDeactivatedScreens[l_I].SetActive(true); + + m_HMUIDeactivatedScreens.Clear(); } } } diff --git a/BeatSaberPlus/Properties/AssemblyInfo.cs b/BeatSaberPlus/Properties/AssemblyInfo.cs index 0e9f3de..e92d6df 100644 --- a/BeatSaberPlus/Properties/AssemblyInfo.cs +++ b/BeatSaberPlus/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/BeatSaberPlus/SDK/BSPModuleBase.cs b/BeatSaberPlus/SDK/BSPModuleBase.cs deleted file mode 100644 index be9b0d4..0000000 --- a/BeatSaberPlus/SDK/BSPModuleBase.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace BeatSaberPlus.SDK -{ - /// - /// Module base interface - /// - public interface IBSPModuleBase : CP_SDK.IModuleBase - { - /// - /// Get Module settings UI - /// - (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUI(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Module base interface - /// - public abstract class BSPModuleBase : CP_SDK.ModuleBase, IBSPModuleBase - where T : BSPModuleBase, new() - { - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get Module settings UI - /// - public (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUI() => GetSettingsUIImplementation(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get Module settings UI - /// - protected abstract (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation(); - } -} diff --git a/BeatSaberPlus/SDK/Chat/Service.cs b/BeatSaberPlus/SDK/Chat/Service.cs index 29c62ef..a4b6aa8 100644 --- a/BeatSaberPlus/SDK/Chat/Service.cs +++ b/BeatSaberPlus/SDK/Chat/Service.cs @@ -55,7 +55,7 @@ public static void Release(bool p_OnExit = false) /// Open web configurator /// public static void OpenWebConfigurator() - => CP_SDK.Chat.Service.OpenWebConfigurator(); + => CP_SDK.Chat.Service.OpenWebConfiguration(); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/SDK/Game/BeatMaps/MapDetail.cs b/BeatSaberPlus/SDK/Game/BeatMaps/MapDetail.cs index 2faf174..a0c68ba 100644 --- a/BeatSaberPlus/SDK/Game/BeatMaps/MapDetail.cs +++ b/BeatSaberPlus/SDK/Game/BeatMaps/MapDetail.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using System; using System.Linq; +using System.Security.Policy; namespace BeatSaberPlus.SDK.Game.BeatMaps { @@ -23,6 +24,8 @@ public class MapDetail [JsonIgnore] public bool Partial = true; + [JsonIgnore] + public string PartialHash = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -33,16 +36,24 @@ public class MapDetail /// ID of the BeatMap /// public static MapDetail PartialFromKey(string p_Key) - { - return new MapDetail() { id = p_Key }; - } + => new MapDetail() { id = p_Key }; + /// + /// Partial BeatMap from ID + /// + /// Hash of the BeatMap + /// + public static MapDetail PartialFromHash(string p_Hash) + => new MapDetail() { PartialHash = p_Hash }; /// /// Populate partial BeatMap /// /// Completion/failure callback public void Populate(Action p_Callback) { - BeatMapsClient.PopulateOnlineByKey(this, p_Callback); + if (string.IsNullOrEmpty(PartialHash) && Partial) + BeatMapsClient.PopulateOnlineByKey(this, p_Callback); + else if (!string.IsNullOrEmpty(PartialHash) && Partial) + BeatMapsClient.PopulateOnlineByHash(this, p_Callback); } //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/SDK/Game/BeatMaps/MapVersion.cs b/BeatSaberPlus/SDK/Game/BeatMaps/MapVersion.cs index 5073f56..1b84373 100644 --- a/BeatSaberPlus/SDK/Game/BeatMaps/MapVersion.cs +++ b/BeatSaberPlus/SDK/Game/BeatMaps/MapVersion.cs @@ -108,7 +108,7 @@ public List GetDifficultiesPerCharacteristicSerializedName(string /// Callback(p_Valid, p_Bytes) public void CoverImageBytes(Action p_Callback) { - BeatMapsClient.WebClient.DownloadAsync(coverURL, CancellationToken.None, (p_Result) => + BeatMapsClient.WebClient.GetAsync(coverURL, CancellationToken.None, (p_Result) => { try { @@ -118,7 +118,7 @@ public void CoverImageBytes(Action p_Callback) return; } - p_Callback?.Invoke(true, p_Result); + p_Callback?.Invoke(true, p_Result.BodyBytes); } catch (Exception l_Exception) { @@ -133,10 +133,10 @@ public void CoverImageBytes(Action p_Callback) /// /// Cancellation token /// Callback on result - /// Progress reporter /// Should not retry in case of failure? + /// Progress reporter /// - public void ZipBytes(CancellationToken p_Token, Action p_Callback, IProgress p_Progress, bool p_DontRetry = true) - => BeatMapsClient.WebClient.DownloadAsync(downloadURL, p_Token, p_Callback, p_Progress, p_DontRetry); + public void ZipBytes(CancellationToken p_Token, Action p_Callback, bool p_DontRetry = true, IProgress p_Progress = null) + => BeatMapsClient.WebClient.DownloadAsync(downloadURL, p_Token, (x) => p_Callback?.Invoke(x?.BodyBytes ?? null), p_DontRetry, p_Progress); } } diff --git a/BeatSaberPlus/SDK/Game/BeatMapsClient.cs b/BeatSaberPlus/SDK/Game/BeatMapsClient.cs index 649c293..59ff079 100644 --- a/BeatSaberPlus/SDK/Game/BeatMapsClient.cs +++ b/BeatSaberPlus/SDK/Game/BeatMapsClient.cs @@ -1,12 +1,14 @@ using Newtonsoft.Json; using System; using System.IO; -using System.IO.Compression; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Web; +using BSP_ICSharpCode.SharpZipLib; +using BSP_ICSharpCode.SharpZipLib.Zip; +using System.Runtime.Remoting; +using System.Security.Cryptography; namespace BeatSaberPlus.SDK.Game { @@ -45,7 +47,7 @@ public class BeatMapsClient internal static void Init() { m_CacheFolder = $"UserData/{CP_SDK.ChatPlexSDK.ProductName}/Cache/BeatMaps/"; - m_WebClient = new CP_SDK.Network.WebClient(); + m_WebClient = new CP_SDK.Network.WebClient("", TimeSpan.FromSeconds(10)); m_WebClient.Timeout = 10; m_WebClient.DownloadTimeout = 2 * 60; @@ -116,6 +118,30 @@ public static void PopulateOnlineByKey(BeatMaps.MapDetail p_BeatMap, Action p_Callback) + { + m_WebClient.GetAsync("https://api.beatsaver.com/maps/hash/" + p_BeatMap.PartialHash, CancellationToken.None, (p_Result) => + { + try + { + if (!p_Result.IsSuccessStatusCode) + { + p_Callback?.Invoke(false); + return; + } + + JsonConvert.PopulateObject(p_Result.BodyString, p_BeatMap); + p_BeatMap.Partial = false; + p_Callback?.Invoke(true); + } + catch (Exception l_Exception) + { + CP_SDK.ChatPlexSDK.Logger.Error("[SDK.Game][BeatMapsClient.PopulateOnlineByHash] Error :"); + CP_SDK.ChatPlexSDK.Logger.Error(l_Exception); + p_Callback?.Invoke(false); + } + }); + } public static void GetOnlineByHash(string p_Hash, Action p_Callback) { m_WebClient.GetAsync("https://api.beatsaver.com/maps/hash/" + p_Hash, CancellationToken.None, (p_Result) => @@ -142,7 +168,7 @@ public static void GetOnlineByHash(string p_Hash, Action p_Callback) { - m_WebClient.GetAsync("https://api.beatsaver.com/search/text/0?sortOrder=Relevance&q=" + HttpUtility.UrlEncode(p_Query), CancellationToken.None, (p_Result) => + m_WebClient.GetAsync("https://api.beatsaver.com/search/text/0?sortOrder=Relevance&q=" + CP_SDK_WebSocketSharp.Net.HttpUtility.UrlEncode(p_Query), CancellationToken.None, (p_Result) => { try { @@ -387,7 +413,7 @@ public static void DownloadSong(BeatMaps.MapDetail p_Song, BeatMaps.MapVersion p else CP_SDK.ChatPlexSDK.Logger.Error("[SDK.Game][BeatMapsClient] Failed to download Song!"); } - }, p_Progress); + }, true, p_Progress); } //////////////////////////////////////////////////////////////////////////// @@ -432,14 +458,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE p_Token.ThrowIfCancellationRequested(); Stream l_ZIPStream = new MemoryStream(p_ZIPBytes); + l_ZIPStream.Position = 0; try { CP_SDK.ChatPlexSDK.Logger.Info("[SDK.Game][BeatMapsClient] Extracting..."); - /// Create ZIP archive - ZipArchive l_ZIPArchive = new ZipArchive(l_ZIPStream, ZipArchiveMode.Read); - /// Prepare base path string l_BasePath = p_Song.id + " (" + p_Song.metadata.songName + " - " + p_Song.metadata.levelAuthorName + ")"; l_BasePath = string.Join("", l_BasePath.Split((Path.GetInvalidFileNameChars().Concat(Path.GetInvalidPathChars()).ToArray()))); @@ -465,28 +489,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE CP_SDK.ChatPlexSDK.Logger.Info("[SDK.Game][BeatMapsClient] " + l_OutPath); - foreach (var l_Entry in l_ZIPArchive.Entries) - { - /// Most likely a folder - if (string.IsNullOrEmpty(l_Entry.Name.Trim()) || l_Entry.Name != l_Entry.FullName) - continue; - - /// Name instead of FullName for better security and because song zips don't have nested directories anyway - var l_EntryPath = Path.Combine(l_OutPath, l_Entry.Name); + new FastZip().ExtractZip(l_ZIPStream, l_OutPath, FastZip.Overwrite.Always, null, null, null, true, false, false); - /// Either we're overwriting or there's no existing file - if (p_Overwrite || !File.Exists(l_EntryPath)) - l_Entry.ExtractToFile(l_EntryPath, p_Overwrite); - } - - l_ZIPArchive.Dispose(); l_ZIPStream.Close(); + l_ZIPStream = null; return (true, l_BasePath); } catch (Exception p_Exception) { l_ZIPStream.Close(); + l_ZIPStream = null; if (p_Exception is TaskCanceledException) throw p_Exception; @@ -494,6 +507,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE CP_SDK.ChatPlexSDK.Logger.Error("[SDK.Game][BeatMapsClient] Unable to extract ZIP! Exception"); CP_SDK.ChatPlexSDK.Logger.Error(p_Exception); } + finally + { + if (l_ZIPStream != null) + l_ZIPStream.Close(); + } return (false, ""); } diff --git a/BeatSaberPlus/SDK/Game/LevelData.cs b/BeatSaberPlus/SDK/Game/LevelData.cs index 303def1..a1966c8 100644 --- a/BeatSaberPlus/SDK/Game/LevelData.cs +++ b/BeatSaberPlus/SDK/Game/LevelData.cs @@ -20,6 +20,17 @@ public class LevelData /// public int MaxMultipliedScore { get; internal set; } + /// + /// Level has rotations events + /// + public bool HasRotations + { + get + { + return Data?.transformedBeatmapData?.spawnRotationEventsCount > 0; + } + } + /// /// Is a noodle extension map? /// diff --git a/BeatSaberPlus/SDK/Game/LevelSelection.cs b/BeatSaberPlus/SDK/Game/LevelSelection.cs index 4028914..5faab26 100644 --- a/BeatSaberPlus/SDK/Game/LevelSelection.cs +++ b/BeatSaberPlus/SDK/Game/LevelSelection.cs @@ -78,23 +78,23 @@ private static IEnumerator LevelSelection_SelectLevelCategory(LevelSelectionNavi { yield return new WaitUntil(() => !p_LevelSelectionNavigationController || !p_LevelSelectionNavigationController.isInTransition); - if (Logic.ActiveScene != Logic.SceneType.Menu) + if (Logic.ActiveScene != Logic.ESceneType.Menu) yield break; if (!p_LevelSelectionNavigationController || !p_LevelSelectionNavigationController.isInViewControllerHierarchy || !p_LevelSelectionNavigationController.isActiveAndEnabled) yield break; - var l_LevelFilteringNavigationController = p_LevelSelectionNavigationController.GetField("_levelFilteringNavigationController"); + var l_LevelFilteringNavigationController = p_LevelSelectionNavigationController._levelFilteringNavigationController; if (!l_LevelFilteringNavigationController) yield break; if (l_LevelFilteringNavigationController.selectedLevelCategory != SelectLevelCategoryViewController.LevelCategory.All) { - var l_Selector = l_LevelFilteringNavigationController.GetField("_selectLevelCategoryViewController"); + var l_Selector = l_LevelFilteringNavigationController._selectLevelCategoryViewController; if (l_Selector != null && l_Selector) { - var l_SegmentControl = l_Selector.GetField("_levelFilterCategoryIconSegmentedControl"); - var l_Tags = l_Selector.GetField("_levelCategoryInfos"); + var l_SegmentControl = l_Selector._levelFilterCategoryIconSegmentedControl; + var l_Tags = l_Selector._levelCategoryInfos; var l_IndexToSelect = l_Tags.Select((x => x.levelCategory)).ToList().IndexOf(SelectLevelCategoryViewController.LevelCategory.All); /// Multiplayer : missing extension @@ -105,7 +105,7 @@ private static IEnumerator LevelSelection_SelectLevelCategory(LevelSelectionNavi l_Selector.LevelFilterCategoryIconSegmentedControlDidSelectCell(l_SegmentControl, l_IndexToSelect); CP_SDK.Unity.MTCoroutineStarter.Start(LevelSelection_FilterLevel( - l_LevelFilteringNavigationController.GetField("_levelSearchViewController"), + l_LevelFilteringNavigationController._levelSearchViewController, true )); } @@ -113,7 +113,7 @@ private static IEnumerator LevelSelection_SelectLevelCategory(LevelSelectionNavi else { CP_SDK.Unity.MTCoroutineStarter.Start(LevelSelection_FilterLevel( - l_LevelFilteringNavigationController.GetField("_levelSearchViewController"), + l_LevelFilteringNavigationController._levelSearchViewController, false )); } @@ -126,7 +126,7 @@ private static IEnumerator LevelSelection_SelectLevelCategory(LevelSelectionNavi /// private static IEnumerator LevelSelection_FilterLevel(LevelSearchViewController p_LevelSearchViewController, bool p_Wait) { - if (Logic.ActiveScene != Logic.SceneType.Menu) + if (Logic.ActiveScene != Logic.ESceneType.Menu) yield break; if (p_LevelSearchViewController == null || !p_LevelSearchViewController || m_PendingFilterSong == null) @@ -139,7 +139,7 @@ private static IEnumerator LevelSelection_FilterLevel(LevelSearchViewController if (!p_LevelSearchViewController || !p_LevelSearchViewController.isInViewControllerHierarchy || !p_LevelSearchViewController.isActiveAndEnabled) yield break; - if (Logic.ActiveScene != Logic.SceneType.Menu) + if (Logic.ActiveScene != Logic.ESceneType.Menu) yield break; } @@ -147,7 +147,7 @@ private static IEnumerator LevelSelection_FilterLevel(LevelSearchViewController { p_LevelSearchViewController.didStartLoadingEvent -= LevelSearchViewController_didStartLoadingEvent; p_LevelSearchViewController.ResetCurrentFilterParams(); - var l_InputFieldView = p_LevelSearchViewController.GetField("_searchTextInputFieldView"); + var l_InputFieldView = p_LevelSearchViewController._searchTextInputFieldView; if (l_InputFieldView != null && l_InputFieldView) { //l_InputFieldView.SetText(m_PendingFilterSong.songName); @@ -179,12 +179,12 @@ private static void LevelSearchViewController_didStartLoadingEvent(LevelSearchVi try { - var l_Filter = p_LevelSearchViewController.GetField("_currentFilterParams"); + var l_Filter = p_LevelSearchViewController._currentFilterParams; if (l_Filter != null && l_Filter.filterByLevelIds) { p_LevelSearchViewController.ResetCurrentFilterParams(); - var l_InputFieldView = p_LevelSearchViewController.GetField("_searchTextInputFieldView"); + var l_InputFieldView = p_LevelSearchViewController._searchTextInputFieldView; if (l_InputFieldView != null && l_InputFieldView) { l_InputFieldView.UpdateClearButton(); diff --git a/BeatSaberPlus/SDK/Game/Levels.cs b/BeatSaberPlus/SDK/Game/Levels.cs index fab9d2e..248f25e 100644 --- a/BeatSaberPlus/SDK/Game/Levels.cs +++ b/BeatSaberPlus/SDK/Game/Levels.cs @@ -13,14 +13,11 @@ namespace BeatSaberPlus.SDK.Game /// public class Levels { - /// - /// Get level cancellation token - /// - private static CancellationTokenSource m_GetLevelCancellationTokenSource; - /// - /// Get status cancellation token - /// - private static CancellationTokenSource m_GetStatusCancellationTokenSource; +#if BEATSABER_1_31_0_OR_NEWER + private static BeatmapCharacteristicCollection m_BeatmapCharacteristicCollection; +#endif + private static CancellationTokenSource m_GetLevelCancellationTokenSource; + private static CancellationTokenSource m_GetStatusCancellationTokenSource; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -165,12 +162,14 @@ public static void PlaySong(IBeatmapLevel p_Level, var l_DifficultyBeatmap = p_Level.beatmapLevelData.GetDifficultyBeatmap(p_Characteristic, p_Difficulty); +#if BEATSABER_1_31_0_OR_NEWER l_MenuSceneSetupData.StartStandardLevel( gameMode: "Solo", difficultyBeatmap: l_DifficultyBeatmap, previewBeatmapLevel: p_Level, overrideEnvironmentSettings: p_OverrideEnvironmentSettings, overrideColorScheme: p_ColorScheme, + beatmapOverrideColorScheme: null, gameplayModifiers: p_GameplayModifiers ?? new GameplayModifiers(), playerSpecificSettings: p_PlayerSettings ?? new PlayerSpecificSettings(), practiceSettings: null, @@ -178,9 +177,29 @@ public static void PlaySong(IBeatmapLevel p_Level, useTestNoteCutSoundEffects: false, startPaused: false, beforeSceneSwitchCallback: null, + afterSceneSwitchCallback: null, levelFinishedCallback: (p_StandardLevelScenesTransitionSetupData, p_Results) => p_SongFinishedCallback?.Invoke(p_StandardLevelScenesTransitionSetupData, p_Results, l_DifficultyBeatmap), levelRestartedCallback: null ); +#else + l_MenuSceneSetupData.StartStandardLevel( + gameMode: "Solo", + difficultyBeatmap: l_DifficultyBeatmap, + previewBeatmapLevel: p_Level, + overrideEnvironmentSettings: p_OverrideEnvironmentSettings, + overrideColorScheme: p_ColorScheme, + gameplayModifiers: p_GameplayModifiers ?? new GameplayModifiers(), + playerSpecificSettings: p_PlayerSettings ?? new PlayerSpecificSettings(), + practiceSettings: null, + backButtonText: p_MenuButtonText, + useTestNoteCutSoundEffects: false, + startPaused: false, + beforeSceneSwitchCallback: null, + afterSceneSwitchCallback: null, + levelFinishedCallback: (p_StandardLevelScenesTransitionSetupData, p_Results) => p_SongFinishedCallback?.Invoke(p_StandardLevelScenesTransitionSetupData, p_Results, l_DifficultyBeatmap), + levelRestartedCallback: null + ); +#endif } catch (Exception l_Exception) { @@ -262,9 +281,19 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE /// public static BeatmapCharacteristicSO GetCharacteristicSOBySerializedName(string p_Name) { +#if BEATSABER_1_31_0_OR_NEWER + if (m_BeatmapCharacteristicCollection == null) + { + var l_CustomLevelLoader = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + m_BeatmapCharacteristicCollection = l_CustomLevelLoader?._beatmapCharacteristicCollection; + } + + return m_BeatmapCharacteristicCollection.GetBeatmapCharacteristicBySerializedName(SanitizeCharacteristic(p_Name)); +#else return SongCore.Loader.beatmapCharacteristicCollection.GetBeatmapCharacteristicBySerializedName( SanitizeCharacteristic(p_Name) ); +#endif } /// /// Sanitize characteristic diff --git a/BeatSaberPlus/SDK/Game/Logic.cs b/BeatSaberPlus/SDK/Game/Logic.cs index efa40aa..45b1e5f 100644 --- a/BeatSaberPlus/SDK/Game/Logic.cs +++ b/BeatSaberPlus/SDK/Game/Logic.cs @@ -10,22 +10,10 @@ namespace BeatSaberPlus.SDK.Game /// public class Logic { - /// - /// Last main scene was not menu ? - /// - private static bool m_LastMainSceneWasNotMenu = false; - /// - /// Was in replay ? - /// - private static bool m_WasInReplay = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - /// /// Scenes /// - public enum SceneType + public enum ESceneType { None, Menu, @@ -35,37 +23,23 @@ public enum SceneType //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Active scene type - /// - public static SceneType ActiveScene { get; private set; } = SceneType.None; - /// - /// Current level data - /// - public static LevelData LevelData { get; private set; } = null; - /// - /// Level completion data - /// - public static LevelCompletionData LevelCompletionData { get; private set; } = null; + private static bool m_LastMainSceneWasNotMenu = false; + private static bool m_WasInReplay = false; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// On scene change - /// - public static event Action OnSceneChange; - /// - /// On menu scene loaded - /// - public static event Action OnMenuSceneLoaded; - /// - /// On level started - /// - public static event Action OnLevelStarted; - /// - /// On level ended - /// + public static ESceneType ActiveScene { get; private set; } = ESceneType.None; + public static LevelData LevelData { get; private set; } = null; + public static LevelCompletionData LevelCompletionData { get; private set; } = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public static event Action OnSceneChange; + public static event Action OnMenuSceneLoaded; + + public static event Action OnLevelStarted; public static event Action OnLevelEnded; //////////////////////////////////////////////////////////////////////////// @@ -99,7 +73,7 @@ private static void SceneManager_activeSceneChanged(UnityEngine.SceneManagement. OnGameSceneActive(); else if (p_Next.name == "MainMenu") { - if (ActiveScene != SceneType.Menu) + if (ActiveScene != ESceneType.Menu) OnMenuSceneActive(); var l_GameScenesManager = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); @@ -138,7 +112,7 @@ private static void OnMenuSceneActive() #endif try { - ActiveScene = SceneType.Menu; + ActiveScene = ESceneType.Menu; LevelData = null; CP_SDK.ChatPlexSDK.Fire_OnGenericMenuScene(); @@ -159,6 +133,7 @@ private static void OnMenuSceneActive() /// On menu scene loaded /// /// Transition object + /// Container private static void OnMenuSceneLoadedFresh(ScenesTransitionSetupDataSO p_Object, DiContainer p_DiContainer) { #if DEBUG_SCENES || DEBUG @@ -172,7 +147,7 @@ private static void OnMenuSceneLoadedFresh(ScenesTransitionSetupDataSO p_Object, UI.LevelDetail.Init(); - ActiveScene = SceneType.Menu; + ActiveScene = ESceneType.Menu; LevelData = null; LevelCompletionData = null; m_WasInReplay = false; @@ -201,7 +176,7 @@ private static void OnGameSceneActive() try { /// Catch new map restart mechanic - if (ActiveScene == SceneType.Playing && LevelCompletionData != null) + if (ActiveScene == ESceneType.Playing && LevelCompletionData != null) { OnLevelEnded?.Invoke(LevelCompletionData); @@ -221,7 +196,7 @@ private static void OnGameSceneActive() } } - ActiveScene = SceneType.Playing; + ActiveScene = ESceneType.Playing; m_WasInReplay = Scoring.IsInReplay; CP_SDK.ChatPlexSDK.Fire_OnGenericPlayingScene(); diff --git a/BeatSaberPlus/SDK/Game/PlayerAvatarPicture.cs b/BeatSaberPlus/SDK/Game/PlayerAvatarPicture.cs new file mode 100644 index 0000000..10a27f6 --- /dev/null +++ b/BeatSaberPlus/SDK/Game/PlayerAvatarPicture.cs @@ -0,0 +1,161 @@ +using CP_SDK.Network; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; + +namespace BeatSaberPlus.SDK.Game +{ + /// + /// Player avatar picture provider + /// + public static class PlayerAvatarPicture + { + private static Dictionary m_AvatarCache = new Dictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get player avatar picture + /// + /// ID of the player + /// Cancellation token + /// Request callback + public static void GetPlayerAvatarPicture(string p_PlayerID, CancellationToken p_CancellationToken, Action p_Callback) + { + lock (m_AvatarCache) + { + if (m_AvatarCache.TryGetValue(p_PlayerID, out var l_Avatar)) + { + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => p_Callback?.Invoke(l_Avatar)); + return; + } + } + + GetScoreSaberAvatarPicture(p_PlayerID, p_CancellationToken, p_Callback, () => + { + GetBeatLeaderAvatarPicture(p_PlayerID, p_CancellationToken, p_Callback, () => p_Callback?.Invoke(null)); + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get avatar picture from ScoreSaber + /// + /// ID of the player + /// Cancellation token + /// Request callback + /// On error callback + private static void GetScoreSaberAvatarPicture(string p_PlayerID, CancellationToken p_CancellationToken, Action p_Callback, Action p_OnFailCallback) + { + WebClient.GlobalClient.GetAsync($"https://cdn.scoresaber.com/avatars/{p_PlayerID}.jpg", p_CancellationToken, (p_AvatarResult) => + { + try + { + if (p_AvatarResult == null || !p_AvatarResult.IsSuccessStatusCode || p_AvatarResult.BodyBytes?.Length == 0) + { + p_OnFailCallback?.Invoke(); + return; + } + + ProcessAvatarBytes(p_PlayerID, p_Callback, p_AvatarResult.BodyBytes); + } + catch (Exception l_Exception) + { + CP_SDK.ChatPlexSDK.Logger.Error("[BeatSaberPlus.SDK.Game][PlayerAvatarPicture.GetScoreSaberAvatarPicture] Error:"); + CP_SDK.ChatPlexSDK.Logger.Error(l_Exception); + p_OnFailCallback?.Invoke(); + } + }); + } + /// + /// Get avatar picture from BeatLeader + /// + /// ID of the player + /// Cancellation token + /// Request callback + /// On error callback + private static void GetBeatLeaderAvatarPicture(string p_PlayerID, CancellationToken p_CancellationToken, Action p_Callback, Action p_OnFailCallback) + { + WebClient.GlobalClient.GetAsync($"https://api.beatleader.xyz/player/{p_PlayerID}", p_CancellationToken, (p_PlayerResult) => + { + try + { + if (p_PlayerResult == null || !p_PlayerResult.IsSuccessStatusCode || p_PlayerResult.BodyBytes?.Length == 0) + { + p_OnFailCallback?.Invoke(); + return; + } + + var l_JSON = JObject.Parse(p_PlayerResult.BodyString); + if (l_JSON == null || !l_JSON.ContainsKey("avatar")) + { + p_OnFailCallback?.Invoke(); + return; + } + + WebClient.GlobalClient.GetAsync(l_JSON["avatar"].Value(), p_CancellationToken, (p_AvatarResult) => + { + try + { + if (p_AvatarResult == null || !p_AvatarResult.IsSuccessStatusCode || p_AvatarResult.BodyBytes?.Length == 0) + { + p_OnFailCallback?.Invoke(); + return; + } + + ProcessAvatarBytes(p_PlayerID, p_Callback, p_AvatarResult.BodyBytes); + } + catch (Exception l_Exception) + { + CP_SDK.ChatPlexSDK.Logger.Error("[BeatSaberPlus.SDK.Game][PlayerAvatarPicture.GetBeatLeaderAvatarPicture_2] Error:"); + CP_SDK.ChatPlexSDK.Logger.Error(l_Exception); + p_OnFailCallback?.Invoke(); + } + }); + } + catch (Exception l_Exception) + { + CP_SDK.ChatPlexSDK.Logger.Error("[BeatSaberPlus.SDK.Game][PlayerAvatarPicture.GetBeatLeaderAvatarPicture] Error:"); + CP_SDK.ChatPlexSDK.Logger.Error(l_Exception); + p_OnFailCallback?.Invoke(); + } + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Process received avatar body bytes + /// + /// ID of the player + /// Request callback + /// Avatar bytes + private static void ProcessAvatarBytes(string p_PlayerID, Action p_Callback, byte[] p_BodyBytes) + { + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_Texture = CP_SDK.Unity.Texture2DU.CreateFromRaw(p_BodyBytes); + if (l_Texture == null) + { + p_Callback?.Invoke(null); + return; + } + + var l_Avatar = Sprite.Create(l_Texture, new Rect(0, 0, l_Texture.width, l_Texture.height), new Vector2(0.5f, 0.5f), 100); + lock (m_AvatarCache) + { + if (!m_AvatarCache.ContainsKey(p_PlayerID)) + m_AvatarCache.Add(p_PlayerID, l_Avatar); + } + + p_Callback?.Invoke(l_Avatar); + }); + } + } +} diff --git a/BeatSaberPlus/SDK/Game/Scoring.cs b/BeatSaberPlus/SDK/Game/Scoring.cs index 29b8e35..28444ca 100644 --- a/BeatSaberPlus/SDK/Game/Scoring.cs +++ b/BeatSaberPlus/SDK/Game/Scoring.cs @@ -8,47 +8,22 @@ namespace BeatSaberPlus.SDK.Game /// public static class Scoring { - /// - /// Is initialized - /// private static bool m_Init; - /// - /// Is ScoreSaber present - /// - private static bool m_IsScoreSaberPresent; - /// - /// ScoreSaber PatchHandleHMDUnmounted harmony patch - /// - private static MethodBase m_ScoreSaber_playbackEnabled; - - /// - /// Is BeatLeader present - /// - private static bool m_IsBeatLeaderPresent; - /// - /// BeatLeader PatchHandleHMDUnmounted harmony patch - /// - private static MethodBase m_BeatLeader_RecorderUtils_OnActionButtonWasPressed; + private static bool m_IsScoreSaberPresent; + private static MethodBase m_ScoreSaber_playbackEnabled; + private static bool m_IsBeatLeaderPresent; + private static MethodBase m_BeatLeader_RecorderUtils_OnActionButtonWasPressed; private static PropertyInfo m_BeatLeader_ReplayerMenuLauncher_IsStartedAsReplay; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Is ScoreSaber mod installed - /// - public static bool IsScoreSaberPresent { get { Init(); return m_IsScoreSaberPresent; } } - /// - /// Is BeatLeader mod installed - /// - public static bool IsBeatLeaderPresent { get { Init(); return m_IsBeatLeaderPresent; } } + public static bool IsScoreSaberPresent { get { Init(); return m_IsScoreSaberPresent; } } + public static bool IsBeatLeaderPresent { get { Init(); return m_IsBeatLeaderPresent; } } - /// - /// Is in ScoreSaber or BeatLeader replay - /// - public static bool IsInReplay { get { return ScoreSaber_IsInReplay() || BeatLeader_IsInReplay(); } } + public static bool IsInReplay { get { return ScoreSaber_IsInReplay() || BeatLeader_IsInReplay(); } } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/SDK/Game/UserPlatform.cs b/BeatSaberPlus/SDK/Game/UserPlatform.cs index b16d542..2773177 100644 --- a/BeatSaberPlus/SDK/Game/UserPlatform.cs +++ b/BeatSaberPlus/SDK/Game/UserPlatform.cs @@ -5,7 +5,7 @@ namespace BeatSaberPlus.SDK.Game /// /// UserPlatform helper /// - public class UserPlatform + public static class UserPlatform { /// /// User ID cache @@ -33,7 +33,7 @@ public static string GetUserID() return m_UserID; } /// - /// Get User ID + /// Get User Name /// /// public static string GetUserName() @@ -57,11 +57,10 @@ private static void FetchPlatformInfos() try { var l_PlatformLeaderboardsModels = Resources.FindObjectsOfTypeAll(); - var l_FieldAccessor = typeof(PlatformLeaderboardsModel).GetField("_platformUserModel", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); foreach (var l_Current in l_PlatformLeaderboardsModels) { - var l_PlatformUserModel = l_FieldAccessor.GetValue(l_Current) as IPlatformUserModel; + var l_PlatformUserModel = l_Current._platformUserModel; if (l_PlatformUserModel == null) continue; diff --git a/BeatSaberPlus/SDK/UI/BSMLSettingFormartter.cs b/BeatSaberPlus/SDK/UI/BSMLSettingFormartter.cs deleted file mode 100644 index c8944ff..0000000 --- a/BeatSaberPlus/SDK/UI/BSMLSettingFormartter.cs +++ /dev/null @@ -1,105 +0,0 @@ -using BeatSaberMarkupLanguage.Parser; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// BSML Setting formatter - /// - public class BSMLSettingFormartter - { - private static BSMLAction m_DateMonthFrom2018; - private static BSMLAction m_Time; - private static BSMLAction m_Milliseconds; - private static BSMLAction m_Seconds; - private static BSMLAction m_Minutes; - private static BSMLAction m_Percentage; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public static BSMLAction DateMonthFrom2018 { get { if (m_DateMonthFrom2018 == null) m_DateMonthFrom2018 = BuildAction(nameof(FNDateMonthFrom2018)); return m_DateMonthFrom2018; } } - public static BSMLAction Time { get { if (m_Time == null) m_Time = BuildAction(nameof(FNTime)); return m_Time; } } - public static BSMLAction Milliseconds { get { if (m_Milliseconds == null) m_Milliseconds = BuildAction(nameof(FNMilliseconds)); return m_Milliseconds; } } - public static BSMLAction Seconds { get { if (m_Seconds == null) m_Seconds = BuildAction(nameof(FNSeconds)); return m_Seconds; } } - public static BSMLAction Minutes { get { if (m_Minutes == null) m_Minutes = BuildAction(nameof(FNMinutes)); return m_Minutes; } } - public static BSMLAction Percentage { get { if (m_Percentage == null) m_Percentage = BuildAction(nameof(FNPercentage)); return m_Percentage; } } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private static BSMLAction BuildAction(string p_Name) - { - return new BSMLAction(null, typeof(BSMLSettingFormartter).GetMethod(p_Name, System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On date setting changes - /// - /// New value - /// - private static string FNDateMonthFrom2018(int p_Value) - { - string[] s_Months = new string[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; - int l_Year = 2018 + (p_Value / 12); - - return s_Months[p_Value % 12] + " " + l_Year; - } - /// - /// Format time - /// - /// New value - /// - private static string FNTime(int p_Value) - { - int l_Minutes = p_Value / 60; - int l_Seconds = p_Value - (l_Minutes * 60); - - string l_Result = (l_Minutes != 0 ? l_Minutes : l_Seconds).ToString(); - if (l_Minutes != 0) - l_Result += "m " + l_Seconds + "s"; - else - l_Result += "s"; - - return l_Result; - } - /// - /// Format milliseconds - /// - /// New value - /// - private static string FNMilliseconds(int p_Value) - { - return p_Value + "ms"; - } - /// - /// Format seconds - /// - /// New value - /// - private static string FNSeconds(int p_Value) - { - return p_Value + " Second" + (p_Value > 1 ? "s" : ""); - } - /// - /// Format minutes - /// - /// New value - /// - private static string FNMinutes(int p_Value) - { - return p_Value + " Minute" + (p_Value > 1 ? "s" : ""); - } - /// - /// Format percentage - /// - /// New value - /// - private static string FNPercentage(float p_Value) - { - return System.Math.Round(p_Value * 100f, 2) + " %"; - } - } -} diff --git a/BeatSaberPlus/SDK/UI/Backgroundable.cs b/BeatSaberPlus/SDK/UI/Backgroundable.cs deleted file mode 100644 index 416f330..0000000 --- a/BeatSaberPlus/SDK/UI/Backgroundable.cs +++ /dev/null @@ -1,46 +0,0 @@ -using UnityEngine; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// Backgroundable helper - /// - public class Backgroundable - { - /// - /// Set Backgroundable opacity - /// - /// Backgroundable game object - /// New opacity - /// - public static bool SetOpacity(GameObject p_Backgroundable, float p_Opacity) - { - if (p_Backgroundable == null || !p_Backgroundable) - return false; - - var l_Image = p_Backgroundable?.GetComponent() ?? null; - if (l_Image) - { - /// Update background color - var l_Color = l_Image.color; - l_Color.a = p_Opacity; - - l_Image.color = l_Color; - - return true; - } - - return false; - } - /// - /// Set Backgroundable opacity - /// - /// Backgroundable game object - /// New opacity - /// - public static bool SetOpacity(BeatSaberMarkupLanguage.Components.Backgroundable p_Backgroundable, float p_Opacity) - { - return SetOpacity(p_Backgroundable != null ? p_Backgroundable.gameObject : null, p_Opacity); - } - } -} diff --git a/BeatSaberPlus/SDK/UI/CP_SDK_UI_IViewControllerBridge.cs b/BeatSaberPlus/SDK/UI/CP_SDK_UI_IViewControllerBridge.cs new file mode 100644 index 0000000..6be690f --- /dev/null +++ b/BeatSaberPlus/SDK/UI/CP_SDK_UI_IViewControllerBridge.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BeatSaberPlus.SDK.UI +{ + /// + /// CP_SDK.UI.IViewController bridge component + /// + public class CP_SDK_UI_IViewControllerBridge : CP_SDK.UI.IViewController + { + public override RectTransform RTransform => GetComponent()?.RTransform; + public override RectTransform ModalContainerRTransform => GetComponent()?.ModalContainerRTransform; + public override CanvasGroup CGroup => GetComponent()?.CGroup; + public override CP_SDK.UI.IScreen CurrentScreen => null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Activate + /// + /// Target screen + public override void __Activate(CP_SDK.UI.IScreen p_Screen) { } + /// + /// Deactivate + /// + public override void __Deactivate() { } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Show a modal + /// + /// Modal to show + public override void ShowModal(CP_SDK.UI.IModal p_Modal) + => GetComponent()?.ShowModal(p_Modal); + /// + /// Close a modal + /// + /// Modal to close + public override void CloseModal(CP_SDK.UI.IModal p_Modal) + => GetComponent()?.CloseModal(p_Modal); + /// + /// Close all modals + /// + public override void CloseAllModals() + => GetComponent()?.CloseAllModals(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Show color picker modal + /// + /// Base value + /// Support opacity? + /// On changed callback + /// On cancel callback + public override void ShowColorPickerModal(Color p_Value, bool p_Opacity, Action p_Callback, Action p_CancelCallback = null) + => GetComponent()?.ShowColorPickerModal(p_Value, p_Opacity, p_Callback, p_CancelCallback); + /// + /// Show the confirmation modal + /// + /// Message to display + /// Callback + public override void ShowConfirmationModal(string p_Message, Action p_Callback) + => GetComponent()?.ShowConfirmationModal(p_Message, p_Callback); + /// + /// Show the dropdown modal + /// + /// Available options + /// Selected option + /// Callback + public override void ShowDropdownModal(List p_Options, string p_Selected, Action p_Callback) + => GetComponent()?.ShowDropdownModal(p_Options, p_Selected, p_Callback); + /// + /// Show the keyboard modal + /// + /// Value + /// Callback + /// On cancel callback + /// Custom keys + public override void ShowKeyboardModal(string p_Value, Action p_Callback, Action p_CancelCallback = null, List<(string, Action, string)> p_CustomKeys = null) + => GetComponent()?.ShowKeyboardModal(p_Value, p_Callback, p_CancelCallback, p_CustomKeys); + /// + /// Show the loading modal + /// + /// Message to show + /// Show cancel button + /// On cancel callback + public override void ShowLoadingModal(string p_Message = "", bool p_CancelButton = false, Action p_CancelCallback = null) + => GetComponent()?.ShowLoadingModal(p_Message, p_CancelButton, p_CancelCallback); + /// + /// Show the message modal + /// + /// Message to display + /// Callback + public override void ShowMessageModal(string p_Message, Action p_Callback = null) + => GetComponent()?.ShowMessageModal(p_Message, p_Callback); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get current value + /// + /// + public override string KeyboardModal_GetValue() + => GetComponent()?.KeyboardModal_GetValue(); + /// + /// Set value + /// + /// New value + public override void KeyboardModal_SetValue(string p_Value) + => GetComponent()?.KeyboardModal_SetValue(p_Value); + /// + /// Append + /// + /// Value to append + public override void KeyboardModal_Append(string p_ToAppend) + => GetComponent()?.KeyboardModal_Append(p_ToAppend); + /// + /// Set message + /// + /// New message + public override void LoadingModal_SetMessage(string p_Message) + => GetComponent()?.LoadingModal_SetMessage(p_Message); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Close color picker modal + /// + public override void CloseColorPickerModal() + => GetComponent()?.CloseColorPickerModal(); + /// + /// Close the confirmation modal + /// + public override void CloseConfirmationModal() + => GetComponent()?.CloseConfirmationModal(); + /// + /// Close the dropdown modal + /// + public override void CloseDropdownModal() + => GetComponent()?.CloseDropdownModal(); + /// + /// Close the keyboard modal + /// + public override void CloseKeyboardModal() + => GetComponent()?.CloseKeyboardModal(); + /// + /// Close the loading modal + /// + public override void CloseLoadingModal() + => GetComponent()?.CloseLoadingModal(); + /// + /// Close the message modal + /// + public override void CloseMessageModal() + => GetComponent()?.CloseMessageModal(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Show the tooltip + /// + /// World position + /// Tooltip text + public override void ShowTooltip(Vector3 p_Position, string p_Text) + => GetComponent()?.ShowTooltip(p_Position, p_Text); + /// + /// Hide the tooltip + /// + public override void HideTooltip() + => GetComponent()?.HideTooltip(); + } +} diff --git a/BeatSaberPlus/SDK/UI/ColorSetting.cs b/BeatSaberPlus/SDK/UI/ColorSetting.cs deleted file mode 100644 index 7470660..0000000 --- a/BeatSaberPlus/SDK/UI/ColorSetting.cs +++ /dev/null @@ -1,48 +0,0 @@ -using BeatSaberMarkupLanguage.Parser; -using System.Linq; -using TMPro; - -using BSMLColorSetting = BeatSaberMarkupLanguage.Components.Settings.ColorSetting; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// Color setting helper - /// - public class ColorSetting - { - /// - /// Setup a color setting - /// - /// Setting to setûp - /// Action on change - /// New value - /// Should remove label - public static void Setup(BSMLColorSetting p_Setting, BSMLAction p_Action, UnityEngine.Color p_Value, bool p_RemoveLabel) - { - p_Setting.gameObject.SetActive(false); - - p_Value.a = 1.0f; - p_Setting.CurrentColor = p_Value; - - if (p_Action != null) - p_Setting.onChange = p_Action; - - p_Setting.updateOnChange = true; - - if (p_RemoveLabel) - { - UnityEngine.GameObject.Destroy(p_Setting.gameObject.GetComponentsInChildren().ElementAt(0).transform.gameObject); - - UnityEngine.RectTransform l_RectTransform = p_Setting.gameObject.transform.GetChild(1) as UnityEngine.RectTransform; - l_RectTransform.anchorMin = UnityEngine.Vector2.zero; - l_RectTransform.anchorMax = UnityEngine.Vector2.one; - l_RectTransform.sizeDelta = UnityEngine.Vector2.one; - - p_Setting.gameObject.GetComponent().preferredWidth = -1f; - } - - p_Setting.gameObject.SetActive(true); - } - } -} diff --git a/BeatSaberPlus/SDK/UI/Data/SongListCell.cs b/BeatSaberPlus/SDK/UI/Data/SongListCell.cs new file mode 100644 index 0000000..981d234 --- /dev/null +++ b/BeatSaberPlus/SDK/UI/Data/SongListCell.cs @@ -0,0 +1,134 @@ +using CP_SDK.UI; +using CP_SDK.UI.Components; +using CP_SDK.UI.Data; +using CP_SDK.Unity.Extensions; +using UnityEngine; + +namespace BeatSaberPlus.SDK.UI.Data +{ + /// + /// Song list cell + /// + public class SongListCell : IListCell + { + public CImage CoverMask; + public CImage Cover; + public CImage CoverFrame; + public CText Title; + public CText SubTitle; + public CText Duration; + public CText BPM; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build cell + /// + public override void Build() + { + if (RTransform) + return; + + base.Build(); + + CoverMask = UISystem.ImageFactory.Create("CoverMask", RTransform); + CoverMask.gameObject.AddComponent().showMaskGraphic = false; + CoverMask.SetSprite(UISystem.GetUIRoundBGSprite()); + CoverMask.SetType(UnityEngine.UI.Image.Type.Sliced); + CoverMask.SetPixelsPerUnitMultiplier(4f); + CoverMask.RTransform.anchorMin = new Vector2( 0.0f, 0.0f); + CoverMask.RTransform.anchorMax = new Vector2( 0.0f, 0.0f); + CoverMask.RTransform.pivot = new Vector2( 0.0f, 0.0f); + CoverMask.RTransform.sizeDelta = new Vector2( 9.0f, 9.0f); + CoverMask.RTransform.anchoredPosition = new Vector2( 0.5f, 0.5f); + + Cover = UISystem.ImageFactory.Create("Cover", CoverMask.RTransform); + Cover.RTransform.anchorMin = new Vector2( 0.0f, 0.0f); + Cover.RTransform.anchorMax = new Vector2( 1.0f, 1.0f); + Cover.RTransform.pivot = new Vector2( 0.5f, 0.5f); + Cover.RTransform.sizeDelta = new Vector2( -0.4f, -0.4f); + Cover.RTransform.anchoredPosition = new Vector2( 0.0f, 0.0f); + + CoverFrame = UISystem.ImageFactory.Create("CoverFrame", RTransform); + CoverFrame.SetSprite(UISystem.GetUIRoundSmoothFrameSprite()); + CoverFrame.SetType(UnityEngine.UI.Image.Type.Sliced); + CoverFrame.SetPixelsPerUnitMultiplier(20f); + CoverFrame.SetColor(ColorU.ToUnityColor("#CCCCCC")); + CoverFrame.RTransform.anchorMin = new Vector2( 0.0f, 0.0f); + CoverFrame.RTransform.anchorMax = new Vector2( 0.0f, 0.0f); + CoverFrame.RTransform.pivot = new Vector2( 0.0f, 0.0f); + CoverFrame.RTransform.sizeDelta = new Vector2( 9.0f, 9.0f); + CoverFrame.RTransform.anchoredPosition = new Vector2( 0.5f, 0.5f); + + Title = UISystem.TextFactory.Create("Title", RTransform); + Title.SetAlign(TMPro.TextAlignmentOptions.CaplineLeft); + Title.SetOverflowMode(TMPro.TextOverflowModes.Ellipsis); + Title.SetMargins(1.0f, 0.0f, 1.0f, 0.0f); + Title.SetStyle(TMPro.FontStyles.Bold); + Title.SetFontSizes(4.00f, 2.75f); + Title.RTransform.anchorMin = new Vector2( 0.0f, 0.45f); + Title.RTransform.anchorMax = new Vector2( 1.0f, 1.0f); + Title.RTransform.pivot = new Vector2( 0.5f, 0.5f); + Title.RTransform.sizeDelta = new Vector2(-20.0f, 0.0f); + Title.RTransform.anchoredPosition = new Vector2( 0.0f, 0.0f); + + SubTitle = UISystem.TextFactory.Create("SubTitle", RTransform); + SubTitle.SetAlign(TMPro.TextAlignmentOptions.CaplineLeft); + SubTitle.SetOverflowMode(TMPro.TextOverflowModes.Ellipsis); + SubTitle.SetMargins(1.0f, 0.0f, 1.0f, 0.0f); + SubTitle.SetFontSizes(2.8f, 2.00f); + SubTitle.SetColor(ColorU.ToUnityColor("#BBBBBB")); + SubTitle.RTransform.anchorMin = new Vector2( 0.0f, 0.0f); + SubTitle.RTransform.anchorMax = new Vector2( 1.0f, 0.55f); + SubTitle.RTransform.pivot = new Vector2( 0.5f, 0.5f); + SubTitle.RTransform.sizeDelta = new Vector2(-18.0f, 0.0f); + SubTitle.RTransform.anchoredPosition = new Vector2( 1.0f, 0.0f); + + Duration = UISystem.TextFactory.Create("Duration", RTransform); + Duration.SetAlign(TMPro.TextAlignmentOptions.CaplineRight); + Duration.SetOverflowMode(TMPro.TextOverflowModes.Truncate); + Duration.SetMargins(0.0f, 0.0f, 1.0f, 0.0f); + Duration.SetStyle(TMPro.FontStyles.Bold); + Duration.SetFontSizes(3.75f, 2.75f); + Duration.RTransform.anchorMin = new Vector2( 1.0f, 0.45f); + Duration.RTransform.anchorMax = new Vector2( 1.0f, 1.0f); + Duration.RTransform.pivot = new Vector2( 1.0f, 0.5f); + Duration.RTransform.sizeDelta = new Vector2( 10.0f, 0.0f); + Duration.RTransform.anchoredPosition = new Vector2( 0.0f, 0.0f); + + BPM = UISystem.TextFactory.Create("BPM", RTransform); + BPM.SetAlign(TMPro.TextAlignmentOptions.CaplineRight); + BPM.SetOverflowMode(TMPro.TextOverflowModes.Truncate); + BPM.SetMargins(0.0f, 0.0f, 1.0f, 0.0f); + BPM.SetFontSizes(2.8f, 2.00f); + BPM.SetColor(ColorU.ToUnityColor("#BBBBBB")); + BPM.RTransform.anchorMin = new Vector2( 1.0f, 0.0f); + BPM.RTransform.anchorMax = new Vector2( 1.0f, 0.55f); + BPM.RTransform.pivot = new Vector2( 1.0f, 0.5f); + BPM.RTransform.sizeDelta = new Vector2( 8.0f, 0.0f); + BPM.RTransform.anchoredPosition = new Vector2( 0.0f, 0.0f); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get cell height + /// + /// + public override float GetCellHeight() + => 10.0f; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Add self component + /// + /// Target gameobject + /// + protected override IListCell AddSelfComponent(GameObject p_Target) + => p_Target.GetComponent() ?? p_Target.AddComponent(); + } +} diff --git a/BeatSaberPlus/SDK/UI/Data/SongListController.cs b/BeatSaberPlus/SDK/UI/Data/SongListController.cs new file mode 100644 index 0000000..702a18f --- /dev/null +++ b/BeatSaberPlus/SDK/UI/Data/SongListController.cs @@ -0,0 +1,9 @@ +namespace BeatSaberPlus.SDK.UI.Data +{ + public interface SongListController + { + void OnSongListItemCoverFetched(SongListItem p_Item); + bool PlayPreviewAudio(); + float PreviewAudioVolume(); + } +} diff --git a/BeatSaberPlus/SDK/UI/Data/SongListItem.cs b/BeatSaberPlus/SDK/UI/Data/SongListItem.cs new file mode 100644 index 0000000..e799796 --- /dev/null +++ b/BeatSaberPlus/SDK/UI/Data/SongListItem.cs @@ -0,0 +1,457 @@ +using CP_SDK.UI.Data; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using UnityEngine; +using UnityEngine.Networking; + +namespace BeatSaberPlus.SDK.UI.Data +{ + /// + /// Song list item + /// + public class SongListItem : IListItem + { + private static Sprite m_DefaultCover = null; + private static Dictionary m_CoverCache = new Dictionary(); + private static Dictionary m_AudioClipCache = new Dictionary(); + private static SongPreviewPlayer m_SongPreviewPlayer = null; + private static CP_SDK.Misc.FastCancellationToken m_LoadAudioToken = new CP_SDK.Misc.FastCancellationToken(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private bool m_WasInit = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public string TitlePrefix = string.Empty; + public Game.BeatMaps.MapDetail BeatSaver_Map = null; + public IPreviewBeatmapLevel LocalLevel = null; + public Sprite Cover = null; + public string Tooltip = string.Empty; + public SongListController SongListController = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public bool Invalid => (LocalLevel == null && BeatSaver_Map == null); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /* + /// + /// Constructor + /// + /// Title prefix + /// Remote map + /// Local map + /// Hover hint text + /// User custom dama + /// Song list controller + public SongListItem( + string p_TitlePrefix, + Game.BeatMaps.MapDetail p_BeatSaver_Map, + CustomPreviewBeatmapLevel p_CustomLevel, + string p_HoverHint = "", + SongListController p_SongListController = null + ) + { + TitlePrefix = p_TitlePrefix; + BeatSaver_Map = p_BeatSaver_Map; + m_CustomLevel = p_CustomLevel; + m_HoverHint = p_HoverHint; + m_SongListController = p_SongListController; + } + */ + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void Init() + { + if (m_WasInit) + return; + + if (!m_DefaultCover) + { + var l_GamePrefab = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_GamePrefab) + m_DefaultCover = l_GamePrefab._coverImage?.sprite ?? null; + } + + if (!m_SongPreviewPlayer) + m_SongPreviewPlayer = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); + + if (LocalLevel == null && BeatSaver_Map != null && !BeatSaver_Map.Partial) + { + var l_LocalLevel = SongCore.Loader.GetLevelByHash(GetLevelHash().ToUpper()); + if (l_LocalLevel != null && SongCore.Loader.CustomLevels.ContainsKey(l_LocalLevel.customLevelPath)) + LocalLevel = l_LocalLevel; + } + + m_WasInit = true; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get level ID + /// + /// + public string GetLevelID() + { + if (BeatSaver_Map != null && BeatSaver_Map.SelectMapVersion() != null && BeatSaver_Map.SelectMapVersion().hash != null) + return $"custom_level_{BeatSaver_Map.SelectMapVersion().hash}"; + else if (LocalLevel != null) + return LocalLevel.levelID; + + return ""; + } + /// + /// Get level hash + /// + /// + public string GetLevelHash() + { + if (BeatSaver_Map != null && BeatSaver_Map.SelectMapVersion() != null && BeatSaver_Map.SelectMapVersion().hash != null) + return BeatSaver_Map.SelectMapVersion().hash.ToLower(); + else if (LocalLevel != null && LocalLevel.levelID.StartsWith("custom_level_")) + return LocalLevel.levelID.Replace("custom_level_", "").ToLower(); + + return ""; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On show + /// + public override void OnShow() + { + if (!(Cell is SongListCell l_SongListCell)) + return; + + Init(); + + var l_Title = ""; + var l_SubTitle = ""; + var l_Tooltip = Tooltip == null ? "" : Tooltip; + var l_BPMText = ""; + var l_DurationText = ""; + + if ((BeatSaver_Map != null && !BeatSaver_Map.Partial) || LocalLevel != null) + { + var l_HaveSong = SongCore.Loader.GetLevelById(GetLevelID()) != null; + var l_Scores = Game.Levels.GetScoresByHash(GetLevelHash(), out var l_HaveAnyScore, out var l_HaveAllScores); + + var l_MapName = LocalLevel != null ? LocalLevel.songName : BeatSaver_Map.name; + var l_MapAuthor = LocalLevel != null ? LocalLevel.levelAuthorName : BeatSaver_Map.metadata.levelAuthorName; + var l_MapSongAuthor = LocalLevel != null ? LocalLevel.songAuthorName : BeatSaver_Map.metadata.songAuthorName; + var l_Duration = LocalLevel != null ? LocalLevel.songDuration : BeatSaver_Map.metadata.duration; + var l_BPM = LocalLevel != null ? LocalLevel.beatsPerMinute : BeatSaver_Map.metadata.bpm; + + if (l_Scores.Count != 0) + { + foreach (var l_Row in l_Scores) + { + l_Tooltip += $"\n{l_Row.Key.serializedName} "; + foreach (var l_SubRow in l_Row.Value) + l_Tooltip += (l_SubRow.Item2 != -1 ? " " : "❌ "); + } + } + + var l_TitleBuilder = new StringBuilder(60); + //if (!string.IsNullOrWhiteSpace(TitlePrefix)) + // l_TitleBuilder.Append(TitlePrefix); + // + //if (BeatSaver_Map.ranked) + // l_TitleBuilder.Append("<#F8E600>"); + + if (l_HaveAllScores) + l_TitleBuilder.Append("<#52F700>"); + else if (l_HaveAnyScore) + l_TitleBuilder.Append("<#F8E600>"); + else if (l_HaveSong) + l_TitleBuilder.Append("<#CCCCCC>"); + else + l_TitleBuilder.Append("<#FFFFFF>"); + + l_TitleBuilder.Append(l_MapName); + + l_Title = l_TitleBuilder.ToString(); + l_SubTitle = $"{l_MapSongAuthor} <#FFFFFF>[{l_MapAuthor}]"; + l_BPMText = ((int)l_BPM).ToString(); + l_DurationText = l_Duration >= 0.0 ? $"{Math.Floor((double)l_Duration / 60):N0}:{Math.Floor((double)l_Duration % 60):00}" : "--"; + + LoadLevelCover(); + } + else if (BeatSaver_Map != null && BeatSaver_Map.Partial) + l_Title = "Loading from BeatSaver..."; + else + { + l_Title = "<#FF0000>Invalid song"; + l_SubTitle = LocalLevel != null ? LocalLevel.levelID.Replace("custom_level_", "") : ""; + } + + l_SongListCell.Cover.SetSprite(Cover ?? m_DefaultCover); + l_SongListCell.Title.SetText(l_Title); + l_SongListCell.SubTitle.SetText(l_SubTitle); + + if (!string.IsNullOrEmpty(l_DurationText)) + { + l_SongListCell.Duration.gameObject.SetActive(true); + l_SongListCell.Duration.SetText(l_DurationText); + } + else + l_SongListCell.Duration.gameObject.SetActive(false); + + if (!string.IsNullOrEmpty(l_BPMText)) + { + l_SongListCell.BPM.gameObject.SetActive(true); + l_SongListCell.BPM.SetText(l_BPMText); + } + else + l_SongListCell.BPM.gameObject.SetActive(false); + + l_SongListCell.Tooltip = l_Tooltip; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On select + /// + public override void OnSelect() + { + var l_PlayPreviewAudio = SongListController?.PlayPreviewAudio() ?? false; + var l_PreviewAudioVolume = SongListController?.PreviewAudioVolume() ?? 0.5f; + + if (m_SongPreviewPlayer == null || !m_SongPreviewPlayer || !l_PlayPreviewAudio) + return; + + var l_LocalSong = SongCore.Loader.GetLevelByHash(GetLevelHash()); + if (l_LocalSong != null) + { + if (m_AudioClipCache.TryGetValue(GetLevelHash(), out var l_AudioClip)) + { + if (m_SongPreviewPlayer.activeAudioClip == l_AudioClip) + return; + + m_SongPreviewPlayer.CrossfadeTo(l_AudioClip, l_PreviewAudioVolume, l_LocalSong.previewStartTime, l_LocalSong.previewDuration, () => { }); + } + else + { + m_LoadAudioToken.Cancel(); + CP_SDK.Unity.MTCoroutineStarter.Start(Coroutine_GetAudioAsync(l_LocalSong.songPreviewAudioClipPath, l_PreviewAudioVolume)); + } + } + else + { + /// Stop preview music if any + m_SongPreviewPlayer.CrossfadeToDefault(); + } + } + /// + /// On Unselect + /// + public override void OnUnselect() + { + + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Stop preview music if any + /// + public void StopPreviewMusic() + { + if (m_SongPreviewPlayer != null && m_SongPreviewPlayer) + m_SongPreviewPlayer.CrossfadeToDefault(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Load level cover + /// + private void LoadLevelCover() + { + if (!Cover && m_CoverCache.TryGetValue(GetLevelHash(), out var l_Cover)) + { + CoverLoaded(l_Cover); + return; + } + + var l_LocalSong = SongCore.Loader.GetLevelById(GetLevelHash()); + if (l_LocalSong != null) + { + var l_CoverTask = l_LocalSong.GetCoverImageAsync(CancellationToken.None); + _ = l_CoverTask.ContinueWith(p_CoverTaskResult => + { + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => CoverLoaded(p_CoverTaskResult.Result)); + }); + } + else if (BeatSaver_Map != null) + { + var l_CoverByte = Game.BeatMapsClient.GetCoverImageFromCacheByKey(BeatSaver_Map.id); + if (l_CoverByte != null && l_CoverByte.Length > 0) + { + var l_Texture = CP_SDK.Unity.Texture2DU.CreateFromRaw(l_CoverByte); + if (l_Texture != null) + CoverLoaded(Sprite.Create(l_Texture, new Rect(0, 0, l_Texture.width, l_Texture.height), new Vector2(0.5f, 0.5f), 100)); + } + else + { + /// Fetch cover + BeatSaver_Map.SelectMapVersion().CoverImageBytes((p_Valid, p_CoverTaskResult) => + { + if (p_Valid) + Game.BeatMapsClient.CacheCoverImage(BeatSaver_Map, p_CoverTaskResult); + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_Texture = CP_SDK.Unity.Texture2DU.CreateFromRaw(p_CoverTaskResult); + if (l_Texture != null) + CoverLoaded(Sprite.Create(l_Texture, new Rect(0, 0, l_Texture.width, l_Texture.height), new Vector2(0.5f, 0.5f), 100)); + }); + }); + } + } + } + /// + /// Level cover loaded + /// + /// Loaded cover + private void CoverLoaded(Sprite p_Cover) + { + Cover = p_Cover; + + if (!m_CoverCache.ContainsKey(GetLevelHash())) + m_CoverCache.Add(GetLevelHash(), p_Cover); + + if ((Cell is SongListCell l_SongListCell)) + l_SongListCell.Cover.SetSprite(Cover ?? m_DefaultCover); + + try + { + SongListController?.OnSongListItemCoverFetched(this); + } + catch (Exception l_Exception) + { + CP_SDK.ChatPlexSDK.Logger.Error("[SDK.UI.Data][SongListItem.CoverLoaded] Error:"); + CP_SDK.ChatPlexSDK.Logger.Error(l_Exception.ToString()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Load audio clip + /// + /// Path to load + /// Preview volume + /// + private IEnumerator Coroutine_GetAudioAsync(string p_Path, float p_PreviewAudioVolume) + { + var l_StartSerial = m_LoadAudioToken.Serial; + + yield return new WaitForEndOfFrame(); + + if (m_LoadAudioToken.IsCancelled(l_StartSerial)) + yield break; + + var l_PathParts = p_Path.Split('/'); + var l_SafePath = string.Join("/", l_PathParts.Select(x => x == l_PathParts[0] ? x : Uri.EscapeUriString(x)).ToArray()); + var l_FinalURL = "file://" + l_SafePath.Replace("#", "%23"); + + yield return new WaitForEndOfFrame(); + + UnityWebRequest l_Loader = UnityWebRequestMultimedia.GetAudioClip(l_FinalURL, AudioType.OGGVORBIS); + yield return l_Loader.SendWebRequest(); + + /// Skip if it's not the menu + if (CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + yield break; + + if (m_LoadAudioToken.IsCancelled(l_StartSerial)) + yield break; + + if (l_Loader.isNetworkError + || l_Loader.isHttpError + || !string.IsNullOrEmpty(l_Loader.error)) + { + CP_SDK.ChatPlexSDK.Logger.Error($"[SDK.UI.Data][SongListItem.Coroutine_GetAudioAsync] Can't load audio! {(!string.IsNullOrEmpty(l_Loader.error) ? l_Loader.error : string.Empty)}"); + yield break; + } + + var l_AudioClip = null as AudioClip; + try + { + ((DownloadHandlerAudioClip)l_Loader.downloadHandler).streamAudio = true; + l_AudioClip = DownloadHandlerAudioClip.GetContent(l_Loader); + + if (l_AudioClip == null) + { + CP_SDK.ChatPlexSDK.Logger.Error("[SDK.UI.Data][SongListItem.Coroutine_GetAudioAsync] No audio found"); + yield break; + } + } + catch (Exception p_Exception) + { + CP_SDK.ChatPlexSDK.Logger.Error("[SDK.UI.Data][SongListItem.Coroutine_GetAudioAsync] Can't load audio! Exception:"); + CP_SDK.ChatPlexSDK.Logger.Error(p_Exception); + yield break; + } + + var l_RemainingTry = 15; + var l_Waiter = new WaitForSecondsRealtime(0.1f); + + while (l_AudioClip.loadState != AudioDataLoadState.Loaded + && l_AudioClip.loadState != AudioDataLoadState.Failed) + { + yield return l_Waiter; + l_RemainingTry--; + + if (l_RemainingTry < 0) + yield break; + + if (m_LoadAudioToken.IsCancelled(l_StartSerial)) + yield break; + } + + if (CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + yield break; + + if (m_LoadAudioToken.IsCancelled(l_StartSerial)) + yield break; + + if (l_AudioClip.loadState != AudioDataLoadState.Loaded) + yield break; + + try + { + if (!m_AudioClipCache.ContainsKey(GetLevelHash())) + m_AudioClipCache.Add(GetLevelHash(), l_AudioClip); + + if (m_SongPreviewPlayer && m_SongPreviewPlayer.activeAudioClip != l_AudioClip) + m_SongPreviewPlayer.CrossfadeTo(l_AudioClip, p_PreviewAudioVolume, LocalLevel.previewStartTime, LocalLevel.previewDuration, () => { }); + } + catch (Exception) + { + + } + } + } +} diff --git a/BeatSaberPlus/SDK/UI/DataSource/SimpleTextList.cs b/BeatSaberPlus/SDK/UI/DataSource/SimpleTextList.cs deleted file mode 100644 index dcf2636..0000000 --- a/BeatSaberPlus/SDK/UI/DataSource/SimpleTextList.cs +++ /dev/null @@ -1,119 +0,0 @@ -using HMUI; -using IPA.Utilities; -using System.Collections.Generic; -using System.Linq; -using TMPro; -using UnityEngine; - -namespace BeatSaberPlus.SDK.UI.DataSource -{ - /// - /// Simple text list - /// - public class SimpleTextList : MonoBehaviour, TableView.IDataSource - { - /// - /// Cell template - /// - private SimpleTextTableCell m_SongListTableCellInstance; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Table view instance - /// - public TableView TableViewInstance; - /// - /// Data (text, hover hint) - /// - public List<(string, string)> Data = new List<(string, string)>(); - /// - /// Cell size - /// - public float CellSizeValue = 5.2f; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build cell - /// - /// Table view instance - /// Cell index - /// - public TableCell CellForIdx(TableView p_TableView, int p_Index) - { - SimpleTextTableCell l_Cell = GetTableCell(); - - var l_Text = l_Cell.GetField("_text"); - l_Text.richText = true; - l_Text.enableWordWrapping = true; - l_Text.fontStyle = FontStyles.Normal; - l_Text.enableAutoSizing = true; - l_Text.fontSizeMin = 2f; - l_Text.fontSizeMax = 3.5f; - l_Text.text = Data[p_Index].Item1; - l_Cell.text = Data[p_Index].Item1; - - var l_HoverHint = l_Cell.gameObject.GetComponent(); - if (l_HoverHint == null || !l_HoverHint) - { - l_HoverHint = l_Cell.gameObject.AddComponent(); - l_HoverHint.SetField("_hoverHintController", Resources.FindObjectsOfTypeAll().First()); - } - - if (l_Cell.gameObject.GetComponent()) - GameObject.Destroy(l_Cell.gameObject.GetComponent()); - - if (!string.IsNullOrEmpty(Data[p_Index].Item2)) - { - l_HoverHint.enabled = true; - l_HoverHint.text = Data[p_Index].Item2; - } - else - l_HoverHint.enabled = false; - - return l_Cell; - } - /// - /// Get cell size - /// - /// - public float CellSize() - { - return CellSizeValue; - } - /// - /// Get number of cell - /// - /// - public int NumberOfCells() - { - return Data.Count(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get new table cell or reuse old one - /// - /// - public SimpleTextTableCell GetTableCell() - { - SimpleTextTableCell l_Cell = (SimpleTextTableCell)TableViewInstance.DequeueReusableCellForIdentifier("BSP_SimpleTextList_Cell"); - if (!l_Cell) - { - if (m_SongListTableCellInstance == null) - m_SongListTableCellInstance = Resources.FindObjectsOfTypeAll().First(x => x.name == "SimpleTextTableCell"); - - l_Cell = Instantiate(m_SongListTableCellInstance); - } - - l_Cell.reuseIdentifier = "BSP_SimpleTextList_Cell"; - - return l_Cell; - } - } -} diff --git a/BeatSaberPlus/SDK/UI/DataSource/SongList.cs b/BeatSaberPlus/SDK/UI/DataSource/SongList.cs deleted file mode 100644 index 7f31b52..0000000 --- a/BeatSaberPlus/SDK/UI/DataSource/SongList.cs +++ /dev/null @@ -1,600 +0,0 @@ -using IPA.Utilities; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using TMPro; -using UnityEngine; -using UnityEngine.Networking; - -namespace BeatSaberPlus.SDK.UI.DataSource -{ - /// - /// Song entry list source - /// - public class SongList : UnityEngine.MonoBehaviour, HMUI.TableView.IDataSource - { - /// - /// Cell template - /// - private static LevelListTableCell m_SongListTableCellInstance; - /// - /// Default cover image - /// - private UnityEngine.Sprite m_DefaultCover = null; - /// - /// Song preview player - /// - private SongPreviewPlayer m_SongPreviewPlayer = null; - /// - /// Old loading coroutine - /// - private Coroutine m_OldCoroutine = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Table view instance - /// - public HMUI.TableView TableViewInstance; - /// - /// Data - /// - public List Data = new List(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Cover cache - /// - public static Dictionary CoverCache = new Dictionary(); - /// - /// Audio clip cache - /// - public static Dictionary AudioClipCache = new Dictionary(); - /// - /// Play preview audio ? - /// - public bool PlayPreviewAudio = false; - /// - /// Preview volume - /// - public float PreviewAudioVolume = 1f; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On cover fetched event - /// - public event Action OnCoverFetched; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Song entry - /// - public class Entry - { - /// - /// Was init - /// - private bool m_WasInit = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Title prefix - /// - public string TitlePrefix = ""; - /// - /// Hover hint text - /// - public string HoverHint = null; - /// - /// Hover hint time argument - /// - public DateTime? HoverHintTimeArg = null; - /// - /// Beat saver map - /// - public Game.BeatMaps.MapDetail BeatSaver_Map = null; - /// - /// Custom level instance - /// - public CustomPreviewBeatmapLevel CustomLevel = null; - /// - /// Cover - /// - public UnityEngine.Sprite Cover; - /// - /// Custom data - /// - public object CustomData = null; - /// - /// Is invalid - /// - public bool Invalid => (CustomLevel == null && BeatSaver_Map == null); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Init the entry - /// - public void Init() - { - if (m_WasInit) - return; - - if (CustomLevel == null && BeatSaver_Map != null && !BeatSaver_Map.Partial) - { - var l_LocalLevel = SongCore.Loader.GetLevelByHash(GetLevelHash().ToUpper()); - if (l_LocalLevel != null && SongCore.Loader.CustomLevels.ContainsKey(l_LocalLevel.customLevelPath)) - CustomLevel = l_LocalLevel; - } - - m_WasInit = true; - } - /// - /// Get entry level hash - /// - /// - public string GetLevelHash() - { - if (BeatSaver_Map != null && BeatSaver_Map.SelectMapVersion() != null && BeatSaver_Map.SelectMapVersion().hash != null) - return BeatSaver_Map.SelectMapVersion().hash.ToLower(); - else if (CustomLevel != null && CustomLevel.levelID.StartsWith("custom_level_")) - return CustomLevel.levelID.Replace("custom_level_", "").ToLower(); - - return ""; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Called when the script is being destroyed. - /// - public void OnDestroy() - { - /// Bind event - TableViewInstance.didSelectCellWithIdxEvent -= DidSelectCellWithIdxEvent; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Init - /// - public void Init() - { - /// Find preview player - m_SongPreviewPlayer = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); - - /// Bind event - TableViewInstance.didSelectCellWithIdxEvent -= DidSelectCellWithIdxEvent; - TableViewInstance.didSelectCellWithIdxEvent += DidSelectCellWithIdxEvent; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build cell - /// - /// Table view instance - /// Cell index - /// - public HMUI.TableCell CellForIdx(HMUI.TableView p_TableView, int p_Index) - { - LevelListTableCell l_Cell = GetTableCell(); - - TextMeshProUGUI l_Text = l_Cell.GetField("_songNameText"); - TextMeshProUGUI l_SubText = l_Cell.GetField("_songAuthorText"); - - var l_HoverHint = l_Cell.gameObject.GetComponent(); - if (l_HoverHint == null || !l_HoverHint) - { - l_HoverHint = l_Cell.gameObject.AddComponent(); - l_HoverHint.SetField("_hoverHintController", UnityEngine.Resources.FindObjectsOfTypeAll().First()); - } - - if (l_Cell.gameObject.GetComponent()) - UnityEngine.GameObject.Destroy(l_Cell.gameObject.GetComponent()); - - var l_SongEntry = Data[p_Index]; - l_SongEntry.Init(); - - if ((l_SongEntry.BeatSaver_Map != null && !l_SongEntry.BeatSaver_Map.Partial) || l_SongEntry.CustomLevel != null) - { - var l_HaveSong = l_SongEntry.CustomLevel != null && SongCore.Loader.CustomLevels.ContainsKey(l_SongEntry.CustomLevel.customLevelPath); - var l_Scores = Game.Levels.GetScoresByHash(l_SongEntry.GetLevelHash(), out var l_HaveAnyScore, out var l_HaveAllScores); - - string l_MapName = ""; - string l_MapAuthor = ""; - string l_MapSongAuthor = ""; - float l_Duration = 0f; - float l_BPM = 0f; - - if (l_HaveAnyScore && l_Scores.Count != 0) - { - string l_HoverHintSuffix = ""; - foreach (var l_Row in l_Scores) - { - l_HoverHintSuffix += $"\n{l_Row.Key.serializedName} "; - foreach (var l_SubRow in l_Row.Value) - l_HoverHintSuffix += (l_SubRow.Item2 != -1 ? "✔ " : "❌ "); - } - - l_SongEntry.HoverHint += "\n" + l_HoverHintSuffix; - } - - if (l_SongEntry.CustomLevel != null) - { - l_MapName = l_SongEntry.CustomLevel.songName; - l_MapAuthor = l_SongEntry.CustomLevel.levelAuthorName; - l_MapSongAuthor = l_SongEntry.CustomLevel.songAuthorName; - l_BPM = l_SongEntry.CustomLevel.beatsPerMinute; - l_Duration = l_SongEntry.CustomLevel.songDuration; - } - else - { - l_MapName = l_SongEntry.BeatSaver_Map.name; - l_MapAuthor = l_SongEntry.BeatSaver_Map.metadata.levelAuthorName; - l_MapSongAuthor = l_SongEntry.BeatSaver_Map.metadata.songAuthorName; - l_BPM = l_SongEntry.BeatSaver_Map.metadata.bpm; - l_Duration = l_SongEntry.BeatSaver_Map.metadata.duration; - } - - var l_ColorPrefix = "<#FFFFFF>"; - if (l_HaveAllScores) - l_ColorPrefix = "<#52F700>"; - else if (l_HaveAnyScore) - l_ColorPrefix = "<#F8E600>"; - else if (l_HaveSong) - l_ColorPrefix = "<#7F7F7F>"; - - string l_Title = l_SongEntry.TitlePrefix + (l_SongEntry.TitlePrefix.Length != 0 ? " " : "") + l_ColorPrefix + l_MapName; - string l_SubTitle = l_MapSongAuthor + " [" + l_MapAuthor + "]"; - - if (Regex.Replace(l_Title, "<.*?>", String.Empty).Length > (28 + l_ColorPrefix.Length)) - l_Title = l_Title.Substring(0, 28 + l_ColorPrefix.Length) + "..."; - if (Regex.Replace(l_SubTitle, "<.*?>", String.Empty).Length > 35) - l_SubTitle = l_SubTitle.Substring(0, 35) + "..."; - - /// Enable rich text support for the lower row text - l_SubText.richText = true; - - l_Text.text = l_Title; - l_SubText.text = l_SubTitle; - - var l_BPMText = l_Cell.GetField("_songBpmText"); - l_BPMText.gameObject.SetActive(true); - l_BPMText.text = ((int)l_BPM).ToString(); - - var l_DurationText = l_Cell.GetField("_songDurationText"); - l_DurationText.gameObject.SetActive(true); - l_DurationText.text = l_Duration >= 0.0 ? $"{Math.Floor((double)l_Duration / 60):N0}:{Math.Floor((double)l_Duration % 60):00}" : "--"; - - l_Cell.transform.Find("BpmIcon").gameObject.SetActive(true); - - if (l_SongEntry.Cover != null) - l_Cell.GetField("_coverImage").sprite = l_SongEntry.Cover; - else if (CoverCache.TryGetValue(l_SongEntry.GetLevelHash(), out var l_Cover)) - { - l_SongEntry.Cover = l_Cover; - l_Cell.GetField("_coverImage").sprite = l_SongEntry.Cover; - - OnCoverFetched?.Invoke(l_Cell.idx, l_SongEntry); - } - else - { - l_Cell.GetField("_coverImage").sprite = m_DefaultCover; - - if (l_HaveSong) - { - var l_CoverTask = l_SongEntry.CustomLevel.GetCoverImageAsync(CancellationToken.None); - _ = l_CoverTask.ContinueWith(p_CoverTaskResult => - { - if (l_Cell.idx >= Data.Count || l_SongEntry != Data[l_Cell.idx]) - return; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - /// Update infos - l_SongEntry.Cover = p_CoverTaskResult.Result; - - /// Cache cover - if (!CoverCache.ContainsKey(l_SongEntry.GetLevelHash())) - CoverCache.Add(l_SongEntry.GetLevelHash(), l_SongEntry.Cover); - - if (l_Cell.idx < Data.Count && l_SongEntry == Data[l_Cell.idx]) - { - l_Cell.GetField("_coverImage").sprite = l_SongEntry.Cover; - l_Cell.RefreshVisuals(); - - OnCoverFetched?.Invoke(l_Cell.idx, l_SongEntry); - } - }); - }); - } - else if (l_SongEntry.BeatSaver_Map != null) - { - var l_CoverByte = Game.BeatMapsClient.GetCoverImageFromCacheByKey(l_SongEntry.BeatSaver_Map.id); - if (l_CoverByte != null && l_CoverByte.Length > 0) - { - var l_Texture = CP_SDK.Unity.Texture2DU.CreateFromRaw(l_CoverByte); - if (l_Texture != null) - { - l_SongEntry.Cover = UnityEngine.Sprite.Create(l_Texture, new UnityEngine.Rect(0, 0, l_Texture.width, l_Texture.height), new UnityEngine.Vector2(0.5f, 0.5f), 100); - - /// Cache cover - if (!CoverCache.ContainsKey(l_SongEntry.GetLevelHash())) - CoverCache.Add(l_SongEntry.GetLevelHash(), l_SongEntry.Cover); - - l_Cell.GetField("_coverImage").sprite = l_SongEntry.Cover; - OnCoverFetched?.Invoke(l_Cell.idx, l_SongEntry); - } - } - else - { - /// Fetch cover - l_SongEntry.BeatSaver_Map.SelectMapVersion().CoverImageBytes((p_Valid, p_CoverTaskResult) => - { - if (p_Valid) - Game.BeatMapsClient.CacheCoverImage(l_SongEntry.BeatSaver_Map, p_CoverTaskResult); - - if (l_Cell.idx >= Data.Count || l_SongEntry != Data[l_Cell.idx]) - return; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - var l_Texture = CP_SDK.Unity.Texture2DU.CreateFromRaw(p_CoverTaskResult); - if (l_Texture != null) - { - l_SongEntry.Cover = UnityEngine.Sprite.Create(l_Texture, new UnityEngine.Rect(0, 0, l_Texture.width, l_Texture.height), new UnityEngine.Vector2(0.5f, 0.5f), 100); - - /// Cache cover - if (!CoverCache.ContainsKey(l_SongEntry.GetLevelHash())) - CoverCache.Add(l_SongEntry.GetLevelHash(), l_SongEntry.Cover); - - if (l_Cell.idx < Data.Count && l_SongEntry == Data[l_Cell.idx]) - { - l_Cell.GetField("_coverImage").sprite = l_SongEntry.Cover; - l_Cell.RefreshVisuals(); - - OnCoverFetched?.Invoke(l_Cell.idx, l_SongEntry); - } - } - }); - }); - } - } - } - } - else if (l_SongEntry.BeatSaver_Map != null && l_SongEntry.BeatSaver_Map.Partial) - { - l_Text.text = "Loading from BeatSaver..."; - l_SubText.text = ""; - - l_Cell.GetField("_songBpmText").gameObject.SetActive(false); - l_Cell.GetField("_songDurationText").gameObject.SetActive(false); - l_Cell.transform.Find("BpmIcon").gameObject.SetActive(false); - - l_Cell.GetField("_coverImage").sprite = m_DefaultCover; - } - else - { - l_Text.text = "<#FF0000>Invalid song"; - l_SubText.text = l_SongEntry.CustomLevel != null ? l_SongEntry.CustomLevel.levelID.Replace("custom_level_", "") : ""; - - l_Cell.GetField("_songBpmText").gameObject.SetActive(false); - l_Cell.GetField("_songDurationText").gameObject.SetActive(false); - l_Cell.transform.Find("BpmIcon").gameObject.SetActive(false); - - l_Cell.GetField("_coverImage").sprite = m_DefaultCover; - } - - if (!string.IsNullOrEmpty(l_SongEntry.HoverHint)) - { - var l_HoverHintText = l_SongEntry.HoverHint; - if (l_SongEntry.HoverHintTimeArg.HasValue && l_HoverHintText.Contains("$$time$$")) - { - var l_Replace = ""; - var l_Elapsed = CP_SDK.Misc.Time.UnixTimeNow() - CP_SDK.Misc.Time.ToUnixTime(l_SongEntry.HoverHintTimeArg.Value); - if (l_Elapsed < (60 * 60)) - l_Replace = Math.Max(1, l_Elapsed / 60).ToString() + " minute(s) ago"; - else if (l_Elapsed < (60 * 60 * 24)) - l_Replace = Math.Max(1, l_Elapsed / (60 * 60)).ToString() + " hour(s) ago"; - else - l_Replace = Math.Max(1, l_Elapsed / (60 * 60 * 24)).ToString() + " day(s) ago"; - - l_HoverHintText = l_HoverHintText.Replace("$$time$$", l_Replace); - } - - l_HoverHint.enabled = true; - l_HoverHint.text = l_HoverHintText; - } - else - l_HoverHint.enabled = false; - - return l_Cell; - } - /// - /// Get cell size - /// - /// - public float CellSize() - { - return 8.5f; - } - /// - /// Get number of cell - /// - /// - public int NumberOfCells() - { - return Data.Count(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Stop preview music if any - /// - public void StopPreviewMusic() - { - if (m_SongPreviewPlayer != null && m_SongPreviewPlayer) - m_SongPreviewPlayer.CrossfadeToDefault(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When a cell is selected - /// - /// Table instance - /// Row index - public void DidSelectCellWithIdxEvent(HMUI.TableView p_Table, int p_Row) - { - if (m_SongPreviewPlayer == null || !m_SongPreviewPlayer || !PlayPreviewAudio || p_Row > Data.Count) - return; - - /// Fetch song entry - var l_SongRowData = Data[p_Row]; - - /// Hide if invalid song - if (l_SongRowData == null || l_SongRowData.Invalid) - return; - - if (l_SongRowData.CustomLevel != null && SongCore.Loader.CustomLevels.ContainsKey(l_SongRowData.CustomLevel.customLevelPath)) - { - if (AudioClipCache.TryGetValue(l_SongRowData.GetLevelHash(), out var l_AudioClip)) - m_SongPreviewPlayer.CrossfadeTo(l_AudioClip, PreviewAudioVolume, l_SongRowData.CustomLevel.previewStartTime, l_SongRowData.CustomLevel.previewDuration, () => { }); - else - { - if (m_OldCoroutine != null) - { - StopCoroutine(m_OldCoroutine); - m_OldCoroutine = null; - } - - m_OldCoroutine = StartCoroutine(LoadAudioClip(l_SongRowData, l_SongRowData.CustomLevel.songPreviewAudioClipPath)); - } - } - else - { - /// Stop preview music if any - m_SongPreviewPlayer.CrossfadeToDefault(); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get new table cell or reuse old one - /// - /// - private LevelListTableCell GetTableCell() - { - LevelListTableCell l_Cell = (LevelListTableCell)TableViewInstance.DequeueReusableCellForIdentifier("BSP_SongList_Cell"); - if (!l_Cell) - { - if (m_SongListTableCellInstance == null) - { - m_SongListTableCellInstance = UnityEngine.Resources.FindObjectsOfTypeAll().First(x => (x.name == "LevelListTableCell")); - m_SongListTableCellInstance = Instantiate(m_SongListTableCellInstance); - m_SongListTableCellInstance.gameObject.name = "BSP_LevelListTableCell"; - m_SongListTableCellInstance.transform.SetParent(null, true); - UnityEngine.GameObject.DontDestroyOnLoad(m_SongListTableCellInstance.gameObject); - - TextMeshProUGUI l_Text = m_SongListTableCellInstance.GetField("_songNameText"); - - m_SongListTableCellInstance.transform.Find("PromoBackground")?.gameObject?.SetActive(false); - m_SongListTableCellInstance.GetField("_favoritesBadgeImage").gameObject.SetActive(false); - - if (l_Text.overflowMode != TextOverflowModes.Overflow) - l_Text.overflowMode = TextOverflowModes.Overflow; - - var l_TextComponents = l_Text.GetComponents(); - var l_LayoutWidthLimiter = l_TextComponents.FirstOrDefault(x => x.GetType().Name == "LayoutWidthLimiter"); - if (l_LayoutWidthLimiter) - l_LayoutWidthLimiter.enabled = false; - - l_Text.transform.Find("PromoBadge")?.gameObject?.SetActive(false); - l_Text.transform.Find("UpdatedBadge")?.gameObject?.SetActive(false); - l_Text.rectTransform.sizeDelta = new Vector2(-22.7f, 5.74f); - } - - l_Cell = Instantiate(m_SongListTableCellInstance); - } - - l_Cell.SetField("_notOwned", false); - l_Cell.reuseIdentifier = "BSP_SongList_Cell"; - - if (m_DefaultCover == null) - m_DefaultCover = l_Cell.GetField("_coverImage").sprite; - - return l_Cell; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - IEnumerator LoadAudioClip(Entry p_SongRowData, string p_Path) - { - /// Skip if it's not the menu - if (SDK.Game.Logic.ActiveScene != SDK.Game.Logic.SceneType.Menu) - { - m_OldCoroutine = null; - yield break; - } - - UnityWebRequest l_Song = UnityWebRequestMultimedia.GetAudioClip(p_Path, AudioType.OGGVORBIS); - yield return l_Song.SendWebRequest(); - - /// Skip if it's not the menu - if (SDK.Game.Logic.ActiveScene != SDK.Game.Logic.SceneType.Menu) - { - m_OldCoroutine = null; - yield break; - } - - try - { - ((DownloadHandlerAudioClip)l_Song.downloadHandler).streamAudio = true; - - var l_Clip = DownloadHandlerAudioClip.GetContent(l_Song); - - if (l_Clip != null) - { - if (!AudioClipCache.ContainsKey(p_SongRowData.GetLevelHash())) - AudioClipCache.Add(p_SongRowData.GetLevelHash(), l_Clip); - - m_SongPreviewPlayer.CrossfadeTo(l_Clip, PreviewAudioVolume, p_SongRowData.CustomLevel.previewStartTime, p_SongRowData.CustomLevel.previewDuration, () => { }); - - m_OldCoroutine = null; - yield break; - } - } - catch (Exception) - { - m_OldCoroutine = null; - yield break; - } - } - } -} diff --git a/BeatSaberPlus/SDK/UI/DataSource/SongListCustom.cs b/BeatSaberPlus/SDK/UI/DataSource/SongListCustom.cs deleted file mode 100644 index f14c3fb..0000000 --- a/BeatSaberPlus/SDK/UI/DataSource/SongListCustom.cs +++ /dev/null @@ -1,163 +0,0 @@ -using IPA.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using TMPro; -using UnityEngine; - -namespace BeatSaberPlus.SDK.UI.DataSource -{ - /// - /// Song entry list source - /// - public class SongListCustom : UnityEngine.MonoBehaviour, HMUI.TableView.IDataSource - { - /// - /// Cell template - /// - private static LevelListTableCell m_SongListTableCellInstance; - /// - /// Default cover image - /// - private UnityEngine.Sprite m_DefaultCover = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Table view instance - /// - public HMUI.TableView TableViewInstance; - /// - /// Data - /// - public List<(string, string, string)> Data = new List<(string, string, string)>(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build cell - /// - /// Table view instance - /// Cell index - /// - public HMUI.TableCell CellForIdx(HMUI.TableView p_TableView, int p_Index) - { - LevelListTableCell l_Cell = GetTableCell(); - - TextMeshProUGUI l_Text = l_Cell.GetField("_songNameText"); - TextMeshProUGUI l_SubText = l_Cell.GetField("_songAuthorText"); - - var l_HoverHint = l_Cell.gameObject.GetComponent(); - if (l_HoverHint == null || !l_HoverHint) - { - l_HoverHint = l_Cell.gameObject.AddComponent(); - l_HoverHint.SetField("_hoverHintController", UnityEngine.Resources.FindObjectsOfTypeAll().First()); - } - - if (l_Cell.gameObject.GetComponent()) - UnityEngine.GameObject.Destroy(l_Cell.gameObject.GetComponent()); - - var l_Entry = Data[p_Index]; - string l_Title = l_Entry.Item1; - string l_SubTitle = l_Entry.Item2; - - //if (Regex.Replace(l_Title, "<.*?>", String.Empty).Length > 28) - // l_Title = l_Title.Substring(0, 28) + "..."; - //if (Regex.Replace(l_SubTitle, "<.*?>", String.Empty).Length > 35) - // l_SubTitle = l_SubTitle.Substring(0, 35) + "..."; - - /// Enable rich text support for the lower row text - l_SubText.richText = true; - - l_Text.text = l_Title; - l_SubText.text = l_SubTitle; - - var l_BPMText = l_Cell.GetField("_songBpmText"); - l_BPMText.gameObject.SetActive(false); - - var l_DurationText = l_Cell.GetField("_songDurationText"); - l_DurationText.gameObject.SetActive(false); - - l_Cell.transform.Find("BpmIcon").gameObject.SetActive(false); - - if (!string.IsNullOrEmpty(l_Entry.Item3)) - { - l_HoverHint.enabled = true; - l_HoverHint.text = l_Entry.Item3; - } - else - l_HoverHint.enabled = false; - - return l_Cell; - } - /// - /// Get cell size - /// - /// - public float CellSize() - { - return 8.5f; - } - /// - /// Get number of cell - /// - /// - public int NumberOfCells() - { - return Data.Count(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get new table cell or reuse old one - /// - /// - private LevelListTableCell GetTableCell() - { - LevelListTableCell l_Cell = (LevelListTableCell)TableViewInstance.DequeueReusableCellForIdentifier("BSP_SongListCustom_Cell"); - if (!l_Cell) - { - if (m_SongListTableCellInstance == null) - { - m_SongListTableCellInstance = UnityEngine.Resources.FindObjectsOfTypeAll().First(x => (x.name == "LevelListTableCell")); - m_SongListTableCellInstance = Instantiate(m_SongListTableCellInstance); - m_SongListTableCellInstance.gameObject.name = "BSP_LevelListTableCell"; - m_SongListTableCellInstance.transform.SetParent(null, true); - UnityEngine.GameObject.DontDestroyOnLoad(m_SongListTableCellInstance.gameObject); - - TextMeshProUGUI l_Text = m_SongListTableCellInstance.GetField("_songNameText"); - - m_SongListTableCellInstance.transform.Find("PromoBackground")?.gameObject?.SetActive(false); - m_SongListTableCellInstance.GetField("_favoritesBadgeImage").gameObject.SetActive(false); - - if (l_Text.overflowMode != TextOverflowModes.Overflow) - l_Text.overflowMode = TextOverflowModes.Overflow; - - var l_TextComponents = l_Text.GetComponents(); - var l_LayoutWidthLimiter = l_TextComponents.FirstOrDefault(x => x.GetType().Name == "LayoutWidthLimiter"); - if (l_LayoutWidthLimiter) - l_LayoutWidthLimiter.enabled = false; - - l_Text.transform.Find("PromoBadge")?.gameObject?.SetActive(false); - l_Text.transform.Find("UpdatedBadge")?.gameObject?.SetActive(false); - l_Text.rectTransform.sizeDelta = new Vector2(-22.7f, 5.74f); - } - - l_Cell = Instantiate(m_SongListTableCellInstance); - } - - l_Cell.SetField("_notOwned", false); - l_Cell.reuseIdentifier = "BSP_SongListCustom_Cell"; - - if (m_DefaultCover == null) - m_DefaultCover = l_Cell.GetField("_coverImage").sprite; - - return l_Cell; - } - } -} diff --git a/BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/BS_CFloatingPanel.cs b/BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/BS_CFloatingPanel.cs new file mode 100644 index 0000000..e622d5f --- /dev/null +++ b/BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/BS_CFloatingPanel.cs @@ -0,0 +1,168 @@ +using HMUI; +using System.Linq; +using UnityEngine; +using VRUIControls; + +namespace BeatSaberPlus.SDK.UI.DefaultComponentsOverrides +{ + /// + /// BeatSaber CFloatingPanel component + /// + internal class BS_CFloatingPanel : CP_SDK.UI.DefaultComponents.DefaultCFloatingPanel + { + /// + /// Mover handle + /// + private Subs.SubFloatingPanelMoverHandle m_MoverHandle; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On component creation + /// + public override void Init() + { + var l_ShouldContinue = !m_RTransform; + base.Init(); + + if (!l_ShouldContinue) + return; + + var l_CurvedCanvasSettings = gameObject.AddComponent(); + l_CurvedCanvasSettings.SetRadius(140f); + + CreateMover(); + SetAllowMovement(false); + + Patches.PVRPointer.OnActivated -= CreateMoverOnPointerCreated; + Patches.PVRPointer.OnActivated += CreateMoverOnPointerCreated; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set radius on supported games + /// + /// Canvas radius + /// + public override CP_SDK.UI.Components.CFloatingPanel SetRadius(float p_Radius) + { + base.SetRadius(p_Radius); + gameObject.GetComponent()?.SetRadius(p_Radius); + + return this; + } + /// + /// Set size + /// + /// New size + /// + public override CP_SDK.UI.Components.CFloatingPanel SetSize(Vector2 p_Size) + { + base.SetSize(p_Size); + UpdateMover(); + return this; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On component destroy + /// + protected override void OnDestroy() + { + base.OnDestroy(); + + Patches.PVRPointer.OnActivated -= CreateMoverOnPointerCreated; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create mover + /// + /// VRPointer instance + private void CreateMoverOnPointerCreated(VRPointer p_VRPointer) => CreateMover(p_VRPointer); + /// + /// Create mover + /// + /// VRPointer instance + private void CreateMover(VRPointer p_VRPointer = null) + { + if (p_VRPointer == null) + p_VRPointer = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + + if (p_VRPointer == null) + { + CP_SDK.ChatPlexSDK.Logger.Warning("[BeatSaberPlus.SDK.UI.DefaultComponentsOverrides][BS_CFloatingPanel.CreateMover] Failed to get VRPointer!"); + return; + } + + if (!p_VRPointer.GetComponent()) + p_VRPointer.gameObject.AddComponent(); + + if (m_MoverHandle == null) + { + m_MoverHandle = new GameObject("MoverHandle", typeof(Subs.SubFloatingPanelMoverHandle)).GetComponent(); + m_MoverHandle.transform.SetParent(transform); + m_MoverHandle.transform.localPosition = Vector3.zero; + m_MoverHandle.transform.localRotation = Quaternion.identity; + m_MoverHandle.transform.localScale = Vector3.one; + m_MoverHandle.FloatingPanel = this; + + UpdateMover(); + } + } + /// + /// Update mover collision + /// + private void UpdateMover() + { + if (m_MoverHandle == null) + return; + + var l_Size = m_RTransform.sizeDelta; + + m_MoverHandle.transform.localPosition = new Vector3(0.0f, 0.0f, 0.1f); + m_MoverHandle.transform.localRotation = Quaternion.identity; + m_MoverHandle.transform.localScale = new Vector3(l_Size.x, l_Size.y, 0.1f); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set allow movements + /// + /// Is allowed? + /// + public override CP_SDK.UI.Components.CFloatingPanel SetAllowMovement(bool p_Allow) + { + base.SetAllowMovement(p_Allow); + + if (m_MoverHandle) + m_MoverHandle.gameObject.SetActive(p_Allow); + + if (p_Allow) + { + /// Refresh VR pointer due to bug + var l_VRPointers = Resources.FindObjectsOfTypeAll(); + var l_VRPointer = CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Playing ? l_VRPointers.LastOrDefault() : l_VRPointers.FirstOrDefault(); + + if (l_VRPointer) + { + if (!l_VRPointer.GetComponent()) + l_VRPointer.gameObject.AddComponent(); + } + else + CP_SDK.ChatPlexSDK.Logger.Warning("[BeatSaberPlus.SDK.UI.DefaultComponentsOverrides][BS_CFloatingPanel.SetAllowMovement] Failed to get VRPointer!"); + } + + return this; + } + } +} diff --git a/BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMover.cs b/BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMover.cs new file mode 100644 index 0000000..df48c64 --- /dev/null +++ b/BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMover.cs @@ -0,0 +1,143 @@ +using System.Linq; +using UnityEngine; +using VRUIControls; + +namespace BeatSaberPlus.SDK.UI.DefaultComponentsOverrides.Subs +{ + /// + /// Floating panel mover + /// + internal class SubFloatingPanelMover : MonoBehaviour + { + protected const float MinScrollDistance = 0.25f; + protected const float MaxLaserDistance = 50f; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private BS_CFloatingPanel m_FloatingPanel; + private VRPointer m_VRPointer; + private VRController m_GrabbingController; + private Vector3 m_GrabPosition; + private Quaternion m_GrabRotation; + private FirstPersonFlyingController m_FPFC; + private RaycastHit[] m_RaycastBuffer = new RaycastHit[10]; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On component creation + /// + private void Awake() + { + m_VRPointer = GetComponent(); + m_FPFC = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On component destroy + /// + private void OnDestroy() + { + m_VRPointer = null; + m_FloatingPanel = null; + m_GrabbingController = null; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On frame + /// + private void Update() + { + var l_IsFPFC = IsFPFC(); + +#if BEATSABER_1_29_4_OR_NEWER + var l_VRController = m_VRPointer?.lastSelectedVrController; +#else + var l_VRController = m_VRPointer?.vrController; +#endif + var l_VRControllerTransform = l_VRController?.transform; + var l_ButtonDown = l_VRController?.triggerValue > 0.9f || (l_IsFPFC && Input.GetMouseButton(0)); + + if (l_VRController != null && l_ButtonDown) + { + if (m_GrabbingController != null) + return; + + var l_HitCount = Physics.RaycastNonAlloc( l_VRControllerTransform.position, + l_VRControllerTransform.forward, + m_RaycastBuffer, + MaxLaserDistance, + 1 << CP_SDK.UI.UISystem.UILayer); + + for (var l_I = 0; l_I < l_HitCount; ++l_I) + { + var l_SubFloatingPanelMoverHandle = m_RaycastBuffer[l_I].transform?.GetComponent(); + if (!l_SubFloatingPanelMoverHandle) + continue; + + + m_FloatingPanel = l_SubFloatingPanelMoverHandle.FloatingPanel; + m_GrabbingController = l_VRController; + m_GrabPosition = l_VRControllerTransform.InverseTransformPoint(m_FloatingPanel.RTransform.position); + m_GrabRotation = Quaternion.Inverse(l_VRControllerTransform.rotation) * m_FloatingPanel.RTransform.rotation; + + m_FloatingPanel.FireOnGrab(); + break; + } + } + + if (m_GrabbingController != null && !l_ButtonDown) + { + m_FloatingPanel.FireOnRelease(); + + m_FloatingPanel = null; + m_GrabbingController = null; + } + } + /// + /// On frame (late) + /// + private void LateUpdate() + { + if (m_GrabbingController == null) + return; + +#if BEATSABER_1_29_4_OR_NEWER + var l_Delta = m_GrabbingController.thumbstick.y * Time.unscaledDeltaTime; +#else + var l_Delta = m_GrabbingController.verticalAxisValue * Time.unscaledDeltaTime; +#endif + if (m_GrabPosition.magnitude > MinScrollDistance) m_GrabPosition -= Vector3.forward * l_Delta; + else m_GrabPosition -= Vector3.forward * Mathf.Clamp(l_Delta, float.MinValue, 0f); + + var l_RealPosition = m_GrabbingController.transform.TransformPoint(m_GrabPosition); + var l_RealRotation = m_GrabbingController.transform.rotation * m_GrabRotation; + + m_FloatingPanel.RTransform.position = Vector3.Lerp(m_FloatingPanel.RTransform.position, l_RealPosition, 10f * Time.unscaledDeltaTime); + m_FloatingPanel.RTransform.rotation = Quaternion.Slerp(m_FloatingPanel.RTransform.rotation, l_RealRotation, 5f * Time.unscaledDeltaTime); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Is in FPFC + /// + /// + private bool IsFPFC() + { + if (m_FPFC != null) + return m_FPFC.enabled; + + return false; + } + } +} diff --git a/BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMoverHandle.cs b/BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMoverHandle.cs new file mode 100644 index 0000000..de0ed60 --- /dev/null +++ b/BeatSaberPlus/SDK/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMoverHandle.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +namespace BeatSaberPlus.SDK.UI.DefaultComponentsOverrides.Subs +{ + /// + /// Floating panel mover handle + /// + internal class SubFloatingPanelMoverHandle : MonoBehaviour + { + /// + /// Floating panel instance + /// + internal BS_CFloatingPanel FloatingPanel; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On component creation + /// + private void Awake() + { + gameObject.AddComponent().size = new Vector3(1f, 1f, 0.1f); + gameObject.layer = CP_SDK.UI.UISystem.UILayer; + } + } +} diff --git a/BeatSaberPlus/SDK/UI/DefaultFactoriesOverrides/BS_FloatingPanelFactory.cs b/BeatSaberPlus/SDK/UI/DefaultFactoriesOverrides/BS_FloatingPanelFactory.cs new file mode 100644 index 0000000..9475874 --- /dev/null +++ b/BeatSaberPlus/SDK/UI/DefaultFactoriesOverrides/BS_FloatingPanelFactory.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +namespace BeatSaberPlus.SDK.UI.DefaultFactoriesOverrides +{ + /// + /// BeatSaber CFloatingPanel factory + /// + public class BS_FloatingPanelFactory : CP_SDK.UI.FactoryInterfaces.IFloatingPanelFactory + { + /// + /// Create an CFloatingPanel into the parent + /// + /// Name + /// Parent transform + /// + public CP_SDK.UI.Components.CFloatingPanel Create(string p_Name, Transform p_Parent) + { + var l_GameObject = new GameObject(p_Name, typeof(RectTransform)); + l_GameObject.transform.SetParent(p_Parent, false); + + var l_Element = l_GameObject.AddComponent(); + l_Element.Init(); + + return l_Element; + } + } +} diff --git a/BeatSaberPlus/SDK/UI/DropDownListSetting.cs b/BeatSaberPlus/SDK/UI/DropDownListSetting.cs deleted file mode 100644 index 641c847..0000000 --- a/BeatSaberPlus/SDK/UI/DropDownListSetting.cs +++ /dev/null @@ -1,60 +0,0 @@ -using BeatSaberMarkupLanguage.Parser; -using IPA.Utilities; -using TMPro; - -using BSMLDropDownListSetting = BeatSaberMarkupLanguage.Components.Settings.DropDownListSetting; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// BSML DropDownListSetting helper - /// - public class DropDownListSetting - { - /// - /// Setup a list setting - /// - /// Setting to setûp - /// Action on change - /// Should remove label - public static void Setup(BSMLDropDownListSetting p_Setting, BSMLAction p_Action, bool p_RemoveLabel, float p_NewWidthPct = 1f) - { - p_Setting.gameObject.SetActive(false); - - if (p_Action != null) - p_Setting.onChange = p_Action; - - p_Setting.updateOnChange = true; - - if (p_RemoveLabel) - { - UnityEngine.GameObject.Destroy(p_Setting.transform.parent.Find("Label").gameObject); - - UnityEngine.RectTransform l_RectTransform = p_Setting.gameObject.transform as UnityEngine.RectTransform; - l_RectTransform.anchorMin = new UnityEngine.Vector2(1f - p_NewWidthPct, 0f); - l_RectTransform.anchorMax = new UnityEngine.Vector2(p_NewWidthPct, 1f); - l_RectTransform.sizeDelta = UnityEngine.Vector2.one; - // - //p_Setting.gameObject.GetComponent().preferredWidth = -1f; - } - - p_Setting.gameObject.SetActive(true); - - /// Patch for rich text & style - var l_Text = p_Setting.dropdown.GetField("_text"); - if (l_Text) - { - l_Text.rectTransform.SetInsetAndSizeFromParentEdge(UnityEngine.RectTransform.Edge.Bottom, 0, 0); - l_Text.rectTransform.SetInsetAndSizeFromParentEdge(UnityEngine.RectTransform.Edge.Top, 0, 0); - l_Text.rectTransform.SetInsetAndSizeFromParentEdge(UnityEngine.RectTransform.Edge.Left, 0, 0); - l_Text.rectTransform.SetInsetAndSizeFromParentEdge(UnityEngine.RectTransform.Edge.Right, 0, 0); - l_Text.rectTransform.anchorMin = UnityEngine.Vector2.zero; - l_Text.rectTransform.anchorMax = UnityEngine.Vector2.one; - l_Text.alignment = TextAlignmentOptions.MidlineLeft; - l_Text.overflowMode = TextOverflowModes.Ellipsis; - l_Text.margin = new UnityEngine.Vector4(2.5f, 0, 10, 0); - l_Text.richText = true; - } - } - } -} diff --git a/BeatSaberPlus/SDK/UI/GameFont.cs b/BeatSaberPlus/SDK/UI/GameFont.cs new file mode 100644 index 0000000..65d6e2f --- /dev/null +++ b/BeatSaberPlus/SDK/UI/GameFont.cs @@ -0,0 +1,43 @@ +using System.Linq; +using TMPro; +using UnityEngine; + +namespace BeatSaberPlus.SDK.UI +{ + /// + /// Helpers for game font + /// + public static class GameFont + { + private static TMP_FontAsset m_BaseGameFont = null; + private static Material m_BaseGameFontSharedMaterial = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get main game font + /// + /// + public static TMP_FontAsset GetGameFont() + { + if (m_BaseGameFont || CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + return m_BaseGameFont; + + m_BaseGameFont = Resources.FindObjectsOfTypeAll().Where(t => t.name == "Teko-Medium SDF").FirstOrDefault(); + return m_BaseGameFont; + } + /// + /// Get main game font curved material + /// + /// + public static Material GetGameFontSharedMaterial() + { + if (m_BaseGameFontSharedMaterial || CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + return m_BaseGameFontSharedMaterial; + + m_BaseGameFontSharedMaterial = Material.Instantiate(Resources.FindObjectsOfTypeAll().Where(t => t.name == "Teko-Medium SDF Curved Softer").Last()); + return m_BaseGameFontSharedMaterial; + } + } +} diff --git a/BeatSaberPlus/SDK/UI/HorizontalIconSegmentedControl.cs b/BeatSaberPlus/SDK/UI/HMUIIconSegmentedControl.cs similarity index 54% rename from BeatSaberPlus/SDK/UI/HorizontalIconSegmentedControl.cs rename to BeatSaberPlus/SDK/UI/HMUIIconSegmentedControl.cs index c86fcc6..7db00ed 100644 --- a/BeatSaberPlus/SDK/UI/HorizontalIconSegmentedControl.cs +++ b/BeatSaberPlus/SDK/UI/HMUIIconSegmentedControl.cs @@ -1,14 +1,13 @@ using IPA.Utilities; using System.Linq; using UnityEngine; -using Zenject; namespace BeatSaberPlus.SDK.UI { /// /// Vertical icon segmented control /// - public class HorizontalIconSegmentedControl + public static class HMUIIconSegmentedControl { /// /// Create icon segmented control @@ -18,12 +17,12 @@ public class HorizontalIconSegmentedControl /// GameObject public static HMUI.IconSegmentedControl Create(RectTransform p_Parent, bool p_HideCellBackground) { - HMUI.IconSegmentedControl l_Prefab = Resources.FindObjectsOfTypeAll().First(x => x.name == "BeatmapCharacteristicSegmentedControl" && x.GetField("_container") != null); + HMUI.IconSegmentedControl l_Prefab = Resources.FindObjectsOfTypeAll().First(x => x.name == "BeatmapCharacteristicSegmentedControl" && x._container != null); HMUI.IconSegmentedControl l_Control = MonoBehaviour.Instantiate(l_Prefab, p_Parent, false); - l_Control.name = "BSMLIconSegmentedControl"; - l_Control.SetField("_container", l_Prefab.GetField("_container")); - l_Control.SetField("_hideCellBackground", p_HideCellBackground); + l_Control.name = "BSPIconSegmentedControl"; + l_Control.SetField("_container", l_Prefab._container); + l_Control._hideCellBackground = p_HideCellBackground; RectTransform l_RectTransform = l_Control.transform as RectTransform; l_RectTransform.anchorMin = Vector2.one * 0.5f; @@ -38,5 +37,26 @@ public static HMUI.IconSegmentedControl Create(RectTransform p_Parent, bool p_Hi return l_Control; } + /// + /// Set data and remove hover hints + /// + /// Control instance + /// Data to set + public static void SetDataNoHoverHint(this HMUI.IconSegmentedControl p_Instance, HMUI.IconSegmentedControl.DataItem[] p_Data) + { + p_Instance.SetData(p_Data); + try + { + var l_HoverHints = p_Instance.GetComponentsInChildren(true); + var l_LocalHoverHints = p_Instance.GetComponentsInChildren(true); + + foreach (var l_Current in l_HoverHints) GameObject.Destroy(l_Current); + foreach (var l_Current in l_LocalHoverHints) GameObject.Destroy(l_Current); + } + catch (System.Exception) + { + + } + } } } diff --git a/BeatSaberPlus/SDK/UI/TextSegmentedControl.cs b/BeatSaberPlus/SDK/UI/HMUITextSegmentedControl.cs similarity index 79% rename from BeatSaberPlus/SDK/UI/TextSegmentedControl.cs rename to BeatSaberPlus/SDK/UI/HMUITextSegmentedControl.cs index fb4d0b7..358bc95 100644 --- a/BeatSaberPlus/SDK/UI/TextSegmentedControl.cs +++ b/BeatSaberPlus/SDK/UI/HMUITextSegmentedControl.cs @@ -8,7 +8,7 @@ namespace BeatSaberPlus.SDK.UI /// /// Text segmented control /// - public class TextSegmentedControl + public static class HMUITextSegmentedControl { /// /// Create text segmented control @@ -18,12 +18,12 @@ public class TextSegmentedControl /// GameObject public static HMUI.TextSegmentedControl Create(RectTransform p_Parent, bool p_HideCellBackground, string[] p_Texts = null) { - HMUI.TextSegmentedControl l_Prefab = Resources.FindObjectsOfTypeAll().First(x => x.name == "BeatmapDifficultySegmentedControl" && x.GetField("_container") != null); + HMUI.TextSegmentedControl l_Prefab = Resources.FindObjectsOfTypeAll().First(x => x.name == "BeatmapDifficultySegmentedControl" && x._container != null); HMUI.TextSegmentedControl l_Control = MonoBehaviour.Instantiate((HMUI.TextSegmentedControl)l_Prefab, p_Parent, false); - l_Control.name = "BSMLTextSegmentedControl"; - l_Control.SetField("_container", l_Prefab.GetField("_container")); - l_Control.SetField("_hideCellBackground", p_HideCellBackground); + l_Control.name = "BSPTextSegmentedControl"; + l_Control.SetField("_container", l_Prefab._container); + l_Control._hideCellBackground = p_HideCellBackground; RectTransform l_RectTransform = l_Control.transform as RectTransform; l_RectTransform.anchorMin = Vector2.one * 0.5f; diff --git a/BeatSaberPlus/SDK/UI/HMUIUIUtils.cs b/BeatSaberPlus/SDK/UI/HMUIUIUtils.cs new file mode 100644 index 0000000..5bd722d --- /dev/null +++ b/BeatSaberPlus/SDK/UI/HMUIUIUtils.cs @@ -0,0 +1,97 @@ +using IPA.Utilities; +using System.Linq; +using UnityEngine; +using UnityEngine.EventSystems; +using VRUIControls; + +namespace BeatSaberPlus.SDK.UI +{ + /// + /// View controller utils + /// + public static class HMUIUIUtils + { + private static MainFlowCoordinator m_MainFlowCoordinator; + private static Canvas m_CanvasTemplate; + private static PhysicsRaycasterWithCache m_PhysicsRaycaster; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public static MainFlowCoordinator MainFlowCoordinator { get { + if (m_MainFlowCoordinator) + return m_MainFlowCoordinator; + + m_MainFlowCoordinator = Resources.FindObjectsOfTypeAll().First(); + + return m_MainFlowCoordinator; + } } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create a flow coordinator + /// + /// Flow coordinator type + /// + public static t_Base CreateFlowCoordinator() + where t_Base : HMUI.FlowCoordinator + { + if (m_MainFlowCoordinator == null) + m_MainFlowCoordinator = Resources.FindObjectsOfTypeAll().First(); + + var l_InputModule = m_MainFlowCoordinator._baseInputModule; + var l_Coordinator = new GameObject(typeof(t_Base).Name).AddComponent(); + l_Coordinator.SetField("_baseInputModule", l_InputModule); + + return l_Coordinator; + } + /// + /// Create a view controller + /// + /// View controller type + /// + public static t_Base CreateViewController() + where t_Base : HMUI.ViewController + { + if (m_CanvasTemplate == null) + m_CanvasTemplate = Resources.FindObjectsOfTypeAll().First((x) => x.name == "DropdownTableView"); + + if (m_PhysicsRaycaster == null) + { + m_PhysicsRaycaster = Resources.FindObjectsOfTypeAll().First().GetComponent() + ._physicsRaycaster; + } + + var l_GameObject = new GameObject(typeof(t_Base).Name); + var l_Canvas = l_GameObject.AddComponent(); + l_Canvas.renderMode = m_CanvasTemplate.renderMode; + l_Canvas.scaleFactor = m_CanvasTemplate.scaleFactor; + l_Canvas.referencePixelsPerUnit = m_CanvasTemplate.referencePixelsPerUnit; + l_Canvas.overridePixelPerfect = m_CanvasTemplate.overridePixelPerfect; + l_Canvas.pixelPerfect = m_CanvasTemplate.pixelPerfect; + l_Canvas.planeDistance = m_CanvasTemplate.planeDistance; + l_Canvas.overrideSorting = m_CanvasTemplate.overrideSorting; + l_Canvas.sortingOrder = m_CanvasTemplate.sortingOrder; + l_Canvas.targetDisplay = m_CanvasTemplate.targetDisplay; + l_Canvas.sortingLayerID = m_CanvasTemplate.sortingLayerID; + l_Canvas.additionalShaderChannels = m_CanvasTemplate.additionalShaderChannels; + l_Canvas.sortingLayerName = m_CanvasTemplate.sortingLayerName; + l_Canvas.worldCamera = m_CanvasTemplate.worldCamera; + l_Canvas.normalizedSortingGridSize = m_CanvasTemplate.normalizedSortingGridSize; + + l_GameObject.gameObject.AddComponent().SetField("_physicsRaycaster", m_PhysicsRaycaster); + l_GameObject.gameObject.AddComponent(); + + var l_View = l_GameObject.AddComponent(); + l_View.rectTransform.anchorMin = new Vector2(0.0f, 0.0f); + l_View.rectTransform.anchorMax = new Vector2(1.0f, 1.0f); + l_View.rectTransform.sizeDelta = new Vector2(0.0f, 0.0f); + l_View.rectTransform.anchoredPosition = new Vector2(0.0f, 0.0f); + l_View.gameObject.SetActive(false); + + return l_View; + } + } +} diff --git a/BeatSaberPlus/SDK/UI/ViewFlowCoordinator.cs b/BeatSaberPlus/SDK/UI/HMUIViewFlowCoordinator.cs similarity index 82% rename from BeatSaberPlus/SDK/UI/ViewFlowCoordinator.cs rename to BeatSaberPlus/SDK/UI/HMUIViewFlowCoordinator.cs index 5a912f3..4a6d3f9 100644 --- a/BeatSaberPlus/SDK/UI/ViewFlowCoordinator.cs +++ b/BeatSaberPlus/SDK/UI/HMUIViewFlowCoordinator.cs @@ -1,41 +1,30 @@ -using BeatSaberMarkupLanguage; -using System.Collections; -using System.Collections.Generic; +using System.Collections; using System.Linq; using UnityEngine; +using SwitchQueue = System.Collections.Generic.Queue<(HMUI.ViewController, HMUI.ViewController, HMUI.ViewController)>; + namespace BeatSaberPlus.SDK.UI { /// /// View flow coordinator base class /// - /// - public abstract class ViewFlowCoordinator : HMUI.FlowCoordinator - where T : ViewFlowCoordinator + /// + public abstract class HMUIViewFlowCoordinator : HMUI.FlowCoordinator + where t_Base : HMUIViewFlowCoordinator { - /// - /// Singleton - /// - private static T m_Instance = null; - /// - /// View change queue - /// - private Queue<(HMUI.ViewController, HMUI.ViewController, HMUI.ViewController)> m_SwitchQueue = new Queue<(HMUI.ViewController, HMUI.ViewController, HMUI.ViewController)>(); - /// - /// Is dequeue engaged? - /// - private bool m_IsDequeueEngaged = false; - /// - /// Backup flow coordinator - /// - private HMUI.FlowCoordinator m_BackupFlowCoordinator = null; + private static t_Base m_Instance = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private SwitchQueue m_SwitchQueue = new SwitchQueue(); + private bool m_IsDequeueEngaged = false; + private HMUI.FlowCoordinator m_BackupFlowCoordinator = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Title - /// public abstract string Title { get; } //////////////////////////////////////////////////////////////////////////// @@ -45,10 +34,10 @@ public abstract class ViewFlowCoordinator : HMUI.FlowCoordinator /// Create flow coordinator /// /// - public static T Instance() + public static t_Base Instance() { if (!m_Instance) - m_Instance = BeatSaberUI.CreateFlowCoordinator(); + m_Instance = HMUIUIUtils.CreateFlowCoordinator(); return m_Instance; } @@ -59,10 +48,10 @@ public static T Instance() /// /// Constructor /// - public ViewFlowCoordinator() + public HMUIViewFlowCoordinator() { /// Bind singleton - m_Instance = this as T; + m_Instance = this as t_Base; } //////////////////////////////////////////////////////////////////////////// @@ -74,7 +63,7 @@ public ViewFlowCoordinator() /// Is the first activation ? /// Activation type /// Is the screen system enabling? - protected override sealed void DidActivate(bool p_FirstActivation, bool p_AddedToHierarchy, bool p_ScreenSystemEnabling) + public override sealed void DidActivate(bool p_FirstActivation, bool p_AddedToHierarchy, bool p_ScreenSystemEnabling) { if (p_FirstActivation) { @@ -100,7 +89,7 @@ protected override sealed void DidActivate(bool p_FirstActivation, bool p_AddedT /// When the back button is pressed /// /// Controller instance - protected override sealed void BackButtonWasPressed(HMUI.ViewController p_TopViewController) + public override sealed void BackButtonWasPressed(HMUI.ViewController p_TopViewController) { if (OnBackButtonPressed(p_TopViewController)) return; @@ -150,7 +139,7 @@ public virtual void Present(bool p_IgnoreBackuping = false) } /// Present main view controller else - BeatSaberUI.MainFlowCoordinator.PresentFlowCoordinator(this as HMUI.FlowCoordinator); + HMUIUIUtils.MainFlowCoordinator.PresentFlowCoordinator(this as HMUI.FlowCoordinator); } catch { @@ -181,7 +170,7 @@ public virtual void Dismiss() } /// Back to game main menu - BeatSaberUI.MainFlowCoordinator.DismissFlowCoordinator(this, null); + HMUIUIUtils.MainFlowCoordinator.DismissFlowCoordinator(this); } //////////////////////////////////////////////////////////////////////////// @@ -222,13 +211,10 @@ public void ChangeView(HMUI.ViewController p_NewView, HMUI.ViewController p_Left return; } - if (!m_IsDequeueEngaged && topViewController.isActiveAndEnabled && !topViewController.isInTransition) + if (!m_IsDequeueEngaged && (topViewController == null || (topViewController.isActiveAndEnabled && !topViewController.isInTransition))) DequeueViewController(); if (!m_IsDequeueEngaged && (!topViewController.isActiveAndEnabled || topViewController.isInTransition)) { - if (topViewController is IViewController) - (topViewController as IViewController).ShowViewTransitionLoading(); - m_IsDequeueEngaged = true; CP_SDK.Unity.MTCoroutineStarter.Start(DequeueViewControllerWhileOldInTransition()); } @@ -296,11 +282,10 @@ private IEnumerator DequeueViewControllerWhileOldInTransition() /// /// Create view controller /// - /// + /// /// - public static V CreateViewController() where V : HMUI.ViewController - { - return BeatSaberUI.CreateViewController(); - } + public static t_ViewType CreateViewController() + where t_ViewType : HMUI.ViewController + => HMUIUIUtils.CreateViewController(); } } diff --git a/BeatSaberPlus/SDK/UI/IHMUIViewController.cs b/BeatSaberPlus/SDK/UI/IHMUIViewController.cs new file mode 100644 index 0000000..05c75a0 --- /dev/null +++ b/BeatSaberPlus/SDK/UI/IHMUIViewController.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BeatSaberPlus.SDK.UI +{ + /// + /// IHMUIViewController interface + /// + public abstract class IHMUIViewController : HMUI.ViewController + { + public abstract RectTransform RTransform { get; } + public abstract RectTransform ModalContainerRTransform { get; } + public abstract CanvasGroup CGroup { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Show a modal + /// + /// Modal to show + public abstract void ShowModal(CP_SDK.UI.IModal p_Modal); + /// + /// Close a modal + /// + /// Modal to close + public abstract void CloseModal(CP_SDK.UI.IModal p_Modal); + /// + /// Close all modals + /// + public abstract void CloseAllModals(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Show color picker modal + /// + /// Base value + /// Support opacity? + /// On changed callback + /// On cancel callback + public abstract void ShowColorPickerModal(Color p_Value, bool p_Opacity, Action p_Callback, Action p_CancelCallback = null); + /// + /// Show the confirmation modal + /// + /// Message to display + /// Callback + public abstract void ShowConfirmationModal(string p_Message, Action p_Callback); + /// + /// Show the dropdown modal + /// + /// Available options + /// Selected option + /// Callback + public abstract void ShowDropdownModal(List p_Options, string p_Selected, Action p_Callback); + /// + /// Show the keyboard modal + /// + /// Value + /// Callback + /// On cancel callback + /// Custom keys + public abstract void ShowKeyboardModal(string p_Value, Action p_Callback, Action p_CancelCallback = null, List<(string, Action, string)> p_CustomKeys = null); + /// + /// Show the loading modal + /// + /// Message to show + /// Show cancel button + /// On cancel callback + public abstract void ShowLoadingModal(string p_Message = "", bool p_CancelButton = false, Action p_CancelCallback = null); + /// + /// Show the message modal + /// + /// Message to display + /// Callback + public abstract void ShowMessageModal(string p_Message, Action p_Callback = null); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get current value + /// + /// + public abstract string KeyboardModal_GetValue(); + /// + /// Set value + /// + /// New value + public abstract void KeyboardModal_SetValue(string p_Value); + /// + /// Append + /// + /// Value to append + public abstract void KeyboardModal_Append(string p_ToAppend); + /// + /// Set message + /// + /// New message + public abstract void LoadingModal_SetMessage(string p_Message); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Close color picker modal + /// + public abstract void CloseColorPickerModal(); + /// + /// Close the confirmation modal + /// + public abstract void CloseConfirmationModal(); + /// + /// Close the dropdown modal + /// + public abstract void CloseDropdownModal(); + /// + /// Close the keyboard modal + /// + public abstract void CloseKeyboardModal(); + /// + /// Close the loading modal + /// + public abstract void CloseLoadingModal(); + /// + /// Close the message modal + /// + public abstract void CloseMessageModal(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Show the tooltip + /// + /// World position + /// Tooltip text + public abstract void ShowTooltip(Vector3 p_Position, string p_Text); + /// + /// Hide the tooltip + /// + public abstract void HideTooltip(); + } +} diff --git a/BeatSaberPlus/SDK/UI/IncrementSetting.cs b/BeatSaberPlus/SDK/UI/IncrementSetting.cs deleted file mode 100644 index 0805234..0000000 --- a/BeatSaberPlus/SDK/UI/IncrementSetting.cs +++ /dev/null @@ -1,50 +0,0 @@ -using BeatSaberMarkupLanguage.Parser; -using System.Linq; -using TMPro; - -using BSMLIncrementSetting = BeatSaberMarkupLanguage.Components.Settings.IncrementSetting; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// Increment setting helper - /// - public class IncrementSetting - { - /// - /// Setup a increment setting - /// - /// Setting to setup - /// Action on change - /// Value formatter - /// New value - /// Should remove label - public static void Setup(BSMLIncrementSetting p_Setting, BSMLAction p_Action, BSMLAction p_Formatter, float p_Value, bool p_RemoveLabel) - { - p_Setting.gameObject.SetActive(false); - - if (p_Formatter != null) - p_Setting.formatter = p_Formatter; - - p_Setting.Value = p_Value; - - if (p_Action != null) - p_Setting.onChange = p_Action; - - if (p_RemoveLabel) - { - UnityEngine.GameObject.Destroy(p_Setting.gameObject.GetComponentsInChildren().ElementAt(0).transform.gameObject); - - UnityEngine.RectTransform l_RectTransform = p_Setting.gameObject.transform.GetChild(1) as UnityEngine.RectTransform; - l_RectTransform.anchorMin = UnityEngine.Vector2.zero; - l_RectTransform.anchorMax = UnityEngine.Vector2.one; - l_RectTransform.sizeDelta = UnityEngine.Vector2.one; - - p_Setting.gameObject.GetComponent().preferredWidth = -1f; - } - - p_Setting.gameObject.SetActive(true); - } - - } -} diff --git a/BeatSaberPlus/SDK/UI/Internal/BSMLPrimaryButtonTag.cs b/BeatSaberPlus/SDK/UI/Internal/BSMLPrimaryButtonTag.cs deleted file mode 100644 index ba44351..0000000 --- a/BeatSaberPlus/SDK/UI/Internal/BSMLPrimaryButtonTag.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace BeatSaberPlus.SDK.UI.Internal -{ - ///// - ///// Primary button tag creator - ///// - //public class BSMLPrimaryButtonTag : BeatSaberMarkupLanguage.Tags.ButtonTag - //{ - // public override string[] Aliases => new[] { "primary-button", "action-button" }; - // public override string PrefabButton => "PlayButton"; - - // public override UnityEngine.GameObject CreateObject(UnityEngine.Transform parent) - // { - // return base.CreateObject(parent).AddComponent().gameObject; - // } - //} -} diff --git a/BeatSaberPlus/SDK/UI/LevelDetail.cs b/BeatSaberPlus/SDK/UI/LevelDetail.cs index 632479b..943f218 100644 --- a/BeatSaberPlus/SDK/UI/LevelDetail.cs +++ b/BeatSaberPlus/SDK/UI/LevelDetail.cs @@ -6,6 +6,9 @@ using System.Text.RegularExpressions; using TMPro; using UnityEngine; +using CP_SDK.UI.Components; +using CP_SDK.UI; +using System.Reflection; namespace BeatSaberPlus.SDK.UI { @@ -39,41 +42,48 @@ internal static void Init() //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - private GameObject m_GameObject; - private TextMeshProUGUI m_SongNameText; - private TextMeshProUGUI m_AuthorNameText; - private HMUI.ImageView m_SongCoverImage; - private TextMeshProUGUI m_SongTimeText; - private TextMeshProUGUI m_SongBPMText; - private TextMeshProUGUI m_SongNPSText; - private TextMeshProUGUI m_SongNJSText; - private TextMeshProUGUI m_SongOffsetText; - private TextMeshProUGUI m_SongNotesText; - private TextMeshProUGUI m_SongObstaclesText; - private TextMeshProUGUI m_SongBombsText; - private BeatmapDifficultySegmentedControlController m_DifficultiesSegmentedControllerClone; + private GameObject m_GameObject; + private TextMeshProUGUI m_SongNameText; + private TextMeshProUGUI m_AuthorNameText; + private HMUI.ImageView m_SongCoverImage; + private TextMeshProUGUI m_SongTimeText; + private TextMeshProUGUI m_SongBPMText; + private TextMeshProUGUI m_SongNPSText; + private TextMeshProUGUI m_SongNJSText; + private TextMeshProUGUI m_SongOffsetText; + private TextMeshProUGUI m_SongNotesText; + private TextMeshProUGUI m_SongObstaclesText; + private TextMeshProUGUI m_SongBombsText; + private BeatmapDifficultySegmentedControlController m_DifficultiesSegmentedControllerClone; private BeatmapCharacteristicSegmentedControlController m_CharacteristicSegmentedControllerClone; - private HMUI.TextSegmentedControl m_SongDiffSegmentedControl; - private HMUI.IconSegmentedControl m_SongCharacteristicSegmentedControl; - private UnityEngine.UI.Button m_PracticeButton = null; - private UnityEngine.UI.Button m_PlayButton = null; - private UnityEngine.GameObject m_FavoriteToggle = null; - private CustomPreviewBeatmapLevel m_LocalBeatMap = null; - private Game.BeatMaps.MapDetail m_BeatMap = null; + private HMUI.TextSegmentedControl m_SongDiffSegmentedControl; + private HMUI.IconSegmentedControl m_SongCharacteristicSegmentedControl; + private CSecondaryButton m_SecondaryButton = null; + private CPrimaryButton m_PrimaryButton = null; + private GameObject m_FavoriteToggle = null; + private CustomPreviewBeatmapLevel m_LocalBeatMap = null; + private Game.BeatMaps.MapDetail m_BeatMap = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - private double m_Time = 0; - private float m_BPM = 0; - private float m_NPS = 0; - private int m_NJS = 0; - private float m_Offset = 0; - private int m_Notes = 0; - private int m_Obstacles = 0; - private int m_Bombs = 0; + private double m_Time = 0; + private float m_BPM = 0; + private float m_NPS = 0; + private int m_NJS = 0; + private float m_Offset = 0; + private int m_Notes = 0; + private int m_Obstacles = 0; + private int m_Bombs = 0; + private string m_Difficulty = ""; + private HMUI.IconSegmentedControl.DataItem m_Characteristic = null; - private string m_Difficulty = ""; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public Action OnSecondaryButton; + public Action OnPrimaryButton; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -165,7 +175,7 @@ public HMUI.IconSegmentedControl.DataItem Characteristic { get => m_Characteristic; set { m_Characteristic = value; - m_SongCharacteristicSegmentedControl.SetData(new List() { + m_SongCharacteristicSegmentedControl.SetDataNoHoverHint(new List() { value }.ToArray()); } @@ -184,8 +194,8 @@ public string Difficulty { //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - public BeatmapCharacteristicSO SelectedBeatmapCharacteristicSO = null; - public BeatmapDifficulty SelecteBeatmapDifficulty = BeatmapDifficulty.Easy; + public BeatmapCharacteristicSO SelectedBeatmapCharacteristicSO = null; + public BeatmapDifficulty SelecteBeatmapDifficulty = BeatmapDifficulty.Easy; public event Action OnActiveDifficultyChanged; //////////////////////////////////////////////////////////////////////////// @@ -201,8 +211,8 @@ public LevelDetail(UnityEngine.Transform p_Parent) m_GameObject = UnityEngine.GameObject.Instantiate(m_SongDetailViewTemplate, p_Parent); var l_BSMLObjects = m_GameObject.GetComponentsInChildren().Where(x => x.gameObject.name.StartsWith("BSML")); - var l_HoverHints = m_GameObject.GetComponentsInChildren(); - var l_LocalHoverHints = m_GameObject.GetComponentsInChildren(); + var l_HoverHints = m_GameObject.GetComponentsInChildren(true); + var l_LocalHoverHints = m_GameObject.GetComponentsInChildren(true); foreach (var l_Current in l_BSMLObjects) GameObject.Destroy(l_Current.gameObject); foreach (var l_Current in l_HoverHints) GameObject.Destroy(l_Current); @@ -213,40 +223,52 @@ public LevelDetail(UnityEngine.Transform p_Parent) m_FavoriteToggle.SetActive(false); /// Find play buttons - var l_PracticeButton = m_GameObject.transform.Find("ActionButtons").Find("PracticeButton"); - var l_PlayButton = m_GameObject.transform.Find("ActionButtons").Find("ActionButton"); + var l_ActionButtons = m_GameObject.transform.Find("ActionButtons"); + var l_PracticeButton = l_ActionButtons.Find("PracticeButton"); + var l_PlayButton = l_ActionButtons.Find("ActionButton"); /// Re-bind play button if (l_PlayButton.GetComponent()) { - m_PracticeButton = l_PracticeButton.GetComponent(); - m_PracticeButton.onClick.RemoveAllListeners(); - - m_PlayButton = l_PlayButton.GetComponent(); - m_PlayButton.onClick.RemoveAllListeners(); - - GameObject.Destroy(m_PracticeButton.GetComponentInChildren()); - GameObject.Destroy(m_PlayButton.GetComponentInChildren()); - - SetPracticeButtonEnabled(false); - SetPracticeButtonText("?"); - SetPlayButtonEnabled(true); - SetPlayButtonText("?"); + var l_ActionButtonsRTransform = l_ActionButtons.transform as RectTransform; + l_ActionButtonsRTransform.anchoredPosition = new Vector2(-0.5f, l_ActionButtonsRTransform.anchoredPosition.y); + + var l_ButtonsParent = l_PlayButton.transform.parent; + GameObject.Destroy(l_PracticeButton.gameObject); + GameObject.Destroy(l_PlayButton.gameObject); + + m_SecondaryButton = UISystem.SecondaryButtonFactory.Create("Secondary", l_ButtonsParent); + m_SecondaryButton.SetText("Secondary"); + m_SecondaryButton.SetHeight(8f).SetWidth(30f); + m_SecondaryButton.OnClick(OnSecondaryButtonClicked); + + m_PrimaryButton = UISystem.PrimaryButtonFactory.Create("Primary", l_ButtonsParent); + m_PrimaryButton.SetText("Primary"); + m_PrimaryButton.SetHeight(8f).SetWidth(30f); + m_PrimaryButton.OnClick(OnPrimaryButtonClicked); + + SetSecondaryButtonEnabled(false); + SetSecondaryButtonText("?"); + SetPrimaryButtonEnabled(true); + SetPrimaryButtonText("?"); } m_CharacteristicSegmentedControllerClone = m_GameObject.transform.Find("BeatmapCharacteristic").Find("BeatmapCharacteristicSegmentedControl").GetComponent(); - m_SongCharacteristicSegmentedControl = HorizontalIconSegmentedControl.Create(m_CharacteristicSegmentedControllerClone.transform as RectTransform, true); + m_SongCharacteristicSegmentedControl = HMUIIconSegmentedControl.Create(m_CharacteristicSegmentedControllerClone.transform as RectTransform, true); m_DifficultiesSegmentedControllerClone = m_GameObject.transform.Find("BeatmapDifficulty").GetComponentInChildren(); - m_SongDiffSegmentedControl = TextSegmentedControl.Create(m_DifficultiesSegmentedControllerClone.transform as RectTransform, true); + m_SongDiffSegmentedControl = HMUITextSegmentedControl.Create(m_DifficultiesSegmentedControllerClone.transform as RectTransform, true); var l_LevelBarBig = m_GameObject.transform.Find("LevelBarBig"); m_SongNameText = l_LevelBarBig.GetComponentsInChildren().First(x => x.gameObject.name == "SongNameText"); m_AuthorNameText = l_LevelBarBig.GetComponentsInChildren().First(x => x.gameObject.name == "AuthorNameText"); - m_AuthorNameText.richText = true; m_SongCoverImage = l_LevelBarBig.Find("SongArtwork").GetComponent(); + m_SongCoverImage.rectTransform.anchoredPosition = new Vector2( 2.000f, m_SongCoverImage.rectTransform.anchoredPosition.y); + m_SongNameText.rectTransform.anchoredPosition = new Vector2(-0.195f, m_SongNameText.rectTransform.anchoredPosition.y); + m_AuthorNameText.richText = true; + /// Disable multiline l_LevelBarBig.Find("MultipleLineTextContainer").gameObject.SetActive(false); @@ -281,14 +303,14 @@ public LevelDetail(UnityEngine.Transform p_Parent) (m_SongBombsText.transform.parent.transform as UnityEngine.RectTransform).sizeDelta = l_SizeDelta; /// Patch - var l_OffsetTexture = BeatSaberMarkupLanguage.Utilities.FindTextureInAssembly("BeatSaberPlus.SDK.UI.Resources.Offset.png"); - var l_OffsetSprite = CP_SDK.Unity.SpriteU.CreateFromTexture(l_OffsetTexture, 100f, Vector2.one * 16f); + var l_OffsetTexture = CP_SDK.Unity.Texture2DU.CreateFromRaw(CP_SDK.Misc.Resources.FromPath(Assembly.GetExecutingAssembly(), "BeatSaberPlus.SDK.UI.Resources.Offset.png")); + var l_OffsetSprite = CP_SDK.Unity.SpriteU.CreateFromTextureWithBorders(l_OffsetTexture, 100f, Vector2.one * 16f); m_SongOffsetText = GameObject.Instantiate(m_SongNPSText.transform.parent.gameObject, m_SongNPSText.transform.parent.parent).GetComponentInChildren(); m_SongOffsetText.transform.parent.SetAsFirstSibling(); m_SongOffsetText.transform.parent.GetComponentInChildren().sprite = l_OffsetSprite; - var l_NJSTexture = BeatSaberMarkupLanguage.Utilities.FindTextureInAssembly("BeatSaberPlus.SDK.UI.Resources.NJS.png"); - var l_NJSSprite = CP_SDK.Unity.SpriteU.CreateFromTexture(l_NJSTexture, 100f, Vector2.one * 16f); + var l_NJSTexture = CP_SDK.Unity.Texture2DU.CreateFromRaw(CP_SDK.Misc.Resources.FromPath(Assembly.GetExecutingAssembly(), "BeatSaberPlus.SDK.UI.Resources.NJS.png")); + var l_NJSSprite = CP_SDK.Unity.SpriteU.CreateFromTextureWithBorders(l_NJSTexture, 100f, Vector2.one * 16f); m_SongNJSText = GameObject.Instantiate(m_SongNPSText.transform.parent.gameObject, m_SongNPSText.transform.parent.parent).GetComponentInChildren(); m_SongNJSText.transform.parent.SetAsFirstSibling(); m_SongNJSText.transform.parent.GetComponentInChildren().sprite = l_NJSSprite; @@ -307,6 +329,22 @@ public LevelDetail(UnityEngine.Transform p_Parent) m_SongCharacteristicSegmentedControl.didSelectCellEvent += OnCharacteristicChanged; m_SongDiffSegmentedControl.didSelectCellEvent += OnDifficultyChanged; + try + { + foreach (var l_Text in m_GameObject.GetComponentsInChildren(true)) + l_Text.fontStyle &= ~FontStyles.Italic; + + foreach (var l_Image in m_GameObject.GetComponentsInChildren(true)) + { + m_SongCoverImage._skew = 0f; + m_SongCoverImage.SetAllDirty(); + } + } + catch (System.Exception) + { + + } + m_GameObject.SetActive(true); } @@ -536,7 +574,7 @@ public bool FromSongCore(CustomPreviewBeatmapLevel p_BeatMap, Sprite p_Cover) /// Store beatmap m_LocalBeatMap = p_BeatMap; - m_SongCharacteristicSegmentedControl.SetData(l_Characteristics.ToArray()); + m_SongCharacteristicSegmentedControl.SetDataNoHoverHint(l_Characteristics.ToArray()); m_SongCharacteristicSegmentedControl.SelectCellWithNumber(0); OnCharacteristicChanged(null, 0); @@ -676,7 +714,7 @@ public bool FromBeatSaver(Game.BeatMaps.MapDetail p_BeatMap, Sprite p_Cover) /// Store beatmap m_BeatMap = p_BeatMap; - m_SongCharacteristicSegmentedControl.SetData(l_Characteristics.ToArray()); + m_SongCharacteristicSegmentedControl.SetDataNoHoverHint(l_Characteristics.ToArray()); m_SongCharacteristicSegmentedControl.SelectCellWithNumber(0); OnCharacteristicChanged(null, 0); @@ -705,23 +743,13 @@ public void SetFavoriteToggleEnabled(bool p_Value) /// /// Default image /// Enable image - public void SetFavoriteToggleImage(string p_Default, string p_Enabled) + public void SetFavoriteToggleImage(Sprite p_Default, Sprite p_Enabled) { var l_IVDefault = m_FavoriteToggle.transform.GetChild(0).GetComponent(); var l_IVMarked = m_FavoriteToggle.transform.GetChild(1).GetComponent(); - BeatSaberMarkupLanguage.Utilities.GetData(p_Default, (p_Bytes) => - { - var l_Texture = new Texture2D(2, 2); - if (l_Texture.LoadImage(p_Bytes)) - l_IVDefault.sprite = Sprite.Create(l_Texture, new Rect(0, 0, l_Texture.width, l_Texture.height), UnityEngine.Vector2.one * 0.5f, 100); - }); - BeatSaberMarkupLanguage.Utilities.GetData(p_Enabled, (p_Bytes) => - { - var l_Texture = new Texture2D(2, 2); - if (l_Texture.LoadImage(p_Bytes)) - l_IVMarked.sprite = Sprite.Create(l_Texture, new Rect(0, 0, l_Texture.width, l_Texture.height), UnityEngine.Vector2.one * 0.5f, 100); - }); + l_IVDefault.sprite = p_Default; + l_IVMarked.sprite = p_Enabled; } /// /// Set favorite toggle hover hint @@ -763,7 +791,7 @@ public void SetFavoriteToggleCallback(Action public void ReverseButtonsOrder() { - m_PracticeButton.transform.SetAsLastSibling(); + m_SecondaryButton.transform.SetAsLastSibling(); } //////////////////////////////////////////////////////////////////////////// @@ -773,17 +801,17 @@ public void ReverseButtonsOrder() /// Set button enabled state /// /// New value - public void SetPracticeButtonEnabled(bool p_Value) + public void SetSecondaryButtonEnabled(bool p_Value) { - m_PracticeButton.gameObject.SetActive(p_Value); + m_SecondaryButton.gameObject.SetActive(p_Value); } /// /// Set button enabled state /// /// New value - public void SetPlayButtonEnabled(bool p_Value) + public void SetPrimaryButtonEnabled(bool p_Value) { - m_PlayButton.gameObject.SetActive(p_Value); + m_PrimaryButton.gameObject.SetActive(p_Value); } /// /// Set button enabled interactable @@ -791,49 +819,31 @@ public void SetPlayButtonEnabled(bool p_Value) /// New value public void SetPracticeButtonInteractable(bool p_Value) { - m_PracticeButton.interactable = p_Value; + m_SecondaryButton.SetInteractable(p_Value); } /// /// Set button enabled interactable /// /// New value - public void SetPlayButtonInteractable(bool p_Value) + public void SetPrimaryButtonInteractable(bool p_Value) { - m_PlayButton.interactable = p_Value; + m_PrimaryButton.SetInteractable(p_Value); } /// /// Set button text /// /// New value - public void SetPracticeButtonText(string p_Value) + public void SetSecondaryButtonText(string p_Value) { - m_PracticeButton.transform.Find("Content").GetComponentInChildren().text = p_Value; + m_SecondaryButton.SetText(p_Value); } /// /// Set button text /// /// New value - public void SetPlayButtonText(string p_Value) - { - m_PlayButton.transform.Find("Content").GetComponentInChildren().text = p_Value; - } - /// - /// Set left button action - /// - /// New value - public void SetPracticeButtonAction(UnityEngine.Events.UnityAction p_Value) + public void SetPrimaryButtonText(string p_Value) { - m_PracticeButton.onClick.RemoveAllListeners(); - m_PracticeButton.onClick.AddListener(p_Value); - } - /// - /// Set right button action - /// - /// New value - public void SetPlayButtonAction(UnityEngine.Events.UnityAction p_Value) - { - m_PlayButton.onClick.RemoveAllListeners(); - m_PlayButton.onClick.AddListener(p_Value); + m_PrimaryButton.SetText(p_Value); } //////////////////////////////////////////////////////////////////////////// @@ -987,6 +997,16 @@ private void OnDifficultyChanged(HMUI.SegmentedControl p_SegmentControl, int p_I OnActiveDifficultyChanged.Invoke(GetIDifficultyBeatMap()); } } + /// + /// Secondary button on click + /// + private void OnSecondaryButtonClicked() + => OnSecondaryButton?.Invoke(); + /// + /// Primary button on click + /// + private void OnPrimaryButtonClicked() + => OnPrimaryButton?.Invoke(); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/BeatSaberPlus/SDK/UI/ListSetting.cs b/BeatSaberPlus/SDK/UI/ListSetting.cs deleted file mode 100644 index e0de179..0000000 --- a/BeatSaberPlus/SDK/UI/ListSetting.cs +++ /dev/null @@ -1,44 +0,0 @@ -using BeatSaberMarkupLanguage.Parser; -using System.Linq; -using TMPro; - -using BSMLListSetting = BeatSaberMarkupLanguage.Components.Settings.ListSetting; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// List setting helper - /// - public class ListSetting - { - /// - /// Setup a list setting - /// - /// Setting to setûp - /// Action on change - /// Should remove label - public static void Setup(BSMLListSetting p_Setting, BSMLAction p_Action, bool p_RemoveLabel) - { - p_Setting.gameObject.SetActive(false); - - if (p_Action != null) - p_Setting.onChange = p_Action; - - p_Setting.updateOnChange = true; - - if (p_RemoveLabel) - { - UnityEngine.GameObject.Destroy(p_Setting.gameObject.GetComponentsInChildren().ElementAt(0).transform.gameObject); - - UnityEngine.RectTransform l_RectTransform = p_Setting.gameObject.transform.GetChild(1) as UnityEngine.RectTransform; - l_RectTransform.anchorMin = UnityEngine.Vector2.zero; - l_RectTransform.anchorMax = UnityEngine.Vector2.one; - l_RectTransform.sizeDelta = UnityEngine.Vector2.one; - - p_Setting.gameObject.GetComponent().preferredWidth = -1f; - } - - p_Setting.gameObject.SetActive(true); - } - } -} diff --git a/BeatSaberPlus/SDK/UI/ModalView.cs b/BeatSaberPlus/SDK/UI/ModalView.cs deleted file mode 100644 index d18fa38..0000000 --- a/BeatSaberPlus/SDK/UI/ModalView.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Linq; -using UnityEngine; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// Modal helper class - /// - public class ModalView - { - /// - /// Setup loading control component - /// - /// Modal game object - /// - public static LoadingControl SetupLoadingControl(HMUI.ModalView p_Modal) - { - if (!p_Modal) - return null; - - var l_Control = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_Control == null) - return null; - - var l_LoadingControl = GameObject.Instantiate(l_Control, p_Modal.transform); - l_LoadingControl.transform.SetAsLastSibling(); - - var l_Touchable = l_LoadingControl.GetComponent(); - if (l_Touchable) - GameObject.Destroy(l_Touchable); - - return l_LoadingControl; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Set modal opacity - /// - /// Modal game object - /// New opacity - /// - public static bool SetOpacity(GameObject p_Modal, float p_Opacity) - { - if (!p_Modal) - return false; - - var l_BG = p_Modal.gameObject.transform.Find("BG"); - var l_Image = l_BG?.GetComponent() ?? null; - - if (l_Image) - { - /// Update background color - var l_Color = l_Image.color; - l_Color.a = p_Opacity; - - l_Image.color = l_Color; - - return true; - } - - return false; - } - /// - /// Set modal opacity - /// - /// Modal game object - /// New opacity - /// - public static bool SetOpacity(HMUI.ModalView p_Modal, float p_Opacity) - { - return SetOpacity(p_Modal != null ? p_Modal.gameObject : null, p_Opacity); - } - } -} diff --git a/BeatSaberPlus/SDK/UI/Patches/BSMLColorSetting.cs b/BeatSaberPlus/SDK/UI/Patches/BSMLColorSetting.cs deleted file mode 100644 index edd83e7..0000000 --- a/BeatSaberPlus/SDK/UI/Patches/BSMLColorSetting.cs +++ /dev/null @@ -1,35 +0,0 @@ -using HarmonyLib; -using System; - -namespace BeatSaberPlus.SDK.UI.Patches -{ - [HarmonyPatch(typeof(BeatSaberMarkupLanguage.Components.Settings.ColorSetting), nameof(BeatSaberMarkupLanguage.Components.Settings.ColorSetting.ApplyValue), - new Type[] { })] - public class BSMLColorSetting_ApplyValue - { - /// - /// Prefix - /// - /// ColorSetting instance - internal static void Postfix(ref BeatSaberMarkupLanguage.Components.Settings.ColorSetting __instance) - { - if (__instance.updateOnChange) - __instance.onChange?.Invoke(__instance.CurrentColor); - } - } - - [HarmonyPatch(typeof(BeatSaberMarkupLanguage.Components.Settings.ColorSetting), nameof(BeatSaberMarkupLanguage.Components.Settings.ColorSetting.ReceiveValue), - new Type[] { })] - public class BSMLColorSetting_ReceiveValue - { - /// - /// Prefix - /// - /// ColorSetting instance - internal static void Postfix(ref BeatSaberMarkupLanguage.Components.Settings.ColorSetting __instance) - { - if (__instance.updateOnChange) - __instance.onChange?.Invoke(__instance.CurrentColor); - } - } -} diff --git a/BeatSaberPlus/SDK/UI/Patches/PSimpleTextDropdown.cs b/BeatSaberPlus/SDK/UI/Patches/PSimpleTextDropdown.cs deleted file mode 100644 index c453b4b..0000000 --- a/BeatSaberPlus/SDK/UI/Patches/PSimpleTextDropdown.cs +++ /dev/null @@ -1,27 +0,0 @@ -using HarmonyLib; -using IPA.Utilities; -using System; -using TMPro; - -namespace BeatSaberPlus.SDK.UI.Patches -{ - [HarmonyPatch(typeof(HMUI.SimpleTextDropdown))] - [HarmonyPatch(nameof(HMUI.SimpleTextDropdown.CellForIdx), new Type[] { typeof(HMUI.TableView), typeof(int) })] - public class PSimpleTextDropdown - { - /// - /// Postfix - /// - /// Result - internal static void Postfix(ref HMUI.TableCell __result) - { - if (!(__result is SimpleTextTableCell)) - return; - - var l_Cell = __result as SimpleTextTableCell; - var l_Text = l_Cell.GetField("_text"); - if (l_Text != null && l_Text) - l_Text.richText = true; - } - } -} diff --git a/BeatSaberPlus/SDK/UI/Patches/PVRPointer.cs b/BeatSaberPlus/SDK/UI/Patches/PVRPointer.cs new file mode 100644 index 0000000..2e0b36e --- /dev/null +++ b/BeatSaberPlus/SDK/UI/Patches/PVRPointer.cs @@ -0,0 +1,33 @@ +using HarmonyLib; +using System; +using VRUIControls; + +namespace BeatSaberPlus.SDK.UI.Patches +{ + [HarmonyPatch(typeof(VRPointer))] +#if BEATSABER_1_29_4_OR_NEWER + [HarmonyPatch(nameof(VRPointer.EnabledLastSelectedPointer), new Type[] { })] +#else + [HarmonyPatch(nameof(VRPointer.OnEnable), new Type[] { })] +#endif + internal class PVRPointer + { + /// + /// On enable event + /// + internal static event Action OnActivated; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Prefix + /// + /// VRPointer instance + internal static void Postfix(VRPointer __instance) + { + try { OnActivated?.Invoke(__instance); } + catch { } + } + } +} diff --git a/BeatSaberPlus/SDK/UI/SliderSetting.cs b/BeatSaberPlus/SDK/UI/SliderSetting.cs deleted file mode 100644 index 8524c15..0000000 --- a/BeatSaberPlus/SDK/UI/SliderSetting.cs +++ /dev/null @@ -1,155 +0,0 @@ -using BeatSaberMarkupLanguage.Parser; -using System.Linq; -using TMPro; - -using BSMLSliderSetting = BeatSaberMarkupLanguage.Components.Settings.SliderSetting; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// Slider setting helper - /// - public class SliderSetting - { - /// - /// Setup a toggle setting - /// - /// Setting to setûp - /// Action on change - /// Value formatter - /// New value - /// Should remove label - /// Add Inc/dec buttons - /// New rect min - /// New rect max - public static void Setup(BSMLSliderSetting p_Setting, - BSMLAction p_Action, - BSMLAction p_Formatter, - float p_Value, - bool p_RemoveLabel, - bool p_AddControls = false, - UnityEngine.Vector2 p_NewRectMin = default, - UnityEngine.Vector2 p_NewRectMax = default) - { - p_Setting.gameObject.SetActive(false); - - if (p_Formatter != null) - p_Setting.formatter = p_Formatter; - - p_Setting.slider.value = p_Value; - - if (p_Action != null) - p_Setting.onChange = p_Action; - - p_Setting.updateOnChange = true; - - if (p_RemoveLabel) - { - UnityEngine.GameObject.Destroy(p_Setting.gameObject.GetComponentsInChildren().ElementAt(0).transform.gameObject); - - UnityEngine.RectTransform l_RectTransform = p_Setting.gameObject.transform.GetChild(1) as UnityEngine.RectTransform; - l_RectTransform.anchorMin = UnityEngine.Vector2.zero; - l_RectTransform.anchorMax = UnityEngine.Vector2.one; - l_RectTransform.sizeDelta = UnityEngine.Vector2.one; - - p_Setting.gameObject.GetComponent().preferredWidth = -1f; - - if (p_AddControls) - { - l_RectTransform = p_Setting.gameObject.transform.Find("BSMLSlider") as UnityEngine.RectTransform; - l_RectTransform.anchorMin = p_NewRectMin; - l_RectTransform.anchorMax = p_NewRectMax; - - FormattedFloatListSettingsValueController l_BaseSettings = UnityEngine.MonoBehaviour.Instantiate(UnityEngine.Resources.FindObjectsOfTypeAll().First(x => (x.name == "VRRenderingScale")), p_Setting.gameObject.transform, false); - var l_DecButton = l_BaseSettings.transform.GetChild(1).GetComponentsInChildren().First(); - var l_IncButton = l_BaseSettings.transform.GetChild(1).GetComponentsInChildren().Last(); - - l_DecButton.transform.SetParent(p_Setting.gameObject.transform, false); - l_DecButton.name = "BSP_DecButton"; - l_IncButton.transform.SetParent(p_Setting.gameObject.transform, false); - l_IncButton.name = "BSP_IncButton"; - - l_IncButton.transform.SetAsFirstSibling(); - l_DecButton.transform.SetAsFirstSibling(); - - foreach (UnityEngine.Transform l_Child in l_BaseSettings.transform) - UnityEngine.GameObject.Destroy(l_Child.gameObject); - - UnityEngine.GameObject.Destroy(l_BaseSettings); - - p_Setting.slider.valueDidChangeEvent += (_, p_NewValue) => - { - l_DecButton.interactable = p_NewValue > p_Setting.slider.minValue; - l_IncButton.interactable = p_NewValue < p_Setting.slider.maxValue; - p_Setting.ApplyValue(); - p_Setting.ReceiveValue(); - }; - - l_DecButton.interactable = p_Setting.slider.value > p_Setting.slider.minValue; - l_IncButton.interactable = p_Setting.slider.value < p_Setting.slider.maxValue; - - l_DecButton.onClick.RemoveAllListeners(); - l_DecButton.onClick.AddListener(() => - { - p_Setting.slider.value -= p_Setting.increments; - l_DecButton.interactable = p_Setting.slider.value > p_Setting.slider.minValue; - l_IncButton.interactable = p_Setting.slider.value < p_Setting.slider.maxValue; - p_Setting.slider.HandleNormalizedValueDidChange(p_Setting.slider, p_Setting.slider.normalizedValue); - }); - l_IncButton.onClick.RemoveAllListeners(); - l_IncButton.onClick.AddListener(() => - { - p_Setting.slider.value += p_Setting.increments; - l_DecButton.interactable = p_Setting.slider.value > p_Setting.slider.minValue; - l_IncButton.interactable = p_Setting.slider.value < p_Setting.slider.maxValue; - p_Setting.slider.HandleNormalizedValueDidChange(p_Setting.slider, p_Setting.slider.normalizedValue); - }); - } - } - - p_Setting.gameObject.SetActive(true); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Set slider interactable - /// - /// Instance - /// New state - public static void SetInteractable(BSMLSliderSetting p_Setting, bool p_Interactable) - { - if (p_Setting.slider.interactable == p_Interactable) - return; - - p_Setting.gameObject.SetActive(false); - p_Setting.slider.interactable = p_Interactable; - - if (p_Setting.gameObject.transform.GetChild(2).Find("BG")) - p_Setting.gameObject.transform.GetChild(2).Find("BG").gameObject.SetActive(p_Interactable); - - var l_DecButton = p_Setting.gameObject.transform.Find("BSP_DecButton")?.GetComponent(); - var l_IncButton = p_Setting.gameObject.transform.Find("BSP_IncButton")?.GetComponent(); - - if (l_DecButton != null) l_DecButton.interactable = p_Interactable && p_Setting.slider.value > p_Setting.slider.minValue; - if (l_IncButton != null) l_IncButton.interactable = p_Interactable && p_Setting.slider.value < p_Setting.slider.maxValue; - p_Setting.gameObject.SetActive(true); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Set value - /// - /// Instance - /// New value - public static void SetValue(BSMLSliderSetting p_Setting, float p_Value) - { - p_Setting.slider.value = p_Value; - p_Setting.ApplyValue(); - p_Setting.ReceiveValue(); - } - } -} diff --git a/BeatSaberPlus/SDK/UI/ToggleSetting.cs b/BeatSaberPlus/SDK/UI/ToggleSetting.cs deleted file mode 100644 index c414e9c..0000000 --- a/BeatSaberPlus/SDK/UI/ToggleSetting.cs +++ /dev/null @@ -1,90 +0,0 @@ -using BeatSaberMarkupLanguage.Parser; -using IPA.Utilities; -using System; -using System.Linq; -using TMPro; - -using BSMLToggleSetting = BeatSaberMarkupLanguage.Components.Settings.ToggleSetting; -using BSMLToggleSettingTag = BeatSaberMarkupLanguage.Tags.ToggleSettingTag; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// Toggle setting - /// - public class ToggleSetting - { - /// - /// ToggleSetting creator - /// - private static BSMLToggleSettingTag m_ToggleSettingCreator = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Create a toggle setting - /// - /// Parent transform - /// Toggle caption - /// Toggle callback - /// Hover hint text - public static BSMLToggleSetting Create(UnityEngine.Transform p_Parent, string p_Text, bool p_Enabled, Action p_Action, string p_HoverHint = null) - { - if (m_ToggleSettingCreator == null) - m_ToggleSettingCreator = new BSMLToggleSettingTag(); - - var l_ToggleObject = m_ToggleSettingCreator.CreateObject(p_Parent); - l_ToggleObject.gameObject.SetActive(false); - - var l_Toggle = l_ToggleObject.GetComponent(); - l_Toggle.Text = p_Text; - l_Toggle.Value = p_Enabled; - l_Toggle.toggle.onValueChanged.AddListener((x) => { p_Action(x); }); - - if (!string.IsNullOrEmpty(p_HoverHint)) - { - HMUI.HoverHint l_HoverHint = l_ToggleObject.GetComponent() ?? l_ToggleObject.AddComponent(); - l_HoverHint.text = p_HoverHint; - l_HoverHint.SetField("_hoverHintController", UnityEngine.Resources.FindObjectsOfTypeAll().First()); - } - - l_ToggleObject.gameObject.SetActive(true); - return l_Toggle; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Setup a toggle setting - /// - /// Setting to setûp - /// Action on change - /// New value - /// Should remove label - public static void Setup(BSMLToggleSetting p_Setting, BSMLAction p_Action, bool p_Value, bool p_RemoveLabel) - { - p_Setting.gameObject.SetActive(false); - - p_Setting.Value = p_Value; - - if (p_Action != null) - p_Setting.onChange = p_Action; - - if (p_RemoveLabel) - { - UnityEngine.GameObject.Destroy(p_Setting.gameObject.GetComponentsInChildren().ElementAt(0).transform.gameObject); - - UnityEngine.RectTransform l_RectTransform = p_Setting.gameObject.transform.GetChild(1) as UnityEngine.RectTransform; - l_RectTransform.anchorMin = UnityEngine.Vector2.zero; - l_RectTransform.anchorMax = UnityEngine.Vector2.one; - l_RectTransform.sizeDelta = UnityEngine.Vector2.one; - - p_Setting.gameObject.GetComponent().preferredWidth = -1f; - } - - p_Setting.gameObject.SetActive(true); - } - } -} diff --git a/BeatSaberPlus/SDK/UI/VerticalIconSegmentedControl.cs b/BeatSaberPlus/SDK/UI/VerticalIconSegmentedControl.cs deleted file mode 100644 index 4699902..0000000 --- a/BeatSaberPlus/SDK/UI/VerticalIconSegmentedControl.cs +++ /dev/null @@ -1,42 +0,0 @@ -using IPA.Utilities; -using System.Linq; -using UnityEngine; -using Zenject; - -namespace BeatSaberPlus.SDK.UI -{ - /// - /// Vertical icon segmented control - /// - public class VerticalIconSegmentedControl - { - /// - /// Create icon segmented control - /// - /// Parent game object transform - /// Should hide cell background - /// GameObject - public static HMUI.IconSegmentedControl Create(RectTransform p_Parent, bool p_HideCellBackground) - { - PlatformLeaderboardViewController l_PlatformLeaderboardViewController = Resources.FindObjectsOfTypeAll().First(); - - HMUI.IconSegmentedControl l_Prefab = l_PlatformLeaderboardViewController.GetField("_scopeSegmentedControl"); - HMUI.IconSegmentedControl l_Control = MonoBehaviour.Instantiate(l_Prefab, p_Parent, false); - - l_Control.name = "BSMLVerticalIconSegmentedControl"; - l_Control.SetField("_container", l_Prefab.GetField("_container")); - l_Control.SetField("_hideCellBackground", p_HideCellBackground); - - RectTransform l_RectTransform = l_Control.transform as RectTransform; - l_RectTransform.anchorMin = Vector2.one * 0.5f; - l_RectTransform.anchorMax = Vector2.one * 0.5f; - l_RectTransform.anchoredPosition = Vector2.zero; - l_RectTransform.pivot = Vector2.one * 0.5f; - - foreach (Transform l_Transform in l_Control.transform) - GameObject.Destroy(l_Transform.gameObject); - - return l_Control; - } - } -} diff --git a/BeatSaberPlus/SDK/UI/ViewController.cs b/BeatSaberPlus/SDK/UI/ViewController.cs index 3399695..5457c43 100644 --- a/BeatSaberPlus/SDK/UI/ViewController.cs +++ b/BeatSaberPlus/SDK/UI/ViewController.cs @@ -1,128 +1,49 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; +using CP_SDK.Unity.Extensions; using System; -using System.Collections; -using System.ComponentModel; -using System.Reflection; -using System.Runtime.CompilerServices; -using TMPro; +using System.Collections.Generic; using UnityEngine; +using UnityEngine.UI; namespace BeatSaberPlus.SDK.UI { - /// - /// IViewController interface - /// - public abstract class IViewController : HMUI.ViewController - { - /// - /// Show view transition loading - /// - public abstract void ShowViewTransitionLoading(); - } - - /// - /// Resource view controller base class - /// - /// - public abstract class ResourceViewController : ViewController - where T : ResourceViewController - { - /// - /// Get view content description XML - /// - /// - protected override sealed string GetViewContentDescription() - { -#if DEBUG - CP_SDK.ChatPlexSDK.Logger.Debug("Loading " + string.Join(".", typeof(T).Namespace, typeof(T).Name)); -#endif - return CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(typeof(T)), string.Join(".", typeof(T).Namespace, typeof(T).Name)); - } - } - /// /// View controller base class /// - /// - public abstract class ViewController : IViewController, INotifyPropertyChanged - where T : ViewController + /// + public abstract class ViewController : IHMUIViewController + where t_Base : ViewController { /// - /// Modal coroutine - /// - private Coroutine m_ModalCoroutine = null; - /// - /// Pending message + /// Singleton /// - private string m_PendingMessage = null; + public static t_Base Instance = null; /// - /// Confirmation modal callback + /// Can UI be updated /// - private Action m_ConfirmationModalCallback = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("SDK_MessageModal")] - protected GameObject m_SDK_MessageModal = null; - [UIComponent("SDK_MessageModal_Text")] - protected TextMeshProUGUI m_SDK_MessageModal_Text = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("SDK_ConfirmModal")] - protected HMUI.ModalView m_SDK_ConfirmModal = null; - [UIComponent("SDK_ConfirmModal_Text")] - protected TextMeshProUGUI m_SDK_ConfirmModal_Text = null; - [UIComponent("SDK_ConfirmModal_Button")] - protected UnityEngine.UI.Button m_SDK_ConfirmModal_Button = null; - [UIComponent("SDK_ConfirmModal_DiscardButton")] - protected UnityEngine.UI.Button m_SDK_ConfirmModal_DiscardButton = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("SDK_LoadingModal")] - protected HMUI.ModalView m_SDK_LoadingModal = null; - [UIObject("SDK_LoadingModal_Text")] - protected GameObject m_SDK_LoadingModalText = null; - private LoadingControl m_SDK_LoadingModal_Spinner = null; -#pragma warning restore CS0414 + public static bool CanBeUpdated => Instance != null && Instance && Instance.isInViewControllerHierarchy && Instance.isActiveAndEnabled && Instance.UICreated; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// BSML parser params - /// - private BeatSaberMarkupLanguage.Parser.BSMLParserParams m_ParserParams = null; + private RectTransform m_RTransform; + private CanvasGroup m_CGroup; + private int m_ModalShowCount; + private CP_SDK.UI.Components.CHLayout m_ModalContainer; + private CP_SDK.UI.Modals.ColorPicker m_ColorPickerModal; + private CP_SDK.UI.Modals.Confirmation m_ConfirmationModal; + private CP_SDK.UI.Modals.Dropdown m_DropdownModal; + private CP_SDK.UI.Modals.Keyboard m_KeyboardModal; + private CP_SDK.UI.Modals.Loading m_LoadingModal; + private CP_SDK.UI.Modals.Message m_MessageModal; + private CP_SDK.UI.Tooltip m_Tooltip; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Singleton - /// - public static T Instance = null; - /// - /// Can UI be updated - /// - public static bool CanBeUpdated => Instance != null && Instance && Instance.isInViewControllerHierarchy && Instance.isActiveAndEnabled && Instance.UICreated; - /// - /// Was UI created - /// - public bool UICreated { get; private set; } = false; - /// - /// Has pending message - /// - public bool HasPendingMessage => m_PendingMessage != null; - /// - /// Property changed event - /// - public event PropertyChangedEventHandler PropertyChanged; + public override RectTransform RTransform => m_RTransform; + public override RectTransform ModalContainerRTransform => m_ModalContainer.RTransform; + public override CanvasGroup CGroup => m_CGroup; + public bool UICreated { get; private set; } = false; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -133,68 +54,39 @@ public abstract class ViewController : IViewController, INotifyPropertyChange /// Is the first activation ? /// Activation type /// Is screen system enabled - protected override sealed void DidActivate(bool p_FirstActivation, bool p_AddedToHierarchy, bool p_ScreenSystemEnabling) + public override sealed void DidActivate(bool p_FirstActivation, bool p_AddedToHierarchy, bool p_ScreenSystemEnabling) { /// Bind singleton - Instance = this as T; + Instance = this as t_Base; if (p_FirstActivation) { - var l_UICode = GetViewContentDescription(); + if (!GetComponent()) + gameObject.AddComponent(); - /// Add loading modal & message modal code - if (true) - { - var l_Closure = l_UICode.LastIndexOf('<'); - var l_NewCode = l_UICode.Substring(0, l_Closure); - - l_NewCode += ""; - l_NewCode += " "; - l_NewCode += " "; - l_NewCode += " "; - l_NewCode += " "; - l_NewCode += ""; - - l_NewCode += ""; - l_NewCode += " "; - l_NewCode += " "; - l_NewCode += " "; - l_NewCode += " "; - l_NewCode += " "; - l_NewCode += " "; - l_NewCode += " "; - l_NewCode += ""; - - l_NewCode += ""; - l_NewCode += " "; - l_NewCode += ""; - l_NewCode += l_UICode.Substring(l_Closure); - - l_UICode = l_NewCode; - } + /// Get components + m_RTransform = GetComponent(); + m_CGroup = GetComponent(); - /// Construct UI - m_ParserParams = BSMLParser.instance.Parse(l_UICode, gameObject, this as T); + /// Create modal container + m_ModalContainer = CP_SDK.UI.UISystem.HLayoutFactory.Create("ModalContainer", transform); + m_ModalContainer.HOrVLayoutGroup.enabled = false; + m_ModalContainer.CSizeFitter.enabled = false; + m_ModalContainer.RTransform.sizeDelta = new Vector2(-10.0f, 0.0f); + m_ModalContainer.gameObject.SetActive(false); - /// Change state - UICreated = true; + var l_ModalContainerCanvasGroup = m_ModalContainer.gameObject.AddComponent(); + l_ModalContainerCanvasGroup.ignoreParentGroups = true; - /// Setup loading modal - m_SDK_LoadingModal_Spinner = SDK.UI.ModalView.SetupLoadingControl(m_SDK_LoadingModal); - SDK.UI.ModalView.SetOpacity(m_SDK_MessageModal, 0.75f); - SDK.UI.ModalView.SetOpacity(m_SDK_ConfirmModal, 0.75f); - SDK.UI.ModalView.SetOpacity(m_SDK_LoadingModal, 0.75f); + m_Tooltip = CP_SDK.UI.Tooltip.Create(transform as RectTransform); - /// Bind events - m_SDK_ConfirmModal_Button.onClick.RemoveAllListeners(); - m_SDK_ConfirmModal_Button.onClick.AddListener(OnSDKConfirmModal); - - /// Make sure buttons are active - m_SDK_ConfirmModal_Button.gameObject.SetActive(true); - m_SDK_ConfirmModal_DiscardButton.gameObject.SetActive(true); + //////////////////////////////////////////////////////////////////////////// /// Call implementation OnViewCreation(); + + /// Change state + UICreated = true; } /// Call implementation @@ -205,7 +97,7 @@ protected override sealed void DidActivate(bool p_FirstActivation, bool p_AddedT /// /// Desactivation type /// Is screen system disabling - protected override sealed void DidDeactivate(bool p_RemovedFromHierarchy, bool p_ScreenSystemDisabling) + public override sealed void DidDeactivate(bool p_RemovedFromHierarchy, bool p_ScreenSystemDisabling) { /// Close all remaining modals CloseAllModals(); @@ -216,7 +108,7 @@ protected override sealed void DidDeactivate(bool p_RemovedFromHierarchy, bool p /// /// On destruction /// - protected override sealed void OnDestroy() + public override sealed void OnDestroy() { /// Call implementation OnViewDestruction(); @@ -231,15 +123,6 @@ protected override sealed void OnDestroy() //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Get view content description XML - /// - /// - protected abstract string GetViewContentDescription(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - /// /// On view creation /// @@ -261,274 +144,281 @@ protected virtual void OnViewDestruction() { } //////////////////////////////////////////////////////////////////////////// /// - /// Set a message to display when this view is activated + /// Create a modal of type t_ModalType /// - /// Message to display - public void SetMessageModal_PendingMessage(string p_PendingMessage) + /// Modal type + /// + public t_ModalType CreateModal() + where t_ModalType : CP_SDK.UI.IModal { - /// Set message - m_PendingMessage = p_PendingMessage; + var l_GameObject = new GameObject(typeof(t_ModalType).FullName, typeof(RectTransform), typeof(t_ModalType), CP_SDK.UI.UISystem.Override_UnityComponent_Image); + var l_Modal = l_GameObject.GetComponent(); + + l_Modal.RTransform.SetParent(m_ModalContainer.RTransform, false); + l_Modal.RTransform.anchorMin = new Vector2(0.0f, 0.0f); + l_Modal.RTransform.anchorMax = new Vector2(1.0f, 1.0f); + l_Modal.RTransform.pivot = new Vector2(0.5f, 0.5f); + l_Modal.RTransform.anchoredPosition = new Vector2(0.0f, 0.0f); + l_Modal.RTransform.sizeDelta = new Vector2(0.0f, 0.0f); + + var l_Background = l_GameObject.GetComponent(CP_SDK.UI.UISystem.Override_UnityComponent_Image) as Image; + l_Background.material = CP_SDK.UI.UISystem.Override_GetUIMaterial(); + l_Background.raycastTarget = true; + l_Background.pixelsPerUnitMultiplier = 1; + l_Background.type = Image.Type.Sliced; + l_Background.sprite = CP_SDK.UI.UISystem.GetUIRoundBGSprite(); + l_Background.color = ColorU.WithAlpha(Color.black, 0.80f); + + l_Modal.gameObject.SetActive(false); + + return l_Modal; } /// - /// Set loading modal download progress + /// Show a modal /// - /// - /// Download progress - protected void SetLoadingModal_DownloadProgress(string p_Text, float p_Progress) + /// Modal to show + public override void ShowModal(CP_SDK.UI.IModal p_Modal) { - if (!UICreated) + if (!p_Modal || p_Modal.RTransform.parent != m_ModalContainer.RTransform) { - CP_SDK.ChatPlexSDK.Logger.Error("[SDK.UI][ViewController] Set loading modal download progress \"" + p_Text + "\" called before View UI's creation"); + CP_SDK.ChatPlexSDK.Logger.Error($"[BeatSaberPlus.SDK.UI][ViewController<{typeof(t_Base).FullName}>.ShowModal] Null or invalid parented modal, not showing!"); return; } - try - { - if (m_SDK_LoadingModal_Spinner.gameObject.activeSelf) - m_SDK_LoadingModal_Spinner.ShowDownloadingProgress(p_Text, p_Progress); - } - catch (Exception) + if (!p_Modal.gameObject.activeSelf) { + m_ModalShowCount++; - } - } + if (m_ModalShowCount == 1) + { + m_ModalContainer.RTransform.SetAsLastSibling(); + m_ModalContainer.gameObject.SetActive(true); - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// + m_CGroup.enabled = true; + m_CGroup.blocksRaycasts = false; + } + + p_Modal.transform.SetAsLastSibling(); + p_Modal.VController = GetComponent(); + p_Modal.gameObject.SetActive(true); + try { p_Modal.OnShow(); } + catch (Exception l_Exception) + { + CP_SDK.ChatPlexSDK.Logger.Error($"[BeatSaberPlus.SDK.UI][ViewController<{typeof(t_Base).FullName}>.ShowModal] Error:"); + CP_SDK.ChatPlexSDK.Logger.Error(l_Exception); + } + } + } /// - /// Show the loading modal + /// Close a modal /// - protected void ShowLoadingModal(string p_Message = "", bool p_Download = false) + /// Modal to close + public override void CloseModal(CP_SDK.UI.IModal p_Modal) { - if (!UICreated) + if (!p_Modal || p_Modal.RTransform.parent != m_ModalContainer.RTransform) { - CP_SDK.ChatPlexSDK.Logger.Error("[SDK.UI][ViewController.ShowLoadingModal] Show loading modal \"" + p_Message + "\" called before View UI's creation"); + CP_SDK.ChatPlexSDK.Logger.Error($"[BeatSaberPlus.SDK.UI][ViewController<{typeof(t_Base).FullName}>.CloseModal] Null or invalid parented modal, not closing!"); return; } - /// Change modal text - m_SDK_LoadingModalText.GetComponent().text = p_Download ? "" : p_Message; - - /// Show the modal - ShowModal("SDK_ShowLoadingModal", () => { - try + if (p_Modal.gameObject.activeSelf) + { + try { p_Modal.OnClose(); } + catch (System.Exception l_Exception) { - /// Show animator - if (!p_Download) - m_SDK_LoadingModal_Spinner.ShowLoading(); - else - m_SDK_LoadingModal_Spinner.ShowDownloadingProgress(p_Message, 0); + CP_SDK.ChatPlexSDK.Logger.Error($"[BeatSaberPlus.SDK.UI][ViewController<{typeof(t_Base).FullName}>.CloseModal] Error:"); + CP_SDK.ChatPlexSDK.Logger.Error(l_Exception); } - catch (Exception) - { - } - }); + p_Modal.gameObject.SetActive(false); + + m_ModalShowCount--; + + if (m_ModalShowCount <= 0) + CloseAllModals(); + } } /// - /// Set the loading modal text + /// Close all modals /// - protected void SetLoadingModalText(string p_Message = "", bool p_Download = false) + public override void CloseAllModals() { - if (!UICreated) + foreach (Transform l_Child in m_ModalContainer.RTransform) { - CP_SDK.ChatPlexSDK.Logger.Error("[SDK.UI][ViewController.ShowLoadingModal] Show loading modal \"" + p_Message + "\" called before View UI's creation"); - return; + var l_Modal = l_Child.GetComponent(); + + if (!l_Modal || !l_Modal.gameObject.activeSelf) + continue; + + l_Modal.OnClose(); + l_Modal.gameObject.SetActive(false); } - /// Change modal text - m_SDK_LoadingModalText.GetComponent().text = p_Download ? "" : p_Message; + m_ModalShowCount = 0; + + m_ModalContainer.gameObject.SetActive(false); + + m_CGroup.blocksRaycasts = true; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + /// - /// Show view transition loading + /// Show the color picker modal /// - public override sealed void ShowViewTransitionLoading() + /// Base value + /// Support opacity? + /// On changed callback + /// On cancel callback + public override void ShowColorPickerModal(Color p_Value, bool p_Opacity, Action p_Callback, Action p_CancelCallback) { - ShowLoadingModal(); + if (!m_ColorPickerModal) + m_ColorPickerModal = CreateModal(); + + ShowModal(m_ColorPickerModal); + m_ColorPickerModal.Init(p_Value, p_Opacity, p_Callback, p_CancelCallback); } /// - /// Show confirmation modal + /// Show the confirmation modal /// - /// Message - /// Callback - protected void ShowConfirmationModal(string p_Message, Action p_OnConfirm) + /// Message to display + /// Callback + public override void ShowConfirmationModal(string p_Message, Action p_Callback) { - if (!UICreated) - { - CP_SDK.ChatPlexSDK.Logger.Error("[SDK.UI][ViewController.ShowConfirmationModal] Show confirmation modal \"" + p_Message + "\" called before View UI's creation"); - return; - } - - /// Store callback - m_ConfirmationModalCallback = p_OnConfirm; - - /// Change modal text - m_SDK_ConfirmModal_Text.GetComponent().text = p_Message; + if (!m_ConfirmationModal) + m_ConfirmationModal = CreateModal(); - ShowModal("SDK_ShowConfirmModal"); + ShowModal(m_ConfirmationModal); + m_ConfirmationModal.Init(p_Message, p_Callback); } /// - /// Show no message modal + /// Show the dropdown modal /// - protected void ShowMessageModal() + /// Available options + /// Selected option + /// Callback + public override void ShowDropdownModal(List p_Options, string p_Selected, Action p_Callback) { - if (m_PendingMessage != null) - { - m_SDK_MessageModal_Text.text = m_PendingMessage; - m_PendingMessage = null; - } + if (!m_DropdownModal) + m_DropdownModal = CreateModal(); - HideLoadingModal(); - - ShowModal("SDK_ShowMessageModal"); + ShowModal(m_DropdownModal); + m_DropdownModal.Init(p_Options, p_Selected, p_Callback); } /// - /// Show no message modal + /// Show the keyboard modal /// - /// Message to display - protected void ShowMessageModal(string p_Message) + /// Value + /// Callback + /// On cancel callback + /// Custom keys + public override void ShowKeyboardModal(string p_Value, Action p_Callback, Action p_CancelCallback = null, List<(string, Action, string)> p_CustomKeys = null) { - SetMessageModal_PendingMessage(p_Message); - ShowMessageModal(); + if (!m_KeyboardModal) + m_KeyboardModal = CreateModal(); + + ShowModal(m_KeyboardModal); + m_KeyboardModal.Init(p_Value, p_Callback, p_CancelCallback, p_CustomKeys); } /// - /// Hide the loading modal + /// Show the loading modal /// - protected void HideLoadingModal() + /// Message to show + /// Show cancel button + /// On cancel callback + public override void ShowLoadingModal(string p_Message = "", bool p_CancelButton = false, Action p_CancelCallback = null) { - CloseModal("SDK_CloseLoadingModal"); - m_SDK_LoadingModal_Spinner.Hide(); - - /// Should display a pending message - if (m_PendingMessage != null) - { - /// Show the modal - ShowMessageModal(); + if (!m_LoadingModal) + m_LoadingModal = CreateModal(); - /// Reset back to default - m_PendingMessage = null; - } + ShowModal(m_LoadingModal); + m_LoadingModal.Init(p_Message, p_CancelButton, p_CancelCallback); } /// - /// Hide the confirmation modal - /// - protected void HideConfirmationModal() => CloseModal("SDK_CloseConfirmModal"); - /// - /// Hide the message modal + /// Show the message modal /// - protected void HideMessageModal() => CloseModal("SDK_CloseMessageModal"); + /// Message to display + /// Callback + public override void ShowMessageModal(string p_Message, Action p_Callback = null) + { + if (!m_MessageModal) + m_MessageModal = CreateModal(); + + ShowModal(m_MessageModal); + m_MessageModal.Init(p_Message, p_Callback); + } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /// - /// Show modal + /// Get current value /// - /// Modal event - /// On emite callback - protected void ShowModal(string p_Event, Action p_Callback = null) - { - if (!UICreated) - { - CP_SDK.ChatPlexSDK.Logger.Error("[SDK.UI][ViewController.ShowModal] Show modal \"" + p_Event + "\" called before View UI's creation"); - return; - } - - if (m_ModalCoroutine != null) - { - StopCoroutine(m_ModalCoroutine); - m_ModalCoroutine = null; - } - - if (CanBeUpdated) - m_ModalCoroutine = StartCoroutine(ShowModalCoroutine(p_Event, p_Callback)); - } + /// + public override string KeyboardModal_GetValue() => m_KeyboardModal.GetValue(); /// - /// Hide modal + /// Set value /// - /// - protected void CloseModal(string p_Event) - { - if (!UICreated) - { - CP_SDK.ChatPlexSDK.Logger.Error("[SDK.UI][ViewController.CloseModal] Close modal \"" + p_Event + "\" called before View UI's creation"); - return; - } - - if (m_ModalCoroutine != null) - { - StopCoroutine(m_ModalCoroutine); - m_ModalCoroutine = null; - } - - m_ParserParams.EmitEvent(p_Event); - } + /// New value + public override void KeyboardModal_SetValue(string p_Value) => m_KeyboardModal.SetValue(p_Value); /// - /// Close all modals + /// Append /// - protected void CloseAllModals() - { - if (m_ModalCoroutine != null) - { - StopCoroutine(m_ModalCoroutine); - m_ModalCoroutine = null; - } - - /// Close all remaining modals - m_ParserParams.EmitEvent("CloseAllModals"); - } + /// Value to append + public override void KeyboardModal_Append(string p_ToAppend) => m_KeyboardModal.Append(p_ToAppend); /// - /// Show modal coroutine + /// Set message /// - /// Modal event - /// On emite callback - /// - private IEnumerator ShowModalCoroutine(string p_Event, Action p_Callback = null) - { - yield return new WaitForEndOfFrame(); - yield return new WaitUntil(() => !isInTransition); - - if (!isInViewControllerHierarchy) - yield break; - - m_ParserParams.EmitEvent(p_Event); - p_Callback?.Invoke(); - m_ModalCoroutine = null; - - yield return null; - } + /// New message + public override void LoadingModal_SetMessage(string p_Message) => m_LoadingModal.SetMessage(p_Message); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /// - /// Cleat queue button + /// Close the color picker modal /// - private void OnSDKConfirmModal() - { - HideConfirmationModal(); - m_ConfirmationModalCallback?.Invoke(); - } + public override void CloseColorPickerModal() => CloseModal(m_ColorPickerModal); + /// + /// Close the confirmation modal + /// + public override void CloseConfirmationModal() => CloseModal(m_ConfirmationModal); + /// + /// Close the dropdown modal + /// + public override void CloseDropdownModal() => CloseModal(m_DropdownModal); + /// + /// Close the keyboard modal + /// + public override void CloseKeyboardModal() => CloseModal(m_KeyboardModal); + /// + /// Close the loading modal + /// + public override void CloseLoadingModal() => CloseModal(m_LoadingModal); + /// + /// Close the message modal + /// + public override void CloseMessageModal() => CloseModal(m_MessageModal); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /// - /// Notify property changed + /// Show the tooltip /// - /// Property name - protected void NotifyPropertyChanged([CallerMemberName] string p_PropertyName = "") + /// World position + /// Tooltip text + public override void ShowTooltip(Vector3 p_Position, string p_Text) { - try - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p_PropertyName)); - } - catch (Exception l_Exception) - { - CP_SDK.ChatPlexSDK.Logger.Error($"[SDK][ViewController.NotifyPropertyChanged] Error Invoking PropertyChanged: {l_Exception.Message}"); - CP_SDK.ChatPlexSDK.Logger.Error(l_Exception); - } + m_Tooltip.transform.SetAsLastSibling(); + m_Tooltip.Show(p_Position, p_Text); } + /// + /// Hide the tooltip + /// + public override void HideTooltip() + => m_Tooltip.Hide(); } } diff --git a/BeatSaberPlus/SDK/UI/Button.cs b/BeatSaberPlus/SDK/UI/__OLD__/Button.cs similarity index 100% rename from BeatSaberPlus/SDK/UI/Button.cs rename to BeatSaberPlus/SDK/UI/__OLD__/Button.cs diff --git a/BeatSaberPlus/SDK/Unity/MaterialU.cs b/BeatSaberPlus/SDK/Unity/MaterialU.cs deleted file mode 100644 index 17e3e43..0000000 --- a/BeatSaberPlus/SDK/Unity/MaterialU.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Linq; - -namespace BeatSaberPlus.SDK.Unity -{ - /// - /// Unity material helper - /// - public class MaterialU - { - /// - /// UI no glow material - /// - private static UnityEngine.Material m_UINoGlowMaterial; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// UI no glow material - /// - public static UnityEngine.Material UINoGlowMaterial - { - get - { - if (m_UINoGlowMaterial == null) - { - m_UINoGlowMaterial = UnityEngine.Resources.FindObjectsOfTypeAll().Where(x => x.name == "UINoGlow").FirstOrDefault(); - - if (m_UINoGlowMaterial != null) - m_UINoGlowMaterial = UnityEngine.Material.Instantiate(m_UINoGlowMaterial); - } - - return m_UINoGlowMaterial; - } - } - } -} diff --git a/BeatSaberPlus/SDK/Unity/ShaderU.cs b/BeatSaberPlus/SDK/Unity/ShaderU.cs deleted file mode 100644 index adc1519..0000000 --- a/BeatSaberPlus/SDK/Unity/ShaderU.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Linq; -using TMPro; -using UnityEngine; - -namespace BeatSaberPlus.SDK.Unity -{ - /// - /// Unity shader helper - /// - public class ShaderU - { - /// - /// TextMeshPro no glow font shader - /// - private static UnityEngine.Shader m_TMPNoGlowFontShader; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// TextMeshPro no glow font shader - /// - public static UnityEngine.Shader TMPNoGlowFontShader - { - get - { - if (m_TMPNoGlowFontShader == null) - m_TMPNoGlowFontShader = Resources.FindObjectsOfTypeAll().Last(x => x.name == "Teko-Medium SDF")?.material?.shader; - - return m_TMPNoGlowFontShader; - } - } - } -} diff --git a/BeatSaberPlus/UI/InfoView.bsml b/BeatSaberPlus/UI/InfoView.bsml deleted file mode 100644 index f013ef4..0000000 --- a/BeatSaberPlus/UI/InfoView.bsml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/BeatSaberPlus/UI/InfoView.cs b/BeatSaberPlus/UI/InfoView.cs deleted file mode 100644 index e69e1f3..0000000 --- a/BeatSaberPlus/UI/InfoView.cs +++ /dev/null @@ -1,73 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using System.Diagnostics; -using UnityEngine; - -namespace BeatSaberPlus.UI -{ - /// - /// Info view UI controller - /// - internal class InfoView : SDK.UI.ResourceViewController - { -#pragma warning disable CS0414 - [UIObject("Background")] - internal GameObject m_Background = null; - [UIValue("Line1")] - private readonly string m_Line1 = "Welcome to BeatSaberPlus by HardCPP#1985"; - [UIValue("Line2")] - private readonly string m_Line2 = "Version 5.0.7"; - [UIValue("Line3")] - private readonly string m_Line3 = " "; - [UIValue("Line4")] - private readonly string m_Line4 = " "; - [UIValue("Line5")] - private readonly string m_Line5 = " "; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to patreon - /// - [UIAction("click-patreon-btn-pressed")] - private void OnPatreonButton() - { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://donate.chatplex.org"); - } - /// - /// Go to discord - /// - [UIAction("click-discord-btn-pressed")] - private void OnDiscordButton() - { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://discord.chatplex.org"); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Documentation button - /// - [UIAction("click-documentation-btn-pressed")] - private void OnDocumentationButton() - { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://github.com/hardcpp/BeatSaberPlus/wiki"); - } - } -} diff --git a/BeatSaberPlus/UI/MainView.bsml b/BeatSaberPlus/UI/MainView.bsml deleted file mode 100644 index 82c951e..0000000 --- a/BeatSaberPlus/UI/MainView.bsml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/BeatSaberPlus/UI/MainView.cs b/BeatSaberPlus/UI/MainView.cs deleted file mode 100644 index 548707c..0000000 --- a/BeatSaberPlus/UI/MainView.cs +++ /dev/null @@ -1,86 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; -using System.Linq; -using TMPro; - -namespace BeatSaberPlus.UI -{ - /// - /// Main view controller - /// - internal class MainView : SDK.UI.ResourceViewController - { - /// - /// Module buttons - /// - private Dictionary m_ModulesButton = new Dictionary(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIObject("ButtonGrid")] - public GameObject m_ButtonGrid; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Layout = m_ButtonGrid.GetComponent(); - l_Layout.constraint = GridLayoutGroup.Constraint.FixedColumnCount; - l_Layout.constraintCount = 3; - - foreach (var l_Module in CP_SDK.ChatPlexSDK.GetModules().Where(x => x is SDK.IBSPModuleBase && x.Type == CP_SDK.EIModuleBaseType.Integrated).Select(x => x as SDK.IBSPModuleBase)) - { - var l_Button = SDK.UI.Button.Create(m_ButtonGrid.transform, l_Module.FancyName, () => { - var l_Items = l_Module.GetSettingsUI(); - MainViewFlowCoordinator.Instance().ChangeView(l_Items.Item1, l_Items.Item2, l_Items.Item3); - }, l_Module.Description, 35f, 10f); - - var l_Text = l_Button.GetComponentInChildren(); - l_Text.fontSize = l_Text.fontSize * 0.85f; - m_ModulesButton.Add(l_Module, l_Button); - } - } - /// - /// On view activation - /// - protected override sealed void OnViewActivation() - { - /// Refresh button states - foreach (var l_Current in m_ModulesButton) - l_Current.Value.interactable = l_Current.Key.IsEnabled; - - /// Show welcome message -#if DEBUG - if (true) -#else - if (BSPConfig.Instance.FirstRun) -#endif - { - ShowMessageModal("Welcome to BeatSaberPlus!\nBy default all modules are disabled, you can enable/disable\nthem any time by clicking the Settings button below"); - BSPConfig.Instance.FirstRun = false; - BSPConfig.Instance.Save(); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to settings - /// - [UIAction("OnSettingsPressed")] - private void OnSettingsPressed() - { - MainViewFlowCoordinator.Instance().SwitchToSettingsView(); - } - } -} diff --git a/BeatSaberPlus/UI/MainViewFlowCoordinator.cs b/BeatSaberPlus/UI/MainViewFlowCoordinator.cs deleted file mode 100644 index adb26dc..0000000 --- a/BeatSaberPlus/UI/MainViewFlowCoordinator.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace BeatSaberPlus.UI -{ - /// - /// UI flow coordinator - /// - public class MainViewFlowCoordinator : SDK.UI.ViewFlowCoordinator - { - /// - /// Title - /// - public override string Title => "Beat Saber Plus V5.0.7"; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Info view - /// - private InfoView m_InfoView; - /// - /// Main view - /// - private MainView m_MainView; - /// - /// Settings view - /// - private SettingsView m_SettingsView; - /// - /// Settings left view - /// - private SettingsLeftView m_SettingsLeftView; - /// - /// Settings right view - /// - private SettingsRightView m_SettingsRightView; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public MainViewFlowCoordinator() - { - m_InfoView = CreateViewController(); - m_MainView = CreateViewController(); - m_SettingsView = CreateViewController(); - m_SettingsLeftView = CreateViewController(); - m_SettingsRightView = CreateViewController(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get initial views controller - /// - /// (Middle, Left, Right) - protected override sealed (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetInitialViewsController() => (m_MainView, m_InfoView, null); - /// - /// On back button pressed - /// - /// Current top view controller - /// True if the event is catched, false if we should dismiss the flow coordinator - protected override sealed bool OnBackButtonPressed(HMUI.ViewController p_TopViewController) - { - if (topViewController != m_MainView) - { - SwitchToMainView(); - return true; - } - - return false; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Switch to main view - /// - public void SwitchToMainView() => ChangeView(m_MainView, m_InfoView, null); - /// - /// Switch to settings view - /// - public void SwitchToSettingsView() => ChangeView(m_SettingsView, m_SettingsLeftView, m_SettingsRightView); - } -} diff --git a/BeatSaberPlus/UI/SettingsLeftView.bsml b/BeatSaberPlus/UI/SettingsLeftView.bsml deleted file mode 100644 index 45c0c1c..0000000 --- a/BeatSaberPlus/UI/SettingsLeftView.bsml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/BeatSaberPlus/UI/SettingsRightView.bsml b/BeatSaberPlus/UI/SettingsRightView.bsml deleted file mode 100644 index d5a310f..0000000 --- a/BeatSaberPlus/UI/SettingsRightView.bsml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/BeatSaberPlus/UI/SettingsRightView.cs b/BeatSaberPlus/UI/SettingsRightView.cs deleted file mode 100644 index 6904bc5..0000000 --- a/BeatSaberPlus/UI/SettingsRightView.cs +++ /dev/null @@ -1,290 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.Parser; -using HMUI; -using System; -using System.Linq; -using TMPro; -using UnityEngine; -using UnityEngine.UI; - -namespace BeatSaberPlus.UI -{ - /// - /// Settings right view controller - /// - internal class SettingsRightView : SDK.UI.ResourceViewController - { - #pragma warning disable CS0649 - [UIObject("TabSelector")] private GameObject m_TabSelector; - private TextSegmentedControl m_TabSelector_TabSelectorControl = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Tools Tab - [UIObject("OBSTab")] private GameObject m_OBSTab = null; - [UIComponent("OBSTab_Enabled")] private ToggleSetting m_OBSTab_Enabled; - [UIComponent("OBSTab_Server")] private TextMeshProUGUI m_OBSTab_Server; - [UIComponent("OBSTab_ChangeServer")] private Button m_OBSTab_ChangeServer; - [UIComponent("OBSTab_ChangePassword")] private Button m_OBSTab_ChangePassword; - [UIComponent("OBSTab_Status")] private TextMeshProUGUI m_OBSTab_Status; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Emotes Tab - [UIObject("EmotesTab")] private GameObject m_EmotesTab = null; - [UIComponent("EmotesTab_BBTVEnabled")] private ToggleSetting m_EmotesTab_BBTVEnabled; - [UIComponent("EmotesTab_FFZEnabled")] private ToggleSetting m_EmotesTab_FFZEnabled; - [UIComponent("EmotesTab_7TVEnabled")] private ToggleSetting m_EmotesTab_7TVEnabled; - [UIComponent("EmotesTab_EmojisEnabled")] private ToggleSetting m_EmotesTab_EmojisEnabled; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - //#region Twitch Tab - //[UIObject("TwitchTab")] private GameObject m_TwitchTab = null; - //[UIComponent("TwitchTab_TwitchEnabled")] private ToggleSetting m_TwitchTab_TwitchEnabled; - //[UIComponent("TwitchTab_TwitchCheermotesEnabled")] private ToggleSetting m_TwitchTab_TwitchCheermotesEnabled; - //#endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("InputKeyboard")] - private ModalKeyboard m_InputKeyboard = null; - [UIValue("InputKeyboardValue")] - private string m_InputKeyboardValue = ""; - private Action m_InputKeyboardCallback; - #pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Event = new BSMLAction(this, this.GetType().GetMethod(nameof(SettingsRightView.OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - - /// Create type selector - m_TabSelector_TabSelectorControl = SDK.UI.TextSegmentedControl.Create(m_TabSelector.transform as RectTransform, false); - m_TabSelector_TabSelectorControl.SetTexts(new string[] { "OBS", "Emotes" }); - m_TabSelector_TabSelectorControl.ReloadData(); - m_TabSelector_TabSelectorControl.didSelectCellEvent += OnTabSelected; - - //////////////////////////////////////////////////////////////////////////// - /// Prepare tabs - //////////////////////////////////////////////////////////////////////////// - - SDK.UI.Backgroundable.SetOpacity(m_OBSTab, 0.50f); - SDK.UI.Backgroundable.SetOpacity(m_EmotesTab, 0.50f); - SDK.UI.ModalView.SetOpacity(m_InputKeyboard.modalView, 0.75f); - - #region Emotes Tab - SDK.UI.ToggleSetting.Setup(m_EmotesTab_BBTVEnabled, l_Event, CP_SDK.Chat.ChatModSettings.Instance.Emotes.ParseBTTVEmotes, false); - SDK.UI.ToggleSetting.Setup(m_EmotesTab_FFZEnabled, l_Event, CP_SDK.Chat.ChatModSettings.Instance.Emotes.ParseFFZEmotes, false); - SDK.UI.ToggleSetting.Setup(m_EmotesTab_7TVEnabled, l_Event, CP_SDK.Chat.ChatModSettings.Instance.Emotes.Parse7TVEmotes, false); - SDK.UI.ToggleSetting.Setup(m_EmotesTab_EmojisEnabled, l_Event, CP_SDK.Chat.ChatModSettings.Instance.Emotes.ParseEmojis, false); - #endregion - - #region Tools Tab - SDK.UI.ToggleSetting.Setup(m_OBSTab_Enabled, l_Event, CP_SDK.OBS.OBSModSettings.Instance.Enabled, true); - - m_OBSTab_Server.text = CP_SDK.OBS.OBSModSettings.Instance.Server; - #endregion - - /// Show first tab by default - OnTabSelected(null, 0); - /// Refresh UI - OnSettingChanged(null); - } - /// - /// On view deactivation - /// - protected override sealed void OnViewDeactivation() - { - BSPConfig.Instance.Save(); - CP_SDK.Chat.ChatModSettings.Instance.Save(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On frame - /// - private void Update() - { - if (m_OBSTab.activeSelf) - { - var l_Status = CP_SDK.OBS.Service.Status; - var l_Text = "Status: "; - - switch (l_Status) - { - case CP_SDK.OBS.Service.EStatus.Disconnected: - case CP_SDK.OBS.Service.EStatus.Connecting: - l_Text += ""; - break; - - case CP_SDK.OBS.Service.EStatus.Authing: - l_Text += ""; - break; - - case CP_SDK.OBS.Service.EStatus.Connected: - l_Text += ""; - break; - - case CP_SDK.OBS.Service.EStatus.AuthRejected: - l_Text += ""; - break; - } - - l_Text += l_Status; - - if (m_OBSTab_Status.text != l_Text) - m_OBSTab_Status.text = l_Text; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When a tab is selected - /// - /// Tab control instance - /// Tab index - private void OnTabSelected(SegmentedControl p_SegmentControl, int p_TabIndex) - { - m_OBSTab.SetActive(p_TabIndex == 0); - m_EmotesTab.SetActive(p_TabIndex == 1); - - LayoutRebuilder.ForceRebuildLayoutImmediate(m_EmotesTab.transform.parent.transform as RectTransform); - } - /// - /// On setting changed - /// - /// New value - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - #region OBS Tab - CP_SDK.OBS.OBSModSettings.Instance.Enabled = m_OBSTab_Enabled.Value; - - m_OBSTab_ChangeServer.interactable = CP_SDK.OBS.OBSModSettings.Instance.Enabled; - m_OBSTab_ChangePassword.interactable = CP_SDK.OBS.OBSModSettings.Instance.Enabled; - #endregion - - #region Emotes Tab - CP_SDK.Chat.ChatModSettings.Instance.Emotes.ParseBTTVEmotes = m_EmotesTab_BBTVEnabled.Value; - CP_SDK.Chat.ChatModSettings.Instance.Emotes.ParseFFZEmotes = m_EmotesTab_FFZEnabled.Value; - CP_SDK.Chat.ChatModSettings.Instance.Emotes.Parse7TVEmotes = m_EmotesTab_7TVEnabled.Value; - CP_SDK.Chat.ChatModSettings.Instance.Emotes.ParseEmojis = m_EmotesTab_EmojisEnabled.Value; - #endregion - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Change server button - /// - [UIAction("OBSTab_ChangeServerButton")] - private void OBSTab_ChangeServerButton() - { - UIShowInputKeyboard(CP_SDK.OBS.OBSModSettings.Instance.Server, (x) => - { - CP_SDK.OBS.OBSModSettings.Instance.Server = x; - m_OBSTab_Server.text = CP_SDK.OBS.OBSModSettings.Instance.Server; - }); - } - /// - /// Change password button - /// - [UIAction("OBSTab_ChangePasswordButton")] - private void OBSTab_ChangePasswordButton() - { - UIShowInputKeyboard(CP_SDK.OBS.OBSModSettings.Instance.Password, (x) => - { - CP_SDK.OBS.OBSModSettings.Instance.Password = x; - }); - } - /// - /// On apply setting button - /// - [UIAction("OBSTab_ApplyButton")] - private void OBSTab_ApplyButton() - => CP_SDK.OBS.Service.ApplyConf(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On apply setting button - /// - [UIAction("EmotesTab_ApplyButton")] - private void EmotesTab_ApplyButton() - { - CP_SDK.Chat.Service.RecacheEmotes(); - ShowMessageModal("OK!"); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Show input keyboard - /// - /// Start value - /// On enter callback - /// Custom keys - public void UIShowInputKeyboard(string p_Value, Action p_Callback) - { - m_InputKeyboardValue = p_Value; - - /// Show keyboard - m_InputKeyboardCallback = p_Callback; - m_InputKeyboard.keyboard.KeyboardText.fontSizeMax = 3; - m_InputKeyboard.keyboard.KeyboardText.fontSizeMin = 3; - m_InputKeyboard.keyboard.KeyboardText.enableAutoSizing = true; - m_InputKeyboard.keyboard.KeyboardCursor.fontSizeMax = 3; - m_InputKeyboard.keyboard.KeyboardCursor.fontSizeMin = 3; - m_InputKeyboard.keyboard.KeyboardCursor.enableAutoSizing = true; - m_InputKeyboard.modalView.Show(true); - } - /// - /// Append value to current keyboard input - /// - /// Value to append - public void UIInputKeyboardAppend(string p_Value) - { - m_InputKeyboard.keyboard.KeyboardText.text += p_Value; - } - /// - /// On input keyboard enter pressed - /// - /// - [UIAction("InputKeyboardEnterPressed")] - private void InputKeyboardEnterPressed(string p_Text) - { - m_InputKeyboardCallback?.Invoke(p_Text); - } - } -} \ No newline at end of file diff --git a/BeatSaberPlus/UI/SettingsView.bsml b/BeatSaberPlus/UI/SettingsView.bsml deleted file mode 100644 index 23a8f91..0000000 --- a/BeatSaberPlus/UI/SettingsView.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/BeatSaberPlus/UI/SettingsView.cs b/BeatSaberPlus/UI/SettingsView.cs deleted file mode 100644 index 0e09db1..0000000 --- a/BeatSaberPlus/UI/SettingsView.cs +++ /dev/null @@ -1,93 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System; -using System.Linq; -using System.Collections.Generic; -using UnityEngine; - -namespace BeatSaberPlus.UI -{ - /// - /// Settings view controller - /// - internal class SettingsView : SDK.UI.ResourceViewController - { - /// - /// Module setting - /// - private Dictionary m_ModulesSetting = new Dictionary(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIObject("SettingGrid")] - private GameObject m_SettingGrid; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Layout = m_SettingGrid.GetComponent(); - l_Layout.constraint = UnityEngine.UI.GridLayoutGroup.Constraint.FixedColumnCount; - l_Layout.constraintCount = 3; - - foreach (var l_Module in CP_SDK.ChatPlexSDK.GetModules().Where(x => x.Type == CP_SDK.EIModuleBaseType.Integrated)) - { - try - { - var l_Setting = SDK.UI.ToggleSetting.Create(m_SettingGrid.transform, l_Module.FancyName, l_Module.IsEnabled, (x) => - { - try - { - l_Module.SetEnabled(x); - CheckChatTutorial(l_Module); - } - catch (Exception p_InitException) - { - CP_SDK.ChatPlexSDK.Logger.Error($"[UI][SettingsView.OnViewCreation] Error on module \"{l_Module.FancyName}\" init"); - CP_SDK.ChatPlexSDK.Logger.Error(p_InitException); - } - }, l_Module.Description); - l_Setting.text.fontSize = 2.5f; - - m_ModulesSetting.Add(l_Module, l_Setting); - } - catch (Exception p_InitException) - { - CP_SDK.ChatPlexSDK.Logger.Error($"[UI][SettingsView.OnViewCreation] Error on module \"{l_Module.FancyName}\" init"); - CP_SDK.ChatPlexSDK.Logger.Error(p_InitException); - } - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Check for chat tutorial - /// - /// Plugin instance - private void CheckChatTutorial(CP_SDK.IModuleBase p_Plugin) - { -#if DEBUG - if (p_Plugin.UseChatFeatures && true) -#else - if (p_Plugin.UseChatFeatures && BSPConfig.Instance.FirstChatCoreRun) -#endif - { - ShowMessageModal("Hey it's seems that this is the first time\nyou use a chat module!\nThe configuration page has been opened in your browser!"); - - CP_SDK.Chat.Service.OpenWebConfigurator(); - - BSPConfig.Instance.FirstChatCoreRun = false; - BSPConfig.Instance.Save(); - } - } - } -} diff --git a/BeatSaberPlus/manifest.json b/BeatSaberPlus/manifest.json index bb20504..f9b1c52 100644 --- a/BeatSaberPlus/manifest.json +++ b/BeatSaberPlus/manifest.json @@ -3,13 +3,15 @@ "id": "BeatSaberPlusCORE", "name": "BeatSaberPlus", "author": "HardCPP#1985", - "version": "5.0.7", + "version": "6.0.8", "description": "", - "gameVersion": "1.25.0", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.1.4", - "BeatSaberMarkupLanguage": "^1.3.4" + "BSIPA": "^4.3.0" }, + "loadAfter": [ + "BeatSaberMarkupLanguage" + ], "links": { "project-home": "https://discord.chatplex.org", "donate": "https://donate.chatplex.org" diff --git a/Modules/BeatSaberPlus_Chat/Plugin.cs b/Modules/BeatSaberPlus_Chat/BSIPA.cs similarity index 83% rename from Modules/BeatSaberPlus_Chat/Plugin.cs rename to Modules/BeatSaberPlus_Chat/BSIPA.cs index 261947d..b3c5459 100644 --- a/Modules/BeatSaberPlus_Chat/Plugin.cs +++ b/Modules/BeatSaberPlus_Chat/BSIPA.cs @@ -1,5 +1,4 @@ -using ChatPlexMod_Chat; -using IPA; +using IPA; namespace BeatSaberPlus_Chat { @@ -7,17 +6,17 @@ namespace BeatSaberPlus_Chat /// Main plugin class /// [Plugin(RuntimeOptions.SingleStartInit)] - public class Plugin + public class BSIPA { /// /// Called when the plugin is first loaded by IPA (either when the game starts or when the plugin is enabled if it starts disabled). /// /// Logger instance [Init] - public Plugin(IPA.Logging.Logger p_Logger) + public BSIPA(IPA.Logging.Logger p_Logger) { /// Setup logger - Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); + ChatPlexMod_Chat.Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); } //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_Chat/BeatSaberPlus_Chat.csproj b/Modules/BeatSaberPlus_Chat/BeatSaberPlus_Chat.csproj index 3003d34..9f80785 100644 --- a/Modules/BeatSaberPlus_Chat/BeatSaberPlus_Chat.csproj +++ b/Modules/BeatSaberPlus_Chat/BeatSaberPlus_Chat.csproj @@ -24,15 +24,14 @@ true false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG;BEATSABER;BEATSABER_1_29_4_OR_NEWER prompt 4 true bin\Release\ - - + BEATSABER;BEATSABER_1_29_4_OR_NEWER prompt 4 @@ -47,31 +46,23 @@ OnBuildSuccess - - $(BeatSaberDir)\Plugins\BSML.dll - False - False - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + $(BeatSaberDir)\Libs\Newtonsoft.Json.dll False False - - - - - $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll - False - - + $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll False @@ -91,106 +82,59 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.PhysicsModule.dll - False - False + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextCoreFontEngineModule.dll $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextCoreModule.dll False False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll False + False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\VRUI.dll - False - False - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + - - Settings.cs - - - ChatFloatingWindow.cs - - - ModerationLeft.cs - - - ModerationMain.cs - - - ModerationRight.cs - - - ModerationShortcut.cs - - - SettingsLeft.cs - - - SettingsRight.cs - - - PollFloatingWindow.cs - - - HypeTrainFloatingWindow.cs - - - PredictionFloatingWindow.cs - - - - - @@ -198,6 +142,9 @@ BeatSaberPlus + + + diff --git a/Modules/BeatSaberPlus_Chat/BeatSaberPlus_Chat.csproj.user b/Modules/BeatSaberPlus_Chat/BeatSaberPlus_Chat.csproj.user index 5db8d9ee60929239d9779dc6a24adfdc0c01779f..012781eeb9f58b5f0dd22267773d93a2d711944f 100644 GIT binary patch delta 25 fcmaFI@`q)^I!0av215ot1|tSbAZa*xE#pA|UV#Rx delta 11 Tcmeyv@{VQ0I>yO+7!LpdA+QBv diff --git a/Modules/BeatSaberPlus_Chat/CConfig.cs b/Modules/BeatSaberPlus_Chat/CConfig.cs deleted file mode 100644 index 46afc32..0000000 --- a/Modules/BeatSaberPlus_Chat/CConfig.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; -using UnityEngine; - -namespace ChatPlexMod_Chat -{ - internal class CConfig : CP_SDK.Config.JsonConfig - { - [JsonProperty] internal bool Enabled = true; - - [JsonProperty] internal Vector2 ChatSize = new Vector2(120, 140); - [JsonProperty] internal bool ReverseChatOrder = false; - [JsonProperty] internal float FontSize = 3.4f; - - [JsonProperty] internal bool AlignWithFloor = true; - [JsonProperty] internal bool ShowLockIcon = true; - [JsonProperty] internal bool FollowEnvironementRotation = true; - [JsonProperty] internal bool ShowViewerCount = true; - [JsonProperty] internal bool ShowFollowEvents = true; - [JsonProperty] internal bool ShowSubscriptionEvents = true; - [JsonProperty] internal bool ShowBitsCheeringEvents = true; - [JsonProperty] internal bool ShowChannelPointsEvent = true; - [JsonProperty] internal bool FilterViewersCommands = false; - [JsonProperty] internal bool FilterBroadcasterCommands = false; - - [JsonProperty] internal Color BackgroundColor = new Color(0f, 0f, 0f, 0.7f); - [JsonProperty] internal Color HighlightColor = new Color(0.57f, 0.28f, 1f, 0.12f); - [JsonProperty] internal Color AccentColor = new Color(0.57f, 0.28f, 1f, 1.00f); - [JsonProperty] internal Color TextColor = new Color(1f, 1f, 1f, 1f); - [JsonProperty] internal Color PingColor = new Color(1.00f, 0.00f, 0.00f, 0.18f); - - [JsonProperty] internal Vector3 MenuChatPosition = new Vector3(0, 4.10f, 3.50f); - [JsonProperty] internal Vector3 MenuChatRotation = new Vector3(325f,0,0); - - [JsonProperty] internal Vector3 PlayingChatPosition = new Vector3(0, 4.2f, 5.8f); - [JsonProperty] internal Vector3 PlayingChatRotation = new Vector3(325f, 0, 0); - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - internal List ModerationKeys = new List() - { - - }; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get relative config path - /// - /// - public override string GetRelativePath() - => $"{CP_SDK.ChatPlexSDK.ProductName}/Chat/Config"; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On config init - /// - /// On creation - protected override void OnInit(bool p_OnCreation) - { - if (p_OnCreation) - { - ModerationKeys = new List() - { - "/host", - "/unban", - "/untimeout", - "!bsr" - }; - } - - Save(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset chat positions - /// - internal void ResetPosition() - { - MenuChatPosition = new Vector3(0, 4.10f, 3.50f); - MenuChatRotation = new Vector3(325f, 0, 0); - - PlayingChatPosition = new Vector3(0, 4.2f, 5.8f); - PlayingChatRotation = new Vector3(325f, 0, 0); - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/Chat.cs b/Modules/BeatSaberPlus_Chat/Chat.cs deleted file mode 100644 index e139b5b..0000000 --- a/Modules/BeatSaberPlus_Chat/Chat.cs +++ /dev/null @@ -1,965 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.FloatingScreen; -using CP_SDK.Chat.Interfaces; -using CP_SDK.Unity.Extensions; -using HMUI; -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using TMPro; -using UnityEngine; -using UnityEngine.UI; - -namespace ChatPlexMod_Chat -{ - /// - /// Chat instance - /// - public class Chat : BeatSaberPlus.SDK.BSPModuleBase - { - /// - /// Module type - /// - public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; - /// - /// Name of the Module - /// - public override string Name => "Chat"; - /// - /// Description of the Module - /// - public override string Description => "Allow people to distract you while playing!"; - /// - /// Is the Module using chat features - /// - public override bool UseChatFeatures => true; - /// - /// Is enabled - /// - public override bool IsEnabled { get => CConfig.Instance.Enabled; set { CConfig.Instance.Enabled = value; CConfig.Instance.Save(); } } - /// - /// Activation kind - /// - public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Root GameObject, kept alive between scenes transitions - /// - private GameObject m_RootGameObject = null; - /// - /// Settings view - /// - private UI.Settings m_SettingsView = null; - /// - /// Settings left view - /// - private UI.SettingsLeft m_SettingsLeftView = null; - /// - /// Settings right view - /// - private UI.SettingsRight m_SettingsRightView = null; - /// - /// Floating screen instance - /// - private FloatingScreen m_ChatFloatingScreen = null; - /// - /// View controller for floating screen - /// - private UI.ChatFloatingWindow m_ChatFloatingScreenController = null; - /// - /// Mover handle material - /// - private Material m_ChatFloatingScreenHandleMaterial = null; - /// - /// Chat poll floating screen parent - /// - private GameObject m_ChatPollFloatingScreenOwner = null; - /// - /// Chat poll floating screen - /// - private FloatingScreen m_ChatPollFloatingScreen = null; - /// - /// Chat poll floating screen controller - /// - private UI.PollFloatingWindow m_ChatPollFloatingScreenController = null; - /// - /// Chat hype train floating screen parent - /// - private GameObject m_ChatHypeTrainFloatingScreenOwner = null; - /// - /// Chat hype train floating screen - /// - private FloatingScreen m_ChatHypeTrainFloatingScreen = null; - /// - /// Chat hype train floating screen controller - /// - private UI.HypeTrainFloatingWindow m_ChatHypeTrainFloatingScreenController = null; - /// - /// Chat prediction floating screen parent - /// - private GameObject m_ChatPredictionFloatingScreenOwner = null; - /// - /// Chat prediction floating screen - /// - private FloatingScreen m_ChatPredictionFloatingScreen = null; - /// - /// Chat prediction floating screen controller - /// - private UI.PredictionFloatingWindow m_ChatPredictionFloatingScreenController = null; - /// - /// Chat core instance - /// - private bool m_ChatCoreAcquired = false; - /// - /// Chat service action queue - /// - private ConcurrentQueue m_ActionQueue = new ConcurrentQueue(); - /// - /// Is the action dequeue task running - /// - private bool m_ActionDequeueRun = true; - /// - /// Create button coroutine - /// - private Coroutine m_CreateButtonCoroutine = null; - /// - /// Moderation button - /// - private Button m_ModerationButton = null; - /// - /// Last chat users - /// - private CP_SDK.Misc.RingBuffer<(IChatService, IChatUser)> m_LastChatUsers = null; - /// - /// View count owner - /// - private GameObject m_ViewerCountOwner = null; - /// - /// Viewer count floating screen - /// - private FloatingScreen m_ViewerCountFloatingScreen = null; - /// - /// Viewer count image - /// - private Image m_ViewerCountImage = null; - /// - /// Viewer count text - /// - private TextMeshProUGUI m_ViewerCountText = null; - /// - /// Video playback status - /// - private ConcurrentDictionary m_ChannelsVideoPlaybackStatus = new ConcurrentDictionary(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Last chat users - /// - internal List<(IChatService, IChatUser)> LastChatUsers => m_LastChatUsers == null ? new List<(IChatService, IChatUser)>() : m_LastChatUsers.ToList(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Enable the Module - /// - protected override void OnEnable() - { - /// Create ring buffer - m_LastChatUsers = new CP_SDK.Misc.RingBuffer<(IChatService, IChatUser)>(40); - - /// Clear video playback status - m_ChannelsVideoPlaybackStatus.Clear(); - - /// Bind events - CP_SDK.ChatPlexSDK.OnGenericMenuSceneLoaded += ChatPlexSDK_OnGenericMenuSceneLoaded; - CP_SDK.ChatPlexSDK.OnGenericSceneChange += ChatPlexSDK_OnGenericSceneChange; - - /// If we are already in menu scene, activate - if (CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Menu) - ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.ActiveGenericScene); - - if (!m_ChatCoreAcquired) - { - /// Init chat core - m_ChatCoreAcquired = true; - CP_SDK.Chat.Service.Acquire(); - - /// Run all services - CP_SDK.Chat.Service.Multiplexer.OnSystemMessage += ChatCoreMutiplixer_OnSystemMessage; - CP_SDK.Chat.Service.Multiplexer.OnLogin += ChatCoreMutiplixer_OnLogin; - CP_SDK.Chat.Service.Multiplexer.OnJoinChannel += ChatCoreMutiplixer_OnJoinChannel; - CP_SDK.Chat.Service.Multiplexer.OnLeaveChannel += ChatCoreMutiplixer_OnLeaveChannel; - CP_SDK.Chat.Service.Multiplexer.OnChannelFollow += ChatCoreMutiplixer_OnChannelFollow; - CP_SDK.Chat.Service.Multiplexer.OnChannelBits += ChatCoreMutiplixer_OnChannelBits; - CP_SDK.Chat.Service.Multiplexer.OnChannelPoints += ChatCoreMutiplixer_OnChannelPoints; - CP_SDK.Chat.Service.Multiplexer.OnChannelSubscription += ChatCoreMutiplixer_OnChannelSubscription; - CP_SDK.Chat.Service.Multiplexer.OnTextMessageReceived += ChatCoreMutiplixer_OnTextMessageReceived; - CP_SDK.Chat.Service.Multiplexer.OnRoomStateUpdated += ChatCoreMutiplixer_OnRoomStateUpdated; - CP_SDK.Chat.Service.Multiplexer.OnLiveStatusUpdated += ChatCoreMutiplixer_OnLiveStatusUpdated; - CP_SDK.Chat.Service.Multiplexer.OnChatCleared += ChatCoreMutiplixer_OnChatCleared; - CP_SDK.Chat.Service.Multiplexer.OnMessageCleared += ChatCoreMutiplixer_OnMessageCleared; - - /// Get back channels - foreach (var l_Channel in CP_SDK.Chat.Service.Multiplexer.Channels) - ChatCoreMutiplixer_OnJoinChannel(l_Channel.Item1, l_Channel.Item2); - - /// Enable dequeue system - m_ActionDequeueRun = true; - - /// Start dequeue task - Task.Run(ActionDequeueTask).ConfigureAwait(false); - } - - /// Add button - if (m_CreateButtonCoroutine == null) - m_CreateButtonCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(CreateButtonCoroutine()); - } - /// - /// Disable the Module - /// - protected override void OnDisable() - { - /// Un-init chat core - if (m_ChatCoreAcquired) - { - /// Unbind services - CP_SDK.Chat.Service.Multiplexer.OnSystemMessage -= ChatCoreMutiplixer_OnSystemMessage; - CP_SDK.Chat.Service.Multiplexer.OnLogin -= ChatCoreMutiplixer_OnLogin; - CP_SDK.Chat.Service.Multiplexer.OnJoinChannel -= ChatCoreMutiplixer_OnJoinChannel; - CP_SDK.Chat.Service.Multiplexer.OnLeaveChannel -= ChatCoreMutiplixer_OnLeaveChannel; - CP_SDK.Chat.Service.Multiplexer.OnChannelFollow -= ChatCoreMutiplixer_OnChannelFollow; - CP_SDK.Chat.Service.Multiplexer.OnChannelBits -= ChatCoreMutiplixer_OnChannelBits; - CP_SDK.Chat.Service.Multiplexer.OnChannelPoints -= ChatCoreMutiplixer_OnChannelPoints; - CP_SDK.Chat.Service.Multiplexer.OnChannelSubscription -= ChatCoreMutiplixer_OnChannelSubscription; - CP_SDK.Chat.Service.Multiplexer.OnTextMessageReceived -= ChatCoreMutiplixer_OnTextMessageReceived; - CP_SDK.Chat.Service.Multiplexer.OnRoomStateUpdated -= ChatCoreMutiplixer_OnRoomStateUpdated; - CP_SDK.Chat.Service.Multiplexer.OnLiveStatusUpdated -= ChatCoreMutiplixer_OnLiveStatusUpdated; - CP_SDK.Chat.Service.Multiplexer.OnChatCleared -= ChatCoreMutiplixer_OnChatCleared; - CP_SDK.Chat.Service.Multiplexer.OnMessageCleared -= ChatCoreMutiplixer_OnMessageCleared; - - /// Stop all chat services - CP_SDK.Chat.Service.Release(); - m_ChatCoreAcquired = false; - - /// Stop dequeue task - m_ActionDequeueRun = false; - } - - /// Unbind events - CP_SDK.ChatPlexSDK.OnGenericSceneChange -= ChatPlexSDK_OnGenericSceneChange; - CP_SDK.ChatPlexSDK.OnGenericMenuSceneLoaded -= ChatPlexSDK_OnGenericMenuSceneLoaded; - - /// Stop coroutine - if (m_CreateButtonCoroutine != null) - { - CP_SDK.Unity.MTCoroutineStarter.Stop(m_CreateButtonCoroutine); - m_CreateButtonCoroutine = null; - } - - /// Destroy moderation button - if (m_ModerationButton != null) - { - GameObject.Destroy(m_ModerationButton.gameObject); - m_ModerationButton = null; - } - - /// Destroy - DestroyFloatingWindow(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get Module settings UI - /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() - { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); - if (m_SettingsLeftView == null) - m_SettingsLeftView = BeatSaberUI.CreateViewController(); - if (m_SettingsRightView == null) - m_SettingsRightView = BeatSaberUI.CreateViewController(); - - return (m_SettingsView, m_SettingsLeftView, m_SettingsRightView); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When the menu loaded - /// - private void ChatPlexSDK_OnGenericMenuSceneLoaded() - { - if (m_ModerationButton == null || !m_ModerationButton ) - { - /// Stop coroutine - if (m_CreateButtonCoroutine != null) - { - CP_SDK.Unity.MTCoroutineStarter.Stop(m_CreateButtonCoroutine); - m_CreateButtonCoroutine = null; - } - - /// Destroy moderation button - if (m_ModerationButton != null) - { - GameObject.Destroy(m_ModerationButton.gameObject); - m_ModerationButton = null; - } - - /// Add button - if (m_CreateButtonCoroutine == null) - m_CreateButtonCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(CreateButtonCoroutine()); - } - } - /// - /// When the active scene is changed - /// - /// - private void ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.EGenericScene p_SceneType) - { - if (m_RootGameObject) - m_RootGameObject.transform.localScale = Vector3.one; - - if (p_SceneType == CP_SDK.ChatPlexSDK.EGenericScene.Menu) - UpdateButton(); - - if (m_ChatFloatingScreen == null) - CreateFloatingWindow(p_SceneType); - else - UpdateFloatingWindow(p_SceneType, true); - } - /// - /// When the floating window is moved - /// - /// Event sender - /// Event data - private void OnFloatingWindowMoved(object p_Sender, FloatingScreenHandleEventArgs p_Event) - { - /// Always parallel to the floor - if (CConfig.Instance.AlignWithFloor) - m_ChatFloatingScreen.transform.localEulerAngles = new Vector3(m_ChatFloatingScreen.transform.localEulerAngles.x, m_ChatFloatingScreen.transform.localEulerAngles.y, 0); - - if (CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Playing) - { - CConfig.Instance.PlayingChatPosition = m_ChatFloatingScreen.transform.localPosition; - CConfig.Instance.PlayingChatRotation = m_ChatFloatingScreen.transform.localEulerAngles; - } - else - { - CConfig.Instance.MenuChatPosition = m_ChatFloatingScreen.transform.localPosition; - CConfig.Instance.MenuChatRotation = m_ChatFloatingScreen.transform.localEulerAngles; - } - - CConfig.Instance.Save(); - - m_ViewerCountOwner.transform.localPosition = m_ChatFloatingScreen.transform.localPosition; - m_ViewerCountOwner.transform.localRotation = m_ChatFloatingScreen.transform.localRotation; - - m_ChatPollFloatingScreenOwner.transform.localPosition = m_ChatFloatingScreen.transform.localPosition; - m_ChatPollFloatingScreenOwner.transform.localRotation = m_ChatFloatingScreen.transform.localRotation; - - m_ChatHypeTrainFloatingScreenOwner.transform.localPosition = m_ChatFloatingScreen.transform.localPosition; - m_ChatHypeTrainFloatingScreenOwner.transform.localRotation = m_ChatFloatingScreen.transform.localRotation; - - m_ChatPredictionFloatingScreenOwner.transform.localPosition = m_ChatFloatingScreen.transform.localPosition; - m_ChatPredictionFloatingScreenOwner.transform.localRotation = m_ChatFloatingScreen.transform.localRotation; - } - /// - /// Toggle chat visibility - /// - public void ToggleVisibility() - { - if (m_RootGameObject && m_RootGameObject.transform.localScale.x > 0.5f) - m_RootGameObject.transform.localScale = Vector3.zero; - else if (m_RootGameObject) - m_RootGameObject.transform.localScale = Vector3.one; - } - /// - /// Set visible - /// - /// Is visible - public void SetVisible(bool p_Visible) - { - if (m_RootGameObject) - m_RootGameObject.transform.localScale = p_Visible ? Vector3.one : Vector3.zero; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Create button coroutine - /// - /// - private IEnumerator CreateButtonCoroutine() - { - LevelSelectionNavigationController p_LevelSelectionNavigationController = null; - - while (true) - { - p_LevelSelectionNavigationController = Resources.FindObjectsOfTypeAll().LastOrDefault(); - - if (p_LevelSelectionNavigationController != null && p_LevelSelectionNavigationController.gameObject.transform.childCount >= 2) - break; - - yield return new WaitForSeconds(0.25f); - } - - m_ModerationButton = BeatSaberPlus.SDK.UI.Button.Create(p_LevelSelectionNavigationController.transform, "Chat\nModeration", () => UI.ModerationViewFlowCoordinator.Instance().Present(), null); - m_ModerationButton.transform.localPosition = new Vector3(72.50f, 30.00f - 3, 2.6f); - m_ModerationButton.transform.localScale = new Vector3(0.65f, 0.50f, 0.65f); - m_ModerationButton.transform.SetAsFirstSibling(); - m_ModerationButton.gameObject.SetActive(true); - m_ModerationButton.GetComponentInChildren().margin = new Vector4(0, 4, 0, 0); - - UpdateButton(); - - m_CreateButtonCoroutine = null; - } - /// - /// Update button text - /// - internal void UpdateButton() - { - if (m_ModerationButton == null) - return; - - m_ModerationButton.transform.localPosition = new Vector3(72.50f, 30.00f - 3, 2.6f); - m_ModerationButton.transform.localScale = new Vector3(0.65f, 0.50f, 0.65f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On system message - /// - /// Chat service - /// Message - private void ChatCoreMutiplixer_OnSystemMessage(IChatService p_ChatService, string p_Message) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnSystemMessage(p_ChatService, p_Message)); - } - /// - /// On login - /// - /// Chat service - private void ChatCoreMutiplixer_OnLogin(IChatService p_ChatService) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnLogin(p_ChatService)); - } - /// - /// On channel join - /// - /// Chat service - /// Channel instance - private void ChatCoreMutiplixer_OnJoinChannel(IChatService p_ChatService, IChatChannel p_Channel) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnJoinChannel(p_ChatService, p_Channel)); - } - /// - /// On channel leave - /// - /// Chat service - /// Channel instance - private void ChatCoreMutiplixer_OnLeaveChannel(IChatService p_ChatService, IChatChannel p_Channel) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnLeaveChannel(p_ChatService, p_Channel)); - } - /// - /// On channel follow - /// - /// Chat service - /// Channel instance - /// User instance - private void ChatCoreMutiplixer_OnChannelFollow(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnChannelFollow(p_ChatService, p_Channel, p_User)); - } - /// - /// On channel bits - /// - /// Chat service - /// Channel instance - /// User instance - /// Used bits - private void ChatCoreMutiplixer_OnChannelBits(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User, int p_BitsUsed) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnChannelBits(p_ChatService, p_Channel, p_User, p_BitsUsed)); - } - /// - /// On channel points - /// - /// Chat service - /// Channel instance - /// User instance - /// Event - private void ChatCoreMutiplixer_OnChannelPoints(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User, IChatChannelPointEvent p_Event) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnChannelPoints(p_ChatService, p_Channel, p_User, p_Event)); - } - /// - /// On channel subscription - /// - /// Chat service - /// Channel instance - /// User instance - /// Event - private void ChatCoreMutiplixer_OnChannelSubscription(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User, IChatSubscriptionEvent p_Event) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnChannelSubsciption(p_ChatService, p_Channel, p_User, p_Event)); - } - /// - /// On text message received - /// - /// Chat service - /// ID of the message - private void ChatCoreMutiplixer_OnTextMessageReceived(IChatService p_ChatService, IChatMessage p_Message) - { - lock (m_LastChatUsers) - { - if (m_LastChatUsers.Count(x => x.Item1 == p_ChatService && x.Item2.UserName == p_Message.Sender.UserName) == 0) - m_LastChatUsers.Add((p_ChatService, p_Message.Sender)); - } - - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnTextMessageReceived(p_Message)); - } - /// - /// On room state changed - /// - /// Chat service - /// Channel instance - private void ChatCoreMutiplixer_OnRoomStateUpdated(IChatService p_ChatService, IChatChannel p_Channel) - { - if (UI.ModerationLeft.Instance != null) - UI.ModerationLeft.Instance.UpdateRoomState(); - - } - /// - /// On room video playback updated - /// - /// Chat service - /// Channel instance - /// Is the stream up - /// Viewer count - private void ChatCoreMutiplixer_OnLiveStatusUpdated(IChatService p_ChatService, IChatChannel p_Channel, bool p_StreamUP, int p_ViewerCount) - { - string l_Key = "[" + p_ChatService.DisplayName + "]_" + p_Channel.Name.ToLower(); - - if (!m_ChannelsVideoPlaybackStatus.ContainsKey(l_Key)) - m_ChannelsVideoPlaybackStatus.TryAdd(l_Key, (p_StreamUP, p_ViewerCount)); - else - m_ChannelsVideoPlaybackStatus[l_Key] = (p_StreamUP, p_ViewerCount); - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => UpdateViewerCount()); - } - /// - /// On chat user cleared - /// - /// Chat service - /// ID of the user - private void ChatCoreMutiplixer_OnChatCleared(IChatService p_ChatService, string p_UserID) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnChatCleared(p_UserID)); - } - /// - /// On message cleared - /// - /// Chat service - /// ID of the message - private void ChatCoreMutiplixer_OnMessageCleared(IChatService p_ChatService, string p_MessageID) - { - QueueOrSendChatAction(() => m_ChatFloatingScreenController.OnMessageCleared(p_MessageID)); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Queue or send an action - /// - /// Action - private void QueueOrSendChatAction(Action p_Action) - { - if (m_ChatFloatingScreenController == null || !m_ChatFloatingScreenController.isActivated) - m_ActionQueue.Enqueue(p_Action); - else - p_Action.Invoke(); - } - /// - /// Dequeue actions - /// - /// - private async Task ActionDequeueTask() - { - await Task.Yield(); - - while (m_ActionDequeueRun) - { - if (m_ChatFloatingScreenController == null || !m_ChatFloatingScreenController.isActivated) - { - await Task.Delay(1000).ConfigureAwait(false); - continue; - } - - if (m_ActionQueue.IsEmpty) - { - await Task.Delay(1000).ConfigureAwait(false); - continue; - } - - /// Work through the queue of messages that has piled up one by one until they're all gone. - while (m_ActionDequeueRun && m_ActionQueue.TryDequeue(out var l_Action)) - l_Action.Invoke(); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Create the floating window - /// - /// Scene type - private void CreateFloatingWindow(CP_SDK.ChatPlexSDK.EGenericScene p_SceneType) - { - if (m_RootGameObject != null) - return; - - try - { - /// Prepare root game object - m_RootGameObject = new GameObject("ChatPlexSDK_Chat"); - GameObject.DontDestroyOnLoad(m_RootGameObject); - - /// Prepare size, position, rotation - Vector2 l_ChatSize = CConfig.Instance.ChatSize; - Vector3 l_ChatPosition = CConfig.Instance.MenuChatPosition; - Vector3 l_ChatRotation = CConfig.Instance.MenuChatRotation; - - if (p_SceneType == CP_SDK.ChatPlexSDK.EGenericScene.Playing) - { - l_ChatPosition = CConfig.Instance.PlayingChatPosition; - l_ChatRotation = CConfig.Instance.PlayingChatRotation; - } - - /// Create floating screen - m_ChatFloatingScreen = FloatingScreen.CreateFloatingScreen(l_ChatSize, true, Vector3.zero, Quaternion.identity); - m_ChatFloatingScreen.GetComponent().sortingOrder = 3; - m_ChatFloatingScreen.GetComponent().SetRadius(0); - - /// Update handle material - m_ChatFloatingScreenHandleMaterial = GameObject.Instantiate(BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial); - m_ChatFloatingScreenHandleMaterial.color = Color.clear; - m_ChatFloatingScreen.handle.gameObject.GetComponent().material = m_ChatFloatingScreenHandleMaterial; - - /// Create UI Controller - m_ChatFloatingScreenController = BeatSaberUI.CreateViewController(); - m_ChatFloatingScreen.SetRootViewController(m_ChatFloatingScreenController, HMUI.ViewController.AnimationType.None); - m_ChatFloatingScreenController.gameObject.SetActive(true); - m_ChatFloatingScreen.GetComponentInChildren().sortingOrder = 4; - - /// Bind floating window to the root game object - m_ChatFloatingScreen.transform.SetParent(m_RootGameObject.transform); - - /// Set position & rotation - m_ChatFloatingScreen.transform.localPosition = l_ChatPosition; - m_ChatFloatingScreen.transform.localRotation = Quaternion.Euler(l_ChatRotation); - - /// Bind event - m_ChatFloatingScreen.HandleReleased += OnFloatingWindowMoved; - - /// Create viewer count owner - m_ViewerCountOwner = new GameObject(); - m_ViewerCountOwner.transform.localPosition = l_ChatPosition; - m_ViewerCountOwner.transform.localRotation = Quaternion.Euler(l_ChatRotation); - m_ViewerCountOwner.transform.SetParent(m_RootGameObject.transform); - - /// Viewer count window - m_ViewerCountFloatingScreen = FloatingScreen.CreateFloatingScreen(new Vector2(25, 8), false, Vector2.zero, Quaternion.identity, 0, false); - m_ViewerCountFloatingScreen.transform.localPosition = new Vector3( ((((float)CConfig.Instance.ChatSize.x) / 2f) * 0.02f) + 0.2f, (((-(float)CConfig.Instance.ChatSize.y) / 2f) * 0.02f) + 0.1f, 0f); - m_ViewerCountFloatingScreen.transform.localRotation = Quaternion.identity; - - /// Horizontal layout - var l_HorizontalTag = new BeatSaberMarkupLanguage.Tags.HorizontalLayoutTag(); - var l_Layout = l_HorizontalTag.CreateObject(m_ViewerCountFloatingScreen.transform); - - /// Viewer icon - var l_ImageTag = new BeatSaberMarkupLanguage.Tags.ImageTag(); - var l_Image = l_ImageTag.CreateObject(l_Layout.transform); - l_Image.transform.localScale = Vector3.one * 0.5f; - m_ViewerCountImage = l_Image.GetComponent(); - BeatSaberUI.SetImage(m_ViewerCountImage, "#PlayerIcon"); - - /// Viewer text - var l_TextTag = new BeatSaberMarkupLanguage.Tags.TextTag(); - var l_Text = l_TextTag.CreateObject(l_Layout.transform); - - m_ViewerCountText = l_Text.GetComponent(); - m_ViewerCountText.margin = new Vector4(-3, 3, 0, 0); - m_ViewerCountText.fontSize = 5; - m_ViewerCountText.text = "0"; - - /// Bind floating window to the root game object - m_ViewerCountFloatingScreen.transform.SetParent(m_ViewerCountOwner.transform, false); - m_ViewerCountFloatingScreen.gameObject.ChangerLayerRecursive(LayerMask.NameToLayer("UI")); - - UpdateViewerCount(); - - /////////////////////////////////////////////// - /// Poll window - var l_PollSize = UI.PollFloatingWindow.SIZE; - var l_PollPosition = new Vector3( - ((CConfig.Instance.ChatSize.x + l_PollSize.x) / 2f) * 0.02f, - ((-CConfig.Instance.ChatSize.y + l_PollSize.y + 16) / 2f) * 0.02f, - 0 - ); - - /// Create viewer count owner - m_ChatPollFloatingScreenOwner = new GameObject(); - m_ChatPollFloatingScreenOwner.transform.localPosition = l_ChatPosition; - m_ChatPollFloatingScreenOwner.transform.localRotation = Quaternion.Euler(l_ChatRotation); - m_ChatPollFloatingScreenOwner.transform.SetParent(m_RootGameObject.transform); - - /// Create floating screen - m_ChatPollFloatingScreen = FloatingScreen.CreateFloatingScreen(l_PollSize, false, Vector2.zero, Quaternion.identity, 0, false); - m_ChatPollFloatingScreen.transform.SetParent(m_ChatPollFloatingScreenOwner.transform, false); - m_ChatPollFloatingScreen.transform.localPosition = l_PollPosition; - m_ChatPollFloatingScreen.transform.localRotation = Quaternion.identity; - - /// Create UI Controller - m_ChatPollFloatingScreenController = BeatSaberUI.CreateViewController(); - m_ChatPollFloatingScreen.SetRootViewController(m_ChatPollFloatingScreenController, HMUI.ViewController.AnimationType.None); - m_ChatPollFloatingScreen.GetComponentInChildren().sortingOrder = -1; - /////////////////////////////////////////////// - - /////////////////////////////////////////////// - /// HypeTrain window - var l_HypeTrainSize = new Vector2(CConfig.Instance.ChatSize.x, UI.HypeTrainFloatingWindow.HEIGHT); - var l_HypeTrainPosition = new Vector3( - 0f, - ((-CConfig.Instance.ChatSize.y - l_HypeTrainSize.y) / 2f) * 0.02f, - 0f - ); - - /// Create viewer count owner - m_ChatHypeTrainFloatingScreenOwner = new GameObject(); - m_ChatHypeTrainFloatingScreenOwner.transform.localPosition = l_ChatPosition; - m_ChatHypeTrainFloatingScreenOwner.transform.localRotation = Quaternion.Euler(l_ChatRotation); - m_ChatHypeTrainFloatingScreenOwner.transform.SetParent(m_RootGameObject.transform); - - /// Create floating screen - m_ChatHypeTrainFloatingScreen = FloatingScreen.CreateFloatingScreen(l_PollSize, false, Vector2.zero, Quaternion.identity, 0, false); - m_ChatHypeTrainFloatingScreen.transform.SetParent(m_ChatHypeTrainFloatingScreenOwner.transform, false); - m_ChatHypeTrainFloatingScreen.transform.localPosition = l_PollPosition; - m_ChatHypeTrainFloatingScreen.transform.localRotation = Quaternion.identity; - - /// Create UI Controller - m_ChatHypeTrainFloatingScreenController = BeatSaberUI.CreateViewController(); - m_ChatHypeTrainFloatingScreen.SetRootViewController(m_ChatHypeTrainFloatingScreenController, HMUI.ViewController.AnimationType.None); - m_ChatHypeTrainFloatingScreen.GetComponentInChildren().sortingOrder = -1; - /////////////////////////////////////////////// - - /////////////////////////////////////////////// - /// Prediction window - var l_PredictionSize = UI.PredictionFloatingWindow.SIZE; - var l_PredictionPosition = new Vector3( - ((-CConfig.Instance.ChatSize.x - l_PredictionSize.x) / 2f) * 0.02f, - ((-CConfig.Instance.ChatSize.y + l_PredictionSize.y + 16) / 2f) * 0.02f, - 0 - ); - - /// Create viewer count owner - m_ChatPredictionFloatingScreenOwner = new GameObject(); - m_ChatPredictionFloatingScreenOwner.transform.localPosition = l_ChatPosition; - m_ChatPredictionFloatingScreenOwner.transform.localRotation = Quaternion.Euler(l_ChatRotation); - m_ChatPredictionFloatingScreenOwner.transform.SetParent(m_RootGameObject.transform); - - /// Create floating screen - m_ChatPredictionFloatingScreen = FloatingScreen.CreateFloatingScreen(l_PredictionSize, false, Vector2.zero, Quaternion.identity, 0, false); - m_ChatPredictionFloatingScreen.transform.SetParent(m_ChatPredictionFloatingScreenOwner.transform, false); - m_ChatPredictionFloatingScreen.transform.localPosition = l_PredictionPosition; - m_ChatPredictionFloatingScreen.transform.localRotation = Quaternion.identity; - - /// Create UI Controller - m_ChatPredictionFloatingScreenController = BeatSaberUI.CreateViewController(); - m_ChatPredictionFloatingScreen.SetRootViewController(m_ChatPredictionFloatingScreenController, HMUI.ViewController.AnimationType.None); - m_ChatPredictionFloatingScreen.GetComponentInChildren().sortingOrder = -1; - /////////////////////////////////////////////// - - UpdateFloatingWindow(p_SceneType, true); - } - catch (System.Exception l_Exception) - { - Logger.Instance.Error("[Chat] Failed to CreateFloatingWindow"); - Logger.Instance.Error(l_Exception); - } - } - /// - /// Destroy the floating window - /// - private void DestroyFloatingWindow() - { - if (m_RootGameObject == null) - return; - - try - { - /// Dismiss controller - m_ChatFloatingScreen.SetRootViewController(null, HMUI.ViewController.AnimationType.None); - - /// Destroy objects - GameObject.Destroy(m_ChatFloatingScreenController); - GameObject.Destroy(m_ChatFloatingScreen); - GameObject.Destroy(m_ChatFloatingScreenHandleMaterial); - GameObject.Destroy(m_ViewerCountFloatingScreen); - GameObject.Destroy(m_ChatPollFloatingScreen); - GameObject.Destroy(m_ChatHypeTrainFloatingScreen); - GameObject.Destroy(m_ChatPredictionFloatingScreen); - GameObject.Destroy(m_RootGameObject); - - /// Reset variables - m_ChatFloatingScreenController = null; - m_ChatFloatingScreen = null; - m_ChatFloatingScreenHandleMaterial = null; - m_ViewerCountFloatingScreen = null; - m_ViewerCountText = null; - m_ChatHypeTrainFloatingScreen = null; - m_ChatPollFloatingScreen = null; - m_ChatPredictionFloatingScreen = null; - m_RootGameObject = null; - } - catch (System.Exception l_Exception) - { - Logger.Instance.Error("[Chat] Failed to DestroyFloatingPlayer"); - Logger.Instance.Error(l_Exception); - } - } - /// - /// Update floating window UI on scene change - /// - /// New scene - /// Is on scene change - internal void UpdateFloatingWindow(CP_SDK.ChatPlexSDK.EGenericScene p_SceneType, bool p_OnSceneChange) - { - if (m_RootGameObject == null) - return; - - try - { - Vector3 l_ChatPosition = CConfig.Instance.MenuChatPosition; - Vector3 l_ChatRotation = CConfig.Instance.MenuChatRotation; - - if (p_SceneType == CP_SDK.ChatPlexSDK.EGenericScene.Playing) - { - l_ChatPosition = CConfig.Instance.PlayingChatPosition; - l_ChatRotation = CConfig.Instance.PlayingChatRotation; - } - - /// Prepare data for level with rotations - var l_Is360Level = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.transformedBeatmapData?.spawnRotationEventsCount > 0; - var l_FlyingGameHUDRotation = l_Is360Level ? Resources.FindObjectsOfTypeAll().FirstOrDefault()?.gameObject : null as GameObject; - - /// Update chat messages display - m_ChatFloatingScreen.ScreenSize = CConfig.Instance.ChatSize; - m_ChatFloatingScreen.handle.transform.localScale = CConfig.Instance.ChatSize; - m_ChatFloatingScreen.handle.transform.localPosition = Vector3.zero; - m_ChatFloatingScreen.handle.transform.localRotation = Quaternion.identity; - m_ChatFloatingScreenController.UpdateUI(p_SceneType, p_OnSceneChange, l_Is360Level, l_FlyingGameHUDRotation); - - /// Update position & rotation - m_ChatFloatingScreen.transform.localPosition = l_ChatPosition; - m_ChatFloatingScreen.transform.localRotation = Quaternion.Euler(l_ChatRotation); - - /// Update viewer count - m_ViewerCountFloatingScreen.transform.localPosition = new Vector3(((((float)CConfig.Instance.ChatSize.x) / 2f) * 0.02f) + 0.2f, (((-(float)CConfig.Instance.ChatSize.y) / 2f) * 0.02f) + 0.1f, 0f); - m_ViewerCountOwner.transform.localPosition = m_ChatFloatingScreen.transform.localPosition; - m_ViewerCountOwner.transform.localRotation = m_ChatFloatingScreen.transform.localRotation; - - /// Update visibility - m_ViewerCountImage.enabled = CConfig.Instance.ShowViewerCount; - m_ViewerCountText.enabled = CConfig.Instance.ShowViewerCount; - - /////////////////////////////////////////////// - /// Poll window - var l_PollSize = UI.PollFloatingWindow.SIZE; - var l_PollPosition = new Vector3( - ((CConfig.Instance.ChatSize.x + l_PollSize.x) / 2f) * 0.02f, - ((-CConfig.Instance.ChatSize.y + l_PollSize.y + 16) / 2f) * 0.02f, - 0 - ); - m_ChatPollFloatingScreen.transform.localPosition = l_PollPosition; - m_ChatPollFloatingScreenOwner.transform.localPosition = m_ChatFloatingScreen.transform.localPosition; - m_ChatPollFloatingScreenOwner.transform.localRotation = m_ChatFloatingScreen.transform.localRotation; - /////////////////////////////////////////////// - - /////////////////////////////////////////////// - /// HypeTrain window - var l_HypeTrainSize = new Vector2(CConfig.Instance.ChatSize.x, UI.HypeTrainFloatingWindow.HEIGHT); - var l_HypeTrainPosition = new Vector3( - 0f, - ((-CConfig.Instance.ChatSize.y - l_HypeTrainSize.y) / 2f) * 0.02f, - 0f - ); - m_ChatHypeTrainFloatingScreen.ScreenSize = l_HypeTrainSize; - m_ChatHypeTrainFloatingScreen.transform.localPosition = l_HypeTrainPosition; - m_ChatHypeTrainFloatingScreenOwner.transform.localPosition = m_ChatFloatingScreen.transform.localPosition; - m_ChatHypeTrainFloatingScreenOwner.transform.localRotation = m_ChatFloatingScreen.transform.localRotation; - /////////////////////////////////////////////// - - /////////////////////////////////////////////// - /// Prediction window - var l_PredictionSize = UI.PredictionFloatingWindow.SIZE; - var l_PredictionPosition = new Vector3( - ((-CConfig.Instance.ChatSize.x - l_PredictionSize.x) / 2f) * 0.02f, - ((-CConfig.Instance.ChatSize.y + l_PredictionSize.y + 16) / 2f) * 0.02f, - 0 - ); - m_ChatPredictionFloatingScreen.transform.localPosition = l_PredictionPosition; - m_ChatPredictionFloatingScreenOwner.transform.localPosition = m_ChatFloatingScreen.transform.localPosition; - m_ChatPredictionFloatingScreenOwner.transform.localRotation = m_ChatFloatingScreen.transform.localRotation; - /////////////////////////////////////////////// - } - catch (System.Exception l_Exception) - { - Logger.Instance.Error("[Chat] Failed to UpdateFloatingWindow"); - Logger.Instance.Error(l_Exception); - } - } - /// - /// Update viewer count - /// - private void UpdateViewerCount() - { - bool l_ShowUp = false; - int l_SumViewers = 0; - - foreach (var l_KVP in m_ChannelsVideoPlaybackStatus) - { - if (!l_KVP.Value.Item1) - continue; - - l_ShowUp = true; - l_SumViewers += l_KVP.Value.Item2; - } - - if (l_ShowUp) - m_ViewerCountText.text = l_SumViewers.ToString(); - else - m_ViewerCountText.text = "Offline"; - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/CConfig.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/CConfig.cs new file mode 100644 index 0000000..8a6fb1c --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/CConfig.cs @@ -0,0 +1,127 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_Chat +{ + /// + /// Chat configuration + /// + internal class CConfig : CP_SDK.Config.JsonConfig + { +#if BEATSABER || UNITY_TESTING + private static Vector3 DefaultChatMenuPosition = new Vector3( 0.00f, 4.10f, 3.50f); + private static Vector3 DefaultChatMenuRotation = new Vector3(325.00f, 0.00f, 0.00f); + + private static Vector3 DefaultChatPlayingPosition = new Vector3( 0.00f, 4.20f, 5.80f); + private static Vector3 DefaultChatPlayingRotation = new Vector3(325.00f, 0.00f, 0.00f); +#elif SYNTHRIDERS + private static Vector3 DefaultChatMenuPosition = new Vector3( 0.00f, 5.00f, 1.70f); + private static Vector3 DefaultChatMenuRotation = new Vector3(325.00f, 0.00f, 0.00f); + + private static Vector3 DefaultChatPlayingPosition = new Vector3( 0.00f, 4.20f, 5.80f); + private static Vector3 DefaultChatPlayingRotation = new Vector3(325.00f, 0.00f, 0.00f); +#elif AUDIOTRIP + private static Vector3 DefaultChatMenuPosition = new Vector3( 0.00f, 4.10f, 3.50f); + private static Vector3 DefaultChatMenuRotation = new Vector3(325.00f, 0.00f, 0.00f); + + private static Vector3 DefaultChatPlayingPosition = new Vector3( 0.00f, 4.20f, 5.80f); + private static Vector3 DefaultChatPlayingRotation = new Vector3(325.00f, 0.00f, 0.00f); +#elif BOOMBOX + private static Vector3 DefaultChatMenuPosition = new Vector3(3.191f, 2.852f, 2.115f); + private static Vector3 DefaultChatMenuRotation = new Vector3(0.000f, 40.000f, 0.000f); + + private static Vector3 DefaultChatPlayingPosition = new Vector3(2.33f, 2.145f, 3.669f); + private static Vector3 DefaultChatPlayingRotation = new Vector3(0.00f, 49.4501f, 0.000f); +#else +#error Missing game implementation +#endif + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + [JsonProperty] internal bool Enabled = true; + + [JsonProperty] internal Vector2 ChatSize = new Vector2(120, 140); + [JsonProperty] internal bool ReverseChatOrder = false; + [JsonProperty] internal bool PlatformOriginColor = true; + + [JsonProperty] internal float FontSize = 3.4f; + + [JsonProperty] internal bool AlignWithFloor = true; + [JsonProperty] internal bool ShowLockIcon = true; + [JsonProperty] internal bool FollowEnvironementRotation = true; + [JsonProperty] internal bool ShowViewerCount = true; + [JsonProperty] internal bool ShowFollowEvents = true; + [JsonProperty] internal bool ShowSubscriptionEvents = true; + [JsonProperty] internal bool ShowBitsCheeringEvents = true; + [JsonProperty] internal bool ShowChannelPointsEvent = true; + [JsonProperty] internal bool FilterViewersCommands = false; + [JsonProperty] internal bool FilterBroadcasterCommands = false; + + [JsonProperty] internal Color BackgroundColor = new Color(0.00f, 0.00f, 0.00f, 0.90f); + [JsonProperty] internal Color HighlightColor = new Color(0.57f, 0.28f, 1.00f, 0.12f); + [JsonProperty] internal Color TextColor = new Color(1.00f, 1.00f, 1.00f, 1.00f); + [JsonProperty] internal Color PingColor = new Color(0.90f, 0.00f, 0.00f, 0.36f); + + [JsonProperty] internal Vector3 MenuChatPosition = DefaultChatMenuPosition; + [JsonProperty] internal Vector3 MenuChatRotation = DefaultChatMenuRotation; + + [JsonProperty] internal Vector3 PlayingChatPosition = DefaultChatPlayingPosition; + [JsonProperty] internal Vector3 PlayingChatRotation = DefaultChatPlayingRotation; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + internal List ModerationKeys = new List() + { + + }; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get relative config path + /// + /// + public override string GetRelativePath() + => $"{CP_SDK.ChatPlexSDK.ProductName}/Chat/Config"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On config init + /// + /// On creation + protected override void OnInit(bool p_OnCreation) + { + if (p_OnCreation) + { + ModerationKeys = new List() + { + "/host", + "/unban", + "/untimeout", + "!bsr" + }; + } + + Save(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset chat positions + /// + internal void ResetPosition() + { + MenuChatPosition = DefaultChatMenuPosition; + MenuChatRotation = DefaultChatMenuRotation; + + PlayingChatPosition = DefaultChatPlayingPosition; + PlayingChatRotation = DefaultChatPlayingRotation; + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Chat.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Chat.cs new file mode 100644 index 0000000..7ee3ad2 --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Chat.cs @@ -0,0 +1,721 @@ +using CP_SDK.Chat.Interfaces; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +#if BEATSABER +using IPA.Utilities; +#endif + +namespace ChatPlexMod_Chat +{ + /// + /// Chat instance + /// + public class Chat : CP_SDK.ModuleBase + { + public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; + public override string Name => "Chat"; + public override string Description => "Allow people to distract you while playing!"; + public override string DocumentationURL => "https://github.com/hardcpp/BeatSaberPlus/wiki#chat"; + public override bool UseChatFeatures => true; + public override bool IsEnabled { get => CConfig.Instance.Enabled; set { CConfig.Instance.Enabled = value; CConfig.Instance.Save(); } } + public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private UI.SettingsLeftView m_SettingsLeftView = null; + private UI.SettingsMainView m_SettingsMainView = null; + private UI.SettingsRightView m_SettingsRightView = null; + + private Transform m_RootTransform = null; + private Transform m_DockedFloatingPanelTransform = null; + private CP_SDK.UI.Components.CFloatingPanel m_ChatFloatingPanel = null; + private UI.ChatFloatingPanelView m_ChatFloatingPanelView = null; + private CP_SDK.UI.Components.CFloatingPanel m_ChatHypeTrainFloatingPanel = null; + private UI.HypeTrainFloatingPanelView m_ChatHypeTrainFloatingPanelView = null; + private CP_SDK.UI.Components.CFloatingPanel m_PollFloatingPanel = null; + private UI.PollFloatingPanelView m_PollFloatingPanelView = null; + private CP_SDK.UI.Components.CFloatingPanel m_ChatPredictionFloatingPanel = null; + private UI.PredictionFloatingPanelView m_ChatPredictionFloatingPanelView = null; + private CP_SDK.UI.Components.CFloatingPanel m_StatusFloatingPanel = null; + private UI.StatusFloatingPanelView m_StatusFloatingPanelView = null; + + private bool m_ChatCoreAcquired = false; + private ConcurrentQueue m_ActionQueue = new ConcurrentQueue(); + private bool m_ActionDequeueRun = true; + + private Coroutine m_CreateButtonCoroutine = null; + private Button m_ModerationButton = null; + + private CP_SDK.Misc.RingBuffer<(IChatService, IChatUser)> m_LastChatUsers = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + internal List<(IChatService, IChatUser)> LastChatUsers => m_LastChatUsers == null ? new List<(IChatService, IChatUser)>() : m_LastChatUsers.ToList(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Enable the Module + /// + protected override void OnEnable() + { + /// Create ring buffer + m_LastChatUsers = new CP_SDK.Misc.RingBuffer<(IChatService, IChatUser)>(40); + + /// Bind events + CP_SDK.ChatPlexSDK.OnGenericMenuSceneLoaded += ChatPlexSDK_OnGenericMenuSceneLoaded; + CP_SDK.ChatPlexSDK.OnGenericSceneChange += ChatPlexSDK_OnGenericSceneChange; + + /// If we are already in menu scene, activate + if (CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Menu) + ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.ActiveGenericScene); + + if (!m_ChatCoreAcquired) + { + /// Init chat core + m_ChatCoreAcquired = true; + CP_SDK.Chat.Service.Acquire(); + + /// Run all services + var l_Multiplexer = CP_SDK.Chat.Service.Multiplexer; + l_Multiplexer.OnSystemMessage += Mutiplixer_OnSystemMessage; + l_Multiplexer.OnLogin += Mutiplixer_OnLogin; + l_Multiplexer.OnJoinChannel += Mutiplixer_OnJoinChannel; + l_Multiplexer.OnLeaveChannel += Mutiplixer_OnLeaveChannel; + l_Multiplexer.OnChannelFollow += Mutiplixer_OnChannelFollow; + l_Multiplexer.OnChannelBits += Mutiplixer_OnChannelBits; + l_Multiplexer.OnChannelPoints += Mutiplixer_OnChannelPoints; + l_Multiplexer.OnChannelSubscription += Mutiplixer_OnChannelSubscription; + l_Multiplexer.OnTextMessageReceived += Mutiplixer_OnTextMessageReceived; + l_Multiplexer.OnRoomStateUpdated += Mutiplixer_OnRoomStateUpdated; + l_Multiplexer.OnChatCleared += Mutiplixer_OnChatCleared; + l_Multiplexer.OnMessageCleared += Mutiplixer_OnMessageCleared; + + /// Get back channels + foreach (var l_Channel in CP_SDK.Chat.Service.Multiplexer.Channels) + Mutiplixer_OnJoinChannel(l_Channel.Item1, l_Channel.Item2); + + /// Enable dequeue system + m_ActionDequeueRun = true; + + /// Start dequeue task + Task.Run(ActionDequeueTask).ConfigureAwait(false); + } + + /// Add button + if (m_CreateButtonCoroutine == null) + m_CreateButtonCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(CreateButtonCoroutine()); + } + /// + /// Disable the Module + /// + protected override void OnDisable() + { + /// Un-init chat core + if (m_ChatCoreAcquired) + { + /// Unbind services + var l_Multiplexer = CP_SDK.Chat.Service.Multiplexer; + l_Multiplexer.OnSystemMessage -= Mutiplixer_OnSystemMessage; + l_Multiplexer.OnLogin -= Mutiplixer_OnLogin; + l_Multiplexer.OnJoinChannel -= Mutiplixer_OnJoinChannel; + l_Multiplexer.OnLeaveChannel -= Mutiplixer_OnLeaveChannel; + l_Multiplexer.OnChannelFollow -= Mutiplixer_OnChannelFollow; + l_Multiplexer.OnChannelBits -= Mutiplixer_OnChannelBits; + l_Multiplexer.OnChannelPoints -= Mutiplixer_OnChannelPoints; + l_Multiplexer.OnChannelSubscription -= Mutiplixer_OnChannelSubscription; + l_Multiplexer.OnTextMessageReceived -= Mutiplixer_OnTextMessageReceived; + l_Multiplexer.OnRoomStateUpdated -= Mutiplixer_OnRoomStateUpdated; + l_Multiplexer.OnChatCleared -= Mutiplixer_OnChatCleared; + l_Multiplexer.OnMessageCleared -= Mutiplixer_OnMessageCleared; + + /// Stop all chat services + CP_SDK.Chat.Service.Release(); + m_ChatCoreAcquired = false; + + /// Stop dequeue task + m_ActionDequeueRun = false; + + m_ActionQueue = new ConcurrentQueue(); + m_LastChatUsers.Clear(); + } + + /// Unbind events + CP_SDK.ChatPlexSDK.OnGenericSceneChange -= ChatPlexSDK_OnGenericSceneChange; + CP_SDK.ChatPlexSDK.OnGenericMenuSceneLoaded -= ChatPlexSDK_OnGenericMenuSceneLoaded; + + /// Stop coroutine + if (m_CreateButtonCoroutine != null) + { + CP_SDK.Unity.MTCoroutineStarter.Stop(m_CreateButtonCoroutine); + m_CreateButtonCoroutine = null; + } + + /// Destroy moderation button + if (m_ModerationButton != null) + { + GameObject.Destroy(m_ModerationButton.gameObject); + m_ModerationButton = null; + } + + /// Destroy + DestroyFloatingPanels(); + + UI.ModerationViewFlowCoordinator.Destroy(); + + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsLeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsMainView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsRightView); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get Module settings UI + /// + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() + { + if (m_SettingsLeftView == null) m_SettingsLeftView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsRightView == null) m_SettingsRightView = CP_SDK.UI.UISystem.CreateViewController(); + + return (m_SettingsMainView, m_SettingsLeftView, m_SettingsRightView); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When the menu loaded + /// + private void ChatPlexSDK_OnGenericMenuSceneLoaded() + { + if (m_ModerationButton == null || !m_ModerationButton ) + { + /// Stop coroutine + if (m_CreateButtonCoroutine != null) + { + CP_SDK.Unity.MTCoroutineStarter.Stop(m_CreateButtonCoroutine); + m_CreateButtonCoroutine = null; + } + + /// Destroy moderation button + if (m_ModerationButton != null) + { + GameObject.Destroy(m_ModerationButton.gameObject); + m_ModerationButton = null; + } + + /// Add button + if (m_CreateButtonCoroutine == null) + m_CreateButtonCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(CreateButtonCoroutine()); + } + } + /// + /// When the active scene is changed + /// + /// + private void ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.EGenericScene p_SceneType) + { + if (m_RootTransform) + m_RootTransform.transform.localScale = Vector3.one; + + if (p_SceneType == CP_SDK.ChatPlexSDK.EGenericScene.Menu) + UpdateButton(); + + if (m_ChatFloatingPanel == null) CreateFloatingPanels(); + else UpdateFloatingPanels(); + } + /// + /// When the floating window is moved + /// + /// Event sender + /// Event data + private void OnFloatingWindowMoved(CP_SDK.UI.Components.CFloatingPanel p_FloatingPanel) + { + m_DockedFloatingPanelTransform.localPosition = m_ChatFloatingPanel.RTransform.localPosition; + m_DockedFloatingPanelTransform.localRotation = m_ChatFloatingPanel.RTransform.localRotation; + } + /// + /// Toggle chat visibility + /// + public void ToggleVisibility() + { + if (m_RootTransform && m_RootTransform.localScale.x > 0.5f) m_RootTransform.localScale = Vector3.zero; + else if (m_RootTransform) m_RootTransform.localScale = Vector3.one; + } + /// + /// Set visible + /// + /// Is visible + public void SetVisible(bool p_Visible) + { + if (!m_RootTransform) + return; + + m_RootTransform.localScale = p_Visible ? Vector3.one : Vector3.zero; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create button coroutine + /// + /// + private IEnumerator CreateButtonCoroutine() + { +#if BEATSABER + var l_LevelSelectionNavigationController = null as LevelSelectionNavigationController; + var l_Waiter = new WaitForSeconds(0.25f); + while (true) + { + if (!l_LevelSelectionNavigationController) + l_LevelSelectionNavigationController = Resources.FindObjectsOfTypeAll().LastOrDefault(); + + if (l_LevelSelectionNavigationController != null && l_LevelSelectionNavigationController.gameObject.transform.childCount >= 2) + break; + + yield return l_Waiter; + } + + m_ModerationButton = BeatSaberPlus.SDK.UI.Button.Create(l_LevelSelectionNavigationController.transform, "Chat\nModeration", () => UI.ModerationViewFlowCoordinator.Instance().Present(), null); + m_ModerationButton.transform.localPosition = new Vector3(72.50f, 27.00f, 2.60f); + m_ModerationButton.transform.localScale = new Vector3( 0.65f, 0.50f, 0.65f); + m_ModerationButton.transform.SetAsFirstSibling(); + m_ModerationButton.gameObject.SetActive(true); + m_ModerationButton.GetComponentInChildren().margin = new Vector4(0, 4, 0, 0); + m_ModerationButton.GetComponentInChildren().fontStyle = FontStyles.Normal; + + var l_Images = m_ModerationButton.GetComponentsInChildren(); + foreach (var l_Image in l_Images) + { + l_Image._skew = 0f; + l_Image.SetAllDirty(); + } + + UpdateButton(); + + m_CreateButtonCoroutine = null; +#elif UNITY_TESTING || SYNTHRIDERS || AUDIOTRIP || BOOMBOX + yield return null; +#else +#error Missing game implementation +#endif + } + /// + /// Update button text + /// + internal void UpdateButton() + { + if (m_ModerationButton == null) + return; + +#if BEATSABER + m_ModerationButton.transform.localPosition = new Vector3(72.50f, 27.00f, 2.60f); + m_ModerationButton.transform.localScale = new Vector3( 0.65f, 0.50f, 0.65f); +#elif UNITY_TESTING || SYNTHRIDERS || AUDIOTRIP || BOOMBOX +#else +#error Missing game implementation +#endif + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create floating panels + /// + private void CreateFloatingPanels() + { + if (m_RootTransform != null) + return; + + try + { + /// Prepare root game object + m_RootTransform = new GameObject("ChatPlexSDK_Chat").transform; + GameObject.DontDestroyOnLoad(m_RootTransform.gameObject); + + m_DockedFloatingPanelTransform = new GameObject("DockedFloatingPanelTransform").transform; + m_DockedFloatingPanelTransform.SetParent(m_RootTransform); + + /////////////////////////////////////////////// + /// Chat + m_ChatFloatingPanel = CP_SDK.UI.UISystem.FloatingPanelFactory.Create("ChatFloatingPanel", m_RootTransform); + m_ChatFloatingPanelView = CP_SDK.UI.UISystem.CreateViewController(); + m_ChatFloatingPanel.SetSize(CConfig.Instance.ChatSize); + m_ChatFloatingPanel.SetAlignWithFloor(CConfig.Instance.AlignWithFloor); + m_ChatFloatingPanel.SetBackground(true, CConfig.Instance.BackgroundColor); + m_ChatFloatingPanel.SetRadius(0); + m_ChatFloatingPanel.SetViewController(m_ChatFloatingPanelView); + m_ChatFloatingPanel.OnRelease(OnFloatingWindowMoved); + m_ChatFloatingPanel.SetSceneTransform(CP_SDK.ChatPlexSDK.EGenericScene.Menu, CConfig.Instance.MenuChatPosition, CConfig.Instance.MenuChatRotation); + m_ChatFloatingPanel.SetSceneTransform(CP_SDK.ChatPlexSDK.EGenericScene.Menu, CConfig.Instance.PlayingChatPosition, CConfig.Instance.PlayingChatRotation); + m_ChatFloatingPanel.OnSceneRelease(CP_SDK.ChatPlexSDK.EGenericScene.Menu, (p_LocalPosition, p_LocalRotation) => + { + CConfig.Instance.MenuChatPosition = p_LocalPosition; + CConfig.Instance.MenuChatRotation = p_LocalRotation; + CConfig.Instance.Save(); + }); + m_ChatFloatingPanel.OnSceneRelease(CP_SDK.ChatPlexSDK.EGenericScene.Playing, (p_LocalPosition, p_LocalRotation) => + { + CConfig.Instance.PlayingChatPosition = p_LocalPosition; + CConfig.Instance.PlayingChatRotation = p_LocalRotation; + CConfig.Instance.Save(); + }); + m_ChatFloatingPanel.OnSceneRelocated(OnFloatingWindowMoved); + m_ChatFloatingPanel.OnGearIcon((_) => + { + var l_Items = GetSettingsViewControllers(); + CP_SDK.UI.FlowCoordinators.MainFlowCoordinator.Instance().Present(); + CP_SDK.UI.FlowCoordinators.MainFlowCoordinator.Instance().ChangeViewControllers(l_Items.Item1, l_Items.Item2, l_Items.Item3); + }); + /////////////////////////////////////////////// + + /////////////////////////////////////////////// + /// HypeTrain + m_ChatHypeTrainFloatingPanel = CP_SDK.UI.UISystem.FloatingPanelFactory.Create("ChatHypeTrainFloatingPanel", m_DockedFloatingPanelTransform.transform); + m_ChatHypeTrainFloatingPanelView = CP_SDK.UI.UISystem.CreateViewController(); + m_ChatHypeTrainFloatingPanel.SetRadius(0); + m_ChatHypeTrainFloatingPanel.SetBackground(false); + m_ChatHypeTrainFloatingPanel.SetViewController(m_ChatHypeTrainFloatingPanelView); + /////////////////////////////////////////////// + + /////////////////////////////////////////////// + /// Poll window + m_PollFloatingPanel = CP_SDK.UI.UISystem.FloatingPanelFactory.Create("PollFloatingPanel", m_DockedFloatingPanelTransform.transform); + m_PollFloatingPanelView = CP_SDK.UI.UISystem.CreateViewController(); + m_PollFloatingPanel.SetSize(UI.PollFloatingPanelView.SIZE); + m_PollFloatingPanel.SetRadius(0); + m_PollFloatingPanel.SetBackground(true); + m_PollFloatingPanel.SetViewController(m_PollFloatingPanelView); + /////////////////////////////////////////////// + + /////////////////////////////////////////////// + /// Prediction window + m_ChatPredictionFloatingPanel = CP_SDK.UI.UISystem.FloatingPanelFactory.Create("PredictionFloatingPanel", m_DockedFloatingPanelTransform.transform); + m_ChatPredictionFloatingPanelView = CP_SDK.UI.UISystem.CreateViewController(); + m_ChatPredictionFloatingPanel.SetSize(UI.PredictionFloatingPanelView.SIZE); + m_ChatPredictionFloatingPanel.SetRadius(0); + m_ChatPredictionFloatingPanel.SetBackground(true); + m_ChatPredictionFloatingPanel.SetViewController(m_ChatPredictionFloatingPanelView); + /////////////////////////////////////////////// + + /////////////////////////////////////////////// + /// Status + m_StatusFloatingPanel = CP_SDK.UI.UISystem.FloatingPanelFactory.Create("StatusFloatingPanel", m_DockedFloatingPanelTransform.transform); + m_StatusFloatingPanelView = CP_SDK.UI.UISystem.CreateViewController(); + m_StatusFloatingPanel.SetSize(UI.StatusFloatingPanelView.SIZE); + m_StatusFloatingPanel.SetBackground(false); + m_StatusFloatingPanel.SetRadius(0); + m_StatusFloatingPanel.SetViewController(m_StatusFloatingPanelView); + /////////////////////////////////////////////// + + UpdateFloatingPanels(); + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[ChatPlexMod_Chat][Chat.CreateFloatingPanels] Failed to CreateFloatingPanels"); + Logger.Instance.Error(l_Exception); + } + } + /// + /// Destroy floating panels + /// + private void DestroyFloatingPanels() + { + if (m_RootTransform == null) + return; + + try + { + CP_SDK.UI.UISystem.DestroyUI(ref m_ChatFloatingPanel, ref m_ChatFloatingPanelView); + CP_SDK.UI.UISystem.DestroyUI(ref m_ChatHypeTrainFloatingPanel, ref m_ChatHypeTrainFloatingPanelView); + CP_SDK.UI.UISystem.DestroyUI(ref m_PollFloatingPanel, ref m_PollFloatingPanelView); + CP_SDK.UI.UISystem.DestroyUI(ref m_ChatPredictionFloatingPanel, ref m_ChatPredictionFloatingPanelView); + CP_SDK.UI.UISystem.DestroyUI(ref m_StatusFloatingPanel, ref m_StatusFloatingPanelView); + + GameObject.Destroy(m_RootTransform.gameObject); + + m_DockedFloatingPanelTransform = null; + m_RootTransform = null; + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[ChatPlexMod_Chat][Chat.DestroyFloatingPanels] Failed to DestroyFloatingPanels"); + Logger.Instance.Error(l_Exception); + } + } + /// + /// Update floating panels + /// + internal void UpdateFloatingPanels() + { + if (m_RootTransform == null) + return; + + try + { + m_RootTransform.localPosition = Vector3.zero; + m_RootTransform.localRotation = Quaternion.identity; + + m_DockedFloatingPanelTransform.localPosition = m_ChatFloatingPanel.RTransform.localPosition; + m_DockedFloatingPanelTransform.localRotation = m_ChatFloatingPanel.RTransform.localRotation; + + /////////////////////////////////////////////// + /// Chat + m_ChatFloatingPanel.SetSize(CConfig.Instance.ChatSize); + m_ChatFloatingPanel.SetAlignWithFloor(CConfig.Instance.AlignWithFloor); + m_ChatFloatingPanel.SetBackgroundColor(CConfig.Instance.BackgroundColor); + m_ChatFloatingPanel.SetSceneTransform(CP_SDK.ChatPlexSDK.EGenericScene.Menu, CConfig.Instance.MenuChatPosition, CConfig.Instance.MenuChatRotation); + m_ChatFloatingPanel.SetSceneTransform(CP_SDK.ChatPlexSDK.EGenericScene.Playing, CConfig.Instance.PlayingChatPosition, CConfig.Instance.PlayingChatRotation); + + if (CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Menu) + m_ChatFloatingPanel.SetGearIcon(CConfig.Instance.ReverseChatOrder ? CP_SDK.UI.Components.CFloatingPanel.ECorner.BottomLeft : CP_SDK.UI.Components.CFloatingPanel.ECorner.TopLeft); + else + m_ChatFloatingPanel.SetGearIcon(CP_SDK.UI.Components.CFloatingPanel.ECorner.None); + + if (CConfig.Instance.ShowLockIcon) + m_ChatFloatingPanel.SetLockIcon(CConfig.Instance.ReverseChatOrder ? CP_SDK.UI.Components.CFloatingPanel.ECorner.BottomRight : CP_SDK.UI.Components.CFloatingPanel.ECorner.TopRight); + else + m_ChatFloatingPanel.SetLockIcon(CP_SDK.UI.Components.CFloatingPanel.ECorner.None); + + /// Prepare data for level with rotations +#if BEATSABER + var l_Is360Level = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.transformedBeatmapData?.spawnRotationEventsCount > 0; + var l_RotationRef = l_Is360Level ? Resources.FindObjectsOfTypeAll().FirstOrDefault()?.gameObject : null as GameObject; +#elif SYNTHRIDERS + var l_RotationRef = Resources.FindObjectsOfTypeAll().FirstOrDefault(x => x.name == "[Score & Misc]"); +#elif UNITY_TESTING || AUDIOTRIP || BOOMBOX + var l_RotationRef = null as GameObject; +#else +#error Missing game implementation +#endif + /// Update chat messages display + m_ChatFloatingPanelView.UpdateUI(l_RotationRef); + /////////////////////////////////////////////// + + /////////////////////////////////////////////// + /// HypeTrain + var l_HypeTrainSize = new Vector2(CConfig.Instance.ChatSize.x, UI.HypeTrainFloatingPanelView.HEIGHT); + var l_HypeTrainPosition = new Vector3( + 0f, + ((-CConfig.Instance.ChatSize.y - l_HypeTrainSize.y) / 2.0f) * 0.02f, + 0.0f + ); + m_ChatHypeTrainFloatingPanel.SetSize(l_HypeTrainSize); + m_ChatHypeTrainFloatingPanel.SetTransformDirect(l_HypeTrainPosition, Vector3.zero); + /////////////////////////////////////////////// + + /////////////////////////////////////////////// + /// Poll window + var l_PollPosition = new Vector3( + (( CConfig.Instance.ChatSize.x + UI.PollFloatingPanelView.SIZE.x ) / 2f) * 0.02f, + ((-CConfig.Instance.ChatSize.y + UI.PollFloatingPanelView.SIZE.y + 16) / 2f) * 0.02f, + 0 + ); + m_PollFloatingPanel.SetBackgroundColor(CConfig.Instance.BackgroundColor); + m_PollFloatingPanel.SetTransformDirect(l_PollPosition, Vector3.zero); + /////////////////////////////////////////////// + + /////////////////////////////////////////////// + /// Prediction window + var l_PredictionPosition = new Vector3( + ((-CConfig.Instance.ChatSize.x - UI.PredictionFloatingPanelView.SIZE.x ) / 2f) * 0.02f, + ((-CConfig.Instance.ChatSize.y + UI.PredictionFloatingPanelView.SIZE.y + 16) / 2f) * 0.02f, + 0 + ); + m_ChatPredictionFloatingPanel.SetBackgroundColor(CConfig.Instance.BackgroundColor); + m_ChatPredictionFloatingPanel.SetTransformDirect(l_PredictionPosition, Vector3.zero); + /////////////////////////////////////////////// + + /////////////////////////////////////////////// + /// Status + var l_StatusPosition = new Vector3( + (( CConfig.Instance.ChatSize.x + UI.StatusFloatingPanelView.SIZE.x) / 2f) * 0.02f, + ((-CConfig.Instance.ChatSize.y + UI.StatusFloatingPanelView.SIZE.y) / 2f) * 0.02f, + 0 + ); + m_StatusFloatingPanel.gameObject.SetActive(CConfig.Instance.ShowViewerCount); + m_StatusFloatingPanel.SetTransformDirect(l_StatusPosition, Vector3.zero); + /////////////////////////////////////////////// + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[ChatPlexMod_Chat][Chat.UpdateFloatingPanels] Failed to UpdateFloatingPanels"); + Logger.Instance.Error(l_Exception); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Queue or send an action + /// + /// Action + private void QueueOrSendChatAction(Action p_Action) + { + if (m_ChatFloatingPanelView == null || !m_ChatFloatingPanelView.UICreated) + m_ActionQueue.Enqueue(p_Action); + else + p_Action.Invoke(); + } + /// + /// Dequeue actions + /// + /// + private async Task ActionDequeueTask() + { + await Task.Yield(); + + while (m_ActionDequeueRun) + { + if (m_ChatFloatingPanelView == null || !m_ChatFloatingPanelView.UICreated || m_ActionQueue.IsEmpty) + { + await Task.Delay(1000).ConfigureAwait(false); + continue; + } + + /// Work through the queue of messages that has piled up one by one until they're all gone. + while (m_ActionDequeueRun && m_ActionQueue.TryDequeue(out var l_Action)) + l_Action.Invoke(); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On system message + /// + /// Chat service + /// Message + private void Mutiplixer_OnSystemMessage(IChatService p_ChatService, string p_Message) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnSystemMessage(p_ChatService, p_Message)); + } + /// + /// On login + /// + /// Chat service + private void Mutiplixer_OnLogin(IChatService p_ChatService) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnLogin(p_ChatService)); + } + /// + /// On channel join + /// + /// Chat service + /// Channel instance + private void Mutiplixer_OnJoinChannel(IChatService p_ChatService, IChatChannel p_Channel) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnJoinChannel(p_ChatService, p_Channel)); + } + /// + /// On channel leave + /// + /// Chat service + /// Channel instance + private void Mutiplixer_OnLeaveChannel(IChatService p_ChatService, IChatChannel p_Channel) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnLeaveChannel(p_ChatService, p_Channel)); + } + /// + /// On channel follow + /// + /// Chat service + /// Channel instance + /// User instance + private void Mutiplixer_OnChannelFollow(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnChannelFollow(p_ChatService, p_Channel, p_User)); + } + /// + /// On channel bits + /// + /// Chat service + /// Channel instance + /// User instance + /// Used bits + private void Mutiplixer_OnChannelBits(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User, int p_BitsUsed) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnChannelBits(p_ChatService, p_Channel, p_User, p_BitsUsed)); + } + /// + /// On channel points + /// + /// Chat service + /// Channel instance + /// User instance + /// Event + private void Mutiplixer_OnChannelPoints(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User, IChatChannelPointEvent p_Event) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnChannelPoints(p_ChatService, p_Channel, p_User, p_Event)); + } + /// + /// On channel subscription + /// + /// Chat service + /// Channel instance + /// User instance + /// Event + private void Mutiplixer_OnChannelSubscription(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User, IChatSubscriptionEvent p_Event) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnChannelSubsciption(p_ChatService, p_Channel, p_User, p_Event)); + } + /// + /// On text message received + /// + /// Chat service + /// ID of the message + private void Mutiplixer_OnTextMessageReceived(IChatService p_ChatService, IChatMessage p_Message) + { + lock (m_LastChatUsers) + { + if (m_LastChatUsers.Count(x => x.Item1 == p_ChatService && x.Item2.UserName == p_Message.Sender.UserName) == 0) + m_LastChatUsers.Add((p_ChatService, p_Message.Sender)); + } + + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnTextMessageReceived(p_ChatService, p_Message)); + } + /// + /// On room state changed + /// + /// Chat service + /// Channel instance + private void Mutiplixer_OnRoomStateUpdated(IChatService p_ChatService, IChatChannel p_Channel) + => UI.ModerationLeftView.Instance?.UpdateRoomState(); + /// + /// On chat user cleared + /// + /// Chat service + /// ID of the user + private void Mutiplixer_OnChatCleared(IChatService p_ChatService, string p_UserID) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnChatCleared(p_UserID)); + } + /// + /// On message cleared + /// + /// Chat service + /// ID of the message + private void Mutiplixer_OnMessageCleared(IChatService p_ChatService, string p_MessageID) + { + QueueOrSendChatAction(() => m_ChatFloatingPanelView.OnMessageCleared(p_MessageID)); + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/Components/ChatImage.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Components/ChatImage.cs similarity index 100% rename from Modules/BeatSaberPlus_Chat/Components/ChatImage.cs rename to Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Components/ChatImage.cs diff --git a/Modules/BeatSaberPlus_Chat/Components/ChatMessageText.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Components/ChatMessageText.cs similarity index 98% rename from Modules/BeatSaberPlus_Chat/Components/ChatMessageText.cs rename to Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Components/ChatMessageText.cs index 6e3b6fa..ac9329a 100644 --- a/Modules/BeatSaberPlus_Chat/Components/ChatMessageText.cs +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Components/ChatMessageText.cs @@ -62,13 +62,13 @@ internal class ChatMessageText : TextMeshProUGUI l_Image.rectTransform.anchorMax = new Vector2(0.5f, 0.5f); l_Image.rectTransform.pivot = new Vector2(0, 0); l_Image.rectTransform.localRotation = Quaternion.identity; - l_Image.material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; + l_Image.material = CP_SDK.UI.UISystem.Override_GetUIMaterial(); l_Image.color = Color.white; l_Image.raycastTarget = false; l_Image.AnimStateUpdater = l_Image.gameObject.AddComponent(); l_Image.AnimStateUpdater.TargetImage= l_Image; - l_Image.gameObject.layer = LayerMask.NameToLayer("UI"); + l_Image.gameObject.layer = CP_SDK.UI.UISystem.UILayer; l_Image.gameObject.SetActive(false); return l_Image; diff --git a/Modules/BeatSaberPlus_Chat/Components/ChatMessageWidget.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Components/ChatMessageWidget.cs similarity index 82% rename from Modules/BeatSaberPlus_Chat/Components/ChatMessageWidget.cs rename to Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Components/ChatMessageWidget.cs index 51545b0..de9a03f 100644 --- a/Modules/BeatSaberPlus_Chat/Components/ChatMessageWidget.cs +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Components/ChatMessageWidget.cs @@ -1,4 +1,5 @@ -using System; +using CP_SDK.Chat.Interfaces; +using System; using System.Runtime.CompilerServices; using TMPro; using UnityEngine; @@ -18,34 +19,22 @@ internal class ChatMessageWidget : MonoBehaviour //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Rect transform instance - /// - internal RectTransform RectTranform; - /// - /// Text instance - /// - internal ChatMessageText Text; - /// - /// SubText instance - /// - internal ChatMessageText SubText; - /// - /// On rebuild complete - /// + private Vector3 m_LocalPosition = Vector3.zero; + private Image m_Highlight = null; + private Image m_Accent = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + internal RectTransform RTranform = null; + internal IChatService Service = null; + internal ChatMessageText Text = null; + internal ChatMessageText SubText = null; + internal bool EnableCallback = false; + internal float Height = 0f; + internal float PositionY = 0f; + internal event Action OnLatePreRenderRebuildComplete; - /// - /// Should enable callbacks? - /// - internal bool EnableCallback = false; - /// - /// Current height - /// - internal float Height = 0f; - /// - /// Position Y - /// - internal float PositionY = 0f; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -94,37 +83,22 @@ internal bool SubTextEnabled //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Local position - /// - private Vector3 m_LocalPosition = Vector3.zero; - /// - /// Highlight image - /// - private Image m_Highlight; - /// - /// Accent image - /// - private Image m_Accent; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// /// /// When the GameObject and his components is ready /// private void Awake() { - RectTranform = transform as RectTransform; + RTranform = transform as RectTransform; /// Prepare highlight m_Highlight = gameObject.AddComponent(); m_Highlight.raycastTarget = false; - m_Highlight.material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; + m_Highlight.material = CP_SDK.UI.UISystem.Override_GetUIMaterial(); - RectTranform.anchorMin = new Vector2(0.5f, 0f); - RectTranform.anchorMax = new Vector2(0.5f, 0f); - RectTranform.pivot = new Vector2(0.5f, 0f); + RTranform.anchorMin = new Vector2(0.5f, 0f); + RTranform.anchorMax = new Vector2(0.5f, 0f); + RTranform.pivot = new Vector2(0.5f, 0f); /// Prepare text Text = new GameObject().AddComponent(); @@ -157,7 +131,9 @@ private void Awake() /// Accent image m_Accent = new GameObject().AddComponent(); m_Accent.raycastTarget = false; - m_Accent.material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; + m_Accent.type = Image.Type.Sliced; + m_Accent.material = CP_SDK.UI.UISystem.Override_GetUIMaterial(); + m_Accent.sprite = CP_SDK.UI.UISystem.GetUIRoundBGSprite(); m_Accent.color = Color.white; /// Disable all sub element by default @@ -196,7 +172,7 @@ internal void SetPositionY(float p_PositionY) { m_LocalPosition.y = p_PositionY; PositionY = p_PositionY; - RectTranform.localPosition = m_LocalPosition; + RTranform.localPosition = m_LocalPosition; } /// /// Set width @@ -206,7 +182,7 @@ internal void SetWidth(float p_Width) { Text.rectTransform.sizeDelta = new Vector2(p_Width, Text.rectTransform.sizeDelta.y); SubText.rectTransform.sizeDelta = new Vector2(p_Width, SubText.rectTransform.sizeDelta.y); - RectTranform.sizeDelta = new Vector2(p_Width, RectTranform.sizeDelta.y); + RTranform.sizeDelta = new Vector2(p_Width, RTranform.sizeDelta.y); OnTextChanged(); } @@ -236,13 +212,13 @@ private void OnTextChanged() // l_TextHeight = Text.GetPreferredValues(" ").y; Height = l_TextHeight + l_SubTextHeight; - RectTranform.sizeDelta = new Vector2(RectTranform.sizeDelta.x, Height); + RTranform.sizeDelta = new Vector2(RTranform.sizeDelta.x, Height); if (SubText.enabled) SubText.rectTransform.localPosition = new Vector3(SubText.rectTransform.localPosition.x, l_SubTextHeight, SubText.rectTransform.localPosition.z); if (m_Accent.enabled) - m_Accent.rectTransform.sizeDelta = new Vector2(s_LeftRightMargins / 2f, RectTranform.sizeDelta.y); + m_Accent.rectTransform.sizeDelta = new Vector2(s_LeftRightMargins / 2f, RTranform.sizeDelta.y); } } } diff --git a/Modules/BeatSaberPlus_Chat/Extensions/EnhancedFontInfo.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Extensions/EnhancedFontInfo.cs similarity index 93% rename from Modules/BeatSaberPlus_Chat/Extensions/EnhancedFontInfo.cs rename to Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Extensions/EnhancedFontInfo.cs index 7f2c3da..739c5fa 100644 --- a/Modules/BeatSaberPlus_Chat/Extensions/EnhancedFontInfo.cs +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Extensions/EnhancedFontInfo.cs @@ -139,7 +139,16 @@ internal bool TryRegisterImageInfo(CP_SDK.Unity.EnhancedImage p_ImageInfo, out u l_ReplaceCharacter = GetNextReplaceChar(); while (Font.characterLookupTable.ContainsKey(l_ReplaceCharacter)); - Font.characterLookupTable.Add(l_ReplaceCharacter, new TMP_Character(l_ReplaceCharacter, new Glyph(l_ReplaceCharacter, new GlyphMetrics(0, 0, 0, 0, p_ImageInfo.Width), new GlyphRect(0, 0, 0, 0)))); + var l_Glypth = new Glyph(l_ReplaceCharacter, new GlyphMetrics(0, 0, 0, 0, p_ImageInfo.Width), new GlyphRect(0, 0, 0, 0)); + +#if BEATSABER && !BEATSABER_1_29_4_OR_NEWER + Font.characterLookupTable.Add(l_ReplaceCharacter, new TMP_Character(l_ReplaceCharacter, l_Glypth)); +#elif BEATSABER_1_29_4_OR_NEWER || UNITY_TESTING || SYNTHRIDERS || AUDIOTRIP || BOOMBOX + Font.characterLookupTable.Add(l_ReplaceCharacter, new TMP_Character(l_ReplaceCharacter, Font, l_Glypth)); +#else +#error Missing game implementation +#endif + m_ReplaceCharacters.TryAdd(p_ImageInfo.ImageID, l_ReplaceCharacter); m_ImageInfos.TryAdd(l_ReplaceCharacter, p_ImageInfo); diff --git a/Modules/BeatSaberPlus_Chat/Logger.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Logger.cs similarity index 100% rename from Modules/BeatSaberPlus_Chat/Logger.cs rename to Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Logger.cs diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Resources/ViewerIcon.png b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Resources/ViewerIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..38e7778376dd4bbd09deabce597de9a6213a8114 GIT binary patch literal 2746 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 i literal 0 HcmV?d00001 diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ChatFloatingPanelView.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ChatFloatingPanelView.cs new file mode 100644 index 0000000..921ee78 --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ChatFloatingPanelView.cs @@ -0,0 +1,654 @@ +using CP_SDK.Chat.Interfaces; +using CP_SDK.Unity.Extensions; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Chat floating panel view + /// + internal sealed class ChatFloatingPanelView : CP_SDK.UI.ViewController + { + private GameObject m_EnvironmentRotationRef; + private Vector2 m_ChatSize; + private float m_FontSize; + private bool m_ReverseChatOrder; + private bool m_PlatformOriginColor; + private Color m_HighlightColor; + private Color m_TextColor; + private Color m_PingColor; + private bool m_FilterViewersCommands; + private bool m_FilterBroadcasterCommands; + + private Extensions.EnhancedFontInfo m_ChatFont = null; + private CP_SDK.Pool.ObjectPool m_MessagePool = null; + private List m_MessagePool_Allocated = new List(); + private List m_Messages = new List(); + private bool m_UpdateMessagePositions = false; + private Components.ChatMessageWidget m_LastMessage = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + /// Update message position origin + RTransform.pivot = new Vector2(0.5f, 0f); + + InitLogic(); + + /// Make icons easier to click + //m_SettingsIcon.gameObject.AddComponent().radius = 10f; + //m_LockIcon.gameObject.AddComponent().radius = 10f; + } + /// + /// On view destruction + /// + protected override sealed void OnViewDestruction() + { + DestroyLogic(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Update UI + /// + /// Flying hame HUD rotation + internal void UpdateUI(GameObject p_EnvironmentRotationRef) + { + m_ChatSize = CConfig.Instance.ChatSize; + m_FontSize = CConfig.Instance.FontSize; + m_ReverseChatOrder = CConfig.Instance.ReverseChatOrder; + m_PlatformOriginColor = CConfig.Instance.PlatformOriginColor; + m_HighlightColor = CConfig.Instance.HighlightColor; + m_TextColor = CConfig.Instance.TextColor; + m_PingColor = CConfig.Instance.PingColor; + + m_FilterViewersCommands = CConfig.Instance.FilterViewersCommands; + m_FilterBroadcasterCommands = CConfig.Instance.FilterBroadcasterCommands; + m_EnvironmentRotationRef = p_EnvironmentRotationRef; + + UpdateMessagesStyleFull(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On frame + /// + private void Update() + { + if (CConfig.Instance.FollowEnvironementRotation && m_EnvironmentRotationRef != null && m_EnvironmentRotationRef) + transform.parent.parent.rotation = m_EnvironmentRotationRef.transform.rotation; + + if (m_UpdateMessagePositions) + { + m_UpdateMessagePositions = false; + float l_PositionY = m_ReverseChatOrder ? m_ChatSize.y : 0; + + for (int l_I = (m_Messages.Count - 1); l_I >= 0; --l_I) + { + var l_CurrentMessage = m_Messages[l_I]; + var l_Height = l_CurrentMessage.Height; + + if (m_ReverseChatOrder) + l_PositionY -= l_Height; + + l_CurrentMessage.SetPositionY(l_PositionY); + + if (!m_ReverseChatOrder) + l_PositionY += l_Height; + } + + for (int l_I = 0; l_I < m_Messages.Count;) + { + var l_Current = m_Messages[l_I]; + if ((m_ReverseChatOrder && l_Current.PositionY < -m_ChatSize.y) || l_Current.PositionY >= m_ChatSize.y) + { + m_Messages.Remove(l_Current); + m_MessagePool.Release(l_Current); + continue; + } + + ++l_I; + } + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Init logic + /// + private void InitLogic() + { + m_ChatFont = new Extensions.EnhancedFontInfo(CP_SDK.Unity.FontManager.GetChatFont()); + + /// Clean reserved characters + m_ChatFont.Font.characterTable.RemoveAll(x => x.glyphIndex > 0xE000 && x.glyphIndex <= 0xF8FF); + m_ChatFont.Font.characterTable.RemoveAll(x => x.glyphIndex > 0xF0000); + + /// Setup message pool + m_MessagePool = new CP_SDK.Pool.ObjectPool( + createFunc: () => + { + var l_Message = null as Components.ChatMessageWidget; + l_Message = new GameObject().AddComponent(); + l_Message.Text.FontInfo = m_ChatFont; + l_Message.Text.font = m_ChatFont.Font; + l_Message.Text.fontSize = m_FontSize; + l_Message.Text.color = m_TextColor; + l_Message.Text.text = "."; + l_Message.Text.SetAllDirty(); + + l_Message.SubText.FontInfo = m_ChatFont; + l_Message.SubText.font = m_ChatFont.Font; + l_Message.SubText.fontSize = m_FontSize; + l_Message.SubText.color = m_TextColor; + l_Message.SubText.text = "."; + l_Message.SubText.SetAllDirty(); + + l_Message.transform.SetParent(RTransform, false); + l_Message.transform.SetAsFirstSibling(); + l_Message.SetWidth(m_ChatSize.x); + + l_Message.transform.localScale = Vector3.zero; + l_Message.gameObject.ChangerLayerRecursive(CP_SDK.UI.UISystem.UILayer); + + UpdateMessageStyle(l_Message); + + l_Message.OnLatePreRenderRebuildComplete += OnMessageRenderRebuildComplete; + + m_MessagePool_Allocated.Add(l_Message); + + return l_Message; + }, + actionOnGet: (p_Message) => + { + p_Message.EnableCallback = true; + }, + actionOnRelease: (p_Message) => + { + try + { + p_Message.transform.localScale = Vector3.zero; + + p_Message.HighlightEnabled = false; + p_Message.AccentEnabled = false; + p_Message.SubTextEnabled = false; + p_Message.Text.ChatMessage = null; + p_Message.SubText.ChatMessage = null; + + p_Message.EnableCallback = false; + + p_Message.Text.ClearImages(); + p_Message.SubText.ClearImages(); + } + catch (System.Exception p_Exception) + { + Logger.Instance.Error("[ChatPlexMod_Chat.UI][ChatFloatingPanelView.InitLogic] An exception occurred while trying to free ChatMessageWidget object:"); + Logger.Instance.Error(p_Exception); + } + }, + actionOnDestroy: (p_Message) => + { + GameObject.Destroy(p_Message.gameObject); + m_MessagePool_Allocated.Remove(p_Message); + }, + collectionCheck: false, + defaultCapacity: 25 + ); + } + /// + /// Destroy logic + /// + private void DestroyLogic() + { + m_Messages.Clear(); + + if (m_MessagePool != null) + { + m_MessagePool.Dispose(); + m_MessagePool = null; + } + + if (m_ChatFont != null) + { + Destroy(m_ChatFont.Font); + m_ChatFont = null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Add a new message to the display + /// + /// Message to add + private void AddMessage(Components.ChatMessageWidget p_NewMessage) + { + p_NewMessage.SetPositionY(m_ReverseChatOrder ? m_ChatSize.y : 0); + + m_Messages.Add(p_NewMessage); + UpdateMessageStyle(p_NewMessage); + + p_NewMessage.transform.localScale = Vector3.one; + } + /// + /// Update all messages + /// + private void UpdateMessagesStyleFull() + { + for (int l_I = 0; l_I < m_MessagePool_Allocated.Count; ++l_I) + UpdateMessageStyleFull(m_MessagePool_Allocated[l_I], true); + + m_UpdateMessagePositions = true; + } + /// + /// Update message + /// + /// Message to update + /// Should flag childs dirty + private void UpdateMessageStyleFull(Components.ChatMessageWidget p_Message, bool p_SetAllDirty = false) + { + p_Message.SetWidth(m_ChatSize.x); + + p_Message.Text.color = m_TextColor; + p_Message.Text.fontSize = m_FontSize; + + p_Message.SubText.color = m_TextColor; + p_Message.SubText.fontSize = m_FontSize; + + UpdateMessageStyle(p_Message); + + if (p_SetAllDirty) + { + p_Message.Text.SetAllDirty(); + + if (p_Message.SubTextEnabled) + p_Message.SubText.SetAllDirty(); + } + } + /// + /// Update message + /// + /// Message to update + /// Should flag childs dirty + private void UpdateMessageStyle(Components.ChatMessageWidget p_Message) + { + p_Message.AccentEnabled = m_PlatformOriginColor; + p_Message.AccentColor = p_Message.Service?.AccentColor ?? Color.gray; + + if (p_Message.Text.ChatMessage == null) + return; + + p_Message.HighlightColor = (p_Message.Text.ChatMessage.IsPing ? m_PingColor : m_HighlightColor); + p_Message.HighlightEnabled = p_Message.Text.ChatMessage.IsHighlighted || p_Message.Text.ChatMessage.IsPing; + } + /// + /// Clear message + /// + /// Message instance + private void ClearMessage(Components.ChatMessageWidget p_Message) + { + string BuildClearedMessage(Components.ChatMessageText p_MessageToClear) + { + var l_StringBuilder = new StringBuilder($"{p_MessageToClear.ChatMessage.Sender.DisplayName}"); + var l_BadgeEndIndex = p_MessageToClear.text.IndexOf(""); + return l_StringBuilder.ToString(); + } + + /// Only clear non-system messages + if (!p_Message.Text.ChatMessage.IsSystemMessage) + { + p_Message.Text.ReplaceContent(BuildClearedMessage(p_Message.Text)); + p_Message.SubTextEnabled = false; + } + + if (p_Message.SubText.ChatMessage != null && !p_Message.SubText.ChatMessage.IsSystemMessage) + p_Message.SubText.ReplaceContent(BuildClearedMessage(p_Message.SubText)); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When a message is rebuilt + /// + private void OnMessageRenderRebuildComplete() + => m_UpdateMessagePositions = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On system message + /// + /// Chat service + /// System message + internal void OnSystemMessage(IChatService p_Service, string p_Message) + { + var l_MessageStr = $"<#FFFFFFBB>[{p_Service.DisplayName}] {p_Message}"; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_NewMessage = m_MessagePool.Get(); + l_NewMessage.Text.ReplaceContent(l_MessageStr); + l_NewMessage.Service = p_Service; + l_NewMessage.HighlightEnabled = true; + l_NewMessage.HighlightColor = ColorU.WithAlpha(Color.gray, 0.18f); + + AddMessage(l_NewMessage); + m_LastMessage = l_NewMessage; + }); + } + /// + /// On login + /// + /// Chat service + internal void OnLogin(IChatService p_Service) + { + var l_MessageStr = $"<#FFFFFFBB>[{p_Service.DisplayName}] Success connecting to {p_Service.DisplayName}"; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_NewMessage = m_MessagePool.Get(); + l_NewMessage.Text.ReplaceContent(l_MessageStr); + l_NewMessage.Service = p_Service; + l_NewMessage.HighlightEnabled = true; + l_NewMessage.HighlightColor = ColorU.WithAlpha(Color.gray, 0.18f); + + AddMessage(l_NewMessage); + m_LastMessage = l_NewMessage; + }); + } + /// + /// On join channel + /// + /// Chat service + /// Channel service + internal void OnJoinChannel(IChatService p_Service, IChatChannel p_Channel) + { + var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; + var l_MessageStr = $"<#FFFFFFBB>[{p_Service.DisplayName}] Success joining {l_Prefix}{p_Channel.Name}"; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_NewMessage = m_MessagePool.Get(); + l_NewMessage.Text.ReplaceContent(l_MessageStr); + l_NewMessage.Service = p_Service; + l_NewMessage.HighlightEnabled = true; + l_NewMessage.HighlightColor = ColorU.WithAlpha(Color.gray, 0.18f); + + AddMessage(l_NewMessage); + m_LastMessage = l_NewMessage; + }); + } + /// + /// On join leave + /// + /// Chat service + /// Channel service + internal void OnLeaveChannel(IChatService p_Service, IChatChannel p_Channel) + { + var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; + var l_MessageStr = $"<#FFFFFFBB>[{p_Service.DisplayName}] Success leaving {l_Prefix}{p_Channel.Name}"; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_NewMessage = m_MessagePool.Get(); + l_NewMessage.Text.ReplaceContent(l_MessageStr); + l_NewMessage.Service = p_Service; + l_NewMessage.HighlightEnabled = true; + l_NewMessage.HighlightColor = ColorU.WithAlpha(Color.gray, 0.18f); + + AddMessage(l_NewMessage); + m_LastMessage = l_NewMessage; + }); + } + /// + /// On channel follow + /// + /// Chat service + /// Channel instance + /// User instance + internal void OnChannelFollow(IChatService p_Service, IChatChannel p_Channel, IChatUser p_User) + { + if (!CConfig.Instance.ShowFollowEvents) + return; + + var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; + var l_MessageStr = $"{l_Prefix}<#FFFFFFBB>[{p_Service.DisplayName}] @{p_User.PaintedName} is now following {p_Channel.Name}"; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_NewMessage = m_MessagePool.Get(); + l_NewMessage.Text.ReplaceContent(l_MessageStr); + l_NewMessage.Service = p_Service; + l_NewMessage.HighlightEnabled = true; + l_NewMessage.HighlightColor = ColorU.WithAlpha(Color.blue, 0.24f); + + AddMessage(l_NewMessage); + m_LastMessage = l_NewMessage; + }); + } + /// + /// On channel bits + /// + /// Chat service + /// Channel instance + /// User instance + /// Bits used + internal void OnChannelBits(IChatService p_Service, IChatChannel p_Channel, IChatUser p_User, int p_BitsUsed) + { + if (!CConfig.Instance.ShowBitsCheeringEvents) + return; + + var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; + var l_MessageStr = $"{l_Prefix}<#FFFFFFBB>[{p_Service.DisplayName}] @{p_User.PaintedName} cheered {p_BitsUsed} bits!"; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_NewMessage = m_MessagePool.Get(); + l_NewMessage.Text.ReplaceContent(l_MessageStr); + l_NewMessage.Service = p_Service; + l_NewMessage.HighlightEnabled = true; + l_NewMessage.HighlightColor = ColorU.WithAlpha(Color.green, 0.24f); + + AddMessage(l_NewMessage); + m_LastMessage = l_NewMessage; + }); + } + /// + /// On channel points + /// + /// Chat service + /// Channel instance + /// User instance + /// Event + internal void OnChannelPoints(IChatService p_Service, IChatChannel p_Channel, IChatUser p_User, IChatChannelPointEvent p_Event) + { + if (!CConfig.Instance.ShowChannelPointsEvent) + return; + + if (!m_ChatFont.HasReplaceCharacter("TwitchChannelPoint_" + p_Event.Title)) + { + TaskCompletionSource l_TaskCompletionSource = new TaskCompletionSource(); + + CP_SDK.Chat.ChatImageProvider.TryCacheSingleImage(EChatResourceCategory.Badge, "TwitchChannelPoint_" + p_Event.Title, p_Event.Image, CP_SDK.Animation.EAnimationType.NONE, (l_Info) => + { + if (l_Info != null && !m_ChatFont.TryRegisterImageInfo(l_Info, out var l_Character)) + Logger.Instance.Warning($"Failed to register emote \"{"TwitchChannelPoint_" + p_Event.Title}\" in font {m_ChatFont.Font.name}."); + + l_TaskCompletionSource.SetResult(l_Info); + }); + + Task.WaitAll(new Task[] { l_TaskCompletionSource.Task }, 15000); + } + + var l_ImagePart = "for"; + + if (m_ChatFont.TryGetReplaceCharacter("TwitchChannelPoint_" + p_Event.Title, out uint p_Character)) + l_ImagePart = char.ConvertFromUtf32((int)p_Character); + + var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; + var l_MessageStr = $"{l_Prefix}<#FFFFFFBB>[{p_Service.DisplayName}] @{p_User.PaintedName} redeemed {p_Event.Title} {l_ImagePart} {p_Event.Cost}!"; + + if (ColorU.TryToUnityColor(p_Event.BackgroundColor, out var l_HighlightColor)) + l_HighlightColor.a = 0.24f; + else + l_HighlightColor = ColorU.WithAlpha(Color.green, 0.24f); + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_NewMessage = m_MessagePool.Get(); + l_NewMessage.Text.ReplaceContent(l_MessageStr); + l_NewMessage.Service = p_Service; + l_NewMessage.HighlightEnabled = true; + l_NewMessage.HighlightColor = l_HighlightColor; + + if (!string.IsNullOrEmpty(p_Event.UserInput)) + { + l_NewMessage.SubText.ReplaceContent(p_Event.UserInput); + l_NewMessage.SubTextEnabled = true; + } + + AddMessage(l_NewMessage); + m_LastMessage = l_NewMessage; + }); + } + /// + /// On channel subscription + /// + /// Chat service + /// Channel instance + /// User instance + /// Event + internal void OnChannelSubsciption(IChatService p_Service, IChatChannel p_Channel, IChatUser p_User, IChatSubscriptionEvent p_Event) + { + if (!CConfig.Instance.ShowSubscriptionEvents) + return; + + var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; + var l_MessageStr = $"{l_Prefix}<#FFFFFFBB>[{p_Service.DisplayName}] @{p_User.PaintedName}"; + if (p_Event.IsGift) + l_MessageStr += $"gifted {p_Event.PurchasedMonthCount} month of {p_Event.SubPlan} to @{p_Event.RecipientDisplayName}!"; + else + l_MessageStr += $"did get a {p_Event.PurchasedMonthCount} month of {p_Event.SubPlan}!"; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + var l_NewMessage = m_MessagePool.Get(); + l_NewMessage.Text.ReplaceContent(l_MessageStr); + l_NewMessage.Service = p_Service; + l_NewMessage.HighlightEnabled = true; + l_NewMessage.HighlightColor = ColorU.WithAlpha(Color.green, 0.36f); + + AddMessage(l_NewMessage); + m_LastMessage = l_NewMessage; + }); + } + /// + /// On chat user message cleared + /// + /// ID of the user + internal void OnChatCleared(string p_UserID) + { + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + foreach (var l_Current in m_Messages) + { + if (l_Current.Text.ChatMessage == null) + continue; + + if (p_UserID == null || l_Current.Text.ChatMessage.Sender.Id == p_UserID) + ClearMessage(l_Current); + } + }); + } + /// + /// On message cleared + /// + /// Message ID + internal void OnMessageCleared(string p_MessageID) + { + if (p_MessageID == null) + return; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + foreach (var l_Current in m_Messages) + { + if (l_Current.Text.ChatMessage == null) + continue; + + if (l_Current.Text.ChatMessage.Id == p_MessageID) + ClearMessage(l_Current); + } + }); + } + /// + /// When a message is received + /// + /// Received message + internal async void OnTextMessageReceived(IChatService p_Service, IChatMessage p_Message) + { + /// Command filters + if (m_FilterViewersCommands || m_FilterBroadcasterCommands) + { + bool l_IsBroadcaster = p_Message.Sender.IsBroadcaster; + + if (m_FilterViewersCommands && !l_IsBroadcaster && p_Message.Message.StartsWith("!")) + return; + + if (m_FilterBroadcasterCommands && l_IsBroadcaster && p_Message.Message.StartsWith("!")) + return; + } + + var l_Prefix = !string.IsNullOrEmpty(p_Message.Channel.Prefix) ? $"[{p_Message.Channel.Prefix}] " : string.Empty; + var l_Message = await Utils.ChatMessageBuilder.BuildMessage(p_Message, m_ChatFont).ConfigureAwait(false); + var l_ParsedMessage = l_Prefix + l_Message; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + if (m_LastMessage != null && !p_Message.IsSystemMessage && m_LastMessage.Text.ChatMessage != null && !string.IsNullOrEmpty(p_Message.Id) && m_LastMessage.Text.ChatMessage.Id == p_Message.Id) + { + /// If the last message received had the same id and isn't a system message, then this was a sub-message of the original and may need to be highlighted along with the original message + m_LastMessage.Service = p_Service; + m_LastMessage.SubText.ChatMessage = p_Message; + m_LastMessage.SubText.ReplaceContent(l_ParsedMessage); + m_LastMessage.SubTextEnabled = true; + + UpdateMessageStyle(m_LastMessage); + } + else + { + var l_NewMsg = m_MessagePool.Get(); + l_NewMsg.Service = p_Service; + l_NewMsg.Text.ChatMessage = p_Message; + l_NewMsg.Text.ReplaceContent(l_ParsedMessage); + + AddMessage(l_NewMsg); + + m_LastMessage = l_NewMsg; + } + }); + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/Data/ChatUserListItem.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/Data/ChatUserListItem.cs new file mode 100644 index 0000000..a8e61bb --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/Data/ChatUserListItem.cs @@ -0,0 +1,52 @@ +using CP_SDK.Chat.Interfaces; + +namespace ChatPlexMod_Chat.UI.Data +{ + /// + /// Chat user list item + /// + internal class ChatUserListItem : CP_SDK.UI.Data.IListItem + { + public IChatService Service; + public IChatUser User; + public string Text; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// Chat service + /// Chat user + public ChatUserListItem(IChatService p_Service, IChatUser p_User) + { + Service = p_Service; + User = p_User; + + Text = "[" + Service.DisplayName + "] "; + if (User.IsModerator || User.IsBroadcaster) Text += "🗡 "; + else if (User.IsVip) Text += "💎 "; + else if (User.IsSubscriber) Text += "👑 "; + Text += User.DisplayName; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On show + /// + public override void OnShow() + { + if (!(Cell is CP_SDK.UI.Data.TextListCell l_TextListCell)) + return; + + l_TextListCell.Text.SetText(Text); + } + /// + /// On hide + /// + public override void OnHide() { } + } +} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/HypeTrainFloatingPanelView.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/HypeTrainFloatingPanelView.cs new file mode 100644 index 0000000..6b125b4 --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/HypeTrainFloatingPanelView.cs @@ -0,0 +1,185 @@ +using CP_SDK.XUI; +using System; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Hype train floating panel view + /// + internal sealed class HypeTrainFloatingPanelView : CP_SDK.UI.ViewController + { + public static float HEIGHT = 7f; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUIHLayout m_Filler = null; + private XUIText m_Label = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private CP_SDK.Chat.Services.Twitch.TwitchService m_TwitchService = null; + private CP_SDK.Chat.Models.Twitch.Helix_HypeTrain m_LastHypeTrain = null; + private float m_CurrentProgression = 0f; + private float m_CurrentExpire = 0f; + private int m_CurrentLevel = 0; + private float m_DisplayedProgression = 0f; + private int m_DisplayedRemaining = 0; + private int m_DisplayedLevel = 0; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override void OnViewCreation() + { + var l_WhiteSprite = CP_SDK.Unity.SpriteU.CreateFromTextureWithBorders(Texture2D.whiteTexture); + + XUIHLayout.Make( + XUIHLayout.Make( + XUIText.Make("LVL 1 - Hype Train!\n33% 1:33") + .SetFontSize(5f) + .SetAlign(TMPro.TextAlignmentOptions.MidlineLeft) + .Bind(ref m_Label) + ) + .SetPadding(0, 2, 0, 2).SetSpacing(0) + .SetBackground(true, new Color32(120, 44, 232, 255)) + .SetBackgroundSprite(l_WhiteSprite, Image.Type.Filled) + .SetBackgroundFillMethod(Image.FillMethod.Horizontal) + .SetBackgroundFillAmount(0.33f) + .OnReady(x => + { + x.HLayoutGroup.childForceExpandWidth = true; + x.HLayoutGroup.childForceExpandHeight = true; + x.CSizeFitter.enabled = false; + x.LElement.ignoreLayout = true; + x.RTransform.anchorMin = Vector2.zero; + x.RTransform.anchorMax = Vector2.one; + }) + .Bind(ref m_Filler) + ) + .SetPadding(0).SetSpacing(0) + .SetBackground(true, new Color32(24, 24, 26, 255)) + .SetBackgroundSprite(l_WhiteSprite, Image.Type.Simple) + .OnReady(x => + { + x.HLayoutGroup.childForceExpandWidth = true; + x.HLayoutGroup.childForceExpandHeight = true; + x.CSizeFitter.enabled = false; + x.LElement.ignoreLayout = true; + x.RTransform.anchorMin = Vector2.zero; + x.RTransform.anchorMax = Vector2.one; + }) + .BuildUI(transform); + + CP_SDK.Chat.Service.Acquire(); + var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); + if (l_TwitchService != null) + { + m_TwitchService = l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService; + m_TwitchService.HelixAPI.OnActiveHypeTrainChanged += HelixAPI_OnActiveHypeTrainChanged; + } + } + /// + /// On view activation + /// + protected override void OnViewActivation() + { + /// Hide by default + CurrentScreen?.gameObject?.SetActive(false); + } + /// + /// On view destruction + /// + protected override void OnViewDestruction() + { + if (m_TwitchService != null) + m_TwitchService.HelixAPI.OnActiveHypeTrainChanged -= HelixAPI_OnActiveHypeTrainChanged; + + CP_SDK.Chat.Service.Release(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On frame + /// + private void Update() + { + if (m_LastHypeTrain != null) + { + var l_HasExpired = (m_CurrentExpire + 60) < Time.realtimeSinceStartup; + if (l_HasExpired) + CurrentScreen?.gameObject?.SetActive(false); + else + { + m_Filler.SetBackgroundFillAmount( + Mathf.Lerp( + m_Filler.Element.GetBackgroundFillAmount(), + Mathf.Min(1f, m_CurrentProgression), + Time.smoothDeltaTime * 2.5f + ) + ); + + var l_NewDisplayProgression = Mathf.Lerp(m_DisplayedProgression, m_CurrentProgression, Time.smoothDeltaTime * 2.5f); + var l_RemainingSeconds = (int)Mathf.Max(0f, m_CurrentExpire - Time.realtimeSinceStartup); + + if (m_CurrentLevel != m_DisplayedLevel || Mathf.Abs(l_NewDisplayProgression - m_DisplayedProgression) >= 0.0001 || l_RemainingSeconds != m_DisplayedRemaining) + { + m_DisplayedLevel = m_CurrentLevel; + m_DisplayedProgression = l_NewDisplayProgression; + m_DisplayedRemaining = l_RemainingSeconds; + + var l_Minutes = l_RemainingSeconds / 60; + var l_Seconds = l_RemainingSeconds - (l_Minutes * 60); + + m_Label.SetText( + $"LVL {m_DisplayedLevel} - Hype Train!\n" + + $"{Mathf.RoundToInt(m_DisplayedProgression * 100.0f)}% {l_Minutes}:{l_Seconds.ToString().PadLeft(2, '0')}" + ); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On active hype train changed + /// + /// Current hype train + private void HelixAPI_OnActiveHypeTrainChanged(CP_SDK.Chat.Models.Twitch.Helix_HypeTrain p_HypeTrain) + { + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + if (p_HypeTrain != null) + { + var l_HasExpired = p_HypeTrain.event_data.expires_at.AddSeconds(60) < DateTime.UtcNow; + if (l_HasExpired && CurrentScreen && CurrentScreen.gameObject.activeSelf) + CurrentScreen.gameObject.SetActive(false); + else if (!l_HasExpired) + { + if (CurrentScreen && !CurrentScreen.gameObject.activeSelf) + CurrentScreen.gameObject.SetActive(true); + + var l_Progress = p_HypeTrain.event_data.goal == 0 ? 0f : (float)p_HypeTrain.event_data.total / (float)p_HypeTrain.event_data.goal; + + m_CurrentExpire = Time.realtimeSinceStartup + (float)((p_HypeTrain.event_data.expires_at - DateTime.UtcNow).TotalSeconds); + m_CurrentProgression = l_Progress; + m_CurrentLevel = p_HypeTrain.event_data.level; + } + } + + m_LastHypeTrain = p_HypeTrain; + }); + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/UI/ModerationLeft.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationLeftView.cs similarity index 62% rename from Modules/BeatSaberPlus_Chat/UI/ModerationLeft.cs rename to Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationLeftView.cs index c15b4da..0c3180e 100644 --- a/Modules/BeatSaberPlus_Chat/UI/ModerationLeft.cs +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationLeftView.cs @@ -1,33 +1,68 @@ -using BeatSaberMarkupLanguage.Attributes; +using CP_SDK.XUI; using System.Collections.Generic; using System.Linq; -using TMPro; +using UnityEngine.UI; namespace ChatPlexMod_Chat.UI { /// /// Moderation left screen /// - internal class ModerationLeft : BeatSaberPlus.SDK.UI.ResourceViewController + internal sealed class ModerationLeftView : CP_SDK.UI.ViewController { -#pragma warning disable CS0649 - /// - /// Room state text - /// - [UIComponent("RoomStateText")] - private TextMeshProUGUI m_RoomStateText = null; -#pragma warning restore CS0414 + private XUIText m_RoomStateText = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /// - /// On view activation + /// On view creation /// - protected override sealed void OnViewActivation() + protected override void OnViewCreation() { - UpdateRoomState(); + Templates.FullRectLayout( + Templates.TitleBar("Channel Actions"), + + XUIVLayout.Make( + XUIVSpacer.Make(5f), + + XUIText.Make("RoomState: Normal") + .SetAlign(TMPro.TextAlignmentOptions.Center) + .Bind(ref m_RoomStateText), + + XUIVSpacer.Make(5f), + + XUIPrimaryButton.Make("Toggle emote only mode", OnToggleEmoteOnlyModeButton), + XUIPrimaryButton.Make("Toggle follower mode", OnToggleFollowerModeButton), + XUIPrimaryButton.Make("Toggle slow mode", OnToggleSlowModeButton), + + XUIVSpacer.Make(5f), + + XUISecondaryButton.Make("Manage shortcuts", OnManageShortcutButton), + + XUIVSpacer.Make(5f) + ) + .SetWidth(60f) + .SetPadding(0) + .ForEachDirect(y => + { + y.SetHeight(8f); + y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained); + }) + .ForEachDirect(y => + { + y.SetHeight(8f); + y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained); + }) + ) + .SetBackground(true, null, true) + .BuildUI(transform); } + /// + /// On view activation + /// + protected override sealed void OnViewActivation() + => UpdateRoomState(); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -49,19 +84,16 @@ internal void UpdateRoomState() { var l_TwitchChannel = l_Channel.Item2 as CP_SDK.Chat.Models.Twitch.TwitchChannel; - if (l_TwitchChannel.Roomstate.EmoteOnly) - l_States.Add("Emotes only"); - if (l_TwitchChannel.Roomstate.FollowersOnly) - l_States.Add("Followers only"); - if (l_TwitchChannel.Roomstate.SlowModeInterval != 0) - l_States.Add("Slow mode (" + l_TwitchChannel.Roomstate.SlowModeInterval + "s)"); + if (l_TwitchChannel.Roomstate.EmoteOnly) l_States.Add("Emotes only"); + if (l_TwitchChannel.Roomstate.FollowersOnly) l_States.Add("Followers only"); + if (l_TwitchChannel.Roomstate.SlowModeInterval != 0) l_States.Add("Slow mode (" + l_TwitchChannel.Roomstate.SlowModeInterval + "s)"); } } if (l_States.Count == 0) l_States.Add("Normal"); - m_RoomStateText.text = "Room State: " + string.Join(", ", l_States.ToArray()); + m_RoomStateText.SetText("Room State: " + string.Join(", ", l_States.ToArray())); } //////////////////////////////////////////////////////////////////////////// @@ -90,7 +122,6 @@ private bool CheckForModeratorPermissions() /// /// Toggle emote only /// - [UIAction("click-toggle-emote-only-mode-btn-pressed")] private void OnToggleEmoteOnlyModeButton() { if (!CheckForModeratorPermissions()) @@ -102,15 +133,14 @@ private void OnToggleEmoteOnlyModeButton() var l_TwitchChannel = l_Channel.Item2 as CP_SDK.Chat.Models.Twitch.TwitchChannel; if (l_TwitchChannel.Roomstate.EmoteOnly) - ShowConfirmationModal("Do you really want to disable emote only mode?", () => l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/emoteonlyoff")); + ShowConfirmationModal("Do you really want to disable emote only mode?", (x) => { if (x) l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/emoteonlyoff"); }); else - ShowConfirmationModal("Do you really want to enable emote only mode?", () => l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/emoteonly")); + ShowConfirmationModal("Do you really want to enable emote only mode?", (x) => { if (x) l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/emoteonly"); }); } } /// /// Toggle follower mode /// - [UIAction("click-toggle-follower-mode-btn-pressed")] private void OnToggleFollowerModeButton() { if (!CheckForModeratorPermissions()) @@ -122,15 +152,14 @@ private void OnToggleFollowerModeButton() var l_TwitchChannel = l_Channel.Item2 as CP_SDK.Chat.Models.Twitch.TwitchChannel; if (l_TwitchChannel.Roomstate.FollowersOnly) - ShowConfirmationModal("Do you really want to disable follower only mode?", () => l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/followersoff")); + ShowConfirmationModal("Do you really want to disable follower only mode?", (x) => { if (x) l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/followersoff"); }); else - ShowConfirmationModal("Do you really want to enable follower only mode?", () => l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/followers")); + ShowConfirmationModal("Do you really want to enable follower only mode?", (x) => { if (x) l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/followers"); }); } } /// /// Toggle slow mode /// - [UIAction("click-toggle-slow-mode-btn-pressed")] private void OnToggleSlowModeButton() { if (!CheckForModeratorPermissions()) @@ -142,18 +171,17 @@ private void OnToggleSlowModeButton() var l_TwitchChannel = l_Channel.Item2 as CP_SDK.Chat.Models.Twitch.TwitchChannel; if (l_TwitchChannel.Roomstate.SlowModeInterval != 0) - ShowConfirmationModal("Do you really want to disable slow mode?", () => l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/slowoff")); + ShowConfirmationModal("Do you really want to disable slow mode?", (x) => { if (x) l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/slowoff"); }); else - ShowConfirmationModal("Do you really want to enable slow mode?", () => l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/slow")); + ShowConfirmationModal("Do you really want to enable slow mode?", (x) => { if (x) l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/slow"); }); } } /// /// Manage shortcuts /// - [UIAction("click-manage-shortcut-btn-pressed")] private void OnManageShortcutButton() { - ModerationViewFlowCoordinator.Instance().SwitchToShortcut(); + ModerationViewFlowCoordinator.Instance().SwitchToShortcuts(); } } } diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationMainView.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationMainView.cs new file mode 100644 index 0000000..d333c93 --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationMainView.cs @@ -0,0 +1,120 @@ +using CP_SDK.XUI; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Moderation main screen + /// + internal sealed class ModerationMainView : CP_SDK.UI.ViewController + { + /// + /// On view creation + /// + protected override void OnViewCreation() + { + Templates.FullRectLayoutMainView( + Templates.TitleBar("Send message") + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + /// + /// On view activation + /// + protected override void OnViewActivation() + { + ShowKeyboard(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Show keyboard + /// + private void ShowKeyboard() + { + var l_CustomKeys = new List<(string, Action, string)>() + { + (" USERNAME ", () => { + if (ModerationRightView.Instance?.SelectedItem == null) + ShowMessageModal("Please select an user on the right panel!"); + else + InsertTextWithSpace(ModerationRightView.Instance.SelectedItem.User.DisplayName); + }, "#7eaffc") + }; + + /// Add custom keys + var l_ConfigCustomKeys = CConfig.Instance.ModerationKeys; + if (l_ConfigCustomKeys != null && l_ConfigCustomKeys.Count != 0) + { + foreach (var l_Var in l_ConfigCustomKeys) + l_CustomKeys.Add((" " + l_Var + " ", () => ChangePrefix(l_Var), null)); + } + + ShowKeyboardModal("", SendPressed, ShowKeyboard, l_CustomKeys); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Change message prefix + /// + /// New prefix + private void ChangePrefix(string p_Prefix) + { + var l_KeyboardValue = KeyboardModal_GetValue(); + if (p_Prefix.StartsWith("!") || p_Prefix.StartsWith("/")) + { + if (!l_KeyboardValue.StartsWith("/") && !l_KeyboardValue.StartsWith("!")) + KeyboardModal_SetValue(p_Prefix + " " + l_KeyboardValue); + else + { + if (l_KeyboardValue.Contains(' ')) + { + var l_Parts = l_KeyboardValue.Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); + KeyboardModal_SetValue(p_Prefix + " " + string.Join(" ", l_Parts.Skip(1).ToArray())); + } + else + KeyboardModal_Append(p_Prefix + " "); + } + } + else + InsertTextWithSpace(p_Prefix); + } + /// + /// Insert text with space + /// + /// Text to insert + private void InsertTextWithSpace(string p_Text) + { + var l_KeyboardValue = KeyboardModal_GetValue(); + + if (l_KeyboardValue.Length == 0) KeyboardModal_Append(p_Text); + else if (l_KeyboardValue[l_KeyboardValue.Length - 1] != ' ') KeyboardModal_Append(" " + p_Text); + else KeyboardModal_Append(p_Text); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On enter pressed + /// + /// + internal void SendPressed(string p_Text) + { + ShowKeyboard(); + + if (CP_SDK.Chat.Service.Multiplexer.Channels.Count == 0) + return; + + foreach (var l_Channel in CP_SDK.Chat.Service.Multiplexer.Channels) + l_Channel.Item1.SendTextMessage(l_Channel.Item2, p_Text); + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationRightView.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationRightView.cs new file mode 100644 index 0000000..eda7f4e --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationRightView.cs @@ -0,0 +1,204 @@ +using CP_SDK.XUI; +using System.Collections.Generic; +using UnityEngine.UI; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Moderation right view + /// + internal sealed class ModerationRightView : CP_SDK.UI.ViewController + { + private XUIVVList m_List = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private List m_Items = new List(); + private Data.ChatUserListItem m_SelectedItem = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public Data.ChatUserListItem SelectedItem => m_SelectedItem; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Channel Active Users (Last 40 ones)"), + + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(CP_SDK.UI.Data.ListCellPrefabs.Get()) + .OnListItemSelected(OnListItemSelect) + .Bind(ref m_List) + ) + .SetHeight(55) + .SetSpacing(0) + .SetPadding(0) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("TimeOut(10 min)").OnClick(OnTimeOutButton), + XUIPrimaryButton.Make("Ban").OnClick(OnBanButton), + XUIPrimaryButton.Make("Mod").OnClick(OnModButton), + XUIPrimaryButton.Make("UnMod").OnClick(OnUnModButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + /// + /// On view activation + /// + protected override void OnViewActivation() + => Refresh(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Refresh event list + /// + internal void Refresh() + { + m_Items.Clear(); + for (var l_I = 0; l_I < Chat.Instance.LastChatUsers.Count; ++l_I) + m_Items.Add(new Data.ChatUserListItem(Chat.Instance.LastChatUsers[l_I].Item1, Chat.Instance.LastChatUsers[l_I].Item2)); + m_Items.Sort((x, y) => x.User.DisplayName.CompareTo(y.User.DisplayName)); + + m_List.SetListItems(m_Items); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On user selected + /// + /// Selected item + private void OnListItemSelect(CP_SDK.UI.Data.IListItem p_SelectedItem) + => m_SelectedItem = (Data.ChatUserListItem)p_SelectedItem; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// TimeOut an user + /// + private void OnTimeOutButton() + { + if (!EnsureItemSelected() || !EnsurePermissions()) + return; + + ShowConfirmationModal($"Do you really want to TimeOut user\n{m_SelectedItem.User.DisplayName}?", (x) => { + if (!x) + return; + + foreach (var l_Current in CP_SDK.Chat.Service.Multiplexer.Channels) + { + if (l_Current.Item1 is CP_SDK.Chat.Services.Twitch.TwitchService) + l_Current.Item1.SendTextMessage(l_Current.Item2, $"/timeout {m_SelectedItem.User.UserName}"); + } + }); + } + /// + /// Ban an user + /// + private void OnBanButton() + { + if (!EnsureItemSelected() || !EnsurePermissions()) + return; + + ShowConfirmationModal($"Do you really want to Ban user\n{m_SelectedItem.User.DisplayName}?", (x) => { + if (!x) + return; + + foreach (var l_Current in CP_SDK.Chat.Service.Multiplexer.Channels) + { + if (l_Current.Item1 is CP_SDK.Chat.Services.Twitch.TwitchService) + l_Current.Item1.SendTextMessage(l_Current.Item2, $"/ban {m_SelectedItem.User.UserName}"); + } + }); + } + /// + /// Mod an user + /// + private void OnModButton() + { + if (!EnsureItemSelected() || !EnsurePermissions()) + return; + + ShowConfirmationModal($"Do you really want to Mod user\n{m_SelectedItem.User.DisplayName}?", (x) => { + if (!x) + return; + + foreach (var l_Current in CP_SDK.Chat.Service.Multiplexer.Channels) + { + if (l_Current.Item1 is CP_SDK.Chat.Services.Twitch.TwitchService) + m_SelectedItem.Service.SendTextMessage(l_Current.Item2, $"/mod {m_SelectedItem.User.UserName}"); + } + }); + } + /// + /// UnMod an user + /// + private void OnUnModButton() + { + if (!EnsureItemSelected() || !EnsurePermissions()) + return; + + ShowConfirmationModal($"Do you really want to UnMod user\n{m_SelectedItem.User.DisplayName}?", (x) => { + if (!x) + return; + + foreach (var l_Current in CP_SDK.Chat.Service.Multiplexer.Channels) + { + if (l_Current.Item1 is CP_SDK.Chat.Services.Twitch.TwitchService) + l_Current.Item1.SendTextMessage(l_Current.Item2, $"/unmod {m_SelectedItem.User.UserName}"); + } + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Ensure that an shortcut is selected + /// + /// + private bool EnsureItemSelected() + { + if (m_SelectedItem == null) + { + ShowMessageModal("Please select an user first!"); + return false; + } + + return true; + } + /// + /// Ensure permissions + /// + /// + private bool EnsurePermissions() + { + if (!(m_SelectedItem.Service is CP_SDK.Chat.Services.Twitch.TwitchService)) + { + ShowMessageModal("Only twitch is supported at the moment!"); + return false; + } + + return true; + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationShortcutsMainView.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationShortcutsMainView.cs new file mode 100644 index 0000000..3dce2e2 --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationShortcutsMainView.cs @@ -0,0 +1,153 @@ +using CP_SDK.UI.Data; +using CP_SDK.XUI; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.UI; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Moderation shortcuts main view + /// + internal sealed class ModerationShortcutsMainView : CP_SDK.UI.ViewController + { + private XUIVVList m_List = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private List m_Items = new List(); + private TextListItem m_SelectedItem = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override void OnViewCreation() + { + Templates.FullRectLayoutMainView( + Templates.TitleBar("Shortcuts"), + + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(ListCellPrefabs.Get()) + .OnListItemSelected(OnListItemSelect) + .Bind(ref m_List) + ) + .SetHeight(55) + .SetSpacing(0) + .SetPadding(0) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("New").OnClick(OnNewButton), + XUIPrimaryButton.Make("Delete").OnClick(OnDeleteButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + /// + /// On view activation + /// + protected override void OnViewActivation() + => Refresh(); + /// + /// On view deactivation + /// + protected override void OnViewDeactivation() + { + CConfig.Instance.ModerationKeys.Clear(); + for (var l_I = 0; l_I < m_Items.Count; ++l_I) + CConfig.Instance.ModerationKeys.Add(m_Items[l_I].Text); + + CConfig.Instance.Save(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Refresh list + /// + private void Refresh() + { + m_Items.Clear(); + for (var l_I = 0; l_I < CConfig.Instance.ModerationKeys.Count; ++l_I) + m_Items.Add(new TextListItem(CConfig.Instance.ModerationKeys[l_I])); + + m_List.SetListItems(m_Items); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On item selected + /// + /// Selected item + private void OnListItemSelect(IListItem p_ListItem) + => m_SelectedItem = (TextListItem)p_ListItem; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// New shortcut button + /// + private void OnNewButton() + { + ShowKeyboardModal(string.Empty, (x) => + { + if (string.IsNullOrEmpty(x)) + return; + + CConfig.Instance.ModerationKeys.Add(x.Trim()); + Refresh(); + + m_List.SetSelectedListItem(m_Items.LastOrDefault()); + }); + } + /// + /// Delete shortcut button + /// + private void OnDeleteButton() + { + if (!EnsureItemSelected()) + return; + + ShowConfirmationModal($"Do you want to delete shortcut\n\"{m_SelectedItem.Text}\"?", (x) => + { + if (!x) + return; + + CConfig.Instance.ModerationKeys.Remove(m_SelectedItem.Text); + m_Items.Remove(m_SelectedItem); + m_List.RemoveListItem(m_SelectedItem); + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Ensure that an shortcut is selected + /// + /// + private bool EnsureItemSelected() + { + if (m_SelectedItem == null) + { + ShowMessageModal("Please select a shortcut first!"); + return false; + } + + return true; + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationViewFlowCoordinator.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationViewFlowCoordinator.cs new file mode 100644 index 0000000..42cfd5c --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/ModerationViewFlowCoordinator.cs @@ -0,0 +1,82 @@ +using UnityEngine; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Moderation UI flow coordinator + /// + internal sealed class ModerationViewFlowCoordinator : CP_SDK.UI.FlowCoordinator + { + public override string Title => "Chat Moderation"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private ModerationLeftView m_LeftView; + private ModerationMainView m_MainView; + private ModerationRightView m_RightView; + private ModerationShortcutsMainView m_ShortcutsMainView; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public override void Init() + { + m_LeftView = CP_SDK.UI.UISystem.CreateViewController(); + m_MainView = CP_SDK.UI.UISystem.CreateViewController(); + m_RightView = CP_SDK.UI.UISystem.CreateViewController(); + m_ShortcutsMainView = CP_SDK.UI.UISystem.CreateViewController(); + } + /// + /// On destroy + /// + private void OnDestroy() + { + CP_SDK.UI.UISystem.DestroyUI(ref m_ShortcutsMainView); + CP_SDK.UI.UISystem.DestroyUI(ref m_RightView); + CP_SDK.UI.UISystem.DestroyUI(ref m_LeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_MainView); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get initial views controller + /// + /// (Middle, Left, Right) + protected override sealed (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetInitialViewsController() + => (m_MainView, m_LeftView, m_RightView); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Switch to shortcut view + /// + internal void SwitchToShortcuts() + => ChangeViewControllers(m_ShortcutsMainView); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On back button pressed + /// + /// Current main view controller + /// True if the event is catched, false if we should dismiss the flow coordinator + public override sealed bool OnBackButtonPressed(CP_SDK.UI.IViewController p_MainViewController) + { + if (p_MainViewController == m_ShortcutsMainView) + { + ChangeViewControllers(m_MainView, m_LeftView, m_RightView); + return true; + } + + return false; + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/UI/PollFloatingWindow.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/PollFloatingPanelView.cs similarity index 54% rename from Modules/BeatSaberPlus_Chat/UI/PollFloatingWindow.cs rename to Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/PollFloatingPanelView.cs index 6eff001..8bbfeb8 100644 --- a/Modules/BeatSaberPlus_Chat/UI/PollFloatingWindow.cs +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/PollFloatingPanelView.cs @@ -1,80 +1,44 @@ -using BeatSaberMarkupLanguage.Attributes; -using HMUI; -using UnityEngine; +using CP_SDK.XUI; using System.Linq; +using UnityEngine; namespace ChatPlexMod_Chat.UI { /// - /// Poll floating window + /// Poll floating panel view /// - internal class PollFloatingWindow : BeatSaberPlus.SDK.UI.ResourceViewController + internal sealed class PollFloatingPanelView : CP_SDK.UI.ViewController { public static Vector2 SIZE = new Vector2(80, 60); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - private static Color PROGRESSBAR_BACKGROUND = new Color32(36, 36, 36, 255); + private static Color PROGRESSBAR_BACKGROUND = new Color32(36, 36, 36, 255); private static Color PROGRESSBAR_BACKGROUND_WIN = new Color32(64, 254, 153, 255); - private static Color PROGRESSBAR_FILLER = new Color32(68, 68, 78, 255); + private static Color PROGRESSBAR_FILLER = new Color32(68, 68, 78, 255); private static Color PROGRESSBAR_FILLER_WIN = new Color32(56, 219, 138, 255); - private static Color TIME_PROGRESSBAR_BACKGROUND = new Color32(70, 70, 73, 255); + private static Color TIME_PROGRESSBAR_BACKGROUND = new Color32( 70, 70, 73, 255); private static Color TIME_PROGRESSBAR_FILLER = new Color32(164, 115, 251, 255); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// -#pragma warning disable CS0649 - [UIComponent("Subject")] private TMPro.TextMeshProUGUI m_Subject = null; - [UIObject("Option1Frame")] private GameObject m_Option1Frame = null; - [UIObject("Option2Frame")] private GameObject m_Option2Frame = null; - [UIObject("Option3Frame")] private GameObject m_Option3Frame = null; - [UIObject("Option4Frame")] private GameObject m_Option4Frame = null; - [UIObject("Option5Frame")] private GameObject m_Option5Frame = null; - [UIObject("TimeFrame")] private GameObject m_TimeFrame = null; -#pragma warning restore CS0649 + private XUIText m_Subject = null; + private XUIHLayout[] m_ProgressBarsBackground = new XUIHLayout[5] { null, null, null, null, null }; + private XUIHLayout[] m_ProgressBars = new XUIHLayout[5] { null, null, null, null, null }; + private XUIText[] m_ProgressBarsLabels = new XUIText[5] { null, null, null, null, null }; + private XUIHLayout m_TimeProgressBar = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Twitch service instance - /// - private CP_SDK.Chat.Services.Twitch.TwitchService m_TwitchService = null; - /// - /// Latest poll data - /// - private CP_SDK.Chat.Models.Twitch.Helix_Poll m_LastPoll = null; - /// - /// Current poll start real time since startup - /// - private float m_CurrentPollStart = 0f; - /// - /// Current poll end real time since startup - /// - private float m_CurrentPollEnd = 1f; - /// - /// Progress bars background - /// - private UnityEngine.UI.Image[] m_ProgressBarsBackground = new UnityEngine.UI.Image[5] { null, null, null, null, null }; - /// - /// Progress bars filler - /// - private UnityEngine.UI.Image[] m_ProgressBars = new UnityEngine.UI.Image[5] { null, null, null, null, null }; - /// - /// Progress bars target value - /// - private float[] m_ProgressBarsLerp = new float[5] { 0f, 0f, 0f, 0f, 0f }; - /// - /// Progress bars labels - /// - private TMPro.TextMeshProUGUI[] m_m_ProgressBarsLabels = new TMPro.TextMeshProUGUI[5] { null, null, null, null, null }; - /// - /// Time progress bar filler - /// - private UnityEngine.UI.Image m_TimeProgressBar = null; + private CP_SDK.Chat.Services.Twitch.TwitchService m_TwitchService = null; + private CP_SDK.Chat.Models.Twitch.Helix_Poll m_LastPoll = null; + private float m_CurrentPollStart = 0f; + private float m_CurrentPollEnd = 1f; + private float[] m_ProgressBarsLerp = new float[5] { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -84,22 +48,54 @@ internal class PollFloatingWindow : BeatSaberPlus.SDK.UI.ResourceViewController< /// protected override sealed void OnViewCreation() { - /// Update background color - GetComponentInChildren().color = new Color(0f, 0f, 0f, 0.9f); - GetComponentInChildren().material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; - - m_Subject.transform.parent.GetComponent().childControlWidth = false; - m_Subject.transform.parent.GetComponent().childControlHeight = false; - m_Subject.rectTransform.sizeDelta = new Vector2(75, 12); - m_Subject.lineSpacing = -50f; - - SetupFrame(0, m_Option1Frame); - SetupFrame(1, m_Option2Frame); - SetupFrame(2, m_Option3Frame); - SetupFrame(3, m_Option4Frame); - SetupFrame(4, m_Option5Frame); - - SetupFrame(-1, m_TimeFrame); + var l_WhiteSprite = CP_SDK.Unity.SpriteU.CreateFromTextureWithBorders(Texture2D.whiteTexture); + + Templates.FullRectLayout( + Templates.TitleBar("Poll"), + + XUIHLayout.Make( + XUIText.Make("Subject") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + .SetColor(Color.yellow) + .SetFontSize(4.5f) + .SetOverflowMode(TMPro.TextOverflowModes.Ellipsis) + .OnReady(x => x.TMProUGUI.lineSpacing = -50.0f) + .Bind(ref m_Subject) + ) + .SetPadding(0) + .SetHeight(15f) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained), + + BuildOption(l_WhiteSprite, 0), + BuildOption(l_WhiteSprite, 1), + BuildOption(l_WhiteSprite, 2), + BuildOption(l_WhiteSprite, 3), + BuildOption(l_WhiteSprite, 4), + + XUIHLayout.Make( + XUIHLayout.Make() + .SetSpacing(0).SetPadding(0, 2, 0, 2) + .SetHeight(1) + .SetBackground(true, TIME_PROGRESSBAR_FILLER) + .SetBackgroundSprite(l_WhiteSprite, UnityEngine.UI.Image.Type.Filled) + .SetBackgroundFillMethod(UnityEngine.UI.Image.FillMethod.Horizontal) + .SetBackgroundFillAmount(1.0f) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .Bind(ref m_TimeProgressBar) + ) + .SetSpacing(0).SetPadding(0) + .SetBackground(true, TIME_PROGRESSBAR_BACKGROUND) + .SetBackgroundSprite(l_WhiteSprite, UnityEngine.UI.Image.Type.Filled) + .SetBackgroundFillMethod(UnityEngine.UI.Image.FillMethod.Horizontal) + .SetBackgroundFillAmount(1.0f) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childAlignment = TextAnchor.MiddleLeft) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + ) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .BuildUI(transform); CP_SDK.Chat.Service.Acquire(); var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); @@ -115,7 +111,7 @@ protected override sealed void OnViewCreation() protected sealed override void OnViewActivation() { /// Hide by default - gameObject.SetActive(false); + CurrentScreen?.gameObject?.SetActive(false); } /// /// On view destruction @@ -131,6 +127,42 @@ protected sealed override void OnViewDestruction() //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// + /// + /// Build option group + /// + /// White sprite + /// Index of the option block + /// + private IXUIElement BuildOption(Sprite p_WhiteSprite, int p_Index) + { + return XUIHLayout.Make( + XUIHLayout.Make( + XUIText.Make("Option " + (p_Index + 1)) + .SetAlign(TMPro.TextAlignmentOptions.MidlineLeft) + .SetFontSize(4.0f) + .Bind(ref m_ProgressBarsLabels[p_Index]) + ) + .SetSpacing(0).SetPadding(0, 2, 0, 2) + .SetBackground(true, PROGRESSBAR_FILLER) + .SetBackgroundSprite(p_WhiteSprite, UnityEngine.UI.Image.Type.Filled) + .SetBackgroundFillMethod(UnityEngine.UI.Image.FillMethod.Horizontal) + .SetBackgroundFillAmount(0.0f) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .Bind(ref m_ProgressBars[p_Index]) + ) + .SetSpacing(0).SetPadding(0) + .SetBackground(true, PROGRESSBAR_BACKGROUND) + .SetBackgroundSprite(p_WhiteSprite, UnityEngine.UI.Image.Type.Filled) + .SetBackgroundFillMethod(UnityEngine.UI.Image.FillMethod.Horizontal) + .SetBackgroundFillAmount(1.0f) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .Bind(ref m_ProgressBarsBackground[p_Index]); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + /// /// On frame /// @@ -146,13 +178,13 @@ private void Update() var l_NewStart = m_CurrentPollStart + l_Offset; var l_NewEnd = m_CurrentPollEnd + l_Offset; - m_TimeProgressBar.fillAmount = Mathf.Max(0, 1f - (((Time.realtimeSinceStartup + l_Offset) - l_NewStart) / (l_NewEnd - l_NewStart))); + m_TimeProgressBar.SetBackgroundFillAmount(Mathf.Max(0, 1f - (((Time.realtimeSinceStartup + l_Offset) - l_NewStart) / (l_NewEnd - l_NewStart)))); } else - m_TimeProgressBar.fillAmount = Mathf.Max(0, 1f - ((Time.realtimeSinceStartup - m_CurrentPollStart) / (m_CurrentPollEnd - m_CurrentPollStart))); + m_TimeProgressBar.SetBackgroundFillAmount(Mathf.Max(0, 1f - ((Time.realtimeSinceStartup - m_CurrentPollStart) / (m_CurrentPollEnd - m_CurrentPollStart)))); for (int l_I = 0; l_I < m_ProgressBars.Length; ++l_I) - m_ProgressBars[l_I].fillAmount = Mathf.Lerp(m_ProgressBars[l_I].fillAmount, m_ProgressBarsLerp[l_I], Time.smoothDeltaTime * 5f); + m_ProgressBars[l_I].SetBackgroundFillAmount(Mathf.Lerp(m_ProgressBars[l_I].Element.GetBackgroundFillAmount(), m_ProgressBarsLerp[l_I], Time.smoothDeltaTime * 5f)); } } @@ -174,9 +206,9 @@ private void HelixAPI_OnActivePollChanged(CP_SDK.Chat.Models.Twitch.Helix_Poll p && p_Poll.status != CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.ARCHIVED && p_Poll.status != CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.INVALID) { - gameObject.SetActive(true); + CurrentScreen?.gameObject?.SetActive(true); - m_Subject.text = p_Poll.title; + m_Subject.SetText(p_Poll.title); /// Update choices if (p_Poll.choices != null) @@ -185,7 +217,7 @@ private void HelixAPI_OnActivePollChanged(CP_SDK.Chat.Models.Twitch.Helix_Poll p for (int l_I = 0; l_I < p_Poll.choices.Count && l_I < m_ProgressBars.Length; ++l_I) l_TotalVotes += p_Poll.choices[l_I].votes; - if (p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.TERMINATED + if ( p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.TERMINATED || p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.COMPLETED) { var l_Sorted = p_Poll.choices.OrderByDescending(x => x.votes).ToArray(); @@ -215,22 +247,22 @@ private void HelixAPI_OnActivePollChanged(CP_SDK.Chat.Models.Twitch.Helix_Poll p m_CurrentPollStart = Time.realtimeSinceStartup - Mathf.Abs((float)(l_UTCNow - l_UTCStart).TotalSeconds); m_CurrentPollEnd = m_CurrentPollStart + p_Poll.duration; - if (p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.TERMINATED + if ( p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.TERMINATED || p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.COMPLETED) m_CurrentPollEnd = Time.realtimeSinceStartup; } else - gameObject.SetActive(false); + CurrentScreen?.gameObject?.SetActive(false); } else if (p_Poll != null && m_LastPoll != null && p_Poll.id == m_LastPoll.id) { if (p_Poll.choices != null) { int l_TotalVotes = 0; - for (int l_I = 0; l_I < p_Poll.choices.Count && l_I < m_ProgressBars.Length; ++l_I) + for (var l_I = 0; l_I < p_Poll.choices.Count && l_I < m_ProgressBars.Length; ++l_I) l_TotalVotes += p_Poll.choices[l_I].votes; - if (p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.TERMINATED + if ( p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.TERMINATED || p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.COMPLETED) { var l_Sorted = p_Poll.choices.OrderByDescending(x => x.votes).ToArray(); @@ -245,14 +277,14 @@ private void HelixAPI_OnActivePollChanged(CP_SDK.Chat.Models.Twitch.Helix_Poll p } } - if (p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.TERMINATED + if ( p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.TERMINATED || p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.COMPLETED) m_CurrentPollEnd = Time.realtimeSinceStartup; - if (p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.ARCHIVED + if ( p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.ARCHIVED || p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.INVALID || p_Poll.status == CP_SDK.Chat.Models.Twitch.Helix_Poll.Status.MODERATED) - gameObject.SetActive(false); + CurrentScreen?.gameObject?.SetActive(false); } m_LastPoll = p_Poll; @@ -262,40 +294,6 @@ private void HelixAPI_OnActivePollChanged(CP_SDK.Chat.Models.Twitch.Helix_Poll p //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Setup frame for progress bar & text - /// - /// Frame index, -1 for the time frame - /// Frame instance - private void SetupFrame(int p_Index, GameObject p_Frame) - { - var l_Background = p_Frame.AddComponent(); - l_Background.sprite = CP_SDK.Unity.SpriteU.CreateFromTexture(Texture2D.whiteTexture); - l_Background.type = UnityEngine.UI.Image.Type.Filled; - l_Background.fillMethod = UnityEngine.UI.Image.FillMethod.Horizontal; - l_Background.fillAmount = 1f; - l_Background.color = p_Index == -1 ? TIME_PROGRESSBAR_BACKGROUND : PROGRESSBAR_BACKGROUND; - l_Background.material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; - - if (p_Index != -1) - m_ProgressBarsBackground[p_Index] = l_Background; - - var l_Filler = p_Frame.transform.GetChild(0).gameObject.AddComponent(); - l_Filler.sprite = CP_SDK.Unity.SpriteU.CreateFromTexture(Texture2D.whiteTexture); - l_Filler.type = UnityEngine.UI.Image.Type.Filled; - l_Filler.fillMethod = UnityEngine.UI.Image.FillMethod.Horizontal; - l_Filler.fillAmount = p_Index == -1 ? 1f : 0f; - l_Filler.color = p_Index == -1 ? TIME_PROGRESSBAR_FILLER : PROGRESSBAR_FILLER; - l_Filler.material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; - - if (p_Index != -1) - m_ProgressBars[p_Index] = l_Filler; - else - m_TimeProgressBar = l_Filler; - - if (p_Index != -1) - m_m_ProgressBarsLabels[p_Index] = p_Frame.GetComponentInChildren(); - } /// /// Set frame visibility /// @@ -308,13 +306,13 @@ private void SetFrameVisibility(int p_Index, bool p_Visible) if (!p_Visible) { - m_ProgressBarsBackground[p_Index].fillAmount = 0f; - m_ProgressBars[p_Index].fillAmount = 0f; - m_m_ProgressBarsLabels[p_Index].text = " "; + m_ProgressBarsBackground[p_Index].SetBackgroundFillAmount(0.0f); + m_ProgressBars[p_Index].SetBackgroundFillAmount(0.0f); + m_ProgressBarsLabels[p_Index].SetText(" "); } else { - m_ProgressBarsBackground[p_Index].fillAmount = 1f; + m_ProgressBarsBackground[p_Index].SetBackgroundFillAmount(1.0f); } } /// @@ -334,19 +332,19 @@ private void UpdateFrame(int p_Index, bool p_Winner, CP_SDK.Chat.Models.Twitch.H if (p_Winner) { - m_ProgressBarsBackground[p_Index].color = PROGRESSBAR_BACKGROUND_WIN; - m_ProgressBars[p_Index].color = PROGRESSBAR_FILLER_WIN; - m_m_ProgressBarsLabels[p_Index].color = Color.black; + m_ProgressBarsBackground[p_Index].SetBackgroundColor(PROGRESSBAR_BACKGROUND_WIN); + m_ProgressBars[p_Index].SetBackgroundColor(PROGRESSBAR_FILLER_WIN); + m_ProgressBarsLabels[p_Index].SetColor(Color.black); } else { - m_ProgressBarsBackground[p_Index].color = PROGRESSBAR_BACKGROUND; - m_ProgressBars[p_Index].color = PROGRESSBAR_FILLER; - m_m_ProgressBarsLabels[p_Index].color = Color.white; + m_ProgressBarsBackground[p_Index].SetBackgroundColor(PROGRESSBAR_BACKGROUND); + m_ProgressBars[p_Index].SetBackgroundColor(PROGRESSBAR_FILLER); + m_ProgressBarsLabels[p_Index].SetColor(Color.white); } m_ProgressBarsLerp[p_Index] = l_VotePct; - m_m_ProgressBarsLabels[p_Index].text = $"{l_Label}\n{Mathf.RoundToInt(l_VotePct * 100.0f)} % ({p_Choice.votes})"; + m_ProgressBarsLabels[p_Index].SetText($"{l_Label}\n{Mathf.RoundToInt(l_VotePct * 100.0f)} % ({p_Choice.votes})"); } } } diff --git a/Modules/BeatSaberPlus_Chat/UI/PredictionFloatingWindow.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/PredictionFloatingPanelView.cs similarity index 56% rename from Modules/BeatSaberPlus_Chat/UI/PredictionFloatingWindow.cs rename to Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/PredictionFloatingPanelView.cs index d67ac94..dea6124 100644 --- a/Modules/BeatSaberPlus_Chat/UI/PredictionFloatingWindow.cs +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/PredictionFloatingPanelView.cs @@ -1,5 +1,5 @@ -using BeatSaberMarkupLanguage.Attributes; -using HMUI; +using CP_SDK.Unity.Extensions; +using CP_SDK.XUI; using System; using System.Linq; using UnityEngine; @@ -8,79 +8,52 @@ namespace ChatPlexMod_Chat.UI { /// - /// Prediction floating window + /// Prediction floating panel view /// - internal class PredictionFloatingWindow : BeatSaberPlus.SDK.UI.ResourceViewController + internal sealed class PredictionFloatingPanelView : CP_SDK.UI.ViewController { public static Vector2 SIZE = new Vector2(80, 75); - private static Color TIME_PROGRESSBAR_BACKGROUND = new Color32(70, 70, 73, 255); - private static Color TIME_PROGRESSBAR_FILLER = new Color32(164, 115, 251, 255); + private static Color TIME_PROGRESSBAR_BACKGROUND = new Color32( 70, 70, 73, 255); + private static Color TIME_PROGRESSBAR_FILLER = new Color32(164, 115, 251, 255); - private static string TAG_BLUE = "<#" + ColorUtility.ToHtmlStringRGB(new Color32( 56, 122, 255, 255)) + ">"; - private static string TAG_PINK = "<#" + ColorUtility.ToHtmlStringRGB(new Color32(245, 0, 155, 255)) + ">"; + private static string TAG_BLUE = "<" + ColorU.ToHexRGB(new Color32( 56, 122, 255, 255)) + ">"; + private static string TAG_PINK = "<" + ColorU.ToHexRGB(new Color32(245, 0, 155, 255)) + ">"; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// -#pragma warning disable CS0649 - [UIComponent("Subject")] private TMPro.TextMeshProUGUI m_Subject = null; - [UIComponent("Labels")] private TMPro.TextMeshProUGUI m_Labels = null; - [UIComponent("Percentages")] private TMPro.TextMeshProUGUI m_Percentages = null; - [UIComponent("Points")] private TMPro.TextMeshProUGUI m_Points = null; - [UIComponent("Ratios")] private TMPro.TextMeshProUGUI m_Ratios = null; - [UIComponent("Votees")] private TMPro.TextMeshProUGUI m_Votees = null; + private XUIText m_Subject = null; + private XUIText m_Labels = null; + private XUIText m_Percentages = null; + private XUIText m_Points = null; + private XUIText m_Ratios = null; + private XUIText m_Votees = null; - [UIObject("LockButtonFrame")] private GameObject m_LockButtonFrame = null; - [UIComponent("LockButton")] private Button m_LockButton = null; + private XUIHLayout m_LockButtonFrame = null; + private XUIPrimaryButton m_LockButton = null; - [UIObject("PickButtons")] private GameObject m_PickButtons = null; - [UIComponent("PickBlueButton")] private Button m_PickBlueButton = null; - [UIComponent("PickPinkButton")] private Button m_PickPinkButton = null; - [UIComponent("CancelButton")] private Button m_CancelButton = null; - [UIObject("TimeFrame")] private GameObject m_TimeFrame = null; -#pragma warning restore CS0649 + private XUIHLayout m_PickButtonFrame = null; + private XUIPrimaryButton m_PickButtonBlue = null; + private XUIPrimaryButton m_PickButtonPink = null; + + private XUIHLayout m_CancelButtonFrame = null; + private XUISecondaryButton m_CancelButton = null; + + private XUIHLayout m_TimeProgressBar = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Twitch service instance - /// - private CP_SDK.Chat.Services.Twitch.TwitchService m_TwitchService = null; - /// - /// Latest prediction data - /// - private CP_SDK.Chat.Models.Twitch.Helix_Prediction m_LastPrediction = null; - /// - /// Blue target % - /// - private float m_BluePctTarget = 0f; - /// - /// Blue displayed % - /// - private float m_BluePctDisplayed = 0f; - /// - /// Pink target % - /// - private float m_PinkPctTarget = 0f; - /// - /// Pink displayed % - /// - private float m_PinkPctDisplayed = 0f; - /// - /// Betting window start RealmTime - /// - private float m_WindowStart = 0f; - /// - /// Betting window end RealmTime - /// - private float m_WindowEnd = 1f; - /// - /// Time progress bar filler - /// - private UnityEngine.UI.Image m_TimeProgressBar = null; + private CP_SDK.Chat.Services.Twitch.TwitchService m_TwitchService = null; + private CP_SDK.Chat.Models.Twitch.Helix_Prediction m_LastPrediction = null; + private float m_BluePctTarget = 0f; + private float m_BluePctDisplayed = 0f; + private float m_PinkPctTarget = 0f; + private float m_PinkPctDisplayed = 0f; + private float m_WindowStart = 0f; + private float m_WindowEnd = 1f; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -90,16 +63,80 @@ internal class PredictionFloatingWindow : BeatSaberPlus.SDK.UI.ResourceViewContr /// protected override sealed void OnViewCreation() { - /// Update background color - GetComponentInChildren().color = new Color(0f, 0f, 0f, 0.9f); - GetComponentInChildren().material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; - - m_Subject.transform.parent.GetComponent().childControlWidth = false; - m_Subject.transform.parent.GetComponent().childControlHeight = false; - m_Subject.rectTransform.sizeDelta = new Vector2(75, 12); - m_Subject.lineSpacing = -50f; - - SetupTimeFrame(); + var l_WhiteSprite = CP_SDK.Unity.SpriteU.CreateFromTextureWithBorders(Texture2D.whiteTexture); + + Templates.FullRectLayout( + Templates.TitleBar("Prediction"), + + XUIHLayout.Make( + XUIText.Make("Subject") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + .SetColor(Color.yellow) + .SetFontSize(4.5f) + .SetOverflowMode(TMPro.TextOverflowModes.Ellipsis) + .OnReady(x => x.TMProUGUI.lineSpacing = -50.0f) + .Bind(ref m_Subject) + ) + .SetPadding(0) + .SetHeight(15f) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true) + .OnReady(x => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained), + + XUIText.Make("Option 1").SetFontSize(5.5f).Bind(ref m_Labels), + XUIText.Make("Option 1").SetFontSize(6.0f).SetStyle(TMPro.FontStyles.Bold).Bind(ref m_Percentages), + XUIText.Make("Option 1").SetFontSize(4.0f).Bind(ref m_Points), + XUIText.Make("Option 1").SetFontSize(4.0f).Bind(ref m_Ratios), + XUIText.Make("Option 1").SetFontSize(4.0f).Bind(ref m_Votees), + + XUIHLayout.Make( + XUIPrimaryButton.Make("Lock votes", OnLockButton).Bind(ref m_LockButton) + ) + .SetPadding(0) + .OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained) + .ForEachDirect((y) => y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained)) + .Bind(ref m_LockButtonFrame), + + XUIHLayout.Make( + XUIPrimaryButton.Make("Pick as winner", OnPickBlueButton).Bind(ref m_PickButtonBlue), + XUIPrimaryButton.Make("Pick as winner", OnPickPinkButton).Bind(ref m_PickButtonPink) + ) + .SetPadding(0) + .OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained) + .ForEachDirect((y) => y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained)) + .Bind(ref m_PickButtonFrame), + + XUIHLayout.Make( + XUISecondaryButton.Make("Cancel", OnCancelButton).Bind(ref m_CancelButton) + ) + .SetPadding(0) + .OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained) + .ForEachDirect((y) => y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained)) + .Bind(ref m_CancelButtonFrame), + + XUIHLayout.Make( + XUIHLayout.Make() + .SetSpacing(0).SetPadding(0, 2, 0, 2) + .SetHeight(1) + .SetBackground(true, TIME_PROGRESSBAR_FILLER) + .SetBackgroundSprite(l_WhiteSprite, Image.Type.Filled) + .SetBackgroundFillMethod(Image.FillMethod.Horizontal) + .SetBackgroundFillAmount(1.0f) + .OnReady(x => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained) + .Bind(ref m_TimeProgressBar) + ) + .SetSpacing(0).SetPadding(0) + .SetBackground(true, TIME_PROGRESSBAR_BACKGROUND) + .SetBackgroundSprite(l_WhiteSprite, Image.Type.Filled) + .SetBackgroundFillMethod(Image.FillMethod.Horizontal) + .SetBackgroundFillAmount(1.0f) + .OnReady(x => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childAlignment = TextAnchor.MiddleLeft) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + ) + .SetSpacing(0) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .BuildUI(transform); CP_SDK.Chat.Service.Acquire(); var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); @@ -115,7 +152,7 @@ protected override sealed void OnViewCreation() protected sealed override void OnViewActivation() { /// Hide by default - gameObject.SetActive(false); + CurrentScreen?.gameObject?.SetActive(false); } /// /// On view destruction @@ -143,7 +180,7 @@ private void Update() || m_LastPrediction.status == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.CANCELED; if (l_HasExpired) - gameObject.SetActive(false); + CurrentScreen?.gameObject?.SetActive(false); else { if (m_LastPrediction.status == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.ACTIVE @@ -155,10 +192,10 @@ private void Update() var l_NewStart = m_WindowStart + l_Offset; var l_NewEnd = m_WindowEnd + l_Offset; - m_TimeProgressBar.fillAmount = Mathf.Max(0, 1f - (((Time.realtimeSinceStartup + l_Offset) - l_NewStart) / (l_NewEnd - l_NewStart))); + m_TimeProgressBar.SetBackgroundFillAmount(Mathf.Max(0, 1f - (((Time.realtimeSinceStartup + l_Offset) - l_NewStart) / (l_NewEnd - l_NewStart)))); } else - m_TimeProgressBar.fillAmount = Mathf.Max(0, 1f - ((Time.realtimeSinceStartup - m_WindowStart) / (m_WindowEnd - m_WindowStart))); + m_TimeProgressBar.SetBackgroundFillAmount(Mathf.Max(0, 1f - ((Time.realtimeSinceStartup - m_WindowStart) / (m_WindowEnd - m_WindowStart)))); } var l_NewBluePct = Mathf.Lerp(m_BluePctDisplayed, m_BluePctTarget, Time.smoothDeltaTime * 2.5f); @@ -169,51 +206,25 @@ private void Update() m_BluePctDisplayed = l_NewBluePct; m_PinkPctDisplayed = l_NewPinkPct; - m_Percentages.text = $"{TAG_BLUE}{Mathf.RoundToInt(l_NewBluePct * 100f)}%\n {TAG_PINK}{Mathf.RoundToInt(m_PinkPctDisplayed * 100f)}%"; + m_Percentages.SetText($"{TAG_BLUE}{Mathf.RoundToInt(l_NewBluePct * 100f)}%\n {TAG_PINK}{Mathf.RoundToInt(m_PinkPctDisplayed * 100f)}%"); } } } } - - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Setup frame for progress bar & text - /// - private void SetupTimeFrame() - { - var l_Background = m_TimeFrame.AddComponent(); - l_Background.sprite = CP_SDK.Unity.SpriteU.CreateFromTexture(Texture2D.whiteTexture); - l_Background.type = Image.Type.Filled; - l_Background.fillMethod = Image.FillMethod.Horizontal; - l_Background.fillAmount = 1f; - l_Background.color = TIME_PROGRESSBAR_BACKGROUND; - l_Background.material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; - - var l_Filler = m_TimeFrame.transform.GetChild(0).gameObject.AddComponent(); - l_Filler.sprite = CP_SDK.Unity.SpriteU.CreateFromTexture(Texture2D.whiteTexture); - l_Filler.type = Image.Type.Filled; - l_Filler.fillMethod = Image.FillMethod.Horizontal; - l_Filler.fillAmount = 1f; - l_Filler.color = TIME_PROGRESSBAR_FILLER; - l_Filler.material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; - m_TimeProgressBar = l_Filler; - } - //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /// /// Lock votes /// - [UIAction("click-lock")] private void OnLockButton() { - ShowConfirmationModal("Lock the votes?", () => + ShowConfirmationModal("Lock the votes?", (p_Confirm) => { - m_LockButton.interactable = false; + if (!p_Confirm) + return; + + m_LockButton.SetInteractable(false); m_TwitchService.HelixAPI.EndPrediction(m_LastPrediction.id, CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.LOCKED, null, (_, x) => { @@ -224,13 +235,15 @@ private void OnLockButton() /// /// Pick blue /// - [UIAction("click-pick-blue")] private void OnPickBlueButton() { - ShowConfirmationModal("Make blue win?", () => + ShowConfirmationModal("Make blue win?", (p_Confirm) => { - m_PickBlueButton.interactable = false; - m_PickPinkButton.interactable = false; + if (!p_Confirm) + return; + + m_PickButtonBlue.SetInteractable(false); + m_PickButtonPink.SetInteractable(false); var l_Winner = m_LastPrediction.outcomes.FirstOrDefault(x => x.color == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Color.BLUE)?.id ?? null; m_TwitchService.HelixAPI.EndPrediction(m_LastPrediction.id, CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.RESOLVED, l_Winner, (_, x) => @@ -242,13 +255,15 @@ private void OnPickBlueButton() /// /// Pick pink /// - [UIAction("click-pick-pink")] private void OnPickPinkButton() { - ShowConfirmationModal("Make pink win?", () => + ShowConfirmationModal("Make pink win?", (p_Confirm) => { - m_PickBlueButton.interactable = false; - m_PickPinkButton.interactable = false; + if (!p_Confirm) + return; + + m_PickButtonBlue.SetInteractable(false); + m_PickButtonPink.SetInteractable(false); var l_Winner = m_LastPrediction.outcomes.FirstOrDefault(x => x.color == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Color.PINK)?.id ?? null; m_TwitchService.HelixAPI.EndPrediction(m_LastPrediction.id, CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.RESOLVED, l_Winner, (_, x) => @@ -260,12 +275,14 @@ private void OnPickPinkButton() /// /// Cancel prediction /// - [UIAction("click-cancel")] private void OnCancelButton() { - ShowConfirmationModal("Cancel prediction?", () => + ShowConfirmationModal("Cancel prediction?", (p_Confirm) => { - m_CancelButton.interactable = false; + if (!p_Confirm) + return; + + m_CancelButton.SetInteractable(false); m_TwitchService.HelixAPI.EndPrediction(m_LastPrediction.id, CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.CANCELED, null, (_, x) => { HelixAPI_OnActivePredictionChanged(x); @@ -292,8 +309,8 @@ private void HelixAPI_OnActivePredictionChanged(CP_SDK.Chat.Models.Twitch.Helix_ { CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => { - if (gameObject.activeSelf) - gameObject.SetActive(false); + if (CurrentScreen?.gameObject?.activeSelf ?? false) + CurrentScreen?.gameObject?.SetActive(false); m_WindowEnd = Time.realtimeSinceStartup; m_LastPrediction = p_Prediction; @@ -315,13 +332,13 @@ private void HelixAPI_OnActivePredictionChanged(CP_SDK.Chat.Models.Twitch.Helix_ CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => { - if (!gameObject.activeSelf) + if (!CurrentScreen?.gameObject?.activeSelf ?? false) { - gameObject.SetActive(true); + CurrentScreen?.gameObject?.SetActive(true); - m_LockButton.interactable = true; - m_PickBlueButton.interactable = true; - m_PickPinkButton.interactable = true; + m_LockButton.SetInteractable(true); + m_PickButtonBlue.SetInteractable(true); + m_PickButtonPink.SetInteractable(true); } if (m_LastPrediction == null || m_LastPrediction.id != p_Prediction.id) @@ -333,9 +350,9 @@ private void HelixAPI_OnActivePredictionChanged(CP_SDK.Chat.Models.Twitch.Helix_ if (l_LeftTitle.Length > l_MaxTitle) l_LeftTitle = l_LeftTitle.Substring(0, l_MaxTitle - 3) + "..."; if (l_RightTitle.Length > l_MaxTitle) l_RightTitle = l_RightTitle.Substring(0, l_MaxTitle - 3) + "..."; - m_Subject.text = p_Prediction.title; - m_Labels.text = $"{TAG_BLUE}{l_LeftTitle}\n{TAG_PINK}{l_RightTitle}"; - m_Percentages.text = $"{TAG_BLUE}0%\n {TAG_PINK}0%"; + m_Subject .SetText(p_Prediction.title); + m_Labels .SetText($"{TAG_BLUE}{l_LeftTitle}\n{TAG_PINK}{l_RightTitle}"); + m_Percentages .SetText($"{TAG_BLUE}0%\n {TAG_PINK}0%"); m_BluePctDisplayed = 0f; m_PinkPctDisplayed = 0f; @@ -359,41 +376,38 @@ private void HelixAPI_OnActivePredictionChanged(CP_SDK.Chat.Models.Twitch.Helix_ m_BluePctDisplayed = m_BluePctTarget; m_PinkPctDisplayed = m_PinkPctTarget; - m_Labels.text = $"{l_LeftPrefix}{l_LeftTitle}\n{l_RightPrefix}{l_RightTitle}"; - m_Percentages.text = $"{l_LeftPrefix}{Mathf.RoundToInt(m_BluePctTarget * 100f)}%\n {l_RightPrefix}{Mathf.RoundToInt(m_PinkPctTarget * 100f)}%"; + m_Labels .SetText($"{l_LeftPrefix}{l_LeftTitle}\n{l_RightPrefix}{l_RightTitle}"); + m_Percentages.SetText($"{l_LeftPrefix}{Mathf.RoundToInt(m_BluePctTarget * 100f)}%\n {l_RightPrefix}{Mathf.RoundToInt(m_PinkPctTarget * 100f)}%"); l_Points = $"{l_LeftPrefix}⭕ {p_Prediction.outcomes[0].channel_points}\n{l_RightPrefix}{p_Prediction.outcomes[1].channel_points} ⭕"; l_Ratios = $"{l_LeftPrefix}🏆 {l_BlueRatio}\n{l_RightPrefix}{l_PinkRatio} 🏆"; l_Votees = $"{l_LeftPrefix}👥 {p_Prediction.outcomes[0].users}\n{l_RightPrefix}{p_Prediction.outcomes[1].users} 👥"; } - m_Points.text = l_Points; - m_Ratios.text = l_Ratios; - m_Votees.text = l_Votees; + m_Points.SetText(l_Points); + m_Ratios.SetText(l_Ratios); + m_Votees.SetText(l_Votees); m_LockButtonFrame.SetActive(p_Prediction.status == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.ACTIVE); - m_PickButtons.SetActive(p_Prediction.status == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.LOCKED - || p_Prediction.status == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.RESOLVED); + m_PickButtonFrame.SetActive(p_Prediction.status == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.LOCKED + || p_Prediction.status == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.RESOLVED); - m_CancelButton.interactable = p_Prediction.status != CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.RESOLVED; + m_CancelButton.SetInteractable(p_Prediction.status != CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.RESOLVED); m_WindowStart = Time.realtimeSinceStartup - Mathf.Abs((float)(DateTime.UtcNow - p_Prediction.created_at).TotalSeconds); m_WindowEnd = m_WindowStart + p_Prediction.prediction_window; if (p_Prediction.status == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.LOCKED) - m_TimeProgressBar.fillAmount = 0f; + m_TimeProgressBar.SetBackgroundFillAmount(0.0f); else if (p_Prediction.status == CP_SDK.Chat.Models.Twitch.Helix_Prediction.Status.RESOLVED) { - m_PickBlueButton.interactable = false; - m_PickPinkButton.interactable = false; + m_PickButtonBlue.SetInteractable(false); + m_PickButtonPink.SetInteractable(false); m_WindowStart = Time.realtimeSinceStartup; m_WindowEnd = Time.realtimeSinceStartup + 60f; } - if (m_LastPrediction == null || m_LastPrediction.status != p_Prediction.status) - UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(m_PickButtons.transform.parent.transform as RectTransform); - m_LastPrediction = p_Prediction; }); } @@ -402,8 +416,8 @@ private void HelixAPI_OnActivePredictionChanged(CP_SDK.Chat.Models.Twitch.Helix_ { CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => { - if (gameObject.activeSelf) - gameObject.SetActive(false); + if (CurrentScreen?.gameObject?.activeSelf ?? false) + CurrentScreen?.gameObject?.SetActive(false); m_LastPrediction = p_Prediction; m_WindowEnd = Time.realtimeSinceStartup; diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsLeftView.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsLeftView.cs new file mode 100644 index 0000000..a1462c0 --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsLeftView.cs @@ -0,0 +1,113 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Setting left view + /// + internal sealed class SettingsLeftView : CP_SDK.UI.ViewController + { + private static readonly string s_InformationStr = + "Thanks to brian for original ChatCore lib" + + "\n" + " - https://github.com/brian91292/ChatCore"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Information / Credits"), + + Templates.ScrollableInfos(50, + XUIText.Make(s_InformationStr) + .SetAlign(TMPro.TextAlignmentOptions.Left) + ), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("Reset", OnResetButton), + XUIPrimaryButton.Make("Reset Position", OnResetPositionButton), + XUIPrimaryButton.Make("Web Configuration", OnWebConfigurationButton) + ), + Templates.ExpandedButtonsLine( + XUISecondaryButton.Make("Documentation", OnDocumentationButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset button + /// + private void OnResetButton() + { + ShowConfirmationModal("Do you really want to reset all chat settings?", (x) => + { + if (!x) + return; + + /// Reset settings + CConfig.Instance.Reset(); + CConfig.Instance.Enabled = true; + CConfig.Instance.Save(); + + /// Refresh values + SettingsMainView.Instance.RefreshSettings(); + SettingsRightView.Instance.RefreshSettings(); + + /// Update floating view + Chat.Instance.UpdateFloatingPanels(); + }); + } + /// + /// Reset position button + /// + private void OnResetPositionButton() + { + ShowConfirmationModal("Do you really want to reset chat position?", (x) => + { + if (!x) + return; + + /// Reset position settings + CConfig.Instance.ResetPosition(); + CConfig.Instance.Save(); + + /// Refresh values + SettingsMainView.Instance.RefreshSettings(); + SettingsRightView.Instance.RefreshSettings(); + + /// Update floating view + Chat.Instance.UpdateFloatingPanels(); + }); + } + /// + /// Open web configuration button + /// + private void OnWebConfigurationButton() + { + ShowMessageModal("URL opened in your web browser."); + CP_SDK.Chat.Service.OpenWebConfiguration(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Documentation button + /// + private void OnDocumentationButton() + { + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL(Chat.Instance.DocumentationURL); + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsMainView.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsMainView.cs new file mode 100644 index 0000000..592f3a5 --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsMainView.cs @@ -0,0 +1,157 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Settings main view + /// + internal sealed class SettingsMainView : CP_SDK.UI.ViewController + { + public XUISlider m_ChatWidth; + public XUISlider m_ChatHeight; + private XUISlider m_ChatFontSize; + private XUIToggle m_ChatReverse; + private XUIToggle m_ChatPlatformOriginColor; + + private XUIColorInput m_ChatBackgroundColor; + private XUIColorInput m_ChatHighlightColor; + private XUIColorInput m_ChatTextColor; + private XUIColorInput m_ChatPingColor; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private bool m_PreventChanges = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override void OnViewCreation() + { + var l_Config = CConfig.Instance; + + Templates.FullRectLayoutMainView( + Templates.TitleBar("Chat | Settings"), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Width"), + XUISlider.Make().SetMinValue(80.0f).SetMaxValue(300.0f).SetIncrements(1.0f).SetInteger(true).SetValue(l_Config.ChatSize.x).Bind(ref m_ChatWidth), + + XUIText.Make("Height"), + XUISlider.Make().SetMinValue(80.0f).SetMaxValue(300.0f).SetIncrements(1.0f).SetInteger(true).SetValue(l_Config.ChatSize.y).Bind(ref m_ChatHeight), + + XUIText.Make("Font size"), + XUISlider.Make().SetMinValue(1.0f).SetMaxValue(10.0f).SetIncrements(0.1f).SetValue(l_Config.FontSize).Bind(ref m_ChatFontSize), + + XUIText.Make("Reverse chat order"), + XUIToggle.Make().SetValue(l_Config.ReverseChatOrder).Bind(ref m_ChatReverse), + + XUIText.Make("Show platform origin color"), + XUIToggle.Make().SetValue(l_Config.PlatformOriginColor).Bind(ref m_ChatPlatformOriginColor) + ) + .SetSpacing(1) + .SetWidth(40.0f) + .OnReady(x => x.HOrVLayoutGroup.childAlignment = UnityEngine.TextAnchor.UpperCenter) + .ForEachDirect(x => x.SetAlign(TMPro.TextAlignmentOptions.Center)) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())), + + XUIVLayout.Make( + XUIText.Make("Background color"), + XUIColorInput.Make() + .SetAlphaSupport(true).SetValue(l_Config.BackgroundColor) + .Bind(ref m_ChatBackgroundColor), + + XUIText.Make("Highlight color"), + XUIColorInput.Make() + .SetAlphaSupport(true).SetValue(l_Config.HighlightColor) + .Bind(ref m_ChatHighlightColor), + + XUIText.Make("Text color"), + XUIColorInput.Make() + .SetValue(l_Config.TextColor) + .Bind(ref m_ChatTextColor), + + XUIText.Make("Ping color"), + XUIColorInput.Make() + .SetAlphaSupport(true).SetValue(l_Config.PingColor) + .Bind(ref m_ChatPingColor) + ) + .SetSpacing(1) + .SetWidth(40.0f) + .ForEachDirect(x => x.SetAlign(TMPro.TextAlignmentOptions.Center)) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())) + ) + .SetSpacing(10f) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + /// + /// On view deactivation + /// + protected override void OnViewDeactivation() + { + CConfig.Instance.Save(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When settings are changed + /// + private void OnSettingChanged() + { + if (m_PreventChanges) + return; + + var l_Config = CConfig.Instance; + + /// Update config + l_Config.ChatSize = new Vector2((int)m_ChatWidth.Element.GetValue(), (int)m_ChatHeight.Element.GetValue()); + l_Config.FontSize = m_ChatFontSize.Element.GetValue(); + l_Config.ReverseChatOrder = m_ChatReverse.Element.GetValue(); + l_Config.PlatformOriginColor = m_ChatPlatformOriginColor.Element.GetValue(); + + l_Config.BackgroundColor = m_ChatBackgroundColor.Element.GetValue(); + l_Config.HighlightColor = m_ChatHighlightColor.Element.GetValue(); + l_Config.TextColor = m_ChatTextColor.Element.GetValue(); + l_Config.PingColor = m_ChatPingColor.Element.GetValue(); + + /// Update floating view + Chat.Instance.UpdateFloatingPanels(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset settings + /// + internal void RefreshSettings() + { + m_PreventChanges = true; + + var l_Config = CConfig.Instance; + + m_ChatWidth .SetValue(l_Config.ChatSize.x); + m_ChatHeight .SetValue(l_Config.ChatSize.y); + m_ChatReverse .SetValue(l_Config.ReverseChatOrder); + m_ChatPlatformOriginColor.SetValue(l_Config.PlatformOriginColor); + m_ChatFontSize .SetValue(l_Config.FontSize); + + m_ChatBackgroundColor .SetValue(l_Config.BackgroundColor); + m_ChatHighlightColor .SetValue(l_Config.HighlightColor); + m_ChatTextColor .SetValue(l_Config.TextColor); + m_ChatPingColor .SetValue(l_Config.PingColor); + + m_PreventChanges = false; + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsRightView.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsRightView.cs new file mode 100644 index 0000000..be04642 --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/SettingsRightView.cs @@ -0,0 +1,139 @@ +using CP_SDK.XUI; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Settings right view + /// + internal sealed class SettingsRightView : CP_SDK.UI.ViewController + { + private XUIToggle m_AlignWithFloor; + private XUIToggle m_ShowLockIcon; + private XUIToggle m_FollowEnvironementRotations; + private XUIToggle m_ChatViewerCount; + private XUIToggle m_ChatFitlerViewers; + + private XUIToggle m_FollowEvents; + private XUIToggle m_SubscriptionEvents; + private XUIToggle m_BitsCheering; + private XUIToggle m_ChannelPoints; + private XUIToggle m_ChatFilterBroadcaster; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private bool m_PreventChanges = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + var l_Config = CConfig.Instance; + + Templates.FullRectLayout( + Templates.TitleBar("Filters"), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Align with floor on move"), + XUIToggle.Make().SetValue(l_Config.AlignWithFloor).Bind(ref m_AlignWithFloor), + XUIText.Make("Show lock icon for movement"), + XUIToggle.Make().SetValue(l_Config.ShowLockIcon).Bind(ref m_ShowLockIcon), + XUIText.Make("Follow environment rotations"), + XUIToggle.Make().SetValue(l_Config.FollowEnvironementRotation).Bind(ref m_FollowEnvironementRotations), + XUIText.Make("Show viewer count"), + XUIToggle.Make().SetValue(l_Config.ShowViewerCount).Bind(ref m_ChatViewerCount), + XUIText.Make("Filter viewers commands"), + XUIToggle.Make().SetValue(l_Config.FilterViewersCommands).Bind(ref m_ChatFitlerViewers) + ) + .SetSpacing(1) + .SetPadding(2) + .SetWidth(50) + .ForEachDirect( x => x.SetAlign(TMPro.TextAlignmentOptions.Midline)) + .ForEachDirect(x => x.OnValueChanged(_ => OnValueChanged())), + + XUIVLayout.Make( + XUIText.Make("Show follow events"), + XUIToggle.Make().SetValue(l_Config.ShowFollowEvents).Bind(ref m_FollowEvents), + XUIText.Make("Show subscription events"), + XUIToggle.Make().SetValue(l_Config.ShowSubscriptionEvents).Bind(ref m_SubscriptionEvents), + XUIText.Make("Show bits cheering events"), + XUIToggle.Make().SetValue(l_Config.ShowBitsCheeringEvents).Bind(ref m_BitsCheering), + XUIText.Make("Show channel points event"), + XUIToggle.Make().SetValue(l_Config.ShowChannelPointsEvent).Bind(ref m_ChannelPoints), + XUIText.Make("Filter broadcaster commands"), + XUIToggle.Make().SetValue(l_Config.FilterBroadcasterCommands).Bind(ref m_ChatFilterBroadcaster) + ) + .SetSpacing(1) + .SetPadding(2) + .SetWidth(50) + .ForEachDirect( x => x.SetAlign(TMPro.TextAlignmentOptions.Midline)) + .ForEachDirect(x => x.OnValueChanged(_ => OnValueChanged())) + ) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When settings are changed + /// + private void OnValueChanged() + { + if (m_PreventChanges) + return; + + var l_Config = CConfig.Instance; + + l_Config.AlignWithFloor = m_AlignWithFloor.Element.GetValue(); + l_Config.ShowLockIcon = m_ShowLockIcon.Element.GetValue(); + l_Config.FollowEnvironementRotation = m_FollowEnvironementRotations.Element.GetValue(); + l_Config.ShowViewerCount = m_ChatViewerCount.Element.GetValue(); + l_Config.FilterViewersCommands = m_ChatFitlerViewers.Element.GetValue(); + + l_Config.ShowFollowEvents = m_FollowEvents.Element.GetValue(); + l_Config.ShowSubscriptionEvents = m_SubscriptionEvents.Element.GetValue(); + l_Config.ShowBitsCheeringEvents = m_BitsCheering.Element.GetValue(); + l_Config.ShowChannelPointsEvent = m_ChannelPoints.Element.GetValue(); + l_Config.FilterBroadcasterCommands = m_ChatFilterBroadcaster.Element.GetValue(); + + /// Update floating view + Chat.Instance.UpdateFloatingPanels(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset settings + /// + internal void RefreshSettings() + { + m_PreventChanges = true; + + var l_Config = CConfig.Instance; + + m_AlignWithFloor .SetValue(l_Config.AlignWithFloor); + m_ShowLockIcon .SetValue(l_Config.ShowLockIcon); + m_FollowEnvironementRotations .SetValue(l_Config.FollowEnvironementRotation); + m_ChatViewerCount .SetValue(l_Config.ShowViewerCount); + m_ChatFitlerViewers .SetValue(l_Config.FilterViewersCommands); + + m_FollowEvents .SetValue(l_Config.ShowFollowEvents); + m_SubscriptionEvents .SetValue(l_Config.ShowSubscriptionEvents); + m_BitsCheering .SetValue(l_Config.ShowBitsCheeringEvents); + m_ChannelPoints .SetValue(l_Config.ShowChannelPointsEvent); + m_ChatFilterBroadcaster .SetValue(l_Config.FilterBroadcasterCommands); + + m_PreventChanges = false; + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/StatusFloatingPanelView.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/StatusFloatingPanelView.cs new file mode 100644 index 0000000..98e596f --- /dev/null +++ b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/UI/StatusFloatingPanelView.cs @@ -0,0 +1,118 @@ +using CP_SDK.Chat.Interfaces; +using CP_SDK.XUI; +using System.Collections.Concurrent; +using System.Reflection; +using UnityEngine; + +namespace ChatPlexMod_Chat.UI +{ + /// + /// Status floating panel view + /// + internal sealed class StatusFloatingPanelView : CP_SDK.UI.ViewController + { + public static Vector2 SIZE = new Vector2(25, 8); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUIText m_Text; + + private ConcurrentDictionary m_ChannelsLiveStatuses = new ConcurrentDictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override void OnViewCreation() + { + var l_Assembly = Assembly.GetExecutingAssembly(); + var l_ViewerIconSprite = CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromRelPath(l_Assembly, "ChatPlexMod_Chat.Resources.ViewerIcon.png")); + + XUIHLayout.Make( + XUIImage.Make(l_ViewerIconSprite) + .SetWidth(8f).SetHeight(8f), + + XUIText.Make("Offline") + .SetFontSize(5f) + .Bind(ref m_Text) + ) + .SetPadding(0).SetSpacing(0) + .OnReady(x => { + x.CSizeFitter.enabled = false; + x.HLayoutGroup.childAlignment = TextAnchor.MiddleLeft; + }) + .BuildUI(transform); + + CP_SDK.Chat.Service.Acquire(); + CP_SDK.Chat.Service.Multiplexer.OnLiveStatusUpdated += Mutiplixer_OnLiveStatusUpdated; + } + /// + /// On view activation + /// + protected override void OnViewActivation() + { + m_ChannelsLiveStatuses.Clear(); + UpdateViewerCount(false, 0); + } + /// + /// On view destruction + /// + protected sealed override void OnViewDestruction() + { + if (CP_SDK.Chat.Service.Multiplexer != null) + CP_SDK.Chat.Service.Multiplexer.OnLiveStatusUpdated -= Mutiplixer_OnLiveStatusUpdated; + CP_SDK.Chat.Service.Release(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On room video playback updated + /// + /// Chat service + /// Channel instance + /// Is the stream up + /// Viewer count + private void Mutiplixer_OnLiveStatusUpdated(IChatService p_ChatService, IChatChannel p_Channel, bool p_StreamUP, int p_ViewerCount) + { + var l_Key = "[" + p_ChatService.DisplayName + "]_" + p_Channel.Name.ToLower(); + + if (!m_ChannelsLiveStatuses.ContainsKey(l_Key)) + m_ChannelsLiveStatuses.TryAdd(l_Key, (p_StreamUP, p_ViewerCount)); + else + m_ChannelsLiveStatuses[l_Key] = (p_StreamUP, p_ViewerCount); + + var l_ShowUp = false; + var l_SumViewers = 0; + + foreach (var l_KVP in m_ChannelsLiveStatuses) + { + if (!l_KVP.Value.Item1) + continue; + + l_ShowUp = true; + l_SumViewers += l_KVP.Value.Item2; + } + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => UpdateViewerCount(l_ShowUp, l_SumViewers)); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Update viewer count + /// + /// Is up? + /// Viewers count + private void UpdateViewerCount(bool p_IsUp, int p_Viewers) + { + if (p_IsUp) m_Text.SetText(p_Viewers.ToString()); + else m_Text.SetText("Offline"); + } + } +} diff --git a/Modules/BeatSaberPlus_Chat/Utils/ChatMessageBuilder.cs b/Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Utils/ChatMessageBuilder.cs similarity index 100% rename from Modules/BeatSaberPlus_Chat/Utils/ChatMessageBuilder.cs rename to Modules/BeatSaberPlus_Chat/ChatPlexMod_Chat/Utils/ChatMessageBuilder.cs diff --git a/Modules/BeatSaberPlus_Chat/Properties/AssemblyInfo.cs b/Modules/BeatSaberPlus_Chat/Properties/AssemblyInfo.cs index 5364ea0..bffd039 100644 --- a/Modules/BeatSaberPlus_Chat/Properties/AssemblyInfo.cs +++ b/Modules/BeatSaberPlus_Chat/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -32,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/Modules/BeatSaberPlus_Chat/Resources/Locked.png b/Modules/BeatSaberPlus_Chat/Resources/Locked.png deleted file mode 100644 index fe28c20ee50f69c526b958ae7832d8d7cdcecdef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3479 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 diff --git a/Modules/BeatSaberPlus_Chat/Resources/Settings.png b/Modules/BeatSaberPlus_Chat/Resources/Settings.png deleted file mode 100644 index 36af0aafca2edb161140005170e8efa52b7e64e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8938 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 - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow.cs b/Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow.cs deleted file mode 100644 index 246683e..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow.cs +++ /dev/null @@ -1,189 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using BeatSaberMarkupLanguage.FloatingScreen; -using CP_SDK.Unity.Extensions; -using HMUI; -using System.Linq; -using UnityEngine; -using VRUIControls; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Floating window content - /// - internal partial class ChatFloatingWindow : BeatSaberPlus.SDK.UI.ResourceViewController - { - /// - /// Is movement allowed - /// - private bool m__AllowMovement = false; - /// - /// Is movement allowed - /// - private bool m_AllowMovement - { - get => m__AllowMovement; - set { - m__AllowMovement = value; - ColorUtility.TryParseHtmlString(value ? "#FFFFFFFF" : "#FFFFFF80", out var l_ColH); - ColorUtility.TryParseHtmlString(value ? "#FFFFFF80" : "#FFFFFFFF", out var l_ColD); - m_LockIcon.HighlightColor = l_ColH; - m_LockIcon.DefaultColor = l_ColD; - - var l_FloatingScreen = transform.parent.GetComponent(); - l_FloatingScreen.ShowHandle = value; - - if (value) - { - /// Refresh VR pointer due to bug - var l_Pointers = Resources.FindObjectsOfTypeAll(); - var l_Pointer = CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Playing ? l_Pointers.LastOrDefault() : l_Pointers.FirstOrDefault(); - - if (l_Pointer != null) - { - if (l_FloatingScreen.screenMover) - Destroy(l_FloatingScreen.screenMover); - - l_FloatingScreen.screenMover = l_Pointer.gameObject.AddComponent(); - l_FloatingScreen.screenMover.Init(l_FloatingScreen); - } - else - { - Logger.Instance.Warning("Failed to get VRPointer!"); - } - } - } - } - /// - /// Is a 360 level - /// - private bool m_IsRotatingLevel; - /// - /// FlyingGameHUDRotation instance - /// - private GameObject m_EnvironmentRotationRef; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private Vector2 m_ChatSize; - private bool m_ReverseChatOrder; - private float m_FontSize; - private Color m_HighlightColor; - private Color m_AccentColor; - private Color m_TextColor; - private Color m_PingColor; - private bool m_FilterViewersCommands; - private bool m_FilterBroadcasterCommands; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Lock icon - /// - [UIComponent("SettingsIcon")] - private ClickableImage m_SettingsIcon = null; - /// - /// Lock icon - /// - [UIComponent("LockIcon")] - private ClickableImage m_LockIcon = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - /// Update background color - GetComponentInChildren().color = CConfig.Instance.BackgroundColor; - - /// Update message position origin - (transform.GetChild(0).transform as RectTransform).pivot = new Vector2(0.5f, 0f); - - InitLogic(); - - /// Update lock state - m_AllowMovement = false; - - /// Hide/show the lock icon - m_LockIcon.gameObject.SetActive(CConfig.Instance.ShowLockIcon); - gameObject.ChangerLayerRecursive(LayerMask.NameToLayer("UI")); - - /// Make icons easier to click - m_SettingsIcon.gameObject.AddComponent().radius = 10f; - m_LockIcon.gameObject.AddComponent().radius = 10f; - } - /// - /// On view destruction - /// - protected override sealed void OnViewDestruction() - { - DestroyLogic(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Update UI - /// - /// New scene - /// Is the scene changed ? - /// Is a 360 level - /// Flying hame HUD rotation - internal void UpdateUI(CP_SDK.ChatPlexSDK.EGenericScene p_Scene, bool p_OnSceneChange, bool p_IsRotatingLevel, GameObject p_EnvironmentRotationRef) - { - /// Disable settings in play mode - m_SettingsIcon.gameObject.SetActive(p_Scene != CP_SDK.ChatPlexSDK.EGenericScene.Playing); - - /// On scene change, lock movement - if (p_OnSceneChange) - m_AllowMovement = false; - - /// Update background color - GetComponentInChildren().color = CConfig.Instance.BackgroundColor; - - m_ChatSize = CConfig.Instance.ChatSize; - m_ReverseChatOrder = CConfig.Instance.ReverseChatOrder; - m_FontSize = CConfig.Instance.FontSize; - m_HighlightColor = CConfig.Instance.HighlightColor; - m_AccentColor = CConfig.Instance.AccentColor; - m_TextColor = CConfig.Instance.TextColor; - m_PingColor = CConfig.Instance.PingColor; - - m_FilterViewersCommands = CConfig.Instance.FilterViewersCommands; - m_FilterBroadcasterCommands = CConfig.Instance.FilterBroadcasterCommands; - - m_IsRotatingLevel = p_IsRotatingLevel; - m_EnvironmentRotationRef = p_EnvironmentRotationRef; - - if (!m_IsRotatingLevel) - transform.parent.parent.rotation = Quaternion.identity; - - UpdateMessagesStyleFull(); - - /// Hide/show the lock icon - m_LockIcon.gameObject.SetActive(CConfig.Instance.ShowLockIcon); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIAction("settings-pressed")] - internal void OnSettingsPressed() - { - var l_Items = Chat.Instance.GetSettingsUI(); - BeatSaberPlus.UI.MainViewFlowCoordinator.Instance().ChangeView(l_Items.Item1, l_Items.Item2, l_Items.Item3); - } - [UIAction("lock-pressed")] - internal void OnLockPressed() - { - m_AllowMovement = !m_AllowMovement; - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow_Events.cs b/Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow_Events.cs deleted file mode 100644 index b55a7bc..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow_Events.cs +++ /dev/null @@ -1,320 +0,0 @@ -using CP_SDK.Chat.Interfaces; -using System.Runtime.Remoting.Channels; -using System.Threading.Tasks; -using UnityEngine; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Floating window content - /// - internal partial class ChatFloatingWindow - { - /// - /// On system message - /// - /// Chat service - /// System message - internal void OnSystemMessage(IChatService p_Service, string p_Message) - { - var l_MessageStr = $"[{p_Service.DisplayName}] {p_Message}"; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - var l_NewMessage = m_MessagePool.Get(); - l_NewMessage.Text.ReplaceContent(l_MessageStr); - l_NewMessage.HighlightEnabled = true; - l_NewMessage.HighlightColor = Color.gray.ColorWithAlpha(0.18f); - - AddMessage(l_NewMessage); - m_LastMessage = l_NewMessage; - }); - } - /// - /// On login - /// - /// Chat service - internal void OnLogin(IChatService p_Service) - { - var l_MessageStr = $"[{p_Service.DisplayName}] Success connecting to {p_Service.DisplayName}"; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - var l_NewMessage = m_MessagePool.Get(); - l_NewMessage.Text.ReplaceContent(l_MessageStr); - l_NewMessage.HighlightEnabled = true; - l_NewMessage.HighlightColor = Color.gray.ColorWithAlpha(0.18f); - - AddMessage(l_NewMessage); - m_LastMessage = l_NewMessage; - }); - } - /// - /// On join channel - /// - /// Chat service - /// Channel service - internal void OnJoinChannel(IChatService p_Service, IChatChannel p_Channel) - { - var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; - var l_MessageStr = $"[{p_Service.DisplayName}] Success joining {l_Prefix}{p_Channel.Name}"; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - var l_NewMessage = m_MessagePool.Get(); - l_NewMessage.Text.ReplaceContent(l_MessageStr); - l_NewMessage.HighlightEnabled = true; - l_NewMessage.HighlightColor = Color.gray.ColorWithAlpha(0.18f); - - AddMessage(l_NewMessage); - m_LastMessage = l_NewMessage; - }); - } - /// - /// On join leave - /// - /// Chat service - /// Channel service - internal void OnLeaveChannel(IChatService p_Service, IChatChannel p_Channel) - { - var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; - var l_MessageStr = $"[{p_Service.DisplayName}] Success leaving {l_Prefix}{p_Channel.Name}"; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - var l_NewMessage = m_MessagePool.Get(); - l_NewMessage.Text.ReplaceContent(l_MessageStr); - l_NewMessage.HighlightEnabled = true; - l_NewMessage.HighlightColor = Color.gray.ColorWithAlpha(0.18f); - - AddMessage(l_NewMessage); - m_LastMessage = l_NewMessage; - }); - } - /// - /// On channel follow - /// - /// Chat service - /// Channel instance - /// User instance - internal void OnChannelFollow(IChatService p_Service, IChatChannel p_Channel, IChatUser p_User) - { - if (!CConfig.Instance.ShowFollowEvents) - return; - - var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; - var l_MessageStr = $"{l_Prefix}[{p_Service.DisplayName}] @{p_User.PaintedName} is now following {p_Channel.Name}"; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - var l_NewMessage = m_MessagePool.Get(); - l_NewMessage.Text.ReplaceContent(l_MessageStr); - l_NewMessage.HighlightEnabled = true; - l_NewMessage.HighlightColor = Color.blue.ColorWithAlpha(0.24f); - - AddMessage(l_NewMessage); - m_LastMessage = l_NewMessage; - }); - } - /// - /// On channel bits - /// - /// Chat service - /// Channel instance - /// User instance - /// Bits used - internal void OnChannelBits(IChatService p_Service, IChatChannel p_Channel, IChatUser p_User, int p_BitsUsed) - { - if (!CConfig.Instance.ShowBitsCheeringEvents) - return; - - var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; - var l_MessageStr = $"{l_Prefix}[{p_Service.DisplayName}] @{p_User.PaintedName} cheered {p_BitsUsed} bits!"; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - var l_NewMessage = m_MessagePool.Get(); - l_NewMessage.Text.ReplaceContent(l_MessageStr); - l_NewMessage.HighlightEnabled = true; - l_NewMessage.HighlightColor = Color.green.ColorWithAlpha(0.24f); - - AddMessage(l_NewMessage); - m_LastMessage = l_NewMessage; - }); - } - /// - /// On channel points - /// - /// Chat service - /// Channel instance - /// User instance - /// Event - internal void OnChannelPoints(IChatService p_Service, IChatChannel p_Channel, IChatUser p_User, IChatChannelPointEvent p_Event) - { - if (!CConfig.Instance.ShowChannelPointsEvent) - return; - - if (!m_ChatFont.HasReplaceCharacter("TwitchChannelPoint_" + p_Event.Title)) - { - TaskCompletionSource l_TaskCompletionSource = new TaskCompletionSource(); - - CP_SDK.Chat.ChatImageProvider.TryCacheSingleImage(EChatResourceCategory.Badge, "TwitchChannelPoint_" + p_Event.Title, p_Event.Image, CP_SDK.Animation.EAnimationType.NONE, (l_Info) => - { - if (l_Info != null && !m_ChatFont.TryRegisterImageInfo(l_Info, out var l_Character)) - Logger.Instance.Warning($"Failed to register emote \"{"TwitchChannelPoint_" + p_Event.Title}\" in font {m_ChatFont.Font.name}."); - - l_TaskCompletionSource.SetResult(l_Info); - }); - - Task.WaitAll(new Task[] { l_TaskCompletionSource.Task }, 15000); - } - - var l_ImagePart = "for"; - - if (m_ChatFont.TryGetReplaceCharacter("TwitchChannelPoint_" + p_Event.Title, out uint p_Character)) - l_ImagePart = char.ConvertFromUtf32((int)p_Character); - - var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; - var l_MessageStr = $"{l_Prefix}[{p_Service.DisplayName}] @{p_User.PaintedName} redeemed {p_Event.Title} {l_ImagePart} {p_Event.Cost}!"; - - if (ColorUtility.TryParseHtmlString(p_Event.BackgroundColor + "FF", out var l_HighlightColor)) - l_HighlightColor.a = 0.24f; - else - l_HighlightColor = Color.green.ColorWithAlpha(0.24f); - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - var l_NewMessage = m_MessagePool.Get(); - l_NewMessage.Text.ReplaceContent(l_MessageStr); - l_NewMessage.HighlightEnabled = true; - l_NewMessage.HighlightColor = l_HighlightColor; - - if (!string.IsNullOrEmpty(p_Event.UserInput)) - { - l_NewMessage.SubText.ReplaceContent(p_Event.UserInput); - l_NewMessage.SubTextEnabled = true; - } - - AddMessage(l_NewMessage); - m_LastMessage = l_NewMessage; - }); - } - /// - /// On channel subscription - /// - /// Chat service - /// Channel instance - /// User instance - /// Event - internal void OnChannelSubsciption(IChatService p_Service, IChatChannel p_Channel, IChatUser p_User, IChatSubscriptionEvent p_Event) - { - if (!CConfig.Instance.ShowSubscriptionEvents) - return; - - var l_Prefix = !string.IsNullOrEmpty(p_Channel.Prefix) ? $"[{p_Channel.Prefix}] " : string.Empty; - var l_MessageStr = $"{l_Prefix}[{p_Service.DisplayName}] @{p_User.PaintedName} "; - if (p_Event.IsGift) - l_MessageStr += $"gifted {p_Event.PurchasedMonthCount} month of {p_Event.SubPlan} to @{p_Event.RecipientDisplayName}!"; - else - l_MessageStr += $"did get a {p_Event.PurchasedMonthCount} month of {p_Event.SubPlan}!"; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - var l_NewMessage = m_MessagePool.Get(); - l_NewMessage.Text.ReplaceContent(l_MessageStr); - l_NewMessage.HighlightEnabled = true; - l_NewMessage.HighlightColor = Color.green.ColorWithAlpha(0.36f); - - AddMessage(l_NewMessage); - m_LastMessage = l_NewMessage; - }); - } - /// - /// On chat user message cleared - /// - /// ID of the user - internal void OnChatCleared(string p_UserID) - { - if (p_UserID == null) - return; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - foreach (var l_Current in m_Messages) - { - if (l_Current.Text.ChatMessage == null) - continue; - - if (l_Current.Text.ChatMessage.Sender.Id == p_UserID) - ClearMessage(l_Current); - } - }); - } - /// - /// On message cleared - /// - /// Message ID - internal void OnMessageCleared(string p_MessageID) - { - if (p_MessageID == null) - return; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - foreach (var l_Current in m_Messages) - { - if (l_Current.Text.ChatMessage == null) - continue; - - if (l_Current.Text.ChatMessage.Id == p_MessageID) - ClearMessage(l_Current); - } - }); - } - /// - /// When a message is received - /// - /// Received message - internal async void OnTextMessageReceived(IChatMessage p_Message) - { - /// Command filters - if (m_FilterViewersCommands || m_FilterBroadcasterCommands) - { - bool l_IsBroadcaster = p_Message.Sender.IsBroadcaster; - - if (m_FilterViewersCommands && !l_IsBroadcaster && p_Message.Message.StartsWith("!")) - return; - - if (m_FilterBroadcasterCommands && l_IsBroadcaster && p_Message.Message.StartsWith("!")) - return; - } - - var l_Prefix = !string.IsNullOrEmpty(p_Message.Channel.Prefix) ? $"[{p_Message.Channel.Prefix}] " : string.Empty; - var l_Message = await Utils.ChatMessageBuilder.BuildMessage(p_Message, m_ChatFont).ConfigureAwait(false); - var l_ParsedMessage = l_Prefix + l_Message; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - if (m_LastMessage != null && !p_Message.IsSystemMessage && m_LastMessage.Text.ChatMessage != null && !string.IsNullOrEmpty(p_Message.Id) && m_LastMessage.Text.ChatMessage.Id == p_Message.Id) - { - /// If the last message received had the same id and isn't a system message, then this was a sub-message of the original and may need to be highlighted along with the original message - m_LastMessage.SubText.ChatMessage = p_Message; - m_LastMessage.SubText.ReplaceContent(l_ParsedMessage); - m_LastMessage.SubTextEnabled = true; - - UpdateMessageStyle(m_LastMessage); - } - else - { - var l_NewMsg = m_MessagePool.Get(); - l_NewMsg.Text.ChatMessage = p_Message; - l_NewMsg.Text.ReplaceContent(l_ParsedMessage); - - AddMessage(l_NewMsg); - - m_LastMessage = l_NewMsg; - } - }); - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow_Logic.cs b/Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow_Logic.cs deleted file mode 100644 index f5199a0..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ChatFloatingWindow_Logic.cs +++ /dev/null @@ -1,325 +0,0 @@ -using CP_SDK.Chat.Interfaces; -using CP_SDK.Unity.Extensions; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Text; -using TMPro; -using UnityEngine; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Floating window content - /// - internal partial class ChatFloatingWindow - { - /// - /// Backup message queue, keep a track of the messages if the game reload - /// - private static ConcurrentQueue m_BackupMessageQueue = new ConcurrentQueue(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Chat font - /// - private Extensions.EnhancedFontInfo m_ChatFont; - /// - /// Message pool - /// - private CP_SDK.Pool.ObjectPool m_MessagePool; - /// - /// All allocated messages - /// - private List m_MessagePool_Allocated = new List(); - /// - /// Visible message queue - /// - private List m_Messages = new List(); - /// - /// Should update message positions - /// - private bool m_UpdateMessagePositions = false; - /// - /// Last message added - /// - private Components.ChatMessageWidget m_LastMessage; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Init logic - /// - private void InitLogic() - { - m_ChatFont = new Extensions.EnhancedFontInfo(CP_SDK.Unity.FontManager.GetChatFont()); - - /// Clean reserved characters - m_ChatFont.Font.characterTable.RemoveAll(x => x.glyphIndex > 0xE000 && x.glyphIndex <= 0xF8FF); - m_ChatFont.Font.characterTable.RemoveAll(x => x.glyphIndex > 0xF0000); - - /// Setup message pool - m_MessagePool = new CP_SDK.Pool.ObjectPool( - createFunc: () => - { - var l_Message = null as Components.ChatMessageWidget; - l_Message = new GameObject().AddComponent(); - l_Message.Text.FontInfo = m_ChatFont; - l_Message.Text.font = m_ChatFont.Font; - l_Message.Text.fontSize = m_FontSize; - l_Message.Text.color = m_TextColor; - l_Message.Text.text = "."; - l_Message.Text.SetAllDirty(); - - l_Message.SubText.FontInfo = m_ChatFont; - l_Message.SubText.font = m_ChatFont.Font; - l_Message.SubText.fontSize = m_FontSize; - l_Message.SubText.color = m_TextColor; - l_Message.SubText.text = "."; - l_Message.SubText.SetAllDirty(); - - l_Message.transform.SetParent(transform.GetChild(0).transform, false); - l_Message.transform.SetAsFirstSibling(); - l_Message.SetWidth(m_ChatSize.x); - - //l_Message.gameObject.SetActive(false); - l_Message.transform.localScale = Vector3.zero; - l_Message.gameObject.ChangerLayerRecursive(LayerMask.NameToLayer("UI")); - - UpdateMessageStyle(l_Message); - - l_Message.OnLatePreRenderRebuildComplete += OnMessageRenderRebuildComplete; - - m_MessagePool_Allocated.Add(l_Message); - - return l_Message; - }, - actionOnGet: (p_Message) => - { - p_Message.EnableCallback = true; - }, - actionOnRelease: (p_Message) => - { - try - { - //p_Message.gameObject.SetActive(false); - p_Message.transform.localScale = Vector3.zero; - - p_Message.HighlightEnabled = false; - p_Message.AccentEnabled = false; - p_Message.SubTextEnabled = false; - p_Message.Text.ChatMessage = null; - p_Message.SubText.ChatMessage = null; - - p_Message.EnableCallback = false; - - p_Message.Text.ClearImages(); - p_Message.SubText.ClearImages(); - } - catch (Exception p_Exception) - { - Logger.Instance.Error("An exception occurred while trying to free CustomText object"); - Logger.Instance.Error(p_Exception); - } - }, - actionOnDestroy: (p_Message) => - { - GameObject.Destroy(p_Message.gameObject); - m_MessagePool_Allocated.Remove(p_Message); - }, - collectionCheck: false, - defaultCapacity: 25 - ); - - while (m_BackupMessageQueue.TryDequeue(out var l_Current)) - OnTextMessageReceived(l_Current); - } - /// - /// Destroy logic - /// - private void DestroyLogic() - { - /// Backup messages - for (int l_I = 0; l_I < m_Messages.Count; ++l_I) - { - var l_Current = m_Messages[l_I]; - if (l_Current.Text.ChatMessage != null) - m_BackupMessageQueue.Enqueue(l_Current.Text.ChatMessage); - - if (l_Current.SubText.ChatMessage != null) - m_BackupMessageQueue.Enqueue(l_Current.SubText.ChatMessage); - - m_MessagePool.Release(m_Messages[l_I]); - } - - m_Messages.Clear(); - - if (m_MessagePool != null) - { - m_MessagePool.Dispose(); - m_MessagePool = null; - } - - if (m_ChatFont != null) - { - Destroy(m_ChatFont.Font); - m_ChatFont = null; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On frame - /// - private void Update() - { - if (m_IsRotatingLevel && CConfig.Instance.FollowEnvironementRotation && m_EnvironmentRotationRef != null && m_EnvironmentRotationRef) - transform.parent.parent.rotation = m_EnvironmentRotationRef.transform.rotation; - - if (m_UpdateMessagePositions) - { - m_UpdateMessagePositions = false; - float l_PositionY = m_ReverseChatOrder ? m_ChatSize.y : 0; - - for (int l_I = (m_Messages.Count - 1); l_I >= 0; --l_I) - { - var l_CurrentMessage = m_Messages[l_I]; - var l_Height = l_CurrentMessage.Height; - - if (m_ReverseChatOrder) - l_PositionY -= l_Height; - - l_CurrentMessage.SetPositionY(l_PositionY); - - if (!m_ReverseChatOrder) - l_PositionY += l_Height; - } - - for (int l_I = 0; l_I < m_Messages.Count;) - { - var l_Current = m_Messages[l_I]; - if ((m_ReverseChatOrder && l_Current.PositionY < -m_ChatSize.y) || l_Current.PositionY >= m_ChatSize.y) - { - m_Messages.Remove(l_Current); - m_MessagePool.Release(l_Current); - continue; - } - - ++l_I; - } - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Add a new message to the display - /// - /// Message to add - private void AddMessage(Components.ChatMessageWidget p_NewMessage) - { - p_NewMessage.SetPositionY(m_ReverseChatOrder ? m_ChatSize.y : 0); - - m_Messages.Add(p_NewMessage); - UpdateMessageStyle(p_NewMessage); - - p_NewMessage.transform.localScale = Vector3.one; - //p_NewMessage.gameObject.SetActive(true); - } - /// - /// Update all messages - /// - private void UpdateMessagesStyleFull() - { - for (int l_I = 0; l_I < m_MessagePool_Allocated.Count; ++l_I) - UpdateMessageStyleFull(m_MessagePool_Allocated[l_I], true); - - m_UpdateMessagePositions = true; - } - /// - /// Update message - /// - /// Message to update - /// Should flag childs dirty - private void UpdateMessageStyleFull(Components.ChatMessageWidget p_Message, bool p_SetAllDirty = false) - { - p_Message.SetWidth(m_ChatSize.x); - - p_Message.AccentColor = m_AccentColor.ColorWithAlpha(0.36f); - - p_Message.Text.color = m_TextColor; - p_Message.Text.fontSize = m_FontSize; - - p_Message.SubText.color = m_TextColor; - p_Message.SubText.fontSize = m_FontSize; - - UpdateMessageStyle(p_Message); - - if (p_SetAllDirty) - { - p_Message.Text.SetAllDirty(); - - if (p_Message.SubTextEnabled) - p_Message.SubText.SetAllDirty(); - } - } - /// - /// Update message - /// - /// Message to update - /// Should flag childs dirty - private void UpdateMessageStyle(Components.ChatMessageWidget p_Message) - { - if (p_Message.Text.ChatMessage != null) - { - p_Message.HighlightColor = (p_Message.Text.ChatMessage.IsPing ? m_PingColor.ColorWithAlpha(0.36f) : m_HighlightColor); - p_Message.HighlightEnabled = p_Message.Text.ChatMessage.IsHighlighted || p_Message.Text.ChatMessage.IsPing; - p_Message.AccentEnabled = !p_Message.Text.ChatMessage.IsPing && (p_Message.HighlightEnabled || p_Message.SubText.ChatMessage != null); - } - } - /// - /// Clear message - /// - /// Message instance - private void ClearMessage(Components.ChatMessageWidget p_Message) - { - string BuildClearedMessage(Components.ChatMessageText p_MessageToClear) - { - StringBuilder l_StringBuilder = new StringBuilder($"{p_MessageToClear.ChatMessage.Sender.DisplayName}"); - var l_BadgeEndIndex = p_MessageToClear.text.IndexOf(""); - return l_StringBuilder.ToString(); - } - - /// Only clear non-system messages - if (!p_Message.Text.ChatMessage.IsSystemMessage) - { - p_Message.Text.ReplaceContent(BuildClearedMessage(p_Message.Text)); - p_Message.SubTextEnabled = false; - } - - if (p_Message.SubText.ChatMessage != null && !p_Message.SubText.ChatMessage.IsSystemMessage) - p_Message.SubText.ReplaceContent(BuildClearedMessage(p_Message.SubText)); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When a message is rebuilt - /// - private void OnMessageRenderRebuildComplete() - { - m_UpdateMessagePositions = true; - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/HypeTrainFloatingWindow.bsml b/Modules/BeatSaberPlus_Chat/UI/HypeTrainFloatingWindow.bsml deleted file mode 100644 index 1a9ef74..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/HypeTrainFloatingWindow.bsml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/HypeTrainFloatingWindow.cs b/Modules/BeatSaberPlus_Chat/UI/HypeTrainFloatingWindow.cs deleted file mode 100644 index dfcec7f..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/HypeTrainFloatingWindow.cs +++ /dev/null @@ -1,181 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using System; -using System.Linq; -using UnityEngine; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Hype train floating window - /// - internal class HypeTrainFloatingWindow : BeatSaberPlus.SDK.UI.ResourceViewController - { - public static float HEIGHT = 7f; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIObject("TopFrame")] private GameObject m_TopFrame = null; - [UIComponent("Label")] private TMPro.TextMeshProUGUI m_Label = null; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Twitch service instance - /// - private CP_SDK.Chat.Services.Twitch.TwitchService m_TwitchService = null; - /// - /// Latest hype train data - /// - private CP_SDK.Chat.Models.Twitch.Helix_HypeTrain m_LastHypeTrain = null; - /// - /// Filler progress bar - /// - private UnityEngine.UI.Image m_Filler = null; - /// - /// Current hype train progression - /// - private float m_CurrentProgression = 0f; - /// - /// Current hype train expire time - /// - private float m_CurrentExpire = 0f; - /// - /// Current hype train level - /// - private int m_CurrentLevel = 0; - /// - /// Displayed hype train progression - /// - private float m_DisplayedProgression = 0f; - /// - /// Displayed hype train remaining time - /// - private int m_DisplayedRemaining = 0; - /// - /// Displayed hype train level - /// - private int m_DisplayedLevel = 0; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Background = m_TopFrame.AddComponent(); - l_Background.sprite = CP_SDK.Unity.SpriteU.CreateFromTexture(Texture2D.whiteTexture); - l_Background.type = UnityEngine.UI.Image.Type.Filled; - l_Background.fillMethod = UnityEngine.UI.Image.FillMethod.Horizontal; - l_Background.fillAmount = 1f; - l_Background.color = new Color32(24, 24, 26, 255); - l_Background.material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; - - m_Filler = m_TopFrame.transform.GetChild(0).gameObject.AddComponent(); - m_Filler.sprite = CP_SDK.Unity.SpriteU.CreateFromTexture(Texture2D.whiteTexture); - m_Filler.type = UnityEngine.UI.Image.Type.Filled; - m_Filler.fillMethod = UnityEngine.UI.Image.FillMethod.Horizontal; - m_Filler.fillAmount = 0.33f; - m_Filler.color = new Color32(120, 44, 232, 255); - m_Filler.material = BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial; - - CP_SDK.Chat.Service.Acquire(); - var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); - if (l_TwitchService != null) - { - m_TwitchService = l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService; - m_TwitchService.HelixAPI.OnActiveHypeTrainChanged += HelixAPI_OnActiveHypeTrainChanged; - } - } - /// - /// On view activation - /// - protected sealed override void OnViewActivation() - { - /// Hide by default - gameObject.SetActive(false); - } - /// - /// On view destruction - /// - protected sealed override void OnViewDestruction() - { - if (m_TwitchService != null) - m_TwitchService.HelixAPI.OnActiveHypeTrainChanged -= HelixAPI_OnActiveHypeTrainChanged; - - CP_SDK.Chat.Service.Release(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On frame - /// - private void Update() - { - if (m_LastHypeTrain != null) - { - var l_HasExpired = (m_CurrentExpire + 60) < Time.realtimeSinceStartup; - if (l_HasExpired) - gameObject.SetActive(false); - else - { - m_Filler.fillAmount = Mathf.Lerp(m_Filler.fillAmount, Mathf.Min(1f, m_CurrentProgression), Time.smoothDeltaTime * 2.5f); - var l_NewDisplayProgression = Mathf.Lerp(m_DisplayedProgression, m_CurrentProgression, Time.smoothDeltaTime * 2.5f); - var l_RemainingSeconds = (int)Mathf.Max(0f, m_CurrentExpire - Time.realtimeSinceStartup); - - if (m_CurrentLevel != m_DisplayedLevel || Mathf.Abs(l_NewDisplayProgression - m_DisplayedProgression) >= 0.0001 || l_RemainingSeconds != m_DisplayedRemaining) - { - m_DisplayedLevel = m_CurrentLevel; - m_DisplayedProgression = l_NewDisplayProgression; - m_DisplayedRemaining = l_RemainingSeconds; - - var l_Minutes = l_RemainingSeconds / 60; - var l_Seconds = l_RemainingSeconds - (l_Minutes * 60); - - m_Label.text = $"LVL {m_DisplayedLevel} - Hype Train!\n{Mathf.RoundToInt(m_DisplayedProgression * 100.0f)}% {l_Minutes}:{l_Seconds.ToString().PadLeft(2, '0')}"; - } - } - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On active hype train changed - /// - /// Current hype train - private void HelixAPI_OnActiveHypeTrainChanged(CP_SDK.Chat.Models.Twitch.Helix_HypeTrain p_HypeTrain) - { - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - if (p_HypeTrain != null) - { - var l_HasExpired = p_HypeTrain.event_data.expires_at.AddSeconds(60) < DateTime.UtcNow; - if (l_HasExpired && gameObject.activeSelf) - gameObject.SetActive(false); - else if (!l_HasExpired) - { - if (!gameObject.activeSelf) - gameObject.SetActive(true); - - var l_Progress = p_HypeTrain.event_data.goal == 0 ? 0f : (float)p_HypeTrain.event_data.total / (float)p_HypeTrain.event_data.goal; - - m_CurrentExpire = Time.realtimeSinceStartup + (float)((p_HypeTrain.event_data.expires_at - DateTime.UtcNow).TotalSeconds); - m_CurrentProgression = l_Progress; - m_CurrentLevel = p_HypeTrain.event_data.level; - } - } - - m_LastHypeTrain = p_HypeTrain; - }); - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/ModerationLeft.bsml b/Modules/BeatSaberPlus_Chat/UI/ModerationLeft.bsml deleted file mode 100644 index 2198ea4..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ModerationLeft.bsml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/ModerationMain.bsml b/Modules/BeatSaberPlus_Chat/UI/ModerationMain.bsml deleted file mode 100644 index 764b9f8..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ModerationMain.bsml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/ModerationMain.cs b/Modules/BeatSaberPlus_Chat/UI/ModerationMain.cs deleted file mode 100644 index f67d6e8..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ModerationMain.cs +++ /dev/null @@ -1,181 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using System.Linq; -using TMPro; -using UnityEngine; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Moderation main screen - /// - internal class ModerationMain : BeatSaberPlus.SDK.UI.ResourceViewController - { - /// - /// Keyboard original key count - /// - private int m_InputKeyboardInitialKeyCount = -1; - - - private float m_BaseMargin = -35f; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIComponent("MessageKeyboard")] - private ModalKeyboard m_MessageKeyboard = null; - [UIValue("MessageContent")] - private string m_MessageContent = ""; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override void OnViewCreation() - { - /// Update opacity - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_MessageKeyboard.modalView, 0.75f); - - var l_OGSize = m_MessageKeyboard.keyboard.KeyboardText.fontSize; - m_MessageKeyboard.keyboard.KeyboardText.fontSizeMin = l_OGSize / 3; - m_MessageKeyboard.keyboard.KeyboardText.fontSizeMax = l_OGSize; - - var l_FirstButton = m_MessageKeyboard.keyboard.BaseButton.GetComponentInChildren(); - var l_Color = new Color(0f, 1f, 0); - var l_ButtonY = 11f; - var l_Margin = 1f; - var l_Position = new Vector2(m_BaseMargin, l_ButtonY); - var l_Width = l_FirstButton.GetPreferredValues("USERNAME").x * 2.0f; - var l_Key = new KEYBOARD.KEY(m_MessageKeyboard.keyboard, l_Position, "USERNAME", l_Width, 10f, l_Color); - - m_BaseMargin += ((l_Width / 2.0f) + l_Margin); - l_Key.keyaction += (_) => - { - if (ModerationRight.Instance?.SelectedUser == null) - ShowMessageModal("Please select an user on the right panel!"); - else - InsertTextWithSpace(ModerationRight.Instance.SelectedUser.DisplayName); - }; - - m_MessageKeyboard.keyboard.keys.Add(l_Key); - } - /// - /// On view activation - /// - protected override sealed void OnViewActivation() - { - ShowKeyboard(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private void ShowKeyboard() - { - /// Clear old keys - if (m_InputKeyboardInitialKeyCount == -1) - m_InputKeyboardInitialKeyCount = m_MessageKeyboard.keyboard.keys.Count; - - while (m_MessageKeyboard.keyboard.keys.Count > m_InputKeyboardInitialKeyCount) - { - var l_Key = m_MessageKeyboard.keyboard.keys.ElementAt(m_MessageKeyboard.keyboard.keys.Count - 1); - m_MessageKeyboard.keyboard.Clear(l_Key); - m_MessageKeyboard.keyboard.keys.RemoveAt(m_MessageKeyboard.keyboard.keys.Count - 1); - - GameObject.Destroy(l_Key.mybutton.gameObject); - } - - /// Add custom keys - var l_CustomKeys = CConfig.Instance.ModerationKeys; - if (l_CustomKeys != null && l_CustomKeys.Count != 0) - { - var l_FirstButton = m_MessageKeyboard.keyboard.BaseButton.GetComponentInChildren(); - var l_Color = new Color(0.92f, 0.64f, 0); - var l_ButtonY = 11f; - var l_Margin = 1f; - var l_TotalLeft = m_BaseMargin; - - var l_I = 0; - foreach (var l_Var in l_CustomKeys) - { - var l_Position = new Vector2(l_TotalLeft, l_ButtonY); - var l_Width = l_FirstButton.GetPreferredValues(" " + l_Var + " ").x * 2.0f; - var l_Key = new KEYBOARD.KEY(m_MessageKeyboard.keyboard, l_Position, " " + l_Var + " ", l_Width, 10f, l_Color); - - l_TotalLeft += ((l_Width / 2.0f) + l_Margin); - l_Key.keyaction += (_) => ChangePrefix(l_Var); - - m_MessageKeyboard.keyboard.keys.Add(l_Key); - ++l_I; - } - } - - ShowModal("OpenMessageModal"); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Change message prefix - /// - /// New prefix - private void ChangePrefix(string p_Prefix) - { - if (p_Prefix.StartsWith("!") || p_Prefix.StartsWith("/")) - { - if (!m_MessageKeyboard.keyboard.KeyboardText.text.StartsWith("/") && !m_MessageKeyboard.keyboard.KeyboardText.text.StartsWith("!")) - m_MessageKeyboard.keyboard.KeyboardText.text = p_Prefix + " " + m_MessageKeyboard.keyboard.KeyboardText.text; - else - { - if (m_MessageKeyboard.keyboard.KeyboardText.text.Contains(' ')) - { - var l_Parts = m_MessageKeyboard.keyboard.KeyboardText.text.Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); - m_MessageKeyboard.keyboard.KeyboardText.text = p_Prefix + " " + string.Join(" ", l_Parts.Skip(1).ToArray()); - } - else - m_MessageKeyboard.keyboard.KeyboardText.text = p_Prefix + " "; - } - } - else - m_MessageKeyboard.keyboard.KeyboardText.text = m_MessageKeyboard.keyboard.KeyboardText.text + " " + p_Prefix; - } - /// - /// Insert text with space - /// - /// Text to insert - internal void InsertTextWithSpace(string p_Text) - { - if (m_MessageKeyboard.keyboard.KeyboardText.text.Length == 0) - m_MessageKeyboard.keyboard.KeyboardText.text += p_Text; - else if (m_MessageKeyboard.keyboard.KeyboardText.text[m_MessageKeyboard.keyboard.KeyboardText.text.Length - 1] != ' ') - m_MessageKeyboard.keyboard.KeyboardText.text += " " + p_Text; - else - m_MessageKeyboard.keyboard.KeyboardText.text += p_Text; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On enter pressed - /// - /// - [UIAction("SendPressed")] - internal void SendPressed(string p_Text) - { - if (CP_SDK.Chat.Service.Multiplexer.Channels.Count == 0) - return; - - foreach (var l_Channel in CP_SDK.Chat.Service.Multiplexer.Channels) - l_Channel.Item1.SendTextMessage(l_Channel.Item2, p_Text); - - m_MessageContent = ""; - ShowModal("OpenMessageModal"); - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/ModerationRight.bsml b/Modules/BeatSaberPlus_Chat/UI/ModerationRight.bsml deleted file mode 100644 index 51671a7..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ModerationRight.bsml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/ModerationRight.cs b/Modules/BeatSaberPlus_Chat/UI/ModerationRight.cs deleted file mode 100644 index 3243f21..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ModerationRight.cs +++ /dev/null @@ -1,350 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using CP_SDK.Chat.Interfaces; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Moderation right screen - /// - internal class ModerationRight : BeatSaberPlus.SDK.UI.ResourceViewController - { - /// - /// User line per page - /// - private static int USER_PER_PAGE = 10; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIObject("Background")] - private GameObject m_Background = null; - [UIComponent("UsersUpButton")] - private Button m_UsersUpButton = null; - [UIObject("UsersList")] - private GameObject m_UsersListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_UsersList = null; - [UIComponent("UsersDownButton")] - private Button m_UsersDownButton = null; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Complete user list - /// - private List<(IChatService, IChatUser)> m_Users = new List<(IChatService, IChatUser)>(); - /// - /// Current user list page - /// - private int m_CurrentPage = 1; - /// - /// Total user page count - /// - private int m_PageCount = 1; - /// - /// Selected index - /// - private int m_SelectedIndex = -1; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Current selected user - /// - public IChatUser SelectedUser { - get - { - if (m_SelectedIndex == -1 || m_SelectedIndex >= m_Users.Count) - return null; - - return m_Users[m_SelectedIndex].Item2; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override void OnViewCreation() - { - /// Update background color - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); - - /// Scale down up & down button - m_UsersUpButton.transform.localScale = Vector3.one * 0.5f; - m_UsersDownButton.transform.localScale = Vector3.one * 0.5f; - - /// Prepare user list - var l_BSMLTableView = m_UsersListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_UsersListView.GetComponentInChildren()); - m_UsersList = l_BSMLTableView.gameObject.AddComponent(); - m_UsersList.TableViewInstance = l_BSMLTableView; - l_BSMLTableView.SetDataSource(m_UsersList, false); - - /// Bind events - m_UsersUpButton.onClick.AddListener(OnUsersPageUpPressed); - l_BSMLTableView.didSelectCellWithIdxEvent += OnUserSelected; - m_UsersDownButton.onClick.AddListener(OnUsersPageDownPressed); - } - /// - /// On view activation - /// - protected override void OnViewActivation() - { - Refresh(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Refresh event list - /// - internal void Refresh() - { - /// Clear previous scores - ClearDisplayedData(); - - /// Build full list - m_Users = Chat.Instance.LastChatUsers; - m_Users.Sort((x,y) => x.Item2.DisplayName.CompareTo(y.Item2.DisplayName)); - - /// Compute page count - m_PageCount = Mathf.CeilToInt((float)(m_Users.Count) / (float)(USER_PER_PAGE)); - - /// Rebuild list - RebuildList(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous user page - /// - private void OnUsersPageUpPressed() - { - /// Underflow check - if (m_CurrentPage < 2) - return; - - /// Decrement current page - m_CurrentPage--; - - /// Clear previous users - ClearDisplayedData(); - - /// Rebuild list - RebuildList(); - } - /// - /// On user selected - /// - /// TableView instance - /// Relative index - private void OnUserSelected(HMUI.TableView p_TableView, int p_UserRelIndex) - { - if (((m_CurrentPage - 1) * USER_PER_PAGE) + p_UserRelIndex < m_Users.Count) - m_SelectedIndex = ((m_CurrentPage - 1) * USER_PER_PAGE) + p_UserRelIndex; - else - m_SelectedIndex = -1; - } - /// - /// Go to next user page - /// - private void OnUsersPageDownPressed() - { - /// Increment current page - m_CurrentPage++; - - /// Clear previous users - ClearDisplayedData(); - - /// Rebuild list - RebuildList(); - } - /// - /// Rebuild list - /// - private void RebuildList() - { - if (!UICreated) - return; - - /// Clear old entries - ClearDisplayedData(); - - /// Reset selection - m_SelectedIndex = -1; - - /// Reset page - m_CurrentPage = m_CurrentPage > m_PageCount ? 1 : m_CurrentPage; - - for (int l_I = (m_CurrentPage - 1) * USER_PER_PAGE; l_I < m_Users.Count && l_I < (m_CurrentPage * USER_PER_PAGE); ++l_I) - m_UsersList.Data.Add(BuildLineString(m_Users[l_I])); - - /// Refresh - m_UsersList.TableViewInstance.ReloadData(); - - /// Update UI - m_UsersUpButton.interactable = m_CurrentPage > 1; - m_UsersDownButton.interactable = m_CurrentPage < m_PageCount; - } - /// - /// Clear the user list - /// - private void ClearDisplayedData() - { - if (!UICreated) - return; - - m_UsersList.TableViewInstance.ClearSelection(); - m_UsersList.Data.Clear(); - m_UsersList.TableViewInstance.ReloadData(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// TimeOut an user - /// - [UIAction("click-timeout-btn-pressed")] - private void OnTimeOutButton() - { - if (m_SelectedIndex == -1) - { - ShowMessageModal("Please select an user first!"); - return; - } - - if (!(m_Users[m_SelectedIndex].Item1 is CP_SDK.Chat.Services.Twitch.TwitchService)) - { - ShowMessageModal("Only twitch is supported at the moment!"); - return; - } - - ShowConfirmationModal($"Do you really want to TimeOut user\n{m_Users[m_SelectedIndex].Item2.DisplayName}?", () => { - foreach (var l_Current in CP_SDK.Chat.Service.Multiplexer.Channels) - { - if (l_Current.Item1 is CP_SDK.Chat.Services.Twitch.TwitchService) - l_Current.Item1.SendTextMessage(l_Current.Item2, $"/timeout {m_Users[m_SelectedIndex].Item2.UserName}"); - } - }); - } - /// - /// Ban an user - /// - [UIAction("click-ban-btn-pressed")] - private void OnBanButton() - { - if (m_SelectedIndex == -1) - { - ShowMessageModal("Please select an user first!"); - return; - } - - if (!(m_Users[m_SelectedIndex].Item1 is CP_SDK.Chat.Services.Twitch.TwitchService)) - { - ShowMessageModal("Only twitch is supported at the moment!"); - return; - } - - ShowConfirmationModal($"Do you really want to Ban user\n{m_Users[m_SelectedIndex].Item2.DisplayName}?", () => { - foreach (var l_Current in CP_SDK.Chat.Service.Multiplexer.Channels) - { - if (l_Current.Item1 is CP_SDK.Chat.Services.Twitch.TwitchService) - l_Current.Item1.SendTextMessage(l_Current.Item2, $"/ban {m_Users[m_SelectedIndex].Item2.UserName}"); - } - }); - } - /// - /// Mod an user - /// - [UIAction("click-mod-btn-pressed")] - private void OnModButton() - { - if (m_SelectedIndex == -1) - { - ShowMessageModal("Please select an user first!"); - return; - } - - if (!(m_Users[m_SelectedIndex].Item1 is CP_SDK.Chat.Services.Twitch.TwitchService)) - { - ShowMessageModal("Only twitch is supported at the moment!"); - return; - } - - ShowConfirmationModal($"Do you really want to Mod user\n{m_Users[m_SelectedIndex].Item2.DisplayName}?", () => { - foreach (var l_Current in CP_SDK.Chat.Service.Multiplexer.Channels) - { - if (l_Current.Item1 is CP_SDK.Chat.Services.Twitch.TwitchService) - l_Current.Item1.SendTextMessage(l_Current.Item2, $"/mod {m_Users[m_SelectedIndex].Item2.UserName}"); - } - }); - } - /// - /// UnMod an user - /// - [UIAction("click-unmod-btn-pressed")] - private void OnUnModButton() - { - if (m_SelectedIndex == -1) - { - ShowMessageModal("Please select an user first!"); - return; - } - - if (!(m_Users[m_SelectedIndex].Item1 is CP_SDK.Chat.Services.Twitch.TwitchService)) - { - ShowMessageModal("Only twitch is supported at the moment!"); - return; - } - - ShowConfirmationModal($"Do you really want to UnMod user\n{m_Users[m_SelectedIndex].Item2.DisplayName}?", () => { - foreach (var l_Current in CP_SDK.Chat.Service.Multiplexer.Channels) - { - if (l_Current.Item1 is CP_SDK.Chat.Services.Twitch.TwitchService) - l_Current.Item1.SendTextMessage(l_Current.Item2, $"/unmod {m_Users[m_SelectedIndex].Item2.UserName}"); - } - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build user line - /// - /// User entry - /// Built user line - private (string, string) BuildLineString((IChatService, IChatUser) p_Item) - { - /// Result line - string l_Text = "[" + p_Item.Item1.DisplayName + "] "; - - /// Handle request limits - if (p_Item.Item2.IsModerator || p_Item.Item2.IsBroadcaster) - l_Text += "🗡 "; - else if (p_Item.Item2.IsVip) - l_Text += "💎 "; - else if (p_Item.Item2.IsSubscriber) - l_Text += "👑 "; - - l_Text += p_Item.Item2.DisplayName; - - return (l_Text, null); - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/ModerationShortcut.bsml b/Modules/BeatSaberPlus_Chat/UI/ModerationShortcut.bsml deleted file mode 100644 index 0affe2e..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ModerationShortcut.bsml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/ModerationShortcut.cs b/Modules/BeatSaberPlus_Chat/UI/ModerationShortcut.cs deleted file mode 100644 index b40fa8b..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ModerationShortcut.cs +++ /dev/null @@ -1,272 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using BeatSaberMarkupLanguage.ViewControllers; -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using UnityEngine.UI; - -namespace ChatPlexMod_Chat.UI -{ - internal class ModerationShortcut : BeatSaberPlus.SDK.UI.ResourceViewController - { - /// - /// Event line per page - /// - private static int EVENT_PER_PAGE = 8; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIComponent("ShortcutUpButton")] - private Button m_ShortcutUpButton = null; - [UIObject("Shortcut_Background")] - private GameObject m_Shortcut_Background = null; - [UIObject("ShortcutList")] - private GameObject m_ShortcutListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_ShortcutList = null; - [UIComponent("ShortcutDownButton")] - private Button m_ShortcutDownButton = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("NewKeyboard")] - private ModalKeyboard m_NewKeyboard = null; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Filtered list - /// - private List m_FilteredList = new List(); - /// - /// Current event list page - /// - private int m_CurrentPage = 1; - /// - /// Selected index - /// - private int m_SelectedIndex = -1; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override void OnViewCreation() - { - /// Update background color - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Shortcut_Background, 0.5f); - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_NewKeyboard.modalView, 0.75f); - - /// Scale down up & down button - m_ShortcutUpButton.transform.localScale = Vector3.one * 0.5f; - m_ShortcutDownButton.transform.localScale = Vector3.one * 0.5f; - - /// Prepare event list - var l_BSMLTableView = m_ShortcutListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_ShortcutListView.GetComponentInChildren()); - m_ShortcutList = l_BSMLTableView.gameObject.AddComponent(); - m_ShortcutList.TableViewInstance = l_BSMLTableView; - m_ShortcutList.CellSizeValue = 4.8f; - l_BSMLTableView.didSelectCellWithIdxEvent += OnShortcutSelected; - l_BSMLTableView.SetDataSource(m_ShortcutList, false); - - /// Bind events - m_ShortcutUpButton.onClick.AddListener(OnPageUpPressed); - m_ShortcutDownButton.onClick.AddListener(OnPageDownPressed); - - /// Build the shortcut list - m_FilteredList = new List(CConfig.Instance.ModerationKeys); - } - /// - /// On view activation - /// - protected override void OnViewActivation() - { - /// Rebuild list - m_CurrentPage = 1; - RebuildList(null); - } - /// - /// On view deactivation - /// - protected override void OnViewDeactivation() - { - CConfig.Instance.ModerationKeys = new List(m_FilteredList); - CConfig.Instance.Save(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous shortcut page - /// - private void OnPageUpPressed() - { - /// Underflow check - if (m_CurrentPage < 2) - return; - - /// Decrement current page - m_CurrentPage--; - - /// Rebuild list - RebuildList(null); - } - /// - /// Rebuild list - /// - /// Event to auto select - private void RebuildList(string p_ShortcutToFocus) - { - if (!UICreated) - return; - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(m_FilteredList.Count) / (float)(EVENT_PER_PAGE))); - - if (p_ShortcutToFocus != null) - { - var l_Index = m_FilteredList.IndexOf(p_ShortcutToFocus); - if (l_Index != -1) - m_CurrentPage = (l_Index / EVENT_PER_PAGE) + 1; - else - OnShortcutSelected(null, -1); - } - - /// Update overflow - m_CurrentPage = Math.Max(1, Math.Min(m_CurrentPage, l_PageCount)); - - /// Update UI - m_ShortcutUpButton.interactable = m_CurrentPage > 1; - m_ShortcutDownButton.interactable = m_CurrentPage < l_PageCount; - - /// Clear old entries - m_ShortcutList.TableViewInstance.ClearSelection(); - m_ShortcutList.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (m_CurrentPage - 1) * EVENT_PER_PAGE; - l_I < m_FilteredList.Count && l_I < (m_CurrentPage * EVENT_PER_PAGE); - ++l_I) - { - var l_ShortCut = m_FilteredList[l_I]; - - m_ShortcutList.Data.Add((l_ShortCut, null)); - - if (l_ShortCut == p_ShortcutToFocus) - l_RelIndexToFocus = m_ShortcutList.Data.Count - 1; - } - - /// Refresh - m_ShortcutList.TableViewInstance.ReloadData(); - - /// Update focus - if (m_FilteredList.Count == 0) - OnShortcutSelected(null, -1); - else if (l_RelIndexToFocus != -1) - m_ShortcutList.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// On shortcut selected - /// - /// TableView instance - /// Relative index - private void OnShortcutSelected(HMUI.TableView p_TableView, int p_RelIndex) - { - int l_EventIndex = ((m_CurrentPage - 1) * EVENT_PER_PAGE) + p_RelIndex; - - if (p_RelIndex < 0 || l_EventIndex >= m_FilteredList.Count) - { - m_SelectedIndex = -1; - return; - } - - m_SelectedIndex = l_EventIndex; - } - /// - /// Go to next shortcut page - /// - private void OnPageDownPressed() - { - /// Increment current page - m_CurrentPage++; - - /// Rebuild list - RebuildList(null); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// New shortcut button - /// - [UIAction("click-new-btn-pressed")] - private void OnNewButton() - { - m_NewKeyboard.SetText(""); - ShowModal("OpenNewModal"); - } - /// - /// Delete shortcut button - /// - [UIAction("click-delete-btn-pressed")] - private void OnDeleteButton() - { - if (!EnsureShortcutSelected()) - return; - - var l_Shortcut = m_FilteredList[m_SelectedIndex]; - - ShowConfirmationModal($"Do you want to delete shortcut\n\"{l_Shortcut}\"?", () => - { - OnShortcutSelected(null, -1); - m_FilteredList.Remove(l_Shortcut); - RebuildList(null); - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On rename keyboard enter pressed - /// - /// - [UIAction("NewKeyboardPressed")] - internal void NewKeyboardPressed(string p_Text) - { - m_FilteredList.Add(p_Text); - RebuildList(p_Text); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Ensure that an shortcut is selected - /// - /// - private bool EnsureShortcutSelected() - { - if (m_SelectedIndex == -1) - { - ShowMessageModal("Please select an shortcut first!"); - return false; - } - - return true; - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/ModerationViewFlowCoordinator.cs b/Modules/BeatSaberPlus_Chat/UI/ModerationViewFlowCoordinator.cs deleted file mode 100644 index 5934061..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/ModerationViewFlowCoordinator.cs +++ /dev/null @@ -1,113 +0,0 @@ -using HMUI; -using UnityEngine; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Moderation UI flow coordinator - /// - internal class ModerationViewFlowCoordinator : BeatSaberPlus.SDK.UI.ViewFlowCoordinator - { - /// - /// Title - /// - public override string Title => "Chat Moderation"; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Main view - /// - private ModerationMain m_MainView; - /// - /// Left view - /// - private ModerationLeft m_LeftView; - /// - /// Details view - /// - private ModerationRight m_RightView; - /// - /// Moderation shortcut view - /// - private ModerationShortcut m_ShortcutMainView; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get initial views controller - /// - /// (Middle, Left, Right) - protected override sealed (ViewController, ViewController, ViewController) GetInitialViewsController() => (m_MainView, m_LeftView, m_RightView); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - internal ModerationViewFlowCoordinator() - { - m_MainView = CreateViewController(); - m_LeftView = CreateViewController(); - m_RightView = CreateViewController(); - m_ShortcutMainView = CreateViewController(); - } - /// - /// On destroy - /// - internal void OnDestroy() - { - if (m_ShortcutMainView != null) - { - GameObject.Destroy(m_ShortcutMainView.gameObject); - m_ShortcutMainView = null; - } - if (m_MainView != null) - { - GameObject.Destroy(m_MainView.gameObject); - m_MainView = null; - } - if (m_LeftView != null) - { - GameObject.Destroy(m_LeftView.gameObject); - m_LeftView = null; - } - if (m_RightView != null) - { - GameObject.Destroy(m_RightView.gameObject); - m_RightView = null; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Switch to shortcut view - /// - internal void SwitchToShortcut() - => ChangeView(m_ShortcutMainView); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On back button pressed - /// - /// Current top view controller - /// True if the event is catched, false if we should dismiss the flow coordinator - protected override sealed bool OnBackButtonPressed(HMUI.ViewController p_TopViewController) - { - if (p_TopViewController == m_ShortcutMainView) - { - ChangeView(m_MainView, m_LeftView, m_RightView); - return true; - } - - return false; - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/PollFloatingWindow.bsml b/Modules/BeatSaberPlus_Chat/UI/PollFloatingWindow.bsml deleted file mode 100644 index 1d5cbf9..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/PollFloatingWindow.bsml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/PredictionFloatingWindow.bsml b/Modules/BeatSaberPlus_Chat/UI/PredictionFloatingWindow.bsml deleted file mode 100644 index c28dab0..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/PredictionFloatingWindow.bsml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/Settings.bsml b/Modules/BeatSaberPlus_Chat/UI/Settings.bsml deleted file mode 100644 index 6c2901e..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/Settings.bsml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/Settings.cs b/Modules/BeatSaberPlus_Chat/UI/Settings.cs deleted file mode 100644 index 22da2cf..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/Settings.cs +++ /dev/null @@ -1,132 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using UnityEngine; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Stream chat settings view - /// - internal class Settings : BeatSaberPlus.SDK.UI.ResourceViewController - { -#pragma warning disable CS0649 - [UIComponent("chat-width")] - public IncrementSetting m_ChatWidth; - [UIComponent("chat-height")] - public IncrementSetting m_ChatHeight; - [UIComponent("chat-reverse")] - private ToggleSetting m_ChatReverse; - [UIComponent("chat-opacity")] - private IncrementSetting m_ChatOpacity; - [UIComponent("chat-fontsize")] - private IncrementSetting m_ChatFontSize; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("chat-background")] - private ColorSetting m_ChatBackgroundColor; - [UIComponent("chat-highlight")] - private ColorSetting m_ChatHighlightColor; - [UIComponent("chat-accent")] - private ColorSetting m_ChatAccentColor; - [UIComponent("chat-text")] - private ColorSetting m_ChatTextColor; - [UIComponent("chat-ping")] - private ColorSetting m_ChatPingColor; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - - /// Left - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_ChatWidth, l_Event, null, CConfig.Instance.ChatSize.x, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_ChatHeight, l_Event, null, CConfig.Instance.ChatSize.y, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ChatReverse, l_Event, CConfig.Instance.ReverseChatOrder, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_ChatOpacity, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, CConfig.Instance.BackgroundColor.a, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_ChatFontSize, l_Event, null, CConfig.Instance.FontSize, true); - - /// Right - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_ChatBackgroundColor, l_Event, CConfig.Instance.BackgroundColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_ChatHighlightColor, l_Event, CConfig.Instance.HighlightColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_ChatAccentColor, l_Event, CConfig.Instance.AccentColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_ChatTextColor, l_Event, CConfig.Instance.TextColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_ChatPingColor, l_Event, CConfig.Instance.PingColor, true); - } - /// - /// On view deactivation - /// - protected override sealed void OnViewDeactivation() - { - CConfig.Instance.Save(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When settings are changed - /// - /// - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - /// Update config - CConfig.Instance.ChatSize = new Vector2((int)m_ChatWidth.Value, (int)m_ChatHeight.Value); - CConfig.Instance.ReverseChatOrder = m_ChatReverse.Value; - CConfig.Instance.FontSize = m_ChatFontSize.Value; - CConfig.Instance.BackgroundColor = m_ChatBackgroundColor.CurrentColor.ColorWithAlpha(m_ChatOpacity.Value); - CConfig.Instance.HighlightColor = m_ChatHighlightColor.CurrentColor; - CConfig.Instance.AccentColor = m_ChatAccentColor.CurrentColor; - CConfig.Instance.TextColor = m_ChatTextColor.CurrentColor; - CConfig.Instance.PingColor = m_ChatPingColor.CurrentColor; - - /// Update floating view - Chat.Instance.UpdateFloatingWindow(CP_SDK.ChatPlexSDK.ActiveGenericScene, false); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset settings - /// - internal void RefreshSettings() - { - m_PreventChanges = true; - - /// Set values - m_ChatWidth.Value = CConfig.Instance.ChatSize.x; - m_ChatHeight.Value = CConfig.Instance.ChatSize.y; - m_ChatReverse.Value = CConfig.Instance.ReverseChatOrder; - m_ChatOpacity.Value = CConfig.Instance.BackgroundColor.a; - m_ChatFontSize.Value = CConfig.Instance.FontSize; - - /// Set values - m_ChatBackgroundColor.CurrentColor = CConfig.Instance.BackgroundColor.ColorWithAlpha(1f); - m_ChatHighlightColor.CurrentColor = CConfig.Instance.HighlightColor.ColorWithAlpha(1f); - m_ChatAccentColor.CurrentColor = CConfig.Instance.AccentColor.ColorWithAlpha(1f); - m_ChatTextColor.CurrentColor = CConfig.Instance.TextColor.ColorWithAlpha(1f); - m_ChatPingColor.CurrentColor = CConfig.Instance.PingColor.ColorWithAlpha(1f); - - m_PreventChanges = false; - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/SettingsLeft.bsml b/Modules/BeatSaberPlus_Chat/UI/SettingsLeft.bsml deleted file mode 100644 index 1cedf39..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/SettingsLeft.bsml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/SettingsLeft.cs b/Modules/BeatSaberPlus_Chat/UI/SettingsLeft.cs deleted file mode 100644 index ba0a861..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/SettingsLeft.cs +++ /dev/null @@ -1,105 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using System.Diagnostics; -using UnityEngine; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Stream chat credits - /// - internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController - { -#pragma warning disable CS0414 - [UIObject("Background")] - internal GameObject m_Background = null; - [UIValue("Line1")] - private readonly string m_Line1 = "Thanks to brian for original ChatCore lib"; - [UIValue("Line2")] - private readonly string m_Line2 = " - https://github.com/brian91292/ChatCore"; - [UIValue("Line3")] - private readonly string m_Line3 = " "; - [UIValue("Line4")] - private readonly string m_Line4 = " "; - [UIValue("Line5")] - private readonly string m_Line5 = " "; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset button - /// - [UIAction("click-reset-btn-pressed")] - private void OnResetButton() - { - ShowConfirmationModal("Do you really want to reset all chat settings?", () => - { - /// Reset settings - CConfig.Instance.Reset(); - CConfig.Instance.Enabled = true; - CConfig.Instance.Save(); - - /// Refresh values - Settings.Instance.RefreshSettings(); - SettingsRight.Instance.RefreshSettings(); - - /// Update floating view - Chat.Instance.UpdateFloatingWindow(CP_SDK.ChatPlexSDK.ActiveGenericScene, true); - }); - } - /// - /// Reset position button - /// - [UIAction("click-reset-position-btn-pressed")] - private void OnResetPositionButton() - { - ShowConfirmationModal("Do you really want to reset chat position?", () => - { - /// Reset position settings - CConfig.Instance.ResetPosition(); - CConfig.Instance.Save(); - - /// Refresh values - Settings.Instance.RefreshSettings(); - SettingsRight.Instance.RefreshSettings(); - - /// Update floating view - Chat.Instance.UpdateFloatingWindow(CP_SDK.ChatPlexSDK.ActiveGenericScene, true); - }); - } - /// - /// Open web configuration button - /// - [UIAction("click-open-web-configuration-btn-pressed")] - private void OnWebConfigurationButton() - { - ShowMessageModal("URL opened in your desktop browser."); - CP_SDK.Chat.Service.OpenWebConfigurator(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Documentation button - /// - [UIAction("click-documentation-btn-pressed")] - private void OnDocumentationButton() - { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://github.com/hardcpp/BeatSaberPlus/wiki#chat"); - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/UI/SettingsRight.bsml b/Modules/BeatSaberPlus_Chat/UI/SettingsRight.bsml deleted file mode 100644 index 0f45f68..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/SettingsRight.bsml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_Chat/UI/SettingsRight.cs b/Modules/BeatSaberPlus_Chat/UI/SettingsRight.cs deleted file mode 100644 index 1c02af2..0000000 --- a/Modules/BeatSaberPlus_Chat/UI/SettingsRight.cs +++ /dev/null @@ -1,128 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; - -namespace ChatPlexMod_Chat.UI -{ - /// - /// Chat filters settings - /// - internal class SettingsRight : BeatSaberPlus.SDK.UI.ResourceViewController - { -#pragma warning disable CS0649 - [UIComponent("alignwithfloor-toggle")] - public ToggleSetting m_AlignWithFloor; - [UIComponent("showlockicon-toggle")] - public ToggleSetting m_ShowLockIcon; - [UIComponent("followenvironementrotations-toggle")] - public ToggleSetting m_FollowEnvironementRotations; - [UIComponent("chat-viewercount")] - private ToggleSetting m_ChatViewerCount; - [UIComponent("chat-filterviewers")] - private ToggleSetting m_ChatFitlerViewers; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("followevent-toggle")] - public ToggleSetting m_FollowEvents; - [UIComponent("subscriptionevents-toggle")] - public ToggleSetting m_SubscriptionEvents; - [UIComponent("bitscheering-toggle")] - public ToggleSetting m_BitsCheering; - [UIComponent("channelpoints-toggle")] - public ToggleSetting m_ChannelPoints; - [UIComponent("chat-filterbroadcaster")] - private ToggleSetting m_ChatFilterBroadcaster; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - - /// Prepare - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_AlignWithFloor, l_Event, CConfig.Instance.AlignWithFloor, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ShowLockIcon, l_Event, CConfig.Instance.ShowLockIcon, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_FollowEnvironementRotations, l_Event, CConfig.Instance.FollowEnvironementRotation, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ChatViewerCount, l_Event, CConfig.Instance.ShowViewerCount, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ChatFitlerViewers, l_Event, CConfig.Instance.FilterViewersCommands, true); - - /// Prepare - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_FollowEvents, l_Event, CConfig.Instance.ShowFollowEvents, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SubscriptionEvents, l_Event, CConfig.Instance.ShowSubscriptionEvents, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_BitsCheering, l_Event, CConfig.Instance.ShowBitsCheeringEvents, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ChannelPoints, l_Event, CConfig.Instance.ShowChannelPointsEvent, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ChatFilterBroadcaster, l_Event, CConfig.Instance.FilterBroadcasterCommands, true); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When settings are changed - /// - /// - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - /// Update config - CConfig.Instance.AlignWithFloor = m_AlignWithFloor.Value; - CConfig.Instance.ShowLockIcon = m_ShowLockIcon.Value; - CConfig.Instance.FollowEnvironementRotation = m_FollowEnvironementRotations.Value; - CConfig.Instance.ShowViewerCount = m_ChatViewerCount.Value; - CConfig.Instance.FilterViewersCommands = m_ChatFitlerViewers.Value; - - /// Set values - CConfig.Instance.ShowFollowEvents = m_FollowEvents.Value; - CConfig.Instance.ShowSubscriptionEvents = m_SubscriptionEvents.Value; - CConfig.Instance.ShowBitsCheeringEvents = m_BitsCheering.Value; - CConfig.Instance.ShowChannelPointsEvent = m_ChannelPoints.Value; - CConfig.Instance.FilterBroadcasterCommands = m_ChatFilterBroadcaster.Value; - - /// Update floating view - Chat.Instance.UpdateFloatingWindow(CP_SDK.ChatPlexSDK.ActiveGenericScene, false); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset settings - /// - internal void RefreshSettings() - { - m_PreventChanges = true; - - /// Set values - m_AlignWithFloor.Value = CConfig.Instance.AlignWithFloor; - m_ShowLockIcon.Value = CConfig.Instance.ShowLockIcon; - m_FollowEnvironementRotations.Value = CConfig.Instance.FollowEnvironementRotation; - m_ChatViewerCount.Value = CConfig.Instance.ShowViewerCount; - m_ChatFitlerViewers.Value = CConfig.Instance.FilterViewersCommands; - - /// Set values - m_FollowEvents.Value = CConfig.Instance.ShowFollowEvents; - m_SubscriptionEvents.Value = CConfig.Instance.ShowSubscriptionEvents; - m_BitsCheering.Value = CConfig.Instance.ShowBitsCheeringEvents; - m_ChannelPoints.Value = CConfig.Instance.ShowChannelPointsEvent; - m_ChatFilterBroadcaster.Value = CConfig.Instance.FilterBroadcasterCommands; - - m_PreventChanges = false; - } - } -} diff --git a/Modules/BeatSaberPlus_Chat/manifest.json b/Modules/BeatSaberPlus_Chat/manifest.json index a76672e..1170bb1 100644 --- a/Modules/BeatSaberPlus_Chat/manifest.json +++ b/Modules/BeatSaberPlus_Chat/manifest.json @@ -3,13 +3,12 @@ "id": "BeatSaberPlus_Chat", "name": "BeatSaberPlus_Chat", "author": "HardCPP#1985", - "version": "5.0.7", + "version": "6.0.8", "description": "", - "gameVersion": "1.25.0", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.0.1", - "BeatSaberMarkupLanguage": "^1.3.4", - "BeatSaberPlusCORE": "^5.0.7" + "BSIPA": "^4.3.0", + "BeatSaberPlusCORE": "^6.0.8" }, "links": { "project-home": "https://discord.chatplex.org", diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/Plugin.cs b/Modules/BeatSaberPlus_ChatEmoteRain/BSIPA.cs similarity index 83% rename from Modules/BeatSaberPlus_ChatEmoteRain/Plugin.cs rename to Modules/BeatSaberPlus_ChatEmoteRain/BSIPA.cs index d18e742..ba1e3e3 100644 --- a/Modules/BeatSaberPlus_ChatEmoteRain/Plugin.cs +++ b/Modules/BeatSaberPlus_ChatEmoteRain/BSIPA.cs @@ -1,5 +1,4 @@ -using ChatPlexMod_ChatEmoteRain; -using IPA; +using IPA; namespace BeatSaberPlus_ChatEmoteRain { @@ -7,17 +6,17 @@ namespace BeatSaberPlus_ChatEmoteRain /// Main plugin class /// [Plugin(RuntimeOptions.SingleStartInit)] - public class Plugin + public class BSIPA { /// /// Called when the plugin is first loaded by IPA (either when the game starts or when the plugin is enabled if it starts disabled). /// /// Logger instance [Init] - public Plugin(IPA.Logging.Logger p_Logger) + public BSIPA(IPA.Logging.Logger p_Logger) { /// Setup logger - Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); + ChatPlexMod_ChatEmoteRain.Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); } //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/BeatSaberPlus_ChatEmoteRain.csproj b/Modules/BeatSaberPlus_ChatEmoteRain/BeatSaberPlus_ChatEmoteRain.csproj index c3b90aa..8e8043a 100644 --- a/Modules/BeatSaberPlus_ChatEmoteRain/BeatSaberPlus_ChatEmoteRain.csproj +++ b/Modules/BeatSaberPlus_ChatEmoteRain/BeatSaberPlus_ChatEmoteRain.csproj @@ -47,34 +47,16 @@ OnBuildSuccess - - $(BeatSaberDir)\Plugins\BSML.dll - False - False - $(BeatSaberDir)\Libs\Newtonsoft.Json.dll False False - - - - - $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll - False - $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll False @@ -101,8 +83,8 @@ False False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.PhysicsModule.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll False False @@ -110,48 +92,24 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll - False - - - - - + + + + - - - - + + + + - - Settings.cs - - - - EmitterWidget.cs - - - SettingsLeft.cs - - - SettingsRight.cs - + diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/BeatSaberPlus_ChatEmoteRain.csproj.user b/Modules/BeatSaberPlus_ChatEmoteRain/BeatSaberPlus_ChatEmoteRain.csproj.user index 5db8d9ee60929239d9779dc6a24adfdc0c01779f..012781eeb9f58b5f0dd22267773d93a2d711944f 100644 GIT binary patch delta 25 fcmaFI@`q)^I!0av215ot1|tSbAZa*xE#pA|UV#Rx delta 11 Tcmeyv@{VQ0I>yO+7!LpdA+QBv diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/CERConfig.cs b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/CERConfig.cs similarity index 71% rename from Modules/BeatSaberPlus_ChatEmoteRain/CERConfig.cs rename to Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/CERConfig.cs index cb38e49..0df6838 100644 --- a/Modules/BeatSaberPlus_ChatEmoteRain/CERConfig.cs +++ b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/CERConfig.cs @@ -59,6 +59,56 @@ protected override void OnInit(bool p_OnCreation) { if (p_OnCreation) { +#if BOOMBOX + MenuEmitters.Add(new EmitterConfig() + { + Enabled = true, + Name = "Top", + Size = 1.00f, + Speed = 1.00f, + PosX = 0.00f, + PosY = 6.016f, + PosZ = 4.05f, + RotX = 90.00f, + RotY = 0.00f, + RotZ = 0.00f, + SizeX = 6.84f, + SizeY = 2.88f, + SizeZ = 2.09f + }); + SongEmitters.Add(new EmitterConfig() + { + Enabled = true, + Name = "LeftSide", + Size = 1.00f, + Speed = 1.00f, + PosX = -10.00f, + PosY = 9.00f, + PosZ = 13.728f, + RotX = 90.00f, + RotY = 0.00f, + RotZ = -31.24f, + SizeX = 10.00f, + SizeY = 4.50f, + SizeZ = 6.12f + }); + SongEmitters.Add(new EmitterConfig() + { + Enabled = true, + Name = "RightSide", + Size = 1.00f, + Speed = 1.00f, + PosX = 10.00f, + PosY = 9.00f, + PosZ = 13.728f, + RotX = 90.00f, + RotY = 0.00f, + RotZ = -31.24f, + SizeX = 10.00f, + SizeY = 4.50f, + SizeZ = 6.12f + }); +#else MenuEmitters.Add(new EmitterConfig() { Enabled = true, @@ -107,6 +157,7 @@ protected override void OnInit(bool p_OnCreation) SizeY = 4.50f, SizeZ = 4.40f }); +#endif } //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/ChatEmoteRain.cs b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/ChatEmoteRain.cs similarity index 80% rename from Modules/BeatSaberPlus_ChatEmoteRain/ChatEmoteRain.cs rename to Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/ChatEmoteRain.cs index 854d80d..58255be 100644 --- a/Modules/BeatSaberPlus_ChatEmoteRain/ChatEmoteRain.cs +++ b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/ChatEmoteRain.cs @@ -1,6 +1,4 @@ -using BeatSaberMarkupLanguage; -using CP_SDK.Chat.Interfaces; -using System; +using CP_SDK.Chat.Interfaces; using System.Collections.Generic; using System.IO; using System.Linq; @@ -14,84 +12,38 @@ namespace ChatPlexMod_ChatEmoteRain /// /// Chat Emote Rain instance /// - public class ChatEmoteRain : BeatSaberPlus.SDK.BSPModuleBase + public class ChatEmoteRain : CP_SDK.ModuleBase { - /// - /// Warm-up size per scene - /// - private static int POOL_SIZE_PER_SCENE = 50; + private static int POOL_SIZE_PER_SCENE = 50; + private static string CUSTOM_RAIN_PATH = Path.Combine(CP_SDK.ChatPlexSDK.BasePath, "CustomSubRain"); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Module type - /// - public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; - /// - /// Name of the Module - /// - public override string Name => "Chat Emote Rain"; - /// - /// Description of the Module - /// - public override string Description => "Make chat emotes rain in game!"; - /// - /// Is the Module using chat features - /// - public override bool UseChatFeatures => true; - /// - /// Is enabled - /// - public override bool IsEnabled { get => CERConfig.Instance.Enabled; set { CERConfig.Instance.Enabled = value; CERConfig.Instance.Save(); } } - /// - /// Activation kind - /// - public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; + public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; + public override string Name => "Chat Emote Rain"; + public override string Description => "Make chat emotes rain in game!"; + public override string DocumentationURL => "https://github.com/hardcpp/BeatSaberPlus/wiki#chat-emote-rain"; + public override bool UseChatFeatures => true; + public override bool IsEnabled { get => CERConfig.Instance.Enabled; set { CERConfig.Instance.Enabled = value; CERConfig.Instance.Save(); } } + public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Emote rain view - /// - private UI.Settings m_SettingsView = null; - /// - /// Emote rain left view - /// - private UI.SettingsLeft m_SettingsLeftView = null; - /// - /// Emote rain right view - /// - private UI.SettingsRight m_SettingsRightView = null; - /// - /// Chat core instance - /// + private UI.SettingsLeftView m_SettingsLeftView = null; + private UI.SettingsMainView m_SettingsMainView = null; + private UI.SettingsRightView m_SettingsRightView = null; + private bool m_ChatCoreAcquired = false; - /// - /// Preview material asset bundle - /// + private AssetBundle m_PreviewMateralAssetBundle = null; - /// - /// Preview material - /// - private Material m_PreviewMaterial; - /// - /// Menu emitter manager - /// - private CP_SDK.Unity.Components.EnhancedImageParticleEmitterManager m_MenuManager; - /// - /// Playing emitter manager - /// - private CP_SDK.Unity.Components.EnhancedImageParticleEmitterManager m_PlayingManager; - /// - /// SubRain emotes - /// - private List m_SubRainTextures = new List(); - /// - /// Temp disable - /// - private bool m_TempDisable = false; + private Material m_PreviewMaterial = null; + + private CP_SDK.Unity.Components.EnhancedImageParticleEmitterManager m_MenuManager = null; + private CP_SDK.Unity.Components.EnhancedImageParticleEmitterManager m_PlayingManager = null; + private List m_SubRainTextures = new List(); + private bool m_TempDisable = false; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -105,8 +57,8 @@ protected override void OnEnable() CP_SDK.ChatPlexSDK.OnGenericSceneChange += ChatPlexSDK_OnGenericSceneChange; /// Create CustomMenuSongs directory if not existing - if (!Directory.Exists("CustomSubRain")) - Directory.CreateDirectory("CustomSubRain"); + if (!Directory.Exists(CUSTOM_RAIN_PATH)) + Directory.CreateDirectory(CUSTOM_RAIN_PATH); LoadAssets(); @@ -151,6 +103,10 @@ protected override void OnDisable() if (m_MenuManager != null) GameObject.DestroyImmediate(m_MenuManager.gameObject); if (m_PlayingManager != null) GameObject.DestroyImmediate(m_PlayingManager.gameObject); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsLeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsMainView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsRightView); + /// Unload assets UnloadAssets(); @@ -167,20 +123,13 @@ protected override void OnDisable() /// /// Get Module settings UI /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsLeftView == null) - m_SettingsLeftView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsRightView == null) - m_SettingsRightView = BeatSaberUI.CreateViewController(); - - /// Change main view - return (m_SettingsView, m_SettingsLeftView, m_SettingsRightView); + if (m_SettingsLeftView == null) m_SettingsLeftView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsRightView == null) m_SettingsRightView = CP_SDK.UI.UISystem.CreateViewController(); + + return (m_SettingsMainView, m_SettingsLeftView, m_SettingsRightView); } //////////////////////////////////////////////////////////////////////////// @@ -213,7 +162,14 @@ private void ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.EGenericScene p /// internal void OnSettingsChanged() { + m_MenuManager.Size = CERConfig.Instance.MenuSize; + m_MenuManager.Speed = CERConfig.Instance.MenuSpeed; + m_MenuManager.Delay = CERConfig.Instance.EmoteDelay; m_MenuManager.UpdateFromConfig(); + + m_PlayingManager.Size = CERConfig.Instance.SongSize; + m_PlayingManager.Speed = CERConfig.Instance.SongSpeed; + m_PlayingManager.Delay = CERConfig.Instance.EmoteDelay; m_PlayingManager.UpdateFromConfig(); } @@ -233,10 +189,8 @@ internal void UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene p_Scene) } internal void SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene p_Scene, bool p_Enabled, EmitterConfig p_Focus) { - if (p_Scene == CP_SDK.ChatPlexSDK.EGenericScene.Menu) - m_MenuManager.SetPreview(p_Enabled, p_Focus); - else if (p_Scene == CP_SDK.ChatPlexSDK.EGenericScene.Playing) - m_PlayingManager.SetPreview(p_Enabled, p_Focus); + if (p_Scene == CP_SDK.ChatPlexSDK.EGenericScene.Menu) m_MenuManager.SetPreview(p_Enabled, p_Focus); + else if (p_Scene == CP_SDK.ChatPlexSDK.EGenericScene.Playing) m_PlayingManager.SetPreview(p_Enabled, p_Focus); } //////////////////////////////////////////////////////////////////////////// @@ -247,7 +201,9 @@ internal void SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene p_Scene, bool /// private void LoadAssets() { - m_PreviewMateralAssetBundle = AssetBundle.LoadFromMemory(CP_SDK.Misc.Resources.FromRelPath(Assembly.GetExecutingAssembly(), "Resources.PreviewMaterial.bundle")); + m_PreviewMateralAssetBundle = AssetBundle.LoadFromMemory( + CP_SDK.Misc.Resources.FromRelPath(Assembly.GetExecutingAssembly(), "ChatPlexMod_ChatEmoteRain.Resources.PreviewMaterial.bundle") + ); m_PreviewMaterial = m_PreviewMateralAssetBundle.LoadAsset("PreviewMaterial"); } @@ -256,11 +212,11 @@ private void LoadAssets() /// private void UnloadAssets() { - if (m_PreviewMateralAssetBundle != null) - { - m_PreviewMateralAssetBundle.Unload(true); - m_PreviewMateralAssetBundle = null; - } + if (m_PreviewMateralAssetBundle == null) + return; + + m_PreviewMateralAssetBundle.Unload(true); + m_PreviewMateralAssetBundle = null; } //////////////////////////////////////////////////////////////////////////// @@ -273,9 +229,9 @@ internal void LoadSubRainFiles() { m_SubRainTextures.Clear(); - var l_Files = Directory.GetFiles("CustomSubRain", "*.png") - .Union(Directory.GetFiles("CustomSubRain", "*.gif")) - .Union(Directory.GetFiles("CustomSubRain", "*.apng")).ToArray(); + var l_Files = Directory.GetFiles(CUSTOM_RAIN_PATH, "*.png") + .Union(Directory.GetFiles(CUSTOM_RAIN_PATH, "*.gif")) + .Union(Directory.GetFiles(CUSTOM_RAIN_PATH, "*.apng")).ToArray(); foreach (string l_CurrentFile in l_Files) { diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/Logger.cs b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/Logger.cs similarity index 100% rename from Modules/BeatSaberPlus_ChatEmoteRain/Logger.cs rename to Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/Logger.cs diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/Resources/PreviewMaterial.bundle b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/Resources/PreviewMaterial.bundle similarity index 100% rename from Modules/BeatSaberPlus_ChatEmoteRain/Resources/PreviewMaterial.bundle rename to Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/Resources/PreviewMaterial.bundle diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsLeftView.cs b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsLeftView.cs new file mode 100644 index 0000000..74c6ee7 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsLeftView.cs @@ -0,0 +1,90 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace ChatPlexMod_ChatEmoteRain.UI +{ + /// + /// Settings left view + /// + internal sealed class SettingsLeftView : CP_SDK.UI.ViewController + { + private static readonly string s_InformationStr = "Original mod made by Cr4 and Uialeth" + + "\n" + + "\n" + "Commands" + + "\n" + "- [Moderator] !er rain #EMOTE #COUNT\nTrigger a emote rain" + + "\n" + "- [Moderator] !er toggle\nDisable any emote rain until a Menu/GamePlay scene change" + + "\n" + "- [Moderator] !er clear\nClear all raining emotes"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Information / Credits"), + + Templates.ScrollableInfos(50, + XUIText.Make(s_InformationStr) + .SetAlign(TMPro.TextAlignmentOptions.Left) + ), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("Reset", OnResetButton), + XUIPrimaryButton.Make("Web Configuration", OnWebConfigurationButton) + ), + Templates.ExpandedButtonsLine( + XUISecondaryButton.Make("Documentation", OnDocumentationButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset button + /// + private void OnResetButton() + { + ShowConfirmationModal("Do you really want to reset\nall chat emote rain settings?", (p_Confirm) => + { + if (!p_Confirm) + return; + + /// Reset settings + CERConfig.Instance.Reset(); + CERConfig.Instance.Enabled = true; + CERConfig.Instance.Save(); + + /// Refresh values + SettingsMainView.Instance.RefreshSettings(); + SettingsRightView.Instance.RefreshSettings(); + }); + } + /// + /// Open web configuration button + /// + private void OnWebConfigurationButton() + { + ShowMessageModal("URL opened in your web browser."); + CP_SDK.Chat.Service.OpenWebConfiguration(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Documentation button + /// + private void OnDocumentationButton() + { + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL(ChatEmoteRain.Instance.DocumentationURL); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsMainView.cs b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsMainView.cs new file mode 100644 index 0000000..fe53b5e --- /dev/null +++ b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsMainView.cs @@ -0,0 +1,515 @@ +using CP_SDK.UI.Data; +using CP_SDK.XUI; +using System.Linq; +using UnityEngine.UI; + +using EmitterConfig = CP_SDK.Unity.Components.EnhancedImageParticleEmitter.EmitterConfig; + +namespace ChatPlexMod_ChatEmoteRain.UI +{ + /// + /// Settings main view + /// + internal sealed class SettingsMainView : CP_SDK.UI.ViewController + { + internal class EmitterConfigListItem : IListItem + { + internal EmitterConfig EConfig; + + internal EmitterConfigListItem(EmitterConfig p_EmitterConfig) + => EConfig = p_EmitterConfig; + + public override void OnShow() => RefreshVisual(); + public override void OnHide() { } + + internal void RefreshVisual() + { + if (!(Cell is TextListCell l_TextListCell)) + return; + + l_TextListCell.Text.SetText((EConfig.Enabled ? "" : "") + EConfig.Name); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUITabControl m_TabControl = null; + + private XUIToggle m_GeneralTab_MenuRain = null; + private XUISlider m_GeneralTab_MenuRainSize = null; + private XUISlider m_GeneralTab_MenuFallSpeed = null; + private XUIToggle m_GeneralTab_PlayingRain = null; + private XUISlider m_GeneralTab_PlayingRainSize = null; + private XUISlider m_GeneralTab_PlayingFallSpeed = null; + + private XUIVVList m_MenuEmittersTab_List = null; + private Widgets.EmitterWidget m_MenuEmittersTab_EmitterWidget = null; + + private XUIVVList m_PlayingEmittersTab_List = null; + private Widgets.EmitterWidget m_PlayingEmittersTab_EmitterWidget = null; + + private XUIToggle m_CommandsTab_ModeratorPowerToggle = null; + private XUIToggle m_CommandsTab_VIPPowerToggle = null; + private XUIToggle m_CommandsTab_SubscriberPowerToggle = null; + private XUIToggle m_CommandsTab_UserPowerToggle = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private bool m_PreventChanges = false; + private EmitterConfigListItem m_SelectedItemMenu = null; + private EmitterConfigListItem m_SelectedItemPlaying = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override void OnViewCreation() + { + Templates.FullRectLayoutMainView( + Templates.TitleBar("Chat Emote Rain | Settings"), + + XUITabControl.Make( + ("General", BuildGeneralTab()), + ("Menu Emitters", BuildMenuEmittersTab()), + ("Playing Emitters", BuildPlayingEmittersTab()), + ("Chat Commands", BuildChatCommandsTab()) + ) + .OnActiveChanged(OnTabSelected) + .Bind(ref m_TabControl) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + + RefreshSettings(); + } + /// + /// On view activation + /// + protected override void OnViewActivation() + { + m_MenuEmittersTab_List.SetListItems(CERConfig.Instance.MenuEmitters.Select(x => new EmitterConfigListItem(x)).ToList()); + m_PlayingEmittersTab_List.SetListItems(CERConfig.Instance.SongEmitters.Select(x => new EmitterConfigListItem(x)).ToList()); + } + /// + /// On view deactivation + /// + protected override void OnViewDeactivation() + { + if (ChatEmoteRain.Instance != null) + { + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Menu, false, null); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Playing, false, null); + } + + CERConfig.Instance.Save(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build general tab + /// + /// + private IXUIElement BuildGeneralTab() + { + return XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Rain in menu"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_GeneralTab_MenuRain), + + XUIText.Make("Emote size in menu"), + XUISlider.Make() + .SetMinValue(0.1f).SetMaxValue(5.0f).SetIncrements(0.1f) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_GeneralTab_MenuRainSize), + + XUIText.Make("Emote fall speed in menu"), + XUISlider.Make() + .SetMinValue(1.1f).SetMaxValue(10.0f).SetIncrements(0.1f) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_GeneralTab_MenuFallSpeed) + ) + .SetWidth(40.0f), + + XUIVLayout.Make( + XUIText.Make("Rain while playing"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_GeneralTab_PlayingRain), + + XUIText.Make("Emote size while playing"), + XUISlider.Make() + .SetMinValue(0.1f).SetMaxValue(5.0f).SetIncrements(0.1f) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_GeneralTab_PlayingRainSize), + + XUIText.Make("Emote fall speed while playing"), + XUISlider.Make() + .SetMinValue(1.1f).SetMaxValue(10.0f).SetIncrements(0.1f) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_GeneralTab_PlayingFallSpeed) + ) + .SetWidth(40.0f) + ); + } + /// + /// Build menu emitters tab + /// + /// + private IXUIElement BuildMenuEmittersTab() + { + return XUIHLayout.Make( + XUIVLayout.Make( + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(ListCellPrefabs.Get()) + .OnListItemSelected((x) => OnEmitterSelected(m_MenuEmittersTab_List, x)) + .Bind(ref m_MenuEmittersTab_List) + ) + .SetSpacing(0).SetPadding(0) + .SetHeight(50) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + XUIHLayout.Make( + XUIPrimaryButton .Make("+", OnEmitterAdd) .SetWidth(10.0f), + XUISecondaryButton.Make("Toggle", OnEmitterToggle).SetWidth(15.0f), + XUISecondaryButton.Make("-", OnEmitterDelete).SetWidth(10.0f) + ) + .SetPadding(0) + ) + .SetPadding(0) + .SetWidth(41.0f), + + XUIVLayout.Make( + + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.LElement.flexibleWidth = 1000.0f) + .OnReady(x => m_MenuEmittersTab_EmitterWidget = x.gameObject.AddComponent()) + ) + .SetSpacing(0).SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true); + } + /// + /// Build playing emitters tab + /// + /// + private IXUIElement BuildPlayingEmittersTab() + { + return XUIHLayout.Make( + XUIVLayout.Make( + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(ListCellPrefabs.Get()) + .OnListItemSelected((x) => OnEmitterSelected(m_PlayingEmittersTab_List, x)) + .Bind(ref m_PlayingEmittersTab_List) + ) + .SetSpacing(0).SetPadding(0) + .SetHeight(50) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + XUIHLayout.Make( + XUIPrimaryButton.Make("+", OnEmitterAdd).SetWidth(10.0f), + XUISecondaryButton.Make("Toggle", OnEmitterToggle).SetWidth(15.0f), + XUISecondaryButton.Make("-", OnEmitterDelete).SetWidth(10.0f) + ) + .SetPadding(0) + ) + .SetPadding(0) + .SetWidth(41.0f), + + XUIVLayout.Make( + + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.LElement.flexibleWidth = 1000.0f) + .OnReady(x => m_PlayingEmittersTab_EmitterWidget = x.gameObject.AddComponent()) + ) + .SetSpacing(0).SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true); + } + /// + /// Build chat commands tab + /// + /// + private IXUIElement BuildChatCommandsTab() + { + return XUIVLayout.Make( + XUIText.Make("Give moderators power"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_CommandsTab_ModeratorPowerToggle), + + XUIText.Make("Give VIP power"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_CommandsTab_VIPPowerToggle), + + XUIText.Make("Give subscriber power"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_CommandsTab_SubscriberPowerToggle), + + XUIText.Make("Give user power"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_CommandsTab_UserPowerToggle) + ); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When a tab is selected + /// + /// Tab index + private void OnTabSelected(int p_TabIndex) + { + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Menu, p_TabIndex == 1, null); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Playing, p_TabIndex == 2, null); + } + /// + /// When settings are changed + /// + private void OnSettingChanged() + { + if (m_PreventChanges) + return; + + #region General Tab + var l_Config = CERConfig.Instance; + l_Config.EnableMenu = m_GeneralTab_MenuRain.Element.GetValue(); + l_Config.MenuSize = m_GeneralTab_MenuRainSize.Element.GetValue(); + l_Config.MenuSpeed = m_GeneralTab_MenuFallSpeed.Element.GetValue(); + + l_Config.EnableSong = m_GeneralTab_PlayingRain.Element.GetValue(); + l_Config.SongSize = m_GeneralTab_PlayingRainSize.Element.GetValue(); + l_Config.SongSpeed = m_GeneralTab_PlayingFallSpeed.Element.GetValue(); + + m_GeneralTab_MenuRainSize.SetInteractable(l_Config.EnableMenu); + m_GeneralTab_MenuFallSpeed.SetInteractable(l_Config.EnableMenu); + + m_GeneralTab_PlayingRainSize.SetInteractable(l_Config.EnableSong); + m_GeneralTab_PlayingFallSpeed.SetInteractable(l_Config.EnableSong); + #endregion + + #region ChatCommands Tab + var l_ChatCommands = CERConfig.Instance.ChatCommands; + l_ChatCommands.ModeratorPower = m_CommandsTab_ModeratorPowerToggle.Element.GetValue(); + l_ChatCommands.VIPPower = m_CommandsTab_VIPPowerToggle.Element.GetValue(); + l_ChatCommands.SubscriberPower = m_CommandsTab_SubscriberPowerToggle.Element.GetValue(); + l_ChatCommands.UserPower = m_CommandsTab_UserPowerToggle.Element.GetValue(); + #endregion + + ChatEmoteRain.Instance.OnSettingsChanged(); + } + /// + /// Reset settings + /// + internal void RefreshSettings() + { + m_PreventChanges = true; + + #region General Tab + var l_Config = CERConfig.Instance; + m_GeneralTab_MenuRain .SetValue(l_Config.EnableMenu); + m_GeneralTab_MenuRainSize .SetValue(l_Config.MenuSize); + m_GeneralTab_MenuFallSpeed.SetValue(l_Config.MenuSpeed); + + m_GeneralTab_PlayingRain .SetValue(l_Config.EnableSong); + m_GeneralTab_PlayingRainSize .SetValue(l_Config.SongSize); + m_GeneralTab_PlayingFallSpeed.SetValue(l_Config.SongSpeed); + + m_GeneralTab_MenuRainSize.SetInteractable(l_Config.EnableMenu); + m_GeneralTab_MenuFallSpeed.SetInteractable(l_Config.EnableMenu); + + m_GeneralTab_PlayingRainSize.SetInteractable(l_Config.EnableSong); + m_GeneralTab_PlayingFallSpeed.SetInteractable(l_Config.EnableSong); + #endregion + + #region ChatCommands Tab + var l_ChatCommands = CERConfig.Instance.ChatCommands; + m_CommandsTab_ModeratorPowerToggle .SetValue(l_ChatCommands.ModeratorPower); + m_CommandsTab_VIPPowerToggle .SetValue(l_ChatCommands.VIPPower); + m_CommandsTab_SubscriberPowerToggle .SetValue(l_ChatCommands.SubscriberPower); + m_CommandsTab_UserPowerToggle .SetValue(l_ChatCommands.UserPower); + #endregion + + m_PreventChanges = false; + + ChatEmoteRain.Instance.OnSettingsChanged(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When an emitter is selected + /// + /// Source list + /// Selected item + private void OnEmitterSelected(XUIVVList p_List, IListItem p_Item) + { + var l_IsMenu = m_TabControl.Element.GetActiveTab() == 1; + if (p_List == m_MenuEmittersTab_List) + { + m_SelectedItemMenu = (EmitterConfigListItem)p_Item; + m_MenuEmittersTab_EmitterWidget.SetCurrent(m_SelectedItemMenu); + + ChatEmoteRain.Instance.SetTemplatesPreview( + CP_SDK.ChatPlexSDK.EGenericScene.Menu, + m_TabControl.Element.GetActiveTab() == 1, + m_SelectedItemMenu?.EConfig + ); + } + else + { + m_SelectedItemPlaying = (EmitterConfigListItem)p_Item; + m_PlayingEmittersTab_EmitterWidget.SetCurrent(m_SelectedItemPlaying); + + ChatEmoteRain.Instance.SetTemplatesPreview( + CP_SDK.ChatPlexSDK.EGenericScene.Playing, + m_TabControl.Element.GetActiveTab() == 2, + m_SelectedItemPlaying?.EConfig + ); + } + } + /// + /// On add emitter button + /// + private void OnEmitterAdd() + { + var l_New = new EmitterConfigListItem(new EmitterConfig()); + if (m_TabControl.Element.GetActiveTab() == 1) + { + CERConfig.Instance.MenuEmitters.Add(l_New.EConfig); + m_MenuEmittersTab_List.AddListItem(l_New); + m_MenuEmittersTab_List.SetSelectedListItem(l_New); + ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Menu); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Menu, true, l_New.EConfig); + } + else + { + CERConfig.Instance.SongEmitters.Add(l_New.EConfig); + m_PlayingEmittersTab_List.AddListItem(l_New); + m_PlayingEmittersTab_List.SetSelectedListItem(l_New); + ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Playing); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Playing, true, l_New.EConfig); + } + } + /// + /// On toggle emitter button + /// + private void OnEmitterToggle() + { + var l_IsMenu = m_TabControl.Element.GetActiveTab() == 1; + var l_DataSource = l_IsMenu ? CERConfig.Instance.MenuEmitters : CERConfig.Instance.SongEmitters; + var l_Selected = l_IsMenu ? m_SelectedItemMenu : m_SelectedItemPlaying; + + if (l_Selected == null) + { + ShowMessageModal("Please select an emitter first!"); + return; + } + + if (l_Selected.EConfig.Enabled) + { + ShowConfirmationModal($"Do you want to disable emitter\n\"{l_Selected.EConfig.Name}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + l_Selected.EConfig.Enabled = false; + l_Selected.RefreshVisual(); + + if (l_IsMenu) + { + ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Menu); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Menu, true, l_Selected?.EConfig); + } + else + { + ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Playing); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Playing, true, l_Selected?.EConfig); + } + }); + } + else + { + ShowConfirmationModal($"Do you want to enable emitter\n\"{l_Selected.EConfig.Name}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + l_Selected.EConfig.Enabled = true; + l_Selected.RefreshVisual(); + + if (l_IsMenu) + { + ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Menu); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Menu, true, l_Selected?.EConfig); + } + else + { + ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Playing); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Playing, true, l_Selected?.EConfig); + } + }); + } + } + /// + /// On delete emitter button + /// + private void OnEmitterDelete() + { + var l_IsMenu = m_TabControl.Element.GetActiveTab() == 1; + var l_DataSource = l_IsMenu ? CERConfig.Instance.MenuEmitters : CERConfig.Instance.SongEmitters; + var l_Selected = l_IsMenu ? m_SelectedItemMenu : m_SelectedItemPlaying; + + if (l_Selected == null) + { + ShowMessageModal("Please select an emitter first!"); + return; + } + + ShowConfirmationModal($"Do you want to delete emitter\n\"{l_Selected.EConfig.Name}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + if (l_IsMenu) + { + m_MenuEmittersTab_List.RemoveListItem(l_Selected); + CERConfig.Instance.MenuEmitters.Remove(l_Selected.EConfig); + ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Menu); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Menu, true, l_Selected?.EConfig); + } + else + { + m_PlayingEmittersTab_List.RemoveListItem(l_Selected); + CERConfig.Instance.SongEmitters.Remove(l_Selected.EConfig); + ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Playing); + ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Playing, true, l_Selected?.EConfig); + } + }); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsRightView.cs b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsRightView.cs new file mode 100644 index 0000000..027694b --- /dev/null +++ b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/SettingsRightView.cs @@ -0,0 +1,102 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace ChatPlexMod_ChatEmoteRain.UI +{ + /// + /// Settings right view + /// + internal sealed class SettingsRightView : CP_SDK.UI.ViewController + { + private XUIToggle m_Enabled; + private XUISlider m_EmoteCount; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Subrain"), + + XUIVLayout.Make( + XUIText.Make("Enabled") + .SetColor(Color.yellow) + .SetAlign(TMPro.TextAlignmentOptions.Midline), + XUIToggle.Make() + .SetValue(CERConfig.Instance.SubRain) + .OnValueChanged((x) => CERConfig.Instance.SubRain = x) + .Bind(ref m_Enabled), + XUIText.Make("Emote count") + .SetColor(Color.yellow) + .SetAlign(TMPro.TextAlignmentOptions.Midline), + XUISlider.Make() + .SetMinValue(1).SetMaxValue(100).SetIncrements(1).SetInteger(true) + .SetValue(CERConfig.Instance.SubRainEmoteCount) + .OnValueChanged((x) => CERConfig.Instance.SubRainEmoteCount = (int)x) + .Bind(ref m_EmoteCount) + ) + .SetWidth(80f) + .SetSpacing(0) + .SetBackground(true), + + XUIVLayout.Make( + XUIText.Make( + "SubRain folder is located at Beat Saber/CustomSubRain" + + "\nPaste in your favorite PNGs to set as SubRain!" + + "\n1:1 ratio recommended" + ) + .SetAlign(TMPro.TextAlignmentOptions.Center) + ) + .SetSpacing(0) + .SetBackground(true), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("Reload SubRain textures", OnReloadSubRainButton) + ), + Templates.ExpandedButtonsLine( + XUISecondaryButton.Make("Test it", OnTestSubRainButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset settings + /// + internal void RefreshSettings() + { + m_Enabled.SetValue(CERConfig.Instance.SubRain, false); + m_EmoteCount.SetValue(CERConfig.Instance.SubRainEmoteCount, false); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On reload button pressed + /// + private void OnReloadSubRainButton() + { + /// Reload sub rain + ChatEmoteRain.Instance.LoadSubRainFiles(); + + /// Show message + ShowMessageModal("SubRain textures were reloaded!"); + } + /// + /// On test button pressed + /// + private void OnTestSubRainButton() + { + ChatEmoteRain.Instance.StartSubRain(); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/Widgets/EmitterWidget.cs b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/Widgets/EmitterWidget.cs new file mode 100644 index 0000000..2fed367 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatEmoteRain/ChatPlexMod_ChatEmoteRain/UI/Widgets/EmitterWidget.cs @@ -0,0 +1,211 @@ +using CP_SDK.Unity.Extensions; +using CP_SDK.XUI; +using System; +using TMPro; +using UnityEngine; + +namespace ChatPlexMod_ChatEmoteRain.UI.Widgets +{ + /// + /// Emitter widget + /// + internal sealed class EmitterWidget : MonoBehaviour + { + private XUIVLayout m_NoneFrame = null; + private XUIVLayout m_EditFrame = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUITextInput m_Name = null; + private XUISlider m_Speed = null; + private XUISlider m_Size = null; + + private XUISlider m_PosX = null; + private XUISlider m_PosY = null; + private XUISlider m_PosZ = null; + + private XUISlider m_RotX = null; + private XUISlider m_RotY = null; + private XUISlider m_RotZ = null; + + private XUISlider m_ScaX = null; + private XUISlider m_ScaY = null; + private XUISlider m_ScaZ = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private SettingsMainView.EmitterConfigListItem m_Current = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On component creation + /// + private void Awake() + { + Action l_ControlsTextStyle = (x) => x.SetStyle(FontStyles.Bold).SetColor(Color.yellow); + + XUIVLayout.Make( + XUIText.Make("Please select an emitter in the list!").OnReady(l_ControlsTextStyle) + ) + .SetSpacing(1f).SetPadding(0, 2, 0, 2) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .Bind(ref m_NoneFrame) + .BuildUI(transform); + + m_NoneFrame.SetActive(true); + + XUIVLayout.Make( + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Name:").OnReady(l_ControlsTextStyle), + XUIText.Make("Speed:").OnReady(l_ControlsTextStyle), + XUIText.Make("Size:").OnReady(l_ControlsTextStyle) + ) + .SetMinWidth(15f).SetWidth(15f) + .OnReady(x => x.VLayoutGroup.childAlignment = TextAnchor.MiddleLeft), + + XUIVLayout.Make( + XUITextInput.Make("Name...").OnValueChanged((_) => OnSettingChanged()).Bind(ref m_Name), + XUISlider.Make().SetMinValue(0.1f).SetMaxValue(5.0f).SetIncrements(0.01f).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_Speed), + XUISlider.Make().SetMinValue(0.1f).SetMaxValue(5.0f).SetIncrements(0.01f).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_Size) + ) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.LElement.flexibleWidth = 1000.0f) + ) + .SetSpacing(0).SetPadding(0) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .ForEachDirect(x => x.SetSpacing(0)), + + XUIVLayout.Make( + XUIText.Make("Position X Y Z").SetAlign(TextAlignmentOptions.CaplineLeft).OnReady(l_ControlsTextStyle), + + XUIHLayout.Make( + XUISlider.Make().SetMinValue(-20.0f).SetMaxValue(20.0f).SetIncrements(0.01f).Bind(ref m_PosX), + XUISlider.Make().SetMinValue(-20.0f).SetMaxValue(20.0f).SetIncrements(0.01f).Bind(ref m_PosY), + XUISlider.Make().SetMinValue(-20.0f).SetMaxValue(20.0f).SetIncrements(0.01f).Bind(ref m_PosZ) + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.LElement.flexibleWidth = 1000.0f) + .ForEachDirect(x => x.SetColor(ColorU.ToUnityColor("#c869ff")).OnValueChanged((_) => OnSettingChanged())) + ) + .SetSpacing(0).SetPadding(1) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained), + + XUIVLayout.Make( + XUIText.Make("Rotation X Y Z").SetAlign(TextAlignmentOptions.CaplineLeft).OnReady(l_ControlsTextStyle), + + XUIHLayout.Make( + XUISlider.Make().SetMinValue(0.0f).SetMaxValue(360.0f).SetIncrements(1.0f).SetInteger(true).Bind(ref m_RotX), + XUISlider.Make().SetMinValue(0.0f).SetMaxValue(360.0f).SetIncrements(1.0f).SetInteger(true).Bind(ref m_RotY), + XUISlider.Make().SetMinValue(0.0f).SetMaxValue(360.0f).SetIncrements(1.0f).SetInteger(true).Bind(ref m_RotZ) + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.LElement.flexibleWidth = 1000.0f) + .ForEachDirect(x => x.SetColor(ColorU.ToUnityColor("#3d9eff")).OnValueChanged((_) => OnSettingChanged())) + ) + .SetSpacing(0).SetPadding(1) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained), + + XUIVLayout.Make( + XUIText.Make("Scale X Y Z").SetAlign(TextAlignmentOptions.CaplineLeft).OnReady(l_ControlsTextStyle), + + XUIHLayout.Make( + XUISlider.Make().SetMinValue(0.0f).SetMaxValue(30.0f).SetIncrements(0.01f).Bind(ref m_ScaX), + XUISlider.Make().SetMinValue(0.0f).SetMaxValue(30.0f).SetIncrements(0.01f).Bind(ref m_ScaY), + XUISlider.Make().SetMinValue(0.0f).SetMaxValue(30.0f).SetIncrements(0.01f).Bind(ref m_ScaZ) + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.LElement.flexibleWidth = 1000.0f) + .ForEachDirect(x => x.SetColor(ColorU.ToUnityColor("#FF6C11")).OnValueChanged((_) => OnSettingChanged())) + ) + .SetSpacing(0).SetPadding(1) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + ) + .SetSpacing(1f).SetPadding(0, 2, 0, 2) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + .Bind(ref m_EditFrame) + .BuildUI(transform); + + m_EditFrame.SetActive(false); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set current + /// + /// + internal void SetCurrent(SettingsMainView.EmitterConfigListItem p_Current) + { + m_Current = null; + + if (p_Current != null) + { + m_Name.SetValue(p_Current.EConfig.Name); + m_Speed.SetValue(p_Current.EConfig.Speed); + m_Size.SetValue(p_Current.EConfig.Size); + + m_PosX.SetValue(p_Current.EConfig.PosX); + m_PosY.SetValue(p_Current.EConfig.PosY); + m_PosZ.SetValue(p_Current.EConfig.PosZ); + m_RotX.SetValue(p_Current.EConfig.RotX); + m_RotY.SetValue(p_Current.EConfig.RotY); + m_RotZ.SetValue(p_Current.EConfig.RotZ); + m_ScaX.SetValue(p_Current.EConfig.SizeX); + m_ScaY.SetValue(p_Current.EConfig.SizeY); + m_ScaZ.SetValue(p_Current.EConfig.SizeZ); + + m_NoneFrame.SetActive(false); + m_EditFrame.SetActive(true); + } + else + { + m_EditFrame.SetActive(false); + m_NoneFrame.SetActive(true); + } + + m_Current = p_Current; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When any value change + /// + private void OnSettingChanged() + { + if (m_Current == null) + return; + + m_Current.EConfig.Name = m_Name.Element.GetValue(); + m_Current.EConfig.Speed = m_Speed.Element.GetValue(); + m_Current.EConfig.Size = m_Size.Element.GetValue(); + + m_Current.EConfig.PosX = m_PosX.Element.GetValue(); + m_Current.EConfig.PosY = m_PosY.Element.GetValue(); + m_Current.EConfig.PosZ = m_PosZ.Element.GetValue(); + m_Current.EConfig.RotX = m_RotX.Element.GetValue(); + m_Current.EConfig.RotY = m_RotY.Element.GetValue(); + m_Current.EConfig.RotZ = m_RotZ.Element.GetValue(); + m_Current.EConfig.SizeX = m_ScaX.Element.GetValue(); + m_Current.EConfig.SizeY = m_ScaY.Element.GetValue(); + m_Current.EConfig.SizeZ = m_ScaZ.Element.GetValue(); + + m_Current.RefreshVisual(); + ChatEmoteRain.Instance.OnSettingsChanged(); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/Properties/AssemblyInfo.cs b/Modules/BeatSaberPlus_ChatEmoteRain/Properties/AssemblyInfo.cs index 6599aad..8e7d3cc 100644 --- a/Modules/BeatSaberPlus_ChatEmoteRain/Properties/AssemblyInfo.cs +++ b/Modules/BeatSaberPlus_ChatEmoteRain/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/UI/EmitterWidget.bsml b/Modules/BeatSaberPlus_ChatEmoteRain/UI/EmitterWidget.bsml deleted file mode 100644 index 2e6bc27..0000000 --- a/Modules/BeatSaberPlus_ChatEmoteRain/UI/EmitterWidget.bsml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/UI/EmitterWidget.cs b/Modules/BeatSaberPlus_ChatEmoteRain/UI/EmitterWidget.cs deleted file mode 100644 index 6721b0d..0000000 --- a/Modules/BeatSaberPlus_ChatEmoteRain/UI/EmitterWidget.cs +++ /dev/null @@ -1,173 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using BeatSaberMarkupLanguage.Components.Settings; -using System; -using System.Reflection; -using UnityEngine; - -using EmitterConfig = CP_SDK.Unity.Components.EnhancedImageParticleEmitter.EmitterConfig; - -namespace ChatPlexMod_ChatEmoteRain.UI -{ - class EmitterWidget - { -#pragma warning disable CS0414 - [UIComponent("NameText")] - public TMPro.TextMeshProUGUI m_NameText = null; - [UIComponent("SpeedSlider")] - public SliderSetting m_SpeedSlider = null; - [UIComponent("SizeSlider")] - public SliderSetting m_SizeSlider = null; - - [UIComponent("PosX")] - public SliderSetting m_PosX = null; - [UIComponent("PosY")] - public SliderSetting m_PosY = null; - [UIComponent("PosZ")] - public SliderSetting m_PosZ = null; - - [UIComponent("RotX")] - public SliderSetting m_RotX = null; - [UIComponent("RotY")] - public SliderSetting m_RotY = null; - [UIComponent("RotZ")] - public SliderSetting m_RotZ = null; - - [UIComponent("SizeX")] - public SliderSetting m_SizeX = null; - [UIComponent("SizeY")] - public SliderSetting m_SizeY = null; - [UIComponent("SizeZ")] - public SliderSetting m_SizeZ = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("InputKeyboard")] - private ModalKeyboard m_InputKeyboard = null; - [UIValue("InputKeyboardValue")] - private string m_InputKeyboardValue = ""; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Current focused emitter - /// - private EmitterConfig m_CurrentEmitter = null; - /// - /// Input keyboard callback - /// - private Action m_InputKeyboardCallback = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public void BuildUI(Transform p_Parent, EmitterConfig l_Emitter) - { - m_CurrentEmitter = l_Emitter; - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, GetType().Name)); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - m_NameText.text = l_Emitter.Name; - - var l_AnchorMin = new Vector2(0.15f, -0.05f); - var l_AnchorMax = new Vector2(0.88f, 1.05f); - - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_SpeedSlider, l_Event, null, l_Emitter.Speed, true, true, l_AnchorMin, l_AnchorMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_SizeSlider, l_Event, null, l_Emitter.Size, true, true, l_AnchorMin, l_AnchorMax); - - l_AnchorMin = new Vector2(0.20f, -0.05f); - l_AnchorMax = new Vector2(0.80f, 1.05f); - - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_PosX, l_Event, null, l_Emitter.PosX, true, true, l_AnchorMin, l_AnchorMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_PosY, l_Event, null, l_Emitter.PosY, true, true, l_AnchorMin, l_AnchorMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_PosZ, l_Event, null, l_Emitter.PosZ, true, true, l_AnchorMin, l_AnchorMax); - - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_RotX, l_Event, null, l_Emitter.RotX, true, true, l_AnchorMin, l_AnchorMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_RotY, l_Event, null, l_Emitter.RotY, true, true, l_AnchorMin, l_AnchorMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_RotZ, l_Event, null, l_Emitter.RotZ, true, true, l_AnchorMin, l_AnchorMax); - - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_SizeX, l_Event, null, l_Emitter.SizeX, true, true, l_AnchorMin, l_AnchorMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_SizeY, l_Event, null, l_Emitter.SizeY, true, true, l_AnchorMin, l_AnchorMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_SizeZ, l_Event, null, l_Emitter.SizeZ, true, true, l_AnchorMin, l_AnchorMax); - } - /// - /// When any value change - /// - /// Event sender - private void OnSettingChanged(object p_Value) - { - m_CurrentEmitter.Speed = m_SpeedSlider.Value; - m_CurrentEmitter.Size = m_SizeSlider.Value; - - m_CurrentEmitter.PosX = m_PosX.Value; - m_CurrentEmitter.PosY = m_PosY.Value; - m_CurrentEmitter.PosZ = m_PosZ.Value; - m_CurrentEmitter.RotX = m_RotX.Value; - m_CurrentEmitter.RotY = m_RotY.Value; - m_CurrentEmitter.RotZ = m_RotZ.Value; - m_CurrentEmitter.SizeX = m_SizeX.Value; - m_CurrentEmitter.SizeY = m_SizeY.Value; - m_CurrentEmitter.SizeZ = m_SizeZ.Value; - - ChatEmoteRain.Instance.OnSettingsChanged(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Name button pressed - /// - [UIAction("click-name-btn-pressed")] - private void OnTitleButton() - { - UIShowInputKeyboard(m_CurrentEmitter.Name, (x) => - { - m_CurrentEmitter.Name = x.Length > 45 ? x.Substring(0, 45) : x; - m_NameText.text = m_CurrentEmitter.Name; - OnSettingChanged(null); - Settings.Instance.RebuildEmitterList(m_CurrentEmitter); - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Show input keyboard - /// - /// Start value - /// On enter callback - /// Custom keys - public void UIShowInputKeyboard(string p_Value, Action p_Callback) - { - m_InputKeyboardValue = p_Value; - - /// Show keyboard - m_InputKeyboardCallback = p_Callback; - m_InputKeyboard.keyboard.KeyboardText.fontSizeMax = 3; - m_InputKeyboard.keyboard.KeyboardText.fontSizeMin = 3; - m_InputKeyboard.keyboard.KeyboardText.enableAutoSizing = true; - m_InputKeyboard.keyboard.KeyboardCursor.fontSizeMax = 3; - m_InputKeyboard.keyboard.KeyboardCursor.fontSizeMin = 3; - m_InputKeyboard.keyboard.KeyboardCursor.enableAutoSizing = true; - m_InputKeyboard.modalView.Show(true); - } - /// - /// On input keyboard enter pressed - /// - /// - [UIAction("InputKeyboardEnterPressed")] - private void InputKeyboardEnterPressed(string p_Text) - { - m_InputKeyboardCallback?.Invoke(p_Text); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/UI/Settings.bsml b/Modules/BeatSaberPlus_ChatEmoteRain/UI/Settings.bsml deleted file mode 100644 index b0c9601..0000000 --- a/Modules/BeatSaberPlus_ChatEmoteRain/UI/Settings.bsml +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/UI/Settings.cs b/Modules/BeatSaberPlus_ChatEmoteRain/UI/Settings.cs deleted file mode 100644 index 42b1873..0000000 --- a/Modules/BeatSaberPlus_ChatEmoteRain/UI/Settings.cs +++ /dev/null @@ -1,597 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.Parser; -using HMUI; -using System; -using UnityEngine; -using UnityEngine.UI; - -using EmitterConfig = CP_SDK.Unity.Components.EnhancedImageParticleEmitter.EmitterConfig; - -namespace ChatPlexMod_ChatEmoteRain.UI -{ - /// - /// Chat Emote Rain settings main view - /// - internal class Settings : BeatSaberPlus.SDK.UI.ResourceViewController - { - private static int s_EMITTER_PER_PAGE = 8; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIObject("TabSelector")] - private GameObject m_TabSelector; - private TextSegmentedControl m_TabSelector_TabSelectorControl = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("GeneralTab")] - private GameObject m_GeneralTab = null; - [UIComponent("GeneralTab_MenuRainToggle")] - public ToggleSetting m_GeneralTab_MenuRain; - [UIComponent("GeneralTab_MenuRainSizeSlider")] - public SliderSetting m_GeneralTab_MenuRainSizeSlider; - [UIComponent("GeneralTab_MenuFallSpeedSlider")] - public SliderSetting m_GeneralTab_MenuFallSpeedSlider; - [UIComponent("GeneralTab_SongRainToggle")] - public ToggleSetting m_GeneralTab_SongRain; - [UIComponent("GeneralTab_SongRainSizeSlider")] - public SliderSetting m_GeneralTab_SongRainSizeSlider; - [UIComponent("GeneralTab_SongFallSpeedSlider")] - public SliderSetting m_GeneralTab_SongFallSpeedSlider; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("MenuEmittersTab")] - private GameObject m_MenuEmittersTab = null; - - [UIComponent("MenuEmittersTab_UpButton")] - private Button m_MenuEmittersTab_UpButton = null; - [UIObject("MenuEmittersTab_List")] - private GameObject m_MenuEmittersTab_ListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_MenuEmittersTab_List = null; - [UIComponent("MenuEmittersTab_DownButton")] - private Button m_MenuEmittersTab_DownButton = null; - - [UIObject("MenuEmittersTab_Content")] - private GameObject m_MenuEmittersTab_Content = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("SongEmittersTab")] - private GameObject m_SongEmittersTab = null; - - [UIComponent("SongEmittersTab_UpButton")] - private Button m_SongEmittersTab_UpButton = null; - [UIObject("SongEmittersTab_List")] - private GameObject m_SongEmittersTab_ListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_SongEmittersTab_List = null; - [UIComponent("SongEmittersTab_DownButton")] - private Button m_SongEmittersTab_DownButton = null; - - [UIObject("SongEmittersTab_Content")] - private GameObject m_SongEmittersTab_Content = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("CommandsTab")] - private GameObject m_CommandsTab = null; - [UIComponent("CommandsTab_ModeratorPowerToggle")] - public ToggleSetting m_CommandsTab_ModeratorPowerToggle; - [UIComponent("CommandsTab_VIPPowerToggle")] - public ToggleSetting m_CommandsTab_VIPPowerToggle; - [UIComponent("CommandsTab_SubscriberPowerToggle")] - public ToggleSetting m_CommandsTab_SubscriberPowerToggle; - [UIComponent("CommandsTab_UserPowerToggle")] - public ToggleSetting m_CommandsTab_UserPowerToggle; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - /// - /// Menu emitters current page - /// - private int m_MenuEmittersCurrentPage = 1; - /// - /// Song current selected emitter index - /// - private int m_MenuEmittersSelected = -1; - /// - /// Song emitters current page - /// - private int m_SongEmittersCurrentPage = 1; - /// - /// Menu current selected emitter index - /// - private int m_SongEmittersSelected = -1; - /// - /// Emitter widget - /// - private EmitterWidget m_EmitterWidget = new EmitterWidget(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_GeneralTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_MenuEmittersTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_MenuEmittersTab_Content, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_SongEmittersTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_SongEmittersTab_Content, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_CommandsTab, 0.50f); - - var l_Event = new BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - - /// Create type selector - m_TabSelector_TabSelectorControl = BeatSaberPlus.SDK.UI.TextSegmentedControl.Create(m_TabSelector.transform as RectTransform, false); - m_TabSelector_TabSelectorControl.SetTexts(new string[] { "General", "Menu Emitters", "Song Emitters", "Chat Commands" }); - m_TabSelector_TabSelectorControl.ReloadData(); - m_TabSelector_TabSelectorControl.didSelectCellEvent += OnTabSelected; - - /// General tab setup - if (true) - { - var l_AnchorMin = new Vector2(0.18f, -0.05f); - var l_AnchorMax = new Vector2(0.86f, 1.05f); - - /// First row - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_GeneralTab_MenuRain, l_Event, CERConfig.Instance.EnableMenu, true); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_GeneralTab_MenuRainSizeSlider, l_Event, null, CERConfig.Instance.MenuSize, true, true, l_AnchorMin, l_AnchorMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_GeneralTab_MenuFallSpeedSlider, l_Event, null, CERConfig.Instance.MenuSpeed, true, true, l_AnchorMin, l_AnchorMax); - - /// Second row - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_GeneralTab_SongRain, l_Event, CERConfig.Instance.EnableSong, true); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_GeneralTab_SongRainSizeSlider, l_Event, null, CERConfig.Instance.SongSize, true, true, l_AnchorMin, l_AnchorMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_GeneralTab_SongFallSpeedSlider, l_Event, null, CERConfig.Instance.SongSpeed, true, true, l_AnchorMin, l_AnchorMax); - } - - /// Menu emitters - if (true) - { - /// Scale down up & down button - m_MenuEmittersTab_UpButton.transform.localScale = Vector3.one * 0.6f; - m_MenuEmittersTab_DownButton.transform.localScale = Vector3.one * 0.6f; - - var l_LayoutElement = m_MenuEmittersTab_ListView.GetComponent(); - l_LayoutElement.preferredWidth = 45; - l_LayoutElement.preferredHeight = 40; - - var l_BSMLTableView = m_MenuEmittersTab_ListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_MenuEmittersTab_ListView.GetComponentInChildren()); - m_MenuEmittersTab_List = l_BSMLTableView.gameObject.AddComponent(); - m_MenuEmittersTab_List.TableViewInstance = l_BSMLTableView; - m_MenuEmittersTab_List.CellSizeValue = 5f; - l_BSMLTableView.didSelectCellWithIdxEvent += OnEmitterSelected; - l_BSMLTableView.SetDataSource(m_MenuEmittersTab_List, false); - - /// Bind events - m_MenuEmittersTab_UpButton.onClick.AddListener(OnEmitterPageUpPressed); - m_MenuEmittersTab_DownButton.onClick.AddListener(OnEmitterPageDownPressed); - } - - /// Song emitters - if (true) - { - /// Scale down up & down button - m_SongEmittersTab_UpButton.transform.localScale = Vector3.one * 0.6f; - m_SongEmittersTab_DownButton.transform.localScale = Vector3.one * 0.6f; - - var l_LayoutElement = m_SongEmittersTab_ListView.GetComponent(); - l_LayoutElement.preferredWidth = 45; - l_LayoutElement.preferredHeight = 40; - - var l_BSMLTableView = m_SongEmittersTab_ListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_SongEmittersTab_ListView.GetComponentInChildren()); - m_SongEmittersTab_List = l_BSMLTableView.gameObject.AddComponent(); - m_SongEmittersTab_List.TableViewInstance = l_BSMLTableView; - m_SongEmittersTab_List.CellSizeValue = 5f; - l_BSMLTableView.didSelectCellWithIdxEvent += OnEmitterSelected; - l_BSMLTableView.SetDataSource(m_SongEmittersTab_List, false); - - /// Bind events - m_SongEmittersTab_UpButton.onClick.AddListener(OnEmitterPageUpPressed); - m_SongEmittersTab_DownButton.onClick.AddListener(OnEmitterPageDownPressed); - } - - /// Commands tab - if (true) - { - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_CommandsTab_ModeratorPowerToggle, l_Event, CERConfig.Instance.ChatCommands.ModeratorPower, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_CommandsTab_VIPPowerToggle, l_Event, CERConfig.Instance.ChatCommands.VIPPower, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_CommandsTab_SubscriberPowerToggle, l_Event, CERConfig.Instance.ChatCommands.SubscriberPower, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_CommandsTab_UserPowerToggle, l_Event, CERConfig.Instance.ChatCommands.UserPower, true); - } - - /// Show first tab by default - OnTabSelected(null, 0); - } - /// - /// On view deactivation - /// - protected override sealed void OnViewDeactivation() - { - ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Menu, false, null); - ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Playing, false, null); - - CERConfig.Instance.Save(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When a tab is selected - /// - /// Tab control instance - /// Tab index - private void OnTabSelected(SegmentedControl p_SegmentControl, int p_TabIndex) - { - m_GeneralTab.SetActive(p_TabIndex == 0); - m_MenuEmittersTab.SetActive(p_TabIndex == 1); - m_SongEmittersTab.SetActive(p_TabIndex == 2); - m_CommandsTab.SetActive(p_TabIndex == 3); - - if (p_TabIndex == 1 || p_TabIndex == 2) - { - m_MenuEmittersCurrentPage = 1; - m_MenuEmittersSelected = -1; - m_MenuEmittersCurrentPage = 1; - m_SongEmittersSelected = -1; - - ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Menu, p_TabIndex == 1, null); - ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Playing, p_TabIndex == 2, null); - - RebuildEmitterList(null); - } - } - /// - /// Emitter list page UP - /// - private void OnEmitterPageUpPressed() - { - var l_CurrentPage = m_MenuEmittersTab.activeSelf ? m_MenuEmittersCurrentPage : m_SongEmittersCurrentPage; - - /// Underflow check - if (l_CurrentPage < 2) - return; - - /// Decrement current page - l_CurrentPage--; - - if (m_MenuEmittersTab.activeSelf) - m_MenuEmittersCurrentPage = l_CurrentPage; - else - m_SongEmittersCurrentPage = l_CurrentPage; - - /// Rebuild list - RebuildEmitterList(null); - } - /// - /// Rebuilt emitter list - /// - /// Emitter to focus - public void RebuildEmitterList(EmitterConfig p_EmitterToFocus) - { - if (!UICreated) - return; - - var l_TargetList = m_MenuEmittersTab.activeSelf ? m_MenuEmittersTab_List : m_SongEmittersTab_List; - var l_DataSource = m_MenuEmittersTab.activeSelf ? CERConfig.Instance.MenuEmitters : CERConfig.Instance.SongEmitters; - var l_CurrentPage = m_MenuEmittersTab.activeSelf ? m_MenuEmittersCurrentPage : m_SongEmittersCurrentPage; - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(l_DataSource.Count) / (float)(s_EMITTER_PER_PAGE))); - - if (p_EmitterToFocus != null) - { - var l_Index = l_DataSource.IndexOf(p_EmitterToFocus); - if (l_Index != -1) - l_CurrentPage = (l_Index / s_EMITTER_PER_PAGE) + 1; - else - OnEmitterSelected(null, -1); - } - - /// Update overflow - l_CurrentPage = Math.Max(1, Math.Min(l_CurrentPage, l_PageCount)); - - /// Update UI - if (m_MenuEmittersTab.activeSelf) - { - m_MenuEmittersCurrentPage = l_CurrentPage; - m_MenuEmittersTab_UpButton.interactable = l_CurrentPage > 1; - m_MenuEmittersTab_DownButton.interactable = l_CurrentPage < l_PageCount; - } - else - { - m_SongEmittersCurrentPage = l_CurrentPage; - m_SongEmittersTab_UpButton.interactable = l_CurrentPage > 1; - m_SongEmittersTab_DownButton.interactable = l_CurrentPage < l_PageCount; - } - - /// Clear old entries - l_TargetList.TableViewInstance.ClearSelection(); - l_TargetList.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (l_CurrentPage - 1) * s_EMITTER_PER_PAGE; - l_I < l_DataSource.Count && l_I < (l_CurrentPage * s_EMITTER_PER_PAGE); - ++l_I) - { - var l_Emitter = l_DataSource[l_I]; - var l_Name = l_Emitter.Name; - - l_TargetList.Data.Add(("" + (l_Emitter.Enabled ? "" : "") + l_Name, null)); - - if (l_Emitter == p_EmitterToFocus) - l_RelIndexToFocus = l_TargetList.Data.Count - 1; - } - - /// Refresh - l_TargetList.TableViewInstance.ReloadData(); - - /// Update focus - if (l_DataSource.Count == 0) - OnEmitterSelected(null, -1); - else if (l_RelIndexToFocus != -1) - l_TargetList.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// When an emitter is selected - /// - /// List instance - /// Selected index - private void OnEmitterSelected(TableView p_List, int p_RelIndex) - { - var l_DataSource = m_MenuEmittersTab.activeSelf ? CERConfig.Instance.MenuEmitters : CERConfig.Instance.SongEmitters; - var l_CurrentPage = m_MenuEmittersTab.activeSelf ? m_MenuEmittersCurrentPage : m_SongEmittersCurrentPage; - - /// Clean up old widget - if (m_MenuEmittersTab.activeSelf) - { - if (m_MenuEmittersTab_Content.transform.childCount != 0) - GameObject.DestroyImmediate(m_MenuEmittersTab_Content.transform.GetChild(0).gameObject); - } - else - { - if (m_SongEmittersTab_Content.transform.childCount != 0) - GameObject.DestroyImmediate(m_SongEmittersTab_Content.transform.GetChild(0).gameObject); - } - - int l_EmitterIndex = ((l_CurrentPage - 1) * s_EMITTER_PER_PAGE) + p_RelIndex; - if (p_RelIndex < 0 || l_EmitterIndex >= l_DataSource.Count) - { - if (m_MenuEmittersTab.activeSelf) - m_MenuEmittersSelected = -1; - else - m_SongEmittersSelected = -1; - return; - } - - if (m_MenuEmittersTab.activeSelf) - m_MenuEmittersSelected = l_EmitterIndex; - else - m_SongEmittersSelected = l_EmitterIndex; - - var l_Emitter = l_DataSource[l_EmitterIndex]; - if (m_MenuEmittersTab.activeSelf) - { - m_EmitterWidget.BuildUI(m_MenuEmittersTab_Content.transform, l_Emitter); - ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Menu, true, l_Emitter); - } - else - { - m_EmitterWidget.BuildUI(m_SongEmittersTab_Content.transform, l_Emitter); - ChatEmoteRain.Instance.SetTemplatesPreview(CP_SDK.ChatPlexSDK.EGenericScene.Playing, true, l_Emitter); - } - } - /// - /// Emitter list page DOWN - /// - private void OnEmitterPageDownPressed() - { - var l_CurrentPage = m_MenuEmittersTab.activeSelf ? m_MenuEmittersCurrentPage : m_SongEmittersCurrentPage; - - /// Increment current page - l_CurrentPage++; - - if (m_MenuEmittersTab.activeSelf) - m_MenuEmittersCurrentPage = l_CurrentPage; - else - m_SongEmittersCurrentPage = l_CurrentPage; - - /// Rebuild list - RebuildEmitterList(null); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On add emitter button - /// - [UIAction("click-emitters-add-btn-pressed")] - private void OnEmitterAdd() - { - var l_Emitter = new EmitterConfig(); - if (m_MenuEmittersTab.activeSelf) - { - CERConfig.Instance.MenuEmitters.Add(l_Emitter); - ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Menu); - } - else - { - CERConfig.Instance.SongEmitters.Add(l_Emitter); - ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Playing); - } - - RebuildEmitterList(l_Emitter); - } - /// - /// On toggle emitter button - /// - [UIAction("click-emitters-toggle-btn-pressed")] - private void OnEmitterToggle() - { - var l_DataSource = m_MenuEmittersTab.activeSelf ? CERConfig.Instance.MenuEmitters : CERConfig.Instance.SongEmitters; - var l_SelectedIndex = m_MenuEmittersTab.activeSelf ? m_MenuEmittersSelected : m_SongEmittersSelected; - - if (l_SelectedIndex == -1) - { - ShowMessageModal("Please select an emitter first!"); - return; - } - - var l_Emitter = l_DataSource[l_SelectedIndex]; - if (l_Emitter.Enabled) - { - ShowConfirmationModal($"Do you want to disable emitter\n\"{l_Emitter.Name}\"?", () => - { - l_Emitter.Enabled = false; - RebuildEmitterList(l_Emitter); - - if (m_MenuEmittersTab.activeSelf) - ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Menu); - else - ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Playing); - }); - } - else - { - ShowConfirmationModal($"Do you want to enable emitter\n\"{l_Emitter.Name}\"?", () => - { - l_Emitter.Enabled = true; - RebuildEmitterList(l_Emitter); - - if (m_MenuEmittersTab.activeSelf) - ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Menu); - else - ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Playing); - }); - } - } - /// - /// On delete emitter button - /// - [UIAction("click-emitters-delete-btn-pressed")] - private void OnEmitterDelete() - { - var l_DataSource = m_MenuEmittersTab.activeSelf ? CERConfig.Instance.MenuEmitters : CERConfig.Instance.SongEmitters; - var l_SelectedIndex = m_MenuEmittersTab.activeSelf ? m_MenuEmittersSelected : m_SongEmittersSelected; - - if (l_SelectedIndex == -1) - { - ShowMessageModal("Please select an emitter first!"); - return; - } - - var l_Emitter = l_DataSource[l_SelectedIndex]; - ShowConfirmationModal($"Do you want to delete emitter\n\"{l_Emitter.Name}\"?", () => - { - OnEmitterSelected(null, -1); - l_DataSource.Remove(l_Emitter); - RebuildEmitterList(null); - - if (m_MenuEmittersTab.activeSelf) - ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Menu); - else - ChatEmoteRain.Instance.UpdateTemplateFor(CP_SDK.ChatPlexSDK.EGenericScene.Playing); - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When settings are changed - /// - /// - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - /// General tab setup - if (true) - { - /// First row - CERConfig.Instance.EnableMenu = m_GeneralTab_MenuRain.Value; - CERConfig.Instance.MenuSize = m_GeneralTab_MenuRainSizeSlider.slider.value; - CERConfig.Instance.MenuSpeed = m_GeneralTab_MenuFallSpeedSlider.slider.value; - - /// Second row - CERConfig.Instance.EnableSong = m_GeneralTab_SongRain.Value; - CERConfig.Instance.SongSize = m_GeneralTab_SongRainSizeSlider.slider.value; - CERConfig.Instance.SongSpeed = m_GeneralTab_SongFallSpeedSlider.slider.value; - } - - /// Commands tab - if (true) - { - CERConfig.Instance.ChatCommands.ModeratorPower = m_CommandsTab_ModeratorPowerToggle.Value; - CERConfig.Instance.ChatCommands.VIPPower = m_CommandsTab_VIPPowerToggle.Value; - CERConfig.Instance.ChatCommands.SubscriberPower = m_CommandsTab_SubscriberPowerToggle.Value; - CERConfig.Instance.ChatCommands.UserPower = m_CommandsTab_UserPowerToggle.Value; - } - - ChatEmoteRain.Instance.OnSettingsChanged(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset settings - /// - internal void RefreshSettings() - { - m_PreventChanges = true; - - /// General tab setup - if (true) - { - /// First row - m_GeneralTab_MenuRain.Value = CERConfig.Instance.EnableMenu; - BeatSaberPlus.SDK.UI.SliderSetting.SetValue(m_GeneralTab_MenuRainSizeSlider, CERConfig.Instance.MenuSize); - BeatSaberPlus.SDK.UI.SliderSetting.SetValue(m_GeneralTab_MenuFallSpeedSlider, CERConfig.Instance.MenuSpeed); - - /// Second row - m_GeneralTab_SongRain.Value = CERConfig.Instance.EnableSong; - BeatSaberPlus.SDK.UI.SliderSetting.SetValue(m_GeneralTab_SongRainSizeSlider, CERConfig.Instance.SongSize); - BeatSaberPlus.SDK.UI.SliderSetting.SetValue(m_GeneralTab_SongFallSpeedSlider, CERConfig.Instance.SongSpeed); - } - - RebuildEmitterList(null); - - /// Commands tab - if (true) - { - m_CommandsTab_ModeratorPowerToggle.Value = CERConfig.Instance.ChatCommands.ModeratorPower; - m_CommandsTab_VIPPowerToggle.Value = CERConfig.Instance.ChatCommands.VIPPower; - m_CommandsTab_SubscriberPowerToggle.Value = CERConfig.Instance.ChatCommands.SubscriberPower; - m_CommandsTab_UserPowerToggle.Value = CERConfig.Instance.ChatCommands.UserPower; - } - - m_PreventChanges = false; - - ChatEmoteRain.Instance.OnSettingsChanged(); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsLeft.bsml b/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsLeft.bsml deleted file mode 100644 index 1ca6e5b..0000000 --- a/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsLeft.bsml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsLeft.cs b/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsLeft.cs deleted file mode 100644 index 1362cdb..0000000 --- a/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsLeft.cs +++ /dev/null @@ -1,90 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using System.Diagnostics; -using UnityEngine; - -namespace ChatPlexMod_ChatEmoteRain.UI -{ - /// - /// Emote rain settings credits view - /// - internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController - { - private static readonly string s_InformationsStr = "Original mod made by Cr4 and Uialeth" - + "\n" - + "\n" + "Commands" - + "\n" + "- [Moderator]!er toggle\nDisable any emote rain until a Menu/GamePlay scene change" - + "\n" + "- [Moderator]!er rain #EMOTE #COUNT\nTrigger a emote rain" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n"; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("Background")] - private GameObject m_Background = null; - [UIComponent("Informations")] - private HMUI.TextPageScrollView m_Informations = null; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); - m_Informations.SetText(s_InformationsStr); - m_Informations.UpdateVerticalScrollIndicator(0); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset button - /// - [UIAction("click-reset-btn-pressed")] - private void OnResetButton() - { - ShowConfirmationModal("Do you really want to reset\nall chat emote rain settings?", () => - { - /// Reset settings - CERConfig.Instance.Reset(); - CERConfig.Instance.Enabled = true; - CERConfig.Instance.Save(); - - /// Refresh values - Settings.Instance.RefreshSettings(); - SettingsRight.Instance.RefreshSettings(); - }); - } - /// - /// Open web configuration button - /// - [UIAction("click-open-web-configuration-btn-pressed")] - private void OnWebConfigurationButton() - { - ShowMessageModal("URL opened in your desktop browser."); - CP_SDK.Chat.Service.OpenWebConfigurator(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Documentation button - /// - [UIAction("click-documentation-btn-pressed")] - private void OnDocumentationButton() - { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://github.com/hardcpp/BeatSaberPlus/wiki#chat-emote-rain"); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsRight.bsml b/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsRight.bsml deleted file mode 100644 index e611a55..0000000 --- a/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsRight.bsml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsRight.cs b/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsRight.cs deleted file mode 100644 index c27ad2a..0000000 --- a/Modules/BeatSaberPlus_ChatEmoteRain/UI/SettingsRight.cs +++ /dev/null @@ -1,137 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.Parser; -using HMUI; -using UnityEngine; - -namespace ChatPlexMod_ChatEmoteRain.UI -{ - /// - /// Chat Emote Rain settings right view - /// - internal class SettingsRight : BeatSaberPlus.SDK.UI.ResourceViewController - { -#pragma warning disable CS0649 - [UIObject("TypeSegmentPanel")] - private GameObject m_TypeSegmentPanel; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("SubRainPanel")] - private GameObject m_SubRainPanel; - - [UIComponent("SubRainPanel_EnableToggle")] - private ToggleSetting m_SubRainPanel_EnableToggle; - [UIComponent("SubRainPanel_EmoteCountSlider")] - private SliderSetting m_SubRainPanel_EmoteCountSlider; - [UIObject("SubRainPanel_InfoBackground")] - private GameObject m_SubRainPanel_InfoBackground; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - /// - /// Type segment control - /// - private TextSegmentedControl m_TypeSegmentControl = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - /// Create event - var l_Event = new BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - var l_AnchorMin = new Vector2(0.15f, -0.05f); - var l_AnchorMax = new Vector2(0.85f, 1.05f); - - /// Create type selector - m_TypeSegmentControl = BeatSaberPlus.SDK.UI.TextSegmentedControl.Create(m_TypeSegmentPanel.transform as RectTransform, false, new string[] { "SubRain" }); - m_TypeSegmentControl.didSelectCellEvent += OnTypeChanged; - - /// SubRain panel - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_SubRainPanel_InfoBackground, 0.5f); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SubRainPanel_EnableToggle, l_Event, CERConfig.Instance.SubRain, true); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_SubRainPanel_EmoteCountSlider, l_Event, null, CERConfig.Instance.SubRainEmoteCount, true, true, l_AnchorMin, l_AnchorMax); - - /// Force change to tab SubRain - OnTypeChanged(null, 0); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When the type is changed - /// - /// Event sender - /// Tab index - private void OnTypeChanged(SegmentedControl p_Sender, int p_Index) - { - m_SubRainPanel.SetActive(p_Index == 0); - } - /// - /// When settings are changed - /// - /// - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - /// SubRain panel - CERConfig.Instance.SubRain = m_SubRainPanel_EnableToggle.Value; - CERConfig.Instance.SubRainEmoteCount = (int)m_SubRainPanel_EmoteCountSlider.slider.value; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset settings - /// - internal void RefreshSettings() - { - m_PreventChanges = true; - - /// SubRain panel - m_SubRainPanel_EnableToggle.Value = CERConfig.Instance.SubRain; - BeatSaberPlus.SDK.UI.SliderSetting.SetValue(m_SubRainPanel_EmoteCountSlider, CERConfig.Instance.SubRainEmoteCount); - - m_PreventChanges = false; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On reload button pressed - /// - [UIAction("click-reload-subrain-btn-pressed")] - private void OnReloadSubRainButton() - { - /// Reload sub rain - ChatEmoteRain.Instance.LoadSubRainFiles(); - - /// Show message - ShowMessageModal("SubRain textures were reloaded!"); - } - /// - /// On test button pressed - /// - [UIAction("click-test-subrain-btn-pressed")] - private void OnTestSubRainButton() - { - ChatEmoteRain.Instance.StartSubRain(); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatEmoteRain/manifest.json b/Modules/BeatSaberPlus_ChatEmoteRain/manifest.json index 67456b0..9550350 100644 --- a/Modules/BeatSaberPlus_ChatEmoteRain/manifest.json +++ b/Modules/BeatSaberPlus_ChatEmoteRain/manifest.json @@ -3,13 +3,12 @@ "id": "BeatSaberPlus_ChatEmoteRain", "name": "BeatSaberPlus_ChatEmoteRain", "author": "HardCPP#1985", - "version": "5.0.7", + "version": "6.0.8", "description": "", - "gameVersion": "1.25.0", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.0.1", - "BeatSaberMarkupLanguage": "^1.3.4", - "BeatSaberPlusCORE": "^5.0.7" + "BSIPA": "^4.3.0", + "BeatSaberPlusCORE": "^6.0.8" }, "links": { "project-home": "https://discord.chatplex.org", diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Camera2.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Camera2.cs deleted file mode 100644 index e743985..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Camera2.cs +++ /dev/null @@ -1,291 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class Camera2Builder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - default: - break; - } - - return new List() - { - new Camera2_SwitchToDefaultScene(), - new Camera2_SwitchToScene(), - new Camera2_ToggleCamera(), - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class Camera2_SwitchToDefaultScene : Interfaces.IAction - { - public override string Description => "Switch to default camera2 scene"; - - public Camera2_SwitchToDefaultScene() { UIPlaceHolder = "Switch to default camera2 scene"; UIPlaceHolderTestButton = true; } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModPresence.Camera2) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); - yield break; - } - - Camera2.SDK.Scenes.ShowNormalScene(); - - yield return null; - } - protected override void OnUIPlaceholderTestButton() - { - if (!ModPresence.Camera2) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); - return; - } - - Camera2.SDK.Scenes.ShowNormalScene(); - } - } - - public class Camera2_SwitchToScene : Interfaces.IAction - { - public override string Description => "Change active Camera2 scene"; - -#pragma warning disable CS0414 - [UIComponent("Scene_DropDown")] - protected DropDownListSetting m_Scene_DropDown = null; - [UIValue("Scene_DropDownOptions")] - private List m_Scene_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Scene_DropDown, l_Event, true); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (ModPresence.Camera2 && ModPresence.Camera2Fixed) - { - l_Choices = Camera2.SDK.Scenes.customScenes.Select(x => x.Key).ToList(); - - if (l_Choices.Count == 0) - l_Choices.Add("None"); - } - else if (!ModPresence.Camera2) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); - else if (!ModPresence.Camera2Fixed) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 is not updated!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.SceneName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Scene_DropDownOptions = l_Choices; - m_Scene_DropDown.values = l_Choices; - m_Scene_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Scene_DropDown.UpdateChoices(); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.SceneName = m_Scene_DropDown.Value as string; - } - - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (!ModPresence.Camera2) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); - return; - } - if (!ModPresence.Camera2Fixed) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 is not updated!"); - return; - } - - if (Camera2.SDK.Scenes.customScenes.ContainsKey(Model.SceneName)) - Camera2.SDK.Scenes.SwitchToCustomScene(Model.SceneName); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:Camera2_SwitchToScene Scene:{Model.SceneName} not found!"); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModPresence.Camera2) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); - yield break; - } - if (!ModPresence.Camera2Fixed) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 is not updated!"); - yield break; - } - - if (Camera2.SDK.Scenes.customScenes.ContainsKey(Model.SceneName)) - Camera2.SDK.Scenes.SwitchToCustomScene(Model.SceneName); - else - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:Camera2_SwitchToScene Scene:{Model.SceneName} not found!"); - } - - yield return null; - } - } - - public class Camera2_ToggleCamera : Interfaces.IAction - { - public override string Description => "Toggle Camera2 camera visibility"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Toggle", "On", "Off" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; - - [UIComponent("Camera_DropDown")] - protected DropDownListSetting m_Camera_DropDown = null; - [UIValue("Camera_DropDownOptions")] - private List m_Camera_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ToggleType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup( m_TypeList, l_Event, false); - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup( m_Camera_DropDown, l_Event, true); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (ModPresence.Camera2) - l_Choices = Camera2.SDK.Cameras.available.ToList(); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.CameraName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Camera_DropDownOptions = l_Choices; - m_Camera_DropDown.values = l_Choices; - m_Camera_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Camera_DropDown.UpdateChoices(); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.ToggleType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - Model.CameraName = m_Camera_DropDown.Value as string; - } - - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (!ModPresence.Camera2) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); - return; - } - - if (Camera2.SDK.Cameras.available.Contains(Model.CameraName)) - { - switch (Model.ToggleType) - { - case 0: - Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, !Camera2.SDK.Cameras.active.Contains(Model.CameraName)); - break; - case 1: - Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, true); - break; - case 2: - Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, false); - break; - } - } - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:Camera2_ToggleCamera Camera:{Model.CameraName} not found!"); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModPresence.Camera2) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); - yield break; - } - - if (Camera2.SDK.Cameras.available.Contains(Model.CameraName)) - { - switch (Model.ToggleType) - { - case 0: - Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, !Camera2.SDK.Cameras.active.Contains(Model.CameraName)); - break; - case 1: - Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, true); - break; - case 2: - Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, false); - break; - } - } - else - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:Camera2_ToggleCamera Camera:{Model.CameraName} not found!"); - } - - yield return null; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Chat.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Chat.cs deleted file mode 100644 index bf3e32f..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Chat.cs +++ /dev/null @@ -1,241 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.Parser; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using BeatSaberPlus_ChatIntegrations.Models; -using CP_SDK.Chat.Interfaces; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using TMPro; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class ChatBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - default: - break; - } - - return new List() - { - new Chat_SendMessage(), - new Chat_ToggleEmoteOnly(), - new Chat_ToggleVisibility() - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class Chat_SendMessage : Interfaces.IAction - { - public override string Description => "Send a message in the chat"; - - private BSMLParserParams m_ParserParams; - -#pragma warning disable CS0414 - [UIComponent("CurrentMessageText")] - private HMUI.TextPageScrollView m_CurrentMessageText = null; - [UIComponent("AddTTSPrefixToggle")] - private ToggleSetting m_AddTTSPrefixToggle = null; - - [UIComponent("ChatInputModal")] - protected HMUI.ModalView m_ChatInputModal = null; - [UIComponent("ChatInputModal_Text")] - protected TextMeshProUGUI m_ChatInputModal_Text = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - m_ParserParams = BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_AddTTSPrefixToggle, l_Event, Model.AddTTSPefix, false); - - /// Change opacity - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_ChatInputModal, 0.75f); - - /// Update UI - UpdateUI(); - } - private void OnSettingChanged(object p_Value) - { - Model.AddTTSPefix = m_AddTTSPrefixToggle.Value; - } - private void UpdateUI() - { - m_CurrentMessageText.SetText(Model.BaseValue); - } - - [UIAction("click-set-game-btn-pressed")] - private void OnSetFromGameButton() - { - var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == IValueType.String || x.Item1 == IValueType.Integer || x.Item1 == IValueType.Floating).ToArray(); - var l_Keys = new System.Collections.Generic.List<(string, System.Action)>(); - - foreach (var l_Var in l_Variables) - l_Keys.Add(("$" + l_Var.Item2, () => UI.Settings.Instance.UIInputKeyboardAppend("$" + l_Var.Item2))); - - UI.Settings.Instance.UIShowInputKeyboard(Model.BaseValue, (p_Result) => - { - Model.BaseValue = p_Result; - - /// Update UI - UpdateUI(); - - }, l_Keys); - } - [UIAction("click-set-chat-btn-pressed")] - private void OnSetFromChatButton() - { - ChatIntegrations.Instance.OnBroadcasterChatMessage += Instance_OnBroadcasterChatMessage; - - var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == IValueType.String || x.Item1 == IValueType.Integer || x.Item1 == IValueType.Floating).ToArray(); - var l_Message = "Please input a message in chat with your streaming account.\nProvided values:\n"; - l_Message += string.Join(", ", l_Variables.Select(x => "$" + x.Item2).ToArray()); - - m_ChatInputModal_Text.text = l_Message; - - m_ParserParams.EmitEvent("ShowChatInputModal"); - } - private void Instance_OnBroadcasterChatMessage(IChatMessage p_Message) - { - Model.BaseValue = p_Message.Message; - ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; - - m_ParserParams.EmitEvent("CloseChatInputModal"); - - UpdateUI(); - } - [UIAction("click-cancel-set-chat-btn-pressed")] - private void OnCancelSetFromChatButton() - { - m_ParserParams.EmitEvent("CloseChatInputModal"); - ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public override IEnumerator Eval(Models.EventContext p_Context) - { - /// Temp - Model.BaseValue = Model.BaseValue.Replace("$SenderName", "$UserName"); - - var l_Message = (Model.AddTTSPefix ? "! " : "") + Model.BaseValue; - var l_Variables = p_Context.GetValues(IValueType.String, IValueType.Integer, IValueType.Floating); - - foreach (var l_Var in l_Variables) - { - var l_Key = "$" + l_Var.Item2; - var l_ReplaceValue = l_Var.Item1 == IValueType.String ? "" : "0"; - - if (l_Var.Item1 == IValueType.Integer && p_Context.GetIntegerValue(l_Var.Item2, out var l_IntegerVal)) - l_ReplaceValue = l_IntegerVal.Value.ToString(); - else if (l_Var.Item1 == IValueType.Floating && p_Context.GetFloatingValue(l_Var.Item2, out var l_FloatVal)) - l_ReplaceValue = l_FloatVal.Value.ToString(); - else if (l_Var.Item1 == IValueType.String && p_Context.GetStringValue(l_Var.Item2, out var l_StringVal)) - l_ReplaceValue = l_StringVal; - - l_Message = l_Message.Replace(l_Key, l_ReplaceValue); - } - - if (p_Context.ChatService != null && p_Context.Channel != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, l_Message); - else - CP_SDK.Chat.Service.BroadcastMessage(l_Message); - - yield return null; - } - } - - public class Chat_ToggleEmoteOnly : Interfaces.IAction - { - public override string Description => "Enable or disable emote only in the chat"; - - public Chat_ToggleEmoteOnly() => UIPlaceHolder = "Enable or disable emote only in the chat"; - - public override IEnumerator Eval(EventContext p_Context) - { - var l_Channel = CP_SDK.Chat.Service.Multiplexer.Channels.First(); - if (l_Channel.Item2 is CP_SDK.Chat.Models.Twitch.TwitchChannel l_TwitchChannel) - { - if (l_TwitchChannel.Roomstate.EmoteOnly) - l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/emoteonlyoff"); - else - l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/emoteonly"); - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class Chat_ToggleVisibility : Interfaces.IAction - { - public override string Description => "Show or hide the chat ingame"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Toggle", "On", "Off" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ToggleType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.ToggleType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.Chat) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("Chat: Action failed, Chat module is missing!"); - yield break; - } - - switch (Model.ToggleType) - { - case 0: - ChatPlexMod_Chat.Chat.Instance?.ToggleVisibility(); - break; - case 1: - ChatPlexMod_Chat.Chat.Instance?.SetVisible(true); - break; - case 2: - ChatPlexMod_Chat.Chat.Instance?.SetVisible(false); - break; - } - - yield return null; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/EmoteRain.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/EmoteRain.cs deleted file mode 100644 index 7ec8184..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/EmoteRain.cs +++ /dev/null @@ -1,280 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.Parser; -using BeatSaberPlus_ChatIntegrations.Models; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class EmoteRainBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - default: - break; - } - - return new List() - { - new EmoteRain_CustomRain(), - new EmoteRain_EmoteBombRain(), - new EmoteRain_SubRain() - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class EmoteRain_CustomRain : Interfaces.IAction - { - public override string Description => "Make rain custom emotes"; - - private BSMLParserParams m_ParserParams; - private CP_SDK.Unity.EnhancedImage m_LoadedImage = null; - private string m_LoadedImageID = ""; - private string m_LoadedImageName = ""; - -#pragma warning disable CS0414 - [UIComponent("File_DropDown")] - protected DropDownListSetting m_File_DropDown = null; - [UIValue("File_DropDownOptions")] - private List m_File_DropDownOptions = new List() { "Loading...", }; - [UIComponent("CountIncrement")] - protected IncrementSetting m_CountIncrement = null; - [UIObject("InfoPanel_Background")] - private GameObject m_InfoPanel_Background = null; - - [UIObject("EmoteRainNotEnabledModal")] - private GameObject m_EmoteRainNotEnabledModal = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - m_ParserParams = BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoPanel_Background, 0.75f); - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_EmoteRainNotEnabledModal, 0.75f); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_CountIncrement, l_Event, null, Model.Count, false); - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_File_DropDown, l_Event, true); - - var l_Files = Directory.GetFiles(ChatIntegrations.s_EMOTE_RAIN_ASSETS_PATH, "*.png") - .Union(Directory.GetFiles(ChatIntegrations.s_EMOTE_RAIN_ASSETS_PATH, "*.gif")) - .Union(Directory.GetFiles(ChatIntegrations.s_EMOTE_RAIN_ASSETS_PATH, "*.apng")).ToArray(); - - bool l_ChoiceExist = false; - var l_Choices = new List(); - l_Choices.Add("None"); - foreach (var l_CurrentFile in l_Files) - { - var l_Filtered = Path.GetFileName(l_CurrentFile); - l_Choices.Add(l_Filtered); - - if (l_Filtered == Model.BaseValue) - l_ChoiceExist = true; - } - - m_File_DropDownOptions = l_Choices; - m_File_DropDown.values = l_Choices; - m_File_DropDown.Value = l_ChoiceExist ? Model.BaseValue : l_Choices[0]; - m_File_DropDown.UpdateChoices(); - - if (!ModulePresence.ChatEmoteRain) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: ChatEmoteRain module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.BaseValue = (string)m_File_DropDown.Value; - Model.Count = (uint)m_CountIncrement.Value; - - if ((string)p_Value == "None") - Model.BaseValue = ""; - } - - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance == null || !ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) - { - m_ParserParams.EmitEvent("ShowEmoteRainNotEnabledModal"); - return; - } - - MakeItRain(); - } - - public override IEnumerator Eval(EventContext p_Context) - { - if (!ModulePresence.ChatEmoteRain) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatEmoteRain module is missing!"); - yield break; - } - - if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance != null && ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) - MakeItRain(); - else - p_Context.HasActionFailed = true; - - yield return null; - } - - private void MakeItRain() - { - EnsureLoaded((p_Loaded) => - { - if (m_LoadedImage == null) - return; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.EmitEnhancedImage(m_LoadedImage, Model.Count)); - }); - } - private void EnsureLoaded(Action p_Callback) - { - CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => - { - if (Model.BaseValue == "None") - { - p_Callback?.Invoke(false); - return; - } - - if (m_LoadedImageName != Model.BaseValue) - { - m_LoadedImageName = Model.BaseValue; - - string l_Path = Path.Combine(ChatIntegrations.s_EMOTE_RAIN_ASSETS_PATH, Model.BaseValue); - if (File.Exists(l_Path)) - { - m_LoadedImageID = "$BSP$CI$_" + Model.BaseValue; - CP_SDK.Unity.EnhancedImage.FromFile(l_Path, m_LoadedImageID, (p_Result) => - { - m_LoadedImage = p_Result; - p_Callback?.Invoke(m_LoadedImage != null); - }); - } - else - p_Callback?.Invoke(false); - } - - p_Callback?.Invoke(true); - }); - } - } - - public class EmoteRain_EmoteBombRain : Interfaces.IAction - { - public override string Description => "Trigger a massive emote bomb rain"; - - private BSMLParserParams m_ParserParams; - -#pragma warning disable CS0414 - [UIComponent("KindIncrement")] - protected IncrementSetting m_KindIncrement = null; - [UIComponent("CountIncrement")] - protected IncrementSetting m_CountIncrement = null; - - [UIObject("EmoteRainNotEnabledModal")] - private GameObject m_EmoteRainNotEnabledModal = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - m_ParserParams = BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_EmoteRainNotEnabledModal, 0.75f); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_KindIncrement, l_Event, null, Model.EmoteKindCount, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_CountIncrement, l_Event, null, Model.CountPerEmote, false); - - if (!ModulePresence.ChatEmoteRain) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: ChatEmoteRain module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.EmoteKindCount = (uint)m_KindIncrement.Value; - Model.CountPerEmote = (uint)m_CountIncrement.Value; - } - - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance == null || !ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) - { - m_ParserParams.EmitEvent("ShowEmoteRainNotEnabledModal"); - return; - } - - CP_SDK.Unity.MTCoroutineStarter.Start(Eval(null)); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.ChatEmoteRain) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatEmoteRain module is missing!"); - yield break; - } - - if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance != null && ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) - { - CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => - { - var l_Emotes = - CP_SDK.Chat.ChatImageProvider.CachedEmoteInfo.Values.OrderBy(_ => UnityEngine.Random.Range(0, 1000)).Take((int)Model.EmoteKindCount); - - foreach (var l_Emote in l_Emotes) - ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.EmitEnhancedImage(l_Emote, Model.CountPerEmote); - }); - } - else if (p_Context != null) - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class EmoteRain_SubRain : Interfaces.IAction - { - public override string Description => "Trigger a subscription rain"; - - public EmoteRain_SubRain() => UIPlaceHolder = "Will trigger a subscription emote rain"; - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.ChatEmoteRain) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatEmoteRain module is missing!"); - yield break; - } - - if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance != null && ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) - ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.StartSubRain(); - else - p_Context.HasActionFailed = true; - - yield return null; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Event.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Event.cs deleted file mode 100644 index d3e0040..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Event.cs +++ /dev/null @@ -1,215 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberPlus_ChatIntegrations.Models; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class EventBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - default: - break; - } - - return new List() - { - new Event_ExecuteDummy(), - new Event_Toggle() - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class Event_ExecuteDummy : Interfaces.IAction - { - public override string Description => "Execute a dummy event"; - -#pragma warning disable CS0414 - [UIComponent("Event_DropDown")] - protected DropDownListSetting m_Event_DropDown = null; - [UIValue("Event_DropDownOptions")] - private List m_Event_DropDownOptions = new List() { "Loading...", }; - [UIObject("InfoPanel_Background")] - private GameObject m_InfoPanel_Background = null; - [UIComponent("ContinueToggle")] - private ToggleSetting m_ContinueToggle = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoPanel_Background, 0.75f); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Event_DropDown, l_Event, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ContinueToggle, l_Event, Model.Continue, false); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - var l_Events = ChatIntegrations.Instance.GetEventsByType(typeof(Events.Dummy)); - l_Events.Sort((x, y) => (x.GetTypeNameShort() + x.GenericModel.Name).CompareTo(y.GetTypeNameShort() + y.GenericModel.Name)); - foreach (var l_EventBase in l_Events) - { - if (l_EventBase == Event) - continue; - - l_Choices.Add(l_EventBase.GenericModel.Name); - - if (Model.BaseValue != "" && l_EventBase.GenericModel.GUID == Model.BaseValue) - l_ChoiceIndex = l_Choices.Count - 1; - } - - m_Event_DropDownOptions = l_Choices; - m_Event_DropDown.values = l_Choices; - m_Event_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Event_DropDown.UpdateChoices(); - } - private void OnSettingChanged(object p_Value) - { - if ((string)p_Value == "None") - Model.BaseValue = ""; - - var l_SelectedEvent = ChatIntegrations.Instance.GetEventByName((string)m_Event_DropDown.Value); - - if (l_SelectedEvent != null && l_SelectedEvent is Events.Dummy) - Model.BaseValue = l_SelectedEvent.GenericModel.GUID; - else - { - Model.BaseValue = ""; - m_Event_DropDown.Value = m_Event_DropDownOptions[0]; - } - } - - public override IEnumerator Eval(EventContext p_Context) - { - var l_SelectedEvent = ChatIntegrations.Instance.GetEventByGUID(Model.BaseValue); - if (l_SelectedEvent != null && l_SelectedEvent is Events.Dummy) - { - if (Model.Continue) - ChatIntegrations.Instance.ExecuteEvent(l_SelectedEvent, new EventContext() { Type = Interfaces.TriggerType.Dummy }); - else - p_Context.HasActionFailed = !ChatIntegrations.Instance.ExecuteEvent(l_SelectedEvent, new EventContext() { Type = Interfaces.TriggerType.Dummy }); - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class Event_Toggle : Interfaces.IAction - { - public override string Description => "Toggle an event"; - -#pragma warning disable CS0414 - [UIComponent("Event_DropDown")] protected DropDownListSetting m_Event_DropDown = null; - [UIValue("Event_DropDownOptions")] private List m_Event_DropDownOptions = new List() { "Loading...", }; - [UIComponent("TypeList")] private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] private List m_TypeListList_Choices = new List() { "Toggle", "Enable", "Disable" }; - [UIValue("TypeList_Value")] private string m_TypeList_Value; -#pragma warning restore CS0414 - - private Dictionary m_NameToGUID = new Dictionary(); - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ChangeType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Event_DropDown, l_Event, true); - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - m_NameToGUID.Clear(); - - var l_Events = ChatIntegrations.Instance.GetEventsByType(null); - l_Events.Sort((x, y) => (x.GetTypeNameShort() + x.GenericModel.Name).CompareTo(y.GetTypeNameShort() + y.GenericModel.Name)); - foreach (var l_EventBase in l_Events) - { - var l_Line = BuildLineString(l_EventBase); - l_Choices.Add(l_Line); - m_NameToGUID.Add(l_Line, l_EventBase.GenericModel.GUID); - - if (Model.BaseValue != "" && l_EventBase.GenericModel.GUID == Model.BaseValue) - l_ChoiceIndex = l_Choices.Count - 1; - } - - m_Event_DropDownOptions = l_Choices; - m_Event_DropDown.values = l_Choices; - m_Event_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Event_DropDown.UpdateChoices(); - } - private void OnSettingChanged(object p_Value) - { - if ((string)p_Value == "None") - Model.BaseValue = ""; - - if (m_NameToGUID.TryGetValue((string)m_Event_DropDown.Value, out var l_SelectedGUID)) - Model.BaseValue = l_SelectedGUID; - else - { - Model.BaseValue = ""; - m_Event_DropDown.Value = m_Event_DropDownOptions[0]; - } - - Model.ChangeType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - } - - public override IEnumerator Eval(EventContext p_Context) - { - var l_SelectedEvent = string.IsNullOrEmpty(Model.BaseValue) ? null : ChatIntegrations.Instance.GetEventByGUID(Model.BaseValue); - if (l_SelectedEvent != null) - { - if (Model.ChangeType == 0 || (Model.ChangeType == 1 && !l_SelectedEvent.IsEnabled) || (Model.ChangeType == 2 && l_SelectedEvent.IsEnabled)) - ChatIntegrations.Instance.ToggleEvent(l_SelectedEvent); - } - - yield return null; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build event line - /// - /// Event to build for - private string BuildLineString(Interfaces.IEventBase p_Event) - { - /// Result line - string l_Text = ""; - l_Text += ""; - - /// Left part - l_Text += "[" + p_Event.GetTypeNameShort() + "] "; - l_Text += p_Event.GenericModel.Name; - l_Text += ""; - - return l_Text; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/GamePlay.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/GamePlay.cs deleted file mode 100644 index 5f88313..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/GamePlay.cs +++ /dev/null @@ -1,1387 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberPlus_ChatIntegrations.Models; -using IPA.Utilities; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class GamePlayBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - case Events.LevelEnded _: - return new List() - { - - }; - } - - return new List() - { - new GamePlay_ChangeBombColor(), - new GamePlay_ChangeBombScale(), - new GamePlay_ChangeDebris(), - new GamePlay_ChangeLightIntensity(), - new GamePlay_ChangeMusicVolume(), - new GamePlay_ChangeNoteColors(), - new GamePlay_ChangeNoteScale(), - new GamePlay_Pause(), - new GamePlay_Quit(), - new GamePlay_Restart(), - new GamePlay_Resume(), - new GamePlay_SpawnBombPatterns(), - new GamePlay_SpawnSquatWalls(), - new GamePlay_ToggleHUD() - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - - public class GamePlay_ChangeBombColor : Interfaces.IAction - { - public override string Description => "Change bomb color"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] private List m_TypeListList_Choices = new List() { "Default", "Input", "EventInput" }; - [UIValue("TypeList_Value")] private string m_TypeList_Value; - [UIComponent("Color")] protected ColorSetting m_Color = null; - [UIComponent("SendMessageToggle")] private ToggleSetting m_SendMessageToggle = null; -#pragma warning restore CS0414 - - private Color? m_ColorCache; - - public override sealed void BuildUI(Transform p_Parent) - { - if (Event.GetType() != typeof(Events.ChatCommand) - && Event.GetType() != typeof(Events.ChatPointsReward)) - { - m_TypeListList_Choices.Remove("EventInput"); - } - - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ValueType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - EnsureColorCache(); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_Color, l_Event, m_ColorCache.Value, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SendMessageToggle, l_Event, Model.SendChatMessage, false); - - OnSettingChanged(null); - - if (!ModulePresence.NoteTweaker) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: NoteTweaker module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.ValueType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - m_ColorCache = m_Color.CurrentColor; - Model.SendChatMessage = m_SendMessageToggle.Value; - - Model.Color = "#" + ColorUtility.ToHtmlStringRGB(m_ColorCache.Value); - - m_Color.interactable = Model.ValueType == 1; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.NoteTweaker) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); - yield break; - } - - bool l_Failed = true; - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing - || Model.ValueType == 0) - { - if (Model.ValueType == 0) - { - BeatSaberPlus_NoteTweaker.Patches.PBombController.SetBombColorOverride(false, Color.black); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} bomb color is back to default!"); - - l_Failed = false; - } - else - { - var l_Hex = "#" + Model.Color; - - if (Model.ValueType == 2 && (p_Context.Message != null || p_Context.PointsEvent != null)) /// Event user input - { - var l_Src = (p_Context.Message?.Message ?? p_Context.PointsEvent?.UserInput) ?? ""; - var l_Parts = l_Src.Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); - if (l_Parts.Length >= 1 - && ColorUtility.TryParseHtmlString(l_Parts[l_Parts.Length - 1], out var l_LeftColor)) - { - m_ColorCache = l_LeftColor; - l_Hex = l_Parts[l_Parts.Length - 2]; - l_Failed = false; - } - else if (p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - { - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} the syntax is: #HEXCOLOR"); - } - } - else - { - l_Failed = false; - EnsureColorCache(); - } - - if (!l_Failed) - { - BeatSaberPlus_NoteTweaker.Patches.PBombController.SetBombColorOverride(true, m_ColorCache.Value); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} bomb color is changed to {l_Hex}"); - } - } - } - - if (l_Failed) - p_Context.HasActionFailed = true; - - yield return null; - } - - private void EnsureColorCache() - { - if (!m_ColorCache.HasValue) - { - if (ColorUtility.TryParseHtmlString(Model.Color, out var l_Color)) - m_ColorCache = l_Color; - else - m_ColorCache = Color.black; - } - } - } - - public class GamePlay_ChangeBombScale : Interfaces.IAction - { - public override string Description => "Choose a random bomb scale"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Random", "Input", "EventInput", "Config" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; - [UIComponent("UserIncrement")] - protected IncrementSetting m_UserIncrement = null; - [UIComponent("MinIncrement")] - protected IncrementSetting m_MinIncrement = null; - [UIComponent("MaxIncrement")] - protected IncrementSetting m_MaxIncrement = null; - [UIComponent("SendMessageToggle")] - private ToggleSetting m_SendMessageToggle = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ValueType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_UserIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.UserValue, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MinIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.Min, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MaxIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.Max, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SendMessageToggle, l_Event, Model.SendChatMessage, false); - - OnSettingChanged(null); - - if (!ModulePresence.GameTweaker) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: GameTweaker module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.ValueType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - Model.UserValue = m_UserIncrement.Value; - Model.Min = m_MinIncrement.Value; - Model.Max = m_MaxIncrement.Value; - Model.SendChatMessage = m_SendMessageToggle.Value; - - m_MinIncrement.interactable = Model.ValueType == 0 || Model.ValueType == 2; - m_MaxIncrement.interactable = Model.ValueType == 0 || Model.ValueType == 2; - m_UserIncrement.interactable = Model.ValueType == 1; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.NoteTweaker) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); - yield break; - } - - bool l_Failed = true; - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing - || Model.ValueType == 3) - { - if (Model.ValueType == 3) - { - BeatSaberPlus_NoteTweaker.Patches.PBombController.SetTemp(false, 0f); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} note scale was set to default"); - } - else - { - var l_NewValue = 0f; - - /// Random - if (Model.ValueType == 0) - l_NewValue = UnityEngine.Random.Range(Model.Min, Model.Max); - /// User input - else if (Model.ValueType == 1) - l_NewValue = Model.UserValue; - /// Event input - else if (Model.ValueType == 2) - { - var l_FirstInteger = p_Context.GetFirstValueOfType(Interfaces.IValueType.Integer); - var l_EventInput = 1f; - - if (l_FirstInteger != default && p_Context.GetIntegerValue(l_FirstInteger.Item2, out var l_ContextVar)) - { - l_EventInput = (((float)l_ContextVar.Value) / 100.0f); - l_EventInput = Mathf.Max(Model.Min, l_EventInput); - l_EventInput = Mathf.Min(Model.Max, l_EventInput); - } - - l_NewValue = l_EventInput; - } - - BeatSaberPlus_NoteTweaker.Patches.PBombController.SetTemp(true, l_NewValue); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} bomb scale was set to {Mathf.RoundToInt(l_NewValue * 100f)}%"); - } - - l_Failed = false; - } - - if (l_Failed) - { - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} no map is currently played!"); - - p_Context.HasActionFailed = true; - } - - yield return null; - } - } - - public class GamePlay_ChangeDebris : Interfaces.IAction - { - public override string Description => "Turn on or off debris"; - -#pragma warning disable CS0414 - [UIComponent("DebrisToggle")] - private ToggleSetting m_DebrisToggle = null; -#pragma warning restore CS0414 - - public override void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_DebrisToggle, l_Event, Model.Debris, false); - - OnSettingChanged(null); - - if (!ModulePresence.GameTweaker) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: GameTweaker module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.Debris = m_DebrisToggle.Value; - } - - public override IEnumerator Eval(EventContext p_Context) - { - if (!ModulePresence.GameTweaker) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, GameTweaker module is missing!"); - yield break; - } - - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - BeatSaberPlus_GameTweaker.Patches.PNoteDebrisSpawner.SetTemp(!Model.Debris); - else - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class GamePlay_ChangeLightIntensity : Interfaces.IAction - { - public override string Description => "Choose a random light intensity"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Random", "Input", "EventInput", "Config" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; - [UIComponent("UserIncrement")] - protected IncrementSetting m_UserIncrement = null; - [UIComponent("MinIncrement")] - protected IncrementSetting m_MinIncrement = null; - [UIComponent("MaxIncrement")] - protected IncrementSetting m_MaxIncrement = null; - [UIComponent("SendMessageToggle")] - private ToggleSetting m_SendMessageToggle = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ValueType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_UserIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.UserValue, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MinIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.Min, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MaxIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.Max, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SendMessageToggle, l_Event, Model.SendChatMessage, false); - - OnSettingChanged(null); - - if (!ModulePresence.GameTweaker) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: GameTweaker module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.ValueType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - Model.UserValue = m_UserIncrement.Value; - Model.Min = m_MinIncrement.Value; - Model.Max = m_MaxIncrement.Value; - Model.SendChatMessage = m_SendMessageToggle.Value; - - m_MinIncrement.interactable = Model.ValueType == 0 || Model.ValueType == 2; - m_MaxIncrement.interactable = Model.ValueType == 0 || Model.ValueType == 2; - m_UserIncrement.interactable = Model.ValueType == 1; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.GameTweaker) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, GameTweaker module is missing!"); - yield break; - } - - bool l_Failed = true; - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - { - var l_Level = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.difficultyBeatmap?.difficulty; - var l_Effects = l_Level == BeatmapDifficulty.ExpertPlus - ? BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.playerSpecificSettings?.environmentEffectsFilterExpertPlusPreset - : BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.playerSpecificSettings?.environmentEffectsFilterDefaultPreset; - - if (l_Effects != EnvironmentEffectsFilterPreset.NoEffects) - { - if (Model.ValueType == 3) - { - BeatSaberPlus_GameTweaker.Patches.Lights.PLightsPatches.SetFromConfig(); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} lights was set to default"); - } - else - { - var l_NewValue = 0f; - - /// Random - if (Model.ValueType == 0) - l_NewValue = UnityEngine.Random.Range(Model.Min, Model.Max); - /// User input - else if (Model.ValueType == 1) - l_NewValue = Model.UserValue; - /// Event input - else if (Model.ValueType == 2) - { - var l_FirstInteger = p_Context.GetFirstValueOfType(Interfaces.IValueType.Integer); - var l_EventInput = 1f; - - if (l_FirstInteger != default && p_Context.GetIntegerValue(l_FirstInteger.Item2, out var l_ContextVar)) - { - l_EventInput = (((float)l_ContextVar.Value) / 100.0f); - l_EventInput = Mathf.Max(Model.Min, l_EventInput); - l_EventInput = Mathf.Min(Model.Max, l_EventInput); - } - - l_NewValue = l_EventInput; - } - - BeatSaberPlus_GameTweaker.Patches.Lights.PLightsPatches.SetTempLightIntensity(l_NewValue); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} lights was set to {Mathf.RoundToInt(l_NewValue * 100f)}%"); - } - - l_Failed = false; - } - } - - if (l_Failed) - { - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} no map is currently played!"); - - p_Context.HasActionFailed = true; - } - - yield return null; - } - } - - public class GamePlay_ChangeMusicVolume : Interfaces.IAction - { - public override string Description => "Choose a random volume music"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Random", "Input", "EventInput" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; - [UIComponent("UserIncrement")] - protected IncrementSetting m_UserIncrement = null; - [UIComponent("MinIncrement")] - protected IncrementSetting m_MinIncrement = null; - [UIComponent("MaxIncrement")] - protected IncrementSetting m_MaxIncrement = null; - [UIComponent("SendMessageToggle")] - private ToggleSetting m_SendMessageToggle = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ValueType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_UserIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.UserValue, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MinIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.Min, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MaxIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.Max, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SendMessageToggle, l_Event, Model.SendChatMessage, false); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.ValueType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - Model.UserValue = m_UserIncrement.Value; - Model.Min = m_MinIncrement.Value; - Model.Max = m_MaxIncrement.Value; - Model.SendChatMessage = m_SendMessageToggle.Value; - - m_MinIncrement.interactable = Model.ValueType != 1; - m_MaxIncrement.interactable = Model.ValueType != 1; - m_UserIncrement.interactable = Model.ValueType == 1; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - { - var l_NewValue = 0f; - var l_AudioTimeSyncController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - - if (l_AudioTimeSyncController != null && l_AudioTimeSyncController) - { - /// Random - if (Model.ValueType == 0) - l_NewValue = UnityEngine.Random.Range(Model.Min, Model.Max); - /// User input - else if (Model.ValueType == 1) - l_NewValue = Model.UserValue; - /// Event input - else if (Model.ValueType == 2) - { - var l_FirstInteger = p_Context.GetFirstValueOfType(Interfaces.IValueType.Integer); - var l_EventInput = 1f; - - if (l_FirstInteger != default && p_Context.GetIntegerValue(l_FirstInteger.Item2, out var l_ContextVar)) - { - l_EventInput = (((float)l_ContextVar.Value) / 100.0f); - l_EventInput = Mathf.Max(Model.Min, l_EventInput); - l_EventInput = Mathf.Min(Model.Max, l_EventInput); - } - - l_NewValue = l_EventInput; - } - - l_AudioTimeSyncController.GetField("_audioSource").volume = l_NewValue; - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} music volume was set to {Mathf.RoundToInt(l_NewValue * 100f)}]%"); - } - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class GamePlay_ChangeNoteScale : Interfaces.IAction - { - public override string Description => "Choose a random note scale"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Random", "Input", "EventInput", "Config" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; - [UIComponent("UserIncrement")] - protected IncrementSetting m_UserIncrement = null; - [UIComponent("MinIncrement")] - protected IncrementSetting m_MinIncrement = null; - [UIComponent("MaxIncrement")] - protected IncrementSetting m_MaxIncrement = null; - [UIComponent("SendMessageToggle")] - private ToggleSetting m_SendMessageToggle = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ValueType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_UserIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.UserValue, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MinIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.Min, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MaxIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.Max, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SendMessageToggle, l_Event, Model.SendChatMessage, false); - - OnSettingChanged(null); - - if (!ModulePresence.GameTweaker) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: GameTweaker module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.ValueType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - Model.UserValue = m_UserIncrement.Value; - Model.Min = m_MinIncrement.Value; - Model.Max = m_MaxIncrement.Value; - Model.SendChatMessage = m_SendMessageToggle.Value; - - m_MinIncrement.interactable = Model.ValueType == 0 || Model.ValueType == 2; - m_MaxIncrement.interactable = Model.ValueType == 0 || Model.ValueType == 2; - m_UserIncrement.interactable = Model.ValueType == 1; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.NoteTweaker) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); - yield break; - } - - bool l_Failed = true; - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing - || Model.ValueType == 3) - { - if (Model.ValueType == 3) - { - BeatSaberPlus_NoteTweaker.Patches.PGameNoteController.SetTemp(false, 0f); - BeatSaberPlus_NoteTweaker.Patches.PBurstSliderGameNoteController.SetTemp(false, 0f); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} note scale was set to default"); - } - else - { - var l_NewValue = 0f; - - /// Random - if (Model.ValueType == 0) - l_NewValue = UnityEngine.Random.Range(Model.Min, Model.Max); - /// User input - else if (Model.ValueType == 1) - l_NewValue = Model.UserValue; - /// Event input - else if (Model.ValueType == 2) - { - var l_FirstInteger = p_Context.GetFirstValueOfType(Interfaces.IValueType.Integer); - var l_EventInput = 1f; - - if (l_FirstInteger != default && p_Context.GetIntegerValue(l_FirstInteger.Item2, out var l_ContextVar)) - { - l_EventInput = (((float)l_ContextVar.Value) / 100.0f); - l_EventInput = Mathf.Max(Model.Min, l_EventInput); - l_EventInput = Mathf.Min(Model.Max, l_EventInput); - } - - l_NewValue = l_EventInput; - } - - BeatSaberPlus_NoteTweaker.Patches.PGameNoteController.SetTemp(true, l_NewValue); - BeatSaberPlus_NoteTweaker.Patches.PBurstSliderGameNoteController.SetTemp(true, l_NewValue); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} note scale was set to {Mathf.RoundToInt(l_NewValue * 100f)}%"); - } - - l_Failed = false; - } - - if (l_Failed) - { - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} no map is currently played!"); - - p_Context.HasActionFailed = true; - } - - yield return null; - } - } - - public class GamePlay_ChangeNoteColors : Interfaces.IAction - { - public override string Description => "Change notes colors"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Default", "Input", "EventInput" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; - [UIComponent("LeftColor")] - protected ColorSetting m_LeftColor = null; - [UIComponent("RightColor")] - protected ColorSetting m_RightColor = null; - [UIComponent("SendMessageToggle")] - private ToggleSetting m_SendMessageToggle = null; -#pragma warning restore CS0414 - - private Color? m_LeftColorCache; - private Color? m_RightColorCache; - - public override sealed void BuildUI(Transform p_Parent) - { - if (Event.GetType() != typeof(Events.ChatCommand) - && Event.GetType() != typeof(Events.ChatPointsReward)) - { - m_TypeListList_Choices.Remove("EventInput"); - } - - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ValueType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - EnsureColorCache(); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_LeftColor, l_Event, m_LeftColorCache.Value, false); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_RightColor, l_Event, m_RightColorCache.Value, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SendMessageToggle, l_Event, Model.SendChatMessage, false); - - OnSettingChanged(null); - - if (!ModulePresence.NoteTweaker) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: NoteTweaker module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.ValueType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - m_LeftColorCache = m_LeftColor.CurrentColor; - m_RightColorCache = m_RightColor.CurrentColor; - Model.SendChatMessage = m_SendMessageToggle.Value; - - Model.Left = "#" + ColorUtility.ToHtmlStringRGB(m_LeftColorCache.Value); - Model.Right = "#" + ColorUtility.ToHtmlStringRGB(m_RightColorCache.Value); - - m_LeftColor.interactable = Model.ValueType == 1; - m_RightColor.interactable = Model.ValueType == 1; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.NoteTweaker) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); - yield break; - } - - bool l_Failed = true; - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing - || Model.ValueType == 0) - { - if (Model.ValueType == 0) - { - BeatSaberPlus_NoteTweaker.Patches.PColorNoteVisuals.SetBlockColorOverride(false, Color.black, Color.black); - PatchSabers(true); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} colors are back to default!"); - - l_Failed = false; - } - else - { - string l_LeftHex = "#" + Model.Left; - string l_RightHex = "#" + Model.Right; - - if (Model.ValueType == 2 && (p_Context.Message != null || p_Context.PointsEvent != null)) /// Event user input - { - var l_Src = (p_Context.Message?.Message ?? p_Context.PointsEvent?.UserInput) ?? ""; - var l_Parts = l_Src.Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); - if (l_Parts.Length >= 2 - && ColorUtility.TryParseHtmlString(l_Parts[l_Parts.Length - 2], out var l_LeftColor) - && ColorUtility.TryParseHtmlString(l_Parts[l_Parts.Length - 1], out var l_RightColor)) - { - m_LeftColorCache = l_LeftColor; - m_RightColorCache = l_RightColor; - l_LeftHex = l_Parts[l_Parts.Length - 2]; - l_RightHex = l_Parts[l_Parts.Length - 1]; - l_Failed = false; - } - else if (p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - { - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} the syntax is: #LEFTHEX #RIGHTHEX"); - } - } - else - { - l_Failed = false; - EnsureColorCache(); - } - - if (!l_Failed) - { - BeatSaberPlus_NoteTweaker.Patches.PColorNoteVisuals.SetBlockColorOverride(true, m_LeftColorCache.Value, m_RightColorCache.Value); - PatchSabers(false); - - if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} colors are changed to {l_LeftHex} {l_RightHex}"); - } - } - } - - if (l_Failed) - p_Context.HasActionFailed = true; - - yield return null; - } - - private void EnsureColorCache() - { - if (!m_LeftColorCache.HasValue) - { - if (ColorUtility.TryParseHtmlString(Model.Left, out var l_LeftColor)) - m_LeftColorCache = l_LeftColor; - else - m_LeftColorCache = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.colorScheme?.saberAColor ?? Color.red; - } - if (!m_RightColorCache.HasValue) - { - if (ColorUtility.TryParseHtmlString(Model.Right, out var l_RightColor)) - m_RightColorCache = l_RightColor; - else - m_RightColorCache = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.colorScheme?.saberBColor ?? Color.blue; - } - } - private void PatchSabers(bool p_UseDefault) - { - /// todo - return; - - var l_Sabers = Resources.FindObjectsOfTypeAll(); - var l_ColorManager = null as ColorManager; - var l_ColorSchemeBackup = null as ColorScheme; - - for (int l_I = 0; l_I < l_Sabers.Length; ++l_I) - { - if (l_I == 0) - { - l_ColorManager = l_Sabers[l_I].GetField("_colorManager"); - - if (l_ColorManager != null) - { - l_ColorSchemeBackup = l_ColorManager.GetProperty("_colorScheme"); - if (l_ColorSchemeBackup != null && !p_UseDefault) - { - var l_ColorScheme = new ColorScheme("", "", false, "", false, - m_LeftColorCache.Value, m_RightColorCache.Value, l_ColorSchemeBackup.environmentColor0, - l_ColorSchemeBackup.environmentColor1, l_ColorSchemeBackup.supportsEnvironmentColorBoost, - l_ColorSchemeBackup.environmentColor0Boost, l_ColorSchemeBackup.environmentColor1Boost, - l_ColorSchemeBackup.obstaclesColor); - - l_ColorManager.SetProperty("_colorScheme", l_ColorScheme); - } - } - } - - if (l_ColorSchemeBackup == null) - break; - - var l_SaberTrail = l_Sabers[l_I].GetField("_saberTrail"); - var l_SetSaberGlowColors = l_Sabers[l_I].GetField("_setSaberGlowColors"); - var l_SetSaberFakeGlowColors = l_Sabers[l_I].GetField("_setSaberFakeGlowColors"); - var l_SaberLight = l_Sabers[l_I].GetField("_saberLight"); - - if (l_SaberTrail == null || l_SetSaberGlowColors == null || l_SetSaberFakeGlowColors == null || l_SaberLight == null) - continue; - - var l_SaberType = l_SaberLight.color == l_ColorSchemeBackup.saberAColor ? SaberType.SaberA : SaberType.SaberB; - var l_Color = l_SaberType == SaberType.SaberA ? m_LeftColorCache.Value : m_RightColorCache.Value; - - //l_SaberTrail.Setup((l_Color * this._initData.trailTintColor).linear, (IBladeMovementData)saber.movementData); - - foreach (var l_SetSaberGlowColor in l_SetSaberGlowColors) - l_SetSaberGlowColor.SetColors(); - foreach (var l_SaberFakeGlowColor in l_SetSaberFakeGlowColors) - l_SaberFakeGlowColor.SetColors(); - - l_SaberLight.color = l_Color; - - if (!p_UseDefault && l_I == (l_Sabers.Length - 1)) - l_ColorManager.SetProperty("_colorScheme", l_ColorSchemeBackup); - } - } - } - - public class GamePlay_Pause : Interfaces.IAction - { - public override string Description => "Trigger a pause during a song"; - -#pragma warning disable CS0414 - [UIComponent("HideUI")] - private ToggleSetting m_HideUI = null; -#pragma warning restore CS0414 - - public override void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_HideUI, l_Event, Model.HideUI, false); - - OnSettingChanged(null); - - if (!ModulePresence.GameTweaker) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: GameTweaker module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.HideUI = m_HideUI.Value; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - { - var l_BSP_MP_BigRank = GameObject.Find("BSP_MP_BigRank"); - if (l_BSP_MP_BigRank && l_BSP_MP_BigRank.activeSelf) - p_Context.HasActionFailed = true; - else - { - var l_PauseController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_PauseController && l_PauseMenuManager) - { - l_PauseController.Pause(); - if (Model.HideUI) - { - l_PauseController.didResumeEvent += PauseController_didResumeEvent; - l_PauseMenuManager.transform.Find("Wrapper/MenuWrapper/Canvas")?.gameObject?.SetActive(false); - } - } - } - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - - private void PauseController_didResumeEvent() - { - var l_PauseController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_PauseController) - l_PauseController.didResumeEvent -= PauseController_didResumeEvent; - - var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_PauseMenuManager) - l_PauseMenuManager.transform.Find("Wrapper/MenuWrapper/Canvas")?.gameObject?.SetActive(true); - } - } - - public class GamePlay_Quit : Interfaces.IAction - { - public override string Description => "Exit current song"; - - public GamePlay_Quit() => UIPlaceHolder = "Will exit the current map"; - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - { - var l_BSP_MP_BigRank = GameObject.Find("BSP_MP_BigRank"); - if (l_BSP_MP_BigRank && l_BSP_MP_BigRank.activeSelf) - p_Context.HasActionFailed = true; - else - { - var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_PauseMenuManager != null && l_PauseMenuManager) - l_PauseMenuManager.MenuButtonPressed(); - } - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class GamePlay_Restart : Interfaces.IAction - { - public override string Description => "Restart current song"; - - public GamePlay_Restart() => UIPlaceHolder = "Will restart the map"; - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - { - var l_BSP_MP_BigRank = GameObject.Find("BSP_MP_BigRank"); - if (l_BSP_MP_BigRank && l_BSP_MP_BigRank.activeSelf) - p_Context.HasActionFailed = true; - else - { - var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_PauseMenuManager != null && l_PauseMenuManager) - l_PauseMenuManager.RestartButtonPressed(); - } - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class GamePlay_Resume : Interfaces.IAction - { - public override string Description => "Resume current song"; - - public GamePlay_Resume() => UIPlaceHolder = "Will resume the map"; - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - { - var l_BSP_MP_BigRank = GameObject.Find("BSP_MP_BigRank"); - if (l_BSP_MP_BigRank && l_BSP_MP_BigRank.activeSelf) - p_Context.HasActionFailed = true; - else - { - var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_PauseMenuManager != null && l_PauseMenuManager) - l_PauseMenuManager.ContinueButtonPressed(); - } - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class GamePlay_SpawnBombPatterns : Interfaces.IAction - { - public override string Description => "Spawn bomb patterns in a map"; - -#pragma warning disable CS0414 - [UIComponent("IntervalIncrement")] - protected IncrementSetting m_IntervalIncrement = null; - [UIComponent("CountIncrement")] - protected IncrementSetting m_CountIncrement = null; - - [UIComponent("L0R2")] - protected ToggleSetting m_L0R2 = null; - [UIComponent("L1R2")] - protected ToggleSetting m_L1R2 = null; - [UIComponent("L2R2")] - protected ToggleSetting m_L2R2 = null; - [UIComponent("L3R2")] - protected ToggleSetting m_L3R2 = null; - - [UIComponent("L0R1")] - protected ToggleSetting m_L0R1 = null; - [UIComponent("L1R1")] - protected ToggleSetting m_L1R1 = null; - [UIComponent("L2R1")] - protected ToggleSetting m_L2R1 = null; - [UIComponent("L3R1")] - protected ToggleSetting m_L3R1 = null; - - [UIComponent("L0R0")] - protected ToggleSetting m_L0R0 = null; - [UIComponent("L1R0")] - protected ToggleSetting m_L1R0 = null; - [UIComponent("L2R0")] - protected ToggleSetting m_L2R0 = null; - [UIComponent("L3R0")] - protected ToggleSetting m_L3R0 = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_IntervalIncrement, l_Event, null, Model.Interval, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_CountIncrement, l_Event, null, Model.Count, false); - - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L0R2, l_Event, (Model.L0 & (1 << 2)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L1R2, l_Event, (Model.L1 & (1 << 2)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L2R2, l_Event, (Model.L2 & (1 << 2)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L3R2, l_Event, (Model.L3 & (1 << 2)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L0R1, l_Event, (Model.L0 & (1 << 1)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L1R1, l_Event, (Model.L1 & (1 << 1)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L2R1, l_Event, (Model.L2 & (1 << 1)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L3R1, l_Event, (Model.L3 & (1 << 1)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L0R0, l_Event, (Model.L0 & (1 << 0)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L1R0, l_Event, (Model.L1 & (1 << 0)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L2R0, l_Event, (Model.L2 & (1 << 0)) != 0, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_L3R0, l_Event, (Model.L3 & (1 << 0)) != 0, true); - - m_L0R2.transform.localScale = 0.8f * Vector3.one; - m_L1R2.transform.localScale = 0.8f * Vector3.one; - m_L2R2.transform.localScale = 0.8f * Vector3.one; - m_L3R2.transform.localScale = 0.8f * Vector3.one; - m_L0R1.transform.localScale = 0.8f * Vector3.one; - m_L1R1.transform.localScale = 0.8f * Vector3.one; - m_L2R1.transform.localScale = 0.8f * Vector3.one; - m_L3R1.transform.localScale = 0.8f * Vector3.one; - m_L0R0.transform.localScale = 0.8f * Vector3.one; - m_L1R0.transform.localScale = 0.8f * Vector3.one; - m_L2R0.transform.localScale = 0.8f * Vector3.one; - m_L3R0.transform.localScale = 0.8f * Vector3.one; - - } - private void OnSettingChanged(object p_Value) - { - Model.Interval = m_IntervalIncrement.Value; - Model.Count = (int)m_CountIncrement.Value; - Model.L0 = (byte)((m_L0R0.Value ? (1 << 0) : 0) | (m_L0R1.Value ? (1 << 1) : 0) | (m_L0R2.Value ? (1 << 2) : 0)); - Model.L1 = (byte)((m_L1R0.Value ? (1 << 0) : 0) | (m_L1R1.Value ? (1 << 1) : 0) | (m_L1R2.Value ? (1 << 2) : 0)); - Model.L2 = (byte)((m_L2R0.Value ? (1 << 0) : 0) | (m_L2R1.Value ? (1 << 1) : 0) | (m_L2R2.Value ? (1 << 2) : 0)); - Model.L3 = (byte)((m_L3R0.Value ? (1 << 0) : 0) | (m_L3R1.Value ? (1 << 1) : 0) | (m_L3R2.Value ? (1 << 2) : 0)); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing - && BeatSaberPlus.SDK.Game.Logic.LevelData != null - && !BeatSaberPlus.SDK.Game.Logic.LevelData.IsNoodle - && !BeatSaberPlus.SDK.Game.Scoring.IsInReplay) - { - var l_AudioTimeSyncController = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); - var l_BeatmapObjectSpawnController = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); - - if (l_AudioTimeSyncController != null && l_AudioTimeSyncController - && l_BeatmapObjectSpawnController != null && l_BeatmapObjectSpawnController) - { - List<(int, int)> l_SpawnList = new List<(int, int)>(); - for (int l_B = 0; l_B < 3; ++l_B) - { - var l_Mask = 1 << l_B; - - if ((Model.L0 & l_Mask) != 0) - l_SpawnList.Add((0, l_B)); - if ((Model.L1 & l_Mask) != 0) - l_SpawnList.Add((1, l_B)); - if ((Model.L2 & l_Mask) != 0) - l_SpawnList.Add((2, l_B)); - if ((Model.L3 & l_Mask) != 0) - l_SpawnList.Add((3, l_B)); - } - - float l_Time = 2f; - for (int l_I = 0; l_I < Model.Count; ++l_I) - { - for (int l_S = 0; l_S < l_SpawnList.Count; ++l_S) - { - var l_Current = l_SpawnList[l_S]; - l_BeatmapObjectSpawnController.HandleNoteDataCallback(NoteData.CreateBombNoteData( - l_AudioTimeSyncController.songTime + l_Time, - l_Current.Item1, - (NoteLineLayer)l_Current.Item2 - ) - ); - } - - l_Time += Model.Interval; - } - } - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class GamePlay_SpawnSquatWalls : Interfaces.IAction - { - public override string Description => "Spawn fake squat walls in a map"; - -#pragma warning disable CS0414 - [UIComponent("IntervalIncrement")] - protected IncrementSetting m_IntervalIncrement = null; - [UIComponent("CountIncrement")] - protected IncrementSetting m_CountIncrement = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_IntervalIncrement, l_Event, null, Model.Interval, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_CountIncrement, l_Event, null, Model.Count, false); - } - private void OnSettingChanged(object p_Value) - { - Model.Interval = m_IntervalIncrement.Value; - Model.Count = (int)m_CountIncrement.Value; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing - && BeatSaberPlus.SDK.Game.Logic.LevelData != null - && !BeatSaberPlus.SDK.Game.Logic.LevelData.IsNoodle - && !BeatSaberPlus.SDK.Game.Scoring.IsInReplay) - { - var l_AudioTimeSyncController = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); - var l_BeatmapObjectSpawnController = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); - - if ( l_AudioTimeSyncController != null && l_AudioTimeSyncController - && l_BeatmapObjectSpawnController != null && l_BeatmapObjectSpawnController) - { - float l_Time = 2f; - for (int l_I = 0; l_I < Model.Count; ++l_I) - { - l_BeatmapObjectSpawnController.HandleObstacleDataCallback(new ObstacleData( - l_AudioTimeSyncController.songTime + l_Time, 4, NoteLineLayer.Top, 0.3f, -4, 3 - )); - l_Time += Model.Interval; - } - } - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - } - - public class GamePlay_ToggleHUD : Interfaces.IAction - { - public override string Description => "Toggle HUD visibility"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Toggle", "On", "Off" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ToggleType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.ToggleType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - { - var l_CoreGameHUDController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_CoreGameHUDController != null && l_CoreGameHUDController) - { - switch(Model.ToggleType) - { - case 0: - l_CoreGameHUDController.gameObject.SetActive(!l_CoreGameHUDController.gameObject.activeSelf); - break; - case 1: - l_CoreGameHUDController.gameObject.SetActive(true); - break; - case 2: - l_CoreGameHUDController.gameObject.SetActive(false); - break; - } - } - - var l_NoteCutScoreSpawner = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_NoteCutScoreSpawner != null && l_NoteCutScoreSpawner) - { - switch (Model.ToggleType) - { - case 0: - if (l_NoteCutScoreSpawner.gameObject.activeSelf) - { - l_NoteCutScoreSpawner.OnDestroy(); - l_NoteCutScoreSpawner.gameObject.SetActive(false); - } - else - { - l_NoteCutScoreSpawner.gameObject.SetActive(true); - l_NoteCutScoreSpawner.Start(); - } - break; - case 1: - l_NoteCutScoreSpawner.gameObject.SetActive(true); - l_NoteCutScoreSpawner.Start(); - break; - case 2: - l_NoteCutScoreSpawner.OnDestroy(); - l_NoteCutScoreSpawner.gameObject.SetActive(false); - break; - } - } - - var l_BadNoteCutEffectSpawner = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_BadNoteCutEffectSpawner != null && l_BadNoteCutEffectSpawner) - { - switch (Model.ToggleType) - { - case 0: - if (l_BadNoteCutEffectSpawner.gameObject.activeSelf) - { - l_BadNoteCutEffectSpawner.OnDestroy(); - l_BadNoteCutEffectSpawner.gameObject.SetActive(false); - } - else - { - l_BadNoteCutEffectSpawner.gameObject.SetActive(true); - l_BadNoteCutEffectSpawner.Start(); - } - break; - case 1: - l_BadNoteCutEffectSpawner.gameObject.SetActive(true); - l_BadNoteCutEffectSpawner.Start(); - break; - case 2: - l_BadNoteCutEffectSpawner.OnDestroy(); - l_BadNoteCutEffectSpawner.gameObject.SetActive(false); - break; - } - } - - var l_MissedNoteEffectSpawner = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (l_MissedNoteEffectSpawner != null && l_BadNoteCutEffectSpawner) - { - switch (Model.ToggleType) - { - case 0: - if (l_MissedNoteEffectSpawner.gameObject.activeSelf) - { - l_MissedNoteEffectSpawner.OnDestroy(); - l_MissedNoteEffectSpawner.gameObject.SetActive(false); - } - else - { - l_MissedNoteEffectSpawner.gameObject.SetActive(true); - l_MissedNoteEffectSpawner.Start(); - } - break; - case 1: - l_MissedNoteEffectSpawner.gameObject.SetActive(true); - l_MissedNoteEffectSpawner.Start(); - break; - case 2: - l_MissedNoteEffectSpawner.OnDestroy(); - l_MissedNoteEffectSpawner.gameObject.SetActive(false); - break; - } - } - } - else - p_Context.HasActionFailed = true; - - yield return null; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Misc.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Misc.cs deleted file mode 100644 index 0b2e77e..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Misc.cs +++ /dev/null @@ -1,262 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberPlus_ChatIntegrations.Models; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using UnityEngine; -using UnityEngine.Networking; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class MiscBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - default: - break; - } - - return new List() - { - new Misc_Delay(), - new Misc_PlaySound(), - new Misc_WaitMenuScene(), - new Misc_WaitPlayingScene() - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class Misc_Delay : Interfaces.IAction - { - public override string Description => "Delay next actions"; - -#pragma warning disable CS0414 - [UIComponent("DelaySlider")] - private SliderSetting m_DelaySlider = null; - [UIComponent("DelayMsSlider")] - private SliderSetting m_DelayMsSlider = null; - [UIComponent("PreventFailureToggle")] - private ToggleSetting m_PreventFailureToggle = null; - - [UIObject("InfoPanel_Background")] - private GameObject m_InfoPanel_Background = null; -#pragma warning restore CS0414 - - public override void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoPanel_Background, 0.75f); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_DelaySlider, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Time, Model.Delay, true, true, new Vector2(0.08f, 0.10f), new Vector2(0.93f, 0.90f)); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_DelayMsSlider, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Milliseconds, Model.DelayMs, true, true, new Vector2(0.08f, 0.10f), new Vector2(0.93f, 0.90f)); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_PreventFailureToggle, l_Event, Model.PreventNextActionFailure, false); - } - private void OnSettingChanged(object p_Value) - { - Model.Delay = (uint)m_DelaySlider.slider.value; - Model.DelayMs = (uint)m_DelayMsSlider.slider.value; - Model.PreventNextActionFailure = m_PreventFailureToggle.Value; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (Model.PreventNextActionFailure) - p_Context.PreventNextActionFailure = true; - - yield return new WaitForSecondsRealtime((float)Model.Delay + (((float)Model.DelayMs) / 1000f)); - yield return null; - } - } - - public class Misc_PlaySound : Interfaces.IAction - { - public override string Description => "Play a sound clip"; - - private string m_PathCache = null; - private AudioClip m_AudioClip = null; - private AudioSource m_AudioSource = null; - -#pragma warning disable CS0414 - [UIComponent("File_DropDown")] - protected DropDownListSetting m_File_DropDown = null; - [UIValue("File_DropDownOptions")] - private List m_File_DropDownOptions = new List() { "Loading...", }; - [UIComponent("VolumeIncrement")] - protected IncrementSetting m_VolumeIncrement = null; - [UIComponent("PitchMinIncrement")] - protected IncrementSetting m_PitchMinIncrement = null; - [UIComponent("PitchMaxIncrement")] - protected IncrementSetting m_PitchMaxIncrement = null; - [UIComponent("KillToggle")] - private ToggleSetting m_KillToggle = null; - - [UIObject("InfoPanel_Background")] - private GameObject m_InfoPanel_Background = null; -#pragma warning restore CS0414 - - public override void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoPanel_Background, 0.75f); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_File_DropDown, l_Event, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_VolumeIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.Volume, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_PitchMinIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.PitchMin, false); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_PitchMaxIncrement, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, Model.PitchMax, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_KillToggle, l_Event, Model.KillOnSceneSwitch, false); - - var l_Files = Directory.GetFiles(ChatIntegrations.s_SOUND_CLIPS_ASSETS_PATH, "*.ogg").ToArray(); - - bool l_ChoiceExist = false; - var l_Choices = new List(); - l_Choices.Add("None"); - - foreach (var l_CurrentFile in l_Files) - { - var l_Filtered = Path.GetFileName(l_CurrentFile); - l_Choices.Add(l_Filtered); - - if (l_Filtered == Model.BaseValue) - l_ChoiceExist = true; - } - - m_File_DropDownOptions = l_Choices; - m_File_DropDown.values = l_Choices; - m_File_DropDown.Value = l_ChoiceExist ? Model.BaseValue : l_Choices[0]; - m_File_DropDown.UpdateChoices(); - } - private void OnSettingChanged(object p_Value) - { - if (Model.BaseValue != (string)m_File_DropDown.Value) - { - m_PathCache = null; - m_AudioClip = null; - } - - Model.BaseValue = (string)m_File_DropDown.Value; - Model.Volume = m_VolumeIncrement.Value; - Model.PitchMin = m_PitchMinIncrement.Value; - Model.PitchMax = m_PitchMaxIncrement.Value; - - if ((string)p_Value == "None") - Model.BaseValue = ""; - } - - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - CP_SDK.Unity.MTCoroutineStarter.Start(Eval(null)); - } - - public override IEnumerator Eval(EventContext p_Context) - { - if (Model.BaseValue != null) - { - if (m_PathCache == null) - m_PathCache = Path.Combine(Environment.CurrentDirectory, ChatIntegrations.s_SOUND_CLIPS_ASSETS_PATH, Model.BaseValue); - - yield return PlayAudioClip(m_PathCache); - } - else if (p_Context != null) - p_Context.HasActionFailed = true; - - yield return null; - } - - private IEnumerator PlayAudioClip(string p_File) - { - if (m_AudioClip == null && File.Exists(p_File)) - { - UnityWebRequest l_Song = UnityWebRequestMultimedia.GetAudioClip(p_File, AudioType.OGGVORBIS); - yield return l_Song.SendWebRequest(); - - AudioClip l_Clip = null; - try - { - ((DownloadHandlerAudioClip)l_Song.downloadHandler).streamAudio = true; - l_Clip = DownloadHandlerAudioClip.GetContent(l_Song); - - if (l_Clip == null) - { - Logger.Instance.Debug("[Modules.ChatIntegrations.Actions][Misc_PlaySound.PlayAudioClip] No audio found!"); - yield break; - } - - } - catch (Exception p_Exception) - { - Logger.Instance.Error("[Modules.ChatIntegrations.Actions][Misc_PlaySound.PlayAudioClip] Can't load audio! Exception: "); - Logger.Instance.Error(p_Exception); - - yield break; - } - - yield return new WaitUntil(() => l_Clip); - - m_AudioClip = l_Clip; - } - - if (m_AudioClip != null) - { - if (m_AudioSource == null || !m_AudioSource) - { - m_AudioSource = new GameObject("BSP_CI_Misc_PlaySound").AddComponent(); - m_AudioSource.loop = false; - m_AudioSource.spatialize = false; - m_AudioSource.playOnAwake = false; - m_AudioSource.ignoreListenerPause = true; - - if (!Model.KillOnSceneSwitch) - GameObject.DontDestroyOnLoad(m_AudioSource); - } - - m_AudioSource.clip = m_AudioClip; - m_AudioSource.volume = Model.Volume; - m_AudioSource.pitch = UnityEngine.Random.Range(Model.PitchMin, Model.PitchMax); - m_AudioSource.Play(); - } - } - } - - public class Misc_WaitMenuScene : Interfaces.IAction - { - public override string Description => "Wait for menu scene"; - - public Misc_WaitMenuScene() { UIPlaceHolder = "Wait for menu scene"; UIPlaceHolderTestButton = false; } - - public override IEnumerator Eval(EventContext p_Context) - { - yield return new WaitUntil(() => BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu); - } - } - - public class Misc_WaitPlayingScene : Interfaces.IAction - { - public override string Description => "Wait for playing scene"; - - public Misc_WaitPlayingScene() { UIPlaceHolder = "Wait for playing scene"; UIPlaceHolderTestButton = false; } - - public override IEnumerator Eval(EventContext p_Context) - { - yield return new WaitUntil(() => BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/NoteTweaker.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/NoteTweaker.cs deleted file mode 100644 index fe62b71..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/NoteTweaker.cs +++ /dev/null @@ -1,109 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.Parser; -using BeatSaberPlus_ChatIntegrations.Models; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using TMPro; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class NoteTweakerBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - default: - break; - } - - return new List() - { - new NoteTweaker_SwitchProfile() - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class NoteTweaker_SwitchProfile : Interfaces.IAction - { - public override string Description => "Change active NoteTweaker profile"; - -#pragma warning disable CS0414 - [UIComponent("Profile_DropDown")] protected DropDownListSetting m_Profile_DropDown = null; - [UIValue("Profile_DropDownOptions")] private List m_Profile_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Profile_DropDown, l_Event, true); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (ModulePresence.NoteTweaker) - { - l_Choices = BeatSaberPlus_NoteTweaker.NoteTweaker.Instance.GetAvailableProfiles().ToList(); - - if (l_Choices.Count == 0) - l_Choices.Add("None"); - } - else if (!ModulePresence.NoteTweaker) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.BaseValue) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Profile_DropDownOptions = l_Choices; - m_Profile_DropDown.values = l_Choices; - m_Profile_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Profile_DropDown.UpdateChoices(); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.BaseValue = m_Profile_DropDown.Value as string; - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.NoteTweaker) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); - yield break; - } - - if (BeatSaberPlus_NoteTweaker.NoteTweaker.Instance.GetAvailableProfiles().Contains(Model.BaseValue)) - BeatSaberPlus_NoteTweaker.NoteTweaker.Instance.SwitchToProfile(BeatSaberPlus_NoteTweaker.NoteTweaker.Instance.GetAvailableProfiles().IndexOf(Model.BaseValue)); - else - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:NoteTweaker_SwitchProfile Profile:{Model.BaseValue} not found!"); - } - - yield return null; - } - } - -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/OBS.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/OBS.cs deleted file mode 100644 index 224eee2..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/OBS.cs +++ /dev/null @@ -1,1158 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.Parser; -using BeatSaberPlus_ChatIntegrations.Models; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using TMPro; -using UnityEngine; - -using OBSService = CP_SDK.OBS.Service; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class OBSBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - default: - break; - } - - return new List() - { - new OBS_RenameLastRecord(), - new OBS_StartRecording(), - new OBS_StartStreaming(), - new OBS_SetRecordFilenameFormat(), - new OBS_StopRecording(), - new OBS_StopStreaming(), - new OBS_SwitchPreviewToScene(), - new OBS_SwitchToScene(), - new OBS_ToggleStudioMode(), - new OBS_ToggleSource(), - new OBS_ToggleSourceAudio(), - new OBS_Transition() - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class OBS_RenameLastRecord : Interfaces.IAction - { - public override string Description => "Rename last record file"; - - private BSMLParserParams m_ParserParams; - -#pragma warning disable CS0414 - [UIComponent("CurrentMessageText")] - private HMUI.TextPageScrollView m_CurrentMessageText = null; - - [UIComponent("ChatInputModal")] - protected HMUI.ModalView m_ChatInputModal = null; - [UIComponent("ChatInputModal_Text")] - protected TextMeshProUGUI m_ChatInputModal_Text = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - m_ParserParams = BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_ChatInputModal, 0.75f); - - /// Update UI - UpdateUI(); - } - private void UpdateUI() - { - m_CurrentMessageText.SetText(Model.Format); - } - - [UIAction("click-set-game-btn-pressed")] - private void OnSetFromGameButton() - { - var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.IValueType.String || x.Item1 == Interfaces.IValueType.Integer || x.Item1 == Interfaces.IValueType.Floating).ToArray(); - var l_Keys = new List<(string, System.Action)>(); - - l_Keys.Add(("$OriginalName", () => UI.Settings.Instance.UIInputKeyboardAppend("$OriginalName"))); - foreach (var l_Var in l_Variables) - l_Keys.Add(("$" + l_Var.Item2, () => UI.Settings.Instance.UIInputKeyboardAppend("$" + l_Var.Item2))); - - UI.Settings.Instance.UIShowInputKeyboard(Model.Format, (p_Result) => - { - Model.Format = p_Result; - - /// Update UI - UpdateUI(); - - }, l_Keys); - } - [UIAction("click-set-chat-btn-pressed")] - private void OnSetFromChatButton() - { - ChatIntegrations.Instance.OnBroadcasterChatMessage += Instance_OnBroadcasterChatMessage; - - var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.IValueType.String || x.Item1 == Interfaces.IValueType.Integer || x.Item1 == Interfaces.IValueType.Floating).ToList(); - var l_Message = "Please input a message in chat with your streaming account.\nProvided values:\n"; - - l_Variables.Insert(0, (Interfaces.IValueType.String, "$OriginalName")); - l_Message += string.Join(", ", l_Variables.Select(x => "$" + x.Item2).ToArray()); - - m_ChatInputModal_Text.text = l_Message; - - m_ParserParams.EmitEvent("ShowChatInputModal"); - } - private void Instance_OnBroadcasterChatMessage(CP_SDK.Chat.Interfaces.IChatMessage p_Message) - { - Model.Format = p_Message.Message; - ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; - - m_ParserParams.EmitEvent("CloseChatInputModal"); - - UpdateUI(); - } - [UIAction("click-cancel-set-chat-btn-pressed")] - private void OnCancelSetFromChatButton() - { - m_ParserParams.EmitEvent("CloseChatInputModal"); - ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - var l_ExistingFile = OBSService.LastRecordedFileName; - if (string.IsNullOrEmpty(l_ExistingFile) || !File.Exists(l_ExistingFile)) - { - p_Context.HasActionFailed = true; - yield break; - } - - var l_Path = Path.GetDirectoryName(l_ExistingFile); - var l_Result = Model.Format; - var l_Variables = p_Context.GetValues(Interfaces.IValueType.String, Interfaces.IValueType.Integer, Interfaces.IValueType.Floating); - l_Variables.Add((Interfaces.IValueType.String, "OriginalName")); - - for (int l_I = 0; l_I < l_Variables.Count; ++l_I) - { - var l_Var = l_Variables[l_I]; - var l_Key = "$" + l_Var.Item2; - var l_ReplaceValue = l_Var.Item1 == Interfaces.IValueType.String ? "" : "0"; - - if (l_Var.Item1 == Interfaces.IValueType.String && l_Var.Item2 == "OriginalName") - l_ReplaceValue = !string.IsNullOrEmpty(l_ExistingFile) ? Path.GetFileNameWithoutExtension(l_ExistingFile) : ""; - else if (l_Var.Item1 == Interfaces.IValueType.Integer && p_Context.GetIntegerValue(l_Var.Item2, out var l_IntegerVal)) - l_ReplaceValue = l_IntegerVal.Value.ToString(); - else if (l_Var.Item1 == Interfaces.IValueType.Floating && p_Context.GetFloatingValue(l_Var.Item2, out var l_FloatVal)) - l_ReplaceValue = l_FloatVal.Value.ToString(); - else if (l_Var.Item1 == Interfaces.IValueType.String && p_Context.GetStringValue(l_Var.Item2, out var l_StringVal)) - l_ReplaceValue = string.Join("_", l_StringVal.Split(Path.GetInvalidFileNameChars())); - - l_Result = l_Result.Replace(l_Key, l_ReplaceValue); - } - - var l_NewFile = Path.Combine(l_Path, l_Result + Path.GetExtension(l_ExistingFile)); - - Task.Run(async () => - { - /// Wait for OBS to finish IO - await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false); - - try - { - if (File.Exists(l_NewFile)) - { - l_NewFile = Path.Combine(l_Path, l_Result + CP_SDK.Misc.Time.UnixTimeNow() + Path.GetExtension(l_ExistingFile)); - File.Move(l_ExistingFile, l_NewFile); - } - else - File.Move(l_ExistingFile, l_NewFile); - } - catch (Exception) - { - - } - }).ConfigureAwait(false); - - yield return null; - } - } - - public class OBS_StartRecording : Interfaces.IAction - { - public override string Description => "Start recording"; - - public OBS_StartRecording() { UIPlaceHolder = "Start recording"; UIPlaceHolderTestButton = true; } - - public override IEnumerator Eval(EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - OBSService.StartRecording(); - - yield return null; - } - protected override void OnUIPlaceholderTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - OBSService.StartRecording(); - } - } - - public class OBS_StartStreaming : Interfaces.IAction - { - public override string Description => "Start streaming"; - - public OBS_StartStreaming() { UIPlaceHolder = "Start streaming"; UIPlaceHolderTestButton = true; } - - public override IEnumerator Eval(EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - OBSService.StartStreaming(); - - yield return null; - } - protected override void OnUIPlaceholderTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - OBSService.StartStreaming(); - } - } - - public class OBS_SetRecordFilenameFormat : Interfaces.IAction - { - public override string Description => "Set record filename format"; - - private BSMLParserParams m_ParserParams; - -#pragma warning disable CS0414 - [UIComponent("CurrentMessageText")] - private HMUI.TextPageScrollView m_CurrentMessageText = null; - - [UIComponent("ChatInputModal")] - protected HMUI.ModalView m_ChatInputModal = null; - [UIComponent("ChatInputModal_Text")] - protected TextMeshProUGUI m_ChatInputModal_Text = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - m_ParserParams = BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_ChatInputModal, 0.75f); - - /// Update UI - UpdateUI(); - } - private void UpdateUI() - { - m_CurrentMessageText.SetText(Model.Format); - } - - [UIAction("click-set-game-btn-pressed")] - private void OnSetFromGameButton() - { - var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.IValueType.String || x.Item1 == Interfaces.IValueType.Integer || x.Item1 == Interfaces.IValueType.Floating).ToArray(); - var l_Keys = new List<(string, System.Action)>(); - - foreach (var l_Var in l_Variables) - l_Keys.Add(("$" + l_Var.Item2, () => UI.Settings.Instance.UIInputKeyboardAppend("$" + l_Var.Item2))); - - UI.Settings.Instance.UIShowInputKeyboard(Model.Format, (p_Result) => - { - Model.Format = p_Result; - - /// Update UI - UpdateUI(); - - }, l_Keys); - } - [UIAction("click-set-chat-btn-pressed")] - private void OnSetFromChatButton() - { - ChatIntegrations.Instance.OnBroadcasterChatMessage += Instance_OnBroadcasterChatMessage; - - var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.IValueType.String || x.Item1 == Interfaces.IValueType.Integer || x.Item1 == Interfaces.IValueType.Floating).ToArray(); - var l_Message = "Please input a message in chat with your streaming account.\nProvided values:\n"; - l_Message += string.Join(", ", l_Variables.Select(x => "$" + x.Item2).ToArray()); - - m_ChatInputModal_Text.text = l_Message; - - m_ParserParams.EmitEvent("ShowChatInputModal"); - } - private void Instance_OnBroadcasterChatMessage(CP_SDK.Chat.Interfaces.IChatMessage p_Message) - { - Model.Format = p_Message.Message; - ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; - - m_ParserParams.EmitEvent("CloseChatInputModal"); - - UpdateUI(); - } - [UIAction("click-cancel-set-chat-btn-pressed")] - private void OnCancelSetFromChatButton() - { - m_ParserParams.EmitEvent("CloseChatInputModal"); - ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - var l_Result = Model.Format; - var l_Variables = p_Context.GetValues(Interfaces.IValueType.String, Interfaces.IValueType.Integer, Interfaces.IValueType.Floating); - - for (int l_I = 0; l_I < l_Variables.Count; ++l_I) - { - var l_Var = l_Variables[l_I]; - var l_Key = "$" + l_Var.Item2; - var l_ReplaceValue = l_Var.Item1 == Interfaces.IValueType.String ? "" : "0"; - - if (l_Var.Item1 == Interfaces.IValueType.Integer && p_Context.GetIntegerValue(l_Var.Item2, out var l_IntegerVal)) - l_ReplaceValue = l_IntegerVal.Value.ToString(); - else if (l_Var.Item1 == Interfaces.IValueType.Floating && p_Context.GetFloatingValue(l_Var.Item2, out var l_FloatVal)) - l_ReplaceValue = l_FloatVal.Value.ToString(); - else if (l_Var.Item1 == Interfaces.IValueType.String && p_Context.GetStringValue(l_Var.Item2, out var l_StringVal)) - l_ReplaceValue = string.Join("_", l_StringVal.Split(System.IO.Path.GetInvalidFileNameChars())); - - l_Result = l_Result.Replace(l_Key, l_ReplaceValue); - } - - OBSService.SetRecordFilenameFormat(l_Result); - - yield return null; - } - } - - public class OBS_StopRecording : Interfaces.IAction - { - public override string Description => "Stop recording"; - - public OBS_StopRecording() { UIPlaceHolder = "Stop recording"; UIPlaceHolderTestButton = true; } - - public override IEnumerator Eval(EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - OBSService.StopRecording(); - - yield return null; - } - protected override void OnUIPlaceholderTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - OBSService.StopRecording(); - } - } - - public class OBS_StopStreaming : Interfaces.IAction - { - public override string Description => "Stop streaming"; - - public OBS_StopStreaming() { UIPlaceHolder = "Stop streaming"; UIPlaceHolderTestButton = true; } - - public override IEnumerator Eval(EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - OBSService.StopStreaming(); - - yield return null; - } - protected override void OnUIPlaceholderTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - OBSService.StopStreaming(); - } - } - - public class OBS_SwitchPreviewToScene : Interfaces.IAction - { - public override string Description => "Change active OBS scene"; - -#pragma warning disable CS0414 - [UIComponent("Scene_DropDown")] - protected DropDownListSetting m_Scene_DropDown = null; - [UIValue("Scene_DropDownOptions")] - private List m_Scene_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Scene_DropDown, l_Event, true); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (OBSService.Status == OBSService.EStatus.Connected) - l_Choices.AddRange(OBSService.Scenes.Keys.ToList()); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.SceneName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Scene_DropDownOptions = l_Choices; - m_Scene_DropDown.values = l_Choices; - m_Scene_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Scene_DropDown.UpdateChoices(); - } - private void OnSettingChanged(object p_Value) - { - Model.SceneName = m_Scene_DropDown.Value as string; - } - - [UIAction("click-activescene-btn-pressed")] - private void OnActiveSceneButton() - { - Model.SceneName = OBSService.ActiveScene?.name; - m_Scene_DropDown.Value = OBSService.ActiveScene?.name; - } - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) - l_Scene.SetAsPreview(); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_SwitchPreviewToScene Scene:{Model.SceneName} not found!"); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) - l_Scene.SetAsPreview(); - else - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_SwitchPreviewToScene Scene:{Model.SceneName} not found!"); - } - - yield return null; - } - } - - public class OBS_SwitchToScene : Interfaces.IAction - { - public override string Description => "Change active OBS scene"; - -#pragma warning disable CS0414 - [UIComponent("Scene_DropDown")] - protected DropDownListSetting m_Scene_DropDown = null; - [UIValue("Scene_DropDownOptions")] - private List m_Scene_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Scene_DropDown, l_Event, true); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (OBSService.Status == OBSService.EStatus.Connected) - l_Choices.AddRange(OBSService.Scenes.Keys.ToList()); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.SceneName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Scene_DropDownOptions = l_Choices; - m_Scene_DropDown.values = l_Choices; - m_Scene_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Scene_DropDown.UpdateChoices(); - } - private void OnSettingChanged(object p_Value) - { - Model.SceneName = m_Scene_DropDown.Value as string; - } - - [UIAction("click-activescene-btn-pressed")] - private void OnActiveSceneButton() - { - Model.SceneName = OBSService.ActiveScene?.name; - m_Scene_DropDown.Value = OBSService.ActiveScene?.name; - } - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) - l_Scene.SwitchTo(); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_SwitchToScene Scene:{Model.SceneName} not found!"); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) - l_Scene.SwitchTo(); - else - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_SwitchToScene Scene:{Model.SceneName} not found!"); - } - - yield return null; - } - } - - public class OBS_ToggleStudioMode : Interfaces.IAction - { - public override string Description => "Enable or disable studio mode"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Toggle", "On", "Off" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ToggleType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.ToggleType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - } - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - switch (Model.ToggleType) - { - case 0: - if (OBSService.IsInStudioMode) - OBSService.DisableStudioMode(); - else - OBSService.EnableStudioMode(); - break; - case 1: - OBSService.EnableStudioMode(); - break; - case 2: - OBSService.DisableStudioMode(); - break; - } - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - switch (Model.ToggleType) - { - case 0: - if (OBSService.IsInStudioMode) - OBSService.DisableStudioMode(); - else - OBSService.EnableStudioMode(); - break; - case 1: - OBSService.EnableStudioMode(); - break; - case 2: - OBSService.DisableStudioMode(); - break; - } - - yield return null; - } - } - - public class OBS_ToggleSource : Interfaces.IAction - { - public override string Description => "Toggle source visibility"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Toggle", "On", "Off" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; - - [UIComponent("Scene_DropDown")] - protected DropDownListSetting m_Scene_DropDown = null; - [UIValue("Scene_DropDownOptions")] - private List m_Scene_DropDownOptions = new List() { "Loading...", }; - - [UIComponent("Source_DropDown")] - protected DropDownListSetting m_Source_DropDown = null; - [UIValue("Source_DropDownOptions")] - private List m_Source_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ToggleType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - var l_EventSrc = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChangedSrc), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup( m_TypeList, l_Event, true); - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup( m_Scene_DropDown, l_Event, true); - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup( m_Source_DropDown, l_EventSrc, true); - - RebuildSceneList(); - RebuildSourceList(); - } - - private void OnSettingChanged(object p_Value) - { - var l_SceneChanged = Model.SceneName != m_Scene_DropDown.Value as string; - - Model.ToggleType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - Model.SceneName = m_Scene_DropDown.Value as string; - - if (l_SceneChanged) - RebuildSourceList(); - } - private void OnSettingChangedSrc(object p_Value) - { - Model.SourceName = m_Source_DropDown.Value as string; - } - - private void RebuildSceneList() - { - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (OBSService.Status == OBSService.EStatus.Connected) - l_Choices.AddRange(OBSService.Scenes.Keys.ToList()); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.SceneName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Scene_DropDownOptions = l_Choices; - m_Scene_DropDown.values = l_Choices; - m_Scene_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Scene_DropDown.UpdateChoices(); - } - private void RebuildSourceList() - { - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (OBSService.Status == OBSService.EStatus.Connected) - { - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) - { - if (l_Scene.sources.Count != 0) - { - for (int l_I = 0;l_I < l_Scene.sources.Count; ++l_I) - { - var l_Source = l_Scene.sources[l_I]; - l_Choices.Add(l_Source.name); - - for (int l_Y = 0; l_Y < l_Source.groupChildren.Count; ++l_Y) - l_Choices.Add(l_Source.groupChildren[l_Y].name); - } - } - } - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSource Scene:{Model.SceneName} not found!"); - } - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.SourceName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Source_DropDownOptions = l_Choices; - m_Source_DropDown.values = l_Choices; - m_Source_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Source_DropDown.UpdateChoices(); - } - - [UIAction("click-activescene-btn-pressed")] - private void OnActiveSceneButton() - { - Model.SceneName = OBSService.ActiveScene?.name; - m_Scene_DropDown.Value = OBSService.ActiveScene?.name; - RebuildSourceList(); - } - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - CP_SDK.OBS.Models.Source l_Source = null; - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene) && (l_Source = l_Scene.GetSourceByName(Model.SourceName)) != null) - l_Source.SetVisible(Model.ToggleType == 0 ? !l_Source.render : (Model.ToggleType == 1 ? true : false)); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSource Scene:{Model.SceneName} not found!"); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - CP_SDK.OBS.Models.Source l_Source = null; - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene) && (l_Source = l_Scene.GetSourceByName(Model.SourceName)) != null) - l_Source.SetVisible(Model.ToggleType == 0 ? !l_Source.render : (Model.ToggleType == 1 ? true : false)); - else - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSource Scene:{Model.SceneName} not found!"); - } - - yield return null; - } - } - - public class OBS_ToggleSourceAudio : Interfaces.IAction - { - public override string Description => "Toggle source audio"; - -#pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "On", "Off" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; - - [UIComponent("Scene_DropDown")] - protected DropDownListSetting m_Scene_DropDown = null; - [UIValue("Scene_DropDownOptions")] - private List m_Scene_DropDownOptions = new List() { "Loading...", }; - - [UIComponent("Source_DropDown")] - protected DropDownListSetting m_Source_DropDown = null; - [UIValue("Source_DropDownOptions")] - private List m_Source_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ToggleType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - var l_EventSrc = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChangedSrc), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup( m_TypeList, l_Event, true); - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup( m_Scene_DropDown, l_Event, true); - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup( m_Source_DropDown, l_EventSrc, true); - - RebuildSceneList(); - RebuildSourceList(); - } - - private void OnSettingChanged(object p_Value) - { - var l_SceneChanged = Model.SceneName != m_Scene_DropDown.Value as string; - - Model.ToggleType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - Model.SceneName = m_Scene_DropDown.Value as string; - - if (l_SceneChanged) - RebuildSourceList(); - } - private void OnSettingChangedSrc(object p_Value) - { - Model.SourceName = m_Source_DropDown.Value as string; - } - - private void RebuildSceneList() - { - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (OBSService.Status == OBSService.EStatus.Connected) - l_Choices.AddRange(OBSService.Scenes.Keys.ToList()); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.SceneName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Scene_DropDownOptions = l_Choices; - m_Scene_DropDown.values = l_Choices; - m_Scene_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Scene_DropDown.UpdateChoices(); - } - private void RebuildSourceList() - { - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (OBSService.Status == OBSService.EStatus.Connected) - { - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) - { - if (l_Scene.sources.Count != 0) - { - for (int l_I = 0;l_I < l_Scene.sources.Count; ++l_I) - { - var l_Source = l_Scene.sources[l_I]; - l_Choices.Add(l_Source.name); - - for (int l_Y = 0; l_Y < l_Source.groupChildren.Count; ++l_Y) - l_Choices.Add(l_Source.groupChildren[l_Y].name); - } - } - } - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSource Scene:{Model.SceneName} not found!"); - } - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.SourceName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Source_DropDownOptions = l_Choices; - m_Source_DropDown.values = l_Choices; - m_Source_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Source_DropDown.UpdateChoices(); - } - - [UIAction("click-activescene-btn-pressed")] - private void OnActiveSceneButton() - { - Model.SceneName = OBSService.ActiveScene?.name; - m_Scene_DropDown.Value = OBSService.ActiveScene?.name; - RebuildSourceList(); - } - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - CP_SDK.OBS.Models.Source l_Source = null; - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene) && (l_Source = l_Scene.GetSourceByName(Model.SourceName)) != null) - l_Source.SetMuted(Model.ToggleType == 0 ? false : true); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSourceAudio Scene:{Model.SceneName} not found!"); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - CP_SDK.OBS.Models.Source l_Source = null; - if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene) && (l_Source = l_Scene.GetSourceByName(Model.SourceName)) != null) - l_Source.SetMuted(Model.ToggleType == 0 ? false : true); - else - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSourceAudio Scene:{Model.SceneName} not found!"); - } - - yield return null; - } - } - - public class OBS_Transition : Interfaces.IAction - { - public override string Description => "Transition between preview to active"; - -#pragma warning disable CS0414 - - [UIComponent("OverrideDuration")] - private ToggleSetting m_OverrideDuration = null; - [UIComponent("Duration")] - private SliderSetting m_Duration = null; - [UIComponent("OverrideTransition")] - private ToggleSetting m_OverrideTransition = null; - [UIComponent("Transition_DropDown")] - protected DropDownListSetting m_Transition_DropDown = null; - [UIValue("Transition_DropDown")] - private List m_Transition_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup( m_OverrideDuration, l_Event, Model.OverrideDuration, true); - BeatSaberPlus.SDK.UI.SliderSetting.Setup( m_Duration, l_Event, null, Model.Duration, true, true, new Vector2(0.08f, 0.10f), new Vector2(0.93f, 0.90f)); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup( m_OverrideTransition, l_Event, Model.OverrideTransition, true); - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup( m_Transition_DropDown, l_Event, true); - - RebuildTransitionList(); - - m_Duration.interactable = Model.OverrideDuration; - m_Transition_DropDown.interactable = Model.OverrideTransition; - } - - private void OnSettingChanged(object p_Value) - { - Model.OverrideDuration = m_OverrideDuration.Value; - Model.Duration = (int)m_Duration.Value; - Model.OverrideTransition = m_OverrideTransition.Value; - Model.Transition = m_Transition_DropDown.Value as string; - - m_Duration.interactable = Model.OverrideDuration; - m_Transition_DropDown.interactable = Model.OverrideTransition; - } - - private void RebuildTransitionList() - { - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (OBSService.Status == OBSService.EStatus.Connected) - l_Choices.AddRange(OBSService.Transitions.ToList()); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.Transition) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Transition_DropDownOptions = l_Choices; - m_Transition_DropDown.values = l_Choices; - m_Transition_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Transition_DropDown.UpdateChoices(); - } - - [UIAction("click-test-btn-pressed")] - private void OnTestButton() - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - return; - } - - if (Model.OverrideDuration && Model.OverrideTransition) - OBSService.PreviewTransitionToScene(Model.Duration, Model.Transition); - else if (Model.OverrideDuration) - OBSService.PreviewTransitionToScene(Model.Duration); - else if (Model.OverrideTransition) - OBSService.PreviewTransitionToScene(-1, Model.Transition); - else - OBSService.PreviewTransitionToScene(); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); - yield break; - } - - - if (Model.OverrideDuration && Model.OverrideTransition) - OBSService.PreviewTransitionToScene(Model.Duration, Model.Transition); - else if (Model.OverrideDuration) - OBSService.PreviewTransitionToScene(Model.Duration); - else if (Model.OverrideTransition) - OBSService.PreviewTransitionToScene(-1, Model.Transition); - else - OBSService.PreviewTransitionToScene(); - - yield return null; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/SongChartVisualizer.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/SongChartVisualizer.cs deleted file mode 100644 index cf7d934..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/SongChartVisualizer.cs +++ /dev/null @@ -1,89 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class SongChartVisualizerBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - default: - break; - } - - return new List() - { - new SongChartVisualizer_ToggleVisibility() - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class SongChartVisualizer_ToggleVisibility : Interfaces.IAction - { - public override string Description => "Show or hide the SongChartVisualizer ingame"; - - #pragma warning disable CS0414 - [UIComponent("TypeList")] - private ListSetting m_TypeList = null; - [UIValue("TypeList_Choices")] - private List m_TypeListList_Choices = new List() { "Toggle", "On", "Off" }; - [UIValue("TypeList_Value")] - private string m_TypeList_Value; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_TypeList_Value = (string)m_TypeListList_Choices.ElementAt(Model.ToggleType % m_TypeListList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_TypeList, l_Event, false); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.ToggleType = m_TypeListList_Choices.Select(x => (string)x).ToList().IndexOf(m_TypeList.Value); - } - - public override IEnumerator Eval(Models.EventContext p_Context) - { - if (!ModulePresence.SongChartVisualizer) - { - p_Context.HasActionFailed = true; - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("SongChartVisualizer: Action failed, SongChartVisualizer module is missing!"); - yield break; - } - - switch (Model.ToggleType) - { - case 0: - BeatSaberPlus_SongChartVisualizer.SongChartVisualizer.Instance?.ToggleVisibility(); - break; - case 1: - BeatSaberPlus_SongChartVisualizer.SongChartVisualizer.Instance?.SetVisible(true); - break; - case 2: - BeatSaberPlus_SongChartVisualizer.SongChartVisualizer.Instance?.SetVisible(false); - break; - } - - yield return null; - } - } - -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Twitch.cs b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Twitch.cs deleted file mode 100644 index e04e7d4..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Twitch.cs +++ /dev/null @@ -1,220 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Parser; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using CP_SDK.Chat.Interfaces; -using Newtonsoft.Json.Linq; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Text; -using System.Threading; -using TMPro; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Actions -{ - internal class TwitchBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - switch (p_Event) - { - default: - break; - } - - return new List() - { - new Twitch_AddMarker(), - new Twitch_CreateClip() - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class Twitch_AddMarker : Interfaces.IAction - { - public override string Description => "Add a marker on the video"; - - private BSMLParserParams m_ParserParams; - -#pragma warning disable CS0414 - [UIComponent("CurrentMarkerText")] - private HMUI.TextPageScrollView m_CurrentMarkerText = null; - - [UIComponent("ChatInputModal")] - protected HMUI.ModalView m_ChatInputModal = null; - [UIComponent("ChatInputModal_Text")] - protected TextMeshProUGUI m_ChatInputModal_Text = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - m_ParserParams = BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_ChatInputModal, 0.75f); - - /// Update UI - UpdateUI(); - } - private void UpdateUI() - { - m_CurrentMarkerText.SetText(Model.BaseValue); - } - - [UIAction("click-set-game-btn-pressed")] - private void OnSetFromGameButton() - { - var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == IValueType.String || x.Item1 == IValueType.Integer || x.Item1 == IValueType.Floating).ToArray(); - var l_Keys = new System.Collections.Generic.List<(string, System.Action)>(); - - foreach (var l_Var in l_Variables) - l_Keys.Add(("$" + l_Var.Item2, () => UI.Settings.Instance.UIInputKeyboardAppend("$" + l_Var.Item2))); - - UI.Settings.Instance.UIShowInputKeyboard(Model.BaseValue, (p_Result) => - { - Model.BaseValue = p_Result; - - /// Update UI - UpdateUI(); - - }, l_Keys); - } - [UIAction("click-set-chat-btn-pressed")] - private void OnSetFromChatButton() - { - ChatIntegrations.Instance.OnBroadcasterChatMessage += Instance_OnBroadcasterChatMessage; - - var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == IValueType.String || x.Item1 == IValueType.Integer || x.Item1 == IValueType.Floating).ToArray(); - var l_Message = "Please input a message in chat with your streaming account.\nProvided values:\n"; - l_Message += string.Join(", ", l_Variables.Select(x => "$" + x.Item2).ToArray()); - - m_ChatInputModal_Text.text = l_Message; - - m_ParserParams.EmitEvent("ShowChatInputModal"); - } - private void Instance_OnBroadcasterChatMessage(IChatMessage p_Message) - { - Model.BaseValue = p_Message.Message; - ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; - - m_ParserParams.EmitEvent("CloseChatInputModal"); - - UpdateUI(); - } - [UIAction("click-cancel-set-chat-btn-pressed")] - private void OnCancelSetFromChatButton() - { - m_ParserParams.EmitEvent("CloseChatInputModal"); - ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public override IEnumerator Eval(Models.EventContext p_Context) - { - var l_Message = Model.BaseValue; - var l_Variables = p_Context.GetValues(IValueType.String, IValueType.Integer, IValueType.Floating); - - foreach (var l_Var in l_Variables) - { - var l_Key = "$" + l_Var.Item2; - var l_ReplaceValue = l_Var.Item1 == IValueType.String ? "" : "0"; - - if (l_Var.Item1 == IValueType.Integer && p_Context.GetIntegerValue(l_Var.Item2, out var l_IntegerVal)) - l_ReplaceValue = l_IntegerVal.Value.ToString(); - else if (l_Var.Item1 == IValueType.Floating && p_Context.GetFloatingValue(l_Var.Item2, out var l_FloatVal)) - l_ReplaceValue = l_FloatVal.Value.ToString(); - else if (l_Var.Item1 == IValueType.String && p_Context.GetStringValue(l_Var.Item2, out var l_StringVal)) - l_ReplaceValue = l_StringVal; - - l_Message = l_Message.Replace(l_Key, l_ReplaceValue); - } - - var l_Channel = CP_SDK.Chat.Service.Multiplexer.Channels.First(); - if (l_Channel.Item2 is CP_SDK.Chat.Models.Twitch.TwitchChannel l_TwitchChannel) - { - var l_URL = $"https://api.twitch.tv/helix/streams/markers"; - var l_APIClient = new CP_SDK.Network.APIClient("", TimeSpan.FromSeconds(10), false); - var l_OAuthToken = (l_Channel.Item1 as CP_SDK.Chat.Services.Twitch.TwitchService).OAuthToken.Replace("oauth:", ""); - - l_APIClient.InternalClient.DefaultRequestHeaders.Add("client-id", ChatIntegrations.s_BEATSABERPLUS_CLIENT_ID); - l_APIClient.InternalClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + l_OAuthToken); - - var l_Content = new JObject() - { - ["user_id"] = l_TwitchChannel.Roomstate.RoomId, - ["description"] = l_Message - }; - var l_ContentStr = new StringContent(l_Content.ToString(), Encoding.UTF8, "application/json"); - - l_APIClient.PostAsync(l_URL, l_ContentStr, CancellationToken.None, true).ContinueWith((x) => - { - if (x.Result != null && !x.Result.IsSuccessStatusCode) - Logger.Instance.Error("[ChatIntegrations.Actions][Twitch.Twitch_AddMarker] Error:" + x.Result.BodyString); - }).ConfigureAwait(false); - } - - yield return null; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class Twitch_CreateClip : Interfaces.IAction - { - public override string Description => "Create clip, and put the edit URL in beatsaberplus_clips.txt"; - - public Twitch_CreateClip() => UIPlaceHolder = "Create clip, and put the edit URL in beatsaberplus_clips.txt"; - - public override IEnumerator Eval(Models.EventContext p_Context) - { - var l_Channel = CP_SDK.Chat.Service.Multiplexer.Channels.First(); - if (l_Channel.Item2 is CP_SDK.Chat.Models.Twitch.TwitchChannel l_TwitchChannel) - { - var l_URL = $"https://api.twitch.tv/helix/clips?broadcaster_id=" + l_TwitchChannel.Roomstate.RoomId; - var l_APIClient = new CP_SDK.Network.APIClient("", TimeSpan.FromSeconds(10), false); - var l_OAuthToken = (l_Channel.Item1 as CP_SDK.Chat.Services.Twitch.TwitchService).OAuthToken.Replace("oauth:", ""); - - l_APIClient.InternalClient.DefaultRequestHeaders.Add("client-id", ChatIntegrations.s_BEATSABERPLUS_CLIENT_ID); - l_APIClient.InternalClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + l_OAuthToken); - - var l_ContentStr = new StringContent("{}", Encoding.UTF8, "application/json"); - - l_APIClient.PostAsync(l_URL, l_ContentStr, CancellationToken.None, true).ContinueWith((x) => - { - if (x.Result == null) - return; - - if (!x.Result.IsSuccessStatusCode) - Logger.Instance.Error("[ChatIntegrations.Actions][Twitch.Twitch_CreateClip] Error:" + x.Result.BodyString); - else - { - try - { - var l_JSON = JObject.Parse(x.Result.BodyString); - if (l_JSON != null && l_JSON.ContainsKey("data") && (l_JSON["data"][0] as JObject).ContainsKey("edit_url")) - System.IO.File.AppendAllLines("beatsaberplus_clips.txt", new List() { l_JSON["data"][0]["edit_url"].Value() ?? "invalid" }); - } - catch - { - - } - } - }).ConfigureAwait(false); - } - - yield return null; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Camera2_SwitchToScene.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Camera2_SwitchToScene.bsml deleted file mode 100644 index 132551d..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Camera2_SwitchToScene.bsml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Camera2_ToggleCamera.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Camera2_ToggleCamera.bsml deleted file mode 100644 index 8cef6a0..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Camera2_ToggleCamera.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Chat_SendMessage.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Chat_SendMessage.bsml deleted file mode 100644 index b955ef4..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Chat_SendMessage.bsml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Chat_ToggleVisibility.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Chat_ToggleVisibility.bsml deleted file mode 100644 index 212dc01..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Chat_ToggleVisibility.bsml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/EmoteRain_CustomRain.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/EmoteRain_CustomRain.bsml deleted file mode 100644 index 20d14eb..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/EmoteRain_CustomRain.bsml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/EmoteRain_EmoteBombRain.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/EmoteRain_EmoteBombRain.bsml deleted file mode 100644 index 615449f..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/EmoteRain_EmoteBombRain.bsml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Event_ExecuteDummy.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Event_ExecuteDummy.bsml deleted file mode 100644 index 513e4e3..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Event_ExecuteDummy.bsml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Event_Toggle.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Event_Toggle.bsml deleted file mode 100644 index f819559..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Event_Toggle.bsml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeBombColor.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeBombColor.bsml deleted file mode 100644 index c30b1ed..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeBombColor.bsml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeBombScale.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeBombScale.bsml deleted file mode 100644 index e201900..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeBombScale.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeDebris.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeDebris.bsml deleted file mode 100644 index 3c23a70..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeDebris.bsml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeLightIntensity.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeLightIntensity.bsml deleted file mode 100644 index 607f150..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeLightIntensity.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeMusicVolume.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeMusicVolume.bsml deleted file mode 100644 index 4cb1d1a..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeMusicVolume.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeNoteColors.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeNoteColors.bsml deleted file mode 100644 index a8a214b..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeNoteColors.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeNoteScale.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeNoteScale.bsml deleted file mode 100644 index e201900..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ChangeNoteScale.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_Pause.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_Pause.bsml deleted file mode 100644 index 1288144..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_Pause.bsml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_SpawnBombPatterns.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_SpawnBombPatterns.bsml deleted file mode 100644 index 890cba7..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_SpawnBombPatterns.bsml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_SpawnSquatWalls.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_SpawnSquatWalls.bsml deleted file mode 100644 index bd66fcd..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_SpawnSquatWalls.bsml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ToggleHUD.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ToggleHUD.bsml deleted file mode 100644 index 212dc01..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ToggleHUD.bsml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ToggleLights.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ToggleLights.bsml deleted file mode 100644 index 708dff5..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/GamePlay_ToggleLights.bsml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Misc_Delay.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Misc_Delay.bsml deleted file mode 100644 index 381f246..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Misc_Delay.bsml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Misc_PlaySound.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Misc_PlaySound.bsml deleted file mode 100644 index 799956e..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Misc_PlaySound.bsml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/NoteTweaker_SwitchProfile.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/NoteTweaker_SwitchProfile.bsml deleted file mode 100644 index 4f64469..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/NoteTweaker_SwitchProfile.bsml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_RenameLastRecord.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_RenameLastRecord.bsml deleted file mode 100644 index 6d6b725..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_RenameLastRecord.bsml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SetRecordFilenameFormat.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SetRecordFilenameFormat.bsml deleted file mode 100644 index 6d6b725..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SetRecordFilenameFormat.bsml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SwitchPreviewToScene.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SwitchPreviewToScene.bsml deleted file mode 100644 index 18120a1..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SwitchPreviewToScene.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SwitchToScene.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SwitchToScene.bsml deleted file mode 100644 index ef9931b..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_SwitchToScene.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleSource.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleSource.bsml deleted file mode 100644 index da211ef..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleSource.bsml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleSourceAudio.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleSourceAudio.bsml deleted file mode 100644 index da211ef..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleSourceAudio.bsml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleStudioMode.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleStudioMode.bsml deleted file mode 100644 index 0097ddf..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_ToggleStudioMode.bsml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_Transition.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_Transition.bsml deleted file mode 100644 index 7a7a56d..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/OBS_Transition.bsml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/SongChartVisualizer_ToggleVisibility.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/SongChartVisualizer_ToggleVisibility.bsml deleted file mode 100644 index 212dc01..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/SongChartVisualizer_ToggleVisibility.bsml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Twitch_AddMarker.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Twitch_AddMarker.bsml deleted file mode 100644 index 732438a..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Actions/Views/Twitch_AddMarker.bsml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Plugin.cs b/Modules/BeatSaberPlus_ChatIntegrations/BSIPA.cs similarity index 82% rename from Modules/BeatSaberPlus_ChatIntegrations/Plugin.cs rename to Modules/BeatSaberPlus_ChatIntegrations/BSIPA.cs index aae9141..7df0583 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Plugin.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/BSIPA.cs @@ -6,17 +6,18 @@ namespace BeatSaberPlus_ChatIntegrations /// Main plugin class /// [Plugin(RuntimeOptions.SingleStartInit)] - public class Plugin + public class BSIPA { /// /// Called when the plugin is first loaded by IPA (either when the game starts or when the plugin is enabled if it starts disabled). /// /// Logger instance [Init] - public Plugin(IPA.Logging.Logger p_Logger) + public BSIPA(IPA.Logging.Logger p_Logger) { /// Setup logger - Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); + ChatPlexMod_ChatIntegrations.Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); + BeatSaber.Manager.Init(); } //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/Camera2.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/Camera2.cs new file mode 100644 index 0000000..67927dd --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/Camera2.cs @@ -0,0 +1,283 @@ +using CP_SDK.XUI; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Actions +{ + public class Camera2_SwitchToDefaultScene + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + public override string Description => "Switch to default camera2 scene"; + public override string UIPlaceHolder => "Switch to default camera2 scene"; + public override bool UIPlaceHolderTestButton => true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + protected override void OnUIPlaceholderTestButton() + { + if (!ModPresence.Camera2) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); + return; + } + + Camera2.SDK.Scenes.ShowNormalScene(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModPresence.Camera2) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); + yield break; + } + + Camera2.SDK.Scenes.ShowNormalScene(); + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Camera2_SwitchToScene + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_Scene = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Change active Camera2 scene"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Choices = new List() { "None" }; + var l_SelectedChoice = "None"; + + if (ModPresence.Camera2) + { + l_Choices = Camera2.SDK.Scenes.customScenes.Select(x => x.Key).ToList(); + + if (l_Choices.Count == 0) + l_Choices.Add("None"); + } + else if (!ModPresence.Camera2) + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); + + for (int l_I = 0; l_I < l_Choices.Count; ++l_I) + { + if (l_Choices[l_I] != Model.SceneName) + continue; + + l_SelectedChoice = l_Choices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Scene", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_SelectedChoice) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Scene) + ), + + XUIPrimaryButton.Make("Test", OnTestButton) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.SceneName = m_Scene.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnTestButton() + { + if (!ModPresence.Camera2) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); + return; + } + + if (Camera2.SDK.Scenes.customScenes.ContainsKey(Model.SceneName)) + Camera2.SDK.Scenes.SwitchToCustomScene(Model.SceneName); + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:Camera2_SwitchToScene Scene:{Model.SceneName} not found!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModPresence.Camera2) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); + yield break; + } + + if (Camera2.SDK.Scenes.customScenes.ContainsKey(Model.SceneName)) + Camera2.SDK.Scenes.SwitchToCustomScene(Model.SceneName); + else + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:Camera2_SwitchToScene Scene:{Model.SceneName} not found!"); + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Camera2_ToggleCamera + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_Camera = null; + private XUIDropdown m_ChangeType = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Toggle Camera2 camera visibility"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Choices = new List() { "None" }; + var l_SelectedChoice = "None"; + if (ModPresence.Camera2) + l_Choices = Camera2.SDK.Cameras.available.ToList(); + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); + + for (int l_I = 0; l_I < l_Choices.Count; ++l_I) + { + if (l_Choices[l_I] != Model.CameraName) + continue; + + l_SelectedChoice = l_Choices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Camera", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_SelectedChoice) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Camera) + ), + + Templates.SettingsHGroup("Change type", + XUIDropdown.Make() + .SetOptions(ChatPlexMod_ChatIntegrations.Enums.Toggle.S).SetValue(ChatPlexMod_ChatIntegrations.Enums.Toggle.ToStr(Model.ChangeType)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ChangeType) + ), + + XUIPrimaryButton.Make("Test", OnTestButton) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.CameraName = m_Camera.Element.GetValue(); + Model.ChangeType = ChatPlexMod_ChatIntegrations.Enums.Toggle.ToEnum(m_ChangeType.Element.GetValue()); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnTestButton() + { + if (!ModPresence.Camera2) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); + return; + } + + if (Camera2.SDK.Cameras.available.Contains(Model.CameraName)) + { + switch (Model.ChangeType) + { + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle: + Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, !Camera2.SDK.Cameras.active.Contains(Model.CameraName)); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Enable: + Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, true); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Disable: + Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, false); + break; + } + } + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:Camera2_ToggleCamera Camera:{Model.CameraName} not found!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModPresence.Camera2) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, Camera2 mod is missing!"); + yield break; + } + + if (Camera2.SDK.Cameras.available.Contains(Model.CameraName)) + { + switch (Model.ChangeType) + { + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle: + Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, !Camera2.SDK.Cameras.active.Contains(Model.CameraName)); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Enable: + Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, true); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Disable: + Camera2.SDK.Cameras.SetCameraActive(Model.CameraName, false); + break; + } + } + else + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:Camera2_ToggleCamera Camera:{Model.CameraName} not found!"); + } + + yield return null; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/GamePlay.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/GamePlay.cs new file mode 100644 index 0000000..f2d33a6 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/GamePlay.cs @@ -0,0 +1,1607 @@ +using CP_SDK.Unity.Extensions; +using CP_SDK.XUI; +using IPA.Utilities; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Actions +{ + public class GamePlay_ChangeBombColor + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_ValueSource = null; + private XUIColorInput m_Color = null; + private XUIToggle m_SendMessage = null; + + private Color? m_ColorCache; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Change bomb color"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Choices = new List(Enums.ValueSource.S); + if ( Event.GetType() != typeof(ChatPlexMod_ChatIntegrations.Events.ChatCommand) + && Event.GetType() != typeof(ChatPlexMod_ChatIntegrations.Events.ChatPointsReward)) + l_Choices.Remove(Enums.ValueSource.ToStr(Enums.ValueSource.E.Event)); + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Value source", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(Enums.ValueSource.ToStr(Model.ValueSource)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ValueSource) + ), + + Templates.SettingsHGroup("User color", + XUIColorInput.Make() + .SetValue(ColorU.ToUnityColor(Model.Color)).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Color) + ), + + Templates.SettingsHGroup("Send chat message?", + XUIToggle.Make() + .SetValue(Model.SendChatMessage).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_SendMessage) + ) + }; + + BuildUIAuto(p_Parent); + + OnSettingChanged(); + + if (!ModulePresence.NoteTweaker) + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: NoteTweaker module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + m_ColorCache = m_Color.Element.GetValue(); + + Model.ValueSource = Enums.ValueSource.ToEnum(m_ValueSource.Element.GetValue()); + Model.Color = ColorU.ToHexRGB(m_ColorCache.Value); + Model.SendChatMessage = m_SendMessage.Element.GetValue(); + + m_Color.SetInteractable(Model.ValueSource == Enums.ValueSource.E.User); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.NoteTweaker) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); + yield break; + } + + bool l_Failed = true; + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing + || Model.ValueSource == Enums.ValueSource.E.Config) + { + if (Model.ValueSource == Enums.ValueSource.E.Config) + { + BeatSaberPlus_NoteTweaker.Patches.PBombController.SetBombColorOverride(false, Color.black); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} bomb color is back to default!"); + + l_Failed = false; + } + else + { + var l_Hex = Model.Color; + + if (Model.ValueSource == Enums.ValueSource.E.Random) + { + m_ColorCache = UnityEngine.Random.ColorHSV(); + l_Hex = ColorU.ToHexRGB(m_ColorCache.Value); + } + else if (Model.ValueSource == Enums.ValueSource.E.Event && (p_Context.Message != null || p_Context.PointsEvent != null)) /// Event user input + { + var l_Src = (p_Context.Message?.Message ?? p_Context.PointsEvent?.UserInput) ?? ""; + var l_Parts = l_Src.Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); + if (l_Parts.Length >= 1 + && ColorU.TryToUnityColor(l_Parts[l_Parts.Length - 1], out var l_LeftColor)) + { + m_ColorCache = l_LeftColor; + l_Hex = l_Parts[l_Parts.Length - 2]; + l_Failed = false; + } + else if (p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + { + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} the syntax is: #HEXCOLOR"); + } + } + else if (Model.ValueSource == Enums.ValueSource.E.User) + { + l_Failed = false; + EnsureColorCache(); + } + + if (!l_Failed) + { + BeatSaberPlus_NoteTweaker.Patches.PBombController.SetBombColorOverride(true, m_ColorCache.Value); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} bomb color is changed to {l_Hex}"); + } + } + } + + if (l_Failed) + p_Context.HasActionFailed = true; + + yield return null; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void EnsureColorCache() + { + if (m_ColorCache.HasValue) + return; + + m_ColorCache = ColorU.ToUnityColor(Model.Color); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeBombScale + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_ValueSource = null; + private XUISlider m_UserValue = null; + private XUISlider m_Min = null; + private XUISlider m_Max = null; + private XUIToggle m_SendMessage = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Choose a random bomb scale"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Value source", + XUIDropdown.Make() + .SetOptions(Enums.ValueSource.S).SetValue(Enums.ValueSource.ToStr(Model.ValueSource)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ValueSource) + ), + + Templates.SettingsHGroup("User value", + XUISlider.Make() + .SetMinValue(0.4f).SetMaxValue(1.5f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.UserValue).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_UserValue) + ), + + Templates.SettingsHGroup("Random/Event value min/max", + XUISlider.Make() + .SetMinValue(0.4f).SetMaxValue(1.5f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.Min).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Min), + + XUISlider.Make() + .SetMinValue(0.4f).SetMaxValue(1.5f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.Max).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Max) + ), + + Templates.SettingsHGroup("Send chat message?", + XUIToggle.Make() + .SetValue(Model.SendChatMessage).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_SendMessage) + ) + }; + + BuildUIAuto(p_Parent); + + OnSettingChanged(); + + if (!ModulePresence.GameTweaker) + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: GameTweaker module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.ValueSource = Enums.ValueSource.ToEnum(m_ValueSource.Element.GetValue()); + Model.UserValue = m_UserValue.Element.GetValue(); + Model.Min = m_Min.Element.GetValue(); + Model.Max = m_Max.Element.GetValue(); + Model.SendChatMessage = m_SendMessage.Element.GetValue(); + + m_UserValue.SetInteractable(Model.ValueSource == Enums.ValueSource.E.User); + m_Min.SetInteractable(Model.ValueSource == Enums.ValueSource.E.Random || Model.ValueSource == Enums.ValueSource.E.Event); + m_Max.SetInteractable(Model.ValueSource == Enums.ValueSource.E.Random || Model.ValueSource == Enums.ValueSource.E.Event); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.NoteTweaker) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); + yield break; + } + + bool l_Failed = true; + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing + || Model.ValueSource == Enums.ValueSource.E.Config) + { + if (Model.ValueSource == Enums.ValueSource.E.Config) + { + BeatSaberPlus_NoteTweaker.Patches.PBombController.SetTemp(false, 0f); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} note scale was set to default"); + } + else + { + var l_NewValue = 0f; + + /// Random + if (Model.ValueSource == Enums.ValueSource.E.Random) + l_NewValue = UnityEngine.Random.Range(Model.Min, Model.Max); + /// User input + else if (Model.ValueSource == Enums.ValueSource.E.User) + l_NewValue = Model.UserValue; + /// Event input + else if (Model.ValueSource == Enums.ValueSource.E.Event) + { + var l_FirstInteger = p_Context.GetFirstValueOfType(ChatPlexMod_ChatIntegrations.Interfaces.EValueType.Integer); + var l_EventInput = 1f; + + if (l_FirstInteger != default && p_Context.GetIntegerValue(l_FirstInteger.Item2, out var l_ContextVar)) + { + l_EventInput = (((float)l_ContextVar.Value) / 100.0f); + l_EventInput = Mathf.Max(Model.Min, l_EventInput); + l_EventInput = Mathf.Min(Model.Max, l_EventInput); + } + + l_NewValue = l_EventInput; + } + + BeatSaberPlus_NoteTweaker.Patches.PBombController.SetTemp(true, l_NewValue); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} bomb scale was set to {Mathf.RoundToInt(l_NewValue * 100f)}%"); + } + + l_Failed = false; + } + + if (l_Failed) + { + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} no map is currently played!"); + + p_Context.HasActionFailed = true; + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeDebris + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIToggle m_Debris = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Turn on or off debris"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Enable debris", + XUIToggle.Make() + .SetValue(Model.Debris).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Debris) + ) + }; + + BuildUIAuto(p_Parent); + + if (!ModulePresence.GameTweaker) + View.ShowMessageModal("GameTweaker module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + => Model.Debris = m_Debris.Element.GetValue(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.GameTweaker) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, GameTweaker module is missing!"); + yield break; + } + + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) + BeatSaberPlus_GameTweaker.Patches.PNoteDebrisSpawner.SetTemp(!Model.Debris); + else + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeLightIntensity + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_ValueSource = null; + private XUISlider m_UserValue = null; + private XUISlider m_Min = null; + private XUISlider m_Max = null; + private XUIToggle m_SendMessage = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Choose a random light intensity"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Value source", + XUIDropdown.Make() + .SetOptions(Enums.ValueSource.S).SetValue(Enums.ValueSource.ToStr(Model.ValueSource)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ValueSource) + ), + + Templates.SettingsHGroup("User value", + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(20.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.UserValue).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_UserValue) + ), + + Templates.SettingsHGroup("Random/Event value min/max", + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(20.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.Min).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Min), + + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(20.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.Max).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Max) + ), + + Templates.SettingsHGroup("Send chat message?", + XUIToggle.Make() + .SetValue(Model.SendChatMessage).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_SendMessage) + ) + }; + + BuildUIAuto(p_Parent); + + OnSettingChanged(); + + if (!ModulePresence.GameTweaker) + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: GameTweaker module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.ValueSource = Enums.ValueSource.ToEnum(m_ValueSource.Element.GetValue()); + Model.UserValue = m_UserValue.Element.GetValue(); + Model.Min = m_Min.Element.GetValue(); + Model.Max = m_Max.Element.GetValue(); + Model.SendChatMessage = m_SendMessage.Element.GetValue(); + + m_UserValue.SetInteractable(Model.ValueSource == Enums.ValueSource.E.User); + m_Min.SetInteractable(Model.ValueSource == Enums.ValueSource.E.Random || Model.ValueSource == Enums.ValueSource.E.Event); + m_Max.SetInteractable(Model.ValueSource == Enums.ValueSource.E.Random || Model.ValueSource == Enums.ValueSource.E.Event); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.GameTweaker) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, GameTweaker module is missing!"); + yield break; + } + + bool l_Failed = true; + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) + { + var l_Level = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.difficultyBeatmap?.difficulty; + var l_Effects = l_Level == BeatmapDifficulty.ExpertPlus + ? BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.playerSpecificSettings?.environmentEffectsFilterExpertPlusPreset + : BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.playerSpecificSettings?.environmentEffectsFilterDefaultPreset; + + if (l_Effects != EnvironmentEffectsFilterPreset.NoEffects) + { + if (Model.ValueSource == Enums.ValueSource.E.Config) + { + BeatSaberPlus_GameTweaker.Patches.Lights.PLightsPatches.SetFromConfig(); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} lights was set to default"); + } + else + { + var l_NewValue = 0f; + + /// Random + if (Model.ValueSource == Enums.ValueSource.E.Random) + l_NewValue = UnityEngine.Random.Range(Model.Min, Model.Max); + /// User input + else if (Model.ValueSource == Enums.ValueSource.E.User) + l_NewValue = Model.UserValue; + /// Event input + else if (Model.ValueSource == Enums.ValueSource.E.Event) + { + var l_FirstInteger = p_Context.GetFirstValueOfType(ChatPlexMod_ChatIntegrations.Interfaces.EValueType.Integer); + var l_EventInput = 1f; + + if (l_FirstInteger != default && p_Context.GetIntegerValue(l_FirstInteger.Item2, out var l_ContextVar)) + { + l_EventInput = (((float)l_ContextVar.Value) / 100.0f); + l_EventInput = Mathf.Max(Model.Min, l_EventInput); + l_EventInput = Mathf.Min(Model.Max, l_EventInput); + } + + l_NewValue = l_EventInput; + } + + BeatSaberPlus_GameTweaker.Patches.Lights.PLightsPatches.SetTempLightIntensity(l_NewValue); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} lights was set to {Mathf.RoundToInt(l_NewValue * 100f)}%"); + } + + l_Failed = false; + } + } + + if (l_Failed) + { + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} no map is currently played!"); + + p_Context.HasActionFailed = true; + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeMusicVolume + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_ValueSource = null; + private XUISlider m_UserValue = null; + private XUISlider m_Min = null; + private XUISlider m_Max = null; + private XUIToggle m_SendMessage = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Choose a random volume music"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Value source", + XUIDropdown.Make() + .SetOptions(Enums.ValueSource.S).SetValue(Enums.ValueSource.ToStr(Model.ValueSource)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ValueSource) + ), + + Templates.SettingsHGroup("User value", + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.UserValue).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_UserValue) + ), + + Templates.SettingsHGroup("Random/Event value min/max", + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.Min).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Min), + + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.Max).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Max) + ), + + Templates.SettingsHGroup("Send chat message?", + XUIToggle.Make() + .SetValue(Model.SendChatMessage).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_SendMessage) + ) + }; + + BuildUIAuto(p_Parent); + + OnSettingChanged(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.ValueSource = Enums.ValueSource.ToEnum(m_ValueSource.Element.GetValue()); + Model.UserValue = m_UserValue.Element.GetValue(); + Model.Min = m_Min.Element.GetValue(); + Model.Max = m_Max.Element.GetValue(); + Model.SendChatMessage = m_SendMessage.Element.GetValue(); + + m_UserValue.SetInteractable(Model.ValueSource == Enums.ValueSource.E.User); + m_Min.SetInteractable(Model.ValueSource == Enums.ValueSource.E.Random || Model.ValueSource == Enums.ValueSource.E.Event); + m_Max.SetInteractable(Model.ValueSource == Enums.ValueSource.E.Random || Model.ValueSource == Enums.ValueSource.E.Event); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) + { + var l_NewValue = 1f; + var l_AudioTimeSyncController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + + if (l_AudioTimeSyncController != null && l_AudioTimeSyncController) + { + /// Random + if (Model.ValueSource == Enums.ValueSource.E.Random) + l_NewValue = UnityEngine.Random.Range(Model.Min, Model.Max); + /// User input + else if (Model.ValueSource == Enums.ValueSource.E.User) + l_NewValue = Model.UserValue; + /// Event input + else if (Model.ValueSource == Enums.ValueSource.E.Event) + { + var l_FirstInteger = p_Context.GetFirstValueOfType(ChatPlexMod_ChatIntegrations.Interfaces.EValueType.Integer); + var l_EventInput = 1f; + + if (l_FirstInteger != default && p_Context.GetIntegerValue(l_FirstInteger.Item2, out var l_ContextVar)) + { + l_EventInput = (((float)l_ContextVar.Value) / 100.0f); + l_EventInput = Mathf.Max(Model.Min, l_EventInput); + l_EventInput = Mathf.Min(Model.Max, l_EventInput); + } + + l_NewValue = l_EventInput; + } + + l_AudioTimeSyncController._audioSource.volume = l_NewValue; + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} music volume was set to {Mathf.RoundToInt(l_NewValue * 100f)}]%"); + } + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeNoteColors + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_ValueSource = null; + private XUIColorInput m_LeftColor = null; + private XUIColorInput m_RightColor = null; + private XUIToggle m_SendMessage = null; + + private Color? m_LeftColorCache; + private Color? m_RightColorCache; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Change notes colors"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Choices = new List(Enums.ValueSource.S); + if ( Event.GetType() != typeof(ChatPlexMod_ChatIntegrations.Events.ChatCommand) + && Event.GetType() != typeof(ChatPlexMod_ChatIntegrations.Events.ChatPointsReward)) + l_Choices.Remove(Enums.ValueSource.ToStr(Enums.ValueSource.E.Event)); + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Value source", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(Enums.ValueSource.ToStr(Model.ValueSource)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ValueSource) + ), + + Templates.SettingsHGroup("Left/Right user color", + XUIColorInput.Make() + .SetValue(ColorU.ToUnityColor(Model.Left)).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_LeftColor), + + XUIColorInput.Make() + .SetValue(ColorU.ToUnityColor(Model.Right)).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_RightColor) + ), + + Templates.SettingsHGroup("Send chat message?", + XUIToggle.Make() + .SetValue(Model.SendChatMessage).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_SendMessage) + ) + }; + + BuildUIAuto(p_Parent); + + OnSettingChanged(); + + if (!ModulePresence.NoteTweaker) + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: NoteTweaker module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.ValueSource = Enums.ValueSource.ToEnum(m_ValueSource.Element.GetValue()); + m_LeftColorCache = m_LeftColor.Element.GetValue(); + m_RightColorCache = m_RightColor.Element.GetValue(); + Model.SendChatMessage = m_SendMessage.Element.GetValue(); + + Model.Left = ColorU.ToHexRGB(m_LeftColorCache.Value); + Model.Right = ColorU.ToHexRGB(m_RightColorCache.Value); + + m_LeftColor.SetInteractable(Model.ValueSource == Enums.ValueSource.E.User); + m_RightColor.SetInteractable(Model.ValueSource == Enums.ValueSource.E.User); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.NoteTweaker) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); + yield break; + } + + bool l_Failed = true; + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing + || Model.ValueSource == Enums.ValueSource.E.Config) + { + if (Model.ValueSource == Enums.ValueSource.E.Config) + { + BeatSaberPlus_NoteTweaker.Patches.PColorNoteVisuals.SetBlockColorOverride(false, Color.black, Color.black); + PatchSabers(true); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} colors are back to default!"); + + l_Failed = false; + } + else + { + string l_LeftHex = Model.Left; + string l_RightHex = Model.Right; + + if (Model.ValueSource == Enums.ValueSource.E.Random) + { + m_LeftColorCache = UnityEngine.Random.ColorHSV(); + m_RightColorCache = UnityEngine.Random.ColorHSV(); + + l_LeftHex = ColorU.ToHexRGB(m_LeftColorCache.Value); + l_RightHex = ColorU.ToHexRGB(m_RightColorCache.Value); + } + else if (Model.ValueSource == Enums.ValueSource.E.Event && (p_Context.Message != null || p_Context.PointsEvent != null)) /// Event user input + { + var l_Src = (p_Context.Message?.Message ?? p_Context.PointsEvent?.UserInput) ?? ""; + var l_Parts = l_Src.Split(new char[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries); + if (l_Parts.Length >= 2 + && ColorU.TryToUnityColor(l_Parts[l_Parts.Length - 2], out var l_LeftColor) + && ColorU.TryToUnityColor(l_Parts[l_Parts.Length - 1], out var l_RightColor)) + { + m_LeftColorCache = l_LeftColor; + m_RightColorCache = l_RightColor; + l_LeftHex = l_Parts[l_Parts.Length - 2]; + l_RightHex = l_Parts[l_Parts.Length - 1]; + l_Failed = false; + } + else if (p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + { + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} the syntax is: #LEFTHEX #RIGHTHEX"); + } + } + else if (Model.ValueSource == Enums.ValueSource.E.User) + { + l_Failed = false; + EnsureColorCache(); + } + + if (!l_Failed) + { + BeatSaberPlus_NoteTweaker.Patches.PColorNoteVisuals.SetBlockColorOverride(true, m_LeftColorCache.Value, m_RightColorCache.Value); + PatchSabers(false); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} colors are changed to {l_LeftHex} {l_RightHex}"); + } + } + } + + if (l_Failed) + p_Context.HasActionFailed = true; + + yield return null; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void EnsureColorCache() + { + if (!m_LeftColorCache.HasValue) + { + if (ColorU.TryToUnityColor(Model.Left, out var l_LeftColor)) + m_LeftColorCache = l_LeftColor; + else + m_LeftColorCache = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.colorScheme?.saberAColor ?? Color.red; + } + if (!m_RightColorCache.HasValue) + { + if (ColorU.TryToUnityColor(Model.Right, out var l_RightColor)) + m_RightColorCache = l_RightColor; + else + m_RightColorCache = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.colorScheme?.saberBColor ?? Color.blue; + } + } + private void PatchSabers(bool p_UseDefault) + { + /// todo + return; + + var l_Sabers = Resources.FindObjectsOfTypeAll(); + var l_ColorManager = null as ColorManager; + var l_ColorSchemeBackup = null as ColorScheme; + + for (int l_I = 0; l_I < l_Sabers.Length; ++l_I) + { + if (l_I == 0) + { + l_ColorManager = l_Sabers[l_I].GetField("_colorManager"); + + if (l_ColorManager != null) + { + l_ColorSchemeBackup = l_ColorManager.GetProperty("_colorScheme"); + if (l_ColorSchemeBackup != null && !p_UseDefault) + { + var l_ColorScheme = new ColorScheme("", "", false, "", false, + m_LeftColorCache.Value, m_RightColorCache.Value, l_ColorSchemeBackup.environmentColor0, + l_ColorSchemeBackup.environmentColor1, l_ColorSchemeBackup.supportsEnvironmentColorBoost, + l_ColorSchemeBackup.environmentColor0Boost, l_ColorSchemeBackup.environmentColor1Boost, + l_ColorSchemeBackup.obstaclesColor); + + l_ColorManager.SetProperty("_colorScheme", l_ColorScheme); + } + } + } + + if (l_ColorSchemeBackup == null) + break; + + var l_SaberTrail = l_Sabers[l_I].GetField("_saberTrail"); + var l_SetSaberGlowColors = l_Sabers[l_I].GetField("_setSaberGlowColors"); + var l_SetSaberFakeGlowColors = l_Sabers[l_I].GetField("_setSaberFakeGlowColors"); + var l_SaberLight = l_Sabers[l_I].GetField("_saberLight"); + + if (l_SaberTrail == null || l_SetSaberGlowColors == null || l_SetSaberFakeGlowColors == null || l_SaberLight == null) + continue; + + var l_SaberType = l_SaberLight.color == l_ColorSchemeBackup.saberAColor ? SaberType.SaberA : SaberType.SaberB; + var l_Color = l_SaberType == SaberType.SaberA ? m_LeftColorCache.Value : m_RightColorCache.Value; + + //l_SaberTrail.Setup((l_Color * this._initData.trailTintColor).linear, (IBladeMovementData)saber.movementData); + + foreach (var l_SetSaberGlowColor in l_SetSaberGlowColors) + l_SetSaberGlowColor.SetColors(); + foreach (var l_SaberFakeGlowColor in l_SetSaberFakeGlowColors) + l_SaberFakeGlowColor.SetColors(); + + l_SaberLight.color = l_Color; + + if (!p_UseDefault && l_I == (l_Sabers.Length - 1)) + l_ColorManager.SetProperty("_colorScheme", l_ColorSchemeBackup); + } + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeNoteScale + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_ValueSource = null; + private XUISlider m_UserValue = null; + private XUISlider m_Min = null; + private XUISlider m_Max = null; + private XUIToggle m_SendMessage = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Choose a random note scale"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Value source", + XUIDropdown.Make() + .SetOptions(Enums.ValueSource.S).SetValue(Enums.ValueSource.ToStr(Model.ValueSource)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ValueSource) + ), + + Templates.SettingsHGroup("User value", + XUISlider.Make() + .SetMinValue(0.4f).SetMaxValue(1.5f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.UserValue).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_UserValue) + ), + + Templates.SettingsHGroup("Random/Event value min/max", + XUISlider.Make() + .SetMinValue(0.4f).SetMaxValue(1.5f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.Min).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Min), + + XUISlider.Make() + .SetMinValue(0.4f).SetMaxValue(1.5f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.Max).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Max) + ), + + Templates.SettingsHGroup("Send chat message?", + XUIToggle.Make() + .SetValue(Model.SendChatMessage).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_SendMessage) + ) + }; + + BuildUIAuto(p_Parent); + + OnSettingChanged(); + + if (!ModulePresence.GameTweaker) + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: GameTweaker module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.ValueSource = Enums.ValueSource.ToEnum(m_ValueSource.Element.GetValue()); + Model.UserValue = m_UserValue.Element.GetValue(); + Model.Min = m_Min.Element.GetValue(); + Model.Max = m_Max.Element.GetValue(); + Model.SendChatMessage = m_SendMessage.Element.GetValue(); + + m_UserValue.SetInteractable(Model.ValueSource == Enums.ValueSource.E.User); + m_Min.SetInteractable(Model.ValueSource == Enums.ValueSource.E.Random || Model.ValueSource == Enums.ValueSource.E.Event); + m_Max.SetInteractable(Model.ValueSource == Enums.ValueSource.E.Random || Model.ValueSource == Enums.ValueSource.E.Event); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.NoteTweaker) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); + yield break; + } + + bool l_Failed = true; + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing + || Model.ValueSource == Enums.ValueSource.E.Config) + { + if (Model.ValueSource == Enums.ValueSource.E.Config) + { + BeatSaberPlus_NoteTweaker.Patches.PGameNoteController.SetTemp(false, 0f); + BeatSaberPlus_NoteTweaker.Patches.PBurstSliderGameNoteController.SetTemp(false, 0f); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} note scale was set to default"); + } + else + { + var l_NewValue = 0f; + + /// Random + if (Model.ValueSource == Enums.ValueSource.E.Random) + l_NewValue = UnityEngine.Random.Range(Model.Min, Model.Max); + /// User input + else if (Model.ValueSource == Enums.ValueSource.E.User) + l_NewValue = Model.UserValue; + /// Event input + else if (Model.ValueSource == Enums.ValueSource.E.Event) + { + var l_FirstInteger = p_Context.GetFirstValueOfType(ChatPlexMod_ChatIntegrations.Interfaces.EValueType.Integer); + var l_EventInput = 1f; + + if (l_FirstInteger != default && p_Context.GetIntegerValue(l_FirstInteger.Item2, out var l_ContextVar)) + { + l_EventInput = (((float)l_ContextVar.Value) / 100.0f); + l_EventInput = Mathf.Max(Model.Min, l_EventInput); + l_EventInput = Mathf.Min(Model.Max, l_EventInput); + } + + l_NewValue = l_EventInput; + } + + BeatSaberPlus_NoteTweaker.Patches.PGameNoteController.SetTemp(true, l_NewValue); + BeatSaberPlus_NoteTweaker.Patches.PBurstSliderGameNoteController.SetTemp(true, l_NewValue); + + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} note scale was set to {Mathf.RoundToInt(l_NewValue * 100f)}%"); + } + + l_Failed = false; + } + + if (l_Failed) + { + if (Model.SendChatMessage && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} no map is currently played!"); + + p_Context.HasActionFailed = true; + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_Pause + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIToggle m_HideUI = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Trigger a pause during a song"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Hide UI", + XUIToggle.Make() + .SetValue(Model.HideUI).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_HideUI) + ) + }; + + BuildUIAuto(p_Parent); + + if (!ModulePresence.GameTweaker) + View.ShowMessageModal("GameTweaker module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + => Model.HideUI = m_HideUI.Element.GetValue(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) + { + var l_BSP_MP_BigRank = GameObject.Find("BSP_MP_BigRank"); + if (l_BSP_MP_BigRank && l_BSP_MP_BigRank.activeSelf) + p_Context.HasActionFailed = true; + else + { + var l_PauseController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_PauseController && l_PauseMenuManager) + { + l_PauseController.Pause(); + if (Model.HideUI) + { + l_PauseController.didResumeEvent += PauseController_didResumeEvent; + l_PauseMenuManager.transform.Find("Wrapper/MenuWrapper/Canvas")?.gameObject?.SetActive(false); + } + } + } + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void PauseController_didResumeEvent() + { + var l_PauseController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_PauseController) + l_PauseController.didResumeEvent -= PauseController_didResumeEvent; + + var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_PauseMenuManager) + l_PauseMenuManager.transform.Find("Wrapper/MenuWrapper/Canvas")?.gameObject?.SetActive(true); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_Quit + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + public override string Description => "Exit current song"; + public override string UIPlaceHolder => "Will exit the current map"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) + { + var l_BSP_MP_BigRank = GameObject.Find("BSP_MP_BigRank"); + if (l_BSP_MP_BigRank && l_BSP_MP_BigRank.activeSelf) + p_Context.HasActionFailed = true; + else + { + var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_PauseMenuManager != null && l_PauseMenuManager) + l_PauseMenuManager.MenuButtonPressed(); + } + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_Restart + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + public override string Description => "Restart current song"; + public override string UIPlaceHolder => "Will restart the map"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) + { + var l_BSP_MP_BigRank = GameObject.Find("BSP_MP_BigRank"); + if (l_BSP_MP_BigRank && l_BSP_MP_BigRank.activeSelf) + p_Context.HasActionFailed = true; + else + { + var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_PauseMenuManager != null && l_PauseMenuManager) + l_PauseMenuManager.RestartButtonPressed(); + } + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_Resume + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + public override string Description => "Resume current song"; + public override string UIPlaceHolder => "Will resume the map"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) + { + var l_BSP_MP_BigRank = GameObject.Find("BSP_MP_BigRank"); + if (l_BSP_MP_BigRank && l_BSP_MP_BigRank.activeSelf) + p_Context.HasActionFailed = true; + else + { + var l_PauseMenuManager = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_PauseMenuManager != null && l_PauseMenuManager) + l_PauseMenuManager.ContinueButtonPressed(); + } + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_SpawnBombPatterns + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + + private XUISlider m_Interval = null; + private XUISlider m_Count = null; + + protected XUIToggle m_L0R2 = null; + protected XUIToggle m_L1R2 = null; + protected XUIToggle m_L2R2 = null; + protected XUIToggle m_L3R2 = null; + + protected XUIToggle m_L0R1 = null; + protected XUIToggle m_L1R1 = null; + protected XUIToggle m_L2R1 = null; + protected XUIToggle m_L3R1 = null; + + protected XUIToggle m_L0R0 = null; + protected XUIToggle m_L1R0 = null; + protected XUIToggle m_L2R0 = null; + protected XUIToggle m_L3R0 = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Spawn bomb patterns in a map"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Interval between bombs in seconds", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(20.0f).SetIncrements(1.0f).SetInteger(true).SetFormatter(CP_SDK.UI.ValueFormatters.TimeShortBaseSeconds) + .SetValue(Model.Interval).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Interval) + ), + + Templates.SettingsHGroup("Number of bombs pattern", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(20.0f).SetIncrements(1.0f).SetInteger(true) + .SetValue(Model.Count).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Count) + ), + + Templates.SettingsHGroup("Top", + XUIToggle.Make().SetValue((Model.L0 & (1 << 2)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L0R2), + XUIToggle.Make().SetValue((Model.L1 & (1 << 2)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L1R2), + XUIToggle.Make().SetValue((Model.L2 & (1 << 2)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L2R2), + XUIToggle.Make().SetValue((Model.L3 & (1 << 2)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L3R2) + ), + Templates.SettingsHGroup("Middle", + XUIToggle.Make().SetValue((Model.L0 & (1 << 1)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L0R1), + XUIToggle.Make().SetValue((Model.L1 & (1 << 1)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L1R1), + XUIToggle.Make().SetValue((Model.L2 & (1 << 1)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L2R1), + XUIToggle.Make().SetValue((Model.L3 & (1 << 1)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L3R1) + ), + Templates.SettingsHGroup("Bottom", + XUIToggle.Make().SetValue((Model.L0 & (1 << 0)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L0R0), + XUIToggle.Make().SetValue((Model.L1 & (1 << 0)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L1R0), + XUIToggle.Make().SetValue((Model.L2 & (1 << 0)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L2R0), + XUIToggle.Make().SetValue((Model.L3 & (1 << 0)) != 0).OnValueChanged((_) => OnSettingChanged()).Bind(ref m_L3R0) + ) + }; + + BuildUIAuto(p_Parent); + + OnSettingChanged(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Interval = (int)m_Interval.Element.GetValue(); + Model.Count = (int)m_Count.Element.GetValue(); + Model.L0 = (byte)((m_L0R0.Element.GetValue() ? (1 << 0) : 0) | (m_L0R1.Element.GetValue() ? (1 << 1) : 0) | (m_L0R2.Element.GetValue() ? (1 << 2) : 0)); + Model.L1 = (byte)((m_L1R0.Element.GetValue() ? (1 << 0) : 0) | (m_L1R1.Element.GetValue() ? (1 << 1) : 0) | (m_L1R2.Element.GetValue() ? (1 << 2) : 0)); + Model.L2 = (byte)((m_L2R0.Element.GetValue() ? (1 << 0) : 0) | (m_L2R1.Element.GetValue() ? (1 << 1) : 0) | (m_L2R2.Element.GetValue() ? (1 << 2) : 0)); + Model.L3 = (byte)((m_L3R0.Element.GetValue() ? (1 << 0) : 0) | (m_L3R1.Element.GetValue() ? (1 << 1) : 0) | (m_L3R2.Element.GetValue() ? (1 << 2) : 0)); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing + && BeatSaberPlus.SDK.Game.Logic.LevelData != null + && !BeatSaberPlus.SDK.Game.Logic.LevelData.IsNoodle + && !BeatSaberPlus.SDK.Game.Scoring.IsInReplay) + { + var l_AudioTimeSyncController = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); + var l_BeatmapObjectSpawnController = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); + + if (l_AudioTimeSyncController != null && l_AudioTimeSyncController + && l_BeatmapObjectSpawnController != null && l_BeatmapObjectSpawnController) + { + List<(int, int)> l_SpawnList = new List<(int, int)>(); + for (int l_B = 0; l_B < 3; ++l_B) + { + var l_Mask = 1 << l_B; + + if ((Model.L0 & l_Mask) != 0) + l_SpawnList.Add((0, l_B)); + if ((Model.L1 & l_Mask) != 0) + l_SpawnList.Add((1, l_B)); + if ((Model.L2 & l_Mask) != 0) + l_SpawnList.Add((2, l_B)); + if ((Model.L3 & l_Mask) != 0) + l_SpawnList.Add((3, l_B)); + } + + float l_Time = 2f; + for (int l_I = 0; l_I < Model.Count; ++l_I) + { + for (int l_S = 0; l_S < l_SpawnList.Count; ++l_S) + { + var l_Current = l_SpawnList[l_S]; + l_BeatmapObjectSpawnController.HandleNoteDataCallback(NoteData.CreateBombNoteData( + l_AudioTimeSyncController.songTime + l_Time, + l_Current.Item1, + (NoteLineLayer)l_Current.Item2 + ) + ); + } + + l_Time += Model.Interval; + } + } + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_SpawnSquatWalls + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUISlider m_Interval = null; + private XUISlider m_Count = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Spawn fake squat walls in a map"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Interval between squat walls in seconds", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(20.0f).SetIncrements(1.0f).SetInteger(true).SetFormatter(CP_SDK.UI.ValueFormatters.TimeShortBaseSeconds) + .SetValue(Model.Interval).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Interval) + ), + + Templates.SettingsHGroup("Number of squat wall", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(20.0f).SetIncrements(1.0f).SetInteger(true) + .SetValue(Model.Count).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Count) + ) + }; + + BuildUIAuto(p_Parent); + + OnSettingChanged(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Interval = (int)m_Interval.Element.GetValue(); + Model.Count = (int)m_Count.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing + && BeatSaberPlus.SDK.Game.Logic.LevelData != null + && !BeatSaberPlus.SDK.Game.Logic.LevelData.IsNoodle + && !BeatSaberPlus.SDK.Game.Scoring.IsInReplay) + { + var l_AudioTimeSyncController = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); + var l_BeatmapObjectSpawnController = UnityEngine.Resources.FindObjectsOfTypeAll().FirstOrDefault(); + + if ( l_AudioTimeSyncController != null && l_AudioTimeSyncController + && l_BeatmapObjectSpawnController != null && l_BeatmapObjectSpawnController) + { + float l_Time = 2f; + for (int l_I = 0; l_I < Model.Count; ++l_I) + { + l_BeatmapObjectSpawnController.HandleObstacleDataCallback(new ObstacleData( + l_AudioTimeSyncController.songTime + l_Time, 4, NoteLineLayer.Top, 0.3f, -4, 3 + )); + l_Time += Model.Interval; + } + } + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ToggleHUD + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_ChangeType = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Toggle HUD visibility"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Change type", + XUIDropdown.Make() + .SetOptions(ChatPlexMod_ChatIntegrations.Enums.Toggle.S).SetValue(ChatPlexMod_ChatIntegrations.Enums.Toggle.ToStr(Model.ChangeType)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ChangeType) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + => Model.ChangeType = ChatPlexMod_ChatIntegrations.Enums.Toggle.ToEnum(m_ChangeType.Element.GetValue()); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) + { + var l_CoreGameHUDController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_CoreGameHUDController != null && l_CoreGameHUDController) + { + switch (Model.ChangeType) + { + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle: + l_CoreGameHUDController.gameObject.SetActive(!l_CoreGameHUDController.gameObject.activeSelf); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Enable: + l_CoreGameHUDController.gameObject.SetActive(true); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Disable: + l_CoreGameHUDController.gameObject.SetActive(false); + break; + } + } + + var l_NoteCutScoreSpawner = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_NoteCutScoreSpawner != null && l_NoteCutScoreSpawner) + { + switch (Model.ChangeType) + { + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle: + if (l_NoteCutScoreSpawner.gameObject.activeSelf) + { + l_NoteCutScoreSpawner.OnDestroy(); + l_NoteCutScoreSpawner.gameObject.SetActive(false); + } + else + { + l_NoteCutScoreSpawner.gameObject.SetActive(true); + l_NoteCutScoreSpawner.Start(); + } + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Enable: + l_NoteCutScoreSpawner.gameObject.SetActive(true); + l_NoteCutScoreSpawner.Start(); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Disable: + l_NoteCutScoreSpawner.OnDestroy(); + l_NoteCutScoreSpawner.gameObject.SetActive(false); + break; + } + } + + var l_BadNoteCutEffectSpawner = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_BadNoteCutEffectSpawner != null && l_BadNoteCutEffectSpawner) + { + switch (Model.ChangeType) + { + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle: + if (l_BadNoteCutEffectSpawner.gameObject.activeSelf) + { + l_BadNoteCutEffectSpawner.OnDestroy(); + l_BadNoteCutEffectSpawner.gameObject.SetActive(false); + } + else + { + l_BadNoteCutEffectSpawner.gameObject.SetActive(true); + l_BadNoteCutEffectSpawner.Start(); + } + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Enable: + l_BadNoteCutEffectSpawner.gameObject.SetActive(true); + l_BadNoteCutEffectSpawner.Start(); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Disable: + l_BadNoteCutEffectSpawner.OnDestroy(); + l_BadNoteCutEffectSpawner.gameObject.SetActive(false); + break; + } + } + + var l_MissedNoteEffectSpawner = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_MissedNoteEffectSpawner != null && l_BadNoteCutEffectSpawner) + { + switch (Model.ChangeType) + { + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle: + if (l_MissedNoteEffectSpawner.gameObject.activeSelf) + { + l_MissedNoteEffectSpawner.OnDestroy(); + l_MissedNoteEffectSpawner.gameObject.SetActive(false); + } + else + { + l_MissedNoteEffectSpawner.gameObject.SetActive(true); + l_MissedNoteEffectSpawner.Start(); + } + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Enable: + l_MissedNoteEffectSpawner.gameObject.SetActive(true); + l_MissedNoteEffectSpawner.Start(); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Disable: + l_MissedNoteEffectSpawner.OnDestroy(); + l_MissedNoteEffectSpawner.gameObject.SetActive(false); + break; + } + } + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/NoteTweaker.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/NoteTweaker.cs new file mode 100644 index 0000000..9f5ac27 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/NoteTweaker.cs @@ -0,0 +1,98 @@ +using CP_SDK.XUI; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Actions +{ + public class NoteTweaker_SwitchProfile + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_Profile = null; + private XUIToggle m_Temporary = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Change active NoteTweaker profile"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Choices = new List() { "None" }; + var l_SelectedChoice = "None"; + if (ModulePresence.NoteTweaker) + { + l_Choices = BeatSaberPlus_NoteTweaker.NoteTweaker.Instance.GetAvailableProfiles(); + if (l_Choices.Count == 0) + l_Choices.Add("None"); + } + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); + + for (int l_I = 0; l_I < l_Choices.Count; ++l_I) + { + if (l_Choices[l_I] != Model.Profile) + continue; + + l_SelectedChoice = l_Choices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Profile", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_SelectedChoice) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Profile) + ), + + Templates.SettingsHGroup("Temporary", + XUIToggle.Make() + .SetValue(Model.Temporary) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Temporary) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Profile = m_Profile.Element.GetValue(); + Model.Temporary = m_Temporary.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.NoteTweaker) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, NoteTweaker module is missing!"); + yield break; + } + + var l_Instance = BeatSaberPlus_NoteTweaker.NoteTweaker.Instance; + var l_Profiles = l_Instance.GetAvailableProfiles(); + if (l_Profiles.Contains(Model.Profile)) + l_Instance.SwitchToProfile(l_Profiles.IndexOf(Model.Profile), Model.Temporary); + else + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:NoteTweaker_SwitchProfile Profile:{Model.Profile} not found!"); + } + + yield return null; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/SongChartVisualizer.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/SongChartVisualizer.cs new file mode 100644 index 0000000..f2fc80a --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Actions/SongChartVisualizer.cs @@ -0,0 +1,70 @@ +using CP_SDK.XUI; +using System.Collections; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Actions +{ + public class SongChartVisualizer_ToggleVisibility + : ChatPlexMod_ChatIntegrations.Interfaces.IAction + { + private XUIDropdown m_ChangeType = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Show or hide the SongChartVisualizer ingame"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Change type", + XUIDropdown.Make() + .SetOptions(ChatPlexMod_ChatIntegrations.Enums.Toggle.S).SetValue(ChatPlexMod_ChatIntegrations.Enums.Toggle.ToStr(Model.ChangeType)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ChangeType) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + => Model.ChangeType = ChatPlexMod_ChatIntegrations.Enums.Toggle.ToEnum(m_ChangeType.Element.GetValue()); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.SongChartVisualizer) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("SongChartVisualizer: Action failed, SongChartVisualizer module is missing!"); + yield break; + } + + var l_Instance = ChatPlexMod_SongChartVisualizer.SongChartVisualizer.Instance; + switch (Model.ChangeType) + { + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle: + l_Instance?.ToggleVisibility(); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Enable: + l_Instance?.SetVisible(true); + break; + case ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Disable: + l_Instance?.SetVisible(false); + break; + } + + yield return null; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Conditions/ChatRequest.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Conditions/ChatRequest.cs new file mode 100644 index 0000000..40420f2 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Conditions/ChatRequest.cs @@ -0,0 +1,281 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Conditions +{ + public class ChatRequest_QueueDuration + : ChatPlexMod_ChatIntegrations.Interfaces.ICondition + { + private XUIDropdown m_Comparison = null; + private XUISlider m_Duration = null; + private XUIToggle m_SendMessageOnFail = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Add conditions on chat request queue duration!"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Comparison", + XUIDropdown.Make() + .SetOptions(ChatPlexMod_ChatIntegrations.Enums.Comparison.S).SetValue(ChatPlexMod_ChatIntegrations.Enums.Comparison.ToStr(Model.Comparison)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Comparison) + ), + + Templates.SettingsHGroup("Duration", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(10800.0f).SetIncrements(1.0f).SetInteger(true).SetFormatter(CP_SDK.UI.ValueFormatters.TimeShortBaseSeconds) + .SetValue(Model.Duration).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Duration) + ), + + Templates.SettingsHGroup("Send chat message on fail", + XUIToggle.Make() + .SetValue(Model.SendChatMessageOnFail) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_SendMessageOnFail) + ), + }; + + BuildUIAuto(p_Parent); + + if (!ModulePresence.ChatRequest) + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: ChatRequest module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Comparison = ChatPlexMod_ChatIntegrations.Enums.Comparison.ToEnum(m_Comparison.Element.GetValue()); + Model.Duration = (uint)m_Duration.Element.GetValue(); + Model.SendChatMessageOnFail = m_SendMessageOnFail.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.ChatRequest) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatRequest module is missing!"); + return false; + } + + var l_ChatRequest = BeatSaberPlus_ChatRequest.ChatRequest.Instance; + if (l_ChatRequest == null || !l_ChatRequest.IsEnabled) + { + if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} chat request is not enabled"); + + return false; + } + + if (ChatPlexMod_ChatIntegrations.Enums.Comparison.Evaluate(Model.Comparison, (int)l_ChatRequest.QueueDuration, (int)Model.Duration)) + return true; + + if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + { + if (Model.Comparison <= ChatPlexMod_ChatIntegrations.Enums.Comparison.E.Equal) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is too short"); + else + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is too long"); + } + + return false; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class ChatRequest_QueueSize + : ChatPlexMod_ChatIntegrations.Interfaces.ICondition + { + private XUIDropdown m_Comparison = null; + private XUISlider m_Count = null; + private XUIToggle m_SendMessageOnFail = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Add conditions on chat request queue size!"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Comparison", + XUIDropdown.Make() + .SetOptions(ChatPlexMod_ChatIntegrations.Enums.Comparison.S).SetValue(ChatPlexMod_ChatIntegrations.Enums.Comparison.ToStr(Model.Comparison)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Comparison) + ), + + Templates.SettingsHGroup("Count", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(200.0f).SetIncrements(1.0f).SetInteger(true).SetFormatter(CP_SDK.UI.ValueFormatters.TimeShortBaseSeconds) + .SetValue(Model.Count).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Count) + ), + + Templates.SettingsHGroup("Send chat message on fail", + XUIToggle.Make() + .SetValue(Model.SendChatMessageOnFail) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_SendMessageOnFail) + ), + }; + + BuildUIAuto(p_Parent); + + if (!ModulePresence.ChatRequest) + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: ChatRequest module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + private void OnSettingChanged() + { + Model.Comparison = ChatPlexMod_ChatIntegrations.Enums.Comparison.ToEnum(m_Comparison.Element.GetValue()); + Model.Count = (uint)m_Count.Element.GetValue(); + Model.SendChatMessageOnFail = m_SendMessageOnFail.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.ChatRequest) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatRequest module is missing!"); + return false; + } + + var l_ChatRequest = BeatSaberPlus_ChatRequest.ChatRequest.Instance; + if (l_ChatRequest == null || !l_ChatRequest.IsEnabled) + { + if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} chat request is not enabled"); + + return false; + } + + if (ChatPlexMod_ChatIntegrations.Enums.Comparison.Evaluate(Model.Comparison, (int)l_ChatRequest.SongQueueCount, (int)Model.Count)) + return true; + + if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + { + if (Model.Comparison <= ChatPlexMod_ChatIntegrations.Enums.Comparison.E.Equal) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is too small"); + else + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is too big"); + } + + return false; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class ChatRequest_QueueStatus + : ChatPlexMod_ChatIntegrations.Interfaces.ICondition + { + private XUIDropdown m_Status = null; + private XUIToggle m_SendMessageOnFail = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Add conditions on chat request queue!"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Status", + XUIDropdown.Make() + .SetOptions(Enums.QueueStatus.S).SetValue(Enums.QueueStatus.ToStr(Model.Status)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Status) + ), + + Templates.SettingsHGroup("Send chat message on fail", + XUIToggle.Make() + .SetValue(Model.SendChatMessageOnFail) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_SendMessageOnFail) + ), + }; + + BuildUIAuto(p_Parent); + + if (!ModulePresence.ChatRequest) + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: ChatRequest module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Status = Enums.QueueStatus.ToEnum(m_Status.Element.GetValue()); + Model.SendChatMessageOnFail = m_SendMessageOnFail.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (!ModulePresence.ChatRequest) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatRequest module is missing!"); + return false; + } + + var l_ChatRequest = BeatSaberPlus_ChatRequest.ChatRequest.Instance; + if (l_ChatRequest == null || !l_ChatRequest.IsEnabled) + { + if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} chat request is not enabled"); + + return false; + } + + if (Model.Status == Enums.QueueStatus.E.Open && !l_ChatRequest.QueueOpen) + { + if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is closed"); + + return false; + } + else if (Model.Status == Enums.QueueStatus.E.Closed && l_ChatRequest.QueueOpen) + { + if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is open"); + + return false; + } + + return true; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Conditions/GamePlay.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Conditions/GamePlay.cs new file mode 100644 index 0000000..1423ebe --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Conditions/GamePlay.cs @@ -0,0 +1,209 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Conditions +{ + public class GamePlay_InMenu + : ChatPlexMod_ChatIntegrations.Interfaces.ICondition + { + public override string Description => "Are we currently in the menu?"; + public override string UIPlaceHolder => "Ensure that you are currently in the menu"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + return BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Menu; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_LevelEndType + : ChatPlexMod_ChatIntegrations.Interfaces.ICondition + { + private XUIToggle m_Quit = null; + private XUIToggle m_Restart = null; + private XUIToggle m_Pass = null; + private XUIToggle m_Fail = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Kind of level end!"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIText.Make("Valid types:"), + + Templates.SettingsHGroup("On quit", + XUIToggle.Make() + .SetValue(Model.Quit).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Quit) + ), + + Templates.SettingsHGroup("On restart", + XUIToggle.Make() + .SetValue(Model.Restart).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Restart) + ), + + Templates.SettingsHGroup("On pass", + XUIToggle.Make() + .SetValue(Model.Pass).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Pass) + ), + + Templates.SettingsHGroup("On fail", + XUIToggle.Make() + .SetValue(Model.Fail).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Fail) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Quit = m_Quit.Element.GetValue(); + Model.Restart = m_Restart.Element.GetValue(); + Model.Pass = m_Pass.Element.GetValue(); + Model.Fail = m_Fail.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + var l_LevelCompletionDataResults = BeatSaberPlus.SDK.Game.Logic.LevelCompletionData.Results; + var l_LevelEndAction = l_LevelCompletionDataResults.levelEndAction; + var l_LevelEndStateType = l_LevelCompletionDataResults.levelEndStateType; + + var l_IsQuit = l_LevelEndAction == LevelCompletionResults.LevelEndAction.Quit; + var l_IsRestart = l_LevelEndAction == LevelCompletionResults.LevelEndAction.Restart; + var l_IsPass = l_LevelEndStateType == LevelCompletionResults.LevelEndStateType.Cleared; + var l_IsFail = l_LevelEndStateType == LevelCompletionResults.LevelEndStateType.Failed; + + return (Model.Quit && l_IsQuit) || (Model.Restart && l_IsRestart) || (Model.Pass && l_IsPass) || (Model.Fail && l_IsFail); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_PlayingMap + : ChatPlexMod_ChatIntegrations.Interfaces.ICondition + { + private XUIDropdown m_LevelType = null; + private XUIDropdown m_BeatmapType = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Are we currently playing a map?"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIText.Make("Level type"), + XUIDropdown.Make() + .SetOptions(Enums.LevelType.S).SetValue(Enums.LevelType.ToStr(Model.LevelType)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_LevelType), + + XUIText.Make("Beatmap Mod type"), + XUIDropdown.Make() + .SetOptions(Enums.BeatmapModType.S).SetValue(Enums.BeatmapModType.ToStr(Model.BeatmapModType)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_BeatmapType), + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.LevelType = Enums.LevelType.ToEnum(m_LevelType.Element.GetValue()); + Model.BeatmapModType = Enums.BeatmapModType.ToEnum(m_BeatmapType.Element.GetValue()); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + if (CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Playing) + return false; + + var l_LevelData = BeatSaberPlus.SDK.Game.Logic.LevelData; + + if (l_LevelData == null) + return false; + + var l_IsInReplay = BeatSaberPlus.SDK.Game.Scoring.IsInReplay; + var l_LevelTypeCond = false; + var l_BeatMapTypeCond = false; + + switch (Model.LevelType) + { + case Enums.LevelType.E.Solo: + l_LevelTypeCond = !l_IsInReplay && l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Solo; + break; + case Enums.LevelType.E.Multiplayer: + l_LevelTypeCond = !l_IsInReplay && l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Multiplayer; + break; + case Enums.LevelType.E.Replay: + l_LevelTypeCond = l_IsInReplay && l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Solo; + break; + case Enums.LevelType.E.SoloAndMultiplayer: + l_LevelTypeCond = !l_IsInReplay && (l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Solo || l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Multiplayer); + break; + case Enums.LevelType.E.Any: + default: + l_LevelTypeCond = true; + break; + } + + switch (Model.BeatmapModType) + { + case Enums.BeatmapModType.E.NonNoodle: + l_BeatMapTypeCond = !l_LevelData.IsNoodle; + break; + case Enums.BeatmapModType.E.Noodle: + l_BeatMapTypeCond = l_LevelData.IsNoodle; + break; + case Enums.BeatmapModType.E.Chroma: + l_BeatMapTypeCond = l_LevelData.IsChroma; + break; + case Enums.BeatmapModType.E.NoodleOrChroma: + l_BeatMapTypeCond = l_LevelData.IsNoodle || l_LevelData.IsChroma; + break; + case Enums.BeatmapModType.E.All: + default: + l_BeatMapTypeCond = true; + break; + } + + return l_LevelTypeCond && l_BeatMapTypeCond; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/BeatmapModType.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/BeatmapModType.cs new file mode 100644 index 0000000..af9ac3b --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/BeatmapModType.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Enums +{ + public static class BeatmapModType + { + public enum E + { + All, + NonNoodle, + Noodle, + Chroma, + NoodleOrChroma + } + + public static List S = new List() + { + "All", + "NonNoodle", + "Noodle", + "Chroma", + "NoodleOrChroma" + }; + + public static int ValueCount => S.Count; + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/LevelType.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/LevelType.cs new file mode 100644 index 0000000..b7df85c --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/LevelType.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Enums +{ + public static class LevelType + { + public enum E + { + Any, + Solo, + Multiplayer, + SoloAndMultiplayer, + Replay, + } + + public static List S = new List() + { + "Any", + "Solo", + "Multiplayer", + "SoloAndMultiplayer", + "Replay" + }; + + public static int ValueCount => S.Count; + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/QueueStatus.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/QueueStatus.cs new file mode 100644 index 0000000..ba02c75 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/QueueStatus.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Enums +{ + public static class QueueStatus + { + public enum E + { + Open, + Closed + } + + public static List S = new List() + { + "Open", + "Closed" + }; + + public static int ValueCount => S.Count; + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/ValueSource.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/ValueSource.cs new file mode 100644 index 0000000..8ec10f6 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Enums/ValueSource.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Enums +{ + public static class ValueSource + { + public enum E + { + Random, + User, + Event, + Config + } + + public static List S = new List() + { + "Random", + "User", + "Event", + "Config" + }; + + public static int ValueCount => S.Count; + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelEnded.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelEnded.cs new file mode 100644 index 0000000..4dc31d8 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelEnded.cs @@ -0,0 +1,111 @@ +using ChatPlexMod_ChatIntegrations; +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Events +{ + /// + /// Level ended event + /// + public class LevelEnded : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public LevelEnded() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.Integer, "NoteCount"), + (EValueType.Integer, "HitCount"), + (EValueType.Integer, "MissCount"), + (EValueType.Floating, "Accuracy"), + (EValueType.String, "SongName"), + (EValueType.String, "Difficulty") + }.AsReadOnly(); + + RegisterCustomCondition("GamePlay_LevelEndType", () => new Conditions.GamePlay_LevelEndType(), true); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("This event will be triggered whenever you finish/fail/restart/quit a map (Include replays)") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.LevelEnded || p_Context.CustomData == null) + return false; + + return true; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + var l_LevelCompletionData = p_Context.CustomData as BeatSaberPlus.SDK.Game.LevelCompletionData; + Int64 l_NoteCount = l_LevelCompletionData.Data.transformedBeatmapData.cuttableNotesCount; + Int64 l_HitCount = l_LevelCompletionData.Results.goodCutsCount; + Int64 l_MissCount = l_NoteCount - l_HitCount; + float l_Accuracy = (float)System.Math.Round(100.0f * BeatSaberPlus.SDK.Game.Levels.GetScorePercentage(l_LevelCompletionData.MaxMultipliedScore, l_LevelCompletionData.Results.multipliedScore), 2); + string l_GameMode = l_LevelCompletionData.Data.difficultyBeatmap.parentDifficultyBeatmapSet.beatmapCharacteristic.serializedName; + string l_Difficulty = l_LevelCompletionData.Data.difficultyBeatmap.difficulty.Name(); + + p_Context.AddValue(EValueType.Integer, "NoteCount", (Int64?)l_NoteCount); + p_Context.AddValue(EValueType.Integer, "HitCount", (Int64?)l_HitCount); + p_Context.AddValue(EValueType.Integer, "MissCount", (Int64?)l_MissCount); + p_Context.AddValue(EValueType.Floating, "Accuracy", (float?)l_Accuracy); + p_Context.AddValue(EValueType.String, "SongName", l_LevelCompletionData.Data.difficultyBeatmap.level.songAuthorName + " - " + l_LevelCompletionData.Data.difficultyBeatmap.level.songName); + p_Context.AddValue(EValueType.String, "Difficulty", l_GameMode + " - " + l_Difficulty); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelPaused.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelPaused.cs new file mode 100644 index 0000000..7135968 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelPaused.cs @@ -0,0 +1,96 @@ +using ChatPlexMod_ChatIntegrations; +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Events +{ + /// + /// Level paused event + /// + public class LevelPaused : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public LevelPaused() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.String, "SongName"), + (EValueType.String, "Difficulty") + }.AsReadOnly(); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("This event will be triggered whenever the map is paused") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.LevelPaused || p_Context.CustomData == null) + return false; + + return true; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + var l_LevelData = p_Context.CustomData as BeatSaberPlus.SDK.Game.LevelData; + var l_GameMode = l_LevelData.Data.difficultyBeatmap.parentDifficultyBeatmapSet.beatmapCharacteristic.serializedName; + var l_Difficulty = l_LevelData.Data.difficultyBeatmap.difficulty.Name(); + + p_Context.AddValue(EValueType.String, "SongName", l_LevelData.Data.difficultyBeatmap.level.songAuthorName + " - " + l_LevelData.Data.difficultyBeatmap.level.songName); + p_Context.AddValue(EValueType.String, "Difficulty", l_GameMode + " - " + l_Difficulty); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelResumed.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelResumed.cs new file mode 100644 index 0000000..44a449b --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelResumed.cs @@ -0,0 +1,96 @@ +using ChatPlexMod_ChatIntegrations; +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Events +{ + /// + /// Level resumed event + /// + public class LevelResumed : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public LevelResumed() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.String, "SongName"), + (EValueType.String, "Difficulty") + }.AsReadOnly(); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("This event will be triggered whenever the map is resumed") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.LevelResumed || p_Context.CustomData == null) + return false; + + return true; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + var l_LevelData = p_Context.CustomData as BeatSaberPlus.SDK.Game.LevelData; + var l_GameMode = l_LevelData.Data.difficultyBeatmap.parentDifficultyBeatmapSet.beatmapCharacteristic.serializedName; + var l_Difficulty = l_LevelData.Data.difficultyBeatmap.difficulty.Name(); + + p_Context.AddValue(EValueType.String, "SongName", l_LevelData.Data.difficultyBeatmap.level.songAuthorName + " - " + l_LevelData.Data.difficultyBeatmap.level.songName); + p_Context.AddValue(EValueType.String, "Difficulty", l_GameMode + " - " + l_Difficulty); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelStarted.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelStarted.cs new file mode 100644 index 0000000..9a2bca6 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Events/LevelStarted.cs @@ -0,0 +1,96 @@ +using ChatPlexMod_ChatIntegrations; +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Events +{ + /// + /// Level started event + /// + public class LevelStarted : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public LevelStarted() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.String, "SongName"), + (EValueType.String, "Difficulty") + }.AsReadOnly(); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("This event will be triggered whenever you start a map (Include replays)") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.LevelStarted || p_Context.CustomData == null) + return false; + + return true; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(ChatPlexMod_ChatIntegrations.Models.EventContext p_Context) + { + var l_LevelData = p_Context.CustomData as BeatSaberPlus.SDK.Game.LevelData; + var l_GameMode = l_LevelData.Data.difficultyBeatmap.parentDifficultyBeatmapSet.beatmapCharacteristic.serializedName; + var l_Difficulty = l_LevelData.Data.difficultyBeatmap.difficulty.Name(); + + p_Context.AddValue(EValueType.String, "SongName", l_LevelData.Data.difficultyBeatmap.level.songAuthorName + " - " + l_LevelData.Data.difficultyBeatmap.level.songName); + p_Context.AddValue(EValueType.String, "Difficulty", l_GameMode + " - " + l_Difficulty); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Manager.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Manager.cs new file mode 100644 index 0000000..1fd03e2 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Manager.cs @@ -0,0 +1,230 @@ +using System.Collections; +using UnityEngine; + +using CI = ChatPlexMod_ChatIntegrations.ChatIntegrations; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber +{ + /// + /// BeatSaber chat integration manager + /// + internal static class Manager + { + /// + /// Init + /// + internal static void Init() + { + CI.OnModuleEnable -= Register; + CI.OnModuleEnable += Register; + + CI.OnModuleDisable -= UnRegister; + CI.OnModuleDisable += UnRegister; + + CI.RegisterEventType("LevelEnded", () => new Events.LevelEnded()); + CI.RegisterEventType("LevelPaused", () => new Events.LevelPaused()); + CI.RegisterEventType("LevelResumed", () => new Events.LevelResumed()); + CI.RegisterEventType("LevelStarted", () => new Events.LevelStarted()); + + //////////////////////////////////////////////////////////////////////////// + + CI.RegisterConditionType("ChatRequest_QueueDuration", () => new Conditions.ChatRequest_QueueDuration()); + CI.RegisterConditionType("ChatRequest_QueueSize", () => new Conditions.ChatRequest_QueueSize()); + CI.RegisterConditionType("ChatRequest_QueueStatus", () => new Conditions.ChatRequest_QueueStatus()); + + CI.RegisterConditionType("GamePlay_InMenu", () => new Conditions.GamePlay_InMenu()); + CI.RegisterConditionType("GamePlay_PlayingMap", () => new Conditions.GamePlay_PlayingMap()); + + //////////////////////////////////////////////////////////////////////////// + + CI.RegisterActionType("Camera2_SwitchToDefaultScene", () => new Actions.Camera2_SwitchToDefaultScene()); + CI.RegisterActionType("Camera2_SwitchToScene", () => new Actions.Camera2_SwitchToScene()); + CI.RegisterActionType("Camera2_ToggleCamera", () => new Actions.Camera2_ToggleCamera()); + + CI.RegisterActionType("GamePlay_ChangeBombColor", () => new Actions.GamePlay_ChangeBombColor()); + CI.RegisterActionType("GamePlay_ChangeBombScale", () => new Actions.GamePlay_ChangeBombScale()); + CI.RegisterActionType("GamePlay_ChangeDebris", () => new Actions.GamePlay_ChangeDebris()); + CI.RegisterActionType("GamePlay_ChangeLightIntensity", () => new Actions.GamePlay_ChangeLightIntensity()); + CI.RegisterActionType("GamePlay_ChangeMusicVolume", () => new Actions.GamePlay_ChangeMusicVolume()); + CI.RegisterActionType("GamePlay_ChangeNoteColors", () => new Actions.GamePlay_ChangeNoteColors()); + CI.RegisterActionType("GamePlay_ChangeNoteScale", () => new Actions.GamePlay_ChangeNoteScale()); + CI.RegisterActionType("GamePlay_Pause", () => new Actions.GamePlay_Pause()); + CI.RegisterActionType("GamePlay_Quit", () => new Actions.GamePlay_Quit()); + CI.RegisterActionType("GamePlay_Restart", () => new Actions.GamePlay_Restart()); + CI.RegisterActionType("GamePlay_Resume", () => new Actions.GamePlay_Resume()); + CI.RegisterActionType("GamePlay_SpawnBombPatterns", () => new Actions.GamePlay_SpawnBombPatterns()); + CI.RegisterActionType("GamePlay_SpawnSquatWalls", () => new Actions.GamePlay_SpawnSquatWalls()); + CI.RegisterActionType("GamePlay_ToggleHUD", () => new Actions.GamePlay_ToggleHUD()); + + CI.RegisterActionType("NoteTweaker_SwitchProfile", () => new Actions.NoteTweaker_SwitchProfile()); + + CI.RegisterActionType("SongChartVisualizer_ToggleVisibility", () => new Actions.SongChartVisualizer_ToggleVisibility()); + + //////////////////////////////////////////////////////////////////////////// + + CI.RegisterTemplate("ChatPointReward : 5 Squats", () => + { + var l_Event = new ChatPlexMod_ChatIntegrations.Events.ChatPointsReward(); + l_Event.Model.Cooldown = 60; + l_Event.Model.Cost = 100; + l_Event.Model.Name = "5 Squats (Template)"; + l_Event.Model.Title = "5 Squats (Template)"; + + l_Event.AddCondition(new Conditions.GamePlay_PlayingMap() { Event = l_Event, IsEnabled = true }); + + var l_SquatAction = new Actions.GamePlay_SpawnSquatWalls() { Event = l_Event, IsEnabled = true }; + l_SquatAction.Model.Count = 5; + l_SquatAction.Model.Interval = 5; + l_Event.AddOnSuccessAction(l_SquatAction); + + var l_MessageAction = new ChatPlexMod_ChatIntegrations.Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "5 squats from $SenderName, let's gooo!"; + l_Event.AddOnSuccessAction(l_MessageAction); + + return l_Event; + }); + CI.RegisterTemplate("ChatCommand : 250% lights for 10 seconds with cooldown", () => + { + var l_Event = new ChatPlexMod_ChatIntegrations.Events.ChatCommand(); + l_Event.Model.Name = "10 seconds of 250% lights with cooldown (Template)"; + l_Event.Model.Command = "!lights"; + + l_Event.AddCondition(new Conditions.GamePlay_PlayingMap() { Event = l_Event, IsEnabled = true }); + + var l_CooldownCondition = new ChatPlexMod_ChatIntegrations.Conditions.Misc_Cooldown() { Event = l_Event, IsEnabled = true }; + l_CooldownCondition.Model.PerUser = true; + l_CooldownCondition.Model.NotifyUser = true; + l_CooldownCondition.Model.CooldownTime = 60; + l_Event.Conditions.Add(l_CooldownCondition); + + l_CooldownCondition = new ChatPlexMod_ChatIntegrations.Conditions.Misc_Cooldown() { Event = l_Event, IsEnabled = true }; + l_CooldownCondition.Model.PerUser = false; + l_CooldownCondition.Model.NotifyUser = true; + l_CooldownCondition.Model.CooldownTime = 20; + l_Event.Conditions.Add(l_CooldownCondition); + + var l_MessageAction = new ChatPlexMod_ChatIntegrations.Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "Lights go brrrrr"; + l_Event.AddOnSuccessAction(l_MessageAction); + + var l_LightAction = new Actions.GamePlay_ChangeLightIntensity() { Event = l_Event, IsEnabled = true }; + l_LightAction.Model.UserValue = 2.5f; + l_LightAction.Model.SendChatMessage = false; + l_LightAction.Model.ValueSource = Enums.ValueSource.E.User; + l_Event.AddOnSuccessAction(l_LightAction); + + var l_DelayAction = new ChatPlexMod_ChatIntegrations.Actions.Misc_Delay() { Event = l_Event, IsEnabled = true }; + l_DelayAction.Model.Delay = 10; + l_DelayAction.Model.PreventNextActionFailure = true; + l_Event.AddOnSuccessAction(l_DelayAction); + + l_LightAction = new Actions.GamePlay_ChangeLightIntensity() { Event = l_Event, IsEnabled = true }; + l_LightAction.Model.ValueSource = Enums.ValueSource.E.Config; + l_LightAction.Model.SendChatMessage = false; + l_Event.AddOnSuccessAction(l_LightAction); + + return l_Event; + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On enable + /// + private static void Register() + { + BeatSaberPlus.SDK.Game.Logic.OnLevelStarted -= Game_OnLevelStarted; + BeatSaberPlus.SDK.Game.Logic.OnLevelStarted += Game_OnLevelStarted; + + BeatSaberPlus.SDK.Game.Logic.OnLevelEnded -= Game_OnLevelEnded; + BeatSaberPlus.SDK.Game.Logic.OnLevelEnded += Game_OnLevelEnded; + } + /// + /// On disable + /// + private static void UnRegister() + { + BeatSaberPlus.SDK.Game.Logic.OnLevelEnded -= Game_OnLevelEnded; + BeatSaberPlus.SDK.Game.Logic.OnLevelStarted -= Game_OnLevelStarted; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On level started + /// + /// Level data + private static void Game_OnLevelStarted(BeatSaberPlus.SDK.Game.LevelData p_Data) + { + var l_Instance = CI.Instance; + if (l_Instance == null || !l_Instance.IsEnabled) + return; + + CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => { + l_Instance.HandleEvents(new ChatPlexMod_ChatIntegrations.Models.EventContext() + { + Type = ChatPlexMod_ChatIntegrations.Interfaces.ETriggerType.LevelStarted, + CustomData = p_Data + }); + }); + CP_SDK.Unity.MTCoroutineStarter.Start(Game_FindPauseManager(p_Data)); + } + private static IEnumerator Game_FindPauseManager(BeatSaberPlus.SDK.Game.LevelData p_Data) + { + if (p_Data.Type == BeatSaberPlus.SDK.Game.LevelType.Multiplayer) + yield break; + + var l_PauseController = null as PauseController; + yield return new WaitUntil(() => (l_PauseController = GameObject.FindObjectOfType())); + + var l_Instance = CI.Instance; + if (l_Instance == null || !l_Instance.IsEnabled) + yield break; + + if (l_PauseController) + { + l_PauseController.didPauseEvent += () => + { + CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => { + l_Instance?.HandleEvents(new ChatPlexMod_ChatIntegrations.Models.EventContext() + { + Type = ChatPlexMod_ChatIntegrations.Interfaces.ETriggerType.LevelPaused, + CustomData = p_Data + }); + }); + }; + l_PauseController.didResumeEvent += () => + { + CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => { + l_Instance?.HandleEvents(new ChatPlexMod_ChatIntegrations.Models.EventContext() + { + Type = ChatPlexMod_ChatIntegrations.Interfaces.ETriggerType.LevelResumed, + CustomData = p_Data + }); + }); + }; + } + } + /// + /// On level ended + /// + /// Completion data + private static void Game_OnLevelEnded(BeatSaberPlus.SDK.Game.LevelCompletionData p_Data) + { + var l_Instance = CI.Instance; + if (l_Instance == null || !l_Instance.IsEnabled) + return; + + CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => { + l_Instance.HandleEvents(new ChatPlexMod_ChatIntegrations.Models.EventContext() + { + Type = ChatPlexMod_ChatIntegrations.Interfaces.ETriggerType.LevelEnded, + CustomData = p_Data + }); + }); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/ModPresence.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/ModPresence.cs new file mode 100644 index 0000000..4ff0151 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/ModPresence.cs @@ -0,0 +1,17 @@ +namespace BeatSaberPlus_ChatIntegrations.BeatSaber +{ + internal static class ModPresence + { + private static bool? m_Camera2; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + internal static bool Camera2 { get { + if (!m_Camera2.HasValue) + m_Camera2 = IPA.Loader.PluginManager.GetPluginFromId("Camera2") != null; + + return m_Camera2.Value; + } } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/Camera2.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/Camera2.cs new file mode 100644 index 0000000..9bba505 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/Camera2.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Models.Actions +{ + public class Camera2_SwitchToScene : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string SceneName = ""; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Camera2_ToggleCamera : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string CameraName = ""; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public ChatPlexMod_ChatIntegrations.Enums.Toggle.E ChangeType = ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ChangeType") + || !p_Serialized.ContainsKey("ToggleType")) + return; + + ChangeType = ChatPlexMod_ChatIntegrations.Enums.Toggle.ToEnum(p_Serialized["ToggleType"].Value()); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/GamePlay.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/GamePlay.cs new file mode 100644 index 0000000..9944f47 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/GamePlay.cs @@ -0,0 +1,250 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Models.Actions +{ + public class GamePlay_ChangeBombColor : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.ValueSource.E ValueSource = Enums.ValueSource.E.Random; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string Color = "#CCCCCC"; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool SendChatMessage = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ValueSource") + || !p_Serialized.ContainsKey("ValueType")) + return; + + switch (p_Serialized["ValueType"].Value()) + { + case 0: ValueSource = Enums.ValueSource.E.Config; break; + case 1: ValueSource = Enums.ValueSource.E.User; break; + case 2: ValueSource = Enums.ValueSource.E.Event; break; + } + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeBombScale : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.ValueSource.E ValueSource = Enums.ValueSource.E.Random; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float UserValue = 0.5f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Min = 0.4f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Max = 1.5f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool SendChatMessage = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ValueSource") + || !p_Serialized.ContainsKey("ValueType")) + return; + + ValueSource = Enums.ValueSource.ToEnum(p_Serialized["ValueType"].Value()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeDebris : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool Debris = false; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeLightIntensity : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.ValueSource.E ValueSource = Enums.ValueSource.E.Random; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float UserValue = 0.5f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Min = 0.5f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Max = 2f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool SendChatMessage = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ValueSource") + || !p_Serialized.ContainsKey("ValueType")) + return; + + ValueSource = Enums.ValueSource.ToEnum(p_Serialized["ValueType"].Value()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeMusicVolume : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.ValueSource.E ValueSource = Enums.ValueSource.E.Random; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float UserValue = 0.5f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Min = 0.5f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Max = 2f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool SendChatMessage = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ValueSource") + || !p_Serialized.ContainsKey("ValueType")) + return; + + ValueSource = Enums.ValueSource.ToEnum(p_Serialized["ValueType"].Value()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeNoteColors : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.ValueSource.E ValueSource = Enums.ValueSource.E.Random; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string Left = "#FF0000"; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string Right = "#0000FF"; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool SendChatMessage = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ValueSource") + || !p_Serialized.ContainsKey("ValueType")) + return; + + switch (p_Serialized["ValueType"].Value()) + { + case 0: ValueSource = Enums.ValueSource.E.Config; break; + case 1: ValueSource = Enums.ValueSource.E.User; break; + case 2: ValueSource = Enums.ValueSource.E.Event; break; + } + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ChangeNoteScale : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.ValueSource.E ValueSource = Enums.ValueSource.E.Random; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float UserValue = 0.5f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Min = 0.4f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Max = 1.5f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool SendChatMessage = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ValueSource") + || !p_Serialized.ContainsKey("ValueType")) + return; + + ValueSource = Enums.ValueSource.ToEnum(p_Serialized["ValueType"].Value()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_Pause : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool HideUI = false; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_SpawnSquatWalls : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Interval = 2f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public int Count = 10; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_SpawnBombPatterns : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public float Interval = 2f; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public int Count = 1; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public byte L0 = 0b00000111; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public byte L1 = 0b00000111; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public byte L2 = 0b00000111; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public byte L3 = 0b00000111; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class GamePlay_ToggleHUD : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public ChatPlexMod_ChatIntegrations.Enums.Toggle.E ChangeType = ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ChangeType") + || !p_Serialized.ContainsKey("ToggleType")) + return; + + ChangeType = ChatPlexMod_ChatIntegrations.Enums.Toggle.ToEnum(p_Serialized["ToggleType"].Value()); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/NoteTweaker.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/NoteTweaker.cs new file mode 100644 index 0000000..119a969 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/NoteTweaker.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Models.Actions +{ + public class NoteTweaker_SwitchProfile : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string Profile = ""; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool Temporary = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("Profile") + || !p_Serialized.ContainsKey("BaseValue")) + return; + + Profile = p_Serialized["BaseValue"].Value(); + } + } +} \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/SongChartVisualizer.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/SongChartVisualizer.cs new file mode 100644 index 0000000..2944da6 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Actions/SongChartVisualizer.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Models.Actions +{ + public class SongChartVisualizer_ToggleVisibility : ChatPlexMod_ChatIntegrations.Models.Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public ChatPlexMod_ChatIntegrations.Enums.Toggle.E ChangeType = ChatPlexMod_ChatIntegrations.Enums.Toggle.E.Toggle; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ChangeType") + || !p_Serialized.ContainsKey("ToggleType")) + return; + + ChangeType = ChatPlexMod_ChatIntegrations.Enums.Toggle.ToEnum(p_Serialized["ToggleType"].Value()); + } + } +} \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Conditions/ChatRequest.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Conditions/ChatRequest.cs new file mode 100644 index 0000000..009e995 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Conditions/ChatRequest.cs @@ -0,0 +1,85 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Models.Conditions +{ + public class ChatRequest_QueueDuration : ChatPlexMod_ChatIntegrations.Models.Condition + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public ChatPlexMod_ChatIntegrations.Enums.Comparison.E Comparison = ChatPlexMod_ChatIntegrations.Enums.Comparison.E.GreaterOrEqual; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public uint Duration = 10 * 60; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool SendChatMessageOnFail = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("Comparison") + || !p_Serialized.ContainsKey("IsGreaterThan")) + return; + + if (p_Serialized["IsGreaterThan"].Value()) + Comparison = ChatPlexMod_ChatIntegrations.Enums.Comparison.E.Greater; + else + Comparison = ChatPlexMod_ChatIntegrations.Enums.Comparison.E.Less; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class ChatRequest_QueueSize : ChatPlexMod_ChatIntegrations.Models.Condition + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public ChatPlexMod_ChatIntegrations.Enums.Comparison.E Comparison = ChatPlexMod_ChatIntegrations.Enums.Comparison.E.GreaterOrEqual; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public uint Count = 10; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool SendChatMessageOnFail = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("Comparison") + || !p_Serialized.ContainsKey("IsGreaterThan")) + return; + + if (p_Serialized["IsGreaterThan"].Value()) + Comparison = ChatPlexMod_ChatIntegrations.Enums.Comparison.E.Greater; + else + Comparison = ChatPlexMod_ChatIntegrations.Enums.Comparison.E.Less; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class ChatRequest_QueueStatus : ChatPlexMod_ChatIntegrations.Models.Condition + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.QueueStatus.E Status = Enums.QueueStatus.E.Open; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool SendChatMessageOnFail = true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("Status") + || !p_Serialized.ContainsKey("IsOpen")) + return; + + if (p_Serialized["IsOpen"].Value()) + Status = Enums.QueueStatus.E.Open; + else + Status = Enums.QueueStatus.E.Closed; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Conditions/GamePlay.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Conditions/GamePlay.cs new file mode 100644 index 0000000..3c4ac51 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/Models/Conditions/GamePlay.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber.Models.Conditions +{ + public class GamePlay_LevelEndType : ChatPlexMod_ChatIntegrations.Models.Condition + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool Pass = true; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool Fail = true; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool Quit = true; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool Restart = true; + } + + public class GamePlay_PlayingMap : ChatPlexMod_ChatIntegrations.Models.Condition + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public Enums.LevelType.E LevelType = Enums.LevelType.E.Solo; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public Enums.BeatmapModType.E BeatmapModType = Enums.BeatmapModType.E.All; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/ModulePresence.cs b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/ModulePresence.cs new file mode 100644 index 0000000..f7e20b5 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaber/ModulePresence.cs @@ -0,0 +1,40 @@ +using System.Linq; + +namespace BeatSaberPlus_ChatIntegrations.BeatSaber +{ + internal static class ModulePresence + { + private static bool? m_ChatRequest; + private static bool? m_GameTweaker; + private static bool? m_NoteTweaker; + private static bool? m_SongChartVisualizer; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + internal static bool ChatRequest { get { + if (!m_ChatRequest.HasValue) + m_ChatRequest = CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Chat Request"); + + return m_ChatRequest.Value; + } } + internal static bool GameTweaker { get { + if (!m_GameTweaker.HasValue) + m_GameTweaker = CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Game Tweaker"); + + return m_GameTweaker.Value; + } } + internal static bool NoteTweaker { get { + if (!m_NoteTweaker.HasValue) + m_NoteTweaker = CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Note Tweaker"); + + return m_NoteTweaker.Value; + } } + internal static bool SongChartVisualizer { get { + if (!m_SongChartVisualizer.HasValue) + m_SongChartVisualizer = CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Song Chart Visualizer"); + + return m_SongChartVisualizer.Value; + } } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaberPlus_ChatIntegrations.csproj b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaberPlus_ChatIntegrations.csproj index 448ee0d..61ac04a 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaberPlus_ChatIntegrations.csproj +++ b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaberPlus_ChatIntegrations.csproj @@ -47,16 +47,17 @@ OnBuildSuccess - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll False False - - $(BeatSaberDir)\Plugins\BSML.dll - False - False - $(BeatSaberDir)\Plugins\Camera2.dll False @@ -67,12 +68,12 @@ False False - + $(BeatSaberDir)\Beat Saber_Data\Managed\Core.dll False False - + $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll False False @@ -82,7 +83,7 @@ $(BeatSaberDir)\Libs\Hive.Versioning.dll False - + $(BeatSaberDir)\Beat Saber_Data\Managed\HMRendering.dll False False @@ -95,11 +96,7 @@ - - - - - + $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False @@ -132,8 +129,8 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.IMGUIModule.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll False False @@ -141,14 +138,6 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll - False - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestAudioModule.dll False @@ -159,161 +148,111 @@ False False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Settings.cs - - - SettingsLeft.cs - - - SettingsRight.cs - - - - @@ -345,6 +284,7 @@ BeatSaberPlus_SongChartVisualizer + diff --git a/Modules/BeatSaberPlus_ChatIntegrations/BeatSaberPlus_ChatIntegrations.csproj.user b/Modules/BeatSaberPlus_ChatIntegrations/BeatSaberPlus_ChatIntegrations.csproj.user index 5db8d9ee60929239d9779dc6a24adfdc0c01779f..012781eeb9f58b5f0dd22267773d93a2d711944f 100644 GIT binary patch delta 25 fcmaFI@`q)^I!0av215ot1|tSbAZa*xE#pA|UV#Rx delta 11 Tcmeyv@{VQ0I>yO+7!LpdA+QBv diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations.cs deleted file mode 100644 index 5de055e..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations.cs +++ /dev/null @@ -1,517 +0,0 @@ -using BeatSaberMarkupLanguage; -using CP_SDK.Chat.Interfaces; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; - -namespace BeatSaberPlus_ChatIntegrations -{ - /// - /// ChatIntegrations instance - /// - public partial class ChatIntegrations : BeatSaberPlus.SDK.BSPModuleBase - { - /// - /// Old database file - /// - private static string s_OLD_DATABASE_FILE = "UserData/BeatSaberPlus/ChatIntegrations.json"; - /// - /// Database file path - /// - private static string s_DATABASE_FILE { get => Path.Combine(CIConfig.Instance.DataLocation, "Database.json"); } - /// - /// Export folder - /// - public static string s_EXPORT_PATH { get => Path.Combine(CIConfig.Instance.DataLocation, "Export/"); } - /// - /// Import folder - /// - public static string s_IMPORT_PATH { get => Path.Combine(CIConfig.Instance.DataLocation, "Import/"); } - /// - /// EmoteRain assets folder - /// - public static string s_EMOTE_RAIN_ASSETS_PATH { get => Path.Combine(CIConfig.Instance.DataLocation, "Assets/EmoteRain/"); } - /// - /// Sound clips assets folder - /// - public static string s_SOUND_CLIPS_ASSETS_PATH { get => Path.Combine(CIConfig.Instance.DataLocation, "Assets/SoundClips/"); } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Twitch client ID for BeatSaberPlus - /// - public static string s_BEATSABERPLUS_CLIENT_ID = "23vjr9ec2cwoddv2fc3xfbx9nxv8vi"; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Module type - /// - public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; - /// - /// Name of the Module - /// - public override string Name => "Chat Integrations"; - /// - /// Description of the Module - /// - public override string Description => "Create cool & tights integration with your chat!"; - /// - /// Is the Module using chat features - /// - public override bool UseChatFeatures => true; - /// - /// Is enabled - /// - public override bool IsEnabled { get => CIConfig.Instance.Enabled; set { CIConfig.Instance.Enabled = value; CIConfig.Instance.Save(); } } - /// - /// Activation kind - /// - public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private static List m_RegisteredEventTypes = new List() - { - new Events.ChatBits(), - new Events.ChatCommand(), - new Events.ChatFollow(), - new Events.ChatPointsReward(), - new Events.ChatRaid(), - new Events.ChatSubscription(), - new Events.Dummy(), - new Events.LevelEnded(), - new Events.LevelPaused(), - new Events.LevelResumed(), - new Events.LevelStarted(), - new Events.VoiceAttackCommand() - /// todo GameStart - /// todo GameStop - }; - /// - /// Registered event types - /// - public static IReadOnlyList RegisteredEventTypes = m_RegisteredEventTypes.AsReadOnly(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Settings view - /// - private UI.Settings m_SettingsView = null; - /// - /// Settings left view - /// - private UI.SettingsLeft m_SettingsLeftView = null; - /// - /// Settings right view - /// - private UI.SettingsRight m_SettingsRightView = null; - /// - /// Chat core instance - /// - private bool m_ChatCoreAcquired = false; - /// - /// OBS instance - /// - private bool m_OBSAcquired = false; - /// - /// Voice attack instance - /// - private bool m_VoiceAttackAcquired = false; - /// - /// Events - /// - private List m_Events = new List(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Event list - /// - public IReadOnlyList Events; - /// - /// On broadcaster message - /// - public Action OnBroadcasterChatMessage = null; - /// - /// On voice attack command executed - /// - public Action OnVoiceAttackCommandExecuted = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Enable the Module - /// - protected override void OnEnable() - { - if (!Directory.Exists(s_EXPORT_PATH)) - Directory.CreateDirectory(s_EXPORT_PATH); - - if (!Directory.Exists(s_IMPORT_PATH)) - Directory.CreateDirectory(s_IMPORT_PATH); - - if (!Directory.Exists(s_EMOTE_RAIN_ASSETS_PATH)) - Directory.CreateDirectory(s_EMOTE_RAIN_ASSETS_PATH); - - if (!Directory.Exists(s_SOUND_CLIPS_ASSETS_PATH)) - Directory.CreateDirectory(s_SOUND_CLIPS_ASSETS_PATH); - - /// Create read only list - RegisteredEventTypes = m_RegisteredEventTypes.AsReadOnly(); - Events = new ReadOnlyCollection(m_Events); - - /// Load database - if (!LoadDatabase()) - { - /// todo : create basic samples - } - - if (!m_ChatCoreAcquired) - { - /// Init chat core - m_ChatCoreAcquired = true; - CP_SDK.Chat.Service.Acquire(); - - /// Run all services - CP_SDK.Chat.Service.Multiplexer.OnJoinChannel += ChatCoreMutiplixer_OnJoinChannel; - CP_SDK.Chat.Service.Multiplexer.OnChannelFollow += ChatCoreMutiplixer_OnChannelFollow; - CP_SDK.Chat.Service.Multiplexer.OnChannelBits += ChatCoreMutiplixer_OnChannelBits; - CP_SDK.Chat.Service.Multiplexer.OnChannelPoints += ChatCoreMutiplixer_OnChannelPoints; - CP_SDK.Chat.Service.Multiplexer.OnChannelRaid += ChatCoreMutiplexer_OnChannelRaid; - CP_SDK.Chat.Service.Multiplexer.OnChannelSubscription += ChatCoreMutiplixer_OnChannelSubscription; - CP_SDK.Chat.Service.Multiplexer.OnTextMessageReceived += ChatCoreMutiplixer_OnTextMessageReceived; - } - - if (!m_OBSAcquired) - { - /// Init OBS - m_OBSAcquired = true; - CP_SDK.OBS.Service.Acquire(); - } - - if (!m_VoiceAttackAcquired) - { - /// Init voice attack - m_VoiceAttackAcquired = true; - CP_SDK.VoiceAttack.Service.Acquire(); - - /// Run all services - CP_SDK.VoiceAttack.Service.OnCommandExecuted += VoiceAttack_OnCommandExecuted; - } - - if (CP_SDK.Chat.Service.Multiplexer.Channels.Count != 0) - { - var l_Channel = CP_SDK.Chat.Service.Multiplexer.Channels.First(); - if (l_Channel.Item2 is CP_SDK.Chat.Models.Twitch.TwitchChannel l_TwitchChannel) - { - m_Events.ForEach(x => - { - /// Re enabled channel points reward on join - if (x.IsEnabled) - x.OnEnable(); - }); - } - } - - BeatSaberPlus.SDK.Game.Logic.OnLevelStarted += Game_OnLevelStarted; - BeatSaberPlus.SDK.Game.Logic.OnLevelEnded += Game_OnLevelEnded; - } - /// - /// Disable the Module - /// - protected override void OnDisable() - { - BeatSaberPlus.SDK.Game.Logic.OnLevelStarted -= Game_OnLevelStarted; - BeatSaberPlus.SDK.Game.Logic.OnLevelEnded -= Game_OnLevelEnded; - - /// Fake disable events for integrations - m_Events.ForEach(x => - { - if (x.IsEnabled) - x.OnDisable(); - }); - - /// Save events - SaveDatabase(); - - /// Clear database - m_Events.Clear(); - - /// Un-init voice attack - if (m_VoiceAttackAcquired) - { - /// Unbind services - CP_SDK.VoiceAttack.Service.OnCommandExecuted -= VoiceAttack_OnCommandExecuted; - - /// Stop all voice attack services - CP_SDK.VoiceAttack.Service.Release(); - m_VoiceAttackAcquired = false; - } - - if (m_OBSAcquired) - { - /// Release OBS service - CP_SDK.OBS.Service.Release(); - m_OBSAcquired = false; - } - - /// Un-init chat core - if (m_ChatCoreAcquired) - { - /// Unbind services - CP_SDK.Chat.Service.Multiplexer.OnJoinChannel -= ChatCoreMutiplixer_OnJoinChannel; - CP_SDK.Chat.Service.Multiplexer.OnChannelFollow -= ChatCoreMutiplixer_OnChannelFollow; - CP_SDK.Chat.Service.Multiplexer.OnChannelBits -= ChatCoreMutiplixer_OnChannelBits; - CP_SDK.Chat.Service.Multiplexer.OnChannelPoints -= ChatCoreMutiplixer_OnChannelPoints; - CP_SDK.Chat.Service.Multiplexer.OnChannelRaid -= ChatCoreMutiplexer_OnChannelRaid; - CP_SDK.Chat.Service.Multiplexer.OnChannelSubscription -= ChatCoreMutiplixer_OnChannelSubscription; - CP_SDK.Chat.Service.Multiplexer.OnTextMessageReceived -= ChatCoreMutiplixer_OnTextMessageReceived; - - /// Stop all chat services - CP_SDK.Chat.Service.Release(); - m_ChatCoreAcquired = false; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get Module settings UI - /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() - { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); - if (m_SettingsLeftView == null) - m_SettingsLeftView = BeatSaberUI.CreateViewController(); - if (m_SettingsRightView == null) - m_SettingsRightView = BeatSaberUI.CreateViewController(); - - return (m_SettingsView, m_SettingsLeftView, m_SettingsRightView); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Register custom event type - /// - /// Type - public static void RegisterCustomEventType() where TEvent : Interfaces.IEventBase, new() - { - m_RegisteredEventTypes.Add(new TEvent()); - RegisteredEventTypes = m_RegisteredEventTypes.AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Add an event - /// - /// Event instance - internal void AddEvent(Interfaces.IEventBase p_Event) - { - lock (m_Events) - { - m_Events.Add(p_Event); - m_Events = m_Events.OrderBy((x) => x.GetTypeNameShort() + !x.IsEnabled + x.GenericModel.Name).ToList(); - Events = new ReadOnlyCollection(m_Events); - } - - if (p_Event.IsEnabled) - p_Event.OnEnable(); - } - /// - /// Add an event - /// - /// Event - /// Is an import - /// Is a clone - internal Interfaces.IEventBase AddEventFromSerialized(JObject p_JSON, bool p_IsImport, bool p_IsClone, out string p_Error) - { - if (!p_JSON.ContainsKey("Type")) - { - p_Error = "Event doesn't have a valid type"; - Logger.Instance?.Error($"[Modules.ChatIntegrations][ChatIntegrations.AddEventFromSerialized] Can't find event type\n\"{p_JSON.ToString()}\""); - return null; - } - - var l_EventType = p_JSON["Type"].Value(); - - if (l_EventType.StartsWith("BeatSaberPlus.Modules.ChatIntegrations.")) - { - l_EventType = l_EventType.Replace("BeatSaberPlus.Modules.ChatIntegrations.", "BeatSaberPlus_ChatIntegrations."); - p_JSON["Type"] = l_EventType; - } - - var l_MatchingType = m_RegisteredEventTypes.Where(x => x.GetTypeName() == l_EventType).FirstOrDefault(); - - if (l_MatchingType == null) - { - /// Todo backup this event to avoid loss - p_Error = "Event type \"" + l_EventType.Split('.').LastOrDefault() + "\" not found"; - Logger.Instance?.Error($"[Modules.ChatIntegrations][ChatIntegrations.AddEventFromSerialized] Missing event type \"{l_EventType}\""); - return null; - } - - /// Create instance - var l_NewEvent = Activator.CreateInstance(l_MatchingType.GetType()) as Interfaces.IEventBase; - - /// Unserialize event - if (!l_NewEvent.Unserialize(p_JSON, out p_Error)) - { - /// Todo backup this event to avoid loss - Logger.Instance?.Error($"[Modules.ChatIntegrations][ChatIntegrations.AddEventFromSerialized] Failed to unserialize event\n\"{p_JSON.ToString()}\" \"{p_Error}\""); - return null; - } - - if (p_IsImport || p_IsClone) - { - l_NewEvent.OnImportOrClone(p_IsImport, p_IsClone); - - if (l_NewEvent.IsEnabled) - l_NewEvent.OnEnable(); - } - - /// Avoid GUID conflict - if (GetEventByGUID(l_NewEvent.GenericModel.GUID) != null) - l_NewEvent.GenericModel.GUID = Guid.NewGuid().ToString(); - - lock (m_Events) - { - m_Events.Add(l_NewEvent); - m_Events = m_Events.OrderBy((x) => x.GetTypeNameShort() + !x.IsEnabled + x.GenericModel.Name).ToList(); - Events = new ReadOnlyCollection(m_Events); - } - - p_Error = ""; - return l_NewEvent; - } - /// - /// Get event by name - /// - /// Event name - /// - public Interfaces.IEventBase GetEventByName(string p_Name) - { - lock (m_Events) - { - return m_Events.Where(x => x.GenericModel.Name == p_Name).FirstOrDefault(); - } - } - /// - /// Get event by GUID - /// - /// Event GUID - /// - public Interfaces.IEventBase GetEventByGUID(string p_GUID) - { - lock (m_Events) - { - return m_Events.Where(x => x.GenericModel.GUID == p_GUID).FirstOrDefault(); - } - } - /// - /// Get events by type - /// - /// Type - /// - public List GetEventsByType(Type p_Type) - { - lock (m_Events) - { - return m_Events.Where(x => p_Type == null || p_Type.IsAssignableFrom(x.GetType())).ToList(); - } - } - /// - /// Toggle an event - /// - /// Event instance - internal void ToggleEvent(Interfaces.IEventBase p_Event) - { - p_Event.IsEnabled = !p_Event.IsEnabled; - if (p_Event.IsEnabled) - p_Event.OnEnable(); - else - p_Event.OnDisable(); - - lock (m_Events) - { - m_Events = m_Events.OrderBy((x) => x.GetTypeNameShort() + !x.IsEnabled + x.GenericModel.Name).ToList(); - Events = new ReadOnlyCollection(m_Events); - } - } - /// - /// Delete an event - /// - /// Event instance - internal void DeleteEvent(Interfaces.IEventBase p_Event) - { - p_Event.OnDelete(); - - lock (m_Events) - { - m_Events.Remove(p_Event); - Events = new ReadOnlyCollection(m_Events); - } - } - /// - /// Handle events - /// - /// Event context - public void HandleEvents(Models.EventContext p_Context) - { - lock (m_Events) - { - foreach (var l_Event in m_Events) - { - if (!l_Event.IsEnabled || !l_Event.Handle((Models.EventContext)p_Context.Clone())) - continue; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - l_Event.GenericModel.UsageCount++; - l_Event.GenericModel.LastUsageDate = CP_SDK.Misc.Time.UnixTimeNow(); - }); - } - } - } - /// - /// Execute event - /// - /// Event to execute - /// Execution context - /// - public bool ExecuteEvent(Interfaces.IEventBase p_Event, Models.EventContext p_Context) - { - if (p_Event == null) - return false; - - if (!p_Event.IsEnabled || !p_Event.Handle((Models.EventContext)p_Context.Clone())) - return false; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - p_Event.GenericModel.UsageCount++; - p_Event.GenericModel.LastUsageDate = CP_SDK.Misc.Time.UnixTimeNow(); - }); - - return true; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Chat.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Chat.cs new file mode 100644 index 0000000..8ca53be --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Chat.cs @@ -0,0 +1,244 @@ +using CP_SDK.Chat.Interfaces; +using CP_SDK.XUI; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; + +namespace ChatPlexMod_ChatIntegrations.Actions +{ + internal class ChatRegistration + { + internal static void Register() + { + ChatIntegrations.RegisterActionType("Chat_SendMessage", () => new Chat_SendMessage()); + ChatIntegrations.RegisterActionType("Chat_ToggleEmoteOnly", () => new Chat_ToggleEmoteOnly()); + ChatIntegrations.RegisterActionType("Chat_ToggleVisibility", () => new Chat_ToggleVisibility()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Chat_SendMessage + : Interfaces.IAction + { + private XUIText m_Message = null; + private XUIToggle m_AddTTSPrefix = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Send a message in the chat"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Message", + XUIText.Make(Model.BaseValue) + .SetAlign(TMPro.TextAlignmentOptions.Center) + .SetWrapping(true) + .Bind(ref m_Message) + ), + + Templates.SettingsHGroup("Add TTS prefix", + XUIToggle.Make() + .SetValue(Model.AddTTSPefix).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_AddTTSPrefix) + ), + + XUIHLayout.Make( + XUIPrimaryButton.Make("Set from game", OnSetFromGameButton), + XUIPrimaryButton.Make("Set from chat", OnSetFromChatButton) + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .ForEachDirect (y => { + y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained); + }) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + => Model.AddTTSPefix = m_AddTTSPrefix.Element.GetValue(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSetFromGameButton() + { + var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.EValueType.String || x.Item1 == Interfaces.EValueType.Integer || x.Item1 == Interfaces.EValueType.Floating).ToArray(); + var l_Keys = new List<(string, System.Action, string)>(); + + foreach (var l_Var in l_Variables) + l_Keys.Add(("$" + l_Var.Item2, () => View.KeyboardModal_Append("$" + l_Var.Item2), null)); + + View.ShowKeyboardModal(Model.BaseValue, (p_Result) => + { + Model.BaseValue = p_Result; + m_Message.SetText(Model.BaseValue); + }, null, l_Keys); + } + private void OnSetFromChatButton() + { + ChatIntegrations.Instance.OnBroadcasterChatMessage += Instance_OnBroadcasterChatMessage; + + var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.EValueType.String || x.Item1 == Interfaces.EValueType.Integer || x.Item1 == Interfaces.EValueType.Floating).ToArray(); + var l_Message = "Please input a message in chat with your streaming account.\nProvided values:\n"; + l_Message += string.Join(", ", l_Variables.Select(x => "$" + x.Item2).ToArray()); + + View.ShowLoadingModal(l_Message, true, () => + { + ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void Instance_OnBroadcasterChatMessage(IChatMessage p_Message) + { + Model.BaseValue = p_Message.Message; + ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; + + View.CloseLoadingModal(); + + m_Message.SetText(Model.BaseValue); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + var l_Message = (Model.AddTTSPefix ? "! " : "") + Model.BaseValue; + var l_Variables = p_Context.GetValues(Interfaces.EValueType.String, Interfaces.EValueType.Integer, Interfaces.EValueType.Floating); + + foreach (var l_Var in l_Variables) + { + var l_Key = "$" + l_Var.Item2; + var l_ReplaceValue = l_Var.Item1 == Interfaces.EValueType.String ? "" : "0"; + + if (l_Var.Item1 == Interfaces.EValueType.Integer && p_Context.GetIntegerValue(l_Var.Item2, out var l_IntegerVal)) + l_ReplaceValue = l_IntegerVal.Value.ToString(); + else if (l_Var.Item1 == Interfaces.EValueType.Floating && p_Context.GetFloatingValue(l_Var.Item2, out var l_FloatVal)) + l_ReplaceValue = l_FloatVal.Value.ToString(); + else if (l_Var.Item1 == Interfaces.EValueType.String && p_Context.GetStringValue(l_Var.Item2, out var l_StringVal)) + l_ReplaceValue = l_StringVal; + + l_Message = l_Message.Replace(l_Key, l_ReplaceValue); + } + + if (p_Context.ChatService != null && p_Context.Channel != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, l_Message); + else + CP_SDK.Chat.Service.BroadcastMessage(l_Message); + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Chat_ToggleEmoteOnly + : Interfaces.IAction + { + public override string Description => "Enable or disable emote only in the chat"; + public override string UIPlaceHolder => "Enable or disable emote only in the chat"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + var l_Channel = CP_SDK.Chat.Service.Multiplexer.Channels.First(); + if (l_Channel.Item2 is CP_SDK.Chat.Models.Twitch.TwitchChannel l_TwitchChannel) + { + if (l_TwitchChannel.Roomstate.EmoteOnly) + l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/emoteonlyoff"); + else + l_Channel.Item1.SendTextMessage(l_Channel.Item2, "/emoteonly"); + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Chat_ToggleVisibility + : Interfaces.IAction + { + private XUIDropdown m_ChangeType = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Show or hide the chat ingame"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Change type", + XUIDropdown.Make() + .SetOptions(Enums.Toggle.S).SetValue(Enums.Toggle.ToStr(Model.ToggleType)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ChangeType) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + => Model.ToggleType = Enums.Toggle.ToInt(m_ChangeType.Element.GetValue()); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (!ModulePresence.Chat) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("Chat: Action failed, Chat module is missing!"); + yield break; + } + + switch ((Enums.Toggle.E)Model.ToggleType) + { + case Enums.Toggle.E.Toggle: + ChatPlexMod_Chat.Chat.Instance?.ToggleVisibility(); + break; + case Enums.Toggle.E.Enable: + ChatPlexMod_Chat.Chat.Instance?.SetVisible(true); + break; + case Enums.Toggle.E.Disable: + ChatPlexMod_Chat.Chat.Instance?.SetVisible(false); + break; + } + + yield return null; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/EmoteRain.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/EmoteRain.cs new file mode 100644 index 0000000..4fb126f --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/EmoteRain.cs @@ -0,0 +1,300 @@ +using ChatPlexMod_ChatIntegrations.Models; +using CP_SDK.XUI; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Actions +{ + internal class EmoteRainRegistration + { + internal static void Register() + { + ChatIntegrations.RegisterActionType("EmoteRain_CustomRain", () => new EmoteRain_CustomRain()); + ChatIntegrations.RegisterActionType("EmoteRain_EmoteBombRain", () => new EmoteRain_EmoteBombRain()); + ChatIntegrations.RegisterActionType("EmoteRain_SubRain", () => new EmoteRain_SubRain()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class EmoteRain_CustomRain + : Interfaces.IAction + { + private XUIDropdown m_Dropdown = null; + private XUISlider m_Count = null; + + private CP_SDK.Unity.EnhancedImage m_LoadedImage = null; + private string m_LoadedImageID = ""; + private string m_LoadedImageName = ""; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Make rain custom emotes"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Files = Directory.GetFiles(ChatIntegrations.s_EMOTE_RAIN_ASSETS_PATH, "*.png") + .Union(Directory.GetFiles(ChatIntegrations.s_EMOTE_RAIN_ASSETS_PATH, "*.gif")) + .Union(Directory.GetFiles(ChatIntegrations.s_EMOTE_RAIN_ASSETS_PATH, "*.apng")).ToArray(); + + var l_Selected = "None"; + var l_Choices = new List() { "None" }; + foreach (var l_CurrentFile in l_Files) + { + var l_Filtered = Path.GetFileName(l_CurrentFile); + l_Choices.Add(l_Filtered); + + if (l_Filtered == Model.BaseValue) + l_Selected = l_Filtered; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Custom emote", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_Selected).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Dropdown) + ), + + Templates.SettingsHGroup("Count per emote", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(100.0f).SetIncrements(1.0f).SetInteger(true).SetValue(Model.Count).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Count) + ), + + XUIPrimaryButton.Make("Test", OnTestButton) + }; + + BuildUIAuto(p_Parent); + + if (!ModulePresence.ChatEmoteRain) + View.ShowMessageModal("ChatEmoteRain module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.BaseValue = m_Dropdown.Element.GetValue(); + Model.Count = (uint)m_Count.Element.GetValue(); + + if (m_Dropdown.Element.GetValue() == "None") + Model.BaseValue = ""; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnTestButton() + { + if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance == null || !ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) + { + View.ShowMessageModal("ChatEmoteRain is not enabled!"); + return; + } + + MakeItRain(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(EventContext p_Context) + { + if (!ModulePresence.ChatEmoteRain) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatEmoteRain module is missing!"); + yield break; + } + + if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance != null && ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) + MakeItRain(); + else + p_Context.HasActionFailed = true; + + yield return null; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void MakeItRain() + { + EnsureLoaded((p_Loaded) => + { + if (m_LoadedImage == null) + return; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.EmitEnhancedImage(m_LoadedImage, Model.Count)); + }); + } + private void EnsureLoaded(Action p_Callback) + { + CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => + { + if (Model.BaseValue == "None") + { + p_Callback?.Invoke(false); + return; + } + + if (m_LoadedImageName != Model.BaseValue) + { + m_LoadedImageName = Model.BaseValue; + + string l_Path = Path.Combine(ChatIntegrations.s_EMOTE_RAIN_ASSETS_PATH, Model.BaseValue); + if (File.Exists(l_Path)) + { + m_LoadedImageID = "$CPM$CI$_" + Model.BaseValue; + CP_SDK.Unity.EnhancedImage.FromFile(l_Path, m_LoadedImageID, (p_Result) => + { + m_LoadedImage = p_Result; + p_Callback?.Invoke(m_LoadedImage != null); + }); + } + else + p_Callback?.Invoke(false); + } + + p_Callback?.Invoke(true); + }); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class EmoteRain_EmoteBombRain + : Interfaces.IAction + { + private XUISlider m_EmoteKind = null; + private XUISlider m_CountPerEmote = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Trigger a massive emote bomb rain"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Emote kind", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(100.0f).SetIncrements(1.0f).SetInteger(true) + .SetValue(Model.EmoteKindCount).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_EmoteKind) + ), + + Templates.SettingsHGroup("Count per emote", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(100.0f).SetIncrements(1.0f).SetInteger(true) + .SetValue(Model.CountPerEmote).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_CountPerEmote) + ), + + XUIPrimaryButton.Make("Test", OnTestButton) + }; + + BuildUIAuto(p_Parent); + + if (!ModulePresence.ChatEmoteRain) + View.ShowMessageModal("ChatEmoteRain module is missing!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.EmoteKindCount = (uint)m_EmoteKind.Element.GetValue(); + Model.CountPerEmote = (uint)m_CountPerEmote.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnTestButton() + { + if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance == null || !ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) + { + View.ShowMessageModal("ChatEmoteRain is not enabled!"); + return; + } + + CP_SDK.Unity.MTCoroutineStarter.Start(Eval(null)); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (!ModulePresence.ChatEmoteRain) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatEmoteRain module is missing!"); + yield break; + } + + if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance != null && ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) + { + CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => + { + var l_Emotes = + CP_SDK.Chat.ChatImageProvider.CachedEmoteInfo.Values.OrderBy(_ => UnityEngine.Random.Range(0, 1000)).Take((int)Model.EmoteKindCount); + + foreach (var l_Emote in l_Emotes) + ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.EmitEnhancedImage(l_Emote, Model.CountPerEmote); + }); + } + else if (p_Context != null) + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class EmoteRain_SubRain : Interfaces.IAction + { + public override string Description => "Trigger a subscription rain"; + public override string UIPlaceHolder => "Will trigger a subscription emote rain"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (!ModulePresence.ChatEmoteRain) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatEmoteRain module is missing!"); + yield break; + } + + if (ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance != null && ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.IsEnabled) + ChatPlexMod_ChatEmoteRain.ChatEmoteRain.Instance.StartSubRain(); + else + p_Context.HasActionFailed = true; + + yield return null; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Event.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Event.cs new file mode 100644 index 0000000..becb847 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Event.cs @@ -0,0 +1,209 @@ +using ChatPlexMod_ChatIntegrations.Models; +using CP_SDK.XUI; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Actions +{ + internal class EventRegistration + { + internal static void Register() + { + ChatIntegrations.RegisterActionType("Event_ExecuteDummy", () => new Event_ExecuteDummy()); + ChatIntegrations.RegisterActionType("Event_Toggle", () => new Event_Toggle()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Event_ExecuteDummy + : Interfaces.IAction + { + private XUIDropdown m_Dropdown = null; + private XUIToggle m_Continue = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Execute a dummy event"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Events = ChatIntegrations.Instance.GetEventsByType(typeof(Events.Dummy)); + var l_Choices = new List() { "None" }; + var l_Selected = "None"; + l_Events.Sort((x, y) => (x.GetTypeName() + x.GenericModel.Name).CompareTo(y.GetTypeName() + y.GenericModel.Name)); + + foreach (var l_EventBase in l_Events) + { + if (l_EventBase == Event) + continue; + + l_Choices.Add(l_EventBase.GenericModel.Name); + + if (Model.BaseValue != "" && l_EventBase.GenericModel.GUID == Model.BaseValue) + l_Selected = l_EventBase.GenericModel.Name; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Dummy event to execute", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_Selected).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Dropdown) + ), + + Templates.SettingsHGroup("Continue execution if failed", + XUIToggle.Make() + .SetValue(Model.Continue).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Continue) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + if (m_Dropdown.Element.GetValue() == "None") + Model.BaseValue = ""; + + var l_SelectedEvent = ChatIntegrations.Instance.GetEventByName(m_Dropdown.Element.GetValue()); + if (l_SelectedEvent != null && l_SelectedEvent is Events.Dummy) + Model.BaseValue = l_SelectedEvent.GenericModel.GUID; + else + { + Model.BaseValue = ""; + m_Dropdown.SetValue("None", false); + } + + Model.Continue = m_Continue.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(EventContext p_Context) + { + var l_SelectedEvent = ChatIntegrations.Instance.GetEventByGUID(Model.BaseValue); + if (l_SelectedEvent != null && l_SelectedEvent is Events.Dummy) + { + if (Model.Continue) + ChatIntegrations.Instance.ExecuteEvent(l_SelectedEvent, new EventContext() { Type = Interfaces.ETriggerType.Dummy }); + else + p_Context.HasActionFailed = !ChatIntegrations.Instance.ExecuteEvent(l_SelectedEvent, new EventContext() { Type = Interfaces.ETriggerType.Dummy }); + } + else + p_Context.HasActionFailed = true; + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Event_Toggle + : Interfaces.IAction + { + private XUIDropdown m_Event = null; + private XUIDropdown m_ChangeType = null; + + private Dictionary m_NameToGUID = new Dictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Toggle an event"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + m_NameToGUID.Clear(); + + var l_Events = ChatIntegrations.Instance.GetEventsByType(null); + var l_Choices = new List() { "None" }; + var l_Selected = "None"; + l_Events.Sort((x, y) => (x.GetTypeName() + x.GenericModel.Name).CompareTo(y.GetTypeName() + y.GenericModel.Name)); + + foreach (var l_EventBase in l_Events) + { + var l_Line = BuildLineString(l_EventBase); + l_Choices.Add(l_Line); + m_NameToGUID.Add(l_Line, l_EventBase.GenericModel.GUID); + + if (Model.BaseValue != "" && l_EventBase.GenericModel.GUID == Model.BaseValue) + l_Selected = l_Line; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Dummy event to execute", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_Selected).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Event) + ), + + Templates.SettingsHGroup("Change type", + XUIDropdown.Make() + .SetOptions(Enums.Toggle.S).SetValue(Enums.Toggle.ToStr(Model.ChangeType)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ChangeType) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + if (m_Event.Element.GetValue() == "None") + Model.BaseValue = ""; + + if (m_NameToGUID.TryGetValue(m_Event.Element.GetValue(), out var l_SelectedGUID)) + Model.BaseValue = l_SelectedGUID; + else + { + Model.BaseValue = ""; + m_Event.SetValue("None", false); + } + + Model.ChangeType = Enums.Toggle.ToInt(m_ChangeType.Element.GetValue()); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(EventContext p_Context) + { + var l_SelectedEvent = string.IsNullOrEmpty(Model.BaseValue) ? null : ChatIntegrations.Instance.GetEventByGUID(Model.BaseValue); + if (l_SelectedEvent != null) + { + if (Model.ChangeType == (int)Enums.Toggle.E.Toggle + || (Model.ChangeType == (int)Enums.Toggle.E.Enable && !l_SelectedEvent.IsEnabled) + || (Model.ChangeType == (int)Enums.Toggle.E.Disable && l_SelectedEvent.IsEnabled)) + ChatIntegrations.Instance.ToggleEvent(l_SelectedEvent); + } + + yield return null; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private string BuildLineString(Interfaces.IEventBase p_Event) + => $"[{p_Event.GetTypeName()}] {p_Event.GenericModel.Name}"; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Misc.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Misc.cs new file mode 100644 index 0000000..ba3139d --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Misc.cs @@ -0,0 +1,317 @@ +using ChatPlexMod_ChatIntegrations.Models; +using CP_SDK.XUI; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; +using UnityEngine.Networking; + +namespace ChatPlexMod_ChatIntegrations.Actions +{ + internal class MiscRegistration + { + internal static void Register() + { + ChatIntegrations.RegisterActionType("Misc_Delay", () => new Misc_Delay()); + ChatIntegrations.RegisterActionType("Misc_PlaySound", () => new Misc_PlaySound()); + ChatIntegrations.RegisterActionType("Misc_WaitMenuScene", () => new Misc_WaitMenuScene()); + ChatIntegrations.RegisterActionType("Misc_WaitPlayingScene", () => new Misc_WaitPlayingScene()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Misc_Delay + : Interfaces.IAction + { + private XUISlider m_Delay = null; + private XUISlider m_DelayMs = null; + private XUIToggle m_PreventNextActionsFailure = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Delay next actions"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsVGroup("Delay", + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1200.0f).SetIncrements(1.0f).SetFormatter(CP_SDK.UI.ValueFormatters.TimeShortBaseSeconds) + .SetValue(Model.Delay) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Delay), + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1000.0f).SetIncrements(1.0f).SetFormatter(CP_SDK.UI.ValueFormatters.MillisecondsShort) + .SetValue(Model.DelayMs) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_DelayMs) + ), + + Templates.SettingsHGroup("Prevent next actions failure", + XUIToggle.Make() + .SetValue(Model.PreventNextActionFailure) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_PreventNextActionsFailure) + ), + + XUIVLayout.Make( + XUIText.Make("This actions will delay next actions execution"), + XUIText.Make("If prevent next actions failure is enabled,"), + XUIText.Make("any failed action won't refund the user") + ) + .SetBackground(true), + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Delay = (uint)m_Delay.Element.GetValue(); + Model.DelayMs = (uint)m_DelayMs.Element.GetValue(); + Model.PreventNextActionFailure = m_PreventNextActionsFailure.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (Model.PreventNextActionFailure) + p_Context.PreventNextActionFailure = true; + + yield return new WaitForSecondsRealtime((float)Model.Delay + (((float)Model.DelayMs) / 1000f)); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Misc_PlaySound + : Interfaces.IAction + { + private XUIDropdown m_Dropdown = null; + private XUISlider m_Volume = null; + private XUISlider m_PitchMin = null; + private XUISlider m_PitchMax = null; + private XUIToggle m_KillOnSceneChange = null; + + private string m_PathCache = null; + private AudioClip m_AudioClip = null; + private AudioSource m_AudioSource = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Play a sound clip"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void BuildUI(Transform p_Parent) + { + var l_Files = Directory.GetFiles(ChatIntegrations.s_SOUND_CLIPS_ASSETS_PATH, "*.ogg").ToArray(); + var l_Choices = new List() { "None" }; + var l_Selected = "None"; + + foreach (var l_CurrentFile in l_Files) + { + var l_Filtered = Path.GetFileName(l_CurrentFile); + l_Choices.Add(l_Filtered); + + if (l_Filtered == Model.BaseValue) + l_Selected = l_Filtered; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Sound clip", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_Selected).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Dropdown) + ), + + Templates.SettingsHGroup("Volume", + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.PitchMin).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Volume) + ), + + Templates.SettingsHGroup("Pitch min/max", + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(2.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.PitchMin).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_PitchMin), + + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(2.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(Model.PitchMax).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_PitchMax) + ), + + Templates.SettingsHGroup("Kill on scene switch?", + XUIToggle.Make() + .SetValue(Model.KillOnSceneSwitch).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_KillOnSceneChange) + ), + + XUIPrimaryButton.Make("Test", OnTestButton) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + if (Model.BaseValue != m_Dropdown.Element.GetValue()) + { + m_PathCache = null; + m_AudioClip = null; + } + + Model.BaseValue = m_Dropdown.Element.GetValue(); + Model.Volume = m_Volume.Element.GetValue(); + Model.PitchMin = m_PitchMin.Element.GetValue(); + Model.PitchMax = m_PitchMax.Element.GetValue(); + Model.KillOnSceneSwitch = m_KillOnSceneChange.Element.GetValue(); + + if (Model.BaseValue == "None") + Model.BaseValue = ""; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnTestButton() + { + CP_SDK.Unity.MTCoroutineStarter.Start(Eval(null)); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(EventContext p_Context) + { + if (Model.BaseValue != null) + { + if (m_PathCache == null) + m_PathCache = Path.Combine(Environment.CurrentDirectory, ChatIntegrations.s_SOUND_CLIPS_ASSETS_PATH, Model.BaseValue); + + yield return PlayAudioClip(m_PathCache); + } + else if (p_Context != null) + p_Context.HasActionFailed = true; + + yield return null; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private IEnumerator PlayAudioClip(string p_File) + { + if (m_AudioClip == null && File.Exists(p_File)) + { + UnityWebRequest l_Song = UnityWebRequestMultimedia.GetAudioClip(p_File, AudioType.OGGVORBIS); + yield return l_Song.SendWebRequest(); + + AudioClip l_Clip = null; + try + { + ((DownloadHandlerAudioClip)l_Song.downloadHandler).streamAudio = true; + l_Clip = DownloadHandlerAudioClip.GetContent(l_Song); + + if (l_Clip == null) + { + Logger.Instance.Debug("[ChatPlexMod_ChatIntegrations.Actions][Misc_PlaySound.PlayAudioClip] No audio found!"); + yield break; + } + + } + catch (Exception p_Exception) + { + Logger.Instance.Error("[ChatPlexMod_ChatIntegrations.Actions][Misc_PlaySound.PlayAudioClip] Can't load audio! Exception: "); + Logger.Instance.Error(p_Exception); + + yield break; + } + + yield return new WaitUntil(() => l_Clip); + + m_AudioClip = l_Clip; + } + + if (m_AudioClip != null) + { + if (m_AudioSource == null || !m_AudioSource) + { + m_AudioSource = new GameObject("BSP_CI_Misc_PlaySound").AddComponent(); + m_AudioSource.loop = false; + m_AudioSource.spatialize = false; + m_AudioSource.playOnAwake = false; + m_AudioSource.ignoreListenerPause = true; + + if (!Model.KillOnSceneSwitch) + GameObject.DontDestroyOnLoad(m_AudioSource); + } + + m_AudioSource.clip = m_AudioClip; + m_AudioSource.volume = Model.Volume; + m_AudioSource.pitch = UnityEngine.Random.Range(Model.PitchMin, Model.PitchMax); + m_AudioSource.Play(); + } + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Misc_WaitMenuScene + : Interfaces.IAction + { + public override string Description => "Wait for menu scene"; + public override string UIPlaceHolder => "Wait for menu scene"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(EventContext p_Context) + { + yield return new WaitUntil(() => CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Menu); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Misc_WaitPlayingScene + : Interfaces.IAction + { + public override string Description => "Wait for playing scene"; + public override string UIPlaceHolder => "Wait for playing scene"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(EventContext p_Context) + { + yield return new WaitUntil(() => CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Playing); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/OBS.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/OBS.cs new file mode 100644 index 0000000..0edbd0d --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/OBS.cs @@ -0,0 +1,1326 @@ +using CP_SDK.Chat.Interfaces; +using CP_SDK.XUI; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.UI; + +using OBSService = CP_SDK.OBS.Service; + +namespace ChatPlexMod_ChatIntegrations.Actions +{ + internal class OBSRegistration + { + internal static void Register() + { + ChatIntegrations.RegisterActionType("OBS_RenameLastRecord", () => new OBS_RenameLastRecord()); + ChatIntegrations.RegisterActionType("OBS_StartRecording", () => new OBS_StartRecording()); + ChatIntegrations.RegisterActionType("OBS_StartStreaming", () => new OBS_StartStreaming()); + ChatIntegrations.RegisterActionType("OBS_SetRecordFilenameFormat", () => new OBS_SetRecordFilenameFormat()); + ChatIntegrations.RegisterActionType("OBS_StopRecording", () => new OBS_StopRecording()); + ChatIntegrations.RegisterActionType("OBS_StopStreaming", () => new OBS_StopStreaming()); + ChatIntegrations.RegisterActionType("OBS_SwitchPreviewToScene", () => new OBS_SwitchPreviewToScene()); + ChatIntegrations.RegisterActionType("OBS_SwitchToScene", () => new OBS_SwitchToScene()); + ChatIntegrations.RegisterActionType("OBS_ToggleStudioMode", () => new OBS_ToggleStudioMode()); + ChatIntegrations.RegisterActionType("OBS_ToggleSource", () => new OBS_ToggleSource()); + ChatIntegrations.RegisterActionType("OBS_ToggleSourceAudio", () => new OBS_ToggleSourceAudio()); + ChatIntegrations.RegisterActionType("OBS_Transition", () => new OBS_Transition()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_RenameLastRecord + : Interfaces.IAction + { + private XUIText m_Format = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Rename last record file"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("Marker name") + .SetColor(Color.yellow), + + XUIText.Make(Model.Format) + .Bind(ref m_Format) + ) + .SetBackground(true), + + XUIHLayout.Make( + XUIPrimaryButton.Make("Set from game", OnSetFromGameButton), + XUIPrimaryButton.Make("Set from chat", OnSetFromChatButton) + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .ForEachDirect (y => { + y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained); + }) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSetFromGameButton() + { + var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.EValueType.String || x.Item1 == Interfaces.EValueType.Integer || x.Item1 == Interfaces.EValueType.Floating).ToArray(); + var l_Keys = new List<(string, System.Action, string)>(); + + foreach (var l_Var in l_Variables) + l_Keys.Add(("$" + l_Var.Item2, () => View.KeyboardModal_Append("$" + l_Var.Item2), null)); + + View.ShowKeyboardModal(Model.Format, (p_Result) => + { + Model.Format = p_Result; + m_Format.SetText(Model.Format); + }, null, l_Keys); + } + private void OnSetFromChatButton() + { + ChatIntegrations.Instance.OnBroadcasterChatMessage += Instance_OnBroadcasterChatMessage; + + var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.EValueType.String || x.Item1 == Interfaces.EValueType.Integer || x.Item1 == Interfaces.EValueType.Floating).ToArray(); + var l_Message = "Please input a message in chat with your streaming account.\nProvided values:\n"; + l_Message += string.Join(", ", l_Variables.Select(x => "$" + x.Item2).ToArray()); + + View.ShowLoadingModal(l_Message, true, () => + { + ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void Instance_OnBroadcasterChatMessage(IChatMessage p_Message) + { + Model.Format = p_Message.Message; + ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; + + View.CloseLoadingModal(); + + m_Format.SetText(Model.Format); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + var l_ExistingFile = OBSService.LastRecordedFileName; + if (string.IsNullOrEmpty(l_ExistingFile) || !File.Exists(l_ExistingFile)) + { + p_Context.HasActionFailed = true; + yield break; + } + + var l_Path = Path.GetDirectoryName(l_ExistingFile); + var l_Result = Model.Format; + var l_Variables = p_Context.GetValues(Interfaces.EValueType.String, Interfaces.EValueType.Integer, Interfaces.EValueType.Floating); + l_Variables.Add((Interfaces.EValueType.String, "OriginalName")); + + for (int l_I = 0; l_I < l_Variables.Count; ++l_I) + { + var l_Var = l_Variables[l_I]; + var l_Key = "$" + l_Var.Item2; + var l_ReplaceValue = l_Var.Item1 == Interfaces.EValueType.String ? "" : "0"; + + if (l_Var.Item1 == Interfaces.EValueType.String && l_Var.Item2 == "OriginalName") + l_ReplaceValue = !string.IsNullOrEmpty(l_ExistingFile) ? Path.GetFileNameWithoutExtension(l_ExistingFile) : ""; + else if (l_Var.Item1 == Interfaces.EValueType.Integer && p_Context.GetIntegerValue(l_Var.Item2, out var l_IntegerVal)) + l_ReplaceValue = l_IntegerVal.Value.ToString(); + else if (l_Var.Item1 == Interfaces.EValueType.Floating && p_Context.GetFloatingValue(l_Var.Item2, out var l_FloatVal)) + l_ReplaceValue = l_FloatVal.Value.ToString(); + else if (l_Var.Item1 == Interfaces.EValueType.String && p_Context.GetStringValue(l_Var.Item2, out var l_StringVal)) + l_ReplaceValue = string.Join("_", l_StringVal.Split(Path.GetInvalidFileNameChars())); + + l_Result = l_Result.Replace(l_Key, l_ReplaceValue); + } + + var l_NewFile = Path.Combine(l_Path, l_Result + Path.GetExtension(l_ExistingFile)); + + Task.Run(async () => + { + /// Wait for OBS to finish IO + await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false); + + try + { + if (File.Exists(l_NewFile)) + { + l_NewFile = Path.Combine(l_Path, l_Result + CP_SDK.Misc.Time.UnixTimeNow() + Path.GetExtension(l_ExistingFile)); + File.Move(l_ExistingFile, l_NewFile); + } + else + File.Move(l_ExistingFile, l_NewFile); + } + catch (Exception) + { + + } + }).ConfigureAwait(false); + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_StartRecording + : Interfaces.IAction + { + public override string Description => "Start recording"; + public override string UIPlaceHolder => "Start recording"; + public override bool UIPlaceHolderTestButton => true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + protected override void OnUIPlaceholderTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + OBSService.StartRecording(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + OBSService.StartRecording(); + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_StartStreaming + : Interfaces.IAction + { + public override string Description => "Start streaming"; + public override string UIPlaceHolder => "Start streaming"; + public override bool UIPlaceHolderTestButton => true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + protected override void OnUIPlaceholderTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + OBSService.StartStreaming(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + OBSService.StartStreaming(); + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_SetRecordFilenameFormat + : Interfaces.IAction + { + private XUIText m_MarkerName = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Set record filename format"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("Marker name") + .SetColor(Color.yellow), + + XUIText.Make(Model.Format) + .Bind(ref m_MarkerName) + ) + .SetBackground(true), + + XUIHLayout.Make( + XUIPrimaryButton.Make("Set from game", OnSetFromGameButton), + XUIPrimaryButton.Make("Set from chat", OnSetFromChatButton) + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .ForEachDirect (y => { + y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained); + }) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSetFromGameButton() + { + var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.EValueType.String || x.Item1 == Interfaces.EValueType.Integer || x.Item1 == Interfaces.EValueType.Floating).ToArray(); + var l_Keys = new List<(string, System.Action, string)>(); + + foreach (var l_Var in l_Variables) + l_Keys.Add(("$" + l_Var.Item2, () => View.KeyboardModal_Append("$" + l_Var.Item2), null)); + + View.ShowKeyboardModal(Model.Format, (p_Result) => + { + Model.Format = p_Result; + m_MarkerName.SetText(Model.Format); + }, null, l_Keys); + } + private void OnSetFromChatButton() + { + ChatIntegrations.Instance.OnBroadcasterChatMessage += Instance_OnBroadcasterChatMessage; + + var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.EValueType.String || x.Item1 == Interfaces.EValueType.Integer || x.Item1 == Interfaces.EValueType.Floating).ToArray(); + var l_Message = "Please input a message in chat with your streaming account.\nProvided values:\n"; + l_Message += string.Join(", ", l_Variables.Select(x => "$" + x.Item2).ToArray()); + + View.ShowLoadingModal(l_Message, true, () => + { + ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void Instance_OnBroadcasterChatMessage(IChatMessage p_Message) + { + Model.Format = p_Message.Message; + ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; + + View.CloseLoadingModal(); + + m_MarkerName.SetText(Model.Format); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + var l_Result = Model.Format; + var l_Variables = p_Context.GetValues(Interfaces.EValueType.String, Interfaces.EValueType.Integer, Interfaces.EValueType.Floating); + + for (int l_I = 0; l_I < l_Variables.Count; ++l_I) + { + var l_Var = l_Variables[l_I]; + var l_Key = "$" + l_Var.Item2; + var l_ReplaceValue = l_Var.Item1 == Interfaces.EValueType.String ? "" : "0"; + + if (l_Var.Item1 == Interfaces.EValueType.Integer && p_Context.GetIntegerValue(l_Var.Item2, out var l_IntegerVal)) + l_ReplaceValue = l_IntegerVal.Value.ToString(); + else if (l_Var.Item1 == Interfaces.EValueType.Floating && p_Context.GetFloatingValue(l_Var.Item2, out var l_FloatVal)) + l_ReplaceValue = l_FloatVal.Value.ToString(); + else if (l_Var.Item1 == Interfaces.EValueType.String && p_Context.GetStringValue(l_Var.Item2, out var l_StringVal)) + l_ReplaceValue = string.Join("_", l_StringVal.Split(System.IO.Path.GetInvalidFileNameChars())); + + l_Result = l_Result.Replace(l_Key, l_ReplaceValue); + } + + OBSService.SetRecordFilenameFormat(l_Result); + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_StopRecording + : Interfaces.IAction + { + public override string Description => "Stop recording"; + public override string UIPlaceHolder => "Stop recording"; + public override bool UIPlaceHolderTestButton => true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + protected override void OnUIPlaceholderTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + OBSService.StopRecording(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + OBSService.StopRecording(); + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_StopStreaming + : Interfaces.IAction + { + public override string Description => "Stop streaming"; + public override string UIPlaceHolder => "Stop streaming"; + public override bool UIPlaceHolderTestButton => true; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + protected override void OnUIPlaceholderTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + OBSService.StopStreaming(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + OBSService.StopStreaming(); + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_SwitchPreviewToScene + : Interfaces.IAction + { + private XUIDropdown m_Scene = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Change active preview scene in OBS"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Selected = ""; + var l_Choices = new List() { "None" }; + + if (OBSService.Status == OBSService.EStatus.Connected) + l_Choices.AddRange(OBSService.Scenes.Keys); + else + { + XUIElements = new IXUIElement[] + { + XUIText.Make("OBS is not connected!") + .SetColor(Color.red) + .SetAlign(TMPro.TextAlignmentOptions.Center) + }; + + BuildUIAuto(p_Parent); + return; + } + + for (int l_I = 0; l_I < l_Choices.Count; ++l_I) + { + if (l_Choices[l_I] != Model.SceneName) + continue; + + l_Selected = l_Choices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + XUIText.Make("Dummy event to execute"), + XUIDropdown.Make().SetOptions(l_Choices).SetValue(l_Selected) + .OnValueChanged((_, __) => OnSettingChanged()).Bind(ref m_Scene), + + XUISecondaryButton.Make("Select active scene", OnSelectActiveSceneButton), + XUIPrimaryButton.Make("Test", OnTestButton) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + /// Do not saved if OBS is not connected + if (OBSService.Status != OBSService.EStatus.Connected) + return; + + Model.SceneName = m_Scene.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSelectActiveSceneButton() + => m_Scene.SetValue(OBSService.ActiveScene?.name); + private void OnTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) + l_Scene.SetAsPreview(); + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_SwitchPreviewToScene Scene:{Model.SceneName} not found!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) + l_Scene.SetAsPreview(); + else + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_SwitchPreviewToScene Scene:{Model.SceneName} not found!"); + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_SwitchToScene + : Interfaces.IAction + { + private XUIDropdown m_Scene = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Change active scene in OBS"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Selected = ""; + var l_Choices = new List() { "None" }; + + if (OBSService.Status == OBSService.EStatus.Connected) + l_Choices.AddRange(OBSService.Scenes.Keys); + else + { + XUIElements = new IXUIElement[] + { + XUIText.Make("OBS is not connected!") + .SetColor(Color.red) + .SetAlign(TMPro.TextAlignmentOptions.Center) + }; + + BuildUIAuto(p_Parent); + return; + } + + for (int l_I = 0; l_I < l_Choices.Count; ++l_I) + { + if (l_Choices[l_I] != Model.SceneName) + continue; + + l_Selected = l_Choices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + XUIText.Make("Dummy event to execute"), + XUIDropdown.Make().SetOptions(l_Choices).SetValue(l_Selected) + .OnValueChanged((_, __) => OnSettingChanged()).Bind(ref m_Scene), + + XUISecondaryButton.Make("Select active scene", OnSelectActiveSceneButton), + XUIPrimaryButton.Make("Test", OnTestButton) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + /// Do not saved if OBS is not connected + if (OBSService.Status != OBSService.EStatus.Connected) + return; + + Model.SceneName = m_Scene.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSelectActiveSceneButton() + => m_Scene.SetValue(OBSService.ActiveScene?.name); + private void OnTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) + l_Scene.SwitchTo(); + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_SwitchToScene Scene:{Model.SceneName} not found!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) + l_Scene.SwitchTo(); + else + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_SwitchToScene Scene:{Model.SceneName} not found!"); + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_ToggleStudioMode : Interfaces.IAction + { + private XUIDropdown m_ChangeType = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Enable or disable studio mode"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIText.Make("Change type"), + XUIDropdown.Make().SetOptions(Enums.Toggle.S).SetValue(Enums.Toggle.ToStr(Model.ChangeType)) + .OnValueChanged((_, __) => OnSettingChanged()).Bind(ref m_ChangeType), + + XUIPrimaryButton.Make("Test", OnTestButton) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + => Model.ChangeType = Enums.Toggle.ToEnum(m_ChangeType.Element.GetValue()); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + switch (Model.ChangeType) + { + case Enums.Toggle.E.Toggle: + if (OBSService.IsInStudioMode) + OBSService.DisableStudioMode(); + else + OBSService.EnableStudioMode(); + break; + case Enums.Toggle.E.Enable: + OBSService.EnableStudioMode(); + break; + case Enums.Toggle.E.Disable: + OBSService.DisableStudioMode(); + break; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + switch (Model.ChangeType) + { + case Enums.Toggle.E.Toggle: + if (OBSService.IsInStudioMode) + OBSService.DisableStudioMode(); + else + OBSService.EnableStudioMode(); + break; + case Enums.Toggle.E.Enable: + OBSService.EnableStudioMode(); + break; + case Enums.Toggle.E.Disable: + OBSService.DisableStudioMode(); + break; + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_ToggleSource + : Interfaces.IAction + { + private XUIDropdown m_ChangeType = null; + private XUIDropdown m_Scene = null; + private XUIDropdown m_Source = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Toggle source visibility"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + XUIElements = new IXUIElement[] + { + XUIText.Make("OBS is not connected!") + .SetColor(Color.red) + .SetAlign(TMPro.TextAlignmentOptions.Center) + }; + + BuildUIAuto(p_Parent); + return; + } + + var l_SceneChoices = new List() { "None" }; + var l_SelectedScene = "None"; + + if (OBSService.Status == OBSService.EStatus.Connected) + l_SceneChoices.AddRange(OBSService.Scenes.Keys.ToList()); + + for (int l_I = 0; l_I < l_SceneChoices.Count; ++l_I) + { + if (l_SceneChoices[l_I] != Model.SceneName) + continue; + + l_SelectedScene = l_SceneChoices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + XUIText.Make("Change type").SetColor(Color.yellow), + XUIDropdown.Make() + .SetOptions(Enums.Toggle.S).SetValue(Enums.Toggle.ToStr(Model.ChangeType)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ChangeType), + + XUIText.Make("Scene").SetColor(Color.yellow), + XUIDropdown.Make() + .SetOptions(l_SceneChoices).SetValue(l_SelectedScene).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Scene), + + XUIText.Make("Source").SetColor(Color.yellow), + XUIDropdown.Make() + .OnValueChanged((_, __) => OnSettingChangedSrc()) + .Bind(ref m_Source), + + XUIHLayout.Make( + XUIPrimaryButton.Make("Select active scene", OnSelectActiveSceneButton), + XUIPrimaryButton.Make("Test", OnTestButton) + ) + }; + + BuildUIAuto(p_Parent); + + RebuildSourceList(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + /// Do not saved if OBS is not connected + if (OBSService.Status != OBSService.EStatus.Connected) + return; + + var l_SceneChanged = Model.SceneName != m_Scene.Element.GetValue(); + + Model.ChangeType = Enums.Toggle.ToEnum(m_ChangeType.Element.GetValue()); + Model.SceneName = m_Scene.Element.GetValue(); + + if (l_SceneChanged) + RebuildSourceList(); + } + private void OnSettingChangedSrc() + { + /// Do not saved if OBS is not connected + if (OBSService.Status != OBSService.EStatus.Connected) + return; + + Model.SourceName = m_Source.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void RebuildSourceList() + { + var l_SourceChoices = new List() { "None" }; + var l_SelectedSource = "None"; + + if (OBSService.Status == OBSService.EStatus.Connected) + { + if (!OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSource Scene:{Model.SceneName} not found!"); + return; + } + + for (int l_I = 0;l_I < l_Scene.sources.Count; ++l_I) + { + var l_Source = l_Scene.sources[l_I]; + l_SourceChoices.Add(l_Source.name); + + for (int l_Y = 0; l_Y < l_Source.groupChildren.Count; ++l_Y) + l_SourceChoices.Add(l_Source.groupChildren[l_Y].name); + } + } + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + + for (int l_I = 0; l_I < l_SourceChoices.Count; ++l_I) + { + if (l_SourceChoices[l_I] != Model.SourceName) + continue; + + l_SelectedSource = l_SourceChoices[l_I]; + break; + } + + m_Source.SetOptions(l_SourceChoices).SetValue(l_SelectedSource); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSelectActiveSceneButton() + { + Model.SceneName = OBSService.ActiveScene?.name; + m_Scene.SetValue(OBSService.ActiveScene?.name); + } + private void OnTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + CP_SDK.OBS.Models.Source l_Source = null; + if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene) && (l_Source = l_Scene.GetSourceByName(Model.SourceName)) != null) + l_Source.SetVisible(Model.ChangeType == Enums.Toggle.E.Toggle ? !l_Source.render : (Model.ChangeType == Enums.Toggle.E.Enable ? true : false)); + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSource Scene:{Model.SceneName} not found!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + CP_SDK.OBS.Models.Source l_Source = null; + if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene) && (l_Source = l_Scene.GetSourceByName(Model.SourceName)) != null) + l_Source.SetVisible(Model.ChangeType == Enums.Toggle.E.Toggle ? !l_Source.render : (Model.ChangeType == Enums.Toggle.E.Enable ? true : false)); + else + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSource Scene:{Model.SceneName} not found!"); + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_ToggleSourceAudio + : Interfaces.IAction + { + private XUIDropdown m_ChangeType = null; + private XUIDropdown m_Scene = null; + private XUIDropdown m_Source = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Toggle source audio"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + XUIElements = new IXUIElement[] + { + XUIText.Make("OBS is not connected!") + .SetColor(Color.red) + .SetAlign(TMPro.TextAlignmentOptions.Center) + }; + + BuildUIAuto(p_Parent); + return; + } + + var l_SceneChoices = new List() { "None" }; + var l_SelectedScene = "None"; + + if (OBSService.Status == OBSService.EStatus.Connected) + l_SceneChoices.AddRange(OBSService.Scenes.Keys.ToList()); + + for (int l_I = 0; l_I < l_SceneChoices.Count; ++l_I) + { + if (l_SceneChoices[l_I] != Model.SceneName) + continue; + + l_SelectedScene = l_SceneChoices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + XUIText.Make("Change type").SetColor(Color.yellow), + XUIDropdown.Make() + .SetOptions(Enums.Toggle.S).SetValue(Enums.Toggle.ToStr(Model.ChangeType)).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_ChangeType), + + XUIText.Make("Scene").SetColor(Color.yellow), + XUIDropdown.Make() + .SetOptions(l_SceneChoices).SetValue(l_SelectedScene).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Scene), + + XUIText.Make("Source").SetColor(Color.yellow), + XUIDropdown.Make() + .OnValueChanged((_, __) => OnSettingChangedSrc()) + .Bind(ref m_Source), + + XUIHLayout.Make( + XUIPrimaryButton.Make("Select active scene", OnSelectActiveSceneButton), + XUIPrimaryButton.Make("Test", OnTestButton) + ) + }; + + BuildUIAuto(p_Parent); + + RebuildSourceList(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + /// Do not saved if OBS is not connected + if (OBSService.Status != OBSService.EStatus.Connected) + return; + + var l_SceneChanged = Model.SceneName != m_Scene.Element.GetValue(); + + Model.ChangeType = Enums.Toggle.ToEnum(m_ChangeType.Element.GetValue()); + Model.SceneName = m_Scene.Element.GetValue(); + + if (l_SceneChanged) + RebuildSourceList(); + } + private void OnSettingChangedSrc() + { + /// Do not saved if OBS is not connected + if (OBSService.Status != OBSService.EStatus.Connected) + return; + + Model.SourceName = m_Source.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void RebuildSourceList() + { + var l_SourceChoices = new List() { "None" }; + var l_SelectedSource = "None"; + + if (OBSService.Status == OBSService.EStatus.Connected) + { + if (!OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene)) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSourceAudio Scene:{Model.SceneName} not found!"); + return; + } + + for (int l_I = 0;l_I < l_Scene.sources.Count; ++l_I) + { + var l_Source = l_Scene.sources[l_I]; + l_SourceChoices.Add(l_Source.name); + + for (int l_Y = 0; l_Y < l_Source.groupChildren.Count; ++l_Y) + l_SourceChoices.Add(l_Source.groupChildren[l_Y].name); + } + } + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + + for (int l_I = 0; l_I < l_SourceChoices.Count; ++l_I) + { + if (l_SourceChoices[l_I] != Model.SourceName) + continue; + + l_SelectedSource = l_SourceChoices[l_I]; + break; + } + + m_Source.SetOptions(l_SourceChoices).SetValue(l_SelectedSource); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSelectActiveSceneButton() + { + Model.SceneName = OBSService.ActiveScene?.name; + m_Scene.SetValue(OBSService.ActiveScene?.name); + } + private void OnTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + CP_SDK.OBS.Models.Source l_Source = null; + if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene) && (l_Source = l_Scene.GetSourceByName(Model.SourceName)) != null) + l_Source.SetMuted(Model.ChangeType == Enums.Toggle.E.Toggle ? !l_Source.render : (Model.ChangeType == Enums.Toggle.E.Enable ? true : false)); + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSourceAudio Scene:{Model.SceneName} not found!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + CP_SDK.OBS.Models.Source l_Source = null; + if (OBSService.Scenes.TryGetValue(Model.SceneName, out var l_Scene) && (l_Source = l_Scene.GetSourceByName(Model.SourceName)) != null) + l_Source.SetMuted(Model.ChangeType == Enums.Toggle.E.Toggle ? !l_Source.render : (Model.ChangeType == Enums.Toggle.E.Enable ? true : false)); + else + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage($"ChatIntegrations: Event:{Event.GenericModel.Name} Action:OBS_ToggleSourceAudio Scene:{Model.SceneName} not found!"); + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_Transition + : Interfaces.IAction + { + private XUIToggle m_OverrideDuration = null; + private XUISlider m_Duration = null; + private XUIToggle m_OverrideTransition = null; + private XUIDropdown m_Transition = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Transition between preview to active"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_TransitionChoices = new List() { "None" }; + var l_SelectedTransition = "None"; + + if (OBSService.Status == OBSService.EStatus.Connected) + l_TransitionChoices.AddRange(OBSService.Transitions.ToList()); + else + { + XUIElements = new IXUIElement[] + { + XUIText.Make("OBS is not connected!") + .SetColor(Color.red) + .SetAlign(TMPro.TextAlignmentOptions.Center) + }; + + BuildUIAuto(p_Parent); + return; + } + + for (int l_I = 0; l_I < l_TransitionChoices.Count; ++l_I) + { + if (l_TransitionChoices[l_I] != Model.Transition) + continue; + + l_SelectedTransition = l_TransitionChoices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + XUIText.Make("Override duration"), + XUIToggle.Make() + .SetValue(Model.OverrideDuration).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_OverrideDuration), + + XUIText.Make("Duration"), + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(10000.0f).SetIncrements(1.0f).SetInteger(true) + .SetValue(Model.Duration).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Duration), + + XUIText.Make("Override transition"), + XUIToggle.Make() + .SetValue(Model.OverrideTransition).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_OverrideTransition), + + XUIText.Make("Transition"), + XUIDropdown.Make() + .SetOptions(l_TransitionChoices).SetValue(l_SelectedTransition).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Transition), + + XUIPrimaryButton.Make("Test", OnTestButton) + }; + + BuildUIAuto(p_Parent); + OnSettingChanged(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + /// Do not saved if OBS is not connected + if (OBSService.Status != OBSService.EStatus.Connected) + return; + + Model.OverrideDuration = m_OverrideDuration.Element.GetValue(); + Model.Duration = (int)m_Duration.Element.GetValue(); + Model.OverrideTransition = m_OverrideTransition.Element.GetValue(); + Model.Transition = m_Transition.Element.GetValue(); + + m_Duration.SetInteractable(Model.OverrideDuration); + m_Transition.SetInteractable(Model.OverrideTransition); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnTestButton() + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + return; + } + + if (Model.OverrideDuration && Model.OverrideTransition) + OBSService.PreviewTransitionToScene(Model.Duration, Model.Transition); + else if (Model.OverrideDuration) + OBSService.PreviewTransitionToScene(Model.Duration); + else if (Model.OverrideTransition) + OBSService.PreviewTransitionToScene(-1, Model.Transition); + else + OBSService.PreviewTransitionToScene(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + p_Context.HasActionFailed = true; + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, not connected to OBS!"); + yield break; + } + + + if (Model.OverrideDuration && Model.OverrideTransition) + OBSService.PreviewTransitionToScene(Model.Duration, Model.Transition); + else if (Model.OverrideDuration) + OBSService.PreviewTransitionToScene(Model.Duration); + else if (Model.OverrideTransition) + OBSService.PreviewTransitionToScene(-1, Model.Transition); + else + OBSService.PreviewTransitionToScene(); + + yield return null; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Twitch.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Twitch.cs new file mode 100644 index 0000000..b39a628 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Actions/Twitch.cs @@ -0,0 +1,171 @@ +using CP_SDK.Chat.Interfaces; +using CP_SDK.XUI; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; + +namespace ChatPlexMod_ChatIntegrations.Actions +{ + internal class TwitchRegistration + { + internal static void Register() + { + ChatIntegrations.RegisterActionType("Twitch_AddMarker", () => new Twitch_AddMarker()); + ChatIntegrations.RegisterActionType("Twitch_CreateClip", () => new Twitch_CreateClip()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Twitch_AddMarker + : Interfaces.IAction + { + private XUIText m_MarkerName = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Add a marker on the video"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Marker name", + XUIText.Make(Model.BaseValue) + .SetAlign(TMPro.TextAlignmentOptions.Center) + .SetWrapping(true) + .Bind(ref m_MarkerName) + ), + + XUIHLayout.Make( + XUIPrimaryButton.Make("Set from game", OnSetFromGameButton), + XUIPrimaryButton.Make("Set from chat", OnSetFromChatButton) + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .ForEachDirect (y => { + y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained); + }) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSetFromGameButton() + { + var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.EValueType.String || x.Item1 == Interfaces.EValueType.Integer || x.Item1 == Interfaces.EValueType.Floating).ToArray(); + var l_Keys = new List<(string, Action, string)>(); + + foreach (var l_Var in l_Variables) + l_Keys.Add(("$" + l_Var.Item2, () => View.KeyboardModal_Append("$" + l_Var.Item2), null)); + + View.ShowKeyboardModal(Model.BaseValue, (p_Result) => + { + Model.BaseValue = p_Result; + m_MarkerName.SetText(Model.BaseValue); + }, null, l_Keys); + } + private void OnSetFromChatButton() + { + ChatIntegrations.Instance.OnBroadcasterChatMessage += Instance_OnBroadcasterChatMessage; + + var l_Variables = Event.ProvidedValues.Where(x => x.Item1 == Interfaces.EValueType.String || x.Item1 == Interfaces.EValueType.Integer || x.Item1 == Interfaces.EValueType.Floating).ToArray(); + var l_Message = "Please input a message in chat with your streaming account.\nProvided values:\n"; + l_Message += string.Join(", ", l_Variables.Select(x => "$" + x.Item2).ToArray()); + + View.ShowLoadingModal(l_Message, true, () => + { + ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void Instance_OnBroadcasterChatMessage(IChatMessage p_Message) + { + Model.BaseValue = p_Message.Message; + ChatIntegrations.Instance.OnBroadcasterChatMessage -= Instance_OnBroadcasterChatMessage; + + View.CloseLoadingModal(); + + m_MarkerName.SetText(Model.BaseValue); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + var l_Message = Model.BaseValue; + var l_Variables = p_Context.GetValues(Interfaces.EValueType.String, Interfaces.EValueType.Integer, Interfaces.EValueType.Floating); + + foreach (var l_Var in l_Variables) + { + var l_Key = "$" + l_Var.Item2; + var l_ReplaceValue = l_Var.Item1 == Interfaces.EValueType.String ? "" : "0"; + + if (l_Var.Item1 == Interfaces.EValueType.Integer && p_Context.GetIntegerValue(l_Var.Item2, out var l_IntegerVal)) + l_ReplaceValue = l_IntegerVal.Value.ToString(); + else if (l_Var.Item1 == Interfaces.EValueType.Floating && p_Context.GetFloatingValue(l_Var.Item2, out var l_FloatVal)) + l_ReplaceValue = l_FloatVal.Value.ToString(); + else if (l_Var.Item1 == Interfaces.EValueType.String && p_Context.GetStringValue(l_Var.Item2, out var l_StringVal)) + l_ReplaceValue = l_StringVal; + + l_Message = l_Message.Replace(l_Key, l_ReplaceValue); + } + + var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); + if (l_TwitchService != null) + { + var l_HelixAPI = (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI; + l_HelixAPI.CreateMarker(l_Message, null); + } + + yield return null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Twitch_CreateClip + : Interfaces.IAction + { + public override string Description => $"Create clip, and put the edit URL in {CP_SDK.ChatPlexSDK.ProductName}_TwitchClips.txt"; + public override string UIPlaceHolder => $"Create clip, and put the edit URL in {CP_SDK.ChatPlexSDK.ProductName}_TwitchClips.txt"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override IEnumerator Eval(Models.EventContext p_Context) + { + var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); + if (l_TwitchService != null) + { + var l_HelixAPI = (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI; + l_HelixAPI.CreateClip((p_Status, p_Result) => + { + if (p_Status != CP_SDK.Chat.Services.Twitch.TwitchHelixResult.OK) + return; + + try { System.IO.File.AppendAllLines($"{CP_SDK.ChatPlexSDK.ProductName}_TwitchClips.txt", new List() { p_Result?.edit_url ?? "invalid" }); } + catch { } + }); + } + + yield return null; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/CIConfig.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/CIConfig.cs similarity index 77% rename from Modules/BeatSaberPlus_ChatIntegrations/CIConfig.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/CIConfig.cs index 5af5e34..e569bf9 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/CIConfig.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/CIConfig.cs @@ -1,11 +1,13 @@ using Newtonsoft.Json; +using System.IO; -namespace BeatSaberPlus_ChatIntegrations +namespace ChatPlexMod_ChatIntegrations { internal class CIConfig : CP_SDK.Config.JsonConfig { [JsonProperty] internal bool Enabled = false; - [JsonProperty] internal string DataLocation = "UserData/BeatSaberPlus/ChatIntegrations/"; + [JsonProperty] internal string DataLocation = Path.Combine(CP_SDK.ChatPlexSDK.BasePath, $"UserData/{CP_SDK.ChatPlexSDK.ProductName}/ChatIntegrations/"); + [JsonProperty] internal string LastBackup = ""; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations.cs new file mode 100644 index 0000000..3c36efc --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations.cs @@ -0,0 +1,721 @@ +using CP_SDK.Chat.Interfaces; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; + +namespace ChatPlexMod_ChatIntegrations +{ + /// + /// ChatIntegrations instance + /// + public partial class ChatIntegrations : CP_SDK.ModuleBase + { + public static string s_DATABASE_FILE => Path.Combine(CIConfig.Instance.DataLocation, "Database.json"); + public static string s_EXPORT_PATH => Path.Combine(CIConfig.Instance.DataLocation, "Export/"); + public static string s_IMPORT_PATH => Path.Combine(CIConfig.Instance.DataLocation, "Import/"); + public static string s_EMOTE_RAIN_ASSETS_PATH => Path.Combine(CIConfig.Instance.DataLocation, "Assets/EmoteRain/"); + public static string s_SOUND_CLIPS_ASSETS_PATH => Path.Combine(CIConfig.Instance.DataLocation, "Assets/SoundClips/"); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; + public override string Name => "Chat Integrations"; + public override string Description => "Create cool & tights integration with your chat!"; + public override string DocumentationURL => "https://github.com/hardcpp/BeatSaberPlus/wiki#chat-integrations"; + public override bool UseChatFeatures => true; + public override bool IsEnabled { get => CIConfig.Instance.Enabled; set { CIConfig.Instance.Enabled = value; CIConfig.Instance.Save(); } } + public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private static bool m_RegisteringInternalTypes = false; + + private static List m_RegisteredEventTypes = new List(); + private static List m_RegisteredGlobalConditionsTypes = new List(); + private static List m_RegisteredGlobalActionsTypes = new List(); + + private static Dictionary> m_RegisteredEventFuncs = new Dictionary>(); + private static Dictionary> m_RegisteredGlobalConditionsFuncs = new Dictionary>(); + private static Dictionary> m_RegisteredGlobalActionsFuncs = new Dictionary>(); + private static Dictionary> m_RegisteredTemplates = new Dictionary>(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public static IReadOnlyList RegisteredEventTypes = m_RegisteredEventTypes.AsReadOnly(); + public static IReadOnlyList RegisteredGlobalConditionsTypes = m_RegisteredGlobalConditionsTypes.AsReadOnly(); + public static IReadOnlyList RegisteredGlobalActionsTypes = m_RegisteredGlobalActionsTypes.AsReadOnly(); + + public static IReadOnlyDictionary> RegisteredTemplates = new ReadOnlyDictionary>(m_RegisteredTemplates); + + public static event Action OnModuleEnable; + public static event Action OnModuleDisable; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private UI.SettingsLeftView m_SettingsLeftView = null; + private UI.SettingsMainView m_SettingsMainView = null; + private UI.SettingsRightView m_SettingsRightView = null; + + private bool m_ChatCoreAcquired = false; + private bool m_OBSAcquired = false; + private bool m_VoiceAttackAcquired = false; + + private List m_Events = new List(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public IReadOnlyList Events; + + public Action OnBroadcasterChatMessage = null; + public Action OnVoiceAttackCommandExecuted = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Enable the Module + /// + protected override void OnEnable() + { + if (!Directory.Exists(s_EXPORT_PATH)) Directory.CreateDirectory(s_EXPORT_PATH); + if (!Directory.Exists(s_IMPORT_PATH)) Directory.CreateDirectory(s_IMPORT_PATH); + if (!Directory.Exists(s_EMOTE_RAIN_ASSETS_PATH)) Directory.CreateDirectory(s_EMOTE_RAIN_ASSETS_PATH); + if (!Directory.Exists(s_SOUND_CLIPS_ASSETS_PATH)) Directory.CreateDirectory(s_SOUND_CLIPS_ASSETS_PATH); + + RegisterInternalTypes(); + + /// Create read only list + Events = m_Events.AsReadOnly(); + + /// Load database + if (!LoadDatabase()) + { + /// todo : create basic samples + } + + if (!m_ChatCoreAcquired) + { + /// Init chat core + m_ChatCoreAcquired = true; + CP_SDK.Chat.Service.Acquire(); + + /// Run all services + var l_Multiplexer = CP_SDK.Chat.Service.Multiplexer; + l_Multiplexer.OnJoinChannel += ChatCoreMutiplixer_OnJoinChannel; + l_Multiplexer.OnChannelFollow += ChatCoreMutiplixer_OnChannelFollow; + l_Multiplexer.OnChannelBits += ChatCoreMutiplixer_OnChannelBits; + l_Multiplexer.OnChannelPoints += ChatCoreMutiplixer_OnChannelPoints; + l_Multiplexer.OnChannelRaid += ChatCoreMutiplexer_OnChannelRaid; + l_Multiplexer.OnChannelSubscription += ChatCoreMutiplixer_OnChannelSubscription; + l_Multiplexer.OnTextMessageReceived += ChatCoreMutiplixer_OnTextMessageReceived; + } + + if (!m_OBSAcquired) + { + /// Init OBS + m_OBSAcquired = true; + CP_SDK.OBS.Service.Acquire(); + } + + if (!m_VoiceAttackAcquired) + { + /// Init voice attack + m_VoiceAttackAcquired = true; + CP_SDK.VoiceAttack.Service.Acquire(); + + /// Run all services + CP_SDK.VoiceAttack.Service.OnCommandExecuted += VoiceAttack_OnCommandExecuted; + } + + try { OnModuleEnable?.Invoke(); } + catch (System.Exception l_Exception) + { + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.OnEnable] Error:"); + Logger.Instance.Error(l_Exception); + } + + if (CP_SDK.Chat.Service.Multiplexer.Channels.Count != 0) + { + var l_Channel = CP_SDK.Chat.Service.Multiplexer.Channels.First(); + if (l_Channel.Item2 is CP_SDK.Chat.Models.Twitch.TwitchChannel l_TwitchChannel) + { + m_Events.ForEach(x => + { + /// Re enabled channel points reward on join + if (x.IsEnabled) + x.OnEnable(); + }); + } + } + } + /// + /// Disable the Module + /// + protected override void OnDisable() + { + /// Fake disable events for integrations + m_Events.ForEach(x => + { + if (x.IsEnabled) + x.OnDisable(); + }); + + /// Save events + SaveDatabase(); + + try { OnModuleDisable?.Invoke(); } + catch (System.Exception l_Exception) + { + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.OnDisable] Error:"); + Logger.Instance.Error(l_Exception); + } + + /// Clear database + m_Events.Clear(); + + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsLeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsMainView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsRightView); + + /// Un-init voice attack + if (m_VoiceAttackAcquired) + { + /// Unbind services + CP_SDK.VoiceAttack.Service.OnCommandExecuted -= VoiceAttack_OnCommandExecuted; + + /// Stop all voice attack services + CP_SDK.VoiceAttack.Service.Release(); + m_VoiceAttackAcquired = false; + } + + if (m_OBSAcquired) + { + /// Release OBS service + CP_SDK.OBS.Service.Release(); + m_OBSAcquired = false; + } + + /// Un-init chat core + if (m_ChatCoreAcquired) + { + /// Unbind services + var l_Multiplexer = CP_SDK.Chat.Service.Multiplexer; + l_Multiplexer.OnJoinChannel -= ChatCoreMutiplixer_OnJoinChannel; + l_Multiplexer.OnChannelFollow -= ChatCoreMutiplixer_OnChannelFollow; + l_Multiplexer.OnChannelBits -= ChatCoreMutiplixer_OnChannelBits; + l_Multiplexer.OnChannelPoints -= ChatCoreMutiplixer_OnChannelPoints; + l_Multiplexer.OnChannelRaid -= ChatCoreMutiplexer_OnChannelRaid; + l_Multiplexer.OnChannelSubscription -= ChatCoreMutiplixer_OnChannelSubscription; + l_Multiplexer.OnTextMessageReceived -= ChatCoreMutiplixer_OnTextMessageReceived; + + /// Stop all chat services + CP_SDK.Chat.Service.Release(); + m_ChatCoreAcquired = false; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get Module settings UI + /// + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() + { + if (m_SettingsLeftView == null) m_SettingsLeftView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsRightView == null) m_SettingsRightView = CP_SDK.UI.UISystem.CreateViewController(); + + return (m_SettingsMainView, m_SettingsLeftView, m_SettingsRightView); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create event by type name + /// + /// Type name + /// + internal static Interfaces.IEventBase CreateEvent(string p_Type) + { + if (!m_RegisteredEventFuncs.TryGetValue(p_Type, out var l_Func)) + { + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.CreateEvent] Type \"{p_Type}\" missing"); + return null; + } + + return l_Func(); + } + /// + /// Add an event + /// + /// Event instance + internal void AddEvent(Interfaces.IEventBase p_Event) + { + lock (m_Events) + { + m_Events.Add(p_Event); + m_Events.Sort((x, y) => (x.GetTypeName() + !x.IsEnabled + x.GenericModel.Name).CompareTo(y.GetTypeName() + !y.IsEnabled + y.GenericModel.Name)); + } + + if (p_Event.IsEnabled) + p_Event.OnEnable(); + + SaveDatabase(); + } + /// + /// Add an event + /// + /// Event + /// Is an import + /// Is a clone + internal Interfaces.IEventBase AddEventFromSerialized(JObject p_JSON, bool p_IsImport, bool p_IsClone, out string p_Error) + { + if (!p_JSON.ContainsKey("Type")) + { + p_Error = "Event doesn't have a valid type"; + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.AddEventFromSerialized] Can't find event type\n\"{p_JSON.ToString()}\""); + return null; + } + + var l_EventType = GetPatchedTypeName(p_JSON["Type"].Value()); + p_JSON["Type"] = l_EventType; + + /// Create instance + var l_NewEvent = CreateEvent(l_EventType); + if (l_NewEvent == null) + { + /// Todo backup this event to avoid loss + p_Error = "Event type \"" + l_EventType.Split('.').LastOrDefault() + "\" not found"; + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.AddEventFromSerialized] Missing event type \"{l_EventType}\""); + return null; + } + + /// Unserialize + if (!l_NewEvent.Unserialize(p_JSON, out p_Error)) + { + /// Todo backup this event to avoid loss + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.AddEventFromSerialized] Failed to unserialize event\n\"{p_JSON.ToString()}\" \"{p_Error}\""); + return null; + } + + if (p_IsImport || p_IsClone) + { + l_NewEvent.OnImportOrClone(p_IsImport, p_IsClone); + + if (l_NewEvent.IsEnabled) + l_NewEvent.OnEnable(); + } + + /// Avoid GUID conflict + if (GetEventByGUID(l_NewEvent.GenericModel.GUID) != null) + l_NewEvent.GenericModel.GUID = Guid.NewGuid().ToString(); + + lock (m_Events) + { + m_Events.Add(l_NewEvent); + m_Events.Sort((x, y) => (x.GetTypeName() + !x.IsEnabled + x.GenericModel.Name).CompareTo(y.GetTypeName() + !y.IsEnabled + y.GenericModel.Name)); + } + + p_Error = ""; + return l_NewEvent; + } + /// + /// Get event by name + /// + /// Event name + /// + public Interfaces.IEventBase GetEventByName(string p_Name) + { + lock (m_Events) + { + return m_Events.Where(x => x.GenericModel.Name == p_Name).FirstOrDefault(); + } + } + /// + /// Get event by GUID + /// + /// Event GUID + /// + public Interfaces.IEventBase GetEventByGUID(string p_GUID) + { + lock (m_Events) + { + return m_Events.Where(x => x.GenericModel.GUID == p_GUID).FirstOrDefault(); + } + } + /// + /// Get events by type + /// + /// Type + /// + public List GetEventsByType(Type p_Type) + { + lock (m_Events) + { + return m_Events.Where(x => p_Type == null || p_Type.IsAssignableFrom(x.GetType())).ToList(); + } + } + /// + /// Toggle an event + /// + /// Event instance + internal void ToggleEvent(Interfaces.IEventBase p_Event) + { + p_Event.IsEnabled = !p_Event.IsEnabled; + if (p_Event.IsEnabled) + p_Event.OnEnable(); + else + p_Event.OnDisable(); + + lock (m_Events) + { + m_Events.Sort((x, y) => (x.GetTypeName() + !x.IsEnabled + x.GenericModel.Name).CompareTo(y.GetTypeName() + !y.IsEnabled + y.GenericModel.Name)); + } + } + /// + /// Delete an event + /// + /// Event instance + internal void DeleteEvent(Interfaces.IEventBase p_Event) + { + p_Event.OnDelete(); + + lock (m_Events) + { + m_Events.Remove(p_Event); + } + } + /// + /// Handle events + /// + /// Event context + public void HandleEvents(Models.EventContext p_Context) + { + lock (m_Events) + { + var l_EventCount = m_Events.Count; + for (var l_I = 0; l_I < l_EventCount; ++l_I) + { + var l_Event = m_Events[l_I]; + if (!l_Event.IsEnabled || !l_Event.Handle((Models.EventContext)p_Context.Clone())) + continue; + + l_Event.GenericModel.UsageCount++; + l_Event.GenericModel.LastUsageDate = CP_SDK.Misc.Time.UnixTimeNow(); + } + } + } + /// + /// Execute event + /// + /// Event to execute + /// Execution context + /// + public bool ExecuteEvent(Interfaces.IEventBase p_Event, Models.EventContext p_Context) + { + if (p_Event == null) + return false; + + if (!p_Event.IsEnabled || !p_Event.Handle((Models.EventContext)p_Context.Clone())) + return false; + + p_Event.GenericModel.UsageCount++; + p_Event.GenericModel.LastUsageDate = CP_SDK.Misc.Time.UnixTimeNow(); + + return true; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create event by type name + /// + /// Type name + /// + internal static Interfaces.IConditionBase CreateCondition(string p_Type) + { + if (!m_RegisteredGlobalConditionsFuncs.TryGetValue(p_Type, out var l_Func)) + { + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.CreateCondition] Type \"{p_Type}\" missing"); + return null; + } + + return l_Func(); + } + /// + /// Create event by type name + /// + /// Type name + /// + internal static Interfaces.IActionBase CreateAction(string p_Type) + { + if (!m_RegisteredGlobalActionsFuncs.TryGetValue(p_Type, out var l_Func)) + { + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.CreateAction] Type \"{p_Type}\" missing"); + return null; + } + + return l_Func(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Register internal types + /// + internal static void RegisterInternalTypes() + { + try + { + if (m_RegisteringInternalTypes) + return; + + m_RegisteringInternalTypes = true; + + RegisterEventType("ChatBits" , () => new Events.ChatBits()); + RegisterEventType("ChatCommand" , () => new Events.ChatCommand()); + RegisterEventType("ChatFollow" , () => new Events.ChatFollow()); + RegisterEventType("ChatPointsReward" , () => new Events.ChatPointsReward()); + RegisterEventType("ChatRaid" , () => new Events.ChatRaid()); + RegisterEventType("ChatSubscription" , () => new Events.ChatSubscription()); + RegisterEventType("Dummy" , () => new Events.Dummy()); + RegisterEventType("VoiceAttackCommand", () => new Events.VoiceAttackCommand()); + /// todo GameStart + /// todo GameStop + + Conditions.EventRegistration.Register(); + Conditions.MiscRegistration.Register(); + Conditions.OBSRegistration.Register(); + + Actions.ChatRegistration.Register(); + Actions.EmoteRainRegistration.Register(); + Actions.EventRegistration.Register(); + Actions.MiscRegistration.Register(); + Actions.OBSRegistration.Register(); + Actions.TwitchRegistration.Register(); + + RegisterTemplate("ChatPointReward : Countdown + Emote bomb", () => + { + var l_Event = new Events.ChatPointsReward(); + l_Event.Model.Cooldown = 30; + l_Event.Model.Cost = 100; + l_Event.Model.Name = "Countdown + Emote bomb (Template)"; + l_Event.Model.Title = "Emote bomb (Template)"; + + var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "Explosion in..."; + l_Event.AddOnSuccessAction(l_MessageAction); + l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "3..."; + l_Event.AddOnSuccessAction(l_MessageAction); + + var l_DelayAction = new Actions.Misc_Delay() { Event = l_Event, IsEnabled = true }; + l_DelayAction.Model.Delay = 1; + l_DelayAction.Model.PreventNextActionFailure = false; + l_Event.AddOnSuccessAction(l_DelayAction); + + l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "2..."; + l_Event.AddOnSuccessAction(l_MessageAction); + + l_DelayAction = new Actions.Misc_Delay() { Event = l_Event, IsEnabled = true }; + l_DelayAction.Model.Delay = 1; + l_DelayAction.Model.PreventNextActionFailure = false; + l_Event.AddOnSuccessAction(l_DelayAction); + + l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "1..."; + l_Event.AddOnSuccessAction(l_MessageAction); + + var l_EmoteBombAction = new Actions.EmoteRain_EmoteBombRain() { Event = l_Event, IsEnabled = true }; + l_EmoteBombAction.Model.EmoteKindCount = 25; + l_EmoteBombAction.Model.CountPerEmote = 40; + l_Event.AddOnSuccessAction(l_EmoteBombAction); + + l_DelayAction = new Actions.Misc_Delay() { Event = l_Event, IsEnabled = true }; + l_DelayAction.Model.Delay = 1; + l_DelayAction.Model.PreventNextActionFailure = false; + l_Event.AddOnSuccessAction(l_DelayAction); + + l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "BOOM!"; + l_Event.AddOnSuccessAction(l_MessageAction); + + return l_Event; + }); + RegisterTemplate("ChatBits : Thanks message + emote bomb", () => + { + var l_Event = new Events.ChatBits(); + l_Event.Model.Name = "Thanks message + emote bomb (Template)"; + + var l_CooldownCondition = new Conditions.Misc_Cooldown() { Event = l_Event, IsEnabled = true }; + l_CooldownCondition.Model.PerUser = true; + l_CooldownCondition.Model.NotifyUser = false; + l_CooldownCondition.Model.CooldownTime = 20; + l_Event.Conditions.Add(l_CooldownCondition); + + var l_EmoteBombAction = new Actions.EmoteRain_EmoteBombRain() { Event = l_Event, IsEnabled = true }; + l_EmoteBombAction.Model.EmoteKindCount = 10; + l_EmoteBombAction.Model.CountPerEmote = 10; + l_Event.AddOnSuccessAction(l_EmoteBombAction); + + var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "Thanks $UserName for the $Bits bits!"; + l_Event.AddOnSuccessAction(l_MessageAction); + + return l_Event; + }); + RegisterTemplate("ChatSubscription : Thanks message + emote bomb", () => + { + var l_Event = new Events.ChatSubscription(); + l_Event.Model.Name = "Thanks message + emote bomb (Template)"; + + var l_EmoteBombAction = new Actions.EmoteRain_EmoteBombRain() { Event = l_Event, IsEnabled = true }; + l_EmoteBombAction.Model.EmoteKindCount = 10; + l_EmoteBombAction.Model.CountPerEmote = 10; + l_Event.AddOnSuccessAction(l_EmoteBombAction); + + var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "Thanks $UserName for the $MonthCount of $SubPlan!"; + l_Event.AddOnSuccessAction(l_MessageAction); + + return l_Event; + }); + RegisterTemplate("ChatFollow : Thanks message + emote bomb", () => + { + var l_Event = new Events.ChatFollow(); + l_Event.Model.Name = "Thanks message + emote bomb (Template)"; + + var l_CooldownCondition = new Conditions.Misc_Cooldown() { Event = l_Event, IsEnabled = true }; + l_CooldownCondition.Model.PerUser = true; + l_CooldownCondition.Model.NotifyUser = false; + l_CooldownCondition.Model.CooldownTime = 20 * 60; + l_Event.Conditions.Add(l_CooldownCondition); + + var l_EmoteBombAction = new Actions.EmoteRain_EmoteBombRain() { Event = l_Event, IsEnabled = true }; + l_EmoteBombAction.Model.EmoteKindCount = 5; + l_EmoteBombAction.Model.CountPerEmote = 5; + l_Event.AddOnSuccessAction(l_EmoteBombAction); + + var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "Thanks $UserName for the follow!"; + l_Event.AddOnSuccessAction(l_MessageAction); + + return l_Event; + }); + RegisterTemplate("ChatCommand : Discord command", () => + { + var l_Event = new Events.ChatCommand(); + l_Event.Model.Name = "Discord command (Template)"; + l_Event.Model.Command = "!discord"; + + var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; + l_MessageAction.Model.BaseValue = "@$UserName join my amazing discord at https://discord.gg/K4X94Ea"; + l_Event.AddOnSuccessAction(l_MessageAction); + + return l_Event; + }); + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[ChatPlexMod_ChatIntegrations][ChatIntegrations.RegisterInternalTypes] Error:"); + Logger.Instance.Error(l_Exception); + } + } + /// + /// Register event type + /// + /// Name + /// Create func + /// Should fail silently + public static void RegisterEventType(string p_Name, Func p_Func, bool p_SilentFail = false) + { + RegisterInternalTypes(); + if (m_RegisteredEventTypes.Contains(p_Name)) + { + if (!p_SilentFail) + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.RegisterEventType] Type \"{p_Name}\" already registered"); + return; + } + + m_RegisteredEventTypes.Add(p_Name); + + if (!m_RegisteredEventFuncs.ContainsKey(p_Name)) + m_RegisteredEventFuncs.Add(p_Name, p_Func); + } + /// + /// Register global condition type + /// + /// Name + /// Create func + /// Should fail silently + /// Is non global + public static void RegisterConditionType(string p_Name, Func p_Func, bool p_SilentFail = false, bool p_NonGlobal = false) + { + RegisterInternalTypes(); + if (!p_NonGlobal) + { + if (m_RegisteredGlobalConditionsTypes.Contains(p_Name)) + { + if (!p_SilentFail) + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.RegisterGlobalConditionType] Type \"{p_Name}\" already registered"); + return; + } + + m_RegisteredGlobalConditionsTypes.Add(p_Name); + } + + if (!m_RegisteredGlobalConditionsFuncs.ContainsKey(p_Name)) + m_RegisteredGlobalConditionsFuncs.Add(p_Name, p_Func); + } + /// + /// Register global action type + /// + /// Name + /// Create func + /// Should fail silently + /// Is non global + public static void RegisterActionType(string p_Name, Func p_Func, bool p_SilentFail = false, bool p_NonGlobal = false) + { + RegisterInternalTypes(); + if (!p_NonGlobal) + { + if (m_RegisteredGlobalActionsTypes.Contains(p_Name)) + { + if (!p_SilentFail) + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations][ChatIntegrations.RegisterGlobalActionType] Type \"{p_Name}\" already registered"); + return; + } + + m_RegisteredGlobalActionsTypes.Add(p_Name); + } + + if (!m_RegisteredGlobalActionsFuncs.ContainsKey(p_Name)) + m_RegisteredGlobalActionsFuncs.Add(p_Name, p_Func); + } + /// + /// Register a template + /// + /// Template name + /// Func + public static void RegisterTemplate(string p_Name, Func p_Func) + { + if (m_RegisteredTemplates.ContainsKey(p_Name)) + return; + + m_RegisteredTemplates.Add(p_Name, p_Func); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations_Database.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations_Database.cs similarity index 64% rename from Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations_Database.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations_Database.cs index 162c709..3dc6f10 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations_Database.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations_Database.cs @@ -1,10 +1,9 @@ using Newtonsoft.Json.Linq; -using System; using System.IO; using System.Linq; using System.Text; -namespace BeatSaberPlus_ChatIntegrations +namespace ChatPlexMod_ChatIntegrations { /// /// ChatIntegrations database logic @@ -17,22 +16,6 @@ public partial class ChatIntegrations /// private bool LoadDatabase() { - /// Migrate old database if any - try - { - if (File.Exists(s_OLD_DATABASE_FILE)) - { - if (!File.Exists(s_DATABASE_FILE)) - File.Move(s_OLD_DATABASE_FILE, s_DATABASE_FILE); - else - File.Move(s_OLD_DATABASE_FILE, s_DATABASE_FILE + CP_SDK.Misc.Time.UnixTimeNow()); - } - } - catch - { - - } - /// Create folder if needed if (!Directory.Exists(CIConfig.Instance.DataLocation)) Directory.CreateDirectory(CIConfig.Instance.DataLocation); @@ -40,6 +23,27 @@ private bool LoadDatabase() if (!File.Exists(s_DATABASE_FILE)) return false; + if (CIConfig.Instance.LastBackup != CP_SDK.ChatPlexSDK.ProductVersion) + { + try + { + var l_DatabaseFolder = Path.GetDirectoryName(s_DATABASE_FILE); + var l_BackupFolder = Path.Combine(l_DatabaseFolder, "Backup"); + + if (!Directory.Exists(l_BackupFolder)) + Directory.CreateDirectory(l_BackupFolder); + + File.Copy(s_DATABASE_FILE, Path.Combine(l_BackupFolder, $"backup_pre_{CP_SDK.ChatPlexSDK.ProductVersion}.json")); + } + catch (System.Exception) + { + + } + + CIConfig.Instance.LastBackup = CP_SDK.ChatPlexSDK.ProductVersion; + CIConfig.Instance.Save(); + } + string l_JSONContent = File.ReadAllText(s_DATABASE_FILE, Encoding.Unicode); try { @@ -56,7 +60,7 @@ private bool LoadDatabase() } catch (System.Exception l_Exception) { - Logger.Instance?.Error("[Modules.ChatIntegrations][ChatIntegrations.LoadDatabase] Failed"); + Logger.Instance?.Error("[ChatPlexMod_ChatIntegrations][ChatIntegrations.LoadDatabase] Failed"); Logger.Instance?.Error(l_Exception); try { File.Move(s_DATABASE_FILE, s_DATABASE_FILE + CP_SDK.Misc.Time.UnixTimeNow()); } @@ -92,7 +96,7 @@ internal void SaveDatabase() } catch (System.Exception l_Exception) { - Logger.Instance?.Error("[Modules.ChatIntegrations][ChatIntegrations.SaveDatabase] Failed"); + Logger.Instance?.Error("[ChatPlexMod_ChatIntegrations][ChatIntegrations.SaveDatabase] Failed"); Logger.Instance?.Error(l_Exception); if (!string.IsNullOrEmpty(l_OldJSONContent)) @@ -102,5 +106,22 @@ internal void SaveDatabase() } } } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get patched type name + /// + /// Input value + /// + internal static string GetPatchedTypeName(string p_Input) + { + var l_Index = p_Input.LastIndexOf('.'); + if (l_Index == -1) + return p_Input; + + return p_Input.Substring(l_Index + 1); + } } } diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations_Events.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations_Events.cs similarity index 59% rename from Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations_Events.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations_Events.cs index 128dca3..1dd3161 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/ChatIntegrations_Events.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ChatIntegrations_Events.cs @@ -1,9 +1,6 @@ using CP_SDK.Chat.Interfaces; -using System.Collections; -using System.Threading.Tasks; -using UnityEngine; -namespace BeatSaberPlus_ChatIntegrations +namespace ChatPlexMod_ChatIntegrations { /// /// ChatIntegrations instance @@ -37,7 +34,7 @@ private void ChatCoreMutiplixer_OnJoinChannel(IChatService p_ChatService, IChatC /// Channel instance /// User instance private void ChatCoreMutiplixer_OnChannelFollow(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User) - => HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.ChatFollow, ChatService = p_ChatService, Channel = p_Channel, User = p_User }); + => HandleEvents(new Models.EventContext() { Type = Interfaces.ETriggerType.ChatFollow, ChatService = p_ChatService, Channel = p_Channel, User = p_User }); /// /// On channel bits /// @@ -46,7 +43,7 @@ private void ChatCoreMutiplixer_OnChannelFollow(IChatService p_ChatService, ICha /// User instance /// Used bits private void ChatCoreMutiplixer_OnChannelBits(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User, int p_BitsUsed) - => HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.ChatBits, ChatService = p_ChatService, Channel = p_Channel, User = p_User, BitsEvent = p_BitsUsed }); + => HandleEvents(new Models.EventContext() { Type = Interfaces.ETriggerType.ChatBits, ChatService = p_ChatService, Channel = p_Channel, User = p_User, BitsEvent = p_BitsUsed }); /// /// On channel points /// @@ -58,12 +55,12 @@ private void ChatCoreMutiplixer_OnChannelPoints(IChatService p_ChatService, ICha { try { - HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.ChatPointsReward, ChatService = p_ChatService, Channel = p_Channel, User = p_User, PointsEvent = p_Event }); + HandleEvents(new Models.EventContext() { Type = Interfaces.ETriggerType.ChatPointsReward, ChatService = p_ChatService, Channel = p_Channel, User = p_User, PointsEvent = p_Event }); } catch (System.Exception p_Exception) { - Logger.Instance?.Error("[Modules.ChatIntegrations][ChatIntegration.ChatCoreMutiplixer_OnChannelPoints] Error :"); - Logger.Instance?.Error(p_Exception); + Logger.Instance.Error("[ChatPlexMod_ChatIntegrations][ChatIntegration.ChatCoreMutiplixer_OnChannelPoints] Error :"); + Logger.Instance.Error(p_Exception); } } /// @@ -74,7 +71,7 @@ private void ChatCoreMutiplixer_OnChannelPoints(IChatService p_ChatService, ICha /// User instance /// Event private void ChatCoreMutiplexer_OnChannelRaid(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User, int p_Event) - => HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.ChatRaid, ChatService = p_ChatService, Channel = p_Channel, User = p_User, RaidEvent = p_Event }); + => HandleEvents(new Models.EventContext() { Type = Interfaces.ETriggerType.ChatRaid, ChatService = p_ChatService, Channel = p_Channel, User = p_User, RaidEvent = p_Event }); /// /// On channel subscription /// @@ -83,7 +80,7 @@ private void ChatCoreMutiplexer_OnChannelRaid(IChatService p_ChatService, IChatC /// User instance /// Event private void ChatCoreMutiplixer_OnChannelSubscription(IChatService p_ChatService, IChatChannel p_Channel, IChatUser p_User, IChatSubscriptionEvent p_Event) - => HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.ChatSubscription, ChatService = p_ChatService, Channel = p_Channel, User = p_User, SubscriptionEvent = p_Event }); + => HandleEvents(new Models.EventContext() { Type = Interfaces.ETriggerType.ChatSubscription, ChatService = p_ChatService, Channel = p_Channel, User = p_User, SubscriptionEvent = p_Event }); /// /// On text message received /// @@ -96,59 +93,18 @@ private void ChatCoreMutiplixer_OnTextMessageReceived(IChatService p_ChatService if (p_Message.Sender.IsBroadcaster && OnBroadcasterChatMessage != null) CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => OnBroadcasterChatMessage?.Invoke(p_Message)); else - HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.ChatMessage, ChatService = p_ChatService, Channel = p_Message.Channel, User = p_Message.Sender, Message = p_Message }); + HandleEvents(new Models.EventContext() { Type = Interfaces.ETriggerType.ChatMessage, ChatService = p_ChatService, Channel = p_Message.Channel, User = p_Message.Sender, Message = p_Message }); } catch (System.Exception p_Exception) { - Logger.Instance?.Error("[Modules.ChatIntegrations][ChatIntegration.ChatCoreMutiplixer_OnTextMessageReceived] Error :"); - Logger.Instance?.Error(p_Exception); + Logger.Instance.Error("[ChatPlexMod_ChatIntegrations][ChatIntegration.ChatCoreMutiplixer_OnTextMessageReceived] Error :"); + Logger.Instance.Error(p_Exception); } } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// On level started - /// - /// Level data - private void Game_OnLevelStarted(BeatSaberPlus.SDK.Game.LevelData p_Data) - { - CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.LevelStarted, LevelData = p_Data })); - CP_SDK.Unity.MTCoroutineStarter.Start(Game_FindPauseManager(p_Data)); - } - private IEnumerator Game_FindPauseManager(BeatSaberPlus.SDK.Game.LevelData p_Data) - { - if (p_Data.Type == BeatSaberPlus.SDK.Game.LevelType.Multiplayer) - yield break; - - var l_PauseController = null as PauseController; - yield return new WaitUntil(() => (l_PauseController = GameObject.FindObjectOfType())); - - if (l_PauseController) - { - l_PauseController.didPauseEvent += () => - { - CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.LevelPaused, LevelData = p_Data })); - }; - l_PauseController.didResumeEvent += () => - { - CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.LevelResumed, LevelData = p_Data })); - }; - } - } - /// - /// On level ended - /// - /// Completion data - private void Game_OnLevelEnded(BeatSaberPlus.SDK.Game.LevelCompletionData p_Data) - { - CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.LevelEnded, LevelCompletionData = p_Data })); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - /// /// On VoiceAttack command executed /// @@ -159,8 +115,7 @@ private void VoiceAttack_OnCommandExecuted(string p_GUID, string p_Name) if (OnVoiceAttackCommandExecuted != null) CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => OnVoiceAttackCommandExecuted?.Invoke(p_GUID, p_Name)); else - CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => HandleEvents(new Models.EventContext() { Type = Interfaces.TriggerType.VoiceAttackCommand, VoiceAttackCommandGUID = p_GUID, VoiceAttackCommandName = p_GUID })); + CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => HandleEvents(new Models.EventContext() { Type = Interfaces.ETriggerType.VoiceAttackCommand, VoiceAttackCommandGUID = p_GUID, VoiceAttackCommandName = p_GUID })); } - } } diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Bits.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Bits.cs new file mode 100644 index 0000000..d8c3dbe --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Bits.cs @@ -0,0 +1,62 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Conditions +{ + public class Bits_Amount + : Interfaces.ICondition + { + private XUIDropdown m_Comparison = null; + private XUISlider m_Count = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Add conditions on chat request queue size!"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Comparison", + XUIDropdown.Make() + .SetOptions(Enums.Comparison.S).SetValue(Enums.Comparison.ToStr(Model.Comparison)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Comparison) + ), + + Templates.SettingsHGroup("Count", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(10000.0f).SetIncrements(1.0f).SetInteger(true) + .SetValue(Model.Count).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Count) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Comparison = Enums.Comparison.ToEnum(m_Comparison.Element.GetValue()); + Model.Count = (uint)m_Count.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + if (!p_Context.BitsEvent.HasValue) + return false; + + return Enums.Comparison.Evaluate(Model.Comparison, (uint)p_Context.BitsEvent.Value, Model.Count); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Event.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Event.cs new file mode 100644 index 0000000..c2c7dd9 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Event.cs @@ -0,0 +1,234 @@ +using CP_SDK.XUI; +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Conditions +{ + internal class EventRegistration + { + internal static void Register() + { + ChatIntegrations.RegisterConditionType("Event_AlwaysFail", () => new Event_AlwaysFail()); + ChatIntegrations.RegisterConditionType("Event_Disabled", () => new Event_Disabled()); + ChatIntegrations.RegisterConditionType("Event_Enabled", () => new Event_Enabled()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Event_AlwaysFail + : Interfaces.ICondition + { + public override string Description => "Always fail the event"; + public override string UIPlaceHolder => "Make the event to always fail"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + return false; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Event_Disabled + : Interfaces.ICondition + { + private XUIDropdown m_Event = null; + + private Dictionary m_NameToGUID = new Dictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Ensure that an event is disabled"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + m_NameToGUID.Clear(); + + var l_Events = ChatIntegrations.Instance.GetEventsByType(null); + var l_Choices = new List() { "None" }; + var l_Selected = "None"; + l_Events.Sort((x, y) => (x.GetTypeName() + x.GenericModel.Name).CompareTo(y.GetTypeName() + y.GenericModel.Name)); + + foreach (var l_EventBase in l_Events) + { + var l_Line = BuildLineString(l_EventBase); + l_Choices.Add(l_Line); + m_NameToGUID.Add(l_Line, l_EventBase.GenericModel.GUID); + + if (Model.EventGUID != "" && l_EventBase.GenericModel.GUID == Model.EventGUID) + l_Selected = l_Line; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Event", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_Selected).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Event) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + if (m_Event.Element.GetValue() == "None") + Model.EventGUID = ""; + + if (m_NameToGUID.TryGetValue(m_Event.Element.GetValue(), out var l_SelectedGUID)) + Model.EventGUID = l_SelectedGUID; + else + { + Model.EventGUID = ""; + m_Event.SetValue("None", false); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + var l_SelectedEvent = string.IsNullOrEmpty(Model.EventGUID) ? null : ChatIntegrations.Instance.GetEventByGUID(Model.EventGUID); + if (l_SelectedEvent != null) + return !l_SelectedEvent.IsEnabled; + + return false; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build event line + /// + /// Event to build for + private string BuildLineString(Interfaces.IEventBase p_Event) + { + /// Result line + string l_Text = ""; + l_Text += ""; + + /// Left part + l_Text += "[" + p_Event.GetTypeName() + "] "; + l_Text += p_Event.GenericModel.Name; + l_Text += ""; + + return l_Text; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Event_Enabled + : Interfaces.ICondition + { + private XUIDropdown m_Event = null; + + private Dictionary m_NameToGUID = new Dictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Ensure that an event is enabled"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + m_NameToGUID.Clear(); + + var l_Events = ChatIntegrations.Instance.GetEventsByType(null); + var l_Choices = new List() { "None" }; + var l_Selected = "None"; + l_Events.Sort((x, y) => (x.GetTypeName() + x.GenericModel.Name).CompareTo(y.GetTypeName() + y.GenericModel.Name)); + + foreach (var l_EventBase in l_Events) + { + var l_Line = BuildLineString(l_EventBase); + l_Choices.Add(l_Line); + m_NameToGUID.Add(l_Line, l_EventBase.GenericModel.GUID); + + if (Model.EventGUID != "" && l_EventBase.GenericModel.GUID == Model.EventGUID) + l_Selected = l_Line; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Event", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_Selected).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Event) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + if (m_Event.Element.GetValue() == "None") + Model.EventGUID = ""; + + if (m_NameToGUID.TryGetValue(m_Event.Element.GetValue(), out var l_SelectedGUID)) + Model.EventGUID = l_SelectedGUID; + else + { + Model.EventGUID = ""; + m_Event.SetValue("None", false); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + var l_SelectedEvent = string.IsNullOrEmpty(Model.EventGUID) ? null : ChatIntegrations.Instance.GetEventByGUID(Model.EventGUID); + if (l_SelectedEvent != null) + return l_SelectedEvent.IsEnabled; + + return false; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build event line + /// + /// Event to build for + private string BuildLineString(Interfaces.IEventBase p_Event) + { + /// Result line + string l_Text = ""; + l_Text += ""; + + /// Left part + l_Text += "[" + p_Event.GetTypeName() + "] "; + l_Text += p_Event.GenericModel.Name; + l_Text += ""; + + return l_Text; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Misc.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Misc.cs new file mode 100644 index 0000000..b7a5122 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Misc.cs @@ -0,0 +1,128 @@ +using CP_SDK.XUI; +using System.Collections.Concurrent; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Conditions +{ + internal class MiscRegistration + { + internal static void Register() + { + ChatIntegrations.RegisterConditionType("Misc_Cooldown", () => new Misc_Cooldown()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Misc_Cooldown + : Interfaces.ICondition + { + private XUISlider m_Cooldown = null; + private XUIToggle m_PerUser = null; + private XUIToggle m_NotifyUser = null; + + private long m_LastTime = 0; + private ConcurrentDictionary m_Cooldowns = new ConcurrentDictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Add a cooldown on your event"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Cooldown", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(1200.0f).SetIncrements(1.0f).SetInteger(true) + .SetValue(Model.CooldownTime).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Cooldown) + ), + + Templates.SettingsHGroup("Use a per user cooldown instead of global", + XUIToggle.Make() + .SetValue(Model.PerUser).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_PerUser) + ), + + Templates.SettingsHGroup("Notify user on cooldown", + XUIToggle.Make() + .SetValue(Model.NotifyUser).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_NotifyUser) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.CooldownTime = (uint)m_Cooldown.Element.GetValue(); + Model.PerUser = m_PerUser.Element.GetValue(); + Model.NotifyUser = m_NotifyUser.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + if (Model.PerUser) + { + if (p_Context.User == null) + return true; + + if (m_Cooldowns.TryGetValue(p_Context.User.UserName, out var l_LastTime)) + { + var l_Remaining = (l_LastTime + Model.CooldownTime) - CP_SDK.Misc.Time.UnixTimeNow(); + if (l_Remaining > 0) + { + if (Model.NotifyUser && p_Context.ChatService != null && p_Context.Channel != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, BuildFailedMessage(p_Context.User, l_Remaining)); + return false; + } + + m_Cooldowns.TryUpdate(p_Context.User.UserName, CP_SDK.Misc.Time.UnixTimeNow(), l_LastTime); + } + else + m_Cooldowns.TryAdd(p_Context.User.UserName, CP_SDK.Misc.Time.UnixTimeNow()); + } + else + { + var l_Remaining = (m_LastTime + Model.CooldownTime) - CP_SDK.Misc.Time.UnixTimeNow(); + if (l_Remaining > 0) + { + if (Model.NotifyUser && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, BuildFailedMessage(p_Context.User, l_Remaining)); + return false; + } + + m_LastTime = CP_SDK.Misc.Time.UnixTimeNow(); + } + + return true; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private string BuildFailedMessage(CP_SDK.Chat.Interfaces.IChatUser p_User, long p_Remaining) + { + var l_Minutes = p_Remaining / 60; + var l_Seconds = p_Remaining - (l_Minutes * 60); + + if (l_Minutes != 0) + return $"! @{p_User.DisplayName} command is on cooldown, {l_Minutes}m{l_Seconds}s remaining!"; + + return $"! @{p_User.DisplayName} command is on cooldown, {l_Seconds}s remaining!"; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/OBS.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/OBS.cs new file mode 100644 index 0000000..373e18c --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/OBS.cs @@ -0,0 +1,322 @@ +using CP_SDK.XUI; +using System.Collections.Generic; +using UnityEngine; + +using OBSService = CP_SDK.OBS.Service; + +namespace ChatPlexMod_ChatIntegrations.Conditions +{ + internal class OBSRegistration + { + internal static void Register() + { + ChatIntegrations.RegisterConditionType("OBS_IsConnected", () => new OBS_IsConnected()); + ChatIntegrations.RegisterConditionType("OBS_IsNotConnected", () => new OBS_IsNotConnected()); + ChatIntegrations.RegisterConditionType("OBS_IsStreaming", () => new OBS_IsStreaming()); + ChatIntegrations.RegisterConditionType("OBS_IsNotStreaming", () => new OBS_IsNotStreaming()); + ChatIntegrations.RegisterConditionType("OBS_IsRecording", () => new OBS_IsRecording()); + ChatIntegrations.RegisterConditionType("OBS_IsNotRecording", () => new OBS_IsNotRecording()); + ChatIntegrations.RegisterConditionType("OBS_IsInStudioMode", () => new OBS_IsInStudioMode()); + ChatIntegrations.RegisterConditionType("OBS_IsNotInStudioMode", () => new OBS_IsNotInStudioMode()); + ChatIntegrations.RegisterConditionType("OBS_IsInScene", () => new OBS_IsInScene()); + ChatIntegrations.RegisterConditionType("OBS_IsNotInScene", () => new OBS_IsNotInScene()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsConnected + : Interfaces.ICondition + { + public override string Description => "Is OBS connected?"; + public override string UIPlaceHolder => "Ensure that OBS is connected"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + => OBSService.Status == OBSService.EStatus.Connected; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsNotConnected + : Interfaces.ICondition + { + public override string Description => "Is OBS not connected?"; + public override string UIPlaceHolder => "Ensure that OBS is not connected"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + => OBSService.Status != OBSService.EStatus.Connected; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsStreaming + : Interfaces.ICondition + { + public override string Description => "Is OBS streaming?"; + public override string UIPlaceHolder => "Ensure that OBS is streaming"; + + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + => OBSService.Status == OBSService.EStatus.Connected && OBSService.IsStreaming; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsNotStreaming + : Interfaces.ICondition + { + public override string Description => "Is OBS not streaming?"; + public override string UIPlaceHolder => "Ensure that OBS is not streaming"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + => OBSService.Status == OBSService.EStatus.Connected && !OBSService.IsStreaming; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsRecording + : Interfaces.ICondition + { + public override string Description => "Is OBS recording?"; + public override string UIPlaceHolder => "Ensure that OBS is recording"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + => OBSService.Status == OBSService.EStatus.Connected && OBSService.IsRecording; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsNotRecording + : Interfaces.ICondition + { + public override string Description => "Is OBS not recording?"; + public override string UIPlaceHolder => "Ensure that OBS is not recording"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + => OBSService.Status == OBSService.EStatus.Connected && !OBSService.IsRecording; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsInStudioMode + : Interfaces.ICondition + { + public override string Description => "Is OBS in studio mode?"; + public override string UIPlaceHolder => "Ensure that OBS is in studio mode"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + => OBSService.Status == OBSService.EStatus.Connected && OBSService.IsInStudioMode; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsNotInStudioMode + : Interfaces.ICondition + { + public override string Description => "Is OBS not in studio mode?"; + public override string UIPlaceHolder => "Ensure that OBS is not in studio mode"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + => OBSService.Status == OBSService.EStatus.Connected && !OBSService.IsInStudioMode; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsInScene + : Interfaces.ICondition + { + private XUIDropdown m_Dropdown = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Is OBS in scene"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Choices = new List() { "None" }; + var l_Selected = "None"; + + if (OBSService.Status == OBSService.EStatus.Connected) + l_Choices.AddRange(OBSService.Scenes.Keys); + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Condition failed, not connected to OBS!"); + + for (int l_I = 0; l_I < l_Choices.Count; ++l_I) + { + if (l_Choices[l_I] != Model.SceneName) + continue; + + l_Selected = l_Choices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Scene", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_Selected).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Dropdown) + ), + + XUIPrimaryButton.Make("Select active scene", OnSelectActiveSceneButton) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + if (OBSService.Status != OBSService.EStatus.Connected) + return; + + Model.SceneName = m_Dropdown.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSelectActiveSceneButton() + { + Model.SceneName = OBSService.ActiveScene?.name; + m_Dropdown.SetValue(OBSService.ActiveScene?.name); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Condition failed, not connected to OBS!"); + return false; + } + + return OBSService.ActiveScene?.name == Model.SceneName; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_IsNotInScene + : Interfaces.ICondition + { + private XUIDropdown m_Dropdown = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Is OBS not in scene"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + var l_Choices = new List() { "None" }; + var l_Selected = "None"; + + if (OBSService.Status == OBSService.EStatus.Connected) + l_Choices.AddRange(OBSService.Scenes.Keys); + else + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Condition failed, not connected to OBS!"); + + for (int l_I = 0; l_I < l_Choices.Count; ++l_I) + { + if (l_Choices[l_I] != Model.SceneName) + continue; + + l_Selected = l_Choices[l_I]; + break; + } + + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Scene", + XUIDropdown.Make() + .SetOptions(l_Choices).SetValue(l_Selected).OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Dropdown) + ), + + XUIPrimaryButton.Make("Select active scene", OnSelectActiveSceneButton) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + if (OBSService.Status != OBSService.EStatus.Connected) + return; + + Model.SceneName = m_Dropdown.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSelectActiveSceneButton() + { + Model.SceneName = OBSService.ActiveScene?.name; + m_Dropdown.SetValue(OBSService.ActiveScene?.name); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + if (OBSService.Status != OBSService.EStatus.Connected) + { + CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Condition failed, not connected to OBS!"); + return false; + } + + return !(OBSService.ActiveScene?.name == Model.SceneName); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Subscription.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Subscription.cs new file mode 100644 index 0000000..479403e --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/Subscription.cs @@ -0,0 +1,129 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Conditions +{ + public class Subscription_IsGift + : Interfaces.ICondition + { + public override string Description => "Is a gift subscription event?"; + public override string UIPlaceHolder => "Ensure that this is a subscription gift"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + return p_Context.SubscriptionEvent.IsGift; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Subscription_PlanType + : Interfaces.ICondition + { + private XUIDropdown m_Dropdown = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Put condition on the kind of subscription"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Dummy event to execute", + XUIDropdown.Make() + .SetOptions(Enums.TwitchSubscribtionPlanType.S).SetValue(Enums.TwitchSubscribtionPlanType.ToStr(Model.SubscribtionPlanType)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Dropdown) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.SubscribtionPlanType = Enums.TwitchSubscribtionPlanType.ToEnum(m_Dropdown.Element.GetValue()); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + if (p_Context.SubscriptionEvent != null + && p_Context.SubscriptionEvent.SubPlan.ToLower() == Enums.TwitchSubscribtionPlanType.ToStr(Model.SubscribtionPlanType).ToLower()) + return true; + + return false; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class Subscription_PurchasedMonthCount + : Interfaces.ICondition + { + private XUIDropdown m_Comparison = null; + private XUISlider m_Count = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Check for purchased month count"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Comparison", + XUIDropdown.Make() + .SetOptions(Enums.Comparison.S).SetValue(Enums.Comparison.ToStr(Model.Comparison)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_Comparison) + ), + + Templates.SettingsHGroup("Count", + XUISlider.Make() + .SetMinValue(1.0f).SetMaxValue(100.0f).SetIncrements(1.0f).SetInteger(true) + .SetValue(Model.Count).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Count) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Comparison = Enums.Comparison.ToEnum(m_Comparison.Element.GetValue()); + Model.Count = (uint)m_Count.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + return p_Context.SubscriptionEvent != null && Enums.Comparison.Evaluate(Model.Comparison, (uint)p_Context.SubscriptionEvent.PurchasedMonthCount, Model.Count); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/User.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/User.cs new file mode 100644 index 0000000..6baa7d1 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Conditions/User.cs @@ -0,0 +1,92 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Conditions +{ + public class User_Permissions + : Interfaces.ICondition + { + private XUIToggle m_Subscriber = null; + private XUIToggle m_VIP = null; + private XUIToggle m_Moderator = null; + private XUIToggle m_Notify = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override string Description => "Check user permissions"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + Templates.SettingsHGroup("Is a subscriber?", + XUIToggle.Make() + .SetValue(Model.Subscriber).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Subscriber) + ), + + Templates.SettingsHGroup("Is a VIP?", + XUIToggle.Make() + .SetValue(Model.VIP).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_VIP) + ), + + Templates.SettingsHGroup("Is a moderator?", + XUIToggle.Make() + .SetValue(Model.Moderator).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Moderator) + ), + + Templates.SettingsHGroup("Notify when no power?", + XUIToggle.Make() + .SetValue(Model.NotifyWhenNoPermission).OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_Notify) + ) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private void OnSettingChanged() + { + Model.Subscriber = m_Subscriber.Element.GetValue(); + Model.VIP = m_VIP.Element.GetValue(); + Model.Moderator = m_Moderator.Element.GetValue(); + Model.NotifyWhenNoPermission = m_Notify.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override bool Eval(Models.EventContext p_Context) + { + if (p_Context.User.IsBroadcaster) + return true; + + var l_IsModerator = p_Context.User.IsBroadcaster || p_Context.User.IsModerator; + var l_IsSuscriber = p_Context.User.IsSubscriber; + var l_IsVIP = p_Context.User.IsVip; + + if (Model.Subscriber && l_IsSuscriber) + return true; + + if (Model.VIP && l_IsVIP) + return true; + + if (Model.Moderator && l_IsModerator) + return true; + + if (Model.NotifyWhenNoPermission && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} You can't use this command!"); + + return false; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/ChangeType.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/ChangeType.cs new file mode 100644 index 0000000..a969caa --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/ChangeType.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Enums +{ + public static class ChangeType + { + public enum E + { + Random, + Input, + EventInput + } + + public static List S = new List() + { + "Random", + "Input", + "EventInput" + }; + + public static int ValueCount => S.Count; + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Comparison.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Comparison.cs new file mode 100644 index 0000000..e1a0e12 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Comparison.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Enums +{ + public static class Comparison + { + public enum E + { + Less, + LessOrEqual, + Equal, + GreaterOrEqual, + Greater + } + + public static List S = new List() + { + "Less", + "LessOrEqual", + "Equal", + "GreaterOrEqual", + "Greater" + }; + + public static int ValueCount => S.Count; + + public static bool Evaluate(E p_Comparison, int p_Left, int p_Right) + { + switch (p_Comparison) + { + case E.Less: return p_Left < p_Right; + case E.LessOrEqual: return p_Left <= p_Right; + case E.Equal: return p_Left == p_Right; + case E.GreaterOrEqual: return p_Left >= p_Right; + case E.Greater: return p_Left > p_Right; + } + return false; + } + public static bool Evaluate(E p_Comparison, uint p_Left, uint p_Right) + { + switch (p_Comparison) + { + case E.Less: return p_Left < p_Right; + case E.LessOrEqual: return p_Left <= p_Right; + case E.Equal: return p_Left == p_Right; + case E.GreaterOrEqual: return p_Left >= p_Right; + case E.Greater: return p_Left > p_Right; + } + return false; + } + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/EVisibility.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/EVisibility.cs new file mode 100644 index 0000000..93a294c --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/EVisibility.cs @@ -0,0 +1,8 @@ +namespace ChatPlexMod_ChatIntegrations.Enums +{ + public enum EVisibility + { + Visible, + Hidden + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Toggle.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Toggle.cs new file mode 100644 index 0000000..fd49356 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Toggle.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Enums +{ + public static class Toggle + { + public enum E + { + Toggle = 0, + Enable = 1, + Disable = 2 + } + + public static List S = new List() + { + "Toggle", + "Enable", + "Disable" + }; + + public static int ValueCount => S.Count; + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/TwitchSubscribtionPlanType.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/TwitchSubscribtionPlanType.cs new file mode 100644 index 0000000..02e88ab --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/TwitchSubscribtionPlanType.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Enums +{ + public static class TwitchSubscribtionPlanType + { + public enum E + { + Prime, + Tier1, + Tier2, + Tier3 + } + + public static List S = new List() + { + "Prime", + "Tier1", + "Tier2", + "Tier3" + }; + + public static int ValueCount => S.Count; + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Visibility.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Visibility.cs new file mode 100644 index 0000000..354f85b --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Enums/Visibility.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Enums +{ + public static class Visibility + { + public enum E + { + Visible, + Hidden + } + + public static List S = new List() + { + "Visible", + "Hidden" + }; + + public static int ValueCount => S.Count; + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatBits.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatBits.cs new file mode 100644 index 0000000..46e5dbc --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatBits.cs @@ -0,0 +1,94 @@ +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Events +{ + /// + /// Chat bits event + /// + public class ChatBits : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public ChatBits() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.Integer, "Bits"), + (EValueType.String, "UserName") + }.AsReadOnly(); + + RegisterCustomCondition("Bits_Amount", () => new Conditions.Bits_Amount(), true); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("This event will be triggered whenever someone spend bits your channel!") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.ChatBits || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null || p_Context.BitsEvent == null) + return false; + + return true; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(Models.EventContext p_Context) + { + p_Context.AddValue(EValueType.Integer, "Bits", (Int64?)p_Context.BitsEvent.Value); + p_Context.AddValue(EValueType.String, "UserName", p_Context.User.DisplayName); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatCommand.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatCommand.cs new file mode 100644 index 0000000..d1667ca --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatCommand.cs @@ -0,0 +1,159 @@ +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Events +{ + /// + /// Chat command event + /// + public class ChatCommand : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public ChatCommand() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.Emotes, "MessageEmotes"), + (EValueType.Integer, "MessageNumber"), + (EValueType.String, "MessageContent"), + (EValueType.String, "UserName") + }.AsReadOnly(); + + RegisterCustomCondition("User_Permissions", () => new Conditions.User_Permissions(), true); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUIText m_CurrentCommandText = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make( "This event will be triggered when someone uses the command which you've configured\n" + + "You can change the command by clicking the rebind button bellow") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ) + .SetBackground(true), + + XUIText.Make("Current command : ") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + .Bind(ref m_CurrentCommandText), + + XUIPrimaryButton.Make("Rebind", OnRebindButton) + }; + + BuildUIAuto(p_Parent); + + UpdateUI(); + } + /// + /// Update UI component values + /// + private void UpdateUI() => m_CurrentCommandText.Element.SetText("Current command: " + Model.Command); + /// + /// Rebind button pressed + /// + private void OnRebindButton() + { + UI.SettingsMainView.Instance.ShowKeyboardModal(Model.Command, (p_Result) => + { + if (p_Result.Length > 0 && p_Result[0] != '!') + p_Result = "!" + p_Result; + + var l_FirstSpaceIndex = p_Result.IndexOf(' '); + + Model.Command = (l_FirstSpaceIndex != -1 ? p_Result.Substring(0, l_FirstSpaceIndex) : p_Result).ToLower(); + + /// Update UI + UpdateUI(); + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.ChatMessage || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null || p_Context.Message == null) + return false; + + /// Look for command sign + if (p_Context.Message.Message.Length < 2 || p_Context.Message.Message[0] != '!') + return false; + + var l_FirstSpaceIndex = p_Context.Message.Message.IndexOf(' '); + var l_Command = (l_FirstSpaceIndex != -1 ? p_Context.Message.Message.Substring(0, l_FirstSpaceIndex) : p_Context.Message.Message).ToLower(); + + return l_Command == Model.Command; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(Models.EventContext p_Context) + { + var l_FirstSpaceIndex = p_Context.Message.Message.IndexOf(' '); + + var l_Emotes = p_Context.Message.Emotes; + var l_Number = (Int64?)null; + var l_Content = null as string; + + if (l_FirstSpaceIndex != -1) + { + var l_Remaining = p_Context.Message.Message.Substring(l_FirstSpaceIndex + 1); + + if (Int64.TryParse(Regex.Match(l_Remaining, @"-?\d+").Value, out var l_NumberVal)) + l_Number = (Int64?)l_NumberVal; + + l_Content = l_Remaining; + } + + p_Context.AddValue(EValueType.String, "UserName", p_Context.User.DisplayName); + p_Context.AddValue(EValueType.Emotes, "MessageEmotes", l_Emotes); + p_Context.AddValue(EValueType.Integer, "MessageNumber", l_Number); + p_Context.AddValue(EValueType.String, "MessageContent", l_Content); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatFollow.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatFollow.cs new file mode 100644 index 0000000..a527e1c --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatFollow.cs @@ -0,0 +1,89 @@ +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Events +{ + /// + /// Chat follow event + /// + public class ChatFollow : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public ChatFollow() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.String, "UserName") + }.AsReadOnly(); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("This event will be triggered whenever someone follows your channel!") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.ChatFollow || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null) + return false; + + return true; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(Models.EventContext p_Context) + { + p_Context.AddValue(EValueType.String, "UserName", p_Context.User.DisplayName); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatPointsReward.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatPointsReward.cs new file mode 100644 index 0000000..5e1d7ee --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatPointsReward.cs @@ -0,0 +1,471 @@ +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using TMPro; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Events +{ + /// + /// Chat command event + /// + public class ChatPointsReward : Interfaces.IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public ChatPointsReward() + { + /// Build provided values list + ProvidedValues = new List<(Interfaces.EValueType, string)>() + { + (Interfaces.EValueType.Integer, "MessageNumber"), + (Interfaces.EValueType.String, "MessageContent"), + (Interfaces.EValueType.String, "UserName") + }.AsReadOnly(); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUITextInput m_TitleInput; + private XUITextInput m_PromptInput; + private XUITextInput m_CostInput; + + private XUIToggle m_RequireInput = XUIToggle.Make(); + private XUISlider m_Cooldown = XUISlider.Make().SetInteger(true).SetMinValue(0f).SetMaxValue(1200f).SetIncrements(1.0f); + private XUIToggle m_AutoFullfillRefund = XUIToggle.Make(); + private XUISlider m_MaxPerStream = XUISlider.Make().SetInteger(true).SetMinValue(0f).SetMaxValue(100f).SetIncrements(1.0f); + private XUISlider m_MaxPerUserPerStream = XUISlider.Make().SetInteger(true).SetMinValue(0f).SetMaxValue(100f).SetIncrements(1.0f); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + Action l_ControlsTextStyle = (x) => x.SetStyle(FontStyles.Bold).SetColor(Color.yellow); + Action l_ControlsTextStyleC = (x) => x.SetStyle(FontStyles.Bold).SetColor(Color.yellow).SetAlign(TextAlignmentOptions.Center); + + XUIElements = new IXUIElement[] + { + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Title:").OnReady(l_ControlsTextStyle), + XUIText.Make("Prompt:").OnReady(l_ControlsTextStyle), + XUIText.Make("Cost:").OnReady(l_ControlsTextStyle) + ) + .SetMinWidth(20f).SetWidth(20f) + .OnReady(x => x.VLayoutGroup.childAlignment = TextAnchor.UpperLeft), + + XUIVLayout.Make( + XUITextInput.Make("Title...").SetValue(Model.Title).Bind(ref m_TitleInput), + XUITextInput.Make("Prompt...").SetValue(Model.Prompt).Bind(ref m_PromptInput), + XUITextInput.Make("Cost...").SetValue(Model.Cost.ToString()).Bind(ref m_CostInput) + ) + .SetMinWidth(110f).SetWidth(110f) + ) + .SetMinWidth(130f).SetWidth(130f) + .SetSpacing(0).SetPadding(0) + .SetBackground(true) + .ForEachDirect(x => x.SetSpacing(0)), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Require input").OnReady(l_ControlsTextStyleC), + m_RequireInput + ), + XUIVLayout.Make( + XUIText.Make("Redeem Cooldown").OnReady(l_ControlsTextStyleC), + m_Cooldown + ), + XUIVLayout.Make( + XUIText.Make("Auto fullfill/refund").OnReady(l_ControlsTextStyleC), + m_AutoFullfillRefund + ) + ) + .SetMinWidth(130f).SetWidth(130f) + .SetPadding(0) + .ForEachDirect((x) => { x.SetMinWidth(42f).SetWidth(42f).SetSpacing(0f); }), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Max per stream").OnReady(l_ControlsTextStyleC), + m_MaxPerStream + ), + XUIVLayout.Make( + XUIVSpacer.Make(4f), + XUIPrimaryButton.Make("Update reward", CreateOrUpdateReward) + ), + XUIVLayout.Make( + XUIText.Make("Max per user per stream").OnReady(l_ControlsTextStyleC), + m_MaxPerUserPerStream + ) + ) + .SetMinWidth(130f).SetWidth(130f) + .SetPadding(0) + .ForEachDirect((x) => { x.SetMinWidth(42f).SetWidth(42f).SetSpacing(0f); }), + + XUIVLayout.Make( + XUIText.Make("The mod will automatically create and update the reward on your twitch account\n" + + "The reward get disabled when you quit the game, and re-enabled when you start it") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true), + }; + + BuildUIAuto(p_Parent); + + m_MaxPerStream.SetFormatter((x) => ((int)x) == 0 ? "Unlimited" : ((int)x).ToString()); + m_MaxPerUserPerStream.SetFormatter((x) => ((int)x) == 0 ? "Unlimited" : ((int)x).ToString()); + m_Cooldown.SetFormatter((x) => { + int l_IntValue = (int)x; + if (l_IntValue == 0) return "Unlimited"; + + int l_Minutes = l_IntValue / 60; + int l_Seconds = l_IntValue - (l_Minutes * 60); + + string l_Result = (l_Minutes != 0 ? l_Minutes : l_Seconds).ToString(); + + if (l_Minutes != 0) return l_Result + "m " + l_Seconds + "s"; + return l_Result + "s"; + }); + + m_RequireInput. SetValue(Model.RequireInput); + m_Cooldown. SetValue(Model.Cooldown); + m_AutoFullfillRefund. SetValue(Model.AutoFullfillRefund); + m_MaxPerStream. SetValue(Model.MaxPerStream); + m_MaxPerUserPerStream. SetValue(Model.MaxPerUserPerStream); + + m_TitleInput. OnValueChanged((x) => Model.Title = x.Length > 45 ? x.Substring(0, 45) : x); + m_PromptInput. OnValueChanged((x) => Model.Prompt = x.Length > 45 ? x.Substring(0, 45) : x); + m_CostInput. OnValueChanged((x) => + { + int l_NewValue = 0; + if (!int.TryParse(x, out l_NewValue)) + l_NewValue = Model.Cost; + else + l_NewValue = Mathf.Clamp(l_NewValue, 0, 10000000); + + Model.Cost = l_NewValue; + }); + m_RequireInput. OnValueChanged((x) => Model.RequireInput = x); + m_Cooldown. OnValueChanged((x) => Model.Cooldown = (int)x); + m_AutoFullfillRefund. OnValueChanged((x) => Model.AutoFullfillRefund = x); + m_MaxPerStream. OnValueChanged((x) => Model.MaxPerStream = (int)x); + m_MaxPerUserPerStream. OnValueChanged((x) => Model.MaxPerUserPerStream = (int)x); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On import or clone + /// + /// Is an import + /// Is a clone + public override void OnImportOrClone(bool p_IsImport, bool p_IsClone) + { + base.OnImportOrClone(p_IsImport, p_IsClone); + + if (p_IsImport) Model.Title += " (Import)"; + if (p_IsClone) Model.Title += " (Clone)"; + + Model.RewardID = ""; + } + /// + /// When the event is enabled + /// + public override sealed void OnEnable() => CreateOrUpdateReward(); + /// + /// When the event is successful + /// + /// Event context + public override void OnSuccess(Models.EventContext p_Context) + { + if (!Model.AutoFullfillRefund) + return; + + var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); + var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; + + if (l_TwitchHelix != null) + { + var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id={l_TwitchHelix.BroadcasterID}&reward_id={Model.RewardID}&id={p_Context.PointsEvent.TransactionID}"; + var l_Content = new JObject() + { + ["status"] = "FULFILLED", + }; + var l_ContentStr = new StringContent(l_Content.ToString(), Encoding.UTF8); + + l_TwitchHelix.WebClient.PatchAsync(l_URL, l_ContentStr, "application/json", CancellationToken.None, null, true); + } + } + /// + /// When the event failed + /// + /// Event context + public override sealed void OnEventFailed(Models.EventContext p_Context) + { + if (!Model.AutoFullfillRefund) + return; + + var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); + var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; + + if (l_TwitchHelix != null) + { + var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id={l_TwitchHelix.BroadcasterID}&reward_id={Model.RewardID}&id={p_Context.PointsEvent.TransactionID}"; + var l_Content = new JObject() + { + ["status"] = "CANCELED", + }; + var l_ContentStr = new StringContent(l_Content.ToString(), Encoding.UTF8); + + l_TwitchHelix.WebClient.PatchAsync(l_URL, l_ContentStr, "application/json", CancellationToken.None, (x) => + { + if (p_Context.ChatService != null && p_Context.Channel != null) + p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.UserName} Event failed, your points were refunded!"); + }, true); + } + } + /// + /// When the event is disabled + /// + public override sealed void OnDisable() + { + if (CP_SDK.Chat.Service.Multiplexer.Channels.Count == 0) + return; + + if (!string.IsNullOrEmpty(Model.RewardID)) + { + var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); + var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; + + if (l_TwitchHelix != null) + { + var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={l_TwitchHelix.BroadcasterID}&id={Model.RewardID}"; + var l_Content = new JObject() + { + ["is_enabled"] = false + }; + var l_ContentStr = new StringContent(l_Content.ToString(), Encoding.UTF8); + + l_TwitchHelix.WebClientEx.PatchAsync(l_URL, l_ContentStr, "application/json", CancellationToken.None, null, true); + } + } + } + /// + /// When the event is deleted + /// + public override sealed void OnDelete() => DeleteReward(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != Interfaces.ETriggerType.ChatPointsReward || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null || p_Context.PointsEvent == null) + return false; + + return p_Context.PointsEvent.RewardID == Model.RewardID; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(Models.EventContext p_Context) + { + p_Context.AddValue(Interfaces.EValueType.String, "UserName", (string)p_Context.User.DisplayName); + p_Context.AddValue(Interfaces.EValueType.String, "MessageContent", (string)p_Context.PointsEvent.UserInput); + + if (!string.IsNullOrEmpty(p_Context.PointsEvent.UserInput) && Int64.TryParse(Regex.Match(p_Context.PointsEvent.UserInput, @"-?\d+").Value, out var l_Number)) + p_Context.AddValue(Interfaces.EValueType.Integer, "MessageNumber", (Int64?)l_Number); + else + p_Context.AddValue(Interfaces.EValueType.Integer, "MessageNumber", (Int64?)null); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create or update the reward on twitch + /// + private void CreateOrUpdateReward() + { + if (UI.SettingsMainView.Instance != null) + UI.SettingsMainView.Instance.ShowLoadingModal(); + + if (string.IsNullOrEmpty(Model.RewardID)) + CreateOrUpdateReward_Callback(null); + else + { + var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); + var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; + var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={l_TwitchHelix.BroadcasterID}&id={Model.RewardID}"; + + if (l_TwitchHelix != null) + l_TwitchHelix.WebClient.GetAsync(l_URL, CancellationToken.None, CreateOrUpdateReward_Callback, true); + } + } + /// + /// Create or update the reward on twitch + /// + private void CreateOrUpdateReward_Callback(CP_SDK.Network.WebResponse p_GetReply) + { + var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); + var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; + var l_ShouldCreate = p_GetReply == null || !p_GetReply.IsSuccessStatusCode; + var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={l_TwitchHelix.BroadcasterID}"; + + if (l_ShouldCreate) + { + Model.RewardID = string.Empty; + ChatIntegrations.Instance?.SaveDatabase(); + } + + if (!l_ShouldCreate) + l_URL += $"&id={Model.RewardID}"; + + var l_Content = new JObject() + { + ["title"] = Model.Title, + ["is_user_input_required"] = Model.RequireInput, + ["prompt"] = Model.Prompt, + ["cost"] = Mathf.Max(1, Model.Cost), + ["is_enabled"] = Model.Enabled, + ["is_max_per_stream_enabled"] = Model.MaxPerStream > 0, + ["max_per_stream"] = Mathf.Max(1, Model.MaxPerStream), + ["is_max_per_user_per_stream_enabled"] = Model.MaxPerUserPerStream > 0, + ["max_per_user_per_stream"] = Mathf.Max(1, Model.MaxPerUserPerStream), + ["is_global_cooldown_enabled"] = Model.Cooldown > 0, + ["global_cooldown_seconds"] = Mathf.Max(1, Model.Cooldown) + }; + + var l_ContentStr = new StringContent(l_Content.ToString(), Encoding.UTF8); + Action l_Callback = (CP_SDK.Network.WebResponse p_SecondReply) => + { + if (p_SecondReply != null) + { + if (p_SecondReply.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + if (UI.SettingsMainView.Instance != null) + { + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + if (p_SecondReply.BodyString.Contains("CREATE_CUSTOM_REWARD_DUPLICATE_REWARD")) + UI.SettingsMainView.Instance.ShowMessageModal("Twitch error,\nA reward with the same name already exist on your channel\nPlease delete on twitch.tv any conflicting reward"); + else + UI.SettingsMainView.Instance.ShowMessageModal("Twitch error,\n" + p_SecondReply.BodyString); + + UI.SettingsMainView.Instance.CloseLoadingModal(); + }); + } + return; + } + + if (l_ShouldCreate) + { + var l_Response = JObject.Parse(p_SecondReply.BodyString); + if (l_Response == null + || !l_Response.ContainsKey("data") + || l_Response["data"].Type != JTokenType.Array + || (l_Response["data"] as JArray).Count != 1 + || !((l_Response["data"] as JArray)[0] as JObject).ContainsKey("id") + ) + { + Logger.Instance.Error("[ChatIntegration][ChatPointReward.CreateOrUpdateReward] Error:"); + Logger.Instance.Error(p_SecondReply.BodyString != null ? p_SecondReply.BodyString : "empty response"); + + if (UI.SettingsMainView.Instance != null) + { + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + UI.SettingsMainView.Instance.ShowMessageModal("Internal error,\nplease contact BS+ support!"); + UI.SettingsMainView.Instance.CloseLoadingModal(); + }); + } + return; + } + + Model.RewardID = l_Response["data"][0]["id"].Value(); + ChatIntegrations.Instance?.SaveDatabase(); + } + + if (UI.SettingsMainView.Instance != null) + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => UI.SettingsMainView.Instance.CloseLoadingModal()); + } + else + { + Logger.Instance.Error("[ChatIntegration][ChatPointReward.CreateOrUpdateReward] Error 2:"); + Logger.Instance.Error("empty response"); + + if (UI.SettingsMainView.Instance != null) + { + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + UI.SettingsMainView.Instance.ShowMessageModal("Internal error,\nplease contact BS+ support!"); + UI.SettingsMainView.Instance.CloseLoadingModal(); + }); + } + } + }; + + if (l_ShouldCreate) + l_TwitchHelix.WebClient.PostAsync(l_URL, l_ContentStr, "application/json", CancellationToken.None, l_Callback, true); + else + l_TwitchHelix.WebClient.PatchAsync(l_URL, l_ContentStr, "application/json", CancellationToken.None, l_Callback, true); + } + /// + /// Delete the reward on twitch + /// + private void DeleteReward() + { + if (string.IsNullOrEmpty(Model.RewardID)) + return; + + var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); + var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; + + if (l_TwitchHelix != null) + { + var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={l_TwitchHelix.BroadcasterID}&id={Model.RewardID}"; + + l_TwitchHelix.WebClient.DeleteAsync(l_URL, CancellationToken.None, null, true); + } + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatRaid.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatRaid.cs new file mode 100644 index 0000000..a9712b2 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatRaid.cs @@ -0,0 +1,92 @@ +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Events +{ + /// + /// Chat command event + /// + public class ChatRaid : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public ChatRaid() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.Integer, "RaiderCount"), + (EValueType.String, "UserName") + }.AsReadOnly(); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("This event will be triggered whenever someone raid your channel!") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.ChatRaid || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null || p_Context.RaidEvent == null) + return false; + + return true; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(Models.EventContext p_Context) + { + p_Context.AddValue(EValueType.Integer, "RaiderCount", (Int64?)p_Context.RaidEvent.Value); + p_Context.AddValue(EValueType.String, "UserName", p_Context.User.DisplayName); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatSubscription.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatSubscription.cs new file mode 100644 index 0000000..89d7bcf --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/ChatSubscription.cs @@ -0,0 +1,100 @@ +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Events +{ + /// + /// Chat subscription event + /// + public class ChatSubscription : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public ChatSubscription() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.String, "UserName"), + (EValueType.String, "SubPlan"), + (EValueType.String, "RecipientName"), + (EValueType.Integer, "MonthCount"), + }.AsReadOnly(); + + RegisterCustomCondition("Subscription_IsGift", () => new Conditions.Subscription_IsGift(), true); + RegisterCustomCondition("Subscription_PurchasedMonthCount", () => new Conditions.Subscription_PurchasedMonthCount(), true); + RegisterCustomCondition("Subscription_PlanType", () => new Conditions.Subscription_PlanType(), true); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("This event will be triggered whenever someone subscribe or subgift to your channel!") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true) + }; + + BuildUIAuto(p_Parent); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.ChatSubscription || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null || p_Context.SubscriptionEvent == null) + return false; + + return true; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(Models.EventContext p_Context) + { + p_Context.AddValue(EValueType.String, "UserName", p_Context.User.DisplayName); + p_Context.AddValue(EValueType.String, "SubPlan", p_Context.SubscriptionEvent.SubPlan); + p_Context.AddValue(EValueType.String, "RecipientName", p_Context.SubscriptionEvent.RecipientDisplayName ?? ""); + p_Context.AddValue(EValueType.Integer, "MonthCount", (Int64?)p_Context.SubscriptionEvent.PurchasedMonthCount); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/Dummy.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/Dummy.cs new file mode 100644 index 0000000..bf0bc54 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/Dummy.cs @@ -0,0 +1,91 @@ +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Events +{ + /// + /// Dummy event + /// + public class Dummy : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public Dummy() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + + }.AsReadOnly(); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("Dummy event that can get triggered by other events!") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ).SetBackground(true), + + XUIPrimaryButton.Make("Execute", OnExecutePressed) + }; + + BuildUIAuto(p_Parent); + } + /// + /// Execute button pressed + /// + private void OnExecutePressed() + { + ChatIntegrations.Instance.ExecuteEvent(this, new Models.EventContext() { Type = ETriggerType.Dummy }); + UI.SettingsMainView.Instance.ShowMessageModal("Ok!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.Dummy) + return false; + + return true; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/VoiceAttackCommand.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/VoiceAttackCommand.cs new file mode 100644 index 0000000..599acec --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Events/VoiceAttackCommand.cs @@ -0,0 +1,136 @@ +using ChatPlexMod_ChatIntegrations.Interfaces; +using CP_SDK.XUI; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Events +{ + /// + /// VoiceAttack command event + /// + public class VoiceAttackCommand : IEvent + { + public override IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public override IReadOnlyList AvailableConditions { get; protected set; } + public override IReadOnlyList AvailableActions { get; protected set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public VoiceAttackCommand() + { + /// Build provided values list + ProvidedValues = new List<(EValueType, string)>() + { + (EValueType.String, "CommandGUID"), + (EValueType.String, "CommandName") + }.AsReadOnly(); + + /// Build possible list + AvailableConditions = new List() + .Union(ChatIntegrations.RegisteredGlobalConditionsTypes) + .Union(GetCustomConditionTypes()) + .Distinct().ToList().AsReadOnly(); + + /// Build possible list + AvailableActions = new List() + .Union(ChatIntegrations.RegisteredGlobalActionsTypes) + .Union(GetCustomActionTypes()) + .Distinct().ToList().AsReadOnly(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUIText m_CurrentCommandText = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override sealed void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] + { + XUIVLayout.Make( + XUIText.Make("This event will get trigerred when you trigger the VoiceAttack command you did configured") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + ) + .SetBackground(true), + + XUIText.Make("Current command : ") + .SetAlign(TMPro.TextAlignmentOptions.Midline) + .Bind(ref m_CurrentCommandText), + + XUIPrimaryButton.Make("Rebind", OnRebindButton) + }; + + BuildUIAuto(p_Parent); + + UpdateUI(); + } + /// + /// Update UI component values + /// + private void UpdateUI() => m_CurrentCommandText.Element.SetText("Current command: " + Model.CommandName); + /// + /// Rebind button pressed + /// + private void OnRebindButton() + { + ChatIntegrations.Instance.OnVoiceAttackCommandExecuted += VoiceAttack_OnCommandExecuted; + View.ShowLoadingModal("Please trigger any VoiceAttack command", true, () => + { + ChatIntegrations.Instance.OnVoiceAttackCommandExecuted -= VoiceAttack_OnCommandExecuted; + }); + } + /// + /// On VoiceAttack command executed + /// + /// Command GUID + /// Command Name + private void VoiceAttack_OnCommandExecuted(string p_GUID, string p_Name) + { + Model.CommandGUID = p_GUID; + Model.CommandName = p_Name; + + ChatIntegrations.Instance.OnVoiceAttackCommandExecuted -= VoiceAttack_OnCommandExecuted; + + View.CloseLoadingModal(); + + UpdateUI(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected override sealed bool CanBeExecuted(Models.EventContext p_Context) + { + /// Ensure that we have all data + if (p_Context.Type != ETriggerType.VoiceAttackCommand || p_Context.VoiceAttackCommandGUID == null || p_Context.VoiceAttackCommandName == null) + return false; + + return p_Context.VoiceAttackCommandGUID == Model.CommandGUID; + } + /// + /// Build provided value dictionary + /// + /// Event context + protected override sealed void BuildProvidedValues(Models.EventContext p_Context) + { + p_Context.AddValue(EValueType.String, "CommandGUID", p_Context.VoiceAttackCommandGUID); + p_Context.AddValue(EValueType.String, "CommandName", p_Context.VoiceAttackCommandName); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/TriggerType.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/ETriggerType.cs similarity index 80% rename from Modules/BeatSaberPlus_ChatIntegrations/Interfaces/TriggerType.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/ETriggerType.cs index 5a7e417..7c54c6f 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/TriggerType.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/ETriggerType.cs @@ -1,9 +1,9 @@ -namespace BeatSaberPlus_ChatIntegrations.Interfaces +namespace ChatPlexMod_ChatIntegrations.Interfaces { /// /// Trigger type /// - public enum TriggerType : uint + public enum ETriggerType : uint { None, Dummy, diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/ValueType.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/EValueType.cs similarity index 68% rename from Modules/BeatSaberPlus_ChatIntegrations/Interfaces/ValueType.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/EValueType.cs index 996c29f..2f4a514 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/ValueType.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/EValueType.cs @@ -1,11 +1,11 @@ using System; -namespace BeatSaberPlus_ChatIntegrations.Interfaces +namespace ChatPlexMod_ChatIntegrations.Interfaces { /// /// Value type /// - public enum IValueType + public enum EValueType { Integer, Floating, diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IAction.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IAction.cs new file mode 100644 index 0000000..34f3613 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IAction.cs @@ -0,0 +1,96 @@ +using CP_SDK.XUI; +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Interfaces +{ + /// + /// IAction generic class + /// + public abstract class IAction : IActionBase + where t_Action : IAction, new() + where t_Model : Models.Action, new() + { + public override bool IsEnabled { get => Model.Enabled; set { Model.Enabled = value; } } + public virtual string UIPlaceHolder => "No available settings..."; + public virtual bool UIPlaceHolderTestButton => false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public t_Model Model { get; protected set; } = new t_Model(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get type name + /// + /// + public override string GetTypeName() => typeof(t_Action).Name; + /// + /// Serialize + /// + /// + public override JObject Serialize() + { + Model.Type = GetTypeName(); + return JObject.FromObject(Model); + } + /// + /// Unserialize + /// + /// + public override bool Unserialize(JObject p_Serialized) + { + if (!p_Serialized.ContainsKey("Type")) + return false; + + var l_Type = p_Serialized["Type"].Value(); + if (l_Type != GetTypeName()) + return false; + + try + { + Model = p_Serialized.ToObject(); + Model.OnDeserialized(p_Serialized); + + return true; + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][IAction<{typeof(t_Action).Name}, {typeof(t_Model).Name}.Unserialize] Error:"); + Logger.Instance.Error(l_Exception); + } + + return false; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override void BuildUI(Transform p_Parent) + { + var l_Label = XUIText.Make(UIPlaceHolder); + if (UIPlaceHolderTestButton) + { + var l_Button = XUIPrimaryButton.Make("Test"); + l_Button.OnClick(OnUIPlaceholderTestButton); + + XUIElements = new IXUIElement[] { l_Label, l_Button }; + } + else + XUIElements = new IXUIElement[] { l_Label }; + + BuildUIAuto(p_Parent); + } + /// + /// On UI placeholder test button pressed + /// + protected virtual void OnUIPlaceholderTestButton() { } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IActionBase.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IActionBase.cs new file mode 100644 index 0000000..73c820b --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IActionBase.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json.Linq; +using System.Collections; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Interfaces +{ + /// + /// IAction generic class + /// + public abstract class IActionBase : IUIConfigurable + { + public IEventBase Event { get; set; } + public abstract string Description { get; } + public abstract bool IsEnabled { get; set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get type name + /// + /// + public abstract string GetTypeName(); + /// + /// Serialize + /// + /// + public abstract JObject Serialize(); + /// + /// Unserialize + /// + /// + public abstract bool Unserialize(JObject p_Serialized); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public abstract void BuildUI(Transform p_Parent); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + public abstract IEnumerator Eval(Models.EventContext p_Context); + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/ICondition.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/ICondition.cs new file mode 100644 index 0000000..1818100 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/ICondition.cs @@ -0,0 +1,85 @@ +using CP_SDK.XUI; +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Interfaces +{ + /// + /// ICondition generic class + /// + public abstract class ICondition : IConditionBase + where t_Condition : ICondition, new() + where t_Model : Models.Condition, new() + { + public override bool IsEnabled { get => Model.Enabled; set { Model.Enabled = value; } } + public virtual string UIPlaceHolder => "No available settings..."; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Model + /// + public t_Model Model { get; protected set; } = new t_Model(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get type name + /// + /// + public override string GetTypeName() => typeof(t_Condition).Name; + /// + /// Serialize + /// + /// + public override JObject Serialize() + { + Model.Type = GetTypeName(); + return JObject.FromObject(Model); + } + /// + /// Unserialize + /// + /// + public override bool Unserialize(JObject p_Serialized) + { + if (!p_Serialized.ContainsKey("Type")) + return false; + + var l_Type = p_Serialized["Type"].Value(); + if (l_Type != GetTypeName()) + return false; + + try + { + Model = p_Serialized.ToObject(); + Model.OnDeserialized(p_Serialized); + + return true; + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][ICondition<{typeof(t_Condition).Name}, {typeof(t_Model).Name}.Unserialize] Error:"); + Logger.Instance.Error(l_Exception); + } + + return false; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public override void BuildUI(Transform p_Parent) + { + XUIElements = new IXUIElement[] { XUIText.Make(UIPlaceHolder) }; + + BuildUIAuto(p_Parent); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IConditionBase.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IConditionBase.cs new file mode 100644 index 0000000..da47bb9 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IConditionBase.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Interfaces +{ + /// + /// ICondition generic class + /// + public abstract class IConditionBase : IUIConfigurable + { + public IEventBase Event { get; set; } + public abstract string Description { get; } + public abstract bool IsEnabled { get; set; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get type name + /// + /// + public abstract string GetTypeName(); + /// + /// Serialize + /// + /// + public abstract JObject Serialize(); + /// + /// Unserialize + /// + /// + public abstract bool Unserialize(JObject p_Serialized); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public abstract void BuildUI(Transform p_Parent); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + public abstract bool Eval(Models.EventContext p_Context); + } + +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IEvent.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IEvent.cs new file mode 100644 index 0000000..1d5f30a --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IEvent.cs @@ -0,0 +1,247 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ChatPlexMod_ChatIntegrations.Interfaces +{ + /// + /// IEvent generic class + /// + public abstract class IEvent : IEventBase + where t_Event : IEvent, new() + where t_Model : Models.Event, new() + { + public override sealed Models.Event GenericModel => Model; + public override sealed bool IsEnabled { get => Model.Enabled; set { Model.Enabled = value; } } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public t_Model Model { get; protected set; } = new t_Model() { + GUID = Guid.NewGuid().ToString(), + Enabled = true, + CreationDate = CP_SDK.Misc.Time.UnixTimeNow() + }; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private static List m_CustomAvailableConditions = new List(); + private static List m_CustomAvailableActions = new List(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get type name + /// + /// + public override string GetTypeName() => typeof(t_Event).Name; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Register custom condition + /// + /// Name + /// Create func + /// Should fail silently + public static void RegisterCustomCondition(string p_Name, Func p_Func, bool p_SilentFail = false) + { + if (m_CustomAvailableConditions.Contains(p_Name)) + { + if (!p_SilentFail) + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][IEvent<{typeof(t_Event).Name}, {typeof(t_Model).Name}>.RegisterCustomCondition] Type \"{p_Name}\" already registered"); + return; + } + + m_CustomAvailableConditions.Add(p_Name); + ChatIntegrations.RegisterConditionType(p_Name, p_Func, p_SilentFail, true); + } + /// + /// Register custom action + /// + /// Name + /// Create func + /// Should fail silently + public static void RegisterCustomAction(string p_Name, Func p_Func, bool p_SilentFail = false) + { + if (m_CustomAvailableActions.Contains(p_Name)) + { + if (!p_SilentFail) + Logger.Instance.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][IEvent<{typeof(t_Event).Name}, {typeof(t_Model).Name}>.RegisterCustomAction] Type \"{p_Name}\" already registered"); + return; + } + + m_CustomAvailableActions.Add(p_Name); + ChatIntegrations.RegisterActionType(p_Name, p_Func, p_SilentFail, true); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + protected IReadOnlyList GetCustomConditionTypes() + => m_CustomAvailableConditions.AsReadOnly(); + protected IReadOnlyList GetCustomActionTypes() + => m_CustomAvailableActions.AsReadOnly(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Serialize + /// + /// + public override sealed JObject Serialize() + { + Model.Type = GetTypeName(); + + return new JObject() { + ["Type"] = GetTypeName(), + ["Event"] = JObject.FromObject(Model), + ["Conditions"] = JArray.FromObject(Conditions.Select(x => x.Serialize()).ToArray()), + ["Actions"] = JArray.FromObject(OnSuccessActions.Select(x => x.Serialize()).ToArray()), + ["OnFailActions"] = JArray.FromObject(OnFailActions.Select(x => x.Serialize()).ToArray()) + }; + } + /// + /// Unserialize + /// + /// + /// Error output + public override sealed bool Unserialize(JObject p_Serialized, out string p_Error) + { + if (!p_Serialized.ContainsKey("Event") || !p_Serialized.ContainsKey("Conditions")) + { + p_Error = "Invalid event format"; + return false; + } + + if (!(p_Serialized["Event"] as JObject).ContainsKey("Type")) + { + p_Error = "Invalid event format for type " + GetTypeName(); + return false; + } + + p_Serialized["Event"]["Type"] = ChatIntegrations.GetPatchedTypeName(p_Serialized["Event"]["Type"].Value()); + if (p_Serialized["Event"]["Type"].Value() != GetTypeName()) + { + p_Error = "Invalid event format for type " + GetTypeName(); + return false; + } + + Model = p_Serialized["Event"].ToObject(); + + if (p_Serialized.ContainsKey("Conditions") && p_Serialized["Conditions"].Type == JTokenType.Array) + { + var l_ConditionsJArray = p_Serialized["Conditions"] as JArray; + foreach (JObject l_SerializedCondition in l_ConditionsJArray) + { + if (!l_SerializedCondition.ContainsKey("Type")) + continue; + + var l_ConditionType = ChatIntegrations.GetPatchedTypeName(l_SerializedCondition["Type"].Value()); + l_SerializedCondition["Type"] = l_ConditionType; + + /// Create instance + var l_NewCondition = ChatIntegrations.CreateCondition(l_ConditionType); + if (l_NewCondition == null) + { + /// Todo backup this condition to avoid loss + Logger.Instance?.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][IEvent.Unserialize] Missing condition type \"{l_ConditionType}\""); + continue; + } + + l_NewCondition.Event = this; + + /// Unserialize condition + if (!l_NewCondition.Unserialize(l_SerializedCondition)) + { + /// Todo backup this condition to avoid loss + Logger.Instance?.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][IEvent.Unserialize] Failed to unserialize condition\n\"{l_ConditionType.ToString()}\""); + continue; + } + + AddCondition(l_NewCondition); + } + } + + if (p_Serialized.ContainsKey("Actions") && p_Serialized["Actions"].Type == JTokenType.Array) + { + var l_OnSuccessActionsJArray = p_Serialized["Actions"] as JArray; + foreach (JObject l_SerializedOnSuccessAction in l_OnSuccessActionsJArray) + { + if (!l_SerializedOnSuccessAction.ContainsKey("Type")) + continue; + + var l_OnSuccessActionType = ChatIntegrations.GetPatchedTypeName(l_SerializedOnSuccessAction["Type"].Value()); + l_SerializedOnSuccessAction["Type"] = l_OnSuccessActionType; + + /// Create instance + var l_NewOnSuccessAction = ChatIntegrations.CreateAction(l_OnSuccessActionType); + if (l_NewOnSuccessAction == null) + { + /// Todo backup this action to avoid loss + Logger.Instance?.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][IEvent.Unserialize] Missing action type \"{l_OnSuccessActionType}\""); + continue; + } + + l_NewOnSuccessAction.Event = this; + + /// Unserialize action + if (!l_NewOnSuccessAction.Unserialize(l_SerializedOnSuccessAction)) + { + /// Todo backup this event to avoid loss + Logger.Instance?.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][IEvent.Unserialize] Failed to unserialize action\n\"{l_OnSuccessActionType.ToString()}\""); + continue; + } + + AddOnSuccessAction(l_NewOnSuccessAction); + } + } + + if (p_Serialized.ContainsKey("OnFailActions") && p_Serialized["OnFailActions"].Type == JTokenType.Array) + { + var l_OnFailActionsJArray = p_Serialized["OnFailActions"] as JArray; + foreach (JObject l_SerializedOnFailAction in l_OnFailActionsJArray) + { + if (!l_SerializedOnFailAction.ContainsKey("Type")) + continue; + + var l_OnFailActionType = ChatIntegrations.GetPatchedTypeName(l_SerializedOnFailAction["Type"].Value()); + l_SerializedOnFailAction["Type"] = l_OnFailActionType; + + /// Create instance + var l_NewOnFailAction = ChatIntegrations.CreateAction(l_OnFailActionType); + if (l_NewOnFailAction == null) + { + /// Todo backup this action to avoid loss + Logger.Instance?.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][IEvent.Unserialize] Missing action type \"{l_OnFailActionType}\""); + continue; + } + + l_NewOnFailAction.Event = this; + + /// Unserialize action + if (!l_NewOnFailAction.Unserialize(l_SerializedOnFailAction)) + { + /// Todo backup this event to avoid loss + Logger.Instance?.Error($"[ChatPlexMod_ChatIntegrations.Interfaces][IEvent.Unserialize] Failed to unserialize action\n\"{l_OnFailActionType.ToString()}\""); + continue; + } + + AddOnFailAction(l_NewOnFailAction); + } + } + + p_Error = ""; + + if (string.IsNullOrEmpty(Model.GUID)) + Model.GUID = Guid.NewGuid().ToString(); + + return true; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IEventBase.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IEventBase.cs new file mode 100644 index 0000000..f308493 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IEventBase.cs @@ -0,0 +1,322 @@ +using Newtonsoft.Json.Linq; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.Interfaces +{ + /// + /// IEvent generic class + /// + public abstract class IEventBase : IUIConfigurable + { + public abstract Models.Event GenericModel { get; } + public abstract bool IsEnabled { get; set; } + public abstract IReadOnlyList<(EValueType, string)> ProvidedValues { get; protected set; } + public abstract IReadOnlyList AvailableConditions { get; protected set; } + public abstract IReadOnlyList AvailableActions { get; protected set; } + public List Conditions { get; protected set; } = new List(); + public List OnSuccessActions { get; protected set; } = new List(); + public List OnFailActions { get; protected set; } = new List(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + public bool Handle(Models.EventContext p_Context) + { + try + { + if (!CanBeExecuted(p_Context)) + return false; + + BuildProvidedValues(p_Context); + + if (p_Context.VariableCount != ProvidedValues.Count) + { + Logger.Instance?.Error(string.Format( + "[ChatPlexMod_ChatIntegrations.Interfaces][IEvent.Handle] Event {0} provided {1} values, {2} excepted, event discarded!", + GetTypeName(), p_Context.VariableCount, ProvidedValues.Count)); + + OnEventFailed(p_Context); + + return false; + } + + var l_ConditionCount = Conditions.Count; + for (var l_I = 0; l_I < l_ConditionCount; ++l_I) + { + var l_Condition = Conditions[l_I]; + if (l_Condition.IsEnabled && !l_Condition.Eval(p_Context)) + { + CP_SDK.Unity.MTCoroutineStarter.EnqueueFromThread(DoOnFailActions(p_Context)); + OnEventFailed(p_Context); + return false; + } + } + + CP_SDK.Unity.MTCoroutineStarter.EnqueueFromThread(DoActions(p_Context)); + } + catch (System.Exception l_Exception) + { + Logger.Instance?.Error("[ChatPlexMod_ChatIntegrations.Interfaces][IEvent.Handle] Error:"); + Logger.Instance?.Error(l_Exception); + } + + return true; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get type name + /// + /// + public abstract string GetTypeName(); + /// + /// Serialize + /// + /// + public abstract JObject Serialize(); + /// + /// Unserialize + /// + /// + /// Error output + public abstract bool Unserialize(JObject p_Serialized, out string p_Error); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Add an condition to the event + /// + /// Condition to add + public void AddCondition(IConditionBase p_Condition) + { + Conditions.Add(p_Condition); + } + /// + /// Move condition + /// + /// Condition to move + public int MoveCondition(IConditionBase p_Condition, bool p_Up) + { + var l_Index = Conditions.IndexOf(p_Condition); + + if (l_Index == -1 || (l_Index == 0 && p_Up) || (l_Index == (Conditions.Count - 1) && !p_Up)) + return -1; + + Conditions.Remove(p_Condition); + Conditions.Insert(l_Index + (p_Up ? -1 : 1), p_Condition); + + return Conditions.IndexOf(p_Condition); + } + /// + /// Delete an condition from the event + /// + /// Condition to delete + public void DeleteCondition(IConditionBase p_Condition) + { + Conditions.Remove(p_Condition); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Add an action to the event + /// + /// Action to add + public void AddOnSuccessAction(IActionBase p_Action) + { + OnSuccessActions.Add(p_Action); + } + /// + /// Move action + /// + /// Action to move + public int MoveOnSuccessAction(IActionBase p_Action, bool p_Up) + { + var l_Index = OnSuccessActions.IndexOf(p_Action); + + if (l_Index == -1 || (l_Index == 0 && p_Up) || (l_Index == (OnSuccessActions.Count - 1) && !p_Up)) + return -1; + + OnSuccessActions.Remove(p_Action); + OnSuccessActions.Insert(l_Index + (p_Up ? -1 : 1), p_Action); + + return OnSuccessActions.IndexOf(p_Action); + } + /// + /// Delete an action from the event + /// + /// Action to delete + public void DeleteOnSuccessAction(IActionBase p_Action) + { + OnSuccessActions.Remove(p_Action); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Add an on fail action to the event + /// + /// Action to add + public void AddOnFailAction(IActionBase p_OnFailAction) + { + OnFailActions.Add(p_OnFailAction); + } + /// + /// Move an on fail action + /// + /// Action to move + public int MoveOnFailAction(IActionBase p_OnFailAction, bool p_Up) + { + var l_Index = OnFailActions.IndexOf(p_OnFailAction); + + if (l_Index == -1 || (l_Index == 0 && p_Up) || (l_Index == (OnFailActions.Count - 1) && !p_Up)) + return -1; + + OnFailActions.Remove(p_OnFailAction); + OnFailActions.Insert(l_Index + (p_Up ? -1 : 1), p_OnFailAction); + + return OnFailActions.IndexOf(p_OnFailAction); + } + /// + /// Delete an on fail action from the event + /// + /// Action to delete + public void DeleteOnFailAction(IActionBase p_OnFailAction) + { + OnFailActions.Remove(p_OnFailAction); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public abstract void BuildUI(Transform p_Parent); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On import or clone + /// + /// Is an import + /// Is a clone + public virtual void OnImportOrClone(bool p_IsImport, bool p_IsClone) + { + if (p_IsImport) + GenericModel.Name += " (Import)"; + if (p_IsClone) + GenericModel.Name += " (Clone)"; + + GenericModel.CreationDate = CP_SDK.Misc.Time.UnixTimeNow(); + GenericModel.LastUsageDate = 0; + GenericModel.UsageCount = 0; + } + /// + /// When the event is enabled + /// + public virtual void OnEnable() { } + /// + /// When the event is successful + /// + /// Event context + public virtual void OnSuccess(Models.EventContext p_Context) { } + /// + /// When the event failed + /// + /// Event context + public virtual void OnEventFailed(Models.EventContext p_Context) { } + /// + /// When the event is disabled + /// + public virtual void OnDisable() { } + /// + /// When the event is deleted + /// + public virtual void OnDelete() { } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Handle + /// + /// Event context + protected abstract bool CanBeExecuted(Models.EventContext p_Context); + /// + /// Build provided value dictionary + /// + /// Event context + protected virtual void BuildProvidedValues(Models.EventContext p_Context) + { + + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Do actions + /// + /// Event context + /// + private IEnumerator DoActions(Models.EventContext p_Context) + { + var l_OnSuccessActionCount = OnSuccessActions.Count; + for (int l_I = 0; l_I < l_OnSuccessActionCount; ++l_I) + { + var l_Action = OnSuccessActions[l_I]; + if (!l_Action.IsEnabled) + continue; + + yield return l_Action.Eval(p_Context); + + if (!p_Context.PreventNextActionFailure && p_Context.HasActionFailed) + { + OnEventFailed(p_Context); + break; + } + } + + if (!p_Context.PreventNextActionFailure && !p_Context.HasActionFailed) + OnSuccess(p_Context); + + yield return null; + } + /// + /// Do on fail actions + /// + /// Event context + /// + private IEnumerator DoOnFailActions(Models.EventContext p_Context) + { + var l_OnFailActionCount = OnFailActions.Count; + for (int l_I = 0; l_I < l_OnFailActionCount; ++l_I) + { + var l_OnFailAction = OnFailActions[l_I]; + if (!l_OnFailAction.IsEnabled) + continue; + + yield return l_OnFailAction.Eval(p_Context); + + if (!p_Context.PreventNextActionFailure && p_Context.HasActionFailed) + break; + } + + yield return null; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IUIConfigurable.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IUIConfigurable.cs new file mode 100644 index 0000000..ded0b24 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Interfaces/IUIConfigurable.cs @@ -0,0 +1,73 @@ +using CP_SDK.XUI; +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace ChatPlexMod_ChatIntegrations.Interfaces +{ + /// + /// UI Configurable interface + /// + public abstract class IUIConfigurable + { + /// + /// Fields + /// + public CP_SDK.XUI.IXUIElement[] XUIElements; + /// + /// View instance + /// + public UI.SettingsMainView View => UI.SettingsMainView.Instance; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build editing UI + /// + /// Parent transform + public void BuildUIAuto(Transform p_Parent) + { + var l_Title = string.Empty; + + if (this is IEventBase l_EventBase) l_Title = l_EventBase.GetTypeName() + " | " + l_EventBase.GenericModel.Name; + else if (this is IActionBase l_ActionBase) l_Title = l_ActionBase.GetTypeName(); + else if (this is IConditionBase l_ConditionBase) l_Title = l_ConditionBase.GetTypeName(); + + l_Title = "" + l_Title.Replace("_", "::"); + + var l_FinalList = new List() + { + XUIText.Make(l_Title).SetStyle(FontStyles.Bold).SetAlign(TextAlignmentOptions.Center) + }; + + if (XUIElements != null) + l_FinalList.AddRange(XUIElements); + + try + { + XUIVScrollView.Make( + XUIVLayout.Make(l_FinalList.ToArray()) + .OnReady(x => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained) + ) + .OnReady(x => x.Container.GetComponent().padding = new RectOffset(0, 0, 0, 0)) + .BuildUI(p_Parent); + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error(l_Exception); + } + + LayoutRebuilder.ForceRebuildLayoutImmediate(p_Parent.parent as RectTransform); + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + if (!p_Parent || !p_Parent.parent) + return; + + LayoutRebuilder.ForceRebuildLayoutImmediate(p_Parent.parent as RectTransform); + }); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Logger.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Logger.cs similarity index 84% rename from Modules/BeatSaberPlus_ChatIntegrations/Logger.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Logger.cs index 293f2de..ae75c51 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Logger.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Logger.cs @@ -1,4 +1,4 @@ -namespace BeatSaberPlus_ChatIntegrations +namespace ChatPlexMod_ChatIntegrations { /// /// Logger instance holder diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Action.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Action.cs new file mode 100644 index 0000000..075ebc7 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Action.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace ChatPlexMod_ChatIntegrations.Models +{ + /// + /// Action data modal + /// + [Serializable] + public class Action + { + [JsonProperty] + public string Type = "?"; + [JsonProperty] + public bool Enabled = false; + [JsonProperty] + public string BaseValue = ""; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On deserialized + /// + /// Input data + public virtual void OnDeserialized(JObject p_Serialized) + { + + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Chat.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/Chat.cs similarity index 68% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Chat.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/Chat.cs index c4c4ef5..b5f4545 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Chat.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/Chat.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Actions +namespace ChatPlexMod_ChatIntegrations.Models.Actions { public class Chat_SendMessage : Action { @@ -13,6 +13,9 @@ public Chat_SendMessage() } } + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + public class Chat_ToggleVisibility : Action { [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/EmoteRain.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/EmoteRain.cs similarity index 69% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/EmoteRain.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/EmoteRain.cs index 6b80dee..887c48d 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/EmoteRain.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/EmoteRain.cs @@ -1,12 +1,16 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Actions +namespace ChatPlexMod_ChatIntegrations.Models.Actions { public class EmoteRain_CustomRain : Action { [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] public uint Count = 20; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + public class EmoteRain_EmoteBombRain : Action { [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Event.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/Event.cs similarity index 62% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Event.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/Event.cs index 966b19d..cfe57e5 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Event.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/Event.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Actions +namespace ChatPlexMod_ChatIntegrations.Models.Actions { public class Event_ExecuteDummy : Action { @@ -8,6 +8,9 @@ public class Event_ExecuteDummy : Action public bool Continue = true; } + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + public class Event_Toggle : Action { [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Misc.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/Misc.cs similarity index 81% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Misc.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/Misc.cs index e342991..86d9822 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Misc.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/Misc.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Actions +namespace ChatPlexMod_ChatIntegrations.Models.Actions { public class Misc_Delay : Action { @@ -12,6 +12,9 @@ public class Misc_Delay : Action public bool PreventNextActionFailure = true; } + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + public class Misc_PlaySound : Action { [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/OBS.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/OBS.cs new file mode 100644 index 0000000..b6db331 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Actions/OBS.cs @@ -0,0 +1,137 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace ChatPlexMod_ChatIntegrations.Models.Actions +{ + public class OBS_RenameLastRecord : Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string Format = "$OriginalName (Completed!)"; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_SetRecordFilenameFormat : Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string Format = "%CCYY-%MM-%DD %hh-%mm-%ss"; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_SwitchPreviewToScene : Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string SceneName = ""; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_SwitchToScene : Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string SceneName = ""; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_ToggleStudioMode : Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.Toggle.E ChangeType = Enums.Toggle.E.Toggle; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On deserialized + /// + /// Input data + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ChangeType") + || !p_Serialized.ContainsKey("ToggleType")) + return; + + ChangeType = Enums.Toggle.ToEnum(p_Serialized["ToggleType"].Value()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_ToggleSource : Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.Toggle.E ChangeType = Enums.Toggle.E.Toggle; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string SceneName = ""; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string SourceName = ""; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On deserialized + /// + /// Input data + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ChangeType") + || !p_Serialized.ContainsKey("ToggleType")) + return; + + ChangeType = Enums.Toggle.ToEnum(p_Serialized["ToggleType"].Value()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_ToggleSourceAudio : Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.Toggle.E ChangeType = Enums.Toggle.E.Toggle; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string SceneName = ""; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string SourceName = ""; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On deserialized + /// + /// Input data + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("ChangeType") + || !p_Serialized.ContainsKey("ToggleType")) + return; + + ChangeType = Enums.Toggle.ToEnum(p_Serialized["ToggleType"].Value()); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public class OBS_Transition : Action + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool OverrideDuration = false; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public int Duration = 300; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public bool OverrideTransition = false; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public string Transition = "Fade"; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Condition.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Condition.cs similarity index 93% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Condition.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Condition.cs index f2f0bf5..a2a85be 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Condition.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Condition.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json.Linq; using System; -namespace BeatSaberPlus_ChatIntegrations.Models +namespace ChatPlexMod_ChatIntegrations.Models { /// /// Condition data modal diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Bits.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Bits.cs new file mode 100644 index 0000000..ef2400c --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Bits.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace ChatPlexMod_ChatIntegrations.Models.Conditions +{ + public class Bits_Amount : Condition + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.Comparison.E Comparison = Enums.Comparison.E.GreaterOrEqual; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public uint Count = 10; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("Comparison") + || !p_Serialized.ContainsKey("IsGreaterThan")) + return; + + if (p_Serialized["IsGreaterThan"].Value()) + Comparison = Enums.Comparison.E.Greater; + else + Comparison = Enums.Comparison.E.Less; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Events.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Events.cs similarity index 86% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Events.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Events.cs index 1d7f41d..763be63 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Events.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Events.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Conditions +namespace ChatPlexMod_ChatIntegrations.Models.Conditions { public class Event_Enabled : Condition { diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Misc.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Misc.cs similarity index 87% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Misc.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Misc.cs index c28d993..5b14499 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Misc.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Misc.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Conditions +namespace ChatPlexMod_ChatIntegrations.Models.Conditions { public class Misc_Cooldown : Condition { diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/OBS.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/OBS.cs similarity index 86% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/OBS.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/OBS.cs index 4a21296..492387f 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/OBS.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/OBS.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Conditions +namespace ChatPlexMod_ChatIntegrations.Models.Conditions { public class OBS_IsInScene : Condition { diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Subscription.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Subscription.cs new file mode 100644 index 0000000..fbd8fda --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/Subscription.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace ChatPlexMod_ChatIntegrations.Models.Conditions +{ + public class Subscription_PlanType : Condition + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.TwitchSubscribtionPlanType.E SubscribtionPlanType = Enums.TwitchSubscribtionPlanType.E.Tier1; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On deserialized + /// + /// Input data + public override void OnDeserialized(JObject p_Serialized) + { + if (p_Serialized.ContainsKey("SubscribtionPlanType") + || !p_Serialized.ContainsKey("PlanType")) + return; + + SubscribtionPlanType = Enums.TwitchSubscribtionPlanType.ToEnum(p_Serialized["PlanType"].Value()); + } + } + + public class Subscription_PurchasedMonthCount : Condition + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(StringEnumConverter))] + public Enums.Comparison.E Comparison = Enums.Comparison.E.GreaterOrEqual; + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public uint Count = 0; + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/User.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/User.cs similarity index 90% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/User.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/User.cs index 0805b37..6a855f1 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/User.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Conditions/User.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Conditions +namespace ChatPlexMod_ChatIntegrations.Models.Conditions { public class User_Permissions : Condition { diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Event.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Event.cs similarity index 92% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Event.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Event.cs index bef5333..a18f545 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Event.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Event.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using System; -namespace BeatSaberPlus_ChatIntegrations.Models +namespace ChatPlexMod_ChatIntegrations.Models { /// /// Event data modal diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/EventContext.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/EventContext.cs similarity index 74% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/EventContext.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/EventContext.cs index 6258418..079b6ea 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/EventContext.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/EventContext.cs @@ -1,10 +1,10 @@ -using BeatSaberPlus_ChatIntegrations.Interfaces; +using ChatPlexMod_ChatIntegrations.Interfaces; using CP_SDK.Chat.Interfaces; using System; using System.Collections.Generic; using System.Linq; -namespace BeatSaberPlus_ChatIntegrations.Models +namespace ChatPlexMod_ChatIntegrations.Models { /// /// Event context @@ -14,7 +14,7 @@ public class EventContext : ICloneable /// /// Provided values /// - private Dictionary<(IValueType, string), object> m_Values = new Dictionary<(IValueType, string), object>(); + private Dictionary<(EValueType, string), object> m_Values = new Dictionary<(EValueType, string), object>(); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -22,7 +22,7 @@ public class EventContext : ICloneable /// /// Trigger type /// - public TriggerType Type = TriggerType.None; + public ETriggerType Type = ETriggerType.None; /// /// Service instance /// @@ -56,14 +56,6 @@ public class EventContext : ICloneable /// public IChatSubscriptionEvent SubscriptionEvent = null; /// - /// Level data - /// - public BeatSaberPlus.SDK.Game.LevelData LevelData = null; - /// - /// Level completion results - /// - public BeatSaberPlus.SDK.Game.LevelCompletionData LevelCompletionData = null; - /// /// VoiceAttack command GUID /// public string VoiceAttackCommandGUID = null; @@ -71,6 +63,10 @@ public class EventContext : ICloneable /// VoiceAttack command name /// public string VoiceAttackCommandName = null; + /// + /// Custom event data + /// + public object CustomData = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -98,7 +94,7 @@ public class EventContext : ICloneable public object Clone() { var l_Result = this.MemberwiseClone(); - (l_Result as EventContext).m_Values = new Dictionary<(IValueType, string), object>(); + (l_Result as EventContext).m_Values = new Dictionary<(EValueType, string), object>(); return l_Result; } @@ -112,38 +108,38 @@ public object Clone() /// IValueType Type /// Value name /// Value data - public void AddValue(IValueType p_Type, string p_Name, object p_Value) + public void AddValue(EValueType p_Type, string p_Name, object p_Value) { var l_Key = (p_Type, p_Name); if (m_Values.ContainsKey(l_Key)) - throw new System.Exception($"[BeatSaberPlus_ChatIntegrations.Models][EventContext.AddValue] Duplicate for {p_Type}.{p_Name}!"); + throw new System.Exception($"[ChatPlexMod_ChatIntegrations.Models][EventContext.AddValue] Duplicate for {p_Type}.{p_Name}!"); switch (p_Type) { - case IValueType.Boolean: + case EValueType.Boolean: if (p_Value != null && !(p_Value is bool?)) - throw new System.Exception($"[BeatSaberPlus_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, bool? excepted, got {p_Value.GetType()}!"); + throw new System.Exception($"[ChatPlexMod_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, bool? excepted, got {p_Value.GetType()}!"); break; - case IValueType.Integer: + case EValueType.Integer: if (p_Value != null && !(p_Value is Int64?)) - throw new System.Exception($"[BeatSaberPlus_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, Int64? excepted, got {p_Value.GetType()}!"); + throw new System.Exception($"[ChatPlexMod_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, Int64? excepted, got {p_Value.GetType()}!"); break; - case IValueType.Floating: + case EValueType.Floating: if (p_Value != null && !(p_Value is float?)) - throw new System.Exception($"[BeatSaberPlus_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, float? excepted, got {p_Value.GetType()}!"); + throw new System.Exception($"[ChatPlexMod_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, float? excepted, got {p_Value.GetType()}!"); break; - case IValueType.String: + case EValueType.String: if (p_Value != null && !(p_Value is string)) - throw new System.Exception($"[BeatSaberPlus_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, string excepted, got {p_Value.GetType()}!"); + throw new System.Exception($"[ChatPlexMod_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, string excepted, got {p_Value.GetType()}!"); break; - case IValueType.Emotes: + case EValueType.Emotes: if (p_Value != null && !(p_Value is IChatEmote[])) - throw new System.Exception($"[BeatSaberPlus_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, List excepted, got {p_Value.GetType()}!"); + throw new System.Exception($"[ChatPlexMod_ChatIntegrations.Models][EventContext.AddValue] Wrong value type for {p_Type}.{p_Name}, List excepted, got {p_Value.GetType()}!"); break; } @@ -155,7 +151,7 @@ public void AddValue(IValueType p_Type, string p_Name, object p_Value) /// /// Types /// - public List<(IValueType, string)> GetValues(params IValueType[] p_Types) + public List<(EValueType, string)> GetValues(params EValueType[] p_Types) { return m_Values.Select(x => x.Key).Where(x => p_Types.Contains(x.Item1)).ToList(); } @@ -164,7 +160,7 @@ public void AddValue(IValueType p_Type, string p_Name, object p_Value) /// /// Variable /// - public (IValueType, string) GetFirstValueOfType(IValueType p_Type) + public (EValueType, string) GetFirstValueOfType(EValueType p_Type) { return m_Values.Select(x => x.Key).Where(x => x.Item1 == p_Type).FirstOrDefault(); } @@ -182,7 +178,7 @@ public bool GetBooleanValue(string p_Name, out bool? p_Out) { p_Out = null; - if (m_Values != null && m_Values.TryGetValue((IValueType.Boolean, p_Name), out var l_Result)) + if (m_Values != null && m_Values.TryGetValue((EValueType.Boolean, p_Name), out var l_Result)) { p_Out = (bool?)l_Result; return p_Out != null && p_Out.HasValue; @@ -200,7 +196,7 @@ public bool GetIntegerValue(string p_Name, out Int64? p_Out) { p_Out = null; - if (m_Values != null && m_Values.TryGetValue((IValueType.Integer, p_Name), out var l_Result)) + if (m_Values != null && m_Values.TryGetValue((EValueType.Integer, p_Name), out var l_Result)) { p_Out = (Int64?)l_Result; return p_Out != null && p_Out.HasValue; @@ -218,7 +214,7 @@ public bool GetFloatingValue(string p_Name, out float? p_Out) { p_Out = null; - if (m_Values != null && m_Values.TryGetValue((IValueType.Floating, p_Name), out var l_Result)) + if (m_Values != null && m_Values.TryGetValue((EValueType.Floating, p_Name), out var l_Result)) { p_Out = (float?)l_Result; return p_Out != null && p_Out.HasValue; @@ -236,7 +232,7 @@ public bool GetStringValue(string p_Name, out string p_Out) { p_Out = null; - if (m_Values != null && m_Values.TryGetValue((IValueType.String, p_Name), out var l_Result)) + if (m_Values != null && m_Values.TryGetValue((EValueType.String, p_Name), out var l_Result)) { p_Out = (string)l_Result; return p_Out != null; @@ -254,7 +250,7 @@ public bool GetEmotesValue(string p_Name, out IChatEmote[] p_Out) { p_Out = null; - if (m_Values != null && m_Values.TryGetValue((IValueType.Emotes, p_Name), out var l_Result)) + if (m_Values != null && m_Values.TryGetValue((EValueType.Emotes, p_Name), out var l_Result)) { p_Out = (IChatEmote[])l_Result; return p_Out != null; diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatBits.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatBits.cs similarity index 82% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatBits.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatBits.cs index 1629042..c7c504d 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatBits.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatBits.cs @@ -1,4 +1,4 @@ -namespace BeatSaberPlus_ChatIntegrations.Models.Events +namespace ChatPlexMod_ChatIntegrations.Models.Events { /// /// Chat bits event model diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatCommand.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatCommand.cs similarity index 91% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatCommand.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatCommand.cs index cbc7b63..d72dacc 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatCommand.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatCommand.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Events +namespace ChatPlexMod_ChatIntegrations.Models.Events { /// /// Chat command event model diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatFollow.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatFollow.cs similarity index 82% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatFollow.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatFollow.cs index fcc3517..79e179d 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatFollow.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatFollow.cs @@ -1,4 +1,4 @@ -namespace BeatSaberPlus_ChatIntegrations.Models.Events +namespace ChatPlexMod_ChatIntegrations.Models.Events { /// /// Chat follow event model diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatPointsReward.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatPointsReward.cs similarity index 96% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatPointsReward.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatPointsReward.cs index 7616c25..25cdbc3 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatPointsReward.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatPointsReward.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Events +namespace ChatPlexMod_ChatIntegrations.Models.Events { /// /// Chat points reward event model diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatSubscription.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatSubscription.cs similarity index 84% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatSubscription.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatSubscription.cs index 8f3defa..bdde0a7 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/ChatSubscription.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/ChatSubscription.cs @@ -1,4 +1,4 @@ -namespace BeatSaberPlus_ChatIntegrations.Models.Events +namespace ChatPlexMod_ChatIntegrations.Models.Events { /// /// Chat subscription event model diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/VoiceAttackCommand.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/VoiceAttackCommand.cs similarity index 87% rename from Modules/BeatSaberPlus_ChatIntegrations/Models/Events/VoiceAttackCommand.cs rename to Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/VoiceAttackCommand.cs index ce76ffb..78748b0 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Events/VoiceAttackCommand.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/Models/Events/VoiceAttackCommand.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace BeatSaberPlus_ChatIntegrations.Models.Events +namespace ChatPlexMod_ChatIntegrations.Models.Events { /// /// VoiceAttack command event model diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ModulePresence.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ModulePresence.cs new file mode 100644 index 0000000..101c31f --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/ModulePresence.cs @@ -0,0 +1,27 @@ +using System.Linq; + +namespace ChatPlexMod_ChatIntegrations +{ + internal static class ModulePresence + { + private static bool? m_Chat; + private static bool? m_ChatEmoteRain; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public static bool Chat { get { + if (!m_Chat.HasValue) + m_Chat = CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Chat"); + + return m_Chat.Value; + } } + + public static bool ChatEmoteRain { get { + if (!m_ChatEmoteRain.HasValue) + m_ChatEmoteRain = CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Chat Emote Rain"); + + return m_ChatEmoteRain.Value; + } } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/ActionListItem.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/ActionListItem.cs new file mode 100644 index 0000000..13f657d --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/ActionListItem.cs @@ -0,0 +1,49 @@ +namespace ChatPlexMod_ChatIntegrations.UI.Data +{ + /// + /// Action list item + /// + internal class ActionListItem : CP_SDK.UI.Data.IListItem + { + public Interfaces.IActionBase Action; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// Action + public ActionListItem(Interfaces.IActionBase p_Action) + { + Action = p_Action; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On show + /// + public override void OnShow() => Refresh(); + /// + /// On hide + /// + public override void OnHide() { } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Refresh + /// + public void Refresh() + { + if (Cell == null || !(Cell is CP_SDK.UI.Data.TextListCell l_TextListCell)) + return; + + var l_Text = "" + (Action.IsEnabled ? "" + Action.GetTypeName().Replace("_", "::") : "" + Action.GetTypeName().Replace("_", "::")); + l_TextListCell.Text.SetText(l_Text); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/ConditionListItem.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/ConditionListItem.cs new file mode 100644 index 0000000..20365fa --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/ConditionListItem.cs @@ -0,0 +1,49 @@ +namespace ChatPlexMod_ChatIntegrations.UI.Data +{ + /// + /// Condition list item + /// + internal class ConditionListItem : CP_SDK.UI.Data.IListItem + { + public Interfaces.IConditionBase Condition; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// Action + public ConditionListItem(Interfaces.IConditionBase p_Condition) + { + Condition = p_Condition; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On show + /// + public override void OnShow() => Refresh(); + /// + /// On hide + /// + public override void OnHide() { } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Refresh + /// + public void Refresh() + { + if (Cell == null || !(Cell is CP_SDK.UI.Data.TextListCell l_TextListCell)) + return; + + var l_Text = "" + (Condition.IsEnabled ? "" + Condition.GetTypeName().Replace("_", "::") : "" + Condition.GetTypeName().Replace("_", "::")); + l_TextListCell.Text.SetText(l_Text); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/EventListItem.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/EventListItem.cs new file mode 100644 index 0000000..5b78291 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Data/EventListItem.cs @@ -0,0 +1,66 @@ +namespace ChatPlexMod_ChatIntegrations.UI.Data +{ + /// + /// Event list item + /// + internal class EventListItem : CP_SDK.UI.Data.IListItem + { + public Interfaces.IEventBase Event; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// Event + public EventListItem(Interfaces.IEventBase p_Event) + { + Event = p_Event; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On show + /// + public override void OnShow() => Refresh(); + /// + /// On hide + /// + public override void OnHide() { } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Refresh + /// + public void Refresh() + { + if (Cell == null || !(Cell is CP_SDK.UI.Data.TextListCell l_TextListCell)) + return; + + var l_Text = ""; + + if (!Event.IsEnabled) + l_Text += ""; + else + l_Text += ""; + + l_Text += (Event.IsEnabled ? "" : "") + "[" + Event.GetTypeName() + "] " + (Event.IsEnabled ? "" : ""); + l_Text += " "; + l_Text += Event.GenericModel.Name.Length > 40 ? (Event.GenericModel.Name.Substring(0, 37) + "...") : Event.GenericModel.Name; + + if (!Event.IsEnabled) + l_Text += " (Disabled)"; + else + l_Text += ""; + + l_Text += "\n" + Event.GenericModel.UsageCount + " use(s)"; + + l_TextListCell.Text.SetText(l_Text); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/AddXModal.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/AddXModal.cs new file mode 100644 index 0000000..5350144 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/AddXModal.cs @@ -0,0 +1,196 @@ +using CP_SDK; +using CP_SDK.UI.Data; +using CP_SDK.XUI; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.UI; + +namespace ChatPlexMod_ChatIntegrations.UI.Modals +{ + /// + /// Add Condition/Action modal + /// + internal sealed class AddXModal : CP_SDK.UI.IModal + { + internal class CategoryListItem : TextListItem + { + public List Types; + internal CategoryListItem(string p_Category, int p_Order, List p_Types) + : base($"{p_Order} - {p_Category}") + { + Types = p_Types; + } + } + internal class TypeListItem : TextListItem + { + public string TypeName; + internal TypeListItem(string p_TypeName, int p_Order) + : base($"{p_Order} - ") + { + TypeName = p_TypeName; + Text += "" + p_TypeName.Substring(0, p_TypeName.IndexOf("_")) + + "::" + p_TypeName.Substring(p_TypeName.IndexOf("_") + 1) + ""; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUIVVList m_CategoryList = null; + private XUIVVList m_TypeList = null; + + private Action m_Callback; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On modal show + /// + public override void OnShow() + { + if (m_CategoryList != null) + return; + + Templates.ModalRectLayout( + XUIHLayout.Make( + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(ListCellPrefabs.Get()) + .OnListItemSelected(OnCategorySelected) + .Bind(ref m_CategoryList) + ) + .SetSpacing(0).SetPadding(0) + .SetWidth(40.0f).SetHeight(50.0f) + .SetBackground(true, CP_SDK.UI.UISystem.ListBGColor) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(ListCellPrefabs.Get()) + .Bind(ref m_TypeList) + ) + .SetSpacing(0).SetPadding(0) + .SetWidth(80.0f).SetHeight(50.0f) + .SetBackground(true, CP_SDK.UI.UISystem.ListBGColor) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true) + ), + XUIHLayout.Make( + XUISecondaryButton.Make("Cancel", OnCancelButton).SetWidth(30f), + XUIPrimaryButton.Make("Create", OnCreateButton).SetWidth(30f) + ) + .SetPadding(0) + ) + .BuildUI(transform); + } + /// + /// On modal close + /// + public override void OnClose() + { + + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Init + /// + /// Availables types + /// Callback + public void Init(IReadOnlyList p_Availables, Action p_Callback) + { + var l_Availables = p_Availables; + var l_PerCategory = new Dictionary>(); + + for (var l_I = 0; l_I < l_Availables.Count; ++l_I) + { + var l_Type = l_Availables[l_I]; + var l_Category = "Others"; + + if (l_Type.Contains("_")) + l_Category = l_Type.Substring(0, l_Type.IndexOf("_")); + + if (!l_PerCategory.ContainsKey(l_Category)) + l_PerCategory.Add(l_Category, new List() { l_Type }); + else + l_PerCategory[l_Category].Add(l_Type); + } + + l_PerCategory = l_PerCategory.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); + + var l_Order = 0; + var l_Items = new List(); + foreach (var l_KVP in l_PerCategory) + { + l_KVP.Value.Sort((x, y) => x.CompareTo(y)); + + var l_Types = new List(); + var l_SubOrder = 0; + for (var l_TI = 0; l_TI < l_KVP.Value.Count; ++l_TI) + l_Types.Add(new TypeListItem(l_KVP.Value[l_TI], ++l_SubOrder)); + + l_Items.Add(new CategoryListItem(l_KVP.Key, ++l_Order, l_Types)); + } + + m_CategoryList.SetListItems(l_Items); + m_CategoryList.SetSelectedListItem(l_Items.FirstOrDefault()); + + m_Callback = p_Callback; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On category selected + /// + /// Selected item + private void OnCategorySelected(IListItem p_SelectedItem) + { + var l_SelectedCategory = p_SelectedItem as CategoryListItem; + if (l_SelectedCategory == null) + { + m_TypeList.SetListItems(null); + return; + } + + m_TypeList.SetListItems(l_SelectedCategory.Types); + m_TypeList.SetSelectedListItem(l_SelectedCategory.Types.FirstOrDefault()); + } + /// + /// On cancel button + /// + private void OnCancelButton() + { + VController.CloseModal(this); + } + /// + /// On create button + /// + private void OnCreateButton() + { + var l_SelectedType = m_TypeList.Element.GetSelectedItem() as TypeListItem; + if (l_SelectedType == null) + { + VController.ShowMessageModal("Please select a type first!"); + return; + } + + VController.CloseModal(this); + + try { m_Callback?.Invoke(l_SelectedType.TypeName); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[ChatPlexMod_ChatIntegrations.UI][AddXModal.OnCreateButton] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventCreateModal.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventCreateModal.cs new file mode 100644 index 0000000..dbba78b --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventCreateModal.cs @@ -0,0 +1,123 @@ +using CP_SDK; +using CP_SDK.XUI; +using System; +using System.Linq; + +namespace ChatPlexMod_ChatIntegrations.UI.Modals +{ + /// + /// Event create modal + /// + internal sealed class EventCreateModal : CP_SDK.UI.IModal + { + private XUIDropdown m_Type = null; + private XUITextInput m_Name = null; + + private Action m_Callback = null; + private string m_Selected = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On modal show + /// + public override void OnShow() + { + if (m_Type != null) + return; + + Templates.ModalRectLayout( + XUIText.Make("What kind of event do you want to create?"), + + XUIDropdown.Make() + .SetOptions(ChatIntegrations.RegisteredEventTypes.ToList()) + .OnValueChanged((_, p_Selected) => m_Selected = p_Selected) + .Bind(ref m_Type), + + XUITextInput.Make("", "Name...") + .Bind(ref m_Name), + + XUIHLayout.Make( + XUISecondaryButton.Make("Cancel", OnCancelButton).SetWidth(30f), + XUIPrimaryButton.Make("Create", OnCreateButton).SetWidth(30f) + ) + .SetPadding(0) + ) + .SetWidth(90.0f) + .BuildUI(transform); + } + /// + /// On modal close + /// + public override void OnClose() + { + + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Init + /// + /// Callback + public void Init(Action p_Callback) + { + m_Callback = p_Callback; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On cancel button + /// + private void OnCancelButton() + { + VController.CloseModal(this); + } + /// + /// On create button + /// + private void OnCreateButton() + { + try + { + var l_NewEvent = ChatIntegrations.CreateEvent(m_Selected); + if (l_NewEvent != null) + { + var l_Name = m_Name.Element.GetValue(); + if (string.IsNullOrEmpty(l_Name)) + l_Name = "New " + m_Selected + " " + CP_SDK.Misc.Time.UnixTimeNow(); + + l_NewEvent.GenericModel.Name = l_Name; + + ChatIntegrations.Instance.AddEvent(l_NewEvent); + + VController.CloseModal(this); + + try { m_Callback?.Invoke(l_NewEvent); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[ChatPlexMod_ChatIntegrations.UI][EventCreateModal.OnCreateButton] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + else + { + VController.CloseModal(this); + VController.ShowMessageModal("No type selected!"); + } + } + catch (System.Exception l_Exception) + { + VController.CloseModal(this); + VController.ShowMessageModal("Unknown error!"); + + ChatPlexSDK.Logger.Error($"[ChatPlexMod_ChatIntegrations.UI][EventCreateModal.OnCreateButton] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventImportModal.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventImportModal.cs new file mode 100644 index 0000000..4281754 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventImportModal.cs @@ -0,0 +1,133 @@ +using CP_SDK; +using CP_SDK.XUI; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; + +namespace ChatPlexMod_ChatIntegrations.UI.Modals +{ + /// + /// Event import modal + /// + internal sealed class EventImportModal : CP_SDK.UI.IModal + { + private XUIDropdown m_File = null; + + private Action m_Callback = null; + private string m_Selected = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On modal show + /// + public override void OnShow() + { + if (m_File != null) + return; + + Templates.ModalRectLayout( + XUIText.Make("Which event do you want to import?"), + + XUIDropdown.Make() + .OnValueChanged((_, p_Selected) => m_Selected = p_Selected) + .Bind(ref m_File), + + XUIHLayout.Make( + XUISecondaryButton.Make("Cancel", OnCancelButton).SetWidth(30f), + XUIPrimaryButton.Make("Import", OnImportButton).SetWidth(30f) + ) + .SetPadding(0) + ) + .SetWidth(90.0f) + .BuildUI(transform); + } + /// + /// On modal close + /// + public override void OnClose() + { + + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Init + /// + /// Callback + public void Init(Action p_Callback) + { + m_Selected = string.Empty; + + var l_Files = new List(); + foreach (var l_File in System.IO.Directory.GetFiles(ChatIntegrations.s_IMPORT_PATH, "*.bspci")) + l_Files.Add(System.IO.Path.GetFileNameWithoutExtension(l_File)); + + m_File.SetOptions(l_Files); + + m_Callback = p_Callback; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On cancel button + /// + private void OnCancelButton() + { + VController.CloseModal(this); + } + /// + /// On import button + /// + private void OnImportButton() + { + var l_FileName = ChatIntegrations.s_IMPORT_PATH + m_Selected + ".bspci"; + + if (System.IO.File.Exists(l_FileName)) + { + var l_Raw = System.IO.File.ReadAllText(l_FileName, System.Text.Encoding.Unicode); + + try + { + var l_JObject = JObject.Parse(l_Raw); + var l_NewEvent = ChatIntegrations.Instance.AddEventFromSerialized(l_JObject, true, false, out var l_Error); + + if (l_NewEvent != null) + { + VController.CloseModal(this); + + try { m_Callback?.Invoke(l_NewEvent); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[ChatPlexMod_ChatIntegrations.UI][ProfileImportModal.EventImportModal] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + else + { + VController.CloseModal(this); + VController.ShowMessageModal("Error importing profile!"); + } + } + catch (System.Exception l_Exception) + { + VController.CloseModal(this); + VController.ShowMessageModal("Unknown error!"); + + ChatPlexSDK.Logger.Error($"[ChatPlexMod_ChatIntegrations.UI][EventCreateModal.OnCreateButton] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + else + { + VController.CloseModal(this); + VController.ShowMessageModal("File not found!"); + } + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventTemplateModal.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventTemplateModal.cs new file mode 100644 index 0000000..531169f --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/Modals/EventTemplateModal.cs @@ -0,0 +1,119 @@ +using CP_SDK; +using CP_SDK.XUI; +using System; +using System.Linq; + +namespace ChatPlexMod_ChatIntegrations.UI.Modals +{ + /// + /// Event template modal + /// + internal sealed class EventTemplateModal : CP_SDK.UI.IModal + { + private XUIDropdown m_Template = null; + + private Action m_Callback = null; + private string m_Selected = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On modal show + /// + public override void OnShow() + { + if (m_Template != null) + return; + + Templates.ModalRectLayout( + XUIText.Make("Which template do you want to use?"), + + XUIDropdown.Make() + .SetOptions(ChatIntegrations.RegisteredTemplates.Keys.ToList()) + .OnValueChanged((_, p_Selected) => m_Selected = p_Selected) + .Bind(ref m_Template), + + XUIHLayout.Make( + XUISecondaryButton.Make("Cancel", OnCancelButton).SetWidth(30f), + XUIPrimaryButton.Make("Create", OnCreateButton).SetWidth(30f) + ) + .SetPadding(0) + ) + .SetWidth(90.0f) + .BuildUI(transform); + } + /// + /// On modal close + /// + public override void OnClose() + { + + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Init + /// + /// Callback + public void Init(Action p_Callback) + { + m_Callback = p_Callback; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On cancel button + /// + private void OnCancelButton() + { + VController.CloseModal(this); + } + /// + /// On create button + /// + private void OnCreateButton() + { + if (!ChatIntegrations.RegisteredTemplates.TryGetValue(m_Selected, out var l_CreateFunc)) + { + VController.ShowMessageModal("Template not found!"); + return; + } + + try + { + var l_NewEvent = l_CreateFunc(); + if (l_NewEvent != null) + { + ChatIntegrations.Instance.AddEvent(l_NewEvent); + + VController.CloseModal(this); + + try { m_Callback?.Invoke(l_NewEvent); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[ChatPlexMod_ChatIntegrations.UI][EventTemplateModal.OnCreateButton] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + else + { + VController.CloseModal(this); + VController.ShowMessageModal("No template selected!"); + } + } + catch (System.Exception l_Exception) + { + VController.CloseModal(this); + VController.ShowMessageModal("Unknown error!"); + + ChatPlexSDK.Logger.Error($"[ChatPlexMod_ChatIntegrations.UI][EventCreateModal.OnCreateButton] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsLeftView.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsLeftView.cs new file mode 100644 index 0000000..51b0524 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsLeftView.cs @@ -0,0 +1,81 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace ChatPlexMod_ChatIntegrations.UI +{ + /// + /// Settings left view controller + /// + internal sealed class SettingsLeftView : CP_SDK.UI.ViewController + { + private static readonly string s_InformationStr = + "Special thanks to HypersonicSharkz#3301 for help on TwitchAPI and some Actions code!" + + "\n" + "This module allow you execute actions on your game when triggered by events." + + "\n" + "" + + "\n" + "Events" + + "\n" + "- ChatBits\nWhen someone spends bits your channel!" + + "\n" + "- ChatCommand\nAllow you to create chat commands and execute actions with them" + + "\n" + "- ChatFollow\nWhen someone follows your channel" + + "\n" + "- ChatPointsReward\nAllow you to create channel points rewards and fully configure them and bind some actions to them" + + "\n" + "- ChatSubscription\nWhen someone subscribes or subgifts" + + "\n" + "- Dummy\nDummy event that can get triggered by other events" + + "\n" + "- LevelEnded\nWhen you exit a map" + + "\n" + "- LevelPaused\nWhen you pause a map" + + "\n" + "- LevelResumed\nWhen you resume a map" + + "\n" + "- LevelStarted\nWhen you enter a map" + + "\n" + "- VoiceAttackCommand\nBind VoiceAttack commands to BS+" + + "\n" + "" + + "\n" + ""; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Information"), + + Templates.ScrollableInfos(50, + XUIText.Make(s_InformationStr) + .SetAlign(TMPro.TextAlignmentOptions.Left) + ), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("Web Configuration", OnWebConfigurationButton) + ), + Templates.ExpandedButtonsLine( + XUISecondaryButton.Make("Documentation", OnDocumentationButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Open web configuration button + /// + private void OnWebConfigurationButton() + { + ShowMessageModal("URL opened in your web browser."); + CP_SDK.Chat.Service.OpenWebConfiguration(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Documentation button + /// + private void OnDocumentationButton() + { + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL(ChatIntegrations.Instance.DocumentationURL); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsMainView.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsMainView.cs new file mode 100644 index 0000000..a0cc1f5 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsMainView.cs @@ -0,0 +1,704 @@ +using CP_SDK.Unity.Extensions; +using CP_SDK.XUI; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.UI; + +namespace ChatPlexMod_ChatIntegrations.UI +{ + /// + /// Settings main view controller + /// + public partial class SettingsMainView : CP_SDK.UI.ViewController + { + private XUIVLayout m_EmptyFrame = null; + private XUIVLayout m_MainFrame = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUITabControl m_TabControl = null; + + private XUIVLayout m_TriggerTab_Content = null; + + private XUIVVList m_ConditionsTab_List = null; + private XUIVLayout m_ConditionsTab_Content = null; + + private XUIVVList m_OnSuccessActionsTab_List = null; + private XUIVLayout m_OnSuccessActionsTab_Content = null; + + private XUIVVList m_OnFailActionsTab_List = null; + private XUIVLayout m_OnFailActionsTab_Content = null; + + private Modals.AddXModal m_AddXModal = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private Interfaces.IEventBase m_CurrentEvent = null; + + private List m_ConditionsTab_Items = new List(); + private List m_OnSuccessActionsTab_Items = new List(); + private List m_OnFailActionsTab_Items = new List(); + + private Data.ConditionListItem m_SelectedConditionListItem = null; + private Data.ActionListItem m_SelectedOnSuccessActionListItem = null; + private Data.ActionListItem m_SelectedOnFailActionListItem = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override void OnViewCreation() + { + Templates.FullRectLayoutMainView( + Templates.TitleBar("Chat Integrations | Settings"), + + XUIText.Make("Please select an event to edit on right screen") + .SetFontSize(4.5f) + ) + .SetBackground(true, null, true) + .Bind(ref m_EmptyFrame) + .BuildUI(transform); + + m_EmptyFrame.SetActive(false); + + Templates.FullRectLayoutMainView( + Templates.TitleBar("Chat Integrations | Settings"), + + XUITabControl.Make( + ("Trigger", BuildTriggerTab()), + ("Conditions", BuildConditionsTab()), + ("On Success Actions", BuildOnSuccessActionsTab()), + ("On Fail Actions", BuildOnFailActionsTab()) + ) + .Bind(ref m_TabControl) + ) + .SetBackground(true, null, true) + .Bind(ref m_MainFrame) + .BuildUI(transform); + + m_MainFrame.SetActive(false); + + m_AddXModal = CreateModal(); + + /// Select a null event to hide everything + SelectEvent(null); + } + /// + /// On view deactivation + /// + protected override void OnViewDeactivation() + { + var l_Instance = ChatIntegrations.Instance; + if (l_Instance != null) + { + l_Instance.SaveDatabase(); + l_Instance.OnBroadcasterChatMessage = null; + l_Instance.OnVoiceAttackCommandExecuted = null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build trigger tab + /// + /// + private IXUIElement BuildTriggerTab() + { + return XUIVLayout.Make( + + ) + .SetSpacing(0).SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true) + .Bind(ref m_TriggerTab_Content); + } + /// + /// Build conditions tab + /// + /// + private IXUIElement BuildConditionsTab() + { + return XUIHLayout.Make( + XUIVLayout.Make( + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(CP_SDK.UI.Data.ListCellPrefabs.Get()) + .OnListItemSelected((x) => OnConditionSelected(x)) + .Bind(ref m_ConditionsTab_List) + ) + .SetSpacing(0).SetPadding(0) + .SetHeight(45) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + XUIHLayout.Make( + XUISecondaryButton.Make("▼", OnConditionMoveDownPressed).SetWidth(22.0f), + XUISecondaryButton.Make("▲", OnConditionMoveUpPressed) .SetWidth(22.0f) + ) + .SetPadding(0), + + XUIHLayout.Make( + XUIPrimaryButton .Make("+", OnConditionTabCreate) .SetWidth(10.0f), + XUISecondaryButton.Make("Toggle", OnConditionToggleButton).SetWidth(22.0f), + XUISecondaryButton.Make("-", OnConditionDeleteButton).SetWidth(10.0f) + ) + .SetPadding(0) + ) + .SetPadding(0) + .SetWidth(50.0f) + .SetBackground(true), + + XUIVLayout.Make( + + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.VLayoutGroup.childForceExpandWidth = x.VLayoutGroup.childForceExpandHeight = true) + .OnReady(x => x.LElement.flexibleWidth = 1000.0f) + .Bind(ref m_ConditionsTab_Content) + ) + .SetSpacing(0).SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true); + } + /// + /// Build on success actions tab + /// + /// + private IXUIElement BuildOnSuccessActionsTab() + { + return XUIHLayout.Make( + XUIVLayout.Make( + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(CP_SDK.UI.Data.ListCellPrefabs.Get()) + .OnListItemSelected((x) => OnOnSuccessActionSelected(x)) + .Bind(ref m_OnSuccessActionsTab_List) + ) + .SetSpacing(0).SetPadding(0) + .SetHeight(45) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + XUIHLayout.Make( + XUISecondaryButton.Make("▼", OnOnSuccessActionMoveDownPressed).SetWidth(22.0f), + XUISecondaryButton.Make("▲", OnOnSuccessActionMoveUpPressed) .SetWidth(22.0f) + ) + .SetPadding(0), + + XUIHLayout.Make( + XUIPrimaryButton .Make("+", OnOnSuccessActionTabCreate) .SetWidth(10.0f), + XUISecondaryButton.Make("Toggle", OnOnSuccessActionToggleButton).SetWidth(22.0f), + XUISecondaryButton.Make("-", OnOnSuccessActionDeleteButton).SetWidth(10.0f) + ) + .SetPadding(0) + ) + .SetPadding(0) + .SetWidth(50.0f) + .SetBackground(true), + + XUIVLayout.Make( + + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.VLayoutGroup.childForceExpandWidth = x.VLayoutGroup.childForceExpandHeight = true) + .OnReady(x => x.LElement.flexibleWidth = 1000.0f) + .Bind(ref m_OnSuccessActionsTab_Content) + ) + .SetSpacing(0).SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true); + } + /// + /// Build on fail actions tab + /// + /// + private IXUIElement BuildOnFailActionsTab() + { + return XUIHLayout.Make( + XUIVLayout.Make( + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(CP_SDK.UI.Data.ListCellPrefabs.Get()) + .OnListItemSelected((x) => OnOnFailActionSelected(x)) + .Bind(ref m_OnFailActionsTab_List) + ) + .SetSpacing(0).SetPadding(0) + .SetHeight(45) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + XUIHLayout.Make( + XUISecondaryButton.Make("▼", OnOnFailActionMoveDownPressed).SetWidth(22.0f), + XUISecondaryButton.Make("▲", OnOnFailActionMoveUpPressed) .SetWidth(22.0f) + ) + .SetPadding(0), + + XUIHLayout.Make( + XUIPrimaryButton .Make("+", OnOnFailActionTabCreate) .SetWidth(10.0f), + XUISecondaryButton.Make("Toggle", OnOnFailActionToggleButton).SetWidth(22.0f), + XUISecondaryButton.Make("-", OnOnFailActionDeleteButton).SetWidth(10.0f) + ) + .SetPadding(0) + ) + .SetPadding(0) + .SetWidth(50.0f) + .SetBackground(true), + + XUIVLayout.Make( + + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.VLayoutGroup.childForceExpandWidth = x.VLayoutGroup.childForceExpandHeight = true) + .OnReady(x => x.LElement.flexibleWidth = 1000.0f) + .Bind(ref m_OnFailActionsTab_Content) + ) + .SetSpacing(0).SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Select event to edit + /// + /// + internal void SelectEvent(Interfaces.IEventBase p_Event) + { + ChatIntegrations.Instance.OnBroadcasterChatMessage = null; + ChatIntegrations.Instance.OnVoiceAttackCommandExecuted = null; + + CloseAllModals(); + + m_CurrentEvent = p_Event; + + /// Clean up trigger specific UI + m_TriggerTab_Content.Element.gameObject.DestroyChilds(); + + /// Hide everything if no event selection + if (p_Event == null) + { + m_MainFrame.SetActive(false); + m_EmptyFrame.SetActive(true); + return; + } + + p_Event.BuildUI(m_TriggerTab_Content.Element.RTransform); + + RebuildConditionList(m_CurrentEvent.Conditions.FirstOrDefault()); + RebuildOnSuccessActionList(m_CurrentEvent.OnSuccessActions.FirstOrDefault()); + RebuildOnFailActionList(m_CurrentEvent.OnFailActions.FirstOrDefault()); + + /// Update UI + m_EmptyFrame.SetActive(false); + m_MainFrame.SetActive(true); + + /// Force first tab to be active + m_TabControl.SetActiveTab(0); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Rebuilt condition list + /// + /// To focus + private void RebuildConditionList(Interfaces.IConditionBase p_ConditionToFocus) + { + if (!UICreated) + return; + + m_ConditionsTab_Items = m_CurrentEvent.Conditions.Select(x => new Data.ConditionListItem(x)).ToList(); + m_ConditionsTab_List.SetListItems(m_ConditionsTab_Items); + m_ConditionsTab_List.SetSelectedListItem(m_ConditionsTab_Items.FirstOrDefault(x => x.Condition == p_ConditionToFocus) ?? m_ConditionsTab_Items.FirstOrDefault()); + } + /// + /// When a condition is selected + /// + /// Selected item + private void OnConditionSelected(CP_SDK.UI.Data.IListItem p_SelectedItem) + { + /// Clean up condition specific UI + m_ConditionsTab_Content.Element.gameObject.DestroyChilds(); + + m_SelectedConditionListItem = p_SelectedItem as Data.ConditionListItem; + if (m_SelectedConditionListItem == null) + return; + + m_SelectedConditionListItem.Condition.BuildUI(m_ConditionsTab_Content.Element.RTransform); + } + /// + /// Move condition down + /// + private void OnConditionMoveDownPressed() + { + if (m_SelectedConditionListItem == null) + return; + + m_CurrentEvent.MoveCondition(m_SelectedConditionListItem.Condition, false); + RebuildConditionList(m_SelectedConditionListItem.Condition); + } + /// + /// Move condition up + /// + private void OnConditionMoveUpPressed() + { + if (m_SelectedConditionListItem == null) + return; + + m_CurrentEvent.MoveCondition(m_SelectedConditionListItem.Condition, true); + RebuildConditionList(m_SelectedConditionListItem.Condition); + } + /// + /// On condition create button + /// + private void OnConditionTabCreate() + { + ShowModal(m_AddXModal); + m_AddXModal.Init(m_CurrentEvent.AvailableConditions, (p_Type) => + { + var l_Condition = ChatIntegrations.CreateCondition(p_Type); + l_Condition.Event = m_CurrentEvent; + l_Condition.IsEnabled = true; + + m_CurrentEvent.AddCondition(l_Condition); + + var l_ListItem = new Data.ConditionListItem(l_Condition); + m_ConditionsTab_List.AddListItem(l_ListItem); + m_ConditionsTab_List.SetSelectedListItem(l_ListItem); + }); + } + /// + /// Toggle condition button + /// + private void OnConditionToggleButton() + { + if (m_SelectedConditionListItem == null) + { + ShowMessageModal("Please select a condition first!"); + return; + } + + var l_Condition = m_SelectedConditionListItem.Condition; + if (l_Condition.IsEnabled) + { + ShowConfirmationModal($"Do you want to disable condition\n\"{l_Condition.GetTypeName()}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + l_Condition.IsEnabled = false; + m_SelectedConditionListItem.Refresh(); + }); + } + else + { + ShowConfirmationModal($"Do you want to enable condition\n\"{l_Condition.GetTypeName()}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + l_Condition.IsEnabled = true; + m_SelectedConditionListItem.Refresh(); + }); + } + } + /// + /// On delete condition button + /// + private void OnConditionDeleteButton() + { + if (m_SelectedConditionListItem == null) + { + ShowMessageModal("Please select a condition first!"); + return; + } + + var l_Condition = m_SelectedConditionListItem.Condition; + ShowConfirmationModal($"Do you want to delete condition\n\"{l_Condition.GetTypeName()}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + m_ConditionsTab_Items.Remove(m_SelectedConditionListItem); + m_ConditionsTab_List.RemoveListItem(m_SelectedConditionListItem); + l_Condition.Event.DeleteCondition(l_Condition); + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Rebuilt action list + /// + /// Should keep actual focus + private void RebuildOnSuccessActionList(Interfaces.IActionBase p_ActionToFocus) + { + if (!UICreated) + return; + + var l_Items = m_CurrentEvent.OnSuccessActions.Select(x => new Data.ActionListItem(x)).ToList(); + m_OnSuccessActionsTab_List.SetListItems(l_Items); + m_OnSuccessActionsTab_List.SetSelectedListItem(l_Items.FirstOrDefault(x => x.Action == p_ActionToFocus) ?? l_Items.FirstOrDefault()); + } + /// + /// When an action is selected + /// + /// Selected item + private void OnOnSuccessActionSelected(CP_SDK.UI.Data.IListItem p_SelectedItem) + { + /// Clean up condition specific UI + m_OnSuccessActionsTab_Content.Element.gameObject.DestroyChilds(); + + m_SelectedOnSuccessActionListItem = p_SelectedItem as Data.ActionListItem; + if (m_SelectedOnSuccessActionListItem == null) + return; + + m_SelectedOnSuccessActionListItem.Action.BuildUI(m_OnSuccessActionsTab_Content.Element.RTransform); + } + /// + /// Move action down + /// + private void OnOnSuccessActionMoveDownPressed() + { + if (m_SelectedOnSuccessActionListItem == null) + return; + + m_CurrentEvent.MoveOnSuccessAction(m_SelectedOnSuccessActionListItem.Action, false); + RebuildOnSuccessActionList(m_SelectedOnSuccessActionListItem.Action); + } + /// + /// Move action up + /// + private void OnOnSuccessActionMoveUpPressed() + { + if (m_SelectedOnSuccessActionListItem == null) + return; + + m_CurrentEvent.MoveOnSuccessAction(m_SelectedOnSuccessActionListItem.Action, true); + RebuildOnSuccessActionList(m_SelectedOnSuccessActionListItem.Action); + } + /// + /// On create on success action button + /// + private void OnOnSuccessActionTabCreate() + { + ShowModal(m_AddXModal); + m_AddXModal.Init(m_CurrentEvent.AvailableActions, (p_Type) => + { + var l_Action = ChatIntegrations.CreateAction(p_Type); + l_Action.Event = m_CurrentEvent; + l_Action.IsEnabled = true; + + m_CurrentEvent.AddOnSuccessAction(l_Action); + + var l_ListItem = new Data.ActionListItem(l_Action); + m_OnSuccessActionsTab_List.AddListItem(l_ListItem); + m_OnSuccessActionsTab_List.SetSelectedListItem(l_ListItem); + }); + } + /// + /// Toggle action button + /// + private void OnOnSuccessActionToggleButton() + { + if (m_SelectedOnSuccessActionListItem == null) + { + ShowMessageModal("Please select an action first!"); + return; + } + + var l_OnSuccessAction = m_SelectedOnSuccessActionListItem.Action; + if (l_OnSuccessAction.IsEnabled) + { + ShowConfirmationModal($"Do you want to disable action\n\"{l_OnSuccessAction.GetTypeName()}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + l_OnSuccessAction.IsEnabled = false; + m_SelectedOnSuccessActionListItem.Refresh(); + }); + } + else + { + ShowConfirmationModal($"Do you want to enable action\n\"{l_OnSuccessAction.GetTypeName()}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + l_OnSuccessAction.IsEnabled = true; + m_SelectedOnSuccessActionListItem.Refresh(); + }); + } + } + /// + /// On delete action button + /// + private void OnOnSuccessActionDeleteButton() + { + if (m_SelectedOnSuccessActionListItem == null) + { + ShowMessageModal("Please select an action first!"); + return; + } + + var l_OnSuccessAction = m_SelectedOnSuccessActionListItem.Action; + ShowConfirmationModal($"Do you want to delete action\n\"{l_OnSuccessAction.GetTypeName()}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + m_OnSuccessActionsTab_Items.Remove(m_SelectedOnSuccessActionListItem); + m_OnSuccessActionsTab_List.RemoveListItem(m_SelectedOnSuccessActionListItem); + l_OnSuccessAction.Event.DeleteOnSuccessAction(l_OnSuccessAction); + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Rebuilt action list + /// + /// Should keep actual focus + private void RebuildOnFailActionList(Interfaces.IActionBase p_OnFailActionToFocus) + { + if (!UICreated) + return; + + var l_Items = m_CurrentEvent.OnFailActions.Select(x => new Data.ActionListItem(x)).ToList(); + m_OnFailActionsTab_List.SetListItems(l_Items); + m_OnFailActionsTab_List.SetSelectedListItem(l_Items.FirstOrDefault(x => x.Action == p_OnFailActionToFocus) ?? l_Items.FirstOrDefault()); + } + /// + /// When an on fail action is selected + /// + /// Selected item + private void OnOnFailActionSelected(CP_SDK.UI.Data.IListItem p_SelectedItem) + { + /// Clean up condition specific UI + m_OnFailActionsTab_Content.Element.gameObject.DestroyChilds(); + + m_SelectedOnFailActionListItem = p_SelectedItem as Data.ActionListItem; + if (m_SelectedOnFailActionListItem == null) + return; + + m_SelectedOnFailActionListItem.Action.BuildUI(m_OnFailActionsTab_Content.Element.RTransform); + } + /// + /// Move on fail action down + /// + private void OnOnFailActionMoveDownPressed() + { + if (m_SelectedOnFailActionListItem == null) + return; + + m_CurrentEvent.MoveOnFailAction(m_SelectedOnFailActionListItem.Action, false); + RebuildOnSuccessActionList(m_SelectedOnFailActionListItem.Action); + } + /// + /// Move on fail action up + /// + private void OnOnFailActionMoveUpPressed() + { + if (m_SelectedOnFailActionListItem == null) + return; + + m_CurrentEvent.MoveOnFailAction(m_SelectedOnFailActionListItem.Action, true); + RebuildOnSuccessActionList(m_SelectedOnFailActionListItem.Action); + } + /// + /// On create on fail action button + /// + private void OnOnFailActionTabCreate() + { + ShowModal(m_AddXModal); + m_AddXModal.Init(m_CurrentEvent.AvailableActions, (p_Type) => + { + var l_Action = ChatIntegrations.CreateAction(p_Type); + l_Action.Event = m_CurrentEvent; + l_Action.IsEnabled = true; + + m_CurrentEvent.AddOnFailAction(l_Action); + + var l_ListItem = new Data.ActionListItem(l_Action); + m_OnFailActionsTab_List.AddListItem(l_ListItem); + m_OnFailActionsTab_List.SetSelectedListItem(l_ListItem); + }); + } + /// + /// Toggle on fail action button + /// + private void OnOnFailActionToggleButton() + { + if (m_SelectedOnFailActionListItem == null) + { + ShowMessageModal("Please select an action first!"); + return; + } + + var l_OnFailAction = m_SelectedOnFailActionListItem.Action; + if (l_OnFailAction.IsEnabled) + { + ShowConfirmationModal($"Do you want to disable action\n\"{l_OnFailAction.GetTypeName()}\"?", (p_Confirm) => + { + l_OnFailAction.IsEnabled = false; + m_SelectedOnFailActionListItem.Refresh(); + }); + } + else + { + ShowConfirmationModal($"Do you want to enable action\n\"{l_OnFailAction.GetTypeName()}\"?", (p_Confirm) => + { + l_OnFailAction.IsEnabled = true; + m_SelectedOnFailActionListItem.Refresh(); + }); + } + } + /// + /// On delete on fail action button + /// + private void OnOnFailActionDeleteButton() + { + if (m_SelectedOnFailActionListItem == null) + { + ShowMessageModal("Please select an action first!"); + return; + } + + var l_OnFailAction = m_SelectedOnFailActionListItem.Action; + ShowConfirmationModal($"Do you want to delete action\n\"{l_OnFailAction.GetTypeName()}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + m_OnFailActionsTab_Items.Remove(m_SelectedOnFailActionListItem); + m_OnFailActionsTab_List.RemoveListItem(m_SelectedOnFailActionListItem); + l_OnFailAction.Event.DeleteOnFailAction(l_OnFailAction); + }); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsRightView.cs b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsRightView.cs new file mode 100644 index 0000000..0881751 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatIntegrations/ChatPlexMod_ChatIntegrations/UI/SettingsRightView.cs @@ -0,0 +1,370 @@ +using CP_SDK.XUI; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.UI; + +namespace ChatPlexMod_ChatIntegrations.UI +{ + /// + /// Settings right view controller + /// + internal sealed class SettingsRightView : CP_SDK.UI.ViewController + { + private XUIDropdown m_Filter = null; + private XUIVVList m_Events = null; + + private Modals.EventCreateModal m_EventCreateModal = null; + private Modals.EventImportModal m_EventImportModal = null; + private Modals.EventTemplateModal m_EventTemplateModal = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private List m_FilteredList = new List(); + private Data.EventListItem m_SelectedListItem = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override void OnViewCreation() + { + Templates.FullRectLayout( + XUIHLayout.Make( + XUIText.Make("Filter"), + XUIDropdown.Make() + .SetOptions(new List() { "All" }.Union(ChatIntegrations.RegisteredEventTypes).ToList()) + .OnValueChanged((_, x) => OnFilterChanged(x)) + .Bind(ref m_Filter) + ) + .OnReady(x => x.CSizeFitter.enabled = false) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true), + + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(CP_SDK.UI.Data.ListCellPrefabs.Get()) + .OnListItemSelected(OnEventSelected) + .Bind(ref m_Events) + ) + .SetHeight(50) + .SetSpacing(0) + .SetPadding(0) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + XUIHLayout.Make("ExpandedButtonsLine", + XUIPrimaryButton.Make("New") .OnClick(OnNewButton), + XUIPrimaryButton.Make("Rename") .OnClick(OnRenameButton), + XUIPrimaryButton.Make("Toggle") .OnClick(OnToggleButton), + XUIPrimaryButton.Make("Delete") .OnClick(OnDeleteButton) + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .ForEachDirect(y => y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained)), + + XUIHLayout.Make("ExpandedButtonsLine", + XUISecondaryButton.Make("Export") .OnClick(OnExportButton), + XUISecondaryButton.Make("Import") .OnClick(OnImportButton), + XUISecondaryButton.Make("Clone") .OnClick(OnCloneButton), + XUISecondaryButton.Make("Templates").OnClick(OnTemplatesButton), + XUISecondaryButton.Make("Convert") .OnClick(OnConvertButton) + ) + .SetPadding(0) + .OnReady(x => x.CSizeFitter.enabled = false) + .ForEachDirect(y => y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained)) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + + m_EventCreateModal = CreateModal(); + m_EventImportModal = CreateModal(); + m_EventTemplateModal = CreateModal(); + + m_Filter.SetValue("All"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On filter changed + /// + /// New value + private void OnFilterChanged(string p_Value) + { + m_FilteredList.Clear(); + var l_Events = ChatIntegrations.Instance.Events; + var l_EventCount = l_Events.Count; + for (var l_I = 0; l_I < l_EventCount; ++l_I) + { + var l_Event = l_Events[l_I]; + if ((p_Value == null || p_Value == "All") || l_Event.GetTypeName() == p_Value) + m_FilteredList.Add(new Data.EventListItem(l_Event)); + } + m_FilteredList.Sort((x, y) => (x.Event.GetTypeName() + x.Event.GenericModel.Name).CompareTo((y.Event.GetTypeName() + y.Event.GenericModel.Name))); + + m_Events.SetListItems(m_FilteredList); + } + /// + /// On event selected + /// + /// Selected item + private void OnEventSelected(CP_SDK.UI.Data.IListItem p_SelectedItem) + { + m_SelectedListItem = p_SelectedItem as Data.EventListItem; + if (m_SelectedListItem == null) + { + SettingsMainView.Instance?.SelectEvent(null); + return; + } + + SettingsMainView.Instance?.SelectEvent(m_SelectedListItem.Event); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// New event button + /// + private void OnNewButton() + { + ShowModal(m_EventCreateModal); + m_EventCreateModal.Init((p_CreatedEvent) => + { + if (p_CreatedEvent == null) + return; + + if (m_Filter.Element.GetValue() == "All" || m_Filter.Element.GetValue() == p_CreatedEvent.GetTypeName()) + { + var l_NewItem = new Data.EventListItem(p_CreatedEvent); + m_FilteredList.Add(l_NewItem); + m_Events.AddListItem(l_NewItem); + m_Events.SetSelectedListItem(l_NewItem); + } + else + m_Filter.SetValue(p_CreatedEvent.GetTypeName()); + }); + } + /// + /// Rename event button + /// + private void OnRenameButton() + { + if (!EnsureEventSelected()) + return; + + ShowKeyboardModal(m_SelectedListItem.Event.GenericModel.Name, (x) => + { + m_SelectedListItem.Event.GenericModel.Name = x; + m_SelectedListItem.Refresh(); + }); + } + /// + /// Toggle enable/disable on a event + /// + private void OnToggleButton() + { + if (!EnsureEventSelected()) + return; + + if (m_SelectedListItem.Event.IsEnabled) + { + ShowConfirmationModal($"Do you want to disable event\n\"{m_SelectedListItem.Event.GenericModel.Name}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + ChatIntegrations.Instance.ToggleEvent(m_SelectedListItem.Event); + m_SelectedListItem.Refresh(); + }); + } + else + { + ShowConfirmationModal($"Do you want to enable event\n\"{m_SelectedListItem.Event.GenericModel.Name}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + ChatIntegrations.Instance.ToggleEvent(m_SelectedListItem.Event); + m_SelectedListItem.Refresh(); + }); + } + } + /// + /// Delete event button + /// + private void OnDeleteButton() + { + if (!EnsureEventSelected()) + return; + + ShowConfirmationModal($"Do you want to delete event\n\"{m_SelectedListItem.Event.GenericModel.Name}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + ChatIntegrations.Instance.DeleteEvent(m_SelectedListItem.Event); + + m_FilteredList.Remove(m_SelectedListItem); + m_Events.RemoveListItem(m_SelectedListItem); + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Export an event + /// + private void OnExportButton() + { + if (!EnsureEventSelected()) + return; + + var l_Event = m_SelectedListItem.Event; + var l_Serialized = l_Event.Serialize(); + + var l_EventName = l_Event.GenericModel.Name; + if (l_EventName.Length > 20) + l_EventName = l_EventName.Substring(0, 20); + + var l_FileName = CP_SDK.Misc.Time.UnixTimeNow() + "_" + l_Event.GetTypeName() + "_" + l_EventName + ".bspci"; + l_FileName = string.Concat(l_FileName.Split(System.IO.Path.GetInvalidFileNameChars())); + + System.IO.File.WriteAllText(ChatIntegrations.s_EXPORT_PATH + l_FileName, l_Serialized.ToString(Newtonsoft.Json.Formatting.Indented), System.Text.Encoding.Unicode); + + ShowMessageModal("Event exported in\n" + ChatIntegrations.s_EXPORT_PATH); + } + /// + /// Import an event + /// + private void OnImportButton() + { + ShowModal(m_EventImportModal); + m_EventImportModal.Init((System.Action)((p_ImportedEvent) => + { + if (p_ImportedEvent == null) + return; + + if (m_Filter.Element.GetValue() == "All" || m_Filter.Element.GetValue() == p_ImportedEvent.GetTypeName()) + { + var l_NewItem = new Data.EventListItem(p_ImportedEvent); + m_FilteredList.Add(l_NewItem); + m_Events.AddListItem(l_NewItem); + m_Events.SetSelectedListItem(l_NewItem); + } + else + m_Filter.SetValue(p_ImportedEvent.GetTypeName()); + })); + } + /// + /// Clone an event + /// + private void OnCloneButton() + { + if (!EnsureEventSelected()) + return; + + var l_Event = m_SelectedListItem.Event; + var l_Serialized = l_Event.Serialize(); + var l_NewEvent = ChatIntegrations.Instance.AddEventFromSerialized(l_Serialized, false, true, out var _); + + if (l_NewEvent == null) + ShowMessageModal("Clone failed, check logs!"); + else + { + var l_NewItem = new Data.EventListItem(l_NewEvent); + m_FilteredList.Add(l_NewItem); + m_Events.AddListItem(l_NewItem); + m_Events.SetSelectedListItem(l_NewItem); + } + } + /// + /// Template event + /// + private void OnTemplatesButton() + { + ShowModal(m_EventTemplateModal); + m_EventTemplateModal.Init((p_CreatedEvent) => + { + if (p_CreatedEvent == null) + return; + + if (m_Filter.Element.GetValue() == "All" || m_Filter.Element.GetValue() == p_CreatedEvent.GetTypeName()) + { + var l_NewItem = new Data.EventListItem(p_CreatedEvent); + m_FilteredList.Add(l_NewItem); + m_Events.AddListItem(l_NewItem); + m_Events.SetSelectedListItem(l_NewItem); + } + else + m_Filter.SetValue(p_CreatedEvent.GetTypeName()); + }); + } + /// + /// Convert an event + /// + private void OnConvertButton() + { + if (!EnsureEventSelected()) + return; + + var l_Event = m_SelectedListItem.Event; + if (l_Event is Events.Dummy) + { + ShowMessageModal("This event is already a dummy event!"); + return; + } + + ShowConfirmationModal($"Do you want to convert event\n\"{l_Event.GenericModel.Name}\" to Dummy?", (p_Confirm) => + { + if (!p_Confirm) + return; + + var l_Serialized = l_Event.Serialize(); + l_Serialized["Type"] = string.Join(".", typeof(Events.Dummy).Namespace, typeof(Events.Dummy).Name); + l_Serialized["Event"]["Type"] = string.Join(".", typeof(Events.Dummy).Namespace, typeof(Events.Dummy).Name); + l_Serialized["Event"]["Name"] += " (Converted)"; + + var l_NewEvent = ChatIntegrations.Instance.AddEventFromSerialized(l_Serialized, false, true, out var _); + if (l_NewEvent == null) + ShowMessageModal("Clone failed, check logs!"); + else + { + if (m_Filter.Element.GetValue() == "All" || m_Filter.Element.GetValue() == l_NewEvent.GetTypeName()) + { + var l_NewItem = new Data.EventListItem(l_NewEvent); + m_FilteredList.Add(l_NewItem); + m_Events.AddListItem(l_NewItem); + m_Events.SetSelectedListItem(l_NewItem); + } + else + m_Filter.SetValue(l_NewEvent.GetTypeName()); + } + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Ensure that an event is selected + /// + /// + private bool EnsureEventSelected() + { + if (m_SelectedListItem == null) + { + ShowMessageModal("Please select an event first!"); + return false; + } + + return true; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Bits.cs b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Bits.cs deleted file mode 100644 index 67b6a31..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Bits.cs +++ /dev/null @@ -1,65 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Conditions -{ - public class Bits_Amount : Interfaces.ICondition - { - public override string Description => "Add conditions on chat request queue size!"; - -#pragma warning disable CS0414 - [UIComponent("CheckTypeList")] - private ListSetting m_CheckTypeList = null; - [UIValue("CheckTypeList_Choices")] - private List m_CheckTypeList_Choices = new List() { "Greater than", "Less than" }; - [UIValue("CheckTypeList_Value")] - private string m_CheckTypeList_Value; - [UIComponent("CountSlider")] - private SliderSetting m_CountSlider = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_CheckTypeList_Value = (string)m_CheckTypeList_Choices.ElementAt(Model.IsGreaterThan ? 0 : 1); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_CheckTypeList, l_Event, false); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_CountSlider, l_Event, null, Model.Count, true, true, new Vector2(0.08f, 0.10f), new Vector2(0.93f, 0.90f)); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.IsGreaterThan = m_CheckTypeList_Choices.Select(x => (string)x).ToList().IndexOf(m_CheckTypeList.Value) == 0; - Model.Count = (uint)m_CountSlider.slider.value; - } - - public override bool Eval(Models.EventContext p_Context) - { - if (!p_Context.BitsEvent.HasValue) - return false; - - if (Model.IsGreaterThan) - { - if (p_Context.BitsEvent.Value > Model.Count) - return true; - } - else - { - if (p_Context.BitsEvent.Value < Model.Count) - return true; - } - - return false; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/ChatRequest.cs b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/ChatRequest.cs deleted file mode 100644 index a979273..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/ChatRequest.cs +++ /dev/null @@ -1,249 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Conditions -{ - public class ChatRequest_QueueDuration : Interfaces.ICondition - { - public override string Description => "Add conditions on chat request queue duration!"; - -#pragma warning disable CS0414 - [UIComponent("CheckTypeList")] - private ListSetting m_CheckTypeList = null; - [UIValue("CheckTypeList_Choices")] - private List m_CheckTypeList_Choices = new List() { "Greater than", "Less than" }; - [UIValue("CheckTypeList_Value")] - private string m_CheckTypeList_Value; - [UIComponent("DurationSlider")] - private SliderSetting m_DurationSlider = null; - [UIComponent("SendMessageOnFailToggle")] - private ToggleSetting m_SendMessageOnFailToggle = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_CheckTypeList_Value = (string)m_CheckTypeList_Choices.ElementAt(Model.IsGreaterThan ? 0 : 1); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_CheckTypeList, l_Event, false); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_DurationSlider, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Time, Model.Duration, true, true, new Vector2(0.08f, 0.10f), new Vector2(0.93f, 0.90f)); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SendMessageOnFailToggle, l_Event, Model.SendChatMessageOnFail, false); - - OnSettingChanged(null); - - if (!ModulePresence.ChatRequest) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: ChatRequest module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.IsGreaterThan = m_CheckTypeList_Choices.Select(x => (string)x).ToList().IndexOf(m_CheckTypeList.Value) == 0; - Model.Duration = (uint)m_DurationSlider.slider.value; - Model.SendChatMessageOnFail = m_SendMessageOnFailToggle.Value; - } - - public override bool Eval(Models.EventContext p_Context) - { - if (!ModulePresence.ChatRequest) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatRequest module is missing!"); - return false; - } - - var l_ChatRequest = BeatSaberPlus_ChatRequest.ChatRequest.Instance; - - if (l_ChatRequest == null || !l_ChatRequest.IsEnabled) - { - if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} chat request is not enabled"); - - return false; - } - - if (Model.IsGreaterThan) - { - if (l_ChatRequest.QueueDuration > Model.Duration) - return true; - - if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is too short"); - } - else - { - if (l_ChatRequest.QueueDuration < Model.Duration) - return true; - - if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is too long"); - } - - return false; - } - } - - public class ChatRequest_QueueSize : Interfaces.ICondition - { - public override string Description => "Add conditions on chat request queue size!"; - -#pragma warning disable CS0414 - [UIComponent("CheckTypeList")] - private ListSetting m_CheckTypeList = null; - [UIValue("CheckTypeList_Choices")] - private List m_CheckTypeList_Choices = new List() { "Greater than", "Less than" }; - [UIValue("CheckTypeList_Value")] - private string m_CheckTypeList_Value; - [UIComponent("CountSlider")] - private SliderSetting m_CountSlider = null; - [UIComponent("SendMessageOnFailToggle")] - private ToggleSetting m_SendMessageOnFailToggle = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_CheckTypeList_Value = (string)m_CheckTypeList_Choices.ElementAt(Model.IsGreaterThan ? 0 : 1); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_CheckTypeList, l_Event, false); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_CountSlider, l_Event, null, Model.Count, true, true, new Vector2(0.08f, 0.10f), new Vector2(0.93f, 0.90f)); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SendMessageOnFailToggle, l_Event, Model.SendChatMessageOnFail, false); - - OnSettingChanged(null); - - if (!ModulePresence.ChatRequest) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: ChatRequest module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.IsGreaterThan = m_CheckTypeList_Choices.Select(x => (string)x).ToList().IndexOf(m_CheckTypeList.Value) == 0; - Model.Count = (uint)m_CountSlider.slider.value; - Model.SendChatMessageOnFail = m_SendMessageOnFailToggle.Value; - } - - public override bool Eval(Models.EventContext p_Context) - { - if (!ModulePresence.ChatRequest) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatRequest module is missing!"); - return false; - } - - var l_ChatRequest = BeatSaberPlus_ChatRequest.ChatRequest.Instance; - - if (l_ChatRequest == null || !l_ChatRequest.IsEnabled) - { - if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} chat request is not enabled"); - - return false; - } - - int l_QueueSize = l_ChatRequest.SongQueueCount; - - if (Model.IsGreaterThan) - { - if (l_QueueSize > Model.Count) - return true; - - if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is too small"); - } - else - { - if (l_QueueSize < Model.Count) - return true; - - if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is too big"); - } - - return false; - } - } - - public class ChatRequest_QueueStatus : Interfaces.ICondition - { - public override string Description => "Add conditions on chat request queue!"; - -#pragma warning disable CS0414 - [UIComponent("StatusList")] - private ListSetting m_StatusList = null; - [UIValue("StatusList_Choices")] - private List m_StatusList_Choices = new List() { "Open", "Closed" }; - [UIValue("StatusList_Value")] - private string m_StatusList_Value; - [UIComponent("SendMessageOnFailToggle")] - private ToggleSetting m_SendMessageOnFailToggle = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_StatusList_Value = (string)m_StatusList_Choices.ElementAt(Model.IsOpen ? 0 : 1); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_StatusList, l_Event, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SendMessageOnFailToggle, l_Event, Model.SendChatMessageOnFail, false); - - OnSettingChanged(null); - - if (!ModulePresence.ChatRequest) - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: ChatRequest module is missing!"); - } - private void OnSettingChanged(object p_Value) - { - Model.IsOpen = m_StatusList_Choices.Select(x => (string)x).ToList().IndexOf(m_StatusList.Value) == 0; - Model.SendChatMessageOnFail = m_SendMessageOnFailToggle.Value; - } - - public override bool Eval(Models.EventContext p_Context) - { - if (!ModulePresence.ChatRequest) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Action failed, ChatRequest module is missing!"); - return false; - } - - var l_ChatRequest = BeatSaberPlus_ChatRequest.ChatRequest.Instance; - - if (l_ChatRequest == null || !l_ChatRequest.IsEnabled) - { - if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} chat request is not enabled"); - - return false; - } - - if (Model.IsOpen && !l_ChatRequest.QueueOpen) - { - if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is closed"); - - return false; - } - else if (!Model.IsOpen && l_ChatRequest.QueueOpen) - { - if (Model.SendChatMessageOnFail && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} queue is open"); - - return false; - } - - return true; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Event.cs b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Event.cs deleted file mode 100644 index 84ae21d..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Event.cs +++ /dev/null @@ -1,220 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System.Collections.Generic; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Conditions -{ - internal class EventBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - var l_Result = new List() - { - new Event_AlwaysFail(), - new Event_Disabled(), - new Event_Enabled(), - }; - - switch (p_Event) - { - default: - break; - } - - return l_Result; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class Event_AlwaysFail : Interfaces.ICondition - { - public override string Description => "Always fail the event"; - - public Event_AlwaysFail() => UIPlaceHolder = "Make the event to always fail"; - - public override bool Eval(Models.EventContext p_Context) - { - return false; - } - } - - public class Event_Disabled : Interfaces.ICondition - { - public override string Description => "Ensure that an event is disabled"; - -#pragma warning disable CS0414 - [UIComponent("Event_DropDown")] protected DropDownListSetting m_Event_DropDown = null; - [UIValue("Event_DropDownOptions")] private List m_Event_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - private Dictionary m_NameToGUID = new Dictionary(); - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Event_DropDown, l_Event, true); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - m_NameToGUID.Clear(); - - var l_Events = ChatIntegrations.Instance.GetEventsByType(null); - l_Events.Sort((x, y) => (x.GetTypeNameShort() + x.GenericModel.Name).CompareTo(y.GetTypeNameShort() + y.GenericModel.Name)); - foreach (var l_EventBase in l_Events) - { - var l_Line = BuildLineString(l_EventBase); - l_Choices.Add(l_Line); - m_NameToGUID.Add(l_Line, l_EventBase.GenericModel.GUID); - - if (Model.EventGUID != "" && l_EventBase.GenericModel.GUID == Model.EventGUID) - l_ChoiceIndex = l_Choices.Count - 1; - } - - m_Event_DropDownOptions = l_Choices; - m_Event_DropDown.values = l_Choices; - m_Event_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Event_DropDown.UpdateChoices(); - } - private void OnSettingChanged(object p_Value) - { - if ((string)p_Value == "None") - Model.EventGUID = ""; - - if (m_NameToGUID.TryGetValue((string)m_Event_DropDown.Value, out var l_SelectedGUID)) - Model.EventGUID = l_SelectedGUID; - else - { - Model.EventGUID = ""; - m_Event_DropDown.Value = m_Event_DropDownOptions[0]; - } - } - - public override bool Eval(Models.EventContext p_Context) - { - var l_SelectedEvent = string.IsNullOrEmpty(Model.EventGUID) ? null : ChatIntegrations.Instance.GetEventByGUID(Model.EventGUID); - if (l_SelectedEvent != null) - return !l_SelectedEvent.IsEnabled; - - return false; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build event line - /// - /// Event to build for - private string BuildLineString(Interfaces.IEventBase p_Event) - { - /// Result line - string l_Text = ""; - l_Text += ""; - - /// Left part - l_Text += "[" + p_Event.GetTypeNameShort() + "] "; - l_Text += p_Event.GenericModel.Name; - l_Text += ""; - - return l_Text; - } - } - public class Event_Enabled : Interfaces.ICondition - { - public override string Description => "Ensure that an event is enabled"; - -#pragma warning disable CS0414 - [UIComponent("Event_DropDown")] protected DropDownListSetting m_Event_DropDown = null; - [UIValue("Event_DropDownOptions")] private List m_Event_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - private Dictionary m_NameToGUID = new Dictionary(); - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Event_DropDown, l_Event, true); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - m_NameToGUID.Clear(); - - var l_Events = ChatIntegrations.Instance.GetEventsByType(null); - l_Events.Sort((x, y) => (x.GetTypeNameShort() + x.GenericModel.Name).CompareTo(y.GetTypeNameShort() + y.GenericModel.Name)); - foreach (var l_EventBase in l_Events) - { - var l_Line = BuildLineString(l_EventBase); - l_Choices.Add(l_Line); - m_NameToGUID.Add(l_Line, l_EventBase.GenericModel.GUID); - - if (Model.EventGUID != "" && l_EventBase.GenericModel.GUID == Model.EventGUID) - l_ChoiceIndex = l_Choices.Count - 1; - } - - m_Event_DropDownOptions = l_Choices; - m_Event_DropDown.values = l_Choices; - m_Event_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Event_DropDown.UpdateChoices(); - } - private void OnSettingChanged(object p_Value) - { - if ((string)p_Value == "None") - Model.EventGUID = ""; - - if (m_NameToGUID.TryGetValue((string)m_Event_DropDown.Value, out var l_SelectedGUID)) - Model.EventGUID = l_SelectedGUID; - else - { - Model.EventGUID = ""; - m_Event_DropDown.Value = m_Event_DropDownOptions[0]; - } - } - - public override bool Eval(Models.EventContext p_Context) - { - var l_SelectedEvent = string.IsNullOrEmpty(Model.EventGUID) ? null : ChatIntegrations.Instance.GetEventByGUID(Model.EventGUID); - if (l_SelectedEvent != null) - return l_SelectedEvent.IsEnabled; - - return false; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build event line - /// - /// Event to build for - private string BuildLineString(Interfaces.IEventBase p_Event) - { - /// Result line - string l_Text = ""; - l_Text += ""; - - /// Left part - l_Text += "[" + p_Event.GetTypeNameShort() + "] "; - l_Text += p_Event.GenericModel.Name; - l_Text += ""; - - return l_Text; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/GamePlay.cs b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/GamePlay.cs deleted file mode 100644 index 3b98149..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/GamePlay.cs +++ /dev/null @@ -1,188 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberPlus_ChatIntegrations.Models; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Conditions -{ - public class GamePlay_InMenu : Interfaces.ICondition - { - public override string Description => "Are we currently in the menu?"; - - public GamePlay_InMenu() => UIPlaceHolder = "Ensure that you are currently in the menu"; - - public override bool Eval(Models.EventContext p_Context) - { - return BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu; - } - } - - public class GamePlay_LevelEndType : Interfaces.ICondition - { - public override string Description => "Kind of level end!"; - -#pragma warning disable CS0414 - [UIComponent("QuitToggle")] - private ToggleSetting m_QuitToggle = null; - [UIComponent("RestartToggle")] - private ToggleSetting m_RestartToggle = null; - [UIComponent("PassToggle")] - private ToggleSetting m_PassToggle = null; - [UIComponent("FailToggle")] - private ToggleSetting m_FailToggle = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_QuitToggle, l_Event, Model.Quit, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_RestartToggle, l_Event, Model.Restart, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_PassToggle, l_Event, Model.Pass, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_FailToggle, l_Event, Model.Fail, false); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.Quit = m_QuitToggle.Value; - Model.Restart = m_RestartToggle.Value; - Model.Pass = m_PassToggle.Value; - Model.Fail = m_FailToggle.Value; - } - - public override bool Eval(EventContext p_Context) - { - bool l_IsQuit = BeatSaberPlus.SDK.Game.Logic.LevelCompletionData.Results.levelEndAction == LevelCompletionResults.LevelEndAction.Quit; - bool l_IsRestart = BeatSaberPlus.SDK.Game.Logic.LevelCompletionData.Results.levelEndAction == LevelCompletionResults.LevelEndAction.Restart; - bool l_IsPass = BeatSaberPlus.SDK.Game.Logic.LevelCompletionData.Results.levelEndStateType == LevelCompletionResults.LevelEndStateType.Cleared; - bool l_IsFail = BeatSaberPlus.SDK.Game.Logic.LevelCompletionData.Results.levelEndStateType == LevelCompletionResults.LevelEndStateType.Failed; - - return (Model.Quit && l_IsQuit) || (Model.Restart && l_IsRestart) || (Model.Pass && l_IsPass) || (Model.Fail && l_IsFail); - } - } - - public class GamePlay_PlayingMap : Interfaces.ICondition - { - public override string Description => "Are we currently playing a map?"; - -#pragma warning disable CS0414 - [UIComponent("LevelTypeList")] - private ListSetting m_LevelTypeList = null; - [UIValue("LevelTypeList_Choices")] - private List m_LevelTypeList_Choices = new List() { }; - [UIValue("LevelTypeList_Value")] - private string m_LevelTypeList_Value; - - [UIComponent("BeatmapList")] - private ListSetting m_BeatmapTypeList = null; - [UIValue("BeatmapList_Choices")] - private List m_BeatmapTypeList_Choices = new List() { }; - [UIValue("BeatmapList_Value")] - private string m_BeatmapTypeList_Value; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - if (m_LevelTypeList_Choices.Count == 0) - { - foreach (var l_Current in System.Enum.GetValues(typeof(Models.Conditions.GamePlay_PlayingMap.ELevelType))) - m_LevelTypeList_Choices.Add((object)l_Current.ToString()); - } - if (m_BeatmapTypeList_Choices.Count == 0) - { - foreach (var l_Current in System.Enum.GetValues(typeof(Models.Conditions.GamePlay_PlayingMap.EBeatmapModType))) - m_BeatmapTypeList_Choices.Add((object)l_Current.ToString()); - } - - m_LevelTypeList_Value = (string)m_LevelTypeList_Choices.ElementAt((int)Model.LevelType % m_LevelTypeList_Choices.Count); - m_BeatmapTypeList_Value = (string)m_BeatmapTypeList_Choices.ElementAt((int)Model.BeatmapModType % m_BeatmapTypeList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_LevelTypeList, l_Event, false); - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_BeatmapTypeList, l_Event, false); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.LevelType = (Models.Conditions.GamePlay_PlayingMap.ELevelType)m_LevelTypeList_Choices.IndexOf(m_LevelTypeList.Value); - Model.BeatmapModType = (Models.Conditions.GamePlay_PlayingMap.EBeatmapModType)m_BeatmapTypeList_Choices.IndexOf(m_BeatmapTypeList.Value); - } - - public override bool Eval(Models.EventContext p_Context) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - return false; - - var l_LevelData = BeatSaberPlus.SDK.Game.Logic.LevelData; - - if (l_LevelData == null) - return false; - - var l_IsInReplay = BeatSaberPlus.SDK.Game.Scoring.IsInReplay; - var l_LevelTypeCond = false; - var l_BeatMapTypeCond = false; - - switch (Model.LevelType) - { - case Models.Conditions.GamePlay_PlayingMap.ELevelType.Solo: - l_LevelTypeCond = !l_IsInReplay && l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Solo; - break; - case Models.Conditions.GamePlay_PlayingMap.ELevelType.Multiplayer: - l_LevelTypeCond = !l_IsInReplay && l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Multiplayer; - break; - - case Models.Conditions.GamePlay_PlayingMap.ELevelType.Replay: - l_LevelTypeCond = l_IsInReplay && l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Solo; - break; - - case Models.Conditions.GamePlay_PlayingMap.ELevelType.SoloAndMultiplayer: - l_LevelTypeCond = !l_IsInReplay && (l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Solo || l_LevelData.Type == BeatSaberPlus.SDK.Game.LevelType.Multiplayer); - break; - - case Models.Conditions.GamePlay_PlayingMap.ELevelType.Any: - default: - l_LevelTypeCond = true; - break; - } - - switch (Model.BeatmapModType) - { - case Models.Conditions.GamePlay_PlayingMap.EBeatmapModType.NonNoodle: - l_BeatMapTypeCond = !l_LevelData.IsNoodle; - break; - - case Models.Conditions.GamePlay_PlayingMap.EBeatmapModType.Noodle: - l_BeatMapTypeCond = l_LevelData.IsNoodle; - break; - - case Models.Conditions.GamePlay_PlayingMap.EBeatmapModType.Chroma: - l_BeatMapTypeCond = l_LevelData.IsChroma; - break; - - case Models.Conditions.GamePlay_PlayingMap.EBeatmapModType.NoodleOrChroma: - l_BeatMapTypeCond = l_LevelData.IsNoodle || l_LevelData.IsChroma; - break; - - case Models.Conditions.GamePlay_PlayingMap.EBeatmapModType.All: - default: - l_BeatMapTypeCond = true; - break; - } - - return l_LevelTypeCond && l_BeatMapTypeCond; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Misc.cs b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Misc.cs deleted file mode 100644 index ce820b4..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Misc.cs +++ /dev/null @@ -1,93 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System.Collections.Concurrent; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Conditions -{ - public class Misc_Cooldown : Interfaces.ICondition - { - public override string Description => "Add a cooldown on your event"; - -#pragma warning disable CS0414 - [UIComponent("CooldownSlider")] - private SliderSetting m_CooldownSlider = null; - [UIComponent("PerUserToggle")] - private ToggleSetting m_PerUserToggle = null; - [UIComponent("NotifyUserToggle")] - private ToggleSetting m_NotifyUserToggle = null; -#pragma warning restore CS0414 - - public override void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_CooldownSlider, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Time, Model.CooldownTime, true, true, new Vector2(0.08f, 0f), new Vector2(0.93f, 1f)); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_PerUserToggle, l_Event, Model.PerUser, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_NotifyUserToggle, l_Event, Model.NotifyUser, false); - } - private void OnSettingChanged(object p_Value) - { - Model.CooldownTime = (uint)m_CooldownSlider.slider.value; - Model.PerUser = m_PerUserToggle.Value; - Model.NotifyUser = m_NotifyUserToggle.Value; - } - - private long m_LastTime = 0; - private ConcurrentDictionary m_Cooldowns = new ConcurrentDictionary(); - - public override bool Eval(Models.EventContext p_Context) - { - if (Model.PerUser) - { - if (p_Context.User == null) - return true; - - if (m_Cooldowns.TryGetValue(p_Context.User.UserName, out var l_LastTime)) - { - var l_Remaining = (l_LastTime + Model.CooldownTime) - CP_SDK.Misc.Time.UnixTimeNow(); - if (l_Remaining > 0) - { - if (Model.NotifyUser && p_Context.ChatService != null && p_Context.Channel != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, BuildFailedMessage(p_Context.User, l_Remaining)); - return false; - } - - m_Cooldowns.TryUpdate(p_Context.User.UserName, CP_SDK.Misc.Time.UnixTimeNow(), l_LastTime); - } - else - m_Cooldowns.TryAdd(p_Context.User.UserName, CP_SDK.Misc.Time.UnixTimeNow()); - } - else - { - var l_Remaining = (m_LastTime + Model.CooldownTime) - CP_SDK.Misc.Time.UnixTimeNow(); - if (l_Remaining > 0) - { - if (Model.NotifyUser && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, BuildFailedMessage(p_Context.User, l_Remaining)); - return false; - } - - m_LastTime = CP_SDK.Misc.Time.UnixTimeNow(); - } - - return true; - } - - private string BuildFailedMessage(CP_SDK.Chat.Interfaces.IChatUser p_User, long p_Remaining) - { - var l_Minutes = p_Remaining / 60; - var l_Seconds = p_Remaining - (l_Minutes * 60); - - if (l_Minutes != 0) - return $"! @{p_User.DisplayName} command is on cooldown, {l_Minutes}m{l_Seconds}s remaining!"; - - return $"! @{p_User.DisplayName} command is on cooldown, {l_Seconds}s remaining!"; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/OBS.cs b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/OBS.cs deleted file mode 100644 index c92b135..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/OBS.cs +++ /dev/null @@ -1,239 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -using OBSService = CP_SDK.OBS.Service; - -namespace BeatSaberPlus_ChatIntegrations.Conditions -{ - internal class OBSBuilder - { - internal static List BuildFor(Interfaces.IEventBase p_Event) - { - var l_Result = new List() - { - new OBS_IsConnected(), - new OBS_IsNotConnected(), - - new OBS_IsStreaming(), - new OBS_IsNotStreaming(), - - new OBS_IsRecording(), - new OBS_IsNotRecording(), - - new OBS_IsInStudioMode(), - new OBS_IsNotInStudioMode(), - - new OBS_IsInScene(), - new OBS_IsNotInScene() - }; - - switch (p_Event) - { - default: - break; - } - - return l_Result; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - public class OBS_IsConnected : Interfaces.ICondition - { - public override string Description => "Is OBS connected?"; - public OBS_IsConnected() => UIPlaceHolder = "Ensure that OBS is connected"; - public override bool Eval(Models.EventContext p_Context) - => OBSService.Status == OBSService.EStatus.Connected; - } - public class OBS_IsNotConnected : Interfaces.ICondition - { - public override string Description => "Is OBS not connected?"; - public OBS_IsNotConnected() => UIPlaceHolder = "Ensure that OBS is not connected"; - public override bool Eval(Models.EventContext p_Context) - => OBSService.Status != OBSService.EStatus.Connected; - } - - public class OBS_IsStreaming : Interfaces.ICondition - { - public override string Description => "Is OBS streaming?"; - public OBS_IsStreaming() => UIPlaceHolder = "Ensure that OBS is streaming"; - public override bool Eval(Models.EventContext p_Context) - => OBSService.Status == OBSService.EStatus.Connected && OBSService.IsStreaming; - } - public class OBS_IsNotStreaming : Interfaces.ICondition - { - public override string Description => "Is OBS not streaming?"; - public OBS_IsNotStreaming() => UIPlaceHolder = "Ensure that OBS is not streaming"; - public override bool Eval(Models.EventContext p_Context) - => OBSService.Status == OBSService.EStatus.Connected && !OBSService.IsStreaming; - } - - public class OBS_IsRecording : Interfaces.ICondition - { - public override string Description => "Is OBS recording?"; - public OBS_IsRecording() => UIPlaceHolder = "Ensure that OBS is recording"; - public override bool Eval(Models.EventContext p_Context) - => OBSService.Status == OBSService.EStatus.Connected && OBSService.IsRecording; - } - public class OBS_IsNotRecording : Interfaces.ICondition - { - public override string Description => "Is OBS not recording?"; - public OBS_IsNotRecording() => UIPlaceHolder = "Ensure that OBS is not recording"; - public override bool Eval(Models.EventContext p_Context) - => OBSService.Status == OBSService.EStatus.Connected && !OBSService.IsRecording; - } - - public class OBS_IsInStudioMode : Interfaces.ICondition - { - public override string Description => "Is OBS in studio mode?"; - public OBS_IsInStudioMode() => UIPlaceHolder = "Ensure that OBS is in studio mode"; - public override bool Eval(Models.EventContext p_Context) - => OBSService.Status == OBSService.EStatus.Connected && OBSService.IsInStudioMode; - } - public class OBS_IsNotInStudioMode : Interfaces.ICondition - { - public override string Description => "Is OBS not in studio mode?"; - public OBS_IsNotInStudioMode() => UIPlaceHolder = "Ensure that OBS is not in studio mode"; - public override bool Eval(Models.EventContext p_Context) - => OBSService.Status == OBSService.EStatus.Connected && !OBSService.IsInStudioMode; - } - - public class OBS_IsInScene : Interfaces.ICondition - { - public override string Description => "Is OBS in scene"; - -#pragma warning disable CS0414 - [UIComponent("Scene_DropDown")] - protected DropDownListSetting m_Scene_DropDown = null; - [UIValue("Scene_DropDownOptions")] - private List m_Scene_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Scene_DropDown, l_Event, true); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (OBSService.Status == OBSService.EStatus.Connected) - l_Choices.AddRange(OBSService.Scenes.Keys.ToList()); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Condition failed, not connected to OBS!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.SceneName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Scene_DropDownOptions = l_Choices; - m_Scene_DropDown.values = l_Choices; - m_Scene_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Scene_DropDown.UpdateChoices(); - } - private void OnSettingChanged(object p_Value) - { - Model.SceneName = m_Scene_DropDown.Value as string; - } - - [UIAction("click-activescene-btn-pressed")] - private void OnActiveSceneButton() - { - Model.SceneName = OBSService.ActiveScene?.name; - m_Scene_DropDown.Value = OBSService.ActiveScene?.name; - } - public override bool Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Condition failed, not connected to OBS!"); - return false; - } - - return OBSService.ActiveScene?.name == Model.SceneName; - } - } - public class OBS_IsNotInScene : Interfaces.ICondition - { - public override string Description => "Is OBS not in scene"; - -#pragma warning disable CS0414 - [UIComponent("Scene_DropDown")] - protected DropDownListSetting m_Scene_DropDown = null; - [UIValue("Scene_DropDownOptions")] - private List m_Scene_DropDownOptions = new List() { "Loading...", }; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_Scene_DropDown, l_Event, true); - - int l_ChoiceIndex = 0; - var l_Choices = new List(); - l_Choices.Add("None"); - - if (OBSService.Status == OBSService.EStatus.Connected) - l_Choices.AddRange(OBSService.Scenes.Keys.ToList()); - else - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Condition failed, not connected to OBS!"); - - for (int l_I = 0; l_I < l_Choices.Count; ++l_I) - { - if (l_Choices[l_I] as string != Model.SceneName) - continue; - - l_ChoiceIndex = l_I; - break; - } - - m_Scene_DropDownOptions = l_Choices; - m_Scene_DropDown.values = l_Choices; - m_Scene_DropDown.Value = l_Choices[l_ChoiceIndex]; - m_Scene_DropDown.UpdateChoices(); - } - private void OnSettingChanged(object p_Value) - { - Model.SceneName = m_Scene_DropDown.Value as string; - } - - [UIAction("click-activescene-btn-pressed")] - private void OnActiveSceneButton() - { - Model.SceneName = OBSService.ActiveScene?.name; - m_Scene_DropDown.Value = OBSService.ActiveScene?.name; - } - - public override bool Eval(Models.EventContext p_Context) - { - if (OBSService.Status != OBSService.EStatus.Connected) - { - CP_SDK.Chat.Service.Multiplexer?.InternalBroadcastSystemMessage("ChatIntegrations: Condition failed, not connected to OBS!"); - return false; - } - - return !(OBSService.ActiveScene?.name == Model.SceneName); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Subscription.cs b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Subscription.cs deleted file mode 100644 index b6272ed..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Subscription.cs +++ /dev/null @@ -1,94 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Conditions -{ - public class Subscription_IsGift : Interfaces.ICondition - { - public override string Description => "Is a gift subscription event?"; - - public Subscription_IsGift() => UIPlaceHolder = "Ensure that this is a subscription gift"; - - public override bool Eval(Models.EventContext p_Context) - { - return p_Context.SubscriptionEvent.IsGift; - } - } - - public class Subscription_PlanType : Interfaces.ICondition - { - public override string Description => "Put condition on the kind of subscription"; - -#pragma warning disable CS0414 - [UIComponent("PlanTypeList")] - private ListSetting m_PlanTypeList = null; - [UIValue("PlanTypeList_Choices")] - private List m_PlanTypeList_Choices = new List() { "Prime", "Tier1", "Tier2", "Tier3" }; - [UIValue("PlanTypeList_Value")] - private string m_PlanTypeList_Value; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - m_PlanTypeList_Value = (string)m_PlanTypeList_Choices.ElementAt(Model.PlanType % m_PlanTypeList_Choices.Count); - - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_PlanTypeList, l_Event, false); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.PlanType = m_PlanTypeList_Choices.Select(x => (string)x).ToList().IndexOf(m_PlanTypeList.Value); - } - - public override bool Eval(Models.EventContext p_Context) - { - if (p_Context.SubscriptionEvent != null - && p_Context.SubscriptionEvent.SubPlan.ToLower() == ((string)m_PlanTypeList_Choices.ElementAt(Model.PlanType % m_PlanTypeList_Choices.Count)).ToLower()) - return true; - - return false; - } - } - - public class Subscription_PurchasedMonthCount : Interfaces.ICondition - { - public override string Description => "Check for purchased month count"; - -#pragma warning disable CS0414 - [UIComponent("CountSlider")] - private SliderSetting m_CountSlider = null; -#pragma warning restore CS0414 - - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_CountSlider, l_Event, null, Model.Count, false); - - OnSettingChanged(null); - } - private void OnSettingChanged(object p_Value) - { - Model.Count = (uint)m_CountSlider.slider.value; - } - - public override bool Eval(Models.EventContext p_Context) - { - return p_Context.SubscriptionEvent != null && p_Context.SubscriptionEvent.PurchasedMonthCount == Model.Count; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/User.cs b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/User.cs deleted file mode 100644 index eee190b..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/User.cs +++ /dev/null @@ -1,68 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Conditions -{ - public class User_Permissions : Interfaces.ICondition - { - public override string Description => "Check user permissions"; - -#pragma warning disable CS0414 - [UIComponent("SubscriberToggle")] - private ToggleSetting m_SubscriberToggle = null; - [UIComponent("VIPToggle")] - private ToggleSetting m_VIPToggle = null; - [UIComponent("ModeratorToggle")] - private ToggleSetting m_ModeratorToggle = null; - [UIComponent("NotifyToggle")] - private ToggleSetting m_NotifyToggle = null; -#pragma warning restore CS0414 - - public override void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_SubscriberToggle, l_Event, Model.Subscriber, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_VIPToggle, l_Event, Model.VIP, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ModeratorToggle, l_Event, Model.Moderator, false); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_NotifyToggle, l_Event, Model.NotifyWhenNoPermission, false); - } - private void OnSettingChanged(object p_Value) - { - Model.Subscriber = m_SubscriberToggle.Value; - Model.VIP = m_VIPToggle.Value; - Model.Moderator = m_ModeratorToggle.Value; - Model.NotifyWhenNoPermission = m_NotifyToggle.Value; - } - - public override bool Eval(Models.EventContext p_Context) - { - if (p_Context.User.IsBroadcaster) - return true; - - var l_IsModerator = p_Context.User.IsBroadcaster || p_Context.User.IsModerator; - var l_IsSuscriber = p_Context.User.IsSubscriber; - var l_IsVIP = p_Context.User.IsVip; - - if (Model.Subscriber && l_IsSuscriber) - return true; - - if (Model.VIP && l_IsVIP) - return true; - - if (Model.Moderator && l_IsModerator) - return true; - - if (Model.NotifyWhenNoPermission && p_Context.ChatService != null && p_Context.Channel != null && p_Context.User != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.DisplayName} You can't use this command!"); - - return false; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Bits_Amount.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Bits_Amount.bsml deleted file mode 100644 index 581587e..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Bits_Amount.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueDuration.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueDuration.bsml deleted file mode 100644 index 5ef54ca..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueDuration.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueSize.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueSize.bsml deleted file mode 100644 index e664796..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueSize.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueStatus.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueStatus.bsml deleted file mode 100644 index f3c8459..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/ChatRequest_QueueStatus.bsml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Event_Disabled.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Event_Disabled.bsml deleted file mode 100644 index 725945b..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Event_Disabled.bsml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Event_Enabled.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Event_Enabled.bsml deleted file mode 100644 index 725945b..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Event_Enabled.bsml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/GamePlay_LevelEndType.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/GamePlay_LevelEndType.bsml deleted file mode 100644 index e3959d5..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/GamePlay_LevelEndType.bsml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/GamePlay_PlayingMap.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/GamePlay_PlayingMap.bsml deleted file mode 100644 index d1844de..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/GamePlay_PlayingMap.bsml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Misc_Cooldown.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Misc_Cooldown.bsml deleted file mode 100644 index ca521bb..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Misc_Cooldown.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/OBS_IsInScene.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/OBS_IsInScene.bsml deleted file mode 100644 index c5ab39c..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/OBS_IsInScene.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/OBS_IsNotInScene.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/OBS_IsNotInScene.bsml deleted file mode 100644 index c5ab39c..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/OBS_IsNotInScene.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Subscription_PlanType.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Subscription_PlanType.bsml deleted file mode 100644 index 67bc8c5..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Subscription_PlanType.bsml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Subscription_PurchasedMonthCount.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Subscription_PurchasedMonthCount.bsml deleted file mode 100644 index 7bda0e3..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/Subscription_PurchasedMonthCount.bsml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/User_Permissions.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/User_Permissions.bsml deleted file mode 100644 index b87a474..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Conditions/Views/User_Permissions.bsml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatBits.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatBits.cs deleted file mode 100644 index 6211a12..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatBits.cs +++ /dev/null @@ -1,123 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Chat bits event - /// - public class ChatBits : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public ChatBits() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.Integer, "Bits"), - (IValueType.String, "UserName") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.Bits_Amount(), - new Conditions.GamePlay_InMenu(), - new Conditions.GamePlay_PlayingMap(), - new Conditions.Misc_Cooldown(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.ChatBits || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null || p_Context.BitsEvent == null) - return false; - - return true; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - p_Context.AddValue(IValueType.Integer, "Bits", (Int64?)p_Context.BitsEvent.Value); - p_Context.AddValue(IValueType.String, "UserName", p_Context.User.DisplayName); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatCommand.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatCommand.cs deleted file mode 100644 index f10432c..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatCommand.cs +++ /dev/null @@ -1,187 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using TMPro; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Chat command event - /// - public class ChatCommand : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public ChatCommand() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.Emotes, "MessageEmotes"), - (IValueType.Integer, "MessageNumber"), - (IValueType.String, "MessageContent"), - (IValueType.String, "UserName") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.GamePlay_InMenu(), - new Conditions.GamePlay_PlayingMap(), - new Conditions.Misc_Cooldown(), - - new Conditions.User_Permissions(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; - [UIComponent("CurrentTriggerText")] - private TextMeshProUGUI m_CurrentTriggerText = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - - /// Update UI - UpdateUI(); - } - /// - /// Update UI component values - /// - private void UpdateUI() - { - m_CurrentTriggerText.SetText("Current command : " + Model.Command); - } - /// - /// Rebind button pressed - /// - [UIAction("click-rebind-btn-pressed")] - private void OnRebindButton() - { - UI.Settings.Instance.UIShowInputKeyboard(Model.Command, (p_Result) => - { - if (p_Result.Length > 0 && p_Result[0] != '!') - p_Result = "!" + p_Result; - - var l_FirstSpaceIndex = p_Result.IndexOf(' '); - - Model.Command = (l_FirstSpaceIndex != -1 ? p_Result.Substring(0, l_FirstSpaceIndex) : p_Result).ToLower(); - - /// Update UI - UpdateUI(); - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.ChatMessage || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null || p_Context.Message == null) - return false; - - /// Look for command sign - if (p_Context.Message.Message.Length < 2 || p_Context.Message.Message[0] != '!') - return false; - - var l_FirstSpaceIndex = p_Context.Message.Message.IndexOf(' '); - var l_Command = (l_FirstSpaceIndex != -1 ? p_Context.Message.Message.Substring(0, l_FirstSpaceIndex) : p_Context.Message.Message).ToLower(); - - return l_Command == Model.Command; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - var l_FirstSpaceIndex = p_Context.Message.Message.IndexOf(' '); - - var l_Emotes = p_Context.Message.Emotes; - var l_Number = (Int64?)null; - var l_Content = null as string; - - if (l_FirstSpaceIndex != -1) - { - var l_Remaining = p_Context.Message.Message.Substring(l_FirstSpaceIndex + 1); - - if (Int64.TryParse(Regex.Match(l_Remaining, @"-?\d+").Value, out var l_NumberVal)) - l_Number = (Int64?)l_NumberVal; - - l_Content = l_Remaining; - } - - p_Context.AddValue(IValueType.String, "UserName", p_Context.User.DisplayName); - p_Context.AddValue(IValueType.Emotes, "MessageEmotes", l_Emotes); - p_Context.AddValue(IValueType.Integer, "MessageNumber", l_Number); - p_Context.AddValue(IValueType.String, "MessageContent", l_Content); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatFollow.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatFollow.cs deleted file mode 100644 index 5573a26..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatFollow.cs +++ /dev/null @@ -1,122 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Chat follow event - /// - public class ChatFollow : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public ChatFollow() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.String, "UserName") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.GamePlay_InMenu(), - new Conditions.GamePlay_PlayingMap(), - new Conditions.Misc_Cooldown(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.ChatFollow || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null) - return false; - - return true; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - p_Context.AddValue(IValueType.String, "UserName", p_Context.User.DisplayName); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatPointsReward.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatPointsReward.cs deleted file mode 100644 index 6fb5bdb..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatPointsReward.cs +++ /dev/null @@ -1,530 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.Parser; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using BeatSaberPlus_ChatIntegrations.Models; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using TMPro; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Chat command event - /// - public class ChatPointsReward : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public ChatPointsReward() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.Integer, "MessageNumber"), - (IValueType.String, "MessageContent"), - (IValueType.String, "UserName") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.GamePlay_InMenu(), - new Conditions.GamePlay_PlayingMap(), - new Conditions.Misc_Cooldown(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; - - [UIComponent("TitleText")] - private TextMeshProUGUI m_TitleText = null; - [UIComponent("PromptText")] - private TextMeshProUGUI m_PromptText = null; - [UIComponent("CostText")] - private TextMeshProUGUI m_CostText = null; - - [UIComponent("RequireInputToggle")] - private ToggleSetting m_RequireInputToggle = null; - [UIComponent("MaxPerStreamIncrement")] - private IncrementSetting m_MaxPerStreamIncrement = null; - [UIComponent("MaxPerUserPerStreamIncrement")] - private IncrementSetting m_MaxPerUserPerStreamIncrement = null; - [UIComponent("CooldownIncrement")] - private IncrementSetting m_CooldownIncrement = null; - [UIComponent("AutoFullfillRefund")] - private ToggleSetting m_AutoFullfillRefund = null; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// BSML parser params instance - /// - private BSMLParserParams m_ParserParams; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - m_ParserParams = BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - var l_Event = new BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - var l_QuantityFormatter = new BSMLAction(this, this.GetType().GetMethod(nameof(QuantityFormatter), BindingFlags.Instance | BindingFlags.NonPublic)); - var l_CooldownFormatter = new BSMLAction(this, this.GetType().GetMethod(nameof(CooldownFormatter), BindingFlags.Instance | BindingFlags.NonPublic)); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_RequireInputToggle, l_Event, Model.RequireInput, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MaxPerStreamIncrement, l_Event, l_QuantityFormatter, Model.MaxPerStream, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_MaxPerUserPerStreamIncrement, l_Event, l_QuantityFormatter, Model.MaxPerUserPerStream, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_CooldownIncrement, l_Event, l_CooldownFormatter, Model.Cooldown, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_AutoFullfillRefund, l_Event, Model.AutoFullfillRefund, true); - - m_PromptText.fontSizeMax = m_PromptText.fontSize; - m_PromptText.fontSizeMin = 1; - m_PromptText.enableAutoSizing = true; - - /// Update UI - UpdateUI(); - } - /// - /// On setting changed - /// - /// Value - private void OnSettingChanged(object p_Value) - { - Model.RequireInput = m_RequireInputToggle.Value; - Model.MaxPerStream = (int)m_MaxPerStreamIncrement.Value; - Model.MaxPerUserPerStream = (int)m_MaxPerUserPerStreamIncrement.Value; - Model.Cooldown = (int)m_CooldownIncrement.Value; - Model.AutoFullfillRefund = m_AutoFullfillRefund.Value; - } - /// - /// Update UI component values - /// - private void UpdateUI() - { - m_TitleText.SetText(Model.Title + " "); - m_PromptText.SetText(Model.Prompt + " "); - m_CostText.SetText(Model.Cost + " "); - } - /// - /// Title button pressed - /// - [UIAction("click-title-btn-pressed")] - private void OnTitleButton() - { - UI.Settings.Instance.UIShowInputKeyboard(Model.Title, (x) => - { - Model.Title = x.Length > 45 ? x.Substring(0, 45) : x; - UpdateUI(); - }); - } - /// - /// Prompt button pressed - /// - [UIAction("click-prompt-btn-pressed")] - private void OnPromptButton() - { - UI.Settings.Instance.UIShowInputKeyboard(Model.Prompt, (x) => - { - Model.Prompt = x.Length > 70 ? x.Substring(0, 70) : x; - UpdateUI(); - }); - } - /// - /// Cost button pressed - /// - [UIAction("click-cost-btn-pressed")] - private void OnCostButton() - { - UI.Settings.Instance.UIShowInputKeyboard(Model.Cost.ToString(), (x) => - { - int l_NewValue = 0; - if (!int.TryParse(x, out l_NewValue)) - l_NewValue = Model.Cost; - else - l_NewValue = Mathf.Clamp(l_NewValue, 0, 10000000); - - Model.Cost = l_NewValue; - UpdateUI(); - }); - } - /// - /// Update reward button pressed - /// - [UIAction("click-update-reward-btn-pressed")] - private void OnUpdateRewardButton() - { - CreateOrUpdateReward(); - } - /// - /// Quantity formatter - /// - /// New value - /// - private string QuantityFormatter(int p_Value) - { - return p_Value == 0 ? "Unlimited" : p_Value.ToString(); - } - /// - /// Cooldown formatter - /// - /// New value - /// - private string CooldownFormatter(int p_Value) - { - if (p_Value == 0) - return "Unlimited"; - - int l_Minutes = p_Value / 60; - int l_Seconds = p_Value - (l_Minutes * 60); - - string l_Result = (l_Minutes != 0 ? l_Minutes : l_Seconds).ToString(); - if (l_Minutes != 0) - l_Result += "m " + l_Seconds + "s"; - else - l_Result += "s"; - - return l_Result; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On import or clone - /// - /// Is an import - /// Is a clone - public override void OnImportOrClone(bool p_IsImport, bool p_IsClone) - { - base.OnImportOrClone(p_IsImport, p_IsClone); - - if (p_IsImport) - Model.Title += " (Import)"; - if (p_IsClone) - Model.Title += " (Clone)"; - - Model.RewardID = ""; - } - /// - /// When the event is enabled - /// - public override sealed void OnEnable() - { - CreateOrUpdateReward(); - } - /// - /// When the event is successful - /// - /// Event context - public override void OnSuccess(EventContext p_Context) - { - if (!Model.AutoFullfillRefund) - return; - - var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); - var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; - - if (l_TwitchHelix != null) - { - var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id={l_TwitchHelix.BroadcasterID}&reward_id={Model.RewardID}&id={p_Context.PointsEvent.TransactionID}"; - var l_Content = new JObject() - { - ["status"] = "FULFILLED", - }; - var l_ContentStr = new StringContent(l_Content.ToString(), Encoding.UTF8, "application/json"); - - l_TwitchHelix.APIClient.PatchAsync(l_URL, l_ContentStr, CancellationToken.None, true).ConfigureAwait(false); - } - } - /// - /// When the event failed - /// - /// Event context - public override sealed void OnEventFailed(Models.EventContext p_Context) - { - if (!Model.AutoFullfillRefund) - return; - - var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); - var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; - - if (l_TwitchHelix != null) - { - var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards/redemptions?broadcaster_id={l_TwitchHelix.BroadcasterID}&reward_id={Model.RewardID}&id={p_Context.PointsEvent.TransactionID}"; - var l_Content = new JObject() - { - ["status"] = "CANCELED", - }; - var l_ContentStr = new StringContent(l_Content.ToString(), Encoding.UTF8, "application/json"); - - l_TwitchHelix.APIClient.PatchAsync(l_URL, l_ContentStr, CancellationToken.None, true).ContinueWith((x) => - { - if (p_Context.ChatService != null && p_Context.Channel != null) - p_Context.ChatService.SendTextMessage(p_Context.Channel, $"! @{p_Context.User.UserName} Event failed, your points were refunded!"); - }).ConfigureAwait(false); - } - } - /// - /// When the event is disabled - /// - public override sealed void OnDisable() - { - if (CP_SDK.Chat.Service.Multiplexer.Channels.Count == 0) - return; - - var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); - var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; - - if (l_TwitchHelix != null) - { - var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={l_TwitchHelix.BroadcasterID}&id={Model.RewardID}"; - var l_Content = new JObject() - { - ["is_enabled"] = false - }; - var l_ContentStr = new StringContent(l_Content.ToString(), Encoding.UTF8, "application/json"); - - l_TwitchHelix.APIClient.PatchAsync(l_URL, l_ContentStr, CancellationToken.None, true).ConfigureAwait(false); - } - } - /// - /// When the event is deleted - /// - public override sealed void OnDelete() - { - DeleteReward(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.ChatPointsReward || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null) - return false; - - return p_Context.PointsEvent.RewardID == Model.RewardID; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - p_Context.AddValue(IValueType.String, "UserName", (string)p_Context.User.DisplayName); - p_Context.AddValue(IValueType.String, "MessageContent", (string)p_Context.PointsEvent.UserInput); - - if (!string.IsNullOrEmpty(p_Context.PointsEvent.UserInput) && Int64.TryParse(Regex.Match(p_Context.PointsEvent.UserInput, @"-?\d+").Value, out var l_Number)) - p_Context.AddValue(IValueType.Integer, "MessageNumber", (Int64?)l_Number); - else - p_Context.AddValue(IValueType.Integer, "MessageNumber", (Int64?)null); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Create or update the reward on twitch - /// - private void CreateOrUpdateReward() - { - var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); - var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; - - if (l_TwitchHelix != null) - { - var l_ShouldCreate = string.IsNullOrEmpty(Model.RewardID); - var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={l_TwitchHelix.BroadcasterID}&id={Model.RewardID}"; - - if (UI.Settings.Instance != null) - UI.Settings.Instance.UIShowLoading(); - - l_TwitchHelix.APIClient.GetAsync(l_URL, CancellationToken.None, true).ContinueWith((p_GetReply) => - { - l_ShouldCreate = p_GetReply.Result == null || !p_GetReply.Result.IsSuccessStatusCode; - - l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={l_TwitchHelix.BroadcasterID}"; - if (!l_ShouldCreate) - l_URL += $"&id={Model.RewardID}"; - - var l_Content = new JObject() - { - ["title"] = Model.Title, - ["is_user_input_required"] = Model.RequireInput, - ["prompt"] = Model.Prompt, - ["cost"] = Model.Cost, - ["is_enabled"] = Model.Enabled, - ["is_max_per_stream_enabled"] = Model.MaxPerStream > 0, - ["max_per_stream"] = Model.MaxPerStream, - ["is_max_per_user_per_stream_enabled"] = Model.MaxPerUserPerStream > 0, - ["max_per_user_per_stream"] = Model.MaxPerUserPerStream, - ["is_global_cooldown_enabled"] = Model.Cooldown > 0, - ["global_cooldown_seconds"] = Model.Cooldown - }; - - var l_ContentStr = new StringContent(l_Content.ToString(), Encoding.UTF8, "application/json"); - - var l_Task = l_ShouldCreate ? - l_TwitchHelix.APIClient.PostAsync(l_URL, l_ContentStr, CancellationToken.None, true) - : - l_TwitchHelix.APIClient.PatchAsync(l_URL, l_ContentStr, CancellationToken.None, true); - - l_Task.ContinueWith((p_SecondReply) => - { - if (p_SecondReply.Result != null) - { - if (p_SecondReply.Result.StatusCode == System.Net.HttpStatusCode.BadRequest) - { - if (UI.Settings.Instance != null) - { - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - if (p_SecondReply.Result.BodyString.Contains("CREATE_CUSTOM_REWARD_DUPLICATE_REWARD")) - UI.Settings.Instance.UISetPendingMessage("Twitch error,\nA reward with the same name already exist on your channel\nPlease delete on twitch.tv any conflicting reward"); - else - UI.Settings.Instance.UISetPendingMessage("Twitch error,\n" + p_SecondReply.Result.BodyString); - - UI.Settings.Instance.UIHideLoading(); - }); - } - return; - } - - if (l_ShouldCreate) - { - JObject l_Response = JObject.Parse(p_SecondReply.Result.BodyString); - if (l_Response == null - || !l_Response.ContainsKey("data") - || l_Response["data"].Type != JTokenType.Array - || (l_Response["data"] as JArray).Count != 1 - || !((l_Response["data"] as JArray)[0] as JObject).ContainsKey("id") - ) - { - Logger.Instance.Error("[ChatIntegration][ChatPointReward.CreateOrUpdateReward] Error:"); - Logger.Instance.Error(p_SecondReply.Result.BodyString != null ? p_SecondReply.Result.BodyString : "empty response"); - - if (UI.Settings.Instance != null) - { - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - UI.Settings.Instance.UISetPendingMessage("Internal error,\nplease contact BS+ support!"); - UI.Settings.Instance.UIHideLoading(); - }); - } - return; - } - - Model.RewardID = l_Response["data"][0]["id"].Value(); - } - - if (UI.Settings.Instance != null) - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => UI.Settings.Instance.UIHideLoading()); - } - else - { - Logger.Instance.Error("[ChatIntegration][ChatPointReward.CreateOrUpdateReward] Error 2:"); - Logger.Instance.Error("empty response"); - - if (UI.Settings.Instance != null) - { - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - UI.Settings.Instance.UISetPendingMessage("Internal error,\nplease contact BS+ support!"); - UI.Settings.Instance.UIHideLoading(); - }); - } - } - }); - }).ConfigureAwait(false); - } - } - /// - /// Delete the reward on twitch - /// - private void DeleteReward() - { - var l_TwitchService = CP_SDK.Chat.Service.Multiplexer.Services.FirstOrDefault(x => x is CP_SDK.Chat.Services.Twitch.TwitchService); - var l_TwitchHelix = l_TwitchService != null ? (l_TwitchService as CP_SDK.Chat.Services.Twitch.TwitchService).HelixAPI : null; - - if (l_TwitchHelix != null) - { - var l_URL = $"https://api.twitch.tv/helix/channel_points/custom_rewards?broadcaster_id={l_TwitchHelix.BroadcasterID}&id={Model.RewardID}"; - - l_TwitchHelix.APIClient.DeleteAsync(l_URL, CancellationToken.None, true).ConfigureAwait(false); - } - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatRaid.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatRaid.cs deleted file mode 100644 index 30bf477..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatRaid.cs +++ /dev/null @@ -1,126 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Chat command event - /// - public class ChatRaid : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public ChatRaid() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.Integer, "RaiderCount"), - (IValueType.String, "UserName") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.GamePlay_InMenu(), - new Conditions.GamePlay_PlayingMap(), - new Conditions.Misc_Cooldown(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.ChatRaid || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null || p_Context.RaidEvent == null) - return false; - - return true; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - p_Context.AddValue(IValueType.Integer, "RaiderCount", (Int64?)p_Context.RaidEvent.Value); - p_Context.AddValue(IValueType.String, "UserName", p_Context.User.DisplayName); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatSubscription.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatSubscription.cs deleted file mode 100644 index fb854bc..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/ChatSubscription.cs +++ /dev/null @@ -1,133 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Chat subscription event - /// - public class ChatSubscription : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public ChatSubscription() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.String, "UserName"), - (IValueType.String, "SubPlan"), - (IValueType.String, "RecipientName"), - (IValueType.Integer, "MonthCount"), - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.GamePlay_InMenu(), - new Conditions.GamePlay_PlayingMap(), - new Conditions.Misc_Cooldown(), - - new Conditions.Subscription_IsGift(), - new Conditions.Subscription_PurchasedMonthCount(), - new Conditions.Subscription_PlanType() - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.ChatSubscription || p_Context.ChatService == null || p_Context.Channel == null || p_Context.User == null || p_Context.SubscriptionEvent == null) - return false; - - return true; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - p_Context.AddValue(IValueType.String, "UserName", p_Context.User.DisplayName); - p_Context.AddValue(IValueType.String, "SubPlan", p_Context.SubscriptionEvent.SubPlan); - p_Context.AddValue(IValueType.String, "RecipientName", p_Context.SubscriptionEvent.RecipientDisplayName ?? ""); - p_Context.AddValue(IValueType.Integer, "MonthCount", (Int64?)p_Context.SubscriptionEvent.PurchasedMonthCount); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Dummy.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/Dummy.cs deleted file mode 100644 index e0ef353..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Dummy.cs +++ /dev/null @@ -1,122 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Dummy event - /// - public class Dummy : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public Dummy() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.Misc_Cooldown(), - new Conditions.GamePlay_PlayingMap(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - } - /// - /// Execute button pressed - /// - [UIAction("ExecutePressed")] - private void OnExecutePressed() - { - ChatIntegrations.Instance.ExecuteEvent(this, new Models.EventContext() { Type = TriggerType.Dummy }); - UI.Settings.Instance.UIShowMessageModal("Ok!"); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.Dummy) - return false; - - return true; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelEnded.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelEnded.cs deleted file mode 100644 index 9644e66..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelEnded.cs +++ /dev/null @@ -1,139 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Level ended event - /// - public class LevelEnded : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public LevelEnded() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.Integer, "NoteCount"), - (IValueType.Integer, "HitCount"), - (IValueType.Integer, "MissCount"), - (IValueType.Floating, "Accuracy"), - (IValueType.String, "SongName"), - (IValueType.String, "Difficulty") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.Misc_Cooldown(), - new Conditions.GamePlay_LevelEndType(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.LevelEnded || p_Context.LevelCompletionData == null) - return false; - - return true; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - Int64 l_NoteCount = p_Context.LevelCompletionData.Data.transformedBeatmapData.cuttableNotesCount; - Int64 l_HitCount = p_Context.LevelCompletionData.Results.goodCutsCount; - Int64 l_MissCount = l_NoteCount - l_HitCount; - float l_Accuracy = (float)System.Math.Round(100.0f * BeatSaberPlus.SDK.Game.Levels.GetScorePercentage(BeatSaberPlus.SDK.Game.Levels.GetMaxScore((int)l_NoteCount), p_Context.LevelCompletionData.Results.modifiedScore), 2); - string l_GameMode = p_Context.LevelCompletionData.Data.difficultyBeatmap.parentDifficultyBeatmapSet.beatmapCharacteristic.serializedName; - string l_Difficulty = p_Context.LevelCompletionData.Data.difficultyBeatmap.difficulty.Name(); - - p_Context.AddValue(IValueType.Integer, "NoteCount", (Int64?)l_NoteCount); - p_Context.AddValue(IValueType.Integer, "HitCount", (Int64?)l_HitCount); - p_Context.AddValue(IValueType.Integer, "MissCount", (Int64?)l_MissCount); - p_Context.AddValue(IValueType.Floating, "Accuracy", (float?)l_Accuracy); - p_Context.AddValue(IValueType.String, "SongName", p_Context.LevelCompletionData.Data.difficultyBeatmap.level.songAuthorName + " - " + p_Context.LevelCompletionData.Data.difficultyBeatmap.level.songName); - p_Context.AddValue(IValueType.String, "Difficulty", l_GameMode + " - " + l_Difficulty); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelPaused.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelPaused.cs deleted file mode 100644 index 78466d6..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelPaused.cs +++ /dev/null @@ -1,126 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Level paused event - /// - class LevelPaused : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public LevelPaused() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.String, "SongName"), - (IValueType.String, "Difficulty") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.Misc_Cooldown(), - new Conditions.GamePlay_PlayingMap(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.LevelPaused || p_Context.LevelData == null) - return false; - - return true; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - string l_GameMode = p_Context.LevelData.Data.difficultyBeatmap.parentDifficultyBeatmapSet.beatmapCharacteristic.serializedName; - string l_Difficulty = p_Context.LevelData.Data.difficultyBeatmap.difficulty.Name(); - - p_Context.AddValue(IValueType.String, "SongName", p_Context.LevelData.Data.difficultyBeatmap.level.songAuthorName + " - " + p_Context.LevelData.Data.difficultyBeatmap.level.songName); - p_Context.AddValue(IValueType.String, "Difficulty", l_GameMode + " - " + l_Difficulty); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelResumed.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelResumed.cs deleted file mode 100644 index 28fefea..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelResumed.cs +++ /dev/null @@ -1,126 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Level resumed event - /// - class LevelResumed : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public LevelResumed() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.String, "SongName"), - (IValueType.String, "Difficulty") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.Misc_Cooldown(), - new Conditions.GamePlay_PlayingMap(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.LevelResumed || p_Context.LevelData == null) - return false; - - return true; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - string l_GameMode = p_Context.LevelData.Data.difficultyBeatmap.parentDifficultyBeatmapSet.beatmapCharacteristic.serializedName; - string l_Difficulty = p_Context.LevelData.Data.difficultyBeatmap.difficulty.Name(); - - p_Context.AddValue(IValueType.String, "SongName", p_Context.LevelData.Data.difficultyBeatmap.level.songAuthorName + " - " + p_Context.LevelData.Data.difficultyBeatmap.level.songName); - p_Context.AddValue(IValueType.String, "Difficulty", l_GameMode + " - " + l_Difficulty); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelStarted.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelStarted.cs deleted file mode 100644 index 8aa59e6..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/LevelStarted.cs +++ /dev/null @@ -1,127 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// Level started event - /// - class LevelStarted : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public LevelStarted() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.String, "SongName"), - (IValueType.String, "Difficulty") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.ChatRequest_QueueDuration(), - new Conditions.ChatRequest_QueueSize(), - new Conditions.ChatRequest_QueueStatus(), - new Conditions.Misc_Cooldown(), - new Conditions.GamePlay_PlayingMap(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; -#pragma warning restore CS0414 - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.LevelStarted || p_Context.LevelData == null) - return false; - - return true; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - string l_GameMode = p_Context.LevelData.Data.difficultyBeatmap.parentDifficultyBeatmapSet.beatmapCharacteristic.serializedName; - string l_Difficulty = p_Context.LevelData.Data.difficultyBeatmap.difficulty.Name(); - - p_Context.AddValue(IValueType.String, "SongName", p_Context.LevelData.Data.difficultyBeatmap.level.songAuthorName + " - " + p_Context.LevelData.Data.difficultyBeatmap.level.songName); - p_Context.AddValue(IValueType.String, "Difficulty", l_GameMode + " - " + l_Difficulty); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatBits.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatBits.bsml deleted file mode 100644 index ba46507..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatBits.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatCommand.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatCommand.bsml deleted file mode 100644 index 350748d..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatCommand.bsml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatFollow.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatFollow.bsml deleted file mode 100644 index ec5d879..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatFollow.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatPointsReward.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatPointsReward.bsml deleted file mode 100644 index b6f5c6c..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatPointsReward.bsml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatRaid.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatRaid.bsml deleted file mode 100644 index 8a7050d..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatRaid.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatSubscription.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatSubscription.bsml deleted file mode 100644 index 2dd7ed1..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/ChatSubscription.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/Dummy.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/Dummy.bsml deleted file mode 100644 index baa7703..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/Dummy.bsml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelEnded.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelEnded.bsml deleted file mode 100644 index 0f430c7..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelEnded.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelPaused.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelPaused.bsml deleted file mode 100644 index 5f9c561..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelPaused.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelResumed.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelResumed.bsml deleted file mode 100644 index 08fb83a..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelResumed.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelStarted.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelStarted.bsml deleted file mode 100644 index 36f2b36..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/LevelStarted.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/VoiceAttackCommand.bsml b/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/VoiceAttackCommand.bsml deleted file mode 100644 index e8f26e3..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/Views/VoiceAttackCommand.bsml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Events/VoiceAttackCommand.cs b/Modules/BeatSaberPlus_ChatIntegrations/Events/VoiceAttackCommand.cs deleted file mode 100644 index 00ec7b2..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Events/VoiceAttackCommand.cs +++ /dev/null @@ -1,188 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Parser; -using BeatSaberPlus_ChatIntegrations.Interfaces; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using TMPro; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Events -{ - /// - /// VoiceAttack command event - /// - public class VoiceAttackCommand : IEvent - { - /// - /// Provided values list - /// - public override IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public override IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Available actions list - /// - public override IReadOnlyList AvailableActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - public VoiceAttackCommand() - { - /// Build provided values list - ProvidedValues = new List<(IValueType, string)>() - { - (IValueType.String, "CommandGUID"), - (IValueType.String, "CommandName") - }.AsReadOnly(); - - /// Build possible list - AvailableConditions = new List() - { - new Conditions.GamePlay_InMenu(), - new Conditions.GamePlay_PlayingMap(), - new Conditions.Misc_Cooldown(), - } - .Union(BeatSaberPlus_ChatIntegrations.Conditions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Conditions.OBSBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomConditionList()) - .Distinct().ToList().AsReadOnly(); - - /// Build possible list - AvailableActions = new List() - { - - } - .Union(BeatSaberPlus_ChatIntegrations.Actions.Camera2Builder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.ChatBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EmoteRainBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.EventBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.GamePlayBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.MiscBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.NoteTweakerBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.OBSBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.TwitchBuilder.BuildFor(this)) - .Union(BeatSaberPlus_ChatIntegrations.Actions.SongChartVisualizerBuilder.BuildFor(this)) - .Union(GetInstanciatedCustomActionList()) - .Distinct().ToList().AsReadOnly(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("InfoBackground")] - private GameObject m_InfoBackground = null; - [UIComponent("CurrentCommandText")] - private TextMeshProUGUI m_CurrentCommandText = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("RebindModal")] - protected HMUI.ModalView m_RebindModal = null; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// BSML parser params instance - /// - private BSMLParserParams m_ParserParams; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build editing UI - /// - /// Parent transform - public override sealed void BuildUI(Transform p_Parent) - { - string l_BSML = CP_SDK.Misc.Resources.FromPathStr(Assembly.GetAssembly(GetType()), string.Join(".", GetType().Namespace, "Views", GetType().Name) + ".bsml"); - m_ParserParams = BSMLParser.instance.Parse(l_BSML, p_Parent.gameObject, this); - - /// Change opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_InfoBackground, 0.5f); - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_RebindModal, 0.5f); - - /// Update UI - UpdateUI(); - } - /// - /// Update UI component values - /// - private void UpdateUI() - { - m_CurrentCommandText.SetText("Current command : " + Model.CommandName); - } - /// - /// Rebind button pressed - /// - [UIAction("click-rebind-btn-pressed")] - private void OnRebindButton() - { - ChatIntegrations.Instance.OnVoiceAttackCommandExecuted += VoiceAttack_OnCommandExecuted; - - m_ParserParams.EmitEvent("ShowRebindModal"); - } - /// - /// On VoiceAttack command executed - /// - /// Command GUID - /// Command Name - private void VoiceAttack_OnCommandExecuted(string p_GUID, string p_Name) - { - Model.CommandGUID = p_GUID; - Model.CommandName = p_Name; - - ChatIntegrations.Instance.OnVoiceAttackCommandExecuted -= VoiceAttack_OnCommandExecuted; - - m_ParserParams.EmitEvent("CloseRebindModal"); - - UpdateUI(); - } - /// - /// Cancel rebind button pressed - /// - [UIAction("click-cancel-rebind-btn-pressed")] - private void OnCancelSetFromChatButton() - { - m_ParserParams.EmitEvent("CloseRebindModal"); - ChatIntegrations.Instance.OnVoiceAttackCommandExecuted -= VoiceAttack_OnCommandExecuted; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected override sealed bool CanBeExecuted(Models.EventContext p_Context) - { - /// Ensure that we have all data - if (p_Context.Type != TriggerType.VoiceAttackCommand || p_Context.VoiceAttackCommandGUID == null || p_Context.VoiceAttackCommandName == null) - return false; - - return p_Context.VoiceAttackCommandGUID == Model.CommandGUID; - } - /// - /// Build provided value dictionary - /// - /// Event context - protected override sealed void BuildProvidedValues(Models.EventContext p_Context) - { - p_Context.AddValue(IValueType.String, "CommandGUID", p_Context.VoiceAttackCommandGUID); - p_Context.AddValue(IValueType.String, "CommandName", p_Context.VoiceAttackCommandName); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/IAction.cs b/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/IAction.cs deleted file mode 100644 index 40c2fcf..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/IAction.cs +++ /dev/null @@ -1,194 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using Newtonsoft.Json.Linq; -using System.Collections; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Interfaces -{ - /// - /// IAction generic class - /// - public interface IActionBase - { - /// - /// Event instance - /// - IEventBase Event { get; set; } - /// - /// Action description - /// - string Description { get; } - /// - /// UI PlaceHolder description - /// - string UIPlaceHolder { get; } - /// - /// Is enabled - /// - bool IsEnabled { get; set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get type name - /// - /// - string GetTypeName(); - /// - /// Get type name - /// - /// - string GetTypeNameShort(); - /// - /// Serialize - /// - /// - JObject Serialize(); - /// - /// Unserialize - /// - /// - bool Unserialize(JObject p_Serialized); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build editing UI - /// - /// Parent transform - void BuildUI(Transform p_Parent); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - IEnumerator Eval(Models.EventContext p_Context); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// IAction generic class - /// - public abstract class IAction : IActionBase - where T : IAction, new() - where M : Models.Action, new() - { - /// - /// Event instance - /// - public IEventBase Event { get; set; } - /// - /// Action description - /// - public abstract string Description { get; } - /// - /// UI PlaceHolder description - /// - [UIValue("BSPCIUIPlaceHolder")] - public string UIPlaceHolder { get; protected set; } = "No available settings..."; - /// - /// Should display a test button? - /// - public bool UIPlaceHolderTestButton { get; protected set; } = false; - /// - /// Is enabled - /// - public bool IsEnabled { get => Model.Enabled; set { Model.Enabled = value; } } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Model - /// - public M Model { get; protected set; } = new M(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get type name - /// - /// - public string GetTypeName() - { - return string.Join(".", typeof(T).Namespace, typeof(T).Name); - } - /// - /// Get type name - /// - /// - public string GetTypeNameShort() - { - return typeof(T).Name; - } - /// - /// Serialize - /// - /// - public JObject Serialize() - { - Model.Type = GetTypeName(); - return JObject.FromObject(Model); - } - /// - /// Unserialize - /// - /// - public bool Unserialize(JObject p_Serialized) - { - if (!p_Serialized.ContainsKey(nameof(Models.Condition.Type)) || p_Serialized[nameof(Models.Condition.Type)].Value() != GetTypeName()) - return false; - - Model = p_Serialized.ToObject(); - - return true; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build editing UI - /// - /// Parent transform - public virtual void BuildUI(Transform p_Parent) - { - BSMLParser.instance.Parse( - "" - + - "" - + - (UIPlaceHolderTestButton ? - "" - : - "" - ) - + - "", - p_Parent.gameObject, this); - } - /// - /// On UI placeholder test button pressed - /// - [UIAction("UIPlaceholderTestButton")] - protected virtual void OnUIPlaceholderTestButton() { } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - public abstract IEnumerator Eval(Models.EventContext p_Context); - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/ICondition.cs b/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/ICondition.cs deleted file mode 100644 index 4ccb96b..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/ICondition.cs +++ /dev/null @@ -1,173 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Interfaces -{ - /// - /// ICondition generic class - /// - public interface IConditionBase - { - /// - /// Event instance - /// - IEventBase Event { get; set; } - /// - /// Condition description - /// - string Description { get; } - /// - /// UI PlaceHolder description - /// - string UIPlaceHolder { get; } - /// - /// Is enabled - /// - bool IsEnabled { get; set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get type name - /// - /// - string GetTypeName(); - /// - /// Get type name - /// - /// - string GetTypeNameShort(); - /// - /// Serialize - /// - /// - JObject Serialize(); - /// - /// Unserialize - /// - /// - bool Unserialize(JObject p_Serialized); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build editing UI - /// - /// Parent transform - void BuildUI(Transform p_Parent); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - bool Eval(Models.EventContext p_Context); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// ICondition generic class - /// - public abstract class ICondition : IConditionBase - where T : ICondition, new() - where M : Models.Condition, new() - { - /// - /// Event instance - /// - public IEventBase Event { get; set; } - /// - /// Condition description - /// - public abstract string Description { get; } - /// - /// UI PlaceHolder description - /// - [UIValue("BSPCIUIPlaceHolder")] - public string UIPlaceHolder { get; protected set; } = "No available settings..."; - /// - /// Is enabled - /// - public bool IsEnabled { get => Model.Enabled; set { Model.Enabled = value; } } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Model - /// - public M Model { get; protected set; } = new M(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get type name - /// - /// - public string GetTypeName() - { - return string.Join(".", typeof(T).Namespace, typeof(T).Name); - } - /// - /// Get type name - /// - /// - public string GetTypeNameShort() - { - return typeof(T).Name; - } - /// - /// Serialize - /// - /// - public JObject Serialize() - { - Model.Type = GetTypeName(); - return JObject.FromObject(Model); - } - /// - /// Unserialize - /// - /// - public bool Unserialize(JObject p_Serialized) - { - if (!p_Serialized.ContainsKey(nameof(Models.Condition.Type)) || p_Serialized[nameof(Models.Condition.Type)].Value() != GetTypeName()) - return false; - - Model = p_Serialized.ToObject(); - Model.OnDeserialized(p_Serialized); - - return true; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build editing UI - /// - /// Parent transform - public virtual void BuildUI(Transform p_Parent) - { - BSMLParser.instance.Parse("", p_Parent.gameObject, this); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - public abstract bool Eval(Models.EventContext p_Context); - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/IEvent.cs b/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/IEvent.cs deleted file mode 100644 index 119d8f7..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Interfaces/IEvent.cs +++ /dev/null @@ -1,709 +0,0 @@ -using Newtonsoft.Json.Linq; -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.Interfaces -{ - /// - /// IEvent generic class - /// - public abstract class IEventBase : INotifyPropertyChanged - { - /// - /// Generic model - /// - public abstract Models.Event GenericModel { get; } - /// - /// Is enabled - /// - public abstract bool IsEnabled { get; set; } - /// - /// Provided values list - /// - public abstract IReadOnlyList<(IValueType, string)> ProvidedValues { get; protected set; } - /// - /// Available conditions list - /// - public abstract IReadOnlyList AvailableConditions { get; protected set; } - /// - /// Condition list - /// - public abstract List Conditions { get; protected set; } - /// - /// Available actions list - /// - public abstract IReadOnlyList AvailableActions { get; protected set; } - /// - /// Action list - /// - public abstract List Actions { get; protected set; } - /// - /// On fail Action list - /// - public abstract List OnFailActions { get; protected set; } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - public bool Handle(Models.EventContext p_Context) - { - try - { - if (!CanBeExecuted(p_Context)) - return false; - - BuildProvidedValues(p_Context); - - if (p_Context.VariableCount != ProvidedValues.Count) - { - Logger.Instance?.Error(string.Format( - "[Modules.ChatIntegrations.Interfaces][IEvent.Handle] Event {0} provided {1} values, {2} excepted, event discarded!", - GetTypeName(), p_Context.VariableCount, ProvidedValues.Count)); - - OnEventFailed(p_Context); - - return false; - } - - foreach (var l_Condition in Conditions) - { - if (l_Condition.IsEnabled && !l_Condition.Eval(p_Context)) - { - CP_SDK.Unity.MTCoroutineStarter.EnqueueFromThread(DoOnFailActions(p_Context)); - OnEventFailed(p_Context); - return false; - } - } - - CP_SDK.Unity.MTCoroutineStarter.EnqueueFromThread(DoActions(p_Context)); - } - catch (System.Exception l_Exception) - { - Logger.Instance?.Error("[Modules.ChatIntegrations.Interfaces][IEvent.Handle] Error:"); - Logger.Instance?.Error(l_Exception); - } - - return true; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get type name - /// - /// - public abstract string GetTypeName(); - /// - /// Get type name - /// - /// - public abstract string GetTypeNameShort(); - /// - /// Serialize - /// - /// - public abstract JObject Serialize(); - /// - /// Unserialize - /// - /// - /// Error output - public abstract bool Unserialize(JObject p_Serialized, out string p_Error); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Add an condition to the event - /// - /// Condition to add - public abstract void AddCondition(IConditionBase p_Condition); - /// - /// Move condition - /// - /// Condition to move - public abstract int MoveCondition(IConditionBase p_Condition, bool p_Up); - /// - /// Delete an condition from the event - /// - /// Condition to delete - public abstract void DeleteCondition(IConditionBase p_Condition); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Add an action to the event - /// - /// Action to add - public abstract void AddAction(IActionBase p_Action); - /// - /// Move action - /// - /// Action to move - public abstract int MoveAction(IActionBase p_Action, bool p_Up); - /// - /// Delete an action from the event - /// - /// Action to delete - public abstract void DeleteAction(IActionBase p_Action); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Add an on fail action to the event - /// - /// Action to add - public abstract void AddOnFailAction(IActionBase p_OnFailAction); - /// - /// Move an on fail action - /// - /// Action to move - public abstract int MoveOnFailAction(IActionBase p_OnFailAction, bool p_Up); - /// - /// Delete an on fail action from the event - /// - /// Action to delete - public abstract void DeleteOnFailAction(IActionBase p_OnFailAction); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build editing UI - /// - /// Parent transform - public abstract void BuildUI(Transform p_Parent); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On import or clone - /// - /// Is an import - /// Is a clone - public virtual void OnImportOrClone(bool p_IsImport, bool p_IsClone) - { - if (p_IsImport) - GenericModel.Name += " (Import)"; - if (p_IsClone) - GenericModel.Name += " (Clone)"; - - GenericModel.CreationDate = CP_SDK.Misc.Time.UnixTimeNow(); - GenericModel.LastUsageDate = 0; - GenericModel.UsageCount = 0; - } - /// - /// When the event is enabled - /// - public virtual void OnEnable() { } - /// - /// When the event is successful - /// - /// Event context - public virtual void OnSuccess(Models.EventContext p_Context) { } - /// - /// When the event failed - /// - /// Event context - public virtual void OnEventFailed(Models.EventContext p_Context) { } - /// - /// When the event is disabled - /// - public virtual void OnDisable() { } - /// - /// When the event is deleted - /// - public virtual void OnDelete() { } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Property changed event - /// - public event PropertyChangedEventHandler PropertyChanged; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Handle - /// - /// Event context - protected abstract bool CanBeExecuted(Models.EventContext p_Context); - /// - /// Build provided value dictionary - /// - /// Event context - protected virtual void BuildProvidedValues(Models.EventContext p_Context) - { - - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Notify property changed - /// - /// Property name - protected void NotifyPropertyChanged([CallerMemberName] string p_PropertyName = "") - { - try - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p_PropertyName)); - } - catch (Exception l_Exception) - { - Logger.Instance?.Error($"[Modules.ChatIntegrations][IEvent.NotifyPropertyChanged] Error Invoking PropertyChanged: {l_Exception.Message}"); - Logger.Instance?.Error(l_Exception); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Do actions - /// - /// Event context - /// - private IEnumerator DoActions(Models.EventContext p_Context) - { - for (int l_I = 0; l_I < Actions.Count; ++l_I) - { - var l_Action = Actions[l_I]; - if (!l_Action.IsEnabled) - continue; - - yield return l_Action.Eval(p_Context); - - if (!p_Context.PreventNextActionFailure && p_Context.HasActionFailed) - { - OnEventFailed(p_Context); - break; - } - } - - if (!p_Context.PreventNextActionFailure && !p_Context.HasActionFailed) - OnSuccess(p_Context); - - yield return null; - } - /// - /// Do on fail actions - /// - /// Event context - /// - private IEnumerator DoOnFailActions(Models.EventContext p_Context) - { - for (int l_I = 0; l_I < OnFailActions.Count; ++l_I) - { - var l_OnFailAction = OnFailActions[l_I]; - if (!l_OnFailAction.IsEnabled) - continue; - - yield return l_OnFailAction.Eval(p_Context); - - if (!p_Context.PreventNextActionFailure && p_Context.HasActionFailed) - break; - } - - yield return null; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// IEvent generic class - /// - public abstract class IEvent : IEventBase - where T : IEvent, new() - where M : Models.Event, new() - { - /// - /// Generic model - /// - public override sealed Models.Event GenericModel { get => Model; } - /// - /// Is enabled - /// - public override sealed bool IsEnabled { get => Model.Enabled; set { Model.Enabled = value; } } - /// - /// Condition list - /// - public override sealed List Conditions { get; protected set; } = new List(); - /// - /// Action list - /// - public override sealed List Actions { get; protected set; } = new List(); - /// - /// On fail action list - /// - public override sealed List OnFailActions { get; protected set; } = new List(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Model - /// - public M Model { get; protected set; } = new M() { GUID = Guid.NewGuid().ToString(), Enabled = true, CreationDate = CP_SDK.Misc.Time.UnixTimeNow() }; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Custom available conditions - /// - private static List m_CustomAvailableConditions = new List(); - /// - /// Custom available actions - /// - private static List m_CustomAvailableActions = new List(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get type name - /// - /// - public override sealed string GetTypeName() - { - return string.Join(".", typeof(T).Namespace, typeof(T).Name); - } - /// - /// Get type name - /// - /// - public override sealed string GetTypeNameShort() - { - return typeof(T).Name; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Register custom condition - /// - /// Condition to register - public static void RegisterCustomCondition() where TCondition : IConditionBase, new() - => m_CustomAvailableConditions.Add(new TCondition()); - /// - /// Register custom action - /// - /// Action to register - public static void RegisterCustomAction() where TAction : IActionBase, new() - => m_CustomAvailableActions.Add(new TAction()); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - protected List GetInstanciatedCustomConditionList() - => m_CustomAvailableConditions.Select(x => Activator.CreateInstance(x.GetType()) as IConditionBase).ToList(); - protected List GetInstanciatedCustomActionList() - => m_CustomAvailableActions.Select(x => Activator.CreateInstance(x.GetType()) as IActionBase).ToList(); - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Serialize - /// - /// - public override sealed JObject Serialize() - { - Model.Type = GetTypeName(); - - return new JObject() { - ["Type"] = GetTypeName(), - ["Event"] = JObject.FromObject(Model), - ["Conditions"] = JArray.FromObject(Conditions.Select(x => x.Serialize()).ToArray()), - ["Actions"] = JArray.FromObject(Actions.Select(x => x.Serialize()).ToArray()), - ["OnFailActions"] = JArray.FromObject(OnFailActions.Select(x => x.Serialize()).ToArray()) - }; - } - /// - /// Unserialize - /// - /// - /// Error output - public override sealed bool Unserialize(JObject p_Serialized, out string p_Error) - { - if (!p_Serialized.ContainsKey("Event") || !p_Serialized.ContainsKey("Conditions")) - { - p_Error = "Invalid event format"; - return false; - } - - if (!(p_Serialized["Event"] as JObject).ContainsKey("Type")) - { - p_Error = "Invalid event format for type " + GetTypeName(); - return false; - } - - if (p_Serialized["Event"]["Type"].Value().StartsWith("BeatSaberPlus.Modules.ChatIntegrations.")) - p_Serialized["Event"]["Type"] = p_Serialized["Event"]["Type"].Value().Replace("BeatSaberPlus.Modules.ChatIntegrations.", "BeatSaberPlus_ChatIntegrations."); - - if (p_Serialized["Event"]["Type"].Value() != GetTypeName()) - { - p_Error = "Invalid event format for type " + GetTypeName(); - return false; - } - - Model = p_Serialized["Event"].ToObject(); - - if (p_Serialized.ContainsKey("Conditions") && p_Serialized["Conditions"].Type == JTokenType.Array) - { - foreach (JObject l_SerializedCondition in (p_Serialized["Conditions"] as JArray)) - { - if (!l_SerializedCondition.ContainsKey(nameof(Models.Condition.Type))) - continue; - - var l_ConditionType = l_SerializedCondition[nameof(Models.Condition.Type)].Value(); - - if (l_ConditionType.StartsWith("BeatSaberPlus.Modules.ChatIntegrations.")) - { - l_ConditionType = l_ConditionType.Replace("BeatSaberPlus.Modules.ChatIntegrations.", "BeatSaberPlus_ChatIntegrations."); - l_SerializedCondition[nameof(Models.Condition.Type)] = l_ConditionType; - } - - var l_MatchingType = AvailableConditions.Where(x => x.GetTypeName() == l_ConditionType).FirstOrDefault(); - - if (l_MatchingType == null) - { - /// Todo backup this condition to avoid loss - Logger.Instance?.Error($"[Modules.ChatIntegrations.Interfaces][IEvent.Unserialize] Missing condition type \"{l_ConditionType}\""); - continue; - } - - /// Create instance - var l_NewCondition = Activator.CreateInstance(l_MatchingType.GetType()) as Interfaces.IConditionBase; - l_NewCondition.Event = this; - - /// Unserialize condition - if (!l_NewCondition.Unserialize(l_SerializedCondition)) - { - /// Todo backup this condition to avoid loss - Logger.Instance?.Error($"[Modules.ChatIntegrations.Interfaces][IEvent.Unserialize] Failed to unserialize condition\n\"{l_ConditionType.ToString()}\""); - continue; - } - - Conditions.Add(l_NewCondition); - } - } - - if (p_Serialized.ContainsKey("Actions") && p_Serialized["Actions"].Type == JTokenType.Array) - { - foreach (JObject l_SerializedAction in (p_Serialized["Actions"] as JArray)) - { - if (!l_SerializedAction.ContainsKey(nameof(Models.Condition.Type))) - continue; - - var l_ActionType = l_SerializedAction[nameof(Models.Action.Type)].Value(); - - if (l_ActionType.StartsWith("BeatSaberPlus.Modules.ChatIntegrations.")) - { - l_ActionType = l_ActionType.Replace("BeatSaberPlus.Modules.ChatIntegrations.", "BeatSaberPlus_ChatIntegrations."); - l_SerializedAction[nameof(Models.Action.Type)] = l_ActionType; - } - - var l_MatchingType = AvailableActions.Where(x => x.GetTypeName() == l_ActionType).FirstOrDefault(); - - if (l_MatchingType == null) - { - /// Todo backup this action to avoid loss - Logger.Instance?.Error($"[Modules.ChatIntegrations.Interfaces][IEvent.Unserialize] Missing action type \"{l_ActionType}\""); - continue; - } - - /// Create instance - var l_NewAction = Activator.CreateInstance(l_MatchingType.GetType()) as Interfaces.IActionBase; - l_NewAction.Event = this; - - /// Unserialize action - if (!l_NewAction.Unserialize(l_SerializedAction)) - { - /// Todo backup this event to avoid loss - Logger.Instance?.Error($"[Modules.ChatIntegrations.Interfaces][IEvent.Unserialize] Failed to unserialize action\n\"{l_ActionType.ToString()}\""); - continue; - } - - Actions.Add(l_NewAction); - } - } - - if (p_Serialized.ContainsKey("OnFailActions") && p_Serialized["OnFailActions"].Type == JTokenType.Array) - { - foreach (JObject l_SerializedOnFailAction in (p_Serialized["OnFailActions"] as JArray)) - { - if (!l_SerializedOnFailAction.ContainsKey(nameof(Models.Condition.Type))) - continue; - - var l_OnFailActionType = l_SerializedOnFailAction[nameof(Models.Action.Type)].Value(); - - if (l_OnFailActionType.StartsWith("BeatSaberPlus.Modules.ChatIntegrations.")) - { - l_OnFailActionType = l_OnFailActionType.Replace("BeatSaberPlus.Modules.ChatIntegrations.", "BeatSaberPlus_ChatIntegrations."); - l_SerializedOnFailAction[nameof(Models.Action.Type)] = l_OnFailActionType; - } - - var l_MatchingType = AvailableActions.Where(x => x.GetTypeName() == l_OnFailActionType).FirstOrDefault(); - - if (l_MatchingType == null) - { - /// Todo backup this action to avoid loss - Logger.Instance?.Error($"[Modules.ChatIntegrations.Interfaces][IEvent.Unserialize] Missing action type \"{l_OnFailActionType}\""); - continue; - } - - /// Create instance - var l_NewOnFailAction = Activator.CreateInstance(l_MatchingType.GetType()) as Interfaces.IActionBase; - l_NewOnFailAction.Event = this; - - /// Unserialize action - if (!l_NewOnFailAction.Unserialize(l_SerializedOnFailAction)) - { - /// Todo backup this event to avoid loss - Logger.Instance?.Error($"[Modules.ChatIntegrations.Interfaces][IEvent.Unserialize] Failed to unserialize action\n\"{l_OnFailActionType.ToString()}\""); - continue; - } - - OnFailActions.Add(l_NewOnFailAction); - } - } - - p_Error = ""; - - if (string.IsNullOrEmpty(Model.GUID)) - Model.GUID = Guid.NewGuid().ToString(); - - return true; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Add an condition to the event - /// - /// Condition to add - public override sealed void AddCondition(IConditionBase p_Condition) - { - Conditions.Add(p_Condition); - } - /// - /// Move condition - /// - /// Condition to move - public override sealed int MoveCondition(IConditionBase p_Condition, bool p_Up) - { - var l_Index = Conditions.IndexOf(p_Condition); - - if (l_Index == -1 || (l_Index == 0 && p_Up) || (l_Index == (Conditions.Count - 1) && !p_Up)) - return -1; - - Conditions.Remove(p_Condition); - Conditions.Insert(l_Index + (p_Up ? -1 : 1), p_Condition); - - return Conditions.IndexOf(p_Condition); - } - /// - /// Delete an condition from the event - /// - /// Condition to delete - public override sealed void DeleteCondition(IConditionBase p_Condition) - { - Conditions.Remove(p_Condition); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Add an action to the event - /// - /// Action to add - public override sealed void AddAction(IActionBase p_Action) - { - Actions.Add(p_Action); - } - /// - /// Move action - /// - /// Action to move - public override sealed int MoveAction(IActionBase p_Action, bool p_Up) - { - var l_Index = Actions.IndexOf(p_Action); - - if (l_Index == -1 || (l_Index == 0 && p_Up) || (l_Index == (Actions.Count - 1) && !p_Up)) - return -1; - - Actions.Remove(p_Action); - Actions.Insert(l_Index + (p_Up ? -1 : 1), p_Action); - - return Actions.IndexOf(p_Action); - } - /// - /// Delete an action from the event - /// - /// Action to delete - public override sealed void DeleteAction(IActionBase p_Action) - { - Actions.Remove(p_Action); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Add an on fail action to the event - /// - /// Action to add - public override sealed void AddOnFailAction(IActionBase p_OnFailAction) - { - OnFailActions.Add(p_OnFailAction); - } - /// - /// Move an on fail action - /// - /// Action to move - public override sealed int MoveOnFailAction(IActionBase p_OnFailAction, bool p_Up) - { - var l_Index = OnFailActions.IndexOf(p_OnFailAction); - - if (l_Index == -1 || (l_Index == 0 && p_Up) || (l_Index == (OnFailActions.Count - 1) && !p_Up)) - return -1; - - OnFailActions.Remove(p_OnFailAction); - OnFailActions.Insert(l_Index + (p_Up ? -1 : 1), p_OnFailAction); - - return OnFailActions.IndexOf(p_OnFailAction); - } - /// - /// Delete an on fail action from the event - /// - /// Action to delete - public override sealed void DeleteOnFailAction(IActionBase p_OnFailAction) - { - OnFailActions.Remove(p_OnFailAction); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ModPresence.cs b/Modules/BeatSaberPlus_ChatIntegrations/ModPresence.cs deleted file mode 100644 index 322353d..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/ModPresence.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace BeatSaberPlus_ChatIntegrations -{ - internal class ModPresence - { - public static bool Camera2 - { - get - { - if (!m_Camera2.HasValue) - m_Camera2 = IPA.Loader.PluginManager.GetPluginFromId("Camera2") != null; - - return m_Camera2.Value; - } - } - public static bool Camera2Fixed - { - get - { - if (!Camera2) - return false; - - if (!m_Camera2Fixed.HasValue) - m_Camera2Fixed = IPA.Loader.PluginManager.GetPluginFromId("Camera2").HVersion >= new Hive.Versioning.Version(0, 6, 9); - - return m_Camera2Fixed.Value; - } - } - private static bool? m_Camera2; - private static bool? m_Camera2Fixed; - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Action.cs b/Modules/BeatSaberPlus_ChatIntegrations/Models/Action.cs deleted file mode 100644 index b0693c7..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Action.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace BeatSaberPlus_ChatIntegrations.Models -{ - /// - /// Action data modal - /// - [Serializable] - public class Action - { - [JsonProperty] - public string Type = "?"; - [JsonProperty] - public bool Enabled = false; - [JsonProperty] - public string BaseValue = ""; - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Camera2.cs b/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Camera2.cs deleted file mode 100644 index c64c05f..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/Camera2.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Newtonsoft.Json; - -namespace BeatSaberPlus_ChatIntegrations.Models.Actions -{ - public class Camera2_SwitchToScene : Action - { - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string SceneName = ""; - } - - public class Camera2_ToggleCamera : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ToggleType = 0; - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string CameraName = ""; - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/GamePlay.cs b/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/GamePlay.cs deleted file mode 100644 index 2b01db2..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/GamePlay.cs +++ /dev/null @@ -1,130 +0,0 @@ -using Newtonsoft.Json; - -namespace BeatSaberPlus_ChatIntegrations.Models.Actions -{ - public class GamePlay_ChangeBombColor : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ValueType = 0; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string Color = "#CCCCCC"; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool SendChatMessage = true; - } - - public class GamePlay_ChangeBombScale : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ValueType = 0; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float UserValue = 0.5f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Min = 0.4f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Max = 1.5f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool SendChatMessage = true; - } - - public class GamePlay_ChangeDebris : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool Debris = false; - } - - public class GamePlay_ChangeLightIntensity : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ValueType = 0; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float UserValue = 0.5f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Min = 0.5f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Max = 2f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool SendChatMessage = true; - } - - public class GamePlay_ChangeMusicVolume : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ValueType = 0; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float UserValue = 0.5f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Min = 0.5f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Max = 2f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool SendChatMessage = true; - } - - public class GamePlay_ChangeNoteColors : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ValueType = 0; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string Left = "#FF0000"; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string Right = "#0000FF"; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool SendChatMessage = true; - } - - public class GamePlay_ChangeNoteScale : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ValueType = 0; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float UserValue = 0.5f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Min = 0.4f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Max = 1.5f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool SendChatMessage = true; - } - - public class GamePlay_Pause : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool HideUI = false; - } - - public class GamePlay_SpawnSquatWalls : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Interval = 2f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int Count = 10; - } - - public class GamePlay_SpawnBombPatterns : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public float Interval = 2f; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int Count = 1; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public byte L0 = 0b00000111; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public byte L1 = 0b00000111; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public byte L2 = 0b00000111; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public byte L3 = 0b00000111; - } - - public class GamePlay_ToggleHUD : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ToggleType = 0; - } - - public class GamePlay_ToggleLights : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ChangeType = 0; - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/OBS.cs b/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/OBS.cs deleted file mode 100644 index a595d27..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/OBS.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Newtonsoft.Json; - -namespace BeatSaberPlus_ChatIntegrations.Models.Actions -{ - public class OBS_RenameLastRecord : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string Format = "$OriginalName (Completed!)"; - } - - public class OBS_SetRecordFilenameFormat : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string Format = "%CCYY-%MM-%DD %hh-%mm-%ss"; - } - - public class OBS_SwitchPreviewToScene : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string SceneName = ""; - } - - public class OBS_SwitchToScene : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string SceneName = ""; - } - - public class OBS_ToggleStudioMode : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ToggleType = 0; - } - - public class OBS_ToggleSource : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ToggleType = 0; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string SceneName = ""; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string SourceName = ""; - } - - public class OBS_ToggleSourceAudio : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ToggleType = 0; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string SceneName = ""; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string SourceName = ""; - } - - public class OBS_Transition : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool OverrideDuration = false; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int Duration = 300; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool OverrideTransition = false; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public string Transition = "Fade"; - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/SongChartVisualizer.cs b/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/SongChartVisualizer.cs deleted file mode 100644 index df1804c..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Actions/SongChartVisualizer.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace BeatSaberPlus_ChatIntegrations.Models.Actions -{ - public class SongChartVisualizer_ToggleVisibility : Action - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int ToggleType = 0; - } -} \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Bits.cs b/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Bits.cs deleted file mode 100644 index 8e89fe2..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Bits.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace BeatSaberPlus_ChatIntegrations.Models.Conditions -{ - public class Bits_Amount : Condition - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool IsGreaterThan = true; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public uint Count = 10; - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/ChatRequest.cs b/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/ChatRequest.cs deleted file mode 100644 index d6f4a62..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/ChatRequest.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; - -namespace BeatSaberPlus_ChatIntegrations.Models.Conditions -{ - public class ChatRequest_QueueDuration : Condition - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool IsGreaterThan = true; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public uint Duration = 10 * 60; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool SendChatMessageOnFail = true; - } - - public class ChatRequest_QueueSize : Condition - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool IsGreaterThan = true; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public uint Count = 10; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool SendChatMessageOnFail = true; - } - - public class ChatRequest_QueueStatus : Condition - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool IsOpen = true; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool SendChatMessageOnFail = true; - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/GamePlay.cs b/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/GamePlay.cs deleted file mode 100644 index d30c260..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/GamePlay.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace BeatSaberPlus_ChatIntegrations.Models.Conditions -{ - public class GamePlay_LevelEndType : Condition - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool Pass = true; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool Fail = true; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool Quit = true; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public bool Restart = true; - } - - public class GamePlay_PlayingMap : Condition - { - public enum ELevelType - { - Any, - Solo, - Multiplayer, - SoloAndMultiplayer, - Replay, - } - public enum EBeatmapModType - { - All, - NonNoodle, - Noodle, - Chroma, - NoodleOrChroma - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - public ELevelType LevelType = ELevelType.Solo; - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include), JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - public EBeatmapModType BeatmapModType = 0; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On deserialized - /// - /// Input data - public override void OnDeserialized(JObject p_Serialized) - { - if (p_Serialized.ContainsKey("LevelType") - || !p_Serialized.ContainsKey("Solo") - || !p_Serialized.ContainsKey("Multi") - || !p_Serialized.ContainsKey("Replay") - || !p_Serialized.ContainsKey("BeatmapType")) - return; - - if (p_Serialized["Solo"].Value() && p_Serialized["Multi"].Value() && p_Serialized["Replay"].Value()) - LevelType = ELevelType.Any; - else if (p_Serialized["Solo"].Value() && p_Serialized["Multi"].Value() && !p_Serialized["Replay"].Value()) - LevelType = ELevelType.SoloAndMultiplayer; - else if (!p_Serialized["Solo"].Value() && p_Serialized["Multi"].Value() && !p_Serialized["Replay"].Value()) - LevelType = ELevelType.Multiplayer; - else if (!p_Serialized["Solo"].Value() && !p_Serialized["Multi"].Value() && p_Serialized["Replay"].Value()) - LevelType = ELevelType.Replay; - else - LevelType = ELevelType.Any; - - BeatmapModType = (EBeatmapModType)p_Serialized["BeatmapType"].Value(); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Subscription.cs b/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Subscription.cs deleted file mode 100644 index 0c68a43..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/Models/Conditions/Subscription.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace BeatSaberPlus_ChatIntegrations.Models.Conditions -{ - public class Subscription_PlanType : Condition - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public int PlanType = 0; - } - - public class Subscription_PurchasedMonthCount : Condition - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] - public uint Count = 0; - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/ModulePresence.cs b/Modules/BeatSaberPlus_ChatIntegrations/ModulePresence.cs deleted file mode 100644 index 8ba68e8..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/ModulePresence.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace BeatSaberPlus_ChatIntegrations -{ - internal class ModulePresence - { - public static bool Chat - { - get - { - if (!m_Chat.HasValue) - m_Chat = IPA.Loader.PluginManager.GetPluginFromId("BeatSaberPlus_Chat") != null; - - return m_Chat.Value; - } - } - private static bool? m_Chat; - - public static bool ChatEmoteRain - { - get - { - if (!m_ChatEmoteRain.HasValue) - m_ChatEmoteRain = IPA.Loader.PluginManager.GetPluginFromId("BeatSaberPlus_ChatEmoteRain") != null; - - return m_ChatEmoteRain.Value; - } - } - private static bool? m_ChatEmoteRain; - - public static bool ChatRequest - { - get - { - if (!m_ChatRequest.HasValue) - m_ChatRequest = IPA.Loader.PluginManager.GetPluginFromId("BeatSaberPlus_ChatRequest") != null; - - return m_ChatRequest.Value; - } - } - private static bool? m_ChatRequest; - - public static bool GameTweaker { get - { - if (!m_GameTweaker.HasValue) - m_GameTweaker = IPA.Loader.PluginManager.GetPluginFromId("BeatSaberPlus_GameTweaker") != null; - - return m_GameTweaker.Value; - } - } - private static bool? m_GameTweaker; - - public static bool NoteTweaker - { - get - { - if (!m_NoteTweaker.HasValue) - m_NoteTweaker = IPA.Loader.PluginManager.GetPluginFromId("BeatSaberPlus_NoteTweaker") != null; - - return m_NoteTweaker.Value; - } - } - private static bool? m_NoteTweaker; - - public static bool SongChartVisualizer - { - get - { - if (!m_SongChartVisualizer.HasValue) - m_SongChartVisualizer = IPA.Loader.PluginManager.GetPluginFromId("BeatSaberPlus_SongChartVisualizer") != null; - - return m_SongChartVisualizer.Value; - } - } - private static bool? m_SongChartVisualizer; - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/Properties/AssemblyInfo.cs b/Modules/BeatSaberPlus_ChatIntegrations/Properties/AssemblyInfo.cs index 297a13e..d1d6033 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/Properties/AssemblyInfo.cs +++ b/Modules/BeatSaberPlus_ChatIntegrations/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] \ No newline at end of file +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings.bsml b/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings.bsml deleted file mode 100644 index 88b29b4..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings.bsml +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings.cs b/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings.cs deleted file mode 100644 index 3b6a78d..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings.cs +++ /dev/null @@ -1,1061 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using BeatSaberMarkupLanguage.Components.Settings; -using HMUI; -using System; -using System.Collections.Generic; -using System.Linq; -using TMPro; -using UnityEngine; -using UnityEngine.UI; - -namespace BeatSaberPlus_ChatIntegrations.UI -{ - /// - /// Chat integrations main settings view - /// - internal partial class Settings : BeatSaberPlus.SDK.UI.ResourceViewController - { - /// - /// Maximum item on a list page - /// - private static int s_CONDITION_ACTION_PER_PAGE = 8; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIObject("MessageFrame")] - private GameObject m_MessageFrame = null; - [UIObject("MessageFrame_Background")] - private GameObject m_MessageFrame_Background = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("EventFrame")] - private GameObject m_EventFrame = null; - - [UIObject("EventFrame_TabSelector")] - private GameObject m_EventFrame_TabSelector; - private TextSegmentedControl m_EventFrame_TabSelectorControl = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("EventFrame_TriggerTab")] - private GameObject m_EventFrame_TriggerTab = null; - [UIComponent("EventFrame_TriggerTab_Title")] - private TextMeshProUGUI m_EventFrame_TriggerTab_Title = null; - [UIObject("EventFrame_TriggerTab_EventContent")] - private GameObject m_EventFrame_TriggerTab_EventContent = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("EventFrame_ConditionsTab")] - private GameObject m_EventFrame_ConditionsTab = null; - - [UIComponent("EventFrame_ConditionsTab_UpButton")] - private Button m_EventFrame_ConditionsTab_UpButton = null; - [UIObject("EventFrame_ConditionsTab_List")] - private GameObject m_EventFrame_ConditionsTab_ListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_EventFrame_ConditionsTab_List = null; - [UIComponent("EventFrame_ConditionsTab_DownButton")] - private Button m_EventFrame_ConditionsTab_DownButton = null; - - [UIObject("EventFrame_ConditionsTab_ConditionContent")] - private GameObject m_EventFrame_ConditionsTab_ConditionContent = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("EventFrame_ActionsTab")] - private GameObject m_EventFrame_ActionsTab = null; - - [UIComponent("EventFrame_ActionsTab_UpButton")] - private Button m_EventFrame_ActionsTab_UpButton = null; - [UIObject("EventFrame_ActionsTab_List")] - private GameObject m_EventFrame_ActionsTab_ListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_EventFrame_ActionsTab_List = null; - [UIComponent("EventFrame_ActionsTab_DownButton")] - private Button m_EventFrame_ActionsTab_DownButton = null; - - [UIObject("EventFrame_ActionsTab_ActionContent")] - private GameObject m_EventFrame_ActionsTab_ActionContent = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("EventFrame_OnFailActionsTab")] - private GameObject m_EventFrame_OnFailActionsTab = null; - - [UIComponent("EventFrame_OnFailActionsTab_UpButton")] - private Button m_EventFrame_OnFailActionsTab_UpButton = null; - [UIObject("EventFrame_OnFailActionsTab_List")] - private GameObject m_EventFrame_OnFailActionsTab_ListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_EventFrame_OnFailActionsTab_List = null; - [UIComponent("EventFrame_OnFailActionsTab_DownButton")] - private Button m_EventFrame_OnFailActionsTab_DownButton = null; - - [UIObject("EventFrame_OnFailActionsTab_ActionContent")] - private GameObject m_EventFrame_OnFailActionsTab_ActionContent = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("InputKeyboard")] - private ModalKeyboard m_InputKeyboard = null; - [UIValue("InputKeyboardValue")] - private string m_InputKeyboardValue = ""; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private Interfaces.IEventBase m_CurrentEvent = null; - private int m_ConditionListPage = 1; - private int m_SelectedCondition = -1; - private int m_ActionListPage = 1; - private int m_SelectedAction = -1; - private int m_OnFailActionListPage = 1; - private int m_SelectedOnFailAction = -1; - - private Dictionary m_ConditionAddingMatches = new Dictionary(); - - /// - /// Keyboard original key count - /// - private int m_InputKeyboardInitialKeyCount = -1; - /// - /// Input keyboard callback - /// - private Action m_InputKeyboardCallback = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override void OnViewCreation() - { - /// Update opacity - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_MessageFrame_Background, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_EventFrame_TriggerTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_EventFrame_ConditionsTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_EventFrame_ConditionsTab_ConditionContent, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_EventFrame_ActionsTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_EventFrame_ActionsTab_ActionContent, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_EventFrame_OnFailActionsTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_EventFrame_OnFailActionsTab_ActionContent, 0.50f); - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_InputKeyboard.modalView, 0.75f); - - /// Scale down up & down button - m_EventFrame_ConditionsTab_UpButton.transform.localScale = Vector3.one * 0.6f; - m_EventFrame_ConditionsTab_DownButton.transform.localScale = Vector3.one * 0.6f; - m_EventFrame_ActionsTab_UpButton.transform.localScale = Vector3.one * 0.6f; - m_EventFrame_ActionsTab_DownButton.transform.localScale = Vector3.one * 0.6f; - m_EventFrame_OnFailActionsTab_UpButton.transform.localScale = Vector3.one * 0.6f; - m_EventFrame_OnFailActionsTab_DownButton.transform.localScale = Vector3.one * 0.6f; - - /// Setup condition list - if (m_EventFrame_ConditionsTab_ListView.GetComponent()) - { - var l_LayoutElement = m_EventFrame_ConditionsTab_ListView.GetComponent(); - l_LayoutElement.preferredWidth = 45; - l_LayoutElement.preferredHeight = 40; - - var l_BSMLTableView = m_EventFrame_ConditionsTab_ListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_EventFrame_ConditionsTab_ListView.GetComponentInChildren()); - m_EventFrame_ConditionsTab_List = l_BSMLTableView.gameObject.AddComponent(); - m_EventFrame_ConditionsTab_List.TableViewInstance = l_BSMLTableView; - m_EventFrame_ConditionsTab_List.CellSizeValue = 5f; - l_BSMLTableView.didSelectCellWithIdxEvent += OnConditionSelected; - l_BSMLTableView.SetDataSource(m_EventFrame_ConditionsTab_List, false); - - /// Bind events - m_EventFrame_ConditionsTab_UpButton.onClick.AddListener(OnConditionPageUpPressed); - m_EventFrame_ConditionsTab_DownButton.onClick.AddListener(OnConditionPageDownPressed); - } - - /// Setup action list - if (m_EventFrame_ActionsTab_ListView.GetComponent()) - { - var l_LayoutElement = m_EventFrame_ActionsTab_ListView.GetComponent(); - l_LayoutElement.preferredWidth = 45; - l_LayoutElement.preferredHeight = 40; - - var l_BSMLTableView = m_EventFrame_ActionsTab_ListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_EventFrame_ActionsTab_ListView.GetComponentInChildren()); - m_EventFrame_ActionsTab_List = l_BSMLTableView.gameObject.AddComponent(); - m_EventFrame_ActionsTab_List.TableViewInstance = l_BSMLTableView; - m_EventFrame_ActionsTab_List.CellSizeValue = 5f; - l_BSMLTableView.didSelectCellWithIdxEvent += OnActionSelected; - l_BSMLTableView.SetDataSource(m_EventFrame_ActionsTab_List, false); - - /// Bind events - m_EventFrame_ActionsTab_UpButton.onClick.AddListener(OnActionPageUpPressed); - m_EventFrame_ActionsTab_DownButton.onClick.AddListener(OnActionPageDownPressed); - } - - /// Setup on fail action list - if (m_EventFrame_OnFailActionsTab_ListView.GetComponent()) - { - var l_LayoutElement = m_EventFrame_OnFailActionsTab_ListView.GetComponent(); - l_LayoutElement.preferredWidth = 45; - l_LayoutElement.preferredHeight = 40; - - var l_BSMLTableView = m_EventFrame_OnFailActionsTab_ListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_EventFrame_OnFailActionsTab_ListView.GetComponentInChildren()); - m_EventFrame_OnFailActionsTab_List = l_BSMLTableView.gameObject.AddComponent(); - m_EventFrame_OnFailActionsTab_List.TableViewInstance = l_BSMLTableView; - m_EventFrame_OnFailActionsTab_List.CellSizeValue = 5f; - l_BSMLTableView.didSelectCellWithIdxEvent += OnOnFailActionSelected; - l_BSMLTableView.SetDataSource(m_EventFrame_OnFailActionsTab_List, false); - - /// Bind events - m_EventFrame_OnFailActionsTab_UpButton.onClick.AddListener(OnOnFailActionPageUpPressed); - m_EventFrame_OnFailActionsTab_DownButton.onClick.AddListener(OnOnFailActionPageDownPressed); - } - SetupAddConditionFrame(); - SetupAddActionFrame(); - - /// Create type selector - m_EventFrame_TabSelectorControl = BeatSaberPlus.SDK.UI.TextSegmentedControl.Create(m_EventFrame_TabSelector.transform as RectTransform, false); - m_EventFrame_TabSelectorControl.SetTexts(new string[] { "Trigger", "Conditions", "On Success Actions", "On Fail Actions" }); - m_EventFrame_TabSelectorControl.ReloadData(); - m_EventFrame_TabSelectorControl.didSelectCellEvent += OnTabSelected; - - /// Select a null event to hide everything - SelectEvent(null); - } - /// - /// On view activation - /// - protected override void OnViewActivation() - { - - } - /// - /// On view deactivation - /// - protected override void OnViewDeactivation() - { - ChatIntegrations.Instance.OnBroadcasterChatMessage = null; - ChatIntegrations.Instance.OnVoiceAttackCommandExecuted = null; - ChatIntegrations.Instance.SaveDatabase(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On GUI event - /// - private void OnGUI() - { - if (!m_InputKeyboard || !m_InputKeyboard.modalView.gameObject.activeInHierarchy) - return; - - var l_Event = Event.current; - if (l_Event.isKey && l_Event.type == EventType.KeyDown) - { - var l_KeyCode = l_Event.keyCode; - - /// Convert top row keyboard numbers to numpad numbers - if (l_KeyCode >= KeyCode.Alpha0 && l_KeyCode <= KeyCode.Alpha9) - l_KeyCode += 208; - - switch (l_KeyCode) - { - case KeyCode.Backspace: - if (m_InputKeyboard.keyboard.KeyboardText.text.Length > 0) - m_InputKeyboard.SetText(m_InputKeyboard.keyboard.KeyboardText.text.Substring(0, m_InputKeyboard.keyboard.KeyboardText.text.Length - 1)); - break; - - default: - if (l_Event.character != '\0' && !char.IsControl(l_Event.character)) - m_InputKeyboard.SetText(m_InputKeyboard.keyboard.KeyboardText.text + l_Event.character); - break; - } - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Select event to edit - /// - /// - internal void SelectEvent(Interfaces.IEventBase p_Event) - { - ChatIntegrations.Instance.OnBroadcasterChatMessage = null; - ChatIntegrations.Instance.OnVoiceAttackCommandExecuted = null; - - CloseAllModals(); - HideKeyboard(); - - m_CurrentEvent = p_Event; - - /// Clean up trigger specific UI - if (m_EventFrame_TriggerTab_EventContent.transform.childCount != 0) - GameObject.DestroyImmediate(m_EventFrame_TriggerTab_EventContent.transform.GetChild(0).gameObject); - - OnConditionSelected(null, -1); - OnActionSelected(null, -1); - OnOnFailActionSelected(null, -1); - - /// Hide everything if no event selection - if (p_Event == null) - { - m_EventFrame.SetActive(false); - m_AddConditionFrame.SetActive(false); - m_AddActionFrame.SetActive(false); - m_MessageFrame.SetActive(true); - return; - } - - //////////////////////////////////////////////////////////////////////////// - /// Trigger tab - m_EventFrame_TriggerTab_Title.SetText("" + p_Event.GetTypeNameShort() + " | " + p_Event.GenericModel.Name + ""); - p_Event.BuildUI(m_EventFrame_TriggerTab_EventContent.transform); - //////////////////////////////////////////////////////////////////////////// - /// Conditions tab - m_ConditionListPage = 1; - m_SelectedCondition = -1; - RebuildConditionList(m_CurrentEvent.Conditions.FirstOrDefault()); - //////////////////////////////////////////////////////////////////////////// - /// Actions - m_ActionListPage = 1; - m_SelectedAction = -1; - RebuildActionList(m_CurrentEvent.Actions.FirstOrDefault()); - //////////////////////////////////////////////////////////////////////////// - /// OnFailActions - m_OnFailActionListPage = 1; - m_SelectedOnFailAction = -1; - RebuildOnFailActionList(m_CurrentEvent.OnFailActions.FirstOrDefault()); - //////////////////////////////////////////////////////////////////////////// - - /// Update UI - m_MessageFrame.SetActive(false); - m_AddConditionFrame.SetActive(false); - m_AddActionFrame.SetActive(false); - m_EventFrame.SetActive(true); - - /// Force first tab to be active - m_EventFrame_TabSelectorControl.SelectCellWithNumber(0); - OnTabSelected(null, 0); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When a tab is selected - /// - /// Tab control instance - /// Tab index - private void OnTabSelected(SegmentedControl p_SegmentControl, int p_TabIndex) - { - m_EventFrame_TriggerTab.SetActive(p_TabIndex == 0); - m_EventFrame_ConditionsTab.SetActive(p_TabIndex == 1); - m_EventFrame_ActionsTab.SetActive(p_TabIndex == 2); - m_EventFrame_OnFailActionsTab.SetActive(p_TabIndex == 3); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous condition page - /// - private void OnConditionPageUpPressed() - { - /// Underflow check - if (m_ConditionListPage < 2) - return; - - /// Decrement current page - m_ConditionListPage--; - - /// Rebuild list - RebuildConditionList(null); - } - /// - /// Rebuilt condition list - /// - /// To focus - private void RebuildConditionList(Interfaces.IConditionBase p_ConditionToFocus) - { - if (!UICreated) - return; - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(m_CurrentEvent.Conditions.Count) / (float)(s_CONDITION_ACTION_PER_PAGE))); - - if (p_ConditionToFocus != null) - { - var l_Index = m_CurrentEvent.Conditions.IndexOf(p_ConditionToFocus); - if (l_Index != -1) - m_ConditionListPage = (l_Index / s_CONDITION_ACTION_PER_PAGE) + 1; - else - OnConditionSelected(null, -1); - } - - /// Update overflow - m_ConditionListPage = Math.Max(1, Math.Min(m_ConditionListPage, l_PageCount)); - - /// Update UI - m_EventFrame_ConditionsTab_UpButton.interactable = m_ConditionListPage > 1; - m_EventFrame_ConditionsTab_DownButton.interactable = m_ConditionListPage < l_PageCount; - - /// Clear old entries - m_EventFrame_ConditionsTab_List.TableViewInstance.ClearSelection(); - m_EventFrame_ConditionsTab_List.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (m_ConditionListPage - 1) * s_CONDITION_ACTION_PER_PAGE; - l_I < m_CurrentEvent.Conditions.Count && l_I < (m_ConditionListPage * s_CONDITION_ACTION_PER_PAGE); - ++l_I) - { - var l_Condition = m_CurrentEvent.Conditions[l_I]; - var l_Name = FancyShortTypeName(l_Condition.GetTypeNameShort(), l_Condition.IsEnabled); - var l_Description = l_Condition.Description; - - m_EventFrame_ConditionsTab_List.Data.Add((l_Name, l_Description)); - - if (l_Condition == p_ConditionToFocus) - l_RelIndexToFocus = m_EventFrame_ConditionsTab_List.Data.Count - 1; - } - - /// Refresh - m_EventFrame_ConditionsTab_List.TableViewInstance.ReloadData(); - - /// Update focus - if (m_CurrentEvent.Conditions.Count == 0) - OnConditionSelected(null, -1); - else if (l_RelIndexToFocus != -1) - m_EventFrame_ConditionsTab_List.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// When an condition is selected - /// - /// List instance - /// Selected index - private void OnConditionSelected(TableView p_List, int p_RelIndex) - { - /// Clean up condition specific UI - if (m_EventFrame_ConditionsTab_ConditionContent.transform.childCount != 0) - GameObject.DestroyImmediate(m_EventFrame_ConditionsTab_ConditionContent.transform.GetChild(0).gameObject); - - int l_ConditionIndex = ((m_ConditionListPage - 1) * s_CONDITION_ACTION_PER_PAGE) + p_RelIndex; - - if (p_RelIndex < 0 || l_ConditionIndex >= m_CurrentEvent.Conditions.Count) - { - m_SelectedCondition = -1; - return; - } - - m_SelectedCondition = l_ConditionIndex; - - var l_Condition = m_CurrentEvent.Conditions[l_ConditionIndex]; - l_Condition.BuildUI(m_EventFrame_ConditionsTab_ConditionContent.transform); - } - /// - /// Go to next condition page - /// - private void OnConditionPageDownPressed() - { - /// Increment current page - m_ConditionListPage++; - - /// Rebuild list - RebuildConditionList(null); - } - /// - /// Move condition down - /// - [UIAction("click-condition-movedown-btn-pressed")] - private void OnConditionMoveDownPressed() - { - if (m_SelectedCondition == -1) - return; - - var l_Condition = m_CurrentEvent.Conditions[m_SelectedCondition]; - m_SelectedCondition = m_CurrentEvent.MoveCondition(l_Condition, false); - - RebuildConditionList(l_Condition); - } - /// - /// Move condition up - /// - [UIAction("click-condition-moveup-btn-pressed")] - private void OnConditionMoveUpPressed() - { - if (m_SelectedCondition == -1) - return; - - var l_Condition = m_CurrentEvent.Conditions[m_SelectedCondition]; - m_SelectedCondition = m_CurrentEvent.MoveCondition(l_Condition, true); - - RebuildConditionList(l_Condition); - } - /// - /// Toggle condition button - /// - [UIAction("click-condition-toggle-btn-pressed")] - private void OnConditionToggleButton() - { - if (m_SelectedCondition == -1) - { - ShowMessageModal("Please select an condition first!"); - return; - } - - var l_Condition = m_CurrentEvent.Conditions[m_SelectedCondition]; - if (l_Condition.IsEnabled) - { - ShowConfirmationModal($"Do you want to disable condition\n\"{l_Condition.GetTypeNameShort()}\"?", () => - { - l_Condition.IsEnabled = false; - RebuildConditionList(null); - }); - } - else - { - ShowConfirmationModal($"Do you want to enable condition\n\"{l_Condition.GetTypeNameShort()}\"?", () => - { - l_Condition.IsEnabled = true; - RebuildConditionList(null); - }); - } - } - /// - /// On delete condition button - /// - [UIAction("click-condition-delete-btn-pressed")] - private void OnConditionDeleteButton() - { - if (m_SelectedCondition == -1) - { - ShowMessageModal("Please select an condition first!"); - return; - } - - var l_Condition = m_CurrentEvent.Conditions[m_SelectedCondition]; - ShowConfirmationModal($"Do you want to delete condition\n\"{l_Condition.GetTypeNameShort()}\"?", () => - { - OnConditionSelected(null, -1); - m_CurrentEvent.DeleteCondition(l_Condition); - RebuildConditionList(null); - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous action page - /// - private void OnActionPageUpPressed() - { - /// Underflow check - if (m_ActionListPage < 2) - return; - - /// Decrement current page - m_ActionListPage--; - - /// Rebuild list - RebuildActionList(null); - } - /// - /// Rebuilt action list - /// - /// Should keep actual focus - private void RebuildActionList(Interfaces.IActionBase p_ActionToFocus) - { - if (!UICreated) - return; - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(m_CurrentEvent.Actions.Count) / (float)(s_CONDITION_ACTION_PER_PAGE))); - - if (p_ActionToFocus != null) - { - var l_Index = m_CurrentEvent.Actions.IndexOf(p_ActionToFocus); - if (l_Index != -1) - m_ActionListPage = (l_Index / s_CONDITION_ACTION_PER_PAGE) + 1; - else - OnActionSelected(null, -1); - } - - /// Update overflow - m_ActionListPage = Math.Max(1, Math.Min(m_ActionListPage, l_PageCount)); - - /// Update UI - m_EventFrame_ActionsTab_UpButton.interactable = m_ActionListPage > 1; - m_EventFrame_ActionsTab_DownButton.interactable = m_ActionListPage < l_PageCount; - - /// Clear old entries - m_EventFrame_ActionsTab_List.TableViewInstance.ClearSelection(); - m_EventFrame_ActionsTab_List.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (m_ActionListPage - 1) * s_CONDITION_ACTION_PER_PAGE; - l_I < m_CurrentEvent.Actions.Count && l_I < (m_ActionListPage * s_CONDITION_ACTION_PER_PAGE); - ++l_I) - { - var l_Action = m_CurrentEvent.Actions[l_I]; - var l_Name = FancyShortTypeName(l_Action.GetTypeNameShort(), l_Action.IsEnabled); - var l_Description = l_Action.Description; - - m_EventFrame_ActionsTab_List.Data.Add((l_Name, l_Description)); - - if (l_Action == p_ActionToFocus) - l_RelIndexToFocus = m_EventFrame_ActionsTab_List.Data.Count - 1; - } - - /// Refresh - m_EventFrame_ActionsTab_List.TableViewInstance.ReloadData(); - - /// Update focus - if (m_CurrentEvent.Actions.Count == 0) - OnActionSelected(null, -1); - else if (l_RelIndexToFocus != -1) - m_EventFrame_ActionsTab_List.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// When an action is selected - /// - /// List instance - /// Selected index - private void OnActionSelected(TableView p_List, int p_RelIndex) - { - /// Clean up action specific UI - if (m_EventFrame_ActionsTab_ActionContent.transform.childCount != 0) - GameObject.DestroyImmediate(m_EventFrame_ActionsTab_ActionContent.transform.GetChild(0).gameObject); - - int l_ActionIndex = ((m_ActionListPage - 1) * s_CONDITION_ACTION_PER_PAGE) + p_RelIndex; - - if (p_RelIndex < 0 || p_RelIndex >= m_CurrentEvent.Actions.Count) - { - m_SelectedAction = -1; - return; - } - - m_SelectedAction = l_ActionIndex; - - var l_Action = m_CurrentEvent.Actions[l_ActionIndex]; - l_Action.BuildUI(m_EventFrame_ActionsTab_ActionContent.transform); - } - /// - /// Go to next action page - /// - private void OnActionPageDownPressed() - { - /// Increment current page - m_ActionListPage++; - - /// Rebuild list - RebuildActionList(null); - } - /// - /// Move action down - /// - [UIAction("click-action-movedown-btn-pressed")] - private void OnActionMoveDownPressed() - { - if (m_SelectedAction == -1) - return; - - var l_Action = m_CurrentEvent.Actions[m_SelectedAction]; - m_SelectedAction = m_CurrentEvent.MoveAction(l_Action, false); - - RebuildActionList(l_Action); - } - /// - /// Move action up - /// - [UIAction("click-action-moveup-btn-pressed")] - private void OnActionMoveUpPressed() - { - if (m_SelectedAction == -1) - return; - - var l_Action = m_CurrentEvent.Actions[m_SelectedAction]; - m_SelectedAction = m_CurrentEvent.MoveAction(l_Action, true); - - RebuildActionList(l_Action); - } - /// - /// Toggle action button - /// - [UIAction("click-action-toggle-btn-pressed")] - private void OnActionToggleButton() - { - if (m_SelectedAction == -1) - { - ShowMessageModal("Please select an action first!"); - return; - } - - var l_Action = m_CurrentEvent.Actions[m_SelectedAction]; - if (l_Action.IsEnabled) - { - ShowConfirmationModal($"Do you want to disable action\n\"{l_Action.GetTypeNameShort()}\"?", () => - { - l_Action.IsEnabled = false; - RebuildActionList(l_Action); - }); - } - else - { - ShowConfirmationModal($"Do you want to enable action\n\"{l_Action.GetTypeNameShort()}\"?", () => - { - l_Action.IsEnabled = true; - RebuildActionList(l_Action); - }); - } - } - /// - /// On delete action button - /// - [UIAction("click-action-delete-btn-pressed")] - private void OnActionDeleteButton() - { - if (m_SelectedAction == -1) - { - ShowMessageModal("Please select an action first!"); - return; - } - - var l_Action = m_CurrentEvent.Actions[m_SelectedAction]; - ShowConfirmationModal($"Do you want to delete action\n\"{l_Action.GetTypeNameShort()}\"?", () => - { - OnActionSelected(null, -1); - m_CurrentEvent.DeleteAction(l_Action); - RebuildActionList(null); - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous action page - /// - private void OnOnFailActionPageUpPressed() - { - /// Underflow check - if (m_OnFailActionListPage < 2) - return; - - /// Decrement current page - m_OnFailActionListPage--; - - /// Rebuild list - RebuildOnFailActionList(null); - } - /// - /// Rebuilt action list - /// - /// Should keep actual focus - private void RebuildOnFailActionList(Interfaces.IActionBase p_OnFailActionToFocus) - { - if (!UICreated) - return; - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(m_CurrentEvent.OnFailActions.Count) / (float)(s_CONDITION_ACTION_PER_PAGE))); - - if (p_OnFailActionToFocus != null) - { - var l_Index = m_CurrentEvent.OnFailActions.IndexOf(p_OnFailActionToFocus); - if (l_Index != -1) - m_OnFailActionListPage = (l_Index / s_CONDITION_ACTION_PER_PAGE) + 1; - else - OnOnFailActionSelected(null, -1); - } - - /// Update overflow - m_OnFailActionListPage = Math.Max(1, Math.Min(m_OnFailActionListPage, l_PageCount)); - - /// Update UI - m_EventFrame_OnFailActionsTab_UpButton.interactable = m_OnFailActionListPage > 1; - m_EventFrame_OnFailActionsTab_DownButton.interactable = m_OnFailActionListPage < l_PageCount; - - /// Clear old entries - m_EventFrame_OnFailActionsTab_List.TableViewInstance.ClearSelection(); - m_EventFrame_OnFailActionsTab_List.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (m_OnFailActionListPage - 1) * s_CONDITION_ACTION_PER_PAGE; - l_I < m_CurrentEvent.OnFailActions.Count && l_I < (m_OnFailActionListPage * s_CONDITION_ACTION_PER_PAGE); - ++l_I) - { - var l_OnFailAction = m_CurrentEvent.OnFailActions[l_I]; - var l_Name = FancyShortTypeName(l_OnFailAction.GetTypeNameShort(), l_OnFailAction.IsEnabled); - var l_Description = l_OnFailAction.Description; - - m_EventFrame_OnFailActionsTab_List.Data.Add((l_Name, l_Description)); - - if (l_OnFailAction == p_OnFailActionToFocus) - l_RelIndexToFocus = m_EventFrame_OnFailActionsTab_List.Data.Count - 1; - } - - /// Refresh - m_EventFrame_OnFailActionsTab_List.TableViewInstance.ReloadData(); - - /// Update focus - if (m_CurrentEvent.OnFailActions.Count == 0) - OnOnFailActionSelected(null, -1); - else if (l_RelIndexToFocus != -1) - m_EventFrame_OnFailActionsTab_List.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// When an on fail action is selected - /// - /// List instance - /// Selected index - private void OnOnFailActionSelected(TableView p_List, int p_RelIndex) - { - /// Clean up action specific UI - if (m_EventFrame_OnFailActionsTab_ActionContent.transform.childCount != 0) - GameObject.DestroyImmediate(m_EventFrame_OnFailActionsTab_ActionContent.transform.GetChild(0).gameObject); - - int l_OnFailActionIndex = ((m_OnFailActionListPage - 1) * s_CONDITION_ACTION_PER_PAGE) + p_RelIndex; - - if (p_RelIndex < 0 || p_RelIndex >= m_CurrentEvent.OnFailActions.Count) - { - m_SelectedOnFailAction = -1; - return; - } - - m_SelectedOnFailAction = l_OnFailActionIndex; - - var l_OnFailAction = m_CurrentEvent.OnFailActions[l_OnFailActionIndex]; - l_OnFailAction.BuildUI(m_EventFrame_OnFailActionsTab_ActionContent.transform); - } - /// - /// Go to next on fail action page - /// - private void OnOnFailActionPageDownPressed() - { - /// Increment current page - m_OnFailActionListPage++; - - /// Rebuild list - RebuildOnFailActionList(null); - } - /// - /// Move on fail action down - /// - [UIAction("click-onfailaction-movedown-btn-pressed")] - private void OnOnFailActionMoveDownPressed() - { - if (m_SelectedOnFailAction == -1) - return; - - var l_OnFailAction = m_CurrentEvent.OnFailActions[m_SelectedOnFailAction]; - m_SelectedOnFailAction = m_CurrentEvent.MoveOnFailAction(l_OnFailAction, false); - - RebuildOnFailActionList(l_OnFailAction); - } - /// - /// Move on fail action up - /// - [UIAction("click-onfailaction-moveup-btn-pressed")] - private void OnOnFailActionMoveUpPressed() - { - if (m_SelectedOnFailAction == -1) - return; - - var l_OnFailAction = m_CurrentEvent.OnFailActions[m_SelectedOnFailAction]; - m_SelectedOnFailAction = m_CurrentEvent.MoveOnFailAction(l_OnFailAction, true); - - RebuildOnFailActionList(l_OnFailAction); - } - /// - /// Toggle on fail action button - /// - [UIAction("click-onfailaction-toggle-btn-pressed")] - private void OnOnFailActionToggleButton() - { - if (m_SelectedOnFailAction == -1) - { - ShowMessageModal("Please select an action first!"); - return; - } - - var l_OnFailAction = m_CurrentEvent.OnFailActions[m_SelectedOnFailAction]; - if (l_OnFailAction.IsEnabled) - { - ShowConfirmationModal($"Do you want to disable action\n\"{l_OnFailAction.GetTypeNameShort()}\"?", () => - { - l_OnFailAction.IsEnabled = false; - RebuildOnFailActionList(l_OnFailAction); - }); - } - else - { - ShowConfirmationModal($"Do you want to enable action\n\"{l_OnFailAction.GetTypeNameShort()}\"?", () => - { - l_OnFailAction.IsEnabled = true; - RebuildOnFailActionList(l_OnFailAction); - }); - } - } - /// - /// On delete on fail action button - /// - [UIAction("click-onfailaction-delete-btn-pressed")] - private void OnOnFailActionDeleteButton() - { - if (m_SelectedOnFailAction == -1) - { - ShowMessageModal("Please select an action first!"); - return; - } - - var l_OnFailAction = m_CurrentEvent.OnFailActions[m_SelectedOnFailAction]; - ShowConfirmationModal($"Do you want to delete action\n\"{l_OnFailAction.GetTypeNameShort()}\"?", () => - { - OnOnFailActionSelected(null, -1); - m_CurrentEvent.DeleteOnFailAction(l_OnFailAction); - RebuildOnFailActionList(null); - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Show input keyboard - /// - /// Start value - /// On enter callback - /// Custom keys - public void UIShowInputKeyboard(string p_Value, Action p_Callback, List<(string, Action)> p_CustomKeys = null) - { - m_InputKeyboardValue = p_Value; - - /// Clear old keys - if (m_InputKeyboardInitialKeyCount == -1) - m_InputKeyboardInitialKeyCount = m_InputKeyboard.keyboard.keys.Count; - - while (m_InputKeyboard.keyboard.keys.Count > m_InputKeyboardInitialKeyCount) - { - var l_Key = m_InputKeyboard.keyboard.keys.ElementAt(m_InputKeyboard.keyboard.keys.Count - 1); - m_InputKeyboard.keyboard.Clear(l_Key); - m_InputKeyboard.keyboard.keys.RemoveAt(m_InputKeyboard.keyboard.keys.Count - 1); - - GameObject.Destroy(l_Key.mybutton.gameObject); - } - - /// Add custom keys - if (p_CustomKeys != null && p_CustomKeys.Count != 0) - { - var l_FirstButton = m_InputKeyboard.keyboard.BaseButton.GetComponentInChildren(); - var l_Color = new Color(0.92f, 0.64f, 0); - var l_ButtonY = 11f; - var l_Margin = 1f; - var l_TotalLeft = -35.0f; - - var l_I = 0; - foreach (var l_Var in p_CustomKeys) - { - var l_Position = new Vector2(l_TotalLeft, l_ButtonY); - var l_Width = l_FirstButton.GetPreferredValues(l_Var.Item1).x * 2.0f; - var l_Key = new KEYBOARD.KEY(m_InputKeyboard.keyboard, l_Position, l_Var.Item1, l_Width, 10f, l_Color); - - l_TotalLeft += ((l_Width / 2.0f) + l_Margin); - l_Key.keyaction += (_) => l_Var.Item2.Invoke(); - - m_InputKeyboard.keyboard.keys.Add(l_Key); - ++l_I; - } - } - - /// Show keyboard - m_InputKeyboardCallback = p_Callback; - m_InputKeyboard.keyboard.KeyboardText.fontSizeMax = 3; - m_InputKeyboard.keyboard.KeyboardText.fontSizeMin = 3; - m_InputKeyboard.keyboard.KeyboardText.enableAutoSizing = true; - m_InputKeyboard.keyboard.KeyboardCursor.fontSizeMax = 3; - m_InputKeyboard.keyboard.KeyboardCursor.fontSizeMin = 3; - m_InputKeyboard.keyboard.KeyboardCursor.enableAutoSizing = true; - m_InputKeyboard.modalView.Show(true); - } - /// - /// Append value to current keyboard input - /// - /// Value to append - public void UIInputKeyboardAppend(string p_Value) - { - m_InputKeyboard.keyboard.KeyboardText.text += p_Value; - } - /// - /// On input keyboard enter pressed - /// - /// - [UIAction("InputKeyboardEnterPressed")] - private void InputKeyboardEnterPressed(string p_Text) - { - m_InputKeyboardCallback?.Invoke(p_Text); - } - /// - /// Close keyboard - /// - private void HideKeyboard() - { - m_InputKeyboard.modalView.Hide(false); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Show loading modal - /// - public void UIShowLoading() - { - ShowLoadingModal(); - } - /// - /// Show message modal - /// - public void UIShowMessageModal(string p_Message) - { - ShowMessageModal(p_Message); - } - /// - /// Hide loading modal - /// - public void UIHideLoading() - { - HideLoadingModal(); - } - /// - /// Set pending message - /// - /// Message - public void UISetPendingMessage(string p_Message) - { - SetMessageModal_PendingMessage(p_Message); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private string FancyShortTypeName(string p_Input, bool p_Enabled) - { - return "" + (p_Enabled ? "" + p_Input.Replace("_", "::") : "" + p_Input.Replace("_", "::")); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsLeft.bsml b/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsLeft.bsml deleted file mode 100644 index 29461fa..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsLeft.bsml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsLeft.cs b/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsLeft.cs deleted file mode 100644 index 447332e..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsLeft.cs +++ /dev/null @@ -1,77 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using System.Diagnostics; -using UnityEngine; - -namespace BeatSaberPlus_ChatIntegrations.UI -{ - internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController - { - private static readonly string s_InformationsStr = "" - + "\nSpecial thanks to HypersonicSharkz#3301 for help on TwitchAPI and some Actions code!" - + "\nThis module allow you execute actions on your game when triggered by events." - + "\n" - + "\nEvents" - + "\n- ChatBits\nWhen someone spends bits your channel!" - + "\n- ChatCommand\nAllow you to create chat commands and execute actions with them" - + "\n- ChatFollow\nWhen someone follows your channel" - + "\n- ChatPointsReward\nAllow you to create channel points rewards and fully configure them and bind some actions to them" - + "\n- ChatSubscription\nWhen someone subscribes or subgifts" - + "\n- Dummy\nDummy event that can get triggered by other events" - + "\n- LevelEnded\nWhen you exit a map" - + "\n- LevelPaused\nWhen you pause a map" - + "\n- LevelResumed\nWhen you resume a map" - + "\n- LevelStarted\nWhen you enter a map" - + "\n- VoiceAttackCommand\nBind VoiceAttack commands to BS+" - + "\n" - + "\n"; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("Background")] - private GameObject m_Background = null; - [UIComponent("Informations")] - private HMUI.TextPageScrollView m_Informations = null; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); - m_Informations.SetText(s_InformationsStr); - m_Informations.UpdateVerticalScrollIndicator(0); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Open web configuration button - /// - [UIAction("click-open-web-configuration-btn-pressed")] - private void OnWebConfigurationButton() - { - ShowMessageModal("URL opened in your desktop browser."); - CP_SDK.Chat.Service.OpenWebConfigurator(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Documentation button - /// - [UIAction("click-documentation-btn-pressed")] - private void OnDocumentationButton() - { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://github.com/hardcpp/BeatSaberPlus/wiki#chat-integrations"); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsRight.bsml b/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsRight.bsml deleted file mode 100644 index 98a281e..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsRight.bsml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsRight.cs b/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsRight.cs deleted file mode 100644 index a41e1c2..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/UI/SettingsRight.cs +++ /dev/null @@ -1,988 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using BeatSaberMarkupLanguage.Components.Settings; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; -using UnityEngine.UI; - -namespace BeatSaberPlus_ChatIntegrations.UI -{ - /// - /// Settings right event list view - /// - internal class SettingsRight : BeatSaberPlus.SDK.UI.ResourceViewController - { - /// - /// Event line per page - /// - private static int EVENT_PER_PAGE = 10; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Sub view enum - /// - private enum SubView - { - Main, - AddEvent, - ImportEvent, - TemplateEvent - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIObject("FilterFrame")] - private GameObject m_FilterFrame = null; - [UIComponent("FilterFrame_DropDown")] - private DropDownListSetting m_FilterFrame_DropDown; - [UIValue("FilterFrame_DropDownOptions")] - private List m_FilterFrame_DropDownOptions = new List() { "All" }; - - [UIObject("EventListFrame_Background")] - private GameObject m_EventListFrame = null; - [UIObject("EventListFrame_Background")] - private GameObject m_EventListFrame_Background = null; - [UIObject("EventsList")] - private GameObject m_EventsListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_EventsList = null; - - [UIComponent("EventsUpButton")] - private Button m_EventsUpButton = null; - [UIComponent("EventsDownButton")] - private Button m_EventsDownButton = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("EventListButtonsFrame")] - private GameObject m_EventListButtonsFrame = null; - [UIObject("EventListButtonsFrame2")] - private GameObject m_EventListButtonsFrame2 = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("AddEventFrame")] - private GameObject m_AddEventFrame = null; - [UIObject("AddEventFrame_Background")] - private GameObject m_AddEventFrame_Background = null; - [UIComponent("AddEventFrame_DropDown")] - private DropDownListSetting m_AddEventFrame_DropDown; - [UIValue("AddEventFrame_DropDownOptions")] - private List m_AddEventFrame_DropDownOptions = new List() { "Loading...", }; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("ImportEventFrame")] - private GameObject m_ImportEventFrame = null; - [UIObject("ImportEventFrame_Background")] - private GameObject m_ImportEventFrame_Background = null; - [UIComponent("ImportEventFrame_DropDown")] - private DropDownListSetting m_ImportEventFrame_DropDown; - [UIValue("ImportEventFrame_DropDownOptions")] - private List m_ImportEventFrame_DropDownOptions = new List() { "Loading...", }; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("TemplateEventFrame")] - private GameObject m_TemplateEventFrame = null; - [UIObject("TemplateEventFrame_Background")] - private GameObject m_TemplateEventFrame_Background = null; - [UIComponent("TemplateEventFrame_DropDown")] - private DropDownListSetting m_TemplateEventFrame_DropDown; - [UIValue("TemplateEventFrame_DropDownOptions")] - private List m_TemplateEventFrame_DropDownOptions = new List() { "Loading...", }; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("NewEventNameKeyboard")] - private ModalKeyboard m_NewEventNameKeyboard = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("RenameKeyboard")] - private ModalKeyboard m_RenameKeyboard = null; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Current filter - /// - private string m_CurrentFilter = null; - /// - /// Filtered list - /// - private List m_FilteredList = new List(); - /// - /// Current event list page - /// - private int m_CurrentPage = 1; - /// - /// Selected index - /// - private int m_SelectedIndex = -1; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override void OnViewCreation() - { - /// Update background color - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_EventListFrame_Background, 0.5f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_AddEventFrame_Background, 0.75f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_ImportEventFrame_Background, 0.75f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_TemplateEventFrame_Background, 0.75f); - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_NewEventNameKeyboard.modalView, 0.75f); - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_RenameKeyboard.modalView, 0.75f); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnFilterChanged), BindingFlags.Instance | BindingFlags.NonPublic)); - - /// Setup filters - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_FilterFrame_DropDown, l_Event, true, 1f); - - /// Scale down up & down button - m_EventsUpButton.transform.localScale = Vector3.one * 0.5f; - m_EventsDownButton.transform.localScale = Vector3.one * 0.5f; - - /// Prepare event list - var l_BSMLTableView = m_EventsListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_EventsListView.GetComponentInChildren()); - m_EventsList = l_BSMLTableView.gameObject.AddComponent(); - m_EventsList.TableViewInstance = l_BSMLTableView; - m_EventsList.CellSizeValue = 4.8f; - l_BSMLTableView.didSelectCellWithIdxEvent += OnEventSelected; - l_BSMLTableView.SetDataSource(m_EventsList, false); - - /// Bind events - m_EventsUpButton.onClick.AddListener(OnPageUpPressed); - m_EventsDownButton.onClick.AddListener(OnPageDownPressed); - - /// Remove new event type selector label - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_AddEventFrame_DropDown, null, true, 0.95f); - - /// Remove import event type selector label - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_ImportEventFrame_DropDown, null, true, 0.95f); - - /// Remove template event type selector label - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_TemplateEventFrame_DropDown, null, true, 0.95f); - - OnFilterChanged(null); - } - /// - /// On view activation - /// - protected override void OnViewActivation() - { - var l_Filters = new List() { "All" }; - var l_Types = new List(); - - foreach (var l_CurrentType in ChatIntegrations.RegisteredEventTypes) - { - l_Filters.Add(l_CurrentType.GetType().Name); - l_Types.Add(l_CurrentType.GetType().Name); - } - - m_FilterFrame_DropDownOptions = l_Filters; - m_FilterFrame_DropDown.values = l_Filters; - m_FilterFrame_DropDown.UpdateChoices(); - - m_AddEventFrame_DropDownOptions = l_Types; - m_AddEventFrame_DropDown.values = l_Types; - m_AddEventFrame_DropDown.UpdateChoices(); - - SwitchSubView(SubView.Main); - - RebuildList(null); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On GUI event - /// - private void OnGUI() - { - if ((!m_NewEventNameKeyboard || !m_NewEventNameKeyboard.modalView.gameObject.activeInHierarchy) - && (!m_RenameKeyboard || !m_RenameKeyboard.modalView.gameObject.activeInHierarchy)) - return; - - var l_Event = Event.current; - if (l_Event.isKey && l_Event.type == EventType.KeyDown) - { - var l_KeyCode = l_Event.keyCode; - - /// Convert top row keyboard numbers to numpad numbers - if (l_KeyCode >= KeyCode.Alpha0 && l_KeyCode <= KeyCode.Alpha9) - l_KeyCode += 208; - - switch (l_KeyCode) - { - case KeyCode.Backspace: - if (m_NewEventNameKeyboard.keyboard.KeyboardText.text.Length > 0) - m_NewEventNameKeyboard.SetText(m_NewEventNameKeyboard.keyboard.KeyboardText.text.Substring(0, m_NewEventNameKeyboard.keyboard.KeyboardText.text.Length - 1)); - - if (m_RenameKeyboard.keyboard.KeyboardText.text.Length > 0) - m_RenameKeyboard.SetText(m_RenameKeyboard.keyboard.KeyboardText.text.Substring(0, m_RenameKeyboard.keyboard.KeyboardText.text.Length - 1)); - break; - - default: - if (l_Event.character != '\0' && !char.IsControl(l_Event.character)) - { - m_NewEventNameKeyboard.SetText(m_NewEventNameKeyboard.keyboard.KeyboardText.text + l_Event.character); - m_RenameKeyboard.SetText(m_RenameKeyboard.keyboard.KeyboardText.text + l_Event.character); - } - break; - } - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On filter changed - /// - /// New value - private void OnFilterChanged(object p_Value) - { - m_CurrentFilter = (string)m_FilterFrame_DropDown.Value; - - m_FilteredList.Clear(); - foreach (var l_Event in ChatIntegrations.Instance.Events) - { - if ((m_CurrentFilter == null || m_CurrentFilter == "All") || m_CurrentFilter == l_Event.GetTypeNameShort()) - m_FilteredList.Add(l_Event); - } - m_FilteredList.Sort((x, y) => (x.GetTypeNameShort() + x.GenericModel.Name).CompareTo((y.GetTypeNameShort() + y.GenericModel.Name))); - m_CurrentPage = 1; - - RebuildList(null); - } - /// - /// Go to previous event page - /// - private void OnPageUpPressed() - { - /// Underflow check - if (m_CurrentPage < 2) - return; - - /// Decrement current page - m_CurrentPage--; - - /// Rebuild list - RebuildList(null); - } - /// - /// Rebuild list - /// - /// Event to auto select - private void RebuildList(Interfaces.IEventBase p_EventToSelect) - { - if (!UICreated) - return; - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(m_FilteredList.Count) / (float)(EVENT_PER_PAGE))); - - if (p_EventToSelect != null) - { - var l_Index = m_FilteredList.IndexOf(p_EventToSelect); - if (l_Index != -1) - m_CurrentPage = (l_Index / EVENT_PER_PAGE) + 1; - else - OnEventSelected(null, -1); - } - - /// Update overflow - m_CurrentPage = Math.Max(1, Math.Min(m_CurrentPage, l_PageCount)); - - /// Update UI - m_EventsUpButton.interactable = m_CurrentPage > 1; - m_EventsDownButton.interactable = m_CurrentPage < l_PageCount; - - /// Clear old entries - m_EventsList.TableViewInstance.ClearSelection(); - m_EventsList.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (m_CurrentPage - 1) * EVENT_PER_PAGE; - l_I < m_FilteredList.Count && l_I < (m_CurrentPage * EVENT_PER_PAGE); - ++l_I) - { - var l_Event = m_FilteredList[l_I]; - - m_EventsList.Data.Add(BuildLineString(l_Event)); - - if (l_Event == p_EventToSelect) - l_RelIndexToFocus = m_EventsList.Data.Count - 1; - } - - /// Refresh - m_EventsList.TableViewInstance.ReloadData(); - - /// Update focus - if (m_FilteredList.Count == 0) - OnEventSelected(null, -1); - else if (l_RelIndexToFocus != -1) - m_EventsList.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// On event selected - /// - /// TableView instance - /// Relative index - private void OnEventSelected(HMUI.TableView p_TableView, int p_RelIndex) - { - int l_EventIndex = ((m_CurrentPage - 1) * EVENT_PER_PAGE) + p_RelIndex; - - if (p_RelIndex < 0 || l_EventIndex >= m_FilteredList.Count) - { - m_SelectedIndex = -1; - Settings.Instance?.SelectEvent(null); - return; - } - - m_SelectedIndex = l_EventIndex; - - Settings.Instance?.SelectEvent(m_FilteredList[m_SelectedIndex]); - } - /// - /// Go to next event page - /// - private void OnPageDownPressed() - { - /// Increment current page - m_CurrentPage++; - - /// Rebuild list - RebuildList(null); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// New event button - /// - [UIAction("click-new-btn-pressed")] - private void OnNewButton() - { - SwitchSubView(SubView.AddEvent); - } - /// - /// New event cancel button - /// - [UIAction("click-cancel-add-event-btn-pressed")] - private void OnAddEventCancelButton() - { - SwitchSubView(SubView.Main); - } - /// - /// New event create button - /// - [UIAction("click-add-event-btn-pressed")] - private void OnAddEventCreateButton() - { - SwitchSubView(SubView.Main); - ShowModal("OpenNewEventNameModal"); - } - /// - /// On new event name keyboard enter pressed - /// - /// - [UIAction("NewEventNameKeyboardPressed")] - internal void NewEventNameKeyboardPressed(string p_Text) - { - var l_TypeName = (string)m_AddEventFrame_DropDown.Value; - var l_EventName = p_Text; - var l_MatchingType = ChatIntegrations.RegisteredEventTypes.Where(x => x.GetType().Name == l_TypeName).FirstOrDefault(); - - if (l_MatchingType != null) - { - /// Create instance - var l_NewEvent = Activator.CreateInstance(l_MatchingType.GetType()) as Interfaces.IEventBase; - l_NewEvent.GenericModel.Name = l_EventName; - - ChatIntegrations.Instance.AddEvent(l_NewEvent); - - if (m_FilterFrame_DropDown.Value != null && (string)m_FilterFrame_DropDown.Value != "All" && (string)m_FilterFrame_DropDown.Value != l_NewEvent.GetTypeNameShort()) - m_FilterFrame_DropDown.Value = (object)l_NewEvent.GetTypeNameShort(); - - OnFilterChanged(null); - RebuildList(l_NewEvent); - } - } - /// - /// Rename event button - /// - [UIAction("click-rename-btn-pressed")] - private void OnRenameButton() - { - if (!EnsureEventSelected()) - return; - - var l_Event = m_FilteredList[m_SelectedIndex]; - m_RenameKeyboard.SetText(l_Event.GenericModel.Name); - ShowModal("OpenRenameModal"); - } - /// - /// On rename keyboard enter pressed - /// - /// - [UIAction("RenameKeyboardPressed")] - internal void RenameKeyboardPressed(string p_Text) - { - var l_Event = m_FilteredList[m_SelectedIndex]; - l_Event.GenericModel.Name = string.IsNullOrEmpty(p_Text) ? "No name..." : p_Text; - - OnFilterChanged(null); - RebuildList(l_Event); - } - /// - /// Delete event button - /// - [UIAction("click-delete-btn-pressed")] - private void OnDeleteButton() - { - if (!EnsureEventSelected()) - return; - - var l_Event = m_FilteredList[m_SelectedIndex]; - ShowConfirmationModal($"Do you want to delete event\n\"{l_Event.GenericModel.Name}\"?", () => - { - OnEventSelected(null, -1); - ChatIntegrations.Instance.DeleteEvent(l_Event); - OnFilterChanged(null); - RebuildList(null); - }); - } - /// - /// Toggle enable/disable on a event - /// - [UIAction("click-toggle-btn-pressed")] - private void OnToggleButton() - { - if (!EnsureEventSelected()) - return; - - var l_Event = m_FilteredList[m_SelectedIndex]; - if (l_Event.IsEnabled) - { - ShowConfirmationModal($"Do you want to disable event\n\"{l_Event.GenericModel.Name}\"?", () => - { - ChatIntegrations.Instance.ToggleEvent(l_Event); - OnFilterChanged(null); - RebuildList(l_Event); - }); - } - else - { - ShowConfirmationModal($"Do you want to enable event\n\"{l_Event.GenericModel.Name}\"?", () => - { - ChatIntegrations.Instance.ToggleEvent(l_Event); - OnFilterChanged(null); - RebuildList(l_Event); - }); - } - } - /// - /// Export an event - /// - [UIAction("click-export-btn-pressed")] - private void OnExportButton() - { - if (!EnsureEventSelected()) - return; - - var l_Event = m_FilteredList[m_SelectedIndex]; - var l_Serialized = l_Event.Serialize(); - - var l_EventName = l_Event.GenericModel.Name; - if (l_EventName.Length > 20) - l_EventName = l_EventName.Substring(0, 20); - - var l_FileName = CP_SDK.Misc.Time.UnixTimeNow() + "_" + l_Event.GetTypeNameShort() + "_" + l_EventName + ".bspci"; - l_FileName = string.Concat(l_FileName.Split(System.IO.Path.GetInvalidFileNameChars())); - - System.IO.File.WriteAllText(ChatIntegrations.s_EXPORT_PATH + l_FileName, l_Serialized.ToString(Newtonsoft.Json.Formatting.Indented), System.Text.Encoding.Unicode); - - ShowMessageModal("Event exported in\n" + ChatIntegrations.s_EXPORT_PATH); - } - /// - /// Import an event - /// - [UIAction("click-import-btn-pressed")] - private void OnImportButton() - { - SwitchSubView(SubView.ImportEvent); - - var l_Files = new List(); - foreach (var l_File in System.IO.Directory.GetFiles(ChatIntegrations.s_IMPORT_PATH, "*.bspci")) - l_Files.Add(System.IO.Path.GetFileNameWithoutExtension(l_File)); - - m_ImportEventFrame_DropDownOptions = l_Files; - m_ImportEventFrame_DropDown.values = l_Files; - m_ImportEventFrame_DropDown.UpdateChoices(); - } - /// - /// Import event cancel button - /// - [UIAction("click-cancel-import-event-btn-pressed")] - private void OnImportEventCancelButton() - { - SwitchSubView(SubView.Main); - } - /// - /// Import event button - /// - [UIAction("click-import-event-btn-pressed")] - private void OnImportEventButton() - { - var l_FileName = ChatIntegrations.s_IMPORT_PATH + (string)m_ImportEventFrame_DropDown.Value + ".bspci"; - - if (System.IO.File.Exists(l_FileName)) - { - var l_Raw = System.IO.File.ReadAllText(l_FileName, System.Text.Encoding.Unicode); - - try - { - var l_JObject = JObject.Parse(l_Raw); - var l_NewEvent = ChatIntegrations.Instance.AddEventFromSerialized(l_JObject, true, false, out var l_Error); - - if (l_NewEvent != null) - { - SwitchSubView(SubView.Main); - - if (m_FilterFrame_DropDown.Value != null && (string)m_FilterFrame_DropDown.Value != "All" && (string)m_FilterFrame_DropDown.Value != l_NewEvent.GetTypeNameShort()) - m_FilterFrame_DropDown.Value = (object)l_NewEvent.GetTypeNameShort(); - - OnFilterChanged(null); - RebuildList(l_NewEvent); - } - else - ShowMessageModal(l_Error); - } - catch - { - ShowMessageModal("Invalid file!"); - } - } - else - ShowMessageModal("File not found!"); - } - /// - /// Clone an event - /// - [UIAction("click-clone-btn-pressed")] - private void OnCloneButton() - { - if (!EnsureEventSelected()) - return; - - var l_Event = m_FilteredList[m_SelectedIndex]; - var l_Serialized = l_Event.Serialize(); - var l_NewEvent = ChatIntegrations.Instance.AddEventFromSerialized(l_Serialized, false, true, out var _); - - if (l_NewEvent == null) - ShowMessageModal("Clone failed, check BeatSaberPlus logs!"); - else - { - OnFilterChanged(null); - RebuildList(l_NewEvent); - } - } - /// - /// Template event - /// - [UIAction("click-templates-btn-pressed")] - private void OnTemplatesButton() - { - SwitchSubView(SubView.TemplateEvent); - - var l_Templates = new List(); - foreach (var l_Template in GetTemplates()) - l_Templates.Add(l_Template); - - m_TemplateEventFrame_DropDownOptions = l_Templates; - m_TemplateEventFrame_DropDown.values = l_Templates; - m_TemplateEventFrame_DropDown.UpdateChoices(); - } - /// - /// Template event cancel button - /// - [UIAction("click-cancel-template-event-btn-pressed")] - private void OnTemplateEventCancelButton() - { - SwitchSubView(SubView.Main); - } - /// - /// Template event create button - /// - [UIAction("click-create-template-event-btn-pressed")] - private void OnCreateTemplateEventButton() - { - var l_Template = (string)m_TemplateEventFrame_DropDown.Value; - var l_NewEvent = CreateFromTemplate(l_Template); - - if (l_NewEvent == null) - ShowMessageModal("Clone failed, check BeatSaberPlus logs!"); - else - { - SwitchSubView(SubView.Main); - ChatIntegrations.Instance.AddEvent(l_NewEvent); - OnFilterChanged(null); - RebuildList(l_NewEvent); - } - } - /// - /// Convert an event - /// - [UIAction("click-convert-btn-pressed")] - private void OnConvertButton() - { - if (!EnsureEventSelected()) - return; - - var l_Event = m_FilteredList[m_SelectedIndex]; - if (l_Event is Events.Dummy) - { - ShowMessageModal("This event is already a dummy event!"); - return; - } - - ShowConfirmationModal($"Do you want to convert event\n\"{l_Event.GenericModel.Name}\" to Dummy?", () => - { - var l_Serialized = l_Event.Serialize(); - l_Serialized["Type"] = string.Join(".", typeof(Events.Dummy).Namespace, typeof(Events.Dummy).Name); - l_Serialized["Event"]["Type"] = string.Join(".", typeof(Events.Dummy).Namespace, typeof(Events.Dummy).Name); - l_Serialized["Event"]["Name"] += " (Converted)"; - - var l_NewEvent = ChatIntegrations.Instance.AddEventFromSerialized(l_Serialized, false, true, out var _); - - if (l_NewEvent == null) - ShowMessageModal("Clone failed, check BeatSaberPlus logs!"); - else - { - OnFilterChanged(null); - RebuildList(l_NewEvent); - } - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get template lists - /// - /// - private List GetTemplates() - { - return new List() - { - "ChatPointReward : 5 Squats", - "ChatPointReward : Countdown + Emote bomb", - "ChatBits : Thanks message + emote bomb", - "ChatSubscription : Thanks message + emote bomb", - "ChatFollow : Thanks message + emote bomb", - "ChatCommand : Discord command", - "ChatCommand : 250% lights for 10 seconds with cooldown" - }.OrderBy(x => x).ToList(); - } - /// - /// Create from template - /// - /// Template name - /// - private Interfaces.IEventBase CreateFromTemplate(string p_Template) - { - if (p_Template == "ChatPointReward : 5 Squats") - { - var l_Event = new Events.ChatPointsReward(); - l_Event.Model.Cooldown = 60; - l_Event.Model.Cost = 100; - l_Event.Model.Name = "5 Squats (Template)"; - l_Event.Model.Title = "5 Squats (Template)"; - - l_Event.AddCondition(new Conditions.GamePlay_PlayingMap() { Event = l_Event, IsEnabled = true }); - - var l_SquatAction = new Actions.GamePlay_SpawnSquatWalls() { Event = l_Event, IsEnabled = true }; - l_SquatAction.Model.Count = 5; - l_SquatAction.Model.Interval = 5; - l_Event.AddAction(l_SquatAction); - - var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "5 squats from $SenderName, let's gooo!"; - l_Event.AddAction(l_MessageAction); - - return l_Event; - } - else if (p_Template == "ChatPointReward : Countdown + Emote bomb") - { - var l_Event = new Events.ChatPointsReward(); - l_Event.Model.Cooldown = 30; - l_Event.Model.Cost = 100; - l_Event.Model.Name = "Countdown + Emote bomb (Template)"; - l_Event.Model.Title = "Emote bomb (Template)"; - - var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "Explosion in..."; - l_Event.AddAction(l_MessageAction); - l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "3..."; - l_Event.AddAction(l_MessageAction); - - var l_DelayAction = new Actions.Misc_Delay() { Event = l_Event, IsEnabled = true }; - l_DelayAction.Model.Delay = 1; - l_DelayAction.Model.PreventNextActionFailure = false; - l_Event.AddAction(l_DelayAction); - - l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "2..."; - l_Event.AddAction(l_MessageAction); - - l_DelayAction = new Actions.Misc_Delay() { Event = l_Event, IsEnabled = true }; - l_DelayAction.Model.Delay = 1; - l_DelayAction.Model.PreventNextActionFailure = false; - l_Event.AddAction(l_DelayAction); - - l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "1..."; - l_Event.AddAction(l_MessageAction); - - var l_EmoteBombAction = new Actions.EmoteRain_EmoteBombRain() { Event = l_Event, IsEnabled = true }; - l_EmoteBombAction.Model.EmoteKindCount = 25; - l_EmoteBombAction.Model.CountPerEmote = 40; - l_Event.AddAction(l_EmoteBombAction); - - l_DelayAction = new Actions.Misc_Delay() { Event = l_Event, IsEnabled = true }; - l_DelayAction.Model.Delay = 1; - l_DelayAction.Model.PreventNextActionFailure = false; - l_Event.AddAction(l_DelayAction); - - l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "BOOM!"; - l_Event.AddAction(l_MessageAction); - - return l_Event; - } - else if (p_Template == "ChatBits : Thanks message + emote bomb") - { - var l_Event = new Events.ChatBits(); - l_Event.Model.Name = "Thanks message + emote bomb (Template)"; - - var l_CooldownCondition = new Conditions.Misc_Cooldown() { Event = l_Event, IsEnabled = true }; - l_CooldownCondition.Model.PerUser = true; - l_CooldownCondition.Model.NotifyUser = false; - l_CooldownCondition.Model.CooldownTime = 20; - l_Event.Conditions.Add(l_CooldownCondition); - - var l_EmoteBombAction = new Actions.EmoteRain_EmoteBombRain() { Event = l_Event, IsEnabled = true }; - l_EmoteBombAction.Model.EmoteKindCount = 10; - l_EmoteBombAction.Model.CountPerEmote = 10; - l_Event.AddAction(l_EmoteBombAction); - - var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "Thanks $UserName for the $Bits bits!"; - l_Event.AddAction(l_MessageAction); - - return l_Event; - } - else if (p_Template == "ChatSubscription : Thanks message + emote bomb") - { - var l_Event = new Events.ChatSubscription(); - l_Event.Model.Name = "Thanks message + emote bomb (Template)"; - - var l_EmoteBombAction = new Actions.EmoteRain_EmoteBombRain() { Event = l_Event, IsEnabled = true }; - l_EmoteBombAction.Model.EmoteKindCount = 10; - l_EmoteBombAction.Model.CountPerEmote = 10; - l_Event.AddAction(l_EmoteBombAction); - - var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "Thanks $UserName for the $MonthCount of $SubPlan!"; - l_Event.AddAction(l_MessageAction); - - return l_Event; - } - else if (p_Template == "ChatFollow : Thanks message + emote bomb") - { - var l_Event = new Events.ChatFollow(); - l_Event.Model.Name = "Thanks message + emote bomb (Template)"; - - var l_CooldownCondition = new Conditions.Misc_Cooldown() { Event = l_Event, IsEnabled = true }; - l_CooldownCondition.Model.PerUser = true; - l_CooldownCondition.Model.NotifyUser = false; - l_CooldownCondition.Model.CooldownTime = 20 * 60; - l_Event.Conditions.Add(l_CooldownCondition); - - var l_EmoteBombAction = new Actions.EmoteRain_EmoteBombRain() { Event = l_Event, IsEnabled = true }; - l_EmoteBombAction.Model.EmoteKindCount = 5; - l_EmoteBombAction.Model.CountPerEmote = 5; - l_Event.AddAction(l_EmoteBombAction); - - var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "Thanks $UserName for the follow!"; - l_Event.AddAction(l_MessageAction); - - return l_Event; - } - else if (p_Template == "ChatCommand : Discord command") - { - var l_Event = new Events.ChatCommand(); - l_Event.Model.Name = "Discord command (Template)"; - l_Event.Model.Command = "!discord"; - - var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "@$UserName join my amazing discord at https://discord.gg/K4X94Ea"; - l_Event.AddAction(l_MessageAction); - - return l_Event; - } - else if (p_Template == "ChatCommand : 250% lights for 10 seconds with cooldown") - { - var l_Event = new Events.ChatCommand(); - l_Event.Model.Name = "10 seconds of 250% lights with cooldown (Template)"; - l_Event.Model.Command = "!lights"; - - l_Event.AddCondition(new Conditions.GamePlay_PlayingMap() { Event = l_Event, IsEnabled = true }); - - var l_CooldownCondition = new Conditions.Misc_Cooldown() { Event = l_Event, IsEnabled = true }; - l_CooldownCondition.Model.PerUser = true; - l_CooldownCondition.Model.NotifyUser = true; - l_CooldownCondition.Model.CooldownTime = 60; - l_Event.Conditions.Add(l_CooldownCondition); - - l_CooldownCondition = new Conditions.Misc_Cooldown() { Event = l_Event, IsEnabled = true }; - l_CooldownCondition.Model.PerUser = false; - l_CooldownCondition.Model.NotifyUser = true; - l_CooldownCondition.Model.CooldownTime = 20; - l_Event.Conditions.Add(l_CooldownCondition); - - var l_MessageAction = new Actions.Chat_SendMessage() { Event = l_Event, IsEnabled = true }; - l_MessageAction.Model.BaseValue = "Lights go brrrrr"; - l_Event.AddAction(l_MessageAction); - - var l_LightAction = new Actions.GamePlay_ChangeLightIntensity() { Event = l_Event, IsEnabled = true }; - l_LightAction.Model.UserValue = 2.5f; - l_LightAction.Model.SendChatMessage = false; - l_LightAction.Model.ValueType = 1; - l_Event.AddAction(l_LightAction); - - var l_DelayAction = new Actions.Misc_Delay() { Event = l_Event, IsEnabled = true }; - l_DelayAction.Model.Delay = 10; - l_DelayAction.Model.PreventNextActionFailure = true; - l_Event.AddAction(l_DelayAction); - - l_LightAction = new Actions.GamePlay_ChangeLightIntensity() { Event = l_Event, IsEnabled = true }; - l_LightAction.Model.ValueType = 3; - l_LightAction.Model.SendChatMessage = false; - l_Event.AddAction(l_LightAction); - - return l_Event; - } - - return null; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Switch to sub view - /// - /// New subview - private void SwitchSubView(SubView p_SubView) - { - m_FilterFrame.SetActive(p_SubView == SubView.Main); - m_EventListFrame.SetActive(p_SubView == SubView.Main); - m_EventListButtonsFrame.SetActive(p_SubView == SubView.Main); - m_EventListButtonsFrame2.SetActive(p_SubView == SubView.Main); - m_EventsUpButton.gameObject.SetActive(p_SubView == SubView.Main); - m_EventsDownButton.gameObject.SetActive(p_SubView == SubView.Main); - m_AddEventFrame.SetActive(p_SubView == SubView.AddEvent); - m_ImportEventFrame.SetActive(p_SubView == SubView.ImportEvent); - m_TemplateEventFrame.SetActive(p_SubView == SubView.TemplateEvent); - - LayoutRebuilder.ForceRebuildLayoutImmediate(m_FilterFrame.transform.parent.transform as RectTransform); - } - /// - /// Ensure that an event is selected - /// - /// - private bool EnsureEventSelected() - { - if (m_SelectedIndex == -1) - { - ShowMessageModal("Please select an event first!"); - return false; - } - - return true; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Build pick ban line - /// - /// Is a ban - /// Player name - /// Name of the map - /// Built pick ban line - private (string, string) BuildLineString(Interfaces.IEventBase p_Event) - { - /// Result line - string l_Text = ""; - - /// Fake line height - l_Text += ""; - - if (!p_Event.IsEnabled) - l_Text += ""; - else - l_Text += ""; - - /// Left part - l_Text += (p_Event.IsEnabled ? "" : "") + "[" + p_Event.GetTypeNameShort() + "] " + (p_Event.IsEnabled ? "" : ""); - l_Text += " "; - l_Text += p_Event.GenericModel.Name; - - if (!p_Event.IsEnabled) - l_Text += " (Disabled)"; - else - l_Text += ""; - - /// Line break - l_Text += "\n"; - /// Restore line height - l_Text += ""; - - /// Right part - l_Text += ""; - l_Text += "Used " + p_Event.GenericModel.UsageCount + " time(s)"; - - return (l_Text, null); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings_AddActionFrame.cs b/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings_AddActionFrame.cs deleted file mode 100644 index 4fe509b..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings_AddActionFrame.cs +++ /dev/null @@ -1,406 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using HMUI; -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using UnityEngine.UI; - -namespace BeatSaberPlus_ChatIntegrations.UI -{ - /// - /// Chat integrations main settings view - /// - internal partial class Settings - { - private static int s_ADD_ACTION_FRAME_CATEGORY_PER_PAGE = 8; - private static int s_ADD_ACTION_FRAME_TYPE_PER_PAGE = 8; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("AddActionFrame")] - private GameObject m_AddActionFrame = null; - - [UIObject("AddActionFrame_LeftBackground")] - private GameObject m_AddActionFrame_LeftBackground = null; - [UIComponent("AddActionFrame_CategoryUpButton")] - private Button m_AddActionFrame_CategoryUpButton = null; - [UIObject("AddActionFrame_CategoryList")] - private GameObject m_AddActionFrame_CategoryListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_AddActionFrame_CategoryList = null; - [UIComponent("AddActionFrame_CategoryDownButton")] - private Button m_AddActionFrame_CategoryDownButton = null; - - [UIObject("AddActionFrame_RightBackground")] - private GameObject m_AddActionFrame_RightBackground = null; - [UIComponent("AddActionFrame_TypeUpButton")] - private Button m_AddActionFrame_TypeUpButton = null; - [UIObject("AddActionFrame_TypeList")] - private GameObject m_AddActionFrame_TypeListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_AddActionFrame_TypeList = null; - [UIComponent("AddActionFrame_TypeDownButton")] - private Button m_AddActionFrame_TypeDownButton = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private int m_AddActionFrame_CategoryListPage = 1; - private int m_AddActionFrame_SelectedCategory = -1; - - private int m_AddActionFrame_TypeListPage = 1; - private int m_AddActionFrame_SelectedType = -1; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private void SetupAddActionFrame() - { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_AddActionFrame_LeftBackground, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_AddActionFrame_RightBackground, 0.50f); - - m_AddActionFrame_CategoryUpButton.transform.localScale = Vector3.one * 0.6f; - m_AddActionFrame_CategoryDownButton.transform.localScale = Vector3.one * 0.6f; - - /// Setup add action category list - if (m_AddActionFrame_CategoryListView.GetComponent()) - { - var l_LayoutElement = m_AddActionFrame_CategoryListView.GetComponent(); - l_LayoutElement.preferredWidth = 45; - l_LayoutElement.preferredHeight = 40; - - var l_BSMLTableView = m_AddActionFrame_CategoryListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_AddActionFrame_CategoryListView.GetComponentInChildren()); - m_AddActionFrame_CategoryList = l_BSMLTableView.gameObject.AddComponent(); - m_AddActionFrame_CategoryList.TableViewInstance = l_BSMLTableView; - m_AddActionFrame_CategoryList.CellSizeValue = 5f; - l_BSMLTableView.didSelectCellWithIdxEvent += AddActionFrame_CategorySelected; - l_BSMLTableView.SetDataSource(m_AddActionFrame_CategoryList, false); - - /// Bind events - m_AddActionFrame_CategoryUpButton.onClick.AddListener(AddActionFrame_CategoryPageUpPressed); - m_AddActionFrame_CategoryDownButton.onClick.AddListener(AddActionFrame_CategoryPageDownPressed); - } - - m_AddActionFrame_TypeUpButton.transform.localScale = Vector3.one * 0.6f; - m_AddActionFrame_TypeDownButton.transform.localScale = Vector3.one * 0.6f; - - /// Setup add action type list - if (m_AddActionFrame_TypeListView.GetComponent()) - { - var l_LayoutElement = m_AddActionFrame_TypeListView.GetComponent(); - l_LayoutElement.preferredWidth = 45; - l_LayoutElement.preferredHeight = 40; - - var l_BSMLTableView = m_AddActionFrame_TypeListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_AddActionFrame_TypeListView.GetComponentInChildren()); - m_AddActionFrame_TypeList = l_BSMLTableView.gameObject.AddComponent(); - m_AddActionFrame_TypeList.TableViewInstance = l_BSMLTableView; - m_AddActionFrame_TypeList.CellSizeValue = 5f; - l_BSMLTableView.didSelectCellWithIdxEvent += AddActionFrame_TypeSelected; - l_BSMLTableView.SetDataSource(m_AddActionFrame_TypeList, false); - - /// Bind events - m_AddActionFrame_TypeUpButton.onClick.AddListener(AddActionFrame_TypePageUpPressed); - m_AddActionFrame_TypeDownButton.onClick.AddListener(AddActionFrame_TypePageDownPressed); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// New action button - /// - [UIAction("AddActionFrame_Show")] - private void AddActionFrame_Show() - { - AddActionFrame_CategoryRebuildList(); - - if (m_AddActionFrame_SelectedCategory < 0 || m_AddActionFrame_SelectedCategory > AddActionFrame_GetActionCategories().Count) - AddActionFrame_CategorySelected(null, 0); - - AddActionFrame_TypeRebuildList(); - - m_EventFrame.SetActive(false); - m_AddActionFrame.SetActive(true); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous page - /// - private void AddActionFrame_CategoryPageUpPressed() - { - /// Underflow check - if (m_AddActionFrame_CategoryListPage < 2) - return; - - /// Decrement current page - m_AddActionFrame_CategoryListPage--; - - /// Rebuild list - AddActionFrame_CategoryRebuildList(); - } - /// - /// Rebuilt list - /// - private void AddActionFrame_CategoryRebuildList() - { - if (!UICreated) - return; - - var l_Candidates = AddActionFrame_GetActionCategories(); - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(l_Candidates.Count) / (float)(s_ADD_ACTION_FRAME_CATEGORY_PER_PAGE))); - - /// Update overflow - m_AddActionFrame_CategoryListPage = Math.Max(1, Math.Min(m_AddActionFrame_CategoryListPage, l_PageCount)); - - /// Update UI - m_AddActionFrame_CategoryUpButton.interactable = m_AddActionFrame_CategoryListPage > 1; - m_AddActionFrame_CategoryDownButton.interactable = m_AddActionFrame_CategoryListPage < l_PageCount; - - /// Clear old entries - m_AddActionFrame_CategoryList.TableViewInstance.ClearSelection(); - m_AddActionFrame_CategoryList.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (m_AddActionFrame_CategoryListPage - 1) * s_ADD_ACTION_FRAME_CATEGORY_PER_PAGE; - l_I < l_Candidates.Count && l_I < (m_AddActionFrame_CategoryListPage * s_ADD_ACTION_FRAME_CATEGORY_PER_PAGE); - ++l_I) - { - m_AddActionFrame_CategoryList.Data.Add((" " + (l_I + 1) + " - " + l_Candidates[l_I], null)); - } - - /// Refresh - m_AddActionFrame_CategoryList.TableViewInstance.ReloadData(); - - /// Update focus - if (m_CurrentEvent.Conditions.Count == 0) - AddActionFrame_CategorySelected(null, -1); - else if (l_RelIndexToFocus != -1) - m_AddActionFrame_CategoryList.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// When an element is selected - /// - /// List instance - /// Selected index - private void AddActionFrame_CategorySelected(TableView p_List, int p_RelIndex) - { - int l_ConditionIndex = ((m_AddActionFrame_CategoryListPage - 1) * s_ADD_ACTION_FRAME_CATEGORY_PER_PAGE) + p_RelIndex; - - var l_Candidates = AddActionFrame_GetActionCategories(); - if (p_RelIndex < 0 || l_ConditionIndex >= l_Candidates.Count) - { - m_AddActionFrame_SelectedCategory = -1; - return; - } - - m_AddActionFrame_SelectedCategory = l_ConditionIndex; - AddActionFrame_TypeRebuildList(); - } - /// - /// Go to next page - /// - private void AddActionFrame_CategoryPageDownPressed() - { - /// Increment current page - m_AddActionFrame_CategoryListPage++; - - /// Rebuild list - AddActionFrame_CategoryRebuildList(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous page - /// - private void AddActionFrame_TypePageUpPressed() - { - /// Underflow check - if (m_AddActionFrame_TypeListPage < 2) - return; - - /// Decrement current page - m_AddActionFrame_TypeListPage--; - - /// Rebuild list - AddActionFrame_TypeRebuildList(); - } - /// - /// Rebuilt list - /// - private void AddActionFrame_TypeRebuildList() - { - if (!UICreated) - return; - - var l_Candidates = AddActionFrame_GetActionTypes(m_AddActionFrame_SelectedCategory); - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(l_Candidates.Count) / (float)(s_ADD_ACTION_FRAME_TYPE_PER_PAGE))); - - /// Update overflow - m_AddActionFrame_TypeListPage = Math.Max(1, Math.Min(m_AddActionFrame_TypeListPage, l_PageCount)); - - /// Update UI - m_AddActionFrame_TypeUpButton.interactable = m_AddActionFrame_TypeListPage > 1; - m_AddActionFrame_TypeDownButton.interactable = m_AddActionFrame_TypeListPage < l_PageCount; - - /// Clear old entries - m_AddActionFrame_TypeList.TableViewInstance.ClearSelection(); - m_AddActionFrame_TypeList.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (m_AddActionFrame_TypeListPage - 1) * s_ADD_ACTION_FRAME_TYPE_PER_PAGE; - l_I < l_Candidates.Count && l_I < (m_AddActionFrame_TypeListPage * s_ADD_ACTION_FRAME_TYPE_PER_PAGE); - ++l_I) - { - m_AddActionFrame_TypeList.Data.Add((" " + (l_I + 1) + " - " + l_Candidates[l_I].Item2, null)); - } - - /// Refresh - m_AddActionFrame_TypeList.TableViewInstance.ReloadData(); - - /// Update focus - if (m_CurrentEvent.Conditions.Count == 0) - AddActionFrame_TypeSelected(null, -1); - else if (l_RelIndexToFocus != -1) - m_AddActionFrame_TypeList.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// When an element is selected - /// - /// List instance - /// Selected index - private void AddActionFrame_TypeSelected(TableView p_List, int p_RelIndex) - { - int l_ConditionIndex = ((m_AddActionFrame_TypeListPage - 1) * s_ADD_ACTION_FRAME_TYPE_PER_PAGE) + p_RelIndex; - - var l_Candidates = AddActionFrame_GetActionTypes(m_AddActionFrame_SelectedCategory); - if (p_RelIndex < 0 || l_ConditionIndex >= l_Candidates.Count) - { - m_AddActionFrame_SelectedType = -1; - return; - } - - m_AddActionFrame_SelectedType = l_ConditionIndex; - } - /// - /// Go to next page - /// - private void AddActionFrame_TypePageDownPressed() - { - /// Increment current page - m_AddActionFrame_TypeListPage++; - - /// Rebuild list - AddActionFrame_TypeRebuildList(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On cancel add action button pressed - /// - [UIAction("AddActionFrame_OnCancelAddActionButton")] - private void AddActionFrame_OnCancelAddActionButton() - { - m_AddActionFrame.SetActive(false); - m_EventFrame.SetActive(true); - } - /// - /// On add action button pressed - /// - [UIAction("AddActionFrame_OnAddActionButton")] - private void AddActionFrame_OnAddActionButton() - { - if (m_AddActionFrame_SelectedCategory == -1 || m_AddActionFrame_SelectedType == -1) - { - ShowMessageModal("Please select an action!"); - return; - } - - var l_Candidates = AddActionFrame_GetActionTypes(m_AddActionFrame_SelectedCategory); - var l_NewAction = Activator.CreateInstance(l_Candidates[m_AddActionFrame_SelectedType].Item1.GetType()) as Interfaces.IActionBase; - l_NewAction.Event = m_CurrentEvent; - l_NewAction.IsEnabled = true; - - if (m_EventFrame_ActionsTab.gameObject.activeSelf) - { - m_CurrentEvent.AddAction(l_NewAction); - RebuildActionList(l_NewAction); - } - else if (m_EventFrame_OnFailActionsTab.gameObject.activeSelf) - { - m_CurrentEvent.AddOnFailAction(l_NewAction); - RebuildOnFailActionList(l_NewAction); - } - - m_AddActionFrame.SetActive(false); - m_EventFrame.SetActive(true); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get action categories - /// - /// - private List AddActionFrame_GetActionCategories() - { - List l_Result = new List(); - - foreach (var l_Current in m_CurrentEvent.AvailableActions.Select(x => x.GetTypeNameShort())) - { - if (l_Current.Contains("_")) - l_Result.Add(l_Current.Substring(0, l_Current.IndexOf("_"))); - else - l_Result.Add("Others"); - } - - l_Result = l_Result.Distinct().OrderBy(x => x).ToList(); - return l_Result; - } - /// - /// Get actions types for specific category - /// - /// - /// - private List<(Interfaces.IActionBase, string)> AddActionFrame_GetActionTypes(int p_CategoryIndex) - { - var l_Result = new List<(Interfaces.IActionBase, string)>(); - var l_Categories = AddActionFrame_GetActionCategories(); - - if (p_CategoryIndex == -1 || p_CategoryIndex > l_Categories.Count) - return l_Result; - - foreach (var l_Current in m_CurrentEvent.AvailableActions) - { - var l_ShortName = l_Current.GetTypeNameShort(); - if (l_ShortName.Contains("_") && l_ShortName.Substring(0, l_ShortName.IndexOf("_")) == l_Categories[p_CategoryIndex]) - { - l_Result.Add((l_Current, "" + l_ShortName.Substring(0, l_ShortName.IndexOf("_")) - + "::" + l_ShortName.Substring(l_ShortName.IndexOf("_") + 1) + "")); - } - else if (!l_ShortName.Contains("_") && l_Categories[p_CategoryIndex] == "Others") - l_Result.Add((l_Current, "" + l_ShortName + "")); - } - - l_Result = l_Result.Distinct().OrderBy(x => x.Item2).ToList(); - return l_Result; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings_AddConditionFrame.cs b/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings_AddConditionFrame.cs deleted file mode 100644 index 1f6a3e4..0000000 --- a/Modules/BeatSaberPlus_ChatIntegrations/UI/Settings_AddConditionFrame.cs +++ /dev/null @@ -1,399 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using HMUI; -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using UnityEngine.UI; - -namespace BeatSaberPlus_ChatIntegrations.UI -{ - /// - /// Chat integrations main settings view - /// - internal partial class Settings - { - private static int s_ADD_CONDITION_FRAME_CATEGORY_PER_PAGE = 8; - private static int s_ADD_CONDITION_FRAME_TYPE_PER_PAGE = 8; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("AddConditionFrame")] - private GameObject m_AddConditionFrame = null; - - [UIObject("AddConditionFrame_LeftBackground")] - private GameObject m_AddConditionFrame_LeftBackground = null; - [UIComponent("AddConditionFrame_CategoryUpButton")] - private Button m_AddConditionFrame_CategoryUpButton = null; - [UIObject("AddConditionFrame_CategoryList")] - private GameObject m_AddConditionFrame_CategoryListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_AddConditionFrame_CategoryList = null; - [UIComponent("AddConditionFrame_CategoryDownButton")] - private Button m_AddConditionFrame_CategoryDownButton = null; - - [UIObject("AddConditionFrame_RightBackground")] - private GameObject m_AddConditionFrame_RightBackground = null; - [UIComponent("AddConditionFrame_TypeUpButton")] - private Button m_AddConditionFrame_TypeUpButton = null; - [UIObject("AddConditionFrame_TypeList")] - private GameObject m_AddConditionFrame_TypeListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_AddConditionFrame_TypeList = null; - [UIComponent("AddConditionFrame_TypeDownButton")] - private Button m_AddConditionFrame_TypeDownButton = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private int m_AddConditionFrame_CategoryListPage = 1; - private int m_AddConditionFrame_SelectedCategory = -1; - - private int m_AddConditionFrame_TypeListPage = 1; - private int m_AddConditionFrame_SelectedType = -1; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - private void SetupAddConditionFrame() - { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_AddConditionFrame_LeftBackground, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_AddConditionFrame_RightBackground, 0.50f); - - m_AddConditionFrame_CategoryUpButton.transform.localScale = Vector3.one * 0.6f; - m_AddConditionFrame_CategoryDownButton.transform.localScale = Vector3.one * 0.6f; - - /// Setup add condition category list - if (m_AddConditionFrame_CategoryListView.GetComponent()) - { - var l_LayoutElement = m_AddConditionFrame_CategoryListView.GetComponent(); - l_LayoutElement.preferredWidth = 45; - l_LayoutElement.preferredHeight = 40; - - var l_BSMLTableView = m_AddConditionFrame_CategoryListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_AddConditionFrame_CategoryListView.GetComponentInChildren()); - m_AddConditionFrame_CategoryList = l_BSMLTableView.gameObject.AddComponent(); - m_AddConditionFrame_CategoryList.TableViewInstance = l_BSMLTableView; - m_AddConditionFrame_CategoryList.CellSizeValue = 5f; - l_BSMLTableView.didSelectCellWithIdxEvent += AddConditionFrame_CategorySelected; - l_BSMLTableView.SetDataSource(m_AddConditionFrame_CategoryList, false); - - /// Bind events - m_AddConditionFrame_CategoryUpButton.onClick.AddListener(AddConditionFrame_CategoryPageUpPressed); - m_AddConditionFrame_CategoryDownButton.onClick.AddListener(AddConditionFrame_CategoryPageDownPressed); - } - - m_AddConditionFrame_TypeUpButton.transform.localScale = Vector3.one * 0.6f; - m_AddConditionFrame_TypeDownButton.transform.localScale = Vector3.one * 0.6f; - - /// Setup add condition type list - if (m_AddConditionFrame_TypeListView.GetComponent()) - { - var l_LayoutElement = m_AddConditionFrame_TypeListView.GetComponent(); - l_LayoutElement.preferredWidth = 45; - l_LayoutElement.preferredHeight = 40; - - var l_BSMLTableView = m_AddConditionFrame_TypeListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_AddConditionFrame_TypeListView.GetComponentInChildren()); - m_AddConditionFrame_TypeList = l_BSMLTableView.gameObject.AddComponent(); - m_AddConditionFrame_TypeList.TableViewInstance = l_BSMLTableView; - m_AddConditionFrame_TypeList.CellSizeValue = 5f; - l_BSMLTableView.didSelectCellWithIdxEvent += AddConditionFrame_TypeSelected; - l_BSMLTableView.SetDataSource(m_AddConditionFrame_TypeList, false); - - /// Bind events - m_AddConditionFrame_TypeUpButton.onClick.AddListener(AddConditionFrame_TypePageUpPressed); - m_AddConditionFrame_TypeDownButton.onClick.AddListener(AddConditionFrame_TypePageDownPressed); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// New condition button - /// - [UIAction("AddConditionFrame_Show")] - private void AddConditionFrame_Show() - { - AddConditionFrame_CategoryRebuildList(); - - if (m_AddConditionFrame_SelectedCategory < 0 || m_AddConditionFrame_SelectedCategory > AddConditionFrame_GetConditionCategories().Count) - AddConditionFrame_CategorySelected(null, 0); - - AddConditionFrame_TypeRebuildList(); - - m_EventFrame.SetActive(false); - m_AddConditionFrame.SetActive(true); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous page - /// - private void AddConditionFrame_CategoryPageUpPressed() - { - /// Underflow check - if (m_AddConditionFrame_CategoryListPage < 2) - return; - - /// Decrement current page - m_AddConditionFrame_CategoryListPage--; - - /// Rebuild list - AddConditionFrame_CategoryRebuildList(); - } - /// - /// Rebuilt list - /// - private void AddConditionFrame_CategoryRebuildList() - { - if (!UICreated) - return; - - var l_Candidates = AddConditionFrame_GetConditionCategories(); - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(l_Candidates.Count) / (float)(s_ADD_CONDITION_FRAME_CATEGORY_PER_PAGE))); - - /// Update overflow - m_AddConditionFrame_CategoryListPage = Math.Max(1, Math.Min(m_AddConditionFrame_CategoryListPage, l_PageCount)); - - /// Update UI - m_AddConditionFrame_CategoryUpButton.interactable = m_AddConditionFrame_CategoryListPage > 1; - m_AddConditionFrame_CategoryDownButton.interactable = m_AddConditionFrame_CategoryListPage < l_PageCount; - - /// Clear old entries - m_AddConditionFrame_CategoryList.TableViewInstance.ClearSelection(); - m_AddConditionFrame_CategoryList.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (m_AddConditionFrame_CategoryListPage - 1) * s_ADD_CONDITION_FRAME_CATEGORY_PER_PAGE; - l_I < l_Candidates.Count && l_I < (m_AddConditionFrame_CategoryListPage * s_ADD_CONDITION_FRAME_CATEGORY_PER_PAGE); - ++l_I) - { - m_AddConditionFrame_CategoryList.Data.Add((" " + (l_I + 1) + " - " + l_Candidates[l_I], null)); - } - - /// Refresh - m_AddConditionFrame_CategoryList.TableViewInstance.ReloadData(); - - /// Update focus - if (m_CurrentEvent.Conditions.Count == 0) - AddConditionFrame_CategorySelected(null, -1); - else if (l_RelIndexToFocus != -1) - m_AddConditionFrame_CategoryList.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// When an element is selected - /// - /// List instance - /// Selected index - private void AddConditionFrame_CategorySelected(TableView p_List, int p_RelIndex) - { - int l_ConditionIndex = ((m_AddConditionFrame_CategoryListPage - 1) * s_ADD_CONDITION_FRAME_CATEGORY_PER_PAGE) + p_RelIndex; - - var l_Candidates = AddConditionFrame_GetConditionCategories(); - if (p_RelIndex < 0 || l_ConditionIndex >= l_Candidates.Count) - { - m_AddConditionFrame_SelectedCategory = -1; - return; - } - - m_AddConditionFrame_SelectedCategory = l_ConditionIndex; - AddConditionFrame_TypeRebuildList(); - } - /// - /// Go to next page - /// - private void AddConditionFrame_CategoryPageDownPressed() - { - /// Increment current page - m_AddConditionFrame_CategoryListPage++; - - /// Rebuild list - AddConditionFrame_CategoryRebuildList(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous page - /// - private void AddConditionFrame_TypePageUpPressed() - { - /// Underflow check - if (m_AddConditionFrame_TypeListPage < 2) - return; - - /// Decrement current page - m_AddConditionFrame_TypeListPage--; - - /// Rebuild list - AddConditionFrame_TypeRebuildList(); - } - /// - /// Rebuilt list - /// - private void AddConditionFrame_TypeRebuildList() - { - if (!UICreated) - return; - - var l_Candidates = AddConditionFrame_GetConditionTypes(m_AddConditionFrame_SelectedCategory); - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(l_Candidates.Count) / (float)(s_ADD_CONDITION_FRAME_TYPE_PER_PAGE))); - - /// Update overflow - m_AddConditionFrame_TypeListPage = Math.Max(1, Math.Min(m_AddConditionFrame_TypeListPage, l_PageCount)); - - /// Update UI - m_AddConditionFrame_TypeUpButton.interactable = m_AddConditionFrame_TypeListPage > 1; - m_AddConditionFrame_TypeDownButton.interactable = m_AddConditionFrame_TypeListPage < l_PageCount; - - /// Clear old entries - m_AddConditionFrame_TypeList.TableViewInstance.ClearSelection(); - m_AddConditionFrame_TypeList.Data.Clear(); - - int l_RelIndexToFocus = -1; - for (int l_I = (m_AddConditionFrame_TypeListPage - 1) * s_ADD_CONDITION_FRAME_TYPE_PER_PAGE; - l_I < l_Candidates.Count && l_I < (m_AddConditionFrame_TypeListPage * s_ADD_CONDITION_FRAME_TYPE_PER_PAGE); - ++l_I) - { - m_AddConditionFrame_TypeList.Data.Add((" " + (l_I + 1) + " - " + l_Candidates[l_I].Item2, null)); - } - - /// Refresh - m_AddConditionFrame_TypeList.TableViewInstance.ReloadData(); - - /// Update focus - if (m_CurrentEvent.Conditions.Count == 0) - AddConditionFrame_TypeSelected(null, -1); - else if (l_RelIndexToFocus != -1) - m_AddConditionFrame_TypeList.TableViewInstance.SelectCellWithIdx(l_RelIndexToFocus, true); - } - /// - /// When an element is selected - /// - /// List instance - /// Selected index - private void AddConditionFrame_TypeSelected(TableView p_List, int p_RelIndex) - { - int l_ConditionIndex = ((m_AddConditionFrame_TypeListPage - 1) * s_ADD_CONDITION_FRAME_TYPE_PER_PAGE) + p_RelIndex; - - var l_Candidates = AddConditionFrame_GetConditionTypes(m_AddConditionFrame_SelectedCategory); - if (p_RelIndex < 0 || l_ConditionIndex >= l_Candidates.Count) - { - m_AddConditionFrame_SelectedType = -1; - return; - } - - m_AddConditionFrame_SelectedType = l_ConditionIndex; - } - /// - /// Go to next page - /// - private void AddConditionFrame_TypePageDownPressed() - { - /// Increment current page - m_AddConditionFrame_TypeListPage++; - - /// Rebuild list - AddConditionFrame_TypeRebuildList(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On cancel add condition button pressed - /// - [UIAction("AddConditionFrame_OnCancelAddConditionButton")] - private void AddConditionFrame_OnCancelAddConditionButton() - { - m_AddConditionFrame.SetActive(false); - m_EventFrame.SetActive(true); - } - /// - /// On add condition button pressed - /// - [UIAction("AddConditionFrame_OnAddConditionButton")] - private void AddConditionFrame_OnAddConditionButton() - { - if (m_AddConditionFrame_SelectedCategory == -1 || m_AddConditionFrame_SelectedType == -1) - { - ShowMessageModal("Please select a condition!"); - return; - } - - var l_Candidates = AddConditionFrame_GetConditionTypes(m_AddConditionFrame_SelectedCategory); - var l_NewCondition = Activator.CreateInstance(l_Candidates[m_AddConditionFrame_SelectedType].Item1.GetType()) as Interfaces.IConditionBase; - l_NewCondition.Event = m_CurrentEvent; - l_NewCondition.IsEnabled = true; - - m_CurrentEvent.AddCondition(l_NewCondition); - - RebuildConditionList(l_NewCondition); - - m_AddConditionFrame.SetActive(false); - m_EventFrame.SetActive(true); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get condition categories - /// - /// - private List AddConditionFrame_GetConditionCategories() - { - List l_Result = new List(); - - foreach (var l_Current in m_CurrentEvent.AvailableConditions.Select(x => x.GetTypeNameShort())) - { - if (l_Current.Contains("_")) - l_Result.Add(l_Current.Substring(0, l_Current.IndexOf("_"))); - else - l_Result.Add("Others"); - } - - l_Result = l_Result.Distinct().OrderBy(x => x).ToList(); - return l_Result; - } - /// - /// Get conditions types for specific category - /// - /// - /// - private List<(Interfaces.IConditionBase, string)> AddConditionFrame_GetConditionTypes(int p_CategoryIndex) - { - var l_Result = new List<(Interfaces.IConditionBase, string)>(); - var l_Categories = AddConditionFrame_GetConditionCategories(); - - if (p_CategoryIndex == -1 || p_CategoryIndex > l_Categories.Count) - return l_Result; - - foreach (var l_Current in m_CurrentEvent.AvailableConditions) - { - var l_ShortName = l_Current.GetTypeNameShort(); - if (l_ShortName.Contains("_") && l_ShortName.Substring(0, l_ShortName.IndexOf("_")) == l_Categories[p_CategoryIndex]) - { - l_Result.Add((l_Current, "" + l_ShortName.Substring(0, l_ShortName.IndexOf("_")) - + "::" + l_ShortName.Substring(l_ShortName.IndexOf("_") + 1) + "")); - } - else if (!l_ShortName.Contains("_") && l_Categories[p_CategoryIndex] == "Others") - l_Result.Add((l_Current, "" + l_ShortName + "")); - } - - l_Result = l_Result.Distinct().OrderBy(x => x.Item2).ToList(); - return l_Result; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatIntegrations/manifest.json b/Modules/BeatSaberPlus_ChatIntegrations/manifest.json index a41a5d3..2227e1b 100644 --- a/Modules/BeatSaberPlus_ChatIntegrations/manifest.json +++ b/Modules/BeatSaberPlus_ChatIntegrations/manifest.json @@ -3,13 +3,12 @@ "id": "BeatSaberPlus_ChatIntegrations", "name": "BeatSaberPlus_ChatIntegrations", "author": "HardCPP#1985", - "version": "5.0.7", + "version": "6.0.8", "description": "", - "gameVersion": "1.25.0", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.0.1", - "BeatSaberMarkupLanguage": "^1.3.4", - "BeatSaberPlusCORE": "^5.0.7" + "BSIPA": "^4.3.0", + "BeatSaberPlusCORE": "^6.0.8" }, "loadAfter": [ "BeatSaberPlus_Chat", diff --git a/Modules/BeatSaberPlus_ChatRequest/BeatSaberPlus_ChatRequest.csproj b/Modules/BeatSaberPlus_ChatRequest/BeatSaberPlus_ChatRequest.csproj index a549c8d..fafaa53 100644 --- a/Modules/BeatSaberPlus_ChatRequest/BeatSaberPlus_ChatRequest.csproj +++ b/Modules/BeatSaberPlus_ChatRequest/BeatSaberPlus_ChatRequest.csproj @@ -47,11 +47,12 @@ OnBuildSuccess - - $(BeatSaberDir)\Plugins\BSML.dll - False - False - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + $(BeatSaberDir)\Libs\Newtonsoft.Json.dll False @@ -64,10 +65,6 @@ - - - - $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False @@ -76,7 +73,7 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll False - + $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll False @@ -100,18 +97,10 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll - False - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll - False - @@ -119,40 +108,23 @@ + - - - + + + - - - + + + - - Settings.cs - - - ManagerLeft.cs - - - ManagerMain.cs - - - ManagerRight.cs - - - SettingsLeft.cs - - - SettingsRight.cs - diff --git a/Modules/BeatSaberPlus_ChatRequest/BeatSaberPlus_ChatRequest.csproj.user b/Modules/BeatSaberPlus_ChatRequest/BeatSaberPlus_ChatRequest.csproj.user index 5db8d9ee60929239d9779dc6a24adfdc0c01779f..012781eeb9f58b5f0dd22267773d93a2d711944f 100644 GIT binary patch delta 25 fcmaFI@`q)^I!0av215ot1|tSbAZa*xE#pA|UV#Rx delta 11 Tcmeyv@{VQ0I>yO+7!LpdA+QBv diff --git a/Modules/BeatSaberPlus_ChatRequest/ChatRequest.cs b/Modules/BeatSaberPlus_ChatRequest/ChatRequest.cs index 65bea7f..ea83e5a 100644 --- a/Modules/BeatSaberPlus_ChatRequest/ChatRequest.cs +++ b/Modules/BeatSaberPlus_ChatRequest/ChatRequest.cs @@ -1,7 +1,6 @@ -using BeatSaberMarkupLanguage; +using IPA.Utilities; using System.Collections; using System.Linq; -using System.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -11,36 +10,23 @@ namespace BeatSaberPlus_ChatRequest /// /// Chat Request instance /// - public partial class ChatRequest : BeatSaberPlus.SDK.BSPModuleBase + public partial class ChatRequest : CP_SDK.ModuleBase { - /// - /// Module type - /// - public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; - /// - /// Name of the Module - /// - public override string Name => "Chat Request"; - /// - /// Description of the Module - /// - public override string Description => "Take song request from your chat!"; - /// - /// Is the Module using chat features - /// - public override bool UseChatFeatures => true; - /// - /// Is enabled - /// - public override bool IsEnabled { get => CRConfig.Instance.Enabled; set { CRConfig.Instance.Enabled = value; CRConfig.Instance.Save(); } } - /// - /// Activation kind - /// - public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; + public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; + public override string Name => "Chat Request"; + public override string Description => "Take song request from your chat!"; + public override string DocumentationURL => "https://github.com/hardcpp/BeatSaberPlus/wiki#chat-request"; + public override bool UseChatFeatures => true; + public override bool IsEnabled { get => CRConfig.Instance.Enabled; set { CRConfig.Instance.Enabled = value; CRConfig.Instance.Save(); } } + public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// + private UI.SettingsLeftView m_SettingsLeftView = null; + private UI.SettingsMainView m_SettingsMainView = null; + private UI.SettingsRightView m_SettingsRightView = null; + /// /// Create button coroutine /// @@ -58,18 +44,6 @@ public partial class ChatRequest : BeatSaberPlus.SDK.BSPModuleBase /// private UI.ManagerViewFlowCoordinator m_ManagerViewFlowCoordinator = null; /// - /// Chat Request view - /// - private UI.Settings m_SettingsView = null; - /// - /// Chat Request left view - /// - private UI.SettingsLeft m_SettingsLeftView = null; - /// - /// Chat Request right view - /// - private UI.SettingsRight m_SettingsRightView = null; - /// /// Chat core instance /// private bool m_ChatCoreAcquired = false; @@ -95,43 +69,9 @@ protected override void OnEnable() Logger.Instance.Error(l_Exception); } - /// Move old file - try - { - if (System.IO.File.Exists(m_DBFilePathOld)) - { - if (System.IO.File.Exists(m_DBFilePath)) - System.IO.File.Delete(m_DBFilePathOld); - else - System.IO.File.Move(m_DBFilePathOld, m_DBFilePath); - } - } - catch (System.Exception l_Exception) - { - Logger.Instance.Error($"[ChatRequest][ChatRequest.OnEnable] Failed to move database \"{m_DBFilePathOld}\""); - Logger.Instance.Error(l_Exception); - } - - /// Move old file - try - { - if (System.IO.File.Exists(m_SimpleQueueFilePathOld)) - { - if (System.IO.File.Exists(m_SimpleQueueFilePath)) - System.IO.File.Delete(m_SimpleQueueFilePathOld); - else - System.IO.File.Move(m_SimpleQueueFilePathOld, m_SimpleQueueFilePath); - } - } - catch (System.Exception l_Exception) - { - Logger.Instance.Error($"[ChatRequest][ChatRequest.OnEnable] Failed to move database \"{m_DBFilePathOld}\""); - Logger.Instance.Error(l_Exception); - } - /// Try to load DB LoadDatabase(); - UpdateSimpleQueueFile(); + OnQueueChanged(true, true); /// Build command table BuildCommandTable(); @@ -206,6 +146,12 @@ protected override void OnDisable() m_ManagerViewFlowCoordinator = null; } + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsLeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsMainView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsRightView); + + UI.ManagerViewFlowCoordinator.Destroy(); + /// Clear database SongQueue.Clear(); SongHistory.Clear(); @@ -223,20 +169,13 @@ protected override void OnDisable() /// /// Get Module settings UI /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsLeftView == null) - m_SettingsLeftView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsRightView == null) - m_SettingsRightView = BeatSaberUI.CreateViewController(); - - /// Change main view - return (m_SettingsView, m_SettingsLeftView, m_SettingsRightView); + if (m_SettingsLeftView == null) m_SettingsLeftView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsRightView == null) m_SettingsRightView = CP_SDK.UI.UISystem.CreateViewController(); + + return (m_SettingsMainView, m_SettingsLeftView, m_SettingsRightView); } //////////////////////////////////////////////////////////////////////////// @@ -277,11 +216,11 @@ private void OnMenuSceneLoaded() /// When the active scene is changed /// /// New scene - private void OnSceneChange(BeatSaberPlus.SDK.Game.Logic.SceneType p_Scene) + private void OnSceneChange(BeatSaberPlus.SDK.Game.Logic.ESceneType p_Scene) { - if (p_Scene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) + if (p_Scene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Menu) UpdateButton(); - else if (p_Scene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) + else if (p_Scene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) { try { @@ -302,10 +241,10 @@ private void OnSceneChange(BeatSaberPlus.SDK.Game.Logic.SceneType p_Scene) var l_Hash = l_CurrentMap.level.levelID.Substring("custom_level_".Length).ToLower(); if (l_Hash != "") { - SongEntry l_CachedEntry = null; + var l_CachedEntry = null as Data.SongEntry; lock (SongHistory) - l_CachedEntry = SongHistory.Where(x => x.BeatMap != null && x.BeatMap.SelectMapVersion().hash.ToLower() == l_Hash).FirstOrDefault(); + l_CachedEntry = SongHistory.Where(x => x.BeatSaver_Map != null && x.BeatSaver_Map.SelectMapVersion().hash.ToLower() == l_Hash).FirstOrDefault(); if (l_CachedEntry == null) { @@ -320,7 +259,7 @@ private void OnSceneChange(BeatSaberPlus.SDK.Game.Logic.SceneType p_Scene) } else { - m_LastPlayingLevelResponse += " https://beatsaver.com/maps/" + l_CachedEntry.BeatMap.id; + m_LastPlayingLevelResponse += " https://beatsaver.com/maps/" + l_CachedEntry.BeatSaver_Map.id; } } } @@ -344,31 +283,47 @@ private void OnSceneChange(BeatSaberPlus.SDK.Game.Logic.SceneType p_Scene) /// private IEnumerator CreateButtonCoroutine() { - LevelSelectionNavigationController p_LevelSelectionNavigationController = null; - + var l_LevelSelectionNavigationController = null as LevelSelectionNavigationController; + var l_Waiter = new WaitForSeconds(0.25f); while (true) { - p_LevelSelectionNavigationController = Resources.FindObjectsOfTypeAll().LastOrDefault(); + if (!l_LevelSelectionNavigationController) + l_LevelSelectionNavigationController = Resources.FindObjectsOfTypeAll().LastOrDefault(); - if (p_LevelSelectionNavigationController != null && p_LevelSelectionNavigationController.gameObject.transform.childCount >= 2) + if (l_LevelSelectionNavigationController != null && l_LevelSelectionNavigationController.gameObject.transform.childCount >= 2) break; - yield return new WaitForSeconds(0.25f); + yield return l_Waiter; } - m_ManagerButtonP = BeatSaberPlus.SDK.UI.Button.CreatePrimary(p_LevelSelectionNavigationController.transform, "Chat\nRequest", () => UI.ManagerViewFlowCoordinator.Instance().Present(), null); + m_ManagerButtonP = BeatSaberPlus.SDK.UI.Button.CreatePrimary(l_LevelSelectionNavigationController.transform, "Chat\nRequest", () => UI.ManagerViewFlowCoordinator.Instance().Present(), null); m_ManagerButtonP.transform.localPosition = new Vector3(72.50f, 41.50f - 3, 2.6f); m_ManagerButtonP.transform.localScale = new Vector3(0.8f, 0.6f, 0.8f); m_ManagerButtonP.transform.SetAsFirstSibling(); m_ManagerButtonP.gameObject.SetActive(false); + m_ManagerButtonP.GetComponentInChildren().fontStyle = FontStyles.Normal; - m_ManagerButtonS = BeatSaberPlus.SDK.UI.Button.Create(p_LevelSelectionNavigationController.transform, "Chat\nRequest", () => UI.ManagerViewFlowCoordinator.Instance().Present(), null); + m_ManagerButtonS = BeatSaberPlus.SDK.UI.Button.Create(l_LevelSelectionNavigationController.transform, "Chat\nRequest", () => UI.ManagerViewFlowCoordinator.Instance().Present(), null); m_ManagerButtonS.transform.localPosition = new Vector3(72.50f, 38.50f - 3, 2.6f); m_ManagerButtonS.transform.localScale = new Vector3(0.8f, 0.6f, 0.8f); m_ManagerButtonS.transform.SetAsFirstSibling(); m_ManagerButtonS.gameObject.SetActive(true); + m_ManagerButtonP.GetComponentInChildren().fontStyle = FontStyles.Normal; m_ManagerButtonS.GetComponentInChildren().margin = new Vector4(0, 4, 0, 0); + var l_Images = m_ManagerButtonP.GetComponentsInChildren(); + foreach (var l_Image in l_Images) + { + l_Image._skew = 0f; + l_Image.SetAllDirty(); + } + l_Images = m_ManagerButtonS.GetComponentsInChildren(); + foreach (var l_Image in l_Images) + { + l_Image._skew = 0f; + l_Image.SetAllDirty(); + } + UpdateButton(); m_CreateButtonCoroutine = null; diff --git a/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Commands.cs b/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Commands.cs index 1c30d0d..ee89343 100644 --- a/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Commands.cs +++ b/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Commands.cs @@ -177,10 +177,10 @@ private void Command_BSR(IChatService p_Service, IChatMessage p_Message, string[ { lock (SongBlackList) { - var l_BeatMap = SongBlackList.Where(x => x.BeatMap.id.ToLower() == l_Key).FirstOrDefault(); + var l_BeatMap = SongBlackList.Where(x => x.BeatSaver_Map.id.ToLower() == l_Key).FirstOrDefault(); if (l_BeatMap != null) { - SendChatMessage(CRConfig.Instance.Commands.BSRCommand_Blacklisted, p_Service, p_Message, l_BeatMap.BeatMap); + SendChatMessage(CRConfig.Instance.Commands.BSRCommand_Blacklisted, p_Service, p_Message, l_BeatMap.BeatSaver_Map); return; } } @@ -189,10 +189,10 @@ private void Command_BSR(IChatService p_Service, IChatMessage p_Message, string[ /// Check if already in queue lock (SongQueue) { - var l_BeatMap = SongQueue.Where(x => x.BeatMap.id.ToLower() == l_Key).FirstOrDefault(); + var l_BeatMap = SongQueue.Where(x => x.BeatSaver_Map.id.ToLower() == l_Key).FirstOrDefault(); if (l_BeatMap != null) { - SendChatMessage(CRConfig.Instance.Commands.BSRCommand_AlreadyQueued, p_Service, p_Message, l_BeatMap.BeatMap); + SendChatMessage(CRConfig.Instance.Commands.BSRCommand_AlreadyQueued, p_Service, p_Message, l_BeatMap.BeatSaver_Map); return; } @@ -273,12 +273,12 @@ private void Command_BSR(IChatService p_Service, IChatMessage p_Message, string[ l_NamePrefix = string.Empty; } - var l_Entry = new SongEntry() + var l_Entry = new Data.SongEntry() { - BeatMap = p_BeatMap, + BeatSaver_Map = p_BeatMap, RequesterName = l_RequesterName, RequestTime = DateTime.Now, - NamePrefix = l_NamePrefix + TitlePrefix = l_NamePrefix }; lock (SongQueue) @@ -317,7 +317,7 @@ private void Command_BSR(IChatService p_Service, IChatMessage p_Message, string[ private void Command_BSRHelp(IChatService p_Service, IChatMessage p_Message, string[] p_Params) { if (p_Params?.Length > 0) - SendChatMessage(CRConfig.Instance.Commands.BSRHelpCommand_Reply.Replace("$UserName", p_Params[0]), p_Service, p_Message); + SendChatMessage(CRConfig.Instance.Commands.BSRHelpCommand_Reply.Replace("$UserName", p_Params[0].Replace("@", "")), p_Service, p_Message); else SendChatMessage(CRConfig.Instance.Commands.BSRHelpCommand_Reply, p_Service, p_Message); } @@ -366,7 +366,7 @@ private void Command_Queue(IChatService p_Service, IChatMessage p_Message, strin if (l_I != 0) l_Reply += ", "; - l_Reply += " (bsr " + SongQueue[l_I].BeatMap.id.ToLower() + ") " + (CRConfig.Instance.SafeMode2 ? string.Empty : SongQueue[l_I].BeatMap.name); + l_Reply += " (bsr " + SongQueue[l_I].BeatSaver_Map.id.ToLower() + ") " + (CRConfig.Instance.SafeMode2 ? string.Empty : SongQueue[l_I].BeatSaver_Map.name); } if (l_I < SongQueue.Count) @@ -397,7 +397,7 @@ private void Command_QueueStatus(IChatService p_Service, IChatMessage p_Message, } private void Command_Wrong(IChatService p_Service, IChatMessage p_Message, string[] p_Params) { - SongEntry l_SongEntry = null; + var l_SongEntry = null as Data.SongEntry; if (p_Params.Length == 0) { @@ -410,7 +410,7 @@ private void Command_Wrong(IChatService p_Service, IChatMessage p_Message, strin if (l_SongEntry != null) { - SendChatMessage("@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName is removed from queue!", p_Service, p_Message, l_SongEntry.BeatMap); + SendChatMessage("@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName is removed from queue!", p_Service, p_Message, l_SongEntry.BeatSaver_Map); /// Update request manager OnQueueChanged(); @@ -426,7 +426,7 @@ private void Command_Wrong(IChatService p_Service, IChatMessage p_Message, strin l_Key = l_Key.TrimStart('0'); lock (SongQueue) { - l_SongEntry = SongQueue.Where(x => x.RequesterName == p_Message.Sender.UserName && x.BeatMap.id == l_Key).LastOrDefault(); + l_SongEntry = SongQueue.Where(x => x.RequesterName == p_Message.Sender.UserName && x.BeatSaver_Map.id == l_Key).LastOrDefault(); if (l_SongEntry != null) SongQueue.Remove(l_SongEntry); } @@ -434,7 +434,7 @@ private void Command_Wrong(IChatService p_Service, IChatMessage p_Message, strin if (l_SongEntry != null) { - SendChatMessage("@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName is removed from queue!", p_Service, p_Message, l_SongEntry.BeatMap); + SendChatMessage("@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName is removed from queue!", p_Service, p_Message, l_SongEntry.BeatSaver_Map); /// Update request manager OnQueueChanged(); @@ -523,9 +523,9 @@ private void Command_SongMessage(IChatService p_Service, IChatMessage p_Message, var l_Key = p_Params[0].ToLower(); var l_Message = string.Join(" ", p_Params.Skip(1)); - SongEntry l_SongEntry = null; + var l_SongEntry = null as Data.SongEntry; lock (SongQueue) - l_SongEntry = SongQueue.Where(x => (l_Key.StartsWith("@") ? (l_Key.ToLower() == ("@" + x.RequesterName.ToLower())) : x.BeatMap.id.ToLower() == l_Key)).FirstOrDefault(); + l_SongEntry = SongQueue.Where(x => (l_Key.StartsWith("@") ? (l_Key.ToLower() == ("@" + x.RequesterName.ToLower())) : x.BeatSaver_Map.id.ToLower() == l_Key)).FirstOrDefault(); if (l_SongEntry != null) { @@ -550,13 +550,13 @@ private void Command_MoveToTop(IChatService p_Service, IChatMessage p_Message, s string l_Key = p_Params.Length > 0 ? p_Params[0].ToLower().Trim() : ""; lock (SongQueue) { - var l_SongEntry = SongQueue.Where(x => (l_Key.StartsWith("@") ? (l_Key.ToLower() == ("@" + x.RequesterName.ToLower())) : x.BeatMap.id.ToLower() == l_Key)).FirstOrDefault(); + var l_SongEntry = SongQueue.Where(x => (l_Key.StartsWith("@") ? (l_Key.ToLower() == ("@" + x.RequesterName.ToLower())) : x.BeatSaver_Map.id.ToLower() == l_Key)).FirstOrDefault(); if (l_SongEntry != null) { SongQueue.Remove(l_SongEntry); SongQueue.Insert(0, l_SongEntry); - SendChatMessage($"@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName requested by @{l_SongEntry.RequesterName} is now on top of queue!", p_Service, p_Message, l_SongEntry.BeatMap); + SendChatMessage($"@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName requested by @{l_SongEntry.RequesterName} is now on top of queue!", p_Service, p_Message, l_SongEntry.BeatSaver_Map); /// Update request manager OnQueueChanged(); @@ -582,10 +582,10 @@ private void Command_Remove(IChatService p_Service, IChatMessage p_Message, stri return; } - SongEntry l_SongEntry = null; + var l_SongEntry = null as Data.SongEntry; lock (SongQueue) { - l_SongEntry = SongQueue.Where(x => (l_Key.StartsWith("@") ? (l_Key.ToLower() == ("@" + x.RequesterName.ToLower())) : x.BeatMap.id.ToLower() == l_Key)).FirstOrDefault(); + l_SongEntry = SongQueue.Where(x => (l_Key.StartsWith("@") ? (l_Key.ToLower() == ("@" + x.RequesterName.ToLower())) : x.BeatSaver_Map.id.ToLower() == l_Key)).FirstOrDefault(); if (l_SongEntry != null) SongQueue.Remove(l_SongEntry); @@ -593,7 +593,7 @@ private void Command_Remove(IChatService p_Service, IChatMessage p_Message, stri if (l_SongEntry != null) { - SendChatMessage($"@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName request by @{l_SongEntry.RequesterName} is removed from queue!", p_Service, p_Message, l_SongEntry.BeatMap); + SendChatMessage($"@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName request by @{l_SongEntry.RequesterName} is removed from queue!", p_Service, p_Message, l_SongEntry.BeatSaver_Map); /// Update request manager OnQueueChanged(); @@ -789,10 +789,10 @@ private void Command_Block(IChatService p_Service, IChatMessage p_Message, strin lock (SongBlackList) { - var l_SongEntry = SongBlackList.Where(x => x.BeatMap.id.ToLower() == l_Key).FirstOrDefault(); + var l_SongEntry = SongBlackList.Where(x => x.BeatSaver_Map.id.ToLower() == l_Key).FirstOrDefault(); if (l_SongEntry != null) { - SendChatMessage("@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName is already blacklisted!", p_Service, p_Message, l_SongEntry.BeatMap); + SendChatMessage("@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName is already blacklisted!", p_Service, p_Message, l_SongEntry.BeatSaver_Map); return; } } @@ -808,12 +808,12 @@ private void Command_Block(IChatService p_Service, IChatMessage p_Message, strin l_Reply = "@$UserName (bsr $BSRKey) $SongName / $LevelAuthorName is now blacklisted!"; lock (SongQueue) { lock (SongHistory) { lock (SongBlackList) { - SongQueue.RemoveAll(x => x.BeatMap.id == p_BeatMap.id); - SongHistory.RemoveAll(x => x.BeatMap.id == p_BeatMap.id); + SongQueue.RemoveAll(x => x.BeatSaver_Map.id == p_BeatMap.id); + SongHistory.RemoveAll(x => x.BeatSaver_Map.id == p_BeatMap.id); /// Add to blacklist - SongBlackList.RemoveAll(x => x.BeatMap.id == p_BeatMap.id); - SongBlackList.Insert(0, new SongEntry() { BeatMap = p_BeatMap, NamePrefix = "🗡", RequesterName = p_Message.Sender.DisplayName }); + SongBlackList.RemoveAll(x => x.BeatSaver_Map.id == p_BeatMap.id); + SongBlackList.Insert(0, new Data.SongEntry() { BeatSaver_Map = p_BeatMap, TitlePrefix = "🗡", RequesterName = p_Message.Sender.DisplayName }); } } } /// Update request manager diff --git a/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Database.cs b/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Database.cs index 6c0a242..cf1113b 100644 --- a/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Database.cs +++ b/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Database.cs @@ -12,12 +12,10 @@ public partial class ChatRequest /// DB File path /// private string m_DBFilePath = System.IO.Directory.GetCurrentDirectory() + "\\UserData\\BeatSaberPlus\\ChatRequest\\Database.json"; - private string m_DBFilePathOld = System.IO.Directory.GetCurrentDirectory() + "\\UserData\\BeatSaberPlus_ChatRequestDB.json"; /// /// Simple queue File path /// private string m_SimpleQueueFilePath = System.IO.Directory.GetCurrentDirectory() + "\\UserData\\BeatSaberPlus\\ChatRequest\\SimpleQueue.txt"; - private string m_SimpleQueueFilePathOld = System.IO.Directory.GetCurrentDirectory() + "\\UserData\\BeatSaberPlus_ChatRequest_SimpleQueue.txt"; /// /// Simple queue status File path /// @@ -45,96 +43,45 @@ private void LoadDatabase() { foreach (JObject l_Current in (JArray)l_JSON["queue"]) { - var l_Key = l_Current["key"]?.Value() ?? ""; - var l_Time = l_Current["rqt"]?.Value() ?? CP_SDK.Misc.Time.UnixTimeNow(); - var l_Requester = l_Current["rqn"]?.Value() ?? ""; - var l_Prefix = l_Current["npr"]?.Value() ?? ""; - var l_Message = l_Current["msg"]?.Value() ?? ""; - - if (l_Key == "" && l_Current.ContainsKey("id")) - l_Key = l_Current["id"].Value().ToString("x"); - - if (l_Key == "") + var l_Entry = Data.SongEntry.Deserialize(l_Current); + if (l_Entry == null) continue; - SongEntry l_Entry = new SongEntry() - { - BeatMap = BeatSaberPlus.SDK.Game.BeatMapsClient.GetFromCacheByKey(l_Key) ?? BeatSaberPlus.SDK.Game.BeatMaps.MapDetail.PartialFromKey(l_Key), - RequestTime = CP_SDK.Misc.Time.FromUnixTime(l_Time), - RequesterName = l_Requester, - NamePrefix = l_Prefix, - Message = l_Message - }; - SongQueue.Add(l_Entry); /// Start populate - if (l_Entry.BeatMap.Partial) - l_Entry.BeatMap.Populate((x) => OnBeatmapPopulated(x, l_Entry)); + if (l_Entry.BeatSaver_Map.Partial) + l_Entry.BeatSaver_Map.Populate((x) => OnBeatmapPopulated(x, l_Entry)); } } if (l_JSON["history"] != null && l_JSON["history"].Type == JTokenType.Array) { foreach (JObject l_Current in (JArray)l_JSON["history"]) { - var l_Key = l_Current["key"]?.Value() ?? ""; - var l_Time = l_Current["rqt"]?.Value() ?? CP_SDK.Misc.Time.UnixTimeNow(); - var l_Requester = l_Current["rqn"]?.Value() ?? ""; - var l_Prefix = l_Current["npr"]?.Value() ?? ""; - var l_Message = l_Current["msg"]?.Value() ?? ""; - - if (l_Key == "" && l_Current.ContainsKey("id")) - l_Key = l_Current["id"].Value().ToString("x"); - - if (l_Key == "") + var l_Entry = Data.SongEntry.Deserialize(l_Current); + if (l_Entry == null) continue; - SongEntry l_Entry = new SongEntry() - { - BeatMap = BeatSaberPlus.SDK.Game.BeatMapsClient.GetFromCacheByKey(l_Key) ?? BeatSaberPlus.SDK.Game.BeatMaps.MapDetail.PartialFromKey(l_Key), - RequestTime = CP_SDK.Misc.Time.FromUnixTime(l_Time), - RequesterName = l_Requester, - NamePrefix = l_Prefix, - Message = l_Message - }; - SongHistory.Add(l_Entry); /// Start populate - if (l_Entry.BeatMap.Partial) - l_Entry.BeatMap.Populate((x) => OnBeatmapPopulated(x, l_Entry)); + if (l_Entry.BeatSaver_Map.Partial) + l_Entry.BeatSaver_Map.Populate((x) => OnBeatmapPopulated(x, l_Entry)); } } if (l_JSON["blacklist"] != null && l_JSON["blacklist"].Type == JTokenType.Array) { foreach (JObject l_Current in (JArray)l_JSON["blacklist"]) { - var l_Key = l_Current["key"]?.Value() ?? ""; - var l_Time = l_Current["rqt"]?.Value() ?? CP_SDK.Misc.Time.UnixTimeNow(); - var l_Requester = l_Current["rqn"]?.Value() ?? ""; - var l_Prefix = l_Current["npr"]?.Value() ?? ""; - var l_Message = l_Current["msg"]?.Value() ?? ""; - - if (l_Key == "" && l_Current.ContainsKey("id")) - l_Key = l_Current["id"].Value().ToString("x"); - - if (l_Key == "") + var l_Entry = Data.SongEntry.Deserialize(l_Current); + if (l_Entry == null) continue; - SongEntry l_Entry = new SongEntry() - { - BeatMap = BeatSaberPlus.SDK.Game.BeatMapsClient.GetFromCacheByKey(l_Key) ?? BeatSaberPlus.SDK.Game.BeatMaps.MapDetail.PartialFromKey(l_Key), - RequestTime = CP_SDK.Misc.Time.FromUnixTime(l_Time), - RequesterName = l_Requester, - NamePrefix = l_Prefix, - Message = l_Message - }; - SongBlackList.Add(l_Entry); /// Start populate - if (l_Entry.BeatMap.Partial) - l_Entry.BeatMap.Populate((x) => OnBeatmapPopulated(x, l_Entry)); + if (l_Entry.BeatSaver_Map.Partial) + l_Entry.BeatSaver_Map.Populate((x) => OnBeatmapPopulated(x, l_Entry)); } } if (l_JSON["bannedusers"] != null && l_JSON["bannedusers"].Type == JTokenType.Array) @@ -183,41 +130,13 @@ private void SaveDatabase() try { - var l_Requests = new JArray(); - foreach (var l_Current in SongQueue) - { - var l_Object = new JObject(); - l_Object["key"] = l_Current.BeatMap.id; - l_Object["rqt"] = l_Current.RequestTime.HasValue ? CP_SDK.Misc.Time.ToUnixTime(l_Current.RequestTime.Value) : CP_SDK.Misc.Time.UnixTimeNow(); - l_Object["rqn"] = l_Current.RequesterName; - l_Object["npr"] = l_Current.NamePrefix; - l_Object["msg"] = l_Current.Message; - l_Requests.Add(l_Object); - } - - var l_History = new JArray(); - foreach (var l_Current in SongHistory) - { - var l_Object = new JObject(); - l_Object["key"] = l_Current.BeatMap.id; - l_Object["rqt"] = l_Current.RequestTime.HasValue ? CP_SDK.Misc.Time.ToUnixTime(l_Current.RequestTime.Value) : CP_SDK.Misc.Time.UnixTimeNow(); - l_Object["rqn"] = l_Current.RequesterName; - l_Object["npr"] = l_Current.NamePrefix; - l_Object["msg"] = l_Current.Message; - l_History.Add(l_Object); - } - + var l_Requests = new JArray(); + var l_History = new JArray(); var l_BlackList = new JArray(); - foreach (var l_Current in SongBlackList) - { - var l_Object = new JObject(); - l_Object["key"] = l_Current.BeatMap.id; - l_Object["rqt"] = l_Current.RequestTime.HasValue ? CP_SDK.Misc.Time.ToUnixTime(l_Current.RequestTime.Value) : CP_SDK.Misc.Time.UnixTimeNow(); - l_Object["rqn"] = l_Current.RequesterName; - l_Object["npr"] = l_Current.NamePrefix; - l_Object["msg"] = l_Current.Message; - l_BlackList.Add(l_Object); - } + + for (var l_I = 0; l_I < SongQueue.Count; ++l_I) l_Requests.Add(Data.SongEntry.Serialize(SongQueue[l_I])); + for (var l_I = 0; l_I < SongHistory.Count; ++l_I) l_History.Add(Data.SongEntry.Serialize(SongHistory[l_I])); + for (var l_I = 0; l_I < SongBlackList.Count; ++l_I) l_BlackList.Add(Data.SongEntry.Serialize(SongBlackList[l_I])); var l_Remaps = new JArray(); foreach (var l_KVP in Remaps) @@ -229,17 +148,19 @@ private void SaveDatabase() }); } - var l_JSON = new JObject(); - l_JSON.Add("queue", l_Requests); - l_JSON.Add("history", l_History); - l_JSON.Add("blacklist", l_BlackList); - l_JSON.Add("bannedusers", new JArray(BannedUsers.ToArray())); - l_JSON.Add("bannedmappers", new JArray(BannedMappers.ToArray())); - l_JSON.Add("remaps", l_Remaps); - l_JSON.Add("allowlist", new JArray(AllowList.ToArray())); + var l_JSON = new JObject + { + { "queue", l_Requests }, + { "history", l_History }, + { "blacklist", l_BlackList }, + { "bannedusers", new JArray(BannedUsers.ToArray()) }, + { "bannedmappers", new JArray(BannedMappers.ToArray()) }, + { "remaps", l_Remaps }, + { "allowlist", new JArray(AllowList.ToArray()) } + }; string l_ResultJSON = l_JSON.ToString(); - System.IO.File.WriteAllText(m_DBFilePath, l_ResultJSON, UTF8Encoding.UTF8); + System.IO.File.WriteAllText(m_DBFilePath, l_ResultJSON, Encoding.UTF8); } catch (System.Exception p_Exception) { @@ -267,14 +188,14 @@ private void UpdateSimpleQueueFile() int l_Added = 0; for (int l_I = 0; l_I < SongQueue.Count && l_Added < CRConfig.Instance.OverlayIntegration.SimpleQueueFileCount; ++l_I) { - if (SongQueue[l_I].BeatMap == null || SongQueue[l_I].BeatMap.Partial) + if (SongQueue[l_I].BeatSaver_Map == null || SongQueue[l_I].BeatSaver_Map.Partial) continue; string l_Line = l_Format.Replace("%i", (l_I + 1).ToString()) - .Replace("%n", SongQueue[l_I].BeatMap.name) - .Replace("%m", SongQueue[l_I].BeatMap.metadata.levelAuthorName) + .Replace("%n", SongQueue[l_I].BeatSaver_Map.name) + .Replace("%m", SongQueue[l_I].BeatSaver_Map.metadata.levelAuthorName) .Replace("%r", SongQueue[l_I].RequesterName) - .Replace("%k", SongQueue[l_I].BeatMap.id); + .Replace("%k", SongQueue[l_I].BeatSaver_Map.id); if (l_I > 0) l_Content += "\n"; diff --git a/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Logic.cs b/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Logic.cs index 781c0f4..91a38a9 100644 --- a/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Logic.cs +++ b/Modules/BeatSaberPlus_ChatRequest/ChatRequest_Logic.cs @@ -11,57 +11,8 @@ namespace BeatSaberPlus_ChatRequest /// public partial class ChatRequest { - /// - /// Song entry - /// - internal class SongEntry - { - internal BeatSaberPlus.SDK.Game.BeatMaps.MapDetail BeatMap = null; - internal DateTime? RequestTime = null; - internal string RequesterName = ""; - internal string NamePrefix = ""; - internal string Message = ""; - } - - /// - /// Queue - /// - internal List SongQueue = new List(); - /// - /// History - /// - internal List SongHistory = new List(); - /// - /// Blacklist - /// - internal List SongBlackList = new List(); - /// - /// Banned user list - /// - internal List BannedUsers = new List(); - /// - /// Banned mapper list - /// - internal List BannedMappers = new List(); - /// - /// BSR codes remap lookup dictionary - /// - internal Dictionary Remaps = new Dictionary(); - /// - /// Map allow list - /// - internal List AllowList = new List(); - /// - /// Is the queue open - /// public bool QueueOpen { get; private set; } = false; - /// - /// Total queue duration - /// public int QueueDuration { get; private set; } = 0; - /// - /// Song queue count - /// public int SongQueueCount { get { lock (SongQueue) @@ -72,22 +23,23 @@ public int SongQueueCount { get //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Requested this session songs - /// - private ConcurrentBag m_RequestedThisSession = new ConcurrentBag(); - /// - /// Last queue command time - /// - private DateTime m_LastQueueCommandTime = DateTime.Now; - /// - /// Link command cache - /// - private IBeatmapLevel m_LastPlayingLevel = null; - /// - /// Link command cache - /// - private string m_LastPlayingLevelResponse = ""; + internal List SongQueue = new List(); + internal List SongHistory = new List(); + internal List SongBlackList = new List(); + + internal List AllowList = new List(); + internal List BannedUsers = new List(); + internal List BannedMappers = new List(); + + internal Dictionary Remaps = new Dictionary(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private ConcurrentBag m_RequestedThisSession = new ConcurrentBag(); + private DateTime m_LastQueueCommandTime = DateTime.Now; + private IBeatmapLevel m_LastPlayingLevel = null; + private string m_LastPlayingLevelResponse = ""; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -112,8 +64,8 @@ private void OnQueueChanged(bool p_UpdateSimpleQueueFile = true, bool p_UpdateSo { SongQueue.ForEach(x => { - if (!x.BeatMap.Partial) - QueueDuration += (int)x.BeatMap.metadata.duration; + if (!x.BeatSaver_Map.Partial) + QueueDuration += (int)x.BeatSaver_Map.metadata.duration; }); } } @@ -124,14 +76,14 @@ private void OnQueueChanged(bool p_UpdateSimpleQueueFile = true, bool p_UpdateSo } /// Avoid saving during play - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) { CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => { UpdateButton(); - if (p_UpdateSongList && UI.ManagerMain.CanBeUpdated) - UI.ManagerMain.Instance.RebuildSongList(); + if (p_UpdateSongList && UI.ManagerMainView.CanBeUpdated) + UI.ManagerMainView.Instance.RebuildSongList(); SaveDatabase(); }); @@ -141,7 +93,7 @@ private void OnQueueChanged(bool p_UpdateSimpleQueueFile = true, bool p_UpdateSo /// When a beatmap get fully loaded /// /// Task instance - private void OnBeatmapPopulated(bool p_Valid, SongEntry p_Entry) + private void OnBeatmapPopulated(bool p_Valid, Data.SongEntry p_Entry) { if (!p_Valid) { @@ -151,14 +103,14 @@ private void OnBeatmapPopulated(bool p_Valid, SongEntry p_Entry) SongBlackList.RemoveAll(z => z == p_Entry); } } } - BeatSaberPlus.SDK.Game.BeatMapsClient.ClearCache(p_Entry.BeatMap.id); + BeatSaberPlus.SDK.Game.BeatMapsClient.ClearCache(p_Entry.BeatSaver_Map.id); CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => { UpdateButton(); - if (UI.ManagerMain.CanBeUpdated) - UI.ManagerMain.Instance.RebuildSongList(); + if (UI.ManagerMainView.CanBeUpdated) + UI.ManagerMainView.Instance.RebuildSongList(); SaveDatabase(); }); @@ -167,7 +119,7 @@ private void OnBeatmapPopulated(bool p_Valid, SongEntry p_Entry) if (!p_Valid) return; - BeatSaberPlus.SDK.Game.BeatMapsClient.Cache(p_Entry.BeatMap); + BeatSaberPlus.SDK.Game.BeatMapsClient.Cache(p_Entry.BeatSaver_Map); /// Update request manager OnQueueChanged(); @@ -244,15 +196,15 @@ internal void ToggleQueueStatus() UpdateSimpleQueueFile(); - if (UI.ManagerLeft.CanBeUpdated) - UI.ManagerLeft.Instance.UpdateQueueStatus(); + if (UI.ManagerLeftView.CanBeUpdated) + UI.ManagerLeftView.Instance.UpdateQueueStatus(); }); } /// /// Re-Enqueue a song by play or skip /// /// Song to dequeue - internal void ReEnqueueSong(SongEntry p_Entry) + internal void ReEnqueueSong(Data.SongEntry p_Entry) { if (p_Entry == null) return; @@ -266,7 +218,7 @@ internal void ReEnqueueSong(SongEntry p_Entry) SongHistory.Remove(p_Entry); /// Move at top of queue - SongQueue.RemoveAll(x => x.BeatMap.id == p_Entry.BeatMap.id); + SongQueue.RemoveAll(x => x.BeatSaver_Map.id == p_Entry.BeatSaver_Map.id); SongQueue.Insert(0, p_Entry); } } @@ -278,7 +230,7 @@ internal void ReEnqueueSong(SongEntry p_Entry) /// Dequeue a song by play or skip /// /// Song to dequeue - internal void DequeueSong(SongEntry p_Entry, bool p_ChatNotify) + internal void DequeueSong(Data.SongEntry p_Entry, bool p_ChatNotify) { if (p_Entry == null) return; @@ -290,7 +242,7 @@ internal void DequeueSong(SongEntry p_Entry, bool p_ChatNotify) SongQueue.Remove(p_Entry); /// Move at top of history - SongHistory.RemoveAll(x => x.BeatMap.id == p_Entry.BeatMap.id); + SongHistory.RemoveAll(x => x.BeatSaver_Map.id == p_Entry.BeatSaver_Map.id); SongHistory.Insert(0, p_Entry); /// Reduce history size @@ -299,8 +251,8 @@ internal void DequeueSong(SongEntry p_Entry, bool p_ChatNotify) var l_ToRemove = SongHistory.ElementAt(SongHistory.Count - 1); /// Clear cache - if (SongBlackList.Count(x => x.BeatMap.id == l_ToRemove.BeatMap.id) == 0) - BeatSaberPlus.SDK.Game.BeatMapsClient.ClearCache(l_ToRemove.BeatMap.id); + if (SongBlackList.Count(x => x.BeatSaver_Map.id == l_ToRemove.BeatSaver_Map.id) == 0) + BeatSaberPlus.SDK.Game.BeatMapsClient.ClearCache(l_ToRemove.BeatSaver_Map.id); SongHistory.RemoveAt(SongHistory.Count - 1); } @@ -310,7 +262,7 @@ internal void DequeueSong(SongEntry p_Entry, bool p_ChatNotify) OnQueueChanged(); if (p_ChatNotify) - SendChatMessage($"$SongName / $LevelAuthorName $Vote% (bsr $BSRKey) requested by @{p_Entry.RequesterName} is next!", null, null, p_Entry.BeatMap); + SendChatMessage($"$SongName / $LevelAuthorName $Vote% (bsr $BSRKey) requested by @{p_Entry.RequesterName} is next!", null, null, p_Entry.BeatSaver_Map); } /// /// Clear queue @@ -329,7 +281,7 @@ internal void ClearQueue() /// Blacklist song /// /// Song to blacklist - internal void BlacklistSong(SongEntry p_Entry) + internal void BlacklistSong(Data.SongEntry p_Entry) { if (p_Entry == null) return; @@ -341,10 +293,10 @@ internal void BlacklistSong(SongEntry p_Entry) SongQueue.Remove(p_Entry); /// Remove from history - SongHistory.RemoveAll(x => x.BeatMap.id == p_Entry.BeatMap.id); + SongHistory.RemoveAll(x => x.BeatSaver_Map.id == p_Entry.BeatSaver_Map.id); /// Add to blacklist - SongBlackList.RemoveAll(x => x.BeatMap.id == p_Entry.BeatMap.id); + SongBlackList.RemoveAll(x => x.BeatSaver_Map.id == p_Entry.BeatSaver_Map.id); SongBlackList.Insert(0, p_Entry); } } } @@ -355,7 +307,7 @@ internal void BlacklistSong(SongEntry p_Entry) /// UnBlacklist song /// /// Song to blacklist - internal void UnBlacklistSong(SongEntry p_Entry) + internal void UnBlacklistSong(Data.SongEntry p_Entry) { if (p_Entry == null) return; @@ -367,7 +319,7 @@ internal void UnBlacklistSong(SongEntry p_Entry) SongBlackList.Remove(p_Entry); /// Move at top of history - SongHistory.RemoveAll(x => x.BeatMap.id == p_Entry.BeatMap.id); + SongHistory.RemoveAll(x => x.BeatSaver_Map.id == p_Entry.BeatSaver_Map.id); SongHistory.Insert(0, p_Entry); /// Reduce history size @@ -389,7 +341,7 @@ internal void ResetBlacklist() SongBlackList.ForEach(l_BlacklistedSong => { /// Move at top of history - SongHistory.RemoveAll(x => x.BeatMap.id == l_BlacklistedSong.BeatMap.id); + SongHistory.RemoveAll(x => x.BeatSaver_Map.id == l_BlacklistedSong.BeatSaver_Map.id); SongHistory.Insert(0, l_BlacklistedSong); }); @@ -492,15 +444,13 @@ private bool FilterBeatMap(BeatSaberPlus.SDK.Game.BeatMaps.MapDetail p_BeatMap, DateTime l_MinUploadDate = new DateTime(2018, 1, 1).AddMonths(CRConfig.Instance.Filters.DateMinV); if (CRConfig.Instance.Filters.DateMin && l_Date < l_MinUploadDate) { - string[] s_Months = new string[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; - p_Reply = $"@{p_SenderName} this song is too old ({s_Months[l_MinUploadDate.Month - 1]} {l_MinUploadDate.Year} minimum)!"; + p_Reply = $"@{p_SenderName} this song is too old ({CP_SDK.Misc.Time.MonthNames[l_MinUploadDate.Month - 1]} {l_MinUploadDate.Year} minimum)!"; return false; } DateTime l_MaxUploadDate = new DateTime(2018, 1, 1).AddMonths(CRConfig.Instance.Filters.DateMaxV + 1); if (CRConfig.Instance.Filters.DateMax && l_Date > l_MaxUploadDate) { - string[] s_Months = new string[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; - p_Reply = $"@{p_SenderName} this song is too recent ({s_Months[l_MinUploadDate.Month - 1]} {l_MinUploadDate.Year} maximum)!"; + p_Reply = $"@{p_SenderName} this song is too recent ({CP_SDK.Misc.Time.MonthNames[l_MinUploadDate.Month - 1]} {l_MinUploadDate.Year} maximum)!"; return false; } diff --git a/Modules/BeatSaberPlus_ChatRequest/Data/SongEntry.cs b/Modules/BeatSaberPlus_ChatRequest/Data/SongEntry.cs new file mode 100644 index 0000000..7b1cfe7 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatRequest/Data/SongEntry.cs @@ -0,0 +1,90 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace BeatSaberPlus_ChatRequest.Data +{ + /// + /// Song entry + /// + public class SongEntry : BeatSaberPlus.SDK.UI.Data.SongListItem + { + internal DateTime? RequestTime = null; + internal string RequesterName = ""; + internal string Message = ""; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Serialize into a JObject + /// + /// Source + /// + internal static JObject Serialize(SongEntry p_SongEntry) + { + return new JObject + { + ["key"] = p_SongEntry.BeatSaver_Map.id, + ["rqt"] = p_SongEntry.RequestTime.HasValue ? CP_SDK.Misc.Time.ToUnixTime(p_SongEntry.RequestTime.Value) : CP_SDK.Misc.Time.UnixTimeNow(), + ["rqn"] = p_SongEntry.RequesterName, + ["npr"] = p_SongEntry.TitlePrefix, + ["msg"] = p_SongEntry.Message + }; + } + /// + /// Deserialize from JObject + /// + /// Source + /// + internal static SongEntry Deserialize(JObject p_JObject) + { + var l_Key = p_JObject["key"]?.Value() ?? ""; + var l_Time = p_JObject["rqt"]?.Value() ?? CP_SDK.Misc.Time.UnixTimeNow(); + var l_Requester = p_JObject["rqn"]?.Value() ?? ""; + var l_Prefix = p_JObject["npr"]?.Value() ?? ""; + var l_Message = p_JObject["msg"]?.Value() ?? ""; + + if (l_Key == "" && p_JObject.ContainsKey("id")) + l_Key = p_JObject["id"].Value().ToString("x"); + + if (l_Key == "") + return null; + + return new SongEntry() + { + BeatSaver_Map = BeatSaberPlus.SDK.Game.BeatMapsClient.GetFromCacheByKey(l_Key) ?? BeatSaberPlus.SDK.Game.BeatMaps.MapDetail.PartialFromKey(l_Key), + RequestTime = CP_SDK.Misc.Time.FromUnixTime(l_Time), + RequesterName = l_Requester, + TitlePrefix = l_Prefix, + Message = l_Message + }; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On show + /// + public override void OnShow() + { + Tooltip = "Requested by " + TitlePrefix + (TitlePrefix.Length != 0 ? " " : "") + RequesterName; + + if (RequestTime.HasValue) + { + var l_Elapsed = CP_SDK.Misc.Time.UnixTimeNow() - CP_SDK.Misc.Time.ToUnixTime(RequestTime.Value); + if (l_Elapsed < (60 * 60)) + Tooltip += "\n" + Math.Max(1, l_Elapsed / 60).ToString() + " minute(s) ago"; + else if (l_Elapsed < (60 * 60 * 24)) + Tooltip += "\n" + Math.Max(1, l_Elapsed / (60 * 60)).ToString() + " hour(s) ago"; + else + Tooltip += "\n" + Math.Max(1, l_Elapsed / (60 * 60 * 24)).ToString() + " day(s) ago"; + } + + if (!string.IsNullOrEmpty(Message)) + Tooltip += "\n" + Message; + + base.OnShow(); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatRequest/Properties/AssemblyInfo.cs b/Modules/BeatSaberPlus_ChatRequest/Properties/AssemblyInfo.cs index d33f216..cc5e05f 100644 --- a/Modules/BeatSaberPlus_ChatRequest/Properties/AssemblyInfo.cs +++ b/Modules/BeatSaberPlus_ChatRequest/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeft.bsml b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeft.bsml deleted file mode 100644 index ac33807..0000000 --- a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeft.bsml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeft.cs b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeftView.cs similarity index 58% rename from Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeft.cs rename to Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeftView.cs index 6220004..91424ed 100644 --- a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeft.cs +++ b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerLeftView.cs @@ -1,5 +1,4 @@ -using BeatSaberMarkupLanguage.Attributes; -using HMUI; +using CP_SDK.XUI; using UnityEngine.UI; namespace BeatSaberPlus_ChatRequest.UI @@ -7,15 +6,10 @@ namespace BeatSaberPlus_ChatRequest.UI /// /// Manager left window /// - internal class ManagerLeft : BeatSaberPlus.SDK.UI.ResourceViewController + internal sealed class ManagerLeftView : CP_SDK.UI.ViewController { -#pragma warning disable CS0649 - [UIComponent("SafeButton")] - private Button m_SafeButton = null; - - [UIComponent("QueueButton")] - private Button m_QueueButton = null; -#pragma warning restore CS0649 + private XUIPrimaryButton m_SafeModeButton; + private XUISecondaryButton m_QueueButton; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -25,8 +19,37 @@ internal class ManagerLeft : BeatSaberPlus.SDK.UI.ResourceViewController protected override sealed void OnViewCreation() { - UpdateSafeMode(); - UpdateQueueStatus(); + Templates.FullRectLayout( + Templates.TitleBar("Tools"), + + XUIVLayout.Make( + XUIPrimaryButton.Make("Select random", OnRandomButton), + + XUIVSpacer.Make(5f), + + XUIPrimaryButton.Make("ENABLE SAFE MODE", OnSafeModeButton).Bind(ref m_SafeModeButton), + XUIPrimaryButton.Make("Remove all request from queue", OnClearQueueButton), + XUIPrimaryButton.Make("Reset blacklist", OnResetBlacklistButton), + + XUIVSpacer.Make(5f), + + XUISecondaryButton.Make("Close queue", OnQueueButton).Bind(ref m_QueueButton) + ) + .SetWidth(60f) + .SetPadding(0) + .ForEachDirect(y => + { + y.SetHeight(8f); + y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained); + }) + .ForEachDirect(y => + { + y.SetHeight(8f); + y.OnReady((x) => x.CSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained); + }) + ) + .SetBackground(true, null, true) + .BuildUI(transform); } /// /// On view activation @@ -45,26 +68,20 @@ protected override sealed void OnViewActivation() /// internal void UpdateSafeMode() { - if (m_SafeButton == null) - return; - if (CRConfig.Instance.SafeMode2) - m_SafeButton.GetComponentInChildren().text = "DISABLE SAFE MODE"; + m_SafeModeButton?.SetText("DISABLE SAFE MODE"); else - m_SafeButton.GetComponentInChildren().text = "ENABLE SAFE MODE"; + m_SafeModeButton?.SetText("ENABLE SAFE MODE"); } /// /// Update queue status /// internal void UpdateQueueStatus() { - if (m_QueueButton == null) - return; - if (ChatRequest.Instance.QueueOpen) - m_QueueButton.GetComponentInChildren().text = "Close queue"; + m_QueueButton?.SetText("Close queue"); else - m_QueueButton.GetComponentInChildren().text = "Open queue"; + m_QueueButton?.SetText("Open queue"); } //////////////////////////////////////////////////////////////////////////// @@ -73,11 +90,8 @@ internal void UpdateQueueStatus() /// /// On random button /// - [UIAction("click-btn-random")] private void OnRandomButton() - { - ManagerMain.Instance.SelectRandom(); - } + => ManagerMainView.Instance.SelectRandom(); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -85,13 +99,15 @@ private void OnRandomButton() /// /// Safe mode button /// - [UIAction("click-btn-safe")] private void OnSafeModeButton() { if (CRConfig.Instance.SafeMode2) { - ShowConfirmationModal("Do you really want to disable safe mode?", () => + ShowConfirmationModal("Do you really want to disable safe mode?", (p_Confirm) => { + if (!p_Confirm) + return; + CRConfig.Instance.SafeMode2 = false; CRConfig.Instance.Save(); UpdateSafeMode(); @@ -99,8 +115,11 @@ private void OnSafeModeButton() } else { - ShowConfirmationModal("Do you really want to enable safe mode?\nThis will hide all song name & uploader in chat.", () => + ShowConfirmationModal("Do you really want to enable safe mode?\nThis will hide all song name & uploader in chat.", (p_Confirm) => { + if (!p_Confirm) + return; + CRConfig.Instance.SafeMode2 = true; CRConfig.Instance.Save(); UpdateSafeMode(); @@ -110,22 +129,26 @@ private void OnSafeModeButton() /// /// Clear queue button /// - [UIAction("click-clear-queue-btn-pressed")] private void OnClearQueueButton() { - ShowConfirmationModal("Do you really want to reset clear the song queue?", () => + ShowConfirmationModal("Do you really want to reset clear the song queue?", (p_Confirm) => { + if (!p_Confirm) + return; + ChatRequest.Instance.ClearQueue(); }); } /// /// Cleat queue button /// - [UIAction("click-reset-blacklist-btn-pressed")] private void OnResetBlacklistButton() { - ShowConfirmationModal("Do you really want to reset your blacklist?", () => + ShowConfirmationModal("Do you really want to reset your blacklist?", (p_Confirm) => { + if (!p_Confirm) + return; + ChatRequest.Instance.ResetBlacklist(); }); } @@ -136,10 +159,7 @@ private void OnResetBlacklistButton() /// /// On queue button /// - [UIAction("click-btn-queue")] private void OnQueueButton() - { - ChatRequest.Instance.ToggleQueueStatus(); - } + => ChatRequest.Instance.ToggleQueueStatus(); } } diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerMain.bsml b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerMain.bsml deleted file mode 100644 index a3612ba..0000000 --- a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerMain.bsml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerMain.cs b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerMain.cs deleted file mode 100644 index e0cd38d..0000000 --- a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerMain.cs +++ /dev/null @@ -1,560 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using HMUI; -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using UnityEngine; -using UnityEngine.UI; - -namespace BeatSaberPlus_ChatRequest.UI -{ - /// - /// Chat request main view controller - /// - internal class ManagerMain : BeatSaberPlus.SDK.UI.ResourceViewController, IProgress - { - /// - /// Amount of song to display per page - /// - private static int s_SONG_PER_PAGE = 7; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIObject("TypeSegmentPanel")] - private GameObject m_TypeSegmentPanel; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("SongUpButton")] - private Button m_SongUpButton; - [UIObject("SongList")] - private GameObject m_SongListView = null; - private BeatSaberPlus.SDK.UI.DataSource.SongList m_SongList = null; - [UIComponent("SongDownButton")] - private Button m_SongDownButton; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("SongInfoPanel")] - private GameObject m_SongInfoPanel; - private BeatSaberPlus.SDK.UI.LevelDetail m_SongInfo_Detail; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Type segment control - /// - private TextSegmentedControl m_TypeSegmentControl = null; - /// - /// Current song list page - /// - private int m_CurrentPage = 1; - /// - /// Selected song - /// - ChatRequest.SongEntry m_SelectedSong = null; - /// - /// Selected song index - /// - private int m_SelectedSongIndex = 0; - /// - /// Song list provider - /// - private List m_SongsProvider = null; - /// - /// Reload expected path - /// - private string m_SongReloadingExpectedPath = ""; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - /// Scale down up & down button - m_SongUpButton.transform.localScale = Vector3.one * 0.6f; - m_SongDownButton.transform.localScale = Vector3.one * 0.6f; - - /// Create type selector - m_TypeSegmentControl = BeatSaberPlus.SDK.UI.TextSegmentedControl.Create(m_TypeSegmentPanel.transform as RectTransform, false); - m_TypeSegmentControl.SetTexts(new string[] { "Requests", "History", "Blacklist" }); - m_TypeSegmentControl.ReloadData(); - m_TypeSegmentControl.didSelectCellEvent += OnQueueTypeChanged; - - /// Prepare song list - var l_BSMLTableView = m_SongListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_SongListView.GetComponentInChildren()); - m_SongList = l_BSMLTableView.gameObject.AddComponent(); - m_SongList.PlayPreviewAudio = CRConfig.Instance.PlayPreviewMusic; - m_SongList.PreviewAudioVolume = 1.0f; - m_SongList.TableViewInstance = l_BSMLTableView; - m_SongList.Init(); - l_BSMLTableView.SetDataSource(m_SongList, false); - - /// Bind events - m_SongUpButton.onClick.AddListener(OnSongPageUpPressed); - m_SongList.OnCoverFetched += OnSongCoverFetched; - m_SongList.TableViewInstance.didSelectCellWithIdxEvent += OnSongSelected; - m_SongDownButton.onClick.AddListener(OnSongPageDownPressed); - - /// Show song info panel - m_SongInfo_Detail = new BeatSaberPlus.SDK.UI.LevelDetail(m_SongInfoPanel.transform); - UnselectSong(); - - m_SongInfo_Detail.SetFavoriteToggleEnabled(true); - m_SongInfo_Detail.SetFavoriteToggleImage("BeatSaberPlus_ChatRequest.Resources.Blacklist.png", "BeatSaberPlus_ChatRequest.Resources.Unblacklist.png"); - m_SongInfo_Detail.SetFavoriteToggleHoverHint("Add/Remove to blacklist"); - m_SongInfo_Detail.SetFavoriteToggleCallback(OnBlacklistButtonPressed); - - m_SongInfo_Detail.SetPracticeButtonEnabled(true); - m_SongInfo_Detail.SetPracticeButtonText("Skip"); - m_SongInfo_Detail.SetPracticeButtonAction(SkipOrAddToQueueSong); - - m_SongInfo_Detail.SetPlayButtonText("Play"); - m_SongInfo_Detail.SetPlayButtonEnabled(true); - m_SongInfo_Detail.SetPlayButtonAction(PlaySong); - - /// Force change to tab Request - OnQueueTypeChanged(null, 0); - } - /// - /// On view activation - /// - protected override sealed void OnViewActivation() - { - m_SongList.PlayPreviewAudio = CRConfig.Instance.PlayPreviewMusic; - - /// Go back to request tab - if (m_TypeSegmentControl.selectedCellNumber != 0) - { - m_TypeSegmentControl.SelectCellWithNumber(0); - OnQueueTypeChanged(null, 0); - } - else - RebuildSongList(true); - } - /// - /// On view deactivation - /// - protected override sealed void OnViewDeactivation() - { - /// Stop preview music if any - m_SongList.StopPreviewMusic(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When the queue type is changed - /// - /// Event sender - /// Tab index - private void OnQueueTypeChanged(SegmentedControl p_Sender, int p_Index) - { - UnselectSong(); - m_SongsProvider = p_Index == 0 ? ChatRequest.Instance.SongQueue : (p_Index == 1 ? ChatRequest.Instance.SongHistory : ChatRequest.Instance.SongBlackList); - RebuildSongList(false); - - m_SongInfo_Detail.SetPracticeButtonText(p_Index == 0 ? "Skip" : "Add to queue"); - } - /// - /// Go to previous song page - /// - private void OnSongPageUpPressed() - { - /// Underflow check - if (m_CurrentPage < 2) - return; - - /// Decrement current page - m_CurrentPage--; - - /// Rebuild song list - RebuildSongList(); - } - /// - /// Go to next song page - /// - private void OnSongPageDownPressed() - { - /// Increment current page - m_CurrentPage++; - - /// Rebuild song list - RebuildSongList(); - } - /// - /// Rebuild song list - /// - /// Is on activation - /// - internal void RebuildSongList(bool p_OnActivation = false) - { - /// Clear selection and items, then refresh the list - m_SongList.TableViewInstance.ClearSelection(); - m_SongList.Data.Clear(); - - lock (m_SongsProvider) - { - /// Append all songs - if (m_SongsProvider.Count > 0) - { - /// Handle page overflow - if (((m_CurrentPage - 1) * s_SONG_PER_PAGE) > m_SongsProvider.Count) - m_CurrentPage = (m_SongsProvider.Count / s_SONG_PER_PAGE) + 1; - - for (int l_I = (m_CurrentPage - 1) * s_SONG_PER_PAGE; l_I < (m_CurrentPage * s_SONG_PER_PAGE); ++l_I) - { - if (l_I >= m_SongsProvider.Count) - break; - - var l_Current = m_SongsProvider[l_I]; - var l_HoverHint = "Requested by " + l_Current.NamePrefix + (l_Current.NamePrefix.Length != 0 ? " " : "") + l_Current.RequesterName; - - if (l_Current.RequestTime.HasValue) - l_HoverHint += "\n$$time$$"; - - if (!string.IsNullOrEmpty(l_Current.Message)) - l_HoverHint += "\n" + l_Current.Message; - - m_SongList.Data.Add(new BeatSaberPlus.SDK.UI.DataSource.SongList.Entry() { - BeatSaver_Map = l_Current.BeatMap, - TitlePrefix = l_Current.NamePrefix + (l_Current.BeatMap.ranked ? "<#F8E600>" : ""), - HoverHint = l_HoverHint, - HoverHintTimeArg = l_Current.RequestTime, - CustomData = l_Current - });; - - if (m_SelectedSong != null && m_SelectedSong.BeatMap.id == l_Current.BeatMap.id) - { - m_SongList.TableViewInstance.SelectCellWithIdx(m_SongList.Data.Count - 1); - OnSongSelected(m_SongList.TableViewInstance, m_SongList.Data.Count - 1); - } - } - - if (m_SelectedSong != null && m_SongsProvider.Where(x => x.BeatMap.id == m_SelectedSong.BeatMap.id).Count() == 0) - { - UnselectSong(); - m_SelectedSong = null; - } - } - else - { - m_CurrentPage = 1; - UnselectSong(); - } - - /// Refresh the list - m_SongList.TableViewInstance.ReloadData(); - - /// Update UI - m_SongUpButton.interactable = m_CurrentPage != 1; - m_SongDownButton.interactable = m_SongsProvider.Count > (m_CurrentPage * s_SONG_PER_PAGE); - } - } - /// - /// When a song is selected - /// - /// Source table - /// Selected row - private void OnSongSelected(TableView p_TableView, int p_Row) - { - /// Unselect previous song - m_SelectedSong = null; - m_SelectedSongIndex = p_Row; - - /// Hide if invalid song - if (p_Row >= m_SongList.Data.Count || m_SongList.Data[p_Row].Invalid || m_SongList.Data[p_Row].BeatSaver_Map == null || (p_TableView != null && m_SongList.Data[p_Row].BeatSaver_Map != null && m_SongList.Data[p_Row].BeatSaver_Map.Partial)) - { - /// Hide song info panel - UnselectSong(); - - return; - } - - /// Fetch song entry - var l_SongEntry = m_SongList.Data[p_Row]; - - /// Show UIs - m_SongInfoPanel.SetActive(true); - ManagerRight.Instance.SetVisible(true); - - /// Update UIs - if (!m_SongInfo_Detail.FromBeatSaver(l_SongEntry.BeatSaver_Map, l_SongEntry.Cover)) - { - /// Hide song info panel - UnselectSong(); - - return; - } - - m_SongInfo_Detail.SetFavoriteToggleValue(m_TypeSegmentControl.selectedCellNumber == 2/* Blacklist */); - ManagerRight.Instance.SetDetail(l_SongEntry.BeatSaver_Map); - - /// Set selected song - m_SelectedSong = l_SongEntry.CustomData as ChatRequest.SongEntry; - - /// Launch preview music if local map - var l_LocalSong = SongCore.Loader.GetLevelByHash(m_SelectedSong.BeatMap.SelectMapVersion().hash); - if (l_LocalSong != null && SongCore.Loader.CustomLevels.ContainsKey(l_LocalSong.customLevelPath)) - m_SongInfo_Detail.SetPlayButtonText("Play"); - else - m_SongInfo_Detail.SetPlayButtonText("Download"); - } - /// - /// Select random song - /// - internal void SelectRandom() - { - OnQueueTypeChanged(null, 0); - - var l_SongIndex = 0; - lock (ChatRequest.Instance.SongQueue) - { - var l_SongCount = ChatRequest.Instance.SongQueue.Count; - - if (l_SongCount > 0) - { - var l_Random = UnityEngine.Random.Range(0, l_SongCount); - m_SelectedSong = ChatRequest.Instance.SongQueue[l_Random]; - m_CurrentPage = (l_Random / s_SONG_PER_PAGE) + 1; - l_SongIndex = l_Random % s_SONG_PER_PAGE; - } - - } - - /// Rebuild song list - RebuildSongList(); - /// Select - OnSongSelected(null, l_SongIndex); - } - /// - /// On song cover fetched - /// - /// Row data - private void OnSongCoverFetched(int p_Index, BeatSaberPlus.SDK.UI.DataSource.SongList.Entry p_RowData) - { - if (m_SelectedSongIndex != p_Index) - return; - - OnSongSelected(null, m_SelectedSongIndex); - } - /// - /// Unselect active song - /// - private void UnselectSong() - { - m_SongInfoPanel.SetActive(false); - ManagerRight.Instance.SetVisible(false); - - m_SelectedSong = null; - - /// Stop preview music if any - m_SongList.StopPreviewMusic(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Skip a song - /// - private void SkipOrAddToQueueSong() - { - if (m_SelectedSong == null) - { - UnselectSong(); - RebuildSongList(); - return; - } - - if (m_TypeSegmentControl.selectedCellNumber == 0/* Request */) - ChatRequest.Instance.DequeueSong(m_SelectedSong, false); - else - ChatRequest.Instance.ReEnqueueSong(m_SelectedSong); - - UnselectSong(); - RebuildSongList(); - } - /// - /// On play song pressed - /// - private void PlaySong() - { - if (m_SelectedSong == null) - { - UnselectSong(); - RebuildSongList(); - return; - } - - try - { - var l_LocalSong = SongCore.Loader.GetLevelByHash(m_SelectedSong.BeatMap.SelectMapVersion().hash); - if (l_LocalSong != null && SongCore.Loader.CustomLevels.ContainsKey(l_LocalSong.customLevelPath)) - { - ChatRequest.Instance.DequeueSong(m_SelectedSong, true); - - BeatSaberPlus.SDK.Game.LevelSelection.FilterToSpecificSong(l_LocalSong); - - ManagerViewFlowCoordinator.Instance().Dismiss(); - UnselectSong(); - } - else - { - /// Show download modal - ShowLoadingModal("Downloading", true); - - /// Start downloading - BeatSaberPlus.SDK.Game.BeatMapsClient.DownloadSong( - m_SelectedSong.BeatMap, - m_SelectedSong.BeatMap.SelectMapVersion(), - CancellationToken.None, - (p_IsSuccess, p_SongReloadingExpectedPath) => { - if (p_IsSuccess) - { - m_SongReloadingExpectedPath = p_SongReloadingExpectedPath; - - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => - { - /// Bind callback - SongCore.Loader.SongsLoadedEvent += OnDownloadedSongLoaded; - /// Refresh loaded songs - SongCore.Loader.Instance.RefreshSongs(false); - }); - } - else - { - /// Show error message - CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => { - HideLoadingModal(); - ShowMessageModal("Download failed!"); - }); - } - }, - this); - - return; - } - } - catch (System.Exception p_Exception) - { - Logger.Instance.Error("[UI][ManagerMain.PlaySong] Error:"); - Logger.Instance.Error(p_Exception); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On black list button pressed - /// - /// State - private void OnBlacklistButtonPressed(ToggleWithCallbacks.SelectionState p_State) - { - if (p_State != ToggleWithCallbacks.SelectionState.Pressed) - return; - - if (m_TypeSegmentControl.selectedCellNumber == 2/* Blacklist */) - ChatRequest.Instance.UnBlacklistSong(m_SelectedSong); - /// Show modal - else - { - ShowConfirmationModal("Do you really want to blacklist this song?", () => { - /// Update UI - m_SongInfo_Detail.SetFavoriteToggleValue(true); - - /// Blacklist the song - ChatRequest.Instance.BlacklistSong(m_SelectedSong); - }); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On download progress reported - /// - /// - void IProgress.Report(float p_Value) - { - SetLoadingModal_DownloadProgress($"Downloading {Mathf.Round((float)(p_Value * 100.0))}%", (float)p_Value); - } - /// - /// When a downloaded song is downloaded - /// - /// Loader instance - /// All loaded songs - private void OnDownloadedSongLoaded(SongCore.Loader p_Loader, ConcurrentDictionary p_Maps) - { - /// Remove callback - SongCore.Loader.SongsLoadedEvent -= OnDownloadedSongLoaded; - - /// Avoid refresh if not active view anymore - if (!CanBeUpdated) - return; - - var l_LocalSong = SongCore.Loader.GetLevelByHash(m_SelectedSong.BeatMap.SelectMapVersion().hash); - if (l_LocalSong == null) - { - foreach (var l_Current in p_Maps) - { - if (!l_Current.Value.customLevelPath.ToLower().Contains(m_SongReloadingExpectedPath.ToLower())) - continue; - - l_LocalSong = l_Current.Value; - break; - } - } - - if (l_LocalSong == null || l_LocalSong.levelID.Replace("custom_level_", "").ToLower() != m_SelectedSong.BeatMap.SelectMapVersion().hash) - { - HideLoadingModal(); - ShowMessageModal("An error occurred while downloading this map.\nDownloaded song doesn't match."); - return; - } - - StartCoroutine(PlayDownloadedLevel()); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Play download song - /// - /// - private IEnumerator PlayDownloadedLevel() - { - yield return new WaitForEndOfFrame(); - - if (!CanBeUpdated) - yield break; - - /// Hide loading modal - HideLoadingModal(); - - /// Reselect the cell - PlaySong(); - - yield return null; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerMainView.cs b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerMainView.cs new file mode 100644 index 0000000..56695dd --- /dev/null +++ b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerMainView.cs @@ -0,0 +1,562 @@ +using CP_SDK.UI.Data; +using CP_SDK.XUI; +using HMUI; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace BeatSaberPlus_ChatRequest.UI +{ + /// + /// Chat request main view controller + /// + internal class ManagerMainView + : CP_SDK.UI.ViewController, + BeatSaberPlus.SDK.UI.Data.SongListController, + IProgress + { + private static CustomPreviewBeatmapLevel m_SongToSelectAfterDismiss = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private XUIText m_Title = null; + private XUITextSegmentedControl m_TypeSegmentControl = null; + private XUIVVList m_SongList = null; + private XUIVLayout m_SongInfoPanelOwner = null; + private XUIVLayout m_SongInfoPanelNoSong = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private BeatSaberPlus.SDK.UI.LevelDetail m_SongInfo_Detail = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private Data.SongEntry m_SelectedSong = null; + private List m_SongsProvider = null; + private string m_SongReloadingExpectedPath = ""; + private int m_LastTab = 0; + private float m_Tab0Scroll = 0.0f; + private float m_Tab1Scroll = 0.0f; + private float m_Tab2Scroll = 0.0f; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayoutMainView( + Templates.TitleBar("Title") + .ForEachDirect(x => x.Bind(ref m_Title)), + + XUITextSegmentedControl.Make(new string[] { "Requests", "History", "Blacklist" }) + .OnActiveChanged(OnQueueTypeChanged) + .Bind(ref m_TypeSegmentControl), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(ListCellPrefabs.Get()) + .OnListItemSelected(OnSongSelected) + .Bind(ref m_SongList) + ) + .SetWidth(67) + .SetHeight(65) + .SetSpacing(0) + .SetPadding(0) + .SetBackground(true, CP_SDK.UI.UISystem.ListBGColor) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + XUIVLayout.Make( + + ) + .SetWidth(77) + .SetHeight(65) + .SetPadding(4, 0, 0, 2) + .SetActive(false) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true) + .Bind(ref m_SongInfoPanelOwner), + + XUIVLayout.Make( + XUIText.Make("Please select a song in the list!") + .SetStyle(FontStyles.Bold) + .SetAlign(TextAlignmentOptions.MidlineGeoAligned) + ) + .SetWidth(77) + .SetHeight(65) + .SetActive(true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true) + .Bind(ref m_SongInfoPanelNoSong) + ) + .SetHeight(65) + .SetSpacing(0) + .SetPadding(2) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + + /// Show song info panel + m_SongInfo_Detail = new BeatSaberPlus.SDK.UI.LevelDetail(m_SongInfoPanelOwner.RTransform); + UnselectSong(); + + m_SongInfo_Detail.SetFavoriteToggleEnabled(true); + m_SongInfo_Detail.SetFavoriteToggleImage( + CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromPath(Assembly.GetExecutingAssembly(), "BeatSaberPlus_ChatRequest.Resources.Blacklist.png")), + CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromPath(Assembly.GetExecutingAssembly(), "BeatSaberPlus_ChatRequest.Resources.Unblacklist.png")) + ); + m_SongInfo_Detail.SetFavoriteToggleHoverHint("Add/Remove to blacklist"); + m_SongInfo_Detail.SetFavoriteToggleCallback(OnBlacklistButtonPressed); + + m_SongInfo_Detail.SetSecondaryButtonEnabled(true); + m_SongInfo_Detail.SetSecondaryButtonText("Skip"); + m_SongInfo_Detail.OnSecondaryButton = SkipOrAddToQueueSong; + + m_SongInfo_Detail.SetPrimaryButtonText("Play"); + m_SongInfo_Detail.SetPrimaryButtonEnabled(true); + m_SongInfo_Detail.OnPrimaryButton = PlaySong; + + /// Force change to tab Request + OnQueueTypeChanged(0); + } + /// + /// On view activation + /// + protected override sealed void OnViewActivation() + { + /// Go back to request tab + if (m_TypeSegmentControl.Element.GetActiveText() != 0) + m_TypeSegmentControl.SetActiveText(0); + else + RebuildSongList(); + + RebuiltTitle(); + } + /// + /// On view deactivation + /// + protected override sealed void OnViewDeactivation() + { + /// Stop preview music if any + m_SelectedSong?.StopPreviewMusic(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When the queue type is changed + /// + /// Tab index + private void OnQueueTypeChanged(int p_Index) + { + var l_OldScroll = m_SongList.Element.ScrollPosition; + if (m_LastTab == 0) m_Tab0Scroll = l_OldScroll; + if (m_LastTab == 1) m_Tab1Scroll = l_OldScroll; + if (m_LastTab == 2) m_Tab2Scroll = l_OldScroll; + + UnselectSong(); + m_SongsProvider = p_Index == 0 ? ChatRequest.Instance.SongQueue : (p_Index == 1 ? ChatRequest.Instance.SongHistory : ChatRequest.Instance.SongBlackList); + RebuildSongList(); + + if (p_Index == 0) m_SongList.Element.ScrollTo(m_Tab0Scroll, false); + if (p_Index == 1) m_SongList.Element.ScrollTo(m_Tab1Scroll, false); + if (p_Index == 2) m_SongList.Element.ScrollTo(m_Tab2Scroll, false); + m_LastTab = p_Index; + + m_SongInfo_Detail.SetSecondaryButtonText(p_Index == 0 ? "Skip" : "Add to queue"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Rebuild song list + /// + /// + internal void RebuildSongList() + { + var l_OldScroll = m_SongList.Element.ScrollPosition; + lock (m_SongsProvider) + { + var l_OldSelected = m_SelectedSong; + + for (var l_I = 0; l_I < m_SongsProvider.Count; ++l_I) + m_SongsProvider[l_I].SongListController = this; + + m_SongList.Element.SetListItems(m_SongsProvider); + if (l_OldSelected != null) + m_SongList.Element.SetSelectedListItem(l_OldSelected); + } + m_SongList.Element.ScrollTo(l_OldScroll, false); + + RebuiltTitle(); + } + /// + /// Rebuild title + /// + internal void RebuiltTitle() + { + var l_Minutes = ChatRequest.Instance.QueueDuration / 60; + var l_Seconds = (ChatRequest.Instance.QueueDuration - (l_Minutes * 60)); + + if (ChatRequest.Instance.QueueOpen) + { + if (l_Minutes != 0 || l_Seconds != 0) + m_Title.SetText($"Queue is open | Duration {l_Minutes}m{l_Seconds}s"); + else + m_Title.SetText($"Queue is open"); + } + else + { + if (l_Minutes != 0 || l_Seconds != 0) + m_Title.SetText($"Queue is closed | Duration {l_Minutes}m{l_Seconds}s"); + else + m_Title.SetText($"Queue is closed"); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When a song is selected + /// + /// Selected item + private void OnSongSelected(IListItem p_SelectedItem) + { + if (p_SelectedItem == null || !(p_SelectedItem is Data.SongEntry l_SongEntry)) + { + UnselectSong(); + return; + } + + m_SelectedSong = null; + + /// Hide if invalid song + if (p_SelectedItem == null + || l_SongEntry.Invalid + || l_SongEntry.BeatSaver_Map == null + || (l_SongEntry.BeatSaver_Map != null && l_SongEntry.BeatSaver_Map.Partial)) + { + UnselectSong(); + return; + } + + /// Show UIs + m_SongInfoPanelNoSong.SetActive(false); + m_SongInfoPanelOwner.SetActive(true); + ManagerRightView.Instance.SetVisible(true); + + /// Update UIs + if (!m_SongInfo_Detail.FromBeatSaver(l_SongEntry.BeatSaver_Map, l_SongEntry.Cover)) + { + UnselectSong(); + return; + } + + m_SongInfo_Detail.SetFavoriteToggleValue(m_TypeSegmentControl.Element.GetActiveText() == 2/* Blacklist */); + ManagerRightView.Instance.SetDetail(l_SongEntry.BeatSaver_Map); + + /// Set selected song + m_SelectedSong = l_SongEntry; + + var l_LocalSong = SongCore.Loader.GetLevelByHash(m_SelectedSong.BeatSaver_Map.SelectMapVersion().hash); + if (l_LocalSong != null && SongCore.Loader.CustomLevels.ContainsKey(l_LocalSong.customLevelPath)) + m_SongInfo_Detail.SetPrimaryButtonText("Play"); + else + m_SongInfo_Detail.SetPrimaryButtonText("Download"); + } + /// + /// Select random song + /// + internal void SelectRandom() + { + m_TypeSegmentControl.SetActiveText(0/* Request */); + + var l_ToSelect = null as Data.SongEntry; + lock (ChatRequest.Instance.SongQueue) + { + var l_SongCount = ChatRequest.Instance.SongQueue.Count; + if (l_SongCount > 0) + l_ToSelect = ChatRequest.Instance.SongQueue[UnityEngine.Random.Range(0, l_SongCount)]; + } + + if (l_ToSelect != null) + m_SongList.Element.SetSelectedListItem(l_ToSelect); + } + /// + /// Unselect active song + /// + private void UnselectSong() + { + m_SongInfoPanelOwner.SetActive(false); + m_SongInfoPanelNoSong.SetActive(true); + ManagerRightView.Instance.SetVisible(false); + + m_SelectedSong?.StopPreviewMusic(); + m_SelectedSong = null; + + m_SongList.Element.SetSelectedListItem(null, false); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Skip a song + /// + private void SkipOrAddToQueueSong() + { + if (m_SelectedSong == null) + { + UnselectSong(); + RebuildSongList(); + return; + } + + if (m_TypeSegmentControl.Element.GetActiveText() == 0/* Request */) + ChatRequest.Instance.DequeueSong(m_SelectedSong, false); + else + ChatRequest.Instance.ReEnqueueSong(m_SelectedSong); + + UnselectSong(); + RebuildSongList(); + } + /// + /// On play song pressed + /// + private void PlaySong() + { + if (m_SelectedSong == null) + { + UnselectSong(); + RebuildSongList(); + return; + } + + try + { + var l_LocalSong = SongCore.Loader.GetLevelByHash(m_SelectedSong.BeatSaver_Map.SelectMapVersion().hash); + if (l_LocalSong != null && SongCore.Loader.CustomLevels.ContainsKey(l_LocalSong.customLevelPath)) + { + ChatRequest.Instance.DequeueSong(m_SelectedSong, true); + + if (m_TypeSegmentControl.Element.GetActiveText() == 0) + m_SongList.RemoveListItem(m_SelectedSong); + + m_SongToSelectAfterDismiss = l_LocalSong; + + CP_SDK.UI.ScreenSystem.OnDismiss -= SelectSongAfterScreenSystemDismiss; + CP_SDK.UI.ScreenSystem.OnDismiss += SelectSongAfterScreenSystemDismiss; + + ManagerViewFlowCoordinator.Instance().Dismiss(); + UnselectSong(); + } + else + { + /// Show download modal + ShowLoadingModal("Downloading", true); + + /// Start downloading + BeatSaberPlus.SDK.Game.BeatMapsClient.DownloadSong( + m_SelectedSong.BeatSaver_Map, + m_SelectedSong.BeatSaver_Map.SelectMapVersion(), + CancellationToken.None, + (p_IsSuccess, p_SongReloadingExpectedPath) => { + if (p_IsSuccess) + { + m_SongReloadingExpectedPath = p_SongReloadingExpectedPath; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + SongCore.Loader.SongsLoadedEvent += OnDownloadedSongLoaded; + SongCore.Loader.Instance.RefreshSongs(false); + }); + } + else + { + /// Show error message + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => { + CloseLoadingModal(); + ShowMessageModal("Download failed!"); + }); + } + }, + this); + + return; + } + } + catch (System.Exception p_Exception) + { + Logger.Instance.Error("[UI][ManagerMain.PlaySong] Error:"); + Logger.Instance.Error(p_Exception); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On black list button pressed + /// + /// State + private void OnBlacklistButtonPressed(ToggleWithCallbacks.SelectionState p_State) + { + if (p_State != ToggleWithCallbacks.SelectionState.Pressed) + return; + + if (m_TypeSegmentControl.Element.GetActiveText() == 2/* Blacklist */) + ChatRequest.Instance.UnBlacklistSong(m_SelectedSong); + /// Show modal + else + { + ShowConfirmationModal("Do you really want to blacklist this song?", (p_Confirm) => { + if (!p_Confirm) + return; + + /// Update UI + m_SongInfo_Detail.SetFavoriteToggleValue(true); + + /// Blacklist the song + ChatRequest.Instance.BlacklistSong(m_SelectedSong); + }); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When a downloaded song is downloaded + /// + /// Loader instance + /// All loaded songs + private void OnDownloadedSongLoaded(SongCore.Loader p_Loader, ConcurrentDictionary p_Maps) + { + /// Remove callback + SongCore.Loader.SongsLoadedEvent -= OnDownloadedSongLoaded; + + /// Avoid refresh if not active view anymore + if (!CanBeUpdated) + return; + + var l_LocalSong = SongCore.Loader.GetLevelByHash(m_SelectedSong.BeatSaver_Map.SelectMapVersion().hash); + if (l_LocalSong == null) + { + foreach (var l_Current in p_Maps) + { + if (!l_Current.Value.customLevelPath.ToLower().Contains(m_SongReloadingExpectedPath.ToLower())) + continue; + + l_LocalSong = l_Current.Value; + break; + } + } + + if (l_LocalSong == null || l_LocalSong.levelID.Replace("custom_level_", "").ToLower() != m_SelectedSong.BeatSaver_Map.SelectMapVersion().hash) + { + CloseLoadingModal(); + ShowMessageModal("An error occurred while downloading this map.\nDownloaded song doesn't match."); + return; + } + + StartCoroutine(PlayDownloadedLevel()); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Play download song + /// + /// + private IEnumerator PlayDownloadedLevel() + { + yield return new WaitForEndOfFrame(); + + if (!CanBeUpdated) + yield break; + + /// Hide loading modal + CloseLoadingModal(); + + /// Reselect the cell + PlaySong(); + + yield return null; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Select song after screen system dismiss + /// + private static void SelectSongAfterScreenSystemDismiss() + { + CP_SDK.UI.ScreenSystem.OnDismiss -= SelectSongAfterScreenSystemDismiss; + + BeatSaberPlus.SDK.Game.LevelSelection.FilterToSpecificSong(m_SongToSelectAfterDismiss); + m_SongToSelectAfterDismiss = null; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On song list item cover fetched + /// + /// Item + public void OnSongListItemCoverFetched(BeatSaberPlus.SDK.UI.Data.SongListItem p_Item) + { + if (m_SelectedSong != p_Item) + return; + + m_SongInfo_Detail.Cover = p_Item.Cover; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Should play preview audio? + /// + /// + public bool PlayPreviewAudio() + => CRConfig.Instance.PlayPreviewMusic; + /// + /// Preview audio playback volume + /// + /// + public float PreviewAudioVolume() + => 1.0f; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On download progress reported + /// + /// + void IProgress.Report(float p_Value) + => LoadingModal_SetMessage($"Downloading {Mathf.Round((float)(p_Value * 100.0))}%"); + } +} diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerRight.bsml b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerRight.bsml deleted file mode 100644 index 299e434..0000000 --- a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerRight.bsml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerRight.cs b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerRightView.cs similarity index 51% rename from Modules/BeatSaberPlus_ChatRequest/UI/ManagerRight.cs rename to Modules/BeatSaberPlus_ChatRequest/UI/ManagerRightView.cs index 0309673..f5832c1 100644 --- a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerRight.cs +++ b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerRightView.cs @@ -1,46 +1,25 @@ -using BeatSaberMarkupLanguage.Attributes; +using CP_SDK.XUI; using System; -using System.Diagnostics; -using TMPro; using UnityEngine; +using UnityEngine.UI; namespace BeatSaberPlus_ChatRequest.UI { /// /// Manager view detail /// - internal class ManagerRight : BeatSaberPlus.SDK.UI.ResourceViewController + internal sealed class ManagerRightView : CP_SDK.UI.ViewController { - /// - /// Month list - /// - private static string[] s_Months = new string[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; + private XUIVLayout m_Root; + private XUIVScrollView m_ScrollView; + private XUIText m_Description; + private XUIText m_Details; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// -#pragma warning disable CS0649 - [UIObject("DetailBackground")] - private GameObject m_DetailBackground = null; - [UIObject("SubDetailBackground")] - private GameObject m_SubDetailBackground = null; - [UIComponent("DetailText")] - private HMUI.TextPageScrollView m_DetailText = null; - [UIObject("SubDetailText")] - private GameObject m_SubDetailText = null; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Pending detail - /// - private BeatSaberPlus.SDK.Game.BeatMaps.MapDetail m_PendingDetail = null; - /// - /// Last detail - /// - private BeatSaberPlus.SDK.Game.BeatMaps.MapDetail m_LastDetail = null; + private BeatSaberPlus.SDK.Game.BeatMaps.MapDetail m_PendingDetail = null; + private BeatSaberPlus.SDK.Game.BeatMaps.MapDetail m_LastDetail = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -50,11 +29,47 @@ internal class ManagerRight : BeatSaberPlus.SDK.UI.ResourceViewController protected override sealed void OnViewCreation() { - /// Update background color - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_DetailBackground, 0.5f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_SubDetailBackground, 0.5f); - - m_SubDetailText.GetComponent().alignment = TextAlignmentOptions.Center; + Templates.FullRectLayout( + Templates.TitleBar("Information"), + + XUIHLayout.Make( + XUIVScrollView.Make( + XUIText.Make($"No description...") + .SetAlign(TMPro.TextAlignmentOptions.Left) + .Bind(ref m_Description) + ) + .Bind(ref m_ScrollView) + ) + .SetHeight(45) + .SetSpacing(0) + .SetPadding(0) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + XUIVLayout.Make( + XUIText.Make("No map\nSelected") + .SetFontSize(3f) + .SetAlign(TMPro.TextAlignmentOptions.Center) + .SetStyle(TMPro.FontStyles.Bold) + .Bind(ref m_Details) + ) + .SetBackground(true) + .SetWidth(85f), + + XUIHLayout.Make( + XUIPrimaryButton.Make("Link song to chat", OnLinkPressed) + .SetHeight(8f).SetWidth(40f), + XUIPrimaryButton.Make("Open in beatsaver.com", OnBeatsaverPressed) + .SetHeight(8f).SetWidth(40f) + ) + .SetBackground(true) + .SetWidth(84f) + ) + .Bind(ref m_Root) + .SetBackground(true, null, true) + .BuildUI(transform); } /// /// On view activation @@ -70,9 +85,7 @@ protected override sealed void OnViewActivation() /// On view deactivation /// protected sealed override void OnViewDeactivation() - { - CloseAllModals(); - } + => CloseAllModals(); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -83,11 +96,10 @@ protected sealed override void OnViewDeactivation() /// Is visible internal void SetVisible(bool p_Visible) { - if (!CanBeUpdated || transform.childCount == 0) - return; + m_Root?.Element?.gameObject?.SetActive(p_Visible); - CloseAllModals(); - transform.GetChild(0).gameObject.SetActive(p_Visible); + if (CanBeUpdated) + CloseAllModals(); } //////////////////////////////////////////////////////////////////////////// @@ -97,7 +109,7 @@ internal void SetVisible(bool p_Visible) /// Set details /// /// p_Detail - internal void SetDetail(BeatSaberPlus.SDK.Game.BeatMaps.MapDetail p_Detail, bool p_SecondTime = false) + internal void SetDetail(BeatSaberPlus.SDK.Game.BeatMaps.MapDetail p_Detail) { if (!CanBeUpdated) { @@ -105,23 +117,22 @@ internal void SetDetail(BeatSaberPlus.SDK.Game.BeatMaps.MapDetail p_Detail, bool return; } - string l_Description = "" + System.Net.WebUtility.HtmlEncode(p_Detail.description.Replace("\r\n", "\n")); - - if (l_Description.Trim().Length == "".Length) + string l_Description = System.Net.WebUtility.HtmlDecode(p_Detail.description.Replace("\r\n", "\n").Replace("<", "<\u200B").Replace(">", "\u200B>")); + if (l_Description.Trim().Length == 0) l_Description = "No description..."; + else + l_Description = "" + l_Description; - l_Description += "\n\n\n\n\n\n\n\n\n\n "; - - m_DetailText.SetText(l_Description); - m_DetailText.ScrollTo(0, true); + m_Description.SetText(l_Description); + m_ScrollView.Element.ScrollTo(0, true); var l_Date = p_Detail.GetUploadTime(); float l_Vote = (float)Math.Round((double)p_Detail.stats.score * 100f, 0); - string l_SubText = $"Votes +{p_Detail.stats.upvotes}/-{p_Detail.stats.downvotes} ({l_Vote}%) {p_Detail.stats.downloads} downloads\n"; - l_SubText += "Uploaded on " + s_Months[l_Date.Month - 1] + " " + l_Date.Day + " of " + l_Date.Year; + string l_SubText = $"Votes +{p_Detail.stats.upvotes}/-{p_Detail.stats.downvotes} ({l_Vote}%)\n"; + l_SubText += "Uploaded on " + CP_SDK.Misc.Time.MonthNames[l_Date.Month - 1] + " " + l_Date.Day + " of " + l_Date.Year; - m_SubDetailText.GetComponent().text = l_SubText; + m_Details.SetText(l_SubText); m_LastDetail = p_Detail; m_PendingDetail = null; @@ -133,7 +144,6 @@ internal void SetDetail(BeatSaberPlus.SDK.Game.BeatMaps.MapDetail p_Detail, bool /// /// On link pressed /// - [UIAction("click-btn-link")] private void OnLinkPressed() { ShowMessageModal("Song sent to the chat."); @@ -144,11 +154,10 @@ private void OnLinkPressed() /// /// On beat saver pressed /// - [UIAction("click-btn-beatsaver")] private void OnBeatsaverPressed() { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://beatmaps.io/maps/" + m_LastDetail.id); + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL("https://beatmaps.io/maps/" + m_LastDetail.id); } } } diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerViewFlowCoordinator.cs b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerViewFlowCoordinator.cs index 1c50fa1..7dda6a8 100644 --- a/Modules/BeatSaberPlus_ChatRequest/UI/ManagerViewFlowCoordinator.cs +++ b/Modules/BeatSaberPlus_ChatRequest/UI/ManagerViewFlowCoordinator.cs @@ -1,4 +1,4 @@ -using HMUI; +using CP_SDK.UI.Views; using UnityEngine; namespace BeatSaberPlus_ChatRequest.UI @@ -6,37 +6,16 @@ namespace BeatSaberPlus_ChatRequest.UI /// /// Manager UI flow coordinator /// - internal class ManagerViewFlowCoordinator : BeatSaberPlus.SDK.UI.ViewFlowCoordinator + internal sealed class ManagerViewFlowCoordinator : CP_SDK.UI.FlowCoordinator { - /// - /// Title - /// public override string Title => "Chat Request"; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Main view - /// - private ManagerMain m_MainView; - /// - /// Left view - /// - private ManagerLeft m_LeftView; - /// - /// Details view - /// - private ManagerRight m_RightView; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get initial views controller - /// - /// (Middle, Left, Right) - protected override sealed (ViewController, ViewController, ViewController) GetInitialViewsController() => (m_MainView, m_LeftView, m_RightView); + private ManagerLeftView m_ManagerLeftView; + private ManagerMainView m_ManagerMainView; + private ManagerRightView m_ManagerRightView; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -44,32 +23,30 @@ internal class ManagerViewFlowCoordinator : BeatSaberPlus.SDK.UI.ViewFlowCoordin /// /// Constructor /// - internal ManagerViewFlowCoordinator() + public override void Init() { - m_MainView = CreateViewController(); - m_LeftView = CreateViewController(); - m_RightView = CreateViewController(); + m_ManagerLeftView = CP_SDK.UI.UISystem.CreateViewController(); + m_ManagerMainView = CP_SDK.UI.UISystem.CreateViewController(); + m_ManagerRightView = CP_SDK.UI.UISystem.CreateViewController(); } /// /// On destroy /// internal void OnDestroy() { - if (m_MainView != null) - { - GameObject.Destroy(m_MainView.gameObject); - m_MainView = null; - } - if (m_LeftView != null) - { - GameObject.Destroy(m_LeftView.gameObject); - m_LeftView = null; - } - if (m_RightView != null) - { - GameObject.Destroy(m_RightView.gameObject); - m_RightView = null; - } + CP_SDK.UI.UISystem.DestroyUI(ref m_ManagerLeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_ManagerMainView); + CP_SDK.UI.UISystem.DestroyUI(ref m_ManagerRightView); } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get initial views controller + /// + /// (Middle, Left, Right) + protected override sealed (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetInitialViewsController() + => (m_ManagerMainView, m_ManagerLeftView, m_ManagerRightView); } } diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/Settings.bsml b/Modules/BeatSaberPlus_ChatRequest/UI/Settings.bsml deleted file mode 100644 index 1a318bd..0000000 --- a/Modules/BeatSaberPlus_ChatRequest/UI/Settings.bsml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/Settings.cs b/Modules/BeatSaberPlus_ChatRequest/UI/Settings.cs deleted file mode 100644 index 16a34b5..0000000 --- a/Modules/BeatSaberPlus_ChatRequest/UI/Settings.cs +++ /dev/null @@ -1,122 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; - -namespace BeatSaberPlus_ChatRequest.UI -{ - /// - /// Chat request settings view - /// - internal class Settings : BeatSaberPlus.SDK.UI.ResourceViewController - { -#pragma warning disable CS0649 - [UIComponent("use-request")] - private IncrementSetting m_UserRequest; - [UIComponent("vip-request")] - private IncrementSetting m_VIPBonusRequest; - [UIComponent("sub-request")] - private IncrementSetting m_SubscriberBonusRequest; - [UIComponent("his-size")] - private IncrementSetting m_HistorySize; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("pre-toggle")] - private ToggleSetting m_PlayPreviewMusic; - [UIComponent("mod-toggle")] - private ToggleSetting m_ModeratorPower; - [UIComponent("que-size")] - private IncrementSetting m_QueueSize; - [UIComponent("que-cool")] - private IncrementSetting m_QueueCooldown; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - - /// Left - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_UserRequest, l_Event, null, CRConfig.Instance.UserMaxRequest, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_VIPBonusRequest, l_Event, null, CRConfig.Instance.VIPBonusRequest, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_SubscriberBonusRequest, l_Event, null, CRConfig.Instance.SubscriberBonusRequest, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_HistorySize, l_Event, null, CRConfig.Instance.HistorySize, true); - - /// Right - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_PlayPreviewMusic, l_Event, CRConfig.Instance.PlayPreviewMusic, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ModeratorPower, l_Event, CRConfig.Instance.ModeratorPower, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_QueueSize, l_Event, null, CRConfig.Instance.QueueCommandShowSize, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_QueueCooldown, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Seconds, CRConfig.Instance.QueueCommandCooldown, true); - } - /// - /// On view deactivation - /// - protected override sealed void OnViewDeactivation() - { - CRConfig.Instance.Save(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When settings are changed - /// - /// - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - /// Left - CRConfig.Instance.UserMaxRequest = (int)m_UserRequest.Value; - CRConfig.Instance.VIPBonusRequest = (int)m_VIPBonusRequest.Value; - CRConfig.Instance.SubscriberBonusRequest = (int)m_SubscriberBonusRequest.Value; - CRConfig.Instance.HistorySize = (int)m_HistorySize.Value; - - /// Right - CRConfig.Instance.PlayPreviewMusic = m_PlayPreviewMusic.Value; - CRConfig.Instance.ModeratorPower = m_ModeratorPower.Value; - CRConfig.Instance.QueueCommandShowSize = (int)m_QueueSize.Value; - CRConfig.Instance.QueueCommandCooldown = (int)m_QueueCooldown.Value; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset settings - /// - internal void RefreshSettings() - { - m_PreventChanges = true; - - /// Left - m_UserRequest.Value = CRConfig.Instance.UserMaxRequest; - m_VIPBonusRequest.Value = CRConfig.Instance.VIPBonusRequest; - m_SubscriberBonusRequest.Value = CRConfig.Instance.SubscriberBonusRequest; - m_HistorySize.Value = CRConfig.Instance.HistorySize; - - /// Right - m_PlayPreviewMusic.Value = CRConfig.Instance.PlayPreviewMusic; - m_ModeratorPower.Value = CRConfig.Instance.ModeratorPower; - m_QueueSize.Value = CRConfig.Instance.QueueCommandShowSize; - m_QueueCooldown.Value = CRConfig.Instance.QueueCommandCooldown; - - m_PreventChanges = false; - } - } -} diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeft.bsml b/Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeft.bsml deleted file mode 100644 index f5f4c25..0000000 --- a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeft.bsml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeft.cs b/Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeftView.cs similarity index 75% rename from Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeft.cs rename to Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeftView.cs index 9e29574..dcc45c2 100644 --- a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeft.cs +++ b/Modules/BeatSaberPlus_ChatRequest/UI/SettingsLeftView.cs @@ -1,15 +1,15 @@ -using BeatSaberMarkupLanguage.Attributes; -using System.Diagnostics; +using CP_SDK.XUI; using UnityEngine; namespace BeatSaberPlus_ChatRequest.UI { /// - /// Chat request settings left screen + /// Settings left view /// - internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController + internal sealed class SettingsLeftView : CP_SDK.UI.ViewController { - private static readonly string s_InformationsStr = "Commands" + private static readonly string s_InformationStr = + "Commands" + "\n" + "- !bsr #KEY/#NAME\nRequest a song by BSR code or search by name" + "\n" + "- !bsrhelp\nDisplay a guide about how to request a song" + "\n" + "- !oops !wrongsong !wrong\nRemove last user requested song" @@ -32,7 +32,7 @@ internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController[Moderator]!sabotage on/off\nEnable or disable LIV streamer kit bombs" + "\n" + "- [Moderator]!songmsg #KEY #MSG\nAllow to set a message on a request" + "\n" - + "\n" + "Filters" + + "\n" + "Filters" + "\n" + "- No BeatSage\nDiscard all auto mapped maps" + "\n" + "- NPS min\nDiscard all maps with no difficulty above NotePerSecond min" + "\n" + "- NPS max\nDiscard all maps with no difficulty below NotePerSecond max" @@ -46,24 +46,30 @@ internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController /// On view creation /// protected override sealed void OnViewCreation() { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); - m_Informations.SetText(s_InformationsStr); - m_Informations.UpdateVerticalScrollIndicator(0); + Templates.FullRectLayout( + Templates.TitleBar("Information"), + + Templates.ScrollableInfos(50, + XUIText.Make(s_InformationStr) + .SetAlign(TMPro.TextAlignmentOptions.Left) + ), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("OBS integration", OnOBSIntegrationButton), + XUIPrimaryButton.Make("Reset", OnResetButton), + XUIPrimaryButton.Make("Web Configuration", OnWebConfigurationButton) + ), + Templates.ExpandedButtonsLine( + XUISecondaryButton.Make("Documentation", OnDocumentationButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); } //////////////////////////////////////////////////////////////////////////// @@ -72,38 +78,38 @@ protected override sealed void OnViewCreation() /// /// Open OBS integration button /// - [UIAction("click-obs-integration-btn-pressed")] private void OnOBSIntegrationButton() { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://github.com/hardcpp/BeatSaberPlus/wiki#chat-request---obs-integration"); + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL("https://github.com/hardcpp/BeatSaberPlus/wiki#chat-request---obs-integration"); } /// /// Reset button /// - [UIAction("click-reset-btn-pressed")] private void OnResetButton() { - ShowConfirmationModal("Do you really want to reset\nchat request configuration and filters?", () => + ShowConfirmationModal("Do you really want to reset\nchat request configuration and filters?", (p_Confirm) => { + if (!p_Confirm) + return; + /// Reset config CRConfig.Instance.Reset(); CRConfig.Instance.Enabled = true; CRConfig.Instance.Save(); /// Refresh values - Settings.Instance.RefreshSettings(); - SettingsRight.Instance.RefreshSettings(); + SettingsMainView.Instance.RefreshSettings(); + SettingsRightView.Instance.RefreshSettings(); }); } /// /// Open web configuration button /// - [UIAction("click-open-web-configuration-btn-pressed")] private void OnWebConfigurationButton() { - ShowMessageModal("URL opened in your desktop browser."); - CP_SDK.Chat.Service.OpenWebConfigurator(); + ShowMessageModal("URL opened in your web browser."); + CP_SDK.Chat.Service.OpenWebConfiguration(); } //////////////////////////////////////////////////////////////////////////// @@ -112,11 +118,10 @@ private void OnWebConfigurationButton() /// /// Documentation button /// - [UIAction("click-documentation-btn-pressed")] private void OnDocumentationButton() { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://github.com/hardcpp/BeatSaberPlus/wiki#chat-request"); + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL(ChatRequest.Instance.DocumentationURL); } } } diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsMainView.cs b/Modules/BeatSaberPlus_ChatRequest/UI/SettingsMainView.cs new file mode 100644 index 0000000..0522985 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatRequest/UI/SettingsMainView.cs @@ -0,0 +1,162 @@ +using CP_SDK.XUI; + +namespace BeatSaberPlus_ChatRequest.UI +{ + /// + /// Settings main view + /// + internal sealed class SettingsMainView : CP_SDK.UI.ViewController + { + private XUISlider m_UserRequest; + private XUISlider m_VIPBonusRequest; + private XUISlider m_SubscriberBonusRequest; + private XUISlider m_HistorySize; + + private XUIToggle m_PlayPreviewMusic; + private XUIToggle m_ModeratorPower; + private XUISlider m_QueueSize; + private XUISlider m_QueueCooldown; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private bool m_PreventChanges = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayoutMainView( + Templates.TitleBar("Chat Request - Settings"), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("User max request"), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue(20f).SetIncrements(1f).SetInteger(true) + .SetValue(CRConfig.Instance.UserMaxRequest) + .Bind(ref m_UserRequest), + + XUIText.Make("VIP bonus request"), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue(20f).SetIncrements(1f).SetInteger(true) + .SetValue(CRConfig.Instance.VIPBonusRequest) + .Bind(ref m_VIPBonusRequest), + + XUIText.Make("Subscriber bonus request"), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue(20f).SetIncrements(1f).SetInteger(true) + .SetValue(CRConfig.Instance.SubscriberBonusRequest) + .Bind(ref m_SubscriberBonusRequest), + + XUIText.Make("History size"), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue(50f).SetIncrements(1f).SetInteger(true) + .SetValue(CRConfig.Instance.HistorySize) + .Bind(ref m_HistorySize) + + ) + .SetSpacing(2) + .SetPadding(2) + .SetWidth(60) + .ForEachDirect( x => x.SetAlign(TMPro.TextAlignmentOptions.Midline)) + .ForEachDirect(x => x.OnValueChanged(_ => OnValueChanged())), + + XUIVLayout.Make( + XUIText.Make("Play preview music if downloaded"), + XUIToggle.Make() + .SetValue(CRConfig.Instance.PlayPreviewMusic) + .Bind(ref m_PlayPreviewMusic), + + XUIText.Make("Give moderators power to manage queue"), + XUIToggle.Make() + .SetValue(CRConfig.Instance.ModeratorPower) + .Bind(ref m_ModeratorPower), + + XUIText.Make("Queue command show count"), + XUISlider.Make() + .SetMinValue(1f).SetMaxValue(10f).SetIncrements(1f).SetInteger(true) + .SetValue(CRConfig.Instance.QueueCommandShowSize) + .Bind(ref m_QueueSize), + + XUIText.Make("Queue command cooldown seconds"), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue(60f).SetIncrements(1f).SetInteger(true) + .SetValue(CRConfig.Instance.QueueCommandCooldown) + .Bind(ref m_QueueCooldown) + ) + .SetSpacing(2) + .SetPadding(2) + .SetWidth(60) + .ForEachDirect( x => x.SetAlign(TMPro.TextAlignmentOptions.Midline)) + .ForEachDirect(x => x.OnValueChanged(_ => OnValueChanged())) + .ForEachDirect(x => x.OnValueChanged(_ => OnValueChanged())) + ) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained), + + XUIVSpacer.Make(5f) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + /// + /// On view deactivation + /// + protected override sealed void OnViewDeactivation() + { + CRConfig.Instance.Save(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When settings are changed + /// + /// + private void OnValueChanged() + { + if (m_PreventChanges) + return; + + /// Left + CRConfig.Instance.UserMaxRequest = (int)m_UserRequest.Element.GetValue(); + CRConfig.Instance.VIPBonusRequest = (int)m_VIPBonusRequest.Element.GetValue(); + CRConfig.Instance.SubscriberBonusRequest = (int)m_SubscriberBonusRequest.Element.GetValue(); + CRConfig.Instance.HistorySize = (int)m_HistorySize.Element.GetValue(); + + /// Right + CRConfig.Instance.PlayPreviewMusic = m_PlayPreviewMusic.Element.GetValue(); + CRConfig.Instance.ModeratorPower = m_ModeratorPower.Element.GetValue(); + CRConfig.Instance.QueueCommandShowSize = (int)m_QueueSize.Element.GetValue(); + CRConfig.Instance.QueueCommandCooldown = (int)m_QueueCooldown.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset settings + /// + internal void RefreshSettings() + { + m_PreventChanges = true; + + m_UserRequest .SetValue(CRConfig.Instance.UserMaxRequest); + m_VIPBonusRequest .SetValue(CRConfig.Instance.VIPBonusRequest); + m_SubscriberBonusRequest.SetValue(CRConfig.Instance.SubscriberBonusRequest); + m_HistorySize .SetValue(CRConfig.Instance.HistorySize); + + m_PlayPreviewMusic .SetValue(CRConfig.Instance.PlayPreviewMusic); + m_ModeratorPower .SetValue(CRConfig.Instance.ModeratorPower); + m_QueueSize .SetValue(CRConfig.Instance.QueueCommandShowSize); + m_QueueCooldown .SetValue(CRConfig.Instance.QueueCommandCooldown); + + m_PreventChanges = false; + } + } +} diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsRight.bsml b/Modules/BeatSaberPlus_ChatRequest/UI/SettingsRight.bsml deleted file mode 100644 index aeb091c..0000000 --- a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsRight.bsml +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsRight.cs b/Modules/BeatSaberPlus_ChatRequest/UI/SettingsRight.cs deleted file mode 100644 index 4cda4b2..0000000 --- a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsRight.cs +++ /dev/null @@ -1,186 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using UnityEngine; - -namespace BeatSaberPlus_ChatRequest.UI -{ - /// - /// Chat request settings filters right screen - /// - internal class SettingsRight : BeatSaberPlus.SDK.UI.ResourceViewController - { -#pragma warning disable CS0649 - [UIComponent("nobeatsage-toggle")] - private ToggleSetting m_NoBeatSageToggle; - [UIComponent("npsmin-toggle")] - private ToggleSetting m_NPSMinToggle; - [UIComponent("npsmax-toggle")] - private ToggleSetting m_NPSMaxToggle; - [UIComponent("njsmin-toggle")] - private ToggleSetting m_NJSMinToggle; - [UIComponent("njsmax-toggle")] - private ToggleSetting m_NJSMaxToggle; - [UIComponent("durationmax-toggle")] - private ToggleSetting m_DurationMaxToggle; - [UIComponent("votemin-toggle")] - private ToggleSetting m_VoteMinToggle; - [UIComponent("datemin-toggle")] - private ToggleSetting m_DateMinToggle; - [UIComponent("datemax-toggle")] - private ToggleSetting m_DateMaxToggle; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("npsmin-slider")] - private SliderSetting m_NPSMin; - [UIComponent("npsmax-slider")] - private SliderSetting m_NPSMax; - [UIComponent("njsmin-slider")] - private SliderSetting m_NJSMin; - [UIComponent("njsmax-slider")] - private SliderSetting m_NJSMax; - [UIComponent("durationmax-slider")] - private SliderSetting m_DurationMax; - [UIComponent("votemin-slider")] - private SliderSetting m_VoteMin; - [UIComponent("datemin-slider")] - private SliderSetting m_DateMin; - [UIComponent("datemax-slider")] - private SliderSetting m_DateMax; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - var l_NewRectMin = new Vector2(0.10f, -0.05f); - var l_NewRectMax = new Vector2(0.92f, 1.05f); - - /// Left - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_NoBeatSageToggle, l_Event, CRConfig.Instance.Filters.NoBeatSage, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_NPSMinToggle, l_Event, CRConfig.Instance.Filters.NPSMin, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_NPSMaxToggle, l_Event, CRConfig.Instance.Filters.NPSMax, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_NJSMinToggle, l_Event, CRConfig.Instance.Filters.NJSMin, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_NJSMaxToggle, l_Event, CRConfig.Instance.Filters.NJSMax, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_DurationMaxToggle, l_Event, CRConfig.Instance.Filters.DurationMax, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_VoteMinToggle, l_Event, CRConfig.Instance.Filters.VoteMin, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_DateMinToggle, l_Event, CRConfig.Instance.Filters.DateMin, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_DateMaxToggle, l_Event, CRConfig.Instance.Filters.DateMax, true); - - /// Right - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_NPSMin, l_Event, null, CRConfig.Instance.Filters.NPSMinV, true, true, l_NewRectMin, l_NewRectMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_NPSMax, l_Event, null, CRConfig.Instance.Filters.NPSMaxV, true, true, l_NewRectMin, l_NewRectMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_NJSMin, l_Event, null, CRConfig.Instance.Filters.NJSMinV, true, true, l_NewRectMin, l_NewRectMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_NJSMax, l_Event, null, CRConfig.Instance.Filters.NJSMaxV, true, true, l_NewRectMin, l_NewRectMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_DurationMax, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Minutes, CRConfig.Instance.Filters.DurationMaxV, true, true, l_NewRectMin, l_NewRectMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_VoteMin, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, CRConfig.Instance.Filters.VoteMinV, true, true, l_NewRectMin, l_NewRectMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_DateMin, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.DateMonthFrom2018, CRConfig.Instance.Filters.DateMinV, true, true, l_NewRectMin, l_NewRectMax); - BeatSaberPlus.SDK.UI.SliderSetting.Setup(m_DateMax, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.DateMonthFrom2018, CRConfig.Instance.Filters.DateMaxV, true, true, l_NewRectMin, l_NewRectMax); - - /// Update interactable - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_NPSMin, m_NPSMinToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_NPSMax, m_NPSMaxToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_NJSMin, m_NJSMinToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_NJSMax, m_NJSMaxToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_DurationMax, m_DurationMaxToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_VoteMin, m_VoteMinToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_DateMin, m_DateMinToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_DateMax, m_DateMaxToggle.Value); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When settings are changed - /// - /// - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - /// Update interactable - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_NPSMin, m_NPSMinToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_NPSMax, m_NPSMaxToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_NJSMin, m_NJSMinToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_NJSMax, m_NJSMaxToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_DurationMax, m_DurationMaxToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_VoteMin, m_VoteMinToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_DateMin, m_DateMinToggle.Value); - BeatSaberPlus.SDK.UI.SliderSetting.SetInteractable(m_DateMax, m_DateMaxToggle.Value); - - /// Left - CRConfig.Instance.Filters.NoBeatSage = m_NoBeatSageToggle.Value; - CRConfig.Instance.Filters.NPSMin = m_NPSMinToggle.Value; - CRConfig.Instance.Filters.NPSMax = m_NPSMaxToggle.Value; - CRConfig.Instance.Filters.NJSMin = m_NJSMinToggle.Value; - CRConfig.Instance.Filters.NJSMax = m_NJSMaxToggle.Value; - CRConfig.Instance.Filters.DurationMax = m_DurationMaxToggle.Value; - CRConfig.Instance.Filters.VoteMin = m_VoteMinToggle.Value; - CRConfig.Instance.Filters.DateMin = m_DateMinToggle.Value; - CRConfig.Instance.Filters.DateMax = m_DateMaxToggle.Value; - - /// Right - CRConfig.Instance.Filters.NPSMinV = (int)m_NPSMin.slider.value; - CRConfig.Instance.Filters.NPSMaxV = (int)m_NPSMax.slider.value; - CRConfig.Instance.Filters.NJSMinV = (int)m_NJSMin.slider.value; - CRConfig.Instance.Filters.NJSMaxV = (int)m_NJSMax.slider.value; - CRConfig.Instance.Filters.DurationMaxV = (int)m_DurationMax.slider.value; - CRConfig.Instance.Filters.VoteMinV = m_VoteMin.slider.value; - CRConfig.Instance.Filters.DateMinV = (int)m_DateMin.slider.value; - CRConfig.Instance.Filters.DateMaxV = (int)m_DateMax.slider.value; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset settings - /// - internal void RefreshSettings() - { - m_PreventChanges = true; - - /// Left - m_NoBeatSageToggle.Value = CRConfig.Instance.Filters.NoBeatSage; - m_NPSMinToggle.Value = CRConfig.Instance.Filters.NPSMin; - m_NPSMaxToggle.Value = CRConfig.Instance.Filters.NPSMax; - m_NJSMinToggle.Value = CRConfig.Instance.Filters.NJSMin; - m_NJSMaxToggle.Value = CRConfig.Instance.Filters.NJSMax; - m_DurationMaxToggle.Value = CRConfig.Instance.Filters.DurationMax; - m_VoteMinToggle.Value = CRConfig.Instance.Filters.VoteMin; - m_DateMinToggle.Value = CRConfig.Instance.Filters.DateMin; - m_DateMaxToggle.Value = CRConfig.Instance.Filters.DateMax; - - /// Right - m_NPSMin.slider.value = CRConfig.Instance.Filters.NPSMinV; - m_NPSMax.slider.value = CRConfig.Instance.Filters.NPSMaxV; - m_NJSMin.slider.value = CRConfig.Instance.Filters.NJSMinV; - m_NJSMax.slider.value = CRConfig.Instance.Filters.NJSMaxV; - m_DurationMax.slider.value = CRConfig.Instance.Filters.DurationMaxV; - m_VoteMin.slider.value = CRConfig.Instance.Filters.VoteMinV; - m_DateMin.slider.value = CRConfig.Instance.Filters.DateMinV; - m_DateMax.slider.value = CRConfig.Instance.Filters.DateMaxV; - - m_PreventChanges = false; - - /// Update sliders - OnSettingChanged(null); - } - } -} diff --git a/Modules/BeatSaberPlus_ChatRequest/UI/SettingsRightView.cs b/Modules/BeatSaberPlus_ChatRequest/UI/SettingsRightView.cs new file mode 100644 index 0000000..e2fcdc6 --- /dev/null +++ b/Modules/BeatSaberPlus_ChatRequest/UI/SettingsRightView.cs @@ -0,0 +1,213 @@ +using CP_SDK.XUI; + +namespace BeatSaberPlus_ChatRequest.UI +{ + /// + /// Chat request settings filters right screen + /// + internal sealed class SettingsRightView : CP_SDK.UI.ViewController + { + private XUIToggle m_NoBeatSageToggle; + private XUIToggle m_NPSMinToggle; + private XUIToggle m_NPSMaxToggle; + private XUIToggle m_NJSMinToggle; + private XUIToggle m_NJSMaxToggle; + private XUIToggle m_DurationMaxToggle; + private XUIToggle m_VoteMinToggle; + private XUIToggle m_DateMinToggle; + private XUIToggle m_DateMaxToggle; + private XUISlider m_NPSMin; + private XUISlider m_NPSMax; + private XUISlider m_NJSMin; + private XUISlider m_NJSMax; + private XUISlider m_DurationMax; + private XUISlider m_VoteMin; + private XUISlider m_DateMin; + private XUISlider m_DateMax; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Should prevent changes + /// + private bool m_PreventChanges = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Filters"), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("No BeatSage"), + XUIText.Make("NPS min"), + XUIText.Make("NPS max"), + XUIText.Make("NJS min"), + XUIText.Make("NJS max"), + XUIText.Make("Duration max"), + XUIText.Make("Vote min"), + XUIText.Make("Upload date min"), + XUIText.Make("Upload date max") + ) + .SetSpacing(2) + .SetPadding(2) + .SetWidth(30) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .ForEachDirect(x => x.SetAlign(TMPro.TextAlignmentOptions.CaplineLeft)), + + XUIVLayout.Make( + XUIToggle.Make().SetValue(CRConfig.Instance.Filters.NoBeatSage ).Bind(ref m_NoBeatSageToggle ), + XUIToggle.Make().SetValue(CRConfig.Instance.Filters.NPSMin ).Bind(ref m_NPSMinToggle ), + XUIToggle.Make().SetValue(CRConfig.Instance.Filters.NPSMax ).Bind(ref m_NPSMaxToggle ), + XUIToggle.Make().SetValue(CRConfig.Instance.Filters.NJSMin ).Bind(ref m_NJSMinToggle ), + XUIToggle.Make().SetValue(CRConfig.Instance.Filters.NJSMax ).Bind(ref m_NJSMaxToggle ), + XUIToggle.Make().SetValue(CRConfig.Instance.Filters.DurationMax).Bind(ref m_DurationMaxToggle), + XUIToggle.Make().SetValue(CRConfig.Instance.Filters.VoteMin ).Bind(ref m_VoteMinToggle ), + XUIToggle.Make().SetValue(CRConfig.Instance.Filters.DateMin ).Bind(ref m_DateMinToggle ), + XUIToggle.Make().SetValue(CRConfig.Instance.Filters.DateMax ).Bind(ref m_DateMaxToggle ) + ) + .SetSpacing(2) + .SetPadding(2) + .SetWidth(20) + .ForEachDirect(x => x.OnValueChanged(_ => OnValueChanged())), + + XUIVLayout.Make( + XUIText.Make("Discard all BeatSage maps"), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue(100f).SetIncrements( 1f).SetInteger(true) + .SetValue(CRConfig.Instance.Filters.NPSMinV) + .Bind(ref m_NPSMin), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue(100f).SetIncrements( 1f).SetInteger(true) + .SetValue(CRConfig.Instance.Filters.NPSMaxV) + .Bind(ref m_NPSMax), + XUISlider.Make() + .SetMinValue(1f).SetMaxValue(100f).SetIncrements( 1f).SetInteger(true) + .SetValue(CRConfig.Instance.Filters.NJSMinV) + .Bind(ref m_NJSMin), + XUISlider.Make() + .SetMinValue(1f).SetMaxValue(100f).SetIncrements( 1f).SetInteger(true) + .SetValue(CRConfig.Instance.Filters.NJSMaxV) + .Bind(ref m_NJSMax), + XUISlider.Make() + .SetMinValue(1f).SetMaxValue( 60f).SetIncrements( 1f).SetInteger(true) + .SetValue(CRConfig.Instance.Filters.DurationMaxV) + .Bind(ref m_DurationMax), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue( 1f).SetIncrements(0.01f) + .SetValue(CRConfig.Instance.Filters.VoteMinV) + .Bind(ref m_VoteMin), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue(100f).SetIncrements( 1f).SetInteger(true) + .SetValue(CRConfig.Instance.Filters.DateMinV) + .Bind(ref m_DateMin), + XUISlider.Make() + .SetMinValue(0f).SetMaxValue(100f).SetIncrements( 1f).SetInteger(true) + .SetValue(CRConfig.Instance.Filters.DateMaxV) + .Bind(ref m_DateMax) + ) + .SetSpacing(2) + .SetPadding(2) + .SetWidth(65) + .ForEachDirect(x => x.OnValueChanged(_ => OnValueChanged())) + ) + .OnReady(x => x.CSizeFitter.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.Unconstrained) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + + m_DurationMax.SetFormatter(CP_SDK.UI.ValueFormatters.Minutes); + m_VoteMin.SetFormatter(CP_SDK.UI.ValueFormatters.Percentage); + m_DateMin.SetFormatter(CP_SDK.UI.ValueFormatters.DateMonthFrom2018Short); + m_DateMax.SetFormatter(CP_SDK.UI.ValueFormatters.DateMonthFrom2018Short); + + OnValueChanged(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When settings are changed + /// + private void OnValueChanged() + { + if (m_PreventChanges) + return; + + /// Update interactable + m_NPSMin.SetInteractable( m_NPSMinToggle.Element.GetValue()); + m_NPSMax.SetInteractable( m_NPSMaxToggle.Element.GetValue()); + m_NJSMin.SetInteractable( m_NJSMinToggle.Element.GetValue()); + m_NJSMax.SetInteractable( m_NJSMaxToggle.Element.GetValue()); + m_DurationMax.SetInteractable( m_DurationMaxToggle.Element.GetValue()); + m_VoteMin.SetInteractable( m_VoteMinToggle.Element.GetValue()); + m_DateMin.SetInteractable( m_DateMinToggle.Element.GetValue()); + m_DateMax.SetInteractable( m_DateMaxToggle.Element.GetValue()); + + /// Left + CRConfig.Instance.Filters.NoBeatSage = m_NoBeatSageToggle.Element.GetValue(); + CRConfig.Instance.Filters.NPSMin = m_NPSMinToggle.Element.GetValue(); + CRConfig.Instance.Filters.NPSMax = m_NPSMaxToggle.Element.GetValue(); + CRConfig.Instance.Filters.NJSMin = m_NJSMinToggle.Element.GetValue(); + CRConfig.Instance.Filters.NJSMax = m_NJSMaxToggle.Element.GetValue(); + CRConfig.Instance.Filters.DurationMax = m_DurationMaxToggle.Element.GetValue(); + CRConfig.Instance.Filters.VoteMin = m_VoteMinToggle.Element.GetValue(); + CRConfig.Instance.Filters.DateMin = m_DateMinToggle.Element.GetValue(); + CRConfig.Instance.Filters.DateMax = m_DateMaxToggle.Element.GetValue(); + + /// Right + CRConfig.Instance.Filters.NPSMinV = (int)m_NPSMin.Element.GetValue(); + CRConfig.Instance.Filters.NPSMaxV = (int)m_NPSMax.Element.GetValue(); + CRConfig.Instance.Filters.NJSMinV = (int)m_NJSMin.Element.GetValue(); + CRConfig.Instance.Filters.NJSMaxV = (int)m_NJSMax.Element.GetValue(); + CRConfig.Instance.Filters.DurationMaxV = (int)m_DurationMax.Element.GetValue(); + CRConfig.Instance.Filters.VoteMinV = m_VoteMin.Element.GetValue(); + CRConfig.Instance.Filters.DateMinV = (int)m_DateMin.Element.GetValue(); + CRConfig.Instance.Filters.DateMaxV = (int)m_DateMax.Element.GetValue(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset settings + /// + internal void RefreshSettings() + { + m_PreventChanges = true; + + /// Left + m_NoBeatSageToggle.SetValue( CRConfig.Instance.Filters.NoBeatSage); + m_NPSMinToggle.SetValue( CRConfig.Instance.Filters.NPSMin); + m_NPSMaxToggle.SetValue( CRConfig.Instance.Filters.NPSMax); + m_NJSMinToggle.SetValue( CRConfig.Instance.Filters.NJSMin); + m_NJSMaxToggle.SetValue( CRConfig.Instance.Filters.NJSMax); + m_DurationMaxToggle.SetValue( CRConfig.Instance.Filters.DurationMax); + m_VoteMinToggle.SetValue( CRConfig.Instance.Filters.VoteMin); + m_DateMinToggle.SetValue( CRConfig.Instance.Filters.DateMin); + m_DateMaxToggle.SetValue( CRConfig.Instance.Filters.DateMax); + + /// Right + m_NPSMin.SetValue( CRConfig.Instance.Filters.NPSMinV); + m_NPSMax.SetValue( CRConfig.Instance.Filters.NPSMaxV); + m_NJSMin.SetValue( CRConfig.Instance.Filters.NJSMinV); + m_NJSMax.SetValue( CRConfig.Instance.Filters.NJSMaxV); + m_DurationMax.SetValue( CRConfig.Instance.Filters.DurationMaxV); + m_VoteMin.SetValue( CRConfig.Instance.Filters.VoteMinV); + m_DateMin.SetValue( CRConfig.Instance.Filters.DateMinV); + m_DateMax.SetValue( CRConfig.Instance.Filters.DateMaxV); + + m_PreventChanges = false; + + OnValueChanged(); + } + } +} diff --git a/Modules/BeatSaberPlus_ChatRequest/manifest.json b/Modules/BeatSaberPlus_ChatRequest/manifest.json index 80e1722..2aa599e 100644 --- a/Modules/BeatSaberPlus_ChatRequest/manifest.json +++ b/Modules/BeatSaberPlus_ChatRequest/manifest.json @@ -3,13 +3,12 @@ "id": "BeatSaberPlus_ChatRequest", "name": "BeatSaberPlus_ChatRequest", "author": "HardCPP#1985", - "version": "5.0.7", + "version": "6.0.8", "description": "", - "gameVersion": "1.25.0", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.0.1", - "BeatSaberMarkupLanguage": "^1.3.4", - "BeatSaberPlusCORE": "^5.0.7" + "BSIPA": "^4.3.0", + "BeatSaberPlusCORE": "^6.0.8" }, "links": { "project-home": "https://discord.chatplex.org", diff --git a/Modules/BeatSaberPlus_GameTweaker/BeatSaberPlus_GameTweaker.csproj b/Modules/BeatSaberPlus_GameTweaker/BeatSaberPlus_GameTweaker.csproj index fb3d103..93f7cdf 100644 --- a/Modules/BeatSaberPlus_GameTweaker/BeatSaberPlus_GameTweaker.csproj +++ b/Modules/BeatSaberPlus_GameTweaker/BeatSaberPlus_GameTweaker.csproj @@ -46,6 +46,12 @@ OnBuildSuccess + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + $(BeatSaberDir)\Libs\0Harmony.dll @@ -57,17 +63,12 @@ False False - - $(BeatSaberDir)\Plugins\BSML.dll - False - False - $(BeatSaberDir)\Beat Saber_Data\Managed\Colors.dll False False - + $(BeatSaberDir)\Beat Saber_Data\Managed\Core.dll False False @@ -77,12 +78,12 @@ False False - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMLibAttributes.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll False False - + $(BeatSaberDir)\Beat Saber_Data\Managed\HMRendering.dll False False @@ -97,26 +98,12 @@ False False - - $(BeatSaberDir)\Plugins\SongCore.dll - False - False - - - - - - - + $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll - False - - + $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll False @@ -150,23 +137,10 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll - False - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll - False - False - @@ -195,20 +169,14 @@ - - + + - - Settings.cs - - - SettingsLeft.cs - diff --git a/Modules/BeatSaberPlus_GameTweaker/BeatSaberPlus_GameTweaker.csproj.user b/Modules/BeatSaberPlus_GameTweaker/BeatSaberPlus_GameTweaker.csproj.user index 5db8d9ee60929239d9779dc6a24adfdc0c01779f..012781eeb9f58b5f0dd22267773d93a2d711944f 100644 GIT binary patch delta 25 fcmaFI@`q)^I!0av215ot1|tSbAZa*xE#pA|UV#Rx delta 11 Tcmeyv@{VQ0I>yO+7!LpdA+QBv diff --git a/Modules/BeatSaberPlus_GameTweaker/Components/FPFCEscape.cs b/Modules/BeatSaberPlus_GameTweaker/Components/FPFCEscape.cs index 280d307..7bafe3c 100644 --- a/Modules/BeatSaberPlus_GameTweaker/Components/FPFCEscape.cs +++ b/Modules/BeatSaberPlus_GameTweaker/Components/FPFCEscape.cs @@ -1,9 +1,7 @@ -using IPA.Utilities; -using Polyglot; +using Polyglot; using System.Linq; using TMPro; using UnityEngine; -using UnityEngine.UI; namespace BeatSaberPlus_GameTweaker.Components { @@ -58,7 +56,7 @@ private PauseController m_PauseController private void Update() { /// Don't activate in menu - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Playing + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing || BeatSaberPlus.SDK.Game.Logic.LevelData?.Type == BeatSaberPlus.SDK.Game.LevelType.Multiplayer) return; @@ -72,9 +70,9 @@ private void Update() if (m_FPFCPause && !m_PauseMenuManager.enabled) { /// Enable localized - m_PauseMenuManager.GetField("_backButton").transform.GetChild(2).GetComponentInChildren().enabled = true; - m_PauseMenuManager.GetField("_restartButton").transform.GetChild(2).GetComponentInChildren().enabled = true; - m_PauseMenuManager.GetField("_continueButton").transform.GetChild(2).GetComponentInChildren().enabled = true; + m_PauseMenuManager._backButton.transform.GetChild(2).GetComponentInChildren().enabled = true; + m_PauseMenuManager._restartButton.transform.GetChild(2).GetComponentInChildren().enabled = true; + m_PauseMenuManager._continueButton.transform.GetChild(2).GetComponentInChildren().enabled = true; m_FPFCPause = false; } @@ -112,14 +110,14 @@ private void Update() if (l_ShouldRestore) { /// Enable localized - m_PauseMenuManager.GetField("_backButton").transform.GetChild(2).GetComponentInChildren().enabled = true; - m_PauseMenuManager.GetField("_restartButton").transform.GetChild(2).GetComponentInChildren().enabled = true; - m_PauseMenuManager.GetField("_continueButton").transform.GetChild(2).GetComponentInChildren().enabled = true; + m_PauseMenuManager._backButton.transform.GetChild(2).GetComponentInChildren().enabled = true; + m_PauseMenuManager._restartButton.transform.GetChild(2).GetComponentInChildren().enabled = true; + m_PauseMenuManager._continueButton.transform.GetChild(2).GetComponentInChildren().enabled = true; m_FPFCPause = false; } else - m_PauseMenuManager.GetField("_backButton").transform.GetChild(2).GetComponentInChildren().text = m_BackButtonText; + m_PauseMenuManager._backButton.transform.GetChild(2).GetComponentInChildren().text = m_BackButtonText; } /// Should pause else if (!m_PauseMenuManager.enabled && Input.GetKeyDown(KeyCode.Escape)) @@ -129,14 +127,14 @@ private void Update() m_PauseMenuManager.ShowMenu(); /// Disable localized - m_PauseMenuManager.GetField("_backButton").transform.GetChild(2).GetComponentInChildren().enabled = false; - m_PauseMenuManager.GetField("_restartButton").transform.GetChild(2).GetComponentInChildren().enabled = false; - m_PauseMenuManager.GetField("_continueButton").transform.GetChild(2).GetComponentInChildren().enabled = false; + m_PauseMenuManager._backButton.transform.GetChild(2).GetComponentInChildren().enabled = false; + m_PauseMenuManager._restartButton.transform.GetChild(2).GetComponentInChildren().enabled = false; + m_PauseMenuManager._continueButton.transform.GetChild(2).GetComponentInChildren().enabled = false; /// Get buttons - var l_BackButton = m_PauseMenuManager.GetField("_backButton").transform.GetChild(2).GetComponentInChildren(); - var l_RestartButton = m_PauseMenuManager.GetField("_restartButton").transform.GetChild(2).GetComponentInChildren(); - var l_ContinueButton = m_PauseMenuManager.GetField("_continueButton").transform.GetChild(2).GetComponentInChildren(); + var l_BackButton = m_PauseMenuManager._backButton.transform.GetChild(2).GetComponentInChildren(); + var l_RestartButton = m_PauseMenuManager._restartButton.transform.GetChild(2).GetComponentInChildren(); + var l_ContinueButton = m_PauseMenuManager._continueButton.transform.GetChild(2).GetComponentInChildren(); /// Enable rich text l_BackButton.richText = true; diff --git a/Modules/BeatSaberPlus_GameTweaker/Components/MusicBandLogoRemover.cs b/Modules/BeatSaberPlus_GameTweaker/Components/MusicBandLogoRemover.cs index a4a7de6..43dcca6 100644 --- a/Modules/BeatSaberPlus_GameTweaker/Components/MusicBandLogoRemover.cs +++ b/Modules/BeatSaberPlus_GameTweaker/Components/MusicBandLogoRemover.cs @@ -35,7 +35,7 @@ public void Update() if (m_AudioTimeSyncController == null || !m_AudioTimeSyncController) m_AudioTimeSyncController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Playing + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing || m_AudioTimeSyncController == null || !m_AudioTimeSyncController) { @@ -48,13 +48,6 @@ public void Update() GameObject l_Object = null; - /// - ///l_Object = Resources.FindObjectsOfTypeAll().FirstOrDefault(x => x.name == "EnergyPanel"); - ///if (l_Object != null && l_Object) - /// l_Object.SetActive(true); - ///l_Object = null; - /// - /// BTS l_Object = Resources.FindObjectsOfTypeAll().FirstOrDefault(x => x.name == "MagicDoorSprite"); if (l_Object != null) diff --git a/Modules/BeatSaberPlus_GameTweaker/GTConfig.cs b/Modules/BeatSaberPlus_GameTweaker/GTConfig.cs index 29f09ab..b39eaaa 100644 --- a/Modules/BeatSaberPlus_GameTweaker/GTConfig.cs +++ b/Modules/BeatSaberPlus_GameTweaker/GTConfig.cs @@ -7,66 +7,59 @@ internal class GTConfig : CP_SDK.Config.JsonConfig { internal class _Environment { - [JsonProperty] internal bool RemoveMusicBandLogo = false; - [JsonProperty] internal bool RemoveFullComboLossAnimation = false; - [JsonProperty] internal bool NoFake360HUD = true; + [JsonProperty] internal bool RemoveMusicBandLogo = false; + [JsonProperty] internal bool RemoveFullComboLossAnimation = false; + [JsonProperty] internal bool NoFake360HUD = true; } internal class _LevelSelection { - [JsonProperty] internal bool RemoveBaseGameFilterButton = false; - [JsonProperty] internal bool DeleteSongButton = true; - [JsonProperty] internal bool DeleteSongBrowserTrashcan = true; - [JsonProperty] internal bool HighlightEnabled = true; - [JsonProperty] internal Color32 HighlightPlayed = new Color32(248,230,0,255); - [JsonProperty] internal Color32 HighlightAllPlayed = new Color32(82,247,0,255); + [JsonProperty] internal bool RemoveBaseGameFilterButton = false; + [JsonProperty] internal bool DeleteSongButton = true; + [JsonProperty] internal bool DeleteSongBrowserTrashcan = true; + [JsonProperty] internal bool HighlightEnabled = true; + [JsonProperty] internal Color32 HighlightPlayed = new Color32(248, 230, 0, 255); + [JsonProperty] internal Color32 HighlightAllPlayed = new Color32( 82, 247, 0, 255); } internal class _PlayerOptions { - [JsonProperty] internal int JumpDurationIncrement = 1; - [JsonProperty] internal bool ReorderPlayerSettings = true; - [JsonProperty] internal bool MergeLightPressetOptions = true; - [JsonProperty] internal bool OverrideLightIntensityOption = true; - [JsonProperty] internal float OverrideLightIntensity = 1.0f; + [JsonProperty] internal bool ReorderPlayerSettings = true; + [JsonProperty] internal bool MergeLightPressetOptions = true; + [JsonProperty] internal bool OverrideLightIntensityOption = true; + [JsonProperty] internal float OverrideLightIntensity = 1.0f; } internal class _MainMenu { - [JsonProperty] internal bool OverrideMenuEnvColors = false; - [JsonProperty] internal Color BaseColor = new Color(0.421376616f, 0.201642916f, 0.6745098f, 1f); - [JsonProperty] internal Color LevelClearedColor = new Color(0.203647852f, 0.479708f, 0.07326582f, 1f); - [JsonProperty] internal Color LevelFailedColor = new Color(0.796078444f, 0.137425855f, 0.0f, 1f); - [JsonProperty] internal bool DisableEditorButtonOnMainMenu = true; - [JsonProperty] internal bool RemoveNewContentPromotional = true; - [JsonProperty] internal bool DisableFireworks = false; + [JsonProperty] internal bool OverrideMenuEnvColors = false; + [JsonProperty] internal Color BaseColor = new Color(0.421376616f, 0.201642916f, 0.67450980f, 1f); + [JsonProperty] internal Color LevelClearedColor = new Color(0.203647852f, 0.479708000f, 0.07326582f, 1f); + [JsonProperty] internal Color LevelFailedColor = new Color(0.796078444f, 0.137425855f, 0.00000000f, 1f); + [JsonProperty] internal bool DisableEditorButtonOnMainMenu = true; + [JsonProperty] internal bool RemoveNewContentPromotional = true; + [JsonProperty] internal bool DisableFireworks = false; } internal class _Tools { - [JsonProperty] internal bool RemoveOldLogs = true; - [JsonProperty] internal int LogEntriesToKeep = 8; - [JsonProperty] internal bool FPFCEscape = false; + [JsonProperty] internal bool RemoveOldLogs = true; + [JsonProperty] internal int LogEntriesToKeep = 8; + [JsonProperty] internal bool FPFCEscape = false; } [JsonProperty] internal bool Enabled = false; /// Gameplay - [JsonProperty] internal bool RemoveDebris = false; - [JsonProperty] internal bool RemoveAllCutParticles = false; - [JsonProperty] internal bool RemoveObstacleParticles = false; - [JsonProperty] internal bool RemoveSaberBurnMarks = false; - [JsonProperty] internal bool RemoveSaberBurnMarkSparkles = false; - [JsonProperty] internal bool RemoveSaberClashEffects = false; - [JsonProperty] internal bool RemoveWorldParticles = false; + [JsonProperty] internal bool RemoveDebris = false; + [JsonProperty] internal bool RemoveAllCutParticles = false; + [JsonProperty] internal bool RemoveObstacleParticles = false; + [JsonProperty] internal bool RemoveSaberBurnMarks = false; + [JsonProperty] internal bool RemoveSaberBurnMarkSparkles = false; + [JsonProperty] internal bool RemoveSaberClashEffects = false; + [JsonProperty] internal bool RemoveWorldParticles = false; - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - internal _Environment Environment = new _Environment(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - internal _LevelSelection LevelSelection = new _LevelSelection(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - internal _PlayerOptions PlayerOptions = new _PlayerOptions(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - internal _MainMenu MainMenu = new _MainMenu(); - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - internal _Tools Tools = new _Tools(); + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] internal _Environment Environment = new _Environment(); + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] internal _LevelSelection LevelSelection = new _LevelSelection(); + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] internal _PlayerOptions PlayerOptions = new _PlayerOptions(); + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] internal _MainMenu MainMenu = new _MainMenu(); + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] internal _Tools Tools = new _Tools(); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_GameTweaker/GameTweaker.cs b/Modules/BeatSaberPlus_GameTweaker/GameTweaker.cs index e158dab..ae9229c 100644 --- a/Modules/BeatSaberPlus_GameTweaker/GameTweaker.cs +++ b/Modules/BeatSaberPlus_GameTweaker/GameTweaker.cs @@ -1,5 +1,4 @@ -using BeatSaberMarkupLanguage; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -10,47 +9,22 @@ namespace BeatSaberPlus_GameTweaker /// /// Game Tweaker instance /// - internal class GameTweaker : BeatSaberPlus.SDK.BSPModuleBase + internal class GameTweaker : CP_SDK.ModuleBase { - /// - /// Module type - /// - public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; - /// - /// Name of the Module - /// - public override string Name => "Game Tweaker"; - /// - /// Description of the Module - /// - public override string Description => "Customize your game play & menu experience!"; - /// - /// Is the Module using chat features - /// - public override bool UseChatFeatures => false; - /// - /// Is enabled - /// - public override bool IsEnabled { get => GTConfig.Instance.Enabled; set { GTConfig.Instance.Enabled = value; GTConfig.Instance.Save(); } } - /// - /// Activation kind - /// - public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; + public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; + public override string Name => "Game Tweaker"; + public override string Description => "Customize your game play & menu experience!"; + public override string DocumentationURL => "https://github.com/hardcpp/BeatSaberPlus/wiki#game-tweaker"; + public override bool UseChatFeatures => false; + public override bool IsEnabled { get => GTConfig.Instance.Enabled; set { GTConfig.Instance.Enabled = value; GTConfig.Instance.Save(); } } + public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Settings view - /// - private UI.Settings m_SettingsView = null; - /// - /// Settings left view - /// - private UI.SettingsLeft m_SettingsLeftView = null; - /// - /// FPFC escape object - /// + private UI.SettingsMainView m_SettingsMainView = null; + private UI.SettingsLeftView m_SettingsLeftView = null; + private Components.FPFCEscape m_FPFCEscape = null; //////////////////////////////////////////////////////////////////////////// @@ -78,6 +52,9 @@ protected override void OnDisable() /// Update patches UpdatePatches(true); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsLeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsMainView); + /// Unbind event BeatSaberPlus.SDK.Game.Logic.OnLevelStarted -= Game_OnLevelStarted; BeatSaberPlus.SDK.Game.Logic.OnSceneChange -= Game_OnSceneChange; @@ -89,17 +66,13 @@ protected override void OnDisable() /// /// Get Module settings UI /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsLeftView == null) - m_SettingsLeftView = BeatSaberUI.CreateViewController(); + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsLeftView == null) m_SettingsLeftView = CP_SDK.UI.UISystem.CreateViewController(); /// Change main view - return (m_SettingsView, m_SettingsLeftView, null); + return (m_SettingsMainView, m_SettingsLeftView, null); } //////////////////////////////////////////////////////////////////////////// @@ -264,9 +237,9 @@ private int CleanLogsInFolder(string p_Directory, int p_EntriesToKeep) /// On game scene change /// /// New scene - private void Game_OnSceneChange(BeatSaberPlus.SDK.Game.Logic.SceneType p_Scene) + private void Game_OnSceneChange(BeatSaberPlus.SDK.Game.Logic.ESceneType p_Scene) { - Patches.Lights.PLightsPatches.SetIsValidScene(p_Scene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing); + Patches.Lights.PLightsPatches.SetIsValidScene(p_Scene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing); UpdateWorldParticles(); } /// diff --git a/Modules/BeatSaberPlus_GameTweaker/Managers/CustomMenuLightManager.cs b/Modules/BeatSaberPlus_GameTweaker/Managers/CustomMenuLightManager.cs index fd82446..baec059 100644 --- a/Modules/BeatSaberPlus_GameTweaker/Managers/CustomMenuLightManager.cs +++ b/Modules/BeatSaberPlus_GameTweaker/Managers/CustomMenuLightManager.cs @@ -1,5 +1,4 @@ -using IPA.Utilities; -using System.Collections; +using System.Collections; using System.Linq; using UnityEngine; @@ -105,7 +104,7 @@ public static void UpdateFromConfig() { if (m_MenuLightsManager && m_DefaultPreset) { - m_MenuLightsManager.SetField("_preset", m_DefaultPreset); + m_MenuLightsManager._preset = m_DefaultPreset; m_MenuLightsManager.enabled = true; } } @@ -126,7 +125,7 @@ public static void SwitchToBase() { if (m_MenuLightsManager && m_DefaultPreset) { - m_MenuLightsManager.SetField("_preset", m_DefaultPreset); + m_MenuLightsManager._preset = m_DefaultPreset; m_MenuLightsManager.enabled = true; } } @@ -147,7 +146,7 @@ public static void SwitchToLevelCleared() { if (m_MenuLightsManager && m_LevelClearedPreset) { - m_MenuLightsManager.SetField("_preset", m_LevelClearedPreset); + m_MenuLightsManager._preset = m_LevelClearedPreset; m_MenuLightsManager.enabled = true; } } @@ -168,7 +167,7 @@ public static void SwitchToLevelFailed() { if (m_MenuLightsManager && m_LevelFailedPreset) { - m_MenuLightsManager.SetField("_preset", m_LevelFailedPreset); + m_MenuLightsManager._preset = m_LevelFailedPreset; m_MenuLightsManager.enabled = true; } } @@ -190,7 +189,7 @@ private static IEnumerator Coroutine_InitLate() yield return new WaitUntil(() => GameObject.FindObjectOfType()); m_MenuLightsManager = GameObject.FindObjectOfType(); - m_DefaultPreset = m_MenuLightsManager.GetField("_defaultPreset"); + m_DefaultPreset = m_MenuLightsManager._defaultPreset; m_DefaultPresetBackup = m_DefaultPreset.lightIdColorPairs.Select(x => (x.intensity, x.baseColor)).ToArray(); UpdateFromConfig(); @@ -198,8 +197,8 @@ private static IEnumerator Coroutine_InitLate() yield return new WaitUntil(() => GameObject.FindObjectOfType()); var l_SoloFreePlayFlowCoordinator = GameObject.FindObjectOfType(); - m_LevelClearedPreset = l_SoloFreePlayFlowCoordinator.GetField("_resultsClearedLightsPreset"); - m_LevelFailedPreset = l_SoloFreePlayFlowCoordinator.GetField("_resultsFailedLightsPreset"); + m_LevelClearedPreset = l_SoloFreePlayFlowCoordinator._resultsClearedLightsPreset; + m_LevelFailedPreset = l_SoloFreePlayFlowCoordinator._resultsFailedLightsPreset; m_LevelClearedPresetBackup = m_LevelClearedPreset.lightIdColorPairs.Select(x => (x.intensity, x.baseColor)).ToArray(); m_LevelFailedPresetBackup = m_LevelFailedPreset.lightIdColorPairs.Select(x => (x.intensity, x.baseColor)).ToArray(); diff --git a/Modules/BeatSaberPlus_GameTweaker/Patches/PLevelListTableCell.cs b/Modules/BeatSaberPlus_GameTweaker/Patches/PLevelListTableCell.cs index 3dc5623..7a5a7ec 100644 --- a/Modules/BeatSaberPlus_GameTweaker/Patches/PLevelListTableCell.cs +++ b/Modules/BeatSaberPlus_GameTweaker/Patches/PLevelListTableCell.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +using CP_SDK.Unity.Extensions; +using HarmonyLib; using TMPro; using UnityEngine; @@ -23,9 +24,9 @@ internal static void Postfix(IPreviewBeatmapLevel level, bool isFavorite, var l_ColorPrefix = ""; if (l_HaveAllScores) - l_ColorPrefix = "<#" + ColorUtility.ToHtmlStringRGB(GTConfig.Instance.LevelSelection.HighlightAllPlayed) + ">"; + l_ColorPrefix = "<" + ColorU.ToHexRGB(GTConfig.Instance.LevelSelection.HighlightAllPlayed) + ">"; else if (l_HaveAnyScore) - l_ColorPrefix = "<#" + ColorUtility.ToHtmlStringRGB(GTConfig.Instance.LevelSelection.HighlightPlayed) + ">"; + l_ColorPrefix = "<" + ColorU.ToHexRGB(GTConfig.Instance.LevelSelection.HighlightPlayed) + ">"; ____songNameText.text = l_ColorPrefix + ____songNameText.text; } diff --git a/Modules/BeatSaberPlus_GameTweaker/Patches/PPlayerSettingsPanelController.cs b/Modules/BeatSaberPlus_GameTweaker/Patches/PPlayerSettingsPanelController.cs index d16bd41..265ffeb 100644 --- a/Modules/BeatSaberPlus_GameTweaker/Patches/PPlayerSettingsPanelController.cs +++ b/Modules/BeatSaberPlus_GameTweaker/Patches/PPlayerSettingsPanelController.cs @@ -1,8 +1,8 @@ -using BeatSaberMarkupLanguage.Components.Settings; +using CP_SDK.Unity.Extensions; using HarmonyLib; +using Polyglot; using System; using System.Collections.Generic; -using System.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -31,7 +31,9 @@ public class PPlayerSettingsPanelController : PlayerSettingsPanelController private static FormattedFloatListSettingsController m_NoteJumpFixedDurationSettingsController = null; private static NoteJumpStartBeatOffsetDropdown m_NoteJumpStartBeatOffsetDropdown = null; - private static IncrementSetting m_OverrideLightsIntensityToggle = null; + private static GameObject m_OverrideLightsIntensitySetting = null; + + private static CP_SDK.UI.Components.CSlider m_CustomReactionTime; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -76,29 +78,34 @@ internal static void SetLayout_Prefix( ref Toggle try { - /* - [ERROR @ 06:07:26 | BeatSaberPlus_GameTweaker] 0,2 - [ERROR @ 06:07:26 | BeatSaberPlus_GameTweaker] 0,3 - [ERROR @ 06:07:26 | BeatSaberPlus_GameTweaker] 0,4 - [ERROR @ 06:07:26 | BeatSaberPlus_GameTweaker] 0,5 - [ERROR @ 06:07:26 | BeatSaberPlus_GameTweaker] 0,6 - [ERROR @ 06:07:26 | BeatSaberPlus_GameTweaker] 0,7 - [ERROR @ 06:07:26 | BeatSaberPlus_GameTweaker] 0,8 - [ERROR @ 06:07:26 | BeatSaberPlus_GameTweaker] 0,9 - [ERROR @ 06:07:26 | BeatSaberPlus_GameTweaker] 1 - */ - - var l_NewReactionTimeList = new List(); - var l_IncrementType = GTConfig.Instance.PlayerOptions.JumpDurationIncrement; - var l_Increment = l_IncrementType == 2 ? 0.100f : (l_IncrementType == 1 ? 0.010f : 0.005f); + if (!m_CustomReactionTime) + { + var l_NewReactionTimeList = new List(); + for (float l_Value = 0.200f; l_Value <= 1.000f; l_Value += 0.001f) + l_NewReactionTimeList.Add(l_Value); - for (float l_Value = (l_IncrementType == 2 ? 0.2f : 0.24f); l_Value < 1f; l_Value += l_Increment) - l_NewReactionTimeList.Add(l_Value); + if (m_NoteJumpFixedDurationSettingsController) + { + m_NoteJumpFixedDurationSettingsController.values = l_NewReactionTimeList.ToArray(); + m_NoteJumpFixedDurationSettingsController.GetInitValues(out var _, out var __); + m_NoteJumpFixedDurationSettingsController.transform.GetChild(2).transform.localScale = Vector3.zero; - if (m_NoteJumpFixedDurationSettingsController) - { - m_NoteJumpFixedDurationSettingsController.values = l_NewReactionTimeList.ToArray(); - typeof(FormattedFloatListSettingsController).GetMethod("GetInitValues", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).Invoke(m_NoteJumpFixedDurationSettingsController, new object[] { (int)0, (int)0 }); + m_CustomReactionTime = CP_SDK.UI.UISystem.SliderFactory.Create("", m_NoteJumpFixedDurationSettingsController.transform); + m_CustomReactionTime.RTransform.anchorMin = new Vector2(0.5f, 0.5f); + m_CustomReactionTime.RTransform.anchorMax = new Vector2(1.0f, 0.5f); + m_CustomReactionTime.RTransform.sizeDelta = new Vector2(0.0f, 5.0f); + m_CustomReactionTime.SetColor(ColorU.ToUnityColor("#404040")); + m_CustomReactionTime.SetMinValue(200); + m_CustomReactionTime.SetMaxValue(1000f); + m_CustomReactionTime.SetIncrements(1f); + m_CustomReactionTime.SetInteger(true); + m_CustomReactionTime.SetFormatter((x) => ((int)x).ToString() + "ms"); + m_CustomReactionTime.OnValueChanged((x) => + { + m_NoteJumpFixedDurationSettingsController.SetValue(m_NoteJumpFixedDurationSettingsController.values[((int)x) - 200], true); + }); + m_CustomReactionTime.SetValue((int)(m_NoteJumpFixedDurationSettingsController.value * 1000f), false); + } } } catch (System.Exception) @@ -135,17 +142,6 @@ internal static void Refresh_Postfix() } } } - /// - /// On NoteJumpFixedDurationSettingsController change - /// - [HarmonyPatch(typeof(FormattedFloatListSettingsController))] - [HarmonyPatch("TextForValue", new Type[] { typeof(int) })] - [HarmonyPostfix] - internal static void NoteJumpFixedDurationSettingsController_TextForValue_Postfix(FormattedFloatListSettingsController __instance, int idx, ref string __result) - { - if (__instance == m_NoteJumpFixedDurationSettingsController) - __result = string.Format("{0:0.000}", __instance.values[idx]).Replace(".", "").Replace(",", "").TrimStart('0') + "ms"; - } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -161,38 +157,38 @@ internal static void SetReorderEnabled(bool p_Enabled, bool p_AddOverrideLightsI m_EnvironmentEffectsFilterDefaultPresetDropdown.transform.parent.parent.GetComponent().enabled = true; - if (p_AddOverrideLightsIntensityOption && (m_OverrideLightsIntensityToggle == null || !m_OverrideLightsIntensityToggle)) + if (p_AddOverrideLightsIntensityOption && (m_OverrideLightsIntensitySetting == null || !m_OverrideLightsIntensitySetting)) { - var l_Creator = new BeatSaberMarkupLanguage.Tags.Settings.IncrementSettingTag(); - var l_Clone = l_Creator.CreateObject(m_EnvironmentEffectsFilterDefaultPresetDropdown.transform.parent.parent); - l_Clone.GetComponentInChildren().text = "Override lights intensity"; - (l_Clone.transform as RectTransform).offsetMax = new Vector2(90, (l_Clone.transform as RectTransform).offsetMax.y); - - if (m_EnvironmentEffectsFilterDefaultPresetDropdown.transform.parent.Find("Icon")) - { - var l_Icon = GameObject.Instantiate(m_EnvironmentEffectsFilterDefaultPresetDropdown.transform.parent.Find("Icon"), l_Clone.transform); - l_Icon.transform.SetAsFirstSibling(); - } - - var l_LabelTemplateRectTransform = m_EnvironmentEffectsFilterDefaultPresetDropdown.transform.parent.GetChild(1).transform as RectTransform; - (l_Clone.transform.GetChild(1).transform as RectTransform).offsetMin = l_LabelTemplateRectTransform.offsetMin; - (l_Clone.transform.GetChild(1).transform as RectTransform).offsetMax = l_LabelTemplateRectTransform.offsetMax; + m_OverrideLightsIntensitySetting = GameObject.Instantiate( + m_EnvironmentEffectsFilterDefaultPresetDropdown.transform.parent.gameObject, + m_EnvironmentEffectsFilterDefaultPresetDropdown.transform.parent.parent + ); - (l_Clone.transform.GetChild(2).transform as RectTransform).offsetMin = new Vector2(-36f, 0f); - (l_Clone.transform.GetChild(2).transform as RectTransform).offsetMax = Vector2.zero; + GameObject.DestroyImmediate(m_OverrideLightsIntensitySetting.transform.Find("SimpleTextDropDown").gameObject); - m_OverrideLightsIntensityToggle = l_Clone.GetComponent(); - m_OverrideLightsIntensityToggle.minValue = 0; - m_OverrideLightsIntensityToggle.maxValue = 20; - m_OverrideLightsIntensityToggle.increments = 0.1f; + m_OverrideLightsIntensitySetting.GetComponentInChildren().enabled = false; + m_OverrideLightsIntensitySetting.GetComponentInChildren().text = "Override lights intensity"; - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(null, typeof(PPlayerSettingsPanelController).GetMethod(nameof(OnOverrideLightIntensityChange), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_OverrideLightsIntensityToggle, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, GTConfig.Instance.PlayerOptions.OverrideLightIntensity, false); + var l_Slider = CP_SDK.UI.UISystem.SliderFactory.Create("", m_OverrideLightsIntensitySetting.transform); + l_Slider.RTransform.anchorMin = new Vector2(0.5f, 0.5f); + l_Slider.RTransform.anchorMax = new Vector2(1.0f, 0.5f); + l_Slider.RTransform.sizeDelta = new Vector2(0.0f, 5.0f); + l_Slider.SetColor(ColorU.ToUnityColor("#404040")); + l_Slider.SetMinValue(0.0f); + l_Slider.SetMaxValue(10.0f); + l_Slider.SetIncrements(0.01f); + l_Slider.SetFormatter(CP_SDK.UI.ValueFormatters.Percentage); + l_Slider.SetValue(GTConfig.Instance.PlayerOptions.OverrideLightIntensity); + l_Slider.OnValueChanged((x) => + { + GTConfig.Instance.PlayerOptions.OverrideLightIntensity = (float)x; + GTConfig.Instance.Save(); + }); } - else if (!p_AddOverrideLightsIntensityOption && m_OverrideLightsIntensityToggle != null && m_OverrideLightsIntensityToggle) + else if (!p_AddOverrideLightsIntensityOption && m_OverrideLightsIntensitySetting != null && m_OverrideLightsIntensitySetting) { - GameObject.DestroyImmediate(m_OverrideLightsIntensityToggle.gameObject); - m_OverrideLightsIntensityToggle = null; + GameObject.DestroyImmediate(m_OverrideLightsIntensitySetting); + m_OverrideLightsIntensitySetting = null; } if (p_Enabled) @@ -203,16 +199,16 @@ internal static void SetReorderEnabled(bool p_Enabled, bool p_AddOverrideLightsI m_AdaptiveSfxToggle.transform.parent.SetAsFirstSibling(); m_SfxVolumeSettingsController.transform.parent.SetAsFirstSibling(); m_LeftHandedToggle.transform.parent.SetAsFirstSibling(); - m_PlayerHeightSettingsController.transform.SetAsFirstSibling(); m_AutomaticPlayerHeightToggle.transform.parent.SetAsFirstSibling(); m_EnvironmentEffectsFilterExpertPlusPresetDropdown.transform.parent.SetAsFirstSibling(); m_EnvironmentEffectsFilterDefaultPresetDropdown.transform.parent.SetAsFirstSibling(); - if (m_OverrideLightsIntensityToggle != null && m_OverrideLightsIntensityToggle) m_OverrideLightsIntensityToggle.transform.SetAsFirstSibling(); + if (m_OverrideLightsIntensitySetting != null && m_OverrideLightsIntensitySetting) m_OverrideLightsIntensitySetting.transform.SetAsFirstSibling(); m_AdvanceHudToggle.transform.parent.SetAsFirstSibling(); m_NoTextsAndHudsToggle.transform.parent.SetAsFirstSibling(); m_NoteJumpFixedDurationSettingsController.transform.SetAsFirstSibling(); m_NoteJumpStartBeatOffsetDropdown.transform.parent.SetAsFirstSibling(); m_NoteJumpDurationTypeSettingsDropdown.transform.SetAsFirstSibling(); + m_PlayerHeightSettingsController.transform.SetAsFirstSibling(); } else { @@ -221,7 +217,7 @@ internal static void SetReorderEnabled(bool p_Enabled, bool p_AddOverrideLightsI m_AdvanceHudToggle.transform.parent.SetAsFirstSibling(); m_NoTextsAndHudsToggle.transform.parent.SetAsFirstSibling(); m_SaberTrailIntensitySettingsController.transform.parent.SetAsFirstSibling(); - if (m_OverrideLightsIntensityToggle != null && m_OverrideLightsIntensityToggle) m_OverrideLightsIntensityToggle.transform.SetAsFirstSibling(); + if (m_OverrideLightsIntensitySetting != null && m_OverrideLightsIntensitySetting) m_OverrideLightsIntensitySetting.transform.SetAsFirstSibling(); m_EnvironmentEffectsFilterExpertPlusPresetDropdown.transform.parent.SetAsFirstSibling(); m_EnvironmentEffectsFilterDefaultPresetDropdown.transform.parent.SetAsFirstSibling(); @@ -257,15 +253,6 @@ internal static void SetLightsOptionMerging(bool p_Enabled) //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// On OverrideLightIntensity setting changes - /// - /// New value - private static void OnOverrideLightIntensityChange(object p_Value) - { - GTConfig.Instance.PlayerOptions.OverrideLightIntensity = (float)p_Value; - GTConfig.Instance.Save(); - } /// /// On light preset change, replicate to the hidden one /// diff --git a/Modules/BeatSaberPlus_GameTweaker/Patches/PStandardLevelDetailView.cs b/Modules/BeatSaberPlus_GameTweaker/Patches/PStandardLevelDetailView.cs index 5b63f87..30b275c 100644 --- a/Modules/BeatSaberPlus_GameTweaker/Patches/PStandardLevelDetailView.cs +++ b/Modules/BeatSaberPlus_GameTweaker/Patches/PStandardLevelDetailView.cs @@ -1,7 +1,7 @@ -#define WITH_SONG_CORE +//#define WITH_SONG_CORE -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.Attributes; +//using BeatSaberMarkupLanguage; +//using BeatSaberMarkupLanguage.Attributes; using HarmonyLib; using HMUI; using IPA.Utilities; @@ -23,11 +23,11 @@ internal class SongBrowserDeleteAlias : MonoBehaviour /// /// StandardLevelDetailView patcher /// - [HarmonyPatch(typeof(StandardLevelDetailView))] - [HarmonyPatch(nameof(StandardLevelDetailView.SetContent))] + //[HarmonyPatch(typeof(StandardLevelDetailView))] + //[HarmonyPatch(nameof(StandardLevelDetailView.SetContent))] internal class PStandardLevelDetailView : StandardLevelDetailView { - /// + /*/// /// StandardLevelDetailView instance /// private static StandardLevelDetailView m_StandardLevelDetailView = null; @@ -82,7 +82,7 @@ internal static void Postfix(ref StandardLevelDetailView __instance) l_SongBrowserButton.transform.SetAsFirstSibling(); } } - } + }*/ //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -93,7 +93,7 @@ internal static void Postfix(ref StandardLevelDetailView __instance) /// New state internal static void SetDeleteSongButtonEnabled(bool p_Enabled) { - /// Wait until it's ready + /*/// Wait until it's ready if (m_StandardLevelDetailView == null) return; @@ -110,12 +110,12 @@ internal static void SetDeleteSongButtonEnabled(bool p_Enabled) { GameObject.DestroyImmediate(m_Patch); m_Patch = null; - } + }*/ } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - + /* /// /// UI Patch /// @@ -262,6 +262,6 @@ private void OnConfirmDeleteButton() m_ParserParams.EmitEvent("hide-delete-confirmation-modal"); } - } + }*/ } } diff --git a/Modules/BeatSaberPlus_GameTweaker/Properties/AssemblyInfo.cs b/Modules/BeatSaberPlus_GameTweaker/Properties/AssemblyInfo.cs index d2fca29..7f9dba9 100644 --- a/Modules/BeatSaberPlus_GameTweaker/Properties/AssemblyInfo.cs +++ b/Modules/BeatSaberPlus_GameTweaker/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/Modules/BeatSaberPlus_GameTweaker/UI/Settings.bsml b/Modules/BeatSaberPlus_GameTweaker/UI/Settings.bsml deleted file mode 100644 index 8dbd0b3..0000000 --- a/Modules/BeatSaberPlus_GameTweaker/UI/Settings.bsml +++ /dev/null @@ -1,240 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_GameTweaker/UI/Settings.cs b/Modules/BeatSaberPlus_GameTweaker/UI/Settings.cs deleted file mode 100644 index eca9b2d..0000000 --- a/Modules/BeatSaberPlus_GameTweaker/UI/Settings.cs +++ /dev/null @@ -1,389 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using BeatSaberMarkupLanguage.Parser; -using HMUI; -using System.Collections.Generic; -using UnityEngine; - -namespace BeatSaberPlus_GameTweaker.UI -{ - /// - /// Saber Tweaker view controller - /// - internal class Settings : BeatSaberPlus.SDK.UI.ResourceViewController - { -#pragma warning disable CS0649 - [UIObject("TabSelector")] - private GameObject m_TabSelector; - private TextSegmentedControl m_TabSelector_TabSelectorControl = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Particles tab - [UIObject("ParticlesTab")] private GameObject m_ParticlesTab = null; - [UIComponent("ParticlesTab_RemoveDebris")] private ToggleSetting m_ParticlesTab_RemoveDebris; - [UIComponent("ParticlesTab_RemoveCutParticles")] private ToggleSetting m_ParticlesTab_RemoveCutParticles; - [UIComponent("ParticlesTab_RemoveObstacleParticles")] private ToggleSetting m_ParticlesTab_RemoveObstacleParticles; - [UIComponent("ParticlesTab_RemoveFloorBurnMarkParticles")] private ToggleSetting m_ParticlesTab_RemoveFloorBurnMarkParticles; - [UIComponent("ParticlesTab_RemoveFloorBurnMarkEffects")] private ToggleSetting m_ParticlesTab_RemoveFloorBurnMarkEffects; - [UIComponent("ParticlesTab_RemoveSaberClashEffects")] private ToggleSetting m_ParticlesTab_RemoveSaberClashEffects; - [UIComponent("ParticlesTab_RemoveWorldParticles")] private ToggleSetting m_ParticlesTab_RemoveWorldParticles; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Environment Tab - [UIObject("EnvironmentTab")] private GameObject m_EnvironmentTab = null; - [UIComponent("EnvironmentTab_RemoveMusicBandLogo")] private ToggleSetting m_EnvironmentTab_RemoveMusicBandLogo; - [UIComponent("EnvironmentTab_RemoveFullComboLossAnimation")] private ToggleSetting m_EnvironmentTab_RemoveFullComboLossAnimation; - [UIComponent("EnvironmentTab_NoFake360Maps")] private ToggleSetting m_EnvironmentTab_NoFake360Maps; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region LevelSelection Tab - [UIObject("LevelSelectionTab")] private GameObject m_LevelSelectionTab = null; - [UIComponent("LevelSelectionTab_RemoveBaseGameFilterButton")] private ToggleSetting m_LevelSelectionTab_RemoveBaseGameFilterButton; - [UIComponent("LevelSelectionTab_DeleteSongButton")] private ToggleSetting m_LevelSelectionTab_DeleteSongButton; - [UIComponent("LevelSelectionTab_RemoveSongBrowserTrashcan")] private ToggleSetting m_LevelSelectionTab_RemoveSongBrowserTrashcan; - [UIComponent("LevelSelectionTab_HighlightPlayedSong")] private ToggleSetting m_LevelSelectionTab_HighlightPlayedSong; - [UIComponent("LevelSelectionTab_HighlightPlayedSongColor")] private ColorSetting m_LevelSelectionTab_HighlightPlayedSongColor; - [UIComponent("LevelSelectionTab_HighlightPlayedSongAllColor")] private ColorSetting m_LevelSelectionTab_HighlightPlayedSongAllColor; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region PlayerOptions Tab - [UIObject("PlayerOptionsTab")] private GameObject m_PlayerOptionsTab = null; - [UIComponent("PlayerOptionsTab_JumpDurationIncrement")] private ListSetting m_PlayerOptionsTab_JumpDurationIncrement; - [UIValue("PlayerOptionsTab_JumpDurationIncrementChoices")] private List m_PlayerOptionsTab_JumpDurationIncrementChoices = new List() { "5ms", "10ms", "100ms" }; - [UIValue("PlayerOptionsTab_JumpDurationIncrementValue")] private string m_PlayerOptionsTab_JumpDurationIncrementValue; - [UIComponent("PlayerOptionsTab_ReorderPlayerSettings")] private ToggleSetting m_PlayerOptionsTab_ReorderPlayerSettings; - [UIComponent("PlayerOptionsTab_AddOverrideLightIntensityOption")] private ToggleSetting m_PlayerOptionsTab_AddOverrideLightIntensityOption; - [UIComponent("PlayerOptionsTab_MergeLightEffectFilterOptions")] private ToggleSetting m_PlayerOptionsTab_MergeLightEffectFilterOptions; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region MainMenu Tab - [UIObject("MainMenuTab")] private GameObject m_MainMenuTab = null; - [UIComponent("MainMenuTab_OverrideMenuEnvColors")] private ToggleSetting m_MainMenuTab_OverrideMenuEnvColors; - [UIComponent("MainMenuTab_BaseColor")] private ColorSetting m_MainMenuTab_BaseColor; - [UIComponent("MainMenuTab_LevelClearedColor")] private ColorSetting m_MainMenuTab_LevelClearedColor; - [UIComponent("MainMenuTab_LevelFailedColor")] private ColorSetting m_MainMenuTab_LevelFailedColor; - [UIComponent("MainMenuTab_DisableEditorButton")] private ToggleSetting m_MainMenuTab_DisableEditorButton; - [UIComponent("MainMenuTab_RemoveNewContentPromotional")] private ToggleSetting m_MainMenuTab_RemoveNewContentPromotional; - [UIComponent("MainMenuTab_DisableFireworks")] private ToggleSetting m_MainMenuTab_DisableFireworks; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Tools Tab - [UIObject("ToolsTab")] private GameObject m_ToolsTab = null; - [UIComponent("ToolsTab_RemoveOldLogs")] private ToggleSetting m_ToolsTab_RemoveOldLogs; - [UIComponent("ToolsTab_LogEntriesToKeep")] private IncrementSetting m_ToolsTab_LogEntriesToKeep; - [UIComponent("ToolsTab_FPFCEscape")] private ToggleSetting m_ToolsTab_FPFCEscape; - #endregion -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Constructor - /// - internal Settings() - { - int l_TypeIndex = GTConfig.Instance.PlayerOptions.JumpDurationIncrement % m_PlayerOptionsTab_JumpDurationIncrementChoices.Count; - if (l_TypeIndex >= 0) - m_PlayerOptionsTab_JumpDurationIncrementValue = m_PlayerOptionsTab_JumpDurationIncrementChoices[l_TypeIndex] as string; - else - { - GTConfig.Instance.PlayerOptions.JumpDurationIncrement = 1; - m_PlayerOptionsTab_JumpDurationIncrementValue = m_PlayerOptionsTab_JumpDurationIncrementChoices[0] as string; - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Event = new BSMLAction(this, this.GetType().GetMethod(nameof(Settings.OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - var l_Event2 = new BSMLAction(this, this.GetType().GetMethod(nameof(Settings.OnSettingChanged2), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - var l_Event3 = new BSMLAction(this, this.GetType().GetMethod(nameof(Settings.OnSettingChanged3), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - var l_Event4 = new BSMLAction(this, this.GetType().GetMethod(nameof(Settings.OnSettingChanged4), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - - /// Create type selector - m_TabSelector_TabSelectorControl = BeatSaberPlus.SDK.UI.TextSegmentedControl.Create(m_TabSelector.transform as RectTransform, false); - m_TabSelector_TabSelectorControl.SetTexts(new string[] { "Particles", "Environment", "LevelSelection", "PlayerOptions", "MainMenu", "Tools" }); - m_TabSelector_TabSelectorControl.ReloadData(); - m_TabSelector_TabSelectorControl.didSelectCellEvent += OnTabSelected; - - //////////////////////////////////////////////////////////////////////////// - /// Prepare tabs - //////////////////////////////////////////////////////////////////////////// - - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_ParticlesTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_EnvironmentTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_LevelSelectionTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_PlayerOptionsTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_MainMenuTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_ToolsTab, 0.50f); - - #region Particles tab - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ParticlesTab_RemoveDebris, l_Event, GTConfig.Instance.RemoveDebris, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ParticlesTab_RemoveCutParticles, l_Event, GTConfig.Instance.RemoveAllCutParticles, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ParticlesTab_RemoveObstacleParticles, l_Event, GTConfig.Instance.RemoveObstacleParticles, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ParticlesTab_RemoveFloorBurnMarkParticles, l_Event, GTConfig.Instance.RemoveSaberBurnMarkSparkles, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ParticlesTab_RemoveFloorBurnMarkEffects, l_Event, GTConfig.Instance.RemoveSaberBurnMarks, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ParticlesTab_RemoveSaberClashEffects, l_Event, GTConfig.Instance.RemoveSaberClashEffects, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ParticlesTab_RemoveWorldParticles, l_Event, GTConfig.Instance.RemoveWorldParticles, true); - #endregion - - #region Environment Tab - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_EnvironmentTab_RemoveMusicBandLogo, l_Event, GTConfig.Instance.Environment.RemoveMusicBandLogo, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_EnvironmentTab_RemoveFullComboLossAnimation, l_Event, GTConfig.Instance.Environment.RemoveFullComboLossAnimation, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_EnvironmentTab_NoFake360Maps, l_Event, GTConfig.Instance.Environment.NoFake360HUD, true); - #endregion - - #region LevelSelection Tab - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_LevelSelectionTab_RemoveBaseGameFilterButton, l_Event, GTConfig.Instance.LevelSelection.RemoveBaseGameFilterButton, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_LevelSelectionTab_DeleteSongButton, l_Event, GTConfig.Instance.LevelSelection.DeleteSongButton, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_LevelSelectionTab_RemoveSongBrowserTrashcan, l_Event, GTConfig.Instance.LevelSelection.DeleteSongBrowserTrashcan, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_LevelSelectionTab_HighlightPlayedSong, l_Event, GTConfig.Instance.LevelSelection.HighlightEnabled, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_LevelSelectionTab_HighlightPlayedSongColor, l_Event, GTConfig.Instance.LevelSelection.HighlightPlayed, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_LevelSelectionTab_HighlightPlayedSongAllColor, l_Event, GTConfig.Instance.LevelSelection.HighlightAllPlayed, true); - #endregion - - #region PlayerOptions Tab - BeatSaberPlus.SDK.UI.ListSetting.Setup(m_PlayerOptionsTab_JumpDurationIncrement, l_Event, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_PlayerOptionsTab_ReorderPlayerSettings, l_Event, GTConfig.Instance.PlayerOptions.ReorderPlayerSettings, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_PlayerOptionsTab_AddOverrideLightIntensityOption, l_Event, GTConfig.Instance.PlayerOptions.OverrideLightIntensityOption, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_PlayerOptionsTab_MergeLightEffectFilterOptions, l_Event, GTConfig.Instance.PlayerOptions.MergeLightPressetOptions, true); - #endregion - - #region MainMenu Tab - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_MainMenuTab_OverrideMenuEnvColors, l_Event2, GTConfig.Instance.MainMenu.OverrideMenuEnvColors, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_MainMenuTab_BaseColor, l_Event2, GTConfig.Instance.MainMenu.BaseColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_MainMenuTab_LevelClearedColor, l_Event3, GTConfig.Instance.MainMenu.LevelClearedColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_MainMenuTab_LevelFailedColor, l_Event4, GTConfig.Instance.MainMenu.LevelFailedColor, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_MainMenuTab_DisableEditorButton, l_Event, GTConfig.Instance.MainMenu.DisableEditorButtonOnMainMenu, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_MainMenuTab_RemoveNewContentPromotional, l_Event, GTConfig.Instance.MainMenu.RemoveNewContentPromotional, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_MainMenuTab_DisableFireworks, l_Event, GTConfig.Instance.MainMenu.DisableFireworks, true); - #endregion - - #region Tools Tab - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ToolsTab_RemoveOldLogs, l_Event, GTConfig.Instance.Tools.RemoveOldLogs, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_ToolsTab_LogEntriesToKeep, l_Event, null, GTConfig.Instance.Tools.LogEntriesToKeep, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ToolsTab_FPFCEscape, l_Event, GTConfig.Instance.Tools.FPFCEscape, true); - #endregion - - /// Show first tab by default - OnTabSelected(null, 0); - /// Refresh UI - OnSettingChanged(null); - } - /// - /// On view deactivation - /// - protected override sealed void OnViewDeactivation() - { - GTConfig.Instance.Save(); - Managers.CustomMenuLightManager.SwitchToBase(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When a tab is selected - /// - /// Tab control instance - /// Tab index - private void OnTabSelected(SegmentedControl p_SegmentControl, int p_TabIndex) - { - m_ParticlesTab.SetActive(p_TabIndex == 0); - m_EnvironmentTab.SetActive(p_TabIndex == 1); - m_LevelSelectionTab.SetActive(p_TabIndex == 2); - m_PlayerOptionsTab.SetActive(p_TabIndex == 3); - m_MainMenuTab.SetActive(p_TabIndex == 4); - m_ToolsTab.SetActive(p_TabIndex == 5); - } - /// - /// On setting changed - /// - /// New value - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - #region Particles tab - GTConfig.Instance.RemoveDebris = m_ParticlesTab_RemoveDebris.Value; - GTConfig.Instance.RemoveAllCutParticles = m_ParticlesTab_RemoveCutParticles.Value; - GTConfig.Instance.RemoveObstacleParticles = m_ParticlesTab_RemoveObstacleParticles.Value; - GTConfig.Instance.RemoveSaberBurnMarkSparkles = m_ParticlesTab_RemoveFloorBurnMarkParticles.Value; - GTConfig.Instance.RemoveSaberBurnMarks = m_ParticlesTab_RemoveFloorBurnMarkEffects.Value; - GTConfig.Instance.RemoveSaberClashEffects = m_ParticlesTab_RemoveSaberClashEffects.Value; - GTConfig.Instance.RemoveWorldParticles = m_ParticlesTab_RemoveWorldParticles.Value; - #endregion - - #region Environment Tab - GTConfig.Instance.Environment.RemoveMusicBandLogo = m_EnvironmentTab_RemoveMusicBandLogo.Value; - GTConfig.Instance.Environment.RemoveFullComboLossAnimation = m_EnvironmentTab_RemoveFullComboLossAnimation.Value; - GTConfig.Instance.Environment.NoFake360HUD = m_EnvironmentTab_NoFake360Maps.Value; - #endregion - - #region LevelSelection Tab - GTConfig.Instance.LevelSelection.RemoveBaseGameFilterButton = m_LevelSelectionTab_RemoveBaseGameFilterButton.Value; - GTConfig.Instance.LevelSelection.DeleteSongButton = m_LevelSelectionTab_DeleteSongButton.Value; - GTConfig.Instance.LevelSelection.DeleteSongBrowserTrashcan = m_LevelSelectionTab_RemoveSongBrowserTrashcan.Value; - GTConfig.Instance.LevelSelection.HighlightEnabled = m_LevelSelectionTab_HighlightPlayedSong.Value; - GTConfig.Instance.LevelSelection.HighlightPlayed = m_LevelSelectionTab_HighlightPlayedSongColor.CurrentColor; - GTConfig.Instance.LevelSelection.HighlightAllPlayed = m_LevelSelectionTab_HighlightPlayedSongAllColor.CurrentColor; - #endregion - - #region PlayerOptions Tab - GTConfig.Instance.PlayerOptions.JumpDurationIncrement = m_PlayerOptionsTab_JumpDurationIncrementChoices.IndexOf(m_PlayerOptionsTab_JumpDurationIncrement.Value as string); - GTConfig.Instance.PlayerOptions.ReorderPlayerSettings = m_PlayerOptionsTab_ReorderPlayerSettings.Value; - GTConfig.Instance.PlayerOptions.OverrideLightIntensityOption = m_PlayerOptionsTab_AddOverrideLightIntensityOption.Value; - GTConfig.Instance.PlayerOptions.MergeLightPressetOptions = m_PlayerOptionsTab_MergeLightEffectFilterOptions.Value; - #endregion - - #region MainMenu Tab - GTConfig.Instance.MainMenu.OverrideMenuEnvColors = m_MainMenuTab_OverrideMenuEnvColors.Value; - GTConfig.Instance.MainMenu.BaseColor = m_MainMenuTab_BaseColor.CurrentColor; - GTConfig.Instance.MainMenu.LevelClearedColor = m_MainMenuTab_LevelClearedColor.CurrentColor; - GTConfig.Instance.MainMenu.LevelFailedColor = m_MainMenuTab_LevelFailedColor.CurrentColor; - GTConfig.Instance.MainMenu.DisableEditorButtonOnMainMenu = m_MainMenuTab_DisableEditorButton.Value; - GTConfig.Instance.MainMenu.RemoveNewContentPromotional = m_MainMenuTab_RemoveNewContentPromotional.Value; - GTConfig.Instance.MainMenu.DisableFireworks = m_MainMenuTab_DisableFireworks.Value; - - m_MainMenuTab_BaseColor.interactable = GTConfig.Instance.MainMenu.OverrideMenuEnvColors; - m_MainMenuTab_LevelClearedColor.interactable = GTConfig.Instance.MainMenu.OverrideMenuEnvColors; - m_MainMenuTab_LevelFailedColor.interactable = GTConfig.Instance.MainMenu.OverrideMenuEnvColors; - #endregion - - #region Tools Tab - GTConfig.Instance.Tools.RemoveOldLogs = m_ToolsTab_RemoveOldLogs.Value; - GTConfig.Instance.Tools.LogEntriesToKeep = (int)m_ToolsTab_LogEntriesToKeep.Value; - GTConfig.Instance.Tools.FPFCEscape = m_ToolsTab_FPFCEscape.Value; - #endregion - - /// Update patches - GameTweaker.Instance.UpdatePatches(false); - } - /// - /// On setting changed - /// - /// New value - private void OnSettingChanged2(object p_Value) - { - OnSettingChanged(p_Value); - Managers.CustomMenuLightManager.UpdateFromConfig(); - Managers.CustomMenuLightManager.SwitchToBase(); - } - /// - /// On setting changed - /// - /// New value - private void OnSettingChanged3(object p_Value) - { - OnSettingChanged(p_Value); - Managers.CustomMenuLightManager.UpdateFromConfig(); - Managers.CustomMenuLightManager.SwitchToLevelCleared(); - } - /// - /// On setting changed - /// - /// New value - private void OnSettingChanged4(object p_Value) - { - OnSettingChanged(p_Value); - Managers.CustomMenuLightManager.UpdateFromConfig(); - Managers.CustomMenuLightManager.SwitchToLevelFailed(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset settings - /// - internal void RefreshSettings() - { - m_PreventChanges = true; - - #region Particles tab - m_ParticlesTab_RemoveDebris.Value = GTConfig.Instance.RemoveDebris; - m_ParticlesTab_RemoveCutParticles.Value = GTConfig.Instance.RemoveAllCutParticles; - m_ParticlesTab_RemoveObstacleParticles.Value = GTConfig.Instance.RemoveObstacleParticles; - m_ParticlesTab_RemoveFloorBurnMarkParticles.Value = GTConfig.Instance.RemoveSaberBurnMarkSparkles; - m_ParticlesTab_RemoveFloorBurnMarkEffects.Value = GTConfig.Instance.RemoveSaberBurnMarks; - m_ParticlesTab_RemoveSaberClashEffects.Value = GTConfig.Instance.RemoveSaberClashEffects; - m_ParticlesTab_RemoveWorldParticles.Value = GTConfig.Instance.RemoveWorldParticles; - #endregion - - #region Environment Tab - m_EnvironmentTab_RemoveMusicBandLogo.Value = GTConfig.Instance.Environment.RemoveMusicBandLogo; - m_EnvironmentTab_RemoveFullComboLossAnimation.Value = GTConfig.Instance.Environment.RemoveFullComboLossAnimation; - m_EnvironmentTab_NoFake360Maps.Value = GTConfig.Instance.Environment.NoFake360HUD; - #endregion - - #region LevelSelection Tab - m_LevelSelectionTab_RemoveBaseGameFilterButton.Value = GTConfig.Instance.LevelSelection.RemoveBaseGameFilterButton; - m_LevelSelectionTab_DeleteSongButton.Value = GTConfig.Instance.LevelSelection.DeleteSongButton; - m_LevelSelectionTab_RemoveSongBrowserTrashcan.Value = GTConfig.Instance.LevelSelection.DeleteSongBrowserTrashcan; - m_LevelSelectionTab_HighlightPlayedSong.Value = GTConfig.Instance.LevelSelection.HighlightEnabled; - m_LevelSelectionTab_HighlightPlayedSongColor.CurrentColor = GTConfig.Instance.LevelSelection.HighlightPlayed; - m_LevelSelectionTab_HighlightPlayedSongAllColor.CurrentColor = GTConfig.Instance.LevelSelection.HighlightAllPlayed; - #endregion - - #region PlayerOptions Tab - m_PlayerOptionsTab_JumpDurationIncrement.Value = m_PlayerOptionsTab_JumpDurationIncrementChoices[GTConfig.Instance.PlayerOptions.JumpDurationIncrement % m_PlayerOptionsTab_JumpDurationIncrementChoices.Count]; - m_PlayerOptionsTab_ReorderPlayerSettings.Value = GTConfig.Instance.PlayerOptions.ReorderPlayerSettings; - m_PlayerOptionsTab_AddOverrideLightIntensityOption.Value = GTConfig.Instance.PlayerOptions.OverrideLightIntensityOption; - m_PlayerOptionsTab_MergeLightEffectFilterOptions.Value = GTConfig.Instance.PlayerOptions.MergeLightPressetOptions; - #endregion - - #region MainMenu Tab - m_MainMenuTab_OverrideMenuEnvColors.Value = GTConfig.Instance.MainMenu.OverrideMenuEnvColors; - m_MainMenuTab_BaseColor.CurrentColor = GTConfig.Instance.MainMenu.BaseColor; - m_MainMenuTab_LevelClearedColor.CurrentColor = GTConfig.Instance.MainMenu.LevelClearedColor; - m_MainMenuTab_LevelFailedColor.CurrentColor = GTConfig.Instance.MainMenu.LevelFailedColor; - m_MainMenuTab_DisableEditorButton.Value = GTConfig.Instance.MainMenu.DisableEditorButtonOnMainMenu; - m_MainMenuTab_RemoveNewContentPromotional.Value = GTConfig.Instance.MainMenu.RemoveNewContentPromotional; - m_MainMenuTab_DisableFireworks.Value = GTConfig.Instance.MainMenu.DisableFireworks; - #endregion - - #region Tools Tab - m_ToolsTab_RemoveOldLogs.Value = GTConfig.Instance.Tools.RemoveOldLogs; - m_ToolsTab_LogEntriesToKeep.Value = GTConfig.Instance.Tools.LogEntriesToKeep; - m_ToolsTab_FPFCEscape.Value = GTConfig.Instance.Tools.FPFCEscape; - #endregion - - m_PreventChanges = false; - - /// Refresh UI - OnSettingChanged(null); - } - } -} diff --git a/Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeft.bsml b/Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeft.bsml deleted file mode 100644 index 29bce97..0000000 --- a/Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeft.bsml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeft.cs b/Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeft.cs deleted file mode 100644 index 3528a0c..0000000 --- a/Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeft.cs +++ /dev/null @@ -1,80 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using System.Diagnostics; -using UnityEngine; - -namespace BeatSaberPlus_GameTweaker.UI -{ - /// - /// Chat request settings left screen - /// - internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController - { - private static readonly string s_InformationsStr = "Game Tweaker" - + "\n" + "This module allow you to customize your game experience, remove some boring base game features, and add new cool features/tweaks" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n"; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("Background")] - private GameObject m_Background = null; - [UIComponent("Informations")] - private HMUI.TextPageScrollView m_Informations = null; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); - m_Informations.SetText(s_InformationsStr); - m_Informations.UpdateVerticalScrollIndicator(0); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset button - /// - [UIAction("click-reset-btn-pressed")] - private void OnResetButton() - { - ShowConfirmationModal("Do you really want to reset\ngame tweaker configuration?", () => - { - /// Reset config - GTConfig.Instance.Reset(); - GTConfig.Instance.Enabled = true; - GTConfig.Instance.Save(); - - /// Refresh values - Settings.Instance.RefreshSettings(); - }); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Documentation button - /// - [UIAction("click-documentation-btn-pressed")] - private void OnDocumentationButton() - { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://github.com/hardcpp/BeatSaberPlus/wiki#game-tweaker"); - } - } -} diff --git a/Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeftView.cs b/Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeftView.cs new file mode 100644 index 0000000..0e9e47c --- /dev/null +++ b/Modules/BeatSaberPlus_GameTweaker/UI/SettingsLeftView.cs @@ -0,0 +1,76 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace BeatSaberPlus_GameTweaker.UI +{ + /// + /// Settings left view + /// + internal sealed class SettingsLeftView : CP_SDK.UI.ViewController + { + private static readonly string s_InformationStr = "Game Tweaker" + + "\n" + "This module allow you to customize your game experience, remove some boring base game features, and add new cool features/tweaks"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Information"), + + Templates.ScrollableInfos(50, + XUIText.Make(s_InformationStr) + .SetAlign(TMPro.TextAlignmentOptions.Left) + ), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("Reset", OnResetButton) + ), + Templates.ExpandedButtonsLine( + XUISecondaryButton.Make("Documentation", OnDocumentationButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset button + /// + private void OnResetButton() + { + ShowConfirmationModal("Do you really want to reset\ngame tweaker configuration?", (p_Confirm) => + { + if (!p_Confirm) + return; + + /// Reset config + GTConfig.Instance.Reset(); + GTConfig.Instance.Enabled = true; + GTConfig.Instance.Save(); + + /// Refresh values + SettingsMainView.Instance.RefreshSettings(); + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Documentation button + /// + private void OnDocumentationButton() + { + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL(GameTweaker.Instance.DocumentationURL); + } + } +} diff --git a/Modules/BeatSaberPlus_GameTweaker/UI/SettingsMainView.cs b/Modules/BeatSaberPlus_GameTweaker/UI/SettingsMainView.cs new file mode 100644 index 0000000..5238dc1 --- /dev/null +++ b/Modules/BeatSaberPlus_GameTweaker/UI/SettingsMainView.cs @@ -0,0 +1,453 @@ +using CP_SDK.XUI; + +namespace BeatSaberPlus_GameTweaker.UI +{ + /// + /// Settings main view + /// + internal sealed class SettingsMainView : CP_SDK.UI.ViewController + { + private XUITabControl m_TabControl; + + private XUIToggle m_ParticlesTab_RemoveDebris; + private XUIToggle m_ParticlesTab_RemoveWorldParticles; + private XUIToggle m_ParticlesTab_RemoveCutParticles; + private XUIToggle m_ParticlesTab_RemoveObstacleParticles; + private XUIToggle m_ParticlesTab_RemoveFloorBurnMarkParticles; + private XUIToggle m_ParticlesTab_RemoveFloorBurnMarkEffects; + private XUIToggle m_ParticlesTab_RemoveSaberClashEffects; + + private XUIToggle m_EnvironmentTab_RemoveMusicBandLogo; + private XUIToggle m_EnvironmentTab_RemoveFullComboLossAnimation; + private XUIToggle m_EnvironmentTab_NoFake360Maps; + + private XUIColorInput m_LevelSelectionTab_HighlightPlayedSongColor; + private XUIToggle m_LevelSelectionTab_HighlightPlayedSong; + private XUIColorInput m_LevelSelectionTab_HighlightPlayedSongAllColor; + private XUIToggle m_LevelSelectionTab_RemoveBaseGameFilterButton; + private XUIToggle m_LevelSelectionTab_DeleteSongButton; + private XUIToggle m_LevelSelectionTab_RemoveSongBrowserTrashcan; + + private XUIToggle m_PlayerOptionsTab_ReorderPlayerSettings; + private XUIToggle m_PlayerOptionsTab_AddOverrideLightIntensityOption; + private XUIToggle m_PlayerOptionsTab_MergeLightEffectFilterOptions; + + private XUIToggle m_MainMenuTab_OverrideMenuEnvColors; + private XUIColorInput m_MainMenuTab_BaseColor; + private XUIColorInput m_MainMenuTab_LevelClearedColor; + private XUIColorInput m_MainMenuTab_LevelFailedColor; + private XUIToggle m_MainMenuTab_DisableEditorButton; + private XUIToggle m_MainMenuTab_RemoveNewContentPromotional; + private XUIToggle m_MainMenuTab_DisableFireworks; + + private XUIToggle m_ToolsTab_RemoveOldLogs; + private XUISlider m_ToolsTab_LogEntriesToKeep; + private XUIToggle m_ToolsTab_FPFCEscape; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private bool m_PreventChanges = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayoutMainView( + Templates.TitleBar("Game Tweaker | Settings"), + + XUITabControl.Make( + ("Particles", BuildParticlesTab()), + ("Environment", BuildEnvironmentTab()), + ("LevelSelection", BuildLevelSelectionTab()), + ("PlayerOptions", BuildPlayerOptionsTab()), + ("MainMenu", BuildMainMenuTab()), + ("Tools", BuildToolsTab()) + ) + .Bind(ref m_TabControl) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + + /// Refresh UI + OnSettingChanged(); + } + /// + /// On view deactivation + /// + protected override sealed void OnViewDeactivation() + { + GTConfig.Instance.Save(); + Managers.CustomMenuLightManager.SwitchToBase(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build particles tab + /// + /// + private IXUIElement BuildParticlesTab() + { + var l_Config = GTConfig.Instance; + return XUIVLayout.Make( + XUIText.Make("Remove all debris spawn"), + XUIToggle.Make().SetValue(l_Config.RemoveDebris).Bind(ref m_ParticlesTab_RemoveDebris).OnValueChanged((_) => OnSettingChanged()), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Remove world particles"), + XUIToggle.Make().SetValue(l_Config.RemoveWorldParticles).Bind(ref m_ParticlesTab_RemoveWorldParticles), + XUIText.Make("Remove floor burn particles"), + XUIToggle.Make().SetValue(l_Config.RemoveSaberBurnMarkSparkles).Bind(ref m_ParticlesTab_RemoveFloorBurnMarkParticles) + ) + .SetPadding(0) + .SetWidth(40.0f) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())), + + XUIVLayout.Make( + XUIText.Make("Remove cut particles"), + XUIToggle.Make().SetValue(l_Config.RemoveAllCutParticles).Bind(ref m_ParticlesTab_RemoveCutParticles), + XUIText.Make("Remove floor burn marks"), + XUIToggle.Make().SetValue(l_Config.RemoveSaberBurnMarks).Bind(ref m_ParticlesTab_RemoveFloorBurnMarkEffects) + ) + .SetPadding(0) + .SetWidth(40.0f) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())), + + XUIVLayout.Make( + XUIText.Make("Remove obstacle particles"), + XUIToggle.Make().SetValue(l_Config.RemoveObstacleParticles).Bind(ref m_ParticlesTab_RemoveObstacleParticles), + XUIText.Make("Remove saber clash effect"), + XUIToggle.Make().SetValue(l_Config.RemoveSaberClashEffects).Bind(ref m_ParticlesTab_RemoveSaberClashEffects) + ) + .SetPadding(0) + .SetWidth(40.0f) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())) + ) + .SetPadding(0) + ); + } + /// + /// Build environment tab + /// + /// + private IXUIElement BuildEnvironmentTab() + { + var l_Config = GTConfig.Instance.Environment; + return XUIVLayout.Make( + XUIText.Make("Remove music group logos (BTS, LinkinPark...) in environments"), + XUIToggle.Make().SetValue(l_Config.RemoveMusicBandLogo).Bind(ref m_EnvironmentTab_RemoveMusicBandLogo), + + XUIText.Make("Remove full combo loss animation"), + XUIToggle.Make().SetValue(l_Config.RemoveFullComboLossAnimation).Bind(ref m_EnvironmentTab_RemoveFullComboLossAnimation), + + XUIText.Make("No fake 360 maps"), + XUIToggle.Make().SetValue(l_Config.NoFake360HUD).Bind(ref m_EnvironmentTab_NoFake360Maps) + ) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())); + } + /// + /// Build level selection tab + /// + /// + private IXUIElement BuildLevelSelectionTab() + { + var l_Config = GTConfig.Instance.LevelSelection; + return XUIVLayout.Make( + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Played color"), + XUIColorInput.Make().SetValue(l_Config.HighlightPlayed).Bind(ref m_LevelSelectionTab_HighlightPlayedSongColor) + ) + .SetPadding(0) + .SetWidth(40.0f) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())), + + XUIVLayout.Make( + XUIText.Make("Highlight played song"), + XUIToggle.Make().SetValue(l_Config.HighlightEnabled).Bind(ref m_LevelSelectionTab_HighlightPlayedSong) + ) + .SetPadding(0) + .SetWidth(40.0f) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())), + + XUIVLayout.Make( + XUIText.Make("All played color"), + XUIColorInput.Make().SetValue(l_Config.HighlightAllPlayed).Bind(ref m_LevelSelectionTab_HighlightPlayedSongAllColor) + ) + .SetPadding(0) + .SetWidth(40.0f) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())) + ) + .SetPadding(0), + + XUIText.Make("Remove base game filter"), + XUIToggle.Make().SetValue(l_Config.RemoveBaseGameFilterButton).Bind(ref m_LevelSelectionTab_RemoveBaseGameFilterButton), + + XUIText.Make("Song delete button"), + XUIToggle.Make().SetValue(l_Config.DeleteSongButton).Bind(ref m_LevelSelectionTab_DeleteSongButton), + + XUIText.Make("Remove SongBrowser trashcan"), + XUIToggle.Make().SetValue(l_Config.DeleteSongButton).Bind(ref m_LevelSelectionTab_RemoveSongBrowserTrashcan) + ) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())); + } + /// + /// Build player options tab + /// + /// + private IXUIElement BuildPlayerOptionsTab() + { + var l_Config = GTConfig.Instance.PlayerOptions; + return XUIVLayout.Make( + XUIText.Make("Better player options menu"), + XUIToggle.Make().SetValue(l_Config.ReorderPlayerSettings).Bind(ref m_PlayerOptionsTab_ReorderPlayerSettings), + + XUIText.Make("Add override lights intensity option"), + XUIToggle.Make().SetValue(l_Config.OverrideLightIntensityOption).Bind(ref m_PlayerOptionsTab_AddOverrideLightIntensityOption), + + XUIText.Make("Merge light effect filter options"), + XUIToggle.Make().SetValue(l_Config.MergeLightPressetOptions).Bind(ref m_PlayerOptionsTab_MergeLightEffectFilterOptions) + ) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())); + } + /// + /// Build main menu tab + /// + /// + private IXUIElement BuildMainMenuTab() + { + var l_Config = GTConfig.Instance.MainMenu; + return XUIVLayout.Make( + XUIText.Make("Override menu environment colors"), + XUIToggle.Make().SetValue(l_Config.OverrideMenuEnvColors).Bind(ref m_MainMenuTab_OverrideMenuEnvColors).OnValueChanged((_) => OnSettingChangedOverrideMenuColor()), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Base color"), + XUIColorInput.Make().SetValue(l_Config.BaseColor).Bind(ref m_MainMenuTab_BaseColor), + + XUIText.Make("Disable Editor button"), + XUIToggle.Make().SetValue(l_Config.DisableEditorButtonOnMainMenu).Bind(ref m_MainMenuTab_DisableEditorButton).OnValueChanged((_) => OnSettingChanged()) + ) + .SetPadding(0) + .SetWidth(40.0f) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChangedOverrideMenuColor())), + + XUIVLayout.Make( + XUIText.Make("Level cleared color"), + XUIColorInput.Make().SetValue(l_Config.LevelClearedColor).Bind(ref m_MainMenuTab_LevelClearedColor), + + XUIText.Make("Remove new content promotional"), + XUIToggle.Make().SetValue(l_Config.RemoveNewContentPromotional).Bind(ref m_MainMenuTab_RemoveNewContentPromotional).OnValueChanged((_) => OnSettingChanged()) + ) + .SetPadding(0) + .SetWidth(40.0f) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChangedLevelClearedColor())), + + XUIVLayout.Make( + XUIText.Make("Level failed color"), + XUIColorInput.Make().SetValue(l_Config.LevelFailedColor).Bind(ref m_MainMenuTab_LevelFailedColor), + + XUIText.Make("Disable fireworks"), + XUIToggle.Make().SetValue(l_Config.DisableFireworks).Bind(ref m_MainMenuTab_DisableFireworks).OnValueChanged((_) => OnSettingChanged()) + ) + .SetPadding(0) + .SetWidth(40.0f) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChangedLevelFailedColor())) + ) + .SetPadding(0) + ); + } + /// + /// Build tools tab + /// + /// + private IXUIElement BuildToolsTab() + { + var l_Config = GTConfig.Instance.Tools; + return XUIVLayout.Make( + XUIText.Make("Remove old logs"), + XUIToggle.Make().SetValue(l_Config.RemoveOldLogs).Bind(ref m_ToolsTab_RemoveOldLogs), + + XUIText.Make("Amount of logs to keep"), + XUISlider.Make().SetMinValue(4.0f).SetMaxValue(20.0f).SetIncrements(1.0f).SetInteger(true).SetValue(l_Config.LogEntriesToKeep).Bind(ref m_ToolsTab_LogEntriesToKeep), + + XUIText.Make("FPFC Escape"), + XUIToggle.Make().SetValue(l_Config.FPFCEscape).Bind(ref m_ToolsTab_FPFCEscape) + ) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On setting changed + /// + private void OnSettingChanged() + { + if (m_PreventChanges) + return; + + #region Particles tab + var l_ParticlesConfig = GTConfig.Instance; + l_ParticlesConfig.RemoveDebris = m_ParticlesTab_RemoveDebris.Element.GetValue(); + l_ParticlesConfig.RemoveAllCutParticles = m_ParticlesTab_RemoveCutParticles.Element.GetValue(); + l_ParticlesConfig.RemoveObstacleParticles = m_ParticlesTab_RemoveObstacleParticles.Element.GetValue(); + l_ParticlesConfig.RemoveSaberBurnMarkSparkles = m_ParticlesTab_RemoveFloorBurnMarkParticles.Element.GetValue(); + l_ParticlesConfig.RemoveSaberBurnMarks = m_ParticlesTab_RemoveFloorBurnMarkEffects.Element.GetValue(); + l_ParticlesConfig.RemoveSaberClashEffects = m_ParticlesTab_RemoveSaberClashEffects.Element.GetValue(); + l_ParticlesConfig.RemoveWorldParticles = m_ParticlesTab_RemoveWorldParticles.Element.GetValue(); + #endregion + + #region Environment Tab + var l_EnvironmentConfig = GTConfig.Instance.Environment; + l_EnvironmentConfig.RemoveMusicBandLogo = m_EnvironmentTab_RemoveMusicBandLogo.Element.GetValue(); + l_EnvironmentConfig.RemoveFullComboLossAnimation = m_EnvironmentTab_RemoveFullComboLossAnimation.Element.GetValue(); + l_EnvironmentConfig.NoFake360HUD = m_EnvironmentTab_NoFake360Maps.Element.GetValue(); + #endregion + + #region LevelSelection Tab + var l_LevelSelectionConfig = GTConfig.Instance.LevelSelection; + l_LevelSelectionConfig.RemoveBaseGameFilterButton = m_LevelSelectionTab_RemoveBaseGameFilterButton.Element.GetValue(); + l_LevelSelectionConfig.DeleteSongButton = m_LevelSelectionTab_DeleteSongButton.Element.GetValue(); + l_LevelSelectionConfig.DeleteSongBrowserTrashcan = m_LevelSelectionTab_RemoveSongBrowserTrashcan.Element.GetValue(); + l_LevelSelectionConfig.HighlightEnabled = m_LevelSelectionTab_HighlightPlayedSong.Element.GetValue(); + l_LevelSelectionConfig.HighlightPlayed = m_LevelSelectionTab_HighlightPlayedSongColor.Element.GetValue(); + l_LevelSelectionConfig.HighlightAllPlayed = m_LevelSelectionTab_HighlightPlayedSongAllColor.Element.GetValue(); + #endregion + + #region PlayerOptions Tab + var l_PlayerOptionConfig = GTConfig.Instance.PlayerOptions; + l_PlayerOptionConfig.ReorderPlayerSettings = m_PlayerOptionsTab_ReorderPlayerSettings.Element.GetValue(); + l_PlayerOptionConfig.OverrideLightIntensityOption = m_PlayerOptionsTab_AddOverrideLightIntensityOption.Element.GetValue(); + l_PlayerOptionConfig.MergeLightPressetOptions = m_PlayerOptionsTab_MergeLightEffectFilterOptions.Element.GetValue(); + #endregion + + #region MainMenu Tab + var l_MainMenuConfig = GTConfig.Instance.MainMenu; + l_MainMenuConfig.OverrideMenuEnvColors = m_MainMenuTab_OverrideMenuEnvColors.Element.GetValue(); + l_MainMenuConfig.BaseColor = m_MainMenuTab_BaseColor.Element.GetValue(); + l_MainMenuConfig.LevelClearedColor = m_MainMenuTab_LevelClearedColor.Element.GetValue(); + l_MainMenuConfig.LevelFailedColor = m_MainMenuTab_LevelFailedColor.Element.GetValue(); + l_MainMenuConfig.DisableEditorButtonOnMainMenu = m_MainMenuTab_DisableEditorButton.Element.GetValue(); + l_MainMenuConfig.RemoveNewContentPromotional = m_MainMenuTab_RemoveNewContentPromotional.Element.GetValue(); + l_MainMenuConfig.DisableFireworks = m_MainMenuTab_DisableFireworks.Element.GetValue(); + + m_MainMenuTab_BaseColor .SetInteractable(l_MainMenuConfig.OverrideMenuEnvColors); + m_MainMenuTab_LevelClearedColor .SetInteractable(l_MainMenuConfig.OverrideMenuEnvColors); + m_MainMenuTab_LevelFailedColor .SetInteractable(l_MainMenuConfig.OverrideMenuEnvColors); + #endregion + + #region Tools Tab + var l_ToolsConfig = GTConfig.Instance.Tools; + + l_ToolsConfig.RemoveOldLogs = m_ToolsTab_RemoveOldLogs.Element.GetValue(); + l_ToolsConfig.LogEntriesToKeep = (int)m_ToolsTab_LogEntriesToKeep.Element.GetValue(); + l_ToolsConfig.FPFCEscape = m_ToolsTab_FPFCEscape.Element.GetValue(); + #endregion + + /// Update patches + GameTweaker.Instance.UpdatePatches(false); + } + /// + /// On setting changed + /// + private void OnSettingChangedOverrideMenuColor() + { + OnSettingChanged(); + Managers.CustomMenuLightManager.UpdateFromConfig(); + Managers.CustomMenuLightManager.SwitchToBase(); + } + /// + /// On setting changed + /// + private void OnSettingChangedLevelClearedColor() + { + OnSettingChanged(); + Managers.CustomMenuLightManager.UpdateFromConfig(); + Managers.CustomMenuLightManager.SwitchToLevelCleared(); + } + /// + /// On setting changed + /// + private void OnSettingChangedLevelFailedColor() + { + OnSettingChanged(); + Managers.CustomMenuLightManager.UpdateFromConfig(); + Managers.CustomMenuLightManager.SwitchToLevelFailed(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset settings + /// + internal void RefreshSettings() + { + m_PreventChanges = true; + + #region Particles tab + m_ParticlesTab_RemoveDebris .SetValue(GTConfig.Instance.RemoveDebris); + m_ParticlesTab_RemoveCutParticles .SetValue(GTConfig.Instance.RemoveAllCutParticles); + m_ParticlesTab_RemoveObstacleParticles .SetValue(GTConfig.Instance.RemoveObstacleParticles); + m_ParticlesTab_RemoveFloorBurnMarkParticles .SetValue(GTConfig.Instance.RemoveSaberBurnMarkSparkles); + m_ParticlesTab_RemoveFloorBurnMarkEffects .SetValue(GTConfig.Instance.RemoveSaberBurnMarks); + m_ParticlesTab_RemoveSaberClashEffects .SetValue(GTConfig.Instance.RemoveSaberClashEffects); + m_ParticlesTab_RemoveWorldParticles .SetValue(GTConfig.Instance.RemoveWorldParticles); + #endregion + + #region Environment Tab + m_EnvironmentTab_RemoveMusicBandLogo .SetValue(GTConfig.Instance.Environment.RemoveMusicBandLogo); + m_EnvironmentTab_RemoveFullComboLossAnimation .SetValue(GTConfig.Instance.Environment.RemoveFullComboLossAnimation); + m_EnvironmentTab_NoFake360Maps .SetValue(GTConfig.Instance.Environment.NoFake360HUD); + #endregion + + #region LevelSelection Tab + m_LevelSelectionTab_RemoveBaseGameFilterButton .SetValue(GTConfig.Instance.LevelSelection.RemoveBaseGameFilterButton); + m_LevelSelectionTab_DeleteSongButton .SetValue(GTConfig.Instance.LevelSelection.DeleteSongButton); + m_LevelSelectionTab_RemoveSongBrowserTrashcan .SetValue(GTConfig.Instance.LevelSelection.DeleteSongBrowserTrashcan); + m_LevelSelectionTab_HighlightPlayedSong .SetValue(GTConfig.Instance.LevelSelection.HighlightEnabled); + m_LevelSelectionTab_HighlightPlayedSongColor .SetValue(GTConfig.Instance.LevelSelection.HighlightPlayed); + m_LevelSelectionTab_HighlightPlayedSongAllColor .SetValue(GTConfig.Instance.LevelSelection.HighlightAllPlayed); + #endregion + + #region PlayerOptions Tab + m_PlayerOptionsTab_ReorderPlayerSettings .SetValue(GTConfig.Instance.PlayerOptions.ReorderPlayerSettings); + m_PlayerOptionsTab_AddOverrideLightIntensityOption .SetValue(GTConfig.Instance.PlayerOptions.OverrideLightIntensityOption); + m_PlayerOptionsTab_MergeLightEffectFilterOptions .SetValue(GTConfig.Instance.PlayerOptions.MergeLightPressetOptions); + #endregion + + #region MainMenu Tab + m_MainMenuTab_OverrideMenuEnvColors .SetValue(GTConfig.Instance.MainMenu.OverrideMenuEnvColors); + m_MainMenuTab_BaseColor .SetValue(GTConfig.Instance.MainMenu.BaseColor); + m_MainMenuTab_LevelClearedColor .SetValue(GTConfig.Instance.MainMenu.LevelClearedColor); + m_MainMenuTab_LevelFailedColor .SetValue(GTConfig.Instance.MainMenu.LevelFailedColor); + m_MainMenuTab_DisableEditorButton .SetValue(GTConfig.Instance.MainMenu.DisableEditorButtonOnMainMenu); + m_MainMenuTab_RemoveNewContentPromotional .SetValue(GTConfig.Instance.MainMenu.RemoveNewContentPromotional); + m_MainMenuTab_DisableFireworks .SetValue(GTConfig.Instance.MainMenu.DisableFireworks); + #endregion + + #region Tools Tab + m_ToolsTab_RemoveOldLogs .SetValue(GTConfig.Instance.Tools.RemoveOldLogs); + m_ToolsTab_LogEntriesToKeep .SetValue(GTConfig.Instance.Tools.LogEntriesToKeep); + m_ToolsTab_FPFCEscape .SetValue(GTConfig.Instance.Tools.FPFCEscape); + #endregion + + m_PreventChanges = false; + + /// Refresh UI + OnSettingChanged(); + } + } +} diff --git a/Modules/BeatSaberPlus_GameTweaker/manifest.json b/Modules/BeatSaberPlus_GameTweaker/manifest.json index fb77d6c..c6a0ea5 100644 --- a/Modules/BeatSaberPlus_GameTweaker/manifest.json +++ b/Modules/BeatSaberPlus_GameTweaker/manifest.json @@ -3,13 +3,12 @@ "id": "BeatSaberPlus_GameTweaker", "name": "BeatSaberPlus_GameTweaker", "author": "HardCPP#1985", - "version": "5.0.7", + "version": "6.0.8", "description": "", - "gameVersion": "1.25.0", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.0.1", - "BeatSaberMarkupLanguage": "^1.3.4", - "BeatSaberPlusCORE": "^5.0.7" + "BSIPA": "^4.3.0", + "BeatSaberPlusCORE": "^6.0.8" }, "links": { "project-home": "https://discord.chatplex.org", diff --git a/Modules/BeatSaberPlus_MenuMusic/Plugin.cs b/Modules/BeatSaberPlus_MenuMusic/BSIPA.cs similarity index 85% rename from Modules/BeatSaberPlus_MenuMusic/Plugin.cs rename to Modules/BeatSaberPlus_MenuMusic/BSIPA.cs index 5ede045..3ac4e59 100644 --- a/Modules/BeatSaberPlus_MenuMusic/Plugin.cs +++ b/Modules/BeatSaberPlus_MenuMusic/BSIPA.cs @@ -6,17 +6,17 @@ namespace BeatSaberPlus_MenuMusic /// Main plugin class /// [Plugin(RuntimeOptions.SingleStartInit)] - public class Plugin + public class BSIPA { /// /// Called when the plugin is first loaded by IPA (either when the game starts or when the plugin is enabled if it starts disabled). /// /// Logger instance [Init] - public Plugin(IPA.Logging.Logger p_Logger) + public BSIPA(IPA.Logging.Logger p_Logger) { /// Setup logger - Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); + ChatPlexMod_MenuMusic.Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); } //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_MenuMusic/BeatSaberPlus_MenuMusic.csproj b/Modules/BeatSaberPlus_MenuMusic/BeatSaberPlus_MenuMusic.csproj index a5ae4d6..2dd3b5d 100644 --- a/Modules/BeatSaberPlus_MenuMusic/BeatSaberPlus_MenuMusic.csproj +++ b/Modules/BeatSaberPlus_MenuMusic/BeatSaberPlus_MenuMusic.csproj @@ -24,15 +24,14 @@ true false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG;BEATSABER prompt 4 true bin\Release\ - - + BEATSABER prompt 4 @@ -47,15 +46,20 @@ OnBuildSuccess - - $(BeatSaberDir)\Plugins\BSML.dll - False - False - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - $(BeatSaberDir)\Libs\Newtonsoft.Json.dll - False - False + ..\..\..\..\..\..\SteamLibrary\steamapps\common\Beat Saber\Libs\Newtonsoft.Json.dll $(BeatSaberDir)\Plugins\SongCore.dll @@ -64,22 +68,10 @@ - - - - - + $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll - False - $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll False @@ -101,12 +93,13 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll False + False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False @@ -123,43 +116,32 @@ False False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll - False - - - - - + + + + + + + + + - - - + + + + - - Settings.cs - - - Player.cs - - - SettingsLeft.cs - - - - - - - + + @@ -167,6 +149,25 @@ BeatSaberPlus + + + + + + + + + + + + + + + + + + + diff --git a/Modules/BeatSaberPlus_MenuMusic/BeatSaberPlus_MenuMusic.csproj.user b/Modules/BeatSaberPlus_MenuMusic/BeatSaberPlus_MenuMusic.csproj.user index 5db8d9ee60929239d9779dc6a24adfdc0c01779f..012781eeb9f58b5f0dd22267773d93a2d711944f 100644 GIT binary patch delta 25 fcmaFI@`q)^I!0av215ot1|tSbAZa*xE#pA|UV#Rx delta 11 Tcmeyv@{VQ0I>yO+7!LpdA+QBv diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/CustomMusicProvider.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/CustomMusicProvider.cs new file mode 100644 index 0000000..10cdf98 --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/CustomMusicProvider.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace ChatPlexMod_MenuMusic.Data +{ + /// + /// Custom music provider + /// + public class CustomMusicProvider : IMusicProvider + { + private List m_Musics = new List(); + private bool m_IsLoading = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override MusicProviderType.E Type => MusicProviderType.E.CustomMusic; + public override bool IsReady => !m_IsLoading; + public override bool SupportPlayIt => false; + public override List Musics => m_Musics; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public CustomMusicProvider() + { + m_IsLoading = true; + CP_SDK.Unity.MTCoroutineStarter.Start(Coroutine_Load()); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Per game implementation of the Play It button + /// + /// Target music + public override bool StartGameSpecificGamePlay(Music p_Music) + { + return false; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Load game songs + /// + /// + private IEnumerator Coroutine_Load() + { + yield return null; + + try + { + var l_BaseDirectory = $"UserData/{CP_SDK.ChatPlexSDK.ProductName}/MenuMusic/CustomMusic"; + var l_Files = new List(); + + if (!Directory.Exists(l_BaseDirectory)) + Directory.CreateDirectory(l_BaseDirectory); + + l_Files.AddRange(Directory.GetFiles(l_BaseDirectory, "*.ogg").Union(Directory.GetFiles(l_BaseDirectory, "*.egg"))); + + foreach (var l_File in l_Files) + { + var l_FixedFile = Path.GetFullPath(l_File); + var l_PathWithoutExtension = Path.Combine(Path.GetDirectoryName(l_FixedFile), Path.GetFileNameWithoutExtension(l_FixedFile)); + var l_CoverPath = null as string; + + if (File.Exists(l_PathWithoutExtension + ".jpg")) + l_CoverPath = l_PathWithoutExtension + ".jpg"; + else if (File.Exists(l_PathWithoutExtension + ".png")) + l_CoverPath = l_PathWithoutExtension + ".png"; + + m_Musics.Add(new Music( + this, + l_FixedFile, + l_CoverPath, + Path.GetFileNameWithoutExtension(l_FixedFile), + " " + )); + } + } + catch (Exception p_Exception) + { + Logger.Instance.Error("[ChatPlexMod_MenuMusic.Data][CustomMusicProvider.Coroutine_Load] GetSongsInDirectory"); + Logger.Instance.Error(p_Exception); + } + + m_IsLoading = false; + } + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/EMusicProviderType.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/EMusicProviderType.cs new file mode 100644 index 0000000..ede0f3d --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/EMusicProviderType.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace ChatPlexMod_MenuMusic.Data +{ + public static class MusicProviderType + { + public enum E + { + GameMusic, + CustomMusic, + //GamePlaylistMusic, + } + + public static List S = new List() + { + "Game Music", + "Custom Music", + //"Game Playlist Music" + }; + + public static int ValueCount => S.Count; + + public static int ToInt(string p_Str) + => Mathf.Clamp(S.IndexOf(p_Str), 0, ValueCount - 1); + public static int ToInt(E p_Enum) + => Mathf.Clamp((int)p_Enum, 0, ValueCount - 1); + + public static E ToEnum(string p_Str) + => (E)ToInt(p_Str); + public static E ToEnum(int p_Int) + => (E)Mathf.Clamp(p_Int, 0, ValueCount - 1); + + public static string ToStr(int p_Int) + => S[Mathf.Clamp(p_Int, 0, ValueCount - 1)]; + public static string ToStr(E p_Enum) + => S[Mathf.Clamp((int)p_Enum, 0, ValueCount - 1)]; + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/GameMusicProvider.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/GameMusicProvider.cs new file mode 100644 index 0000000..0175015 --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/GameMusicProvider.cs @@ -0,0 +1,124 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_MenuMusic.Data +{ + /// + /// Game music provider + /// + public class GameMusicProvider : IMusicProvider + { + private List m_Musics = new List(); + private bool m_IsLoading = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override MusicProviderType.E Type => MusicProviderType.E.GameMusic; + public override bool IsReady => !m_IsLoading; +#if BEATSABER + public override bool SupportPlayIt => true; +#else +#error Missing game implementation +#endif + public override List Musics => m_Musics; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + public GameMusicProvider() + { + m_IsLoading = true; + CP_SDK.Unity.MTCoroutineStarter.Start(Coroutine_LoadGameSongs()); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Per game implementation of the Play It button + /// + /// Target music + public override bool StartGameSpecificGamePlay(Music p_Music) + { +#if BEATSABER + var l_CustomPreviewBeatmapLevel = null as CustomPreviewBeatmapLevel; + try + { + if (p_Music != null) + { + var l_FullPath = p_Music.GetSongPath(); + if (l_FullPath.Contains("Beat Saber_Data/CustomLevels/")) + { + var l_RelativeFolder = l_FullPath.Substring(l_FullPath.IndexOf("Beat Saber_Data/CustomLevels/") + "Beat Saber_Data/CustomLevels/".Length); + if (l_RelativeFolder.Contains("/")) + l_RelativeFolder = l_RelativeFolder.Substring(0, l_RelativeFolder.LastIndexOf("/")); + + l_CustomPreviewBeatmapLevel = SongCore.Loader.CustomLevels.Where(x => x.Value.customLevelPath.Contains(l_RelativeFolder)).Select(x => x.Value).FirstOrDefault(); + } + } + } + catch (System.Exception) + { + + } + + if (l_CustomPreviewBeatmapLevel == null) + return false; + + BeatSaberPlus.SDK.Game.LevelSelection.FilterToSpecificSong(l_CustomPreviewBeatmapLevel); + return true; +#else +#error Missing game implementation +#endif + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Load game songs + /// + /// + private IEnumerator Coroutine_LoadGameSongs() + { +#if BEATSABER + yield return new WaitUntil(() => !SongCore.Loader.AreSongsLoading && SongCore.Loader.CustomLevels.Count > 0); + + foreach (var l_Current in SongCore.Loader.CustomLevels) + { + var l_Extension = Path.GetExtension(l_Current.Value.standardLevelInfoSaveData.songFilename).ToLower(); + if (l_Extension != ".egg" && l_Extension != ".ogg") + continue; + + m_Musics.Add(new Music( + this, + Path.Combine("Beat Saber_Data\\CustomLevels\\", l_Current.Value.customLevelPath, l_Current.Value.standardLevelInfoSaveData.songFilename), + Path.Combine("Beat Saber_Data\\CustomLevels\\", l_Current.Value.customLevelPath, l_Current.Value.standardLevelInfoSaveData.coverImageFilename), + l_Current.Value.songName, + l_Current.Value.songAuthorName + )); + } + + for (var l_I = 0; l_I < m_Musics.Count; ++l_I) + { + var l_Swapped = m_Musics[l_I]; + var l_NewIndex = Random.Range(l_I, m_Musics.Count); + + m_Musics[l_I] = m_Musics[l_NewIndex]; + m_Musics[l_NewIndex] = l_Swapped; + } +#else +#error Missing game implementation +#endif + + m_IsLoading = false; + } + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/IMusicProvider.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/IMusicProvider.cs new file mode 100644 index 0000000..b8acc4e --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/IMusicProvider.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace ChatPlexMod_MenuMusic.Data +{ + /// + /// Music provider interface + /// + public abstract class IMusicProvider + { + public abstract MusicProviderType.E Type { get; } + public abstract bool IsReady { get; } + public abstract bool SupportPlayIt { get; } + public abstract List Musics { get; } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Per game implementation of the Play It button + /// + /// Target music + public abstract bool StartGameSpecificGamePlay(Music p_Music); + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/Music.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/Music.cs new file mode 100644 index 0000000..85e42f3 --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Data/Music.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections; +using System.IO; +using System.Linq; +using System.Reflection; +using UnityEngine; +using UnityEngine.Networking; + +namespace ChatPlexMod_MenuMusic.Data +{ + /// + /// Generic music entry + /// + public class Music + { + private IMusicProvider m_MusicProvider; + private string m_SongPath; + private string m_SongCoverPath; + private string m_SongName; + private string m_SongArtist; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public IMusicProvider MusicProvider => m_MusicProvider; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// + /// Path to the music file + /// Path to the music cover file + /// Name of the song + /// Artist of the song + public Music(IMusicProvider p_MusicProvider, string p_SongPath, string p_SongCoverPath, string p_SongName, string p_SongArtist) + { + m_MusicProvider = p_MusicProvider; + m_SongPath = p_SongPath.Replace('\\', '/'); + m_SongCoverPath = p_SongCoverPath?.Replace('\\', '/'); + m_SongName = p_SongName.Trim(); + m_SongArtist = p_SongArtist.Trim(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get song path + /// + /// + public string GetSongPath() + => m_SongPath; + /// + /// Get cover path + /// + /// + public string GetCoverPath() + => m_SongCoverPath; + /// + /// Get song name + /// + /// + public string GetSongName() + => m_SongName; + /// + /// Get song name + /// + /// + public string GetSongArtist() + => m_SongArtist; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get audio clip async + /// + /// Cancellation token + /// On success callback + /// On error callback + public void GetAudioAsync(CP_SDK.Misc.FastCancellationToken p_Token, Action p_OnSuccess, Action p_OnError) + => CP_SDK.Unity.MTCoroutineStarter.EnqueueFromThread(Coroutine_GetAudioAsync(p_Token, p_OnSuccess, p_OnError)); + /// + /// Get cover async + /// + /// Cancellation token + /// On success callback + /// On error callback + public void GetCoverBytesAsync(CP_SDK.Misc.FastCancellationToken p_Token, Action p_OnSuccess, Action p_OnError) + { + var l_StartSerial = p_Token.Serial; + CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => + { + if (p_Token.IsCancelled(l_StartSerial)) + return; + + try + { + var l_Bytes = null as byte[]; + if (File.Exists(m_SongCoverPath)) + l_Bytes = File.ReadAllBytes(m_SongCoverPath); + else + l_Bytes = CP_SDK.Misc.Resources.FromRelPath(Assembly.GetExecutingAssembly(), "ChatPlexMod_MenuMusic.Resources.DefaultCover.png"); + + if (p_Token.IsCancelled(l_StartSerial)) + return; + + p_OnSuccess?.Invoke(l_Bytes); + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[ChatPlexMod_MenuMusic.Data][Music.GetCoverAsync] Error:"); + Logger.Instance.Error(l_Exception); + p_OnError?.Invoke(); + } + }); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get audio clip async + /// + /// Cancellation token + /// On success callback + /// On error callback + private IEnumerator Coroutine_GetAudioAsync(CP_SDK.Misc.FastCancellationToken p_Token, Action p_OnSuccess, Action p_OnError) + { + var l_StartSerial = p_Token.Serial; + + yield return new WaitForEndOfFrame(); + + if (p_Token.IsCancelled(l_StartSerial)) + yield break; + + var l_PathParts = m_SongPath.Split('/'); + var l_SafePath = string.Join("/", l_PathParts.Select(x => x == l_PathParts[0] ? x : System.Uri.EscapeUriString(x)).ToArray()); + var l_FinalURL = "file://" + l_SafePath.Replace("#", "%23"); + + yield return new WaitForEndOfFrame(); + + UnityWebRequest l_Loader = UnityWebRequestMultimedia.GetAudioClip(l_FinalURL, AudioType.OGGVORBIS); + yield return l_Loader.SendWebRequest(); + + /// Skip if it's not the menu + if (CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + yield break; + + if (p_Token.IsCancelled(l_StartSerial)) + yield break; + + if (l_Loader.isNetworkError + || l_Loader.isHttpError + || !string.IsNullOrEmpty(l_Loader.error)) + { + Logger.Instance.Error($"[ChatPlexMod_MenuMusic.Data][Music.GetAudioAsync] Can't load audio! {(!string.IsNullOrEmpty(l_Loader.error) ? l_Loader.error : string.Empty)}"); + p_OnError?.Invoke(); + yield break; + } + + var l_AudioClip = null as AudioClip; + try + { + ((DownloadHandlerAudioClip)l_Loader.downloadHandler).streamAudio = true; + + l_AudioClip = DownloadHandlerAudioClip.GetContent(l_Loader); + if (l_AudioClip != null) + l_AudioClip.name = m_SongName; + else + { + Logger.Instance.Error("[ChatPlexMod_MenuMusic.Data][Music.GetAudioAsync] No audio found"); + p_OnError?.Invoke(); + yield break; + } + } + catch (Exception p_Exception) + { + Logger.Instance.Error("[ChatPlexMod_MenuMusic.Data][Music.GetAudioAsync] Can't load audio! Exception:"); + Logger.Instance.Error(p_Exception); + + p_OnError?.Invoke(); + yield break; + } + + var l_RemainingTry = 15; + var l_Waiter = new WaitForSecondsRealtime(0.1f); + + while ( l_AudioClip.loadState != AudioDataLoadState.Loaded + && l_AudioClip.loadState != AudioDataLoadState.Failed) + { + yield return l_Waiter; + l_RemainingTry--; + + if (l_RemainingTry < 0) + { + p_OnError?.Invoke(); + yield break; + } + + if (p_Token.IsCancelled(l_StartSerial)) + yield break; + } + + if (CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + yield break; + + if (p_Token.IsCancelled(l_StartSerial)) + yield break; + + if (l_AudioClip.loadState != AudioDataLoadState.Loaded) + { + p_OnError?.Invoke(); + yield break; + } + + p_OnSuccess(l_AudioClip); + } + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/Logger.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Logger.cs similarity index 86% rename from Modules/BeatSaberPlus_MenuMusic/Logger.cs rename to Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Logger.cs index e81285c..ea0b005 100644 --- a/Modules/BeatSaberPlus_MenuMusic/Logger.cs +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Logger.cs @@ -1,4 +1,4 @@ -namespace BeatSaberPlus_MenuMusic +namespace ChatPlexMod_MenuMusic { /// /// Logger instance holder diff --git a/Modules/BeatSaberPlus_MenuMusic/MMConfig.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/MMConfig.cs similarity index 85% rename from Modules/BeatSaberPlus_MenuMusic/MMConfig.cs rename to Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/MMConfig.cs index fad4111..2db217c 100644 --- a/Modules/BeatSaberPlus_MenuMusic/MMConfig.cs +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/MMConfig.cs @@ -1,16 +1,18 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using UnityEngine; -namespace BeatSaberPlus_MenuMusic +namespace ChatPlexMod_MenuMusic { internal class MMConfig : CP_SDK.Config.JsonConfig { [JsonProperty] internal bool Enabled = false; + [JsonProperty, JsonConverter(typeof(StringEnumConverter))] + internal Data.MusicProviderType.E MusicProvider = Data.MusicProviderType.E.GameMusic; + [JsonProperty] internal bool ShowPlayer = true; - [JsonProperty] internal bool ShowPlayTime = true; - [JsonProperty] internal Color BackgroundColor = new Color(0f, 0f, 0f, 0.5f); [JsonProperty] internal bool StartSongFromBeginning = false; [JsonProperty] internal bool StartANewMusicOnSceneChange = true; [JsonProperty] internal bool LoopCurrentMusic = false; diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/MenuMusic.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/MenuMusic.cs new file mode 100644 index 0000000..6e8b192 --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/MenuMusic.cs @@ -0,0 +1,679 @@ +using CP_SDK.Chat.Interfaces; +using System; +using System.Collections; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_MenuMusic +{ + /// + /// Menu Music Module + /// + internal class MenuMusic : CP_SDK.ModuleBase + { + public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; + public override string Name => "Menu Music"; + public override string Description => "Replace boring ambient noise by music!"; + public override string DocumentationURL => "https://github.com/hardcpp/BeatSaberPlus/wiki#menu-music"; + public override bool UseChatFeatures => false; + public override bool IsEnabled { get => MMConfig.Instance.Enabled; set { MMConfig.Instance.Enabled = value; MMConfig.Instance.Save(); } } + public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private UI.SettingsMainView m_SettingsMainView = null; + private UI.SettingsLeftView m_SettingsLeftView = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + + private CP_SDK.UI.Components.CFloatingPanel m_PlayerFloatingPanel = null; + private UI.PlayerFloatingPanel m_PlayerFloatingPanelView = null; + + private Coroutine m_CreateFloatingPlayerCoroutine = null; + private Coroutine m_WaitAndPlayNextSongCoroutine = null; + + private bool m_WantsToQuit = false; + private SongPreviewPlayer m_PreviewPlayer = null; + private AudioClip m_OriginalMenuMusic = null; + private float m_OriginalAmbientVolumeScale = 1f; + private Data.Music m_CurrentMusic = null; + private AudioClip m_CurrentMusicAudioClip = null; + private AudioClip m_BackupTimeClip = null; + private float m_BackupTime = 0f; + private bool m_IsPaused = false; + + private Data.IMusicProvider m_MusicProvider = new Data.GameMusicProvider(); + private int m_CurrentSongIndex = 0; + private Coroutine m_WaitUntillReadyCoroutine = null; + private CP_SDK.Misc.FastCancellationToken m_FastCancellationToken = new CP_SDK.Misc.FastCancellationToken(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + internal float CurrentDuration => m_CurrentMusicAudioClip?.length ?? 0; + internal float CurrentPosition => (m_CurrentMusicAudioClip == m_BackupTimeClip) ? m_BackupTime : 0; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Enable the Module + /// + protected override void OnEnable() + { + /// Bind event + CP_SDK.ChatPlexSDK.OnGenericSceneChange += ChatPlexSDK_OnGenericSceneChange; + CP_SDK.Chat.Service.Discrete_OnTextMessageReceived += ChatService_Discrete_OnTextMessageReceived; + Application.wantsToQuit += Application_wantsToQuit; + + /// Create CustomMenuSongs directory if not existing + try + { + if (!Directory.Exists("CustomMenuSongs")) + Directory.CreateDirectory("CustomMenuSongs"); + } + catch + { + + } + + /// Try to find existing preview player + m_PreviewPlayer = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + + /// Backup original settings + if (m_PreviewPlayer != null) + { + m_OriginalMenuMusic = m_PreviewPlayer._defaultAudioClip; + m_OriginalAmbientVolumeScale = m_PreviewPlayer._ambientVolumeScale; + } + + UpdateMusicProvider(); + + /// Enable at start if in menu + if (CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Menu) + ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.EGenericScene.Menu); + } + /// + /// Disable the Module + /// + protected override void OnDisable() + { + /// Unbind event + Application.wantsToQuit -= Application_wantsToQuit; + CP_SDK.Chat.Service.Discrete_OnTextMessageReceived -= ChatService_Discrete_OnTextMessageReceived; + CP_SDK.ChatPlexSDK.OnGenericSceneChange -= ChatPlexSDK_OnGenericSceneChange; + + /// Stop wait and play next song coroutine + if (m_WaitAndPlayNextSongCoroutine != null) + { + CP_SDK.Unity.MTCoroutineStarter.Stop(m_WaitAndPlayNextSongCoroutine); + m_WaitAndPlayNextSongCoroutine = null; + } + + /// Destroy floating window + DestroyFloatingPlayer(); + + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsLeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsMainView); + + /// Restore original settings + if (!m_WantsToQuit && m_PreviewPlayer != null && m_OriginalMenuMusic != null) + { + m_PreviewPlayer._defaultAudioClip = m_OriginalMenuMusic; + m_PreviewPlayer._ambientVolumeScale = m_OriginalAmbientVolumeScale; + m_PreviewPlayer.CrossfadeToDefault(); + } + else if (m_WantsToQuit && m_PreviewPlayer) + m_PreviewPlayer.PauseCurrentChannel(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get Module settings UI + /// + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() + { + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsLeftView == null) m_SettingsLeftView = CP_SDK.UI.UISystem.CreateViewController(); + + /// Change main view + return (m_SettingsMainView, m_SettingsLeftView, null); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When the active scene change + /// + /// Scene type + private void ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.EGenericScene p_Scene) + { + /// Skip if it's not the menu + if (p_Scene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + { + if (m_PreviewPlayer != null && m_PreviewPlayer && m_OriginalMenuMusic != null && m_OriginalMenuMusic) + m_PreviewPlayer._defaultAudioClip = m_OriginalMenuMusic; + + DestroyFloatingPlayer(); + return; + } + + /// Create player window + if (MMConfig.Instance.ShowPlayer) + CreateFloatingPlayer(); + + m_PreviewPlayer._ambientVolumeScale = 0f; + m_PreviewPlayer._volumeScale = 0f; + + /// Start a new music + if (MMConfig.Instance.StartANewMusicOnSceneChange) + StartNewMusic(false, true); + else + LoadNextMusic(true); + } + /// + /// On text message received + /// + /// Chat service + /// Chat message + private void ChatService_Discrete_OnTextMessageReceived(IChatService p_Service, IChatMessage p_Message) + { + if (p_Message.Message.Length < 2 || p_Message.Message[0] != '!') + return; + + string l_LMessage = p_Message.Message.ToLower(); + if (l_LMessage.StartsWith("!menumusic")) + p_Service.SendTextMessage(p_Message.Channel, $"!: @{p_Message.Sender.DisplayName} current song: {m_CurrentMusic?.GetSongArtist().Replace(".", " . ")} - {m_CurrentMusic?.GetSongName().Replace(".", " . ")}"); + } + /// + /// Application wants to quit + /// + /// + private bool Application_wantsToQuit() + { + m_WantsToQuit = true; + if (m_PreviewPlayer) + m_PreviewPlayer.PauseCurrentChannel(); + return true; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Update the music provider + /// + internal void UpdateMusicProvider() + { + switch (MMConfig.Instance.MusicProvider) + { + case Data.MusicProviderType.E.CustomMusic: + m_MusicProvider = new Data.CustomMusicProvider(); + break; + + default: + m_MusicProvider = new Data.GameMusicProvider(); + break; + } + + StartNewMusic(); + } + /// + /// Update playback volume + /// + /// From config? + internal void UpdatePlaybackVolume(bool p_FromConfig) + { + if (m_PreviewPlayer == null || !m_PreviewPlayer) + return; + + if (CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Audio Tweaker")) + { + var l_ChannelsController = m_PreviewPlayer._audioSourceControllers; + if (l_ChannelsController != null) + { + for (var l_I = 0; l_I < l_ChannelsController.Length; ++l_I) + { + var l_ChannelController = l_ChannelsController[l_I]; + var l_Channel = l_ChannelController.audioSource; + if (l_Channel.isPlaying && l_Channel.clip == m_CurrentMusicAudioClip) + { + m_PreviewPlayer._ambientVolumeScale = 1.0f; + m_PreviewPlayer._volumeScale = 1.0f; + } + } + } + } + else + { + m_PreviewPlayer._ambientVolumeScale = MMConfig.Instance.PlaybackVolume; + m_PreviewPlayer._volumeScale = MMConfig.Instance.PlaybackVolume; + } + + if (p_FromConfig && m_PlayerFloatingPanelView != null && m_PlayerFloatingPanelView) + m_PlayerFloatingPanelView.UpdateVolume(); + + if (!p_FromConfig && m_SettingsMainView && UI.SettingsMainView.CanBeUpdated) + m_SettingsMainView.UpdateVolume(); + + MMConfig.Instance.Save(); + } + /// + /// Update player + /// + internal void UpdatePlayer() + { + if (MMConfig.Instance.ShowPlayer && (m_PlayerFloatingPanel == null || !m_PlayerFloatingPanel)) + CreateFloatingPlayer(); + else if (!MMConfig.Instance.ShowPlayer && m_PlayerFloatingPanel != null && m_PlayerFloatingPanel) + DestroyFloatingPlayer(); + + if (m_PlayerFloatingPanel != null && m_PlayerFloatingPanel) + m_PlayerFloatingPanelView.UpdateText(); + } + /// + /// Toggle pause status + /// + internal void TogglePause() + { + m_IsPaused = !m_IsPaused; + + if (m_PlayerFloatingPanelView) + m_PlayerFloatingPanelView.SetIsPaused(m_IsPaused); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create floating player window + /// + private void CreateFloatingPlayer() + { + if ((m_PlayerFloatingPanel != null && m_PlayerFloatingPanel) || m_CreateFloatingPlayerCoroutine != null) + return; + + m_CreateFloatingPlayerCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(CreateFloatingPlayer_Coroutine()); + } + /// + /// Create floating player window + /// + private IEnumerator CreateFloatingPlayer_Coroutine() + { + if (m_PlayerFloatingPanel != null) + { + m_CreateFloatingPlayerCoroutine = null; + yield break; + } + + GameObject l_ScreenContainer = null; + + var l_Waiter = new WaitForSeconds(0.25f); + while (true) + { + l_ScreenContainer = Resources.FindObjectsOfTypeAll().FirstOrDefault(x => x.name == "ScreenContainer" && x.activeInHierarchy); + + if (l_ScreenContainer != null && l_ScreenContainer) + break; + + yield return l_Waiter; + } + + var l_PlayerPosition = new Vector3(-140.0f, 55.0f, 0f); + if (IPA.Loader.PluginManager.GetPluginFromId("BetterSongSearch") != null) + l_PlayerPosition.y = 62; + + try + { + m_PlayerFloatingPanel = CP_SDK.UI.UISystem.FloatingPanelFactory.Create("ChatPlexMod_MenuMusic", l_ScreenContainer.transform); + m_PlayerFloatingPanel.SetSize(new Vector2(80.0f, 20.0f)); + m_PlayerFloatingPanel.SetRadius(140.0f); + m_PlayerFloatingPanel.SetTransformDirect(l_PlayerPosition, new Vector3(0.0f, 0.0f, 0.0f)); + m_PlayerFloatingPanel.SetBackground(false); + m_PlayerFloatingPanel.RTransform.localScale = Vector3.one; + + m_PlayerFloatingPanelView = CP_SDK.UI.UISystem.CreateViewController(); + m_PlayerFloatingPanel.SetViewController(m_PlayerFloatingPanelView); + m_PlayerFloatingPanel.SetGearIcon(CP_SDK.UI.Components.CFloatingPanel.ECorner.TopRight); + m_PlayerFloatingPanel.OnGearIcon((_) => + { + var l_Items = GetSettingsViewControllers(); + CP_SDK.UI.FlowCoordinators.MainFlowCoordinator.Instance().Present(); + CP_SDK.UI.FlowCoordinators.MainFlowCoordinator.Instance().ChangeViewControllers(l_Items.Item1, l_Items.Item2, l_Items.Item3); + }); + + m_PlayerFloatingPanelView.SetIsPaused(m_IsPaused); + m_PlayerFloatingPanelView.OnMusicChanged(m_CurrentMusic); + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[MenuMusic] Failed to CreateFloatingPlayer"); + Logger.Instance.Error(l_Exception); + } + + m_CreateFloatingPlayerCoroutine = null; + } + /// + /// Destroy floating player window + /// + private void DestroyFloatingPlayer() + { + try + { + if (m_CreateFloatingPlayerCoroutine != null) + { + CP_SDK.Unity.MTCoroutineStarter.Stop(m_CreateFloatingPlayerCoroutine); + m_CreateFloatingPlayerCoroutine = null; + } + + CP_SDK.UI.UISystem.DestroyUI(ref m_PlayerFloatingPanel, ref m_PlayerFloatingPanelView); + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[MenuMusic] Failed to DestroyFloatingPlayer"); + Logger.Instance.Error(l_Exception); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Start a previous music + /// + internal void StartPreviousMusic() + { + if (!m_MusicProvider.IsReady) + { + if (m_WaitUntillReadyCoroutine != null) + CP_SDK.Unity.MTCoroutineStarter.Stop(m_WaitUntillReadyCoroutine); + + m_WaitUntillReadyCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(Coroutine_WaitUntilReady(() => StartPreviousMusic())); + return; + } + + /// Decrement for next song + m_CurrentSongIndex--; + + /// Handle overflow + if (m_CurrentSongIndex < 0) m_CurrentSongIndex = m_MusicProvider.Musics.Count - 1; + if (m_CurrentSongIndex >= m_MusicProvider.Musics.Count) m_CurrentSongIndex = 0; + + /// Load and play audio clip + LoadNextMusic(false); + } + /// + /// Start a new music + /// + /// Pick a random song? + /// On scene transition? + internal void StartNewMusic(bool p_Random = false, bool p_OnSceneTransition = false) + { + if (!m_MusicProvider.IsReady) + { + if (m_WaitUntillReadyCoroutine != null) + CP_SDK.Unity.MTCoroutineStarter.Stop(m_WaitUntillReadyCoroutine); + + m_WaitUntillReadyCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(Coroutine_WaitUntilReady(() => StartNewMusic(p_Random, p_OnSceneTransition))); + return; + } + + if (p_Random) + { + System.Random l_Random = new System.Random(Environment.TickCount); + m_CurrentSongIndex = l_Random.Next(0, m_MusicProvider.Musics.Count); + } + else + m_CurrentSongIndex++; + + /// Load and play audio clip + LoadNextMusic(p_OnSceneTransition); + } + /// + /// Start a next music + /// + internal void StartNextMusic() + { + if (!m_MusicProvider.IsReady) + { + if (m_WaitUntillReadyCoroutine != null) + CP_SDK.Unity.MTCoroutineStarter.Stop(m_WaitUntillReadyCoroutine); + + m_WaitUntillReadyCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(Coroutine_WaitUntilReady(() => StartNextMusic())); + return; + } + + /// Increment for next song + m_CurrentSongIndex++; + + /// Load and play audio clip + LoadNextMusic(false); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Load the next music + /// + /// Is on scene transition? + private void LoadNextMusic(bool p_OnSceneTransition) + { + if (m_MusicProvider.Musics.Count == 0) + return; + + /// Handle overflow + if (m_CurrentSongIndex < 0) m_CurrentSongIndex = m_MusicProvider.Musics.Count - 1; + if (m_CurrentSongIndex >= m_MusicProvider.Musics.Count) m_CurrentSongIndex = 0; + + var l_MusictoLoad = m_MusicProvider.Musics[m_CurrentSongIndex]; + + m_FastCancellationToken.Cancel(); + l_MusictoLoad.GetAudioAsync(m_FastCancellationToken, (p_AudioClip) => { + CP_SDK.Unity.MTCoroutineStarter.EnqueueFromThread(Coroutine_LoadAudioClip(p_OnSceneTransition, l_MusictoLoad, p_AudioClip)); + }, () => StartNextMusic()); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Wait until music provider is ready + /// + /// Callback action + /// + private IEnumerator Coroutine_WaitUntilReady(Action p_Callback) + { + yield return new WaitUntil(() => m_MusicProvider != null && m_MusicProvider.IsReady); + p_Callback?.Invoke(); + } + /// + /// Load the song into the preview player + /// + /// On scene transition? + /// + private IEnumerator Coroutine_LoadAudioClip(bool p_OnSceneTransition, Data.Music p_Music, AudioClip p_AudioClip) + { + if (m_WaitAndPlayNextSongCoroutine != null) + { + CP_SDK.Unity.MTCoroutineStarter.Stop(m_WaitAndPlayNextSongCoroutine); + m_WaitAndPlayNextSongCoroutine = null; + } + + /// Skip if it's not the menu + if (CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + yield break; + + yield return new WaitUntil(() => m_PreviewPlayer = Resources.FindObjectsOfTypeAll().First()); + + if (p_OnSceneTransition) + { + if (m_PreviewPlayer) + m_PreviewPlayer.FadeOut(m_PreviewPlayer._crossFadeToDefaultSpeed); + + yield return new WaitForSeconds(2f); + } + + /// Skip if it's not the menu + if (CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + yield break; + + m_CurrentMusic = p_Music; + m_CurrentMusicAudioClip = p_AudioClip; + + if (m_PreviewPlayer != null && m_PreviewPlayer && m_CurrentMusicAudioClip != null) + { + /// Wait that the song is loaded in background + while (m_CurrentMusicAudioClip.loadState != AudioDataLoadState.Loaded + && m_CurrentMusicAudioClip.loadState != AudioDataLoadState.Failed) + { + yield return null; + } + + /// Check if we changed scene during loading + if (!m_PreviewPlayer || CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + yield break; + + if (m_CurrentMusicAudioClip.loadState == AudioDataLoadState.Loaded) + { + bool l_Failed = false; + + try + { + if (m_WaitAndPlayNextSongCoroutine != null) + { + CP_SDK.Unity.MTCoroutineStarter.Stop(m_WaitAndPlayNextSongCoroutine); + m_WaitAndPlayNextSongCoroutine = null; + } + + m_PreviewPlayer._defaultAudioClip = m_CurrentMusicAudioClip; + + var l_Volume = MMConfig.Instance.PlaybackVolume; + if (CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Audio Tweaker")) + l_Volume = 1.0f; + + m_PreviewPlayer._ambientVolumeScale = l_Volume; + m_PreviewPlayer._volumeScale = l_Volume; + + float l_StartTime = (MMConfig.Instance.StartSongFromBeginning || m_CurrentMusicAudioClip.length < 60) ? 0f : Mathf.Max(UnityEngine.Random.Range(m_CurrentMusicAudioClip.length * 0.2f, m_CurrentMusicAudioClip.length * 0.8f), 0.0f); + + m_PreviewPlayer.CrossfadeTo(m_CurrentMusicAudioClip, l_Volume, l_StartTime, -1f, () => { }); + + m_BackupTimeClip = m_CurrentMusicAudioClip; + m_BackupTime = l_StartTime; + + if (m_PlayerFloatingPanelView != null) + m_PlayerFloatingPanelView.OnMusicChanged(p_Music); + + m_WaitAndPlayNextSongCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(WaitAndPlayNextMusic(m_CurrentMusicAudioClip.length)); + } + catch (Exception p_Exception) + { + Logger.Instance.Error("Can't play audio! Exception: "); + Logger.Instance.Error(p_Exception); + + l_Failed = true; + } + + if (l_Failed) + { + /// Wait until next try + yield return new WaitForSeconds(2f); + + /// Try next music if loading failed + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Menu) + StartNextMusic(); + + yield break; + } + } + /// Try next music if loading failed + else + StartNextMusic(); + } + } + /// + /// Wait and play next music + /// + /// Time to wait + /// + private IEnumerator WaitAndPlayNextMusic(float p_EndTime) + { + var l_Interval = 1f / 16f; + var l_Waiter = new WaitForSeconds(l_Interval); + + do + { + /// Skip if it's not the menu + if (CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu || !m_PreviewPlayer) + { + m_WaitAndPlayNextSongCoroutine = null; + yield break; + } + + var l_ChannelsController = m_PreviewPlayer._audioSourceControllers; + if (l_ChannelsController != null) + { + for (var l_I = 0; l_I < l_ChannelsController.Length; ++l_I) + { + var l_ChannelController = l_ChannelsController[l_I]; + var l_Channel = l_ChannelController.audioSource; + + if (!m_IsPaused && !l_Channel.isPlaying && l_Channel.clip == m_CurrentMusicAudioClip && l_ChannelsController.IndexOf(l_ChannelController) == m_PreviewPlayer._activeChannel) + l_Channel.UnPause(); + + if (l_Channel.isPlaying && l_Channel.clip == m_CurrentMusicAudioClip) + { + if (m_BackupTimeClip == null || m_BackupTimeClip != m_CurrentMusicAudioClip) + { + m_BackupTimeClip = m_CurrentMusicAudioClip; + m_BackupTime = l_Channel.time; + } + else if (Mathf.Abs(l_Channel.time - m_BackupTime) > 1f) + l_Channel.time = m_BackupTime; + else + m_BackupTime = l_Channel.time; + + if (m_IsPaused) + l_Channel.Pause(); + else + { + var l_Volume = MMConfig.Instance.PlaybackVolume; + if (CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Audio Tweaker")) + l_Volume = 1.0f; + + m_PreviewPlayer._ambientVolumeScale = l_Volume; + m_PreviewPlayer._volumeScale = l_Volume; + } + + if (Mathf.Abs(p_EndTime - l_Channel.time) < (MMConfig.Instance.LoopCurrentMusic ? l_Interval : 3f)) + { + m_WaitAndPlayNextSongCoroutine = null; + if (MMConfig.Instance.LoopCurrentMusic && l_Channel.clip.length >= 10f) + { + l_Channel.time = 0f; + m_BackupTime = 0f; + } + else + { + StartNextMusic(); + yield break; + } + } + } + } + } + + yield return l_Waiter; + + } while (true); + } + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/BackgroundMask.png b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/BackgroundMask.png new file mode 100644 index 0000000000000000000000000000000000000000..157138704aac10ee00456232f9a4134724775674 GIT binary patch literal 1399 zcmeAS@N?(olHy`uVBq!ia0y~yU{(OKPjD~;N%g=|4Iot-;1lBdpEdw!JRXB*&u!Zc zRH0T9GpJS45_&F=KAZRCIf-ChrDbmG9tb*%dklg9ZwlpirA3OS~Pry8z(Fm(yPv2EdL*iuOx9O79`wC}`^iMt=b`0x$`CES+u{7k|tD()XW7<3M z#soFBdV|CN83T+ZrCfb!hZgY}R15r)5=WnMNQ7pk#Z zvM@S=%-cuU;W)f^hHDciQ&R)bwKZ^GuPNbz2=#D+!sBw&9e$8!_Osq(g;?iCbZ`M} zO<3p+@-d@hf)6;HTBg@psW33KF-kmpB=j!+(GQ245swrJ1pcAe3Ev@ho1w*0;;rqs z@3JHs@Ep!RU2y5s!fBm;ksB4B*LSS$X??h9mCGZ8^I!io2=B;LV3ZNyXkcbyEO1a@ z;NxI9z}U!Op&-D(&cgJ-z)2xaxZ{BSc7}SkLr3?pH53Q_5%sX@&tmu?+A%>faKR&2 zUWRlrwN%B-l|GTo2WDPkb6O&KzR#55i)-kj4v)$Dk5YLVY*%l6uw>NA%b34} z&YX0MZP7`8-&QAvI<4nQ3b8jFN=~${{GiIcAo!h)Xi#84oXEBbCf3uq=O{7shHkB! z>ABXw@k#5op5>jEAy12Rx&_z1)9%`r|BjV^US{Oft7hM(sKh?m$@`?FZNigNr+W-P zKH2ln{PV(ld0XXD$`^lU{1$$TZ=>Xk$4$+t`m_Jus?bW<)~_evd*->8Z|l>_MY3Ds z)HG+#dE@1|+fb!*`l^}oX0;}bTRJn8jxCw}pQVXWTCXCgW>cE@G3NMNRa{Gc@G2)XJ9r`oYet z?;o}2*5kQL`Ld=mFmU;Jx;TbZ+}1+_m_8e6<=KT z_U7({i8Ef6)z*KP`Y!(KSI^hfeYUSpKV5f!C1>5{tkA3XR3bC>=69RLo?pBp>iY5A z=FLxU|N0WY%CIm07is+Z?Aa3PgW_t_akY` zdV3Rx`*ZEmuSv>3yE<{f#rk%w&NYRHQ?}+b+>2iKs4^vH)*6Wm0S1yS6E{BoF>ArX zgtWHCjom+#9i0y@ke~Nn%QwxrHJ8P zHK$d`>TLE0dzXePpVK(`_8w!!`-G`adQ!q@4CyG2^>?`2AKTFwdZv5Yn$4ZB6B0_CBvVs_c!bmS;luu;QW<$6%miD zrEIpYP0Lx=d1}(srHWCdA=hlL zJMI_JovahC#rx~{pVH#U?&~k>e!X90ew6*@)yr%CuaCIHlKD^fVd;6_3+Fv2KPXyK zdTIKGnZmbg4E}z4bbi8D#d&Q}MM}x1)LvX)wDZl`U5h7f-@-5S|HZ^7i(c;h!~8Y7 V)MHsW8>nr~;OXk;vd$@?2>>#PAgTZW literal 0 HcmV?d00001 diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/DefaultCover.png b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/DefaultCover.png new file mode 100644 index 0000000000000000000000000000000000000000..ee5b9155402421bcd120625dfb7aa8558c484455 GIT binary patch literal 20065 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 IM6N<$f@{<*DgXcg literal 0 HcmV?d00001 diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Glass.png b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Glass.png new file mode 100644 index 0000000000000000000000000000000000000000..fef66729b3c9314c689cd7cb2c0ed73f929d414f GIT binary patch literal 46494 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 UIJP~?Y76)?H?}mQ8M=i35A>lkhyVZp literal 0 HcmV?d00001 diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Next.png b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Next.png new file mode 100644 index 0000000000000000000000000000000000000000..a3e7253359fa058cc0819e30614b79e916f21ec8 GIT binary patch literal 1048 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&6$SW&xB}_#-@ktZgWtb@|M>Ca z@87>)zkdDy|NpOFzdnEd{OQxDKY#vw00STcs1PXi@87>KU%mj@5OE+Is2C^+Gy$j= zs0(N&Q0nK;pFjrC1RxtI1ylyq`<(F-$ad9|AirRSg5K_g?8vg;sZH&%bEZ97`{m8v zO)HmHUwd}zSly|Gm(J~&bmx$amYgF41G9mri(^Q|t+%%_i)K3rv^`|wWj?%5Sy9D% z)BfM_hW49oBnf%WowsV`k_vtY+dR`W&5Udac%7|vZqrGD#R}Y)J7iK%NiwYZcvbn; zms?C3c1gZ`Tz)?SP4}9u7qZtbW`At|C_L9ozvOWGUd2q-eOGwS^Gm$_xYTbK=TEuv ztN#2JkJecV=P`u6O|H7*)uX23sd-ISZhg{~gwRQzVTLWbJ2cOoU?}lo*|1uazag)X z`+(^K#sJ+}|JKRY-M;sM!C~24rVC4a7>?c1WN3f%oxSJ7@^x-38>B_DnGQtH>7I7M zZoxywh7fPHgz%$242OWKt{!4pVD880acmB|gLo%HS?LjG1<$)o2JAKrw@k{p9HtsE zys_RYa6X&+DD#6=F2Wn6dH5SntITQGd)tPAZ*nxls!nzXp{_cn-Oi_&6;}2JGu+yo z#Mq!G!1yNXF!O_2!M6=}RTa7ptT?rV;rBvChUIC?=P(s;B!5^nHJD+N+?idT+gd)Y zI%>?2tMQxjiD~*%afUC)j2V{SFlKnG6yvm0;j=ZPg5;DOrcb{P7&H7nVa%{M-$D5F zw19gz%^w^#f6%*^`TuQ}toyBj5h%sqblgLVK%vTz(x3l(j&%ap5J6A)*5|ekQdy^vSe08 zgOz%F!*?HH{s4_44hy>(X)HHFmnyY4SS8i@njimiJvCyw&A;qLPdH~p_LQ1e#i;&# zpPbIOdDYTKlQ?&w9V$x%by@j=vUi&DqefFKyF8`7*&=DENE2mN};OnSPCf1uHPKF?hQA KxvXvOEF;qA)IiG`~k|RA5o=8IRBO+xy#ruYDTT z)$E$rW>@1oK5k`aRZY*2&KY&iA5||Q+DW;svZ9tE06;~lo%Kq|mq@}BRc;zy5@+55 z{bJOUZx47w>hxE>f9e5ED9CNW64o)-;Vr2QKKHQk#+y^LJb7L~!fA9f^h1)I8A24y zSW;c5$Q>=K;(b%;&zPXd7`m?&$FoZ>?DtVNUj9Hidy@!k9hw<>Q2%gzZ%o|NoA>p69BW@T6K32ge4h7*jZ~_*fweJ4IP?M+ zt`E#R2=w1<_>rMr0Kh*#0_JV_17hGwR5-lw6r6vD6z%Q^)Wdb45A z=Tm22_>b66zSB69^6Q5ncwjgxv=k8 zK?Nhn`)g-w@iV{Dt4((SZ{XrXMaD=^?1#0xEV&_^?C%vpBH^kE{$4$I}ri%-=o zp^a?wn{Sg2d+!!^A2@$l2=D?OemBj12;=bGg#JvZrSa?d@_%mVU_a!CzC z_`iC3R7!H9dV&C6oY?nGVBfFGb}yU2eq{<9OhId8!WulaB@;>8{*8(`d|_>CfLW~w zZwqNlY%J*kw;rTFcBa@V3Lh_gs{ZPUw34kiGK?{YS=uP}%Gky#ZwU!WIUy9G1wrui z&)a0j$G;U9Hwd$NhP5ERRq<}XG@ znMpm(xYUGK%S-+HbxK^0y4J;mC1O4e>t1>V5fM1%N@Z#nm3|RnRxO=XA=PaUTU*mI zLtN}k$mCO9_9gjF%T1qc?!6T|QOqFYk5^KEws^4A)u1jm)l0R7M$Uhecb8rkgp4Pb zW!JO?9_+=8ERJek>N1C!94cQqp28_*A@c6qgToxIHEg(SA)SVZBX($gLpT4HPrm1; zZhyNQe;#i>0E|u%N2_;~Yw0b|YwSeG$Zb#({z2N#CN36*f@r@NKU+Ve(PU>-u1YOE zqo*?;%0TrV-cqCQU{yYAZ_Ua6@Sq6mSAN=jDFXwI@sfPjDnsFNYF5w*E2nhA_!_Nq z4yg#3uZA_7f-ER7k}EE z<0c^r{d%#G~;n(sst_?Yz|6XrL1n5 z?H}x<2-yP}dF`wPe|jRF?-Y=#bh<>XPt|snIhT*}1#yqBKjdWY@gzxeDd^rUQ4u^* zY0k6tQWfFrw6c)SoVhdaPzNSP16m>eO7~Dx7QrN1+n10gxB*tlgu^M!x6`fC_ z@l+)Nx;ez1A4f-B!~z4CqQx4$g#cY&Ck8rUc8WPy9aO#svGA{)SjO_J z#Cc>0k2|S_c=g);5k_r9LK!c~(m0J{m0KOAOJ99DJFZs1sXeDY!}WUGhbe_S0_f)^ zLCYtaFQE%xt(j(PGic~Khez(t%r!tN&Z*9csA_B-Ek=42uQNI~S?3kVi1Z3-tKGnk znrx=`P9HDis*k|g%Ssxak z95rOeva`i0W;A)A!mmbJ!w8OCi-k1bU{QXFdhp1z>;zSi7e@i1dcxFfmU?k`!?cN} z%@_vSrgJEWR;gtXOWrtSYTkq_go*DBd1xbzJWHtf5I5h0Kv#ndZV@$; zrgLdBMxD~k1bq@vv2th>t?$bs>|`V`G}Kd;F-kU7y0KyR<5W2dfhCQc&)!m}UIHTO z%Sj<8!glD{>(akM%5&&f8VO?+fLUHx!@gbs&V|&pzP?*Rs5)aMd7{XWUZ8QM>N3aU=(n=h}cJZu_2X* z10u)a;Rq63tr??s8BZo`Yb%ux0)u&@&u0wWuX$@nIHZUG92vlECI z*J7Qv;gi0rtbZ|V2pbe$>#xUrylSoc2>W6-W#%kbJhV?Q-GgSR3u@M6bRk6-f{Zwm zLj~Fa_qbXX!yh&wqN=>i2-B5WRc@Abns$A9aQFEkYeawsjmco_X{X+gn{E4)R6g-x z?p}Q0G!kO%IE1X>EZ`*0%Jldi%R@Ta%)*7N@ToTL6qC3%v7$vM+R73AbxZd0vnb*a z9q$L7i%wP{wnmLMmvN9xYgtdECp9=W^Dd)K%n&rSoC8HR?TvIx@0K^8_?!8Lt5O?n ziBr3*k_ee+C3aSm>}K)8;98aal^Wy#;Mmi~U)H;a6q*mg2#RsI3)E(*J-}JYJbBIhLS8`7PoAqaAZxtYWpd+J8Mu1qAyGJxng z>NH&JH%r>%^foM@A;r30gUA&7lQ&Z~lN%sLRjAArR>%pKrz;y6z5N0+R4EpN>F8id zB9s6OUu&BO9L3C+2_zk;+674_LbKF`jkhHYN>9LqIm0QC?w%+n=_G^R6%TF%QwoZ~ zjOD{#EO@rVH8eFz*638DH#*`dmWK}VITFD?$@~dG`W21yLse%e zLAWR4v96aB2%$yQ#YvDVB)oMD%T#FEp`?wmQL4@MSK@{bMng|URKZPSv7dxVr7l1v zCyxVLTvK&iNu!ixs0p^!$pNwI4FL=QI$SN^IX38D3_6RjEWk|1<%KH*Qjzv6;#@jKp37z&qh^x(Gp?tra zdzaugRB0Q(2{$ZHl%BxF7wN~Yj&NbXw~8xk&hh%mB95c|Uy z6`;OT_m?9QF@-=rNT-xMPaMMG^JA0a!$A|Rybu+XL>`SgVciS?MiQ(=Am(7my9o*j zI^PvB0i9!)ak)Sy3lYi&d>}3&j2BpnN!Q1JJ5$B&`;N$k4d=^)@RP}BgNF?!9YXM8 ziU@%qc$A%rB8In^Cts^Zo_8O27TyoH&1SXR*VNx;f$g2|*FzUB#Ix&Q_E0@^r7JrFoeO8}9Kvi+mx>JPgjMX@93r z&ONfN(Pd>->*EnJzzRC9d<*)kZBK@(r9pY zbnI_fbJ%xrSXigPWv0q`L&KTMf3nZGassw-*}cQ5Hk^7+t6Civ9-igr>pM`<>YbUD z^`LK!uBfkI>%yh78(^0|`{3xuQQ94y)0+L@>SzN<$8e-OEYZOfE-u6cwurP1b#|TNlA_99 z*$ldM21&hT;h8k?W&KGRe;i7!tgdD`8@q7QkQHUK9#G3WTXQR2UtI@&^RXSR?RzwT z4IXZ+Y?ioHaQLUbiJt|saMA++wT+n&zIcO^VEI+!o@&xP%EG+4dL_8)ucpx~@{#J` z)kR9My{Q)&)x!C!_hM@F()1GIAE#V&zcSNkFo1H5)(7A05Akh9~5 z!%ePCiqBgJPvbD*(?lI6Kh*?V{7>bn=_fS0-PWr8VF&lR?due}e?IlbzM7=QlDGVb zZ7uPo%e!m_y|-mkOy_5Mt_60F1^wVbL{TIp=7$CB&nqhlDG|r2ZO&d6v=w>>{@*C8 z?^=iFjQ-B>@uNMj^vbr_!a`1wgC1<_mtEmS>qA$iNaEr`E&EZcpUtw+SoBNx;z~pE zG=>Ltjj)7<)T9dg@z@j}7uU4@zRC|5en{#j_e!@v z#jbDPmzzFc0_+SK&Z)=3qG?@{NqsGvCDlmI}{DQCi(r1yqwI;6$1hL)3%A+=<&ZPQPFF|yBD8z z8AEWM(#Ru@!OTo4H{^|ZJOk{{pP1`b;J}iAvbNTrWIR9kwkj&i&m4oL7N-F(ha(`Sv9FzY^z#&ACAasH_ zb$HYNNkDmP1CRfg(_H)!?Ejb3Bz+KOV8Tj7!jJN<9e@gf)R9~iwG8QWz|q%0;)P=n z<&+@jL7-##VVwO$An_zz1mR+3I?6i33W!3;i06p*+B)`5$^tTK%Fc`nh?flV0t3SG z+e#&JjA-pH|9Wi=zl8#(PImNKZoJgg+>YO0R#tSJchq`+>%1Sk>mI8<>O!vZbeUs7 zGp8T~&0Sq2U*%TWb({1hTEpl{!}HQvYdcYQhj_`iprBo7qoC8_GK=q~_LBY;VHz`* z5Lf#XEE_W|=y;?~i_$8x5JFD{iIYlhPpdngO`|LGH5Uvp4o~So1Swx(BM2Q^4LTGV z{1?#CquRZijF_b3`hJvj_y#JqFa)q0gG_ciY-}KHMpXyE#LU+rgM`Ojh5eLt$L1Fa zvETs?h{ZzM@o=K@9Y`wBfJGrL2OLpK6-x-FPZ z4!#HFO{e~+5cuak`J_PhR7~PR)ale9!Upl+r@bVSf(M1DKq`tOCbtb0{u)T#P|>T$Xz@1JNPu7jh5$f-E>C(K zI96|;^D`k`nE@G%gyNwjT!-2@eIYeL>OF@0O>f3g-&Y?j;5tMQim*h;)f&)VlDT$4 zY24lmk2jO3)L;)*SwC+FkP^>;P`MuWPU3|kd?{+u$~9jc_RvQkOZ*U0a4NzxOBP%g- z?>kqFi;Nhf=Z4xJ%i9qudaU98py@A_j61I5IO>C?AbSQRRLdW9VYtCMSjX3I=Z)#cB#jphznES~^tHKRE8VXrw-U!~dw1@q=lB(3{KyH4L_ zZE{Q3NNs`#G^C#mH5XrnLGheoYwO?3!lSPaC@D%y2`K^{RkETQ5v^2s9~yFQ8sbe# z1KI~7BLYOR`{w+5qLCzrQw26Dn?4)Yc_UXJjOu@Mev5d%zM7vOXP5x81wv*TYO1`< zfdiRAEsj4%XSXB=H>#Qf^f56ztZf{pr=Bz7764xXt4>LdTuH4QqkBmgachm7kwFoM zvzkV6a+m1z9n9b7_Yv$M3dSY%tK#8t-w)C{Q*6c_(i0rc3w>J3!|+5)k5~KIsWULN zbFN2ZHfyBevBj*j)+p95!ff!pL&_c`ukVYis^kpGuj&DRu&q!!hmeJBA|Zx>RHNcP zH)yGD9X!>mYWtR6*~2}2^n5-w=E_ce;I(M6Ho4Tw@#j@0=T?3PrkMs52TB%a`7KX? z|6cND-Lu>LE{8tm-wsXMYt|ud30=)=W7P5x2>O)vC0$tQDCGV*Hsz$w8mBHaV~Iy9 zZUNT4ad^_OZPkvq^wgrNwvb5JD(Glhs1-o#JSz7L&B)Z6qCG$_$G*Z#^lv4LKWol> zm3&#$SR!Ch+Y!udJ}d-R-VQmbK^BW^IRYHjQiYcMMvw6pyE-$sXD>uTVp2)!?)Vic zmDW;W(Ej?wy(}N@Fi3lX9&sbq@4GC|H7BQ|RJzp6Vsl(hQ$(Sh_&6-_lC{b!KU7XO zO^N_qO^w)P={$Qz21d1l#vbfc7h%w5kA@jvFi)Wj40f`sqj@&>*bZ|($jD+f-8l;Y z_8JWcZf!Ft9jCXrJh`~z1r=UR6G$_VvVNU17O;xVI?h1GRGsl0>RFDI{DDkKj88i5 zWazrb)b6%M;^!&y2_ zH!0=Cq^T7(W&jJENQt9w$?>+2bS7^LszN&+P%X@SV#r{=NKovw6)QsV{2sj?|a&&$k2zk#Y=I;H5~kRPW~ z?mWk6=r*xztpVr7&)!{AxJoKEj4Dy*v_FbC9v-Ehgsj+BHs$BYpDX;X0-A7N=VB<= zXvSM_ttg3HeTb-s zl}V{1K$wtL{$cYmzCpoiC&rouz@?^~jVlHUPOYF)6-6r}(_WAh5FH$`22i-2j;p+W z@r}#;^MZ$*O)HH%v8$-{X)j9p2SaxL0UhkD?MT{t%~4siN!niU5PBn)n&-E1Y&da@ zMcTdYzO!T}M=mxo28AcdiI?PKEDVp9H@18J^R9qCp~SG_+@Ytc8#g{s5FHs>J~IPf zThOm}pc0$ZGrcKk#L9&&H&~P@FBiAOV>^4{!t&8YB9O!SD|XO-ZDteqlj8DidBzlh zdwv-{xM*eFwH)4Dnl!6f9|2IVx!B`pB*}r@%`+5>6zVrvGdtfH7RS93l?x#qI0&&{ z4(j9dZ(@ilu8mqEeKVi@RrV&+U5fL$@3OyL>e-0mzg?#i0ptsnfSi-4|G!-a=6}16 zwT09F@8~H6`V%+wpO={JB!l=rFCqNMANGKNfS{nzN~QiD$&Z?}BvB}&7>_1Rl&OLZ z?fApP14Kd+oFD<>fV7Kojv}zA-d;!;PJ2kvdOoGhXd|!efZ8Tt6&QXUO6L0Pe!N3jv?Hf0HKT#7=Zed zNEEebfR7_TqB#m!4A8?atm-v71W0<$#>9U_?*t0_NuVy0FHe2YR2t5K1`{q*K)lYM zpIu)Ti^t74bcD-qez7c;yMQZ5JCjart6LyRQEb2VWuR%BNTXgW%&^#Ior59DtS!F4p9=zdj07A3Ix7XlDS*Lf6>!`MsgGC>$*`o798Qu>=d2~7#`H-hvk3CuM z@p?2|M7<9{`TbFq@5dtwCM*t5#1%wke@PrhA3_EfOaTb|wLemcN@?ac!DK7}qy}sf zjFf=8UP`t?B_Hlo)QeQ|AW%$PCTsq5E`%lP0%1bPd7q+8!0Y^Va18;mnovX?*OU0x zdf)(xr0;-4N-u_z%T*jNNDc|J2+$yU9Dtes!3m+82Y9b>2hhgRBkz|3SN9k97V#OC zN$@dDA2CPXLFwf~5d*kq0@frH8}J-L(8T)*M>h!p``zN5E*F4MP9INVGDQMd*Ble9 zS&a{l2o9S=-O;wyYoP>CCVkh{g(h9{t6~|_eN{;~PTgYwA#%bZZ}`(#w|unct+dCV zk^q_ZRzZ7j3a#9bg2P_jE{?|drlXDhnidVd=oPZovleG|x1*ysm_WqU55U8T^(u`G z78&z#0occ$unrS6D>c22SDR7*|-VAu-)u#z%J?4prmxXEhD>>&>K+R&=OX zFbaE@q)tF|R(t|;Do%dJre>7-^;Y*N zZO<6#Ec!@zl0A=V`Fr~2#TdxxaK^E-LmwY_X%yx&8FBZkDH%Z%<+ey~ z;h-q)w@>4$g8ODgdS;O3TY4QI5KbK6o^+g6>+TAcD?TPN{)yF^tgZZ_0fi@h1IsHH zHF#-6WG)Yd<#q3x3d8l3B@yA8T{H4&w;Q*c?u5Bzv654LzO<6I z6e|#T12>{KkImlPs#_bq?H4YN+4aMDKG^=!w7NruGjz3XGsh==C9}m#JKw!5m-WAz z_M(4dfI7u_*>5{WINH`Hxx=$n9op)rs%yV=haNS>G-nx8d}qhBJLhlR-7<}RHEBHb z>U)dMFR54XP4qU?b~z5o&dzx*k&K!*QvnNB4?2&F>E&+tD1JrFd#_v$!av-O_-gOR zY!ZAmy$yt|$Z~GTJ{~?!bL;}vr(3ipoNIQ;&(enNXqZG_>JYUkC!|2HTGBf#tY#+y z2Z5_?hb{&wOFB#6*~i?pFy+3D*O!OR_*;Q|?DDe<$!;pRrXzJZ%?HPCsh*W%>=|{? z{~hq%%x}s9Cfo3CboF($CUuKwd&4aEx=W6Rx4hrl>4P$(p6*X52pl##Z^6OH3jZl$ zvN8&SME8G-yC@{EjBPMx%&5PGnmZYC7$f zZxYk@+N`3@+i=^yQT!F0ZEn?lGCyG9AMGyDJ6|D>+M|+pGDneK0GtANWF?I z;!UCbqDhd8EYfW&9(%0X+BQ?OW)ALLI%*ONzmsA03f2eAZ=@UoeEEd2oli{Q< zM=FsaQ>eNQ(ATU)t?vxScQku_k4-Qc=0Yj|PoZq1gRJ5bXh=Xn{+~i|G;#fZ3WeF` zHuL{_RKn}f|MjSx{@0^2ZMWbTCrcJaFr;`7oY7AdR>p(?il5I!oHq}Shp>zw5EA4> zMNvZG7Iw{)S5N|1C;(B|iio5$gwlryHH1qff}+yzb~X`Em}Aa3HT4#Hm-~yyGiii?L z=eHZ*9}K@QLj~)1>;v_(2{(+xF&4Y&C3dj~{CeL;&Mj`o!Eh1fk-ARx{I*(WxVBLEBsbV(Sh=JmSL6UJ!ZBbS#V3M2(l z3^=x=miTdmC@4Y_C5Gnv9RED>(a=YkwgfIQy2=x1*``@ zZyLvi^*uonClvG^cxNu2ZDkE{X)XjBI>6eThzlaoZht(%mI99nn;b|B9;#u*@Z=QD z^(LQ612i}RS@#q-q63p`1r$<$+;IGG*nhFI0g7kg<<2r#6bRJ*IJ~JzH?e&v7kVF9 z&->;*ATRs*RZv#WP%KbfmwzV@+25S~`h|t9DM(DBaK2C~^UsHd5fl|20UlQ_fHsgv z6hVhV_6ww$HWoJmO9YFXEI-wo<8Ts;W*d&1%d!(^=@U1Uj~zVf@MkE>xi94jX&u}6 zO^W{$&G~Ckc&d?tf%CiF^hsZur}8`0e?4cmAQA z6T*!GEZA7UPlCj+ol5xi(iHn}!3?9|IvtFHZ)rhi_{srM3_>i}+Mdf%)D~1b;=;1` zif??hy*Ff>rO_dIs>a-X?5-q_zJC5ZfvG&=719#QVwn>WX3RH#twA~sPXXS%n!F)X z;yUca&w;`RMJ`TLGFX#RzEhkTZ|b_zcd4_IelUI?zo%|qaBchNQ|jlx(0*X1u`&Ch%w<=d_RWq&CWLit7V6=3l*nZR|kF|3gs3Da05uA}m@=mMd`a;)y zyL#f~vV5wdr2W?U6Sd3wTzvBi21|`~qEh`$yEWyuSr<)5V-~bJq~R17=5(>nspp1C zpV(q1tl=WAa{AG1wV5o9MpiDOvwq}w%CQ;c4OyPCvLH*D=+GefUd88EbnejTi%2%F zF#maZ0k-G6MkvA01d({eTTi>Abc>2iP}4lS%a7kXQHM0Bp z2$RDr5pGZWUAOdF;Xl`q)dCbQuWVoFdcC)3gfdFBGQ7`YuEufQnL@S4hm1gz7r?s|m25b+a9($dzZ$%kwe*_jb!zfO z63C?I`Cex+Ir~%lM1k<(XX&q3#a(hgw$vRzXor!}}OmBp1Cg*jy1_q8OiH+0*iM z&{~}t(ja66yYMus{Qiy#J%&c@yINguHu20qZAXD2k32^u(W4jV!&Pa^FuA6sVOnu^ z0M%sbs5hu-O*NeP5N!0gx{C>r-kc2U4@MHuCjwMx6T+Xh!PTwF`R^8`%X7YU>6dby z7f&Ex<)*vlihF510XqW>C!lCpJ$OgaMYlysZG4aUx(|W;r@TQ3l0z@4qQ}i;Dpj_o z5bz@8PKb=o=bAv{a%te`{XJ!?|eREmvqZeNin(A|+a}!#fO4i5Eq(yP- zUz>nQy)ptH`qDoK>WO!&@W4^W&L}HSXii2Or}V0N!vPuLNfV1t=1f2|<`hMeJEN@q z$;oJ$uHzRb%~{hMf1}ses_8Vd*2f7lu-2VVIcmU+kylgQo2T1a_Nx_~k@_NHUdWRRp3vIk zkRZH*e0a0lFMQ**Xv_4lw}^<9K*hr}Qlhc}X~g1vj6iK?HDsnL0{RXso~R6|cu$MW zj-~yGWVD6C=SozOhTvRp`;*giKRBe)4WoEla8jgtaXURB*3MCXPmSTUUK4Zj6$_xq z!SRdImiN^asxGM(cG=i&eK*<*pG>kG&y5R_eapMCOLdM+5i%$xTgIYMsq4m%niN9v=rGMfhx$#?x?*FnpD*(in87~T9V=o$^Rx&e6mK=pu#4N3V2XRFZ)poD6 z(oJ$Qeu4<=I7JQ~G7WB1Qyz?>3qjc99v9C_n-GZN#g-s*ZH)HEy{dRM6cmbC2b^YE z$c%1|rU1Nzh*S2-JmIx=+jN${0m|+Hr9#ohkXBV5Wo>RxFc%Pa9Cor`K z2XpKiygXv8p2Uiap(q)ZezUb=NqB0Y*I+LU4ZhA*mUNEEi!zL3UeQ61nPk7p_>5+E z;T1)&ZLpV`FSVtm;W4t*IX+d_4_qhT2wC@&)$_T*)!aO3?L>Wl4-b-6dSzt$(MIY3 zRn$su>Y+-t!t`^0C{IY1m~E&bE38?jtQ|SCmjSk2Rrdu=3_K5uWuInHmqJBBo>*($ zW%7#AjnQLfF#c;I@kD|DjhDnVH}6)+acVkWD%Mr8pA*s54zw278ZmT~7E)`>TdR#s-psOpmbGWTFJpnnd!H+qeCU21*JD!U!`XTtN8853T)EPCX(U0ZXrS_}D_ zP{t8`+9b!7kL@7FrmpbUY%SUgXIfoVK=*uL12 zw?&UeHdr=InE;X~7+ej^uW7%bT)A!8p8p>6=Hc(syQ#ooJg8SzHsgTJEV}-7K_M&1Cc;NDdK;nE|k1;x^STCOIh3BvPj)})# zot4)voyxA1S-COW+P;3b&#wQJ;&tsWAw6KhoJorM3O|5Jo*uOM(w9WiVP>MYb(1OQ z#1EAiuMdS!iOHTb0_^BXI47tU4n0I9;?H|U(peY=i!aIg1@S`v#QRNlVl0Cx!zN*X z1&Sm4xy8WIdUG^~)%Ie0U%X$eCp((M0~WBo_%rErQ8d%JMHy!n#tn%Ir~AE*Gz@%G zL%C2ff!EAr*<9kfyZF!1c@c2^!%Yw{o@FZ}6L^dB5Gr2~1(!+V6;?e_~Z0(ObS1(F7%eZ$! z_Rf`Cu*-5HQ&w$GRG=hTLfMckWS8r`sM& zNcxkROs*^@lpBbzeEE6Pd0YV;{uJ?dTd$(I*ls*F16nwA92R67b48a^iwsW`$rlpnkxEQmndBMh(kX`$R} z#m{%uxru$2Xa?J-)+@Oo$H9<@L^}e5$iO^3SU5Hr>%eeU{CXlFK9>BpM2vu7C{Fga zj0p#g_q3;{&*z+<%}_w{v4?VaRROcDsU)Hkq`A>%K=C9p94JnpGz-n3BRWA}c!zNE zB<%tL2mY8S+}}X0r1DxDY^Jju4io|9y}7*Ct#n#DO1uja(-|_7@laO2iK75Hi*arK zC-6WX(7ip!2sS=0HVRr_2iCLqxm%EUkmxq@8~XveH%BnH3;F%+HDX`gQS!w=29Gn9 z{-p0q$n>lnW?7yuY^>j5?q7o3y#p~CX3}SS(mp2Sk02#7U<@~y09MpjXFK0VTDY{h zpXSwf_QT(a%Y!pBnjs4T*ctK9qyAqS6#OqYzu$ecs}jLh966iHn$r1Xw|}8h4gu_* zH@*(HzuiK*56!)kp(yUGiUfJ}YQW(E}?QWd~3+vUVj8n=2;!AJ=LRVoo2(EOPAG&chP3UCw96a7F1 zjD=55zTRK87 z$U2AwaP8IxK?%hGiFITsp%B!9&m_u#?^WHsLe!e~NNUSE#n!spE+f>A8GecipmQBE zPXIL7+#HPh@szH$x|$%=#|sp}Z2L!qv+E}xy(v)Di`E1OnvgD6;oP9h$YT68VA~fB zHmp<&ovtBDoAWRMpVzl{WKF9C%@r6U2x5Zx{tqDT(&%o)ZjI3keUl)C=C5dh7kE~}|V_4{dvsGI6; z-M1Qo6q=@JvMr|1mh)4F{_Wy+0+riyX>>8kd(iAnX)vWU4DDW3G){|Iv)Z(>+uF0= zER_$QZRSG(X0oL}4DA48c@|~&FKX5e(OjCSqy6k8qh`&s&|@M0YP=62TD(va@I*qh zT6A{?lCq0r0=Ne#{h){NYo_*hT}Om}GZF>aEWFm57_)%txS8R2YD02)Czvp@apq30 z{Kf_qb4xa^ineP!~HbJkW2FIcw4Eh9PLZbUY1N-e$ju9n^7nhtW434&9*;oO|$j#h3|mPgbUQnlEx^R1ZSEEx$By(N@2!(v`7!WBQuz zLh)C8vEyEKAg_m?rEzQ5$oUbDP^y^c-`;8`la}B*`!Q z`<11px8{t67WQ@kpT^lPl#jExXkZi|hgr(2wNI>czeAGBLe8(<= zmS#D7UeVU9GOAKLR<+CZU}vMeTI;l*+`AAeuX6`HkQ2B7GK*FvYp2$VGh|d7T1>k4 z7hMl&AAUU~CP!BNLkR7BqpT&n#cOIH0Pe-y+BI z^Bhj61mLrbdC(7P)G1WgM!$O#Mj~|09uN}B+Aj)rV(i0TjLCY)I48Tq(b zk?=m*c*Y7s@Q$4;3nj5IX;Bd(A##mrLL;7<+$mWYQenNDFz}YdD*$QRC~fN);HN8O z#@w+~f+Z-)(S`E;h&;vA%$dKD8*)+DO$g@+v~ z^sBA-a35^BV8Nj$f1rr*8vz7hmw;Ach$q~|MXEx6wTr~*^+y^RjV*AW3z5wQcAS7r z71>bqP0B5ou0D#HoD5$IyXU~#JdGy%^^pWU&s9$XHSb1?#35Hwi9UA9z7%rQ&1g;z zn9K>R{iDiq%iPP5i*3BqdFP>4CE;~uvC>b(X)SCaXus^`%>WV-uDTWwQbdD%vkZ3C zs8x7}tFDxu55HJ_#B`HjVvO)av=Hc;vAGfVGIwex@=yLs^o0bzM2&+I&%fc(=bfHi z-L6$0i$sCl^1OouXBv)>iD?g;ox68un6o@RkupjYl;4sT;2eojJaM{)dv85?BD-|i z#&H^DeoI53g>JtluFe#brd_LUr^Er;J4B|k0tq=|-Zxu~S|mp7efwQ5XKDg9-jvq= z&6Ta%9h-Y~EoQ%*tUdtHm65G+yfpjACHl5fHbow6IB0$%MYl$_ABNIy(<>VuK+^MI zkDGU|$q+X4rDw{tt$CQdaq9njT#=$q>Kx?MJ2Ad9wAB;+tQ9R?lmoKE3kw;g`7!7e z{R7)daPSGx6r7#jVwIlF3#5Rg&%G$JesOBgcK2C|Q~aBDE3L6$nx}!ix$1F&g3ZfY zsL{jiJa|AhL~amLE2Ye;SYUffd4HPQ2Q}PmtlAFgVjT9-36TQgS*ZgB`%6MS@?4}D zDoMcoFdWEtjkT{Zc@!%OXusfz>R;oaH+sH#(W39~*zjQ?xeGpuMx+J1LtIKQf!(!q z@jehFa-x@V=T)qhEi-QLTK8UdvWi^z(#pBJi@xvpt9wLj2EB~j%{ae zngVB*)SE{0M%daj_kfvb_in$IVtcm3&F0%Fxmir|=aZN^eyee8Nd^++{mDDa zC@R#!*seS^+8I=R7YOLd9Y~>l+FIK+j3UjHVcZgi?%sht+y#}GI$bg5$zyj9VW)gxNo{=F>7#n_ zJu#QEIL(R|inHo-Y2^H-s@A{8?s@5&@g#q8q=Ve1% zUfHp(xqZ1akEu(GruT5Gu}^z|zf7-RBXG5QfYi5xX(M4mc;8Dqk)T<%i zvC9l)9ca3O+ajjh_SeUH-A2R0BNS%H%Fj}Wc4EA+ZN$z7@Tv>PllwHANd%_~N<+ipQ<`7VE{SfmbM zbYF@pFkMo(?J(~-5(Ux%LVGif_KBi6mYR;u>@J~R5zL1u9Y>ep}M z{sR?AB|De@KdD^zF(v)40Wn9~1#JfUU*l8kzjMVoHEeA(OZ9L#RHgw5A&@u)fv^CC zg@_Ck&B8jO0SP3^e7G>8f`YKBek6=D25|o3B_cZ!R7Nb92nr|)iU5fHd^xXShCRO% z^KTe0A$L+wXYFH$c4a4h@BjY2J9p;RrXFv5Qgk!lwY|D(6BNXl3K0aTI60C%q;}Bg zkH0f@OE*oMCA=u`9lQmeC`}oO^cAy3^34(6Z7y!0H`D}8;RTpdncUm0>*vJ=M?Uhc zLwVIlyIF|*9|1OYpzaz&6Py*AIASD#iX`*w;6S#bg$X>(!_x(w{ z9RMI2)%}qG(m{z9gU|wT&Jpeh`+;5sF!|>(g4sX{n#M#7Th%*m0ufMZkFrSBd?;Xz zH0`w*<45BBACk@IE(t31+Nq$RuUUa`@kZchhvDVVhAThy zUKI}0S;=MtzNx_QuRt(``}_gemfgGgdE-670{q|LVtL<9oB_zBv+!0?!? zm-(K;D-><{D*&vW`)x(|64hu*XEOdj0AxU$zb0sFK_elM8ZD%G07VxX&O})e<(d-d zGB^~e6GOe$N>Zx3o!#yr9&_*aMDA7=y37iTNcEB zNxOODhoJ#t!C^-M`I7)515ibQh7E(P0AmlGfJ>RU({~eS0k=^2*}J9=4-}5t^x4Ocn1L*G00WbepIR_y+h^lH&d~dB416?gOHc89s4=WFacB&SB-f9L zjOEOiFh!-D6-z*5g|m(&^cODDI1Mv2g0lqxz~DYHjWv2qg5!B8{(5-W#4KAzx0h*7#_0`E@n=xqYoN(!0`| zDozt~#c$%t!Ff_qt4Q{DI!-vf0f_Qr>fSj_oefr$rm45AtWC|GmSj`u-#gBs>+s7h zqkGAz@Z>^=q`|YPR9!^+?FEUrH~{3wg9oaA25;p9PIp|&DMh7|#Dhay_hDT)!5 z$!-%WLda=5n&LZidyuWIV6L97 z_%HGmHK&#&y+SlwmZ-+;ug0NdY;9T9#l)NMHz7eJj>^MFUnS+HU`?5`T;#Fln0I-U zSuY0dX9c`uS0Qt|!1QO)RA8+hX6$)}ItIcnmDe+XVkt3zxQ%_|7M@xw%u*}U+=UN-Jlf^YK?Hz2AltkEM6D zxLo-a$DcevtnZ3Uk$|A&Uh}HZR}VFG?d@)zVA{A`qP}4{ML#DZ(f;q~2+cXSf!WTO0)# zN9Ju*w_brY&-dLw)5ldonWZ5kZTvVtZ#uj(E{mjOv)pY9@@2h!#&9b!yTT7Q>i+ub zD?ej*OO4KpNl{h_d+|aQzb;Yp7<;=b#!fh zu(vob?1y5}^%b`h-zx^U>U9)~*SbCLN?zy^xYy4m8jjUn6-S<*<xyerqv9- z-6w>!d6GAq+;J;=f$p4O1!enPaRZ4>YLj9h*a1H$WUW9lMhiz>J_;s7GMEtxxbytJqHM(A*+r6xl)M_Z zDoz&vldSzX1d^by{I9kpuxOKi-831JT90bKD- z&}m3!FQTpIRw>i$(5`Y7rqmm@N>Q$X2DfW}luk(_K+L|`RdaQB*0NsgYZd%6lS~uK@VjuS zWmY>gjFVTyAv!Ld_&ClA*{mEDK~&52-e1egqUObgFalm%R}Z->ciWQd0}tP?w*X`1 zJiU59cq%~RELhEI^LZ|xXD`;n&PI#xQ^d#II~~S{KZpE zzkg>=Q%9S*5OgkwZwPl{BT<0+Z|rCMO>(burz^C+hz*Ko0`Hx9hPaQ1#czzZwNAY| z5J|mPIwqwS$IyDvq1(j}|te$sJ7tRu^M5CYC_wySl34%^;~r z)X&zV;aTK;ov;@K_Q@FSV_^zizyOLH7;eEE^Ob3XgHZL{Rd_)2Bt{QdWo zaxvjn+wc%HikbfUZ&P;ZA)I`>kG;#CB z&2;6OZFAK7HDE*S3)bwP@6aex=Q{k^`aRPK%$@G`Fh!faDGB~&(fUx5_b4GEZ>}e< z`)RY9Vp^baBC@laybEpRNrvS%68RK$Nb-}<6@K!Hbf==NUstX*>~7j%=q@)xQCUVU zTTjU9McAY|rvz9xte8nF0SRDu(vtXsJ+`lIu?k z8|QCLzs2n3qjf1Z-K~t)fC;`X0s{-G5d^{(Ao@TpL4Xqg5<+2PYiDwAb#7!$VRLIP zaBgP`0|IAKOrNhg*#XHtrU+MdRbwJG@<0Y8Xo_J*se%MCK(#<)LF$rYo!-q!p+PjGX7Z_hmB=J(D|k8hj7&Uw!_hxh;g9+urfKdvmXtg9<4E-xaE^gyV(j@J7ABz5mr@($;?YTrYi33~2&>qV2Yq(8nZt z@t>(?_5fqtCnI~8hqo@>z!0(dYY;=xm&NmWi*%VD_j + /// Player floating panel view + /// + internal sealed class PlayerFloatingPanel : CP_SDK.UI.ViewController + { + private XUIHLayout m_MusicBackground = null; + private XUIHLayout m_MusicCover = null; + private XUIText m_SongTitle = null; + private XUIText m_SongArtist = null; + private XUIIconButton m_PlayPauseButton = null; + private XUISlider m_Volume = null; + private XUIPrimaryButton m_PlayItButton = null; + + private CP_SDK.Misc.FastCancellationToken m_CancellationToken = new CP_SDK.Misc.FastCancellationToken(); + private Coroutine m_UpdateCoroutine = null; + private Sprite m_PlaySprite = null; + private Sprite m_PauseSprite = null; + + private Data.Music m_CurrentMusic = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + var l_Assembly = Assembly.GetExecutingAssembly(); + var l_NextSprite = CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromRelPath(l_Assembly, "ChatPlexMod_MenuMusic.Resources.Next.png")); + var l_GlassSprite = CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromRelPath(l_Assembly, "ChatPlexMod_MenuMusic.Resources.Glass.png")); + var l_PauseSprite = CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromRelPath(l_Assembly, "ChatPlexMod_MenuMusic.Resources.Pause.png")); + var l_PlaySprite = CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromRelPath(l_Assembly, "ChatPlexMod_MenuMusic.Resources.Play.png")); + var l_PlaylistSprite = CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromRelPath(l_Assembly, "ChatPlexMod_MenuMusic.Resources.Playlist.png")); + var l_PrevSprite = CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromRelPath(l_Assembly, "ChatPlexMod_MenuMusic.Resources.Prev.png")); + var l_RandSprite = CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromRelPath(l_Assembly, "ChatPlexMod_MenuMusic.Resources.Rand.png")); + var l_SoundSprite = CP_SDK.Unity.SpriteU.CreateFromRawWithBorders(CP_SDK.Misc.Resources.FromRelPath(l_Assembly, "ChatPlexMod_MenuMusic.Resources.Sound.png")); + + m_PlaySprite = l_PlaySprite; + m_PauseSprite = l_PauseSprite; + + Templates.FullRectLayout( + XUIHLayout.Make() + .SetPadding(0).SetSpacing(0) + .SetBackground(true, ColorU.WithAlpha(Color.gray, 0.75f), true) + .OnReady(x => { + x.CSizeFitter.enabled = false; + x.LElement.ignoreLayout = true; + x.RTransform.anchorMin = Vector2.zero; + x.RTransform.anchorMax = Vector2.one; + x.RTransform.sizeDelta = Vector2.zero; + }) + .Bind(ref m_MusicBackground), + + XUIHLayout.Make( + XUIHLayout.Make( + XUIHLayout.Make() + .SetPadding(0).SetSpacing(0) + .SetBackground(true, ColorU.WithAlpha(Color.white, 0.8f)).SetBackgroundSprite(l_GlassSprite) + .OnReady(x => + { + x.CSizeFitter.enabled = false; + x.LElement.ignoreLayout = true; + x.RTransform.anchorMin = Vector2.zero; + x.RTransform.anchorMax = Vector2.one; + x.RTransform.sizeDelta = Vector2.zero; + }) + ) + .SetBackground(true, Color.white) + .SetWidth(18f).SetHeight(18f) + .Bind(ref m_MusicCover), + + XUIVLayout.Make( + XUIText.Make("Song name...") .SetFontSize(3.5f).SetOverflowMode(TMPro.TextOverflowModes.Ellipsis).Bind(ref m_SongTitle), + XUIText.Make("Song artist...") .SetFontSize(3.0f).SetOverflowMode(TMPro.TextOverflowModes.Ellipsis).SetColor(ColorU.ToUnityColor("#A0A0A0")).Bind(ref m_SongArtist), + XUIText.Make(" "), + XUIHLayout.Make( + XUIIconButton.Make(OnPrevPressed) .SetSprite(l_PrevSprite), + XUIIconButton.Make(OnRandPressed) .SetSprite(l_RandSprite), + //XUIIconButton.Make(OnPlaylistPressed) .SetSprite(l_PlaylistSprite), + XUIIconButton.Make(OnPlayPausePressed) .SetSprite(l_PlaySprite).Bind(ref m_PlayPauseButton), + XUIIconButton.Make(OnNextPressed) .SetSprite(l_NextSprite) + ) + .SetPadding(0) + .ForEachDirect(x => x.OnReady((y) => y.RTransform.localScale = 1.5f * Vector3.one)) + ) + .SetPadding(0, 0, 0, 1).SetSpacing(0) + .OnReady(x => { + x.CSizeFitter.enabled = false; + x.LElement.flexibleWidth = 1000.0f; + x.VLayoutGroup.childAlignment = TextAnchor.UpperLeft; + }) + ) + .SetPadding(1).SetSpacing(0) + .OnReady(x => + { + x.HLayoutGroup.childForceExpandWidth = true; + x.HLayoutGroup.childForceExpandHeight = true; + x.CSizeFitter.enabled = false; + x.LElement.ignoreLayout = true; + x.RTransform.anchorMin = Vector2.zero; + x.RTransform.anchorMax = Vector2.one; + }) + ) + .SetPadding(0).SetSpacing(0) + .OnReady(x => { + x.VLayoutGroup.childForceExpandWidth = true; + x.VLayoutGroup.childForceExpandHeight = true; + }) + .BuildUI(transform); + + XUISlider.Make("Volume") + .SetMinValue(0.0f).SetMaxValue(1.0f) + .SetValue(MMConfig.Instance.PlaybackVolume) + .OnValueChanged((_) => OnSettingChanged()) + .SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnReady((x) => + { + x.LElement.enabled = false; + x.RTransform.pivot = new Vector2( 1.00f, 0.00f); + x.RTransform.anchorMin = new Vector2( 1.00f, 0.00f); + x.RTransform.anchorMax = new Vector2( 1.00f, 0.00f); + x.RTransform.anchoredPosition = new Vector2(-10.00f, 1.15f); + x.RTransform.sizeDelta = new Vector2( 35.00f, 5.00f); + x.RTransform.localScale = 0.7f * Vector2.one; + }) + .Bind(ref m_Volume) + .BuildUI(transform); + + XUIPrimaryButton.Make("Play it", OnPlayItPressed) + .OnReady((x) => + { + x.LElement.enabled = false; + x.RTransform.pivot = new Vector2( 1.00f, 0.00f); + x.RTransform.anchorMin = new Vector2( 1.00f, 0.00f); + x.RTransform.anchorMax = new Vector2( 1.00f, 0.00f); + x.RTransform.anchoredPosition = new Vector2(-2.00f, 1.15f); + x.RTransform.localScale = 0.7f * Vector2.one; + }) + .Bind(ref m_PlayItButton) + .BuildUI(transform); + + OnMusicChanged(null); + + if (CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Audio Tweaker")) + m_Volume.SetActive(false); + } + /// + /// On view activation + /// + protected override sealed void OnViewActivation() + { + if (m_UpdateCoroutine == null) + m_UpdateCoroutine = StartCoroutine(UpdateCoroutine()); + + UpdateVolume(); + } + /// + /// On view deactivation + /// + protected override void OnViewDeactivation() + { + if (m_UpdateCoroutine != null) + { + StopCoroutine(m_UpdateCoroutine); + m_UpdateCoroutine = null; + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When settings are changed + /// + private void OnSettingChanged() + { + MMConfig.Instance.PlaybackVolume = m_Volume.Element.GetValue(); + MenuMusic.Instance.UpdatePlaybackVolume(false); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set current played music + /// + /// Current song name + internal void OnMusicChanged(Data.Music p_Music) + { + var l_Str1 = m_SongTitle.Element.TMProUGUI.text = p_Music?.GetSongName() ?? "No name..."; + if (l_Str1.Length > 30) + l_Str1 = l_Str1.Substring(0, 30) + "..."; + + var l_Str2 = m_SongTitle.Element.TMProUGUI.text = p_Music?.GetSongArtist() ?? " "; + if (l_Str2.Length > 50) + l_Str2 = l_Str2.Substring(0, 50) + "..."; + + if (m_SongTitle?.Element?.TMProUGUI) m_SongTitle.Element.TMProUGUI.text = l_Str1; + if (m_SongArtist?.Element?.TMProUGUI) m_SongArtist.Element.TMProUGUI.text = l_Str2; + + m_CancellationToken.Cancel(); + p_Music?.GetCoverBytesAsync(m_CancellationToken, (x) => + { + Utils.ArtProvider.Prepare(x, m_CancellationToken, (p_Cover, p_Background) => + { + m_MusicCover.SetBackgroundSprite(p_Cover); + m_MusicBackground.SetBackgroundSprite(p_Background); + }); + }, null); + + m_CurrentMusic = p_Music; + m_PlayItButton.SetInteractable(m_CurrentMusic?.MusicProvider?.SupportPlayIt ?? false); + } + /// + /// Is the music paused + /// + /// New state + internal void SetIsPaused(bool p_IsPaused) + => m_PlayPauseButton?.SetSprite(p_IsPaused ? m_PlaySprite : m_PauseSprite); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Update status text + /// + internal void UpdateText() + { + /* + if (!UICreated) + return; + + if (MMConfig.Instance.ShowPlayTime) + { + int l_TotalMinutes = (int)(MenuMusic.Instance.CurrentDuration / 60); + int l_TotalSeconds = (int)MenuMusic.Instance.CurrentDuration - (l_TotalMinutes * 60); + + int l_CurrentMinutes = (int)(MenuMusic.Instance.CurrentPosition / 60); + int l_CurrentSeconds = (int)MenuMusic.Instance.CurrentPosition - (l_CurrentMinutes * 60); + + var l_Text = string.Format("{0}:{1} / {2}:{3}", l_CurrentMinutes, l_CurrentSeconds.ToString().PadLeft(2, '0'), l_TotalMinutes, l_TotalSeconds.ToString().PadLeft(2, '0')); + m_SongArtist?.SetText(l_Text); + } + else + m_SongArtist?.SetText(" "); + */ + } + /// + /// Update volume + /// + internal void UpdateVolume() + => m_Volume?.SetValue(MMConfig.Instance.PlaybackVolume, false); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When the previous button is pressed + /// + private void OnPrevPressed() + => MenuMusic.Instance.StartPreviousMusic(); + /// + /// When the random button is pressed + /// + private void OnRandPressed() + => MenuMusic.Instance.StartNewMusic(true); + /// + /// When the playlist button is pressed + /// + private void OnPlaylistPressed() + { + + } + /// + /// When the random button is pressed + /// + private void OnPlayPausePressed() + => MenuMusic.Instance.TogglePause(); + /// + /// When the next button is pressed + /// + private void OnNextPressed() + => MenuMusic.Instance.StartNextMusic(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On play the map pressed + /// + private void OnPlayItPressed() + { + if (m_CurrentMusic == null || !m_CurrentMusic.MusicProvider.SupportPlayIt) + return; + + if (!m_CurrentMusic.MusicProvider.StartGameSpecificGamePlay(m_CurrentMusic)) + ShowMessageModal("Map not found!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Update coroutine + /// + /// + private IEnumerator UpdateCoroutine() + { + var l_Waiter = new WaitForSeconds(1f); + do + { + if (CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Menu) + UpdateText(); + + yield return l_Waiter; + } while (CanBeUpdated); + + m_UpdateCoroutine = null; + } + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/SettingsLeftView.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/SettingsLeftView.cs new file mode 100644 index 0000000..408f029 --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/SettingsLeftView.cs @@ -0,0 +1,87 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace ChatPlexMod_MenuMusic.UI +{ + /// + /// Settings left view + /// + internal sealed class SettingsLeftView : CP_SDK.UI.ViewController + { + private static readonly string s_InformationStr = + "Thanks to Lunikc for original idea" + "\n" + + $"Custom music folder is: UserData/{CP_SDK.ChatPlexSDK.ProductName}/MenuMusic/CustomMusic"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Information / Credits"), + + Templates.ScrollableInfos(50, + XUIText.Make(s_InformationStr) + .SetAlign(TMPro.TextAlignmentOptions.Left) + ), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("Reset", OnResetButton), + XUIPrimaryButton.Make("Reload", OnReloadButton) + ), + Templates.ExpandedButtonsLine( + XUISecondaryButton.Make("Documentation", OnDocumentationButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset button + /// + private void OnResetButton() + { + ShowConfirmationModal("Do you really want to reset\nall Menu Music settings?", (p_Confirm) => + { + if (!p_Confirm) + return; + + MMConfig.Instance.Reset(); + MMConfig.Instance.Enabled = true; + MMConfig.Instance.Save(); + + SettingsMainView.Instance.OnResetButton(); + + OnReloadButton(); + }); + } + /// + /// Reload songs + /// + private void OnReloadButton() + { + MenuMusic.Instance.UpdateMusicProvider(); + + ShowMessageModal("Musics were reload!"); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Documentation button + /// + private void OnDocumentationButton() + { + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL(MenuMusic.Instance.DocumentationURL); + } + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/SettingsMainView.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/SettingsMainView.cs new file mode 100644 index 0000000..e2d617d --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/SettingsMainView.cs @@ -0,0 +1,164 @@ +using CP_SDK.XUI; +using System.Linq; + +namespace ChatPlexMod_MenuMusic.UI +{ + /// + /// Settings main view + /// + internal sealed class SettingsMainView : CP_SDK.UI.ViewController + { + private XUIDropdown m_MusicProvider; + private XUIToggle m_ShowPlayerInterface; + private XUIText m_PlaybackVolumeLabel; + private XUISlider m_PlaybackVolume; + + private XUIToggle m_PlaySongsFromBeginning; + private XUIToggle m_StartANewMusicOnSceneChange; + private XUIToggle m_LoopCurrentMusic; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private bool m_PreventChanges = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + var l_Config = MMConfig.Instance; + + Templates.FullRectLayoutMainView( + Templates.TitleBar("Menu Music | Settings"), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Music provider"), + XUIDropdown.Make() + .SetOptions(Data.MusicProviderType.S).SetValue(Data.MusicProviderType.ToStr(l_Config.MusicProvider)) + .OnValueChanged((_, __) => OnSettingChanged()) + .Bind(ref m_MusicProvider), + + XUIText.Make("Show player interface"), + XUIToggle.Make().SetValue(l_Config.ShowPlayer).Bind(ref m_ShowPlayerInterface), + + XUIText.Make("Playback volume").Bind(ref m_PlaybackVolumeLabel), + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1.0f).SetIncrements(0.01f) + .SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .SetValue(l_Config.PlaybackVolume).Bind(ref m_PlaybackVolume) + ) + .SetSpacing(1) + .SetWidth(60.0f) + .OnReady(x => x.HOrVLayoutGroup.childAlignment = UnityEngine.TextAnchor.UpperCenter) + .ForEachDirect(x => x.SetAlign(TMPro.TextAlignmentOptions.Center)) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())), + + XUIVLayout.Make( + XUIText.Make("Play songs from beginning"), + XUIToggle.Make().SetValue(l_Config.StartSongFromBeginning).Bind(ref m_PlaySongsFromBeginning), + + XUIText.Make("Start a new song on scene change"), + XUIToggle.Make().SetValue(l_Config.StartANewMusicOnSceneChange).Bind(ref m_StartANewMusicOnSceneChange), + + XUIText.Make("Loop current song"), + XUIToggle.Make().SetValue(l_Config.LoopCurrentMusic).Bind(ref m_LoopCurrentMusic) + ) + .SetSpacing(1) + .SetWidth(60.0f) + .OnReady(x => x.HOrVLayoutGroup.childAlignment = UnityEngine.TextAnchor.UpperCenter) + .ForEachDirect(x => x.SetAlign(TMPro.TextAlignmentOptions.Center)) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + + if (CP_SDK.ChatPlexSDK.GetModules().Any(x => x.Name == "Audio Tweaker")) + { + m_PlaybackVolumeLabel.SetActive(false); + m_PlaybackVolume.SetActive(false); + } + } + /// + /// On view activation + /// + protected override void OnViewActivation() + { + m_PreventChanges = true; + m_PlaybackVolume.SetValue(MMConfig.Instance.PlaybackVolume); + m_PreventChanges = false; + } + /// + /// On view deactivation + /// + protected override sealed void OnViewDeactivation() + => MMConfig.Instance.Save(); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When settings are changed + /// + private void OnSettingChanged() + { + if (m_PreventChanges) + return; + + var l_Config = MMConfig.Instance; + var l_OldMusicProvider = l_Config.MusicProvider; + l_Config.MusicProvider = Data.MusicProviderType.ToEnum(m_MusicProvider.Element.GetValue()); + l_Config.ShowPlayer = m_ShowPlayerInterface.Element.GetValue(); + l_Config.PlaybackVolume = m_PlaybackVolume.Element.GetValue(); + + l_Config.StartSongFromBeginning = m_PlaySongsFromBeginning.Element.GetValue(); + l_Config.StartANewMusicOnSceneChange = m_StartANewMusicOnSceneChange.Element.GetValue(); + l_Config.LoopCurrentMusic = m_LoopCurrentMusic.Element.GetValue(); + + /// Update playback volume & player + MenuMusic.Instance.UpdatePlaybackVolume(true); + MenuMusic.Instance.UpdatePlayer(); + + if (l_OldMusicProvider != l_Config.MusicProvider) + MenuMusic.Instance.UpdateMusicProvider(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Update volume + /// + internal void UpdateVolume() + => m_PlaybackVolume?.SetValue(MMConfig.Instance.PlaybackVolume, false); + /// + /// Reset settings + /// + internal void OnResetButton() + { + m_PreventChanges = true; + + var l_Config = MMConfig.Instance; + m_MusicProvider .SetValue(Data.MusicProviderType.ToStr(l_Config.MusicProvider)); + m_ShowPlayerInterface .SetValue(l_Config.ShowPlayer); + m_PlaybackVolume .SetValue(l_Config.PlaybackVolume); + + m_PlaySongsFromBeginning .SetValue(l_Config.StartSongFromBeginning); + m_StartANewMusicOnSceneChange .SetValue(l_Config.StartANewMusicOnSceneChange); + m_LoopCurrentMusic .SetValue(l_Config.LoopCurrentMusic); + + m_PreventChanges = false; + + /// Update playback volume & player + MenuMusic.Instance.UpdatePlaybackVolume(true); + MenuMusic.Instance.UpdatePlayer(); + MenuMusic.Instance.UpdateMusicProvider(); + } + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Utils/ArtProvider.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Utils/ArtProvider.cs new file mode 100644 index 0000000..0dbf2c9 --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Utils/ArtProvider.cs @@ -0,0 +1,122 @@ +using System; +using System.Reflection; +using UnityEngine; + +namespace ChatPlexMod_MenuMusic.Utils +{ + /// + /// Art provider for the player floating panel + /// + internal class ArtProvider + { + private static Color[] m_BackgroundMask = null; + private static Color[] m_CoverMask = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Prepare + /// + /// Input raw byte + /// Cancellation token + /// Result callback + public static void Prepare(byte[] p_RawByte, CP_SDK.Misc.FastCancellationToken p_CancellationToken, Action p_Callback) + => CP_SDK.Unity.MTThreadInvoker.EnqueueOnThread(() => PrepareImpl(p_RawByte, p_CancellationToken, p_Callback)); + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Prepare implementation + /// + /// Input raw byte + /// Cancellation token + /// Result callback + private static void PrepareImpl(byte[] p_RawByte, CP_SDK.Misc.FastCancellationToken p_CancellationToken, Action p_Callback) + { + try + { + var l_StartSerial = p_CancellationToken?.Serial ?? 0; + + if (m_BackgroundMask == null) + { + var l_Bytes = CP_SDK.Misc.Resources.FromRelPath(Assembly.GetExecutingAssembly(), "ChatPlexMod_MenuMusic.Resources.BackgroundMask.png"); + CP_SDK.Unity.TextureRaw.Load(l_Bytes, out _, out _, out m_BackgroundMask); + } + + if (m_CoverMask == null) + { + var l_Bytes = CP_SDK.Misc.Resources.FromRelPath(Assembly.GetExecutingAssembly(), "ChatPlexMod_MenuMusic.Resources.CoverMask.png"); + CP_SDK.Unity.TextureRaw.Load(l_Bytes, out _, out _, out m_CoverMask); + } + + if (p_CancellationToken?.IsCancelled(l_StartSerial) ?? false) + return; + + if (!CP_SDK.Unity.TextureRaw.Load(p_RawByte, out var l_OGWidth, out var l_OGHeight, out var l_OGPixels)) + { + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => p_Callback?.Invoke(null, null)); + return; + } + + var l_CoverSize = new Vector2Int(18 * 4 * 10, 18 * 4 * 10); + var l_BackgroundSize = new Vector2Int(80 * 1 * 10, 20 * 1 * 10); + var l_CoverPixels = CP_SDK.Unity.TextureRaw.ResampleAndCrop(l_OGWidth, l_OGHeight, l_OGPixels, l_CoverSize.x, l_CoverSize.y); + var l_BackgroundPixels = CP_SDK.Unity.TextureRaw.ResampleAndCrop(l_CoverSize.x, l_CoverSize.y, l_CoverPixels, l_BackgroundSize.x, l_BackgroundSize.y); + + if (p_CancellationToken?.IsCancelled(l_StartSerial) ?? false) + return; + + CP_SDK.Unity.TextureRaw.FastGaussianBlur(l_BackgroundSize.x, l_BackgroundSize.y, l_BackgroundPixels, 4); + + if (p_CancellationToken?.IsCancelled(l_StartSerial) ?? false) + return; + + CP_SDK.Unity.TextureRaw.Multiply(l_CoverPixels, m_CoverMask); + CP_SDK.Unity.TextureRaw.Multiply(l_BackgroundPixels, m_BackgroundMask); + + if (p_CancellationToken?.IsCancelled(l_StartSerial) ?? false) + return; + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => + { + try + { + if (p_CancellationToken?.IsCancelled(l_StartSerial) ?? false) + return; + + var l_CoverTexture = new Texture2D(l_CoverSize.x, l_CoverSize.y, TextureFormat.RGBA32, false); + l_CoverTexture.wrapMode = TextureWrapMode.Clamp; + l_CoverTexture.SetPixels(l_CoverPixels); + l_CoverTexture.Apply(true); + + var l_BackgroundTexture = new Texture2D(l_BackgroundSize.x, l_BackgroundSize.y, TextureFormat.RGBA32, false); + l_BackgroundTexture.wrapMode = TextureWrapMode.Clamp; + l_BackgroundTexture.SetPixels(l_BackgroundPixels); + l_BackgroundTexture.Apply(true); + + p_Callback?.Invoke( + CP_SDK.Unity.SpriteU.CreateFromTextureWithBorders(l_CoverTexture), + CP_SDK.Unity.SpriteU.CreateFromTextureWithBorders(l_BackgroundTexture) + ); + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[ChatPlexMod_MenuMusic.Utils][ArtProvider.PrepareImpl] Error:"); + Logger.Instance.Error(l_Exception); + } + }); + + return; + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[ChatPlexMod_MenuMusic.Utils][ArtProvider.PrepareImpl] Error:"); + Logger.Instance.Error(l_Exception); + } + + CP_SDK.Unity.MTMainThreadInvoker.Enqueue(() => p_Callback?.Invoke(null, null)); + } + } +} diff --git a/Modules/BeatSaberPlus_MenuMusic/MenuMusic.cs b/Modules/BeatSaberPlus_MenuMusic/MenuMusic.cs deleted file mode 100644 index 5db7db2..0000000 --- a/Modules/BeatSaberPlus_MenuMusic/MenuMusic.cs +++ /dev/null @@ -1,848 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.FloatingScreen; -using CP_SDK.Chat.Interfaces; -using HMUI; -using IPA.Utilities; -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using UnityEngine; -using UnityEngine.Networking; - -namespace BeatSaberPlus_MenuMusic -{ - /// - /// Menu Music Module - /// - internal class MenuMusic : BeatSaberPlus.SDK.BSPModuleBase - { - /// - /// Module type - /// - public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; - /// - /// Name of the Module - /// - public override string Name => "Menu Music"; - /// - /// Description of the Module - /// - public override string Description => "Replace boring ambient noise by music!"; - /// - /// Is the Module using chat features - /// - public override bool UseChatFeatures => false; - /// - /// Is enabled - /// - public override bool IsEnabled { get => MMConfig.Instance.Enabled; set { MMConfig.Instance.Enabled = value; MMConfig.Instance.Save(); } } - /// - /// Activation kind - /// - public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Saber tweaker view - /// - private UI.Settings m_SettingsView = null; - /// - /// Saber tweaker view - /// - private UI.SettingsLeft m_SettingsLeftView = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Floating screen instance - /// - private FloatingScreen m_PlayerFloatingScreen = null; - /// - /// Floating screen controller - /// - private UI.Player m_PlayerFloatingScreenController; - /// - /// CreateFloatingPlayer coroutine - /// - private Coroutine m_CreateFloatingPlayerCoroutine = null; - /// - /// Wait and play next song coroutine - /// - private Coroutine m_WaitAndPlayNextSongCoroutine = null; - /// - /// Preview music player - /// - private SongPreviewPlayer m_PreviewPlayer = null; - /// - /// Original menu music - /// - private AudioClip m_OriginalMenuMusic = null; - /// - /// Original ambient volume scale - /// - private float m_OriginalAmbientVolumeScale = 1f; - /// - /// All songs - /// - private string[] m_AllSongs = new string[0]; - /// - /// Music to load - /// - private string m_MusicToLoad = ""; - /// - /// Current music - /// - private AudioClip m_CurrentMusic = null; - /// - /// Current song index - /// - private int m_CurrentSongIndex = 0; - /// - /// Backup time clip instance - /// - private AudioClip m_BackupTimeClip = null; - /// - /// Backup time - /// - private float m_BackupTime = 0f; - /// - /// Is music paused - /// - private bool m_IsPaused = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Current play total duration - /// - internal float CurrentDuration => m_CurrentMusic?.length ?? 0; - /// - /// Current play position - /// - internal float CurrentPosition => (m_CurrentMusic == m_BackupTimeClip) ? m_BackupTime : 0; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Enable the Module - /// - protected override void OnEnable() - { - /// Bind event - CP_SDK.ChatPlexSDK.OnGenericSceneChange += ChatPlexSDK_OnGenericSceneChange; - CP_SDK.Chat.Service.Discrete_OnTextMessageReceived += ChatService_Discrete_OnTextMessageReceived; - - /// Create CustomMenuSongs directory if not existing - try - { - if (!Directory.Exists("CustomMenuSongs")) - Directory.CreateDirectory("CustomMenuSongs"); - } - catch - { - - } - - /// Try to find existing preview player - m_PreviewPlayer = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - - /// Backup original settings - if (m_PreviewPlayer != null) - { - m_OriginalMenuMusic = m_PreviewPlayer.GetField("_defaultAudioClip"); - m_OriginalAmbientVolumeScale = m_PreviewPlayer.GetField("_ambientVolumeScale"); - } - - /// Refresh song list - RefreshSongs(); - - /// Enable at start if in menu - if (CP_SDK.ChatPlexSDK.ActiveGenericScene == CP_SDK.ChatPlexSDK.EGenericScene.Menu) - ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.EGenericScene.Menu); - - } - /// - /// Disable the Module - /// - protected override void OnDisable() - { - /// Unbind event - CP_SDK.Chat.Service.Discrete_OnTextMessageReceived -= ChatService_Discrete_OnTextMessageReceived; - CP_SDK.ChatPlexSDK.OnGenericSceneChange -= ChatPlexSDK_OnGenericSceneChange; - - /// Stop wait and play next song coroutine - if (m_WaitAndPlayNextSongCoroutine != null) - { - CP_SDK.Unity.MTCoroutineStarter.Stop(m_WaitAndPlayNextSongCoroutine); - m_WaitAndPlayNextSongCoroutine = null; - } - - /// Destroy floating window - DestroyFloatingPlayer(); - - /// Restore original settings - if (m_PreviewPlayer != null && m_OriginalMenuMusic != null) - { - m_PreviewPlayer.SetField("_defaultAudioClip", m_OriginalMenuMusic); - m_PreviewPlayer.SetField("_ambientVolumeScale", m_OriginalAmbientVolumeScale); - m_PreviewPlayer.CrossfadeToDefault(); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get Module settings UI - /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() - { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsLeftView == null) - m_SettingsLeftView = BeatSaberUI.CreateViewController(); - - /// Change main view - return (m_SettingsView, m_SettingsLeftView, null); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When the active scene change - /// - /// Scene type - private void ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.EGenericScene p_Scene) - { - /// Skip if it's not the menu - if (p_Scene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) - { - if (m_PreviewPlayer != null && m_PreviewPlayer && m_OriginalMenuMusic != null && m_OriginalMenuMusic) - m_PreviewPlayer.SetField("_defaultAudioClip", m_OriginalMenuMusic); - - DestroyFloatingPlayer(); - return; - } - - /// Create player window - if (MMConfig.Instance.ShowPlayer) - CreateFloatingPlayer(); - - m_PreviewPlayer.SetField("_ambientVolumeScale", 0f); - m_PreviewPlayer.SetField("_volumeScale", 0f); - - /// Start a new music - if (MMConfig.Instance.StartANewMusicOnSceneChange) - StartNewMusic(false, true); - else - CP_SDK.Unity.MTCoroutineStarter.Start(LoadAudioClip(true)); - } - /// - /// On text message received - /// - /// Chat service - /// Chat message - private void ChatService_Discrete_OnTextMessageReceived(IChatService p_Service, IChatMessage p_Message) - { - if (p_Message.Message.Length < 2 || p_Message.Message[0] != '!') - return; - - string l_LMessage = p_Message.Message.ToLower(); - if (l_LMessage.StartsWith("!menumusic")) - p_Service.SendTextMessage(p_Message.Channel, $"!: @{p_Message.Sender.DisplayName} current song: {GetCurrenltyPlayingSongName().Replace(".", " . ")}"); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Update playback volume - /// - /// From config? - internal void UpdatePlaybackVolume(bool p_FromConfig) - { - if (m_PreviewPlayer == null || !m_PreviewPlayer) - return; - - m_PreviewPlayer.SetField("_volumeScale", MMConfig.Instance.PlaybackVolume); - - if (p_FromConfig && m_PlayerFloatingScreenController != null && m_PlayerFloatingScreenController) - m_PlayerFloatingScreenController.UpdateVolume(); - - if (!p_FromConfig && m_SettingsView && UI.Settings.CanBeUpdated) - m_SettingsView.OnResetButton(); - - MMConfig.Instance.Save(); - } - /// - /// Update player - /// - internal void UpdatePlayer() - { - if (MMConfig.Instance.ShowPlayer && (m_PlayerFloatingScreen == null || !m_PlayerFloatingScreen)) - CreateFloatingPlayer(); - else if (!MMConfig.Instance.ShowPlayer && m_PlayerFloatingScreen != null && m_PlayerFloatingScreen) - DestroyFloatingPlayer(); - - if (m_PlayerFloatingScreen != null && m_PlayerFloatingScreen) - { - m_PlayerFloatingScreenController.UpdateBackgroundColor(); - m_PlayerFloatingScreenController.UpdateText(); - } - } - /// - /// Toggle pause status - /// - internal void TogglePause() - { - m_IsPaused = !m_IsPaused; - - if (m_PlayerFloatingScreenController) - m_PlayerFloatingScreenController.SetIsPaused(m_IsPaused); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Create floating player window - /// - private void CreateFloatingPlayer() - { - if ((m_PlayerFloatingScreen != null && m_PlayerFloatingScreen) || m_CreateFloatingPlayerCoroutine != null) - return; - - m_CreateFloatingPlayerCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(CreateFloatingPlayer_Coroutine()); - } - /// - /// Create floating player window - /// - private IEnumerator CreateFloatingPlayer_Coroutine() - { - if (m_PlayerFloatingScreen != null) - { - m_CreateFloatingPlayerCoroutine = null; - yield break; - } - - GameObject l_ScreenContainer = null; - - while (true) - { - l_ScreenContainer = Resources.FindObjectsOfTypeAll().FirstOrDefault(x => x.name == "ScreenContainer" && x.activeInHierarchy); - - if (l_ScreenContainer != null && l_ScreenContainer) - break; - - yield return new WaitForSeconds(0.25f); - } - - try - { - Vector2 l_PlayerSize = new Vector2( 120f, 20f); - Vector3 l_PlayerPosition = new Vector3(-140f, 55f, 0f); - Vector3 l_PlayerRotation = new Vector3( 0f, 0f, 0f); - - if (IPA.Loader.PluginManager.GetPluginFromId("BetterSongSearch") != null) - { - l_PlayerPosition.y = 62; - } - - /// Create floating screen - m_PlayerFloatingScreen = FloatingScreen.CreateFloatingScreen(l_PlayerSize, false, Vector3.zero, Quaternion.identity); - m_PlayerFloatingScreen.GetComponent().sortingOrder = 3; - m_PlayerFloatingScreen.GetComponent().SetRadius(150); - m_PlayerFloatingScreen.gameObject.SetActive(true); - - /// Bind floating window to the root game object - m_PlayerFloatingScreen.transform.SetParent(l_ScreenContainer.transform); - - /// Set rotation - m_PlayerFloatingScreen.transform.localPosition = l_PlayerPosition; - m_PlayerFloatingScreen.transform.localScale = Vector3.one; - m_PlayerFloatingScreen.ScreenRotation = Quaternion.Euler(l_PlayerRotation); - - /// Create UI Controller - m_PlayerFloatingScreenController = BeatSaberUI.CreateViewController(); - m_PlayerFloatingScreen.SetRootViewController(m_PlayerFloatingScreenController, HMUI.ViewController.AnimationType.None); - m_PlayerFloatingScreen.GetComponentInChildren().sortingOrder = 4; - m_PlayerFloatingScreenController.SetIsPaused(m_IsPaused); - - /// Update song name - m_PlayerFloatingScreenController.SetPlayingSong(GetCurrenltyPlayingSongName()); - } - catch (System.Exception l_Exception) - { - Logger.Instance.Error("[MenuMusic] Failed to CreateFloatingPlayer"); - Logger.Instance.Error(l_Exception); - } - - m_CreateFloatingPlayerCoroutine = null; - } - /// - /// Destroy floating player window - /// - private void DestroyFloatingPlayer() - { - try - { - if (m_CreateFloatingPlayerCoroutine != null) - { - CP_SDK.Unity.MTCoroutineStarter.Stop(m_CreateFloatingPlayerCoroutine); - m_CreateFloatingPlayerCoroutine = null; - } - - if (m_PlayerFloatingScreen == null) - return; - - /// Destroy objects - GameObject.Destroy(m_PlayerFloatingScreen.gameObject); - - /// Reset variables - m_PlayerFloatingScreenController = null; - m_PlayerFloatingScreen = null; - } - catch (System.Exception l_Exception) - { - Logger.Instance.Error("[MenuMusic] Failed to DestroyFloatingPlayer"); - Logger.Instance.Error(l_Exception); - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Refresh songs - /// - internal void RefreshSongs() - { - try - { - if (MMConfig.Instance.UseOnlyCustomMenuSongsFolder) - m_AllSongs = GetSongsInDirectory("CustomMenuSongs", true).ToArray(); - - if (!MMConfig.Instance.UseOnlyCustomMenuSongsFolder || m_AllSongs.Length == 0) - m_AllSongs = GetSongsInDirectory("Beat Saber_Data\\CustomLevels", false).ToArray(); - - m_CurrentSongIndex = 0; - - System.Random l_Random = new System.Random(Environment.TickCount); - m_AllSongs = m_AllSongs.OrderBy((x) => l_Random.Next()).ToArray(); - } - catch (System.Exception l_Exception) - { - Logger.Instance.Error("[MenuMusic] Failed to refresh songs"); - Logger.Instance.Error(l_Exception); - } - } - /// - /// Start a previous music - /// - internal void StartPreviousMusic() - { - /// Do nothing if no custom song available - if (m_AllSongs.Length == 0) - return; - - /// Decrement for next song - m_CurrentSongIndex--; - - /// Handle overflow - if (m_CurrentSongIndex < 0) - m_CurrentSongIndex = m_AllSongs.Length - 1; - if (m_CurrentSongIndex >= m_AllSongs.Length) - m_CurrentSongIndex = 0; - - /// Select song to load - m_MusicToLoad = m_AllSongs[m_CurrentSongIndex]; - - /// Load and play audio clip - CP_SDK.Unity.MTCoroutineStarter.Start(LoadAudioClip(false)); - } - /// - /// Start a new music - /// - /// Pick a random song? - /// On scene transition? - internal void StartNewMusic(bool p_Random = false, bool p_OnSceneTransition = false) - { - /// Do nothing if no custom song available - if (m_AllSongs.Length == 0) - return; - - if (p_Random) - { - System.Random l_Random = new System.Random(Environment.TickCount); - m_CurrentSongIndex = l_Random.Next(0, m_AllSongs.Length); - } - else - m_CurrentSongIndex++; - - /// Handle overflow - if (m_CurrentSongIndex < 0) - m_CurrentSongIndex = m_AllSongs.Length - 1; - if (m_CurrentSongIndex >= m_AllSongs.Length) - m_CurrentSongIndex = 0; - - /// Select song to load - m_MusicToLoad = m_AllSongs[m_CurrentSongIndex]; - - /// Load and play audio clip - CP_SDK.Unity.MTCoroutineStarter.Start(LoadAudioClip(p_OnSceneTransition)); - } - /// - /// Start a next music - /// - internal void StartNextMusic() - { - /// Do nothing if no custom song available - if (m_AllSongs.Length == 0) - return; - - /// Increment for next song - m_CurrentSongIndex++; - - /// Handle overflow - if (m_CurrentSongIndex < 0) - m_CurrentSongIndex = m_AllSongs.Length - 1; - if (m_CurrentSongIndex >= m_AllSongs.Length) - m_CurrentSongIndex = 0; - - /// Select song to load - m_MusicToLoad = m_AllSongs[m_CurrentSongIndex]; - - /// Load and play audio clip - CP_SDK.Unity.MTCoroutineStarter.Start(LoadAudioClip(false)); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Load the song into the preview player - /// - /// On scene transition? - /// - private IEnumerator LoadAudioClip(bool p_OnSceneTransition) - { - if (m_WaitAndPlayNextSongCoroutine != null) - { - CP_SDK.Unity.MTCoroutineStarter.Stop(m_WaitAndPlayNextSongCoroutine); - m_WaitAndPlayNextSongCoroutine = null; - } - - /// Skip if it's not the menu - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - yield break; - - yield return new WaitUntil(() => m_PreviewPlayer = Resources.FindObjectsOfTypeAll().First()); - - if (p_OnSceneTransition) - { - if (m_PreviewPlayer) - m_PreviewPlayer.FadeOut(m_PreviewPlayer.GetField("_crossFadeToDefaultSpeed")); - - yield return new WaitForSeconds(2f); - } - - ///m_PreviewPlayer.GetField("_audioSources")[m_PreviewPlayer.GetField("_activeChannel")].Stop(); - - UnityWebRequest l_Song = UnityWebRequestMultimedia.GetAudioClip($"{Environment.CurrentDirectory}\\{m_MusicToLoad}", AudioType.OGGVORBIS); - yield return l_Song.SendWebRequest(); - - /// Skip if it's not the menu - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - yield break; - - try - { - ((DownloadHandlerAudioClip)l_Song.downloadHandler).streamAudio = true; - - m_CurrentMusic = DownloadHandlerAudioClip.GetContent(l_Song); - - if (m_CurrentMusic != null) - m_CurrentMusic.name = m_MusicToLoad; - else - { - Logger.Instance.Debug("No audio found!"); - - /// Try next music if loading failed - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - StartNextMusic(); - - yield break; - } - } - catch (Exception p_Exception) - { - Logger.Instance.Error("Can't load audio! Exception: "); - Logger.Instance.Error(p_Exception); - - /// Try next music if loading failed - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - StartNextMusic(); - - yield break; - } - - yield return new WaitUntil(() => m_CurrentMusic); - - /// Skip if it's not the menu - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - yield break; - - if (m_PreviewPlayer != null && m_PreviewPlayer && m_CurrentMusic != null) - { - /// Wait that the song is loaded in background - while (m_CurrentMusic.loadState != AudioDataLoadState.Loaded - && m_CurrentMusic.loadState != AudioDataLoadState.Failed) - { - yield return null; - } - - /// Check if we changed scene during loading - if (!m_PreviewPlayer || BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - yield break; - - if (m_CurrentMusic.loadState == AudioDataLoadState.Loaded) - { - bool l_Failed = false; - - try - { - if (m_WaitAndPlayNextSongCoroutine != null) - { - CP_SDK.Unity.MTCoroutineStarter.Stop(m_WaitAndPlayNextSongCoroutine); - m_WaitAndPlayNextSongCoroutine = null; - } - - m_PreviewPlayer.SetField("_defaultAudioClip", m_CurrentMusic); - - m_PreviewPlayer.SetField("_ambientVolumeScale", MMConfig.Instance.PlaybackVolume); - m_PreviewPlayer.SetField("_volumeScale", MMConfig.Instance.PlaybackVolume); - - float l_StartTime = (MMConfig.Instance.StartSongFromBeginning || m_CurrentMusic.length < 60) ? 0f : Mathf.Max(UnityEngine.Random.Range(m_CurrentMusic.length * 0.2f, m_CurrentMusic.length * 0.8f), 0.0f); - - m_PreviewPlayer.CrossfadeTo(m_CurrentMusic, MMConfig.Instance.PlaybackVolume, l_StartTime, -1f, () => { }); - - m_BackupTimeClip = m_CurrentMusic; - m_BackupTime = l_StartTime; - - if (m_PlayerFloatingScreenController != null) - m_PlayerFloatingScreenController.SetPlayingSong(GetCurrenltyPlayingSongName()); - - m_WaitAndPlayNextSongCoroutine = CP_SDK.Unity.MTCoroutineStarter.Start(WaitAndPlayNextMusic(m_CurrentMusic.length)); - } - catch (Exception p_Exception) - { - Logger.Instance.Error("Can't play audio! Exception: "); - Logger.Instance.Error(p_Exception); - - l_Failed = true; - } - - if (l_Failed) - { - /// Wait until next try - yield return new WaitForSeconds(2f); - - /// Try next music if loading failed - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - StartNextMusic(); - - yield break; - } - } - /// Try next music if loading failed - else - StartNextMusic(); - } - } - /// - /// Wait and play next music - /// - /// Time to wait - /// - private IEnumerator WaitAndPlayNextMusic(float p_EndTime) - { - var l_Field = typeof(SongPreviewPlayer).GetField("_audioSourceControllers", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - var l_AudioSourceField = null as FieldInfo; - var l_Waiter = new WaitForSeconds(1f / 4f); - - do - { - /// Skip if it's not the menu - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Menu || !m_PreviewPlayer) - { - m_WaitAndPlayNextSongCoroutine = null; - yield break; - } - - var l_ChannelsController = l_Field.GetValue(m_PreviewPlayer) as object[]; - if (l_ChannelsController != null) - { - foreach (var l_ChannelController in l_ChannelsController) - { - if (l_AudioSourceField == null) - l_AudioSourceField = l_ChannelController.GetType().GetField("audioSource", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); - - var l_Channel = (AudioSource)l_AudioSourceField.GetValue(l_ChannelController); - - if (!m_IsPaused && !l_Channel.isPlaying && l_Channel.clip == m_CurrentMusic && l_ChannelsController.IndexOf(l_ChannelController) == m_PreviewPlayer.GetField("_activeChannel")) - l_Channel.UnPause(); - - if (l_Channel.isPlaying && l_Channel.clip == m_CurrentMusic) - { - if (m_BackupTimeClip == null || m_BackupTimeClip != m_CurrentMusic) - { - m_BackupTimeClip = m_CurrentMusic; - m_BackupTime = l_Channel.time; - } - else if (Mathf.Abs(l_Channel.time - m_BackupTime) > 1f) - l_Channel.time = m_BackupTime; - else - m_BackupTime = l_Channel.time; - - if (m_IsPaused) - l_Channel.Pause(); - else - { - m_PreviewPlayer.SetField("_ambientVolumeScale", MMConfig.Instance.PlaybackVolume); - m_PreviewPlayer.SetField("_volumeScale", MMConfig.Instance.PlaybackVolume); - } - - if (Mathf.Abs(p_EndTime - l_Channel.time) < 3f) - { - m_WaitAndPlayNextSongCoroutine = null; - if (MMConfig.Instance.LoopCurrentMusic && l_Channel.clip.length >= 10f) - CP_SDK.Unity.MTCoroutineStarter.Start(LoadAudioClip(false)); - else - StartNextMusic(); - yield break; - } - } - } - } - - /// Update 4 time a second - yield return l_Waiter; - - } while (true); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Search songs in a folder - /// - /// Base search directory - /// Include custom menu songs sub directories? - /// - private List GetSongsInDirectory(string p_BaseDirectory, bool p_CustomMenuSongs) - { - List l_Files = new List(); - try - { - if (p_CustomMenuSongs) - { - l_Files.AddRange(Directory.GetFiles(p_BaseDirectory, "*.ogg").Union(Directory.GetFiles(p_BaseDirectory, "*.egg"))); - - foreach (string l_Directory in Directory.GetDirectories(p_BaseDirectory)) - l_Files.AddRange(GetSongsInDirectory(l_Directory, true)); - } - else - { - foreach (string l_Directory in Directory.GetDirectories(p_BaseDirectory)) - l_Files.AddRange(Directory.GetFiles(l_Directory, "*.ogg").Union(Directory.GetFiles(l_Directory, "*.egg"))); - } - } - catch (Exception p_Exception) - { - Logger.Instance.Error("[MenuMusic] GetSongsInDirectory"); - Logger.Instance.Error(p_Exception); - } - - return l_Files; - } - /// - /// Get currently playing song name - /// - /// - private string GetCurrenltyPlayingSongName() - { - string l_Result = "No song playing"; - - try - { - if (m_CurrentMusic != null) - { - if (m_CurrentMusic.name.StartsWith("Beat Saber_Data\\CustomLevels\\")) - { - l_Result = m_CurrentMusic.name.Substring("Beat Saber_Data\\CustomLevels\\".Length); - - if (l_Result.ToLower().Contains("\\")) - l_Result = l_Result.Substring(0, l_Result.LastIndexOf("\\")); - - var l_CustomSong = SongCore.Loader.CustomLevels.Where(x => x.Value.customLevelPath.Contains(l_Result)).Select(x => x.Value).FirstOrDefault(); - - if (l_CustomSong != null) - l_Result = l_CustomSong.levelAuthorName + " - " + l_CustomSong.songName; - else if (l_Result.Contains("(") && l_Result.Contains(")")) - { - l_Result = l_Result.Substring(l_Result.IndexOf("(") + 1); - l_Result = l_Result.Substring(0, l_Result.Length - 1); - } - } - else - l_Result = Path.GetFileName(m_CurrentMusic.name); - } - } - catch (System.Exception) - { - - } - - return l_Result; - } - /// - /// Get currently played CustomPreviewBeatmapLevel - /// - /// - internal CustomPreviewBeatmapLevel GetCurrentlyPlayingSongPreviewBeatmap() - { - try - { - if (m_CurrentMusic != null && m_CurrentMusic.name.StartsWith("Beat Saber_Data\\CustomLevels\\")) - { - var l_Folder = m_CurrentMusic.name.Substring("Beat Saber_Data\\CustomLevels\\".Length); - - if (l_Folder.ToLower().Contains("\\")) - l_Folder = l_Folder.Substring(0, l_Folder.LastIndexOf("\\")); - - var l_CustomSong = SongCore.Loader.CustomLevels.Where(x => x.Value.customLevelPath.Contains(l_Folder)).Select(x => x.Value).FirstOrDefault(); - - return l_CustomSong; - } - } - catch (System.Exception) - { - - } - - return null; - } - } -} diff --git a/Modules/BeatSaberPlus_MenuMusic/Properties/AssemblyInfo.cs b/Modules/BeatSaberPlus_MenuMusic/Properties/AssemblyInfo.cs index fb7152d..e91fdd1 100644 --- a/Modules/BeatSaberPlus_MenuMusic/Properties/AssemblyInfo.cs +++ b/Modules/BeatSaberPlus_MenuMusic/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/Modules/BeatSaberPlus_MenuMusic/Resources/NextIcon.png b/Modules/BeatSaberPlus_MenuMusic/Resources/NextIcon.png deleted file mode 100644 index 8bae5b7e2bcdeceb6c35841c42032ac071b34c74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3282 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 I&yCCZ4alnmmH+?% diff --git a/Modules/BeatSaberPlus_MenuMusic/Resources/Play.png b/Modules/BeatSaberPlus_MenuMusic/Resources/Play.png deleted file mode 100644 index 5e04e4dbfaf9f3a405285561a69ee4d34dc98c26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3415 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 diff --git a/Modules/BeatSaberPlus_MenuMusic/Resources/RefreshIcon.png b/Modules/BeatSaberPlus_MenuMusic/Resources/RefreshIcon.png deleted file mode 100644 index fed6ce34cffe9f6ff7ab77f172be1f507207325a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2015 zcmV<52O#)~P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGizyJUazyWI3i3tDz2Xsk9K~!i%-I{rb zRaG3veap-nof&mpr_!cek<_t}aQwrOiZU`m4F7183JesAGK3b0io$|c=%0kh3QP+( zD3pp!!G^FLm0Uw}Tt-EoV=goEHv4?FFjqEe<8Cf#8(`9ft$=hJ9VGSGzRp06J`PxaIf#={c@+XmP$QaTUl++=D z^usEW(~yrN?}npbe}XJSY6NuoK*bu~3O)`mqH{WtNNflM0-LBiNWKG>^b8c;Ap)sG z%qNfsk<$sJ1{Q3O_HlTQroWP12e>?Po3 zxEYhPD5@}Sdj~l1<7fsB*T}IIMK8c~tOZ~1z*~%a$8a=6FdRZWfWJ2w8dG2(Sz{0M zVlYCo6Gl}8)uxQLvL%_%hT3g`uc5maCaj7)$lDM>x}Kurq~wfX!dG`O>iI7 zvv3ItWnB5{U@%z61)4~HHk?49RnSt%1om`cc2mC;?u435iU-EZ^}GH*i%I_suU~NKQn%Ka=>~Ng)1Ohqan2%b|~O{K<_Gi*fh5Xdg@#V4u!XqcVz-_3Ve=&e^?Rc0T=ujy5mh#Z-Myq zp1Ox1e?+z?L(Yq}=xP<>9T1e(i<*X4A}_$~sm=zdsnd&LuRhz3p+E>~IJ4I)bmpkj zj&y-GCuuUpnEQW0JetwwLCv6%WQc`<4sAyalRYn-Dfn4*{!3@RC#av`5vUo<(drsq zt>h0QtVOUV#vH*Y4!!*=J3Bi^ghHWu($guM3CGdlp-62xvTgu+ar(ZLZTw2=6W(}; zGGICwgjzk8VJa9xa5IrE7Hvh5m3Zy*xqoA1coyqe#zn&~#25Kw8IU6~w6%BkP&#>X25>n@Lu} z$El53ZF{CH&U@9~QeN1~xxH}&cu(i*pOo4a6Fv$bRW;)%(gSQWz7V=k- zj%J}7jjkQz7j$TWDa!&ndeP~?LuUs2>CATI4tN-b$?JfywEezYAkZ<~A z48vyHu0h^Ql^5P=*4#gytE+k55zrO2>eiI1x3lijF3P+y1i8A)bj7X9?-1M-ZG}ID znoRV*@D2!02M>-GgXn5WvIDwyIN~~-K0oLkWHqcE>=4t`n;<|(8z}TvBYHl_a~b%n zjYgyQ;d_!UBzb*w;Jde=oE&Y zAv>FNYqF*rl*yt_HXF(I`1PPzV7DCmPvhsb zpA6GLnL2eA@iH7qQAw)=+G`swf1krkq%A+Gpsby`k4Wm9Zf>BcJ%%l)tH+2Ahyx$!c~0|S#ZbDFQc8l7cC>b6Pb1K1pMd-dKF21SuY+K&}qy}3~IG)>x^>UEqxo+ zA;%&(k>VO;f26(>1)!E#lA3l0P^WK0_3%~N?SVO0D(Gn4xB>R~v8@YvXDe(fkQs0V z>$L`GI~mA1LdUkJkw>8RQLRYrBy~n!!HT|ydVM#oqfQU@5M<8R0J>V?e}&H5NZE5| zy@+AmsYzVPjY!)uo(Xhx=LCJ>i%Ke^a-)(HrtKNHg5*sm40nMa%|f*}cn(K;(dF+| z5m0}jKD5n;IiGLWfKSax4e)Dt92Rvrmvjyo4bb&0u3m&PFy8j6S=JBwtNhGhF~FKRPGV z^s673be49O{2ua6T!^itTVctEFeP`W3+fm`GgcMp!ANa0v=i29LZ3PG(^44f@2hj@ xX!g58XEc+!1`QpP>>KdJ!G@dYV;$<(?>|@9w94R$L1_R0002ovPDHLkV1fk~xg!7o diff --git a/Modules/BeatSaberPlus_MenuMusic/Resources/Settings.png b/Modules/BeatSaberPlus_MenuMusic/Resources/Settings.png deleted file mode 100644 index 36af0aafca2edb161140005170e8efa52b7e64e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8938 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 - - - - - - - - - - - - - - - - - -

de_z&b&+}NISI#S`~vWn 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≫{`w@Y27@!c? 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 Vrw4SSZr%U@002ovPDHLkV1krUqSOEY literal 0 HcmV?d00001 diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Play.png b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Play.png new file mode 100644 index 0000000000000000000000000000000000000000..7f86d2b49baa7b7784bf0053138581dd64ee1e27 GIT binary patch literal 818 zcmV-21I_%2P)C0001QP)t-s0002# z>FMd|>G}Ej_xJbv`}_U<{qpki=jZ42^z`rV@8#v?<>lq&<>lk!+Agd{P*|w=jZ3+gwwI`}^kR=Ire3 z|NsAi6{w&9001O(QchCQ(eHGYejVq<_7tyDPhcCA1ykftOD$Zi`r0cr^30I35xKu#q&K=#S@ zAj<7V&IHuHa-0Bl_M8={gX2s=Qc}_2E5n0e##0G`8C~4eo+}g z4X6rG18N6mz*Gh92aCx4U~%@K1~iWP+3r$#>Swz{0+<00@~7Klf+7U8x_-(<7z-%- z3GDRrrvx+!im-xC*G~zkJrp4z`|-2A1`gxv%kN+QJKw^Sr|AUpznPl`9LlBj+pXXh zkuwZvJS=P}r_bwv-RNLrmL*eymGr)N`NPglGHnS%)zXuvrDBqAF0F&DR=&E8u;wGYa3KZgKI4)g zzhH(VtKFX19MIXbLa=l`$Ii_gW`v)esL1tro4<3~iKgnp7?H~c-R;t1b*4}6Ue8Hf zqW0x;;O)8J85o#udAc};RNQ)dt21x5gTS$mjIW+<*_qSCYuEAK%sn7@Rvvrst0MbA^xSzAf*vQ*Ty2A4NNTRv)Yu=N=izcZY@^T_D~8|Qrc@zOD} zTw4EUY2HDv7t2mO62J34dS9adgXXFHFSnO=9=GXlUlQ7Lpt@RB|AR)pQ&f0~#kOx> zR>(_c%3gf#1J+e%FQGb1Ws)v$&n^e}8Me zH2Be5;W9?2bEYPHggY%YYJ%2z1g!YbIDO)Zsrd`v8YyRXe=2LSx)&tBse4sp)KbZ> z%QBi2CEI|(vvpA>*Q?VTKH0LaFtQR=$+!4kAbg%#DC}m1`m9MSnp^p=bg`Vxu6Z=q z(`TylVJ^=TdTEQ!`7IFs_waVW5@9~Kgd^b-{Un;UDmMDv5RKOndeIX5Q!}5%>fyf4 zlQ>HbZH-aivwR9yiIceOHKw@+9}dj+C~=ZsQ>Af{(>y^{X|4hD@nRRNhxgc&RVDkH z!fX{3rSlkrV&tVBYj+mrp8eCn;`>JCeMF$+re(@oDuRWshlqc*vVHYdt~zdUw3W_FWaG(dDe5GYp6*w?sS0kx+UeD*Xq)aQT zb4{A&Wo)nPc-r4OAXvzBYxb5W;?tLes^+~B{(5kxJExa5uk0)T#Jm!zHpQpv_jGyw UueHzopr02O^6`Tzg` literal 0 HcmV?d00001 diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Prev.png b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Prev.png new file mode 100644 index 0000000000000000000000000000000000000000..683e3a0cc9dd8281a977e1c0834d6d95892268d4 GIT binary patch literal 1091 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&6$SW&xB}_#-@ktZgWtb@|M>Ca z@87>)zkdDy|NpOFzdnEd{OQxDKY#vw00STcs1PXi@87>KU%mj@5OE+Is2C^+Gy$j= zs0(N&Q0nK;pFjrC1RxtI1ylyq`<(F-$ad9|AirRSg5K_g?8vg;sZH&%bEZ97`{m8v zO)HmHUwd}zSly|Gm(J~&bmx$amYgF419P;ei(^Q|tvAy*pspk`x=j4dnw|s zCzHo^Jx0QNxuAKB)ZOcsCTdSit8+azRqbb0o$&Rh>KX4DPBQ=8-dTU-U0`v$&VlO* z;u~gar!ZcWJlx>RxT&G$Nw9za+4l_w3MLY4PtyOX|Fo&FP81JVrmwK8;eK=M0ksAl zh4?03w}@jL%$K;{=uP;}!TgNtaKjR3u>d(E9%iRD9fg$#^%n5xZfbbZk?63KInrT; z0ndl@6vn#Sn*!?F4r>E#;lB}^Fk=F%iflr8qlL4WK?u;YQlL(shMU?QRsyU)g1ZwB z{E?1yh-*%C2vnS4A-;E0K>h)HQx6wCSwo%=rbd}Ii4q1S9A72d7>iq*>RJlBo2(AZ zm)hs3^XL1oZ4F-}S!)m6S+sy(BTD3o@|WK{tY29d$+7-rog~Ox>(Jk3r;vN$0{fhR zi~S#DMQaamoB!Fu{U`H!PMHWxlS0gz!v+lsQgtx_*RIa}UVqf~P2-Na9dl<%@PEFm z8sD|4-6yJK!AFrArq<;Tgf~o9Um*NI`GNXE+gKacdMULzjQS0GzVqy1$ayMR!Fc2B zL16(V{r_5j1MYe53t)Jr8hS@Sym_JUgd4A4O!b-4pmH(n$0u}tt^0v7)% z6UMOYI_H6FdMg-m)(cDseZrdXdas+qIhDHh6bVzNPZdwDS6q^x%a9`)#&FwBogunK zv|(il(}t(IK!f&H?{HV~RQGb2)O#v-*Oo-qYw|BH9bb8}Gr;M@$~X~(~7pGUB8)3PP^gTe~ HDWM4fx;!65 literal 0 HcmV?d00001 diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Rand.png b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Rand.png new file mode 100644 index 0000000000000000000000000000000000000000..390f4fb398a4812c1a5eccb1c21fc2c36041d744 GIT binary patch literal 1485 zcmd6n|2xwO0LMS(JM*O)4P#r#h!{h{e3-=?S~EA~iDI6qoGJDxCCn4zu6$XNjtSQ!k_x$;(|>XA=Xt+hzrKHZ6_A7dbhM1M007V-`TJ6K$@x|d z<=y=xFa5F$F*%S*+~v;yJ{{Y0p}R+2QYpb9fa0GH)zz6tAD^OiHZ(-3@8$;UIT8F& zucP{tOxSo_RDhq230$Z>{Q!3`KfzwwQa~=hLui7Lf&oCKmgMVA&3w81ax%RR)K`<& zDG^nOCvcdK_njkIJAT5;N3M^m7(dB%Wo@{9sTJOA(AUcMsU;8ebn`b>Nse{TFY2v* zkZJ~O5|(@W{WG1&9IhFs8eakSx_4^9J^|--k?7f+bqGeGBuMc2*MdcN%C<_aPUDVQN=9h43)M3%xS%}5` zK+g4?MnO@1uVM_^V`8dyyd%zcTwTz{`6q%EX z!%O`a<@}}^ba&6gs2t@%TYEOJuIielH`uzqBZKM5LHVWBG}RSD1K7CssiG&E)|&*P z*BxqST+;!AB@i+;l~F}!$UHj2(Zp+blG~%EKXrfL!E?j2zfgqk!H1Gk5O!yG3LMpo zRtLq6@IPwaH*Xz~9Po)(u^#?DbAl_hJ676upFw&aMzGN~NgS6$lb8ce>4ix_47PVb zoZWAh75%#Mp<(#m*)8Q?OT4&6*Kz!@DsB?s!h=Q{-G#m4xTIV&(@<9gq4u#0rlJW5 z?0JdOEFjj59hcC^WyktHU=X5uLY&q1F9rtR+o+|YzHn42o< zkiCZU0VBO9lb{k~B6U#mJGPG(S!u{iMbVhbQAVKyGJbMBxeEb}Cp0&xvn&%(v=E&r zQ;z`|K=>QQsO@&&3~%oat9T1;(+n~({kxfVS@QCuoyG6xRCU$q0;z_ryDRMc-D9#2H{~0LB5`M zZH>F9D7Njuex3*Vn4;lQCk}EYAa?#>+0;k8RFRU2EXO=Qf(Oz-Y@wi~I;!nvYLO0z z7)gbv*i0{V@a~lhfL64OOq?z@G4y92M}b>R(G+}EpNa`$#Wh8Y*zv2_Brfu%&GAi(meMU=)fgEPR4?DuMlA=*F$u^PY_Qf1t?1+ED{k*j z8?r5BBt^-YteN!5?((o5x~mENwi`EdYXw8!-tmq%k{&6B={uOp-lT|YYdN=iox4UZ zKgnRKS%)^Z2;X`gVu_bgO);+9qh)ll^+bg9o@OkI^q2>NdZP4Q9Yh0?GD*9U5|$%V zg=7KebZSr+WKbnb4lyDpiz9>K=hrs9FRYl^y;8;Zd-qkS*NkDuuR^|Q2J`i`UiRZp Uqvd&HL%X^INW@^@W}j&8e{}=&P5=M^ literal 0 HcmV?d00001 diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Sound.png b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/Resources/Sound.png new file mode 100644 index 0000000000000000000000000000000000000000..e1bc2badb59b50bfc5761ad8442c29feb979d340 GIT binary patch literal 1493 zcmd6n`#aMM9LK-=ZccU~m(5(lWR6_ICOefb#EjC+Tt|$|t>apWVq|VfVVtEBEm;>t z<+hyEM6S!Nc&I$NRBEKM;)A^-qTaB{S#Z}{x* z$U`^o;y8C@Ljsz!J9)$F|9gaMQyv>dNhj%4H^5kPWZ6wYwzNq)JEjanOp&sz3A(R@ zb%Np~=Cr8U$IAiF59yAuHQdr0cc`s~mmfpr+|{r2wgv#$T_<~6_k`D9u8z{DER@LB zEeY>b3|Y)B9Z(FDt?!Sl$UNZX+?E#6^~!x^&v{Vy{ha3^qScPy{QO#8Ih~}oxe>mG zda03Oktl;_{2B>tc|?g@P_GqR4mm#@uW`OZ9eii`D~n;f#67k5$pm(&{c{N>-Dt>u zKg%FemRmJdu*=8V^90`NrFB+wmf{sXrHOdEoPW>*9a^%IE2p1y_<(n?y`>dXikGeA zJj?uL+Nwho-+&yWoJ!5bOmT1s@WfKM9aP!okb7Db=)ql)q{ww)Kx{!iC$nL$EfAx$uyl=Q5skVn?~t}%^0)3B@Hj0+_CAoyTF*Gzo)cQUS2fg=O<}3omP|Deg)8!w& zr!xSnG3_*{NhC-b1M4l!@^iI~K>8lU0ukSDWhQMO*Yh9)U*U^(t)R*E7SC?6c4c&I zW3Fo|E<{QlpC5a{v)*BB5h%UjT^RrIahZAD@j?)~WMrg$f43r0)*T*518lW*v)x1W z{qv$O%i3W?*C?jjhbx#qC);83IbPKtO^o~N@{%x(q!ZCU9DIZ>H%fs>{g7NUJU7>S zpYYXtO$nT~u_S&f%6M$-mLJ8==DFLDdG+pi9*jyrPO9;U=ZxhSPoYQ<-sDbcHv&ya z3$!ulA~k_wc`dEk-Il{DG-eO=F4)yJfOTYgA04L~3|mH~>OiB>G;+g@YEich*m-h( z9-Kal>i@O`^U2&74850;3tXCL;i9;ai-Xd&=sB?}(V;2Gz>r?Mxg@Nh!1k?~Neoez ziGz1qsOCkhyIw3`JqL**A1iO3s<{AG6#4fO9}G;-qy+8!m$oLH)JjZwS*CkqfA7t0 h=b4COVZT{su0Urc49`?Hi?Z?d04FllzWP7_=Rewj@NxhE literal 0 HcmV?d00001 diff --git a/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.cs b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.cs new file mode 100644 index 0000000..0f0b191 --- /dev/null +++ b/Modules/BeatSaberPlus_MenuMusic/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.cs @@ -0,0 +1,331 @@ +using CP_SDK.Unity.Extensions; +using CP_SDK.XUI; +using System.Collections; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace ChatPlexMod_MenuMusic.UI +{ + ///

/// Profile index - public void SwitchToProfile(int p_Index) + /// Is a temporary change? + public void SwitchToProfile(int p_Index, bool p_Temporary) { - p_Index = Mathf.Clamp(p_Index, 0, NTConfig.Instance.Profiles.Count); - NTConfig.Instance.ActiveProfile = p_Index; + if (p_Temporary) + m_BackupProfileIndex = NTConfig.Instance.ActiveProfile; + else + m_BackupProfileIndex = null; + + NTConfig.Instance.ActiveProfile = Mathf.Clamp(p_Index, 0, NTConfig.Instance.Profiles.Count); Patches.PColorNoteVisuals.SetFromConfig(true); Patches.PGameNoteController.SetFromConfig(true); @@ -127,6 +111,9 @@ public void SwitchToProfile(int p_Index) Patches.PBombController.SetFromConfig(true); Patches.PSliderController.SetFromConfig(true); Patches.PSliderHapticFeedbackInteractionEffect.SetFromConfig(true); + + if (!p_Temporary) + NTConfig.Instance.Save(); } //////////////////////////////////////////////////////////////////////////// @@ -135,20 +122,14 @@ public void SwitchToProfile(int p_Index) /// /// Get Module settings UI /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsLeftView == null) - m_SettingsLeftView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsRightView == null) - m_SettingsRightView = BeatSaberUI.CreateViewController(); + if (m_SettingsLeftView == null) m_SettingsLeftView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsRightView == null) m_SettingsRightView = CP_SDK.UI.UISystem.CreateViewController(); /// Change main view - return (m_SettingsView, m_SettingsLeftView, m_SettingsRightView); + return (m_SettingsMainView, m_SettingsLeftView, m_SettingsRightView); } //////////////////////////////////////////////////////////////////////////// @@ -158,8 +139,11 @@ protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewControlle /// When the active scene change /// /// New active scene - private void OnSceneChange(BeatSaberPlus.SDK.Game.Logic.SceneType p_Scene) + private void OnSceneChange(BeatSaberPlus.SDK.Game.Logic.ESceneType p_Scene) { + if (p_Scene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing && m_BackupProfileIndex.HasValue) + NTConfig.Instance.ActiveProfile = Mathf.Clamp(m_BackupProfileIndex.Value, 0, NTConfig.Instance.Profiles.Count); + Patches.PColorNoteVisuals.SetFromConfig(true); Patches.PColorNoteVisuals.SetBlockColorOverride(false, Color.black, Color.black); Patches.PGameNoteController.SetFromConfig(true); diff --git a/Modules/BeatSaberPlus_NoteTweaker/Patches/PBombContoller.cs b/Modules/BeatSaberPlus_NoteTweaker/Patches/PBombContoller.cs index 6304c63..9eef3f9 100644 --- a/Modules/BeatSaberPlus_NoteTweaker/Patches/PBombContoller.cs +++ b/Modules/BeatSaberPlus_NoteTweaker/Patches/PBombContoller.cs @@ -16,15 +16,15 @@ public class PBombController : BombNoteController //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - private static bool m_Enabled = false; - private static bool m_TempEnabled = false; - private static Color m_Color = DEFAULT_COLOR; - private static Material m_SharedMaterial = null; - private static bool m_ShouldRecolorize = false; - private static Vector3 m_Scale; - private static float m_InvScale; - private static Vector3 m_TempScale; - private static float m_TempInvScale; + private static bool m_Enabled = false; + private static bool m_TempEnabled = false; + private static Color m_Color = DEFAULT_COLOR; + private static Material m_SharedMaterial = null; + private static bool m_ShouldRecolorize = false; + private static Vector3 m_Scale; + private static float m_InvScale; + private static Vector3 m_TempScale; + private static float m_TempInvScale; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -73,7 +73,7 @@ internal static void SetFromConfig(bool p_OnSceneSwitch) if (m_SharedMaterial) { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) m_SharedMaterial.SetColor(SIMPLE_COLOR_ID, m_Color); else m_SharedMaterial.SetColor(SIMPLE_COLOR_ID, DEFAULT_COLOR); diff --git a/Modules/BeatSaberPlus_NoteTweaker/Patches/PColorNoteVisuals.cs b/Modules/BeatSaberPlus_NoteTweaker/Patches/PColorNoteVisuals.cs index a0745e7..bfadf32 100644 --- a/Modules/BeatSaberPlus_NoteTweaker/Patches/PColorNoteVisuals.cs +++ b/Modules/BeatSaberPlus_NoteTweaker/Patches/PColorNoteVisuals.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +using CP_SDK.Unity.Extensions; +using HarmonyLib; using System.Collections.Generic; using UnityEngine; @@ -81,7 +82,7 @@ internal static void Postfix(ColorNoteVisuals __instance, for (int l_MBI = 0; l_MBI < m_ComponentsCache.Count; ++l_MBI) { var l_CurrentBlock = m_ComponentsCache[l_MBI]; - l_CurrentBlock.materialPropertyBlock.SetColor(_colorId, l_Color.ColorWithAlpha(0.6f)); + l_CurrentBlock.materialPropertyBlock.SetColor(_colorId, ColorU.WithAlpha(l_Color, 0.6f)); l_CurrentBlock.ApplyChanges(); } } @@ -98,11 +99,11 @@ internal static void Postfix(ColorNoteVisuals __instance, var l_BaseColor = ____colorManager.ColorForType(l_ColorType); - var l_ArrowColor = (m_OverrideArrowColors ? (l_ColorType == ColorType.ColorB ? m_RightArrowColor : m_LeftArrowColor) : l_BaseColor).ColorWithAlpha(m_ArrowAlpha); - var l_DotColor = (m_OverrideDotColors ? (l_ColorType == ColorType.ColorB ? m_RightCircleColor : m_LeftCircleColor) : l_BaseColor).ColorWithAlpha(m_DotAlpha); + var l_ArrowColor = ColorU.WithAlpha(m_OverrideArrowColors ? (l_ColorType == ColorType.ColorB ? m_RightArrowColor : m_LeftArrowColor) : l_BaseColor, m_ArrowAlpha); + var l_DotColor = ColorU.WithAlpha(m_OverrideDotColors ? (l_ColorType == ColorType.ColorB ? m_RightCircleColor : m_LeftCircleColor) : l_BaseColor, m_DotAlpha); if (m_BlockColorsEnabled) - l_ArrowColor = (l_ColorType == ColorType.ColorA ? m_LeftBlockColor : m_RightBlockColor).ColorWithAlpha(0.6f); + l_ArrowColor = ColorU.WithAlpha(l_ColorType == ColorType.ColorA ? m_LeftBlockColor : m_RightBlockColor, 0.6f); for (int l_I = 0; l_I < ____arrowMeshRenderers.Length; ++l_I) { @@ -184,8 +185,8 @@ internal static void SetDotColorsFromConfig(NTConfig._Profile p_Profile) public static void SetBlockColorOverride(bool p_Enabled, Color p_Left, Color p_Right) { m_BlockColorsEnabled = p_Enabled; - m_LeftBlockColor = p_Left.ColorWithAlpha(1f); - m_RightBlockColor = p_Right.ColorWithAlpha(1f); + m_LeftBlockColor = ColorU.WithAlpha(p_Left, 1f); + m_RightBlockColor = ColorU.WithAlpha(p_Right, 1f); } //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_NoteTweaker/Patches/PSliderController.cs b/Modules/BeatSaberPlus_NoteTweaker/Patches/PSliderController.cs index cb984bc..30e055d 100644 --- a/Modules/BeatSaberPlus_NoteTweaker/Patches/PSliderController.cs +++ b/Modules/BeatSaberPlus_NoteTweaker/Patches/PSliderController.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +using CP_SDK.Unity.Extensions; +using HarmonyLib; using UnityEngine; namespace BeatSaberPlus_NoteTweaker.Patches @@ -23,7 +24,7 @@ public class PSliderController : SliderController internal static void Postfix(ref Color ____initColor) { if (m_Enabled) - ____initColor = ____initColor.ColorWithAlpha(m_Opacity); + ____initColor = ColorU.WithAlpha(____initColor, m_Opacity); } //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_NoteTweaker/Properties/AssemblyInfo.cs b/Modules/BeatSaberPlus_NoteTweaker/Properties/AssemblyInfo.cs index f723740..9f1f3b8 100644 --- a/Modules/BeatSaberPlus_NoteTweaker/Properties/AssemblyInfo.cs +++ b/Modules/BeatSaberPlus_NoteTweaker/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/Modules/BeatSaberPlus_NoteTweaker/UI/Modals/ProfileImportModal.cs b/Modules/BeatSaberPlus_NoteTweaker/UI/Modals/ProfileImportModal.cs new file mode 100644 index 0000000..dd78b57 --- /dev/null +++ b/Modules/BeatSaberPlus_NoteTweaker/UI/Modals/ProfileImportModal.cs @@ -0,0 +1,136 @@ +using CP_SDK; +using CP_SDK.XUI; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace BeatSaberPlus_NoteTweaker.UI.Modals +{ + /// + /// Profile import modal + /// + internal sealed class ProfileImportModal : CP_SDK.UI.IModal + { + private XUIDropdown m_DropDown = null; + + private Action m_Callback = null; + private string m_Selected = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On modal show + /// + public override void OnShow() + { + if (m_DropDown != null) + return; + + Templates.ModalRectLayout( + XUIText.Make("What profile do you want to import?"), + + XUIDropdown.Make() + .OnValueChanged((_, p_Selected) => m_Selected = p_Selected) + .Bind(ref m_DropDown), + + XUIHLayout.Make( + XUISecondaryButton.Make("Cancel", OnCancelButton).SetWidth(30f), + XUIPrimaryButton.Make("Import", OnImportButton).SetWidth(30f) + ) + .SetPadding(0) + ) + .SetWidth(90.0f) + .BuildUI(transform); + } + /// + /// On modal close + /// + public override void OnClose() + { + + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Init + /// + /// Callback + public void Init(Action p_Callback) + { + m_Selected = string.Empty; + + var l_Files = new List(); + foreach (var l_File in System.IO.Directory.GetFiles(NoteTweaker.IMPORT_FOLDER, "*.bspnt")) + l_Files.Add(System.IO.Path.GetFileNameWithoutExtension(l_File)); + + m_DropDown.SetOptions(l_Files); + + m_Callback = p_Callback; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On cancel button + /// + private void OnCancelButton() + { + VController.CloseModal(this); + } + /// + /// On import button + /// + private void OnImportButton() + { + var l_FileName = NoteTweaker.IMPORT_FOLDER + m_Selected + ".bspnt"; + + if (System.IO.File.Exists(l_FileName)) + { + var l_Raw = System.IO.File.ReadAllText(l_FileName, System.Text.Encoding.Unicode); + + try + { + var l_NewProfile = JsonConvert.DeserializeObject(l_Raw, new JsonConverter[] + { + new CP_SDK.Config.JsonConverters.ColorConverter() + }); + + l_NewProfile.Name += " (Imported)"; + + if (l_NewProfile != null) + { + NTConfig.Instance.Profiles.Add(l_NewProfile); + + VController.CloseModal(this); + + try { m_Callback?.Invoke(); } + catch (System.Exception l_Exception) + { + ChatPlexSDK.Logger.Error($"[BeatSaberPlus_NoteTweaker.UI][ProfileImportModal.OnImportButton] Error:"); + ChatPlexSDK.Logger.Error(l_Exception); + } + } + else + { + VController.CloseModal(this); + VController.ShowMessageModal("Error importing profile!"); + } + } + catch + { + VController.CloseModal(this); + VController.ShowMessageModal("Invalid file!"); + } + } + else + { + VController.CloseModal(this); + VController.ShowMessageModal("File not found!"); + } + } + } +} diff --git a/Modules/BeatSaberPlus_NoteTweaker/UI/Settings.bsml b/Modules/BeatSaberPlus_NoteTweaker/UI/Settings.bsml deleted file mode 100644 index fd88d21..0000000 --- a/Modules/BeatSaberPlus_NoteTweaker/UI/Settings.bsml +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_NoteTweaker/UI/Settings.cs b/Modules/BeatSaberPlus_NoteTweaker/UI/Settings.cs deleted file mode 100644 index 7252566..0000000 --- a/Modules/BeatSaberPlus_NoteTweaker/UI/Settings.cs +++ /dev/null @@ -1,652 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using BeatSaberMarkupLanguage.Components.Settings; -using HMUI; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; - -namespace BeatSaberPlus_NoteTweaker.UI -{ - /// - /// Settings main view - /// - internal class Settings : BeatSaberPlus.SDK.UI.ResourceViewController - { - /// - /// Profile line per page - /// - private static int EVENT_PER_PAGE = 10; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0649 - [UIObject("TabSelector")] - private GameObject m_TabSelector; - private TextSegmentedControl m_TabSelector_TabSelectorControl = null; - - [UIObject("Tabs")] private GameObject m_Tabs; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Profiles Tab - [UIObject("ProfilesTab")] private GameObject m_ProfilesTab = null; - [UIObject("ProfileListFrame_Background")] private GameObject m_ProfileListFrame_Background = null; - [UIObject("ProfilesList")] private GameObject m_ProfilesListView = null; - [UIComponent("ProfilesUpButton")] private Button m_ProfilesUpButton = null; - [UIComponent("ProfilesDownButton")] private Button m_ProfilesDownButton = null; - - private BeatSaberPlus.SDK.UI.DataSource.SimpleTextList m_EventsList = null; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Notes Tab - [UIObject("NotesTab")] private GameObject m_NotesTab = null; - [UIComponent("NotesTab_Scale")] private IncrementSetting m_NotesTab_Scale; - [UIComponent("NotesTab_ShowPrecisonDots")] private ToggleSetting m_NotesTab_ShowPrecisonDots; - [UIComponent("NotesTab_PrecisionDotsScale")] private IncrementSetting m_NotesTab_PrecisionDotsScale; - [UIObject("NotesTab_InfoBackground")] private GameObject m_NotesTab_InfoBackground = null; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Arrows Tab - [UIObject("ArrowsTab")] private GameObject m_ArrowsTab = null; - [UIComponent("ArrowsTab_Scale")] private IncrementSetting m_ArrowsTab_Scale; - [UIComponent("ArrowsTab_Intensity")] private IncrementSetting m_ArrowsTab_Intensity; - [UIComponent("ArrowsTab_OverrideColors")] private ToggleSetting m_ArrowsTab_OverrideColors; - [UIComponent("ArrowsTab_LColor")] private ColorSetting m_ArrowsTab_LColor; - [UIComponent("ArrowsTab_RColor")] private ColorSetting m_ArrowsTab_RColor; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Dots Tab - [UIObject("DotsTab")] private GameObject m_DotsTab = null; - [UIComponent("DotsTab_Scale")] private IncrementSetting m_DotsTab_Scale; - [UIComponent("DotsTab_Intensity")] private IncrementSetting m_DotsTab_Intensity; - [UIComponent("DotsTab_OverrideColors")] private ToggleSetting m_DotsTab_OverrideColors; - [UIComponent("DotsTab_LColor")] private ColorSetting m_DotsTab_LColor; - [UIComponent("DotsTab_RColor")] private ColorSetting m_DotsTab_RColor; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Bombs Tab - [UIObject("BombsTab")] private GameObject m_BombsTab = null; - [UIComponent("BombsTab_Scale")] private IncrementSetting m_BombsTab_Scale; - [UIComponent("BombsTab_OverrideColor")] private ToggleSetting m_BombsTab_OverrideColor; - [UIComponent("BombsTab_Color")] private ColorSetting m_BombsTab_Color; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region Arcs Tab - [UIObject("ArcsTab")] private GameObject m_ArcsTab = null; - [UIComponent("ArcsTab_Intensity")] private IncrementSetting m_ArcsTab_Intensity; - [UIComponent("ArcsTab_Haptics")] private ToggleSetting m_ArcsTab_Haptics; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - #region BurstNotes Tab - [UIObject("BurstNotesTab")] private GameObject m_BurstNotesTab = null; - [UIComponent("BurstNotesTab_DotScale")] private IncrementSetting m_BurstNotesTab_DotScale; - #endregion - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIObject("ImportProfileFrame")] private GameObject m_ImportProfileFrame = null; - [UIObject("ImportProfileFrame_Background")] private GameObject m_ImportProfileFrame_Background = null; - [UIComponent("ImportProfileFrame_DropDown")] private DropDownListSetting m_ImportProfileFrame_DropDown; - [UIValue("ImportProfileFrame_DropDownOptions")] private List m_ImportProfileFrame_DropDownOptions = new List() { "Loading...", }; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("NewKeyboard")] private ModalKeyboard m_NewEventNameKeyboard = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("RenameKeyboard")] private ModalKeyboard m_RenameKeyboard = null; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - /// - /// Current profile list page - /// - private int m_CurrentProfilesPage = 1; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Profile = NTConfig.Instance.GetActiveProfile(); - - /// Create type selector - m_TabSelector_TabSelectorControl = BeatSaberPlus.SDK.UI.TextSegmentedControl.Create(m_TabSelector.transform as RectTransform, false); - m_TabSelector_TabSelectorControl.SetTexts(new string[] { "Profiles", "Notes", "Arrows", "Dots", "Bombs", "Arcs", "BurstNotes" }); - m_TabSelector_TabSelectorControl.ReloadData(); - m_TabSelector_TabSelectorControl.didSelectCellEvent += OnTabSelected; - - foreach (var l_Text in m_TabSelector_TabSelectorControl.GetComponentsInChildren()) - l_Text.richText = true; - - //////////////////////////////////////////////////////////////////////////// - /// Prepare tabs - //////////////////////////////////////////////////////////////////////////// - - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_ProfilesTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_NotesTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_ArrowsTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_DotsTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_BombsTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_ArcsTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_BurstNotesTab, 0.50f); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_ImportProfileFrame_Background, 0.75f); - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_NewEventNameKeyboard.modalView, 0.75f); - BeatSaberPlus.SDK.UI.ModalView.SetOpacity(m_RenameKeyboard.modalView, 0.75f); - - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - - #region Profiles Tab - /// Scale down up & down button - m_ProfilesUpButton.transform.localScale = Vector3.one * 0.5f; - m_ProfilesDownButton.transform.localScale = Vector3.one * 0.5f; - - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_ProfileListFrame_Background, 0.5f); - - /// Prepare profiles list - var l_BSMLTableView = m_ProfilesListView.GetComponentInChildren(); - l_BSMLTableView.SetDataSource(null, false); - GameObject.DestroyImmediate(m_ProfilesListView.GetComponentInChildren()); - m_EventsList = l_BSMLTableView.gameObject.AddComponent(); - m_EventsList.TableViewInstance = l_BSMLTableView; - m_EventsList.CellSizeValue = 4.8f; - l_BSMLTableView.didSelectCellWithIdxEvent += OnProfileSelected; - l_BSMLTableView.SetDataSource(m_EventsList, false); - - /// Bind events - m_ProfilesUpButton.onClick.AddListener(OnProfilesPageUpPressed); - m_ProfilesDownButton.onClick.AddListener(OnProfilesPageDownPressed); - #endregion - - #region Notes Tab - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_NotesTab_Scale, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, l_Profile.NotesScale, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_NotesTab_ShowPrecisonDots, l_Event, l_Profile.NotesShowPrecisonDots, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_NotesTab_PrecisionDotsScale, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, l_Profile.NotesPrecisonDotsScale, true); - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_NotesTab_InfoBackground, 0.5f); - #endregion - - #region Arrows Tab - var l_ArrowLColor = l_Profile.ArrowsLColor.ColorWithAlpha(1.00f); - var l_ArrowRColor = l_Profile.ArrowsRColor.ColorWithAlpha(1.00f); - - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_ArrowsTab_Scale, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, l_Profile.ArrowsScale, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_ArrowsTab_Intensity, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, l_Profile.ArrowsIntensity, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ArrowsTab_OverrideColors, l_Event, l_Profile.ArrowsOverrideColors, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_ArrowsTab_LColor, l_Event, l_ArrowLColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_ArrowsTab_RColor, l_Event, l_ArrowRColor, true); - #endregion - - #region Dots Tab - var l_DotLColor = l_Profile.DotsLColor.ColorWithAlpha(1.00f); - var l_DotRColor = l_Profile.DotsRColor.ColorWithAlpha(1.00f); - - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_DotsTab_Scale, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, l_Profile.DotsScale, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_DotsTab_Intensity, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, l_Profile.DotsIntensity, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_DotsTab_OverrideColors, l_Event, l_Profile.DotsOverrideColors, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_DotsTab_LColor, l_Event, l_DotLColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_DotsTab_RColor, l_Event, l_DotRColor, true); - #endregion - - #region Bombs Tab - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_BombsTab_Scale, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, l_Profile.BombsScale, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_BombsTab_OverrideColor, l_Event, l_Profile.BombsOverrideColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_BombsTab_Color, l_Event, l_Profile.BombsColor, true); - #endregion - - #region Arcs Tab - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_ArcsTab_Intensity, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, l_Profile.ArcsIntensity, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ArcsTab_Haptics, l_Event, l_Profile.ArcsHaptics, true); - #endregion - - #region BurstNotes Tab - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_BurstNotesTab_DotScale, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, l_Profile.BurstNotesDotsScale, true); - #endregion - - #region Import Frame - /// Remove import event type selector label - BeatSaberPlus.SDK.UI.DropDownListSetting.Setup(m_ImportProfileFrame_DropDown, null, true, 0.95f); - #endregion - - /// Hide import frame - m_ImportProfileFrame.gameObject.SetActive(false); - /// Show first tab by default - OnTabSelected(null, 0); - /// Rebuild profiles - RebuildProfilesList(); - /// Refresh UI - OnSettingChanged(null); - } - /// - /// On view deactivation - /// - protected override sealed void OnViewDeactivation() - { - NTConfig.Instance.Save(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Go to previous profiles page - /// - private void OnProfilesPageUpPressed() - { - /// Underflow check - if (m_CurrentProfilesPage < 2) - return; - - /// Decrement current page - m_CurrentProfilesPage--; - - /// Rebuild list - RebuildProfilesList(); - } - /// - /// Rebuild list - /// - private void RebuildProfilesList() - { - if (!UICreated) - return; - - /// Update page count - var l_PageCount = Math.Max(1, Mathf.CeilToInt((float)(NTConfig.Instance.Profiles.Count) / (float)(EVENT_PER_PAGE))); - - /// Update overflow - m_CurrentProfilesPage = Math.Max(1, Math.Min(m_CurrentProfilesPage, l_PageCount)); - - /// Update UI - m_ProfilesUpButton.interactable = m_CurrentProfilesPage > 1; - m_ProfilesDownButton.interactable = m_CurrentProfilesPage < l_PageCount; - - /// Clear old entries - m_EventsList.TableViewInstance.ClearSelection(); - m_EventsList.Data.Clear(); - - for (int l_I = (m_CurrentProfilesPage - 1) * EVENT_PER_PAGE; - l_I < NTConfig.Instance.Profiles.Count && l_I < (m_CurrentProfilesPage * EVENT_PER_PAGE); - ++l_I) - { - var l_ProfileName = NTConfig.Instance.Profiles[l_I].Name; - - if (l_I == NTConfig.Instance.ActiveProfile) - l_ProfileName = "" + l_ProfileName + ""; - - m_EventsList.Data.Add((l_ProfileName, null)); - } - - /// Refresh - m_EventsList.TableViewInstance.ReloadData(); - } - /// - /// On profile selected - /// - /// TableView instance - /// Relative index - private void OnProfileSelected(HMUI.TableView p_TableView, int p_RelIndex) - { - int l_ProfileIndex = ((m_CurrentProfilesPage - 1) * EVENT_PER_PAGE) + p_RelIndex; - - if (p_RelIndex < 0 || l_ProfileIndex >= NTConfig.Instance.Profiles.Count) - return; - - NTConfig.Instance.ActiveProfile = l_ProfileIndex; - RefreshSettings(); - RebuildProfilesList(); - } - /// - /// Go to next profiles page - /// - private void OnProfilesPageDownPressed() - { - /// Increment current page - m_CurrentProfilesPage++; - - /// Rebuild list - RebuildProfilesList(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// New profile button - /// - [UIAction("click-new-btn-pressed")] - private void OnNewButton() - { - ShowModal("OpenNewModal"); - } - /// - /// On new profile name keyboard enter pressed - /// - /// - [UIAction("NewKeyboardPressed")] - internal void NewEventNameKeyboardPressed(string p_Text) - { - var l_ProfileName = string.IsNullOrEmpty(p_Text) ? "No name..." : p_Text; - var l_NewProfile = new NTConfig._Profile() { Name = l_ProfileName }; - - NTConfig.Instance.Profiles.Add(l_NewProfile); - NTConfig.Instance.ActiveProfile = NTConfig.Instance.Profiles.IndexOf(l_NewProfile); - - RebuildProfilesList(); - RefreshSettings(); - } - /// - /// Rename profile button - /// - [UIAction("click-rename-btn-pressed")] - private void OnRenameButton() - { - if (NTConfig.Instance.GetActiveProfile().IsDefault()) - { - ShowMessageModal("No changes allowed on default config!"); - return; - } - - m_RenameKeyboard.SetText(NTConfig.Instance.GetActiveProfile().Name); - ShowModal("OpenRenameModal"); - } - /// - /// On rename keyboard enter pressed - /// - /// - [UIAction("RenameKeyboardPressed")] - internal void RenameKeyboardPressed(string p_Text) - { - NTConfig.Instance.GetActiveProfile().Name = string.IsNullOrEmpty(p_Text) ? "No name..." : p_Text; - RebuildProfilesList(); - } - /// - /// Delete profile button - /// - [UIAction("click-delete-btn-pressed")] - private void OnDeleteButton() - { - if (NTConfig.Instance.GetActiveProfile().IsDefault()) - { - ShowMessageModal("No changes allowed on default config!"); - return; - } - - ShowConfirmationModal($"Do you want to delete profile\n\"{NTConfig.Instance.GetActiveProfile().Name}\"?", () => - { - NTConfig.Instance.Profiles.Remove(NTConfig.Instance.GetActiveProfile()); - NTConfig.Instance.ActiveProfile = Mathf.Clamp(NTConfig.Instance.ActiveProfile - 1, 0, NTConfig.Instance.Profiles.Count); - RebuildProfilesList(); - RefreshSettings(); - }); - } - /// - /// Export an profile - /// - [UIAction("click-export-btn-pressed")] - private void OnExportButton() - { - var l_Profile = NTConfig.Instance.GetActiveProfile(); - - if (l_Profile.IsDefault()) - { - ShowMessageModal("Cannot export default config!"); - return; - } - - var l_Serialized = JObject.FromObject(l_Profile); - - var l_FileName = CP_SDK.Misc.Time.UnixTimeNow() + "_" + l_Profile.Name + ".bspnt"; - l_FileName = string.Concat(l_FileName.Split(System.IO.Path.GetInvalidFileNameChars())); - - System.IO.File.WriteAllText(NoteTweaker.EXPORT_FOLDER + l_FileName, l_Serialized.ToString(Formatting.Indented), System.Text.Encoding.Unicode); - - ShowMessageModal("Event exported in\n" + NoteTweaker.EXPORT_FOLDER); - } - /// - /// Import an profile - /// - [UIAction("click-import-btn-pressed")] - private void OnImportButton() - { - m_Tabs.gameObject.SetActive(false); - m_ImportProfileFrame.gameObject.SetActive(true); - - var l_Files = new List(); - foreach (var l_File in System.IO.Directory.GetFiles(NoteTweaker.IMPORT_FOLDER, "*.bspnt")) - l_Files.Add(System.IO.Path.GetFileNameWithoutExtension(l_File)); - - m_ImportProfileFrame_DropDownOptions = l_Files; - m_ImportProfileFrame_DropDown.values = l_Files; - m_ImportProfileFrame_DropDown.UpdateChoices(); - } - /// - /// Import profile cancel button - /// - [UIAction("click-cancel-import-profile-btn-pressed")] - private void OnImportProfileCancelButton() - { - m_ImportProfileFrame.gameObject.SetActive(false); - m_Tabs.gameObject.SetActive(true); - } - /// - /// Import profile button - /// - [UIAction("click-import-profile-btn-pressed")] - private void OnImportProfileButton() - { - m_ImportProfileFrame.gameObject.SetActive(false); - m_Tabs.gameObject.SetActive(true); - - var l_FileName = NoteTweaker.IMPORT_FOLDER + (string)m_ImportProfileFrame_DropDown.Value + ".bspnt"; - - if (System.IO.File.Exists(l_FileName)) - { - var l_Raw = System.IO.File.ReadAllText(l_FileName, System.Text.Encoding.Unicode); - - try - { - var l_NewProfile = JsonConvert.DeserializeObject(l_Raw); - l_NewProfile.Name += " (Imported)"; - - if (l_NewProfile != null) - { - NTConfig.Instance.Profiles.Add(l_NewProfile); - RebuildProfilesList(); - } - else - ShowMessageModal("Error importing profile!"); - } - catch - { - ShowMessageModal("Invalid file!"); - } - } - else - ShowMessageModal("File not found!"); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When a tab is selected - /// - /// Tab control instance - /// Tab index - private void OnTabSelected(SegmentedControl p_SegmentControl, int p_TabIndex) - { - if (NTConfig.Instance.GetActiveProfile().IsDefault() && p_TabIndex > 0) - { - ShowMessageModal("No changes allowed on default config!"); - p_SegmentControl.SelectCellWithNumber(0); - p_TabIndex = 0; - } - - m_ProfilesTab.SetActive(p_TabIndex == 0); - m_NotesTab.SetActive(p_TabIndex == 1); - m_ArrowsTab.SetActive(p_TabIndex == 2); - m_DotsTab.SetActive(p_TabIndex == 3); - m_BombsTab.SetActive(p_TabIndex == 4); - m_ArcsTab.SetActive(p_TabIndex == 5); - m_BurstNotesTab.SetActive(p_TabIndex == 6); - } - /// - /// When settings are changed - /// - /// - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - var l_Profile = NTConfig.Instance.GetActiveProfile(); - - #region Notes Tab - l_Profile.NotesScale = m_NotesTab_Scale.Value; - l_Profile.NotesShowPrecisonDots = m_NotesTab_ShowPrecisonDots.Value; - l_Profile.NotesPrecisonDotsScale = m_NotesTab_PrecisionDotsScale.Value; - #endregion - - #region Arrows Tab - l_Profile.ArrowsScale = m_ArrowsTab_Scale.Value; - l_Profile.ArrowsIntensity = m_ArrowsTab_Intensity.Value; - l_Profile.ArrowsOverrideColors = m_ArrowsTab_OverrideColors.Value; - l_Profile.ArrowsLColor = m_ArrowsTab_LColor.CurrentColor.ColorWithAlpha(1.0f); - l_Profile.ArrowsRColor = m_ArrowsTab_RColor.CurrentColor.ColorWithAlpha(1.0f); - - m_ArrowsTab_LColor.interactable = l_Profile.ArrowsOverrideColors; - m_ArrowsTab_RColor.interactable = l_Profile.ArrowsOverrideColors; - #endregion - - #region Dots Tab - l_Profile.DotsScale = m_DotsTab_Scale.Value; - l_Profile.DotsIntensity = m_DotsTab_Intensity.Value; - l_Profile.DotsOverrideColors = m_DotsTab_OverrideColors.Value; - l_Profile.DotsLColor = m_DotsTab_LColor.CurrentColor.ColorWithAlpha(1.0f); - l_Profile.DotsRColor = m_DotsTab_RColor.CurrentColor.ColorWithAlpha(1.0f); - - m_DotsTab_LColor.interactable = l_Profile.DotsOverrideColors; - m_DotsTab_RColor.interactable = l_Profile.DotsOverrideColors; - #endregion - - #region Bombs Tab - l_Profile.BombsScale = m_BombsTab_Scale.Value; - l_Profile.BombsOverrideColor = m_BombsTab_OverrideColor.Value; - l_Profile.BombsColor = m_BombsTab_Color.CurrentColor.ColorWithAlpha(1.0f); - - m_BombsTab_Color.interactable = l_Profile.BombsOverrideColor; - #endregion - - #region Arcs Tab - l_Profile.ArcsIntensity = m_ArcsTab_Intensity.Value; - l_Profile.ArcsHaptics = m_ArcsTab_Haptics.Value; - #endregion - - #region BurstNotes Tab - l_Profile.BurstNotesDotsScale = m_BurstNotesTab_DotScale.Value; - #endregion - - /// Refresh preview - SettingsRight.Instance.RefreshSettings(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset settings - /// - internal void RefreshSettings() - { - var l_Profile = NTConfig.Instance.GetActiveProfile(); - - m_PreventChanges = true; - - #region Notes Tab - m_NotesTab_Scale.Value = l_Profile.NotesScale; - m_NotesTab_ShowPrecisonDots.Value = l_Profile.NotesShowPrecisonDots; - m_NotesTab_PrecisionDotsScale.Value = l_Profile.NotesPrecisonDotsScale; - #endregion - - #region Arrows Tab - m_ArrowsTab_Scale.Value = l_Profile.ArrowsScale; - m_ArrowsTab_Intensity.Value = l_Profile.ArrowsIntensity; - m_ArrowsTab_OverrideColors.Value = l_Profile.ArrowsOverrideColors; - m_ArrowsTab_LColor.CurrentColor = l_Profile.ArrowsLColor.ColorWithAlpha(1.00f); - m_ArrowsTab_RColor.CurrentColor = l_Profile.ArrowsRColor.ColorWithAlpha(1.00f); - - m_ArrowsTab_LColor.interactable = l_Profile.ArrowsOverrideColors; - m_ArrowsTab_RColor.interactable = l_Profile.ArrowsOverrideColors; - #endregion - - #region Dots Tab - m_DotsTab_Scale.Value = l_Profile.DotsScale; - m_DotsTab_Intensity.Value = l_Profile.DotsIntensity; - m_DotsTab_OverrideColors.Value = l_Profile.DotsOverrideColors; - m_DotsTab_LColor.CurrentColor = l_Profile.DotsLColor.ColorWithAlpha(1.00f); - m_DotsTab_RColor.CurrentColor = l_Profile.DotsRColor.ColorWithAlpha(1.00f); - - m_DotsTab_LColor.interactable = l_Profile.DotsOverrideColors; - m_DotsTab_RColor.interactable = l_Profile.DotsOverrideColors; - #endregion - - #region Bombs Tab - m_BombsTab_Scale.Value = l_Profile.BombsScale; - m_BombsTab_OverrideColor.Value = l_Profile.BombsOverrideColor; - m_BombsTab_Color.CurrentColor = l_Profile.BombsColor; - - m_BombsTab_Color.interactable = l_Profile.BombsOverrideColor; - #endregion - - #region Arcs Tab - m_ArcsTab_Intensity.Value = l_Profile.ArcsIntensity; - m_ArcsTab_Haptics.Value = l_Profile.ArcsHaptics; - #endregion - - #region BurstNotes Tab - m_BurstNotesTab_DotScale.Value = l_Profile.BurstNotesDotsScale; - #endregion - - m_PreventChanges = false; - - SettingsRight.Instance.RefreshSettings(); - } - } -} diff --git a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeft.bsml b/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeft.bsml deleted file mode 100644 index 81ce58b..0000000 --- a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeft.bsml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeft.cs b/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeft.cs deleted file mode 100644 index a4ef4cd..0000000 --- a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeft.cs +++ /dev/null @@ -1,59 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using System.Diagnostics; -using UnityEngine; - -namespace BeatSaberPlus_NoteTweaker.UI -{ - /// - /// Chat request settings left screen - /// - internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController - { - private static readonly string s_InformationsStr = "Note Tweaker" - + "\n" + "This module allow you to customize the default notes" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n" - + "\n"; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - -#pragma warning disable CS0414 - [UIObject("Background")] - private GameObject m_Background = null; - [UIComponent("Informations")] - private HMUI.TextPageScrollView m_Informations = null; -#pragma warning restore CS0414 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); - m_Informations.SetText(s_InformationsStr); - m_Informations.UpdateVerticalScrollIndicator(0); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Documentation button - /// - [UIAction("click-documentation-btn-pressed")] - private void OnDocumentationButton() - { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://github.com/hardcpp/BeatSaberPlus/wiki#note-tweaker"); - } - } -} diff --git a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeftView.cs b/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeftView.cs new file mode 100644 index 0000000..0283d42 --- /dev/null +++ b/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsLeftView.cs @@ -0,0 +1,51 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace BeatSaberPlus_NoteTweaker.UI +{ + /// + /// Settings left view + /// + internal sealed class SettingsLeftView : CP_SDK.UI.ViewController + { + private static readonly string s_InformationStr = + "Note Tweaker" + + "\n" + "This module allows you to customize the default notes"; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Information"), + + Templates.ScrollableInfos(55, + XUIText.Make(s_InformationStr) + .SetAlign(TMPro.TextAlignmentOptions.Left) + ), + + Templates.ExpandedButtonsLine( + XUISecondaryButton.Make("Documentation", OnDocumentationButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Documentation button + /// + private void OnDocumentationButton() + { + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL(NoteTweaker.Instance.DocumentationURL); + } + } +} diff --git a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsMainView.cs b/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsMainView.cs new file mode 100644 index 0000000..e2fc902 --- /dev/null +++ b/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsMainView.cs @@ -0,0 +1,556 @@ +using CP_SDK.UI.Data; +using CP_SDK.Unity.Extensions; +using CP_SDK.XUI; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; + +namespace BeatSaberPlus_NoteTweaker.UI +{ + /// + /// Settings main view + /// + internal sealed class SettingsMainView : CP_SDK.UI.ViewController + { + private XUITabControl m_TabControl = null; + private XUIVVList m_ProfilesTab_List = null; + + private XUISlider m_NotesTab_Scale = null; + private XUIToggle m_NotesTab_ShowPrecisonDots = null; + private XUISlider m_NotesTab_PrecisionDotsScale = null; + + private XUISlider m_ArrowsTab_Scale = null; + private XUISlider m_ArrowsTab_Intensity = null; + private XUIToggle m_ArrowsTab_OverrideColors = null; + private XUIColorInput m_ArrowsTab_LColor = null; + private XUIColorInput m_ArrowsTab_RColor = null; + + private XUISlider m_DotsTab_Scale = null; + private XUISlider m_DotsTab_Intensity = null; + private XUIToggle m_DotsTab_OverrideColors = null; + private XUIColorInput m_DotsTab_LColor = null; + private XUIColorInput m_DotsTab_RColor = null; + + private XUISlider m_BombsTab_Scale = null; + private XUIToggle m_BombsTab_OverrideColor = null; + private XUIColorInput m_BombsTab_Color = null; + + private XUISlider m_ArcsTab_Intensity = null; + private XUIToggle m_ArcsTab_Haptics = null; + + private XUISlider m_BurstNotesTab_DotScale = null; + + private Modals.ProfileImportModal m_ProfileImportModal = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private List m_Items = new List(); + private TextListItem m_SelectedItem = null; + private bool m_PreventChanges = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayoutMainView( + Templates.TitleBar("Note Tweaker | Settings"), + + XUITabControl.Make( + ("Profiles", BuildProfilesTab()), + ("Notes", BuildNotesTab()), + ("Arrows", BuildArrowsTab()), + ("Dots", BuildDotsTab()), + ("Bombs", BuildBombsTab()), + ("Arcs", BuildArcsTab()), + ("BurstNotes", BuildBurstNotesTab()) + ) + .OnActiveChanged(OnTabSelected) + .Bind(ref m_TabControl) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + + m_ProfileImportModal = CreateModal(); + + ProfilesTab_Refresh(); + OnSettingChanged(); + } + /// + /// On view deactivation + /// + protected override sealed void OnViewDeactivation() + { + NTConfig.Instance.Save(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Build profiles tab + /// + /// + private IXUIElement BuildProfilesTab() + { + return XUIVLayout.Make( + XUIHLayout.Make( + XUIVVList.Make() + .SetListCellPrefab(ListCellPrefabs.Get()) + .OnListItemSelected(ProfilesTab_OnListItemSelect) + .Bind(ref m_ProfilesTab_List) + ) + .SetHeight(50) + .SetSpacing(0) + .SetPadding(0) + .SetBackground(true) + .OnReady(x => x.CSizeFitter.horizontalFit = x.CSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandWidth = true) + .OnReady(x => x.HOrVLayoutGroup.childForceExpandHeight = true), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("New").OnClick(ProfilesTab_OnNewButton), + XUIPrimaryButton.Make("Rename").OnClick(ProfilesTab_OnRenameButton), + XUIPrimaryButton.Make("Delete").OnClick(ProfilesTab_OnDeleteButton), + XUISecondaryButton.Make("Export").OnClick(ProfilesTab_OnExportButton), + XUISecondaryButton.Make("Import").OnClick(ProfilesTab_OnImportButton) + ) + ).OnReady(x => x.CSizeFitter.enabled = false); + } + /// + /// Build notes tab + /// + /// + private IXUIElement BuildNotesTab() + { + return XUIVLayout.Make( + XUIVLayout.Make( + XUIText.Make("Note scale"), + XUISlider.Make() + .SetMinValue(0.4f).SetMaxValue(1.2f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_NotesTab_Scale), + + XUIText.Make("Show dot on directional notes"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_NotesTab_ShowPrecisonDots), + + XUIText.Make("Precision dot scale"), + XUISlider.Make() + .SetMinValue(0.2f).SetMaxValue(1.5f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_NotesTab_PrecisionDotsScale) + ) + .SetWidth(80.0f) + .SetPadding(0), + + XUIVLayout.Make( + XUIText.Make("This module change only the visual appearance of the notes, the hitbox will stay the same as default"), + XUIText.Make("The scale settings can conflict with CustomNotes if not 100%") + ) + .SetBackground(true) + ); + } + /// + /// Build arrows tab + /// + /// + private IXUIElement BuildArrowsTab() + { + return XUIVLayout.Make( + XUIText.Make("Arrow scale"), + XUISlider.Make() + .SetMinValue(0.2f).SetMaxValue(1.4f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_ArrowsTab_Scale), + + XUIText.Make("Arrow glow intensity"), + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_ArrowsTab_Intensity), + + XUIText.Make("Override arrow colors"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_ArrowsTab_OverrideColors), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Arrow left color"), + XUIColorInput.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_ArrowsTab_LColor) + ), + XUIVLayout.Make( + XUIText.Make("Arrow right color"), + XUIColorInput.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_ArrowsTab_RColor) + ) + ) + .SetWidth(80.0f) + .SetPadding(0) + ); + } + /// + /// Build dots tab + /// + /// + private IXUIElement BuildDotsTab() + { + return XUIVLayout.Make( + XUIText.Make("Dot scale"), + XUISlider.Make() + .SetMinValue(0.2f).SetMaxValue(1.5f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_DotsTab_Scale), + + XUIText.Make("Dot glow intensity"), + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_DotsTab_Intensity), + + XUIText.Make("Override dot colors"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_DotsTab_OverrideColors), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Dot left color"), + XUIColorInput.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_DotsTab_LColor) + ), + XUIVLayout.Make( + XUIText.Make("Dot right color"), + XUIColorInput.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_DotsTab_RColor) + ) + ) + .SetWidth(80.0f) + .SetPadding(0) + ); + } + /// + /// Build bombs tabs + /// + /// + private IXUIElement BuildBombsTab() + { + return XUIVLayout.Make( + XUIText.Make("Bomb scale"), + XUISlider.Make() + .SetMinValue(0.4f).SetMaxValue(1.2f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_BombsTab_Scale), + + XUIText.Make("Override bomb color"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_BombsTab_OverrideColor), + + XUIText.Make("Bomb color"), + XUIColorInput.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_BombsTab_Color) + ) + .SetWidth(80.0f); + } + /// + /// Build arcs tab + /// + /// + private IXUIElement BuildArcsTab() + { + return XUIVLayout.Make( + XUIText.Make("Arcs intensity"), + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(1.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_ArcsTab_Intensity), + + XUIText.Make("Arcs haptics"), + XUIToggle.Make() + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_ArcsTab_Haptics) + ) + .SetWidth(80.0f); + } + /// + /// Build burst notes tab + /// + /// + private IXUIElement BuildBurstNotesTab() + { + return XUIVLayout.Make( + XUIText.Make("Dot size"), + XUISlider.Make() + .SetMinValue(0.0f).SetMaxValue(3.0f).SetIncrements(0.01f).SetFormatter(CP_SDK.UI.ValueFormatters.Percentage) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_BurstNotesTab_DotScale) + ) + .SetWidth(80.0f); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When a tab is selected + /// + /// Tab index + private void OnTabSelected(int p_TabIndex) + { + if (NTConfig.Instance.GetActiveProfile().IsDefault() && p_TabIndex > 0) + { + ShowMessageModal("No changes allowed on default config!"); + m_TabControl.SetActiveTab(0); + } + } + /// + /// When settings are changed + /// + /// + private void OnSettingChanged() + { + if (m_PreventChanges) + return; + + var l_Profile = NTConfig.Instance.GetActiveProfile(); + + #region Notes Tab + l_Profile.NotesScale = m_NotesTab_Scale.Element.GetValue(); + l_Profile.NotesShowPrecisonDots = m_NotesTab_ShowPrecisonDots.Element.GetValue(); + l_Profile.NotesPrecisonDotsScale = m_NotesTab_PrecisionDotsScale.Element.GetValue(); + #endregion + + #region Arrows Tab + l_Profile.ArrowsScale = m_ArrowsTab_Scale.Element.GetValue(); + l_Profile.ArrowsIntensity = m_ArrowsTab_Intensity.Element.GetValue(); + l_Profile.ArrowsOverrideColors = m_ArrowsTab_OverrideColors.Element.GetValue(); + l_Profile.ArrowsLColor = m_ArrowsTab_LColor.Element.GetValue(); + l_Profile.ArrowsRColor = m_ArrowsTab_RColor.Element.GetValue(); + + m_ArrowsTab_LColor.SetInteractable(l_Profile.ArrowsOverrideColors); + m_ArrowsTab_RColor.SetInteractable(l_Profile.ArrowsOverrideColors); + #endregion + + #region Dots Tab + l_Profile.DotsScale = m_DotsTab_Scale.Element.GetValue(); + l_Profile.DotsIntensity = m_DotsTab_Intensity.Element.GetValue(); + l_Profile.DotsOverrideColors = m_DotsTab_OverrideColors.Element.GetValue(); + l_Profile.DotsLColor = m_DotsTab_LColor.Element.GetValue(); + l_Profile.DotsRColor = m_DotsTab_RColor.Element.GetValue(); + + m_DotsTab_LColor.SetInteractable(l_Profile.DotsOverrideColors); + m_DotsTab_RColor.SetInteractable(l_Profile.DotsOverrideColors); + #endregion + + #region Bombs Tab + l_Profile.BombsScale = m_BombsTab_Scale.Element.GetValue(); + l_Profile.BombsOverrideColor = m_BombsTab_OverrideColor.Element.GetValue(); + l_Profile.BombsColor = m_BombsTab_Color.Element.GetValue(); + + m_BombsTab_Color.SetInteractable(l_Profile.BombsOverrideColor); + #endregion + + #region Arcs Tab + l_Profile.ArcsIntensity = m_ArcsTab_Intensity.Element.GetValue(); + l_Profile.ArcsHaptics = m_ArcsTab_Haptics.Element.GetValue(); + #endregion + + #region BurstNotes Tab + l_Profile.BurstNotesDotsScale = m_BurstNotesTab_DotScale.Element.GetValue(); + #endregion + + /// Refresh preview + SettingsRightView.Instance.RefreshSettings(); + } + /// + /// Reset settings + /// + internal void RefreshSettings() + { + var l_Profile = NTConfig.Instance.GetActiveProfile(); + + m_PreventChanges = true; + + #region Notes Tab + m_NotesTab_Scale .SetValue(l_Profile.NotesScale); + m_NotesTab_ShowPrecisonDots .SetValue(l_Profile.NotesShowPrecisonDots); + m_NotesTab_PrecisionDotsScale .SetValue(l_Profile.NotesPrecisonDotsScale); + #endregion + + #region Arrows Tab + m_ArrowsTab_Scale .SetValue(l_Profile.ArrowsScale); + m_ArrowsTab_Intensity .SetValue(l_Profile.ArrowsIntensity); + m_ArrowsTab_OverrideColors .SetValue(l_Profile.ArrowsOverrideColors); + m_ArrowsTab_LColor .SetValue(ColorU.WithAlpha(l_Profile.ArrowsLColor, 1.00f)); + m_ArrowsTab_RColor .SetValue(ColorU.WithAlpha(l_Profile.ArrowsRColor, 1.00f)); + + m_ArrowsTab_LColor.SetInteractable(l_Profile.ArrowsOverrideColors); + m_ArrowsTab_RColor.SetInteractable(l_Profile.ArrowsOverrideColors); + #endregion + + #region Dots Tab + m_DotsTab_Scale .SetValue(l_Profile.DotsScale); + m_DotsTab_Intensity .SetValue(l_Profile.DotsIntensity); + m_DotsTab_OverrideColors.SetValue(l_Profile.DotsOverrideColors); + m_DotsTab_LColor .SetValue(ColorU.WithAlpha(l_Profile.DotsLColor, 1.00f)); + m_DotsTab_RColor .SetValue(ColorU.WithAlpha(l_Profile.DotsRColor, 1.00f)); + + m_DotsTab_LColor.SetInteractable(l_Profile.DotsOverrideColors); + m_DotsTab_RColor.SetInteractable(l_Profile.DotsOverrideColors); + #endregion + + #region Bombs Tab + m_BombsTab_Scale .SetValue(l_Profile.BombsScale); + m_BombsTab_OverrideColor.SetValue(l_Profile.BombsOverrideColor); + m_BombsTab_Color .SetValue(ColorU.WithAlpha(l_Profile.BombsColor, 1.00f)); + + m_BombsTab_Color.SetInteractable(l_Profile.BombsOverrideColor); + #endregion + + #region Arcs Tab + m_ArcsTab_Intensity .SetValue(l_Profile.ArcsIntensity); + m_ArcsTab_Haptics .SetValue(l_Profile.ArcsHaptics); + #endregion + + #region BurstNotes Tab + m_BurstNotesTab_DotScale.SetValue(l_Profile.BurstNotesDotsScale); + #endregion + + m_PreventChanges = false; + + SettingsRightView.Instance.RefreshSettings(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Refresh list + /// + private void ProfilesTab_Refresh() + { + m_Items.Clear(); + for (var l_I = 0; l_I < NTConfig.Instance.Profiles.Count; ++l_I) + m_Items.Add(new TextListItem(NTConfig.Instance.Profiles[l_I].Name, null, TMPro.TextAlignmentOptions.CaplineGeoAligned)); + + m_ProfilesTab_List.SetListItems(m_Items); + m_ProfilesTab_List.SetSelectedListItem(m_Items[NTConfig.Instance.ActiveProfile]); + } + /// + /// On item selected + /// + /// Selected item + private void ProfilesTab_OnListItemSelect(IListItem p_ListItem) + { + m_SelectedItem = (TextListItem)p_ListItem; + + if (m_SelectedItem != null) + NTConfig.Instance.ActiveProfile = NTConfig.Instance.Profiles.IndexOf(NTConfig.Instance.Profiles.FirstOrDefault(x => x.Name == m_SelectedItem.Text)); + + RefreshSettings(); + } + /// + /// New profile button + /// + private void ProfilesTab_OnNewButton() + { + ShowKeyboardModal("", (p_Name) => + { + var l_ProfileName = string.IsNullOrEmpty(p_Name) ? "No name..." : p_Name; + var l_NewProfile = new NTConfig._Profile() { Name = l_ProfileName }; + + NTConfig.Instance.Profiles.Add(l_NewProfile); + NTConfig.Instance.ActiveProfile = NTConfig.Instance.Profiles.IndexOf(l_NewProfile); + + ProfilesTab_Refresh(); + RefreshSettings(); + }); + } + /// + /// Rename profile button + /// + private void ProfilesTab_OnRenameButton() + { + if (NTConfig.Instance.GetActiveProfile().IsDefault()) + { + ShowMessageModal("No changes allowed on default config!"); + return; + } + + ShowKeyboardModal(NTConfig.Instance.GetActiveProfile().Name, (p_NewName) => + { + NTConfig.Instance.GetActiveProfile().Name = string.IsNullOrEmpty(p_NewName) ? "No name..." : p_NewName; + RefreshSettings(); + }); + } + /// + /// Delete profile button + /// + private void ProfilesTab_OnDeleteButton() + { + if (NTConfig.Instance.GetActiveProfile().IsDefault()) + { + ShowMessageModal("No changes allowed on default config!"); + return; + } + + ShowConfirmationModal($"Do you want to delete profile\n\"{NTConfig.Instance.GetActiveProfile().Name}\"?", (p_Confirm) => + { + if (!p_Confirm) + return; + + NTConfig.Instance.Profiles.Remove(NTConfig.Instance.GetActiveProfile()); + NTConfig.Instance.ActiveProfile = Mathf.Clamp(NTConfig.Instance.ActiveProfile - 1, 0, NTConfig.Instance.Profiles.Count); + ProfilesTab_Refresh(); + RefreshSettings(); + }); + } + /// + /// Export an profile + /// + private void ProfilesTab_OnExportButton() + { + var l_Profile = NTConfig.Instance.GetActiveProfile(); + if (l_Profile.IsDefault()) + { + ShowMessageModal("Cannot export default config!"); + return; + } + + var l_Serialized = JsonConvert.SerializeObject(l_Profile, Formatting.Indented, new JsonConverter[] + { + new CP_SDK.Config.JsonConverters.ColorConverter() + }); + + var l_FileName = CP_SDK.Misc.Time.UnixTimeNow() + "_" + l_Profile.Name + ".bspnt"; + l_FileName = string.Concat(l_FileName.Split(System.IO.Path.GetInvalidFileNameChars())); + + System.IO.File.WriteAllText(NoteTweaker.EXPORT_FOLDER + l_FileName, l_Serialized, System.Text.Encoding.Unicode); + + ShowMessageModal("Event exported in\n" + NoteTweaker.EXPORT_FOLDER); + } + /// + /// Import an profile + /// + private void ProfilesTab_OnImportButton() + { + ShowModal(m_ProfileImportModal); + m_ProfileImportModal.Init(() => ProfilesTab_Refresh()); + } + } +} diff --git a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRight.bsml b/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRight.bsml deleted file mode 100644 index 2405025..0000000 --- a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRight.bsml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRight.cs b/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRightView.cs similarity index 91% rename from Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRight.cs rename to Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRightView.cs index ee937bb..1ee8162 100644 --- a/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRight.cs +++ b/Modules/BeatSaberPlus_NoteTweaker/UI/SettingsRightView.cs @@ -1,14 +1,15 @@ -using UnityEngine; -using System.Linq; -using BeatSaberMarkupLanguage.Attributes; +using CP_SDK.Unity.Extensions; +using CP_SDK.XUI; using IPA.Utilities; +using System.Linq; +using UnityEngine; namespace BeatSaberPlus_NoteTweaker.UI { /// /// Settings right view /// - internal class SettingsRight : BeatSaberPlus.SDK.UI.ResourceViewController + internal sealed class SettingsRightView : CP_SDK.UI.ViewController { private GameObject m_Parent = null; private GameObject m_NoteTemplate = null; @@ -32,19 +33,19 @@ internal class SettingsRight : BeatSaberPlus.SDK.UI.ResourceViewController /// On view creation /// protected override void OnViewCreation() { + Templates.FullRectLayout( + Templates.TitleBar("Custom / Default"), + + XUIVSpacer.Make(65f) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + m_CustomPreviewTL = null; m_CustomPreviewTR = null; m_CustomPreviewDL = null; @@ -60,35 +61,35 @@ protected override void OnViewCreation() m_DefaultPreviewSliderFill = null; m_Parent = new GameObject(); - m_Parent.transform.position = new Vector3(3.50f, 1.35f, 2.28f); - m_Parent.transform.rotation = Quaternion.Euler(0.0f, 140.30f, 0.0f); + m_Parent.transform.position = new Vector3(3.25f, 1.15f, 2.28f); + m_Parent.transform.rotation = Quaternion.Euler(0.0f, 145.30f, 0.0f); GameObject.DontDestroyOnLoad(m_Parent); var l_MenuTransitionsHelper = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - var l_StandardLevelScenesTransitionSetupData = l_MenuTransitionsHelper.GetField("_standardLevelScenesTransitionSetupData"); - var l_StandardGameplaySceneInfo = l_StandardLevelScenesTransitionSetupData.GetField("_standardGameplaySceneInfo"); - var l_GameCoreSceneInfo = l_StandardLevelScenesTransitionSetupData.GetField("_gameCoreSceneInfo"); + var l_StandardLevelScenesTransitionSetupData = l_MenuTransitionsHelper._standardLevelScenesTransitionSetupData; + var l_StandardGameplaySceneInfo = l_StandardLevelScenesTransitionSetupData._standardGameplaySceneInfo; + var l_GameCoreSceneInfo = l_StandardLevelScenesTransitionSetupData._gameCoreSceneInfo; UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(l_GameCoreSceneInfo.sceneName, UnityEngine.SceneManagement.LoadSceneMode.Additive).completed += (_) => { UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(l_StandardGameplaySceneInfo.sceneName, UnityEngine.SceneManagement.LoadSceneMode.Additive).completed += (__) => { var l_BeatmapObjectsInstaller = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - var l_OriginalNotePrefab = l_BeatmapObjectsInstaller.GetField("_normalBasicNotePrefab"); + var l_OriginalNotePrefab = l_BeatmapObjectsInstaller._normalBasicNotePrefab; m_NoteTemplate = GameObject.Instantiate(l_OriginalNotePrefab.transform.GetChild(0).gameObject); m_NoteTemplate.gameObject.SetActive(false); GameObject.DontDestroyOnLoad(m_NoteTemplate); - var l_OriginalBombPrefab = l_BeatmapObjectsInstaller.GetField("_bombNotePrefab"); + var l_OriginalBombPrefab = l_BeatmapObjectsInstaller._bombNotePrefab; m_BombTemplate = GameObject.Instantiate(l_OriginalBombPrefab.transform.GetChild(0).gameObject); m_BombTemplate.gameObject.SetActive(false); GameObject.DontDestroyOnLoad(m_BombTemplate); - var l_OriginalBurstSliderPrefab = l_BeatmapObjectsInstaller.GetField("_burstSliderNotePrefab"); + var l_OriginalBurstSliderPrefab = l_BeatmapObjectsInstaller._burstSliderNotePrefab; m_BurstSliderTemplate = GameObject.Instantiate(l_OriginalBurstSliderPrefab.transform.GetChild(0).gameObject); m_BurstSliderTemplate.gameObject.SetActive(false); @@ -106,8 +107,6 @@ protected override void OnViewCreation() /// protected override void OnViewActivation() { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); - if (m_CustomPreviewTL == null || !m_CustomPreviewTL || m_CustomPreviewTR == null || !m_CustomPreviewTR) { @@ -190,16 +189,16 @@ internal void RefreshSettings() var l_LeftColor = l_ColorScheme != null ? l_ColorScheme.saberAColor : new Color(0.658823549747467f, 0.125490203499794f, 0.125490203499794f); var l_RightColor = l_ColorScheme != null ? l_ColorScheme.saberBColor : new Color(0.125490203499794f, 0.3921568691730499f, 0.658823549747467f); - var l_ArrowLColor = l_Profile.ArrowsOverrideColors ? l_Profile.ArrowsLColor : l_LeftColor.ColorWithAlpha( l_Profile.ArrowsLColor.a); - var l_ArrowRColor = l_Profile.ArrowsOverrideColors ? l_Profile.ArrowsRColor : l_RightColor.ColorWithAlpha(l_Profile.ArrowsRColor.a); + var l_ArrowLColor = l_Profile.ArrowsOverrideColors ? l_Profile.ArrowsLColor : ColorU.WithAlpha(l_LeftColor, l_Profile.ArrowsLColor.a); + var l_ArrowRColor = l_Profile.ArrowsOverrideColors ? l_Profile.ArrowsRColor : ColorU.WithAlpha(l_RightColor, l_Profile.ArrowsRColor.a); - PatchArrow(m_CustomPreviewTL, l_Profile.ArrowsScale, l_ArrowLColor.ColorWithAlpha(l_Profile.ArrowsIntensity), true); - PatchArrow(m_CustomPreviewTR, l_Profile.ArrowsScale, l_ArrowRColor.ColorWithAlpha(l_Profile.ArrowsIntensity), true); - PatchArrow(m_CustomPreviewDL, l_Profile.ArrowsScale, l_ArrowLColor.ColorWithAlpha(l_Profile.ArrowsIntensity), false); - PatchArrow(m_CustomPreviewDR, l_Profile.ArrowsScale, l_ArrowRColor.ColorWithAlpha(l_Profile.ArrowsIntensity), false); + PatchArrow(m_CustomPreviewTL, l_Profile.ArrowsScale, ColorU.WithAlpha(l_ArrowLColor, l_Profile.ArrowsIntensity), true); + PatchArrow(m_CustomPreviewTR, l_Profile.ArrowsScale, ColorU.WithAlpha(l_ArrowRColor, l_Profile.ArrowsIntensity), true); + PatchArrow(m_CustomPreviewDL, l_Profile.ArrowsScale, ColorU.WithAlpha(l_ArrowLColor, l_Profile.ArrowsIntensity), false); + PatchArrow(m_CustomPreviewDR, l_Profile.ArrowsScale, ColorU.WithAlpha(l_ArrowRColor, l_Profile.ArrowsIntensity), false); - var l_DotLColor = l_Profile.DotsOverrideColors ? l_Profile.DotsLColor : l_LeftColor.ColorWithAlpha( l_Profile.DotsLColor.a); - var l_DotRColor = l_Profile.DotsOverrideColors ? l_Profile.DotsRColor : l_RightColor.ColorWithAlpha(l_Profile.DotsRColor.a); + var l_DotLColor = l_Profile.DotsOverrideColors ? l_Profile.DotsLColor : ColorU.WithAlpha(l_LeftColor, l_Profile.DotsLColor.a); + var l_DotRColor = l_Profile.DotsOverrideColors ? l_Profile.DotsRColor : ColorU.WithAlpha(l_RightColor, l_Profile.DotsRColor.a); PatchCircle(m_CustomPreviewTL, l_Profile.NotesPrecisonDotsScale, l_DotLColor, l_Profile.NotesShowPrecisonDots); PatchCircle(m_CustomPreviewTR, l_Profile.NotesPrecisonDotsScale, l_DotRColor, l_Profile.NotesShowPrecisonDots); @@ -312,8 +311,8 @@ private void CreatePreview(GameObject p_NoteTemplate, GameObject p_BombTemplate, PatchNote(m_DefaultPreviewDL, l_LeftColor); PatchNote(m_DefaultPreviewDR, l_RightColor); - PatchArrow(m_DefaultPreviewTL, 1f, l_LeftColor.ColorWithAlpha(0.6f), true); - PatchArrow(m_DefaultPreviewTR, 1f, l_RightColor.ColorWithAlpha(0.6f), true); + PatchArrow(m_DefaultPreviewTL, 1f, ColorU.WithAlpha(l_LeftColor, 0.6f), true); + PatchArrow(m_DefaultPreviewTR, 1f, ColorU.WithAlpha(l_RightColor, 0.6f), true); PatchArrow(m_DefaultPreviewDL, 0f, Color.white, false); PatchArrow(m_DefaultPreviewDR, 0f, Color.white, false); @@ -415,7 +414,7 @@ private void PatchNote(GameObject p_Object, Color p_Color) foreach (var l_PropertyBlockController in p_Object.GetComponents()) { - l_PropertyBlockController.materialPropertyBlock.SetColor(Shader.PropertyToID("_Color"), p_Color.ColorWithAlpha(1f)); + l_PropertyBlockController.materialPropertyBlock.SetColor(Shader.PropertyToID("_Color"), ColorU.WithAlpha(p_Color, 1f)); l_PropertyBlockController.materialPropertyBlock.SetFloat(Shader.PropertyToID("_EnableRimDim"), 0f); l_PropertyBlockController.materialPropertyBlock.SetFloat(Shader.PropertyToID("_EnableFog"), 0f); l_PropertyBlockController.materialPropertyBlock.SetFloat(Shader.PropertyToID("_RimDarkenning"), 0f); @@ -450,7 +449,7 @@ private void PatchArrow(GameObject p_Note, float p_Scale, Color p_Color, bool p_ l_PropertyBlockController.ApplyChanges(); } - l_Glow.GetComponent().enabled = p_Show; + l_Glow.GetComponent().enabled = p_Show; } } /// diff --git a/Modules/BeatSaberPlus_NoteTweaker/manifest.json b/Modules/BeatSaberPlus_NoteTweaker/manifest.json index dbebc24..5cc4e6e 100644 --- a/Modules/BeatSaberPlus_NoteTweaker/manifest.json +++ b/Modules/BeatSaberPlus_NoteTweaker/manifest.json @@ -3,13 +3,12 @@ "id": "BeatSaberPlus_NoteTweaker", "name": "BeatSaberPlus_NoteTweaker", "author": "HardCPP#1985", - "version": "5.0.7", + "version": "6.0.8", "description": "", - "gameVersion": "1.25.0", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.0.1", - "BeatSaberMarkupLanguage": "^1.3.4", - "BeatSaberPlusCORE": "^5.0.7" + "BSIPA": "^4.3.0", + "BeatSaberPlusCORE": "^6.0.8" }, "links": { "project-home": "https://discord.chatplex.org", diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/Plugin.cs b/Modules/BeatSaberPlus_SongChartVisualizer/BSIPA.cs similarity index 91% rename from Modules/BeatSaberPlus_SongChartVisualizer/Plugin.cs rename to Modules/BeatSaberPlus_SongChartVisualizer/BSIPA.cs index 64d9249..1e1dd73 100644 --- a/Modules/BeatSaberPlus_SongChartVisualizer/Plugin.cs +++ b/Modules/BeatSaberPlus_SongChartVisualizer/BSIPA.cs @@ -16,7 +16,7 @@ public class Plugin public Plugin(IPA.Logging.Logger p_Logger) { /// Setup logger - Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); + ChatPlexMod_SongChartVisualizer.Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); } //////////////////////////////////////////////////////////////////////////// diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/BeatSaberPlus_SongChartVisualizer.csproj b/Modules/BeatSaberPlus_SongChartVisualizer/BeatSaberPlus_SongChartVisualizer.csproj index 380f0bc..39f4321 100644 --- a/Modules/BeatSaberPlus_SongChartVisualizer/BeatSaberPlus_SongChartVisualizer.csproj +++ b/Modules/BeatSaberPlus_SongChartVisualizer/BeatSaberPlus_SongChartVisualizer.csproj @@ -24,15 +24,14 @@ true false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG;BEATSABER prompt 4 true bin\Release\ - - + BEATSABER prompt 4 @@ -52,11 +51,6 @@ False False - - $(BeatSaberDir)\Plugins\BSML.dll - False - False - $(BeatSaberDir)\Beat Saber_Data\Managed\Colors.dll False @@ -74,22 +68,10 @@ - - - - $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll - False - $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll False @@ -116,60 +98,39 @@ False False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll False + False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll + + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\VRUI.dll - False - False - - - - + + + + + - - - - - - + + + + + + - - Settings.cs - - - FloatingWindow.cs - - - SettingsLeft.cs - - - SettingsRight.cs - - - - diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/BeatSaberPlus_SongChartVisualizer.csproj.user b/Modules/BeatSaberPlus_SongChartVisualizer/BeatSaberPlus_SongChartVisualizer.csproj.user index 5db8d9ee60929239d9779dc6a24adfdc0c01779f..012781eeb9f58b5f0dd22267773d93a2d711944f 100644 GIT binary patch delta 25 fcmaFI@`q)^I!0av215ot1|tSbAZa*xE#pA|UV#Rx delta 11 Tcmeyv@{VQ0I>yO+7!LpdA+QBv diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/DataPoint.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/DataPoint.cs new file mode 100644 index 0000000..3fcd6a4 --- /dev/null +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/DataPoint.cs @@ -0,0 +1,11 @@ +namespace ChatPlexMod_SongChartVisualizer.Data +{ + /// + /// Graph point + /// + internal struct GraphPoint + { + public float X; + public float Y; + } +} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/Graph.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/Graph.cs new file mode 100644 index 0000000..dee6e23 --- /dev/null +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/Graph.cs @@ -0,0 +1,50 @@ +using UnityEngine; + +namespace ChatPlexMod_SongChartVisualizer.Data +{ + /// + /// Graph instance + /// + internal class Graph + { + internal GraphPoint[] Points; + internal float SongDuration; + internal float MinY; + internal float MaxY; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Constructor + /// + /// Song duration + internal Graph(float p_SongDuration) + { + SongDuration = p_SongDuration; + MinY = 0.0f; + MaxY = 0.0f; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + internal float SongTimeToProgress(float p_SongTime) + => (p_SongTime / (SongDuration + 1)); + internal int ProgressToIndexLow(float p_Progress) + { + var l_Index = (int)(p_Progress * (float)SongChartVisualizer.MaxPoints); + return Mathf.Clamp(l_Index, 0, SongChartVisualizer.MaxPoints - 1); + } + internal float ProgressToIndexRelativeVirtual(float p_Progress) + { + var l_RelativeVirtual = p_Progress * (float)SongChartVisualizer.MaxPoints; + return (float)l_RelativeVirtual - (int)l_RelativeVirtual; + } + internal int ProgressToIndexHi(float p_Progress) + { + var l_Index = (int)(p_Progress * (float)SongChartVisualizer.MaxPoints) + 1; + return Mathf.Clamp(l_Index, 0, SongChartVisualizer.MaxPoints - 1); + } + } +} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/GraphBuilder.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/GraphBuilder.cs new file mode 100644 index 0000000..f308ffa --- /dev/null +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Data/GraphBuilder.cs @@ -0,0 +1,322 @@ +namespace ChatPlexMod_SongChartVisualizer.Data +{ + /// + /// Graph builder utils + /// + internal static class GraphBuilder + { + private static Graph m_SampleGraph = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + +#if BEATSABER + internal static Graph BuildNPSGraph(IReadonlyBeatmapData p_TransformedBeatmapData, float p_SongDuration) +#else +#error Missing game implementation +#endif + { + var l_Graph = new Graph(p_SongDuration); + var l_NPSSRaw = CP_SDK.Pool.ListPool.Get(); + var l_DataPoints = CP_SDK.Pool.ListPool.Get(); + + try + { + l_NPSSRaw.Clear(); + l_DataPoints.Clear(); + + if (l_NPSSRaw.Capacity < (int)(p_SongDuration + 1)) l_NPSSRaw.Capacity = (int)(p_SongDuration + 1); + if (l_DataPoints.Capacity < SongChartVisualizer.MaxPoints) l_DataPoints.Capacity = SongChartVisualizer.MaxPoints; + + for (var l_I = 0; l_I < (int)(p_SongDuration + 1f); ++l_I) + l_NPSSRaw.Add(0.0f); + +#if BEATSABER + var l_Iterator = p_TransformedBeatmapData.allBeatmapDataItems.GetEnumerator(); + while (l_Iterator.MoveNext()) + { + var l_Object = l_Iterator.Current; + if (l_Object.type != BeatmapDataItem.BeatmapDataItemType.BeatmapObject + || !(l_Object is NoteData l_NoteData) + || l_NoteData.colorType == ColorType.None) + continue; + + l_NPSSRaw[(int)l_NoteData.time]++; + } + l_Iterator.Dispose(); +#else +#error Missing game implementation +#endif + + var l_ResamplePoint = ((double)l_NPSSRaw.Count / (double)SongChartVisualizer.MaxPoints); + var l_ResampleMaxIndex = l_NPSSRaw.Count - 1; + for (var l_I = 0; l_I < SongChartVisualizer.MaxPoints; ++l_I) + { + var l_VPoint = (double)l_I * l_ResamplePoint; + var l_FirstIndex = (int)l_VPoint; + var l_SecondIndex = l_FirstIndex >= l_ResampleMaxIndex ? l_FirstIndex : (l_FirstIndex + 1); + var l_DeltaT = l_VPoint - l_FirstIndex; + var l_Value = (float)(l_NPSSRaw[l_FirstIndex] * (1 - l_DeltaT) + l_NPSSRaw[l_SecondIndex] * l_DeltaT); + + l_Graph.MinY = System.Math.Min(l_Graph.MinY, l_Value); + l_Graph.MaxY = System.Math.Max(l_Graph.MaxY, l_Value); + + l_DataPoints.Add(new GraphPoint() + { + X = l_I, + Y = l_Value + }); + } + + l_Graph.Points = l_DataPoints.ToArray(); + } + finally + { + l_NPSSRaw.Clear(); + l_DataPoints.Clear(); + + CP_SDK.Pool.ListPool.Release(l_NPSSRaw); + CP_SDK.Pool.ListPool.Release(l_DataPoints); + } +#if FALSE + var l_Builder = new System.Text.StringBuilder(); + l_Builder.AppendLine("var l_Graph = new Graph(" + p_SongDuration.ToString("0.00").Replace(',', '.') + "f)"); + l_Builder.AppendLine("{"); + l_Builder.AppendLine(" MinY = " + l_Graph.MinY.ToString("0.00").Replace(',', '.') + "f,"); + l_Builder.AppendLine(" MaxY = " + l_Graph.MaxY.ToString("0.00").Replace(',', '.') + "f,"); + l_Builder.AppendLine(" Points = new GraphPoint[]"); + l_Builder.AppendLine(" {"); + + for (var l_I = 0; l_I < l_Graph.Points.Length; ++l_I) + l_Builder.AppendLine(" new GraphPoint { X = " + l_Graph.Points[l_I].X.ToString("0.00").Replace(',', '.') + "f, Y = " + l_Graph.Points[l_I].Y.ToString("0.00").Replace(',', '.') + "f }" + ((l_I < (l_Graph.Points.Length - 1)) ? "," : "")); + + l_Builder.AppendLine(" }"); + l_Builder.AppendLine("};"); + + System.IO.File.WriteAllText("graph.cs", l_Builder.ToString()); +#endif + + return l_Graph; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get sample NPS graph + /// + /// + internal static Graph GetSampleNPSGraph() + { + if (m_SampleGraph != null) + return m_SampleGraph; + + m_SampleGraph = new Graph(225.50f) + { + MinY = 0.00f, + MaxY = 13.60f, + Points = new GraphPoint[] + { + new GraphPoint { X = 0.00f, Y = 0.00f }, + new GraphPoint { X = 1.00f, Y = 3.48f }, + new GraphPoint { X = 2.00f, Y = 4.44f }, + new GraphPoint { X = 3.00f, Y = 4.00f }, + new GraphPoint { X = 4.00f, Y = 4.92f }, + new GraphPoint { X = 5.00f, Y = 2.60f }, + new GraphPoint { X = 6.00f, Y = 4.12f }, + new GraphPoint { X = 7.00f, Y = 3.82f }, + new GraphPoint { X = 8.00f, Y = 5.76f }, + new GraphPoint { X = 9.00f, Y = 3.34f }, + new GraphPoint { X = 10.00f, Y = 3.80f }, + new GraphPoint { X = 11.00f, Y = 3.28f }, + new GraphPoint { X = 12.00f, Y = 5.00f }, + new GraphPoint { X = 13.00f, Y = 3.76f }, + new GraphPoint { X = 14.00f, Y = 4.28f }, + new GraphPoint { X = 15.00f, Y = 5.80f }, + new GraphPoint { X = 16.00f, Y = 6.04f }, + new GraphPoint { X = 17.00f, Y = 6.42f }, + new GraphPoint { X = 18.00f, Y = 5.28f }, + new GraphPoint { X = 19.00f, Y = 4.82f }, + new GraphPoint { X = 20.00f, Y = 5.60f }, + new GraphPoint { X = 21.00f, Y = 4.76f }, + new GraphPoint { X = 22.00f, Y = 6.72f }, + new GraphPoint { X = 23.00f, Y = 2.04f }, + new GraphPoint { X = 24.00f, Y = 4.72f }, + new GraphPoint { X = 25.00f, Y = 6.00f }, + new GraphPoint { X = 26.00f, Y = 11.56f }, + new GraphPoint { X = 27.00f, Y = 12.88f }, + new GraphPoint { X = 28.00f, Y = 3.00f }, + new GraphPoint { X = 29.00f, Y = 8.08f }, + new GraphPoint { X = 30.00f, Y = 12.40f }, + new GraphPoint { X = 31.00f, Y = 10.24f }, + new GraphPoint { X = 32.00f, Y = 5.72f }, + new GraphPoint { X = 33.00f, Y = 0.58f }, + new GraphPoint { X = 34.00f, Y = 5.52f }, + new GraphPoint { X = 35.00f, Y = 2.40f }, + new GraphPoint { X = 36.00f, Y = 8.00f }, + new GraphPoint { X = 37.00f, Y = 2.62f }, + new GraphPoint { X = 38.00f, Y = 10.00f }, + new GraphPoint { X = 39.00f, Y = 8.86f }, + new GraphPoint { X = 40.00f, Y = 8.80f }, + new GraphPoint { X = 41.00f, Y = 9.34f }, + new GraphPoint { X = 42.00f, Y = 11.00f }, + new GraphPoint { X = 43.00f, Y = 0.00f }, + new GraphPoint { X = 44.00f, Y = 0.00f }, + new GraphPoint { X = 45.00f, Y = 4.20f }, + new GraphPoint { X = 46.00f, Y = 5.04f }, + new GraphPoint { X = 47.00f, Y = 5.10f }, + new GraphPoint { X = 48.00f, Y = 5.00f }, + new GraphPoint { X = 49.00f, Y = 6.96f }, + new GraphPoint { X = 50.00f, Y = 8.00f }, + new GraphPoint { X = 51.00f, Y = 5.78f }, + new GraphPoint { X = 52.00f, Y = 6.00f }, + new GraphPoint { X = 53.00f, Y = 0.44f }, + new GraphPoint { X = 54.00f, Y = 0.00f }, + new GraphPoint { X = 55.00f, Y = 2.60f }, + new GraphPoint { X = 56.00f, Y = 3.00f }, + new GraphPoint { X = 57.00f, Y = 5.18f }, + new GraphPoint { X = 58.00f, Y = 10.12f }, + new GraphPoint { X = 59.00f, Y = 3.00f }, + new GraphPoint { X = 60.00f, Y = 0.00f }, + new GraphPoint { X = 61.00f, Y = 0.86f }, + new GraphPoint { X = 62.00f, Y = 3.88f }, + new GraphPoint { X = 63.00f, Y = 3.38f }, + new GraphPoint { X = 64.00f, Y = 3.00f }, + new GraphPoint { X = 65.00f, Y = 3.10f }, + new GraphPoint { X = 66.00f, Y = 10.36f }, + new GraphPoint { X = 67.00f, Y = 10.42f }, + new GraphPoint { X = 68.00f, Y = 11.04f }, + new GraphPoint { X = 69.00f, Y = 7.12f }, + new GraphPoint { X = 70.00f, Y = 8.00f }, + new GraphPoint { X = 71.00f, Y = 9.92f }, + new GraphPoint { X = 72.00f, Y = 1.84f }, + new GraphPoint { X = 73.00f, Y = 1.02f }, + new GraphPoint { X = 74.00f, Y = 5.44f }, + new GraphPoint { X = 75.00f, Y = 6.00f }, + new GraphPoint { X = 76.00f, Y = 6.00f }, + new GraphPoint { X = 77.00f, Y = 5.10f }, + new GraphPoint { X = 78.00f, Y = 8.72f }, + new GraphPoint { X = 79.00f, Y = 8.92f }, + new GraphPoint { X = 80.00f, Y = 7.40f }, + new GraphPoint { X = 81.00f, Y = 12.64f }, + new GraphPoint { X = 82.00f, Y = 9.04f }, + new GraphPoint { X = 83.00f, Y = 7.90f }, + new GraphPoint { X = 84.00f, Y = 13.36f }, + new GraphPoint { X = 85.00f, Y = 13.00f }, + new GraphPoint { X = 86.00f, Y = 8.32f }, + new GraphPoint { X = 87.00f, Y = 0.00f }, + new GraphPoint { X = 88.00f, Y = 0.00f }, + new GraphPoint { X = 89.00f, Y = 13.00f }, + new GraphPoint { X = 90.00f, Y = 13.60f }, + new GraphPoint { X = 91.00f, Y = 12.34f }, + new GraphPoint { X = 92.00f, Y = 12.92f }, + new GraphPoint { X = 93.00f, Y = 13.00f }, + new GraphPoint { X = 94.00f, Y = 11.80f }, + new GraphPoint { X = 95.00f, Y = 11.90f }, + new GraphPoint { X = 96.00f, Y = 13.00f }, + new GraphPoint { X = 97.00f, Y = 13.00f }, + new GraphPoint { X = 98.00f, Y = 4.68f }, + new GraphPoint { X = 99.00f, Y = 0.00f } + } + }; + + return m_SampleGraph; + } + + + /* + if (l_Notes.Count > 0) + { + var maximum_tolerance = .06 + 1e-9;// # Magic number based on maximum tolerated swing speed + var maximum_window_tolerance = .07 + 1e-9; //# For windowed sliders + + NoteData l_LastRed = null; + NoteData l_LastBlue = null; + + Dictionary l_SPSR = new Dictionary(); + Dictionary l_SPSB = new Dictionary(); + for (int l_I = 0; l_I < (l_SongDuration / l_PreviewBeatmapLevel.beatsPerMinute * 60f) + 1; ++l_I) + { + l_SPSR.Add(l_I, 0); + l_SPSB.Add(l_I, 0); + } + + foreach (var l_Note in l_Notes) + { + var real_time = l_Note.time / l_PreviewBeatmapLevel.beatsPerMinute * 60f; + + if (l_Note.colorType == ColorType.ColorA) + { + if (l_LastRed != null) + { + bool l_IsWindow = Mathf.Max(Mathf.Abs(l_Note.lineIndex - l_LastRed.lineIndex), Mathf.Abs(l_Note.noteLineLayer - l_LastRed.noteLineLayer)) >= 2; + if (l_IsWindow && (((l_Note.time - l_LastRed.time) / l_PreviewBeatmapLevel.beatsPerMinute * 60f) > maximum_window_tolerance) + || (((l_Note.time - l_LastRed.time) / l_PreviewBeatmapLevel.beatsPerMinute * 60f) > maximum_tolerance) + ) + l_SPSR[(int)Mathf.Floor(real_time)]++; + } + else + l_SPSR[(int)Mathf.Floor(real_time)]++; + + l_LastRed = l_Note; + } + else if (l_Note.colorType == ColorType.ColorB) + { + + if (l_LastBlue != null) + { + bool l_IsWindow = Mathf.Max(Mathf.Abs(l_Note.lineIndex - l_LastBlue.lineIndex), Mathf.Abs(l_Note.noteLineLayer - l_LastBlue.noteLineLayer)) >= 2; + if (l_IsWindow && (((l_Note.time - l_LastBlue.time) / l_PreviewBeatmapLevel.beatsPerMinute * 60f) > maximum_window_tolerance) + || (((l_Note.time - l_LastBlue.time) / l_PreviewBeatmapLevel.beatsPerMinute * 60f) > maximum_tolerance) + ) + l_SPSB[(int)Mathf.Floor(real_time)]++; + } + else + l_SPSB[(int)Mathf.Floor(real_time)]++; + + l_LastBlue = l_Note; + } + } + + var swing_count_list = new Dictionary(l_SPSR); + foreach (var l_KVP in l_SPSB) + { + if (!swing_count_list.ContainsKey(l_KVP.Key)) + swing_count_list.Add(l_KVP.Key, l_KVP.Value); + else + swing_count_list[l_KVP.Key] += l_KVP.Value; + } + + for (int l_I = 0; l_I < swing_count_list.Count; ++l_I) + { + float l_StartTime = (l_I / 60f) * l_PreviewBeatmapLevel.beatsPerMinute; + float l_SectionEndTime = (((l_I == swing_count_list.Count - 1) ? (l_SongDuration / l_PreviewBeatmapLevel.beatsPerMinute * 60f) : l_I + 1) / 60f) * l_PreviewBeatmapLevel.beatsPerMinute; + l_GraphData.Add((swing_count_list[l_I], l_StartTime, l_SectionEndTime)); + } + + + // float l_PrevSPS = 0f; + // float l_Threshold = 0.1f; + // float l_SectionStart = 0f; + // for (int l_I = 0; l_I < swing_count_list.Max(x => x.Key); ++l_I) + // { + // float l_CurrentSPS = ((float)swing_count_list[l_I]) / ((float)l_I - l_SectionStart); + // + // if (l_I == (swing_count_list.Count - 1) + // || (l_GraphData.Count == 0 && l_CurrentSPS != 0f) + // || (((float)l_I) - l_SectionStart > 1f && Mathf.Abs(l_CurrentSPS - l_PrevSPS) > (l_PrevSPS * l_Threshold))) + // { + // var l_SectionSPS = swing_count_list.Where(x => x.Key >= l_SectionStart && x.Key < (l_I + 1)).Sum(x => x.Value) / (l_I - l_SectionStart); + // l_PrevSPS = l_SectionSPS; + // + // l_GraphData.Add((l_SectionSPS, (l_SectionStart / 60f) * l_DifficultyBeatmap.level.beatsPerMinute, (((float)l_I) / 60f) * l_DifficultyBeatmap.level.beatsPerMinute)); + // l_SectionStart = (float)l_I; + // } + // } + + swing_count_list.ToList().ForEach(x => Logger.Instance.Debug(string.Format("{0} - {1}", x.Key, x.Value))); + + */ + + } +} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/Logger.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Logger.cs similarity index 83% rename from Modules/BeatSaberPlus_SongChartVisualizer/Logger.cs rename to Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Logger.cs index e893b11..df71b65 100644 --- a/Modules/BeatSaberPlus_SongChartVisualizer/Logger.cs +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/Logger.cs @@ -1,4 +1,4 @@ -namespace BeatSaberPlus_SongChartVisualizer +namespace ChatPlexMod_SongChartVisualizer { /// /// Logger instance holder diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/SCVConfig.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/SCVConfig.cs similarity index 74% rename from Modules/BeatSaberPlus_SongChartVisualizer/SCVConfig.cs rename to Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/SCVConfig.cs index 919ae37..aad5fe0 100644 --- a/Modules/BeatSaberPlus_SongChartVisualizer/SCVConfig.cs +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/SCVConfig.cs @@ -1,16 +1,19 @@ using Newtonsoft.Json; using UnityEngine; -namespace BeatSaberPlus_SongChartVisualizer +namespace ChatPlexMod_SongChartVisualizer { + /// + /// Config class + /// internal class SCVConfig : CP_SDK.Config.JsonConfig { [JsonProperty] internal bool Enabled = false; - [JsonProperty] internal bool AlignWithFloor = true; - [JsonProperty] internal bool ShowLockIcon = true; + [JsonProperty] internal bool AlignWithFloor = true; + [JsonProperty] internal bool ShowLockIcon = true; [JsonProperty] internal bool FollowEnvironementRotation = true; - [JsonProperty] internal bool ShowNPSLegend = false; + [JsonProperty] internal bool ShowNPSLegend = false; [JsonProperty] internal Color BackgroundColor = new Color(0.00f, 0.00f, 0.00f, 0.50f); [JsonProperty] internal Color CursorColor = new Color(1.00f, 0.03f, 0.00f, 1.00f); @@ -18,11 +21,11 @@ internal class SCVConfig : CP_SDK.Config.JsonConfig [JsonProperty] internal Color LegendColor = new Color(0.37f, 0.10f, 0.86f, 1.00f); [JsonProperty] internal Color DashLineColor = new Color(0.37f, 0.10f, 0.86f, 0.20f); - [JsonProperty] internal Vector3 ChartStandardPosition = new Vector3( 0f, -0.4f, 2.25f); - [JsonProperty] internal Vector3 ChartStandardRotation = new Vector3(35f, 0.0f, 0.00f); + [JsonProperty] internal Vector3 ChartStandardPosition = new Vector3( 0.0f, -0.4f, 2.25f); + [JsonProperty] internal Vector3 ChartStandardRotation = new Vector3( 35.0f, 0.0f, 0.00f); - [JsonProperty] internal Vector3 Chart360_90Position = new Vector3( 0f, 3.50f, 3.00f); - [JsonProperty] internal Vector3 Chart360_90Rotation = new Vector3(-30f, 0.00f, 0.00f); + [JsonProperty] internal Vector3 ChartRotatingPosition = new Vector3( 0.0f, 3.50f, 3.00f); + [JsonProperty] internal Vector3 ChartRotatingRotation = new Vector3(-30.0f, 0.00f, 0.00f); //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -57,8 +60,8 @@ internal void ResetPosition() ChartStandardPosition = new Vector3(0f, -0.4f, 2.25f); ChartStandardRotation = new Vector3(35f, 0.0f, 0.00f); - Chart360_90Position = new Vector3(0f, 3.50f, 3.00f); - Chart360_90Rotation = new Vector3(-30f, 0.00f, 0.00f); + ChartRotatingPosition = new Vector3(0f, 3.50f, 3.00f); + ChartRotatingRotation = new Vector3(-30f, 0.00f, 0.00f); } } } diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/SongChartVisualizer.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/SongChartVisualizer.cs new file mode 100644 index 0000000..df46f0d --- /dev/null +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/SongChartVisualizer.cs @@ -0,0 +1,324 @@ +using System.Collections; +using System.Linq; +using UnityEngine; + +namespace ChatPlexMod_SongChartVisualizer +{ + /// + /// SongChartVisualizer Module + /// + public class SongChartVisualizer : CP_SDK.ModuleBase + { + public const int MaxPoints = 100; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; + public override string Name => "Song Chart Visualizer"; + public override string Description => "Get spoiled about the map difficulty!"; + public override string DocumentationURL => "https://github.com/hardcpp/BeatSaberPlus/wiki#song-chart-visualizer"; + public override bool UseChatFeatures => false; + public override bool IsEnabled { get => SCVConfig.Instance.Enabled; set { SCVConfig.Instance.Enabled = value; SCVConfig.Instance.Save(); } } + public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private UI.SettingsLeftView m_SettingsLeftView = null; + private UI.SettingsMainView m_SettingsMainView = null; + private UI.SettingsRightView m_SettingsRightView = null; + + private Transform m_RootTransform = null; + private CP_SDK.UI.Components.CFloatingPanel m_ChartFloatingPanel = null; + private UI.ChartFloatingPanelView m_ChartFloatingPanelView = null; + +#if BEATSABER + private AudioTimeSyncController m_AudioTimeSyncController = null; +#else +#error Missing game implementation +#endif + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Enable the Module + /// + protected override void OnEnable() + { + /// Bind event + CP_SDK.ChatPlexSDK.OnGenericSceneChange += ChatPlexSDK_OnGenericSceneChange; +#if BEATSABER + BeatSaberPlus.SDK.Game.Logic.OnLevelStarted += Game_LevelStarted; +#else +#error Missing game implementation +#endif + + try + { + /// Master GameObject + m_RootTransform = new GameObject("ChatPlexMod_SongChartVisualizer").transform; + m_RootTransform.transform.position = Vector3.zero; + m_RootTransform.transform.rotation = Quaternion.identity; + + GameObject.DontDestroyOnLoad(m_RootTransform); + + m_ChartFloatingPanel = CP_SDK.UI.UISystem.FloatingPanelFactory.Create("ChartFloatingPanel", m_RootTransform); + m_ChartFloatingPanel.SetSize(new Vector2(105.0f, 65.0f)); + m_ChartFloatingPanel.SetBackground(true); + m_ChartFloatingPanel.SetBackgroundColor(SCVConfig.Instance.BackgroundColor); + m_ChartFloatingPanel.OnSceneRelease(CP_SDK.ChatPlexSDK.EGenericScene.Playing, ChartFloatingPanel_OnRelease); + m_ChartFloatingPanel.SetRadius(0f); + + m_ChartFloatingPanelView = CP_SDK.UI.UISystem.CreateViewController(); + m_ChartFloatingPanel.SetViewController(m_ChartFloatingPanelView); + + m_RootTransform.gameObject.SetActive(false); + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[ChatPlexMod_SongChartVisualizer][SongChartVisualizer.OnEnable] Failed to destroy floating panel"); + Logger.Instance.Error(l_Exception); + } + } + /// + /// Disable the Module + /// + protected override void OnDisable() + { + try + { + if (m_RootTransform == null) + return; + + CP_SDK.UI.UISystem.DestroyUI(ref m_ChartFloatingPanel, ref m_ChartFloatingPanelView); + + GameObject.Destroy(m_RootTransform.gameObject); + + /// Reset variables + m_RootTransform = null; + } + catch (System.Exception l_Exception) + { + Logger.Instance.Error("[ChatPlexMod_SongChartVisualizer][SongChartVisualizer.OnDisable] Failed to destroy floating panel"); + Logger.Instance.Error(l_Exception); + } + + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsLeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsMainView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsRightView); + + /// Unbind event + CP_SDK.ChatPlexSDK.OnGenericSceneChange -= ChatPlexSDK_OnGenericSceneChange; +#if BEATSABER + BeatSaberPlus.SDK.Game.Logic.OnLevelStarted -= Game_LevelStarted; +#else +#error Missing game implementation +#endif + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Get Module settings UI + /// + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() + { + if (m_SettingsLeftView == null) m_SettingsLeftView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsRightView == null) m_SettingsRightView = CP_SDK.UI.UISystem.CreateViewController(); + + /// Change main view + return (m_SettingsMainView, m_SettingsLeftView, m_SettingsRightView); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On generic scene change + /// + /// + private void ChatPlexSDK_OnGenericSceneChange(CP_SDK.ChatPlexSDK.EGenericScene p_GenericScene) + { + if (p_GenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Menu) + return; + + m_RootTransform.gameObject.SetActive(false); + } +#if BEATSABER + /// + /// When a level start + /// + /// Level data + private void Game_LevelStarted(BeatSaberPlus.SDK.Game.LevelData p_Data) + { + /// Not enabled in multi-player + if (p_Data.Type == BeatSaberPlus.SDK.Game.LevelType.Multiplayer) + return; + + /// Start the task + CP_SDK.Unity.MTCoroutineStarter.Start(PrepareFloatingPanel()); + } +#else +#error Missing game implementation +#endif + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set preview enabled + /// + /// Is enabled? + internal void SetPreview(bool p_Enabled) + { + m_RootTransform.gameObject.SetActive(p_Enabled); + + if (p_Enabled) + { + m_ChartFloatingPanel.SetTransformDirect(new Vector3(3.38f, 1.20f, 2.29f), new Vector3(0.00f, 58.00f, 0.00f)); + m_ChartFloatingPanel.SetLockIcon(CP_SDK.UI.Components.CFloatingPanel.ECorner.None); + m_ChartFloatingPanelView.SetGraph(Data.GraphBuilder.GetSampleNPSGraph()); + } + } + /// + /// Update style + /// + internal void UpdateStyle() + { + m_ChartFloatingPanel?.SetBackgroundColor(SCVConfig.Instance.BackgroundColor); + m_ChartFloatingPanelView?.UpdateStyle(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Toggle chat visibility + /// + public void ToggleVisibility() + { + if (m_RootTransform && m_RootTransform.localScale.x > 0.5f) + m_RootTransform.localScale = Vector3.zero; + else if (m_RootTransform) + m_RootTransform.localScale = Vector3.one; + } + /// + /// Set visible + /// + /// Is visible + public void SetVisible(bool p_Visible) + { + if (!m_RootTransform) + return; + + m_RootTransform.localScale = p_Visible ? Vector3.one : Vector3.zero; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create the chart visualizer + /// + /// + private IEnumerator PrepareFloatingPanel() + { + yield return new WaitForEndOfFrame(); + +#if BEATSABER + if (BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.playerSpecificSettings?.noTextsAndHuds ?? false) + { + m_RootTransform.gameObject.SetActive(false); + yield break; + } + + var l_TransformedBeatmapData = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.transformedBeatmapData; + var l_DifficultyBeatmap = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.difficultyBeatmap; + var l_SongDuration = l_DifficultyBeatmap?.level?.beatmapLevelData?.audioClip?.length ?? -1f; + + if (l_TransformedBeatmapData == null + || l_DifficultyBeatmap == null + || l_SongDuration == -1f) + { + yield break; + } + + var l_HasRotation = BeatSaberPlus.SDK.Game.Logic.LevelData?.HasRotations ?? false; +#else +#error Missing game implementation +#endif + var l_Position = l_HasRotation ? SCVConfig.Instance.ChartRotatingPosition : SCVConfig.Instance.ChartStandardPosition; + var l_Rotation = l_HasRotation ? SCVConfig.Instance.ChartRotatingRotation : SCVConfig.Instance.ChartStandardRotation; + + m_ChartFloatingPanel.SetTransformDirect(l_Position, l_Rotation); + m_RootTransform.gameObject.SetActive(true); + + m_ChartFloatingPanelView.SetGraph(Data.GraphBuilder.BuildNPSGraph(l_TransformedBeatmapData, l_SongDuration)); + m_ChartFloatingPanel.SetLockIcon(SCVConfig.Instance.ShowLockIcon ? CP_SDK.UI.Components.CFloatingPanel.ECorner.TopRight : CP_SDK.UI.Components.CFloatingPanel.ECorner.None); + +#if BEATSABER + var l_Waiter = new WaitForSeconds(0.25f); + + m_AudioTimeSyncController = null; + while (m_AudioTimeSyncController == null) + { + m_AudioTimeSyncController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (m_AudioTimeSyncController != null) + break; + + yield return l_Waiter; + } + + m_ChartFloatingPanelView.SetGetSongTimeFunction(() => m_AudioTimeSyncController?.songTime ?? 0f); + + var l_RotationFollow = null as Transform; + if (l_HasRotation) + { + while (l_RotationFollow == null) + { + var l_FlyingGameHUDRotation = Resources.FindObjectsOfTypeAll().FirstOrDefault(); + if (l_FlyingGameHUDRotation != null) + { + l_RotationFollow = l_FlyingGameHUDRotation.transform; + break; + } + + yield return l_Waiter; + } + } +#else +#error Missing game implementation +#endif + + m_ChartFloatingPanelView.SetRotationFollow(m_RootTransform, l_RotationFollow); + } + /// + /// When the floating panel is moved + /// + /// New local position + /// New local euler angles + private void ChartFloatingPanel_OnRelease(Vector3 p_LocalPosition, Vector3 p_LocalEulerAngles) + { +#if BEATSABER + var l_HasRotation = BeatSaberPlus.SDK.Game.Logic.LevelData?.HasRotations ?? false; +#else +#error Missing game implementation +#endif + + if (!l_HasRotation) + { + SCVConfig.Instance.ChartStandardPosition = p_LocalPosition; + SCVConfig.Instance.ChartStandardRotation = p_LocalEulerAngles; + } + else + { + SCVConfig.Instance.ChartRotatingPosition = p_LocalPosition; + SCVConfig.Instance.ChartRotatingRotation = p_LocalEulerAngles; + } + } + } +} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/ChartFloatingPanelView.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/ChartFloatingPanelView.cs new file mode 100644 index 0000000..c72f067 --- /dev/null +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/ChartFloatingPanelView.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace ChatPlexMod_SongChartVisualizer.UI +{ + /// + /// Floating panel view + /// + internal sealed class ChartFloatingPanelView : CP_SDK.UI.ViewController + { + private const float GraphAreaWidth = 970f; + private const float GraphAreaHeight = 500f; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private RectTransform m_GraphCanvas = null; + private Image m_Point = null; + private RectTransform m_PointRTransform = null; + private CP_SDK.Pool.ObjectPool m_LinePool = null; + private List m_VLegendTexts = new List(); + private List m_VLegendLines = new List(); + private List m_Lines = new List(SongChartVisualizer.MaxPoints); + private List m_Points = new List(SongChartVisualizer.MaxPoints); + private Data.Graph m_Graph = null; + private Transform m_RotationTarget = null; + private Transform m_RotationFollow = null; + private Func m_GetSongTime = null; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + var l_GraphFrame = new GameObject("GraphFrame", typeof(RectTransform)).GetComponent(); + l_GraphFrame.gameObject.layer = CP_SDK.UI.UISystem.UILayer; + l_GraphFrame.SetParent(transform, false); + l_GraphFrame.anchorMin = new Vector2(0.5f, 0.5f); + l_GraphFrame.anchorMax = new Vector2(0.5f, 0.5f); + l_GraphFrame.anchoredPosition = new Vector2(0.0f, 0.0f); + l_GraphFrame.sizeDelta = new Vector2(GraphAreaWidth + 78.0f, GraphAreaHeight + 59.0f); + l_GraphFrame.pivot = new Vector2(0.5f, 0.5f); + + m_GraphCanvas = new GameObject("GraphCanvas", typeof(RectTransform)).GetComponent(); + m_GraphCanvas.gameObject.layer = CP_SDK.UI.UISystem.UILayer; + m_GraphCanvas.transform.SetParent(l_GraphFrame.transform, false); + m_GraphCanvas.anchorMin = new Vector2( 0.0f, 0.0f); + m_GraphCanvas.anchorMax = new Vector2( 0.0f, 0.0f); + m_GraphCanvas.anchoredPosition = new Vector2(524.0f, 280.0f); + m_GraphCanvas.sizeDelta = new Vector2(GraphAreaWidth, GraphAreaHeight); + m_GraphCanvas.pivot = new Vector2( 0.5f, 0.5f); + m_GraphCanvas.localPosition = new Vector3( 2.0f, 0.0f, 0.0f); + m_GraphCanvas.localScale = new Vector3( 0.1f, 0.1f, 0.1f); + + var l_Font = CP_SDK.UI.UISystem.Override_GetUIFont(); + var l_LabelCount = 10; + for (var l_I = 0; l_I < l_LabelCount; ++l_I) + { + m_VLegendTexts.Add(CreateVLegendText(l_I * 1f / l_LabelCount, l_Font)); + m_VLegendLines.Add(CreateVLegendLine(l_I * 1f / l_LabelCount)); + } + + m_LinePool = new CP_SDK.Pool.ObjectPool( + createFunc: () => + { + var l_Line = new GameObject("", typeof(RectTransform), typeof(Image)); + l_Line.transform.SetParent(m_GraphCanvas, false); + l_Line.SetActive(false); + + var l_RectTransform = l_Line.GetComponent(); + l_RectTransform.anchorMin = Vector2.zero; + l_RectTransform.anchorMax = Vector2.zero; + + var l_Image = l_Line.GetComponent(); + l_Image.raycastTarget = false; + l_Image.maskable = false; + l_Image.color = SCVConfig.Instance.LineColor; + + return l_Image; + }, + actionOnGet: (x) => + { + x.color = SCVConfig.Instance.LineColor; + x.gameObject.SetActive(true); + }, + actionOnRelease: (x) => + { + x.gameObject.SetActive(false); + }, + actionOnDestroy: (x) => + { + GameObject.Destroy(x.gameObject); + }, + collectionCheck: false, + defaultCapacity: SongChartVisualizer.MaxPoints, + maxSize: SongChartVisualizer.MaxPoints + ); + + m_Point = new GameObject("Point").AddComponent(); + m_Point.gameObject.layer = CP_SDK.UI.UISystem.UILayer; + m_Point.transform.SetParent(m_GraphCanvas, false); + m_Point.raycastTarget = false; + m_Point.maskable = false; + m_Point.useSpriteMesh = true; + m_Point.color = SCVConfig.Instance.CursorColor; + + m_PointRTransform = m_Point.rectTransform; + m_PointRTransform.sizeDelta = new Vector2(10.0f, 10.0f); + m_PointRTransform.anchorMin = new Vector2( 0.0f, 0.0f); + m_PointRTransform.anchorMax = new Vector2( 0.0f, 0.0f); + m_PointRTransform.anchoredPosition = new Vector2( 0.0f, 0.0f); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Set graph + /// + /// New graph + internal void SetGraph(Data.Graph p_Graph) + { + var l_MinValue = p_Graph.MinY; + var l_MaxValue = p_Graph.MaxY + 2; + + var l_YDelta = l_MaxValue - l_MinValue; + if (l_YDelta <= 0) + l_YDelta = 2.5f; + + l_MinValue = Mathf.Max(0f, l_MinValue - (l_YDelta * 0.2f)); + l_MaxValue = l_MaxValue + (l_YDelta * 0.1f); + + for (var l_I = 0; l_I < m_Lines.Count; ++l_I) + m_LinePool.Release(m_Lines[l_I]); + m_Lines.Clear(); + + if (p_Graph.Points != null) + { + m_Points.Clear(); + + var l_LastPoint = default(Vector2); + var l_PointCount = p_Graph.Points.Length; + for (var l_I = 0; l_I < l_PointCount; ++l_I) + { + var l_PointX = (float)l_I * (GraphAreaWidth / l_PointCount); + var l_PointY = ((p_Graph.Points[l_I].Y - l_MinValue) / (l_MaxValue - l_MinValue)) * GraphAreaHeight; + var l_CurrentPoint = new Vector2(l_PointX, l_PointY); + var l_Line = m_LinePool.Get(); + var l_Direction = (l_CurrentPoint - l_LastPoint).normalized; + var l_Distance = Vector2.Distance(l_LastPoint, l_CurrentPoint); + + l_Line.rectTransform.sizeDelta = new Vector2(l_Distance, 2f); + l_Line.rectTransform.anchoredPosition = l_LastPoint + l_Direction * l_Distance * 0.5f; + l_Line.rectTransform.localEulerAngles = new Vector3(0, 0, Mathf.Atan2(l_Direction.y, l_Direction.x) * Mathf.Rad2Deg); + + m_Lines.Add(l_Line); + m_Points.Add(l_CurrentPoint); + + l_LastPoint = l_CurrentPoint; + } + + if (l_LastPoint != default) + { + var l_Line = m_LinePool.Get(); + var l_Direction = (new Vector2(GraphAreaWidth, 0.0f) - l_LastPoint).normalized; + var l_Distance = Vector2.Distance(l_LastPoint, new Vector2(GraphAreaWidth, 0.0f)); + + l_Line.rectTransform.sizeDelta = new Vector2(l_Distance, 2f); + l_Line.rectTransform.anchoredPosition = l_LastPoint + l_Direction * l_Distance * 0.5f; + l_Line.rectTransform.localEulerAngles = new Vector3(0, 0, Mathf.Atan2(l_Direction.y, l_Direction.x) * Mathf.Rad2Deg); + + m_Lines.Add(l_Line); + m_Points.Add(new Vector2(GraphAreaWidth, 0.0f)); + } + } + + var l_VLegendTextsCount = m_VLegendTexts.Count; + for (var l_I = 0; l_I < l_VLegendTextsCount; ++l_I) + { + var l_NormalizedValue = (l_I * 1f / l_VLegendTextsCount); + m_VLegendTexts[l_I].text = System.Math.Round(l_MinValue + (l_NormalizedValue * (l_MaxValue - l_MinValue))).ToString(); + m_VLegendTexts[l_I].enabled = System.Math.Round(l_MinValue + (l_NormalizedValue * (l_MaxValue - l_MinValue)), 2) >= 0f; + } + + m_Graph = p_Graph; + } + /// + /// Set rotation follow settings + /// + /// Target of rotation + /// Rotation to follow + internal void SetRotationFollow(Transform p_Target, Transform p_Follow) + { + m_RotationTarget = p_Target; + m_RotationFollow = p_Follow; + } + /// + /// Set get song time function + /// + /// New function + internal void SetGetSongTimeFunction(Func p_GetSongTimeFunction) + => m_GetSongTime = p_GetSongTimeFunction; + /// + /// Update visual style + /// + internal void UpdateStyle() + { + var l_VLegendTextsCount = m_VLegendTexts.Count; + for (var l_I = 0; l_I < l_VLegendTextsCount; ++l_I) + { + m_VLegendTexts[l_I].color = SCVConfig.Instance.LegendColor; + m_VLegendTexts[l_I].gameObject.SetActive(SCVConfig.Instance.ShowNPSLegend); + } + + var l_VLegendLineCount = m_VLegendLines.Count; + for (var l_I = 0; l_I < l_VLegendLineCount; ++l_I) + { + m_VLegendLines[l_I].color = SCVConfig.Instance.DashLineColor; + m_VLegendLines[l_I].gameObject.SetActive(SCVConfig.Instance.ShowNPSLegend); + } + + var l_LineCount = m_Lines.Count; + for (var l_I = 0; l_I < l_LineCount; ++l_I) + m_Lines[l_I].color = SCVConfig.Instance.LineColor; + + m_Point.color = SCVConfig.Instance.CursorColor; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On physic frame callback + /// + private void FixedUpdate() + { + if (m_Graph == null || m_GetSongTime == null || CP_SDK.ChatPlexSDK.ActiveGenericScene != CP_SDK.ChatPlexSDK.EGenericScene.Playing) + return; + + if (m_RotationFollow) + m_RotationTarget.localRotation = m_RotationFollow.rotation; + + var l_Progress = m_Graph.SongTimeToProgress(m_GetSongTime()); + var l_Low = m_Points[m_Graph.ProgressToIndexLow(l_Progress)]; + var l_HI = m_Points[m_Graph.ProgressToIndexHi(l_Progress)]; + var l_PointPos = new Vector2( + l_Progress * GraphAreaWidth, + Mathf.Lerp(l_Low.y, l_HI.y, m_Graph.ProgressToIndexRelativeVirtual(l_Progress)) + ); + + m_PointRTransform.anchoredPosition = l_PointPos; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Create VLegend text + /// + /// Position Y + /// Font asset + /// + private TMPro.TextMeshProUGUI CreateVLegendText(float p_PositionY, TMPro.TMP_FontAsset p_Font) + { + var l_LegendLabel = new GameObject("", typeof(RectTransform), typeof(CanvasRenderer), typeof(TMPro.TextMeshProUGUI)); + l_LegendLabel.layer = CP_SDK.UI.UISystem.UILayer; + l_LegendLabel.transform.SetParent(m_GraphCanvas, false); + + var l_RectTransform = l_LegendLabel.GetComponent(); + l_RectTransform.anchorMin = Vector2.zero; + l_RectTransform.anchorMax = Vector2.zero; + l_RectTransform.sizeDelta = new Vector2( 160.0f, 30.0f); + l_RectTransform.pivot = new Vector2( 0.5f, 0.5f); + l_RectTransform.anchoredPosition = new Vector2( -10.0f, p_PositionY * m_GraphCanvas.sizeDelta.y); + l_RectTransform.localPosition = new Vector3(-585.0f, l_RectTransform.localPosition.y, l_RectTransform.localPosition.z); + + var l_Text = l_LegendLabel.GetComponent(); + l_Text.font = p_Font; + l_Text.alignment = TMPro.TextAlignmentOptions.MidlineRight; + l_Text.color = SCVConfig.Instance.LegendColor; + l_Text.fontSize = 25; + l_Text.text = "0"; + l_Text.raycastTarget = false; + l_Text.maskable = false; + + l_LegendLabel.gameObject.SetActive(SCVConfig.Instance.ShowNPSLegend); + + return l_Text; + } + /// + /// Create VLegend line + /// + /// Position Y + /// + private Image CreateVLegendLine( float p_PositionY) + { + var l_LegendLine = new GameObject("", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); + l_LegendLine.layer = CP_SDK.UI.UISystem.UILayer; + l_LegendLine.transform.SetParent(m_GraphCanvas, false); + + var l_RectTransform = l_LegendLine.GetComponent(); + l_RectTransform.anchorMin = new Vector2( 0.5f, 0.0f); + l_RectTransform.anchorMax = new Vector2( 0.5f, 0.0f); + l_RectTransform.sizeDelta = new Vector2(m_GraphCanvas.sizeDelta.x, 3.0f); + l_RectTransform.pivot = new Vector2( 0.5f, 0.5f); + l_RectTransform.anchoredPosition = new Vector2(-4.0f, p_PositionY * m_GraphCanvas.sizeDelta.y); + + var l_Image = l_LegendLine.GetComponent(); + l_Image.sprite = CP_SDK.Unity.SpriteU.CreateFromTextureWithBorders(Texture2D.whiteTexture); + l_Image.type = Image.Type.Simple; + l_Image.color = SCVConfig.Instance.DashLineColor; + l_Image.raycastTarget = false; + l_Image.maskable = false; + + l_LegendLine.gameObject.SetActive(SCVConfig.Instance.ShowNPSLegend); + + return l_Image; + } + } +} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsLeft.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsLeftView.cs similarity index 52% rename from Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsLeft.cs rename to Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsLeftView.cs index 0ee7153..1192a23 100644 --- a/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsLeft.cs +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsLeftView.cs @@ -1,28 +1,15 @@ -using BeatSaberMarkupLanguage.Attributes; -using System.Diagnostics; +using CP_SDK.XUI; using UnityEngine; -namespace BeatSaberPlus_SongChartVisualizer.UI +namespace ChatPlexMod_SongChartVisualizer.UI { /// - /// Settings left credit view + /// Settings left view /// - internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController + internal sealed class SettingsLeftView : CP_SDK.UI.ViewController { -#pragma warning disable CS0414 - [UIObject("Background")] - private GameObject m_Background = null; - [UIValue("Line1")] - private readonly string m_Line1 = "Thanks to Shoko84 & Opzon for original idea"; - [UIValue("Line2")] - private readonly string m_Line2 = " "; - [UIValue("Line3")] - private readonly string m_Line3 = " "; - [UIValue("Line4")] - private readonly string m_Line4 = " "; - [UIValue("Line5")] - private readonly string m_Line5 = " "; -#pragma warning restore CS0414 + private static readonly string s_InformationStr = + "Thanks to Shoko84 & Opzon for the original idea"; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -32,7 +19,23 @@ internal class SettingsLeft : BeatSaberPlus.SDK.UI.ResourceViewController protected override sealed void OnViewCreation() { - BeatSaberPlus.SDK.UI.Backgroundable.SetOpacity(m_Background, 0.5f); + Templates.FullRectLayout( + Templates.TitleBar("Information / Credits"), + + Templates.ScrollableInfos(50, + XUIText.Make(s_InformationStr).SetAlign(TMPro.TextAlignmentOptions.Left) + ), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("Reset", OnResetButton), + XUIPrimaryButton.Make("Reset Position", OnResetPositionButton) + ), + Templates.ExpandedButtonsLine( + XUISecondaryButton.Make("Documentation", OnDocumentationButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); } //////////////////////////////////////////////////////////////////////////// @@ -41,34 +44,38 @@ protected override sealed void OnViewCreation() /// /// Reset button /// - [UIAction("click-reset-btn-pressed")] private void OnResetButton() { - ShowConfirmationModal("Do you really want to reset\nall SongChartVisualizer settings?", () => + ShowConfirmationModal("Do you really want to reset\nall SongChartVisualizer settings?", (p_Confirm) => { + if (!p_Confirm) + return; + /// Reset settings SCVConfig.Instance.Reset(); SCVConfig.Instance.Enabled = true; SCVConfig.Instance.Save(); /// Update main view - Settings.Instance.OnResetButton(); + SettingsMainView.Instance.OnResetButton(); }); } /// /// Reset position button /// - [UIAction("click-reset-position-btn-pressed")] private void OnResetPositionButton() { - ShowConfirmationModal("Do you really want to reset\nSongChartVisualizer position?", () => + ShowConfirmationModal("Do you really want to reset\nSongChartVisualizer position?", (p_Confirm) => { + if (!p_Confirm) + return; + /// Reset settings SCVConfig.Instance.ResetPosition(); SCVConfig.Instance.Save(); /// Update main view - Settings.Instance.OnResetButton(); + SettingsMainView.Instance.OnResetButton(); }); } @@ -78,11 +85,10 @@ private void OnResetPositionButton() /// /// Documentation button /// - [UIAction("click-documentation-btn-pressed")] private void OnDocumentationButton() { - ShowMessageModal("URL opened in your desktop browser."); - Process.Start("https://github.com/hardcpp/BeatSaberPlus/wiki#song-chart-visualizer"); + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL(SongChartVisualizer.Instance.DocumentationURL); } } } diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsMainView.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsMainView.cs new file mode 100644 index 0000000..76c132a --- /dev/null +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsMainView.cs @@ -0,0 +1,178 @@ +using CP_SDK.XUI; + +namespace ChatPlexMod_SongChartVisualizer.UI +{ + /// + /// Settings main view + /// + internal sealed class SettingsMainView : CP_SDK.UI.ViewController + { + private XUIToggle m_AlignWithFloor; + private XUIToggle m_ShowLockIcon; + private XUIToggle m_ShowNPSLegend; + private XUIToggle m_FollowEnvironementRotations; + + private XUIColorInput m_BackgroundColor; + private XUIColorInput m_CursorColor; + private XUIColorInput m_LineColor; + private XUIColorInput m_LegendColor; + private XUIColorInput m_DashColor; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private bool m_PreventChanges = false; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + var l_Config = SCVConfig.Instance; + + Templates.FullRectLayoutMainView( + Templates.TitleBar("Song Chart Visualizer | Settings"), + + XUIHLayout.Make( + XUIVLayout.Make( + XUIText.Make("Align with floor on move"), + XUIToggle.Make().SetValue(l_Config.AlignWithFloor).Bind(ref m_AlignWithFloor), + + XUIText.Make("Show lock icon for movement"), + XUIToggle.Make().SetValue(l_Config.ShowLockIcon).Bind(ref m_ShowLockIcon) + ) + .SetSpacing(1) + .SetWidth(40.0f) + .OnReady(x => x.HOrVLayoutGroup.childAlignment = UnityEngine.TextAnchor.UpperCenter) + .ForEachDirect(x => x.SetAlign(TMPro.TextAlignmentOptions.Center)) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())), + + XUIVLayout.Make( + XUIText.Make("Show NPS legend"), + XUIToggle.Make().SetValue(l_Config.ShowNPSLegend).Bind(ref m_ShowNPSLegend), + + XUIText.Make("Follow environment rotations"), + XUIToggle.Make().SetValue(l_Config.FollowEnvironementRotation).Bind(ref m_FollowEnvironementRotations) + ) + .SetSpacing(1) + .SetWidth(40.0f) + .OnReady(x => x.HOrVLayoutGroup.childAlignment = UnityEngine.TextAnchor.UpperCenter) + .ForEachDirect(x => x.SetAlign(TMPro.TextAlignmentOptions.Center)) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())), + + XUIVLayout.Make( + XUIText.Make("Background color"), + XUIColorInput.Make() + .SetAlphaSupport(true).SetValue(l_Config.BackgroundColor) + .Bind(ref m_BackgroundColor), + + XUIText.Make("Position indicator color"), + XUIColorInput.Make() + .SetAlphaSupport(true).SetValue(l_Config.CursorColor) + .Bind(ref m_CursorColor), + + XUIText.Make("Graph line color"), + XUIColorInput.Make() + .SetAlphaSupport(true).SetValue(l_Config.LineColor) + .Bind(ref m_LineColor), + + XUIText.Make("Legend text color"), + XUIColorInput.Make() + .SetAlphaSupport(true).SetValue(l_Config.LegendColor) + .Bind(ref m_LegendColor), + + XUIText.Make("Dash line color"), + XUIColorInput.Make() + .SetAlphaSupport(true).SetValue(l_Config.DashLineColor) + .Bind(ref m_DashColor) + ) + .SetSpacing(1) + .SetWidth(40.0f) + .ForEachDirect(x => x.SetAlign(TMPro.TextAlignmentOptions.Center)) + .ForEachDirect(x => x.OnValueChanged((_) => OnSettingChanged())) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + /// + /// On view activation + /// + protected override sealed void OnViewActivation() + { + SongChartVisualizer.Instance.SetPreview(true); + } + /// + /// On view deactivation + /// + protected override sealed void OnViewDeactivation() + { + SongChartVisualizer.Instance.SetPreview(false); + SCVConfig.Instance.Save(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// When settings are changed + /// + private void OnSettingChanged() + { + if (m_PreventChanges) + return; + + var l_Config = SCVConfig.Instance; + + l_Config.AlignWithFloor = m_AlignWithFloor.Element.GetValue(); + l_Config.ShowLockIcon = m_ShowLockIcon.Element.GetValue(); + l_Config.ShowNPSLegend = m_ShowNPSLegend.Element.GetValue(); + l_Config.FollowEnvironementRotation = m_FollowEnvironementRotations.Element.GetValue(); + + SCVConfig.Instance.CursorColor = m_BackgroundColor.Element.GetValue(); + SCVConfig.Instance.CursorColor = m_CursorColor.Element.GetValue(); + SCVConfig.Instance.LineColor = m_LineColor.Element.GetValue(); + SCVConfig.Instance.LegendColor = m_LegendColor.Element.GetValue(); + SCVConfig.Instance.DashLineColor = m_DashColor.Element.GetValue(); + + m_LegendColor.SetInteractable(l_Config.ShowNPSLegend); + m_DashColor.SetInteractable(l_Config.ShowNPSLegend); + + SCVConfig.Instance.Save(); + + /// Refresh preview + SongChartVisualizer.Instance.UpdateStyle(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Reset settings + /// + internal void OnResetButton() + { + var l_Config = SCVConfig.Instance; + + m_PreventChanges = true; + + m_AlignWithFloor .SetValue(l_Config.AlignWithFloor); + m_ShowLockIcon .SetValue(l_Config.ShowLockIcon); + m_ShowNPSLegend .SetValue(l_Config.ShowNPSLegend); + m_FollowEnvironementRotations .SetValue(l_Config.FollowEnvironementRotation); + + m_BackgroundColor .SetValue(SCVConfig.Instance.CursorColor); + m_CursorColor .SetValue(SCVConfig.Instance.CursorColor); + m_LineColor .SetValue(SCVConfig.Instance.LineColor); + m_LegendColor .SetValue(SCVConfig.Instance.LegendColor); + m_DashColor .SetValue(SCVConfig.Instance.DashLineColor); + + m_PreventChanges = false; + + SongChartVisualizer.Instance.UpdateStyle(); + } + } +} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsRightView.cs b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsRightView.cs new file mode 100644 index 0000000..d5b2f42 --- /dev/null +++ b/Modules/BeatSaberPlus_SongChartVisualizer/ChatPlexMod_SongChartVisualizer/UI/SettingsRightView.cs @@ -0,0 +1,24 @@ +using CP_SDK.XUI; + +namespace ChatPlexMod_SongChartVisualizer.UI +{ + /// + /// Settings right view + /// + internal sealed class SettingsRightView : CP_SDK.UI.ViewController + { + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Preview"), + + XUIVSpacer.Make(65f) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + } +} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/Components/SongChart.cs b/Modules/BeatSaberPlus_SongChartVisualizer/Components/SongChart.cs deleted file mode 100644 index cb4e625..0000000 --- a/Modules/BeatSaberPlus_SongChartVisualizer/Components/SongChart.cs +++ /dev/null @@ -1,519 +0,0 @@ -using CP_SDK.Unity.Extensions; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; -using UnityEngine.UI; - -namespace BeatSaberPlus_SongChartVisualizer.Components -{ - /// - /// Chart creator component - /// - internal class SongChart : MonoBehaviour - { - /// - /// Audio time sync controller - /// - private AudioTimeSyncController m_AudioTimeSyncController; - /// - /// FlyingGameHUDRotation instance - /// - private GameObject m_FlyingGameHUDRotation = null; - /// - /// NPS sections - /// - private (float, float, float)[] m_GraphData = new (float, float, float)[] { }; - /// - /// Current section index - /// - private int m_CurrentGraphDataIndex; - /// - /// Graph canvas - /// - private RectTransform m_GraphCanvas = null; - /// - /// Graph points - /// - private List m_Points = new List(); - /// - /// Pointer cursor - /// - private GameObject m_Pointer = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On component first frame - /// - private void Start() - { - m_AudioTimeSyncController = Resources.FindObjectsOfTypeAll().FirstOrDefault(); - - bool l_NoUI = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.playerSpecificSettings?.noTextsAndHuds ?? false; - - if (l_NoUI) - { - SongChartVisualizer.Instance.DestroyChart(); - return; - } - - var l_TransformedBeatmapData = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.transformedBeatmapData; - var l_PreviewBeatmapLevel = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.previewBeatmapLevel; - var l_DifficultyBeatmap = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.difficultyBeatmap; - - if ((l_TransformedBeatmapData != null && l_PreviewBeatmapLevel != null && l_DifficultyBeatmap != null) || BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - { - var l_SongDuration = -1f; - - /// Demo data - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - l_SongDuration = 201f; - else - l_SongDuration = l_DifficultyBeatmap?.level?.beatmapLevelData?.audioClip?.length ?? -1f; - - if (l_SongDuration >= 0) - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - FillGraphDataWithDemoValues(); - else if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - { - m_FlyingGameHUDRotation = l_TransformedBeatmapData.spawnRotationEventsCount > 0 ? Resources.FindObjectsOfTypeAll().FirstOrDefault()?.gameObject : null; - - var l_Notes = l_TransformedBeatmapData.allBeatmapDataItems - .Where(x => x.type == BeatmapDataItem.BeatmapDataItemType.BeatmapObject && x is NoteData) - .Select(x => x as NoteData) - .Where(x => x.colorType != ColorType.None) - .OrderBy(x => x.time) - .ToList(); - var l_GraphData = new List<(float, float, float)>(); - - if (true) - { - if (l_Notes.Count > 0) - { - List l_NPSS = new List(); - for (int l_I = 0; l_I < (l_SongDuration + 1f); ++l_I) - { - var l_NoteCount = l_Notes.Count(x => x.time >= l_I && x.time < (l_I + 1f)); - l_NPSS.Add(l_NoteCount == 0 ? 0f : l_NoteCount); - } - - float l_PrevNPS = 0f; - float l_Threshold = 0.1f; - float l_SectionStart = 0f; - for (int l_I = 0; l_I < l_NPSS.Count; ++l_I) - { - float l_CurrentNPS = l_NPSS[l_I]; - - if (l_I == (l_NPSS.Count - 1) - || (l_GraphData.Count == 0 && l_CurrentNPS != 0f) - || (((float)l_I) - l_SectionStart > 1f && Mathf.Abs(l_CurrentNPS - l_PrevNPS) > (l_PrevNPS * l_Threshold))) - { - var l_NoteCount = l_Notes.Count(x => x.time >= l_SectionStart && x.time < (float)l_I); - var l_SectionNPS = l_NoteCount == 0 ? 0f : l_NoteCount / (((float)l_I) - l_SectionStart); - l_PrevNPS = l_SectionNPS; - - l_GraphData.Add((l_SectionNPS, l_SectionStart, (float)l_I)); - l_SectionStart = (float)l_I; - } - } - } - } - else - { - if (l_Notes.Count > 0) - { - var maximum_tolerance = .06 + 1e-9;// # Magic number based on maximum tolerated swing speed - var maximum_window_tolerance = .07 + 1e-9; //# For windowed sliders - - NoteData l_LastRed = null; - NoteData l_LastBlue = null; - - Dictionary l_SPSR = new Dictionary(); - Dictionary l_SPSB = new Dictionary(); - for (int l_I = 0; l_I < (l_SongDuration / l_PreviewBeatmapLevel.beatsPerMinute * 60f) + 1; ++l_I) - { - l_SPSR.Add(l_I, 0); - l_SPSB.Add(l_I, 0); - } - - foreach (var l_Note in l_Notes) - { - var real_time = l_Note.time / l_PreviewBeatmapLevel.beatsPerMinute * 60f; - - if (l_Note.colorType == ColorType.ColorA) - { - if (l_LastRed != null) - { - bool l_IsWindow = Mathf.Max(Mathf.Abs(l_Note.lineIndex - l_LastRed.lineIndex), Mathf.Abs(l_Note.noteLineLayer - l_LastRed.noteLineLayer)) >= 2; - if (l_IsWindow && (((l_Note.time - l_LastRed.time) / l_PreviewBeatmapLevel.beatsPerMinute * 60f) > maximum_window_tolerance) - || (((l_Note.time - l_LastRed.time) / l_PreviewBeatmapLevel.beatsPerMinute * 60f) > maximum_tolerance) - ) - l_SPSR[(int)Mathf.Floor(real_time)]++; - } - else - l_SPSR[(int)Mathf.Floor(real_time)]++; - - l_LastRed = l_Note; - } - else if (l_Note.colorType == ColorType.ColorB) - { - - if (l_LastBlue != null) - { - bool l_IsWindow = Mathf.Max(Mathf.Abs(l_Note.lineIndex - l_LastBlue.lineIndex), Mathf.Abs(l_Note.noteLineLayer - l_LastBlue.noteLineLayer)) >= 2; - if (l_IsWindow && (((l_Note.time - l_LastBlue.time) / l_PreviewBeatmapLevel.beatsPerMinute * 60f) > maximum_window_tolerance) - || (((l_Note.time - l_LastBlue.time) / l_PreviewBeatmapLevel.beatsPerMinute * 60f) > maximum_tolerance) - ) - l_SPSB[(int)Mathf.Floor(real_time)]++; - } - else - l_SPSB[(int)Mathf.Floor(real_time)]++; - - l_LastBlue = l_Note; - } - } - - var swing_count_list = new Dictionary(l_SPSR); - foreach (var l_KVP in l_SPSB) - { - if (!swing_count_list.ContainsKey(l_KVP.Key)) - swing_count_list.Add(l_KVP.Key, l_KVP.Value); - else - swing_count_list[l_KVP.Key] += l_KVP.Value; - } - - for (int l_I = 0; l_I < swing_count_list.Count; ++l_I) - { - float l_StartTime = (l_I / 60f) * l_PreviewBeatmapLevel.beatsPerMinute; - float l_SectionEndTime = (((l_I == swing_count_list.Count - 1) ? (l_SongDuration / l_PreviewBeatmapLevel.beatsPerMinute * 60f) : l_I + 1) / 60f) * l_PreviewBeatmapLevel.beatsPerMinute; - l_GraphData.Add((swing_count_list[l_I], l_StartTime, l_SectionEndTime)); - } - - - // float l_PrevSPS = 0f; - // float l_Threshold = 0.1f; - // float l_SectionStart = 0f; - // for (int l_I = 0; l_I < swing_count_list.Max(x => x.Key); ++l_I) - // { - // float l_CurrentSPS = ((float)swing_count_list[l_I]) / ((float)l_I - l_SectionStart); - // - // if (l_I == (swing_count_list.Count - 1) - // || (l_GraphData.Count == 0 && l_CurrentSPS != 0f) - // || (((float)l_I) - l_SectionStart > 1f && Mathf.Abs(l_CurrentSPS - l_PrevSPS) > (l_PrevSPS * l_Threshold))) - // { - // var l_SectionSPS = swing_count_list.Where(x => x.Key >= l_SectionStart && x.Key < (l_I + 1)).Sum(x => x.Value) / (l_I - l_SectionStart); - // l_PrevSPS = l_SectionSPS; - // - // l_GraphData.Add((l_SectionSPS, (l_SectionStart / 60f) * l_DifficultyBeatmap.level.beatsPerMinute, (((float)l_I) / 60f) * l_DifficultyBeatmap.level.beatsPerMinute)); - // l_SectionStart = (float)l_I; - // } - // } - - swing_count_list.ToList().ForEach(x => Logger.Instance.Debug(string.Format("{0} - {1}", x.Key, x.Value))); - } - - - - } - - m_GraphData = l_GraphData.ToArray(); - } - - ///for (var l_I = 0; l_I < m_GraphData.Length; l_I++) - ///{ - /// var l_NPSInfos = m_GraphData[l_I]; - /// Logger.Instance.Debug($"l_GraphData.Add(({l_NPSInfos.Item1.ToString().Replace(",", ".")}, {l_NPSInfos.Item2.ToString().Replace(",", ".")}, {l_NPSInfos.Item3.ToString().Replace(",", ".")} ));"); - ///} - - if (m_GraphData.Length > 0) - { - var l_GraphFrame = new GameObject("", typeof(RectTransform)); - l_GraphFrame.transform.SetParent(transform, false); - (l_GraphFrame.transform as RectTransform).anchorMin = Vector2.one * 0.5f; - (l_GraphFrame.transform as RectTransform).anchorMax = Vector2.one * 0.5f; - (l_GraphFrame.transform as RectTransform).anchoredPosition = Vector2.zero; - (l_GraphFrame.transform as RectTransform).sizeDelta = new Vector2(1048, 559); - (l_GraphFrame.transform as RectTransform).pivot = Vector2.one * 0.5f; - (l_GraphFrame.transform as RectTransform).offsetMin = ((l_GraphFrame.transform as RectTransform).sizeDelta / 2) * -1f; - (l_GraphFrame.transform as RectTransform).offsetMax = ((l_GraphFrame.transform as RectTransform).sizeDelta / 2) * 1f; - - m_GraphCanvas = (new GameObject("", typeof(RectTransform)).transform as RectTransform); - m_GraphCanvas.gameObject.transform.SetParent(l_GraphFrame.transform, false); - m_GraphCanvas.anchorMin = Vector2.zero; - m_GraphCanvas.anchorMax = Vector2.zero; - m_GraphCanvas.anchoredPosition = new Vector2(524, 280); - m_GraphCanvas.sizeDelta = new Vector2(970, 500); - m_GraphCanvas.pivot = Vector2.one * 0.5f; - m_GraphCanvas.offsetMin = new Vector2(39, 30); - m_GraphCanvas.offsetMax = new Vector2(1009, 530); - m_GraphCanvas.gameObject.transform.localPosition = new Vector3(2, 0, 0); - m_GraphCanvas.gameObject.transform.localScale = Vector3.one * 0.1f; - - BuildGraph(); - - m_Pointer = new GameObject(""); - m_Pointer.transform.SetParent(m_GraphCanvas, false); - - var l_Image = m_Pointer.AddComponent(); - l_Image.useSpriteMesh = true; - l_Image.color = SCVConfig.Instance.CursorColor; - - m_CurrentGraphDataIndex = BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu ? (m_GraphData.Length / 2) : 0; - - var l_RectTransform = m_Pointer.GetComponent(); - l_RectTransform.sizeDelta = Vector2.one * 10f; - (m_Pointer.transform as RectTransform).anchorMin = Vector2.zero; - (m_Pointer.transform as RectTransform).anchorMax = Vector2.zero; - (m_Pointer.transform as RectTransform).anchoredPosition = m_Points.Count > 0 ? m_Points[m_CurrentGraphDataIndex] : Vector2.zero; - - gameObject.ChangerLayerRecursive(LayerMask.NameToLayer("UI")); - } - } - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Called every frames - /// - private void Update() - { - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - return; - - if (m_FlyingGameHUDRotation != null && m_FlyingGameHUDRotation && SCVConfig.Instance.FollowEnvironementRotation) - transform.parent.rotation = m_FlyingGameHUDRotation.transform.rotation; - - if (m_GraphCanvas == null || m_Pointer == null || m_AudioTimeSyncController == null || !m_AudioTimeSyncController || m_AudioTimeSyncController .state == AudioTimeSyncController.State.Stopped) - return; - - float l_CurrentSongPosition = m_AudioTimeSyncController.songTime; - float l_PositionX = (m_GraphCanvas.sizeDelta.x / m_AudioTimeSyncController.songLength) * l_CurrentSongPosition; - float l_PositionY = 0f; - - if (m_AudioTimeSyncController != null && m_GraphData.Length > 1) - { - if (m_GraphData[m_CurrentGraphDataIndex].Item3 < l_CurrentSongPosition && m_CurrentGraphDataIndex != (m_GraphData.Length - 1)) - ++m_CurrentGraphDataIndex; - - float l_LerpFactor = (l_CurrentSongPosition - m_GraphData[m_CurrentGraphDataIndex].Item2) / (m_GraphData[m_CurrentGraphDataIndex].Item3 - m_GraphData[m_CurrentGraphDataIndex].Item2); - l_PositionY = Mathf.Lerp(m_CurrentGraphDataIndex > 0 ? m_Points[m_CurrentGraphDataIndex - 1].y : m_Points[m_CurrentGraphDataIndex].y, m_Points[m_CurrentGraphDataIndex].y, l_LerpFactor); - } - - (m_Pointer.transform as RectTransform).anchoredPosition = new Vector2(l_PositionX, l_PositionY); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Graph builder - /// - public void BuildGraph() - { - var l_MaxValue = m_GraphData.Max(x => x.Item1); - var l_MinValue = m_GraphData.Min(x => x.Item1); - - var l_YDelta = l_MaxValue - l_MinValue; - if (l_YDelta <= 0) - l_YDelta = 2.5f; - - l_MaxValue = l_MaxValue + (l_YDelta * 0.1f); - l_MinValue = Mathf.Max(0f, l_MinValue - (l_YDelta * 0.2f)); - - Vector2 l_LastPoint = default; - foreach (var l_Point in m_GraphData) - { - var l_PointX = m_GraphCanvas.sizeDelta.x * ((l_Point.Item3 - l_Point.Item2) / m_GraphData.Last().Item3) + (m_Points.Count != 0 ? m_Points.Last().x : 0); - var l_CurrentPoint = new Vector2(l_PointX, (l_Point.Item1 - l_MinValue) / (l_MaxValue - l_MinValue) * m_GraphCanvas.sizeDelta.y); - - m_Points.Add(l_CurrentPoint); - - if (l_LastPoint != null) - { - var l_Line = new GameObject("", typeof(Image)); - l_Line.transform.SetParent(m_GraphCanvas, false); - l_Line.GetComponent().color = SCVConfig.Instance.LineColor; - - var l_Direction = (l_CurrentPoint - l_LastPoint).normalized; - var l_Distance = Vector2.Distance(l_LastPoint, l_CurrentPoint); - - var l_RectTransform = l_Line.GetComponent(); - l_RectTransform.anchorMin = new Vector2(0, 0); - l_RectTransform.anchorMax = new Vector2(0, 0); - l_RectTransform.sizeDelta = new Vector2(l_Distance, 2f); - l_RectTransform.anchoredPosition = l_LastPoint + l_Direction * l_Distance * .5f; - l_RectTransform.localEulerAngles = new Vector3(0, 0, Mathf.Atan2(l_Direction.y, l_Direction.x) * Mathf.Rad2Deg); - } - - l_LastPoint = l_CurrentPoint; - } - - if (l_LastPoint != default) - { - var l_Line = new GameObject("", typeof(Image)); - l_Line.transform.SetParent(m_GraphCanvas, false); - l_Line.GetComponent().color = SCVConfig.Instance.LineColor; - - var l_Direction = (new Vector2(m_GraphCanvas.sizeDelta.x, 0) - l_LastPoint).normalized; - var l_Distance = Vector2.Distance(l_LastPoint, new Vector2(m_GraphCanvas.sizeDelta.x, 0)); - - var l_RectTransform = l_Line.GetComponent(); - l_RectTransform.anchorMin = new Vector2(0, 0); - l_RectTransform.anchorMax = new Vector2(0, 0); - l_RectTransform.sizeDelta = new Vector2(l_Distance, 2f); - l_RectTransform.anchoredPosition = l_LastPoint + l_Direction * l_Distance * .5f; - l_RectTransform.localEulerAngles = new Vector3(0, 0, Mathf.Atan2(l_Direction.y, l_Direction.x) * Mathf.Rad2Deg); - } - - if (SCVConfig.Instance.ShowNPSLegend) - { - var l_Font = Resources.FindObjectsOfTypeAll().FirstOrDefault(x => x.name == "Teko-Medium SDF"); - var l_LabelCount = 10; - for (var l_I = 0; l_I <= l_LabelCount; l_I++) - { - var l_NormalizedValue = l_I * 1f / l_LabelCount; - - var l_LegendLabel = new GameObject("", typeof(RectTransform), typeof(CanvasRenderer), typeof(TMPro.TextMeshProUGUI)); - l_LegendLabel.transform.SetParent(m_GraphCanvas.transform, false); - (l_LegendLabel.transform as RectTransform).anchorMin = Vector2.zero; - (l_LegendLabel.transform as RectTransform).anchorMax = Vector2.zero; - (l_LegendLabel.transform as RectTransform).sizeDelta = new Vector2(160, 30); - (l_LegendLabel.transform as RectTransform).pivot = Vector2.one * 0.5f; - (l_LegendLabel.transform as RectTransform).offsetMin = new Vector2(-168.2f, 30.6f); - (l_LegendLabel.transform as RectTransform).offsetMax = new Vector2(-8.2f, 60.6f); - (l_LegendLabel.transform as RectTransform).anchoredPosition = new Vector2(-88.2f, 45.6f); - (l_LegendLabel.transform as RectTransform).anchoredPosition = new Vector2(-10f, l_NormalizedValue * m_GraphCanvas.sizeDelta.y); - (l_LegendLabel.transform as RectTransform).localPosition = new Vector3(-585.00f, (l_LegendLabel.transform as RectTransform).localPosition.y, (l_LegendLabel.transform as RectTransform).localPosition.z); - l_LegendLabel.GetComponent().font = l_Font; - l_LegendLabel.GetComponent().alignment = TMPro.TextAlignmentOptions.MidlineRight; - l_LegendLabel.GetComponent().color = SCVConfig.Instance.LegendColor; - l_LegendLabel.GetComponent().text = Mathf.Round(l_MinValue + (l_NormalizedValue * (l_MaxValue - l_MinValue))).ToString(); - l_LegendLabel.GetComponent().fontSize = 25; - l_LegendLabel.GetComponent().enabled = System.Math.Round(l_MinValue + (l_NormalizedValue * (l_MaxValue - l_MinValue)), 2) >= 0f; - - var l_LegendLine = new GameObject("", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); - l_LegendLine.transform.SetParent(m_GraphCanvas, false); - (l_LegendLine.transform as RectTransform).anchorMin = new Vector2(0.5f, 0); - (l_LegendLine.transform as RectTransform).anchorMax = new Vector2(0.5f, 0); - (l_LegendLine.transform as RectTransform).sizeDelta = new Vector2(970.0f, 3.0f); - (l_LegendLine.transform as RectTransform).pivot = Vector2.one * 0.5f; - (l_LegendLine.transform as RectTransform).offsetMin = new Vector2(-448.1f, 247.6f); - (l_LegendLine.transform as RectTransform).offsetMax = new Vector2(521.9f, 250.6f); - (l_LegendLine.transform as RectTransform).anchoredPosition = new Vector2(-4f, l_NormalizedValue * m_GraphCanvas.sizeDelta.y); - l_LegendLine.GetComponent().sprite = CP_SDK.Unity.SpriteU.CreateFromTexture(Texture2D.whiteTexture); - l_LegendLine.GetComponent().type = Image.Type.Simple; - l_LegendLine.GetComponent().color = SCVConfig.Instance.DashLineColor; - } - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Fill graph data with demo values - /// - private void FillGraphDataWithDemoValues() - { - var l_GraphData = new List<(float, float, float)>(); - l_GraphData.Add((0f, 0f, 2f)); - l_GraphData.Add((7.5f, 2f, 4f)); - l_GraphData.Add((12f, 4f, 6f)); - l_GraphData.Add((12.5f, 6f, 8f)); - l_GraphData.Add((11f, 8f, 12f)); - l_GraphData.Add((10.5f, 12f, 14f)); - l_GraphData.Add((17f, 14f, 16f)); - l_GraphData.Add((18f, 16f, 18f)); - l_GraphData.Add((14.5f, 18f, 20f)); - l_GraphData.Add((11f, 20f, 22f)); - l_GraphData.Add((15f, 22f, 24f)); - l_GraphData.Add((4f, 24f, 26f)); - l_GraphData.Add((2.5f, 26f, 28f)); - l_GraphData.Add((8.5f, 28f, 30f)); - l_GraphData.Add((9.333333f, 30f, 33f)); - l_GraphData.Add((7.5f, 33f, 35f)); - l_GraphData.Add((10.5f, 35f, 37f)); - l_GraphData.Add((9.5f, 37f, 39f)); - l_GraphData.Add((3f, 39f, 41f)); - l_GraphData.Add((5f, 41f, 43f)); - l_GraphData.Add((8f, 43f, 45f)); - l_GraphData.Add((13.5f, 45f, 47f)); - l_GraphData.Add((12.5f, 47f, 49f)); - l_GraphData.Add((9.5f, 49f, 51f)); - l_GraphData.Add((10f, 51f, 53f)); - l_GraphData.Add((13.5f, 53f, 55f)); - l_GraphData.Add((12.5f, 55f, 57f)); - l_GraphData.Add((8.5f, 57f, 59f)); - l_GraphData.Add((10f, 59f, 61f)); - l_GraphData.Add((9.666667f, 61f, 64f)); - l_GraphData.Add((12f, 64f, 66f)); - l_GraphData.Add((8f, 66f, 68f)); - l_GraphData.Add((7f, 68f, 70f)); - l_GraphData.Add((9.5f, 70f, 72f)); - l_GraphData.Add((8.5f, 72f, 74f)); - l_GraphData.Add((8f, 74f, 77f)); - l_GraphData.Add((8f, 77f, 79f)); - l_GraphData.Add((7f, 79f, 81f)); - l_GraphData.Add((8f, 81f, 83f)); - l_GraphData.Add((9f, 83f, 85f)); - l_GraphData.Add((9f, 85f, 87f)); - l_GraphData.Add((8.5f, 87f, 89f)); - l_GraphData.Add((9.666667f, 89f, 92f)); - l_GraphData.Add((9.333333f, 92f, 95f)); - l_GraphData.Add((7.5f, 95f, 97f)); - l_GraphData.Add((3f, 97f, 99f)); - l_GraphData.Add((4.5f, 99f, 101f)); - l_GraphData.Add((12f, 101f, 103f)); - l_GraphData.Add((14f, 103f, 106f)); - l_GraphData.Add((14.5f, 106f, 108f)); - l_GraphData.Add((30.5f, 108f, 110f)); - l_GraphData.Add((43f, 110f, 112f)); - l_GraphData.Add((15f, 112f, 114f)); - l_GraphData.Add((0.5f, 114f, 116f)); - l_GraphData.Add((0f, 116f, 118f)); - l_GraphData.Add((6f, 118f, 120f)); - l_GraphData.Add((11f, 120f, 122f)); - l_GraphData.Add((7f, 122f, 124f)); - l_GraphData.Add((15.5f, 124f, 126f)); - l_GraphData.Add((12f, 126f, 128f)); - l_GraphData.Add((3f, 128f, 130f)); - l_GraphData.Add((8.5f, 130f, 132f)); - l_GraphData.Add((4.5f, 132f, 134f)); - l_GraphData.Add((7.5f, 134f, 136f)); - l_GraphData.Add((10f, 136f, 138f)); - l_GraphData.Add((9.5f, 138f, 140f)); - l_GraphData.Add((5.5f, 140f, 142f)); - l_GraphData.Add((6.666667f, 142f, 145f)); - l_GraphData.Add((7.4f, 145f, 150f)); - l_GraphData.Add((7.5f, 150f, 152f)); - l_GraphData.Add((6f, 152f, 154f)); - l_GraphData.Add((7.5f, 154f, 156f)); - l_GraphData.Add((10.5f, 156f, 158f)); - l_GraphData.Add((16.5f, 158f, 160f)); - l_GraphData.Add((10.5f, 160f, 162f)); - l_GraphData.Add((0f, 162f, 164f)); - l_GraphData.Add((0f, 164f, 169f)); - l_GraphData.Add((9f, 169f, 171f)); - l_GraphData.Add((40f, 171f, 173f)); - l_GraphData.Add((45f, 173f, 175f)); - l_GraphData.Add((36.5f, 175f, 177f)); - l_GraphData.Add((11.5f, 177f, 179f)); - l_GraphData.Add((11f, 179f, 181f)); - l_GraphData.Add((15.5f, 181f, 183f)); - l_GraphData.Add((36.5f, 183f, 185f)); - l_GraphData.Add((40.33333f, 185f, 188f)); - l_GraphData.Add((18f, 188f, 190f)); - l_GraphData.Add((15.33333f, 190f, 193f)); - l_GraphData.Add((1.5f, 193f, 195f)); - l_GraphData.Add((0f, 195f, 197f)); - l_GraphData.Add((0f, 197f, 201f)); - - m_GraphData = l_GraphData.ToArray(); - } - } -} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/Properties/AssemblyInfo.cs b/Modules/BeatSaberPlus_SongChartVisualizer/Properties/AssemblyInfo.cs index fc1aacd..1810958 100644 --- a/Modules/BeatSaberPlus_SongChartVisualizer/Properties/AssemblyInfo.cs +++ b/Modules/BeatSaberPlus_SongChartVisualizer/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/Resources/Locked.png b/Modules/BeatSaberPlus_SongChartVisualizer/Resources/Locked.png deleted file mode 100644 index fe28c20ee50f69c526b958ae7832d8d7cdcecdef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3479 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 diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/SongChartVisualizer.cs b/Modules/BeatSaberPlus_SongChartVisualizer/SongChartVisualizer.cs deleted file mode 100644 index 87ff0ab..0000000 --- a/Modules/BeatSaberPlus_SongChartVisualizer/SongChartVisualizer.cs +++ /dev/null @@ -1,241 +0,0 @@ -using BeatSaberMarkupLanguage; -using BeatSaberMarkupLanguage.FloatingScreen; -using HMUI; -using System.Collections; -using UnityEngine; - -namespace BeatSaberPlus_SongChartVisualizer -{ - /// - /// SongChartVisualizer Module - /// - public class SongChartVisualizer : BeatSaberPlus.SDK.BSPModuleBase - { - /// - /// Module type - /// - public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; - /// - /// Name of the Module - /// - public override string Name => "Song Chart Visualizer"; - /// - /// Description of the Module - /// - public override string Description => "Get spoiled about the map difficulty!"; - /// - /// Is the Module using chat features - /// - public override bool UseChatFeatures => false; - /// - /// Is enabled - /// - public override bool IsEnabled { get => SCVConfig.Instance.Enabled; set { SCVConfig.Instance.Enabled = value; SCVConfig.Instance.Save(); } } - /// - /// Activation kind - /// - public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnMenuSceneLoaded; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// SongChartVisualizer view - /// - private UI.Settings m_SettingsView = null; - /// - /// SongChartVisualizer left view - /// - private UI.SettingsLeft m_SettingsLeftView = null; - /// - /// SongChartVisualizer right view - /// - private UI.SettingsRight m_SettingsRightView = null; - /// - /// Window GameObject - /// - private GameObject m_MasterGOB = null; - /// - /// Chart floating screen - /// - private FloatingScreen m_ChartFloatingScreen = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Enable the Module - /// - protected override void OnEnable() - { - /// Bind event - BeatSaberPlus.SDK.Game.Logic.OnLevelStarted += Game_LevelStarted; - } - /// - /// Disable the Module - /// - protected override void OnDisable() - { - /// Unbind event - BeatSaberPlus.SDK.Game.Logic.OnLevelStarted -= Game_LevelStarted; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Get Module settings UI - /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() - { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsLeftView == null) - m_SettingsLeftView = BeatSaberUI.CreateViewController(); - /// Create view if needed - if (m_SettingsRightView == null) - m_SettingsRightView = BeatSaberUI.CreateViewController(); - - /// Change main view - return (m_SettingsView, m_SettingsLeftView, m_SettingsRightView); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When a level start - /// - /// Level data - private void Game_LevelStarted(BeatSaberPlus.SDK.Game.LevelData p_Data) - { - /// Not enabled in multi-player - if (p_Data.Type == BeatSaberPlus.SDK.Game.LevelType.Multiplayer) - return; - - /// Start the task - CP_SDK.Unity.MTCoroutineStarter.Start(CreateChartVisualizer()); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Refresh the preview - /// - internal void RefreshPreview() - { - DestroyChart(); - CP_SDK.Unity.MTCoroutineStarter.Start(CreateChartVisualizer()); - } - /// - /// Destroy the preview - /// - internal void DestroyChart() - { - if (m_MasterGOB == null || !m_MasterGOB) - return; - - GameObject.Destroy(m_MasterGOB); - m_MasterGOB = null; - } - /// - /// Toggle chat visibility - /// - public void ToggleVisibility() - { - if (m_MasterGOB && m_MasterGOB.transform.localScale.x > 0.5f) - m_MasterGOB.transform.localScale = Vector3.zero; - else if (m_MasterGOB) - m_MasterGOB.transform.localScale = Vector3.one; - } - /// - /// Set visible - /// - /// Is visible - public void SetVisible(bool p_Visible) - { - if (m_MasterGOB) - m_MasterGOB.transform.localScale = p_Visible ? Vector3.one : Vector3.zero; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Create the chart visualizer - /// - /// - private IEnumerator CreateChartVisualizer() - { - yield return new WaitForEndOfFrame(); - - var l_HasRotation = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.transformedBeatmapData?.spawnRotationEventsCount > 0; - var l_Position = l_HasRotation ? SCVConfig.Instance.Chart360_90Position - : SCVConfig.Instance.ChartStandardPosition; - var l_Rotation = l_HasRotation ? SCVConfig.Instance.Chart360_90Rotation - : SCVConfig.Instance.ChartStandardRotation; - - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) - { - l_Position = new Vector3(2.38f, 1.20f, 1.29f); - l_Rotation = new Vector3(0f, 58f, 0f); - } - - m_ChartFloatingScreen = FloatingScreen.CreateFloatingScreen(new Vector2(105, 65), false, l_Position, Quaternion.identity, 0f, true); - m_ChartFloatingScreen.gameObject.AddComponent(); - - /// Set rotation - m_ChartFloatingScreen.ScreenRotation = Quaternion.Euler(l_Rotation); - - /// Update background color - m_ChartFloatingScreen.GetComponentInChildren().color = SCVConfig.Instance.BackgroundColor; - - /// Bind event - m_ChartFloatingScreen.HandleReleased += OnFloatingWindowMoved; - - /// Create UI Controller - var l_ChatFloatingScreenController = BeatSaberUI.CreateViewController(); - m_ChartFloatingScreen.SetRootViewController(l_ChatFloatingScreenController, HMUI.ViewController.AnimationType.None); - l_ChatFloatingScreenController.gameObject.SetActive(true); - m_ChartFloatingScreen.GetComponentInChildren().sortingOrder = 4; - - /// Master GameObject - m_MasterGOB = new GameObject("BeatSaberPlus_SongChartVisualizer"); - m_MasterGOB.transform.position = Vector3.zero; - m_MasterGOB.transform.rotation = Quaternion.identity; - - /// Apply parent - m_ChartFloatingScreen.transform.SetParent(m_MasterGOB.transform); - } - /// - /// When the floating window is moved - /// - /// Event sender - /// Event data - private void OnFloatingWindowMoved(object p_Sender, FloatingScreenHandleEventArgs p_Event) - { - /// Always parallel to the floor - if (SCVConfig.Instance.AlignWithFloor) - m_ChartFloatingScreen.transform.localEulerAngles = new Vector3(m_ChartFloatingScreen.transform.localEulerAngles.x, m_ChartFloatingScreen.transform.localEulerAngles.y, 0); - - /// Don't update from preview - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene != BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) - return; - - var l_HasRotation = BeatSaberPlus.SDK.Game.Logic.LevelData?.Data?.transformedBeatmapData?.spawnRotationEventsCount > 0; - if (!l_HasRotation) - { - SCVConfig.Instance.ChartStandardPosition = m_ChartFloatingScreen.transform.localPosition; - SCVConfig.Instance.ChartStandardRotation = m_ChartFloatingScreen.transform.localEulerAngles; - } - else - { - SCVConfig.Instance.Chart360_90Position = m_ChartFloatingScreen.transform.localPosition; - SCVConfig.Instance.Chart360_90Rotation = m_ChartFloatingScreen.transform.localEulerAngles; - } - } - } -} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/UI/FloatingWindow.bsml b/Modules/BeatSaberPlus_SongChartVisualizer/UI/FloatingWindow.bsml deleted file mode 100644 index eaf26f2..0000000 --- a/Modules/BeatSaberPlus_SongChartVisualizer/UI/FloatingWindow.bsml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/UI/FloatingWindow.cs b/Modules/BeatSaberPlus_SongChartVisualizer/UI/FloatingWindow.cs deleted file mode 100644 index 3d6b776..0000000 --- a/Modules/BeatSaberPlus_SongChartVisualizer/UI/FloatingWindow.cs +++ /dev/null @@ -1,113 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components; -using BeatSaberMarkupLanguage.FloatingScreen; -using CP_SDK.Unity.Extensions; -using HMUI; -using System.Linq; -using UnityEngine; -using VRUIControls; - -namespace BeatSaberPlus_SongChartVisualizer.UI -{ - /// - /// Floating window content - /// - internal class FloatingWindow : BeatSaberPlus.SDK.UI.ResourceViewController - { - /// - /// Is movement allowed - /// - private bool m__AllowMovement = false; - /// - /// Is movement allowed - /// - private bool m_AllowMovement - { - get => m__AllowMovement; - set { - m__AllowMovement = value; - ColorUtility.TryParseHtmlString(value ? "#FFFFFFFF" : "#FFFFFF80", out var l_ColH); - ColorUtility.TryParseHtmlString(value ? "#FFFFFF80" : "#FFFFFFFF", out var l_ColD); - m_LockIcon.HighlightColor = l_ColH; - m_LockIcon.DefaultColor = l_ColD; - - var l_FloatingScreen = transform.parent.GetComponent(); - l_FloatingScreen.ShowHandle = value; - - if (l_FloatingScreen.handle) - { - l_FloatingScreen.handle.transform.localScale = new Vector2(105, 65); - l_FloatingScreen.handle.transform.localPosition = Vector3.zero; - l_FloatingScreen.handle.transform.localRotation = Quaternion.identity; - - /// Update handle material - var l_ChartFloatingScreenHandleMaterial = GameObject.Instantiate(BeatSaberPlus.SDK.Unity.MaterialU.UINoGlowMaterial); - l_ChartFloatingScreenHandleMaterial.color = Color.clear; - l_FloatingScreen.handle.gameObject.GetComponent().material = l_ChartFloatingScreenHandleMaterial; - } - - if (value) - { - /// Refresh VR pointer due to bug - var l_Pointers = Resources.FindObjectsOfTypeAll(); - var l_Pointer = BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing ? l_Pointers.LastOrDefault() : l_Pointers.FirstOrDefault(); - - if (l_Pointer != null) - { - if (l_FloatingScreen.screenMover) - Destroy(l_FloatingScreen.screenMover); - - l_FloatingScreen.screenMover = l_Pointer.gameObject.AddComponent(); - l_FloatingScreen.screenMover.Init(l_FloatingScreen); - } - else - { - Logger.Instance.Warning("Failed to get VRPointer!"); - } - } - } - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Lock icon - /// - [UIComponent("LockIcon")] - private ClickableImage m_LockIcon = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - /// Update background color - GetComponentInChildren().color = Color.white.WithAlpha(0f); - - /// Update lock state - m_AllowMovement = false; - - /// Hide the lock icon - if (!SCVConfig.Instance.ShowLockIcon) - m_LockIcon.gameObject.SetActive(false); - - gameObject.ChangerLayerRecursive(LayerMask.NameToLayer("UI")); - - /// Make icons easier to click - m_LockIcon.gameObject.AddComponent().radius = 10f; - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIAction("lock-pressed")] - internal void OnLockPressed() - { - m_AllowMovement = !m_AllowMovement; - } - } -} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/UI/Settings.bsml b/Modules/BeatSaberPlus_SongChartVisualizer/UI/Settings.bsml deleted file mode 100644 index 2a6849c..0000000 --- a/Modules/BeatSaberPlus_SongChartVisualizer/UI/Settings.bsml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/UI/Settings.cs b/Modules/BeatSaberPlus_SongChartVisualizer/UI/Settings.cs deleted file mode 100644 index be3e0f2..0000000 --- a/Modules/BeatSaberPlus_SongChartVisualizer/UI/Settings.cs +++ /dev/null @@ -1,199 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; -using UnityEngine; - -namespace BeatSaberPlus_SongChartVisualizer.UI -{ - /// - /// Settings main view - /// - internal class Settings : BeatSaberPlus.SDK.UI.ResourceViewController - { -#pragma warning disable CS0649 - [UIComponent("alignwithfloor-toggle")] - public ToggleSetting m_AlignWithFloor; - [UIComponent("showlockicon-toggle")] - public ToggleSetting m_ShowLockIcon; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("followenvironementrotations-toggle")] - private ToggleSetting m_FollowEnvironementRotations; - [UIComponent("backgroundopacity-increment")] - private IncrementSetting m_BackgroundOpacity; - [UIComponent("cursoropacity-increment")] - private IncrementSetting m_CursorOpacity; - [UIComponent("lineopacity-increment")] - private IncrementSetting m_LineOpacity; - [UIComponent("legendopacity-increment")] - private IncrementSetting m_LegendOpacity; - [UIComponent("dashopacity-increment")] - private IncrementSetting m_DashOpacity; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - [UIComponent("shownpslegend-toggle")] - private ToggleSetting m_ShowNPSLegend; - [UIComponent("background-color")] - private ColorSetting m_BackgroundColor; - [UIComponent("cursor-color")] - private ColorSetting m_CursorColor; - [UIComponent("line-color")] - private ColorSetting m_LineColor; - [UIComponent("legend-color")] - private ColorSetting m_LegendColor; - [UIComponent("dash-color")] - private ColorSetting m_DashColor; -#pragma warning restore CS0649 - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Should prevent changes - /// - private bool m_PreventChanges = false; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); - - /// Left - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_AlignWithFloor, l_Event, SCVConfig.Instance.AlignWithFloor, true); - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ShowLockIcon, l_Event, SCVConfig.Instance.ShowLockIcon, true); - - /// Center - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_FollowEnvironementRotations, l_Event, SCVConfig.Instance.FollowEnvironementRotation, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_BackgroundOpacity, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, SCVConfig.Instance.BackgroundColor.a, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_CursorOpacity, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, SCVConfig.Instance.CursorColor.a, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_LineOpacity, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, SCVConfig.Instance.LineColor.a, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_LegendOpacity, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, SCVConfig.Instance.LegendColor.a, true); - BeatSaberPlus.SDK.UI.IncrementSetting.Setup(m_DashOpacity, l_Event, BeatSaberPlus.SDK.UI.BSMLSettingFormartter.Percentage, SCVConfig.Instance.DashLineColor.a, true); - - /// Right - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_ShowNPSLegend, l_Event, SCVConfig.Instance.ShowNPSLegend, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_BackgroundColor, l_Event, SCVConfig.Instance.BackgroundColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_CursorColor, l_Event, SCVConfig.Instance.CursorColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_LineColor, l_Event, SCVConfig.Instance.LineColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_LegendColor, l_Event, SCVConfig.Instance.LegendColor, true); - BeatSaberPlus.SDK.UI.ColorSetting.Setup(m_DashColor, l_Event, SCVConfig.Instance.DashLineColor, true); - } - /// - /// On view activation - /// - protected override sealed void OnViewActivation() - { - SongChartVisualizer.Instance.RefreshPreview(); - } - /// - /// On view deactivation - /// - protected override sealed void OnViewDeactivation() - { - SongChartVisualizer.Instance.DestroyChart(); - SCVConfig.Instance.Save(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// When settings are changed - /// - /// - private void OnSettingChanged(object p_Value) - { - if (m_PreventChanges) - return; - - /// Update config - SCVConfig.Instance.AlignWithFloor = m_AlignWithFloor.Value; - SCVConfig.Instance.ShowLockIcon = m_ShowLockIcon.Value; - SCVConfig.Instance.FollowEnvironementRotation = m_FollowEnvironementRotations.Value; - SCVConfig.Instance.ShowNPSLegend = m_ShowNPSLegend.Value; - - SCVConfig.Instance.CursorColor = new Color( - m_BackgroundColor.CurrentColor.r, - m_BackgroundColor.CurrentColor.g, - m_BackgroundColor.CurrentColor.b, - m_BackgroundOpacity.Value - ); - - SCVConfig.Instance.CursorColor = new Color( - m_CursorColor.CurrentColor.r, - m_CursorColor.CurrentColor.g, - m_CursorColor.CurrentColor.b, - m_CursorOpacity.Value - ); - - SCVConfig.Instance.LineColor = new Color( - m_LineColor.CurrentColor.r, - m_LineColor.CurrentColor.g, - m_LineColor.CurrentColor.b, - m_LineOpacity.Value - ); - - SCVConfig.Instance.LegendColor = new Color( - m_LegendColor.CurrentColor.r, - m_LegendColor.CurrentColor.g, - m_LegendColor.CurrentColor.b, - m_LegendOpacity.Value - ); - - SCVConfig.Instance.DashLineColor = new Color( - m_DashColor.CurrentColor.r, - m_DashColor.CurrentColor.g, - m_DashColor.CurrentColor.b, - m_DashOpacity.Value - ); - - SCVConfig.Instance.Save(); - - /// Refresh preview - SongChartVisualizer.Instance.RefreshPreview(); - } - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - - /// - /// Reset settings - /// - internal void OnResetButton() - { - m_PreventChanges = true; - - /// Set values - m_AlignWithFloor.Value = SCVConfig.Instance.AlignWithFloor; - m_ShowLockIcon.Value = SCVConfig.Instance.ShowLockIcon; - - /// Set values - m_FollowEnvironementRotations.Value = SCVConfig.Instance.FollowEnvironementRotation; - m_BackgroundOpacity.Value = SCVConfig.Instance.BackgroundColor.a; - m_CursorOpacity.Value = SCVConfig.Instance.CursorColor.a; - m_LineOpacity.Value = SCVConfig.Instance.LineColor.a; - m_LegendOpacity.Value = SCVConfig.Instance.LegendColor.a; - m_DashOpacity.Value = SCVConfig.Instance.DashLineColor.a; - - /// Set values - m_ShowNPSLegend.Value = SCVConfig.Instance.ShowNPSLegend; - m_BackgroundColor.CurrentColor = SCVConfig.Instance.BackgroundColor.ColorWithAlpha(1f); - m_CursorColor.CurrentColor = SCVConfig.Instance.CursorColor.ColorWithAlpha(1f); - m_LineColor.CurrentColor = SCVConfig.Instance.LineColor.ColorWithAlpha(1f); - m_LegendColor.CurrentColor = SCVConfig.Instance.LegendColor.ColorWithAlpha(1f); - m_DashColor.CurrentColor = SCVConfig.Instance.DashLineColor.ColorWithAlpha(1f); - - m_PreventChanges = false; - - SongChartVisualizer.Instance.RefreshPreview(); - } - } -} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsLeft.bsml b/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsLeft.bsml deleted file mode 100644 index 50245dc..0000000 --- a/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsLeft.bsml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsRight.bsml b/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsRight.bsml deleted file mode 100644 index ebcaf1a..0000000 --- a/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsRight.bsml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsRight.cs b/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsRight.cs deleted file mode 100644 index d499d4b..0000000 --- a/Modules/BeatSaberPlus_SongChartVisualizer/UI/SettingsRight.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace BeatSaberPlus_SongChartVisualizer.UI -{ - /// - /// Settings right view - /// - internal class SettingsRight : BeatSaberPlus.SDK.UI.ResourceViewController - { - - } -} diff --git a/Modules/BeatSaberPlus_SongChartVisualizer/manifest.json b/Modules/BeatSaberPlus_SongChartVisualizer/manifest.json index 1be457b..928c76f 100644 --- a/Modules/BeatSaberPlus_SongChartVisualizer/manifest.json +++ b/Modules/BeatSaberPlus_SongChartVisualizer/manifest.json @@ -3,13 +3,12 @@ "id": "BeatSaberPlus_SongChartVisualizer", "name": "BeatSaberPlus_SongChartVisualizer", "author": "HardCPP#1985", - "version": "5.0.7", + "version": "6.0.8", "description": "", - "gameVersion": "1.25.0", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.0.1", - "BeatSaberMarkupLanguage": "^1.3.4", - "BeatSaberPlusCORE": "^5.0.7" + "BSIPA": "^4.3.0", + "BeatSaberPlusCORE": "^6.0.8" }, "links": { "project-home": "https://discord.chatplex.org", diff --git a/Modules/BeatSaberPlus_SongOverlay/BeatSaberPlus_SongOverlay.csproj b/Modules/BeatSaberPlus_SongOverlay/BeatSaberPlus_SongOverlay.csproj index 94f98fc..dda604f 100644 --- a/Modules/BeatSaberPlus_SongOverlay/BeatSaberPlus_SongOverlay.csproj +++ b/Modules/BeatSaberPlus_SongOverlay/BeatSaberPlus_SongOverlay.csproj @@ -47,17 +47,18 @@ OnBuildSuccess - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll False False - - $(BeatSaberDir)\Plugins\BSML.dll - False - False - - + $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll False False @@ -68,23 +69,10 @@ False - - - - - - + $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll - False - $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll False @@ -110,18 +98,10 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll - False - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll - False - @@ -134,16 +114,14 @@ - + + - - Settings.cs - diff --git a/Modules/BeatSaberPlus_SongOverlay/Network/OverlayServer.cs b/Modules/BeatSaberPlus_SongOverlay/Network/OverlayServer.cs index f9b5291..050044f 100644 --- a/Modules/BeatSaberPlus_SongOverlay/Network/OverlayServer.cs +++ b/Modules/BeatSaberPlus_SongOverlay/Network/OverlayServer.cs @@ -212,7 +212,7 @@ internal static void OnClientConnected(OverlaySession p_Client) { p_Client.SendData(m_Handshake); - if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) + if (BeatSaberPlus.SDK.Game.Logic.ActiveScene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) { p_Client.SendData(JsonConvert.SerializeObject(m_MapInfoEvent)); p_Client.SendData(JsonConvert.SerializeObject(m_ScoreEvent)); @@ -260,9 +260,9 @@ internal static void OnClientDisconnected(OverlaySession p_Client) /// On Game State changed /// /// New scene - private static void Logic_OnSceneChange(BeatSaberPlus.SDK.Game.Logic.SceneType p_Scene) + private static void Logic_OnSceneChange(BeatSaberPlus.SDK.Game.Logic.ESceneType p_Scene) { - if (p_Scene == BeatSaberPlus.SDK.Game.Logic.SceneType.Playing) + if (p_Scene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Playing) { m_IsPaused = false; @@ -293,7 +293,7 @@ private static void Logic_OnSceneChange(BeatSaberPlus.SDK.Game.Logic.SceneType p CP_SDK.Unity.MTCoroutineStarter.Start(Coroutine_WaitForGameplayReady(l_Map.Type, l_CoverTask)); } - else if (p_Scene == BeatSaberPlus.SDK.Game.Logic.SceneType.Menu) + else if (p_Scene == BeatSaberPlus.SDK.Game.Logic.ESceneType.Menu) { m_IsPaused = false; m_ScoreController = null; @@ -386,7 +386,7 @@ private static IEnumerator Coroutine_WaitForGameplayReady(BeatSaberPlus.SDK.Game m_ScoreController.scoreDidChangeEvent += ScoreController_scoreDidChangeEvent; m_GameEnergyCounter.gameEnergyDidChangeEvent += GameEnergyCounter_gameEnergyDidChangeEvent; - var l_BeatmapObjectManager = m_ScoreController.GetField("_beatmapObjectManager"); + var l_BeatmapObjectManager = m_ScoreController._beatmapObjectManager; if (l_BeatmapObjectManager != null) { l_BeatmapObjectManager.noteWasCutEvent += ScoreController_noteWasCutEvent; @@ -410,7 +410,7 @@ private static IEnumerator Coroutine_WaitForGameplayReady(BeatSaberPlus.SDK.Game m_ResumeEventQueued = true; }; - m_IsPaused = m_PauseController.GetField("_paused"); + m_IsPaused = m_PauseController._paused; } else m_IsPaused = false; diff --git a/Modules/BeatSaberPlus_SongOverlay/Properties/AssemblyInfo.cs b/Modules/BeatSaberPlus_SongOverlay/Properties/AssemblyInfo.cs index 0079a62..64449f7 100644 --- a/Modules/BeatSaberPlus_SongOverlay/Properties/AssemblyInfo.cs +++ b/Modules/BeatSaberPlus_SongOverlay/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.7")] -[assembly: AssemblyFileVersion("5.0.7")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/Modules/BeatSaberPlus_SongOverlay/SongOverlay.cs b/Modules/BeatSaberPlus_SongOverlay/SongOverlay.cs index 3f701a8..5f1a173 100644 --- a/Modules/BeatSaberPlus_SongOverlay/SongOverlay.cs +++ b/Modules/BeatSaberPlus_SongOverlay/SongOverlay.cs @@ -1,47 +1,27 @@ -using BeatSaberMarkupLanguage; -using System.Collections; +using System.Collections; using System.Collections.Generic; using UnityEngine; namespace BeatSaberPlus_SongOverlay { /// - /// Online instance + /// Song Overlay instance /// - internal class SongOverlay : BeatSaberPlus.SDK.BSPModuleBase + public class SongOverlay : CP_SDK.ModuleBase { - /// - /// Module type - /// - public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; - /// - /// Name of the Module - /// - public override string Name => "Song Overlay"; - /// - /// Description of the Module - /// - public override string Description => "Song overlay server for your stream!"; - /// - /// Is the Module using chat features - /// - public override bool UseChatFeatures => false; - /// - /// Is enabled - /// - public override bool IsEnabled { get => SOConfig.Instance.Enabled; set { SOConfig.Instance.Enabled = value; SOConfig.Instance.Save(); } } - /// - /// Activation kind - /// - public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnStart; + public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; + public override string Name => "Song Overlay"; + public override string Description => "Song overlay server for your stream!"; + public override string DocumentationURL => "https://github.com/hardcpp/BeatSaberPlus/wiki#song-overlay"; + public override bool UseChatFeatures => false; + public override bool IsEnabled { get => SOConfig.Instance.Enabled; set { SOConfig.Instance.Enabled = value; SOConfig.Instance.Save(); } } + public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnStart; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Settings view - /// - private UI.Settings m_SettingsView = null; + private UI.SettingsLeftView m_SettingsLeftView = null; + private UI.SettingsMainView m_SettingsMainView = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -60,6 +40,9 @@ protected override void OnEnable() /// protected override void OnDisable() { + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsLeftView); + CP_SDK.UI.UISystem.DestroyUI(ref m_SettingsMainView); + Network.OverlayServer.Stop(); } @@ -69,14 +52,12 @@ protected override void OnDisable() /// /// Get Module settings UI /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); + if (m_SettingsLeftView == null) m_SettingsLeftView = CP_SDK.UI.UISystem.CreateViewController(); - /// Change main view - return (m_SettingsView, null, null); + return (m_SettingsMainView, m_SettingsLeftView, null); } //////////////////////////////////////////////////////////////////////////// @@ -108,6 +89,5 @@ private static IEnumerator Coroutine_CheckCompatibility() + "It's recommended to only use 1 of these mods for performance reasons"); } } - } } diff --git a/Modules/BeatSaberPlus_SongOverlay/UI/Settings.bsml b/Modules/BeatSaberPlus_SongOverlay/UI/Settings.bsml deleted file mode 100644 index 20ac673..0000000 --- a/Modules/BeatSaberPlus_SongOverlay/UI/Settings.bsml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Modules/BeatSaberPlus_SongOverlay/UI/Settings.cs b/Modules/BeatSaberPlus_SongOverlay/UI/Settings.cs deleted file mode 100644 index 0703cfb..0000000 --- a/Modules/BeatSaberPlus_SongOverlay/UI/Settings.cs +++ /dev/null @@ -1,26 +0,0 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; - -namespace BeatSaberPlus_SongOverlay.UI -{ - /// - /// ModuleTemplate settings view controller - /// - internal class Settings : BeatSaberPlus.SDK.UI.ResourceViewController - { - /// - /// On view creation - /// - protected override sealed void OnViewCreation() - { - - } - /// - /// On view deactivation - /// - protected sealed override void OnViewDeactivation() - { - SOConfig.Instance.Save(); - } - } -} diff --git a/Modules/BeatSaberPlus_SongOverlay/UI/SettingsLeftView.cs b/Modules/BeatSaberPlus_SongOverlay/UI/SettingsLeftView.cs new file mode 100644 index 0000000..784f41c --- /dev/null +++ b/Modules/BeatSaberPlus_SongOverlay/UI/SettingsLeftView.cs @@ -0,0 +1,44 @@ +using CP_SDK.XUI; +using UnityEngine; + +namespace BeatSaberPlus_SongOverlay.UI +{ + /// + /// Settings left view + /// + internal sealed class SettingsLeftView : CP_SDK.UI.ViewController + { + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayout( + Templates.TitleBar("Information"), + + Templates.ScrollableInfos(55, + XUIText.Make($"This module provide a server for Web based song overlays!") + .SetAlign(TMPro.TextAlignmentOptions.Left) + ), + + Templates.ExpandedButtonsLine( + XUIPrimaryButton.Make("Documentation", OnDocumentationButton) + ) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// + /// Documentation button + /// + private void OnDocumentationButton() + { + ShowMessageModal("URL opened in your web browser."); + Application.OpenURL(SongOverlay.Instance.DocumentationURL); + } + } +} diff --git a/Modules/BeatSaberPlus_SongOverlay/UI/SettingsMainView.cs b/Modules/BeatSaberPlus_SongOverlay/UI/SettingsMainView.cs new file mode 100644 index 0000000..260db09 --- /dev/null +++ b/Modules/BeatSaberPlus_SongOverlay/UI/SettingsMainView.cs @@ -0,0 +1,35 @@ +using CP_SDK.XUI; + +namespace BeatSaberPlus_SongOverlay.UI +{ + /// + /// Settings main view + /// + internal sealed class SettingsMainView : CP_SDK.UI.ViewController + { + /// + /// On view creation + /// + protected override sealed void OnViewCreation() + { + Templates.FullRectLayoutMainView( + Templates.TitleBar("Song Overlay - Settings"), + + XUIText.Make("No settings available yet...") + .SetStyle(TMPro.FontStyles.Italic) + .SetAlign(TMPro.TextAlignmentOptions.Midline), + + XUIVSpacer.Make(60f) + ) + .SetBackground(true, null, true) + .BuildUI(transform); + } + /// + /// On view deactivation + /// + protected sealed override void OnViewDeactivation() + { + SOConfig.Instance.Save(); + } + } +} diff --git a/Modules/BeatSaberPlus_SongOverlay/manifest.json b/Modules/BeatSaberPlus_SongOverlay/manifest.json index 37bec28..62ba2a8 100644 --- a/Modules/BeatSaberPlus_SongOverlay/manifest.json +++ b/Modules/BeatSaberPlus_SongOverlay/manifest.json @@ -3,13 +3,12 @@ "id": "BeatSaberPlus_SongOverlay", "name": "BeatSaberPlus_SongOverlay", "author": "HardCPP#1985", - "version": "5.0.7", + "version": "6.0.8", "description": "BeatSaberPlus song overlay module.", - "gameVersion": "1.25.0", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.0.2", - "BeatSaberMarkupLanguage": "^1.3.4", - "BeatSaberPlusCORE": "^5.0.7" + "BSIPA": "^4.3.0", + "BeatSaberPlusCORE": "^6.0.8" }, "links": { "project-home": "https://discord.chatplex.org", diff --git a/Samples/BeatSaberPlus_ModuleTemplate/BeatSaberPlus_ModuleTemplate.csproj b/Samples/BeatSaberPlus_ModuleTemplate/BeatSaberPlus_ModuleTemplate.csproj index b0ef324..0eec2cd 100644 --- a/Samples/BeatSaberPlus_ModuleTemplate/BeatSaberPlus_ModuleTemplate.csproj +++ b/Samples/BeatSaberPlus_ModuleTemplate/BeatSaberPlus_ModuleTemplate.csproj @@ -47,20 +47,10 @@ OnBuildSuccess - - $(BeatSaberDir)\Plugins\BSML.dll - False - False - ..\..\..\..\..\..\SteamLibrary\steamapps\common\Beat Saber\Libs\Newtonsoft.Json.dll - - - - - $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False @@ -69,10 +59,6 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll - False - $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll False @@ -93,18 +79,6 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll - False - - - $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll - False - @@ -112,16 +86,13 @@ - + - - Settings.cs - diff --git a/Samples/BeatSaberPlus_ModuleTemplate/BeatSaberPlus_ModuleTemplate.csproj.user b/Samples/BeatSaberPlus_ModuleTemplate/BeatSaberPlus_ModuleTemplate.csproj.user index 5db8d9ee60929239d9779dc6a24adfdc0c01779f..012781eeb9f58b5f0dd22267773d93a2d711944f 100644 GIT binary patch delta 25 fcmaFI@`q)^I!0av215ot1|tSbAZa*xE#pA|UV#Rx delta 11 Tcmeyv@{VQ0I>yO+7!LpdA+QBv diff --git a/Samples/BeatSaberPlus_ModuleTemplate/Logger.cs b/Samples/BeatSaberPlus_ModuleTemplate/Logger.cs index f1352d1..3e4ddd0 100644 --- a/Samples/BeatSaberPlus_ModuleTemplate/Logger.cs +++ b/Samples/BeatSaberPlus_ModuleTemplate/Logger.cs @@ -8,6 +8,6 @@ internal class Logger /// /// Logger instance /// - internal static IPA.Logging.Logger Instance; + internal static CP_SDK.Logging.ILogger Instance; } } diff --git a/Samples/BeatSaberPlus_ModuleTemplate/ModuleTemplate.cs b/Samples/BeatSaberPlus_ModuleTemplate/ModuleTemplate.cs index 429f9b2..afea4d6 100644 --- a/Samples/BeatSaberPlus_ModuleTemplate/ModuleTemplate.cs +++ b/Samples/BeatSaberPlus_ModuleTemplate/ModuleTemplate.cs @@ -1,37 +1,21 @@ -using BeatSaberMarkupLanguage; -using UnityEngine; - -namespace BeatSaberPlus_ModuleTemplate +namespace BeatSaberPlus_ModuleTemplate { /// /// Online instance /// - internal class ModuleTemplate : BeatSaberPlus.SDK.BSPModuleBase + internal class ModuleTemplate : CP_SDK.ModuleBase { - /// - /// Module type - /// - public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; - /// - /// Name of the Module - /// - public override string Name => "Module Template"; - /// - /// Description of the Module - /// - public override string Description => "Hello world!"; - /// - /// Is the Module using chat features - /// - public override bool UseChatFeatures => false; - /// - /// Is enabled - /// - public override bool IsEnabled { get => MTConfig.Instance.Enabled; set { MTConfig.Instance.Enabled = value; MTConfig.Instance.Save(); } } - /// - /// Activation kind - /// - public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnStart; + public override CP_SDK.EIModuleBaseType Type => CP_SDK.EIModuleBaseType.Integrated; + public override string Name => "Module Template"; + public override string Description => "Hello world!"; + public override bool UseChatFeatures => false; + public override bool IsEnabled { get => MTConfig.Instance.Enabled; set { MTConfig.Instance.Enabled = value; MTConfig.Instance.Save(); } } + public override CP_SDK.EIModuleBaseActivationType ActivationType => CP_SDK.EIModuleBaseActivationType.OnStart; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + private UI.SettingsMainView m_SettingsMainView = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -59,27 +43,16 @@ protected override void OnDisable() /// /// Get Module settings UI /// - protected override (HMUI.ViewController, HMUI.ViewController, HMUI.ViewController) GetSettingsUIImplementation() + protected override (CP_SDK.UI.IViewController, CP_SDK.UI.IViewController, CP_SDK.UI.IViewController) GetSettingsViewControllersImplementation() { - /// Create view if needed - if (m_SettingsView == null) - m_SettingsView = BeatSaberUI.CreateViewController(); + if (m_SettingsMainView == null) m_SettingsMainView = CP_SDK.UI.UISystem.CreateViewController(); - /// Change main view - return (m_SettingsView, null, null); + return (m_SettingsMainView, null, null); } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - /// - /// Settings view - /// - private UI.Settings m_SettingsView = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - /// /// On level started /// @@ -89,7 +62,7 @@ private void Game_OnLevelStarted(BeatSaberPlus.SDK.Game.LevelData p_LevelData) var l_MapName = p_LevelData?.Data?.previewBeatmapLevel?.songName ?? "?"; var l_PlatformName = p_LevelData?.Data?.environmentInfo?.serializedName ?? "?"; - Logger.Instance.Warn($"Map {l_MapName} started on platform {l_PlatformName}"); + Logger.Instance.Warning($"Map {l_MapName} started on platform {l_PlatformName}"); } } } diff --git a/Samples/BeatSaberPlus_ModuleTemplate/Plugin.cs b/Samples/BeatSaberPlus_ModuleTemplate/Plugin.cs index 2d7cd58..d9b211c 100644 --- a/Samples/BeatSaberPlus_ModuleTemplate/Plugin.cs +++ b/Samples/BeatSaberPlus_ModuleTemplate/Plugin.cs @@ -1,5 +1,4 @@ using IPA; -using UnityEngine; namespace BeatSaberPlus_ModuleTemplate { @@ -9,18 +8,6 @@ namespace BeatSaberPlus_ModuleTemplate [Plugin(RuntimeOptions.SingleStartInit)] public class Plugin { - /// - /// Plugin instance - /// - internal static Plugin Instance { get; private set; } - /// - /// Custom logo texture - /// - internal static Texture2D CustomLogoTexture = null; - - //////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////// - /// /// Called when the plugin is first loaded by IPA (either when the game starts or when the plugin is enabled if it starts disabled). /// @@ -28,36 +15,26 @@ public class Plugin [Init] public Plugin(IPA.Logging.Logger p_Logger) { - /// Set instance - Instance = this; - /// Setup logger - Logger.Instance = p_Logger; + Logger.Instance = new CP_SDK.Logging.IPALogger(p_Logger); } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /// - /// On BeatSaberPlus_Online enable + /// On BeatSaberPlus enable /// - [OnStart] - public void OnApplicationStart() + [OnEnable] + public void OnEnable() { - try - { - } - catch (System.Exception p_Exception) - { - Logger.Instance.Critical(p_Exception); - } } /// - /// On BeatSaberPlus_Online disable + /// On BeatSaberPlus disable /// - [OnExit] - public void OnApplicationQuit() + [OnDisable] + public void OnDisable() { } diff --git a/Samples/BeatSaberPlus_ModuleTemplate/Properties/AssemblyInfo.cs b/Samples/BeatSaberPlus_ModuleTemplate/Properties/AssemblyInfo.cs index 7260623..480c2ad 100644 --- a/Samples/BeatSaberPlus_ModuleTemplate/Properties/AssemblyInfo.cs +++ b/Samples/BeatSaberPlus_ModuleTemplate/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("5.0.1")] -[assembly: AssemblyFileVersion("5.0.1")] +[assembly: AssemblyVersion("6.0.8")] +[assembly: AssemblyFileVersion("6.0.8")] diff --git a/Samples/BeatSaberPlus_ModuleTemplate/UI/Settings.bsml b/Samples/BeatSaberPlus_ModuleTemplate/UI/Settings.bsml deleted file mode 100644 index b11cfb2..0000000 --- a/Samples/BeatSaberPlus_ModuleTemplate/UI/Settings.bsml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Samples/BeatSaberPlus_ModuleTemplate/UI/Settings.cs b/Samples/BeatSaberPlus_ModuleTemplate/UI/SettingsMainView.cs similarity index 56% rename from Samples/BeatSaberPlus_ModuleTemplate/UI/Settings.cs rename to Samples/BeatSaberPlus_ModuleTemplate/UI/SettingsMainView.cs index 6e59b1d..7992c24 100644 --- a/Samples/BeatSaberPlus_ModuleTemplate/UI/Settings.cs +++ b/Samples/BeatSaberPlus_ModuleTemplate/UI/SettingsMainView.cs @@ -1,17 +1,13 @@ -using BeatSaberMarkupLanguage.Attributes; -using BeatSaberMarkupLanguage.Components.Settings; +using CP_SDK.XUI; namespace BeatSaberPlus_ModuleTemplate.UI { /// /// ModuleTemplate settings view controller /// - internal class Settings : BeatSaberPlus.SDK.UI.ResourceViewController + internal sealed class SettingsMainView : CP_SDK.UI.ViewController { -#pragma warning disable CS0649 - [UIComponent("templatesetting-toggle")] - private ToggleSetting m_TemplateSetting; -#pragma warning restore CS0649 + private XUIToggle m_TemplateSetting = null; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -21,9 +17,17 @@ internal class Settings : BeatSaberPlus.SDK.UI.ResourceViewController /// protected override sealed void OnViewCreation() { - var l_Event = new BeatSaberMarkupLanguage.Parser.BSMLAction(this, this.GetType().GetMethod(nameof(Settings.OnSettingChanged), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)); + Templates.FullRectLayoutMainView( + Templates.TitleBar("Module Template | Settings"), - BeatSaberPlus.SDK.UI.ToggleSetting.Setup(m_TemplateSetting, l_Event, MTConfig.Instance.TemplateSetting, true); + XUIText.Make("Template setting"), + XUIToggle.Make() + .SetValue(MTConfig.Instance.TemplateSetting) + .OnValueChanged((_) => OnSettingChanged()) + .Bind(ref m_TemplateSetting) + ) + .SetBackground(true, null, true) + .BuildUI(transform); } /// /// On view deactivation @@ -39,11 +43,10 @@ protected sealed override void OnViewDeactivation() /// /// On setting changed /// - /// New value - private void OnSettingChanged(object p_Value) + private void OnSettingChanged() { /// Update config - MTConfig.Instance.TemplateSetting = m_TemplateSetting.Value; + MTConfig.Instance.TemplateSetting = m_TemplateSetting.Element.GetValue(); } } } diff --git a/Samples/BeatSaberPlus_ModuleTemplate/manifest.json b/Samples/BeatSaberPlus_ModuleTemplate/manifest.json index c69c31a..3cb8fe0 100644 --- a/Samples/BeatSaberPlus_ModuleTemplate/manifest.json +++ b/Samples/BeatSaberPlus_ModuleTemplate/manifest.json @@ -3,16 +3,15 @@ "id": "BeatSaberPlus_ModuleTemplate", "name": "BeatSaberPlus_ModuleTemplate", "author": "HardCPP#1985", - "version": "5.0.1", - "description": "BeatSaberPlus module template.", - "gameVersion": "1.20.0", + "version": "6.0.8", + "description": "", + "gameVersion": "1.31.0", "dependsOn": { - "BSIPA": "^4.0.2", - "BeatSaberMarkupLanguage": "^1.3.4", - "BeatSaberPlusCORE": "^5.0.1" + "BSIPA": "^4.3.0", + "BeatSaberPlusCORE": "^6.0.8" }, "links": { - "project-home": "https://discord.gg/63ebPMC", - "donate": "https://www.patreon.com/BeatSaberPlus" + "project-home": "https://discord.chatplex.org", + "donate": "https://donate.chatplex.org" } } \ No newline at end of file