Skip to content

Commit 513753c

Browse files
committed
feat(list-fetcher): Remove second listener and prevent infinite loop for third-party level APIs
1 parent 51a7c4a commit 513753c

3 files changed

Lines changed: 68 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [4.0.6] - 2026-01-29
9+
10+
### Fixed
11+
12+
- Infinite loop causing a crash in third-party level APIs
13+
814
## [4.0.5] - 2025-12-29
915

1016
### Changed

src/listfetcher/ListFetcher.cpp

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ ListFetcher::ListFetcher()
2020

2121
matjson::Value ListFetcher::normalListCacheFunction()
2222
{
23+
auto& wrq = WebRequestQueue::get();
24+
2325
// in case something fails, all difficulties have at least 100 pages
2426
matjson::Value defaultObj{};
2527

@@ -31,7 +33,7 @@ matjson::Value ListFetcher::normalListCacheFunction()
3133
}) {
3234
defaultObj[fmt::format("{}", static_cast<int>(difficulty))] = 100;
3335

34-
WebRequestQueue::get().enqueue(WebRequestQueue::Request{
36+
wrq.enqueue(WebRequestQueue::Request{
3537
web::WebRequest()
3638
.userAgent("")
3739
.bodyString(
@@ -63,7 +65,7 @@ matjson::Value ListFetcher::normalListCacheFunction()
6365
});
6466
}
6567

66-
WebRequestQueue::get().flush();
68+
wrq.flush();
6769

6870
return defaultObj;
6971
}
@@ -185,9 +187,11 @@ void ListFetcher::getRandomDemonListLevel(geode::Result<level_pair_t>& result)
185187
const auto array = jsonResp.asArray().unwrap();
186188

187189
std::uint16_t randomIndex;
190+
std::uint8_t iter = 0;
188191
do {
189192
randomIndex = rl::utils::randomNumber(0, array.size() - 1);
190-
} while (array[randomIndex]["level_id"].isNull());
193+
iter++;
194+
} while (array[randomIndex]["level_id"].isNull() && iter < 100);
191195

192196
int levelID = array[randomIndex].template get<int>("level_id").unwrapOr(-1);
193197
if (levelID == -1)
@@ -258,9 +262,11 @@ void ListFetcher::getRandomChallengeListLevel(geode::Result<level_pair_t>& resul
258262
const auto& array = jsonResp.asArray().unwrap();
259263

260264
std::uint16_t randomIndex;
265+
std::uint8_t iter;
261266
do {
262267
randomIndex = rl::utils::randomNumber(0, array.size() - 1);
263-
} while (array[randomIndex]["level_id"].isNull());
268+
iter++;
269+
} while (array[randomIndex]["level_id"].isNull() && iter < 100);
264270

265271
int levelID = array[randomIndex].template get<int>("level_id").unwrapOr(-1);
266272
if (levelID == -1)
@@ -388,12 +394,22 @@ void ListFetcher::getLevelInfo(int levelID, geode::Result<level_pair_t>& result)
388394
{
389395
is_fetching = true;
390396

391-
m_secondary_listener.bind([&](web::WebTask::Event* e) {
392-
if (web::WebResponse* res = e->getValue())
393-
{
397+
web::WebRequest()
398+
.userAgent("")
399+
.bodyString(
400+
fmt::format("secret={}&type={}&str={}", GJ_SECRET, 0, levelID)
401+
)
402+
.post(GJ_LEVELS_URL)
403+
.listen([&](web::WebResponse* res) {
394404
rl::utils::ScopedVar v(is_fetching, true, false);
395405
rl::utils::ScopedFunc f(m_finished_fetching_cb);
396406

407+
if (!res)
408+
{
409+
result = geode::Err("Response object malformed. (getGJLevels21.php, 1)");
410+
return;
411+
}
412+
397413
const auto& respStr = res->string();
398414

399415
if (respStr.isErr())
@@ -436,22 +452,20 @@ void ListFetcher::getLevelInfo(int levelID, geode::Result<level_pair_t>& result)
436452
result = geode::Ok(level_pair_t{
437453
response.levels[0], response.creators[0]
438454
});
439-
}
440-
else if (e->isCancelled())
441-
{
455+
},
456+
[&](web::WebProgress*) {},
457+
[&] {
442458
result = geode::Err("Request was cancelled. (getGJLevels21.php, 1)");
443-
is_fetching = false;
459+
is_fetching = false;
444460

445-
m_finished_fetching_cb();
461+
m_finished_fetching_cb();
446462
}
447-
});
448-
449-
auto req = web::WebRequest()
450-
.userAgent("")
451-
.bodyString(
452-
fmt::format("secret={}&type={}&str={}", GJ_SECRET, 0, levelID)
453-
)
454-
.post(GJ_LEVELS_URL);
463+
);
464+
}
455465

456-
m_secondary_listener.setFilter(req);
466+
void ListFetcher::setFinishedFetchingCallback(std::function<void()>&& cb)
467+
{
468+
m_finished_fetching_cb = [cb = std::move(cb)] mutable {
469+
Loader::get()->queueInMainThread(std::move(cb));
470+
};
457471
}

src/listfetcher/ListFetcher.hpp

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,42 @@ class ListFetcher : public SingletonBase<ListFetcher>
2323
public:
2424
using level_pair_t = std::pair<rtrp::objects::LevelObject, rtrp::objects::CreatorObject>;
2525

26+
/**
27+
* @brief Fetches a random level from the normal list filtered by the given difficulty.
28+
*/
2629
void getRandomNormalListLevel(GJDifficulty, geode::Result<level_pair_t>&);
30+
31+
/**
32+
* @brief Fetches a random level from the demon list.
33+
*/
2734
void getRandomDemonListLevel(geode::Result<level_pair_t>&);
35+
36+
/**
37+
* @brief Fetches a random level from the community challenge list.
38+
*/
2839
void getRandomChallengeListLevel(geode::Result<level_pair_t>&);
40+
41+
/**
42+
* @brief Fetches a random level from a GD level list identified by ID.
43+
*/
2944
void getRandomGDListLevel(int, geode::Result<level_pair_t>&);
3045

46+
/**
47+
* @brief Retrieves detailed level information for a specific level ID.
48+
*/
3149
void getLevelInfo(int, geode::Result<level_pair_t>&);
3250

33-
void setFinishedFetchingCallback(std::function<void()>&& cb) { m_finished_fetching_cb = std::move(cb); }
51+
/**
52+
* @brief Registers a callback invoked once the current fetch finishes.
53+
*/
54+
void setFinishedFetchingCallback(std::function<void()>&& cb);
3455

56+
/**
57+
* @brief Provides a cached response payload for the normal list endpoint.
58+
*/
3559
static matjson::Value normalListCacheFunction();
3660

61+
/// Indicates whether a web fetch is currently running.
3762
std::atomic_bool is_fetching;
3863

3964
private:
@@ -57,9 +82,8 @@ class ListFetcher : public SingletonBase<ListFetcher>
5782
std::uint64_t m_cached_gd_list_id;
5883
std::vector<std::string> m_cached_gd_list_level_ids;
5984

85+
6086
std::function<void()> m_finished_fetching_cb;
6187

6288
geode::EventListener<geode::utils::web::WebTask> m_main_listener;
63-
// used when fetching demonlist/challengelist/gdlist
64-
geode::EventListener<geode::utils::web::WebTask> m_secondary_listener;
6589
};

0 commit comments

Comments
 (0)