diff --git a/.github/workflows/build-ndk.yml b/.github/workflows/build-ndk.yml index 12c5fad..7d7263c 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 ef049f1..88595a3 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 @@ -54,6 +54,7 @@ jobs: qpm_release_bin: true qpm_debug_bin: true qpm_qmod: ${{ env.qmodName }}.qmod + resolve_ndk: true - name: Build & Create Qmod run: | diff --git a/.gitignore b/.gitignore index 9d7352d..f843193 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,6 @@ cmake-build-*/ extern.cmake # QMOD Schema -mod.json.schema \ No newline at end of file +mod.json.schema + +.env \ No newline at end of file diff --git a/addr2line.ps1 b/addr2line.ps1 deleted file mode 100644 index 3d874cf..0000000 --- a/addr2line.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -param($p1, $p2) -$NDKPath = $env:ANDROID_NDK_HOME - -if ($p1 -and $p2) -{ - & $NDKPath\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-addr2line.exe -e .\build\debug\$p1 $p2 -} -else -{ - if ($p1) - { - & $NDKPath\toolchains\llvm\prebuilt\windows-x86_64\bin\llvm-addr2line.exe -e .\build\debug\libqbeatsaberplus-menumusic.so $p1 - } - else - { - echo give at least 1 argument - } -} \ No newline at end of file diff --git a/include/git_info.h b/include/git_info.h index d315608..d5caef6 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 0x9d550ce +#define GIT_COMMIT 0xac42de2 #define GIT_MODIFIED 1 diff --git a/mod.json b/mod.json index c04b91e..0ddcf6f 100644 --- a/mod.json +++ b/mod.json @@ -5,7 +5,7 @@ "id": "qbeatsaberplus-menumusic", "modloader": "Scotland2", "author": "HardCPP", - "version": "6.4.1", + "version": "6.4.5", "packageId": "com.beatgames.beatsaber", "packageVersion": "1.40.8_7379", "description": "Feel good!", @@ -17,24 +17,24 @@ "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": "^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" }, { - "version": "^6.4.1", + "version": "^6.4.5", "id": "chatplex-sdk-bs", - "downloadIfMissing": "https://github.com/hardcpp/QuestChatPlexSDK-BS/releases/download/v6.4.1/ChatPlexSDK-BS.qmod" + "downloadIfMissing": "https://github.com/hardcpp/QuestChatPlexSDK-BS/releases/download/v6.4.5/ChatPlexSDK-BS.qmod" } ], "modFiles": [], diff --git a/qpm.json b/qpm.json index 9d84a7a..5bc845c 100644 --- a/qpm.json +++ b/qpm.json @@ -5,7 +5,7 @@ "info": { "name": "QBeatSaberPlus-MenuMusic", "id": "qbeatsaberplus-menumusic", - "version": "6.4.1", + "version": "6.4.5", "url": "https://github.com/hardcpp/QBeatSaberPlus-MenuMusic", "additionalData": { "overrideSoName": "libqbeatsaberplus-menumusic.so", @@ -15,28 +15,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,12 +52,12 @@ }, { "id": "bs-cordl", - "versionRange": "^4008.*", + "versionRange": "4008.*", "additionalData": {} }, { "id": "custom-types", - "versionRange": "^0.18.3", + "versionRange": "^0.18.4", "additionalData": {} }, { @@ -70,14 +70,14 @@ }, { "id": "songcore", - "versionRange": "^1.1.24", + "versionRange": "^1.1.26", "additionalData": { "private": true } }, { "id": "paper2_scotland2", - "versionRange": "^4.6.4", + "versionRange": "^4.8.0", "additionalData": {} }, { @@ -87,7 +87,7 @@ }, { "id": "chatplex-sdk-bs", - "versionRange": "^6.4.1", + "versionRange": "^6.4.5", "additionalData": { "private": true } diff --git a/qpm.shared.json b/qpm.shared.json index b2750fe..66189c5 100644 --- a/qpm.shared.json +++ b/qpm.shared.json @@ -7,7 +7,7 @@ "info": { "name": "QBeatSaberPlus-MenuMusic", "id": "qbeatsaberplus-menumusic", - "version": "6.4.1", + "version": "6.4.5", "url": "https://github.com/hardcpp/QBeatSaberPlus-MenuMusic", "additionalData": { "overrideSoName": "libqbeatsaberplus-menumusic.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": {} }, { @@ -72,14 +72,14 @@ }, { "id": "songcore", - "versionRange": "^1.1.24", + "versionRange": "^1.1.26", "additionalData": { "private": true } }, { "id": "paper2_scotland2", - "versionRange": "^4.6.4", + "versionRange": "^4.8.0", "additionalData": {} }, { @@ -89,7 +89,7 @@ }, { "id": "chatplex-sdk-bs", - "versionRange": "^6.4.1", + "versionRange": "^6.4.5", "additionalData": { "private": true } @@ -99,90 +99,36 @@ "restoredDependencies": [ { "dependency": { - "id": "kaleb", - "versionRange": "=0.1.9", + "id": "custom-types", + "versionRange": "=0.18.4", "additionalData": { - "headersOnly": true, - "branchName": "version/v0_1_9", + "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.4/CustomTypes.qmod", + "branchName": "version/v0_18_4", "compileOptions": { "cppFlags": [ - "-DKALEB_VERSION=\"0.1.9\"" - ] - }, - "cmake": false - } - }, - "version": "0.1.9" - }, - { - "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" + "-Wno-invalid-offsetof" ] }, - "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": "chatplex-sdk-bs", - "versionRange": "=6.4.1", - "additionalData": { - "soLink": "https://github.com/hardcpp/QuestChatPlexSDK-BS/releases/download/v6.4.1/libchatplex-sdk-bs.so", - "debugSoLink": "https://github.com/hardcpp/QuestChatPlexSDK-BS/releases/download/v6.4.1/debug_libchatplex-sdk-bs.so", - "overrideSoName": "libchatplex-sdk-bs.so", - "modLink": "https://github.com/hardcpp/QuestChatPlexSDK-BS/releases/download/v6.4.1/ChatPlexSDK-BS.qmod", - "branchName": "version/v6_4_1", "cmake": true } }, - "version": "6.4.1" + "version": "0.18.4" }, { "dependency": { - "id": "custom-types", - "versionRange": "=0.18.3", + "id": "scotland2", + "versionRange": "=0.1.7", "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", - "overrideSoName": "libcustom-types.so", - "modLink": "https://github.com/QuestPackageManager/Il2CppQuestTypePatching/releases/download/v0.18.3/CustomTypes.qmod", - "branchName": "version/v0_18_3", - "compileOptions": { - "cppFlags": [ - "-Wno-invalid-offsetof" - ] - }, - "cmake": true + "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.18.3" + "version": "0.1.7" }, { "dependency": { @@ -208,6 +154,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": "libcurl", @@ -223,18 +188,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": { @@ -258,35 +227,66 @@ }, { "dependency": { - "id": "fmt", - "versionRange": "=11.0.2", + "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": "chatplex-sdk-bs", + "versionRange": "=6.4.5", + "additionalData": { + "soLink": "https://github.com/hardcpp/QuestChatPlexSDK-BS/releases/download/v6.4.5/libchatplex-sdk-bs.so", + "debugSoLink": "https://github.com/hardcpp/QuestChatPlexSDK-BS/releases/download/v6.4.5/debug_libchatplex-sdk-bs.so", + "overrideSoName": "libchatplex-sdk-bs.so", + "modLink": "https://github.com/hardcpp/QuestChatPlexSDK-BS/releases/download/v6.4.5/ChatPlexSDK-BS.qmod", + "branchName": "version/v6_4_5", + "cmake": true + } + }, + "version": "6.4.5" + }, + { + "dependency": { + "id": "libil2cpp", + "versionRange": "=0.4.0", "additionalData": { "headersOnly": true, - "branchName": "version/v11_0_2", "compileOptions": { "systemIncludes": [ - "fmt/include/" - ], - "cppFlags": [ - "-DFMT_HEADER_ONLY" + "il2cpp/external/baselib/Include", + "il2cpp/external/baselib/Platforms/Android/Include" ] } } }, - "version": "11.0.2" + "version": "0.4.0" }, { "dependency": { - "id": "scotland2", - "versionRange": "=0.1.6", + "id": "kaleb", + "versionRange": "=0.1.9", "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, + "branchName": "version/v0_1_9", + "compileOptions": { + "cppFlags": [ + "-DKALEB_VERSION=\"0.1.9\"" + ] + }, + "cmake": false } }, - "version": "0.1.6" + "version": "0.1.9" } ] } \ No newline at end of file diff --git a/build.ps1 b/scripts/build.ps1 similarity index 80% rename from build.ps1 rename to scripts/build.ps1 index fe746c3..33f3a7f 100644 --- a/build.ps1 +++ b/scripts/build.ps1 @@ -1,8 +1,8 @@ Param( - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [Switch] $clean, - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [Switch] $help ) @@ -25,7 +25,10 @@ if ($clean.IsPresent) { if (($clean.IsPresent) -or (-not (Test-Path -Path "build"))) { new-item -Path build -ItemType Directory -} +} & cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" -B build -& cmake --build ./build +& cmake --build ./build --parallel 12 + +$ExitCode = $LastExitCode +exit $ExitCode \ No newline at end of file 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 99% rename from copy.ps1 rename to scripts/copy.ps1 index 92456f1..aeabc97 100644 --- a/copy.ps1 +++ b/scripts/copy.ps1 @@ -71,6 +71,7 @@ foreach ($fileName in $modFiles) { } } + & $PSScriptRoot/restart-game.ps1 if ($log -eq $true) { 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 95% rename from createqmod.ps1 rename to scripts/createqmod.ps1 index dacc0e3..79b25f8 100644 --- a/createqmod.ps1 +++ b/scripts/createqmod.ps1 @@ -45,11 +45,6 @@ foreach ($mod in $modJson.lateModFiles) { } foreach ($lib in $modJson.libraryFiles) { - #if ($lib -eq "libchatplex-sdk-bs.so") { - # Continue - #} - - $path = "./build/" + $lib if (-not (Test-Path $path)) { $path = "./extern/libs/" + $lib 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 99% rename from ndk-stack.ps1 rename to scripts/ndk-stack.ps1 index b247dbc..ac30cea 100644 --- a/ndk-stack.ps1 +++ b/scripts/ndk-stack.ps1 @@ -9,7 +9,7 @@ Param( if ($help -eq $true) { Write-Output "`"NDK-Stack`" - Processes a tombstone using the debug .so to find file locations" Write-Output "`n-- Arguments --`n" - + Write-Output "LogName `t`t The file name of the tombstone to process" exit 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 99% rename from pull-tombstone.ps1 rename to scripts/pull-tombstone.ps1 index 17d3f83..10fbc24 100644 --- a/pull-tombstone.ps1 +++ b/scripts/pull-tombstone.ps1 @@ -1,10 +1,10 @@ Param( [Parameter(Mandatory=$false)] [String] $fileName = "RecentCrash.log", - + [Parameter(Mandatory=$false)] [Switch] $analyze, - + [Parameter(Mandatory=$false)] [Switch] $help ) 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 90% rename from pullandanalyze.ps1 rename to scripts/pullandanalyze.ps1 index 813eb55..736f3b4 100644 --- a/pullandanalyze.ps1 +++ b/scripts/pullandanalyze.ps1 @@ -1,4 +1,4 @@ - + $NDKPath = $env:ANDROID_NDK_HOME $stackScript = "$NDKPath/ndk-stack" @@ -8,4 +8,4 @@ if (-not ($PSVersionTable.PSEdition -eq "Core")) { $filename = "tombstone_0" + $args[0] & adb pull /storage/emulated/0/Android/data/com.beatgames.beatsaber/files/$($filename) Get-Content $filename | & $stackScript -sym ./build/debug/ > "$($filename)_unstripped.txt" -& code "$($filename)_unstripped.txt" +& code "$($filename)_unstripped.txt" 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 94% rename from start-logging.ps1 rename to scripts/start-logging.ps1 index 46ea7bf..44320c7 100644 --- a/start-logging.ps1 +++ b/scripts/start-logging.ps1 @@ -55,8 +55,8 @@ if ($all -eq $true) { if ($all -eq $false) { $pattern = "(" if ($self -eq $true) { - $modID = (Get-Content "./mod.json" -Raw | ConvertFrom-Json).id - $pattern += "ChatPlexSDK-BS|$modID|" + $modName = (Get-Content "./mod.json" -Raw | ConvertFrom-Json).name + $pattern += "ChatPlexSDK-BS|$modName|" } if (![string]::IsNullOrEmpty($custom)) { $pattern += "$custom|" diff --git a/scripts/start-logging.sh b/scripts/start-logging.sh new file mode 100755 index 0000000..6bc6b95 --- /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+="ChatPlexSDK-BS|$mod_name|" +fi +[[ -n "$custom" ]] && pattern+="$custom|" +[[ -n "$pattern" ]] || pattern='QuestHook|modloader|scotland2|ChatPlexSDK-BS|' +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/ChatPlexMod_MenuMusic/Data/Music.hpp b/shared/ChatPlexMod_MenuMusic/Data/Music.hpp index ff3627e..21dd10b 100644 --- a/shared/ChatPlexMod_MenuMusic/Data/Music.hpp +++ b/shared/ChatPlexMod_MenuMusic/Data/Music.hpp @@ -22,15 +22,15 @@ namespace ChatPlexMod_MenuMusic { namespace Data { CP_SDK_NO_DEF_CTORS(Music); private: - IMusicProvider::Ptr m_MusicProvider; - std::u16string m_SongPath; - std::u16string m_SongCoverPath; - std::u16string m_SongName; - std::u16string m_SongArtist; - std::u16string m_CustomData; + std::weak_ptr m_MusicProvider; + std::u16string m_SongPath; + std::u16string m_SongCoverPath; + std::u16string m_SongName; + std::u16string m_SongArtist; + std::u16string m_CustomData; public: - const IMusicProvider::Ptr& MusicProvider(); + IMusicProvider::Ptr MusicProvider(); public: using Ptr = std::shared_ptr; @@ -85,4 +85,4 @@ namespace ChatPlexMod_MenuMusic { namespace Data { }; } ///< namespace Data -} ///< namespace ChatPlexMod_MenuMusic \ No newline at end of file +} ///< namespace ChatPlexMod_MenuMusic diff --git a/shared/ChatPlexMod_MenuMusic/MenuMusic.hpp b/shared/ChatPlexMod_MenuMusic/MenuMusic.hpp index caf572b..28e8eea 100644 --- a/shared/ChatPlexMod_MenuMusic/MenuMusic.hpp +++ b/shared/ChatPlexMod_MenuMusic/MenuMusic.hpp @@ -20,9 +20,12 @@ namespace ChatPlexMod_MenuMusic { - using namespace GlobalNamespace; - using namespace UnityEngine; - using namespace System::Collections; + namespace _v + { + using namespace GlobalNamespace; + using namespace UnityEngine; + using namespace System::Collections; + } /// @brief Menu Music Module class MenuMusic : public CP_SDK::ModuleBase @@ -41,33 +44,33 @@ namespace ChatPlexMod_MenuMusic { CP_SDK::EIModuleBaseActivationType ActivationType() const override { return CP_SDK::EIModuleBaseActivationType::OnMenuSceneLoaded; } private: - CP_SDK::Utils::MonoPtr m_SettingsMainView; - CP_SDK::Utils::MonoPtr m_SettingsLeftView; + CP_SDK::Utils::MonoPtr m_SettingsMainView; + CP_SDK::Utils::MonoPtr m_SettingsLeftView; private: - CP_SDK::Utils::MonoPtr m_PlayerFloatingPanel; - CP_SDK::Utils::MonoPtr m_PlayerFloatingPanelView; - - CP_SDK::Utils::MonoPtr m_CreateFloatingPlayerCoroutine; - CP_SDK::Utils::MonoPtr m_WaitAndPlayNextSongCoroutine; - - bool m_WantsToQuit; - CP_SDK::Utils::MonoPtr m_PreviewPlayer; - CP_SDK::Utils::MonoPtr m_OriginalMenuMusic; - float m_OriginalAmbientVolumeScale; - Data::Music::Ptr m_CurrentMusic; - CP_SDK::Utils::MonoPtr m_CurrentMusicAudioClip; - CP_SDK::Utils::MonoPtr m_BackupTimeClip; - float m_BackupTime; - bool m_IsPaused; - - Data::IMusicProvider::Ptr m_MusicProvider; - int m_CurrentSongIndex; - CP_SDK::Utils::MonoPtr m_WaitUntillReadyCoroutine; - CP_SDK::Misc::FastCancellationToken::Ptr m_FastCancellationToken; - - CP_SDK::EGenericScene m_LastActiveScene; - bool m_LastPlayingRescue; + CP_SDK::Utils::MonoPtr m_PlayerFloatingPanel; + CP_SDK::Utils::MonoPtr m_PlayerFloatingPanelView; + + CP_SDK::Utils::MonoPtr<_v::Coroutine> m_CreateFloatingPlayerCoroutine; + CP_SDK::Utils::MonoPtr<_v::Coroutine> m_WaitAndPlayNextSongCoroutine; + + bool m_WantsToQuit; + CP_SDK::Utils::MonoPtr<_v::SongPreviewPlayer> m_PreviewPlayer; + CP_SDK::Utils::MonoPtr<_v::AudioClip> m_OriginalMenuMusic; + float m_OriginalAmbientVolumeScale; + Data::Music::Ptr m_CurrentMusic; + CP_SDK::Utils::MonoPtr<_v::AudioClip> m_CurrentMusicAudioClip; + CP_SDK::Utils::MonoPtr<_v::AudioClip> m_BackupTimeClip; + float m_BackupTime; + bool m_IsPaused; + + Data::IMusicProvider::Ptr m_MusicProvider; + int m_CurrentSongIndex; + CP_SDK::Utils::MonoPtr<_v::Coroutine> m_WaitUntillReadyCoroutine; + CP_SDK::Misc::FastCancellationToken::Ptr m_FastCancellationToken; + + CP_SDK::EGenericScene m_LastActiveScene; + bool m_LastPlayingRescue; public: /// @brief Constructor @@ -132,11 +135,11 @@ namespace ChatPlexMod_MenuMusic { static custom_types::Helpers::Coroutine Coroutine_WaitUntilReady(CP_SDK::Utils::Action<> p_Callback); /// @brief Load the song into the preview player /// @param p_OnSceneTransition On scene transition? - static custom_types::Helpers::Coroutine Coroutine_LoadAudioClip(bool p_OnSceneTransition, Data::Music::Ptr p_Music, CP_SDK::Utils::MonoPtr p_AudioClip); + static custom_types::Helpers::Coroutine Coroutine_LoadAudioClip(bool p_OnSceneTransition, Data::Music::Ptr p_Music, CP_SDK::Utils::MonoPtr<_v::AudioClip> p_AudioClip); /// @brief Wait and play next music /// @param p_WaitTime Time to wait static custom_types::Helpers::Coroutine Coroutine_WaitAndPlayNextMusic(float p_EndTime); }; -} ///< namespace ChatPlexMod_MenuMusic \ No newline at end of file +} ///< namespace ChatPlexMod_MenuMusic diff --git a/shared/ChatPlexMod_MenuMusic/ModulePresence.hpp b/shared/ChatPlexMod_MenuMusic/ModulePresence.hpp new file mode 100644 index 0000000..011ce50 --- /dev/null +++ b/shared/ChatPlexMod_MenuMusic/ModulePresence.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +#include + + +namespace ChatPlexMod_MenuMusic { + + namespace _u + { + using namespace UnityEngine; + } + + class ModulePresence + { + CP_SDK_NO_DEF_CTORS(ModulePresence); + + private: + static std::optional m_ChatRequest; + + public: + static bool ChatRequest(); + + }; + +} ///< namespace ChatPlexMod_MenuMusic \ No newline at end of file diff --git a/shared/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.hpp b/shared/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.hpp index ef8c9d0..75d8127 100644 --- a/shared/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.hpp +++ b/shared/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.hpp @@ -29,6 +29,7 @@ namespace ChatPlexMod_MenuMusic::UI { _u::XUIText::Ptr m_SongArtist; _u::XUIIconButton::Ptr m_PlayPauseButton; _u::XUISlider::Ptr m_Volume; + _u::XUIPrimaryButton::Ptr m_AddToQueueButton; _u::XUIPrimaryButton::Ptr m_PlayItButton; CP_SDK::Misc::FastCancellationToken::Ptr m_CancellationToken; @@ -79,4 +80,4 @@ namespace ChatPlexMod_MenuMusic::UI { } ///< namespace ChatPlexMod_MenuMusic::UI -CP_SDK_IL2CPP_INHERIT_HELPERS(ChatPlexMod_MenuMusic::UI::PlayerFloatingPanel); \ No newline at end of file +CP_SDK_IL2CPP_INHERIT_HELPERS(ChatPlexMod_MenuMusic::UI::PlayerFloatingPanel); diff --git a/shared/ChatPlexMod_MenuMusic/Utils/ArtProvider.hpp b/shared/ChatPlexMod_MenuMusic/Utils/ArtProvider.hpp index 747105e..a3e3434 100644 --- a/shared/ChatPlexMod_MenuMusic/Utils/ArtProvider.hpp +++ b/shared/ChatPlexMod_MenuMusic/Utils/ArtProvider.hpp @@ -8,6 +8,7 @@ #include #include +#include #include namespace ChatPlexMod_MenuMusic::Utils { @@ -25,6 +26,8 @@ namespace ChatPlexMod_MenuMusic::Utils { private: static CP_SDK::Unity::TextureRaw::PixelArray m_BackgroundMask; static CP_SDK::Unity::TextureRaw::PixelArray m_CoverMask; + static std::once_flag m_MasksInitFlag; + static bool m_MasksReady; public: using t_BytesPtr = CP_SDK::Utils::MonoPtr>; @@ -50,4 +53,4 @@ namespace ChatPlexMod_MenuMusic::Utils { }; -} ///< namespace ChatPlexMod_MenuMusic::Utils \ No newline at end of file +} ///< namespace ChatPlexMod_MenuMusic::Utils diff --git a/src/ChatPlexMod_MenuMusic/Data/CustomMusicProvider.cpp b/src/ChatPlexMod_MenuMusic/Data/CustomMusicProvider.cpp index eacc039..3533087 100644 --- a/src/ChatPlexMod_MenuMusic/Data/CustomMusicProvider.cpp +++ b/src/ChatPlexMod_MenuMusic/Data/CustomMusicProvider.cpp @@ -3,8 +3,13 @@ #include "ChatPlexMod_MenuMusic/Logger.hpp" #include +#include +#include +#include +#include #include +#include #include @@ -12,9 +17,23 @@ using namespace UnityEngine; namespace ChatPlexMod_MenuMusic { namespace Data { + namespace { + + struct CustomMusicScanResult + { + std::vector> Musics; + std::string Error; + std::atomic_bool IsDone = false; + }; + + } + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + MusicProviderType::E CustomMusicProvider::Type() { - return MusicProviderType::E::GameMusic; + return MusicProviderType::E::CustomMusic; } bool CustomMusicProvider::IsReady() { @@ -82,57 +101,77 @@ namespace ChatPlexMod_MenuMusic { namespace Data { /// @brief Load game songs custom_types::Helpers::Coroutine CustomMusicProvider::Coroutine_Load(Ptr p_Self) { - auto l_Self = *reinterpret_cast*>(&p_Self); + auto l_Self = std::static_pointer_cast(p_Self); co_yield nullptr; - try - { - auto l_BaseDirectory = CP_SDK::ChatPlexSDK::BasePath() + CP_SDK::ChatPlexSDK::ProductName() + "/MenuMusic/CustomMusic"; - auto l_Files = std::vector(); - - if (!std::filesystem::exists(l_BaseDirectory)) - std::filesystem::create_directories(l_BaseDirectory); + auto l_BaseDirectory = std::filesystem::path( + CP_SDK::ChatPlexSDK::BasePath() + CP_SDK::ChatPlexSDK::ProductName() + "/MenuMusic/CustomMusic" + ); + auto l_ScanResult = std::make_shared(); - for (const auto& l_Entry : std::filesystem::directory_iterator(l_BaseDirectory)) + /// Directory IO can block Quest storage; keep it off the Unity frame thread. + CP_SDK::Unity::MTThreadInvoker::EnqueueOnThread([l_BaseDirectory, l_ScanResult, l_Self]() -> void + { + try + { + if (!std::filesystem::exists(l_BaseDirectory)) + std::filesystem::create_directories(l_BaseDirectory); + + for (const auto& l_Entry : std::filesystem::directory_iterator(l_BaseDirectory)) + { + if (!l_Entry.is_regular_file()) + continue; + + auto l_Path = l_Entry.path(); + auto l_Extension = l_Path.extension(); + if ( l_Extension != ".egg" && l_Extension != ".ogg" + && l_Extension != ".EGG" && l_Extension != ".OGG") + continue; + + auto l_PathTest = l_Path; + std::filesystem::path l_CoverPath; + + l_PathTest.replace_extension(".jpg"); + if (std::filesystem::exists(l_PathTest)) + l_CoverPath = l_PathTest; + else + { + l_PathTest = l_Path; + l_PathTest.replace_extension(".png"); + if (std::filesystem::exists(l_PathTest)) + l_CoverPath = l_PathTest; + } + + l_ScanResult->Musics.emplace_back(std::make_shared( + l_Self, + CP_SDK::Utils::StrToU16Str(l_Path.string()), + CP_SDK::Utils::StrToU16Str(l_CoverPath.string()), + CP_SDK::Utils::StrToU16Str(l_Path.stem().string()), + u" ", + u"" + )); + } + } + catch (const std::exception& l_Exception) { - if (!l_Entry.is_regular_file()) - continue; - - auto l_Path = l_Entry.path(); - auto l_Extension = l_Path.extension(); - if ( l_Extension != ".egg" && l_Extension != ".ogg" - && l_Extension != ".EGG" && l_Extension != ".OGG") - continue; - - auto l_PathTest = l_Path; - std::string l_CoverPath; - - if (std::filesystem::exists(l_PathTest.replace_extension(".jpg"))) - l_CoverPath = l_PathTest.replace_extension(".jpg").string(); - else if (std::filesystem::exists(l_PathTest.replace_extension(".png"))) - l_CoverPath = l_PathTest.replace_extension(".png").string(); - - l_Self->m_Musics.push_back(std::shared_ptr(new Music( - l_Self, - CP_SDK::Utils::StrToU16Str(l_Path.string()), - CP_SDK::Utils::StrToU16Str(l_CoverPath), - CP_SDK::Utils::StrToU16Str(l_Path.stem().string()), - u" ", - u"" - ))); + l_ScanResult->Error = l_Exception.what(); } - l_Self->Shuffle(); - } - catch (const std::exception& l_Exception) - { - Logger::Instance->Error(u"[ChatPlexMod_MenuMusic.Data][CustomMusicProvider.Coroutine_Load] Can't load audio! Exception:"); - Logger::Instance->Error(l_Exception); - } + l_ScanResult->IsDone.store(true, std::memory_order_release); }); + + while (!l_ScanResult->IsDone.load(std::memory_order_acquire)) + co_yield nullptr; + + if (!l_ScanResult->Error.empty()) + Logger::Instance->Error(u"[ChatPlexMod_MenuMusic.Data][CustomMusicProvider.Coroutine_Load] Can't scan custom music: " + CP_SDK::Utils::StrToU16Str(l_ScanResult->Error)); + + std::size_t l_ProcessedFiles = 0; + l_Self->m_Musics.swap(l_ScanResult->Musics); + l_Self->Shuffle(); l_Self->m_IsLoading = false; } } ///< namespace Data -} ///< namespace ChatPlexMod_MenuMusic \ No newline at end of file +} ///< namespace ChatPlexMod_MenuMusic diff --git a/src/ChatPlexMod_MenuMusic/Data/GameMusicProvider.cpp b/src/ChatPlexMod_MenuMusic/Data/GameMusicProvider.cpp index dd06a13..0b05012 100644 --- a/src/ChatPlexMod_MenuMusic/Data/GameMusicProvider.cpp +++ b/src/ChatPlexMod_MenuMusic/Data/GameMusicProvider.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -98,17 +99,21 @@ namespace ChatPlexMod_MenuMusic { namespace Data { while (!SongCore::API::Loading::AreSongsLoaded()) co_yield nullptr; - auto l_Self = *reinterpret_cast*>(&p_Self); + auto l_Self = std::static_pointer_cast(p_Self); try { const auto l_LoadedSongs = SongCore::API::Loading::GetAllLevels(); + std::size_t l_ProcessedSongs = 0; for (auto& l_Current : l_LoadedSongs) { - auto l_FileSystemPreviewMediaData = il2cpp_utils::try_cast(l_Current->___previewMediaData); + if (++l_ProcessedSongs % 32 == 0) + co_yield nullptr; + + auto l_FileSystemPreviewMediaData = il2cpp_utils::try_cast(l_Current->previewMediaData); if (!l_FileSystemPreviewMediaData.has_value()) continue; - auto l_Extension = std::filesystem::path(l_FileSystemPreviewMediaData.value()->____previewAudioClipPath).extension().string(); + auto l_Extension = std::filesystem::path(l_FileSystemPreviewMediaData.value()->_previewAudioClipPath).extension().string(); std::transform(l_Extension.begin(), l_Extension.end(), l_Extension.begin(), ::tolower); if (l_Extension != ".egg" && l_Extension != ".ogg") @@ -116,24 +121,26 @@ namespace ChatPlexMod_MenuMusic { namespace Data { l_Self->m_Musics.push_back(std::shared_ptr(new Music( l_Self, - l_FileSystemPreviewMediaData.value()->____previewAudioClipPath, - l_FileSystemPreviewMediaData.value()->____coverSpritePath, - l_Current->___songName, - l_Current->___songAuthorName, - l_Current->___levelID.operator std::__ndk1::u16string_view() + l_FileSystemPreviewMediaData.value()->_previewAudioClipPath, + l_FileSystemPreviewMediaData.value()->_coverSpritePath, + l_Current->songName, + l_Current->songAuthorName, + l_Current->levelID.operator std::__ndk1::u16string_view() ))); } l_Self->Shuffle(); - l_Self->m_IsLoading = false; } catch(const std::exception& p_Exception) { Logger::Instance->Error(u"[ChatPlexMod_MenuMusic.Data][GameMusicProvider.Coroutine_LoadGameSongs] Error:"); Logger::Instance->Error(p_Exception); } + + /// Always release waiters, even if one malformed song caused an exception. + l_Self->m_IsLoading = false; } } ///< namespace Data -} ///< namespace ChatPlexMod_MenuMusic \ No newline at end of file +} ///< namespace ChatPlexMod_MenuMusic diff --git a/src/ChatPlexMod_MenuMusic/Data/Music.cpp b/src/ChatPlexMod_MenuMusic/Data/Music.cpp index 568c1e8..1b2c60a 100644 --- a/src/ChatPlexMod_MenuMusic/Data/Music.cpp +++ b/src/ChatPlexMod_MenuMusic/Data/Music.cpp @@ -20,9 +20,9 @@ using namespace UnityEngine::Networking; namespace ChatPlexMod_MenuMusic { namespace Data { - const IMusicProvider::Ptr& Music::MusicProvider() + IMusicProvider::Ptr Music::MusicProvider() { - return m_MusicProvider; + return m_MusicProvider.lock(); } //////////////////////////////////////////////////////////////////////////// @@ -109,18 +109,19 @@ namespace ChatPlexMod_MenuMusic { namespace Data { CP_SDK::Utils::CActionRef>> p_OnSuccess, CP_SDK::Utils::CActionRef<> p_OnError) { - auto l_StartSerial = p_Token->Serial(); - CP_SDK::Unity::MTThreadInvoker::EnqueueOnThread([this, p_Token, p_OnSuccess, p_OnError, l_StartSerial]() -> void + auto l_StartSerial = p_Token ? p_Token->Serial() : 0; + auto l_Self = shared_from_this(); + CP_SDK::Unity::MTThreadInvoker::EnqueueOnThread([l_Self, p_Token, p_OnSuccess, p_OnError, l_StartSerial]() -> void { - if (p_Token->IsCancelled(l_StartSerial)) + if (p_Token && p_Token->IsCancelled(l_StartSerial)) return; try { CP_SDK::Utils::MonoPtr> l_Bytes; - if (System::IO::File::Exists(m_SongCoverPath)) + if (System::IO::File::Exists(l_Self->m_SongCoverPath)) { - auto l_Raw = System::IO::File::ReadAllBytes(m_SongCoverPath); + auto l_Raw = System::IO::File::ReadAllBytes(l_Self->m_SongCoverPath); l_Bytes = reinterpret_cast<::Array*>(l_Raw.convert()); } else @@ -129,7 +130,7 @@ namespace ChatPlexMod_MenuMusic { namespace Data { l_Bytes = reinterpret_cast<::Array*>(l_Raw.convert()); } - if (p_Token->IsCancelled(l_StartSerial)) + if (p_Token && p_Token->IsCancelled(l_StartSerial)) return; p_OnSuccess(l_Bytes); @@ -188,6 +189,10 @@ namespace ChatPlexMod_MenuMusic { namespace Data { Logger::Instance->Error(l_Exception); p_OnError(); + + if (l_Loader) + l_Loader->Dispose(); + co_return; } @@ -195,10 +200,20 @@ namespace ChatPlexMod_MenuMusic { namespace Data { /// Skip if it's not the menu if (CP_SDK::ChatPlexSDK::ActiveGenericScene() != CP_SDK::EGenericScene::Menu) + { + if (l_Loader) + l_Loader->Dispose(); + co_return; + } if (p_Token && p_Token->IsCancelled(l_StartSerial)) + { + if (l_Loader) + l_Loader->Dispose(); + co_return; + } if (l_Loader->get_result() == UnityWebRequest::Result::ConnectionError || l_Loader->get_result() == UnityWebRequest::Result::ProtocolError @@ -206,6 +221,10 @@ namespace ChatPlexMod_MenuMusic { namespace Data { { Logger::Instance->Error(u"[ChatPlexMod_MenuMusic.Data][Music.GetAudioAsync] Can't load audio! " + (!System::String::IsNullOrEmpty(l_Loader->get_error()) ? l_Loader->get_error() : u"")); p_OnError(); + + if (l_Loader) + l_Loader->Dispose(); + co_return; } @@ -221,6 +240,10 @@ namespace ChatPlexMod_MenuMusic { namespace Data { { Logger::Instance->Error(u"[ChatPlexMod_MenuMusic.Data][Music.GetAudioAsync] No audio found"); p_OnError(); + + if (l_Loader) + l_Loader->Dispose(); + co_return; } } @@ -230,6 +253,10 @@ namespace ChatPlexMod_MenuMusic { namespace Data { Logger::Instance->Error(l_Exception); p_OnError(); + + if (l_Loader) + l_Loader->Dispose(); + co_return; } @@ -245,13 +272,25 @@ namespace ChatPlexMod_MenuMusic { namespace Data { if (l_RemainingTry < 0) { p_OnError(); + + if (l_Loader) + l_Loader->Dispose(); + co_return; } if (p_Token && p_Token->IsCancelled(l_StartSerial)) + { + if (l_Loader) + l_Loader->Dispose(); + co_return; + } } + if (l_Loader) + l_Loader->Dispose(); + if (CP_SDK::ChatPlexSDK::ActiveGenericScene() != CP_SDK::EGenericScene::Menu) co_return; @@ -268,4 +307,4 @@ namespace ChatPlexMod_MenuMusic { namespace Data { } } ///< namespace Data -} ///< namespace ChatPlexMod_MenuMusic \ No newline at end of file +} ///< namespace ChatPlexMod_MenuMusic diff --git a/src/ChatPlexMod_MenuMusic/MenuMusic.cpp b/src/ChatPlexMod_MenuMusic/MenuMusic.cpp index e9b29f4..a87ea94 100644 --- a/src/ChatPlexMod_MenuMusic/MenuMusic.cpp +++ b/src/ChatPlexMod_MenuMusic/MenuMusic.cpp @@ -34,15 +34,16 @@ namespace ChatPlexMod_MenuMusic { /// Constructor MenuMusic::MenuMusic() { - m_OriginalAmbientVolumeScale = 1.0f; - m_BackupTime = 0.0f; - m_IsPaused = false; + m_WantsToQuit = false; + m_OriginalAmbientVolumeScale = 1.0f; + m_BackupTime = 0.0f; + m_IsPaused = false; - m_CurrentSongIndex = 0; + m_CurrentSongIndex = 0; + m_FastCancellationToken = CP_SDK::Misc::FastCancellationToken::Make(); - m_FastCancellationToken = CP_SDK::Misc::FastCancellationToken::Make(); - - m_LastPlayingRescue = true; + m_LastActiveScene = CP_SDK::EGenericScene::None; + m_LastPlayingRescue = true; } //////////////////////////////////////////////////////////////////////////// @@ -51,6 +52,8 @@ namespace ChatPlexMod_MenuMusic { /// @brief Enable the Module void MenuMusic::OnEnable() { + m_WantsToQuit = false; + /// Bind event CP_SDK::ChatPlexSDK::OnGenericSceneChange += {this, &MenuMusic::ChatPlexSDK_OnGenericSceneChange}; /// Application.wantsToQuit += Application_wantsToQuit; @@ -61,13 +64,13 @@ namespace ChatPlexMod_MenuMusic { /// Backup original settings if (m_PreviewPlayer) { - m_OriginalMenuMusic = m_PreviewPlayer->____defaultAudioClip; - m_OriginalAmbientVolumeScale = m_PreviewPlayer->____ambientVolumeScale; + m_OriginalMenuMusic = m_PreviewPlayer->_defaultAudioClip; + m_OriginalAmbientVolumeScale = m_PreviewPlayer->_ambientVolumeScale; } if (UpdateMusicProvider(true)) { - /// Enable at start if in menu + /// Enable at start if in menu. if (CP_SDK::ChatPlexSDK::ActiveGenericScene() == CP_SDK::EGenericScene::Menu) ChatPlexSDK_OnGenericSceneChange(CP_SDK::EGenericScene::Menu); } @@ -95,8 +98,8 @@ namespace ChatPlexMod_MenuMusic { /// Restore original settings if (!m_WantsToQuit && m_PreviewPlayer && m_OriginalMenuMusic) { - m_PreviewPlayer->____defaultAudioClip = m_OriginalMenuMusic.Ptr(); - m_PreviewPlayer->____ambientVolumeScale = m_OriginalAmbientVolumeScale; + m_PreviewPlayer->_defaultAudioClip = m_OriginalMenuMusic.Ptr(); + m_PreviewPlayer->_ambientVolumeScale = m_OriginalAmbientVolumeScale; m_PreviewPlayer->CrossfadeToDefault(); } else if (m_WantsToQuit && m_PreviewPlayer) @@ -126,7 +129,7 @@ namespace ChatPlexMod_MenuMusic { if (p_Scene != CP_SDK::EGenericScene::Menu) { if (m_PreviewPlayer && m_OriginalMenuMusic) - m_PreviewPlayer->____defaultAudioClip = m_OriginalMenuMusic.Ptr(false); + m_PreviewPlayer->_defaultAudioClip = m_OriginalMenuMusic.Ptr(false); DestroyFloatingPlayer(); m_LastActiveScene = p_Scene; @@ -137,8 +140,14 @@ namespace ChatPlexMod_MenuMusic { if (MMConfig::Instance()->ShowPlayer) CreateFloatingPlayer(); - m_PreviewPlayer->____ambientVolumeScale = 0.0f; - m_PreviewPlayer->____volumeScale = 0.0f; + if (!m_PreviewPlayer) + { + Logger::Instance->Error(u"[MenuMusic] SongPreviewPlayer is not available in the menu scene yet."); + return; + } + + m_PreviewPlayer->_ambientVolumeScale = 0.0f; + m_PreviewPlayer->_volumeScale = 0.0f; /// Start a new music if (p_Scene != m_LastActiveScene && MMConfig::Instance()->StartANewMusicOnSceneChange) @@ -194,25 +203,31 @@ namespace ChatPlexMod_MenuMusic { auto& l_Modules = CP_SDK::ChatPlexSDK::GetModules(); if (std::count_if(l_Modules.begin(), l_Modules.end(), [](auto x) { return x->Name() == u"Audio Tweaker"; })) { - auto l_ChannelsController = m_PreviewPlayer->____audioSourceControllers; + auto l_ChannelsController = m_PreviewPlayer->_audioSourceControllers; if (l_ChannelsController && l_ChannelsController.size()) { for (auto l_I = 0; l_I < l_ChannelsController->get_Length(); ++l_I) { auto l_ChannelController = l_ChannelsController[l_I]; - auto l_Channel = l_ChannelController->___audioSource; + if (!l_ChannelController) + continue; + + auto l_Channel = l_ChannelController->audioSource; + if (!l_Channel) + continue; + if (l_Channel->get_isPlaying() && l_Channel->get_clip() == m_CurrentMusicAudioClip) { - m_PreviewPlayer->____ambientVolumeScale = 1.0f; - m_PreviewPlayer->____volumeScale = 1.0f; + m_PreviewPlayer->_ambientVolumeScale = 1.0f; + m_PreviewPlayer->_volumeScale = 1.0f; } } } } else { - m_PreviewPlayer->____ambientVolumeScale = MMConfig::Instance()->PlaybackVolume; - m_PreviewPlayer->____volumeScale = MMConfig::Instance()->PlaybackVolume; + m_PreviewPlayer->_ambientVolumeScale = MMConfig::Instance()->PlaybackVolume; + m_PreviewPlayer->_volumeScale = MMConfig::Instance()->PlaybackVolume; } if (p_FromConfig && m_PlayerFloatingPanelView) @@ -288,7 +303,7 @@ namespace ChatPlexMod_MenuMusic { try { m_Instance->m_PlayerFloatingPanel = CP_SDK::UI::UISystem::FloatingPanelFactory->Create(u"ChatPlexMod_MenuMusic", l_ScreenContainer->get_transform()); - m_Instance->m_PlayerFloatingPanel->SetSize(Vector2(80.0f, 20.0f)); + m_Instance->m_PlayerFloatingPanel->SetSize(Vector2(90.0f, 20.0f)); m_Instance->m_PlayerFloatingPanel->SetRadius(140.0f); m_Instance->m_PlayerFloatingPanel->SetTransformDirect(l_PlayerPosition, Vector3(0.0f, 0.0f, 0.0f)); m_Instance->m_PlayerFloatingPanel->SetBackground(false); @@ -341,6 +356,9 @@ namespace ChatPlexMod_MenuMusic { /// @brief Start a previous music void MenuMusic::StartPreviousMusic() { + if (!m_MusicProvider) + return; + if (!m_MusicProvider->IsReady()) { if (m_WaitUntillReadyCoroutine) @@ -356,8 +374,8 @@ namespace ChatPlexMod_MenuMusic { m_CurrentSongIndex--; /// Handle overflow - if (m_CurrentSongIndex < 0) m_CurrentSongIndex = m_MusicProvider->Musics().size() - 1; - if (m_CurrentSongIndex >= m_MusicProvider->Musics().size()) m_CurrentSongIndex = 0; + if (m_CurrentSongIndex < 0) m_CurrentSongIndex = static_cast(m_MusicProvider->Musics().size()) - 1; + if (m_CurrentSongIndex >= static_cast(m_MusicProvider->Musics().size())) m_CurrentSongIndex = 0; /// Load and play audio clip LoadNextMusic(false); @@ -367,6 +385,9 @@ namespace ChatPlexMod_MenuMusic { /// @param p_OnSceneTransition On scene transition? void MenuMusic::StartNewMusic(bool p_Random, bool p_OnSceneTransition) { + if (!m_MusicProvider) + return; + if (!m_MusicProvider->IsReady()) { if (m_WaitUntillReadyCoroutine) @@ -386,12 +407,12 @@ namespace ChatPlexMod_MenuMusic { if (m_LastPlayingRescue) { auto l_SongIndex = -1; - for (auto l_I = 0; l_I < m_MusicProvider->Musics().size(); ++l_I) + for (std::size_t l_I = 0; l_I < m_MusicProvider->Musics().size(); ++l_I) { if (m_MusicProvider->Musics()[l_I]->GetSongPath() != MMConfig::Instance()->LastPlayingSongPath) continue; - l_SongIndex = l_I; + l_SongIndex = static_cast(l_I); break; } @@ -407,6 +428,9 @@ namespace ChatPlexMod_MenuMusic { /// @brief Start a next music void MenuMusic::StartNextMusic() { + if (!m_MusicProvider) + return; + if (!m_MusicProvider->IsReady()) { if (m_WaitUntillReadyCoroutine) @@ -432,12 +456,12 @@ namespace ChatPlexMod_MenuMusic { /// @param p_OnSceneTransition Is on scene transition? void MenuMusic::LoadNextMusic(bool p_OnSceneTransition) { - if (m_MusicProvider->Musics().empty()) + if (!m_MusicProvider || m_MusicProvider->Musics().empty()) return; /// Handle overflow - if (m_CurrentSongIndex < 0) m_CurrentSongIndex = m_MusicProvider->Musics().size() - 1; - if (m_CurrentSongIndex >= m_MusicProvider->Musics().size()) m_CurrentSongIndex = 0; + if (m_CurrentSongIndex < 0) m_CurrentSongIndex = static_cast(m_MusicProvider->Musics().size()) - 1; + if (m_CurrentSongIndex >= static_cast(m_MusicProvider->Musics().size())) m_CurrentSongIndex = 0; auto& l_MusicToLoad = m_MusicProvider->Musics()[m_CurrentSongIndex]; @@ -469,6 +493,7 @@ namespace ChatPlexMod_MenuMusic { while (!m_Instance->m_MusicProvider || !m_Instance->m_MusicProvider->IsReady()) co_yield nullptr; + m_Instance->m_WaitUntillReadyCoroutine = nullptr; p_Callback(); } /// @brief Load the song into the preview player @@ -481,8 +506,6 @@ namespace ChatPlexMod_MenuMusic { m_Instance->m_WaitAndPlayNextSongCoroutine = nullptr; } - co_yield nullptr; - /// Skip if it's not the menu if (CP_SDK::ChatPlexSDK::ActiveGenericScene() != CP_SDK::EGenericScene::Menu) co_return; @@ -490,13 +513,13 @@ namespace ChatPlexMod_MenuMusic { while (!m_Instance->m_PreviewPlayer) { co_yield nullptr; - m_Instance->m_PreviewPlayer = Resources::FindObjectsOfTypeAll()->First(); + m_Instance->m_PreviewPlayer = Resources::FindObjectsOfTypeAll()->FirstOrDefault(); } if (p_OnSceneTransition) { if (m_Instance->m_PreviewPlayer) - m_Instance->m_PreviewPlayer->FadeOut(m_Instance->m_PreviewPlayer->____crossFadeToDefaultSpeed); + m_Instance->m_PreviewPlayer->FadeOut(m_Instance->m_PreviewPlayer->_crossFadeToDefaultSpeed); co_yield WaitForSecondsRealtime::New_ctor(2.0f)->i___System__Collections__IEnumerator(); } @@ -533,15 +556,15 @@ namespace ChatPlexMod_MenuMusic { m_Instance->m_WaitAndPlayNextSongCoroutine = nullptr; } - m_Instance->m_PreviewPlayer->____defaultAudioClip = m_Instance->m_CurrentMusicAudioClip.Ptr(); + m_Instance->m_PreviewPlayer->_defaultAudioClip = m_Instance->m_CurrentMusicAudioClip.Ptr(); auto l_Volume = MMConfig::Instance()->PlaybackVolume; auto& l_Modules = CP_SDK::ChatPlexSDK::GetModules(); if (std::count_if(l_Modules.begin(), l_Modules.end(), [](auto x) { return x->Name() == u"Audio Tweaker"; })) l_Volume = 1.0f; - m_Instance->m_PreviewPlayer->____ambientVolumeScale = l_Volume; - m_Instance->m_PreviewPlayer->____volumeScale = l_Volume; + m_Instance->m_PreviewPlayer->_ambientVolumeScale = l_Volume; + m_Instance->m_PreviewPlayer->_volumeScale = l_Volume; float l_StartTime = (MMConfig::Instance()->StartSongFromBeginning || m_Instance->m_CurrentMusicAudioClip->get_length() < 60) ? @@ -604,12 +627,17 @@ namespace ChatPlexMod_MenuMusic { co_return; } - auto l_ChannelsController = m_Instance->m_PreviewPlayer->____audioSourceControllers; + auto l_ChannelsController = m_Instance->m_PreviewPlayer->_audioSourceControllers; if (l_ChannelsController.size() > 0) { for (auto l_ChannelController : l_ChannelsController) { - auto l_Channel = l_ChannelController->___audioSource; + if (!l_ChannelController) + continue; + + auto l_Channel = l_ChannelController->audioSource; + if (!l_Channel) + continue; if ( !m_Instance->m_IsPaused && !l_Channel->get_isPlaying() @@ -640,8 +668,8 @@ namespace ChatPlexMod_MenuMusic { if (std::count_if(l_Modules.begin(), l_Modules.end(), [](auto x) { return x->Name() == u"Audio Tweaker"; })) l_Volume = 1.0f; - m_Instance->m_PreviewPlayer->____ambientVolumeScale = l_Volume; - m_Instance->m_PreviewPlayer->____volumeScale = l_Volume; + m_Instance->m_PreviewPlayer->_ambientVolumeScale = l_Volume; + m_Instance->m_PreviewPlayer->_volumeScale = l_Volume; } if (Mathf::Abs(p_EndTime - l_Channel->get_time()) < (MMConfig::Instance()->LoopCurrentMusic ? l_Interval : 3.0f)) @@ -668,4 +696,4 @@ namespace ChatPlexMod_MenuMusic { } while (true); } -} ///< namespace ChatPlexMod_MenuMusic \ No newline at end of file +} ///< namespace ChatPlexMod_MenuMusic diff --git a/src/ChatPlexMod_MenuMusic/ModulePresence.cpp b/src/ChatPlexMod_MenuMusic/ModulePresence.cpp new file mode 100644 index 0000000..4b09e7f --- /dev/null +++ b/src/ChatPlexMod_MenuMusic/ModulePresence.cpp @@ -0,0 +1,30 @@ +#include "ChatPlexMod_MenuMusic/ModulePresence.hpp" +#include "ChatPlexMod_MenuMusic/Logger.hpp" + +#include +#include + +using namespace UnityEngine; + +namespace ChatPlexMod_MenuMusic { + + std::optional ModulePresence::m_ChatRequest; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + + bool ModulePresence::ChatRequest() + { + if (!m_ChatRequest.has_value()) + { + auto& l_Modules = CP_SDK::ChatPlexSDK::GetModules(); + if (std::count_if(l_Modules.begin(), l_Modules.end(), [](auto x) { return x->Name() == u"Chat Request"; })) + m_ChatRequest = true; + else + m_ChatRequest = false; + } + + return m_ChatRequest.value(); + } + +} ///< namespace ChatPlexMod_MenuMusic \ No newline at end of file diff --git a/src/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.cpp b/src/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.cpp index a135c60..78bff2a 100644 --- a/src/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.cpp +++ b/src/ChatPlexMod_MenuMusic/UI/PlayerFloatingPanel.cpp @@ -2,6 +2,7 @@ #include "ChatPlexMod_MenuMusic/Utils/ArtProvider.hpp" #include "ChatPlexMod_MenuMusic/MenuMusic.hpp" #include "ChatPlexMod_MenuMusic/MMConfig.hpp" +#include "ChatPlexMod_MenuMusic/ModulePresence.hpp" #include "ChatPlexMod_MenuMusic/Logger.hpp" #include "assets.hpp" @@ -11,6 +12,8 @@ #include #include +#include + using namespace CP_SDK::Unity::Extensions; using namespace CP_SDK::XUI; using namespace UnityEngine; @@ -141,13 +144,26 @@ namespace ChatPlexMod_MenuMusic::UI { x->RTransform()->set_pivot (Vector2( 1.00f, 0.00f)); x->RTransform()->set_anchorMin (Vector2( 1.00f, 0.00f)); x->RTransform()->set_anchorMax (Vector2( 1.00f, 0.00f)); - x->RTransform()->set_anchoredPosition(Vector2(-11.00f, 1.15f)); - x->RTransform()->set_sizeDelta (Vector2( 35.00f, 5.00f)); + x->RTransform()->set_anchoredPosition(Vector2(-27.50f, 1.15f)); + x->RTransform()->set_sizeDelta (Vector2( 30.00f, 5.00f)); x->RTransform()->set_localScale (0.7f * Vector3::get_one()); }) ->Bind(&m_Volume) ->BuildUI(get_transform()); + XUIPrimaryButton::Make(u"Add to queue", {this, &PlayerFloatingPanel::OnPlayItPressed}) + ->OnReady([](CP_SDK::UI::Components::CPrimaryButton* x) -> void + { + x->LElement()->set_enabled (false); + x->RTransform()->set_pivot (Vector2( 1.00f, 0.00f)); + x->RTransform()->set_anchorMin (Vector2( 1.00f, 0.00f)); + x->RTransform()->set_anchorMax (Vector2( 1.00f, 0.00f)); + x->RTransform()->set_anchoredPosition(Vector2(-11.00f, 1.15f)); + x->RTransform()->set_localScale (0.7f * Vector3::get_one()); + }) + ->Bind(&m_AddToQueueButton) + ->BuildUI(get_transform()); + XUIPrimaryButton::Make(u"Play it", {this, &PlayerFloatingPanel::OnPlayItPressed}) ->OnReady([](CP_SDK::UI::Components::CPrimaryButton* x) -> void { @@ -166,6 +182,11 @@ namespace ChatPlexMod_MenuMusic::UI { auto& l_Modules = CP_SDK::ChatPlexSDK::GetModules(); if (std::count_if(l_Modules.begin(), l_Modules.end(), [](auto x) { return x->Name() == u"Audio Tweaker"; })) m_Volume->SetActive(false); + + if (!ModulePresence::ChatRequest()) + m_AddToQueueButton->SetActive(false); + + Object::Destroy(get_transform()->get_parent()->GetComponent()); } /// @brief On view activation void PlayerFloatingPanel::OnViewActivation_Impl() @@ -199,22 +220,31 @@ namespace ChatPlexMod_MenuMusic::UI { l_Str2 = l_Str2.substr(0, 50) + u"..."; if (m_SongTitle && m_SongTitle->Element()) m_SongTitle->Element()->TMProUGUI()->set_text(l_Str1); - if (m_SongArtist && m_SongTitle->Element()) m_SongArtist->Element()->TMProUGUI()->set_text(l_Str2); + if (m_SongArtist && m_SongArtist->Element()) m_SongArtist->Element()->TMProUGUI()->set_text(l_Str2); m_CancellationToken->Cancel(); if (p_Music) { - p_Music->GetCoverBytesAsync(m_CancellationToken, [this](const CP_SDK::Utils::MonoPtr>& x) -> void + auto l_CancellationToken = m_CancellationToken; + auto l_View = CP_SDK::Utils::MonoPtr(this); + + p_Music->GetCoverBytesAsync(l_CancellationToken, [l_View, l_CancellationToken](const CP_SDK::Utils::MonoPtr>& x) -> void { - Utils::ArtProvider::Prepare(x, m_CancellationToken, [this](Sprite* p_Cover, Sprite* p_Background) -> void + Utils::ArtProvider::Prepare(x, l_CancellationToken, [l_View](Sprite* p_Cover, Sprite* p_Background) -> void { - m_MusicCover->SetBackgroundSprite(p_Cover); - m_MusicBackground->SetBackgroundSprite(p_Background); + if (l_View) + { + l_View->m_MusicCover->SetBackgroundSprite(p_Cover); + l_View->m_MusicBackground->SetBackgroundSprite(p_Background); + } }); }, nullptr); if (m_PlayItButton) - m_PlayItButton->SetInteractable(p_Music->MusicProvider()->SupportPlayIt()); + { + auto l_Provider = p_Music->MusicProvider(); + m_PlayItButton->SetInteractable(l_Provider && l_Provider->SupportPlayIt()); + } } m_CurrentMusic = p_Music; @@ -272,11 +302,15 @@ namespace ChatPlexMod_MenuMusic::UI { /// @brief On play the map pressed void PlayerFloatingPanel::OnPlayItPressed() { - if (m_CurrentMusic == nullptr || !m_CurrentMusic->MusicProvider()->SupportPlayIt()) + if (m_CurrentMusic == nullptr) + return; + + auto l_Provider = m_CurrentMusic->MusicProvider(); + if (!l_Provider || !l_Provider->SupportPlayIt()) return; - if (!m_CurrentMusic->MusicProvider()->StartGameSpecificGamePlay(m_CurrentMusic)) + if (!l_Provider->StartGameSpecificGamePlay(m_CurrentMusic)) ShowMessageModal(u"Map not found!"); } -} ///< namespace ChatPlexMod_MenuMusic::UI \ No newline at end of file +} ///< namespace ChatPlexMod_MenuMusic::UI diff --git a/src/ChatPlexMod_MenuMusic/Utils/ArtProvider.cpp b/src/ChatPlexMod_MenuMusic/Utils/ArtProvider.cpp index 83bbb06..454c46f 100644 --- a/src/ChatPlexMod_MenuMusic/Utils/ArtProvider.cpp +++ b/src/ChatPlexMod_MenuMusic/Utils/ArtProvider.cpp @@ -11,14 +11,19 @@ #include #include #include +#include #include +#include + using namespace UnityEngine; namespace ChatPlexMod_MenuMusic::Utils { CP_SDK::Unity::TextureRaw::PixelArray ArtProvider::m_BackgroundMask; CP_SDK::Unity::TextureRaw::PixelArray ArtProvider::m_CoverMask; + std::once_flag ArtProvider::m_MasksInitFlag; + bool ArtProvider::m_MasksReady = false; //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// @@ -47,35 +52,50 @@ namespace ChatPlexMod_MenuMusic::Utils { CP_SDK::Misc::FastCancellationToken::Ptr p_CancellationToken, t_Callback p_Callback) { - try + std::call_once(m_MasksInitFlag, []() -> void { - auto l_StartSerial = p_CancellationToken ? p_CancellationToken->Serial() : 0; - - if (!m_BackgroundMask) - { - int l_Width, l_Height; - CP_SDK::Unity::TextureRaw::Load(Assets::BackgroundMask_png, l_Width, l_Height, &m_BackgroundMask); - } + int l_BackgroundWidth = 0; + int l_BackgroundHeight = 0; + int l_CoverWidth = 0; + int l_CoverHeight = 0; + + CP_SDK::Unity::TextureRaw::Load( + Assets::BackgroundMask_png, l_BackgroundWidth, l_BackgroundHeight, &m_BackgroundMask + ); + CP_SDK::Unity::TextureRaw::Load( + Assets::CoverMask_png, l_CoverWidth, l_CoverHeight, &m_CoverMask + ); + + m_MasksReady = true; + }); - if (!m_CoverMask) - { - int l_Width, l_Height; - CP_SDK::Unity::TextureRaw::Load(Assets::CoverMask_png, l_Width, l_Height, &m_CoverMask); - } + if (!m_MasksReady) + throw std::runtime_error("Menu Music artwork masks could not be loaded"); - if (p_CancellationToken && p_CancellationToken->IsCancelled(l_StartSerial)) + auto l_StartSerial = p_CancellationToken ? p_CancellationToken->Serial() : 0; + try + { + if (!p_RawByte || (p_CancellationToken && p_CancellationToken->IsCancelled(l_StartSerial))) return; - int l_OGWidth, l_OGHeight; + + int l_OGWidth = 0; + int l_OGHeight = 0; CP_SDK::Unity::TextureRaw::PixelArray l_OGPixels; if (!CP_SDK::Unity::TextureRaw::Load(p_RawByte.Ptr(), l_OGWidth, l_OGHeight, &l_OGPixels)) { CP_SDK::Unity::MTMainThreadInvoker::Enqueue([=]() -> void { + if (p_CancellationToken && p_CancellationToken->IsCancelled(l_StartSerial)) + return; + p_Callback(nullptr, nullptr); }); return; } + if (l_OGWidth <= 0 || l_OGHeight <= 0 || !l_OGPixels || l_OGPixels->empty()) + throw std::runtime_error("Decoded artwork contains no pixels"); + auto l_CoverSize = Vector2Int(18 * 4 * 10, 18 * 4 * 10); auto l_BackgroundSize = Vector2Int(80 * 1 * 10, 20 * 1 * 10); auto l_CoverPixels = CP_SDK::Unity::TextureRaw::ResampleAndCrop(l_OGWidth, l_OGHeight, l_OGPixels, l_CoverSize.m_X, l_CoverSize.m_Y); @@ -101,22 +121,19 @@ namespace ChatPlexMod_MenuMusic::Utils { if (CP_SDK::ChatPlexSDK::ActiveGenericScene() != CP_SDK::EGenericScene::Menu) return; - try - { - l_CoverPixelsConverted = ::Array::NewLength(l_CoverPixels->size()); - l_BackgroundPixelsConverted = ::Array::NewLength(l_BackgroundPixels->size()); + l_CoverPixelsConverted = ::Array::NewLength(l_CoverPixels->size()); + l_BackgroundPixelsConverted = ::Array::NewLength(l_BackgroundPixels->size()); - memcpy(&l_CoverPixelsConverted->_values[0], &(*l_CoverPixels.get())[0], sizeof(Color) * l_CoverPixels->size()); - memcpy(&l_BackgroundPixelsConverted->_values[0], &(*l_BackgroundPixels.get())[0], sizeof(Color) * l_BackgroundPixels->size()); - } - catch (const std::exception& l_Exception) - { - Logger::Instance->Error(u"[ChatPlexMod_MenuMusic.Utils][ArtProvider.PrepareImpl] Error:"); - Logger::Instance->Error(l_Exception); - } + memcpy(l_CoverPixelsConverted->_values, l_CoverPixels->data(), sizeof(Color) * l_CoverPixels->size()); + memcpy(l_BackgroundPixelsConverted->_values, l_BackgroundPixels->data(), sizeof(Color) * l_BackgroundPixels->size()); CP_SDK::Unity::MTMainThreadInvoker::Enqueue([=]() -> void { + auto l_CoverTexture = CP_SDK::Utils::MonoPtr(nullptr); + auto l_BackgroundTexture = CP_SDK::Utils::MonoPtr(nullptr); + auto l_CoverSprite = CP_SDK::Utils::MonoPtr(nullptr); + auto l_BackgroundSprite = CP_SDK::Utils::MonoPtr(nullptr); + try { if (CP_SDK::ChatPlexSDK::ActiveGenericScene() != CP_SDK::EGenericScene::Menu) @@ -125,23 +142,31 @@ namespace ChatPlexMod_MenuMusic::Utils { if (p_CancellationToken && p_CancellationToken->IsCancelled(l_StartSerial)) return; - auto l_CoverTexture = Texture2D::New_ctor(l_CoverSize.m_X, l_CoverSize.m_Y, TextureFormat::RGBA32, false); + l_CoverTexture = Texture2D::New_ctor(l_CoverSize.m_X, l_CoverSize.m_Y, TextureFormat::RGBA32, false); l_CoverTexture->set_wrapMode(TextureWrapMode::Clamp); l_CoverTexture->SetPixels(l_CoverPixelsConverted.Ptr()); - l_CoverTexture->Apply(true); + l_CoverTexture->Apply(false, true); - auto l_BackgroundTexture = Texture2D::New_ctor(l_BackgroundSize.m_X, l_BackgroundSize.m_Y, TextureFormat::RGBA32, false); + l_BackgroundTexture = Texture2D::New_ctor(l_BackgroundSize.m_X, l_BackgroundSize.m_Y, TextureFormat::RGBA32, false); l_BackgroundTexture->set_wrapMode(TextureWrapMode::Clamp); l_BackgroundTexture->SetPixels(l_BackgroundPixelsConverted.Ptr()); - l_BackgroundTexture->Apply(true); + l_BackgroundTexture->Apply(false, true); - p_Callback( - CP_SDK::Unity::SpriteU::CreateFromTexture(l_CoverTexture), - CP_SDK::Unity::SpriteU::CreateFromTexture(l_BackgroundTexture) - ); + l_CoverSprite = CP_SDK::Unity::SpriteU::CreateFromTexture(l_CoverTexture.Ptr()); + l_BackgroundSprite = CP_SDK::Unity::SpriteU::CreateFromTexture(l_BackgroundTexture.Ptr()); + + if (!l_CoverSprite || !l_BackgroundSprite) + throw std::runtime_error("Unable to create artwork sprites"); + + p_Callback(l_CoverSprite.Ptr(), l_BackgroundSprite.Ptr()); } catch (const std::exception& l_Exception) { + if (l_CoverSprite) Object::Destroy(l_CoverSprite.Ptr(false)); + if (l_BackgroundSprite) Object::Destroy(l_BackgroundSprite.Ptr(false)); + if (l_CoverTexture) Object::Destroy(l_CoverTexture.Ptr(false)); + if (l_BackgroundTexture) Object::Destroy(l_BackgroundTexture.Ptr(false)); + Logger::Instance->Error(u"[ChatPlexMod_MenuMusic.Utils][ArtProvider.PrepareImpl] Error:"); Logger::Instance->Error(l_Exception); } @@ -156,8 +181,11 @@ namespace ChatPlexMod_MenuMusic::Utils { } CP_SDK::Unity::MTMainThreadInvoker::Enqueue([=]() -> void { + if (p_CancellationToken && p_CancellationToken->IsCancelled(l_StartSerial)) + return; + p_Callback(nullptr, nullptr); }); } -} ///< namespace ChatPlexMod_MenuMusic::Utils \ No newline at end of file +} ///< namespace ChatPlexMod_MenuMusic::Utils