Compare commits

..

4 Commits

Author SHA1 Message Date
Ethan O'Brien
50ccecd821 New asset hashes again 2026-07-19 22:50:03 -05:00
Ethan O'Brien
e41d4e61b9 add ios asset hash 2026-07-19 20:57:00 -05:00
Ethan O'Brien
fcaecc7b41 Push new asset hashes for windows/android 2026-07-19 18:49:33 -05:00
Ethan O'Brien
0bf1262b5b fix clear rate deletion bug 2026-07-19 15:49:08 -05:00
4 changed files with 116 additions and 10 deletions

View File

@@ -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::<i64>().unwrap_or(0);
@@ -252,7 +255,7 @@ pub fn dead_music_ids(candidates: &JsonValue) -> JsonValue {
let mut ids: Vec<i64> = 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);
}
}

View File

@@ -28,14 +28,13 @@ static ASSET_VERSIONS: &[AssetVersion] = &[
AssetVersion { region: "GL", platform: "Android", version: "5260ff15dff8ba0c00ad91400f515f55", hash: "d210b28037885f3ef56b8f8aa45ac95b", latest: true },
AssetVersion { region: "GL", platform: "iOS", version: "5260ff15dff8ba0c00ad91400f515f55", hash: "dd7175e4bcdab476f38c33c7f34b5e4d", latest: true },
// Re-written client versions 2.0.0 - 2.1.2
// Re-written client versions 2.0.0 - 2.1.2 (windows only)
AssetVersion { region: "JP", platform: "Windows", version: "4c921d2443335e574a82e04ec9ea243c", hash: "4ed1d077df2d1b29e17d25d64fb37242", latest: false },
// Re-written client versions 2.2.0 -
AssetVersion { region: "JP", platform: "Windows", version: "ced44f266b4e4c8eb05fe417fd5f3d1b", hash: "52a35644bacaefe8f0ae71a1f4d1c217", latest: true },
// For the unity editor
AssetVersion { region: "JP", platform: "Android", version: "ced44f266b4e4c8eb05fe417fd5f3d1b", hash: "0f09d2da7580a6e7dfe44b6094a0e433", latest: false },
AssetVersion { region: "JP", platform: "Windows", version: "ced44f266b4e4c8eb05fe417fd5f3d1b", hash: "765a689bd642f3c4545d8d8e69c57d3e", latest: true },
AssetVersion { region: "JP", platform: "Android", version: "ced44f266b4e4c8eb05fe417fd5f3d1b", hash: "5ba946aef8cc3fbf95867dc465cad336", latest: false },
AssetVersion { region: "JP", platform: "iOS", version: "ced44f266b4e4c8eb05fe417fd5f3d1b", hash: "6cfbebbe32a5cd9391f4b44d1a384f62", latest: false },
//AssetVersion { region: "JP", platform: "WebGL", version: "4c921d2443335e574a82e04ec9ea243c", hash: "e1ff7c74b20c8d216507972b6f24b9df", latest: true },
];

View File

@@ -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<i32> {
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);
}
}

View File

@@ -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);