From 0bf1262b5b19dd33f729bf58c58ef589eed152f7 Mon Sep 17 00:00:00 2001 From: Ethan O'Brien Date: Sun, 19 Jul 2026 15:49:08 -0500 Subject: [PATCH] fix clear rate deletion bug --- src/database/custom_song.rs | 7 ++- src/router/live.rs | 101 ++++++++++++++++++++++++++++++++++++ src/router/userdata/mod.rs | 9 ++-- 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/database/custom_song.rs b/src/database/custom_song.rs index 10fb551..f7ea74b 100644 --- a/src/database/custom_song.rs +++ b/src/database/custom_song.rs @@ -95,8 +95,11 @@ pub fn bump_revision() { // + 異次元フェス) along with bgm 310090xx/320090xx, so the custom range gets // the 5-digit space to itself (10000 -> bgm 31010000/32010000). Ids are never // reused after a delete, so a client's cached copy of a dead id can't get -// confused with a new upload +// confused with a new upload. Stock live ids (master_live_id) are 7-digit +// (>= 1_000_000), so the custom range stops below them - a stock clear must +// never be mistaken for a deleted custom song pub const FIRST_MUSIC_ID: i64 = 10000; +pub const LAST_MUSIC_ID: i64 = 999_999; pub fn next_music_id() -> i64 { let issued = DATABASE.lock_and_select("SELECT last_music_id FROM revision WHERE id=1", params!()).unwrap_or_default().parse::().unwrap_or(0); @@ -252,7 +255,7 @@ pub fn dead_music_ids(candidates: &JsonValue) -> JsonValue { let mut ids: Vec = Vec::new(); for id in candidates.members() { let Some(id) = id.as_i64() else { continue; }; - if id >= FIRST_MUSIC_ID && !ids.contains(&id) { + if id >= FIRST_MUSIC_ID && id <= LAST_MUSIC_ID && !ids.contains(&id) { ids.push(id); } } diff --git a/src/router/live.rs b/src/router/live.rs index d3a54de..7e0592c 100644 --- a/src/router/live.rs +++ b/src/router/live.rs @@ -761,3 +761,104 @@ async fn end(req: HttpRequest, body: String) -> impl Responder { async fn skip(req: HttpRequest, body: String) -> impl Responder { global::api(&req, Some(live_end(&req, &body, true))) } + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::test::TestRequest; + + const STOCK_LIVE_ID: i64 = 1100101; + + fn register_account(token: &str) { + let mut user = userdata::get_acc(token); + user["tutorial_step"] = 130.into(); + userdata::save_acc(token, user); + } + + fn record_clear(token: &str, live_id: i64, level: i64, score: i64, combo: i64) { + let mut user = userdata::get_acc(token); + update_live_data(&mut user, &object!{ + master_live_id: live_id, + level: level, + live_score: { score: score, max_combo: combo } + }, true); + userdata::save_acc(token, user); + } + + fn pulled_clear_count(token: &str, live_id: i64) -> i64 { + get_clear_count(live_id, &userdata::get_acc(token)) + } + + #[test] + fn clear_count_persists_with_custom_songs_disabled() { + let _lock = crate::runtime::lock_test_data_path(); + crate::runtime::set_enable_custom_songs(false); + + let token = "cc_control_disabled"; + register_account(token); + + record_clear(token, STOCK_LIVE_ID, 4, 500000, 320); + assert_eq!(pulled_clear_count(token, STOCK_LIVE_ID), 1); + + record_clear(token, STOCK_LIVE_ID, 4, 480000, 300); + assert_eq!(pulled_clear_count(token, STOCK_LIVE_ID), 2); + + let user = userdata::get_acc(token); + let live = user["live_list"].members().find(|l| l["master_live_id"] == STOCK_LIVE_ID).unwrap(); + assert_eq!(live["clear_count"], 2); + assert_eq!(live["high_score"], 500000); + } + + #[test] + fn clear_count_survives_custom_songs_enabled() { + let _lock = crate::runtime::lock_test_data_path(); + crate::runtime::set_enable_custom_songs(true); + + let token = "cc_custom_enabled"; + register_account(token); + + record_clear(token, STOCK_LIVE_ID, 4, 500000, 320); + assert_eq!(pulled_clear_count(token, STOCK_LIVE_ID), 1); + } + + #[test] + fn custom_song_cleanup_keeps_stock_lives() { + let _lock = crate::runtime::lock_test_data_path(); + crate::runtime::set_enable_custom_songs(true); + + let token = "cc_mixed_ids"; + register_account(token); + + record_clear(token, STOCK_LIVE_ID, 4, 500000, 320); + record_clear(token, crate::database::custom_song::FIRST_MUSIC_ID, 1, 100, 10); + + let user = userdata::get_acc(token); + assert_eq!(get_clear_count(STOCK_LIVE_ID, &user), 1); + } + + fn asset_gate(version: &str, platform: &str, hash: &str) -> Option { + let mut rb = TestRequest::default() + .insert_header(("aoharu-asset-version", version.to_string())) + .insert_header(("aoharu-platform", platform.to_string())); + if !hash.is_empty() { + rb = rb.insert_header(("aoharu-asset-hash", hash.to_string())); + } + let req = rb.to_http_request(); + global::check_asset_headers(req.headers(), true) + } + + #[test] + fn asset_gate_is_platform_conditional() { + let _lock = crate::runtime::lock_test_data_path(); + let current = "ced44f266b4e4c8eb05fe417fd5f3d1b"; + let stock = "4c921d2443335e574a82e04ec9ea243c"; + + assert_eq!(asset_gate(current, "Windows", ""), None); + assert_eq!(asset_gate(current, "android", ""), None); + assert_eq!(asset_gate(current, "ios", ""), Some(global::RESULT_GAME_VERSION_UPDATED)); + + assert_eq!(asset_gate(stock, "android", ""), None); + assert_eq!(asset_gate(stock, "ios", ""), None); + assert_eq!(asset_gate(stock, "Windows", ""), None); + } +} diff --git a/src/router/userdata/mod.rs b/src/router/userdata/mod.rs index fe96450..806d94e 100644 --- a/src/router/userdata/mod.rs +++ b/src/router/userdata/mod.rs @@ -249,7 +249,7 @@ fn remove_deleted_custom_songs(user: &mut JsonValue) -> bool { for key in ["live_list", "live_mission_list"] { for data in user[key].members() { let id = data["master_live_id"].as_i64().unwrap_or(0); - if id >= custom_song::FIRST_MUSIC_ID && !candidates.contains(id) { + if id >= custom_song::FIRST_MUSIC_ID && id <= custom_song::LAST_MUSIC_ID && !candidates.contains(id) { candidates.push(id).unwrap(); } } @@ -672,7 +672,10 @@ mod tests { let private_id = custom_song::next_music_id(); custom_song::insert_song(private_id, 1, &object!{music_id: private_id}, "private", &array![], false); - for id in [1001, deleted_id, private_id] { + // 1100101 is a real stock live-id shape (7-digit, >= FIRST_MUSIC_ID): it + // must survive the custom-song wipe, unlike an unrealistic sub-10000 id + let stock_id = 1100101; + for id in [stock_id, deleted_id, private_id] { user["live_list"].push(object!{ master_live_id: id, level: 4, @@ -699,7 +702,7 @@ mod tests { assert!(!user["live_list"].members().any(|data| data["master_live_id"] == deleted_id)); assert!(!user["live_mission_list"].members().any(|data| data["master_live_id"] == deleted_id)); // Official records are untouchable, invisible-but-alive songs survive - assert!(user["live_list"].members().any(|data| data["master_live_id"] == 1001)); + assert!(user["live_list"].members().any(|data| data["master_live_id"] == stock_id)); assert!(user["live_list"].members().any(|data| data["master_live_id"] == private_id)); assert_eq!(user["live_list"].len(), 2); assert_eq!(user["live_mission_list"].len(), 2);