From 5a8367d7d84c20b214df8565d6b1bdf04d316bae Mon Sep 17 00:00:00 2001 From: HardCPP Date: Wed, 6 Aug 2025 22:57:25 +0200 Subject: [PATCH 01/15] Version 6.4.1 for BS 1.40.8 --- include/git_info.h | 2 +- mod.json | 28 +- mod.template.json | 5 +- qpm.json | 24 +- qpm.shared.json | 106 ++-- shared/CP_SDK/CPConfig.hpp | 6 +- shared/CP_SDK/ChatPlexService.hpp | 119 ++++ shared/CP_SDK/Network/IWebClient.hpp | 82 +-- shared/CP_SDK/Network/JsonRPCClient.hpp | 90 +++ shared/CP_SDK/Network/JsonRPCResult.hpp | 53 ++ shared/CP_SDK/Network/WebClientCore.hpp | 183 ++++++ shared/CP_SDK/Network/WebClientUnity.hpp | 143 ++--- shared/CP_SDK/Network/WebContent.hpp | 8 +- shared/CP_SDK/Network/WebResponse.hpp | 36 +- shared/CP_SDK/UI/Views/SettingsLeftView.hpp | 28 +- shared/CP_SDK/Utils/Il2cpp.hpp | 6 - shared/CP_SDK/Utils/Json.hpp | 3 +- src/CP_SDK/CPConfig.cpp | 2 + src/CP_SDK/ChatPlexSDK.cpp | 6 +- src/CP_SDK/ChatPlexService.cpp | 318 +++++++++++ src/CP_SDK/Network/JsonRPCClient.cpp | 190 +++++++ src/CP_SDK/Network/JsonRPCResult.cpp | 49 ++ src/CP_SDK/Network/WebClientCore.cpp | 594 ++++++++++++++++++++ src/CP_SDK/Network/WebClientUnity.cpp | 255 +++++---- src/CP_SDK/Network/WebContent.cpp | 8 + src/CP_SDK/Network/WebResponse.cpp | 46 +- src/CP_SDK/UI/Views/SettingsLeftView.cpp | 184 +++++- src/CP_SDK/Utils/Il2cpp.cpp | 2 + src/CP_SDK_BS/Game/LevelSelection.cpp | 12 +- 29 files changed, 2243 insertions(+), 345 deletions(-) create mode 100644 shared/CP_SDK/ChatPlexService.hpp create mode 100644 shared/CP_SDK/Network/JsonRPCClient.hpp create mode 100644 shared/CP_SDK/Network/JsonRPCResult.hpp create mode 100644 shared/CP_SDK/Network/WebClientCore.hpp create mode 100644 src/CP_SDK/ChatPlexService.cpp create mode 100644 src/CP_SDK/Network/JsonRPCClient.cpp create mode 100644 src/CP_SDK/Network/JsonRPCResult.cpp create mode 100644 src/CP_SDK/Network/WebClientCore.cpp diff --git a/include/git_info.h b/include/git_info.h index 5c0b8ca..8001fe1 100644 --- a/include/git_info.h +++ b/include/git_info.h @@ -1,5 +1,5 @@ #pragma once #define GIT_USER "HardCPP" #define GIT_BRANCH "dev" -#define GIT_COMMIT 0xc20158d +#define GIT_COMMIT 0x51e3151 #define GIT_MODIFIED 1 diff --git a/mod.json b/mod.json index 6dc76ac..5b4d3dd 100644 --- a/mod.json +++ b/mod.json @@ -5,42 +5,42 @@ "id": "chatplex-sdk-bs", "modloader": "Scotland2", "author": "HardCPP", - "version": "6.4.0", + "version": "6.4.1", "packageId": "com.beatgames.beatsaber", - "packageVersion": "1.40.4_5283", + "packageVersion": "1.40.8_7379", "description": "ChatPlex BeatSaber modding SDK (Dependence for other mods)", "coverImage": "cover.png", "dependencies": [ { - "version": "^6.4.1", + "version": "^6.4.2", "id": "beatsaber-hook", - "downloadIfMissing": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v6.4.1/beatsaber-hook.qmod" + "downloadIfMissing": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v6.4.2/beatsaber-hook.qmod" }, { - "version": "^0.18.2", + "version": "^0.18.3", "id": "custom-types", - "downloadIfMissing": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.2/CustomTypes.qmod" + "downloadIfMissing": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.3/CustomTypes.qmod" }, { - "version": "^0.4.51", + "version": "^0.4.55", "id": "bsml", - "downloadIfMissing": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.51/BSML.qmod" + "downloadIfMissing": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/BSML.qmod" }, { - "version": "^1.1.20", + "version": "^1.1.24", "id": "songcore", - "downloadIfMissing": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.20/SongCore.qmod" + "downloadIfMissing": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.24/SongCore.qmod" }, { - "version": "^4.6.1", + "version": "^4.6.4", "id": "paper2_scotland2", - "downloadIfMissing": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.2/paper2_scotland2.qmod" + "downloadIfMissing": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.4/paper2_scotland2.qmod" } ], - "modFiles": [ + "modFiles": [], + "lateModFiles": [ "libchatplex-sdk-bs.so" ], - "lateModFiles": [], "libraryFiles": [], "fileCopies": [], "copyExtensions": [] diff --git a/mod.template.json b/mod.template.json index 6eabf18..a4e3f1a 100644 --- a/mod.template.json +++ b/mod.template.json @@ -6,11 +6,12 @@ "author": "HardCPP", "version": "${version}", "packageId": "com.beatgames.beatsaber", - "packageVersion": "1.40.4_5283", + "packageVersion": "1.40.8_7379", "description": "ChatPlex BeatSaber modding SDK (Dependence for other mods)", "coverImage": "cover.png", "dependencies": [], - "modFiles": ["${binary}"], + "modFiles": [], + "lateModFiles": ["${binary}"], "libraryFiles": [], "fileCopies": [], "copyExtensions": [] diff --git a/qpm.json b/qpm.json index 88070e9..4b98143 100644 --- a/qpm.json +++ b/qpm.json @@ -5,7 +5,7 @@ "info": { "name": "ChatPlexSDK-BS", "id": "chatplex-sdk-bs", - "version": "6.4.0", + "version": "6.4.1", "url": "https://github.com/hardcpp/QuestChatPlexSDK-BS", "additionalData": { "overrideSoName": "libchatplex-sdk-bs.so", @@ -47,17 +47,17 @@ "dependencies": [ { "id": "beatsaber-hook", - "versionRange": "^6.4.1", + "versionRange": "^6.4.2", "additionalData": {} }, { "id": "bs-cordl", - "versionRange": "^4004.0.0", + "versionRange": "^4008.*", "additionalData": {} }, { "id": "custom-types", - "versionRange": "^0.18.2", + "versionRange": "^0.18.3", "additionalData": {} }, { @@ -70,19 +70,14 @@ }, { "id": "bsml", - "versionRange": "^0.4.51", + "versionRange": "^0.4.55", "additionalData": { "private": true } }, - { - "id": "libil2cpp", - "versionRange": "^0.4.0", - "additionalData": {} - }, { "id": "songcore", - "versionRange": "^1.1.20", + "versionRange": "^1.1.24", "additionalData": { "private": true } @@ -96,13 +91,18 @@ }, { "id": "paper2_scotland2", - "versionRange": "^4.6.1", + "versionRange": "^4.6.4", "additionalData": {} }, { "id": "kaleb", "versionRange": "^0.1.9", "additionalData": {} + }, + { + "id": "libcurl", + "versionRange": "=8.5.0", + "additionalData": {} } ] } \ No newline at end of file diff --git a/qpm.shared.json b/qpm.shared.json index 321cd6e..81aebe6 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -7,7 +7,7 @@ "info": { "name": "ChatPlexSDK-BS", "id": "chatplex-sdk-bs", - "version": "6.4.0", + "version": "6.4.1", "url": "https://github.com/hardcpp/QuestChatPlexSDK-BS", "additionalData": { "overrideSoName": "libchatplex-sdk-bs.so", @@ -49,17 +49,17 @@ "dependencies": [ { "id": "beatsaber-hook", - "versionRange": "^6.4.1", + "versionRange": "^6.4.2", "additionalData": {} }, { "id": "bs-cordl", - "versionRange": "^4004.0.0", + "versionRange": "4008.*", "additionalData": {} }, { "id": "custom-types", - "versionRange": "^0.18.2", + "versionRange": "^0.18.3", "additionalData": {} }, { @@ -72,19 +72,14 @@ }, { "id": "bsml", - "versionRange": "^0.4.51", + "versionRange": "^0.4.55", "additionalData": { "private": true } }, - { - "id": "libil2cpp", - "versionRange": "^0.4.0", - "additionalData": {} - }, { "id": "songcore", - "versionRange": "^1.1.20", + "versionRange": "^1.1.24", "additionalData": { "private": true } @@ -98,13 +93,18 @@ }, { "id": "paper2_scotland2", - "versionRange": "^4.6.1", + "versionRange": "^4.6.4", "additionalData": {} }, { "id": "kaleb", "versionRange": "^0.1.9", "additionalData": {} + }, + { + "id": "libcurl", + "versionRange": "=8.5.0", + "additionalData": {} } ] }, @@ -112,27 +112,27 @@ { "dependency": { "id": "bsml", - "versionRange": "=0.4.51", + "versionRange": "=0.4.55", "additionalData": { - "soLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.51/libbsml.so", - "debugSoLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.51/debug_libbsml.so", + "soLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/libbsml.so", + "debugSoLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/debug_libbsml.so", "overrideSoName": "libbsml.so", - "modLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.51/BSML.qmod", - "branchName": "version/v0_4_51", + "modLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/BSML.qmod", + "branchName": "version/v0_4_55", "cmake": true } }, - "version": "0.4.51" + "version": "0.4.55" }, { "dependency": { "id": "paper2_scotland2", - "versionRange": "=4.6.2", + "versionRange": "=4.6.4", "additionalData": { - "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.2/libpaper2_scotland2.so", + "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.4/libpaper2_scotland2.so", "overrideSoName": "libpaper2_scotland2.so", - "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.2/paper2_scotland2.qmod", - "branchName": "version/v4_6_2", + "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.4/paper2_scotland2.qmod", + "branchName": "version/v4_6_4", "compileOptions": { "systemIncludes": [ "shared/utfcpp/source" @@ -141,7 +141,7 @@ "cmake": false } }, - "version": "4.6.2" + "version": "4.6.4" }, { "dependency": { @@ -162,13 +162,13 @@ { "dependency": { "id": "custom-types", - "versionRange": "=0.18.2", + "versionRange": "=0.18.3", "additionalData": { - "soLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.2/libcustom-types.so", - "debugSoLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.2/debug_libcustom-types.so", + "soLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.3/libcustom-types.so", + "debugSoLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.3/debug_libcustom-types.so", "overrideSoName": "libcustom-types.so", - "modLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.2/CustomTypes.qmod", - "branchName": "version/v0_18_2", + "modLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.3/CustomTypes.qmod", + "branchName": "version/v0_18_3", "compileOptions": { "cppFlags": [ "-Wno-invalid-offsetof" @@ -177,7 +177,7 @@ "cmake": true } }, - "version": "0.18.2" + "version": "0.18.3" }, { "dependency": { @@ -199,10 +199,10 @@ { "dependency": { "id": "bs-cordl", - "versionRange": "=4004.0.0", + "versionRange": "=4008.0.0", "additionalData": { "headersOnly": true, - "branchName": "version/v4004_0_0", + "branchName": "version/v4008_0_0", "compileOptions": { "includePaths": [ "include" @@ -218,7 +218,7 @@ } } }, - "version": "4004.0.0" + "version": "4008.0.0" }, { "dependency": { @@ -232,35 +232,53 @@ }, "version": "0.3.0" }, + { + "dependency": { + "id": "libcurl", + "versionRange": "=8.5.0", + "additionalData": { + "staticLinking": true, + "soLink": "https://github.com/darknight1050/openssl-curl-android/releases/download/v8.5.0/libcurl.a", + "overrideSoName": "libcurl.a", + "branchName": "version-v8.5.0" + } + }, + "version": "8.5.0" + }, { "dependency": { "id": "songcore", - "versionRange": "=1.1.20", + "versionRange": "=1.1.24", "additionalData": { - "soLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.20/libsongcore.so", - "debugSoLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.20/debug_libsongcore.so", + "soLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.24/libsongcore.so", + "debugSoLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.24/debug_libsongcore.so", "overrideSoName": "libsongcore.so", - "modLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.20/SongCore.qmod", - "branchName": "version/v1_1_20", + "modLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.24/SongCore.qmod", + "branchName": "version/v1_1_24", "cmake": true } }, - "version": "1.1.20" + "version": "1.1.24" }, { "dependency": { "id": "beatsaber-hook", - "versionRange": "=6.4.1", + "versionRange": "=6.4.2", "additionalData": { - "soLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v6.4.1/libbeatsaber-hook.so", - "debugSoLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v6.4.1/debug_libbeatsaber-hook.so", + "soLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v6.4.2/libbeatsaber-hook.so", + "debugSoLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v6.4.2/debug_libbeatsaber-hook.so", "overrideSoName": "libbeatsaber-hook.so", - "modLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v6.4.1/beatsaber-hook.qmod", - "branchName": "version/v6_4_1", + "modLink": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v6.4.2/beatsaber-hook.qmod", + "branchName": "version/v6_4_2", + "compileOptions": { + "cppFlags": [ + "-Wno-extra-qualification" + ] + }, "cmake": true } }, - "version": "6.4.1" + "version": "6.4.2" }, { "dependency": { diff --git a/shared/CP_SDK/CPConfig.hpp b/shared/CP_SDK/CPConfig.hpp index 1cfd370..e1cf727 100644 --- a/shared/CP_SDK/CPConfig.hpp +++ b/shared/CP_SDK/CPConfig.hpp @@ -1,6 +1,7 @@ #pragma once #include "Config/JsonConfig.hpp" +#include namespace CP_SDK { @@ -10,8 +11,9 @@ namespace CP_SDK { CP_SDK_CONFIG_JSONCONFIG_INSTANCE_DECL(CPConfig); public: - bool FirstRun = true; - bool FirstChatServiceRun = true; + bool FirstRun = true; + bool FirstChatServiceRun = true; + std::u16string ChatPlexServiceToken = u""; protected: /// @brief Reset config to default diff --git a/shared/CP_SDK/ChatPlexService.hpp b/shared/CP_SDK/ChatPlexService.hpp new file mode 100644 index 0000000..cb6d11d --- /dev/null +++ b/shared/CP_SDK/ChatPlexService.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include "Network/JsonRPCResult.hpp" +#include "Network/JsonRPCClient.hpp" +#include "Utils/Event.hpp" +#include "Utils/Il2cpp.hpp" +#include "Utils/Delegate.hpp" + +#include +#include +#include +#include + +namespace CP_SDK { + + namespace _u + { + using namespace il2cpp_utils; + using namespace System; + using namespace UnityEngine; + } + + namespace _v + { + using namespace CP_SDK::Network; + using namespace CP_SDK::Utils; + } + + /// @brief ChatPlexService + class CP_SDK_EXPORT ChatPlexService + { + CP_SDK_NO_DEF_CTORS(ChatPlexService); + + public: + enum class EState + { + Disconnected, + LinkRequest, + LinkWait, + Connecting, + + Connected, + + Error + }; + + private: + static bool m_ThreadCondition; + static _u::il2cpp_aware_thread* m_Thread; + static EState m_State; + static _v::WebClientCore::Ptr m_WebClientCore; + static _v::JsonRPCClient::Ptr m_JsonRPCClient; + static std::u16string m_LinkRequestID; + static std::u16string m_LinkCode; + static std::u16string m_LastError; + static std::u16string m_ActiveSubscription; + static std::vector m_UnlockedFeatures; + static std::queue<_v::Action<>> m_OnTokenReadyQueue; + static std::mutex m_OnTokenReadyQueueMutex; + static std::u16string m_DeviceName; + + public: + static const EState State(); + static const std::u16string_view Token(); + static const std::u16string_view LinkCode(); + static const std::u16string_view LastError(); + static const std::u16string_view ActiveSubscription(); + static const std::vector& UnlockedFeatures(); + + static _v::Event StateChanged; + + public: + /// @brief Init the service + static void Init(); + /// @brief Release the service + static void Release(); + + public: + /// @brief Add a callback to be called when the token is ready (call immediatly if ready) + /// @param action Callback to be caled + static void OnTokenReady(_v::CActionRef<> action); + + public: + /// @brief Start linking procedure + static void StartLinking(); + /// @brief Stop linking procedure + static void StopLinking(); + /// @brief Refresh the session + static void Refresh(); + /// @brief Disconnect and erase the saved connected application token + static void Disconnect(); + + private: + /// @brief Change state and notify listenners + static void ChangeState(const EState newState); + /// @brief Fire on token ready actions + static void FireOnTokenReady(); + + private: + /// @brief Thread function + static void ThreadRunner(); + + private: + /// @brief Is RPC call result a success result? + /// @param rpcResult Result of the RPC command + /// @return True if success + static bool IsRPCSuccess(_v::JsonRPCResult::Ptr& rpcResult); + + private: + /// @brief When we are Authed + /// @param rpcResult Result of the RPC command + static void OnAuthed(_v::JsonRPCResult::Ptr& rpcResult); + /// @brief On error received + /// @param rpcResult Result of the RPC command + static void OnError(_v::JsonRPCResult::Ptr& rpcResult); + + }; + +} ///< namespace CP_SDK \ No newline at end of file diff --git a/shared/CP_SDK/Network/IWebClient.hpp b/shared/CP_SDK/Network/IWebClient.hpp index 29a9a28..67f7767 100644 --- a/shared/CP_SDK/Network/IWebClient.hpp +++ b/shared/CP_SDK/Network/IWebClient.hpp @@ -24,57 +24,67 @@ namespace CP_SDK::Network { { CP_SDK_NO_COPYMOVE_CTORS(IWebClient); + public: + using IPtr = std::shared_ptr; + protected: /// @brief Constructor IWebClient() = default; public: /// @brief Get header - /// @param p_Name Header name - virtual std::u16string GetHeader(std::u16string_view p_Name) = 0; + /// @param name Header name + virtual std::u16string GetHeader(std::u16string_view name) = 0; /// @brief Set header - /// @param p_Name Header name - /// @param p_Value Header value - virtual void SetHeader(std::u16string_view p_Name, std::u16string_view p_Value) = 0; + /// @param name Header name + /// @param value Header value + virtual void SetHeader(std::u16string_view name, std::u16string_view value) = 0; /// @brief Remove header - /// @param p_Name Header name - virtual void RemoveHeader(std::u16string_view p_Name) = 0; + /// @param name Header name + virtual void RemoveHeader(std::u16string_view name) = 0; public: /// @brief Do Async GET query - /// @param p_URL Target URL - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - /// @param p_Progress Progress reporter - virtual void GetAsync(std::u16string_view p_URL, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false, _v::CActionRef p_Progress = nullptr) = 0; + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + virtual void GetAsync(std::u16string_view url, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false, _v::CActionRef progress = nullptr) = 0; /// @brief Do Async GET query - /// @param p_URL Target URL - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - /// @param p_Progress Progress reporter - virtual void DownloadAsync(std::u16string_view p_URL, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false, _v::CActionRef p_Progress = nullptr) = 0; + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + virtual void DownloadAsync(std::u16string_view url, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false, _v::CActionRef progress = nullptr) = 0; /// @brief Do Async POST query - /// @param p_URL Target URL - /// @param p_Content Optional content to post - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - virtual void PostAsync(std::u16string_view p_URL, const WebContent::Ptr& p_Content, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false) = 0; + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void PostAsync(std::u16string_view url, const WebContent::Ptr& content, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) = 0; /// @brief Do Async PATCH query - /// @param p_URL Target URL - /// @param p_Content Optional content to post - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - virtual void PatchAsync(std::u16string_view p_URL, const WebContent::Ptr& p_Content, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false) = 0; + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void PatchAsync(std::u16string_view url, const WebContent::Ptr& content, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) = 0; + /// @brief Do Async PUT query + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void PutAsync(std::u16string_view url, const WebContent::Ptr& content, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) = 0; /// @brief Do Async DELETE query - /// @param p_URL Target URL - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - virtual void DeleteAsync(std::u16string_view p_URL, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false) = 0; + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void DeleteAsync(std::u16string_view url, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) = 0; }; diff --git a/shared/CP_SDK/Network/JsonRPCClient.hpp b/shared/CP_SDK/Network/JsonRPCClient.hpp new file mode 100644 index 0000000..c247a3b --- /dev/null +++ b/shared/CP_SDK/Network/JsonRPCClient.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include "CP_SDK/Utils/Json.hpp" +#include "JsonRPCResult.hpp" +#include "WebClientCore.hpp" +#include + +namespace CP_SDK::Network { + + namespace _v + { + using namespace CP_SDK::Utils; + } + + /// @brief JsonRPCClient + class CP_SDK_EXPORT JsonRPCClient : public std::enable_shared_from_this + { + CP_SDK_NO_COPYMOVE_CTORS(JsonRPCClient); + CP_SDK_PRIV_TAG(); + + private: + WebClientCore::Ptr m_WebClient; + + public: + using Ptr = std::shared_ptr; + + /// @brief Constructor + /// @param webClient Web client instance + JsonRPCClient(CP_SDK_PRIV_TAG_ARG(), WebClientCore::Ptr& webClient); + /// @brief Destructor + ~JsonRPCClient(); + + public: + /// @brief Create + /// @param webClient Raw web response + static Ptr Create(WebClientCore::Ptr& webClient); + + public: + /// @brief Do a RPC request + /// @param method Target method + /// @param parameters Request parameters + /// @param dontRetry Should not retry? + JsonRPCResult::Ptr Request(std::u16string_view method, std::shared_ptr<_v::Json::U16Value> parameters, bool dontRetry = false); + + public: + /// @brief Do a RPC request + /// @param method Target method + /// @param parameters Request parameters + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry? + void RequestAsync( + std::u16string_view method, + std::shared_ptr<_v::Json::U16Value> parameters, + _u::CancellationToken token, + _v::CActionRef callback, + bool dontRetry = false + ); + + private: + /// @brief Do a RPC request + /// @param method Target method + /// @param content Request content + /// @param dontRetry Should not retry? + JsonRPCResult::Ptr DoRequest( + std::u16string method, + std::shared_ptr<_v::Json::U16Document>& content, + bool dontRetry + ); + /// @brief Do a RPC request + /// @param method Target method + /// @param content Request content + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry? + void DoRequestAsync( + std::u16string method, + std::shared_ptr<_v::Json::U16Document>& content, + _u::CancellationToken token, + _v::CActionRef callback, + bool dontRetry + ); + /// @brief Do a RPC request + /// @param method Target method + /// @param rawResponse Web response + JsonRPCResult::Ptr HandleWebResponse(std::u16string method, WebResponse::Ptr& rawResponse); + + }; + +} ///< namespace CP_SDK::Network \ No newline at end of file diff --git a/shared/CP_SDK/Network/JsonRPCResult.hpp b/shared/CP_SDK/Network/JsonRPCResult.hpp new file mode 100644 index 0000000..190b321 --- /dev/null +++ b/shared/CP_SDK/Network/JsonRPCResult.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "../Utils/Json.hpp" +#include "WebResponse.hpp" + +namespace CP_SDK::Network { + + namespace _v + { + using namespace CP_SDK::Utils; + } + + /// @brief JsonRPCResult + class CP_SDK_EXPORT JsonRPCResult + { + CP_SDK_NO_COPYMOVE_CTORS(JsonRPCResult); + CP_SDK_PRIV_TAG(); + + public: + WebResponse::Ptr RawResponse; + std::shared_ptr<_v::Json::U16Document> Result; + std::shared_ptr<_v::Json::U16Document> Error; + + public: + using Ptr = std::shared_ptr; + + /// @brief Constructor + /// @param rawResponse Raw web response + /// @param result Result json + /// @param error Json content + JsonRPCResult( + CP_SDK_PRIV_TAG_ARG(), + WebResponse::Ptr& rawResponse, + std::shared_ptr<_v::Json::U16Document> result, + std::shared_ptr<_v::Json::U16Document> error + ); + /// @brief Destructor + ~JsonRPCResult(); + + public: + /// @brief Create + /// @param rawResponse Raw web response + /// @param result Result json + /// @param error Json content + static Ptr Create( + WebResponse::Ptr& rawResponse, + std::shared_ptr<_v::Json::U16Document> result, + std::shared_ptr<_v::Json::U16Document> error + ); + + }; + +} ///< namespace CP_SDK::Network \ No newline at end of file diff --git a/shared/CP_SDK/Network/WebClientCore.hpp b/shared/CP_SDK/Network/WebClientCore.hpp new file mode 100644 index 0000000..bbc2693 --- /dev/null +++ b/shared/CP_SDK/Network/WebClientCore.hpp @@ -0,0 +1,183 @@ +#pragma once + +#include "IWebClient.hpp" + +#include + +#include +#include + +namespace CP_SDK::Network { + + namespace _u + { + using namespace System; + using namespace System::Threading; + } + namespace _v + { + using namespace CP_SDK::Utils; + } + + /// @brief WebClientCore + class CP_SDK_EXPORT WebClientCore : public IWebClient, public std::enable_shared_from_this + { + CP_SDK_NO_COPYMOVE_CTORS(WebClientCore); + CP_SDK_PRIV_TAG(); + + public: + using Ptr = std::shared_ptr; + + private: + static Ptr m_GlobalClient; + + public: + /// @brief Global client instance + static WebClientCore* GlobalClient(); + + private: + std::u16string m_BaseAddress; + int m_TimeOut; + std::map m_Headers; + std::mutex m_HeadersLock; + + public: + /// @brief Maximum retry attempt + int MaxRetry; + /// @brief Delay between each retry + int RetryInterval; + + public: + /// @brief Constructor + /// @param baseAddress Base address + /// @param timeOut Requests timeout + /// @param keepAlive Should try to keep the connection alive + /// @param forceCacheDiscard Should force server cache discard + WebClientCore(CP_SDK_PRIV_TAG_ARG(), std::u16string_view baseAddress, _u::TimeSpan timeOut, bool keepAlive, bool forceCacheDiscard); + + /// @brief Constructor + /// @param baseAddress Base address + /// @param timeOut Requests timeout + /// @param keepAlive Should try to keep the connection alive + /// @param forceCacheDiscard Should force server cache discard + static Ptr Make(std::u16string_view baseAddress, _u::TimeSpan timeOut, bool keepAlive = true, bool forceCacheDiscard = false); + + public: + /// @brief Get header + /// @param p_Name Header name + virtual std::u16string GetHeader(std::u16string_view p_Name) override final; + /// @brief Set header + /// @param p_Name Header name + /// @param p_Value Header value + virtual void SetHeader(std::u16string_view p_Name, std::u16string_view p_Value) override final; + /// @brief Remove header + /// @param p_Name Header name + virtual void RemoveHeader(std::u16string_view p_Name) override final; + + public: + /// @brief Do GET query + /// @param url Target URL + /// @param dontRetry Should not retry + /// @param progress Progress reporter + WebResponse::Ptr Get(std::u16string_view url, bool dontRetry = false, _v::CActionRef progress = nullptr); + /// @brief Do GET query + /// @param url Target URL + /// @param dontRetry Should not retry + /// @param progress Progress reporter + WebResponse::Ptr Download(std::u16string_view url, bool dontRetry = false, _v::CActionRef progress = nullptr); + /// @brief Do POST query + /// @param url Target URL + /// @param content Optional content to post + /// @param dontRetry Should not retry + WebResponse::Ptr Post(std::u16string_view url, const WebContent::Ptr& content, bool dontRetry = false); + /// @brief Do PATCH query + /// @param url Target URL + /// @param content Optional content to post + /// @param dontRetry Should not retry + WebResponse::Ptr Patch(std::u16string_view url, const WebContent::Ptr& content, bool dontRetry = false); + /// @brief Do PUT query + /// @param url Target URL + /// @param content Optional content to post + /// @param dontRetry Should not retry + WebResponse::Ptr Put(std::u16string_view url, const WebContent::Ptr& content, bool dontRetry = false); + /// @brief Do DELETE query + /// @param url Target URL + /// @param dontRetry Should not retry + WebResponse::Ptr Delete(std::u16string_view url, bool dontRetry = false); + + public: + /// @brief Do Async GET query + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + virtual void GetAsync(std::u16string_view url, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false, _v::CActionRef progress = nullptr) override final; + /// @brief Do Async GET query + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + virtual void DownloadAsync(std::u16string_view url, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false, _v::CActionRef progress = nullptr) override final; + /// @brief Do Async POST query + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void PostAsync(std::u16string_view url, const WebContent::Ptr& content, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) override final; + /// @brief Do Async PATCH query + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void PatchAsync(std::u16string_view url, const WebContent::Ptr& content, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) override final; + /// @brief Do Async PUT query + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void PutAsync(std::u16string_view url, const WebContent::Ptr& content, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) override final; + /// @brief Do Async DELETE query + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void DeleteAsync(std::u16string_view url, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) override final; + + private: + /// @brief Get URL + /// @param url Request URL + std::u16string GetURL(std::u16string_view url); + /// @brief Safe URL parsing + /// @param url Source URL + std::u16string SafeURL(std::u16string_view url); + + private: + /// @brief Do request + /// @param debugName Method name for logs + /// @param httpMethod Http method + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + static void DoRequest( + Ptr self, + std::u16string debugName, + std::u16string httpMethod, + std::u16string url, + WebContent::Ptr content, + _u::CancellationToken token, + _v::Action callback, + bool dontRetry, + _v::Action progress + ); + + }; + +} ///< namespace CP_SDK::Network \ No newline at end of file diff --git a/shared/CP_SDK/Network/WebClientUnity.hpp b/shared/CP_SDK/Network/WebClientUnity.hpp index c06bb1e..fa1f7ab 100644 --- a/shared/CP_SDK/Network/WebClientUnity.hpp +++ b/shared/CP_SDK/Network/WebClientUnity.hpp @@ -53,96 +53,103 @@ namespace CP_SDK::Network { public: /// @brief Constructor - /// @param p_BaseAddress Base address - /// @param p_TimeOut Requests timeout - /// @param p_ForceCacheDiscard Should force server cache discard - WebClientUnity(CP_SDK_PRIV_TAG_ARG(), std::u16string_view p_BaseAddress, _u::TimeSpan p_TimeOut, bool p_ForceCacheDiscard); + /// @param baseAddress Base address + /// @param timeOut Requests timeout + /// @param forceCacheDiscard Should force server cache discard + WebClientUnity(CP_SDK_PRIV_TAG_ARG(), std::u16string_view baseAddress, _u::TimeSpan timeOut, bool forceCacheDiscard); /// @brief Constructor - /// @param p_BaseAddress Base address - /// @param p_TimeOut Requests timeout - /// @param p_ForceCacheDiscard Should force server cache discard - static Ptr Make(std::u16string_view p_BaseAddress, _u::TimeSpan p_TimeOut, bool p_ForceCacheDiscard = false); + /// @param baseAddress Base address + /// @param timeOut Requests timeout + /// @param forceCacheDiscard Should force server cache discard + static Ptr Make(std::u16string_view baseAddress, _u::TimeSpan timeOut, bool forceCacheDiscard = false); public: /// @brief Get header - /// @param p_Name Header name - virtual std::u16string GetHeader(std::u16string_view p_Name) override final; + /// @param name Header name + virtual std::u16string GetHeader(std::u16string_view name) override final; /// @brief Set header - /// @param p_Name Header name - /// @param p_Value Header value - virtual void SetHeader(std::u16string_view p_Name, std::u16string_view p_Value) override final; + /// @param name Header name + /// @param value Header value + virtual void SetHeader(std::u16string_view name, std::u16string_view value) override final; /// @brief Remove header - /// @param p_Name Header name - virtual void RemoveHeader(std::u16string_view p_Name) override final; + /// @param name Header name + virtual void RemoveHeader(std::u16string_view name) override final; public: /// @brief Do Async GET query - /// @param p_URL Target URL - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - /// @param p_Progress Progress reporter - virtual void GetAsync(std::u16string_view p_URL, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false, _v::CActionRef p_Progress = nullptr) override final; + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + virtual void GetAsync(std::u16string_view url, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false, _v::CActionRef progress = nullptr) override final; /// @brief Do Async GET query - /// @param p_URL Target URL - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - /// @param p_Progress Progress reporter - virtual void DownloadAsync(std::u16string_view p_URL, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false, _v::CActionRef p_Progress = nullptr) override final; + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + virtual void DownloadAsync(std::u16string_view url, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false, _v::CActionRef progress = nullptr) override final; /// @brief Do Async POST query - /// @param p_URL Target URL - /// @param p_Content Optional content to post - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - virtual void PostAsync(std::u16string_view p_URL, const WebContent::Ptr& p_Content, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false) override final; + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void PostAsync(std::u16string_view url, const WebContent::Ptr& content, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) override final; /// @brief Do Async PATCH query - /// @param p_URL Target URL - /// @param p_Content Optional content to post - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - virtual void PatchAsync(std::u16string_view p_URL, const WebContent::Ptr& p_Content, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false) override final; + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void PatchAsync(std::u16string_view url, const WebContent::Ptr& content, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) override final; + /// @brief Do Async PUT query + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void PutAsync(std::u16string_view url, const WebContent::Ptr& content, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) override final; /// @brief Do Async DELETE query - /// @param p_URL Target URL - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - virtual void DeleteAsync(std::u16string_view p_URL, _u::CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry = false) override final; + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + virtual void DeleteAsync(std::u16string_view url, _u::CancellationToken token, _v::CActionRef callback, bool dontRetry = false) override final; private: /// @brief Get URL - /// @param p_URL Request URL - std::u16string GetURL(std::u16string_view p_URL); + /// @param url Request URL + std::u16string GetURL(std::u16string_view url); /// @brief Safe URL parsing - /// @param p_URL Source URL - std::u16string SafeURL(std::u16string_view p_URL); + /// @param url Source URL + std::u16string SafeURL(std::u16string_view url); private: /// @brief Prepare request - /// @param p_Request Request to prepare - /// @param p_IsDownload Is a download request? - void PrepareRequest(_u::UnityWebRequest* p_Request, bool p_IsDownload); + /// @param request Request to prepare + /// @param isDownload Is a download request? + void PrepareRequest(_u::UnityWebRequest* request, bool isDownload); /// @brief Do request - /// @param p_DebugName Method name for logs - /// @param p_HttpMethod Http method - /// @param p_URL Target URL - /// @param p_Content Optional content to post - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - /// @param p_Progress Progress reporter - static custom_types::Helpers::Coroutine Coroutine_DoRequest(Ptr p_Self, - std::u16string p_DebugName, - std::u16string p_HttpMethod, - std::u16string p_URL, - WebContent::Ptr p_Content, - _u::CancellationToken p_Token, - _v::Action p_Callback, - bool p_DontRetry, - _v::Action p_Progress); + /// @param debugName Method name for logs + /// @param httpMethod Http method + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + static custom_types::Helpers::Coroutine Coroutine_DoRequest(Ptr self, + std::u16string debugName, + std::u16string httpMethod, + std::u16string url, + WebContent::Ptr content, + _u::CancellationToken token, + _v::Action callback, + bool dontRetry, + _v::Action progress); }; diff --git a/shared/CP_SDK/Network/WebContent.hpp b/shared/CP_SDK/Network/WebContent.hpp index 750da47..fb4e6ef 100644 --- a/shared/CP_SDK/Network/WebContent.hpp +++ b/shared/CP_SDK/Network/WebContent.hpp @@ -2,6 +2,7 @@ #include "../Utils/Il2cpp.hpp" #include "../Utils/MonoPtr.hpp" +#include "../Utils/Json.hpp" #include #include @@ -37,8 +38,11 @@ namespace CP_SDK::Network { public: /// @brief Constructor from Json - /// @param p_Content Json content - static Ptr FromJson(std::u16string_view p_Content); + /// @param content Json content + static Ptr FromJson(std::u16string_view content); + /// @brief Constructor from Json + /// @param content Json content + static Ptr FromJson(std::shared_ptr<_v::Json::U16Document>& content); }; diff --git a/shared/CP_SDK/Network/WebResponse.hpp b/shared/CP_SDK/Network/WebResponse.hpp index c89ba58..cc3c140 100644 --- a/shared/CP_SDK/Network/WebResponse.hpp +++ b/shared/CP_SDK/Network/WebResponse.hpp @@ -9,6 +9,7 @@ #include #include +#include namespace CP_SDK::Network { @@ -60,16 +61,21 @@ namespace CP_SDK::Network { using Ptr = std::shared_ptr; /// Constructor - /// @p_Request: Reply status - WebResponse(_u::UnityWebRequest* p_Request); + /// @param request: Reply status + WebResponse(_u::UnityWebRequest* request); + /// @brief Constructor + /// @param curlPerformResult CURL perform result + /// @param curlInstance CURL instance + /// @param data Response data + WebResponse(long curlPerformResult, void* curlInstance, std::vector* data); public: /// @brief Get JObject from serialized JSON - /// @param p_Object Result object + /// @param object Result object /// @return True or false - bool TryAsJObject(std::shared_ptr<_v::Json::U16Document>& p_Object) + bool TryAsJObject(std::shared_ptr<_v::Json::U16Document>& object) { - p_Object = nullptr; + object = nullptr; auto l_New = std::make_shared<_v::Json::U16Document>(); try @@ -77,40 +83,40 @@ namespace CP_SDK::Network { if (!_v::Json::TryFromU16String(BodyString(), *l_New.get())) return false; - p_Object = l_New; + object = l_New; } catch (const std::exception& l_Exception) { - p_Object = nullptr; + object = nullptr; return false; } - return p_Object != nullptr; + return object != nullptr; } /// @brief Get Object from serialized JSON /// @tparam t_Type Object type - /// @param p_Object Result object + /// @param object Result object /// @return True or false template - bool TryGetObject(std::shared_ptr& p_Object) + bool TryGetObject(std::shared_ptr& object) { - p_Object = nullptr; + object = nullptr; try { _v::Json::U16Document l_Document; _v::Json::TryFromU16String(BodyString(), l_Document); - p_Object = std::make_shared(); - p_Object->Unserialize(l_Document); + object = std::make_shared(); + object->Unserialize(l_Document); } catch (const std::exception& l_Exception) { - p_Object = nullptr; + object = nullptr; return false; } - return p_Object != nullptr; + return object != nullptr; } }; diff --git a/shared/CP_SDK/UI/Views/SettingsLeftView.hpp b/shared/CP_SDK/UI/Views/SettingsLeftView.hpp index 6eb9c7d..0a16836 100644 --- a/shared/CP_SDK/UI/Views/SettingsLeftView.hpp +++ b/shared/CP_SDK/UI/Views/SettingsLeftView.hpp @@ -1,7 +1,8 @@ #pragma once -#include "../ViewController.hpp" +#include "../../ChatPlexService.hpp" #include "../../XUI/XUI.hpp" +#include "../ViewController.hpp" namespace CP_SDK::UI::Views { @@ -23,9 +24,34 @@ namespace CP_SDK::UI::Views { CP_SDK_IL2CPP_DECLARE_DTOR_MONOBEHAVIOUR_CHILD(SettingsLeftView); CP_SDK_UI_VIEW_CONTROLLER_INSTANCE(); + private: + _v::XUIText::Ptr m_StatusText; + _v::XUIText::Ptr m_SubscriptionText; + _v::XUIPrimaryButton::Ptr m_PrimaryButton; + _v::XUISecondaryButton::Ptr m_SecondaryButton; + + private: + bool m_IsLinking = false; + private: /// @brief On view creation void OnViewCreation_Impl(); + /// @brief On view deactivation + void OnViewDeactivation_Impl(); + + private: + /// @brief On primary button pressed + void OnPrimaryButtonPressed(); + /// @brief On secondary button pressed + void OnSecondaryButtonPressed(); + /// @brief On Loading cancel + void OnLoadingCancel(); + + private: + /// @brief On ChatPlex service state change + /// @param oldState Old state + /// @param newState New state + void ChatPlexService_StateChanged(ChatPlexService::EState oldState, ChatPlexService::EState newState); }; diff --git a/shared/CP_SDK/Utils/Il2cpp.hpp b/shared/CP_SDK/Utils/Il2cpp.hpp index a148ed4..54e5ad6 100644 --- a/shared/CP_SDK/Utils/Il2cpp.hpp +++ b/shared/CP_SDK/Utils/Il2cpp.hpp @@ -3,18 +3,12 @@ #define __CP_SDK_U16STR(__mX) u##__mX #define CP_SDK_U16STR(__mX) __CP_SDK_U16STR(#__mX) -#include "../Logging/PaperLogger.hpp" #include "Internals/Il2cpp_enum.hpp" #include "Internals/Il2cpp_customtype.hpp" #include "Internals/Il2cpp_hook.hpp" #include "Internals/Il2cpp_string.hpp" #include -#include -#include -#include -#include -#include #include #include diff --git a/shared/CP_SDK/Utils/Json.hpp b/shared/CP_SDK/Utils/Json.hpp index 17ba1b1..bbea0d8 100644 --- a/shared/CP_SDK/Utils/Json.hpp +++ b/shared/CP_SDK/Utils/Json.hpp @@ -1,6 +1,8 @@ #pragma once #include "Il2cpp.hpp" +#include +#include "beatsaber-hook/shared/rapidjson/include/rapidjson/document.h" #include #include @@ -8,7 +10,6 @@ #include #include -#include #include #include #include diff --git a/src/CP_SDK/CPConfig.cpp b/src/CP_SDK/CPConfig.cpp index 7b46702..7eb208f 100644 --- a/src/CP_SDK/CPConfig.cpp +++ b/src/CP_SDK/CPConfig.cpp @@ -23,6 +23,7 @@ namespace CP_SDK { { CP_SDK_JSON_SERIALIZE_BOOL(FirstRun); CP_SDK_JSON_SERIALIZE_BOOL(FirstChatServiceRun); + CP_SDK_JSON_SERIALIZE_STRING(ChatPlexServiceToken); } /// @brief Read the document /// @param p_Document Source @@ -30,6 +31,7 @@ namespace CP_SDK { { CP_SDK_JSON_UNSERIALIZE_BOOL(FirstRun); CP_SDK_JSON_UNSERIALIZE_BOOL(FirstChatServiceRun); + CP_SDK_JSON_UNSERIALIZE_STRING(ChatPlexServiceToken); } //////////////////////////////////////////////////////////////////////////// diff --git a/src/CP_SDK/ChatPlexSDK.cpp b/src/CP_SDK/ChatPlexSDK.cpp index 6baed2f..ba55df2 100644 --- a/src/CP_SDK/ChatPlexSDK.cpp +++ b/src/CP_SDK/ChatPlexSDK.cpp @@ -9,6 +9,7 @@ #include "CP_SDK/Unity/MTMainThreadInvoker.hpp" #include "CP_SDK/Unity/MTThreadInvoker.hpp" #include "CP_SDK/ModuleBase.hpp" +#include "CP_SDK/ChatPlexService.hpp" #include @@ -51,7 +52,6 @@ namespace CP_SDK { { InstallWEBPCodecs(); - /// Init config Chat::Service::Init(); } /// @brief On assembly exit @@ -87,6 +87,8 @@ namespace CP_SDK { /// Init UI UI::UISystem::Init(); + + ChatPlexService::Init(); } catch (const std::exception& p_Exception) { @@ -102,6 +104,8 @@ namespace CP_SDK { OnGenericSceneChange.Clear(); OnGenericMenuSceneLoaded.Clear(); + ChatPlexService::Release(); + UI::UISystem::Destroy(); UI::LoadingProgressBar::Destroy(); diff --git a/src/CP_SDK/ChatPlexService.cpp b/src/CP_SDK/ChatPlexService.cpp new file mode 100644 index 0000000..f3191a9 --- /dev/null +++ b/src/CP_SDK/ChatPlexService.cpp @@ -0,0 +1,318 @@ +#include "CP_SDK/Unity/MTThreadInvoker.hpp" +#include "CP_SDK/ChatPlexService.hpp" +#include "CP_SDK/CPConfig.hpp" + +#include +#include +#include +#include +#include + +static std::u16string s_EmptyU16String = u""; + +namespace CP_SDK { + + bool ChatPlexService::m_ThreadCondition = true; + _u::il2cpp_aware_thread* ChatPlexService::m_Thread = nullptr; + ChatPlexService::EState ChatPlexService::m_State = ChatPlexService::EState::Disconnected; + _v::WebClientCore::Ptr ChatPlexService::m_WebClientCore = nullptr; + _v::JsonRPCClient::Ptr ChatPlexService::m_JsonRPCClient = nullptr; + std::u16string ChatPlexService::m_LinkRequestID = u""; + std::u16string ChatPlexService::m_LinkCode = u""; + std::u16string ChatPlexService::m_LastError = u""; + std::u16string ChatPlexService::m_ActiveSubscription = u""; + std::vector ChatPlexService::m_UnlockedFeatures; + std::queue<_v::Action<>> ChatPlexService::m_OnTokenReadyQueue; + std::mutex ChatPlexService::m_OnTokenReadyQueueMutex; + std::u16string ChatPlexService::m_DeviceName = u""; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + const ChatPlexService::EState ChatPlexService::State() + { + return m_State; + } + const std::u16string_view ChatPlexService::Token() + { + return CPConfig::Instance()->ChatPlexServiceToken; + } + const std::u16string_view ChatPlexService::LinkCode() + { + return m_LinkCode; + } + const std::u16string_view ChatPlexService::LastError() + { + return m_LastError; + } + const std::u16string_view ChatPlexService::ActiveSubscription() + { + return m_State == EState::Connected ? m_ActiveSubscription : s_EmptyU16String; + } + const std::vector& ChatPlexService::UnlockedFeatures() + { + return *reinterpret_cast*>(&m_UnlockedFeatures); + } + + _v::Event ChatPlexService::StateChanged; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Init the service + void ChatPlexService::Init() + { + m_WebClientCore = _v::WebClientCore::Make(u"https://api.chatplex.org/", _u::TimeSpan::FromSeconds(10), false, true); + m_JsonRPCClient = _v::JsonRPCClient::Create(m_WebClientCore); + + m_Thread = new _u::il2cpp_aware_thread(&ThreadRunner); + + m_DeviceName = _u::SystemInfo::GetDeviceName(); + } + /// @brief Release the service + void ChatPlexService::Release() + { + m_ThreadCondition = false; + m_Thread->join(); + + delete m_Thread; + m_Thread = nullptr; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Add a callback to be called when the token is ready (call immediatly if ready) + /// @param action Callback to be caled + void ChatPlexService::OnTokenReady(_v::CActionRef<> action) + { + if (m_State == EState::Connected) + action(); + else + { + std::lock_guard l_Lock(m_OnTokenReadyQueueMutex); + m_OnTokenReadyQueue.push(action); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Start linking procedure + void ChatPlexService::StartLinking() + { + if (m_State != EState::Disconnected) + return; + + ChangeState(EState::LinkRequest); + } + /// @brief Stop linking procedure + void ChatPlexService::StopLinking() + { + if (m_State != EState::LinkRequest && m_State != EState::LinkWait) + return; + + ChangeState(EState::Disconnected); + } + /// @brief Refresh the session + void ChatPlexService::Refresh() + { + ChangeState(EState::Disconnected); + } + /// @brief Disconnect and erase the saved connected application token + void ChatPlexService::Disconnect() + { + CPConfig::Instance()->ChatPlexServiceToken = u""; + CPConfig::Instance()->Save(); + + ChangeState(EState::Disconnected); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Change state and notify listenners + void ChatPlexService::ChangeState(const EState newState) + { + auto l_OldState = m_State; + m_State = newState; + + Unity::MTThreadInvoker::EnqueueOnThread([=]() -> void { StateChanged(l_OldState, newState); }); + } + /// @brief Fire on token ready actions + void ChatPlexService::FireOnTokenReady() + { + m_OnTokenReadyQueueMutex.lock(); + while (!m_OnTokenReadyQueue.empty()) + { + auto l_Action = m_OnTokenReadyQueue.front(); + m_OnTokenReadyQueue.pop(); + m_OnTokenReadyQueueMutex.unlock(); + try + { + l_Action(); + } + catch (std::exception l_Exception) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK][ChatPlexService.FireOnTokenReady] Error:"); + ChatPlexSDK::Logger()->Error(l_Exception); + } + m_OnTokenReadyQueueMutex.lock(); + } + + m_OnTokenReadyQueueMutex.unlock(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Thread function + void ChatPlexService::ThreadRunner() + { + while (m_ThreadCondition) + { + if (m_State == EState::Disconnected && !CPConfig::Instance()->ChatPlexServiceToken.empty()) + { + m_WebClientCore->RemoveHeader(u"Authorization"); + + ChangeState(EState::Connecting); + + std::shared_ptr<_v::Json::U16Document> l_Content(new _v::Json::U16Document()); + l_Content->SetObject(); + l_Content->AddMember(u"ConnectedApplicationToken", _v::Json::U16Value(CPConfig::Instance()->ChatPlexServiceToken, l_Content->GetAllocator()), l_Content->GetAllocator()); + + auto l_Result = m_JsonRPCClient->Request( + u"Account_AuthByConnectedApplicationToken", + l_Content + ); + + if (IsRPCSuccess(l_Result)) + OnAuthed(l_Result); + else + { + if (l_Result->Result != nullptr && l_Result->Result->HasMember(u"Result") && (*l_Result->Result)[u"Result"].GetBool() == false) + { + CPConfig::Instance()->ChatPlexServiceToken = u""; + CPConfig::Instance()->Save(); + + ChangeState(EState::Disconnected); + } + else + OnError(l_Result); + } + } + else if (m_State == EState::LinkRequest) + { + std::shared_ptr<_v::Json::U16Document> l_Content(new _v::Json::U16Document()); + l_Content->SetObject(); + l_Content->AddMember(u"ApplicationIdentifier", _v::Json::U16Value(ChatPlexSDK::ProductName().data(), l_Content->GetAllocator()), l_Content->GetAllocator()); + l_Content->AddMember(u"ApplicationDeviceName", _v::Json::U16Value(m_DeviceName, l_Content->GetAllocator()), l_Content->GetAllocator()); + + auto l_Result = m_JsonRPCClient->Request( + u"ConnectedApplication_CreateLinkRequest", + l_Content + ); + + if (IsRPCSuccess(l_Result)) + { + auto& l_ResultR = *l_Result->Result.get(); + m_LinkRequestID = l_ResultR[u"RequestID"].GetString(); + m_LinkCode = l_ResultR[u"Code"].GetString(); + + ChangeState(EState::LinkWait); + } + else + OnError(l_Result); + } + else if (m_State == EState::LinkWait) + { + std::shared_ptr<_v::Json::U16Document> l_Content(new _v::Json::U16Document()); + l_Content->SetObject(); + l_Content->AddMember(u"RequestID", _v::Json::U16Value(m_LinkRequestID, l_Content->GetAllocator()), l_Content->GetAllocator()); + + auto l_Result = m_JsonRPCClient->Request( + u"ConnectedApplication_GetLinkRequestStatus", + l_Content + ); + + if (IsRPCSuccess(l_Result)) + { + auto& l_ResultR = *l_Result->Result.get(); + if (l_ResultR[u"ResultToken"].IsString()) + { + CPConfig::Instance()->ChatPlexServiceToken = l_ResultR[u"ResultToken"].GetString(); + CPConfig::Instance()->Save(); + + ChangeState(EState::Disconnected); + } + } + else + OnError(l_Result); + } + + if (m_State == EState::LinkWait) + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + else + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Is RPC call result a success result? + /// @param rpcResult Result of the RPC command + /// @return True if success + bool ChatPlexService::IsRPCSuccess(_v::JsonRPCResult::Ptr& rpcResult) + { + if (rpcResult->Result == nullptr) + return false; + + auto& l_ResultR = *rpcResult->Result.get(); + return l_ResultR.HasMember(u"Result") && l_ResultR[u"Result"].GetBool() == true; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief When we are Authed + /// @param rpcResult Result of the RPC command + void ChatPlexService::OnAuthed(_v::JsonRPCResult::Ptr& rpcResult) + { + auto& l_ResultR = *rpcResult->Result.get(); + m_ActiveSubscription = l_ResultR.HasMember(u"ActiveSubscription") ? l_ResultR[u"ActiveSubscription"].GetString() : u""; + + m_UnlockedFeatures.clear(); + if (l_ResultR.HasMember(u"UnlockedFeatures") && l_ResultR[u"UnlockedFeatures"].IsArray()) + { + auto l_Array = l_ResultR[u"UnlockedFeatures"].GetArray(); + for (auto& l_CurrentValue : l_Array) + { + if (!l_CurrentValue.IsString()) + continue; + + m_UnlockedFeatures.push_back(l_CurrentValue.GetString()); + } + } + + m_WebClientCore->SetHeader(u"Authorization", std::u16string(u"ConnectedApplicationToken ") + CPConfig::Instance()->ChatPlexServiceToken); + + ChangeState(EState::Connected); + FireOnTokenReady(); + } + /// @brief On error received + /// @param rpcResult Result of the RPC command + void ChatPlexService::OnError(_v::JsonRPCResult::Ptr& rpcResult) + { + std::u16string l_Error = u"Unknow server error!"; + if (rpcResult->Result != nullptr && rpcResult->Result->HasMember(u"Error")) + { + auto& l_ResultR = *rpcResult->Result.get(); + l_Error = l_ResultR[u"Error"].GetString(); + } + + m_LastError = l_Error; + ChangeState(EState::Error); + } + +} ///< namespace CP_SDK \ No newline at end of file diff --git a/src/CP_SDK/Network/JsonRPCClient.cpp b/src/CP_SDK/Network/JsonRPCClient.cpp new file mode 100644 index 0000000..a3b8813 --- /dev/null +++ b/src/CP_SDK/Network/JsonRPCClient.cpp @@ -0,0 +1,190 @@ +#include "CP_SDK/Network/JsonRPCClient.hpp" +#include "CP_SDK/Utils/Json.hpp" +#include "beatsaber-hook/shared/rapidjson/include/rapidjson/rapidjson.h" + +#include +#include + +using namespace System::Text; + +namespace CP_SDK::Network { + + /// @brief Constructor + /// @param webClient Web client instance + JsonRPCClient::JsonRPCClient(CP_SDK_PRIV_TAG_ARG(), WebClientCore::Ptr& webClient) + { + m_WebClient = webClient; + } + /// @brief Destructor + JsonRPCClient::~JsonRPCClient() + { + m_WebClient = nullptr; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Create + /// @param webClient Web client instance + JsonRPCClient::Ptr JsonRPCClient::Create(WebClientCore::Ptr& webClient) + { + return std::make_shared(CP_SDK_PRIV_TAG_VAL(), webClient); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Do a RPC request + /// @param method Target method + /// @param parameters Request parameters + /// @param dontRetry Should not retry? + JsonRPCResult::Ptr JsonRPCClient::Request(std::u16string_view method, std::shared_ptr<_v::Json::U16Value> parameters, bool dontRetry) + { + auto l_Method = std::u16string(method); + + std::shared_ptr<_v::Json::U16Document> l_Content(new _v::Json::U16Document()); + l_Content->SetObject(); + l_Content->AddMember(u"id", _v::Json::U16Value(1), l_Content->GetAllocator()); + l_Content->AddMember(u"jsonrpc", _v::Json::U16Value(u"2.0", l_Content->GetAllocator()), l_Content->GetAllocator()); + l_Content->AddMember(u"method", _v::Json::U16Value(l_Method.c_str(), l_Method.length(), l_Content->GetAllocator()), l_Content->GetAllocator()); + + if (parameters) + { + _v::Json::U16Value l_Copy; + l_Copy.CopyFrom(*parameters, l_Content->GetAllocator()); + + l_Content->AddMember(u"params", l_Copy, l_Content->GetAllocator()); + } + else + l_Content->AddMember(u"params", _v::Json::U16Value(rapidjson::kNullType), l_Content->GetAllocator()); + + return DoRequest(l_Method, l_Content, dontRetry); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Do a RPC request + /// @param method Target method + /// @param parameters Request parameters + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry? + void JsonRPCClient::RequestAsync( + std::u16string_view method, + std::shared_ptr<_v::Json::U16Value> parameters, + _u::CancellationToken token, + _v::CActionRef callback, + bool dontRetry + ) + { + auto l_Method = std::u16string(method); + + std::shared_ptr<_v::Json::U16Document> l_Content(new _v::Json::U16Document()); + l_Content->SetObject(); + l_Content->AddMember(u"id", _v::Json::U16Value(1), l_Content->GetAllocator()); + l_Content->AddMember(u"jsonrpc", _v::Json::U16Value(u"2.0", l_Content->GetAllocator()), l_Content->GetAllocator()); + l_Content->AddMember(u"method", _v::Json::U16Value(l_Method.c_str(), l_Method.length(), l_Content->GetAllocator()), l_Content->GetAllocator()); + + if (parameters) + { + _v::Json::U16Value l_Copy; + l_Copy.CopyFrom(*parameters, l_Content->GetAllocator()); + + l_Content->AddMember(u"params", l_Copy, l_Content->GetAllocator()); + } + else + l_Content->AddMember(u"params", _v::Json::U16Value(rapidjson::kNullType), l_Content->GetAllocator()); + + DoRequestAsync(l_Method, l_Content, token, callback, dontRetry); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Do a RPC request + /// @param method Target method + /// @param parameters Request content + /// @param dontRetry Should not retry? + JsonRPCResult::Ptr JsonRPCClient::DoRequest( + std::u16string method, + std::shared_ptr<_v::Json::U16Document>& content, + bool dontRetry + ) + { + auto l_Response = m_WebClient->Post( + u"", + WebContent::FromJson(content), + dontRetry + ); + return HandleWebResponse(method, l_Response); + } + /// @brief Do a RPC request + /// @param method Target method + /// @param content Request content + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry? + void JsonRPCClient::DoRequestAsync( + std::u16string method, + std::shared_ptr<_v::Json::U16Document>& content, + _u::CancellationToken token, + _v::CActionRef callback, + bool dontRetry + ) + { + auto l_Self = shared_from_this(); + m_WebClient->PostAsync( + u"", + WebContent::FromJson(content), + token, + [=](CP_SDK::Network::WebResponse::Ptr webResponse) -> void { + callback(l_Self->HandleWebResponse(method, webResponse)); + }, + dontRetry + ); + } + /// @brief Do a RPC request + /// @param method Target method + /// @param rawResponse Web response + JsonRPCResult::Ptr JsonRPCClient::HandleWebResponse(std::u16string method, WebResponse::Ptr& rawResponse) + { + if (!rawResponse) + return JsonRPCResult::Create(rawResponse, nullptr, nullptr); + + try + { + auto l_JsonResult = std::make_shared<_v::Json::U16Document>(); + if (!_v::Json::TryFromU16String(rawResponse->BodyString(), *l_JsonResult.get())) + return nullptr; + + auto l_ResultDocument = std::shared_ptr<_v::Json::U16Document>(nullptr); + if (l_JsonResult->HasMember(u"result")) + { + l_ResultDocument = std::make_shared<_v::Json::U16Document>(); + l_ResultDocument->CopyFrom(l_JsonResult->FindMember(u"result")->value, l_ResultDocument->GetAllocator()); + } + + auto l_ErrorDocument = std::shared_ptr<_v::Json::U16Document>(nullptr); + if (l_JsonResult->HasMember(u"error")) + { + l_ErrorDocument = std::make_shared<_v::Json::U16Document>(); + l_ErrorDocument->CopyFrom(l_JsonResult->FindMember(u"error")->value, l_ErrorDocument->GetAllocator()); + } + + return JsonRPCResult::Create( + rawResponse, + l_ResultDocument, + l_ErrorDocument + ); + } + catch (std::exception& l_Exception) + { + ChatPlexSDK::Logger()->Error(u"[CP_API_SDK.Network][JsonRPCClient.HandleResponse] Request " + method + u" failed parsing response:"); + ChatPlexSDK::Logger()->Error(l_Exception); + } + + return nullptr; + } + +} ///< namespace CP_SDK::Network \ No newline at end of file diff --git a/src/CP_SDK/Network/JsonRPCResult.cpp b/src/CP_SDK/Network/JsonRPCResult.cpp new file mode 100644 index 0000000..16c4656 --- /dev/null +++ b/src/CP_SDK/Network/JsonRPCResult.cpp @@ -0,0 +1,49 @@ +#include "CP_SDK/Network/JsonRPCResult.hpp" + +#include +#include + +using namespace System::Text; + +namespace CP_SDK::Network { + + /// @brief Constructor + /// @param rawResponse Raw web response + /// @param result Result json + /// @param error Json content + JsonRPCResult::JsonRPCResult( + CP_SDK_PRIV_TAG_ARG(), + WebResponse::Ptr& rawResponse, + std::shared_ptr<_v::Json::U16Document> result, + std::shared_ptr<_v::Json::U16Document> error + ) + { + RawResponse = rawResponse; + Result = result; + Error = error; + } + /// @brief Destructor + JsonRPCResult::~JsonRPCResult() + { + RawResponse = nullptr; + Result = nullptr; + Error = nullptr; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Create + /// @param rawResponse Raw web response + /// @param result Result json + /// @param error Json content + JsonRPCResult::Ptr JsonRPCResult::Create( + WebResponse::Ptr& rawResponse, + std::shared_ptr<_v::Json::U16Document> result, + std::shared_ptr<_v::Json::U16Document> error + ) + { + return std::make_shared(CP_SDK_PRIV_TAG_VAL(), rawResponse, result, error); + } + +} ///< namespace CP_SDK::Network \ No newline at end of file diff --git a/src/CP_SDK/Network/WebClientCore.cpp b/src/CP_SDK/Network/WebClientCore.cpp new file mode 100644 index 0000000..d5eb88b --- /dev/null +++ b/src/CP_SDK/Network/WebClientCore.cpp @@ -0,0 +1,594 @@ +#include "CP_SDK/Network/WebClientCore.hpp" +#include "CP_SDK/Unity/MTThreadInvoker.hpp" +#include "CP_SDK/ChatPlexSDK.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace System; +using namespace System::Net; +using namespace System::Threading; + +struct ScopedCURL +{ + CURL* Instance; + struct curl_slist* Headers = NULL; + std::vector* Data; + + ScopedCURL() + { + Instance = curl_easy_init(); + Data = new std::vector(); + } + ~ScopedCURL() + { + curl_easy_cleanup(Instance); + if (Headers != NULL) + curl_slist_free_all(Headers); + + delete Data; + } +}; + +namespace CP_SDK::Network { + + WebClientCore::Ptr WebClientCore::m_GlobalClient; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Global client instance + WebClientCore* WebClientCore::GlobalClient() + { + if (!m_GlobalClient) + m_GlobalClient = WebClientCore::Make(u"", TimeSpan::FromSeconds(10), true); + + return m_GlobalClient.get(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Constructor + /// @param baseAddress Base address + /// @param timeOut Requests timeout + /// @param forceCacheDiscard Should force server cache discard + WebClientCore::WebClientCore(CP_SDK_PRIV_TAG_ARG(), std::u16string_view baseAddress, TimeSpan timeOut, bool keepAlive, bool forceCacheDiscard) + : m_Headers({}) + { + MaxRetry = 2; + RetryInterval = 5; + + m_BaseAddress = baseAddress; + + m_TimeOut = (int)timeOut.get_TotalSeconds(); + + if (forceCacheDiscard) + m_Headers[u"Cache-Control"] = u"no-cache, must-revalidate, proxy-revalidate, max-age=0, s-maxage=0, max-stale=0"; + } + + /// @brief Constructor + /// @param baseAddress Base address + /// @param timeOut Requests timeout + /// @param forceCacheDiscard Should force server cache discard + WebClientCore::Ptr WebClientCore::Make(std::u16string_view baseAddress, TimeSpan timeOut, bool keepAlive, bool forceCacheDiscard) + { + return std::make_shared(CP_SDK_PRIV_TAG_VAL(), baseAddress, timeOut, keepAlive, forceCacheDiscard); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Get header + /// @param name Header name + std::u16string WebClientCore::GetHeader(std::u16string_view name) + { + auto l_Name = std::u16string(name); + + std::lock_guard l_Guard(m_HeadersLock); + if (!m_Headers.contains(l_Name)) + return u""; + + return m_Headers[l_Name]; + } + /// @brief Set header + /// @param name Header name + /// @param value Header value + void WebClientCore::SetHeader(std::u16string_view name, std::u16string_view value) + { + auto l_Name = std::u16string(name); + + std::lock_guard l_Guard(m_HeadersLock); + m_Headers[l_Name] = std::u16string(value); + } + /// @brief Remove header + /// @param name Header name + void WebClientCore::RemoveHeader(std::u16string_view name) + { + auto l_Name = std::u16string(name); + + std::lock_guard l_Guard(m_HeadersLock); + auto l_It = m_Headers.find(l_Name); + if (l_It != m_Headers.end()) + m_Headers.erase(l_It); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Do GET query + /// @param url Target URL + /// @param dontRetry Should not retry + /// @param progress Progress reporter + WebResponse::Ptr WebClientCore::Get(std::u16string_view url, bool dontRetry, _v::CActionRef progress) + { + auto l_IsDone = false; + auto l_Reply = WebResponse::Ptr(nullptr); + + DoRequest( + shared_from_this(), + u"Get", + u"GET", + GetURL(url), + nullptr, + CancellationToken::get_None(), + [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, + dontRetry, + progress + ); + + while (!l_IsDone) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + return l_Reply; + } + /// @brief Do GET query + /// @param url Target URL + /// @param dontRetry Should not retry + /// @param progress Progress reporter + WebResponse::Ptr WebClientCore::Download(std::u16string_view url, bool dontRetry, _v::CActionRef progress) + { + auto l_IsDone = false; + auto l_Reply = WebResponse::Ptr(nullptr); + + DoRequest( + shared_from_this(), + u"Download", + u"DOWNLOAD", + GetURL(url), + nullptr, + CancellationToken::get_None(), + [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, + dontRetry, + progress + ); + + while (!l_IsDone) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + return l_Reply; + } + /// @brief Do POST query + /// @param url Target URL + /// @param content Optional content to post + /// @param dontRetry Should not retry + WebResponse::Ptr WebClientCore::Post(std::u16string_view url, const WebContent::Ptr& content, bool dontRetry) + { + auto l_IsDone = false; + auto l_Reply = WebResponse::Ptr(nullptr); + + DoRequest( + shared_from_this(), + u"Post", + u"POST", + GetURL(url), + content, + CancellationToken::get_None(), + [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, + dontRetry, + nullptr + ); + + while (!l_IsDone) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + return l_Reply; + } + /// @brief Do PATCH query + /// @param url Target URL + /// @param content Optional content to post + /// @param dontRetry Should not retry + WebResponse::Ptr WebClientCore::Patch(std::u16string_view url, const WebContent::Ptr& content, bool dontRetry) + { + auto l_IsDone = false; + auto l_Reply = WebResponse::Ptr(nullptr); + + DoRequest( + shared_from_this(), + u"Patch", + u"PATCH", + GetURL(url), + content, + CancellationToken::get_None(), + [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, + dontRetry, + nullptr + ); + + while (!l_IsDone) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + return l_Reply; + } + /// @brief Do PUT query + /// @param url Target URL + /// @param content Optional content to post + /// @param dontRetry Should not retry + WebResponse::Ptr WebClientCore::Put(std::u16string_view url, const WebContent::Ptr& content, bool dontRetry) + { + auto l_IsDone = false; + auto l_Reply = WebResponse::Ptr(nullptr); + + DoRequest( + shared_from_this(), + u"Put", + u"PUT", + GetURL(url), + content, + CancellationToken::get_None(), + [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, + dontRetry, + nullptr + ); + + while (!l_IsDone) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + return l_Reply; + } + /// @brief Do DELETE query + /// @param url Target URL + /// @param dontRetry Should not retry + WebResponse::Ptr WebClientCore::Delete(std::u16string_view url, bool dontRetry) + { + auto l_IsDone = false; + auto l_Reply = WebResponse::Ptr(nullptr); + + DoRequest( + shared_from_this(), + u"Delete", + u"DELETE", + GetURL(url), + nullptr, + CancellationToken::get_None(), + [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, + dontRetry, + nullptr + ); + + while (!l_IsDone) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + return l_Reply; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Do Async GET query + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + void WebClientCore::GetAsync(std::u16string_view url, CancellationToken token, _v::CActionRef callback, bool dontRetry, _v::CActionRef progress) + { + il2cpp_utils::il2cpp_aware_thread( + &DoRequest, + shared_from_this(), + u"GetAsync", + u"GET", + GetURL(url), + nullptr, + token, + callback, + dontRetry, + progress + ).detach(); + } + /// @brief Do Async GET query + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + void WebClientCore::DownloadAsync(std::u16string_view url, CancellationToken token, _v::CActionRef callback, bool dontRetry, _v::CActionRef progress) + { + il2cpp_utils::il2cpp_aware_thread( + &DoRequest, + shared_from_this(), + u"DownloadAsync", + u"DOWNLOAD", + GetURL(url), + nullptr, + token, + callback, + dontRetry, + progress + ).detach(); + } + /// @brief Do Async POST query + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + void WebClientCore::PostAsync(std::u16string_view url, const WebContent::Ptr& content, CancellationToken token, _v::CActionRef callback, bool dontRetry) + { + il2cpp_utils::il2cpp_aware_thread( + &DoRequest, + shared_from_this(), + u"PostAsync", + u"POST", + GetURL(url), + content, + token, + callback, + dontRetry, + nullptr + ).detach(); + } + /// @brief Do Async PATCH query + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + void WebClientCore::PatchAsync(std::u16string_view url, const WebContent::Ptr& content, CancellationToken token, _v::CActionRef callback, bool dontRetry) + { + il2cpp_utils::il2cpp_aware_thread( + &DoRequest, + shared_from_this(), + u"PatchAsync", + u"PATCH", + GetURL(url), + content, + token, + callback, + dontRetry, + nullptr + ).detach(); + } + /// @brief Do Async PATCH query + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + void WebClientCore::PutAsync(std::u16string_view url, const WebContent::Ptr& content, CancellationToken token, _v::CActionRef callback, bool dontRetry) + { + il2cpp_utils::il2cpp_aware_thread( + &DoRequest, + shared_from_this(), + u"PutAsync", + u"PUT", + GetURL(url), + content, + token, + callback, + dontRetry, + nullptr + ).detach(); + } + /// @brief Do Async GET query + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + void WebClientCore::DeleteAsync(std::u16string_view url, CancellationToken token, _v::CActionRef callback, bool dontRetry) + { + il2cpp_utils::il2cpp_aware_thread( + &DoRequest, + shared_from_this(), + u"DeleteAsync", + u"DELETE", + GetURL(url), + nullptr, + token, + callback, + dontRetry, + nullptr + ); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Get URL + /// @param url Request URL + std::u16string WebClientCore::GetURL(std::u16string_view url) + { + if (m_BaseAddress.size() == 0) return std::u16string(url); + if (url.find(u"://") != std::u16string::npos) return std::u16string(url); + if (m_BaseAddress[m_BaseAddress.size() - 1] == '/') return m_BaseAddress + std::u16string(url); + + return m_BaseAddress + u"/" + std::u16string(url); + } + /// @brief Safe URL parsing + /// @param url Source URL + std::u16string WebClientCore::SafeURL(std::u16string_view url) + { + auto l_Position = url.find_first_of('?'); + if (l_Position != std::u16string::npos) + return std::u16string(url).substr(0, l_Position); + + return std::u16string(url); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Do request + /// @param debugName Method name for logs + /// @param httpMethod Http method + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + void WebClientCore::DoRequest( + Ptr self, + std::u16string debugName, + std::u16string httpMethod, + std::u16string url, + WebContent::Ptr content, + CancellationToken token, + _v::Action callback, + bool dontRetry, + _v::Action progress + ) + { +#if DEBUG + ChatPlexSDK::Logger()->Debug(u"[CP_SDK.Network][WebClientCore." + debugName + u"] " + httpMethod + u" " + url); +#endif + + WebResponse::Ptr l_Reply = nullptr; + for (int l_RetryI = 1; l_RetryI <= self->MaxRetry; l_RetryI++) + { + if (token.get_IsCancellationRequested()) + break; + + ScopedCURL l_ScopedCURL; + l_ScopedCURL.Headers = curl_slist_append(l_ScopedCURL.Headers, "Accept: */*"); + + for (auto const& [l_Header, l_Value] : self->m_Headers) + l_ScopedCURL.Headers = curl_slist_append(l_ScopedCURL.Headers, Utils::U16StrToStr(l_Header + ": " + l_Value).c_str()); + + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_URL, Utils::U16StrToStr(url).c_str()); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_TIMEOUT, static_cast(self->m_TimeOut)); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_FOLLOWLOCATION, static_cast(1)); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_USERAGENT, Utils::U16StrToStr(ChatPlexSDK::NetworkUserAgent()).c_str()); + + if (httpMethod == u"GET" || httpMethod == u"DOWNLOAD") + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "GET"); + else if (httpMethod == u"POST" || httpMethod == u"PATCH" || httpMethod == u"PUT") + { + if (httpMethod == u"POST") + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "POST"); + else if (httpMethod == u"PATCH") + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "PATCH"); + else + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "PUT"); + + if (content) + { + l_ScopedCURL.Headers = curl_slist_append(l_ScopedCURL.Headers, Utils::U16StrToStr(std::u16string(u"Content-Type: ") + content->Type).c_str()); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_POSTFIELDSIZE, static_cast(content->Bytes->get_Length())); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_POSTFIELDS, &content->Bytes->_values[0]); + } + } + else if (httpMethod == u"DELETE") + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "DELETE"); + + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_HTTPHEADER, l_ScopedCURL.Headers); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_SSL_VERIFYPEER, false); + + if (progress.IsValid()) + { + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_NOPROGRESS, false); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_XFERINFODATA, (void*)&progress); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_XFERINFOFUNCTION, + +[] (_v::Action* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { + float percentage = (float)dlnow / (float)dltotal * 100.0f; + if(isnan(percentage)) + percentage = 0.0f; + clientp->Invoke(percentage); + return 0; + } + ); + } + + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_WRITEDATA, l_ScopedCURL.Data); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_WRITEFUNCTION, + +[](void *contents, std::size_t size, std::size_t nmemb, std::vector* clientp) -> size_t + { + size_t l_SizeToWrite = size * nmemb; + + try + { + auto l_WritePos = clientp->size(); + clientp->resize(l_WritePos + l_SizeToWrite); + memcpy(clientp->data() + l_WritePos, contents, l_SizeToWrite); + } + catch(std::bad_alloc &e) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientCore.DoRequest] Failed to read from response stream, allocation error"); + return 0; + } + + return l_SizeToWrite; + }); + + if (token.get_IsCancellationRequested()) + break; + + auto l_CURLResult = curl_easy_perform(l_ScopedCURL.Instance); + + l_Reply = std::make_shared(static_cast(l_CURLResult), l_ScopedCURL.Instance, l_ScopedCURL.Data); + if (!l_Reply->IsSuccessStatusCode() && l_Reply->StatusCode() == (HttpStatusCode)429) + { + /* var l_Limits = RateLimitInfo.Get(l_Request); + if (l_Limits != nullptr) + { + int l_TotalMilliseconds = (int)(l_Limits.Reset - DateTime.Now).TotalMilliseconds; + if (l_TotalMilliseconds > 0) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientCore." + debugName + u"] Request {SafeURL(url)} was rate limited, retrying in {l_TotalMilliseconds}ms..."); + + co_yield WaitForSecondsRealtime::New_ctor(RetryInterval)->i_IEnumerator(); + continue; + } + }*/ + } + + if (!l_Reply->IsSuccessStatusCode()) + { + auto l_LogPrefix = u"[CP_SDK.Network][WebClientCore." + debugName + u"] Request " + self->SafeURL(url) + u" failed with code "; + l_LogPrefix += StringW(std::to_string(l_Reply->StatusCode().value__)); + l_LogPrefix += u":\"" + l_Reply->ReasonPhrase() + "\", "; + + if (!l_Reply->ShouldRetry() || dontRetry) + { + ChatPlexSDK::Logger()->Error(l_LogPrefix + u" not retrying"); + break; + } + + ChatPlexSDK::Logger()->Error(l_LogPrefix + u" next try in " + (std::u16string)StringW(std::to_string(self->RetryInterval)) + u" seconds..."); + + std::this_thread::sleep_for(std::chrono::seconds(self->RetryInterval)); + continue; + } + else + { + if (progress.IsValid()) + try { progress(1.0f); } catch (const std::exception&) { } + + break; + } + } + + if (!token.get_IsCancellationRequested() && callback.IsValid()) + Unity::MTThreadInvoker::EnqueueOnThread([=]() -> void { callback(l_Reply); }); + } + +} ///< namespace CP_SDK::Network \ No newline at end of file diff --git a/src/CP_SDK/Network/WebClientUnity.cpp b/src/CP_SDK/Network/WebClientUnity.cpp index 772c79c..0498722 100644 --- a/src/CP_SDK/Network/WebClientUnity.cpp +++ b/src/CP_SDK/Network/WebClientUnity.cpp @@ -6,7 +6,6 @@ #include #include #include -//#include #include #include @@ -36,41 +35,41 @@ namespace CP_SDK::Network { //////////////////////////////////////////////////////////////////////////// /// @brief Constructor - /// @param p_BaseAddress Base address - /// @param p_TimeOut Requests timeout - /// @param p_ForceCacheDiscard Should force server cache discard - WebClientUnity::WebClientUnity(CP_SDK_PRIV_TAG_ARG(), std::u16string_view p_BaseAddress, TimeSpan p_TimeOut, bool p_ForceCacheDiscard) + /// @param baseAddress Base address + /// @param timeOut Requests timeout + /// @param forceCacheDiscard Should force server cache discard + WebClientUnity::WebClientUnity(CP_SDK_PRIV_TAG_ARG(), std::u16string_view baseAddress, TimeSpan timeOut, bool forceCacheDiscard) : m_Headers({}) { DownloadTimeout = 2 * 60; MaxRetry = 2; RetryInterval = 5; - m_BaseAddress = p_BaseAddress; + m_BaseAddress = baseAddress; - m_Timeout = (int)p_TimeOut.get_TotalSeconds(); + m_Timeout = (int)timeOut.get_TotalSeconds(); - if (p_ForceCacheDiscard) + if (forceCacheDiscard) m_Headers[u"Cache-Control"] = u"no-cache, must-revalidate, proxy-revalidate, max-age=0, s-maxage=0, max-stale=0"; } /// @brief Constructor - /// @param p_BaseAddress Base address - /// @param p_TimeOut Requests timeout - /// @param p_ForceCacheDiscard Should force server cache discard - WebClientUnity::Ptr WebClientUnity::Make(std::u16string_view p_BaseAddress, TimeSpan p_TimeOut, bool p_ForceCacheDiscard) + /// @param baseAddress Base address + /// @param timeOut Requests timeout + /// @param forceCacheDiscard Should force server cache discard + WebClientUnity::Ptr WebClientUnity::Make(std::u16string_view baseAddress, TimeSpan timeOut, bool forceCacheDiscard) { - return std::make_shared(CP_SDK_PRIV_TAG_VAL(), p_BaseAddress, p_TimeOut, p_ForceCacheDiscard); + return std::make_shared(CP_SDK_PRIV_TAG_VAL(), baseAddress, timeOut, forceCacheDiscard); } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /// @brief Get header - /// @param p_Name Header name - std::u16string WebClientUnity::GetHeader(std::u16string_view p_Name) + /// @param name Header name + std::u16string WebClientUnity::GetHeader(std::u16string_view name) { - auto l_Name = std::u16string(p_Name); + auto l_Name = std::u16string(name); std::lock_guard l_Guard(m_HeadersLock); if (!m_Headers.contains(l_Name)) @@ -79,20 +78,20 @@ namespace CP_SDK::Network { return m_Headers[l_Name]; } /// @brief Set header - /// @param p_Name Header name - /// @param p_Value Header value - void WebClientUnity::SetHeader(std::u16string_view p_Name, std::u16string_view p_Value) + /// @param name Header name + /// @param value Header value + void WebClientUnity::SetHeader(std::u16string_view name, std::u16string_view value) { - auto l_Name = std::u16string(p_Name); + auto l_Name = std::u16string(name); std::lock_guard l_Guard(m_HeadersLock); - m_Headers[l_Name] = std::u16string(p_Value); + m_Headers[l_Name] = std::u16string(value); } /// @brief Remove header - /// @param p_Name Header name - void WebClientUnity::RemoveHeader(std::u16string_view p_Name) + /// @param name Header name + void WebClientUnity::RemoveHeader(std::u16string_view name) { - auto l_Name = std::u16string(p_Name); + auto l_Name = std::u16string(name); std::lock_guard l_Guard(m_HeadersLock); auto l_It = m_Headers.find(l_Name); @@ -104,153 +103,165 @@ namespace CP_SDK::Network { //////////////////////////////////////////////////////////////////////////// /// @brief Do Async GET query - /// @param p_URL Target URL - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - /// @param p_Progress Progress reporter - void WebClientUnity::GetAsync(std::u16string_view p_URL, CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry, _v::CActionRef p_Progress) + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + void WebClientUnity::GetAsync(std::u16string_view url, CancellationToken token, _v::CActionRef callback, bool dontRetry, _v::CActionRef progress) { - Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"GetAsync", u"GET", GetURL(p_URL), nullptr, p_Token, p_Callback, p_DontRetry, p_Progress))); + Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"GetAsync", u"GET", GetURL(url), nullptr, token, callback, dontRetry, progress))); } /// @brief Do Async GET query - /// @param p_URL Target URL - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - /// @param p_Progress Progress reporter - void WebClientUnity::DownloadAsync(std::u16string_view p_URL, CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry, _v::CActionRef p_Progress) + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + void WebClientUnity::DownloadAsync(std::u16string_view url, CancellationToken token, _v::CActionRef callback, bool dontRetry, _v::CActionRef progress) { - Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"DownloadAsync", u"DOWNLOAD", GetURL(p_URL), nullptr, p_Token, p_Callback, p_DontRetry, p_Progress))); + Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"DownloadAsync", u"DOWNLOAD", GetURL(url), nullptr, token, callback, dontRetry, progress))); } /// @brief Do Async POST query - /// @param p_URL Target URL - /// @param p_Content Optional content to post - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - void WebClientUnity::PostAsync(std::u16string_view p_URL, const WebContent::Ptr& p_Content, CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry) + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + void WebClientUnity::PostAsync(std::u16string_view url, const WebContent::Ptr& content, CancellationToken token, _v::CActionRef callback, bool dontRetry) { - Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"PostAsync", u"POST", GetURL(p_URL), p_Content, p_Token, p_Callback, p_DontRetry, nullptr))); + Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"PostAsync", u"POST", GetURL(url), content, token, callback, dontRetry, nullptr))); } /// @brief Do Async PATCH query - /// @param p_URL Target URL - /// @param p_Content Optional content to post - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - void WebClientUnity::PatchAsync(std::u16string_view p_URL, const WebContent::Ptr& p_Content, CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry) + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + void WebClientUnity::PatchAsync(std::u16string_view url, const WebContent::Ptr& content, CancellationToken token, _v::CActionRef callback, bool dontRetry) { - Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"PatchAsync", u"PATCH", GetURL(p_URL), p_Content, p_Token, p_Callback, p_DontRetry, nullptr))); + Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"PatchAsync", u"PATCH", GetURL(url), content, token, callback, dontRetry, nullptr))); + } + /// @brief Do Async PUT query + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + void WebClientUnity::PutAsync(std::u16string_view url, const WebContent::Ptr& content, CancellationToken token, _v::CActionRef callback, bool dontRetry) + { + Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"PutAsync", u"PUT", GetURL(url), content, token, callback, dontRetry, nullptr))); } /// @brief Do Async GET query - /// @param p_URL Target URL - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - /// @param p_Progress Progress reporter - void WebClientUnity::DeleteAsync(std::u16string_view p_URL, CancellationToken p_Token, _v::CActionRef p_Callback, bool p_DontRetry) + /// @param url Target URL + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + void WebClientUnity::DeleteAsync(std::u16string_view url, CancellationToken token, _v::CActionRef callback, bool dontRetry) { - Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"DownloadAsync", u"DOWNLOAD", GetURL(p_URL), nullptr, p_Token, p_Callback, p_DontRetry, nullptr))); + Unity::MTCoroutineStarter::EnqueueFromThread(custom_types::Helpers::CoroutineHelper::New(Coroutine_DoRequest(shared_from_this(), u"DownloadAsync", u"DOWNLOAD", GetURL(url), nullptr, token, callback, dontRetry, nullptr))); } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /// @brief Get URL - /// @param p_URL Request URL - std::u16string WebClientUnity::GetURL(std::u16string_view p_URL) + /// @param url Request URL + std::u16string WebClientUnity::GetURL(std::u16string_view url) { - if (m_BaseAddress.size() == 0) return std::u16string(p_URL); - if (p_URL.find(u"://") != std::u16string::npos) return std::u16string(p_URL); - if (m_BaseAddress[m_BaseAddress.size() - 1] == '/') return m_BaseAddress + std::u16string(p_URL); + if (m_BaseAddress.size() == 0) return std::u16string(url); + if (url.find(u"://") != std::u16string::npos) return std::u16string(url); + if (m_BaseAddress[m_BaseAddress.size() - 1] == '/') return m_BaseAddress + std::u16string(url); - return m_BaseAddress + u"/" + std::u16string(p_URL); + return m_BaseAddress + u"/" + std::u16string(url); } /// @brief Safe URL parsing - /// @param p_URL Source URL - std::u16string WebClientUnity::SafeURL(std::u16string_view p_URL) + /// @param url Source URL + std::u16string WebClientUnity::SafeURL(std::u16string_view url) { - auto l_Position = p_URL.find_first_of('?'); + auto l_Position = url.find_first_of('?'); if (l_Position != std::u16string::npos) - return std::u16string(p_URL).substr(0, l_Position); + return std::u16string(url).substr(0, l_Position); - return std::u16string(p_URL); + return std::u16string(url); } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /// @brief Prepare request - /// @param p_Request Request to prepare - /// @param p_IsDownload Is a download request? - void WebClientUnity::PrepareRequest(UnityWebRequest* p_Request, bool p_IsDownload) + /// @param request Request to prepare + /// @param isDownload Is a download request? + void WebClientUnity::PrepareRequest(UnityWebRequest* request, bool isDownload) { - if (p_Request->get_downloadHandler() == nullptr) - p_Request->set_downloadHandler(DownloadHandlerBuffer::New_ctor()); + if (request->get_downloadHandler() == nullptr) + request->set_downloadHandler(DownloadHandlerBuffer::New_ctor()); - p_Request->set_timeout(p_IsDownload ? DownloadTimeout : m_Timeout); + request->set_timeout(isDownload ? DownloadTimeout : m_Timeout); std::lock_guard l_Guard(m_HeadersLock); static auto s_UnityWebRequest_InternalSetRequestHeader = il2cpp_utils::resolve_icall("UnityEngine.Networking.UnityWebRequest::InternalSetRequestHeader"); for (auto const& [l_Header, l_Value] : m_Headers) - s_UnityWebRequest_InternalSetRequestHeader(p_Request, l_Header, l_Value); + s_UnityWebRequest_InternalSetRequestHeader(request, l_Header, l_Value); } /// @brief Do request - /// @param p_DebugName Method name for logs - /// @param p_HttpMethod Http method - /// @param p_URL Target URL - /// @param p_Content Optional content to post - /// @param p_Token Cancellation token - /// @param p_Callback Callback - /// @param p_DontRetry Should not retry - /// @param p_Progress Progress reporter - custom_types::Helpers::Coroutine WebClientUnity::Coroutine_DoRequest(Ptr p_Self, - std::u16string p_DebugName, - std::u16string p_HttpMethod, - std::u16string p_URL, - WebContent::Ptr p_Content, - CancellationToken p_Token, - _v::Action p_Callback, - bool p_DontRetry, - _v::Action p_Progress) + /// @param debugName Method name for logs + /// @param httpMethod Http method + /// @param url Target URL + /// @param content Optional content to post + /// @param token Cancellation token + /// @param callback Callback + /// @param dontRetry Should not retry + /// @param progress Progress reporter + custom_types::Helpers::Coroutine WebClientUnity::Coroutine_DoRequest(Ptr self, + std::u16string debugName, + std::u16string httpMethod, + std::u16string url, + WebContent::Ptr content, + CancellationToken token, + _v::Action callback, + bool dontRetry, + _v::Action progress) { #if DEBUG - ChatPlexSDK::Logger()->Debug(u"[CP_SDK.Network][WebClientUnity." + p_DebugName + u"] " + p_HttpMethod + u" " + p_URL); + ChatPlexSDK::Logger()->Debug(u"[CP_SDK.Network][WebClientUnity." + debugName + u"] " + httpMethod + u" " + url); #endif WebResponse::Ptr l_Reply = nullptr; - for (int l_RetryI = 1; l_RetryI <= p_Self->MaxRetry; l_RetryI++) + for (int l_RetryI = 1; l_RetryI <= self->MaxRetry; l_RetryI++) { - if (p_Token.get_IsCancellationRequested()) + if (token.get_IsCancellationRequested()) break; auto l_Request = _v::MonoPtr(nullptr); - if (p_HttpMethod == u"GET" || p_HttpMethod == u"DOWNLOAD") - l_Request = UnityWebRequest::Get(p_URL); - else if (p_HttpMethod == u"POST" || p_HttpMethod == u"PATCH") + if (httpMethod == u"GET" || httpMethod == u"DOWNLOAD") + l_Request = UnityWebRequest::Get(url); + else if (httpMethod == u"POST" || httpMethod == u"PATCH" || httpMethod == u"PUT") { - ChatPlexSDK::Logger()->Error(u"WebClientUnity POST & PATCH are disabled for now"); + ChatPlexSDK::Logger()->Error(u"WebClientUnity POST & PATCH & PUT are disabled for now"); throw std::runtime_error("WebClientUnity POST & PATCH are disabled for now"); /// TODO Disabled until fixed - /*static auto s_UploadHandler_InternalSetContentType = il2cpp_utils::resolve_icall("UnityEngine.Networking.UploadHandler::InternalSetContentType"); - auto l_UploadHandler = UploadHandlerRaw::New_ctor(p_Content->Bytes.Ptr()); - s_UploadHandler_InternalSetContentType(l_UploadHandler, p_Content->Type); + /*static auto s_UploadHandler_InternalSetContentType + = il2cpp_utils::resolve_icall("UnityEngine.Networking.UploadHandler::InternalSetContentType"); + + auto l_UploadHandler = UploadHandlerRaw::New_ctor(content->Bytes.Ptr()); + s_UploadHandler_InternalSetContentType(l_UploadHandler, content->Type); - l_Request = UnityWebRequest::New_ctor(p_URL, p_HttpMethod, DownloadHandlerBuffer::New_ctor(), l_UploadHandler);*/ + l_Request = UnityWebRequest::New_ctor(url, httpMethod, DownloadHandlerBuffer::New_ctor(), l_UploadHandler);*/ } - else if (p_HttpMethod == u"DELETE") - l_Request = UnityWebRequest::New_ctor(p_URL, p_HttpMethod, nullptr, nullptr); + else if (httpMethod == u"DELETE") + l_Request = UnityWebRequest::New_ctor(url, httpMethod, nullptr, nullptr); - p_Self->PrepareRequest(l_Request.Ptr(), p_HttpMethod == u"DOWNLOAD"); + self->PrepareRequest(l_Request.Ptr(), httpMethod == u"DOWNLOAD"); - if (!p_Progress.IsValid()) + if (!progress.IsValid()) co_yield reinterpret_cast(l_Request->SendWebRequest()); else { - try { p_Progress(0.0f); } catch (const std::exception&) { } + try { progress(0.0f); } catch (const std::exception&) { } l_Request->SendWebRequest(); auto l_Waiter = WaitForSecondsRealtime::New_ctor(0.05f); @@ -262,15 +273,15 @@ namespace CP_SDK::Network { static auto s_UnityWebRequest_GetDownloadProgress = il2cpp_utils::resolve_icall("UnityEngine.Networking.UnityWebRequest::GetDownloadProgress"); auto l_Progress = (!s_UnityWebRequest_IsExecuting(l_Request.Ptr()) && !l_Request->get_isDone()) ? -1.0f : s_UnityWebRequest_GetDownloadProgress(l_Request.Ptr()); - p_Progress(l_Progress); + progress(l_Progress); } catch (const std::exception&) { } - if (p_Token.get_IsCancellationRequested() || l_Request->get_isDone() || l_Request->get_result() == UnityWebRequest::Result::ProtocolError || l_Request->get_result() == UnityWebRequest::Result::ConnectionError) + if (token.get_IsCancellationRequested() || l_Request->get_isDone() || l_Request->get_result() == UnityWebRequest::Result::ProtocolError || l_Request->get_result() == UnityWebRequest::Result::ConnectionError) break; } while (true); } - if (p_Token.get_IsCancellationRequested()) + if (token.get_IsCancellationRequested()) break; l_Reply = std::make_shared(l_Request.Ptr()); @@ -283,7 +294,7 @@ namespace CP_SDK::Network { int l_TotalMilliseconds = (int)(l_Limits.Reset - DateTime.Now).TotalMilliseconds; if (l_TotalMilliseconds > 0) { - ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientUnity." + p_DebugName + u"] Request {SafeURL(p_URL)} was rate limited, retrying in {l_TotalMilliseconds}ms..."); + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientUnity." + debugName + u"] Request {SafeURL(url)} was rate limited, retrying in {l_TotalMilliseconds}ms..."); co_yield WaitForSecondsRealtime::New_ctor(RetryInterval)->i_IEnumerator(); continue; @@ -293,32 +304,32 @@ namespace CP_SDK::Network { if (!l_Reply->IsSuccessStatusCode()) { - auto l_LogPrefix = u"[CP_SDK.Network][WebClientUnity." + p_DebugName + u"] Request " + p_Self->SafeURL(p_URL) + u" failed with code "; + auto l_LogPrefix = u"[CP_SDK.Network][WebClientUnity." + debugName + u"] Request " + self->SafeURL(url) + u" failed with code "; l_LogPrefix += StringW(std::to_string(l_Reply->StatusCode().value__)); l_LogPrefix += u":\"" + l_Reply->ReasonPhrase() + "\", "; - if (!l_Reply->ShouldRetry() || p_DontRetry) + if (!l_Reply->ShouldRetry() || dontRetry) { ChatPlexSDK::Logger()->Error(l_LogPrefix + u" not retrying"); break; } - ChatPlexSDK::Logger()->Error(l_LogPrefix + u" next try in " + (std::u16string)StringW(std::to_string(p_Self->RetryInterval)) + u" seconds..."); + ChatPlexSDK::Logger()->Error(l_LogPrefix + u" next try in " + (std::u16string)StringW(std::to_string(self->RetryInterval)) + u" seconds..."); - co_yield WaitForSecondsRealtime::New_ctor(p_Self->RetryInterval)->i___System__Collections__IEnumerator(); + co_yield WaitForSecondsRealtime::New_ctor(self->RetryInterval)->i___System__Collections__IEnumerator(); continue; } else { - if (p_Progress.IsValid()) - try { p_Progress(1.0f); } catch (const std::exception&) { } + if (progress.IsValid()) + try { progress(1.0f); } catch (const std::exception&) { } break; } } - if (!p_Token.get_IsCancellationRequested() && p_Callback.IsValid()) - Unity::MTThreadInvoker::EnqueueOnThread([=]() -> void { p_Callback(l_Reply); }); + if (!token.get_IsCancellationRequested() && callback.IsValid()) + Unity::MTThreadInvoker::EnqueueOnThread([=]() -> void { callback(l_Reply); }); } } ///< namespace CP_SDK::Network \ No newline at end of file diff --git a/src/CP_SDK/Network/WebContent.cpp b/src/CP_SDK/Network/WebContent.cpp index c9ef6b2..f3eb29c 100644 --- a/src/CP_SDK/Network/WebContent.cpp +++ b/src/CP_SDK/Network/WebContent.cpp @@ -1,4 +1,5 @@ #include "CP_SDK/Network/WebContent.hpp" +#include "CP_SDK/Utils/Json.hpp" #include #include @@ -31,5 +32,12 @@ namespace CP_SDK::Network { auto l_Array = Encoding::get_UTF8()->GetBytes(p_Content).operator Array *(); return std::make_shared(CP_SDK_PRIV_TAG_VAL(), l_Array, u"application/json; charset=utf-8"); } + /// @brief Constructor from Json + /// @param content Json content + WebContent::Ptr WebContent::FromJson(std::shared_ptr<_v::Json::U16Document>& content) + { + auto l_Array = Encoding::get_UTF8()->GetBytes(content ? _v::Json::ToU16String(*content, false) : u"").operator Array *(); + return std::make_shared(CP_SDK_PRIV_TAG_VAL(), l_Array, u"application/json; charset=utf-8"); + } } ///< namespace CP_SDK::Network \ No newline at end of file diff --git a/src/CP_SDK/Network/WebResponse.cpp b/src/CP_SDK/Network/WebResponse.cpp index 5ce5aa5..f97e320 100644 --- a/src/CP_SDK/Network/WebResponse.cpp +++ b/src/CP_SDK/Network/WebResponse.cpp @@ -1,6 +1,8 @@ #include "CP_SDK/Network/WebResponse.hpp" -#include "CP_SDK/ChatPlexSDK.hpp" +#include +#include +#include #include #include #include @@ -64,13 +66,10 @@ namespace CP_SDK::Network { //////////////////////////////////////////////////////////////////////////// /// Constructor - /// @p_Request: Reply status + /// @param request: Reply status WebResponse::WebResponse(UnityWebRequest * p_Request) { - m_StatusCode = (HttpStatusCode)p_Request->get_responseCode(); - m_IsSuccessStatusCode = !(p_Request->get_result() == UnityWebRequest::Result::ProtocolError && p_Request->get_result() == UnityWebRequest::Result::ConnectionError); - m_ShouldRetry = IsSuccessStatusCode() ? false : (p_Request->get_responseCode() < 400 || p_Request->get_responseCode() >= 500); - m_BodyBytes = reinterpret_cast<::Array*>(p_Request->get_downloadHandler()->GetData().convert()); + m_StatusCode = (HttpStatusCode)p_Request->get_responseCode(); if (p_Request->get_result() == UnityWebRequest::Result::ConnectionError || p_Request->get_result() == UnityWebRequest::Result::ProtocolError) { @@ -78,7 +77,42 @@ namespace CP_SDK::Network { m_ReasonPhrase = u"HTTP/1.1 " + std::to_string(m_StatusCode.value__) + u" " + p_Request->GetHTTPStatusString(m_StatusCode.value__); else m_ReasonPhrase = p_Request->GetWebErrorString(p_Request->GetError()); + + m_IsSuccessStatusCode = false; } + else + m_IsSuccessStatusCode = ((int)p_Request->get_responseCode() >= 200) && ((int)p_Request->get_responseCode() <= 299); + + m_ShouldRetry = IsSuccessStatusCode() ? false : (p_Request->get_responseCode() < 400 || p_Request->get_responseCode() >= 500); + m_BodyBytes = reinterpret_cast<::Array*>(p_Request->get_downloadHandler()->GetData().convert()); + } + /// @brief Constructor + /// @param curlPerformResult CURL perform result + /// @param curlInstance CURL instance + /// @param data Response data + WebResponse::WebResponse(long curlPerformResult, void* curlInstance, std::vector* data) + { + auto l_CURLInstance = reinterpret_cast(curlInstance); + + if (curlPerformResult != CURLE_OK) + { + m_StatusCode = -curlPerformResult; + m_ReasonPhrase = Utils::StrToU16Str(curl_easy_strerror(static_cast(curlPerformResult))); + m_IsSuccessStatusCode = false; + m_ShouldRetry = false; + } + else + { + long l_HTTPCode(0); + curl_easy_getinfo(l_CURLInstance, CURLINFO_RESPONSE_CODE, &l_HTTPCode); + + m_StatusCode = l_HTTPCode; + m_IsSuccessStatusCode = ((int)l_HTTPCode >= 200) && ((int)l_HTTPCode <= 299); + m_ShouldRetry = IsSuccessStatusCode() ? false : (l_HTTPCode < 400 || l_HTTPCode >= 500); + } + + m_BodyBytes = ::Array::NewLength(data->size()); + memcpy(m_BodyBytes->_values, data->data(), data->size()); } } ///< namespace CP_SDK::Network diff --git a/src/CP_SDK/UI/Views/SettingsLeftView.cpp b/src/CP_SDK/UI/Views/SettingsLeftView.cpp index 3169108..f83b50f 100644 --- a/src/CP_SDK/UI/Views/SettingsLeftView.cpp +++ b/src/CP_SDK/UI/Views/SettingsLeftView.cpp @@ -1,9 +1,14 @@ #include "CP_SDK/UI/Views/SettingsLeftView.hpp" #include "CP_SDK/Unity/SpriteU.hpp" +#include "CP_SDK/Unity/MTMainThreadInvoker.hpp" +#include +#include using namespace CP_SDK::XUI; using namespace UnityEngine; +#include "assets.hpp" + namespace CP_SDK::UI::Views { CP_SDK_IL2CPP_INHERIT_INIT(SettingsLeftView); @@ -14,7 +19,8 @@ namespace CP_SDK::UI::Views { /// @brief Constructor CP_SDK_IL2CPP_DECLARE_CTOR_IMPL(SettingsLeftView) { - OnViewCreation = {this, &SettingsLeftView::OnViewCreation_Impl}; + OnViewCreation = {this, &SettingsLeftView::OnViewCreation_Impl}; + OnViewDeactivation = {this, &SettingsLeftView::OnViewDeactivation_Impl}; } /// @brief Destructor CP_SDK_IL2CPP_DECLARE_DTOR_MONOBEHAVIOUR_IMPL(SettingsLeftView) @@ -28,13 +34,179 @@ namespace CP_SDK::UI::Views { /// @brief On view creation void SettingsLeftView::OnViewCreation_Impl() { + auto l_Sprite = Unity::SpriteU::CreateFromRaw(Assets::ChatPlexLogoTransparent_png); + Templates::FullRectLayout({ - Templates::TitleBar(u"Tools"), + Templates::TitleBar(u"ChatPlex Account"), + + XUIPrimaryButton::Make(u"") + ->SetBackgroundSprite(nullptr) + ->SetIconSprite(l_Sprite) + ->SetWidth(52) + ->SetHeight(52) + ->AsShared(), + + XUIText::Make(u"Not connected") + ->Bind(&m_StatusText) + ->AsShared(), + + XUIText::Make(u" ") + ->Bind(&m_SubscriptionText) + ->AsShared(), + + XUIVLayout::Make({ + XUIPrimaryButton::Make(u"Connect", {this, &SettingsLeftView::OnPrimaryButtonPressed}) + ->Bind(&m_PrimaryButton) + ->AsShared(), + XUISecondaryButton::Make(u"Disconnect", {this, &SettingsLeftView::OnSecondaryButtonPressed}) + ->Bind(&m_SecondaryButton) + ->AsShared() + }) + ->SetWidth(60.0f) + ->SetPadding(0) + ->ForEachDirect([](XUIPrimaryButton* y) -> void + { + y->SetHeight(8.0f); + y->OnReady([](Components::CPrimaryButton* x) -> void + { + x->CSizeFitter()->horizontalFit = ContentSizeFitter::FitMode::Unconstrained; + }); + }) + ->ForEachDirect([](XUISecondaryButton* y ) -> void + { + y->SetHeight(8.0f); + y->OnReady([](Components::CSecondaryButton* x) -> void + { + x->CSizeFitter()->horizontalFit = ContentSizeFitter::FitMode::Unconstrained; + }); + }) + ->AsShared() + }) + ->SetBackground(true, std::nullopt, true) + ->BuildUI(get_transform()); + + ChatPlexService::StateChanged += {this, &SettingsLeftView::ChatPlexService_StateChanged}; + + ChatPlexService_StateChanged(ChatPlexService::State(), ChatPlexService::State()); + } + /// @brief On view deactivation + void SettingsLeftView::OnViewDeactivation_Impl() + { + ChatPlexService::StateChanged -= {this, &SettingsLeftView::ChatPlexService_StateChanged}; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief On primary button pressed + void SettingsLeftView::OnPrimaryButtonPressed() + { + if (ChatPlexService::State() == ChatPlexService::EState::Disconnected) + { + m_IsLinking = true; + ChatPlexService::StartLinking(); + ShowLoadingModal(u"Loading...", true, {this, &SettingsLeftView::OnLoadingCancel}); + } + else if (ChatPlexService::State() == ChatPlexService::EState::Error || ChatPlexService::State() == ChatPlexService::EState::Connected) + { + ChatPlexService::Refresh(); + } + } + /// @brief On secondary button pressed + void SettingsLeftView::OnSecondaryButtonPressed() + { + ChatPlexService::Disconnect(); + } + /// @brief On Loading cancel + void SettingsLeftView::OnLoadingCancel() + { + if (ChatPlexService::State() == ChatPlexService::EState::LinkRequest || ChatPlexService::State() == ChatPlexService::EState::LinkWait) + { + m_IsLinking = false; + ChatPlexService::StopLinking(); + } + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief On ChatPlex service state change + /// @param oldState Old state + /// @param newState New state + void SettingsLeftView::ChatPlexService_StateChanged(ChatPlexService::EState oldState, ChatPlexService::EState newState) + { + Unity::MTMainThreadInvoker::Enqueue([this, oldState, newState]() -> void + { + if (m_IsLinking) + { + if (newState == ChatPlexService::EState::LinkRequest) + ShowLoadingModal(u"Creating link request...", true, {this, &SettingsLeftView::OnLoadingCancel}); + else if (newState == ChatPlexService::EState::LinkWait) + ShowLoadingModal( + std::u16string(u"Go to https://chatplex.org/link and the input following code\n") + ChatPlexService::LinkCode(), + true, + {this, &SettingsLeftView::OnLoadingCancel} + ); + else if (newState == ChatPlexService::EState::Error) + { + m_IsLinking = false; + + CloseLoadingModal(); + ShowMessageModal(u"Error: " + ChatPlexService::LastError()); + } + else + { + m_IsLinking = false; + CloseLoadingModal(); + } + } + + switch (newState) + { + case ChatPlexService::EState::Disconnected: + m_StatusText->SetColor(Color::get_red()); + m_StatusText->SetText(u"Disconected!"); + m_PrimaryButton->SetInteractable(true); + m_PrimaryButton->SetText(u"Connect"); + m_SecondaryButton->SetInteractable(false); + break; + + case ChatPlexService::EState::Error: + m_StatusText->SetColor(Color::get_red()); + m_StatusText->SetText(u"Disconected, error!"); + m_PrimaryButton->SetInteractable(true); + m_PrimaryButton->SetText(u"Connect"); + m_SecondaryButton->SetInteractable(false); + break; + + case ChatPlexService::EState::Connecting: + m_StatusText->SetColor(Color::get_blue()); + m_StatusText->SetText(u"Connecting..."); + m_PrimaryButton->SetInteractable(false); + m_PrimaryButton->SetText(u"Connect"); + m_SecondaryButton->SetInteractable(false); + break; + + case ChatPlexService::EState::LinkRequest: + case ChatPlexService::EState::LinkWait: + m_StatusText->SetColor(Color::get_blue()); + m_StatusText->SetText(u"Linking account..."); + m_PrimaryButton->SetInteractable(false); + m_PrimaryButton->SetText(u"Connect"); + m_SecondaryButton->SetInteractable(false); + break; + + case ChatPlexService::EState::Connected: + m_StatusText->SetColor(Color::get_green()); + m_StatusText->SetText(u"Connected!"); + m_PrimaryButton->SetInteractable(true); + m_PrimaryButton->SetText(u"Refresh"); + m_SecondaryButton->SetInteractable(true); + break; + } - XUIText::Make(u"No available tools at the moment!")->AsShared() - }) - ->SetBackground(true, std::nullopt, true) - ->BuildUI(get_transform()); + m_SubscriptionText->Element()->SetText(ChatPlexService::ActiveSubscription()); + }); } } ///< namespace CP_SDK::UI::Views \ No newline at end of file diff --git a/src/CP_SDK/Utils/Il2cpp.cpp b/src/CP_SDK/Utils/Il2cpp.cpp index 7af7769..bf5e22a 100644 --- a/src/CP_SDK/Utils/Il2cpp.cpp +++ b/src/CP_SDK/Utils/Il2cpp.cpp @@ -1,6 +1,8 @@ #include "CP_SDK/Utils/Il2cpp.hpp" #include "CP_SDK/ChatPlexSDK.hpp" +#include "CP_SDK/Logging/PaperLogger.hpp" + namespace CP_SDK::Utils { std::vector Hooks::m_InstalledFuncs; diff --git a/src/CP_SDK_BS/Game/LevelSelection.cpp b/src/CP_SDK_BS/Game/LevelSelection.cpp index 8d36517..d925926 100644 --- a/src/CP_SDK_BS/Game/LevelSelection.cpp +++ b/src/CP_SDK_BS/Game/LevelSelection.cpp @@ -195,23 +195,23 @@ namespace CP_SDK_BS::Game { try { m_PreventLevelSearchViewController_didStartLoadingEvent = true; - p_LevelSearchViewController->ResetAllFilterSettings(false); + //p_LevelSearchViewController->ResetAllFilterSettings(false); auto l_Filter = GlobalNamespace::LevelFilter(); - l_Filter.songOwned = false; + l_Filter.songOwned = true; l_Filter.songNotOwned = false; l_Filter.songUnplayed = false; - l_Filter.difficulties = _u::BeatmapDifficultyMask(); - l_Filter.songPacks = _u::SongPackMask(); + l_Filter.difficulties = _u::BeatmapDifficultyMask(0); + l_Filter.songPacks = _u::SongPackMask::get_all(); l_Filter.characteristicSerializedName = nullptr; l_Filter.minBpm = 0.0f; l_Filter.maxBpm = 0.0f; - l_Filter.sensitivity = _u::PlayerSensitivityFlag(); + l_Filter.sensitivity = _u::PlayerSensitivityFlag::Unknown; l_Filter.limitIds = ArrayW({ m_PendingFilterSong->___levelID }); l_Filter.searchText = u""; - p_LevelSearchViewController->ResetAllFilterSettings(false); + //p_LevelSearchViewController->ResetAllFilterSettings(false); p_LevelSearchViewController->Refresh( byref(l_Filter) ); From 6794b4dca75a3f2e387b4b6f5a58c1715903ad3d Mon Sep 17 00:00:00 2001 From: HardCPP Date: Tue, 23 Sep 2025 22:10:26 +0200 Subject: [PATCH 02/15] UI/XUI: Fix button text/background color + slider styling --- shared/CP_SDK/XUI/Generics/XUIPOrSButton.hpp | 6 ++++++ src/CP_SDK/UI/Components/Generics/CPOrSButton.cpp | 2 +- src/CP_SDK/UI/DefaultComponents/DefaultCSlider.cpp | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/shared/CP_SDK/XUI/Generics/XUIPOrSButton.hpp b/shared/CP_SDK/XUI/Generics/XUIPOrSButton.hpp index d43e254..0401599 100644 --- a/shared/CP_SDK/XUI/Generics/XUIPOrSButton.hpp +++ b/shared/CP_SDK/XUI/Generics/XUIPOrSButton.hpp @@ -112,9 +112,15 @@ namespace CP_SDK::XUI { t_Base* SetHeight(float p_Height) XUI_PROXY_IMPL_EX(t_Component, { x->SetHeight(p_Height); }) public: + /// @brief Set button background color + /// @param p_Color New color + t_Base* SetBackgroundColor(Color p_Color) XUI_PROXY_IMPL_EX(t_Component, { x->SetBackgroundColor(p_Color); }) /// @brief Set button background sprite /// @param p_Sprite New sprite t_Base* SetBackgroundSprite(_u::Sprite* p_Sprite) XUI_PROXY_IMPL_EX(t_Component, { x->SetBackgroundSprite(p_Sprite); }) + /// @brief Set button text color + /// @param p_Color New color + t_Base* SetColor(Color p_Color) XUI_PROXY_IMPL_EX(t_Component, { x->SetColor(p_Color); }) /// @brief Set font size /// @param p_Size New size t_Base* SetFontSize(float p_Size) XUI_PROXY_IMPL_EX(t_Component, { x->SetFontSize(p_Size); }) diff --git a/src/CP_SDK/UI/Components/Generics/CPOrSButton.cpp b/src/CP_SDK/UI/Components/Generics/CPOrSButton.cpp index 728d4e3..6b2f6ad 100644 --- a/src/CP_SDK/UI/Components/Generics/CPOrSButton.cpp +++ b/src/CP_SDK/UI/Components/Generics/CPOrSButton.cpp @@ -94,7 +94,7 @@ namespace CP_SDK::UI::Components { /// @param p_Color New color CPOrSButton* CPOrSButton::SetColor(Color p_Color) { - BackgroundImageC()->set_color(p_Color); + TextC()->SetColor(p_Color); return this; } /// @brief Set button icon sprite diff --git a/src/CP_SDK/UI/DefaultComponents/DefaultCSlider.cpp b/src/CP_SDK/UI/DefaultComponents/DefaultCSlider.cpp index 3fedd0b..a866e88 100644 --- a/src/CP_SDK/UI/DefaultComponents/DefaultCSlider.cpp +++ b/src/CP_SDK/UI/DefaultComponents/DefaultCSlider.cpp @@ -219,8 +219,8 @@ namespace CP_SDK::UI::DefaultComponents { m_OnColor = p_Color; m_OffColor = p_Color; - m_DecButton->SetColor(p_Color); - m_IncButton->SetColor(p_Color); + m_DecButton->SetBackgroundColor(p_Color); + m_IncButton->SetBackgroundColor(p_Color); UpdateStyle(); } @@ -537,8 +537,8 @@ namespace CP_SDK::UI::DefaultComponents { void DefaultCSlider::UpdateStyle() { auto l_IsInteractable = IsInteractable(); - m_DecButton->SetColor(l_IsInteractable ? m_OnColor : m_OffColor); - m_IncButton->SetColor(l_IsInteractable ? m_OnColor : m_OffColor); + m_DecButton->SetBackgroundColor(l_IsInteractable ? m_OnColor : m_OffColor); + m_IncButton->SetBackgroundColor(l_IsInteractable ? m_OnColor : m_OffColor); if (m_LeftMouseButtonPressed || m_LastPointerEvent) { From 1efcc8a47dd7a4d224efe8a3053439b4c058c30a Mon Sep 17 00:00:00 2001 From: HardCPP Date: Tue, 23 Sep 2025 22:11:32 +0200 Subject: [PATCH 03/15] Game/Levels: Log TryGetBeatmapLevelForLevelID GetBeatmapLevel failure --- src/CP_SDK_BS/Game/Levels.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CP_SDK_BS/Game/Levels.cpp b/src/CP_SDK_BS/Game/Levels.cpp index 302be77..40b5d6c 100644 --- a/src/CP_SDK_BS/Game/Levels.cpp +++ b/src/CP_SDK_BS/Game/Levels.cpp @@ -489,6 +489,8 @@ namespace CP_SDK_BS::Game { if (p_BeatmapLevel) *p_BeatmapLevel = l_Result; return true; } + + CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetBeatmapLevelForLevelID] GetBeatmapLevel failed"); } else CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetBeatmapLevelForLevelID] Invalid BeatmapLevelsModel"); From aa318a4c5739f2f90189f6c040db3987abc517e4 Mon Sep 17 00:00:00 2001 From: HardCPP Date: Wed, 24 Sep 2025 19:56:55 +0200 Subject: [PATCH 04/15] UI/XUI: Adjust design, add a silent set options to dropdown --- .vscode/settings.json | 3 ++- shared/CP_SDK/UI/Components/CDropdown.hpp | 11 ++++---- .../UI/DefaultComponents/DefaultCDropdown.hpp | 5 ++-- shared/CP_SDK/XUI/XUIDropdown.hpp | 5 ++-- src/CP_SDK/UI/Components/CDropDown.cpp | 7 ++--- .../UI/DefaultComponents/DefaultCDropdown.cpp | 26 ++++++++++++------- .../DefaultComponents/DefaultCTextInput.cpp | 2 +- 7 files changed, 35 insertions(+), 24 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7e40a45..a1c4624 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -125,7 +125,8 @@ "memory_resource": "cpp", "ranges": "cpp", "__verbose_abort": "cpp", - "numbers": "cpp" + "numbers": "cpp", + "execution": "cpp" }, "editor.formatOnSave": false, "editor.trimAutoWhitespace": true, diff --git a/shared/CP_SDK/UI/Components/CDropdown.hpp b/shared/CP_SDK/UI/Components/CDropdown.hpp index 70200dd..2fbbb09 100644 --- a/shared/CP_SDK/UI/Components/CDropdown.hpp +++ b/shared/CP_SDK/UI/Components/CDropdown.hpp @@ -30,9 +30,9 @@ namespace CP_SDK::UI::Components { _v::Func m_GetValue; - _v::Action m_SetInteractable; - _v::Action&> m_SetOptions; - _v::Action m_SetValue; + _v::Action m_SetInteractable; + _v::Action&, bool> m_SetOptions; + _v::Action m_SetValue; public: UIFieldRefDel<_u::RectTransform> RTransform; @@ -53,8 +53,9 @@ namespace CP_SDK::UI::Components { /// @param p_Interactable New state CDropdown* SetInteractable(bool p_Interactable); /// @brief Set available options - /// @param p_Options New options list - CDropdown* SetOptions(const std::vector& p_Options); + /// @param options New options list + /// @param notifyOnValueChanged Should notify on value changed? + CDropdown* SetOptions(const std::vector& options, bool notifyOnValueChanged = true); /// @brief Set value /// @param p_Value New value /// @param p_Notify Should notify? diff --git a/shared/CP_SDK/UI/DefaultComponents/DefaultCDropdown.hpp b/shared/CP_SDK/UI/DefaultComponents/DefaultCDropdown.hpp index 83c6fba..d86a2a7 100644 --- a/shared/CP_SDK/UI/DefaultComponents/DefaultCDropdown.hpp +++ b/shared/CP_SDK/UI/DefaultComponents/DefaultCDropdown.hpp @@ -61,8 +61,9 @@ namespace CP_SDK::UI::DefaultComponents { /// @param p_Interactable New state void SetInteractable_Impl(bool p_Interactable); /// @brief Set available options - /// @param p_Options New options list - void SetOptions_Impl(const std::vector& p_Options); + /// @param options New options list + /// @param notifyOnValueChanged Should notify on value changed? + void SetOptions_Impl(const std::vector& options, bool notifyOnValueChanged = true); /// @brief Set value /// @param p_Value New value /// @param p_Notify Should notify? diff --git a/shared/CP_SDK/XUI/XUIDropdown.hpp b/shared/CP_SDK/XUI/XUIDropdown.hpp index d3751a0..fdcd8e9 100644 --- a/shared/CP_SDK/XUI/XUIDropdown.hpp +++ b/shared/CP_SDK/XUI/XUIDropdown.hpp @@ -112,8 +112,9 @@ namespace CP_SDK::XUI { /// @param p_Interactable New state XUIDropdown* SetInteractable(bool p_Interactable) XUI_PROXY_IMPL(_v::CDropdown, { x->SetInteractable(p_Interactable); }) /// @brief Set available options - /// @param p_Options New options list - XUIDropdown* SetOptions(std::vector p_Options) XUI_PROXY_IMPL(_v::CDropdown, { x->SetOptions(p_Options); }) + /// @param options New options list + /// @param notifyOnValueChanged Should notify on value changed? + XUIDropdown* SetOptions(std::vector options, bool notifyOnValueChanged = true) XUI_PROXY_IMPL(_v::CDropdown, { x->SetOptions(options, notifyOnValueChanged); }) /// @brief Set value /// @param p_Value New value /// @param p_Notify Should notify? diff --git a/src/CP_SDK/UI/Components/CDropDown.cpp b/src/CP_SDK/UI/Components/CDropDown.cpp index 4909cd0..058b32c 100644 --- a/src/CP_SDK/UI/Components/CDropDown.cpp +++ b/src/CP_SDK/UI/Components/CDropDown.cpp @@ -54,10 +54,11 @@ namespace CP_SDK::UI::Components { return this; } /// @brief Set available options - /// @param p_Options New options list - CDropdown* CDropdown::SetOptions(const std::vector& p_Options) + /// @param options New options list + /// @param notifyOnValueChanged Should notify on value changed? + CDropdown* CDropdown::SetOptions(const std::vector& options, bool notifyOnValueChanged) { - m_SetOptions(p_Options); + m_SetOptions(options, notifyOnValueChanged); return this; } /// @brief Set value diff --git a/src/CP_SDK/UI/DefaultComponents/DefaultCDropdown.cpp b/src/CP_SDK/UI/DefaultComponents/DefaultCDropdown.cpp index 8b5c883..0bff403 100644 --- a/src/CP_SDK/UI/DefaultComponents/DefaultCDropdown.cpp +++ b/src/CP_SDK/UI/DefaultComponents/DefaultCDropdown.cpp @@ -79,8 +79,8 @@ namespace CP_SDK::UI::DefaultComponents { m_Icon->RTransform()->set_pivot (Vector2( 1.0f, 0.5f)); m_Icon->RTransform()->set_anchorMin (Vector2( 1.0f, 0.5f)); m_Icon->RTransform()->set_anchorMax (Vector2( 1.0f, 0.5f)); - m_Icon->RTransform()->set_anchoredPosition(Vector2(-0.5f, 0.0f)); - m_Icon->RTransform()->set_sizeDelta (Vector2( 4.0f, 4.0f)); + m_Icon->RTransform()->set_anchoredPosition(Vector2(-1.5f, 0.0f)); + m_Icon->RTransform()->set_sizeDelta (Vector2( 3.0f, 3.0f)); m_Icon->SetSprite(UISystem::GetUIDownArrowSprite().Ptr()); m_Icon->OnClick({this, &DefaultCDropdown::Button_OnClick}); @@ -150,22 +150,28 @@ namespace CP_SDK::UI::DefaultComponents { m_Icon->SetInteractable(p_Interactable); } /// @brief Set available options - /// @param p_Options New options list - void DefaultCDropdown::SetOptions_Impl(const std::vector& p_Options) + /// @param options New options list + /// @param notifyOnValueChanged Should notify on value changed? + void DefaultCDropdown::SetOptions_Impl(const std::vector& options, bool notifyOnValueChanged) { + auto l_ValueStr = std::u16string(GetValue_Impl()); m_Options.clear(); - if (!p_Options.empty()) + if (!options.empty()) { - m_Options.reserve(p_Options.size()); - m_Options.insert(m_Options.begin(), p_Options.begin(), p_Options.end()); + m_Options.reserve(options.size()); + m_Options.insert(options.begin(), options.begin(), options.end()); } - if (m_Value > m_Options.size()) + auto l_It = std::find(m_Options.begin(), m_Options.end(), l_ValueStr); + auto l_NewValue = l_It != m_Options.end() ? l_It - m_Options.begin() : -1; + if (m_Value != l_NewValue) { - m_Value = -1; + m_Value = l_NewValue; Refresh(); - Notify(); + + if (notifyOnValueChanged) + Notify(); } else Refresh(); diff --git a/src/CP_SDK/UI/DefaultComponents/DefaultCTextInput.cpp b/src/CP_SDK/UI/DefaultComponents/DefaultCTextInput.cpp index c9c4bee..efa4761 100644 --- a/src/CP_SDK/UI/DefaultComponents/DefaultCTextInput.cpp +++ b/src/CP_SDK/UI/DefaultComponents/DefaultCTextInput.cpp @@ -89,7 +89,7 @@ namespace CP_SDK::UI::DefaultComponents { m_ValueText->SetAlign(TMPro::TextAlignmentOptions::MidlineLeft); m_ValueText->RTransform()->set_anchorMin (Vector2( 0.0f, 0.0f)); m_ValueText->RTransform()->set_anchorMax (Vector2( 1.0f, 1.0f)); - m_ValueText->RTransform()->set_anchoredPosition (Vector2(-2.5f, 0.0f)); + m_ValueText->RTransform()->set_anchoredPosition (Vector2(-1.5f, 0.0f)); m_ValueText->RTransform()->set_sizeDelta (Vector2(-5.0f, 0.0f)); m_Button = get_gameObject()->AddComponent(); From 2d4a38303f31ddd8e19410bbafa2f4f770b8a4cb Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 13:55:53 +0200 Subject: [PATCH 05/15] Data races, use after free fixes (#7) --- .github/workflows/build-ndk.yml | 2 +- .github/workflows/publish.yml | 2 +- .gitignore | 3 +- include/git_info.h | 4 +- qpm.json | 21 +- build.ps1 => scripts/build.ps1 | 0 scripts/build.sh | 36 ++ copy.ps1 => scripts/copy.ps1 | 0 scripts/copy.sh | 93 +++++ createqmod.ps1 => scripts/createqmod.ps1 | 0 scripts/createqmod.sh | 94 +++++ ndk-stack.ps1 => scripts/ndk-stack.ps1 | 0 scripts/ndk-stack.sh | 54 +++ .../pull-tombstone.ps1 | 0 scripts/pull-tombstone.sh | 82 +++++ .../pullandanalyze.ps1 | 0 scripts/pullandanalyze.sh | 32 ++ restart-game.ps1 => scripts/restart-game.ps1 | 0 scripts/restart-game.sh | 11 + .../start-logging.ps1 | 0 scripts/start-logging.sh | 115 +++++++ .../validate-modjson.ps1 | 0 scripts/validate-modjson.sh | 75 ++++ .../Animation/AnimationControllerInstance.hpp | 3 +- .../Animation/AnimationControllerManager.hpp | 2 +- shared/CP_SDK/ChatPlexService.hpp | 19 +- shared/CP_SDK/Network/WebClientCore.hpp | 6 +- shared/CP_SDK/Pool/MTObjectPool.hpp | 70 ++-- shared/CP_SDK/Pool/ObjectPool.hpp | 28 +- shared/CP_SDK/Unity/MTCoroutineStarter.hpp | 18 +- shared/CP_SDK/Unity/MTMainThreadInvoker.hpp | 18 +- shared/CP_SDK/Unity/MTThreadInvoker.hpp | 17 +- shared/CP_SDK/Unity/MonoPtrHolder.hpp | 10 +- shared/CP_SDK/Utils/Event.hpp | 11 +- shared/CP_SDK/Utils/MonoPtr.hpp | 46 ++- .../Animation/AnimationControllerInstance.cpp | 28 +- .../Animation/AnimationControllerManager.cpp | 10 +- src/CP_SDK/Animation/AnimationInfo.cpp | 18 +- src/CP_SDK/Animation/AnimationLoader.cpp | 53 +-- src/CP_SDK/Animation/WEBP/WEBPDecoder.cpp | 41 ++- src/CP_SDK/ChatPlexService.cpp | 303 ++++++++++------- src/CP_SDK/Config/JsonConfig.cpp | 47 ++- src/CP_SDK/Network/WebClientCore.cpp | 321 ++++++++++++------ src/CP_SDK/Network/WebResponse.cpp | 13 +- src/CP_SDK/Unity/MTCoroutineStarter.cpp | 117 +++---- src/CP_SDK/Unity/MTMainThreadInvoker.cpp | 104 +++--- src/CP_SDK/Unity/MTThreadInvoker.cpp | 153 ++++----- src/CP_SDK/Unity/MonoPtrHolder.cpp | 73 +++- src/CP_SDK/Unity/Texture2DU.cpp | 48 ++- src/CP_SDK/Unity/TextureRaw.cpp | 58 +++- src/CP_SDK/Utils/Json.cpp | 7 +- 51 files changed, 1590 insertions(+), 676 deletions(-) rename build.ps1 => scripts/build.ps1 (100%) create mode 100755 scripts/build.sh rename copy.ps1 => scripts/copy.ps1 (100%) create mode 100755 scripts/copy.sh rename createqmod.ps1 => scripts/createqmod.ps1 (100%) create mode 100755 scripts/createqmod.sh rename ndk-stack.ps1 => scripts/ndk-stack.ps1 (100%) create mode 100755 scripts/ndk-stack.sh rename pull-tombstone.ps1 => scripts/pull-tombstone.ps1 (100%) create mode 100755 scripts/pull-tombstone.sh rename pullandanalyze.ps1 => scripts/pullandanalyze.ps1 (100%) create mode 100755 scripts/pullandanalyze.sh rename restart-game.ps1 => scripts/restart-game.ps1 (100%) create mode 100755 scripts/restart-game.sh rename start-logging.ps1 => scripts/start-logging.ps1 (100%) create mode 100755 scripts/start-logging.sh rename validate-modjson.ps1 => scripts/validate-modjson.ps1 (100%) create mode 100755 scripts/validate-modjson.sh diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 37d439f..afc2b5c 100644 --- a/.github/workflows/build-ndk.yml +++ b/.github/workflows/build-ndk.yml @@ -53,7 +53,7 @@ jobs: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> ${GITHUB_OUTPUT} - name: Setup qpm - uses: Fernthedev/qpm-action@main + uses: Fernthedev/qpm-action@v1 with: workflow_token: ${{ secrets.GITHUB_TOKEN }} restore: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8163dde..ce2426f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,7 +43,7 @@ jobs: echo VERSION="${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - name: Setup qpm - uses: Fernthedev/qpm-action@main + uses: Fernthedev/qpm-action@v1 with: workflow_token: ${{ secrets.GITHUB_TOKEN }} restore: true diff --git a/.gitignore b/.gitignore index 61cccbb..4888a4a 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,5 @@ extern.cmake # QMOD Schema mod.json.schema -doxygen-build/ \ No newline at end of file +doxygen-build/ +.env diff --git a/include/git_info.h b/include/git_info.h index 8001fe1..96dda27 100644 --- a/include/git_info.h +++ b/include/git_info.h @@ -1,5 +1,5 @@ #pragma once #define GIT_USER "HardCPP" -#define GIT_BRANCH "dev" -#define GIT_COMMIT 0x51e3151 +#define GIT_BRANCH "review-fixes" +#define GIT_COMMIT 0x44c36c4 #define GIT_MODIFIED 1 diff --git a/qpm.json b/qpm.json index 4b98143..24509b9 100644 --- a/qpm.json +++ b/qpm.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/QuestPackageManager/QPM.Package/refs/heads/main/qpm.schema.json", "version": "0.4.0", "sharedDir": "shared", "dependenciesDir": "extern", @@ -15,28 +16,28 @@ "workspace": { "scripts": { "build": [ - "pwsh ./build.ps1" + "pwsh ./scripts/build.ps1" ], "clean": [ - "pwsh ./build.ps1 -clean" + "pwsh ./scripts/build.ps1 -clean" ], "copy": [ - "pwsh ./copy.ps1" + "pwsh ./scripts/copy.ps1" ], "log": [ - "pwsh ./start-logging.ps1" + "pwsh ./scripts/start-logging.ps1" ], "qmod": [ - "pwsh ./build.ps1 -clean", - "pwsh ./createqmod.ps1 -clean" + "pwsh ./scripts/build.ps1 -clean", + "pwsh ./scripts/createqmod.ps1 -clean" ], "qmod_backup": [ - "pwsh ./build.ps1 -clean", + "pwsh ./scripts/build.ps1 -clean", "qpm qmod build", - "pwsh ./createqmod.ps1 -clean" + "pwsh ./scripts/createqmod.ps1 -clean" ] }, - "ndk": "^27.2.12479018", + "ndk": "^27.3.13750724", "qmodIncludeDirs": [ "build", "extern/libs" @@ -52,7 +53,7 @@ }, { "id": "bs-cordl", - "versionRange": "^4008.*", + "versionRange": "4008.*", "additionalData": {} }, { diff --git a/build.ps1 b/scripts/build.ps1 similarity index 100% rename from build.ps1 rename to scripts/build.ps1 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..692fd07 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +project_root="$(cd -- "$script_dir/.." && pwd -P)" +clean=false + +usage() { + cat <<'EOF' +"Build" - Compiles your mod into a ".so" or ".a" library + +Arguments: + -clean, --clean Delete the build folder before compiling + -help, --help Show this help +EOF +} + +while (($#)); do + case "$1" in + -clean|--clean) clean=true ;; + -help|--help|-h) usage; exit 0 ;; + *) printf 'Unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +cd -- "$project_root" + +if [[ "$clean" == true && -d "$project_root/build" ]]; then + rm -rf -- "$project_root/build" +fi + +mkdir -p -- "$project_root/build" +cmake -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -B "$project_root/build" +cmake --build "$project_root/build" --parallel 12 diff --git a/copy.ps1 b/scripts/copy.ps1 similarity index 100% rename from copy.ps1 rename to scripts/copy.ps1 diff --git a/scripts/copy.sh b/scripts/copy.sh new file mode 100755 index 0000000..fa83112 --- /dev/null +++ b/scripts/copy.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +project_root="$(cd -- "$script_dir/.." && pwd -P)" +clean=false +log=false +use_debug=false +self_only=false +all=false +custom="" +log_file="" + +usage() { + cat <<'EOF' +"Copy" - Builds and copies your mod to your Quest, then starts Beat Saber + +Arguments: + -clean, --clean Perform a clean build + -useDebug, --use-debug Copy files from build/debug + -log, --log Start logging after launching Beat Saber + +Logging arguments: + -self, --self Only show this mod and crash logs + -all, --all Show all logs from the Beat Saber process + -custom, --custom REGEX + Add a custom logging pattern + -file, --file FILE Save logging output under the scripts folder + -help, --help Show this help +EOF +} + +require_value() { + if (($# < 2)); then + printf 'Missing value for %s\n' "$1" >&2 + exit 2 + fi +} + +while (($#)); do + case "$1" in + -clean|--clean) clean=true; shift ;; + -log|--log) log=true; shift ;; + -useDebug|--use-debug) use_debug=true; shift ;; + -self|--self) self_only=true; shift ;; + -all|--all) all=true; shift ;; + -custom|--custom) require_value "$@"; custom="$2"; shift 2 ;; + -custom:*|--custom=*) custom="${1#*:}"; [[ "$1" == --custom=* ]] && custom="${1#*=}"; shift ;; + -file|--file) require_value "$@"; log_file="$2"; shift 2 ;; + -file:*|--file=*) log_file="${1#*:}"; [[ "$1" == --file=* ]] && log_file="${1#*=}"; shift ;; + -help|--help|-h) usage; exit 0 ;; + *) printf 'Unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac +done + +cd -- "$project_root" + +build_args=() +[[ "$clean" == true ]] && build_args+=(--clean) +if "$script_dir/build.sh" "${build_args[@]}"; then + : +else + status=$? + printf 'Failed to build, exiting...\n' >&2 + exit "$status" +fi + +"$script_dir/validate-modjson.sh" + +if ! command -v jq >/dev/null 2>&1; then + printf 'Error: jq is required to read mod.json\n' >&2 + exit 1 +fi + +mapfile -t mod_files < <(jq -r '(.modFiles // [])[], (.lateModFiles // [])[]' "$project_root/mod.json") +for file_name in "${mod_files[@]}"; do + source_dir="$project_root/build" + [[ "$use_debug" == true ]] && source_dir="$project_root/build/debug" + adb push "$source_dir/$file_name" "/sdcard/ModData/com.beatgames.beatsaber/Modloader/mods/$file_name" +done + +"$script_dir/restart-game.sh" + +if [[ "$log" == true ]]; then + adb logcat -c + logging_args=() + [[ "$self_only" == true ]] && logging_args+=(--self) + [[ "$all" == true ]] && logging_args+=(--all) + [[ -n "$custom" ]] && logging_args+=(--custom "$custom") + [[ -n "$log_file" ]] && logging_args+=(--file "$log_file") + "$script_dir/start-logging.sh" "${logging_args[@]}" +fi diff --git a/createqmod.ps1 b/scripts/createqmod.ps1 similarity index 100% rename from createqmod.ps1 rename to scripts/createqmod.ps1 diff --git a/scripts/createqmod.sh b/scripts/createqmod.sh new file mode 100755 index 0000000..bdce95d --- /dev/null +++ b/scripts/createqmod.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +project_root="$(cd -- "$script_dir/.." && pwd -P)" +qmod_name="" +clean=false + +usage() { + cat <<'EOF' +"CreateQmod" - Packages mod.json and its files into a .qmod archive + +Usage: createqmod.sh [QMOD_NAME] [-clean] + +Arguments: + -qmodname, --qmod-name NAME Set the output name (defaults to mod.json's name) + -clean, --clean Build a fresh archive instead of updating an old one + -help, --help Show this help +EOF +} + +while (($#)); do + case "$1" in + -qmodname|--qmod-name) + (($# >= 2)) || { printf 'Missing qmod name\n' >&2; exit 2; } + qmod_name="$2" + shift 2 + ;; + -qmodname:*|--qmod-name=*) + qmod_name="${1#*:}" + [[ "$1" == --qmod-name=* ]] && qmod_name="${1#*=}" + shift + ;; + -clean|--clean) clean=true; shift ;; + -help|--help|-h) usage; exit 0 ;; + -*) printf 'Unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + *) + [[ -z "$qmod_name" ]] || { printf 'Unexpected argument: %s\n' "$1" >&2; exit 2; } + qmod_name="$1" + shift + ;; + esac +done + +cd -- "$project_root" + +for tool in jq zip; do + command -v "$tool" >/dev/null 2>&1 || { printf 'Error: %s is required\n' "$tool" >&2; exit 1; } +done + +mod_file="$project_root/mod.json" +[[ -f "$mod_file" ]] || { printf 'Error: mod.json was not found\n' >&2; exit 1; } + +if [[ -z "$qmod_name" ]]; then + qmod_name="$(jq -er '.name' "$mod_file")" +fi + +files=("$mod_file") +cover="$(jq -r '.coverImage // empty' "$mod_file")" +if [[ -n "$cover" && -f "$project_root/$cover" ]]; then + files+=("$project_root/$cover") +fi + +mapfile -t packaged_files < <(jq -r '(.modFiles // [])[], (.lateModFiles // [])[], (.libraryFiles // [])[]' "$mod_file") +for file_name in "${packaged_files[@]}"; do + path="$project_root/build/$file_name" + [[ -f "$path" ]] || path="$project_root/extern/libs/$file_name" + if [[ ! -f "$path" ]]; then + printf 'Error: could not find dependency: %s\n' "$path" >&2 + exit 1 + fi + files+=("$path") +done + +[[ "$clean" == true ]] && printf 'Making clean Qmod\n' + +output="$project_root/${qmod_name}.qmod" +temp_dir="$(mktemp -d)" +trap 'rm -rf -- "$temp_dir"' EXIT +temp_archive="$temp_dir/archive.zip" + +if [[ "$clean" == false && -f "$output" ]]; then + cp -- "$output" "$temp_archive" +fi + +if [[ -f "$temp_archive" ]]; then + zip -j -u "$temp_archive" "${files[@]}" +else + zip -j "$temp_archive" "${files[@]}" +fi + +mv -- "$temp_archive" "$output" +printf 'Created %s\n' "$output" diff --git a/ndk-stack.ps1 b/scripts/ndk-stack.ps1 similarity index 100% rename from ndk-stack.ps1 rename to scripts/ndk-stack.ps1 diff --git a/scripts/ndk-stack.sh b/scripts/ndk-stack.sh new file mode 100755 index 0000000..faeafa8 --- /dev/null +++ b/scripts/ndk-stack.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +project_root="$(cd -- "$script_dir/.." && pwd -P)" +log_name="log.log" + +usage() { + cat <<'EOF' +"NDK-Stack" - Processes a tombstone using the debug .so files + +Usage: ndk-stack.sh [LOG_NAME] + +Arguments: + -logName, --log-name FILE Tombstone to process (default: log.log) + -help, --help Show this help +EOF +} + +while (($#)); do + case "$1" in + -logName|--log-name) + (($# >= 2)) || { printf 'Missing log file name\n' >&2; exit 2; } + log_name="$2" + shift 2 + ;; + -logName:*|--log-name=*) + log_name="${1#*:}" + [[ "$1" == --log-name=* ]] && log_name="${1#*=}" + shift + ;; + -help|--help|-h) usage; exit 0 ;; + -*) printf 'Unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + *) log_name="$1"; shift ;; + esac +done + +cd -- "$project_root" + +if [[ -f "$project_root/ndkpath.txt" ]]; then + IFS= read -r ndk_path < "$project_root/ndkpath.txt" || true + ndk_path="${ndk_path%$'\r'}" +else + ndk_path="${ANDROID_NDK_HOME:-}" +fi + +[[ -n "$ndk_path" ]] || { printf 'Error: set ANDROID_NDK_HOME or create ndkpath.txt\n' >&2; exit 1; } +stack_script="$ndk_path/ndk-stack" +[[ -x "$stack_script" ]] || { printf 'Error: ndk-stack was not found at %s\n' "$stack_script" >&2; exit 1; } +[[ -f "$log_name" ]] || { printf 'Error: log file was not found: %s\n' "$log_name" >&2; exit 1; } + +"$stack_script" -sym "$project_root/build/debug/" < "$log_name" > "${log_name}_processed.log" +printf 'Created %s\n' "${log_name}_processed.log" diff --git a/pull-tombstone.ps1 b/scripts/pull-tombstone.ps1 similarity index 100% rename from pull-tombstone.ps1 rename to scripts/pull-tombstone.ps1 diff --git a/scripts/pull-tombstone.sh b/scripts/pull-tombstone.sh new file mode 100755 index 0000000..fa339d9 --- /dev/null +++ b/scripts/pull-tombstone.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +project_root="$(cd -- "$script_dir/.." && pwd -P)" +file_name="RecentCrash.log" +analyze=false + +usage() { + cat <<'EOF' +"Pull-Tombstone" - Pulls the most recent Beat Saber tombstone from your Quest + +Arguments: + -fileName, --file-name FILE Output file (default: RecentCrash.log) + -analyze, --analyze Process the file with ndk-stack afterward + -help, --help Show this help +EOF +} + +while (($#)); do + case "$1" in + -fileName|--file-name) + (($# >= 2)) || { printf 'Missing output file name\n' >&2; exit 2; } + file_name="$2" + shift 2 + ;; + -fileName:*|--file-name=*) + file_name="${1#*:}" + [[ "$1" == --file-name=* ]] && file_name="${1#*=}" + shift + ;; + -analyze|--analyze) analyze=true; shift ;; + -help|--help|-h) usage; exit 0 ;; + *) printf 'Unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac +done + +cd -- "$project_root" + +current_epoch="$(date +%s)" +recent_epoch="" +recent_tombstone="" + +for index in 0 1 2; do + remote="/sdcard/Android/data/com.beatgames.beatsaber/files/tombstone_0${index}" + if ! stats="$(adb shell stat "$remote" 2>&1)"; then + printf 'Failed to inspect tombstone_0%s: %s\n' "$index" "$stats" >&2 + exit 1 + fi + + modified="$(sed -nE 's/^Modify:[[:space:]]+([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}).*/\1/p' <<< "$stats" | head -n 1)" + if [[ -z "$modified" ]] || ! modified_epoch="$(date -d "$modified" +%s 2>/dev/null)"; then + printf 'Failed to read the modification time for tombstone_0%s\n' "$index" >&2 + exit 1 + fi + + delta=$((current_epoch - modified_epoch)) + if ((delta >= 0)); then + difference=$(((delta + 30) / 60)) + else + difference=$(((delta - 30) / 60)) + fi + + if ((difference == 1)); then + printf 'Found tombstone_0%s %s minute ago\n' "$index" "$difference" + else + printf 'Found tombstone_0%s %s minutes ago\n' "$index" "$difference" + fi + + if [[ -z "$recent_epoch" ]] || ((modified_epoch > recent_epoch)); then + recent_epoch="$modified_epoch" + recent_tombstone="$index" + fi +done + +printf 'Latest tombstone was tombstone_0%s\n' "$recent_tombstone" +adb pull "/sdcard/Android/data/com.beatgames.beatsaber/files/tombstone_0${recent_tombstone}" "$file_name" + +if [[ "$analyze" == true ]]; then + "$script_dir/ndk-stack.sh" --log-name "$file_name" +fi diff --git a/pullandanalyze.ps1 b/scripts/pullandanalyze.ps1 similarity index 100% rename from pullandanalyze.ps1 rename to scripts/pullandanalyze.ps1 diff --git a/scripts/pullandanalyze.sh b/scripts/pullandanalyze.sh new file mode 100755 index 0000000..d8c1c4b --- /dev/null +++ b/scripts/pullandanalyze.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +project_root="$(cd -- "$script_dir/.." && pwd -P)" + +if [[ "${1:-}" == "-help" || "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + printf 'Usage: pullandanalyze.sh TOMBSTONE_INDEX\n' + exit 0 +fi + +if (($# != 1)); then + printf 'Usage: pullandanalyze.sh TOMBSTONE_INDEX\n' >&2 + exit 2 +fi + +ndk_path="${ANDROID_NDK_HOME:-}" +[[ -n "$ndk_path" ]] || { printf 'Error: ANDROID_NDK_HOME is not set\n' >&2; exit 1; } +stack_script="$ndk_path/ndk-stack" +[[ -x "$stack_script" ]] || { printf 'Error: ndk-stack was not found at %s\n' "$stack_script" >&2; exit 1; } + +cd -- "$project_root" +filename="tombstone_0${1}" +adb pull "/storage/emulated/0/Android/data/com.beatgames.beatsaber/files/$filename" +"$stack_script" -sym "$project_root/build/debug/" < "$filename" > "${filename}_unstripped.txt" + +if command -v code >/dev/null 2>&1; then + code "${filename}_unstripped.txt" +else + printf 'Created %s (the code command was not found, so it was not opened)\n' "${filename}_unstripped.txt" +fi diff --git a/restart-game.ps1 b/scripts/restart-game.ps1 similarity index 100% rename from restart-game.ps1 rename to scripts/restart-game.ps1 diff --git a/scripts/restart-game.sh b/scripts/restart-game.sh new file mode 100755 index 0000000..9444092 --- /dev/null +++ b/scripts/restart-game.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +package="com.beatgames.beatsaber" +activity="$package/com.unity3d.player.UnityPlayerActivity" + +adb shell am force-stop "$package" +adb shell am start "$activity" +sleep 1 +adb shell am start "$activity" diff --git a/start-logging.ps1 b/scripts/start-logging.ps1 similarity index 100% rename from start-logging.ps1 rename to scripts/start-logging.ps1 diff --git a/scripts/start-logging.sh b/scripts/start-logging.sh new file mode 100755 index 0000000..7e6f82b --- /dev/null +++ b/scripts/start-logging.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +project_root="$(cd -- "$script_dir/.." && pwd -P)" +self_only=false +all=false +custom="" +log_file="" +exclude_header=false +show_help=false + +usage() { + if [[ "$exclude_header" == false ]]; then + cat <<'EOF' +"Start-Logging" - Logs Beat Saber using adb logcat + +Arguments: +EOF + fi + cat <<'EOF' + -self, --self Only show this mod and crash logs + -all, --all Show all logs from the Beat Saber process + -custom, --custom REGEX + Add a custom pattern; AndroidRuntime and CRASH are always included + -file, --file FILE Save output under the scripts folder + -help, --help Show this help +EOF +} + +while (($#)); do + case "$1" in + -self|--self) self_only=true; shift ;; + -all|--all) all=true; shift ;; + -custom|--custom) + (($# >= 2)) || { printf 'Missing custom pattern\n' >&2; exit 2; } + custom="$2" + shift 2 + ;; + -custom:*|--custom=*) + custom="${1#*:}" + [[ "$1" == --custom=* ]] && custom="${1#*=}" + shift + ;; + -file|--file) + (($# >= 2)) || { printf 'Missing output file\n' >&2; exit 2; } + log_file="$2" + shift 2 + ;; + -file:*|--file=*) + log_file="${1#*:}" + [[ "$1" == --file=* ]] && log_file="${1#*=}" + shift + ;; + -excludeHeader|--exclude-header) exclude_header=true; shift ;; + -help|--help|-h) show_help=true; shift ;; + *) printf 'Unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac +done + +if [[ "$show_help" == true ]]; then + usage + exit 0 +fi + +cd -- "$project_root" + +output_file="" +if [[ -n "$log_file" ]]; then + if [[ "$log_file" = /* ]]; then + output_file="$log_file" + else + output_file="$script_dir/$log_file" + fi +fi + +if [[ "$all" == true ]]; then + beat_saber_pid="" + for _ in 0 1 2 3; do + beat_saber_pid="$(adb shell pidof com.beatgames.beatsaber 2>/dev/null | tr -d '\r')" + [[ -n "$beat_saber_pid" ]] && break + sleep 0.1 + done + + if [[ -z "$beat_saber_pid" ]]; then + printf 'Could not connect to adb or find Beat Saber, exiting...\n' >&2 + exit 1 + fi + + printf 'Logging using command: adb logcat --pid %s\n' "$beat_saber_pid" + if [[ -n "$output_file" ]]; then + adb logcat --pid "$beat_saber_pid" > "$output_file" + else + adb logcat --pid "$beat_saber_pid" + fi + exit +fi + +pattern="" +if [[ "$self_only" == true ]]; then + command -v jq >/dev/null 2>&1 || { printf 'Error: jq is required for --self\n' >&2; exit 1; } + mod_name="$(jq -er '.name' "$project_root/mod.json")" + pattern+="$mod_name|" +fi +[[ -n "$custom" ]] && pattern+="$custom|" +[[ -n "$pattern" ]] || pattern='QuestHook|modloader|scotland2|ChatPlexSDK|' +pattern+='AndroidRuntime|CRASH' + +printf 'Logging using command: adb logcat | grep -E %q\n' "$pattern" +if [[ -n "$output_file" ]]; then + adb logcat | grep --line-buffered -E "$pattern" > "$output_file" +else + adb logcat | grep --line-buffered -E "$pattern" +fi diff --git a/validate-modjson.ps1 b/scripts/validate-modjson.ps1 similarity index 100% rename from validate-modjson.ps1 rename to scripts/validate-modjson.ps1 diff --git a/scripts/validate-modjson.sh b/scripts/validate-modjson.sh new file mode 100755 index 0000000..9b7cf9a --- /dev/null +++ b/scripts/validate-modjson.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +exit +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +project_root="$(cd -- "$script_dir/.." && pwd -P)" +mod_file="$project_root/mod.json" +template_file="$project_root/mod.template.json" +schema_url="https://raw.githubusercontent.com/Lauriethefish/QuestPatcher.QMod/refs/heads/main/QuestPatcher.QMod/Resources/qmod.schema.json" + +cd -- "$project_root" + +if [[ ! -f "$mod_file" ]]; then + if [[ -f "$template_file" ]]; then + if command -v qpm-rust >/dev/null 2>&1; then + qpm-rust qmod build + elif command -v qpm >/dev/null 2>&1; then + qpm qmod build + else + printf 'Error: qpm is required to generate mod.json from mod.template.json\n' >&2 + exit 1 + fi + else + printf 'Error: mod.json and mod.template.json were not present\n' >&2 + exit 1 + fi +fi + +printf 'Creating qmod from mod.json\n' +command -v curl >/dev/null 2>&1 || { printf 'Error: curl is required to download the qmod schema\n' >&2; exit 1; } +command -v python3 >/dev/null 2>&1 || { printf 'Error: Python 3 and jsonschema are required to validate mod.json\n' >&2; exit 1; } + +schema_file="$(mktemp)" +trap 'rm -f -- "$schema_file"' EXIT +curl -fsSL "$schema_url" -o "$schema_file" + +printf 'Validating mod.json...\n' +python3 - "$mod_file" "$schema_file" <<'PY' +import json +import sys + +try: + import jsonschema +except ImportError: + print("Error: install the Python 'jsonschema' package to validate mod.json", file=sys.stderr) + raise SystemExit(1) + +mod_path, schema_path = sys.argv[1:] +try: + with open(mod_path, encoding="utf-8") as mod_stream: + instance = json.load(mod_stream) + with open(schema_path, encoding="utf-8") as schema_stream: + schema = json.load(schema_stream) +except (OSError, json.JSONDecodeError) as error: + print(f"Error: {error}", file=sys.stderr) + raise SystemExit(1) + +validator_class = jsonschema.validators.validator_for(schema) +try: + validator_class.check_schema(schema) +except jsonschema.SchemaError as error: + print(f"Error: invalid qmod schema: {error.message}", file=sys.stderr) + raise SystemExit(1) + +validator = validator_class(schema, format_checker=validator_class.FORMAT_CHECKER) +errors = sorted(validator.iter_errors(instance), key=lambda error: list(error.absolute_path)) +if errors: + print("Error: mod.json is not valid", file=sys.stderr) + for error in errors: + location = "$" + for part in error.absolute_path: + location += f"[{part}]" if isinstance(part, int) else f".{part}" + print(f" {location}: {error.message}", file=sys.stderr) + raise SystemExit(1) +PY diff --git a/shared/CP_SDK/Animation/AnimationControllerInstance.hpp b/shared/CP_SDK/Animation/AnimationControllerInstance.hpp index b7de08b..f345999 100644 --- a/shared/CP_SDK/Animation/AnimationControllerInstance.hpp +++ b/shared/CP_SDK/Animation/AnimationControllerInstance.hpp @@ -39,6 +39,7 @@ namespace CP_SDK::Animation { std::vector<_v::MonoPtr<_u::Image>> m_ActiveImages; int m_ActiveCount; std::vector<_u::Rect> m_UVs; + _v::MonoPtr<_u::Texture2D> m_Texture; public: _v::MonoPtr<_u::Sprite> FirstFrame; @@ -77,4 +78,4 @@ namespace CP_SDK::Animation { }; -} ///< namespace CP_SDK::Animation \ No newline at end of file +} ///< namespace CP_SDK::Animation diff --git a/shared/CP_SDK/Animation/AnimationControllerManager.hpp b/shared/CP_SDK/Animation/AnimationControllerManager.hpp index e8cb8b2..6deda7d 100644 --- a/shared/CP_SDK/Animation/AnimationControllerManager.hpp +++ b/shared/CP_SDK/Animation/AnimationControllerManager.hpp @@ -51,4 +51,4 @@ namespace CP_SDK::Animation { } ///< namespace CP_SDK::Animation -CP_SDK_IL2CPP_INHERIT_HELPERS(CP_SDK::Animation::AnimationControllerManager); \ No newline at end of file +CP_SDK_IL2CPP_INHERIT_HELPERS(CP_SDK::Animation::AnimationControllerManager); diff --git a/shared/CP_SDK/ChatPlexService.hpp b/shared/CP_SDK/ChatPlexService.hpp index cb6d11d..56805fa 100644 --- a/shared/CP_SDK/ChatPlexService.hpp +++ b/shared/CP_SDK/ChatPlexService.hpp @@ -7,6 +7,7 @@ #include "Utils/Delegate.hpp" #include +#include #include #include #include @@ -45,9 +46,9 @@ namespace CP_SDK { }; private: - static bool m_ThreadCondition; + static std::atomic_bool m_ThreadCondition; static _u::il2cpp_aware_thread* m_Thread; - static EState m_State; + static std::atomic m_State; static _v::WebClientCore::Ptr m_WebClientCore; static _v::JsonRPCClient::Ptr m_JsonRPCClient; static std::u16string m_LinkRequestID; @@ -60,12 +61,12 @@ namespace CP_SDK { static std::u16string m_DeviceName; public: - static const EState State(); - static const std::u16string_view Token(); - static const std::u16string_view LinkCode(); - static const std::u16string_view LastError(); - static const std::u16string_view ActiveSubscription(); - static const std::vector& UnlockedFeatures(); + static const EState State(); + static const std::u16string_view Token(); + static const std::u16string_view LinkCode(); + static const std::u16string_view LastError(); + static const std::u16string_view ActiveSubscription(); + static const std::vector UnlockedFeatures(); static _v::Event StateChanged; @@ -116,4 +117,4 @@ namespace CP_SDK { }; -} ///< namespace CP_SDK \ No newline at end of file +} ///< namespace CP_SDK diff --git a/shared/CP_SDK/Network/WebClientCore.hpp b/shared/CP_SDK/Network/WebClientCore.hpp index bbc2693..7de3ab7 100644 --- a/shared/CP_SDK/Network/WebClientCore.hpp +++ b/shared/CP_SDK/Network/WebClientCore.hpp @@ -4,6 +4,7 @@ #include +#include #include #include @@ -175,9 +176,10 @@ namespace CP_SDK::Network { _u::CancellationToken token, _v::Action callback, bool dontRetry, - _v::Action progress + _v::Action progress, + bool dispatchCallback ); }; -} ///< namespace CP_SDK::Network \ No newline at end of file +} ///< namespace CP_SDK::Network diff --git a/shared/CP_SDK/Pool/MTObjectPool.hpp b/shared/CP_SDK/Pool/MTObjectPool.hpp index ca8a3c4..607334f 100644 --- a/shared/CP_SDK/Pool/MTObjectPool.hpp +++ b/shared/CP_SDK/Pool/MTObjectPool.hpp @@ -53,24 +53,29 @@ namespace CP_SDK::Pool { if (maxSize <= 0) throw std::runtime_error("Max Size must be greater than 0"); + if (defaultCapacity < 0 || defaultCapacity > maxSize) + throw std::runtime_error("Default capacity must be between 0 and maxSize"); m_CreateFunc = createFunc; m_MaxSize = maxSize; m_ActionOnGet = actionOnGet; m_ActionOnRelease = actionOnRelease; m_ActionOnDestroy = actionOnDestroy; + m_CountAll = 0; m_CollectionCheck = collectionCheck; m_Vector.reserve(maxSize); while (defaultCapacity-- > 0) + { m_Vector.push_back(m_CreateFunc()); + m_CountAll++; + } } /// @brief Destructor ~MTObjectPool() { - ChatPlexSDK::Logger()->Error(u"~MTObjectPool"); - Clear(); + try { Clear(); } catch (...) { } } /// @brief Constructor @@ -92,12 +97,20 @@ namespace CP_SDK::Pool { /// @brief Active elements int CountActive() { - return m_CountAll - m_Vector.size(); + // lock (m_Stack) + { + std::lock_guard l_Lock(m_Mutex); + return m_CountAll - static_cast(m_Vector.size()); + } } /// @brief Released element int CountInactive() override { - return m_Vector.size(); + // lock (m_Stack) + { + std::lock_guard l_Lock(m_Mutex); + return static_cast(m_Vector.size()); + } } public: @@ -105,16 +118,14 @@ namespace CP_SDK::Pool { t_Type Get() override { t_Type l_Result{}; + bool l_Create = false; - //lock (m_Stack) + // lock (m_Stack) { std::lock_guard l_Lock(m_Mutex); if (m_Vector.size() == 0) - { - l_Result = m_CreateFunc(); - m_CountAll++; - } + l_Create = true; else { l_Result = m_Vector.back(); @@ -122,6 +133,13 @@ namespace CP_SDK::Pool { } } + if (l_Create) + { + l_Result = m_CreateFunc(); + std::lock_guard l_Lock(m_Mutex); + m_CountAll++; + } + m_ActionOnGet(l_Result); return l_Result; } @@ -129,39 +147,47 @@ namespace CP_SDK::Pool { /// @param p_Element Element to release void Release(t_Type& p_Element) override { - //lock (m_Stack) + m_ActionOnRelease(p_Element); + + bool l_Destroy = false; + + // lock (m_Stack) { std::lock_guard l_Lock(m_Mutex); if (m_CollectionCheck && m_Vector.size() > 0 && std::find(m_Vector.begin(), m_Vector.end(), p_Element) != m_Vector.end()) throw std::runtime_error("Trying to release an object that has already been released to the pool."); - m_ActionOnRelease(p_Element); - - if (CountInactive() < m_MaxSize) + if (static_cast(m_Vector.size()) < m_MaxSize) m_Vector.push_back(const_cast(p_Element)); else - m_ActionOnDestroy(p_Element); + { + m_CountAll--; + l_Destroy = true; + } } + + if (l_Destroy) + m_ActionOnDestroy(p_Element); } public: /// @brief Clear the object pool void Clear() override { - //lock (m_Stack) + std::vector l_ToDestroy; + + // lock (m_Stack) { std::lock_guard l_Lock(m_Mutex); - - for (auto& l_Current : m_Vector) - m_ActionOnDestroy(l_Current); - - m_Vector.clear(); + l_ToDestroy.swap(m_Vector); + m_CountAll -= static_cast(l_ToDestroy.size()); } - m_CountAll = 0; + for (auto& l_Current : l_ToDestroy) + m_ActionOnDestroy(l_Current); } }; -} ///< namespace CP_SDK::Pool \ No newline at end of file +} ///< namespace CP_SDK::Pool diff --git a/shared/CP_SDK/Pool/ObjectPool.hpp b/shared/CP_SDK/Pool/ObjectPool.hpp index 608aede..6986148 100644 --- a/shared/CP_SDK/Pool/ObjectPool.hpp +++ b/shared/CP_SDK/Pool/ObjectPool.hpp @@ -51,23 +51,29 @@ namespace CP_SDK::Pool { if (maxSize <= 0) throw std::runtime_error("Max Size must be greater than 0"); + if (defaultCapacity < 0 || defaultCapacity > maxSize) + throw std::runtime_error("Default capacity must be between 0 and maxSize"); m_CreateFunc = createFunc; m_MaxSize = maxSize; m_ActionOnGet = actionOnGet; m_ActionOnRelease = actionOnRelease; m_ActionOnDestroy = actionOnDestroy; + m_CountAll = 0; m_CollectionCheck = collectionCheck; m_Vector.reserve(maxSize); while (defaultCapacity-- > 0) + { m_Vector.push_back(m_CreateFunc()); + m_CountAll++; + } } /// @brief Destructor ~ObjectPool() { - Clear(); + try { Clear(); } catch (...) { } } /// @brief Constructor @@ -89,12 +95,12 @@ namespace CP_SDK::Pool { /// @brief Active elements int CountActive() { - return m_CountAll - m_Vector.size(); + return m_CountAll - static_cast(m_Vector.size()); } /// @brief Released element int CountInactive() override { - return m_Vector.size(); + return static_cast(m_Vector.size()); } public: @@ -129,21 +135,23 @@ namespace CP_SDK::Pool { if (CountInactive() < m_MaxSize) m_Vector.push_back(const_cast(p_Element)); else + { m_ActionOnDestroy(p_Element); + m_CountAll--; + } } public: /// @brief Clear the object pool void Clear() override { - for (auto& l_Current : m_Vector) - m_ActionOnDestroy(l_Current); - - m_Vector.clear(); + std::vector l_ToDestroy; + l_ToDestroy.swap(m_Vector); + m_CountAll -= static_cast(l_ToDestroy.size()); - m_CountAll = 0; + for (auto &l_Current : l_ToDestroy) + m_ActionOnDestroy(l_Current); } - }; -} ///< namespace CP_SDK::Pool \ No newline at end of file +} ///< namespace CP_SDK::Pool diff --git a/shared/CP_SDK/Unity/MTCoroutineStarter.hpp b/shared/CP_SDK/Unity/MTCoroutineStarter.hpp index 75652a8..63991b8 100644 --- a/shared/CP_SDK/Unity/MTCoroutineStarter.hpp +++ b/shared/CP_SDK/Unity/MTCoroutineStarter.hpp @@ -8,6 +8,7 @@ #include #include +#include #include namespace CP_SDK::Unity { @@ -29,21 +30,12 @@ namespace CP_SDK::Unity { CP_SDK_IL2CPP_DECLARE_CTOR(MTCoroutineStarter); CP_SDK_IL2CPP_DECLARE_DTOR_MONOBEHAVIOUR(MTCoroutineStarter); - private: - /// @brief Queue class - struct Queue - { - _v::MonoPtr* Data; - int WritePos; - }; - private: static MTCoroutineStarter* m_Instance; /// Singleton - static Queue** m_Queues; ///< Queues instance - static bool m_Queued; ///< Have queued actions - static int m_FrontQueue; ///< Current front queue - static std::mutex m_Mutex; ///< Lock mutex + static std::deque<_v::MonoPtr> m_Queue; ///< Pending coroutines + static bool m_Accepting; ///< Accept new coroutines + static std::mutex m_Mutex; ///< Lock mutex public: /// @brief Unity GameObject initialize @@ -76,4 +68,4 @@ namespace CP_SDK::Unity { } ///< namespace CP_SDK::Unity -CP_SDK_IL2CPP_INHERIT_HELPERS(CP_SDK::Unity::MTCoroutineStarter); \ No newline at end of file +CP_SDK_IL2CPP_INHERIT_HELPERS(CP_SDK::Unity::MTCoroutineStarter); diff --git a/shared/CP_SDK/Unity/MTMainThreadInvoker.hpp b/shared/CP_SDK/Unity/MTMainThreadInvoker.hpp index 2d00d10..054fa24 100644 --- a/shared/CP_SDK/Unity/MTMainThreadInvoker.hpp +++ b/shared/CP_SDK/Unity/MTMainThreadInvoker.hpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace CP_SDK::Unity { @@ -27,21 +28,12 @@ namespace CP_SDK::Unity { CP_SDK_IL2CPP_DECLARE_CTOR(MTMainThreadInvoker); CP_SDK_IL2CPP_DECLARE_DTOR_MONOBEHAVIOUR(MTMainThreadInvoker); - private: - /// @brief Queue class - struct Queue - { - _v::Action<> ** Data; - int WritePos; - }; - private: static MTMainThreadInvoker* m_Instance; /// Singleton - static Queue** m_Queues; ///< Queues instance - static bool m_Queued; ///< Have queued actions - static int m_FrontQueue; ///< Current front queue - static std::mutex m_Mutex; ///< Lock mutex + static std::deque<_v::Action<>> m_Queue; ///< Pending actions + static bool m_Accepting; ///< Accept new actions + static std::mutex m_Mutex; ///< Lock mutex public: /// @brief Unity GameObject initialize @@ -62,4 +54,4 @@ namespace CP_SDK::Unity { } ///< namespace CP_SDK::Unity -CP_SDK_IL2CPP_INHERIT_HELPERS(CP_SDK::Unity::MTMainThreadInvoker); \ No newline at end of file +CP_SDK_IL2CPP_INHERIT_HELPERS(CP_SDK::Unity::MTMainThreadInvoker); diff --git a/shared/CP_SDK/Unity/MTThreadInvoker.hpp b/shared/CP_SDK/Unity/MTThreadInvoker.hpp index 4f471a7..316589d 100644 --- a/shared/CP_SDK/Unity/MTThreadInvoker.hpp +++ b/shared/CP_SDK/Unity/MTThreadInvoker.hpp @@ -4,6 +4,8 @@ #include "../Utils/Il2cpp.hpp" #include "../Utils/MonoPtr.hpp" +#include +#include #include #include @@ -23,21 +25,12 @@ namespace CP_SDK::Unity { /// @brief Thread task system class CP_SDK_EXPORT MTThreadInvoker { - private: - /// @brief Queue class - struct Queue - { - _v::Action<>** Data; - int WritePos; - }; - private: static bool m_RunCondition; ///< Run condition static il2cpp_utils::il2cpp_aware_thread* m_UpdateThread; ///< Update thread - static Queue** m_Queues; ///< Queues instance - static bool m_Queued; ///< Have queued actions - static int m_FrontQueue; ///< Current front queue + static std::deque<_v::Action<>> m_Queue; ///< Pending actions static std::mutex m_Mutex; ///< Lock mutex + static std::condition_variable m_Condition; ///< Worker wakeup public: /// @brief Initialize @@ -56,4 +49,4 @@ namespace CP_SDK::Unity { }; -} ///< namespace CP_SDK::Unity \ No newline at end of file +} ///< namespace CP_SDK::Unity diff --git a/shared/CP_SDK/Unity/MonoPtrHolder.hpp b/shared/CP_SDK/Unity/MonoPtrHolder.hpp index b0b529e..038732b 100644 --- a/shared/CP_SDK/Unity/MonoPtrHolder.hpp +++ b/shared/CP_SDK/Unity/MonoPtrHolder.hpp @@ -32,7 +32,9 @@ namespace CP_SDK::Unity { /// @brief Ref counter wrapper class Wrapper { - std::atomic_int32_t m_Count; + friend class MonoPtrHolder; + + std::atomic_int32_t m_Count { 0 }; public: Il2CppObject* Ptr; @@ -40,7 +42,7 @@ namespace CP_SDK::Unity { /// @brief Grab a reference to this wrapper void Grab(); /// @brief Drop a reference to this wrapper - void Drop(); + void Drop() noexcept; }; private: @@ -49,7 +51,7 @@ namespace CP_SDK::Unity { static std::map m_PointersToWrapper; public: - /// @brief Get or register a new wrapper + /// @brief Get or register a new wrapper and acquire one reference /// @param p_Pointer Il2Cpp object static Wrapper* GetOrRegister(Il2CppObject* p_Pointer); /// @brief Release a wrapper @@ -60,4 +62,4 @@ namespace CP_SDK::Unity { } ///< namespace CP_SDK::Unity -CP_SDK_IL2CPP_INHERIT_HELPERS(CP_SDK::Unity::MonoPtrHolder); \ No newline at end of file +CP_SDK_IL2CPP_INHERIT_HELPERS(CP_SDK::Unity::MonoPtrHolder); diff --git a/shared/CP_SDK/Utils/Event.hpp b/shared/CP_SDK/Utils/Event.hpp index fcd5a37..ebcce3e 100644 --- a/shared/CP_SDK/Utils/Event.hpp +++ b/shared/CP_SDK/Utils/Event.hpp @@ -62,9 +62,8 @@ namespace CP_SDK::Utils { /// @brief Clear void Clear() { - m_Mutex.lock(); + std::lock_guard l_Guard(m_Mutex); m_Delegates.clear(); - m_Mutex.unlock(); } /// Invoke @@ -77,9 +76,11 @@ namespace CP_SDK::Utils { /// @p_Args...: Arguments void Invoke(t_Args... p_Args) { - m_Mutex.lock(); - std::vector> l_Copy = m_Delegates; - m_Mutex.unlock(); + std::vector> l_Copy; + { + std::lock_guard l_Guard(m_Mutex); + l_Copy = m_Delegates; + } for (const auto & l_Delegate : l_Copy) l_Delegate.Invoke(std::forward(p_Args)...); diff --git a/shared/CP_SDK/Utils/MonoPtr.hpp b/shared/CP_SDK/Utils/MonoPtr.hpp index 5e08201..5a3c4c8 100644 --- a/shared/CP_SDK/Utils/MonoPtr.hpp +++ b/shared/CP_SDK/Utils/MonoPtr.hpp @@ -15,9 +15,7 @@ namespace CP_SDK::Utils { MonoPtr() : m_Wrapper(nullptr) { } MonoPtr(t_Ptr* p_Pointer) : m_Wrapper(Unity::MonoPtrHolder::GetOrRegister((Il2CppObject*)p_Pointer)) - { - if (m_Wrapper) m_Wrapper->Grab(); - } + { } MonoPtr(const MonoPtr& p_Other) : m_Wrapper(p_Other.m_Wrapper) { @@ -38,10 +36,18 @@ namespace CP_SDK::Utils { t_Ptr* Ptr(bool p_Throw = true) const { auto l_IsDead = !m_Wrapper || !m_Wrapper->Ptr; - if constexpr (std::is_assignable_v) + if (l_IsDead) + { + if (p_Throw) + throw NullHandleException(); + + return nullptr; + } + + if constexpr (std::is_base_of_v) { auto l_UObject = reinterpret_cast(m_Wrapper->Ptr); - if (l_IsDead || !l_UObject->___m_CachedPtr.m_value) + if (!l_UObject->___m_CachedPtr.m_value) l_IsDead = true; } @@ -56,7 +62,7 @@ namespace CP_SDK::Utils { bool IsUnityObject() { - if constexpr (std::is_assignable_v) + if constexpr (std::is_base_of_v) return true; return false; @@ -72,31 +78,43 @@ namespace CP_SDK::Utils { MonoPtr& operator=(t_Ptr* p_Pointer) { if (m_Wrapper && m_Wrapper->Ptr == (Il2CppObject*)p_Pointer) return *this; - if (m_Wrapper) m_Wrapper->Drop(); + if (!p_Pointer) { + auto l_OldWrapper = m_Wrapper; m_Wrapper = nullptr; + if (l_OldWrapper) l_OldWrapper->Drop(); return *this; } - m_Wrapper = Unity::MonoPtrHolder::GetOrRegister((Il2CppObject*)p_Pointer); - if (m_Wrapper) m_Wrapper->Grab(); + auto l_NewWrapper = Unity::MonoPtrHolder::GetOrRegister((Il2CppObject*)p_Pointer); + auto l_OldWrapper = m_Wrapper; + m_Wrapper = l_NewWrapper; + if (l_OldWrapper) l_OldWrapper->Drop(); return *this; } MonoPtr& operator=(const MonoPtr& p_Other) { - if (m_Wrapper) m_Wrapper->Drop(); - m_Wrapper = p_Other.m_Wrapper; - if (m_Wrapper) m_Wrapper->Grab(); + if (this == &p_Other) return *this; + + auto l_NewWrapper = p_Other.m_Wrapper; + if (l_NewWrapper) l_NewWrapper->Grab(); + + auto l_OldWrapper = m_Wrapper; + m_Wrapper = l_NewWrapper; + if (l_OldWrapper) l_OldWrapper->Drop(); return *this; } MonoPtr& operator=(MonoPtr&& p_Other) { - if (m_Wrapper) m_Wrapper->Drop(); + if (this == &p_Other) return *this; + + auto l_OldWrapper = m_Wrapper; m_Wrapper = p_Other.m_Wrapper; p_Other.m_Wrapper = nullptr; + if (l_OldWrapper) l_OldWrapper->Drop(); return *this; } @@ -142,4 +160,4 @@ namespace CP_SDK::Utils { template constexpr bool operator==(UnityW const& p_Left, CP_SDK::Utils::MonoPtr const& p_Right) { return p_Left.isAlive() == p_Right.operator bool() && p_Left.unsafePtr() == p_Right.Ptr(false); -} \ No newline at end of file +} diff --git a/src/CP_SDK/Animation/AnimationControllerInstance.cpp b/src/CP_SDK/Animation/AnimationControllerInstance.cpp index a278912..48bd165 100644 --- a/src/CP_SDK/Animation/AnimationControllerInstance.cpp +++ b/src/CP_SDK/Animation/AnimationControllerInstance.cpp @@ -2,9 +2,14 @@ #include "CP_SDK/ChatPlexSDK.hpp" #include +#include #include #include +#include +#include +#include + using namespace UnityEngine; using namespace UnityEngine::UI; @@ -17,15 +22,19 @@ namespace CP_SDK::Animation { AnimationControllerInstance::AnimationControllerInstance(CP_SDK_PRIV_TAG_ARG(), _v::CMonoPtrRef p_Texture, const std::vector& p_UVs, const std::vector& p_Delays) : m_IsDelayConsistent(true), m_ActiveCount(0), Frames(p_UVs.size()), CurrentFrameIndex(0) { + if (!p_Texture || p_UVs.empty() || p_UVs.size() != p_Delays.size()) + throw std::invalid_argument("Animation atlas, UVs and delays are inconsistent"); + int l_FirstDelay = -1; - m_UVs = p_UVs; + m_UVs = p_UVs; + m_Texture = p_Texture; Frames.resize(p_UVs.size()); - Delays = p_Delays; + Delays = p_Delays; auto l_Width = const_cast<_v::MonoPtr&>(p_Texture)->get_width(); auto l_Height = const_cast<_v::MonoPtr&>(p_Texture)->get_height(); - for (int l_Frame = 0; l_Frame < p_UVs.size(); ++l_Frame) + for (std::size_t l_Frame = 0; l_Frame < p_UVs.size(); ++l_Frame) { auto& l_CurrentUV = p_UVs[l_Frame]; Frames[l_Frame] = Sprite::Create( @@ -54,7 +63,18 @@ namespace CP_SDK::Animation { AnimationControllerInstance::~AnimationControllerInstance() { m_ActiveImages.clear(); + for (auto& l_Frame : Frames) + { + if (l_Frame) + Object::Destroy(l_Frame.Ptr(false)); + } Frames.clear(); + FirstFrame = nullptr; + + if (m_Texture) + Object::Destroy(m_Texture.Ptr(false)); + + m_Texture = nullptr; } //////////////////////////////////////////////////////////////////////////// @@ -136,4 +156,4 @@ namespace CP_SDK::Animation { } } -} ///< namespace CP_SDK::Animation \ No newline at end of file +} ///< namespace CP_SDK::Animation diff --git a/src/CP_SDK/Animation/AnimationControllerManager.cpp b/src/CP_SDK/Animation/AnimationControllerManager.cpp index 51f12af..0f1f816 100644 --- a/src/CP_SDK/Animation/AnimationControllerManager.cpp +++ b/src/CP_SDK/Animation/AnimationControllerManager.cpp @@ -4,6 +4,8 @@ #include #include +#include + using namespace UnityEngine; namespace CP_SDK::Animation { @@ -35,6 +37,9 @@ namespace CP_SDK::Animation { /// @param p_Delays Delays AnimationControllerInstance::Ptr AnimationControllerManager::Register(std::u16string p_ID, _v::CMonoPtrRef p_Atlas, const std::vector& p_UVs, const std::vector& p_Delays) { + if (!p_Atlas || p_UVs.empty() || p_UVs.size() != p_Delays.size()) + return nullptr; + auto l_ControllerInstance = m_RegisteredDict.contains(p_ID) ? m_RegisteredDict[p_ID] : nullptr; if (!l_ControllerInstance) { @@ -46,9 +51,10 @@ namespace CP_SDK::Animation { m_Registered.push_back(l_ControllerInstance); m_QuickUpdateListCount++; } - catch (const std::exception&) + catch (const std::exception& l_Exception) { - + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Animation][AnimationControllerManager.Register] Failed to register animation:"); + ChatPlexSDK::Logger()->Error(l_Exception); } } else diff --git a/src/CP_SDK/Animation/AnimationInfo.cpp b/src/CP_SDK/Animation/AnimationInfo.cpp index e785f7d..68ecac1 100644 --- a/src/CP_SDK/Animation/AnimationInfo.cpp +++ b/src/CP_SDK/Animation/AnimationInfo.cpp @@ -1,5 +1,8 @@ #include "CP_SDK/Animation/AnimationInfo.hpp" +#include +#include + using namespace UnityEngine; namespace CP_SDK::Animation { @@ -10,13 +13,20 @@ namespace CP_SDK::Animation { /// @param p_FrameCount Animation frame count AnimationInfo::AnimationInfo(CP_SDK_PRIV_TAG_ARG(), int p_Width, int p_Height, uint32_t p_FrameCount) { + if (p_Width <= 0 || p_Height <= 0 || p_FrameCount == 0) + throw std::invalid_argument("Animation dimensions and frame count must be positive"); + + const auto l_FrameByteCount = static_cast(p_Width) * static_cast(p_Height) * 4ULL; + if (l_FrameByteCount > static_cast(std::numeric_limits::max())) + throw std::overflow_error("Animation frame is too large"); + Width = p_Width; - Height = p_Width; + Height = p_Height; Frames.reserve(p_FrameCount); Delays.resize(p_FrameCount); - for (int l_I = 0; l_I < p_FrameCount; ++l_I) - Frames.push_back(Array::NewLength(4 * p_Width * p_Height)); + for (uint32_t l_I = 0; l_I < p_FrameCount; ++l_I) + Frames.push_back(Array::NewLength(static_cast(l_FrameByteCount))); } /// @brief Destructor AnimationInfo::~AnimationInfo() @@ -36,4 +46,4 @@ namespace CP_SDK::Animation { return std::make_shared(CP_SDK_PRIV_TAG_VAL(), p_Width, p_Height, p_FrameCount); } -} ///< namespace CP_SDK::Animation \ No newline at end of file +} ///< namespace CP_SDK::Animation diff --git a/src/CP_SDK/Animation/AnimationLoader.cpp b/src/CP_SDK/Animation/AnimationLoader.cpp index fed0d00..1409ed4 100644 --- a/src/CP_SDK/Animation/AnimationLoader.cpp +++ b/src/CP_SDK/Animation/AnimationLoader.cpp @@ -7,6 +7,10 @@ #include #include +#include +#include +#include + using namespace UnityEngine; namespace CP_SDK::Animation { @@ -57,14 +61,29 @@ namespace CP_SDK::Animation { custom_types::Helpers::Coroutine AnimationLoader::Coroutine_ProcessLoadedAnimation(AnimationInfo::Ptr p_AnimationInfo, t_AnimatedCallback p_Callback) { if (!p_AnimationInfo) + { p_Callback(nullptr, {}, {}, 0, 0); + co_return; + } + + if (p_AnimationInfo->Width <= 0 || p_AnimationInfo->Height <= 0 || p_AnimationInfo->Frames.empty() + || p_AnimationInfo->Frames.size() != p_AnimationInfo->Delays.size()) + { + p_Callback(nullptr, {}, {}, 0, 0); + co_return; + } auto l_MaxAtlasTextureSize = GetMaxAtlasTextureSize(p_AnimationInfo); + if (l_MaxAtlasTextureSize <= 0) + { + p_Callback(nullptr, {}, {}, 0, 0); + co_return; + } _v::MonoPtr l_AtlasTexture = Texture2D::New_ctor(p_AnimationInfo->Width, p_AnimationInfo->Height); _v::MonoPtr<::Array> l_SubTextures = ::Array::NewLength(p_AnimationInfo->Frames.size()); - for (int l_FrameI = 0; l_FrameI < p_AnimationInfo->Frames.size(); ++l_FrameI) + for (std::size_t l_FrameI = 0; l_FrameI < p_AnimationInfo->Frames.size(); ++l_FrameI) { auto l_FrameTexture = Texture2D::New_ctor(p_AnimationInfo->Width, p_AnimationInfo->Height, TextureFormat::RGBA32, false); l_FrameTexture->set_wrapMode(TextureWrapMode::Clamp); @@ -95,30 +114,16 @@ namespace CP_SDK::Animation { /// @param p_AnimationInfo Animation infos int AnimationLoader::GetMaxAtlasTextureSize(AnimationInfo::Ptr& p_AnimationInfo) { - auto l_TestNumber = 2; - auto l_FramesInRow = 0; - auto l_FramesInColumn = 0; - auto l_FrameCount = p_AnimationInfo->Frames.size(); - - while (true) - { - /// Make sure the number of frames is cleanly divisible by our testNum - if (!(l_FrameCount % l_TestNumber != 0)) - l_FrameCount += l_FrameCount % l_TestNumber; - - l_FramesInRow = std::max(1, l_FrameCount / l_TestNumber); - l_FramesInColumn = l_FrameCount / l_FramesInRow; - - if (l_FramesInRow <= l_FramesInColumn) - break; - - l_TestNumber += 2; - } + if (!p_AnimationInfo || p_AnimationInfo->Frames.empty() || p_AnimationInfo->Width <= 0 || p_AnimationInfo->Height <= 0) + return 0; - auto l_TextureWidth = Mathf::Clamp(l_FramesInRow * p_AnimationInfo->Width, 0, 2048); - auto l_TextureHeight = Mathf::Clamp(l_FramesInColumn * p_AnimationInfo->Height, 0, 2048); + const auto l_FrameCount = static_cast(p_AnimationInfo->Frames.size()); + const auto l_FramesInRow = static_cast(std::ceil(std::sqrt(static_cast(l_FrameCount)))); + const auto l_FramesInColumn = (l_FrameCount + l_FramesInRow - 1) / l_FramesInRow; + const auto l_TextureWidth = std::min(l_FramesInRow * static_cast(p_AnimationInfo->Width), 2048ULL); + const auto l_TextureHeight = std::min(l_FramesInColumn * static_cast(p_AnimationInfo->Height), 2048ULL); - return std::max(l_TextureWidth, l_TextureHeight); + return static_cast(std::max(l_TextureWidth, l_TextureHeight)); } -} ///< namespace CP_SDK::Animation \ No newline at end of file +} ///< namespace CP_SDK::Animation diff --git a/src/CP_SDK/Animation/WEBP/WEBPDecoder.cpp b/src/CP_SDK/Animation/WEBP/WEBPDecoder.cpp index ad0f08f..9107d16 100644 --- a/src/CP_SDK/Animation/WEBP/WEBPDecoder.cpp +++ b/src/CP_SDK/Animation/WEBP/WEBPDecoder.cpp @@ -11,6 +11,8 @@ #include #include +#include + namespace CP_SDK::Animation::WEBP { /// @brief Async decode WEBP image @@ -20,6 +22,12 @@ namespace CP_SDK::Animation::WEBP { void WEBPDecoder::Process(_v::CMonoPtrRef<::Array> p_Raw, _v::Action p_Callback, _v::Action<_v::CMonoPtrRef> p_StaticCallback) { Unity::MTThreadInvoker::EnqueueOnThread([=]() -> void { + if (!p_Raw || p_Raw->get_Length() <= 0) + { + p_Callback(nullptr); + return; + } + WebPBitstreamFeatures l_Features = {}; if (WebPGetFeatures(p_Raw->_values, p_Raw->get_Length(), &l_Features) != VP8_STATUS_OK) @@ -31,7 +39,9 @@ namespace CP_SDK::Animation::WEBP { if (l_Features.has_animation == 0) { - throw std::runtime_error("EAnimationType::WEBP Static is not implemented!"); + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Animation.WEBP][WEBP.ProcessingThread] Static WEBP is not implemented"); + p_Callback(nullptr); + return; /*auto l_Width = l_Features.width; auto l_Height = l_Features.height; @@ -118,11 +128,13 @@ namespace CP_SDK::Animation::WEBP { l_WebPData.bytes = p_Raw->_values; l_WebPData.size = p_Raw->get_Length(); - auto l_Decoder = WebPAnimDecoderNew(&l_WebPData, nullptr); + using t_DecoderPtr = std::unique_ptr; + t_DecoderPtr l_Decoder(WebPAnimDecoderNew(&l_WebPData, nullptr), &WebPAnimDecoderDelete); if (l_Decoder) { auto l_Infos = WebPAnimInfo(); - if (WebPAnimDecoderGetInfo(l_Decoder, &l_Infos)) + if (WebPAnimDecoderGetInfo(l_Decoder.get(), &l_Infos) && l_Infos.canvas_width > 0 + && l_Infos.canvas_height > 0 && l_Infos.frame_count > 0) { auto l_Buffer = (uint8_t*)nullptr; auto l_TimeStamp = 0; @@ -130,12 +142,20 @@ namespace CP_SDK::Animation::WEBP { auto l_AnimationInfo = AnimationInfo::Make((int)l_Infos.canvas_width, (int)l_Infos.canvas_height, l_Infos.frame_count); auto l_FrameI = 0; - while (WebPAnimDecoderHasMoreFrames(l_Decoder)) + while (WebPAnimDecoderHasMoreFrames(l_Decoder.get())) { - if (!WebPAnimDecoderGetNext(l_Decoder, &l_Buffer, &l_TimeStamp)) + if (l_FrameI >= l_AnimationInfo->Frames.size()) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Animation.WEBP][WEBP.ProcessingThread] Decoder produced too many frames"); + p_Callback(nullptr); + return; + } + + if (!WebPAnimDecoderGetNext(l_Decoder.get(), &l_Buffer, &l_TimeStamp) || !l_Buffer) { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Animation.WEBP][WEBP.ProcessingThread] Failed to decode next frame"); - break; + p_Callback(nullptr); + return; } auto l_TargetArray = l_AnimationInfo->Frames[l_FrameI].Ptr(); @@ -159,7 +179,12 @@ namespace CP_SDK::Animation::WEBP { l_FrameI++; } - WebPAnimDecoderDelete(l_Decoder); + if (l_FrameI != l_AnimationInfo->Frames.size()) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Animation.WEBP][WEBP.ProcessingThread] Decoder produced too few frames"); + p_Callback(nullptr); + return; + } p_Callback(l_AnimationInfo); } @@ -178,4 +203,4 @@ namespace CP_SDK::Animation::WEBP { }); } -} ///< namespace CP_SDK::Animation::WEBP \ No newline at end of file +} ///< namespace CP_SDK::Animation::WEBP diff --git a/src/CP_SDK/ChatPlexService.cpp b/src/CP_SDK/ChatPlexService.cpp index f3191a9..b702159 100644 --- a/src/CP_SDK/ChatPlexService.cpp +++ b/src/CP_SDK/ChatPlexService.cpp @@ -12,46 +12,46 @@ static std::u16string s_EmptyU16String = u""; namespace CP_SDK { - bool ChatPlexService::m_ThreadCondition = true; - _u::il2cpp_aware_thread* ChatPlexService::m_Thread = nullptr; - ChatPlexService::EState ChatPlexService::m_State = ChatPlexService::EState::Disconnected; - _v::WebClientCore::Ptr ChatPlexService::m_WebClientCore = nullptr; - _v::JsonRPCClient::Ptr ChatPlexService::m_JsonRPCClient = nullptr; - std::u16string ChatPlexService::m_LinkRequestID = u""; - std::u16string ChatPlexService::m_LinkCode = u""; - std::u16string ChatPlexService::m_LastError = u""; - std::u16string ChatPlexService::m_ActiveSubscription = u""; - std::vector ChatPlexService::m_UnlockedFeatures; - std::queue<_v::Action<>> ChatPlexService::m_OnTokenReadyQueue; - std::mutex ChatPlexService::m_OnTokenReadyQueueMutex; - std::u16string ChatPlexService::m_DeviceName = u""; + std::atomic_bool ChatPlexService::m_ThreadCondition = false; + _u::il2cpp_aware_thread* ChatPlexService::m_Thread = nullptr; + std::atomic ChatPlexService::m_State = ChatPlexService::EState::Disconnected; + _v::WebClientCore::Ptr ChatPlexService::m_WebClientCore = nullptr; + _v::JsonRPCClient::Ptr ChatPlexService::m_JsonRPCClient = nullptr; + std::u16string ChatPlexService::m_LinkRequestID = u""; + std::u16string ChatPlexService::m_LinkCode = u""; + std::u16string ChatPlexService::m_LastError = u""; + std::u16string ChatPlexService::m_ActiveSubscription = u""; + std::vector ChatPlexService::m_UnlockedFeatures; + std::queue<_v::Action<>> ChatPlexService::m_OnTokenReadyQueue; + std::mutex ChatPlexService::m_OnTokenReadyQueueMutex; + std::u16string ChatPlexService::m_DeviceName = u""; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - const ChatPlexService::EState ChatPlexService::State() + const ChatPlexService::EState ChatPlexService::State() { - return m_State; + return m_State.load(); } - const std::u16string_view ChatPlexService::Token() + const std::u16string_view ChatPlexService::Token() { return CPConfig::Instance()->ChatPlexServiceToken; } - const std::u16string_view ChatPlexService::LinkCode() + const std::u16string_view ChatPlexService::LinkCode() { return m_LinkCode; } - const std::u16string_view ChatPlexService::LastError() + const std::u16string_view ChatPlexService::LastError() { return m_LastError; } - const std::u16string_view ChatPlexService::ActiveSubscription() + const std::u16string_view ChatPlexService::ActiveSubscription() { return m_State == EState::Connected ? m_ActiveSubscription : s_EmptyU16String; } - const std::vector& ChatPlexService::UnlockedFeatures() + const std::vector ChatPlexService::UnlockedFeatures() { - return *reinterpret_cast*>(&m_UnlockedFeatures); + return *reinterpret_cast *>(&m_UnlockedFeatures); } _v::Event ChatPlexService::StateChanged; @@ -62,21 +62,32 @@ namespace CP_SDK { /// @brief Init the service void ChatPlexService::Init() { + if (m_Thread) + return; + + m_DeviceName = _u::SystemInfo::GetDeviceName(); m_WebClientCore = _v::WebClientCore::Make(u"https://api.chatplex.org/", _u::TimeSpan::FromSeconds(10), false, true); m_JsonRPCClient = _v::JsonRPCClient::Create(m_WebClientCore); + m_ThreadCondition.store(true); m_Thread = new _u::il2cpp_aware_thread(&ThreadRunner); - - m_DeviceName = _u::SystemInfo::GetDeviceName(); } /// @brief Release the service void ChatPlexService::Release() { - m_ThreadCondition = false; - m_Thread->join(); + auto l_Thread = m_Thread; + if (!l_Thread || !m_ThreadCondition.load()) + return; - delete m_Thread; + m_ThreadCondition.store(false); + + if (l_Thread->joinable()) + l_Thread->join(); + + delete l_Thread; m_Thread = nullptr; + m_JsonRPCClient.reset(); + m_WebClientCore.reset(); } //////////////////////////////////////////////////////////////////////////// @@ -86,13 +97,17 @@ namespace CP_SDK { /// @param action Callback to be caled void ChatPlexService::OnTokenReady(_v::CActionRef<> action) { - if (m_State == EState::Connected) - action(); - else + bool l_InvokeNow = false; { std::lock_guard l_Lock(m_OnTokenReadyQueueMutex); - m_OnTokenReadyQueue.push(action); + if (m_State.load() == EState::Connected) + l_InvokeNow = true; + else + m_OnTokenReadyQueue.push(action); } + + if (l_InvokeNow) + action(); } //////////////////////////////////////////////////////////////////////////// @@ -101,7 +116,7 @@ namespace CP_SDK { /// @brief Start linking procedure void ChatPlexService::StartLinking() { - if (m_State != EState::Disconnected) + if (m_State.load() != EState::Disconnected) return; ChangeState(EState::LinkRequest); @@ -109,7 +124,8 @@ namespace CP_SDK { /// @brief Stop linking procedure void ChatPlexService::StopLinking() { - if (m_State != EState::LinkRequest && m_State != EState::LinkWait) + const auto l_State = m_State.load(); + if (l_State != EState::LinkRequest && l_State != EState::LinkWait) return; ChangeState(EState::Disconnected); @@ -134,33 +150,37 @@ namespace CP_SDK { /// @brief Change state and notify listenners void ChatPlexService::ChangeState(const EState newState) { - auto l_OldState = m_State; - m_State = newState; + auto l_OldState = m_State.exchange(newState); Unity::MTThreadInvoker::EnqueueOnThread([=]() -> void { StateChanged(l_OldState, newState); }); } /// @brief Fire on token ready actions void ChatPlexService::FireOnTokenReady() { - m_OnTokenReadyQueueMutex.lock(); - while (!m_OnTokenReadyQueue.empty()) + std::queue<_v::Action<>> l_Actions; + { + std::lock_guard l_Lock(m_OnTokenReadyQueueMutex); + l_Actions.swap(m_OnTokenReadyQueue); + } + + while (!l_Actions.empty()) { - auto l_Action = m_OnTokenReadyQueue.front(); - m_OnTokenReadyQueue.pop(); - m_OnTokenReadyQueueMutex.unlock(); + auto l_Action = std::move(l_Actions.front()); + l_Actions.pop(); try { l_Action(); } - catch (std::exception l_Exception) + catch (const std::exception& l_Exception) { ChatPlexSDK::Logger()->Error(u"[CP_SDK][ChatPlexService.FireOnTokenReady] Error:"); ChatPlexSDK::Logger()->Error(l_Exception); } - m_OnTokenReadyQueueMutex.lock(); + catch (...) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK][ChatPlexService.FireOnTokenReady] Unknown error"); + } } - - m_OnTokenReadyQueueMutex.unlock(); } //////////////////////////////////////////////////////////////////////////// @@ -169,28 +189,36 @@ namespace CP_SDK { /// @brief Thread function void ChatPlexService::ThreadRunner() { - while (m_ThreadCondition) + while (m_ThreadCondition.load()) { - if (m_State == EState::Disconnected && !CPConfig::Instance()->ChatPlexServiceToken.empty()) + try { - m_WebClientCore->RemoveHeader(u"Authorization"); - - ChangeState(EState::Connecting); - - std::shared_ptr<_v::Json::U16Document> l_Content(new _v::Json::U16Document()); - l_Content->SetObject(); - l_Content->AddMember(u"ConnectedApplicationToken", _v::Json::U16Value(CPConfig::Instance()->ChatPlexServiceToken, l_Content->GetAllocator()), l_Content->GetAllocator()); + const auto l_State = m_State.load(); + auto l_Token = CPConfig::Instance()->ChatPlexServiceToken; - auto l_Result = m_JsonRPCClient->Request( - u"Account_AuthByConnectedApplicationToken", - l_Content - ); - - if (IsRPCSuccess(l_Result)) - OnAuthed(l_Result); - else + if (l_State == EState::Disconnected && !l_Token.empty()) { - if (l_Result->Result != nullptr && l_Result->Result->HasMember(u"Result") && (*l_Result->Result)[u"Result"].GetBool() == false) + m_WebClientCore->RemoveHeader(u"Authorization"); + + ChangeState(EState::Connecting); + + auto l_Content = std::make_shared<_v::Json::U16Document>(); + l_Content->SetObject(); + l_Content->AddMember(u"ConnectedApplicationToken", _v::Json::U16Value(l_Token, l_Content->GetAllocator()), l_Content->GetAllocator()); + + auto l_RPCResult = m_JsonRPCClient->Request( + u"Account_AuthByConnectedApplicationToken", + l_Content + ); + + if (IsRPCSuccess(l_RPCResult)) + OnAuthed(l_RPCResult); + else if (l_RPCResult + && l_RPCResult->Result + && l_RPCResult->Result->IsObject() + && l_RPCResult->Result->HasMember(u"Result") + && (*l_RPCResult->Result)[u"Result"].IsBool() + && !(*l_RPCResult->Result)[u"Result"].GetBool()) { CPConfig::Instance()->ChatPlexServiceToken = u""; CPConfig::Instance()->Save(); @@ -198,56 +226,79 @@ namespace CP_SDK { ChangeState(EState::Disconnected); } else - OnError(l_Result); + OnError(l_RPCResult); } - } - else if (m_State == EState::LinkRequest) - { - std::shared_ptr<_v::Json::U16Document> l_Content(new _v::Json::U16Document()); - l_Content->SetObject(); - l_Content->AddMember(u"ApplicationIdentifier", _v::Json::U16Value(ChatPlexSDK::ProductName().data(), l_Content->GetAllocator()), l_Content->GetAllocator()); - l_Content->AddMember(u"ApplicationDeviceName", _v::Json::U16Value(m_DeviceName, l_Content->GetAllocator()), l_Content->GetAllocator()); + else if (l_State == EState::LinkRequest) + { + auto l_Content = std::make_shared<_v::Json::U16Document>(); + l_Content->SetObject(); + l_Content->AddMember(u"ApplicationIdentifier", _v::Json::U16Value(ChatPlexSDK::ProductName().data(), l_Content->GetAllocator()), l_Content->GetAllocator()); + l_Content->AddMember(u"ApplicationDeviceName", _v::Json::U16Value(m_DeviceName, l_Content->GetAllocator()), l_Content->GetAllocator()); - auto l_Result = m_JsonRPCClient->Request( - u"ConnectedApplication_CreateLinkRequest", - l_Content - ); + auto l_RPCResult = m_JsonRPCClient->Request( + u"ConnectedApplication_CreateLinkRequest", + l_Content + ); - if (IsRPCSuccess(l_Result)) + if (IsRPCSuccess(l_RPCResult)) + { + auto& l_ResultR = *l_RPCResult->Result; + if (!l_ResultR.HasMember(u"RequestID") || !l_ResultR[u"RequestID"].IsString() + || !l_ResultR.HasMember(u"Code") || !l_ResultR[u"Code"].IsString()) + { + OnError(l_RPCResult); + } + else + { + m_LinkRequestID = l_ResultR[u"RequestID"].GetString(); + m_LinkCode = l_ResultR[u"Code"].GetString(); + + ChangeState(EState::LinkWait); + } + } + else + OnError(l_RPCResult); + } + else if (l_State == EState::LinkWait) { - auto& l_ResultR = *l_Result->Result.get(); - m_LinkRequestID = l_ResultR[u"RequestID"].GetString(); - m_LinkCode = l_ResultR[u"Code"].GetString(); + auto l_Content = std::make_shared<_v::Json::U16Document>(); + l_Content->SetObject(); + l_Content->AddMember(u"RequestID", _v::Json::U16Value(m_LinkRequestID, l_Content->GetAllocator()), l_Content->GetAllocator()); + + auto l_RPCResult = m_JsonRPCClient->Request( + u"ConnectedApplication_GetLinkRequestStatus", + l_Content + ); - ChangeState(EState::LinkWait); + if (IsRPCSuccess(l_RPCResult)) + { + auto& l_ResultR = *l_RPCResult->Result; + if (l_ResultR.HasMember(u"ResultToken") && l_ResultR[u"ResultToken"].IsString()) + { + CPConfig::Instance()->ChatPlexServiceToken = l_ResultR[u"ResultToken"].GetString(); + CPConfig::Instance()->Save(); + + ChangeState(EState::Disconnected); + } + } + else + OnError(l_RPCResult); } - else - OnError(l_Result); } - else if (m_State == EState::LinkWait) + catch (const std::exception& l_Exception) { - std::shared_ptr<_v::Json::U16Document> l_Content(new _v::Json::U16Document()); - l_Content->SetObject(); - l_Content->AddMember(u"RequestID", _v::Json::U16Value(m_LinkRequestID, l_Content->GetAllocator()), l_Content->GetAllocator()); - - auto l_Result = m_JsonRPCClient->Request( - u"ConnectedApplication_GetLinkRequestStatus", - l_Content - ); + ChatPlexSDK::Logger()->Error(u"[CP_SDK][ChatPlexService.ThreadRunner] Error:"); + ChatPlexSDK::Logger()->Error(l_Exception); - if (IsRPCSuccess(l_Result)) - { - auto& l_ResultR = *l_Result->Result.get(); - if (l_ResultR[u"ResultToken"].IsString()) - { - CPConfig::Instance()->ChatPlexServiceToken = l_ResultR[u"ResultToken"].GetString(); - CPConfig::Instance()->Save(); + m_LastError = u"Internal ChatPlex service error"; + ChangeState(EState::Error); + } + catch (...) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK][ChatPlexService.ThreadRunner] Unknown error"); - ChangeState(EState::Disconnected); - } - } - else - OnError(l_Result); + m_LastError = u"Internal ChatPlex service error"; + ChangeState(EState::Error); } if (m_State == EState::LinkWait) @@ -265,11 +316,13 @@ namespace CP_SDK { /// @return True if success bool ChatPlexService::IsRPCSuccess(_v::JsonRPCResult::Ptr& rpcResult) { - if (rpcResult->Result == nullptr) + if (!rpcResult || !rpcResult->Result || !rpcResult->Result->IsObject()) return false; - auto& l_ResultR = *rpcResult->Result.get(); - return l_ResultR.HasMember(u"Result") && l_ResultR[u"Result"].GetBool() == true; + auto& l_ResultR = *rpcResult->Result; + return l_ResultR.HasMember(u"Result") + && l_ResultR[u"Result"].IsBool() + && l_ResultR[u"Result"].GetBool(); } //////////////////////////////////////////////////////////////////////////// @@ -279,10 +332,18 @@ namespace CP_SDK { /// @param rpcResult Result of the RPC command void ChatPlexService::OnAuthed(_v::JsonRPCResult::Ptr& rpcResult) { - auto& l_ResultR = *rpcResult->Result.get(); - m_ActiveSubscription = l_ResultR.HasMember(u"ActiveSubscription") ? l_ResultR[u"ActiveSubscription"].GetString() : u""; + if (!rpcResult || !rpcResult->Result || !rpcResult->Result->IsObject()) + { + OnError(rpcResult); + return; + } + + auto& l_ResultR = *rpcResult->Result; + auto l_ActiveSubscription = std::u16string(); + if (l_ResultR.HasMember(u"ActiveSubscription") && l_ResultR[u"ActiveSubscription"].IsString()) + l_ActiveSubscription = l_ResultR[u"ActiveSubscription"].GetString(); - m_UnlockedFeatures.clear(); + auto l_UnlockedFeatures = std::vector(); if (l_ResultR.HasMember(u"UnlockedFeatures") && l_ResultR[u"UnlockedFeatures"].IsArray()) { auto l_Array = l_ResultR[u"UnlockedFeatures"].GetArray(); @@ -291,10 +352,13 @@ namespace CP_SDK { if (!l_CurrentValue.IsString()) continue; - m_UnlockedFeatures.push_back(l_CurrentValue.GetString()); + l_UnlockedFeatures.push_back(l_CurrentValue.GetString()); } } + m_ActiveSubscription = std::move(l_ActiveSubscription); + m_UnlockedFeatures = std::move(l_UnlockedFeatures); + m_WebClientCore->SetHeader(u"Authorization", std::u16string(u"ConnectedApplicationToken ") + CPConfig::Instance()->ChatPlexServiceToken); ChangeState(EState::Connected); @@ -304,15 +368,26 @@ namespace CP_SDK { /// @param rpcResult Result of the RPC command void ChatPlexService::OnError(_v::JsonRPCResult::Ptr& rpcResult) { - std::u16string l_Error = u"Unknow server error!"; - if (rpcResult->Result != nullptr && rpcResult->Result->HasMember(u"Error")) + std::u16string l_Error = u"Unknown server error!"; + if (rpcResult + && rpcResult->Result + && rpcResult->Result->IsObject() + && rpcResult->Result->HasMember(u"Error") + && (*rpcResult->Result)[u"Error"].IsString()) { - auto& l_ResultR = *rpcResult->Result.get(); - l_Error = l_ResultR[u"Error"].GetString(); + l_Error = (*rpcResult->Result)[u"Error"].GetString(); } + else if (rpcResult + && rpcResult->Error->IsObject() + && rpcResult->Error->HasMember(u"message") + && (*rpcResult->Error)[u"message"].IsString()) + { + l_Error = (*rpcResult->Error)[u"message"].GetString(); + } + + m_LastError = std::move(l_Error); - m_LastError = l_Error; ChangeState(EState::Error); } -} ///< namespace CP_SDK \ No newline at end of file +} ///< namespace CP_SDK diff --git a/src/CP_SDK/Config/JsonConfig.cpp b/src/CP_SDK/Config/JsonConfig.cpp index 146b15f..a08b23f 100644 --- a/src/CP_SDK/Config/JsonConfig.cpp +++ b/src/CP_SDK/Config/JsonConfig.cpp @@ -4,6 +4,7 @@ #include "CP_SDK/ChatPlexSDK.hpp" #include +#include namespace CP_SDK::Config { @@ -138,34 +139,36 @@ namespace CP_SDK::Config { /// @param p_FullPath File path void JsonConfig::WriteFile(std::filesystem::path p_FullPath) { - Unity::MTThreadInvoker::EnqueueOnThread([this, p_FullPath]() -> void + std::string l_Serialized; + try + { + Utils::Json::U16Document l_Document; + l_Document.SetObject(); + Serialize(l_Document, l_Document.GetAllocator()); + l_Serialized = Utils::Json::ToU8String(l_Document, true); + } + catch (const std::exception& l_Exception) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Config][JsonConfig.WriteFile] Serialization error for file " + StringW(p_FullPath.string())); + ChatPlexSDK::Logger()->Error(l_Exception); + return; + } + + Unity::MTThreadInvoker::EnqueueOnThread([p_FullPath, l_Serialized = std::move(l_Serialized)]() -> void { try { auto l_Path = p_FullPath.parent_path(); if (!std::filesystem::exists(l_Path) && !std::filesystem::create_directories(l_Path)) throw std::runtime_error("Failed to create directory for path " + l_Path.string()); - } - catch (const std::exception& l_Exception) - { - ChatPlexSDK::Logger()->Error(u"[CP_SDK.Config][JsonConfig.WriteFile] Error for file " + StringW(p_FullPath.string())); - ChatPlexSDK::Logger()->Error(l_Exception); - } - - std::ofstream l_Stream; - try - { - l_Stream.open(p_FullPath, std::ios::trunc); - Utils::Json::U16Document l_Document; - l_Document.SetObject(); - Serialize(l_Document, l_Document.GetAllocator()); + std::ofstream l_Stream(p_FullPath, std::ios::binary | std::ios::trunc); + if (!l_Stream.is_open()) + throw std::runtime_error("Failed to open config file"); - if (!Utils::Json::TryToU8Stream(l_Document, true, l_Stream)) - { - l_Stream.close(); - throw std::runtime_error("Failed to write json"); - } + l_Stream.write(l_Serialized.data(), static_cast(l_Serialized.size())); + if (!l_Stream.good()) + throw std::runtime_error("Failed to write config file"); l_Stream.close(); } @@ -174,11 +177,7 @@ namespace CP_SDK::Config { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Config][JsonConfig.WriteFile] Error for file " + StringW(p_FullPath.string())); ChatPlexSDK::Logger()->Error(l_Exception); } - - if (l_Stream.is_open()) - l_Stream.close(); }); } } ///< namespace CP_SDK::Config - diff --git a/src/CP_SDK/Network/WebClientCore.cpp b/src/CP_SDK/Network/WebClientCore.cpp index d5eb88b..ca727f9 100644 --- a/src/CP_SDK/Network/WebClientCore.cpp +++ b/src/CP_SDK/Network/WebClientCore.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,8 @@ struct ScopedCURL } ~ScopedCURL() { - curl_easy_cleanup(Instance); + if (Instance) + curl_easy_cleanup(Instance); if (Headers != NULL) curl_slist_free_all(Headers); @@ -142,7 +144,8 @@ namespace CP_SDK::Network { CancellationToken::get_None(), [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, dontRetry, - progress + progress, + false ); while (!l_IsDone) @@ -168,7 +171,8 @@ namespace CP_SDK::Network { CancellationToken::get_None(), [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, dontRetry, - progress + progress, + false ); while (!l_IsDone) @@ -194,7 +198,8 @@ namespace CP_SDK::Network { CancellationToken::get_None(), [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, dontRetry, - nullptr + nullptr, + false ); while (!l_IsDone) @@ -220,7 +225,8 @@ namespace CP_SDK::Network { CancellationToken::get_None(), [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, dontRetry, - nullptr + nullptr, + false ); while (!l_IsDone) @@ -246,7 +252,8 @@ namespace CP_SDK::Network { CancellationToken::get_None(), [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, dontRetry, - nullptr + nullptr, + false ); while (!l_IsDone) @@ -271,7 +278,8 @@ namespace CP_SDK::Network { CancellationToken::get_None(), [&](WebResponse::Ptr p_Result) -> void { l_Reply = p_Result; l_IsDone = true; }, dontRetry, - nullptr + nullptr, + false ); while (!l_IsDone) @@ -301,7 +309,8 @@ namespace CP_SDK::Network { token, callback, dontRetry, - progress + progress, + true ).detach(); } /// @brief Do Async GET query @@ -322,7 +331,8 @@ namespace CP_SDK::Network { token, callback, dontRetry, - progress + progress, + true ).detach(); } /// @brief Do Async POST query @@ -343,7 +353,8 @@ namespace CP_SDK::Network { token, callback, dontRetry, - nullptr + nullptr, + true ).detach(); } /// @brief Do Async PATCH query @@ -364,7 +375,8 @@ namespace CP_SDK::Network { token, callback, dontRetry, - nullptr + nullptr, + true ).detach(); } /// @brief Do Async PATCH query @@ -385,7 +397,8 @@ namespace CP_SDK::Network { token, callback, dontRetry, - nullptr + nullptr, + true ).detach(); } /// @brief Do Async GET query @@ -406,8 +419,9 @@ namespace CP_SDK::Network { token, callback, dontRetry, - nullptr - ); + nullptr, + true + ).detach(); } //////////////////////////////////////////////////////////////////////////// @@ -455,140 +469,225 @@ namespace CP_SDK::Network { CancellationToken token, _v::Action callback, bool dontRetry, - _v::Action progress + _v::Action progress, + bool dispatchCallback ) { + auto l_ReportResult = [&](WebResponse::Ptr p_Reply) noexcept + { + try + { + if (!callback.IsValid()) + return; + + if (!dispatchCallback) + { + callback(p_Reply); + return; + } + + if (!token.get_IsCancellationRequested()) + Unity::MTThreadInvoker::EnqueueOnThread([callback, p_Reply]() -> void { callback(p_Reply); }); + } + catch (...) + { + try { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientCore.DoRequest] Failed to report request result"); } + catch (...) { } + } + }; + + try + { #if DEBUG - ChatPlexSDK::Logger()->Debug(u"[CP_SDK.Network][WebClientCore." + debugName + u"] " + httpMethod + u" " + url); + ChatPlexSDK::Logger()->Debug(u"[CP_SDK.Network][WebClientCore." + debugName + u"] " + httpMethod + u" " + url); #endif - WebResponse::Ptr l_Reply = nullptr; - for (int l_RetryI = 1; l_RetryI <= self->MaxRetry; l_RetryI++) - { - if (token.get_IsCancellationRequested()) - break; + WebResponse::Ptr l_Reply = nullptr; + for (int l_RetryI = 1; l_RetryI <= self->MaxRetry; l_RetryI++) + { + if (token.get_IsCancellationRequested()) + break; - ScopedCURL l_ScopedCURL; - l_ScopedCURL.Headers = curl_slist_append(l_ScopedCURL.Headers, "Accept: */*"); + ScopedCURL l_ScopedCURL; + if (!l_ScopedCURL.Instance) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientCore.DoRequest] curl_easy_init failed"); + break; + } - for (auto const& [l_Header, l_Value] : self->m_Headers) - l_ScopedCURL.Headers = curl_slist_append(l_ScopedCURL.Headers, Utils::U16StrToStr(l_Header + ": " + l_Value).c_str()); + l_ScopedCURL.Headers = curl_slist_append(l_ScopedCURL.Headers, "Accept: */*"); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_URL, Utils::U16StrToStr(url).c_str()); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_TIMEOUT, static_cast(self->m_TimeOut)); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_FOLLOWLOCATION, static_cast(1)); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_USERAGENT, Utils::U16StrToStr(ChatPlexSDK::NetworkUserAgent()).c_str()); + { + std::lock_guard l_Lock(self->m_HeadersLock); + for (auto const &[l_Header, l_Value] : self->m_Headers) + l_ScopedCURL.Headers = curl_slist_append(l_ScopedCURL.Headers, Utils::U16StrToStr(l_Header + ": " + l_Value).c_str()); + } - if (httpMethod == u"GET" || httpMethod == u"DOWNLOAD") - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "GET"); - else if (httpMethod == u"POST" || httpMethod == u"PATCH" || httpMethod == u"PUT") - { - if (httpMethod == u"POST") - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "POST"); - else if (httpMethod == u"PATCH") - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "PATCH"); - else - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_URL, Utils::U16StrToStr(url).c_str()); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_TIMEOUT, static_cast(self->m_TimeOut)); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_FOLLOWLOCATION, static_cast(1)); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_USERAGENT, Utils::U16StrToStr(ChatPlexSDK::NetworkUserAgent()).c_str()); - if (content) + if (httpMethod == u"GET" || httpMethod == u"DOWNLOAD") + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "GET"); + else if (httpMethod == u"POST" || httpMethod == u"PATCH" || httpMethod == u"PUT") { - l_ScopedCURL.Headers = curl_slist_append(l_ScopedCURL.Headers, Utils::U16StrToStr(std::u16string(u"Content-Type: ") + content->Type).c_str()); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_POSTFIELDSIZE, static_cast(content->Bytes->get_Length())); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_POSTFIELDS, &content->Bytes->_values[0]); + if (httpMethod == u"POST") + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "POST"); + else if (httpMethod == u"PATCH") + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "PATCH"); + else + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "PUT"); + + if (content) + { + l_ScopedCURL.Headers = curl_slist_append(l_ScopedCURL.Headers, Utils::U16StrToStr(std::u16string(u"Content-Type: ") + content->Type).c_str()); + const auto l_ContentLength = content->Bytes ? content->Bytes->get_Length() : 0; + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_POSTFIELDSIZE, static_cast(l_ContentLength)); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_POSTFIELDS, l_ContentLength > 0 ? &content->Bytes->_values[0] : nullptr); + } } - } - else if (httpMethod == u"DELETE") - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "DELETE"); + else if (httpMethod == u"DELETE") + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_CUSTOMREQUEST, "DELETE"); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_HTTPHEADER, l_ScopedCURL.Headers); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_SSL_VERIFYPEER, false); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_HTTPHEADER, l_ScopedCURL.Headers); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_SSL_VERIFYPEER, false); + + struct TransferContext + { + _v::Action* Progress; + CancellationToken* Token; + }; + TransferContext l_TransferContext { &progress, &token }; - if (progress.IsValid()) - { curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_NOPROGRESS, false); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_XFERINFODATA, (void*)&progress); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_XFERINFODATA, &l_TransferContext); curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_XFERINFOFUNCTION, - +[] (_v::Action* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { - float percentage = (float)dlnow / (float)dltotal * 100.0f; - if(isnan(percentage)) - percentage = 0.0f; - clientp->Invoke(percentage); + +[] (void* p_Client, curl_off_t p_DownloadTotal, curl_off_t p_DownloadNow, curl_off_t, curl_off_t) -> int + { + auto l_Context = reinterpret_cast(p_Client); + try + { + if (l_Context->Token->get_IsCancellationRequested()) + return 1; + + if (l_Context->Progress->IsValid()) + { + auto l_Percentage = p_DownloadTotal > 0 + ? static_cast(p_DownloadNow) / static_cast(p_DownloadTotal) + : 0.0f; + + if (isnan(l_Percentage)) + l_Percentage = 0.0f; + + try { l_Context->Progress->Invoke(l_Percentage); } catch (const std::exception&) { } + } + } + catch (...) + { + return 1; + } + return 0; } ); - } - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_WRITEDATA, l_ScopedCURL.Data); - curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_WRITEFUNCTION, - +[](void *contents, std::size_t size, std::size_t nmemb, std::vector* clientp) -> size_t - { - size_t l_SizeToWrite = size * nmemb; - - try - { - auto l_WritePos = clientp->size(); - clientp->resize(l_WritePos + l_SizeToWrite); - memcpy(clientp->data() + l_WritePos, contents, l_SizeToWrite); - } - catch(std::bad_alloc &e) + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_WRITEDATA, l_ScopedCURL.Data); + curl_easy_setopt(l_ScopedCURL.Instance, CURLOPT_WRITEFUNCTION, + +[](void *contents, std::size_t size, std::size_t nmemb, std::vector* clientp) -> size_t { - ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientCore.DoRequest] Failed to read from response stream, allocation error"); - return 0; - } + if (size != 0 && nmemb > std::numeric_limits::max() / size) + return 0; - return l_SizeToWrite; - }); + const std::size_t l_SizeToWrite = size * nmemb; + if (l_SizeToWrite > clientp->max_size() - clientp->size()) + return 0; + if (clientp->size() > static_cast(std::numeric_limits::max()) + || l_SizeToWrite > static_cast(std::numeric_limits::max()) - clientp->size()) + return 0; - if (token.get_IsCancellationRequested()) - break; + try + { + auto l_WritePos = clientp->size(); + clientp->resize(l_WritePos + l_SizeToWrite); + memcpy(clientp->data() + l_WritePos, contents, l_SizeToWrite); + } + catch (...) + { + return 0; + } - auto l_CURLResult = curl_easy_perform(l_ScopedCURL.Instance); + return l_SizeToWrite; + }); - l_Reply = std::make_shared(static_cast(l_CURLResult), l_ScopedCURL.Instance, l_ScopedCURL.Data); - if (!l_Reply->IsSuccessStatusCode() && l_Reply->StatusCode() == (HttpStatusCode)429) - { - /* var l_Limits = RateLimitInfo.Get(l_Request); - if (l_Limits != nullptr) + if (token.get_IsCancellationRequested()) + break; + + auto l_CURLResult = curl_easy_perform(l_ScopedCURL.Instance); + + l_Reply = std::make_shared(static_cast(l_CURLResult), l_ScopedCURL.Instance, l_ScopedCURL.Data); + if (!l_Reply->IsSuccessStatusCode() && l_Reply->StatusCode() == (HttpStatusCode)429) { - int l_TotalMilliseconds = (int)(l_Limits.Reset - DateTime.Now).TotalMilliseconds; - if (l_TotalMilliseconds > 0) + /* var l_Limits = RateLimitInfo.Get(l_Request); + if (l_Limits != nullptr) { - ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientCore." + debugName + u"] Request {SafeURL(url)} was rate limited, retrying in {l_TotalMilliseconds}ms..."); + int l_TotalMilliseconds = (int)(l_Limits.Reset - DateTime.Now).TotalMilliseconds; + if (l_TotalMilliseconds > 0) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientCore." + debugName + u"] Request {SafeURL(url)} was rate limited, retrying in {l_TotalMilliseconds}ms..."); + + co_yield WaitForSecondsRealtime::New_ctor(RetryInterval)->i_IEnumerator(); + continue; + } + }*/ + } + + if (!l_Reply->IsSuccessStatusCode()) + { + auto l_LogPrefix = u"[CP_SDK.Network][WebClientCore." + debugName + u"] Request " + self->SafeURL(url) + u" failed with code "; + l_LogPrefix += StringW(std::to_string(l_Reply->StatusCode().value__)); + l_LogPrefix += u":\"" + l_Reply->ReasonPhrase() + "\", "; - co_yield WaitForSecondsRealtime::New_ctor(RetryInterval)->i_IEnumerator(); - continue; + if (!l_Reply->ShouldRetry() || dontRetry) + { + ChatPlexSDK::Logger()->Error(l_LogPrefix + u" not retrying"); + break; } - }*/ - } - if (!l_Reply->IsSuccessStatusCode()) - { - auto l_LogPrefix = u"[CP_SDK.Network][WebClientCore." + debugName + u"] Request " + self->SafeURL(url) + u" failed with code "; - l_LogPrefix += StringW(std::to_string(l_Reply->StatusCode().value__)); - l_LogPrefix += u":\"" + l_Reply->ReasonPhrase() + "\", "; + ChatPlexSDK::Logger()->Error(l_LogPrefix + u" next try in " + (std::u16string)StringW(std::to_string(self->RetryInterval)) + u" seconds..."); - if (!l_Reply->ShouldRetry() || dontRetry) + const auto l_RetryUntil = std::chrono::steady_clock::now() + std::chrono::seconds(self->RetryInterval); + while (!token.get_IsCancellationRequested() && std::chrono::steady_clock::now() < l_RetryUntil) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + else { - ChatPlexSDK::Logger()->Error(l_LogPrefix + u" not retrying"); + if (progress.IsValid()) + try { progress(1.0f); } catch (const std::exception&) { } + break; } - - ChatPlexSDK::Logger()->Error(l_LogPrefix + u" next try in " + (std::u16string)StringW(std::to_string(self->RetryInterval)) + u" seconds..."); - - std::this_thread::sleep_for(std::chrono::seconds(self->RetryInterval)); - continue; } - else - { - if (progress.IsValid()) - try { progress(1.0f); } catch (const std::exception&) { } - break; + l_ReportResult(l_Reply); + } + catch (const std::exception& l_Exception) + { + try + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientCore.DoRequest] Unhandled request error:"); + ChatPlexSDK::Logger()->Error(l_Exception); } + catch (...) { } + l_ReportResult(nullptr); + } + catch (...) + { + try { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Network][WebClientCore.DoRequest] Unknown request error"); } + catch (...) { } + l_ReportResult(nullptr); } - - if (!token.get_IsCancellationRequested() && callback.IsValid()) - Unity::MTThreadInvoker::EnqueueOnThread([=]() -> void { callback(l_Reply); }); } -} ///< namespace CP_SDK::Network \ No newline at end of file +} ///< namespace CP_SDK::Network diff --git a/src/CP_SDK/Network/WebResponse.cpp b/src/CP_SDK/Network/WebResponse.cpp index f97e320..ee556c7 100644 --- a/src/CP_SDK/Network/WebResponse.cpp +++ b/src/CP_SDK/Network/WebResponse.cpp @@ -8,6 +8,9 @@ #include #include +#include +#include + bool StartWith(::Array* p_Array, ArrayW& p_Pattern, int p_PatternSize); using namespace System::Text; @@ -92,6 +95,9 @@ namespace CP_SDK::Network { /// @param data Response data WebResponse::WebResponse(long curlPerformResult, void* curlInstance, std::vector* data) { + if (!data || data->size() > static_cast(std::numeric_limits::max())) + throw std::invalid_argument("WebResponse body is invalid or too large"); + auto l_CURLInstance = reinterpret_cast(curlInstance); if (curlPerformResult != CURLE_OK) @@ -111,8 +117,9 @@ namespace CP_SDK::Network { m_ShouldRetry = IsSuccessStatusCode() ? false : (l_HTTPCode < 400 || l_HTTPCode >= 500); } - m_BodyBytes = ::Array::NewLength(data->size()); - memcpy(m_BodyBytes->_values, data->data(), data->size()); + m_BodyBytes = ::Array::NewLength(static_cast(data->size())); + if (!data->empty()) + memcpy(m_BodyBytes->_values, data->data(), data->size()); } } ///< namespace CP_SDK::Network @@ -132,4 +139,4 @@ bool StartWith(::Array* p_Array, ArrayW& p_Pattern, int p_Patt } return true; -} \ No newline at end of file +} diff --git a/src/CP_SDK/Unity/MTCoroutineStarter.cpp b/src/CP_SDK/Unity/MTCoroutineStarter.cpp index eb8fa91..d93766f 100644 --- a/src/CP_SDK/Unity/MTCoroutineStarter.cpp +++ b/src/CP_SDK/Unity/MTCoroutineStarter.cpp @@ -3,7 +3,7 @@ #include -const int MAX_QUEUE_SIZE = 1000; +constexpr std::size_t MAX_QUEUE_SIZE = 1000; using namespace System::Collections; using namespace UnityEngine; @@ -20,10 +20,9 @@ namespace CP_SDK::Unity { //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - MTCoroutineStarter::Queue** MTCoroutineStarter::m_Queues = nullptr; - bool MTCoroutineStarter::m_Queued = false; - int MTCoroutineStarter::m_FrontQueue = 0; - std::mutex MTCoroutineStarter::m_Mutex; + std::deque> MTCoroutineStarter::m_Queue; + bool MTCoroutineStarter::m_Accepting = false; + std::mutex MTCoroutineStarter::m_Mutex; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -45,37 +44,33 @@ namespace CP_SDK::Unity { /// @brief Unity GameObject initialize void MTCoroutineStarter::Initialize() { + std::lock_guard l_Lock(m_Mutex); if (m_Instance) return; - m_Queues = new MTCoroutineStarter::Queue*[2]; - - for (int l_I = 0; l_I < 2; ++l_I) - { - m_Queues[l_I] = new MTCoroutineStarter::Queue(); - m_Queues[l_I]->Data = new _v::MonoPtr[MAX_QUEUE_SIZE]; - m_Queues[l_I]->WritePos = 0; - } - auto l_GameObject = GameObject::New_ctor(u"[CP_SDK.Unity.MTCoroutineStarter]"); Object::DontDestroyOnLoad(l_GameObject); m_Instance = l_GameObject->AddComponent(); + m_Queue.clear(); + m_Accepting = true; } /// @brief Stop void MTCoroutineStarter::Destroy() { - if (!m_Instance) - return; - - for (int l_I = 0; l_I < 2; ++l_I) - delete[] m_Queues[l_I]->Data; - - delete[] m_Queues; - m_Queues = nullptr; + MTCoroutineStarter* l_Instance = nullptr; + { + std::lock_guard l_Lock(m_Mutex); + if (!m_Instance) + return; + + m_Accepting = false; + m_Queue.clear(); + l_Instance = m_Instance; + m_Instance = nullptr; + } - UnityEngine::GameObject::Destroy(m_Instance->get_gameObject()); - m_Instance = nullptr; + UnityEngine::GameObject::Destroy(l_Instance->get_gameObject()); } //////////////////////////////////////////////////////////////////////////// @@ -85,17 +80,27 @@ namespace CP_SDK::Unity { /// @param p_Coroutine Coroutine to enqueue void MTCoroutineStarter::EnqueueFromThread(IEnumerator* p_Coroutine) { + if (!p_Coroutine) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTCoroutineStarter.EnqueueFromThread] Null coroutine!"); + return; + } + std::lock_guard l_Lock(m_Mutex); - auto l_Queue = m_Queues[m_FrontQueue]; - if (l_Queue->WritePos >= MAX_QUEUE_SIZE) + if (!m_Accepting || !m_Instance) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTCoroutineStarter.EnqueueFromThread] Starter is not running!"); + return; + } + + if (m_Queue.size() >= MAX_QUEUE_SIZE) { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTCoroutineStarter.EnqueueFromThread] Too many coroutines pushed!"); return; } - l_Queue->Data[l_Queue->WritePos++] = reinterpret_cast(p_Coroutine); - m_Queued = true; + m_Queue.emplace_back(reinterpret_cast(p_Coroutine)); } /// @brief Enqueue a new coroutine from thread /// @param p_Coroutine Coroutine to enqueue @@ -107,19 +112,22 @@ namespace CP_SDK::Unity { /// @param p_Coroutine Coroutine to enqueue Coroutine* MTCoroutineStarter::Start(IEnumerator* p_Coroutine) { - return m_Instance->StartCoroutine(p_Coroutine); + return m_Instance && p_Coroutine ? m_Instance->StartCoroutine(p_Coroutine) : nullptr; } /// @brief Start coroutine /// @param p_Coroutine Coroutine to enqueue Coroutine* MTCoroutineStarter::Start(custom_types::Helpers::Coroutine&& p_Coroutine) { - return m_Instance->StartCoroutine(custom_types::Helpers::CoroutineHelper::New(std::move(p_Coroutine))); + return m_Instance + ? m_Instance->StartCoroutine(custom_types::Helpers::CoroutineHelper::New(std::move(p_Coroutine))) + : nullptr; } /// @brief Stop coroutine /// @param p_Coroutine Coroutine to stop void MTCoroutineStarter::Stop(Coroutine* p_Coroutine) { - m_Instance->StopCoroutine(p_Coroutine); + if (m_Instance && p_Coroutine) + m_Instance->StopCoroutine(p_Coroutine); } //////////////////////////////////////////////////////////////////////////// @@ -128,51 +136,28 @@ namespace CP_SDK::Unity { /// @brief Unity GameObject update void MTCoroutineStarter::Update() { - if (!m_Queued) - return; - - auto l_QueueToHandle = m_FrontQueue; - auto l_NextFrontQueue = (m_FrontQueue + 1) & 1; - - m_Mutex.lock(); - m_FrontQueue = l_NextFrontQueue; - m_Queued = false; - m_Mutex.unlock(); - - auto l_Queue = m_Queues[l_QueueToHandle]; - auto l_Count = l_Queue->WritePos; - auto l_I = 0; + std::deque<_v::MonoPtr> l_Coroutines; + { + std::lock_guard l_Lock(m_Mutex); + l_Coroutines.swap(m_Queue); + } - do + for (auto& l_Coroutine : l_Coroutines) { try { - this->StartCoroutine(reinterpret_cast(l_Queue->Data[l_I].Ptr())); - l_Queue->Data[l_I] = nullptr; + this->StartCoroutine(reinterpret_cast(l_Coroutine.Ptr())); } catch (const std::exception& l_Exception) { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTCoroutineStarter.Update] Error:"); ChatPlexSDK::Logger()->Error(l_Exception); } - - ++l_I; - } while (l_I < l_Count); - - if (l_I < l_Count) - { - auto l_ToCopy = l_Count - l_I; - auto l_FrontQueue = m_Queues[m_FrontQueue]; - - m_Mutex.lock(); - memcpy(&l_FrontQueue->Data[l_FrontQueue->WritePos], &l_Queue->Data[l_I], l_ToCopy * sizeof(IEnumerator*)); - l_FrontQueue->WritePos += l_ToCopy; - m_Queued = true; - m_Mutex.unlock(); + catch (...) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTCoroutineStarter.Update] Unknown error"); + } } - - memset(l_Queue->Data, 0, l_Count * sizeof(IEnumerator*)); - l_Queue->WritePos = 0; } -} ///< namespace CP_SDK::Unity \ No newline at end of file +} ///< namespace CP_SDK::Unity diff --git a/src/CP_SDK/Unity/MTMainThreadInvoker.cpp b/src/CP_SDK/Unity/MTMainThreadInvoker.cpp index 45c23af..6266e50 100644 --- a/src/CP_SDK/Unity/MTMainThreadInvoker.cpp +++ b/src/CP_SDK/Unity/MTMainThreadInvoker.cpp @@ -3,7 +3,7 @@ #include -const int MAX_QUEUE_SIZE = 1000; +constexpr std::size_t MAX_QUEUE_SIZE = 1000; using namespace UnityEngine; @@ -19,10 +19,9 @@ namespace CP_SDK::Unity { //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - MTMainThreadInvoker::Queue** MTMainThreadInvoker::m_Queues = nullptr; - bool MTMainThreadInvoker::m_Queued = false; - int MTMainThreadInvoker::m_FrontQueue = 0; - std::mutex MTMainThreadInvoker::m_Mutex; + std::deque> MTMainThreadInvoker::m_Queue; + bool MTMainThreadInvoker::m_Accepting = false; + std::mutex MTMainThreadInvoker::m_Mutex; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -44,37 +43,33 @@ namespace CP_SDK::Unity { /// @brief Unity GameObject initialize void MTMainThreadInvoker::Initialize() { + std::lock_guard l_Lock(m_Mutex); if (m_Instance) return; - m_Queues = new MTMainThreadInvoker::Queue*[2]; - - for (int l_I = 0; l_I < 2; ++l_I) - { - m_Queues[l_I] = new MTMainThreadInvoker::Queue(); - m_Queues[l_I]->Data = new _v::Action<>*[MAX_QUEUE_SIZE]; - m_Queues[l_I]->WritePos = 0; - } - auto l_GameObject = GameObject::New_ctor(u"[CP_SDK.Unity.MTMainThreadInvoker]"); GameObject::DontDestroyOnLoad(l_GameObject); m_Instance = l_GameObject->AddComponent(); + m_Queue.clear(); + m_Accepting = true; } /// @brief Stop void MTMainThreadInvoker::Destroy() { - if (!m_Instance) - return; - - for (int l_I = 0; l_I < 2; ++l_I) - delete[] m_Queues[l_I]->Data; - - delete[] m_Queues; - m_Queues = nullptr; + MTMainThreadInvoker* l_Instance = nullptr; + { + std::lock_guard l_Lock(m_Mutex); + if (!m_Instance) + return; + + m_Accepting = false; + m_Queue.clear(); + l_Instance = m_Instance; + m_Instance = nullptr; + } - GameObject::Destroy(m_Instance->get_gameObject()); - m_Instance = nullptr; + GameObject::Destroy(l_Instance->get_gameObject()); } //////////////////////////////////////////////////////////////////////////// @@ -86,15 +81,19 @@ namespace CP_SDK::Unity { { std::lock_guard l_Lock(m_Mutex); - auto l_Queue = m_Queues[m_FrontQueue]; - if (l_Queue->WritePos >= MAX_QUEUE_SIZE) + if (!m_Accepting || !m_Instance) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTMainThreadInvoker.Enqueue] Invoker is not running!"); + return; + } + + if (m_Queue.size() >= MAX_QUEUE_SIZE) { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTMainThreadInvoker.Enqueue] Too many actions pushed!"); return; } - l_Queue->Data[l_Queue->WritePos++] = new _v::Action<>(p_Delegate); - m_Queued = true; + m_Queue.emplace_back(p_Delegate); } //////////////////////////////////////////////////////////////////////////// @@ -103,53 +102,28 @@ namespace CP_SDK::Unity { /// @brief Unity GameObject update void MTMainThreadInvoker::Update() { - if (!m_Queued) - return; - - auto l_QueueToHandle = m_FrontQueue; - auto l_NextFrontQueue = (m_FrontQueue + 1) & 1; - - m_Mutex.lock(); - m_FrontQueue = l_NextFrontQueue; - m_Queued = false; - m_Mutex.unlock(); - - auto l_Queue = m_Queues[l_QueueToHandle]; - auto l_Count = l_Queue->WritePos; - auto l_I = 0; + std::deque<_v::Action<>> l_Actions; + { + std::lock_guard l_Lock(m_Mutex); + l_Actions.swap(m_Queue); + } - do + for (auto& l_Action : l_Actions) { try { - l_Queue->Data[l_I]->Invoke(); - delete l_Queue->Data[l_I]; + l_Action.Invoke(); } catch (const std::exception& l_Exception) { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTMainThreadInvoker.Update] Error:"); ChatPlexSDK::Logger()->Error(l_Exception); - - delete l_Queue->Data[l_I]; } - - ++l_I; - } while (l_I < l_Count); - - if (l_I < l_Count) - { - auto l_ToCopy = l_Count - l_I; - auto l_FrontQueue = m_Queues[m_FrontQueue]; - - m_Mutex.lock(); - memcpy(&l_FrontQueue->Data[l_FrontQueue->WritePos], &l_Queue->Data[l_I], l_ToCopy * sizeof(_v::Action<>*)); - l_FrontQueue->WritePos += l_ToCopy; - m_Queued = true; - m_Mutex.unlock(); + catch (...) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTMainThreadInvoker.Update] Unknown error"); + } } - - memset(l_Queue->Data, 0, l_Count * sizeof(_v::Action<>*)); - l_Queue->WritePos = 0; } -} ///< namespace CP_SDK::Unity \ No newline at end of file +} ///< namespace CP_SDK::Unity diff --git a/src/CP_SDK/Unity/MTThreadInvoker.cpp b/src/CP_SDK/Unity/MTThreadInvoker.cpp index 6685e96..8280191 100644 --- a/src/CP_SDK/Unity/MTThreadInvoker.cpp +++ b/src/CP_SDK/Unity/MTThreadInvoker.cpp @@ -1,22 +1,17 @@ #include "CP_SDK/Unity/MTThreadInvoker.hpp" #include "CP_SDK/ChatPlexSDK.hpp" -#include -#include -#include +#include -#include - -const int MAX_QUEUE_SIZE = 1000; +constexpr std::size_t MAX_QUEUE_SIZE = 1000; namespace CP_SDK::Unity { bool MTThreadInvoker::m_RunCondition = false; il2cpp_utils::il2cpp_aware_thread* MTThreadInvoker::m_UpdateThread = nullptr; - MTThreadInvoker::Queue** MTThreadInvoker::m_Queues; - bool MTThreadInvoker::m_Queued = false; - int MTThreadInvoker::m_FrontQueue = 0; + std::deque> MTThreadInvoker::m_Queue; std::mutex MTThreadInvoker::m_Mutex; + std::condition_variable MTThreadInvoker::m_Condition; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -24,48 +19,47 @@ namespace CP_SDK::Unity { /// @brief Initialize void MTThreadInvoker::Initialize() { + std::lock_guard l_Lock(m_Mutex); if (m_UpdateThread) return; - m_Queues = new MTThreadInvoker::Queue*[2]; - - for (int l_I = 0; l_I < 2; ++l_I) + m_Queue.clear(); + m_RunCondition = true; + try { - m_Queues[l_I] = new MTThreadInvoker::Queue(); - m_Queues[l_I]->Data = new _v::Action<>*[MAX_QUEUE_SIZE]; - m_Queues[l_I]->WritePos = 0; + m_UpdateThread = new il2cpp_utils::il2cpp_aware_thread(&MTThreadInvoker::__INTERNAL_Update); + } + catch (...) + { + m_RunCondition = false; + throw; } - - m_RunCondition = true; - - m_UpdateThread = new il2cpp_utils::il2cpp_aware_thread(&MTThreadInvoker::__INTERNAL_Update); } /// @brief Stop void MTThreadInvoker::Destroy() { - if (!m_UpdateThread) - return; + il2cpp_utils::il2cpp_aware_thread* l_Thread = nullptr; + { + std::lock_guard l_Lock(m_Mutex); + if (!m_UpdateThread) + return; + + m_RunCondition = false; + l_Thread = m_UpdateThread; + } + m_Condition.notify_all(); - m_RunCondition = false; - if (m_UpdateThread->joinable()) - m_UpdateThread->join(); - delete m_UpdateThread; - m_UpdateThread = nullptr; + if (l_Thread->joinable()) + l_Thread->join(); + delete l_Thread; - for (int l_I = 0; l_I < 2; ++l_I) { - for (int l_Y = 0; l_Y < m_Queues[l_I]->WritePos; ++l_Y) - { - auto l_Delegate = m_Queues[l_I]->Data[l_Y]; - if (l_Delegate) - delete l_Delegate; - } + std::lock_guard l_Lock(m_Mutex); + if (m_UpdateThread == l_Thread) + m_UpdateThread = nullptr; - delete[] m_Queues[l_I]->Data; + m_Queue.clear(); } - - delete[] m_Queues; - m_Queues = nullptr; } //////////////////////////////////////////////////////////////////////////// @@ -77,15 +71,20 @@ namespace CP_SDK::Unity { { std::lock_guard l_Lock(m_Mutex); - auto l_Queue = m_Queues[m_FrontQueue]; - if (l_Queue->WritePos >= MAX_QUEUE_SIZE) + if (!m_RunCondition || !m_UpdateThread) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTThreadInvoker.Enqueue] Invoker is not running!"); + return; + } + + if (m_Queue.size() >= MAX_QUEUE_SIZE) { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTThreadInvoker.Enqueue] Too many actions pushed!"); return; } - l_Queue->Data[l_Queue->WritePos++] = new _v::Action<>(p_Delegate); - m_Queued = true; + m_Queue.emplace_back(p_Delegate); + m_Condition.notify_one(); } //////////////////////////////////////////////////////////////////////////// @@ -94,61 +93,37 @@ namespace CP_SDK::Unity { /// @brief Thread update void MTThreadInvoker::__INTERNAL_Update() { - while (m_RunCondition) + while (true) { - if (!m_Queued) - { - _u::Thread::Sleep(10); - continue; - } - - auto l_QueueToHandle = m_FrontQueue; - auto l_NextFrontQueue = (m_FrontQueue + 1) & 1; - - m_Mutex.lock(); - m_FrontQueue = l_NextFrontQueue; - m_Queued = false; - m_Mutex.unlock(); - - auto l_Queue = m_Queues[l_QueueToHandle]; - auto l_Count = l_Queue->WritePos; - auto l_I = 0; - - do { - try + std::deque<_v::Action<>> l_Actions; { - l_Queue->Data[l_I]->Invoke(); - delete l_Queue->Data[l_I]; - } - catch (const std::exception& l_Exception) - { - ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTThreadInvoker.Update] Error:"); - ChatPlexSDK::Logger()->Error(l_Exception); + std::unique_lock l_Lock(m_Mutex); + m_Condition.wait(l_Lock, [] { return !m_RunCondition || !m_Queue.empty(); }); + if (!m_RunCondition && m_Queue.empty()) + break; - delete l_Queue->Data[l_I]; + l_Actions.swap(m_Queue); } - ++l_I; - } while (l_I < l_Count); - - if (l_I < l_Count) - { - auto l_ToCopy = l_Count - l_I; - auto l_FrontQueue = m_Queues[m_FrontQueue]; - - m_Mutex.lock(); - memcpy(&l_FrontQueue->Data[l_FrontQueue->WritePos], &l_Queue->Data[l_I], l_ToCopy * sizeof(_v::Action<>*)); - l_FrontQueue->WritePos += l_ToCopy; - m_Queued = true; - m_Mutex.unlock(); + for (auto& l_Action : l_Actions) + { + try + { + l_Action.Invoke(); + } + catch (const std::exception& l_Exception) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTThreadInvoker.Update] Error:"); + ChatPlexSDK::Logger()->Error(l_Exception); + } + catch (...) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MTThreadInvoker.Update] Unknown error"); + } + } } - - memset(l_Queue->Data, 0, l_Count * sizeof(_v::Action<>*)); - l_Queue->WritePos = 0; - - _u::Thread::Sleep(10); } } -} ///< namespace CP_SDK::Unity \ No newline at end of file +} ///< namespace CP_SDK::Unity diff --git a/src/CP_SDK/Unity/MonoPtrHolder.cpp b/src/CP_SDK/Unity/MonoPtrHolder.cpp index cb84d86..73ce380 100644 --- a/src/CP_SDK/Unity/MonoPtrHolder.cpp +++ b/src/CP_SDK/Unity/MonoPtrHolder.cpp @@ -1,11 +1,35 @@ #include "CP_SDK/Utils/MonoPtr.hpp" #include "CP_SDK/ChatPlexSDK.hpp" +#include + using namespace System::Collections::Generic; using namespace UnityEngine; namespace CP_SDK::Unity { + namespace { + class MonitorGuard + { + public: + explicit MonitorGuard(Il2CppObject* p_Monitor) : m_Monitor(p_Monitor) + { + il2cpp_functions::monitor_enter(m_Monitor); + } + + ~MonitorGuard() + { + il2cpp_functions::monitor_exit(m_Monitor); + } + + MonitorGuard(const MonitorGuard&) = delete; + MonitorGuard& operator=(const MonitorGuard&) = delete; + + private: + Il2CppObject* m_Monitor; + }; + } + std::map MonoPtrHolder::m_PointersToWrapper; //////////////////////////////////////////////////////////////////////////// @@ -51,10 +75,16 @@ namespace CP_SDK::Unity { m_Count++; } /// @brief Drop a reference to this wrapper - void MonoPtrHolder::Wrapper::Drop() + void MonoPtrHolder::Wrapper::Drop() noexcept { - if (m_Count.fetch_sub(1) == 1) + try + { MonoPtrHolder::Release(Ptr); + } + catch (...) + { + /// Destructors and cleanup paths must never propagate exceptions. + } } //////////////////////////////////////////////////////////////////////////// @@ -73,23 +103,31 @@ namespace CP_SDK::Unity { throw std::runtime_error("MonoPtrHolder was not initialized!"); } - il2cpp_functions::monitor_enter(m_Instance->m_Pointers); + MonitorGuard l_Guard(reinterpret_cast(m_Instance->m_Pointers)); const auto& l_It = m_Instance->m_PointersToWrapper.find(p_Pointer); if (l_It != m_Instance->m_PointersToWrapper.end()) { - il2cpp_functions::monitor_exit(m_Instance->m_Pointers); + l_It->second->Grab(); return l_It->second; } - auto l_NewWrapper = new Wrapper(); + auto l_NewWrapper = std::make_unique(); l_NewWrapper->Ptr = p_Pointer; m_Instance->m_Pointers->Add(p_Pointer); - m_Instance->m_PointersToWrapper[p_Pointer] = l_NewWrapper; + try + { + m_Instance->m_PointersToWrapper.emplace(p_Pointer, l_NewWrapper.get()); + } + catch (...) + { + m_Instance->m_Pointers->Remove(p_Pointer); + throw; + } - il2cpp_functions::monitor_exit(m_Instance->m_Pointers); - return l_NewWrapper; + l_NewWrapper->Grab(); + return l_NewWrapper.release(); } /// @brief Release a wrapper /// @param p_Pointer Il2Cpp object @@ -98,21 +136,28 @@ namespace CP_SDK::Unity { if (!m_Instance) return; ///< Most likely happen on exit, do nothing - il2cpp_functions::monitor_enter(m_Instance->m_Pointers); + MonitorGuard l_Guard(reinterpret_cast(m_Instance->m_Pointers)); const auto& l_It = m_Instance->m_PointersToWrapper.find(p_Pointer); if (l_It == m_Instance->m_PointersToWrapper.end()) { - il2cpp_functions::monitor_exit(m_Instance->m_Pointers); - ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MonoPtrHolder.Release] Try to release a non registered object!"); - throw std::runtime_error("MonoPtrHolder: Try to release a non registered object!"); + return; } + const auto l_Count = l_It->second->m_Count.load(); + if (l_Count <= 0) + { + ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][MonoPtrHolder.Release] Wrapper reference count underflow prevented!"); + return; + } + + if (l_It->second->m_Count.fetch_sub(1) != 1) + return; + m_Instance->m_Pointers->Remove(p_Pointer); delete l_It->second; m_Instance->m_PointersToWrapper.erase(l_It); - il2cpp_functions::monitor_exit(m_Instance->m_Pointers); } -} ///< namespace CP_SDK::Unity \ No newline at end of file +} ///< namespace CP_SDK::Unity diff --git a/src/CP_SDK/Unity/Texture2DU.cpp b/src/CP_SDK/Unity/Texture2DU.cpp index a386b2c..69da84e 100644 --- a/src/CP_SDK/Unity/Texture2DU.cpp +++ b/src/CP_SDK/Unity/Texture2DU.cpp @@ -1,5 +1,6 @@ #include "CP_SDK/Unity/Texture2DU.hpp" #include "CP_SDK/Unity/MTMainThreadInvoker.hpp" +#include "CP_SDK/Unity/MTThreadInvoker.hpp" #include "CP_SDK/Unity/Extensions/ColorU.hpp" #include "CP_SDK/ChatPlexSDK.hpp" @@ -9,6 +10,11 @@ #include #include +#include +#include +#include +#include + using namespace UnityEngine; namespace CP_SDK::Unity { @@ -17,7 +23,7 @@ namespace CP_SDK::Unity { /// @param p_Bytes Raw Texture 2D data Texture2D* Texture2DU::CreateFromRaw(::Array* p_Bytes) { - if (p_Bytes->get_Length() > 0) + if (p_Bytes && p_Bytes->get_Length() > 0) { try { @@ -39,9 +45,13 @@ namespace CP_SDK::Unity { /// @param p_Callback Callback void Texture2DU::CreateFromRawThreaded(_v::MonoPtr<::Array> p_Bytes, _v::Action p_Callback) { - if (p_Bytes && p_Bytes->get_Length() > 0) - { - stbi_uc* l_STBIBuffer = nullptr; + MTThreadInvoker::EnqueueOnThread([p_Bytes = std::move(p_Bytes), p_Callback = std::move(p_Callback)]() -> void { + if (!p_Bytes || p_Bytes->get_Length() <= 0) + { + MTMainThreadInvoker::Enqueue([p_Callback]() -> void { p_Callback(nullptr); }); + return; + } + try { int l_InputChannels; @@ -50,24 +60,31 @@ namespace CP_SDK::Unity { if (!stbi_info_from_memory(p_Bytes->_values, p_Bytes->get_Length(), &l_Width, &l_Height, &l_InputChannels)) throw std::runtime_error("Failed to load picture"); + if (l_Width <= 0 || l_Height <= 0 + || static_cast(l_Width) > static_cast(std::numeric_limits::max()) / static_cast(l_Height)) + throw std::runtime_error("Invalid picture dimensions"); - stbi_set_flip_vertically_on_load(1); + stbi_set_flip_vertically_on_load_thread(1); - l_STBIBuffer = stbi_load_from_memory(p_Bytes->_values, p_Bytes->get_Length(), &l_Width, &l_Height, &l_InputChannels, 4); + auto l_STBIBuffer = std::unique_ptr( + stbi_load_from_memory(p_Bytes->_values, p_Bytes->get_Length(), &l_Width, &l_Height, &l_InputChannels, 4), + &stbi_image_free + ); if (!l_STBIBuffer) throw std::runtime_error("Failed to load picture"); + if (l_Width <= 0 || l_Height <= 0 + || static_cast(l_Width) > static_cast(std::numeric_limits::max()) / static_cast(l_Height)) + throw std::runtime_error("Decoded picture dimensions are invalid"); - _v::MonoPtr<::Array> l_Pixels = ::Array::NewLength(l_Width * l_Height); + const auto l_PixelCount = l_Width * l_Height; + _v::MonoPtr<::Array> l_Pixels = ::Array::NewLength(l_PixelCount); - for (auto l_I = 0; l_I < (l_Width * l_Height); ++l_I) + for (auto l_I = 0; l_I < l_PixelCount; ++l_I) { - auto l_SrcPixel = &l_STBIBuffer[l_I * 4]; + auto l_SrcPixel = &l_STBIBuffer.get()[l_I * 4]; l_Pixels->_values[l_I] = Extensions::ColorU::Convert(Color32(0, l_SrcPixel[0], l_SrcPixel[1], l_SrcPixel[2], l_SrcPixel[3])); } - stbi_image_free(l_STBIBuffer); - l_STBIBuffer = nullptr; - MTMainThreadInvoker::Enqueue([=]() -> void { Texture2D* l_Texture = nullptr; @@ -95,15 +112,16 @@ namespace CP_SDK::Unity { ChatPlexSDK::Logger()->Error(l_Exception); } }); + return; } catch (const std::exception& l_Exception) { ChatPlexSDK::Logger()->Error(u"[CP_SDK.Unity][Texture2D.CreateFromRawThreaded] Error:"); ChatPlexSDK::Logger()->Error(l_Exception); } - } - p_Callback(nullptr); + MTMainThreadInvoker::Enqueue([p_Callback]() -> void { p_Callback(nullptr); }); + }); } -} ///< namespace CP_SDK::Unity \ No newline at end of file +} ///< namespace CP_SDK::Unity diff --git a/src/CP_SDK/Unity/TextureRaw.cpp b/src/CP_SDK/Unity/TextureRaw.cpp index 9e5bdb7..16be794 100644 --- a/src/CP_SDK/Unity/TextureRaw.cpp +++ b/src/CP_SDK/Unity/TextureRaw.cpp @@ -10,6 +10,11 @@ #include #include +#include +#include +#include +#include + using namespace System; using namespace UnityEngine; @@ -30,7 +35,7 @@ namespace CP_SDK::Unity { p_Width = 0; p_Height = 0; - if (p_Bytes == nullptr && p_Bytes->get_Length() == 0) + if (p_Bytes == nullptr || p_Bytes->get_Length() <= 0) return false; int l_InputChannels; @@ -38,25 +43,33 @@ namespace CP_SDK::Unity { if (!stbi_info_from_memory(p_Bytes->_values, p_Bytes->get_Length(), &p_Width, &p_Height, &l_InputChannels)) return false; - stbi_set_flip_vertically_on_load(1); + stbi_set_flip_vertically_on_load_thread(1); - auto l_STBIBuffer = stbi_load_from_memory(p_Bytes->_values, p_Bytes->get_Length(), &p_Width, &p_Height, &l_InputChannels, 4); + auto l_STBIBuffer = std::unique_ptr( + stbi_load_from_memory(p_Bytes->_values, p_Bytes->get_Length(), &p_Width, &p_Height, &l_InputChannels, 4), + &stbi_image_free + ); if (!l_STBIBuffer) return false; + if (p_Width <= 0 || p_Height <= 0 + || static_cast(p_Width) > std::numeric_limits::max() / static_cast(p_Height)) + return false; + + const auto l_PixelCount = static_cast(p_Width) * static_cast(p_Height); + *p_Pixels = std::make_shared>(); auto& l_Pixels = *(*p_Pixels).get(); - l_Pixels.resize(p_Width * p_Height); + l_Pixels.resize(l_PixelCount); - for (auto l_I = 0; l_I < (p_Width * p_Height); ++l_I) + const auto l_STBIBufferRef = l_STBIBuffer.get(); + for (std::size_t l_I = 0; l_I < l_PixelCount; ++l_I) { - auto l_SrcPixel = &l_STBIBuffer[l_I * 4]; + auto l_SrcPixel = &l_STBIBufferRef[l_I * 4]; l_Pixels[l_I] = Extensions::ColorU::Convert(Color32(0, l_SrcPixel[0], l_SrcPixel[1], l_SrcPixel[2], l_SrcPixel[3])); } - stbi_image_free(l_STBIBuffer); - return true; } @@ -70,6 +83,17 @@ namespace CP_SDK::Unity { /// @param p_Radius Blur radius void TextureRaw::FastGaussianBlur(int p_InWidth, int p_InHeight, PixelArray& p_InPixels, int p_Radius) { + if (p_InWidth <= 0 || p_InHeight <= 0 || !p_InPixels) + throw std::invalid_argument("TextureRaw::FastGaussianBlur received invalid image data"); + + const auto l_ExpectedSize = static_cast(p_InWidth) * static_cast(p_InHeight); + if (p_InPixels->size() < l_ExpectedSize) + throw std::invalid_argument("TextureRaw::FastGaussianBlur pixel buffer is too small"); + + p_Radius = std::clamp(p_Radius, 0, std::min(p_InWidth, p_InHeight) - 1); + if (p_Radius == 0) + return; + GaussianBlur4(p_InWidth, p_InHeight, p_InPixels, p_Radius); } /// @brief Multiply image A & B into A @@ -77,6 +101,9 @@ namespace CP_SDK::Unity { /// @param p_ImageB Additional image void TextureRaw::Multiply(PixelArray& p_ImageA, PixelArray& p_ImageB) { + if (!p_ImageA || !p_ImageB) + throw std::invalid_argument("TextureRaw::Multiply received a null image"); + if (p_ImageA->size() != p_ImageB->size()) throw std::runtime_error("[CP_SDK.Unity][TextureRaw.Multiply] Size differ! " + std::to_string(p_ImageA->size()) + " vs " + std::to_string(p_ImageB->size())); @@ -95,6 +122,17 @@ namespace CP_SDK::Unity { /// @param p_YOffsetRel Height anchor TextureRaw::PixelArray TextureRaw::ResampleAndCrop(int p_InWidth, int p_InHeight, PixelArray& p_InPixels, int p_TargetWidth, int p_TargetHeight, float p_YOffsetRel) { + if (p_InWidth <= 0 || p_InHeight <= 0 || p_TargetWidth <= 0 || p_TargetHeight <= 0 || !p_InPixels) + throw std::invalid_argument("TextureRaw::ResampleAndCrop received invalid dimensions"); + + const auto l_InputSize = static_cast(p_InWidth) * static_cast(p_InHeight); + if (p_InPixels->size() < l_InputSize) + throw std::invalid_argument("TextureRaw::ResampleAndCrop pixel buffer is too small"); + + if (static_cast(p_TargetWidth) > std::numeric_limits::max() / static_cast(p_TargetHeight)) + throw std::overflow_error("TextureRaw::ResampleAndCrop target size overflow"); + + p_YOffsetRel = std::clamp(p_YOffsetRel, 0.0f, 1.0f); float l_SourceAspect = (float)p_InWidth / p_InHeight; float l_TargetAspect = (float)p_TargetWidth / p_TargetHeight; @@ -121,7 +159,7 @@ namespace CP_SDK::Unity { auto& l_Result = *(l_ResultPtr.get()); auto& l_InPixels = *(p_InPixels.get()); - l_Result.resize(p_TargetWidth * p_TargetHeight); + l_Result.resize(static_cast(p_TargetWidth) * static_cast(p_TargetHeight)); for (int l_Y = 0; l_Y < p_TargetHeight; ++l_Y) { @@ -260,4 +298,4 @@ namespace CP_SDK::Unity { } } -} ///< namespace CP_SDK::Unity \ No newline at end of file +} ///< namespace CP_SDK::Unity diff --git a/src/CP_SDK/Utils/Json.cpp b/src/CP_SDK/Utils/Json.cpp index 621312c..d3ba386 100644 --- a/src/CP_SDK/Utils/Json.cpp +++ b/src/CP_SDK/Utils/Json.cpp @@ -227,7 +227,10 @@ namespace CP_SDK::Utils { if (!p_Document.HasMember(p_Name)) return; CP_SDK_JSON_ENSURE_TYPE(IsObject); GET_OBJECT(); - uint32_t l_R, l_G, l_B, l_A; + uint32_t l_R = 0; + uint32_t l_G = 0; + uint32_t l_B = 0; + uint32_t l_A = 0; UnserializeUInt32(l_Object, l_R, u"r"); p_Value.r = static_cast(l_R); UnserializeUInt32(l_Object, l_G, u"g"); p_Value.g = static_cast(l_G); UnserializeUInt32(l_Object, l_B, u"b"); p_Value.b = static_cast(l_B); @@ -307,4 +310,4 @@ namespace CP_SDK::Utils { UnserializeFloat(l_Object, p_Value.z, u"z"); } -} ///< namespace CP_SDK::Utils \ No newline at end of file +} ///< namespace CP_SDK::Utils From 2745147f936e04016af5d16c4d2fa8a001c6554d Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:19:22 +0200 Subject: [PATCH 06/15] Fix scripts locations --- include/git_info.h | 4 +- mod.json | 14 +-- qpm.json | 8 +- qpm.shared.json | 236 ++++++++++++++++++++++----------------------- 4 files changed, 131 insertions(+), 131 deletions(-) diff --git a/include/git_info.h b/include/git_info.h index 96dda27..a266d1d 100644 --- a/include/git_info.h +++ b/include/git_info.h @@ -1,5 +1,5 @@ #pragma once #define GIT_USER "HardCPP" -#define GIT_BRANCH "review-fixes" -#define GIT_COMMIT 0x44c36c4 +#define GIT_BRANCH "dev" +#define GIT_COMMIT 0x2d4a383 #define GIT_MODIFIED 1 diff --git a/mod.json b/mod.json index 5b4d3dd..539bcb9 100644 --- a/mod.json +++ b/mod.json @@ -5,7 +5,7 @@ "id": "chatplex-sdk-bs", "modloader": "Scotland2", "author": "HardCPP", - "version": "6.4.1", + "version": "6.4.5", "packageId": "com.beatgames.beatsaber", "packageVersion": "1.40.8_7379", "description": "ChatPlex BeatSaber modding SDK (Dependence for other mods)", @@ -17,9 +17,9 @@ "downloadIfMissing": "https://github.com/QuestPackageManager/beatsaber-hook/releases/download/v6.4.2/beatsaber-hook.qmod" }, { - "version": "^0.18.3", + "version": "^0.18.4", "id": "custom-types", - "downloadIfMissing": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.3/CustomTypes.qmod" + "downloadIfMissing": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.4/CustomTypes.qmod" }, { "version": "^0.4.55", @@ -27,14 +27,14 @@ "downloadIfMissing": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/BSML.qmod" }, { - "version": "^1.1.24", + "version": "^1.1.26", "id": "songcore", - "downloadIfMissing": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.24/SongCore.qmod" + "downloadIfMissing": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.26/SongCore.qmod" }, { - "version": "^4.6.4", + "version": "^4.8.0", "id": "paper2_scotland2", - "downloadIfMissing": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.4/paper2_scotland2.qmod" + "downloadIfMissing": "https://github.com/Fernthedev/paperlog/releases/download/v4.8.0/paper2_scotland2.qmod" } ], "modFiles": [], diff --git a/qpm.json b/qpm.json index 24509b9..ca8e121 100644 --- a/qpm.json +++ b/qpm.json @@ -6,7 +6,7 @@ "info": { "name": "ChatPlexSDK-BS", "id": "chatplex-sdk-bs", - "version": "6.4.1", + "version": "6.4.5", "url": "https://github.com/hardcpp/QuestChatPlexSDK-BS", "additionalData": { "overrideSoName": "libchatplex-sdk-bs.so", @@ -58,7 +58,7 @@ }, { "id": "custom-types", - "versionRange": "^0.18.3", + "versionRange": "^0.18.4", "additionalData": {} }, { @@ -78,7 +78,7 @@ }, { "id": "songcore", - "versionRange": "^1.1.24", + "versionRange": "^1.1.26", "additionalData": { "private": true } @@ -92,7 +92,7 @@ }, { "id": "paper2_scotland2", - "versionRange": "^4.6.4", + "versionRange": "^4.8.0", "additionalData": {} }, { diff --git a/qpm.shared.json b/qpm.shared.json index 81aebe6..959826a 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -7,7 +7,7 @@ "info": { "name": "ChatPlexSDK-BS", "id": "chatplex-sdk-bs", - "version": "6.4.1", + "version": "6.4.5", "url": "https://github.com/hardcpp/QuestChatPlexSDK-BS", "additionalData": { "overrideSoName": "libchatplex-sdk-bs.so", @@ -17,28 +17,28 @@ "workspace": { "scripts": { "build": [ - "pwsh ./build.ps1" + "pwsh ./scripts/build.ps1" ], "clean": [ - "pwsh ./build.ps1 -clean" + "pwsh ./scripts/build.ps1 -clean" ], "copy": [ - "pwsh ./copy.ps1" + "pwsh ./scripts/copy.ps1" ], "log": [ - "pwsh ./start-logging.ps1" + "pwsh ./scripts/start-logging.ps1" ], "qmod": [ - "pwsh ./build.ps1 -clean", - "pwsh ./createqmod.ps1 -clean" + "pwsh ./scripts/build.ps1 -clean", + "pwsh ./scripts/createqmod.ps1 -clean" ], "qmod_backup": [ - "pwsh ./build.ps1 -clean", + "pwsh ./scripts/build.ps1 -clean", "qpm qmod build", - "pwsh ./createqmod.ps1 -clean" + "pwsh ./scripts/createqmod.ps1 -clean" ] }, - "ndk": "^27.2.12479018", + "ndk": "^27.3.13750724", "qmodIncludeDirs": [ "build", "extern/libs" @@ -59,7 +59,7 @@ }, { "id": "custom-types", - "versionRange": "^0.18.3", + "versionRange": "^0.18.4", "additionalData": {} }, { @@ -79,7 +79,7 @@ }, { "id": "songcore", - "versionRange": "^1.1.24", + "versionRange": "^1.1.26", "additionalData": { "private": true } @@ -93,7 +93,7 @@ }, { "id": "paper2_scotland2", - "versionRange": "^4.6.4", + "versionRange": "^4.8.0", "additionalData": {} }, { @@ -109,66 +109,16 @@ ] }, "restoredDependencies": [ - { - "dependency": { - "id": "bsml", - "versionRange": "=0.4.55", - "additionalData": { - "soLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/libbsml.so", - "debugSoLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/debug_libbsml.so", - "overrideSoName": "libbsml.so", - "modLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/BSML.qmod", - "branchName": "version/v0_4_55", - "cmake": true - } - }, - "version": "0.4.55" - }, - { - "dependency": { - "id": "paper2_scotland2", - "versionRange": "=4.6.4", - "additionalData": { - "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.4/libpaper2_scotland2.so", - "overrideSoName": "libpaper2_scotland2.so", - "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.6.4/paper2_scotland2.qmod", - "branchName": "version/v4_6_4", - "compileOptions": { - "systemIncludes": [ - "shared/utfcpp/source" - ] - }, - "cmake": false - } - }, - "version": "4.6.4" - }, - { - "dependency": { - "id": "libil2cpp", - "versionRange": "=0.4.0", - "additionalData": { - "headersOnly": true, - "compileOptions": { - "systemIncludes": [ - "il2cpp/external/baselib/Include", - "il2cpp/external/baselib/Platforms/Android/Include" - ] - } - } - }, - "version": "0.4.0" - }, { "dependency": { "id": "custom-types", - "versionRange": "=0.18.3", + "versionRange": "=0.18.4", "additionalData": { - "soLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.3/libcustom-types.so", - "debugSoLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.3/debug_libcustom-types.so", + "soLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.4/libcustom-types.so", + "debugSoLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.4/debug_libcustom-types.so", "overrideSoName": "libcustom-types.so", - "modLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.3/CustomTypes.qmod", - "branchName": "version/v0_18_3", + "modLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.4/CustomTypes.qmod", + "branchName": "version/v0_18_4", "compileOptions": { "cppFlags": [ "-Wno-invalid-offsetof" @@ -177,24 +127,20 @@ "cmake": true } }, - "version": "0.18.3" + "version": "0.18.4" }, { "dependency": { - "id": "kaleb", - "versionRange": "=0.1.9", + "id": "scotland2", + "versionRange": "=0.1.7", "additionalData": { - "headersOnly": true, - "branchName": "version/v0_1_9", - "compileOptions": { - "cppFlags": [ - "-DKALEB_VERSION=\"0.1.9\"" - ] - }, - "cmake": false + "soLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.7/libsl2.so", + "debugSoLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.7/debug_libsl2.so", + "overrideSoName": "libsl2.so", + "branchName": "version/v0_1_7" } }, - "version": "0.1.9" + "version": "0.1.7" }, { "dependency": { @@ -220,6 +166,25 @@ }, "version": "4008.0.0" }, + { + "dependency": { + "id": "paper2_scotland2", + "versionRange": "=4.8.0", + "additionalData": { + "soLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.8.0/libpaper2_scotland2.so", + "overrideSoName": "libpaper2_scotland2.so", + "modLink": "https://github.com/Fernthedev/paperlog/releases/download/v4.8.0/paper2_scotland2.qmod", + "branchName": "version/v4_8_0", + "compileOptions": { + "systemIncludes": [ + "shared/utfcpp/source" + ] + }, + "cmake": false + } + }, + "version": "4.8.0" + }, { "dependency": { "id": "conditional-dependencies", @@ -232,6 +197,36 @@ }, "version": "0.3.0" }, + { + "dependency": { + "id": "tinyxml2", + "versionRange": "=10.0.0", + "additionalData": { + "soLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/libtinyxml2.so", + "debugSoLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/debug_libtinyxml2.so", + "overrideSoName": "libtinyxml2.so", + "modLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/tinyxml2.qmod", + "branchName": "version/v10_0_0", + "cmake": true + } + }, + "version": "10.0.0" + }, + { + "dependency": { + "id": "songcore", + "versionRange": "=1.1.26", + "additionalData": { + "soLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.26/libsongcore.so", + "debugSoLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.26/debug_libsongcore.so", + "overrideSoName": "libsongcore.so", + "modLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.26/SongCore.qmod", + "branchName": "version/v1_1_26", + "cmake": true + } + }, + "version": "1.1.26" + }, { "dependency": { "id": "libcurl", @@ -247,18 +242,22 @@ }, { "dependency": { - "id": "songcore", - "versionRange": "=1.1.24", + "id": "fmt", + "versionRange": "=11.0.2", "additionalData": { - "soLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.24/libsongcore.so", - "debugSoLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.24/debug_libsongcore.so", - "overrideSoName": "libsongcore.so", - "modLink": "https://github.com/raineaeternal/Quest-SongCore/releases/download/v1.1.24/SongCore.qmod", - "branchName": "version/v1_1_24", - "cmake": true + "headersOnly": true, + "branchName": "version/v11_0_2", + "compileOptions": { + "systemIncludes": [ + "fmt/include/" + ], + "cppFlags": [ + "-DFMT_HEADER_ONLY" + ] + } } }, - "version": "1.1.24" + "version": "11.0.2" }, { "dependency": { @@ -282,50 +281,51 @@ }, { "dependency": { - "id": "fmt", - "versionRange": "=11.0.2", + "id": "bsml", + "versionRange": "=0.4.55", "additionalData": { - "headersOnly": true, - "branchName": "version/v11_0_2", - "compileOptions": { - "systemIncludes": [ - "fmt/include/" - ], - "cppFlags": [ - "-DFMT_HEADER_ONLY" - ] - } + "soLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/libbsml.so", + "debugSoLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/debug_libbsml.so", + "overrideSoName": "libbsml.so", + "modLink": "https://github.com/bsq-ports/Quest-BSML/releases/download/v0.4.55/BSML.qmod", + "branchName": "version/v0_4_55", + "cmake": true } }, - "version": "11.0.2" + "version": "0.4.55" }, { "dependency": { - "id": "scotland2", - "versionRange": "=0.1.6", + "id": "libil2cpp", + "versionRange": "=0.4.0", "additionalData": { - "soLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.6/libsl2.so", - "debugSoLink": "https://github.com/sc2ad/scotland2/releases/download/v0.1.6/debug_libsl2.so", - "overrideSoName": "libsl2.so", - "branchName": "version/v0_1_6" + "headersOnly": true, + "compileOptions": { + "systemIncludes": [ + "il2cpp/external/baselib/Include", + "il2cpp/external/baselib/Platforms/Android/Include" + ] + } } }, - "version": "0.1.6" + "version": "0.4.0" }, { "dependency": { - "id": "tinyxml2", - "versionRange": "=10.0.0", + "id": "kaleb", + "versionRange": "=0.1.9", "additionalData": { - "soLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/libtinyxml2.so", - "debugSoLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/debug_libtinyxml2.so", - "overrideSoName": "libtinyxml2.so", - "modLink": "https://github.com/MillzyDev/NDK-tinyxml2/releases/download/v10.0.0/tinyxml2.qmod", - "branchName": "version/v10_0_0", - "cmake": true + "headersOnly": true, + "branchName": "version/v0_1_9", + "compileOptions": { + "cppFlags": [ + "-DKALEB_VERSION=\"0.1.9\"" + ] + }, + "cmake": false } }, - "version": "10.0.0" + "version": "0.1.9" } ] } \ No newline at end of file From afed996d561853aa32824216cec5eaf0e3411746 Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:21:10 +0200 Subject: [PATCH 07/15] Halloween theme --- shared/CP_SDK/CPConfig.hpp | 1 + src/CP_SDK/CPConfig.cpp | 2 ++ src/CP_SDK/UI/Components/CDropDown.cpp | 1 - .../UI/DefaultComponents/DefaultCDropdown.cpp | 6 +++--- .../DefaultCTextSegmentedControl.cpp | 2 +- .../DefaultComponents/DefaultCVScrollView.cpp | 2 +- src/CP_SDK/UI/UISystem.cpp | 19 ++++++++++++++++++- 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/shared/CP_SDK/CPConfig.hpp b/shared/CP_SDK/CPConfig.hpp index e1cf727..4e96b9f 100644 --- a/shared/CP_SDK/CPConfig.hpp +++ b/shared/CP_SDK/CPConfig.hpp @@ -13,6 +13,7 @@ namespace CP_SDK { public: bool FirstRun = true; bool FirstChatServiceRun = true; + bool EventSpecials = true; std::u16string ChatPlexServiceToken = u""; protected: diff --git a/src/CP_SDK/CPConfig.cpp b/src/CP_SDK/CPConfig.cpp index 7eb208f..1a87e9b 100644 --- a/src/CP_SDK/CPConfig.cpp +++ b/src/CP_SDK/CPConfig.cpp @@ -23,6 +23,7 @@ namespace CP_SDK { { CP_SDK_JSON_SERIALIZE_BOOL(FirstRun); CP_SDK_JSON_SERIALIZE_BOOL(FirstChatServiceRun); + CP_SDK_JSON_SERIALIZE_BOOL(EventSpecials); CP_SDK_JSON_SERIALIZE_STRING(ChatPlexServiceToken); } /// @brief Read the document @@ -31,6 +32,7 @@ namespace CP_SDK { { CP_SDK_JSON_UNSERIALIZE_BOOL(FirstRun); CP_SDK_JSON_UNSERIALIZE_BOOL(FirstChatServiceRun); + CP_SDK_JSON_UNSERIALIZE_BOOL(EventSpecials); CP_SDK_JSON_UNSERIALIZE_STRING(ChatPlexServiceToken); } diff --git a/src/CP_SDK/UI/Components/CDropDown.cpp b/src/CP_SDK/UI/Components/CDropDown.cpp index 058b32c..2a0abd3 100644 --- a/src/CP_SDK/UI/Components/CDropDown.cpp +++ b/src/CP_SDK/UI/Components/CDropDown.cpp @@ -1,5 +1,4 @@ #include "CP_SDK/UI/Components/CDropdown.hpp" -#include "CP_SDK/UI/UISystem.hpp" using namespace UnityEngine; using namespace UnityEngine::UI; diff --git a/src/CP_SDK/UI/DefaultComponents/DefaultCDropdown.cpp b/src/CP_SDK/UI/DefaultComponents/DefaultCDropdown.cpp index 0bff403..b8fc2ed 100644 --- a/src/CP_SDK/UI/DefaultComponents/DefaultCDropdown.cpp +++ b/src/CP_SDK/UI/DefaultComponents/DefaultCDropdown.cpp @@ -160,7 +160,7 @@ namespace CP_SDK::UI::DefaultComponents { if (!options.empty()) { m_Options.reserve(options.size()); - m_Options.insert(options.begin(), options.begin(), options.end()); + m_Options.insert(m_Options.begin(), options.begin(), options.end()); } auto l_It = std::find(m_Options.begin(), m_Options.end(), l_ValueStr); @@ -200,7 +200,7 @@ namespace CP_SDK::UI::DefaultComponents { if (m_Value >= 0 && m_Value < m_Options.size()) m_ValueText->TMProUGUI()->set_text(m_Options[m_Value]); else - m_ValueText->TMProUGUI()->set_text("None"); + m_ValueText->TMProUGUI()->set_text(u"None"); } /// @brief Notify void DefaultCDropdown::Notify() @@ -238,4 +238,4 @@ namespace CP_SDK::UI::DefaultComponents { }); } -} ///< namespace CP_SDK::UI::DefaultComponents \ No newline at end of file +} ///< namespace CP_SDK::UI::DefaultComponents diff --git a/src/CP_SDK/UI/DefaultComponents/DefaultCTextSegmentedControl.cpp b/src/CP_SDK/UI/DefaultComponents/DefaultCTextSegmentedControl.cpp index 4637450..934032e 100644 --- a/src/CP_SDK/UI/DefaultComponents/DefaultCTextSegmentedControl.cpp +++ b/src/CP_SDK/UI/DefaultComponents/DefaultCTextSegmentedControl.cpp @@ -141,7 +141,7 @@ namespace CP_SDK::UI::DefaultComponents { l_Background->set_material (UISystem::Override_GetUIMaterial()); l_Background->set_type (Image::Type::Sliced); l_Background->set_pixelsPerUnitMultiplier(1.0f); - l_Background->set_color (ColorU::ToUnityColor("#727272")); + l_Background->set_color (UISystem::TitleBlockBGColor); if (p_Texts.size() == 1) l_Background->set_sprite(UISystem::GetUIRoundBGSprite().Ptr()); diff --git a/src/CP_SDK/UI/DefaultComponents/DefaultCVScrollView.cpp b/src/CP_SDK/UI/DefaultComponents/DefaultCVScrollView.cpp index 0db76de..e703531 100644 --- a/src/CP_SDK/UI/DefaultComponents/DefaultCVScrollView.cpp +++ b/src/CP_SDK/UI/DefaultComponents/DefaultCVScrollView.cpp @@ -86,7 +86,7 @@ namespace CP_SDK::UI::DefaultComponents { auto l_ScrollBarBG = l_ScrollBar->get_gameObject()->AddComponent(UISystem::Override_UnityComponent_Image.ptr()).try_cast().value_or(nullptr); l_ScrollBarBG->set_material (UISystem::Override_GetUIMaterial()); - l_ScrollBarBG->set_color (ColorU::WithAlpha("#202020", 0.7f)); + l_ScrollBarBG->set_color (ColorU::WithAlpha(UISystem::ModalBGColor, 0.7f)); l_ScrollBarBG->set_pixelsPerUnitMultiplier(1); l_ScrollBarBG->set_type (Image::Type::Sliced); l_ScrollBarBG->set_raycastTarget (false); diff --git a/src/CP_SDK/UI/UISystem.cpp b/src/CP_SDK/UI/UISystem.cpp index e56fc2a..3ca64a6 100644 --- a/src/CP_SDK/UI/UISystem.cpp +++ b/src/CP_SDK/UI/UISystem.cpp @@ -27,7 +27,7 @@ #include "CP_SDK/Unity/FontManager.hpp" #include "CP_SDK/Unity/Texture2DU.hpp" #include "CP_SDK/Unity/SpriteU.hpp" -#include "CP_SDK/ChatPlexSDK.hpp" +#include "CP_SDK/CPConfig.hpp" #include "assets.hpp" #include @@ -35,6 +35,7 @@ #include #include #include +#include using namespace CP_SDK::Unity::Extensions; using namespace TMPro; @@ -140,6 +141,22 @@ namespace CP_SDK::UI { } ); + if (CPConfig::Instance()->EventSpecials && System::DateTime::get_Now().Month == 10) + { + DefaultBGColor = ColorU::WithAlpha("#15003d", 0.5000f); // Ok + NavigationBarBGColor = ColorU::WithAlpha("#9F79F2", 0.6500f); // Ok + PrimaryColor = ColorU::WithAlpha("#F24F13", 1.0000f); // Ok + SecondaryColor = ColorU::WithAlpha("#F27F1B", 1.0000f); // Ok + TitleBlockBGColor = NavigationBarBGColor; + ModalBGColor = ColorU::WithAlpha("#15003d", 0.9750f); // Ok + ListBGColor = ColorU::WithAlpha("#15003d", 0.7500f); + KeyboardTextBGColor = ColorU::WithAlpha("#7F7F7F", 0.5000f); + TooltipBGColor = ColorU::WithAlpha("#300089", 0.9875f); + + TextColor = ColorU::WithAlpha("#FFFFFF", 1.0000f); // Ok + TextColorDisabled = Color::op_Multiply(TextColor, 0.75f); + } + ScreenSystem::Create(); ModMenu::Create(); } From e7a7b9ef5be6be99a58048aa204c4a8c3696d8a9 Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:21:33 +0200 Subject: [PATCH 08/15] Migrate loading progress overlay to in house UI system --- shared/CP_SDK/UI/LoadingProgressBar.hpp | 12 ++-- src/CP_SDK/UI/LoadingProgressBar.cpp | 89 ++++++++++++++----------- 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/shared/CP_SDK/UI/LoadingProgressBar.hpp b/shared/CP_SDK/UI/LoadingProgressBar.hpp index 7744f9a..74e7325 100644 --- a/shared/CP_SDK/UI/LoadingProgressBar.hpp +++ b/shared/CP_SDK/UI/LoadingProgressBar.hpp @@ -1,14 +1,14 @@ #pragma once +#include "Components/CFloatingPanel.hpp" #include "Components/CText.hpp" +#include "Components/CImage.hpp" #include "../Utils/Il2cpp.hpp" #include "../Unity/PersistantSingleton.hpp" #include "../ChatPlexSDK.hpp" #include #include -#include -#include #include @@ -34,10 +34,10 @@ namespace CP_SDK::UI { CP_SDK_UNITY_PERSISTANT_SINGLETON_DECL(CP_SDK::UI::LoadingProgressBar); private: - _v::MonoPtr<_u::Canvas> m_Canvas; - _v::MonoPtr<_v::CText> m_HeaderText; - _v::MonoPtr<_u::Image> m_LoadingBackground; - _v::MonoPtr<_u::Image> m_LoadingBar; + _v::MonoPtr<_v::CFloatingPanel> m_Canvas; + _v::MonoPtr<_v::CText> m_HeaderText; + _v::MonoPtr<_v::CImage> m_LoadingBackground; + _v::MonoPtr<_v::CImage> m_LoadingBar; public: /// @brief On component creation diff --git a/src/CP_SDK/UI/LoadingProgressBar.cpp b/src/CP_SDK/UI/LoadingProgressBar.cpp index f02d172..7d3a635 100644 --- a/src/CP_SDK/UI/LoadingProgressBar.cpp +++ b/src/CP_SDK/UI/LoadingProgressBar.cpp @@ -41,39 +41,28 @@ namespace CP_SDK::UI { /// @brief On component creation void LoadingProgressBar::Awake() { - auto l_Transform = get_transform(); - l_Transform->set_position(UnityEngine::Vector3(0, 2.5f, 4.0f)); - l_Transform->set_eulerAngles(UnityEngine::Vector3(0, 0, 0)); - l_Transform->set_localScale(UnityEngine::Vector3(0.01f, 0.01f, 0.01f)); - - m_Canvas = get_gameObject()->AddComponent(); - m_Canvas->set_renderMode(RenderMode::WorldSpace); - m_Canvas->set_enabled(false); - - auto l_RectTransform = m_Canvas->get_transform().try_cast().value_or(nullptr); - l_RectTransform->set_sizeDelta(Vector2(100, 50)); + m_Canvas = UISystem::FloatingPanelFactory->Create(u"", transform); + m_Canvas->transform->localScale = UnityEngine::Vector3(0.01f, 0.01f, 0.01f); + m_Canvas->SetTransformDirect(Vector3(0, 2.5f, 4.25f), Vector3(0, 0, 0)); + m_Canvas->SetSize(Vector2(150, 40)); + m_Canvas->SetBackground(false); m_HeaderText = UISystem::TextFactory->Create(u"", m_Canvas->get_transform()); if (m_HeaderText) { - l_RectTransform = m_HeaderText->RTransform().Ptr(); - l_RectTransform->set_anchoredPosition(Vector2( 0.0f, 15.0f)); - l_RectTransform->set_sizeDelta (Vector2(100.0f, 20.0f)); + m_HeaderText->RTransform()->set_anchoredPosition(Vector2(0.0f, 15.0f)); + m_HeaderText->RTransform()->set_sizeDelta(Vector2(100.0f, 20.0f)); m_HeaderText->SetFontSize(10.0f); m_HeaderText->SetAlign(TextAlignmentOptions::Midline); + m_HeaderText->SetText(u"..."); } - m_LoadingBackground = GameObject::New_ctor("Background")->AddComponent(); - l_RectTransform = m_LoadingBackground->get_transform().try_cast().value_or(nullptr); - l_RectTransform->SetParent(m_Canvas->get_transform(), false); - l_RectTransform->set_sizeDelta(Vector2(100, 10)); - m_LoadingBackground->set_color(Color(0, 0, 0, 0.2f)); - - m_LoadingBar = GameObject::New_ctor("Loading Bar")->AddComponent(); - l_RectTransform = m_LoadingBar->get_transform().try_cast().value_or(nullptr); - l_RectTransform->SetParent(m_Canvas->get_transform(), false); - l_RectTransform->set_sizeDelta(Vector2(100, 10)); - m_LoadingBar->set_sprite( + + + m_LoadingBackground = UISystem::ImageFactory->Create(u"", m_Canvas->get_transform()); + m_LoadingBackground->SetWidth(100); + m_LoadingBackground->SetHeight(10); + m_LoadingBackground->SetSprite( Sprite::Create( Texture2D::get_whiteTexture(), Rect(0, 0, Texture2D::get_whiteTexture()->get_width(), Texture2D::get_whiteTexture()->get_height()), @@ -85,9 +74,31 @@ namespace CP_SDK::UI { false ) ); - m_LoadingBar->set_type(Image::Type::Filled); - m_LoadingBar->set_fillMethod(Image::FillMethod::Horizontal); - m_LoadingBar->set_color(Color(0.1f, 1, 0.1f, 0.5f)); + m_LoadingBackground->SetColor(Color(0, 0, 0, 0.8f)); + m_LoadingBackground->ImageC()->preserveAspect = false; + + m_LoadingBar = UISystem::ImageFactory->Create(u"", m_Canvas->get_transform()); + m_LoadingBar->SetWidth(100); + m_LoadingBar->SetHeight(10); + m_LoadingBar->SetSprite( + Sprite::Create( + Texture2D::get_whiteTexture(), + Rect(0, 0, Texture2D::get_whiteTexture()->get_width(), Texture2D::get_whiteTexture()->get_height()), + Vector2::get_one() * 0.5f, + 100, + 1, + SpriteMeshType::FullRect, + Vector4::get_zero(), + false + ) + ); + m_LoadingBar->SetType(Image::Type::Filled); + m_LoadingBar->SetColor(Color(0.1f, 1, 0.1f, 0.5f)); + m_LoadingBar->ImageC()->fillMethod = Image::FillMethod::Horizontal; + m_LoadingBar->ImageC()->fillAmount = 0.5f; + m_LoadingBar->ImageC()->preserveAspect = false; + + m_Canvas->GetComponent()->enabled = false; ChatPlexSDK::OnGenericSceneChange += {this, &LoadingProgressBar::ChatPlexSDK_OnGenericSceneChange}; } @@ -105,9 +116,10 @@ namespace CP_SDK::UI { if (m_HeaderText) m_HeaderText->SetText(p_Message); - m_LoadingBar->set_enabled(false); - m_LoadingBackground->set_enabled(false); - m_Canvas->set_enabled(false); + m_LoadingBar->ImageC()->enabled = false; + m_LoadingBackground->ImageC()->enabled = false; + m_LoadingBar->ImageC()->fillAmount = 0.0f; + m_Canvas->GetComponent()->enabled = true; StartCoroutine(custom_types::Helpers::CoroutineHelper::New(Coroutine_DisableCanvas(this, p_Time))); } @@ -121,10 +133,10 @@ namespace CP_SDK::UI { if (m_HeaderText) m_HeaderText->SetText(p_Message); - m_LoadingBar->set_enabled(true); - m_LoadingBar->set_fillAmount(p_Progress); - m_LoadingBackground->set_enabled(true); - m_Canvas->set_enabled(true); + m_LoadingBar->ImageC()->enabled = true; + m_LoadingBar->ImageC()->fillAmount = p_Progress; + m_LoadingBackground->ImageC()->enabled = true; + m_Canvas->GetComponent()->enabled = true; } /// @brief Set current progress and displayed message /// @param p_Message Message to display @@ -136,7 +148,8 @@ namespace CP_SDK::UI { if (m_HeaderText) m_HeaderText->SetText(p_Message); - m_LoadingBar->set_fillAmount(p_Progress); + m_LoadingBar->ImageC()->fillAmount = p_Progress; + m_Canvas->GetComponent()->enabled = true; } /// @brief Set hide timer /// @param p_Time Time in seconds @@ -156,7 +169,7 @@ namespace CP_SDK::UI { if (p_NewScene != EGenericScene::Menu) { StopAllCoroutines(); - m_Canvas->set_enabled(false); + m_Canvas->GetComponent()->enabled = false; } } /// @brief Timed canvas disabler @@ -164,7 +177,7 @@ namespace CP_SDK::UI { custom_types::Helpers::Coroutine LoadingProgressBar::Coroutine_DisableCanvas(LoadingProgressBar* p_Self, float p_Time) { co_yield WaitForSecondsRealtime::New_ctor(p_Time)->i___System__Collections__IEnumerator(); - p_Self->m_Canvas->set_enabled(false); + p_Self->m_Canvas->GetComponent()->enabled = false; } } ///< namespace CP_SDK::UI \ No newline at end of file From 4eb759f0313f6de04a4feb622af4ee7f2cd6ab57 Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:21:48 +0200 Subject: [PATCH 09/15] Allow toggle of event specials --- shared/CP_SDK/UI/Views/SettingsRightView.hpp | 13 ++++++ src/CP_SDK/UI/Views/SettingsRightView.cpp | 49 +++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/shared/CP_SDK/UI/Views/SettingsRightView.hpp b/shared/CP_SDK/UI/Views/SettingsRightView.hpp index fbd77c1..791a451 100644 --- a/shared/CP_SDK/UI/Views/SettingsRightView.hpp +++ b/shared/CP_SDK/UI/Views/SettingsRightView.hpp @@ -2,6 +2,7 @@ #include "../ViewController.hpp" #include "../../XUI/XUI.hpp" +#include "CP_SDK/XUI/XUIToggle.hpp" namespace CP_SDK::UI::Views { @@ -23,10 +24,22 @@ namespace CP_SDK::UI::Views { CP_SDK_IL2CPP_DECLARE_DTOR_MONOBEHAVIOUR_CHILD(SettingsRightView); CP_SDK_UI_VIEW_CONTROLLER_INSTANCE(); + private: + _v::XUITabControl::Ptr m_TabControl; + + _v::XUIToggle::Ptr m_MiscTab_EventSpecialsEnabled; + private: /// @brief On view creation void OnViewCreation_Impl(); + /// @brief Build misc tab + std::shared_ptr<_v::IXUIElement> BuildMiscTab(); + + private: + /// @brief On setting changed + void OnValueChanged(); + }; } ///< namespace CP_SDK::UI::Views diff --git a/src/CP_SDK/UI/Views/SettingsRightView.cpp b/src/CP_SDK/UI/Views/SettingsRightView.cpp index 9c43950..3cfc519 100644 --- a/src/CP_SDK/UI/Views/SettingsRightView.cpp +++ b/src/CP_SDK/UI/Views/SettingsRightView.cpp @@ -1,6 +1,8 @@ #include "CP_SDK/UI/Views/SettingsRightView.hpp" -#include "CP_SDK/Unity/SpriteU.hpp" +#include "CP_SDK/CPConfig.hpp" +#include "CP_SDK/UI/Components/CVLayout.hpp" +using namespace CP_SDK::UI::Components; using namespace CP_SDK::XUI; using namespace UnityEngine; @@ -31,10 +33,53 @@ namespace CP_SDK::UI::Views { Templates::FullRectLayout({ Templates::TitleBar(u"Other settings"), - XUIText::Make(u"No available other settings at the moment!")->AsShared() + XUITabControl::Make({ + {u"Misc", BuildMiscTab()} + }) + ->Bind(&m_TabControl) + ->AsShared() }) ->SetBackground(true, std::nullopt, true) ->BuildUI(get_transform()); } + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Build misc tab + std::shared_ptr SettingsRightView::BuildMiscTab() + { + auto l_CPConfig = CPConfig::Instance(); + + return XUIVLayout::Make({ + XUIHLayout::Make({ + XUIVLayout::Make({ + XUIText::Make(u"Event specials (Require game restart)")->AsShared() + }) + ->OnReady([](CVLayout* x) { x->HOrVLayoutGroup()->childForceExpandWidth = true; }) + ->ForEachDirect([](XUIText* x) { x->SetAlign(TMPro::TextAlignmentOptions::CaplineLeft); }) + ->AsShared(), + + XUIVLayout::Make({ + XUIToggle::Make() + ->SetValue(l_CPConfig->EventSpecials) + ->Bind(&m_MiscTab_EventSpecialsEnabled) + ->AsShared() + }) + ->ForEachDirect([this](XUIToggle* x) { x->OnValueChanged([this](bool) { OnValueChanged(); }); }) + ->AsShared() + }) + })->AsShared(); + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief On setting changed + void SettingsRightView::OnValueChanged() + { + auto l_CPConfig = CPConfig::Instance(); + l_CPConfig->EventSpecials = m_MiscTab_EventSpecialsEnabled->Element()->GetValue(); + } + } ///< namespace CP_SDK::UI::Views \ No newline at end of file From 93a676b2ae4be8c3c6f1977689165393ff270ce7 Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:22:12 +0200 Subject: [PATCH 10/15] Implement IsMainThread and fix a race condition in UserPlatform --- shared/CP_SDK/Unity/MTMainThreadInvoker.hpp | 7 ++++ src/CP_SDK_BS/Game/UserPlatform.cpp | 39 +++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/shared/CP_SDK/Unity/MTMainThreadInvoker.hpp b/shared/CP_SDK/Unity/MTMainThreadInvoker.hpp index 054fa24..4f5d8cb 100644 --- a/shared/CP_SDK/Unity/MTMainThreadInvoker.hpp +++ b/shared/CP_SDK/Unity/MTMainThreadInvoker.hpp @@ -34,6 +34,7 @@ namespace CP_SDK::Unity { static std::deque<_v::Action<>> m_Queue; ///< Pending actions static bool m_Accepting; ///< Accept new actions static std::mutex m_Mutex; ///< Lock mutex + static std::thread::id m_MainThread; ///< Main thread public: /// @brief Unity GameObject initialize @@ -41,12 +42,18 @@ namespace CP_SDK::Unity { /// @brief Stop static void Destroy(); + public: + /// @brief Are we currently on the main thread? + static bool IsMainThread(); + public: /// @brief Enqueue a new action /// @param p_Delegate Action to enqueue static void Enqueue(_v::CActionRef<> p_Delegate); private: + /// @brief Unity GameObject Awake + DECLARE_INSTANCE_METHOD(void, Awake); /// @brief Unity GameObject update DECLARE_INSTANCE_METHOD(void, Update); diff --git a/src/CP_SDK_BS/Game/UserPlatform.cpp b/src/CP_SDK_BS/Game/UserPlatform.cpp index f11bee3..b705634 100644 --- a/src/CP_SDK_BS/Game/UserPlatform.cpp +++ b/src/CP_SDK_BS/Game/UserPlatform.cpp @@ -1,4 +1,5 @@ #include "CP_SDK_BS/Game/UserPlatform.hpp" +#include "CP_SDK/Unity/MTMainThreadInvoker.hpp" #include "CP_SDK/ChatPlexSDK.hpp" #include @@ -8,9 +9,11 @@ #include #include #include +#include using namespace GlobalNamespace; using namespace UnityEngine; +using namespace CP_SDK::Unity; namespace CP_SDK_BS::Game { @@ -26,7 +29,19 @@ namespace CP_SDK_BS::Game { if (!m_UserID.empty()) return m_UserID; - FetchPlatformInfos(); + if (MTMainThreadInvoker::IsMainThread()) + FetchPlatformInfos(); + else + { + auto isReady = false; + MTMainThreadInvoker::Enqueue([&]() -> void { + FetchPlatformInfos(); + isReady = true; + }); + + while (!isReady) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } return m_UserID; } @@ -36,7 +51,19 @@ namespace CP_SDK_BS::Game { if (!m_UserName.empty()) return m_UserName; - FetchPlatformInfos(); + if (MTMainThreadInvoker::IsMainThread()) + FetchPlatformInfos(); + else + { + auto isReady = false; + MTMainThreadInvoker::Enqueue([&]() -> void { + FetchPlatformInfos(); + isReady = true; + }); + + while (!isReady) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } return m_UserName; } @@ -53,17 +80,17 @@ namespace CP_SDK_BS::Game { for (GlobalNamespace::PlatformLeaderboardsModel * l_Current : l_PlatformLeaderboardsModels) { - if (l_Current->____platformUserModel == nullptr) + if (l_Current->_platformUserModel == nullptr) continue; - auto l_Task = l_Current->____platformUserModel->GetUserInfo(System::Threading::CancellationToken::get_None()); + auto l_Task = l_Current->_platformUserModel->GetUserInfo(System::Threading::CancellationToken::get_None()); l_Task->Wait(); - auto l_PlayerID = l_Task->get_Result()->___platformUserId; + auto l_PlayerID = l_Task->get_Result()->platformUserId; if (!System::String::IsNullOrEmpty(l_PlayerID)) { m_UserID = l_PlayerID; - m_UserName = l_Task->get_Result()->___userName; + m_UserName = l_Task->get_Result()->userName; return; } } From 9891795c80dc9535dc778903b05c8fb1f9d2ede6 Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:22:45 +0200 Subject: [PATCH 11/15] Levels: Silent fails + cache PlayerDataLevel --- shared/CP_SDK_BS/Game/Levels.hpp | 28 ++++--- src/CP_SDK/Unity/MTMainThreadInvoker.cpp | 15 ++++ src/CP_SDK_BS/Game/Levels.cpp | 101 +++++++++++++---------- 3 files changed, 90 insertions(+), 54 deletions(-) diff --git a/shared/CP_SDK_BS/Game/Levels.hpp b/shared/CP_SDK_BS/Game/Levels.hpp index d16dd35..dad6a3c 100644 --- a/shared/CP_SDK_BS/Game/Levels.hpp +++ b/shared/CP_SDK_BS/Game/Levels.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,7 @@ namespace CP_SDK_BS::Game { static _v::MonoPtr<_u::CancellationTokenSource> m_GetLevelEntitlementStatusTokenSource; static _v::MonoPtr<_u::MenuTransitionsHelper> m_MenuTransitionsHelper; static _v::MonoPtr<_u::SimpleLevelStarter> m_SimpleLevelStarter; + static _v::MonoPtr<_u::PlayerDataModel> m_PlayerDataModel; static bool m_ReloadSongsInitialized; static std::vector<_v::Action<>> m_ReloadSongsCallbacks; @@ -135,13 +137,15 @@ namespace CP_SDK_BS::Game { /// @brief Try to get BeatmapLevel by level ID /// @param p_LevelID ID of the level /// @param p_BeatmapLevel OUT Found BeatmapLevel or nullptr + /// @param p_SilentFail Should not log map not found error? /// @return true or false - static bool TryGetBeatmapLevelForLevelID(std::u16string_view p_LevelID, _u::BeatmapLevel** p_BeatmapLevel); + static bool TryGetBeatmapLevelForLevelID(std::u16string_view p_LevelID, _u::BeatmapLevel** p_BeatmapLevel, bool silentFail = false); /// @brief Try to get BeatmapLevel by hash /// @param p_Hash Hash of the level /// @param p_BeatmapLevel OUT Found BeatmapLevel or nullptr + /// @param p_SilentFail Should not log map not found error? /// @return true or false - static bool TryGetBeatmapLevelForHash(std::u16string_view p_Hash, _u::BeatmapLevel** p_BeatmapLevel); + static bool TryGetBeatmapLevelForHash(std::u16string_view p_Hash, _u::BeatmapLevel** p_BeatmapLevel, bool silentFail = false); /// @brief For each of BeatmapKey for a BeatmapLevel /// @param p_BeatmapLevel Input beatmap level /// @param p_Functor Functor for each element, return true mean we continue iterating @@ -196,20 +200,22 @@ namespace CP_SDK_BS::Game { /// @param p_BeatmapLevelData Beatmap level data /// @param p_OverrideEnvironmentSettings Environment settings /// @param p_ColorScheme Color scheme + /// @param p_ColorOverrideType Color override type /// @param p_GameplayModifiers Modifiers /// @param p_PlayerSettings Player settings /// @param p_SongFinishedCallback Callback when the song is finished /// @param p_MenuButtonText Menu button text - static void StartBeatmapLevel( _u::BeatmapLevel* p_Level, - _u::BeatmapCharacteristicSO* p_Characteristic, - _u::BeatmapDifficulty p_Difficulty, - _u::IBeatmapLevelData* p_BeatmapLevelData, - _u::OverrideEnvironmentSettings* p_OverrideEnvironmentSettings = nullptr, - _u::ColorScheme* p_ColorScheme = nullptr, - _u::GameplayModifiers* p_GameplayModifiers = nullptr, - _u::PlayerSpecificSettings* p_PlayerSettings = nullptr, + static void StartBeatmapLevel( _u::BeatmapLevel* p_Level, + _u::BeatmapCharacteristicSO* p_Characteristic, + _u::BeatmapDifficulty p_Difficulty, + _u::IBeatmapLevelData* p_BeatmapLevelData, + _u::OverrideEnvironmentSettings* p_OverrideEnvironmentSettings = nullptr, + _u::ColorScheme* p_ColorScheme = nullptr, + std::optional<_u::ColorSchemesSettings::ColorOverrideType> p_ColorOverrideType = std::nullopt, + _u::GameplayModifiers* p_GameplayModifiers = nullptr, + _u::PlayerSpecificSettings* p_PlayerSettings = nullptr, _v::Action<_u::StandardLevelScenesTransitionSetupDataSO*, _u::LevelCompletionResults*> p_SongFinishedCallback = nullptr, - std::u16string_view p_MenuButtonText = u"Menu"); + std::u16string_view p_MenuButtonText = u"Menu"); private: /// @brief Load IBeatmapLevelData from a level ID diff --git a/src/CP_SDK/Unity/MTMainThreadInvoker.cpp b/src/CP_SDK/Unity/MTMainThreadInvoker.cpp index 6266e50..a3be901 100644 --- a/src/CP_SDK/Unity/MTMainThreadInvoker.cpp +++ b/src/CP_SDK/Unity/MTMainThreadInvoker.cpp @@ -22,6 +22,7 @@ namespace CP_SDK::Unity { std::deque> MTMainThreadInvoker::m_Queue; bool MTMainThreadInvoker::m_Accepting = false; std::mutex MTMainThreadInvoker::m_Mutex; + std::thread::id MTMainThreadInvoker::m_MainThread; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -75,6 +76,15 @@ namespace CP_SDK::Unity { //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// + /// @brief Are we currently on the main thread? + bool MTMainThreadInvoker::IsMainThread() + { + return std::this_thread::get_id() == MTMainThreadInvoker::m_MainThread; + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + /// @brief Enqueue a new action /// @param p_Delegate Action to enqueue void MTMainThreadInvoker::Enqueue(_v::CActionRef<> p_Delegate) @@ -99,6 +109,11 @@ namespace CP_SDK::Unity { //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// + /// @brief Unity GameObject Awake + void MTMainThreadInvoker::Awake() + { + m_MainThread = std::this_thread::get_id(); + } /// @brief Unity GameObject update void MTMainThreadInvoker::Update() { diff --git a/src/CP_SDK_BS/Game/Levels.cpp b/src/CP_SDK_BS/Game/Levels.cpp index 40b5d6c..dfed1fe 100644 --- a/src/CP_SDK_BS/Game/Levels.cpp +++ b/src/CP_SDK_BS/Game/Levels.cpp @@ -56,6 +56,7 @@ namespace CP_SDK_BS::Game { _v::MonoPtr Levels::m_GetLevelEntitlementStatusTokenSource; _v::MonoPtr Levels::m_MenuTransitionsHelper; _v::MonoPtr Levels::m_SimpleLevelStarter; + _v::MonoPtr Levels::m_PlayerDataModel; bool Levels::m_ReloadSongsInitialized = false; std::vector<_v::Action<>> Levels::m_ReloadSongsCallbacks; @@ -242,7 +243,7 @@ namespace CP_SDK_BS::Game { { auto l_PlayerDataModel = Resources::FindObjectsOfTypeAll()->FirstOrDefault(); if (l_PlayerDataModel) - m_BeatmapCharacteristicCollection = l_PlayerDataModel->get_playerDataFileModel()->____beatmapCharacteristicCollection; + m_BeatmapCharacteristicCollection = l_PlayerDataModel->get_playerDataFileModel()->_beatmapCharacteristicCollection; } if (!m_BeatmapCharacteristicCollection) @@ -255,7 +256,7 @@ namespace CP_SDK_BS::Game { UnityW l_BeatmapCharacteristicSO; bool l_Result = false; - if (m_BeatmapCharacteristicCollection->____beatmapCharacteristicsBySerializedName->TryGetValue(l_SerializedName, byref(l_BeatmapCharacteristicSO))) + if (m_BeatmapCharacteristicCollection->_beatmapCharacteristicsBySerializedName->TryGetValue(l_SerializedName, byref(l_BeatmapCharacteristicSO))) { l_Result = true; @@ -305,7 +306,7 @@ namespace CP_SDK_BS::Game { auto l_CharacteristicSO = (BeatmapCharacteristicSO*)nullptr; if (TryGetBeatmapCharacteristicSOBySerializedName(p_SerializedName, &l_CharacteristicSO)) - return l_CharacteristicSO->____sortingOrder; + return l_CharacteristicSO->_sortingOrder; return 1000; } @@ -411,13 +412,13 @@ namespace CP_SDK_BS::Game { if (!m_BeatmapLevelsModel) { auto l_MainFlowCoordinator = Resources::FindObjectsOfTypeAll<_u::MainFlowCoordinator*>()->FirstOrDefault([](_u::MainFlowCoordinator* x) -> bool { - return x->____beatmapLevelsModel; + return x->_beatmapLevelsModel; }); if (l_MainFlowCoordinator) - m_BeatmapLevelsModel = l_MainFlowCoordinator->____beatmapLevelsModel; + m_BeatmapLevelsModel = l_MainFlowCoordinator->_beatmapLevelsModel; } - if (m_BeatmapLevelsModel && m_BeatmapLevelsModel->____entitlements) + if (m_BeatmapLevelsModel && m_BeatmapLevelsModel->_entitlements) { if (m_GetLevelEntitlementStatusTokenSource) m_GetLevelEntitlementStatusTokenSource->Cancel(); @@ -426,7 +427,7 @@ namespace CP_SDK_BS::Game { try { - auto l_Task = m_BeatmapLevelsModel->____entitlements->GetLevelEntitlementStatusAsync(p_LevelID, m_GetLevelEntitlementStatusTokenSource->get_Token()); + auto l_Task = m_BeatmapLevelsModel->_entitlements->GetLevelEntitlementStatusAsync(p_LevelID, m_GetLevelEntitlementStatusTokenSource->get_Token()); l_Task->ConfigureAwait(false); _v::AwaitTaskAsync( @@ -456,8 +457,9 @@ namespace CP_SDK_BS::Game { /// @brief Try to get BeatmapLevel by level ID /// @param p_LevelID ID of the level /// @param p_BeatmapLevel OUT Found BeatmapLevel or nullptr + /// @param p_SilentFail Should not log map not found error? /// @return true or false - bool Levels::TryGetBeatmapLevelForLevelID(std::u16string_view p_LevelID, BeatmapLevel** p_BeatmapLevel) + bool Levels::TryGetBeatmapLevelForLevelID(std::u16string_view p_LevelID, BeatmapLevel** p_BeatmapLevel, bool silentFail) { if (p_BeatmapLevel) *p_BeatmapLevel = nullptr; @@ -475,10 +477,10 @@ namespace CP_SDK_BS::Game { if (!m_BeatmapLevelsModel) { auto l_MainFlowCoordinator = Resources::FindObjectsOfTypeAll<_u::MainFlowCoordinator*>()->FirstOrDefault([](_u::MainFlowCoordinator* x) -> bool { - return x->____beatmapLevelsModel; + return x->_beatmapLevelsModel; }); if (l_MainFlowCoordinator) - m_BeatmapLevelsModel = l_MainFlowCoordinator->____beatmapLevelsModel; + m_BeatmapLevelsModel = l_MainFlowCoordinator->_beatmapLevelsModel; } if (m_BeatmapLevelsModel) @@ -490,7 +492,8 @@ namespace CP_SDK_BS::Game { return true; } - CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetBeatmapLevelForLevelID] GetBeatmapLevel failed"); + if (!silentFail) + CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetBeatmapLevelForLevelID] GetBeatmapLevel failed"); } else CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetBeatmapLevelForLevelID] Invalid BeatmapLevelsModel"); @@ -500,14 +503,15 @@ namespace CP_SDK_BS::Game { /// @brief Try to get BeatmapLevel by hash /// @param p_Hash Hash of the level /// @param p_BeatmapLevel OUT Found BeatmapLevel or nullptr + /// @param p_SilentFail Should not log map not found error? /// @return true or false - bool Levels::TryGetBeatmapLevelForHash(std::u16string_view p_Hash, _u::BeatmapLevel** p_BeatmapLevel) + bool Levels::TryGetBeatmapLevelForHash(std::u16string_view p_Hash, _u::BeatmapLevel** p_BeatmapLevel, bool silentFail) { std::u16string l_LevelID; if (!TryGetLevelIDFromHash(p_Hash, &l_LevelID)) return false; - return TryGetBeatmapLevelForLevelID(l_LevelID, p_BeatmapLevel); + return TryGetBeatmapLevelForLevelID(l_LevelID, p_BeatmapLevel, silentFail); } /// @brief For each of BeatmapKey for a BeatmapLevel /// @param p_BeatmapLevel Input beatmap level @@ -522,7 +526,7 @@ namespace CP_SDK_BS::Game { /// Force cache generation p_BeatmapLevel->GetBeatmapKeys(); - for (const auto& l_Current : p_BeatmapLevel->____beatmapKeysCache) + for (const auto& l_Current : p_BeatmapLevel->_beatmapKeysCache) { if (!p_Functor(l_Current)) break; @@ -572,7 +576,7 @@ namespace CP_SDK_BS::Game { BeatmapLevel_ForEachBeatmapKey(p_BeatmapLevel, [&](const BeatmapKey& l_Current) -> bool { auto l_BeatmapCharacteristicSO = l_Current.beatmapCharacteristic.unsafePtr(); - if (l_BeatmapCharacteristicSO->____serializedName != p_BeatmapCharacteristicSO->____serializedName) + if (l_BeatmapCharacteristicSO->_serializedName != p_BeatmapCharacteristicSO->_serializedName) return true; ///< Continue if (l_Current.difficulty != p_BeatmapDifficulty) @@ -604,7 +608,7 @@ namespace CP_SDK_BS::Game { return false; } - if (!LevelID_IsCustom(p_BeatmapLevel->___levelID)) + if (!LevelID_IsCustom(p_BeatmapLevel->levelID)) return false; auto l_CustomLevel = (SongCore::SongLoader::CustomBeatmapLevel*)nullptr; @@ -613,23 +617,23 @@ namespace CP_SDK_BS::Game { if (!l_CustomLevel) { - CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetCustomRequirementsFor] Failed to convert to custom level for id: " + p_BeatmapLevel->___levelID); + CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetCustomRequirementsFor] Failed to convert to custom level for id: " + p_BeatmapLevel->levelID); return false; } auto l_CustomSaveDataInfoWrapper = l_CustomLevel->get_CustomSaveDataInfo(); if (!l_CustomSaveDataInfoWrapper) { - CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetCustomRequirementsFor] Failed to retrieve custom data level for id: " + p_BeatmapLevel->___levelID); + CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetCustomRequirementsFor] Failed to retrieve custom data level for id: " + p_BeatmapLevel->levelID); return false; } auto& l_CustomSaveDataInfo = l_CustomSaveDataInfoWrapper->get(); - auto l_CharacteristicAndDifficultyWrapper = l_CustomSaveDataInfo.TryGetCharacteristicAndDifficulty(p_BeatmapCharacteristicSO->____serializedName, p_BeatmapDifficulty); + auto l_CharacteristicAndDifficultyWrapper = l_CustomSaveDataInfo.TryGetCharacteristicAndDifficulty(p_BeatmapCharacteristicSO->_serializedName, p_BeatmapDifficulty); if (!l_CharacteristicAndDifficultyWrapper) { - CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetCustomRequirementsFor] Failed to retrieve custom data level for id: " + p_BeatmapLevel->___levelID); + CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.Game][Levels.TryGetCustomRequirementsFor] Failed to retrieve custom data level for id: " + p_BeatmapLevel->levelID); return false; } @@ -658,14 +662,14 @@ namespace CP_SDK_BS::Game { /// @param p_Callback Callback void Levels::TryLoadBeatmapLevelCoverAsync(_u::BeatmapLevel* p_BeatmapLevel, _v::Action p_Callback) { - if (!p_BeatmapLevel || !p_BeatmapLevel->___previewMediaData) + if (!p_BeatmapLevel || !p_BeatmapLevel->previewMediaData) { CP_SDK::Unity::MTMainThreadInvoker::Enqueue([=]() { p_Callback(false, Levels::GetDefaultPackCover()); }); return; } _v::MonoPtr l_PreviewBeatmapLevel(p_BeatmapLevel); - auto l_Task = p_BeatmapLevel->___previewMediaData->GetCoverSpriteAsync(); + auto l_Task = p_BeatmapLevel->previewMediaData->GetCoverSpriteAsync(); _v::AwaitTaskAsync>( l_Task, @@ -740,20 +744,22 @@ namespace CP_SDK_BS::Game { /// @param p_BeatmapLevelData Beatmap level data /// @param p_OverrideEnvironmentSettings Environment settings /// @param p_ColorScheme Color scheme + /// @param p_ColorOverrideType Color override type /// @param p_GameplayModifiers Modifiers /// @param p_PlayerSettings Player settings /// @param p_SongFinishedCallback Callback when the song is finished /// @param p_MenuButtonText Menu button text - void Levels::StartBeatmapLevel( _u::BeatmapLevel* p_Level, - _u::BeatmapCharacteristicSO* p_Characteristic, - _u::BeatmapDifficulty p_Difficulty, - _u::IBeatmapLevelData* p_BeatmapLevelData, - _u::OverrideEnvironmentSettings* p_OverrideEnvironmentSettings, - _u::ColorScheme* p_ColorScheme, - _u::GameplayModifiers* p_GameplayModifiers, - _u::PlayerSpecificSettings* p_PlayerSettings, + void Levels::StartBeatmapLevel( _u::BeatmapLevel* p_Level, + _u::BeatmapCharacteristicSO* p_Characteristic, + _u::BeatmapDifficulty p_Difficulty, + _u::IBeatmapLevelData* p_BeatmapLevelData, + _u::OverrideEnvironmentSettings* p_OverrideEnvironmentSettings, + _u::ColorScheme* p_ColorScheme, + std::optional<_u::ColorSchemesSettings::ColorOverrideType> p_ColorOverrideType, + _u::GameplayModifiers* p_GameplayModifiers, + _u::PlayerSpecificSettings* p_PlayerSettings, _v::Action<_u::StandardLevelScenesTransitionSetupDataSO*, _u::LevelCompletionResults*> p_SongFinishedCallback, - std::u16string_view p_MenuButtonText) + std::u16string_view p_MenuButtonText) { if (p_Level == nullptr) return; @@ -772,7 +778,7 @@ namespace CP_SDK_BS::Game { using t_Delegate = System::Action_2, LevelCompletionResults*>*; - auto l_BeatmapKey = BeatmapKey(p_Characteristic, p_Difficulty, p_Level->___levelID); + auto l_BeatmapKey = BeatmapKey(p_Characteristic, p_Difficulty, p_Level->levelID); auto l_Delegate = custom_types::MakeDelegate(std::function([=](UnityW __a, LevelCompletionResults* __b) { p_SongFinishedCallback(__a, __b); })); @@ -784,12 +790,12 @@ namespace CP_SDK_BS::Game { /* IBeatmapLevelData beatmapLevelData: */ p_BeatmapLevelData, /* OverrideEnvironmentSettings overrideEnvironmentSettings: */ p_OverrideEnvironmentSettings, /* ColorScheme playerOverrideColorScheme: */ p_ColorScheme, - /* bool playerOverrideLightshowColors*/ false, + /* bool playerOverrideLightshowColors*/ p_ColorOverrideType.has_value() ? (p_ColorOverrideType.value() == _u::ColorSchemesSettings::ColorOverrideType::All) : false, /* ColorScheme beatmapOverrideColorScheme: */ nullptr, /* GameplayModifiers gameplayModifiers: */ p_GameplayModifiers ? p_GameplayModifiers : GameplayModifiers::New_ctor(), /* PlayerSpecificSettings playerSpecificSettings: */ p_PlayerSettings ? p_PlayerSettings : PlayerSpecificSettings::New_ctor(), /* PracticeSettings practiceSettings: */ nullptr, - /* EnvironmentsListModel environmentsListModel: */ m_SimpleLevelStarter->____environmentsListModel, + /* EnvironmentsListModel environmentsListModel: */ m_SimpleLevelStarter->_environmentsListModel, /* string backButtonText: */ p_MenuButtonText, /* bool useTestNoteCutSoundEffects: */ false, /* bool startPaused: */ false, @@ -822,10 +828,10 @@ namespace CP_SDK_BS::Game { if (!m_BeatmapLevelsModel) { auto l_MainFlowCoordinator = Resources::FindObjectsOfTypeAll<_u::MainFlowCoordinator*>()->FirstOrDefault([](_u::MainFlowCoordinator* x) -> bool { - return x->____beatmapLevelsModel; + return x->_beatmapLevelsModel; }); if (l_MainFlowCoordinator) - m_BeatmapLevelsModel = l_MainFlowCoordinator->____beatmapLevelsModel; + m_BeatmapLevelsModel = l_MainFlowCoordinator->_beatmapLevelsModel; } if (!m_MenuTransitionsHelper) @@ -840,7 +846,7 @@ namespace CP_SDK_BS::Game { try { - auto l_VersionTask = m_MenuTransitionsHelper->____beatmapLevelsEntitlementModel->GetLevelDataVersionAsync(p_LevelID, m_GetLevelCancellationTokenSource->get_Token()); + auto l_VersionTask = m_MenuTransitionsHelper->_beatmapLevelsEntitlementModel->GetLevelDataVersionAsync(p_LevelID, m_GetLevelCancellationTokenSource->get_Token()); _v::AwaitTaskAsync( l_VersionTask, [=](_v::MonoPtrRef> p_VersionTask, bool p_VersionSuccess) { @@ -919,11 +925,21 @@ namespace CP_SDK_BS::Game { /// @return Scores Levels::t_Scores Levels::GetScoresByLevelID(std::u16string_view p_LevelID, bool* p_HaveAnyScore, bool* p_HaveAllScores) { - Levels::t_Scores l_Results; - if (p_HaveAnyScore) *p_HaveAnyScore = false; if (p_HaveAllScores) *p_HaveAllScores = true; + Levels::t_Scores l_Results; + if (!m_PlayerDataModel) + { + m_PlayerDataModel = Resources::FindObjectsOfTypeAll()->FirstOrDefault(); + + if (!m_PlayerDataModel) + { + *p_HaveAllScores = false; + return l_Results; + } + } + auto l_LevelID = SanitizeLevelID(p_LevelID); auto l_BeatmapLevel = (BeatmapLevel*)nullptr; if (!TryGetBeatmapLevelForLevelID(l_LevelID, &l_BeatmapLevel)) @@ -932,8 +948,7 @@ namespace CP_SDK_BS::Game { return l_Results; } - auto l_PlayerDataModel = Resources::FindObjectsOfTypeAll()->FirstOrDefault(); - auto l_PlayerData = l_PlayerDataModel ? l_PlayerDataModel->____playerData : nullptr; + auto l_PlayerData = m_PlayerDataModel ? m_PlayerDataModel->_playerData : nullptr; auto l_LevelStatsData = l_PlayerData ? l_PlayerData->get_levelsStatsData() : nullptr; BeatmapLevel_ForEachBeatmapKey(l_BeatmapLevel, [&](const BeatmapKey& l_Current) -> bool @@ -943,10 +958,10 @@ namespace CP_SDK_BS::Game { l_Results[l_BeatmapCharacteristicSO] = t_CharacteristicScores(); PlayerLevelStatsData* l_PlayerLevelStatsData = nullptr; - if (l_LevelStatsData->TryGetValue(l_Current, byref(l_PlayerLevelStatsData)) && l_PlayerLevelStatsData && l_PlayerLevelStatsData->____playCount) + if (l_LevelStatsData->TryGetValue(l_Current, byref(l_PlayerLevelStatsData)) && l_PlayerLevelStatsData && l_PlayerLevelStatsData->_playCount) { if (p_HaveAnyScore) *p_HaveAnyScore = true; - l_Results[l_BeatmapCharacteristicSO].push_back(std::make_tuple(l_Current.difficulty, l_PlayerLevelStatsData->____highScore)); + l_Results[l_BeatmapCharacteristicSO].push_back(std::make_tuple(l_Current.difficulty, l_PlayerLevelStatsData->_highScore)); } else { From b81df4167aab9781a9a91bf784efdfe6b3cf5b82 Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:23:06 +0200 Subject: [PATCH 12/15] HMUIUIUtils: Implement GameHMUIScreenSystem to prevent conflict with other mods --- shared/CP_SDK_BS/UI/HMUIUIUtils.hpp | 4 ++++ src/main.cpp | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/shared/CP_SDK_BS/UI/HMUIUIUtils.hpp b/shared/CP_SDK_BS/UI/HMUIUIUtils.hpp index 07ded64..3942e8a 100644 --- a/shared/CP_SDK_BS/UI/HMUIUIUtils.hpp +++ b/shared/CP_SDK_BS/UI/HMUIUIUtils.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,7 @@ namespace CP_SDK_BS::UI { using namespace GlobalNamespace; using namespace UnityEngine; using namespace VRUIControls; + using namespace HMUI; } /// @brief View controller utils @@ -25,11 +27,13 @@ namespace CP_SDK_BS::UI { CP_SDK_NO_DEF_CTORS(HMUIUIUtils); private: + static CP_SDK::Utils::MonoPtr<_u::ScreenSystem> m_GameHMUIScreenSystem; static CP_SDK::Utils::MonoPtr<_u::MainFlowCoordinator> m_MainFlowCoordinator; static CP_SDK::Utils::MonoPtr<_u::Canvas> m_CanvasTemplate; static CP_SDK::Utils::MonoPtr<_u::PhysicsRaycasterWithCache> m_PhysicsRaycaster; public: + static _u::ScreenSystem* GameHMUIScreenSystem(); static _u::MainFlowCoordinator* MainFlowCoordinator(); public: diff --git a/src/main.cpp b/src/main.cpp index a68e3c8..551050e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include "CP_SDK_BS/Game/BeatMapsClient.hpp" #include "CP_SDK_BS/Game/Logic.hpp" #include "CP_SDK_BS/UI/DefaultFactoriesOverrides/BS_FloatingPanelFactory.hpp" +#include "CP_SDK_BS/UI/HMUIUIUtils.hpp" #include @@ -57,18 +58,18 @@ void ScreenSystem_OnPresent() if (!m_HMUIScreenSystem) { m_HMUIDeactivatedScreens.clear(); - m_HMUIScreenSystem = UnityEngine::Resources::FindObjectsOfTypeAll()->FirstOrDefault(); + m_HMUIScreenSystem = CP_SDK_BS::UI::HMUIUIUtils::GameHMUIScreenSystem(); } 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); + 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()->get_transform()->set_localScale(m_HMUIScreenSystem->get_transform()->get_localScale()); } @@ -114,7 +115,7 @@ void PatchUI() m_VRGraphicRaycasterCache = UnityEngine::Resources::FindObjectsOfTypeAll()->FirstOrDefault([](auto y) { return y->_physicsRaycaster != nullptr; }); if (m_VRGraphicRaycasterCache) - x->get_gameObject()->AddComponent()->____physicsRaycaster = m_VRGraphicRaycasterCache->____physicsRaycaster; + x->get_gameObject()->AddComponent()->_physicsRaycaster = m_VRGraphicRaycasterCache->_physicsRaycaster; }; //////////////////////////////////////////////////////////////////////////// @@ -200,10 +201,10 @@ extern "C" __attribute__((visibility("default"))) void setup(CModInfo* p_ModInfo auto l_MainFont = UnityEngine::Resources::FindObjectsOfTypeAll()->FirstOrDefault([](auto x) { return x->get_name() == u"Teko-Medium SDF"; }); if (l_MainFont && p_Input) { - p_Input->___material->set_shader(l_MainFont->___material->get_shader()); - p_Input->___material->SetColor("_FaceColor", p_Input->___material->GetColor(u"_FaceColor")); - p_Input->___material->EnableKeyword("CURVED"); - p_Input->___material->EnableKeyword("UNITY_UI_CLIP_RECT"); + p_Input->material->set_shader(l_MainFont->material->get_shader()); + p_Input->material->SetColor("_FaceColor", p_Input->material->GetColor(u"_FaceColor")); + p_Input->material->EnableKeyword("CURVED"); + p_Input->material->EnableKeyword("UNITY_UI_CLIP_RECT"); } return p_Input; From 38aae9b83cf5ecfde486a33c602ff526f3dcaf62 Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:23:10 +0200 Subject: [PATCH 13/15] Update HMUIUIUtils.cpp --- src/CP_SDK_BS/UI/HMUIUIUtils.cpp | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/CP_SDK_BS/UI/HMUIUIUtils.cpp b/src/CP_SDK_BS/UI/HMUIUIUtils.cpp index 73430b8..64a231f 100644 --- a/src/CP_SDK_BS/UI/HMUIUIUtils.cpp +++ b/src/CP_SDK_BS/UI/HMUIUIUtils.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace GlobalNamespace; using namespace UnityEngine; @@ -14,6 +15,7 @@ using namespace VRUIControls; namespace CP_SDK_BS::UI { + CP_SDK::Utils::MonoPtr HMUIUIUtils::m_GameHMUIScreenSystem; CP_SDK::Utils::MonoPtr HMUIUIUtils::m_MainFlowCoordinator; CP_SDK::Utils::MonoPtr HMUIUIUtils::m_CanvasTemplate; CP_SDK::Utils::MonoPtr HMUIUIUtils::m_PhysicsRaycaster; @@ -21,6 +23,23 @@ namespace CP_SDK_BS::UI { //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// + HMUI::ScreenSystem* HMUIUIUtils::GameHMUIScreenSystem() + { + if (m_GameHMUIScreenSystem) + return m_GameHMUIScreenSystem.Ptr(); + + m_GameHMUIScreenSystem = + Resources::FindObjectsOfTypeAll() + ->FirstOrDefault([](HMUI::ScreenSystem *x) -> bool { + return CP_SDK::Utils::IsUnityPtrValid(x) && + x->isActiveAndEnabled && + CP_SDK::Utils::IsUnityPtrValid(x->GetComponent()) && + x->transform->get_parent() && + x->transform->parent->name == u"UI"; + }); + + return m_GameHMUIScreenSystem.Ptr(); + } MainFlowCoordinator* HMUIUIUtils::MainFlowCoordinator() { if (m_MainFlowCoordinator) @@ -43,10 +62,10 @@ namespace CP_SDK_BS::UI { if (!m_MainFlowCoordinator) m_MainFlowCoordinator = Resources::FindObjectsOfTypeAll()->First(); - auto l_InputModule = m_MainFlowCoordinator->____baseInputModule; + auto l_InputModule = m_MainFlowCoordinator->_baseInputModule; auto l_Coordinator = GameObject::New_ctor(p_Type->get_Name())->AddComponent(p_Type).try_cast().value_or(nullptr); - l_Coordinator->____baseInputModule = l_InputModule; + l_Coordinator->_baseInputModule = l_InputModule; return l_Coordinator; } @@ -72,7 +91,7 @@ namespace CP_SDK_BS::UI { } if (!m_PhysicsRaycaster) - m_PhysicsRaycaster = Resources::FindObjectsOfTypeAll()->First()->GetComponent()->____physicsRaycaster; + m_PhysicsRaycaster = Resources::FindObjectsOfTypeAll()->First()->GetComponent()->_physicsRaycaster; auto l_GameObject = GameObject::New_ctor(p_Type->get_Name()); auto l_Canvas = l_GameObject->AddComponent(); @@ -91,7 +110,7 @@ namespace CP_SDK_BS::UI { l_Canvas->set_worldCamera (m_CanvasTemplate->get_worldCamera()); //l_Canvas->set_normalizedSortingGridSize(m_CanvasTemplate->get_normalizedSortingGridSize()); - l_GameObject->get_gameObject()->AddComponent()->____physicsRaycaster = m_PhysicsRaycaster.Ptr(); + l_GameObject->get_gameObject()->AddComponent()->_physicsRaycaster = m_PhysicsRaycaster.Ptr(); l_GameObject->get_gameObject()->AddComponent(); auto l_View = l_GameObject->AddComponent(p_Type).try_cast().value_or(nullptr); From 43489233ff374c36745532dd9f030286e1bf1667 Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:23:21 +0200 Subject: [PATCH 14/15] LevelDetails: Load level basic infos --- shared/CP_SDK_BS/UI/LevelDetail.hpp | 9 ++ src/CP_SDK_BS/UI/LevelDetail.cpp | 222 ++++++++++++++++++++-------- 2 files changed, 168 insertions(+), 63 deletions(-) diff --git a/shared/CP_SDK_BS/UI/LevelDetail.hpp b/shared/CP_SDK_BS/UI/LevelDetail.hpp index 6f02e77..10efb2c 100644 --- a/shared/CP_SDK_BS/UI/LevelDetail.hpp +++ b/shared/CP_SDK_BS/UI/LevelDetail.hpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace CP_SDK_BS::UI { @@ -71,6 +72,7 @@ namespace CP_SDK_BS::UI { _v::MonoPtr<_u::GameObject> m_FavoriteToggle; _v::MonoPtr<_u::BeatmapLevel> m_LocalBeatMap; Game::BeatMaps::MapDetail::Ptr m_BeatMap; + std::optional<_u::BeatmapDifficulty> m_LimitedBeatmapDifficulty; private: double m_Time = 0; @@ -134,7 +136,14 @@ namespace CP_SDK_BS::UI { /// @param p_Active New state void SetActive(bool p_Active); + /// @brief Reset widget state + void Reset(); + public: + /// @brief Set from game + /// @param p_BeatMap BeatMap + /// @param p_Cover Cover texture + bool FromGame(_u::BeatmapLevel* p_BeatMap, _u::Sprite* p_Cover); /// @brief Set from game /// @param p_BeatMap BeatMap /// @param p_Cover Cover texture diff --git a/src/CP_SDK_BS/UI/LevelDetail.cpp b/src/CP_SDK_BS/UI/LevelDetail.cpp index 850ebb9..03d3240 100644 --- a/src/CP_SDK_BS/UI/LevelDetail.cpp +++ b/src/CP_SDK_BS/UI/LevelDetail.cpp @@ -1,24 +1,28 @@ #include "CP_SDK_BS/UI/LevelDetail.hpp" +#include "CP_SDK/ChatPlexSDK.hpp" +#include "CP_SDK/Unity/MTMainThreadInvoker.hpp" +#include "CP_SDK/Utils/MonoPtr.hpp" #include "CP_SDK_BS/UI/HMUIIconSegmentedControl.hpp" #include "CP_SDK_BS/UI/HMUITextSegmentedControl.hpp" #include "CP_SDK_BS/Game/Levels.hpp" #include "CP_SDK/UI/UISystem.hpp" #include "CP_SDK/Unity/SpriteU.hpp" #include "CP_SDK/Unity/Operators.hpp" -#include "CP_SDK/Unity/MTThreadInvoker.hpp" #include "assets.hpp" +#include +#include #include +#include #include -#include #include +#include #include #include #include #include #include -#include #include #include #include @@ -27,18 +31,18 @@ #include #include #include -#include #include #include -#include -#include -#include #include +#include +#include #include +#include #include +#include +#include #include - -#include "songcore/shared/SongLoader/CustomBeatmapLevel.hpp" +#include using namespace GlobalNamespace; using namespace TMPro; @@ -88,7 +92,7 @@ namespace CP_SDK_BS::UI { { auto l_Loader = BeatmapLevelLoader::New_ctor(nullptr, MockBeatmapDataAssetFileModel::New_ctor()->i___GlobalNamespace__IBeatmapDataAssetFileModel(), nullptr, BeatmapLevelLoader::InitData::New_ctor(0)); auto l_Packs = System::Collections::Generic::List_1<::UnityW>::New_ctor(); - l_Component->____beatmapLevelsModel = BeatmapLevelsModel::New_ctor( + l_Component->_beatmapLevelsModel = BeatmapLevelsModel::New_ctor( nullptr, l_Loader->i___GlobalNamespace__IBeatmapLevelLoader(), nullptr, @@ -220,9 +224,6 @@ namespace CP_SDK_BS::UI { void LevelDetail::Characteristic(HMUI::IconSegmentedControl::DataItem* p_Value) { m_Characteristic = p_Value; - HMUIIconSegmentedControl::SetDataNoHoverHint(m_SongCharacteristicSegmentedControl.Ptr(), ::Array::New({ - p_Value - })); } std::u16string LevelDetail::Difficulty() { @@ -388,9 +389,13 @@ namespace CP_SDK_BS::UI { for (auto l_Image : m_GameObject->GetComponentsInChildren(true)) { - m_SongCoverImage->____skew = 0.0f; + m_SongCoverImage->_skew = 0.0f; m_SongCoverImage->SetAllDirty(); } + + auto beatmapLevelVersions = m_SongCoverImage->transform->Find(u"BeatmapLevelVersions"); + if (beatmapLevelVersions) + beatmapLevelVersions->gameObject->SetActive(false); } catch (const std::exception&) { @@ -414,6 +419,31 @@ namespace CP_SDK_BS::UI { { m_GameObject->SetActive(p_Active); } + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + /// @brief Reset widget state + void LevelDetail::Reset() + { + m_LocalBeatMap = nullptr; + m_BeatMap = nullptr; + m_LimitedBeatmapDifficulty = std::nullopt; + m_Characteristic = nullptr; + + SelectedBeatmapCharacteristicSO = nullptr; + SelectedBeatmapDifficulty = BeatmapDifficulty::Easy; + + Name(u"--"); + AuthorNameText(u"--"); + Cover(Game::Levels::GetDefaultPackCover()); + Time(-1.0f); + NPS(-1.0f); + NJS(-1) ; + Offset(std::numeric_limits::quiet_NaN()); + Notes(-1); + Obstacles(-1); + Bombs(-1); + } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -421,12 +451,9 @@ namespace CP_SDK_BS::UI { /// @brief Set from game /// @param p_BeatMap BeatMap /// @param p_Cover Cover texture - /// @param p_Characteristic Game mode - /// @param p_Difficulty Difficulty - bool LevelDetail::FromGame(BeatmapLevel* p_BeatMap, Sprite* p_Cover, BeatmapCharacteristicSO* p_Characteristic, BeatmapDifficulty p_Difficulty) + bool LevelDetail::FromGame(_u::BeatmapLevel *p_BeatMap, _u::Sprite *p_Cover) { - m_LocalBeatMap = nullptr; - m_BeatMap = nullptr; + Reset(); if (p_BeatMap == nullptr) { @@ -434,50 +461,73 @@ namespace CP_SDK_BS::UI { return false; } - /// Display mode - Characteristic(HMUI::IconSegmentedControl::DataItem::New_ctor(p_Characteristic->____icon, BGLib::Polyglot::Localization::Get(p_Characteristic->____descriptionLocalizationKey), true)); + /// Display modes + auto l_Characteristics = ListW(); + p_BeatMap->GetCharacteristics(); + for (auto& l_Current : p_BeatMap->____characteristicsCache) + l_Characteristics->Add(HMUI::IconSegmentedControl::DataItem::New_ctor(l_Current->_icon, BGLib::Polyglot::Localization::Get(l_Current->_descriptionLocalizationKey), true)); + + if (l_Characteristics->get_Count() == 0) + { + CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.UI][LevelDetail.FromBeatSaver2] No valid characteristics found for map \"{p_BeatMap.id}\"!"); + return false; + } - /// Display difficulty - Difficulty(Game::Levels::BeatmapDifficultySerializedNameToDifficultyName(BeatmapDifficultySerializedMethods::SerializedName(p_Difficulty))); + /// Store beatmap + m_LocalBeatMap = p_BeatMap; - auto l_DifficultyBeatmap = p_BeatMap->GetDifficultyBeatmapData(p_Characteristic, p_Difficulty); + HMUIIconSegmentedControl::SetDataNoHoverHint(m_SongCharacteristicSegmentedControl.Ptr(), ::Array::New({m_Characteristic.Ptr()})); + m_SongCharacteristicSegmentedControl->SelectCellWithNumber(0); + OnCharacteristicChanged(nullptr, 0); - Name (p_BeatMap->___songName); - AuthorNameText(u"Mapped by " + p_BeatMap->___allMappers->FirstOrDefault() + u""); + Name (p_BeatMap->songName); + AuthorNameText(u"Mapped by " + p_BeatMap->allMappers->FirstOrDefault() + u""); Cover (p_Cover ? p_Cover : Game::Levels::GetDefaultPackCover()); Time (p_BeatMap->___songDuration); BPM (p_BeatMap->___beatsPerMinute); - NJS ((int)l_DifficultyBeatmap->___noteJumpMovementSpeed); - Offset (l_DifficultyBeatmap->___noteJumpStartBeatOffset); - auto l_CustomBeatmapLevelCast = il2cpp_utils::try_cast(p_BeatMap); - if (l_CustomBeatmapLevelCast) - { - auto l_CustomBeatmapLevel = l_CustomBeatmapLevelCast.value(); + Time(-1.0f); + NPS(-1.0f); + NJS(-1); + Offset(std::numeric_limits::quiet_NaN()); + Notes(-1); + Obstacles(-1); + Bombs(-1); - NPS (-1); - Notes (-1); - Obstacles(-1); - Bombs (-1); + return true; + } + /// @brief Set from game + /// @param p_BeatMap BeatMap + /// @param p_Cover Cover texture + /// @param p_Characteristic Game mode + /// @param p_Difficulty Difficulty + bool LevelDetail::FromGame(BeatmapLevel* p_BeatMap, Sprite* p_Cover, BeatmapCharacteristicSO* p_Characteristic, BeatmapDifficulty p_Difficulty) + { + Reset(); - BeatmapKey l_BeatmapKey; - if (Game::Levels::BeatmapLevel_TryGetBeatmapKey(p_BeatMap, p_Characteristic, p_Difficulty, &l_BeatmapKey)) - { - auto l_DifficultyBeatmap = BeatmapDataLoader::New_ctor()->LoadBasicBeatmapData(l_CustomBeatmapLevel->get_beatmapLevelData(), byref(l_BeatmapKey)); - NPS (static_cast(l_DifficultyBeatmap->get_cuttableNotesCount()) / std::max(static_cast(p_BeatMap->___songDuration), 1.0f)); - Notes (l_DifficultyBeatmap->get_cuttableNotesCount()); - Obstacles (l_DifficultyBeatmap->get_obstaclesCount()); - Bombs (l_DifficultyBeatmap->get_bombsCount()); - } - } - else + if (p_BeatMap == nullptr) { - NPS (static_cast(l_DifficultyBeatmap->___notesCount) / std::max(static_cast(p_BeatMap->___songDuration), 1.0f)); - Notes (l_DifficultyBeatmap->___notesCount); - Obstacles (l_DifficultyBeatmap->___obstaclesCount); - Bombs (l_DifficultyBeatmap->___bombsCount); + CP_SDK::ChatPlexSDK::Logger()->Error(u"[CP_SDK_BS.UI][LevelDetail.FromGame] Null Beatmap provided!"); + return false; } + /// Store beatmap + m_LocalBeatMap = p_BeatMap; + m_LimitedBeatmapDifficulty = p_Difficulty; + + /// Display mode + m_Characteristic = HMUI::IconSegmentedControl::DataItem::New_ctor(p_Characteristic->_icon, BGLib::Polyglot::Localization::Get(p_Characteristic->_descriptionLocalizationKey), true); + + HMUIIconSegmentedControl::SetDataNoHoverHint(m_SongCharacteristicSegmentedControl.Ptr(), ::Array::New({m_Characteristic.Ptr()})); + m_SongCharacteristicSegmentedControl->SelectCellWithNumber(0); + OnCharacteristicChanged(nullptr, 0); + + Name (p_BeatMap->songName); + AuthorNameText(u"Mapped by " + p_BeatMap->allMappers->FirstOrDefault() + u""); + Cover (p_Cover ? p_Cover : Game::Levels::GetDefaultPackCover()); + Time (p_BeatMap->___songDuration); + BPM (p_BeatMap->___beatsPerMinute); + return true; } /// @brief Set from BeatSaver @@ -485,8 +535,7 @@ namespace CP_SDK_BS::UI { /// @param p_Cover Cover texture bool LevelDetail::FromBeatSaver(Game::BeatMaps::MapDetail::Ptr& p_BeatMap, Sprite* p_Cover) { - m_LocalBeatMap = nullptr; - m_BeatMap = nullptr; + Reset(); if (p_BeatMap == nullptr) { @@ -507,7 +556,7 @@ namespace CP_SDK_BS::UI { { auto l_BeatmapCharacteristicSO = (BeatmapCharacteristicSO*)nullptr; if (Game::Levels::TryGetBeatmapCharacteristicSOBySerializedName(l_Current, &l_BeatmapCharacteristicSO)) - l_Characteristics->Add(HMUI::IconSegmentedControl::DataItem::New_ctor(l_BeatmapCharacteristicSO->____icon, BGLib::Polyglot::Localization::Get(l_BeatmapCharacteristicSO->____descriptionLocalizationKey), true)); + l_Characteristics->Add(HMUI::IconSegmentedControl::DataItem::New_ctor(l_BeatmapCharacteristicSO->_icon, BGLib::Polyglot::Localization::Get(l_BeatmapCharacteristicSO->_descriptionLocalizationKey), true)); } if (l_Characteristics->get_Count() == 0) @@ -560,7 +609,7 @@ namespace CP_SDK_BS::UI { if (l_HoverHint == nullptr || !l_HoverHint) { l_HoverHint = m_FavoriteToggle->AddComponent(); - l_HoverHint->____hoverHintController = Resources::FindObjectsOfTypeAll()->First(); + l_HoverHint->_hoverHintController = Resources::FindObjectsOfTypeAll()->First(); } l_HoverHint->set_text(p_Hint); @@ -659,15 +708,26 @@ namespace CP_SDK_BS::UI { SelectedBeatmapCharacteristicSO = l_Characs[p_Index]; auto l_Difficulties = System::Collections::Generic::List_1::New_ctor(); - Game::Levels::BeatmapLevel_ForEachBeatmapKey(m_LocalBeatMap.Ptr(), [&](const BeatmapKey& p_Current) -> bool + if (m_LimitedBeatmapDifficulty.has_value()) { - auto l_Name = Game::Levels::BeatmapDifficultySerializedNameToDifficultyName(BeatmapDifficultySerializedMethods::SerializedName(p_Current.difficulty)); - if (l_Difficulties->Contains(l_Name)) - return true; ///< Continue + l_Difficulties->Add( + Game::Levels::BeatmapDifficultySerializedNameToDifficultyName( + BeatmapDifficultySerializedMethods::SerializedName(m_LimitedBeatmapDifficulty.value()) + ) + ); + } + else + { + Game::Levels::BeatmapLevel_ForEachBeatmapKey(m_LocalBeatMap.Ptr(), [&](const BeatmapKey& p_Current) -> bool + { + auto l_Name = Game::Levels::BeatmapDifficultySerializedNameToDifficultyName(BeatmapDifficultySerializedMethods::SerializedName(p_Current.difficulty)); + if (l_Difficulties->Contains(l_Name)) + return true; ///< Continue - l_Difficulties->Add(l_Name); - return true; ///< Continue - }); + l_Difficulties->Add(l_Name); + return true; ///< Continue + }); + } m_SongDiffSegmentedControl->SetTexts(l_Difficulties->AsReadOnly()->i___System__Collections__Generic__IReadOnlyList_1_T_(), nullptr); m_SongDiffSegmentedControl->SelectCellWithNumber(l_Difficulties->get_Count() - 1); @@ -707,6 +767,9 @@ namespace CP_SDK_BS::UI { if (p_Current.beatmapCharacteristic.unsafePtr() != SelectedBeatmapCharacteristicSO.Ptr(false)) return true; ///< Continue + if (m_LimitedBeatmapDifficulty.has_value() && m_LimitedBeatmapDifficulty.value() != p_Current.difficulty) + return true; ///< Continue + auto l_It = std::find_if(l_Difficulties.begin(), l_Difficulties.end(), [&](const BeatmapKey& p_A) -> bool { return p_A.beatmapCharacteristic.unsafePtr() == p_Current.beatmapCharacteristic.unsafePtr() && p_A.difficulty == p_Current.difficulty && p_A.levelId == p_Current.levelId; }); @@ -729,7 +792,7 @@ namespace CP_SDK_BS::UI { return; } - auto& l_BeatmapKey = l_Difficulties[p_Index]; + auto l_BeatmapKey = l_Difficulties[p_Index]; auto l_DifficultyBeatmap = m_LocalBeatMap->GetDifficultyBeatmapData(SelectedBeatmapCharacteristicSO.Ptr(), l_BeatmapKey.difficulty); Time (m_LocalBeatMap->___songDuration); @@ -741,6 +804,39 @@ namespace CP_SDK_BS::UI { Obstacles (l_DifficultyBeatmap->___obstaclesCount); Bombs (l_DifficultyBeatmap->___bombsCount); + auto l_LocalBeatMap = m_LocalBeatMap.Ptr(); + Game::Levels::LoadBeatmapLevelDataByLevelID( + m_LocalBeatMap->levelID, + [=, this](CP_SDK::Utils::MonoPtr beatmapLevel, CP_SDK::Utils::MonoPtr beatmapLevelData) -> void { + if (!beatmapLevelData || l_LocalBeatMap != m_LocalBeatMap) + return; + + auto beatmpDataLoaderOwner = Resources::FindObjectsOfTypeAll() + ->FirstOrDefault([](auto x) { return x->_beatmapDataLoader != nullptr; }); + auto beatmpDataLoader = beatmpDataLoaderOwner ? beatmpDataLoaderOwner->_beatmapDataLoader : nullptr; + + if (beatmpDataLoader != nullptr) + { + using t_TaskResult = System::Threading::Tasks::Task_1*; + using t_ContinueSig = System::Action_1*; + + CP_SDK::Unity::MTMainThreadInvoker::Enqueue([=, this]() -> void { + BeatmapKey localBeatmapKey = l_BeatmapKey; + auto infos = beatmpDataLoader->LoadBasicBeatmapData(beatmapLevelData.Ptr(false), byref(localBeatmapKey)); + if (!infos || l_LocalBeatMap != m_LocalBeatMap) + return; + + + NPS(((float)infos->cuttableNotesCount / (float)m_LocalBeatMap->songDuration)); + Notes(infos->cuttableNotesCount); + Obstacles(infos->obstaclesCount); + Bombs(infos->bombsCount); + + }); + } + } + ); + OnActiveDifficultyChanged(l_BeatmapKey); } else if (m_BeatMap) From e3dcc67080e5c2e5ae3975703f0c8c3307d88d7a Mon Sep 17 00:00:00 2001 From: HardCPP Date: Sun, 9 Aug 2026 23:23:31 +0200 Subject: [PATCH 15/15] Use new accessors style --- src/CP_SDK/Unity/FontManager.cpp | 12 +++++----- src/CP_SDK_BS/Game/LevelCompletionData.cpp | 4 ++-- src/CP_SDK_BS/Game/LevelData.cpp | 6 ++--- src/CP_SDK_BS/Game/LevelSelection.cpp | 24 +++++++++---------- ...playerLevelScenesTransitionSetupDataSO.cpp | 4 ++-- .../Subs/SubFloatingPanelMover.cpp | 4 ++-- src/CP_SDK_BS/UI/HMUIIconSegmentedControl.cpp | 6 ++--- src/CP_SDK_BS/UI/HMUITextSegmentedControl.cpp | 6 ++--- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/CP_SDK/Unity/FontManager.cpp b/src/CP_SDK/Unity/FontManager.cpp index c331d0a..f4fb7fd 100644 --- a/src/CP_SDK/Unity/FontManager.cpp +++ b/src/CP_SDK/Unity/FontManager.cpp @@ -22,7 +22,7 @@ using namespace UnityEngine; if(!g_SoloButton) { auto l_MainMenuViewController = UnityEngine::Resources::FindObjectsOfTypeAll()->First(); - g_SoloButton = l_MainMenuViewController->____soloButton; + g_SoloButton = l_MainMenuViewController->_soloButton; } p_Button = g_SoloButton.ptr(); @@ -116,11 +116,11 @@ namespace CP_SDK::Unity { /// Temp disable until libunity is properly unstripped /* - m_MainFont->___normalStyle = 0.5f; - m_MainFont->___normalSpacingOffset = -1.0f; - m_MainFont->___boldStyle = 2.0f; - m_MainFont->___boldSpacing = 2.0f; - m_MainFont->___italicStyle = 15; + m_MainFont->normalStyle = 0.5f; + m_MainFont->normalSpacingOffset = -1.0f; + m_MainFont->boldStyle = 2.0f; + m_MainFont->boldSpacing = 2.0f; + m_MainFont->italicStyle = 15; */ } diff --git a/src/CP_SDK_BS/Game/LevelCompletionData.cpp b/src/CP_SDK_BS/Game/LevelCompletionData.cpp index 17a3442..6ee09ed 100644 --- a/src/CP_SDK_BS/Game/LevelCompletionData.cpp +++ b/src/CP_SDK_BS/Game/LevelCompletionData.cpp @@ -14,7 +14,7 @@ namespace CP_SDK_BS::Game { return false; std::vector l_Requirements; - if (Levels::TryGetCustomRequirementsFor(Data->___beatmapLevel, Data->___beatmapKey.beatmapCharacteristic, Data->___beatmapKey.difficulty, &l_Requirements)) + if (Levels::TryGetCustomRequirementsFor(Data->beatmapLevel, Data->beatmapKey.beatmapCharacteristic, Data->beatmapKey.difficulty, &l_Requirements)) { for (auto& l_Current : l_Requirements) { @@ -33,7 +33,7 @@ namespace CP_SDK_BS::Game { return false; std::vector l_Requirements; - if (Levels::TryGetCustomRequirementsFor(Data->___beatmapLevel, Data->___beatmapKey.beatmapCharacteristic, Data->___beatmapKey.difficulty, &l_Requirements)) + if (Levels::TryGetCustomRequirementsFor(Data->beatmapLevel, Data->beatmapKey.beatmapCharacteristic, Data->beatmapKey.difficulty, &l_Requirements)) { for (auto& l_Current : l_Requirements) { diff --git a/src/CP_SDK_BS/Game/LevelData.cpp b/src/CP_SDK_BS/Game/LevelData.cpp index 25de075..3b8216d 100644 --- a/src/CP_SDK_BS/Game/LevelData.cpp +++ b/src/CP_SDK_BS/Game/LevelData.cpp @@ -32,11 +32,11 @@ namespace CP_SDK_BS::Game { } bool LevelData::IsNoodle() { - if (!Data || !Data->___beatmapLevel) + if (!Data || !Data->beatmapLevel) return false; std::vector l_Requirements; - if (Levels::TryGetCustomRequirementsFor(Data->___beatmapLevel, Data->___beatmapKey.beatmapCharacteristic, Data->___beatmapKey.difficulty, &l_Requirements)) + if (Levels::TryGetCustomRequirementsFor(Data->beatmapLevel, Data->beatmapKey.beatmapCharacteristic, Data->beatmapKey.difficulty, &l_Requirements)) { for (auto& l_Current : l_Requirements) { @@ -55,7 +55,7 @@ namespace CP_SDK_BS::Game { return false; std::vector l_Requirements; - if (Levels::TryGetCustomRequirementsFor(Data->___beatmapLevel, Data->___beatmapKey.beatmapCharacteristic, Data->___beatmapKey.difficulty, &l_Requirements)) + if (Levels::TryGetCustomRequirementsFor(Data->beatmapLevel, Data->beatmapKey.beatmapCharacteristic, Data->beatmapKey.difficulty, &l_Requirements)) { for (auto& l_Current : l_Requirements) { diff --git a/src/CP_SDK_BS/Game/LevelSelection.cpp b/src/CP_SDK_BS/Game/LevelSelection.cpp index d925926..931013a 100644 --- a/src/CP_SDK_BS/Game/LevelSelection.cpp +++ b/src/CP_SDK_BS/Game/LevelSelection.cpp @@ -103,7 +103,7 @@ namespace CP_SDK_BS::Game { /// @param p_LevelSelectionNavigationController LevelSelectionNavigationController instance custom_types::Helpers::Coroutine LevelSelection::LevelSelection_SelectLevelCategory(_u::LevelSelectionNavigationController* p_LevelSelectionNavigationController) { - while (!_v::IsUnityPtrValid(p_LevelSelectionNavigationController) || p_LevelSelectionNavigationController->____isInTransition) + while (!_v::IsUnityPtrValid(p_LevelSelectionNavigationController) || p_LevelSelectionNavigationController->_isInTransition) { if (!_v::IsUnityPtrValid(p_LevelSelectionNavigationController)) co_return; @@ -117,22 +117,22 @@ namespace CP_SDK_BS::Game { if (!p_LevelSelectionNavigationController || !p_LevelSelectionNavigationController->get_isInViewControllerHierarchy() || !p_LevelSelectionNavigationController->get_isActiveAndEnabled()) co_return; - auto l_LevelFilteringNavigationController = p_LevelSelectionNavigationController->____levelFilteringNavigationController; + auto l_LevelFilteringNavigationController = p_LevelSelectionNavigationController->_levelFilteringNavigationController; if (!l_LevelFilteringNavigationController.isAlive()) co_return; if (l_LevelFilteringNavigationController->get_selectedLevelCategory() != _u::SelectLevelCategoryViewController::LevelCategory::All) { - auto l_Selector = l_LevelFilteringNavigationController->____selectLevelCategoryViewController; + auto l_Selector = l_LevelFilteringNavigationController->_selectLevelCategoryViewController; if (l_Selector.isAlive()) { - auto l_SegmentControl = l_Selector->____levelFilterCategoryIconSegmentedControl; - auto l_Tags = l_Selector->____levelCategoryInfos; + auto l_SegmentControl = l_Selector->_levelFilterCategoryIconSegmentedControl; + auto l_Tags = l_Selector->_levelCategoryInfos; auto l_IndexToSelect = -1; for (auto l_I = 0; l_I < l_Tags->get_Length(); ++l_I) { - if (l_Tags[l_I]->___levelCategory != _u::SelectLevelCategoryViewController::LevelCategory::All) + if (l_Tags[l_I]->levelCategory != _u::SelectLevelCategoryViewController::LevelCategory::All) continue; l_IndexToSelect = l_I; @@ -148,7 +148,7 @@ namespace CP_SDK_BS::Game { CP_SDK::Unity::MTCoroutineStarter::Start(custom_types::Helpers::CoroutineHelper::New( LevelSelection_FilterLevel( - l_LevelFilteringNavigationController->____levelSearchViewController, + l_LevelFilteringNavigationController->_levelSearchViewController, true ) )); @@ -158,7 +158,7 @@ namespace CP_SDK_BS::Game { { CP_SDK::Unity::MTCoroutineStarter::Start(custom_types::Helpers::CoroutineHelper::New( LevelSelection_FilterLevel( - l_LevelFilteringNavigationController->____levelSearchViewController, + l_LevelFilteringNavigationController->_levelSearchViewController, false ) )); @@ -177,7 +177,7 @@ namespace CP_SDK_BS::Game { if (p_Wait) { - while (!_v::IsUnityPtrValid(p_LevelSearchViewController) || p_LevelSearchViewController->____isInTransition) + while (!_v::IsUnityPtrValid(p_LevelSearchViewController) || p_LevelSearchViewController->_isInTransition) { if (!_v::IsUnityPtrValid(p_LevelSearchViewController)) co_return; @@ -208,7 +208,7 @@ namespace CP_SDK_BS::Game { l_Filter.maxBpm = 0.0f; l_Filter.sensitivity = _u::PlayerSensitivityFlag::Unknown; - l_Filter.limitIds = ArrayW({ m_PendingFilterSong->___levelID }); + l_Filter.limitIds = ArrayW({ m_PendingFilterSong->levelID }); l_Filter.searchText = u""; //p_LevelSearchViewController->ResetAllFilterSettings(false); @@ -235,12 +235,12 @@ namespace CP_SDK_BS::Game { try { - const auto& l_Filter = p_LevelSearchViewController->____currentSearchFilter; + const auto& l_Filter = p_LevelSearchViewController->_currentSearchFilter; if (l_Filter.limitIds && l_Filter.limitIds->get_Length() == 1) { p_LevelSearchViewController->ResetAllFilterSettings(false); - auto l_InputFieldView = p_LevelSearchViewController->____searchTextInputFieldView; + auto l_InputFieldView = p_LevelSearchViewController->_searchTextInputFieldView; if (l_InputFieldView) { l_InputFieldView->UpdateClearButton(); diff --git a/src/CP_SDK_BS/Game/Patches/PMultiplayerLevelScenesTransitionSetupDataSO.cpp b/src/CP_SDK_BS/Game/Patches/PMultiplayerLevelScenesTransitionSetupDataSO.cpp index 2e25d75..c78e995 100644 --- a/src/CP_SDK_BS/Game/Patches/PMultiplayerLevelScenesTransitionSetupDataSO.cpp +++ b/src/CP_SDK_BS/Game/Patches/PMultiplayerLevelScenesTransitionSetupDataSO.cpp @@ -75,8 +75,8 @@ namespace CP_SDK_BS::Game::Patches { { auto l_LocalResultData = __a->get_localPlayerResultData(); - if (l_LocalResultData->___multiplayerLevelCompletionResults) - l_LevelCompletionData->Results = l_LocalResultData->___multiplayerLevelCompletionResults->____levelCompletionResults; + if (l_LocalResultData->multiplayerLevelCompletionResults) + l_LevelCompletionData->Results = l_LocalResultData->multiplayerLevelCompletionResults->_levelCompletionResults; } Scoring::__SetScoreSaberIsInReplay(false); diff --git a/src/CP_SDK_BS/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMover.cpp b/src/CP_SDK_BS/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMover.cpp index 9af3958..94e16f1 100644 --- a/src/CP_SDK_BS/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMover.cpp +++ b/src/CP_SDK_BS/UI/DefaultComponentsOverrides/Subs/SubFloatingPanelMover.cpp @@ -58,8 +58,8 @@ namespace CP_SDK_BS::UI::DefaultComponentsOverrides::Subs { if (m_GrabbingController) return; - auto l_HitCount = Physics::RaycastNonAlloc( l_VRController->____viewAnchorTransform->get_position(), - l_VRController->____viewAnchorTransform->get_forward(), + auto l_HitCount = Physics::RaycastNonAlloc( l_VRController->_viewAnchorTransform->get_position(), + l_VRController->_viewAnchorTransform->get_forward(), m_RaycastBuffer.Ptr(), MaxLaserDistance, static_cast(1 << CP_SDK::UI::UISystem::UILayer)); diff --git a/src/CP_SDK_BS/UI/HMUIIconSegmentedControl.cpp b/src/CP_SDK_BS/UI/HMUIIconSegmentedControl.cpp index 552e813..ed05801 100644 --- a/src/CP_SDK_BS/UI/HMUIIconSegmentedControl.cpp +++ b/src/CP_SDK_BS/UI/HMUIIconSegmentedControl.cpp @@ -36,14 +36,14 @@ namespace CP_SDK_BS::UI { if (!m_Template) { m_Template = Resources::FindObjectsOfTypeAll()->First([](auto x) -> bool { - return x->get_name() == u"BeatmapCharacteristicSegmentedControl" && x->____container != nullptr; + return x->get_name() == u"BeatmapCharacteristicSegmentedControl" && x->_container != nullptr; }); } auto l_Control = GameObject::Instantiate(m_Template.Ptr(), p_Parent, false); l_Control->set_name(u"BSPIconSegmentedControl"); - l_Control->____container = m_Template->____container; - l_Control->____hideCellBackground = p_HideCellBackground; + l_Control->_container = m_Template->_container; + l_Control->_hideCellBackground = p_HideCellBackground; auto l_RectTransform = l_Control->get_transform().try_cast().value_or(nullptr); l_RectTransform->set_anchorMin (Vector2::get_one() * 0.5f); diff --git a/src/CP_SDK_BS/UI/HMUITextSegmentedControl.cpp b/src/CP_SDK_BS/UI/HMUITextSegmentedControl.cpp index b650afa..987b74f 100644 --- a/src/CP_SDK_BS/UI/HMUITextSegmentedControl.cpp +++ b/src/CP_SDK_BS/UI/HMUITextSegmentedControl.cpp @@ -38,14 +38,14 @@ namespace CP_SDK_BS::UI { if (!m_Template) { m_Template = Resources::FindObjectsOfTypeAll()->First([](auto x) -> bool { - return x->get_name() == u"BeatmapDifficultySegmentedControl" && x->____container != nullptr; + return x->get_name() == u"BeatmapDifficultySegmentedControl" && x->_container != nullptr; }); } auto l_Control = GameObject::Instantiate(m_Template.Ptr(), p_Parent, false); l_Control->set_name(u"BSPTextSegmentedControl"); - l_Control->____container = m_Template->____container; - l_Control->____hideCellBackground = p_HideCellBackground; + l_Control->_container = m_Template->_container; + l_Control->_hideCellBackground = p_HideCellBackground; auto l_RectTransform = l_Control->get_transform().try_cast().value_or(nullptr); l_RectTransform->set_anchorMin (Vector2::get_one() * 0.5f);