-
-
Notifications
You must be signed in to change notification settings - Fork 15
Revert back to il2cpp threads and update to fixed dependency #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughSwitches async launcher from StartThread to StartTask across multiple C++ sources handling media loading, entitlement checks, level downloads, and platform hooks. Updates dependency versions and public/restored dependency metadata in qpm config files, adds $schema to qpm.shared.json, and remaps several dependencies with revised build flags and links. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Game
participant Hook as PlatformAuthTokenProvider Hook
participant Task as StartTask<T>
participant Provider as Original Provider
participant Store as Platform API
Game->>Hook: GetAuthenticationToken()
Note over Hook: Intercept call
Hook->>Task: StartTask<AuthenticationToken>(lambda)
rect rgba(230,243,255,0.6)
note right of Task: Async execution (replaces StartThread)
Task->>Provider: GetAuthenticationToken()
Provider->>Store: Fetch platform token
Store-->>Provider: Token/Result
Provider-->>Task: AuthenticationToken
Task-->>Hook: ResultOnSuccess/Failure
end
alt success
Hook->>Game: Completed Task (mapped platform if needed)
else failure/fallback
Hook->>Game: Fallback token task
end
sequenceDiagram
autonumber
actor UI
participant Preview as BeatSaverPreviewMediaData
participant Task as StartTask<T>
participant API as BeatSaver++
participant Unity as Main Thread Loader
UI->>Preview: GetCoverSpriteAsync()
Preview->>Task: StartTask<Sprite*>(lambda)
Task->>API: Fetch map + version by hash
API-->>Task: Cover image bytes
Task->>Unity: Load Sprite on main thread
Unity-->>Task: Sprite
Task-->>Preview: Task<Sprite*>
Preview-->>UI: Return Task
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
src/Hooks/ProvideBeatsaverLevelHooks.cpp (2)
28-33: Respect cancellation and guard against faulted/canceled base task before dereferencing Result.
- The hook receives a CancellationToken but it isn’t passed to StartTask, and the polling loops don’t check for cancellation.
- ‘t->Result’ is read even if ‘t’ was canceled/faulted.
Apply this diff to propagate cancelToken and add guards:
- return MultiplayerCore::StartTask<bool>([=](){ + return MultiplayerCore::StartTask<bool>([=](::System::Threading::CancellationToken token){ using namespace std::chrono_literals; - while (!(t->IsCompleted || t->IsCanceled)) std::this_thread::sleep_for(50ms); - // if orig says it's available, cool beans - if (t->Result) return true; + while (!(t->IsCompleted || t->IsCanceled)) { + if (token.get_IsCancellationRequested()) return false; + std::this_thread::sleep_for(50ms); + } + // if orig says it's available, cool beans + if (t->IsFaulted || t->IsCanceled) return false; + if (t->Result) return true; @@ - }); + }, std::forward<::System::Threading::CancellationToken>(cancelToken));
19-22: Root the captured managed Task to prevent GC collectionAfter inspecting
ProvideBeatsaverLevelHooks.cppandtasks.hpp, it’s clear that the hook’s lambda captures the raw managed pointert(the originalTask<bool>) without any GC rooting. Since the hook returns a newTask<bool>to the managed side, there are no remaining managed references to the originalt, allowing the GC to collect it while the background thread is still polling it—potentially leading to sporadic crashes when callingt->IsCompletedor readingt->Result.To fix this, you must ensure the original
Task<bool>* tremains rooted for the lifetime of the polling logic. Two approaches are acceptable:• Use a SafePtr wrapper around the original task pointer to pin it in the GC heap. For example:
auto t = BeatmapLevelsModel_CheckBeatmapLevelDataExistsAsync(self, levelID, beatmapLevelDataVersion, cancelToken); // root the original Task<bool> MultiplayerCore::SafePtr<System::Threading::Tasks::Task_1<bool>> safeT{t}; // … return MultiplayerCore::StartTask<bool>([=](){ auto* rootedT = safeT.get(); // safely rooted pointer using namespace std::chrono_literals; while (!(rootedT->IsCompleted || rootedT->IsCanceled)) std::this_thread::sleep_for(50ms); if (rootedT->Result) return true; // … });• Alternatively, leverage the managed Task’s continuation API instead of manual polling. For example:
auto t = BeatmapLevelsModel_CheckBeatmapLevelDataExistsAsync(self, levelID, beatmapLevelDataVersion, cancelToken); // return a continuation directly on ‘t’ return t->ContinueWith([](System::Threading::Tasks::Task_1<bool>* completed) { if (completed->Result) return true; // perform Beatsaver lookup and download… });Both approaches ensure the original task object remains alive until your custom logic completes.
Relevant location:
src/Hooks/ProvideBeatsaverLevelHooks.cpp, lines ~19–22, whereauto t = …is declared and captured.src/Objects/MpEntitlementChecker.cpp (1)
84-105: Async captures ‘this’ without lifetime guarantees; risk of UAF during shutdown.The lambda captures ‘this’ and updates members after waiting on baseTask. If the checker is destroyed (e.g., scene change) before the lambda runs, dereferencing ‘this’ yields UAF.
Mitigations:
- Capture a SafePtr/Weak pointer to the checker and early-return if it expired.
- Consider adding a CancellationTokenSource owned by the checker, cancel it in OnDestroy_override, and pass its token to StartTask so pending work exits.
Example pattern (sketch; adjust to your SafePtr utilities):
- auto task = StartTask<GlobalNamespace::EntitlementsStatus>([this, baseTask, levelId](){ + auto self = SafePtr(this); + auto task = StartTask<GlobalNamespace::EntitlementsStatus>([self, baseTask, levelId](){ using namespace std::chrono_literals; + if (!self) return GlobalNamespace::EntitlementsStatus::Unknown; @@ - _entitlementsDictionary[_sessionManager->localPlayer->userId][levelId] = entitlement; + self->_entitlementsDictionary[self->_sessionManager->localPlayer->userId][levelId] = entitlement; return entitlement; });If you prefer cancellation:
- Add a field CancellationTokenSource _cts; cancel it in OnDestroy_override; pass _cts.Token to StartTask and check token inside the lambda loops.
src/Objects/MpLevelLoader.cpp (2)
148-176: Cancellation token is passed but unused; honor it to prevent long-running work after abort.You accept a CancellationToken and spawn a cancellation-aware StartTask, but the lambda doesn’t check ‘token’. Add early-outs before and after blocking calls.
- return StartTask<GlobalNamespace::LoadBeatmapLevelDataResult>([this, levelId](CancellationToken token) -> GlobalNamespace::LoadBeatmapLevelDataResult { + return StartTask<GlobalNamespace::LoadBeatmapLevelDataResult>([this, levelId](CancellationToken token) -> GlobalNamespace::LoadBeatmapLevelDataResult { try { + if (token.get_IsCancellationRequested()) { + DEBUG("Download for {} canceled before start", levelId); + return ::GlobalNamespace::LoadBeatmapLevelDataResult::getStaticF_Error(); + } auto success = _levelDownloader->TryDownloadLevelAsync(levelId, std::bind(&MpLevelLoader::Report, this, std::placeholders::_1)).get(); + if (token.get_IsCancellationRequested()) { + DEBUG("Download for {} canceled after TryDownloadLevelAsync", levelId); + return ::GlobalNamespace::LoadBeatmapLevelDataResult::getStaticF_Error(); + } if (!success) { DEBUG("Failed to download level"); @@ auto level = _runtimeSongLoader->GetLevelByLevelID(levelId); if (!level) { DEBUG("Couldn't get level by id"); return ::GlobalNamespace::LoadBeatmapLevelDataResult::getStaticF_Error(); } if (!level->beatmapLevelData) { DEBUG("level data is null!"); return ::GlobalNamespace::LoadBeatmapLevelDataResult::getStaticF_Error(); } } catch(std::exception const& e) { ERROR("Caught error during beatmap level download: {}, what: {}", typeid(e).name(), e.what()); } return ::GlobalNamespace::LoadBeatmapLevelDataResult::getStaticF_Error(); }, std::forward<CancellationToken>(cancellationToken));
128-133: Bug: requirement check early-returns on has_value(); condition is inverted.If ‘diffDataOpt’ has a value, we should analyze requirements; the current code returns immediately, making the block below unreachable.
- if (diffDataOpt.has_value()) return; + if (!diffDataOpt.has_value()) return; - auto& diffData = diffDataOpt->get(); + auto& diffData = diffDataOpt->get();src/Beatmaps/BeatSaverPreviewMediaData.cpp (1)
73-101: Check UnityWebRequest result and dispose to avoid leaks.After SendWebRequest completes, validate the request result and dispose the handle once content is extracted.
auto webRequest = UnityEngine::Networking::UnityWebRequestMultimedia::GetAudioClip(v->GetPreviewURL(), UnityEngine::AudioType::MPEG); auto www = webRequest->SendWebRequest(); - while (!www->get_isDone()) std::this_thread::sleep_for(std::chrono::milliseconds(100)); + while (!www->get_isDone()) std::this_thread::sleep_for(std::chrono::milliseconds(100)); + // Validate request succeeded + if (webRequest->get_result() != UnityEngine::Networking::UnityWebRequest::Result::Success) { + return (UnityEngine::AudioClip*)nullptr; + } @@ - BSML::MainThreadScheduler::Schedule([webRequest, &result](){ - result = UnityEngine::Networking::DownloadHandlerAudioClip::GetContent(webRequest); + BSML::MainThreadScheduler::Schedule([webRequest, &result](){ + result = UnityEngine::Networking::DownloadHandlerAudioClip::GetContent(webRequest); + // Dispose on main thread after extracting content to free native resources + webRequest->Dispose(); });Also consider using Task_1UnityEngine::AudioClip*::FromResult(_cachedAudioClip) for consistency with GetCoverSpriteAsync’s FromResult usage.
src/Hooks/PlatformAuthenticationTokenProviderHooks.cpp (2)
36-84: Avoid UAF and data races in async token hook; snapshot self fields, make flags atomic, and bound the wait.The lambda runs on a background il2cpp-aware thread and captures
selfby value (raw pointer). If the provider is destroyed before the lambda completes, this can crash. Also,userEntitledandentitltiementCheckRanare written from the Oculus callback and read on this thread without synchronization (data race), and the wait can spin forever if callbacks aren’t pumped.Apply this diff to snapshot fields, fix the racy flags, and bound the wait (5s):
- return MultiplayerCore::StartTask<GlobalNamespace::AuthenticationToken>([=](){ + // Snapshot fields before crossing the thread boundary to avoid UAF on `self` + auto platform_snapshot = self->_platform; + auto userId_snapshot = self->_userId; + auto userName_snapshot = self->_userName; + return MultiplayerCore::StartTask<GlobalNamespace::AuthenticationToken>([=](){ using namespace std::chrono_literals; while (!(t->IsCompleted || t->IsCanceled)) std::this_thread::sleep_for(50ms); GlobalNamespace::AuthenticationToken token = t->ResultOnSuccess; bool hasToken = !System::String::IsNullOrEmpty(token.sessionToken); if (token && hasToken) { INFO("Successfully got auth token, returning it!"); if (token.platform == GlobalNamespace::AuthenticationToken::Platform::Oculus) token.platform = GlobalNamespace::AuthenticationToken::Platform::OculusQuest; // Makes sure platform is set to OculusQuest return token; } - else if (!hasToken && self->_platform.value__ != 20) ERROR("Session token is null or empty!!! Either they don't own the game or modded games no longer have access to tokens"); + else if (!hasToken && platform_snapshot.value__ != 20) ERROR("Session token is null or empty!!! Either they don't own the game or modded games no longer have access to tokens"); WARNING("Failed to get auth token, returning custom authentication token!"); // Check users entitlement to the app - bool userEntitled = true; - if (self->_platform.value__ != 20) + std::atomic_bool userEntitled{true}; + if (platform_snapshot.value__ != 20) { - bool entitltiementCheckRan = false; + std::atomic_bool entitlementCheckRan{false}; INFO("Checking entitlement to the app"); // auto request = Oculus::Platform::Entitlements::IsUserEntitledToApplication(); auto request = Oculus::Platform::Request::New_ctor( Oculus::Platform::CAPI::ovr_Entitlement_GetIsViewerEntitled()); request->OnComplete(custom_types::MakeDelegate<Oculus::Platform::Message::Callback*>( std::function<void(Oculus::Platform::Message*)>([&](Oculus::Platform::Message* msg) { if (msg->IsError) { ERROR("User is not entitled to the app: {}", msg->GetError()->Message); - userEntitled = false; + userEntitled.store(false, std::memory_order_relaxed); } else { INFO("User is entitled to the app, returning custom authentication token!"); // userEntitled = true; } - entitltiementCheckRan = true; + entitlementCheckRan.store(true, std::memory_order_release); } ))); - while (!entitltiementCheckRan) std::this_thread::sleep_for(50ms); + // Bound wait to avoid indefinite hang if callbacks aren't pumped + for (int i = 0; i < 100 && !entitlementCheckRan.load(std::memory_order_acquire); ++i) { + std::this_thread::sleep_for(50ms); + } } return GlobalNamespace::AuthenticationToken( - self->_platform == GlobalNamespace::AuthenticationToken::Platform::Oculus ? GlobalNamespace::AuthenticationToken::Platform::OculusQuest : self->_platform, - self->_userId, // Important for server and client to keep track of players, should not be messed with if possible - self->_userName, - userEntitled ? dummy_auth : nullptr + platform_snapshot == GlobalNamespace::AuthenticationToken::Platform::Oculus ? GlobalNamespace::AuthenticationToken::Platform::OculusQuest : platform_snapshot, + userId_snapshot, // Important for server and client to keep track of players, should not be messed with if possible + userName_snapshot, + userEntitled.load(std::memory_order_relaxed) ? dummy_auth : nullptr ); });Follow-ups:
- Consider replacing literal
20with a named constant for Pico (see separate nit).- If entitlement callbacks require a pump, consider scheduling the entitlement check differently or explicitly documenting where callbacks are pumped. I can help wire a pump into a central tick if needed.
109-137: Fix uninitialized pointer on fault path and propagate cancellation to StartTask.
infois used uninitialized whent->IsFaultedis true, and cancellation fromctxis ignored. Both can cause crashes or hangs.Apply this diff:
- try { - return MultiplayerCore::StartTask<GlobalNamespace::UserInfo*>([=](){ + try { + return MultiplayerCore::StartTask<GlobalNamespace::UserInfo*>([=](){ using namespace std::chrono_literals; DEBUG("Start UserInfoTask"); - while (!(t->IsCompleted || t->IsCanceled)) std::this_thread::sleep_for(50ms); - GlobalNamespace::UserInfo* info; - if (!t->IsFaulted) { - info = t->Result; - } else ERROR("UserInfo Task faulted"); + while (!(t->IsCompleted || t->IsCanceled || t->IsFaulted)) std::this_thread::sleep_for(50ms); + if (t->IsCanceled) { + DEBUG("UserInfo Task canceled"); + return static_cast<GlobalNamespace::UserInfo*>(nullptr); + } + if (t->IsFaulted) { + ERROR("UserInfo Task faulted"); + return static_cast<GlobalNamespace::UserInfo*>(nullptr); + } + GlobalNamespace::UserInfo* info = t->Result; if (info) { // INFO("Successfully got user info, returning it!"); // if (isPico.load()) // { INFO("User is on Pico, changing platform to 20"); info->platform = GlobalNamespace::UserInfo::Platform(20); // } } else { ERROR("UserInfo null!"); } return info; - }); + }, std::move(ctx)); } catch (const std::exception& ex) { ERROR("Could not Start Task: {}", ex.what()); return t; }This prevents undefined behavior and respects the caller’s cancellation token.
🧹 Nitpick comments (8)
src/Hooks/ProvideBeatsaverLevelHooks.cpp (1)
40-44: Handle download failure signal and support cancellation during wait.The callback parameter indicates success but is ignored. If download fails, we still refresh and proceed. Also, the wait loop should honor cancellation.
- bool downloaded = false; - v.DownloadBeatmapAsync(*beatmapRes.responseData, [&downloaded](bool v){ - downloaded = true; - }); - while (!downloaded) std::this_thread::sleep_for(50ms); + bool downloaded = false; + bool success = false; + v.DownloadBeatmapAsync(*beatmapRes.responseData, [&downloaded, &success](bool ok){ + success = ok; + downloaded = true; + }); + while (!downloaded) { + using namespace std::chrono_literals; + // If you applied the cancellation-aware StartTask overload: + // if (token.get_IsCancellationRequested()) return false; + std::this_thread::sleep_for(50ms); + } + if (!success) return false;src/Objects/MpEntitlementChecker.cpp (1)
90-96: Minor: base task fault/cancel handling is covered; consider a timeout or retry.You already guard IsFaulted and propagate NotOwned. If intermittent failures are expected, consider a simple bounded retry (1–2 attempts) or a timeout to avoid indefinite waits.
src/Beatmaps/BeatSaverPreviewMediaData.cpp (1)
32-61: Consider cancellation and avoiding indefinite busy-wait for cover load.
- StartTask overload with CancellationToken isn’t used; no way to cancel while polling external I/O.
- The spin-wait on an atomic pointer can block for long periods without an exit condition.
Options:
- Thread cancellation: add a CancellationTokenSource to this instance, pass its token to StartTask, and check token in the wait loop.
- Replace busy-wait with a simple condition_variable or a completed flag with periodic cancellation checks.
If you keep the busy-wait, at least cap the total wait time and early-return nullptr on timeout.
src/Hooks/PlatformAuthenticationTokenProviderHooks.cpp (2)
20-25: Include missing headers used in this TU.
std::search,std::tolower,std::this_thread::sleep_for, and chrono literals are used but<algorithm>,<cctype>,<thread>, and<chrono>are not included here. Relying on transitive includes is brittle.Add these includes near the top of this file:
#include <algorithm> #include <cctype> #include <thread> #include <chrono>
34-34: Replace magic number 20 with a named constant for Pico platform.Improves readability and avoids scattering literals.
Add once (outside this function):
constexpr auto kPicoPlatform = static_cast<GlobalNamespace::AuthenticationToken::Platform>(20);Then replace:
GlobalNamespace::AuthenticationToken::Platform(20)withkPicoPlatformself->_platform.value__ != 20withself->_platform != kPicoPlatforminfo->platform = GlobalNamespace::UserInfo::Platform(20);withinfo->platform = static_cast<decltype(info->platform)>(kPicoPlatform);Also applies to: 126-127
qpm.shared.json (3)
216-232: Prefer includePaths over systemIncludes for fmt to be consistent.Small cleanup for consistency with other deps using
includePaths.Proposed tweak:
- "compileOptions": { - "systemIncludes": [ - "fmt/include/" - ], - "cppFlags": [ - "-DFMT_HEADER_ONLY" - ] - } + "compileOptions": { + "includePaths": [ + "fmt/include" + ], + "cppFlags": [ + "-DFMT_HEADER_ONLY" + ] + }
262-275: libil2cpp: considerincludePathsinstead ofsystemIncludes.Not required, but aligning with other entries simplifies mental model and avoids implicit “system” semantics.
Suggested change:
- "compileOptions": { - "systemIncludes": [ - "il2cpp/external/baselib/Include", - "il2cpp/external/baselib/Platforms/Android/Include" - ] - } + "compileOptions": { + "includePaths": [ + "il2cpp/external/baselib/Include", + "il2cpp/external/baselib/Platforms/Android/Include" + ] + }
328-344: paper2_scotland2: unify to includePaths.Same consistency nit as above.
Proposed change:
- "compileOptions": { - "systemIncludes": [ - "shared/utfcpp/source" - ] - }, + "compileOptions": { + "includePaths": [ + "shared/utfcpp/source" + ] + },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
qpm.json(2 hunks)qpm.shared.json(6 hunks)src/Beatmaps/BeatSaverPreviewMediaData.cpp(2 hunks)src/Hooks/PlatformAuthenticationTokenProviderHooks.cpp(2 hunks)src/Hooks/ProvideBeatsaverLevelHooks.cpp(1 hunks)src/Objects/MpEntitlementChecker.cpp(1 hunks)src/Objects/MpLevelLoader.cpp(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/Beatmaps/BeatSaverPreviewMediaData.cpp (1)
include/tasks.hpp (4)
StartTask(38-42)StartTask(38-38)StartTask(63-67)StartTask(63-63)
src/Objects/MpEntitlementChecker.cpp (1)
include/tasks.hpp (4)
StartTask(38-42)StartTask(38-38)StartTask(63-67)StartTask(63-63)
src/Hooks/ProvideBeatsaverLevelHooks.cpp (1)
include/tasks.hpp (4)
StartTask(38-42)StartTask(38-38)StartTask(63-67)StartTask(63-63)
src/Objects/MpLevelLoader.cpp (1)
include/tasks.hpp (4)
StartTask(38-42)StartTask(38-38)StartTask(63-67)StartTask(63-63)
src/Hooks/PlatformAuthenticationTokenProviderHooks.cpp (1)
include/tasks.hpp (4)
StartTask(38-42)StartTask(38-38)StartTask(63-67)StartTask(63-63)
🔇 Additional comments (6)
qpm.json (1)
121-123: No dependency drift or conflicts detected
- beatsaverplusplus is set to ^0.2.2 in both qpm.json and qpm.shared.json.
- web-utils appears in both files at version ^0.6.7.
- sombrero is not referenced in either file.
All checks pass—no misalignment or conflicting duplicate entries.
qpm.shared.json (5)
2-2: Nice: adding $schema improves editor validation and DX.Schema hinting is a good addition.
122-124: Dependency bump aligned: beatsaverplusplus ^0.2.2.Top-level versionRange now matches the pinned restored version (0.2.2). Good for determinism.
162-183: bs-cordl include paths look good; flags migrated to includePaths.The move to
compileOptions.includePathsis consistent and helpful.
136-144: Sombrero is transitive-only – no action requiredVerification shows that
sombrerois not listed underconfig.dependenciesand there are no direct includes or namespace usages in the codebase, so it’s correctly pinned as a transitive dependency only.
146-158: Refine artifact link health check to target only soLink/debugSoLink/modLinkThe previous script attempted to curl every key in
additionalData, causing false positives on non-URL fields (e.g.overrideSoName,branchName,cmake). Since we only need to verify the actual artifact URLs, let’s limit the check tosoLink,debugSoLink, andmodLink. All of these currently return HTTP 200 when filtered correctly, so no broken pipelines are expected.Use this updated snippet against
qpm.shared.json(it covers lines 146–158, 186–198, 200–213, 247–259, 277–288, 377–390, and 392–405):#!/usr/bin/env bash set -euo pipefail # Extract only the artifact URLs and verify each resolves with HTTP 200 jq -r ' .restoredDependencies[] | .dependency.additionalData | to_entries[] | select(.key == "soLink" or .key == "debugSoLink" or .key == "modLink") | "\(.key) \(.value)" ' qpm.shared.json \ | while read -r kind url; do echo "Checking $kind: $url" if curl -fsSL -o /dev/null -w "%{http_code}" -L "$url" | grep -q '^200$'; then echo " -> OK" else echo " !! FAILED to fetch $kind: $url" exit 1 fi done echo "All artifact links resolved successfully."– Applies across all restored dependencies in
qpm.shared.json(including the original line ranges).
– Confirms that everysoLink,debugSoLink, andmodLinkreturns HTTP 200.
|
@coderabbitai is there any instance left where StartThread is still called? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain@michael-r-elp Based on my search of the codebase, no instances of The search results show no active
The migration from thread-based to task-based asynchronous operations is complete throughout the codebase. 🏁 Script executed: Length of output: 1192 @michael-r-elp Perfect! The search results confirm that no What the search found are only the function definitions in
These are the actual implementation/declaration of the The conversion from 🐰 |
|
Great, looks good to me, will merge |
This should fix crashes, hopefully
Summary by CodeRabbit
Chores
Refactor