mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 04:35:16 +08:00
feat: add official resource sync pipeline
Add the production-facing official update service and bat-official-sync watch CLI for unattended resource synchronization. Support launcher-resource discovery without installing the launcher, remote marker snapshots, local manifest audit and repair, official seed hash validation, bootstrap caching, richer Addressables coverage, SQLite resource persistence, and FFI JSON helpers.
This commit is contained in:
@@ -3,8 +3,8 @@ use bat_adapters::official::inventory::{
|
||||
YostarJpPlatformCatalogInventory, YostarJpPlatformDownloadInventory,
|
||||
};
|
||||
use bat_adapters::official::yostar_jp::{
|
||||
is_official_yostar_jp_url, verified_official_platforms, PatchPlatform, YostarJpResourceEndpointKind,
|
||||
YostarJpResourceDiscoveryPlan, YostarJpServerInfo,
|
||||
is_official_yostar_jp_url, verified_official_platforms, PatchPlatform,
|
||||
YostarJpResourceDiscoveryPlan, YostarJpResourceEndpointKind, YostarJpServerInfo,
|
||||
};
|
||||
use bat_infrastructure::{
|
||||
build_official_pull_plan_from_platform_inventory, OfficialGameMainConfigBootstrapService,
|
||||
@@ -40,8 +40,14 @@ fn fetch_bootstrap_uses_official_manifest_source_for_latest_zip() {
|
||||
let harness = TestHarness::new();
|
||||
let bootstrap = harness.fetch_bootstrap();
|
||||
|
||||
assert_eq!(bootstrap.game_config.game_latest_version, TEST_LAUNCHER_LATEST_VERSION);
|
||||
assert_eq!(bootstrap.game_config.game_latest_file_path, TEST_LAUNCHER_LATEST_FILE_PATH);
|
||||
assert_eq!(
|
||||
bootstrap.game_config.game_latest_version,
|
||||
TEST_LAUNCHER_LATEST_VERSION
|
||||
);
|
||||
assert_eq!(
|
||||
bootstrap.game_config.game_latest_file_path,
|
||||
TEST_LAUNCHER_LATEST_FILE_PATH
|
||||
);
|
||||
assert_eq!(bootstrap.manifest_url, TEST_LAUNCHER_MANIFEST_URL);
|
||||
assert_eq!(
|
||||
bootstrap.manifest_source.as_deref(),
|
||||
@@ -54,7 +60,10 @@ fn fetch_bootstrap_uses_official_manifest_source_for_latest_zip() {
|
||||
Some(TEST_SERVER_INFO_URL)
|
||||
);
|
||||
assert_eq!(
|
||||
bootstrap.game_main_config.default_connection_group.as_deref(),
|
||||
bootstrap
|
||||
.game_main_config
|
||||
.default_connection_group
|
||||
.as_deref(),
|
||||
Some(TEST_CONNECTION_GROUP)
|
||||
);
|
||||
assert_ne!(
|
||||
@@ -66,6 +75,28 @@ fn fetch_bootstrap_uses_official_manifest_source_for_latest_zip() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_bootstrap_falls_back_to_latest_file_path_when_manifest_source_is_not_zip() {
|
||||
let harness = TestHarness::new_with_manifest_source("/BlueArchive_JP-1.70.436321-game");
|
||||
let bootstrap = harness.fetch_bootstrap();
|
||||
|
||||
assert_eq!(
|
||||
bootstrap.manifest_source.as_deref(),
|
||||
Some("/BlueArchive_JP-1.70.436321-game")
|
||||
);
|
||||
assert_eq!(
|
||||
bootstrap.game_zip_url,
|
||||
format!(
|
||||
"https://launcher-pkg-ba-jp.yo-star.com/{}",
|
||||
TEST_LAUNCHER_LATEST_FILE_PATH
|
||||
)
|
||||
);
|
||||
|
||||
let curl_log = fs::read_to_string(&harness.curl_log).unwrap();
|
||||
assert!(curl_log.contains(TEST_LAUNCHER_LATEST_FILE_PATH));
|
||||
assert!(!curl_log.contains(" /BlueArchive_JP-1.70.436321-game"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_pipeline_can_dry_run_and_pull_without_local_client() {
|
||||
let harness = TestHarness::new();
|
||||
@@ -100,9 +131,7 @@ fn official_pipeline_can_dry_run_and_pull_without_local_client() {
|
||||
assert_eq!(plan.platforms, verified_official_platforms());
|
||||
assert_eq!(all_urls.len(), 19);
|
||||
assert!(all_urls.iter().all(|url| is_official_yostar_jp_url(url)));
|
||||
assert!(all_urls
|
||||
.iter()
|
||||
.all(|url| !url.contains("bluearchive.cafe")));
|
||||
assert!(all_urls.iter().all(|url| !url.contains("bluearchive.cafe")));
|
||||
assert!(all_urls
|
||||
.iter()
|
||||
.any(|url| url.ends_with("/TableBundles/ExcelDB.db")));
|
||||
@@ -132,6 +161,10 @@ struct TestHarness {
|
||||
|
||||
impl TestHarness {
|
||||
fn new() -> Self {
|
||||
Self::new_with_manifest_source(TEST_LAUNCHER_MANIFEST_SOURCE)
|
||||
}
|
||||
|
||||
fn new_with_manifest_source(manifest_source: &str) -> Self {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let resources_assets = synthetic_resources_assets();
|
||||
|
||||
@@ -145,11 +178,14 @@ impl TestHarness {
|
||||
let curl_script = temp.path().join("fake-curl");
|
||||
write_executable(
|
||||
&curl_script,
|
||||
&official_curl_script(&curl_log, &zip_fixture),
|
||||
&official_curl_script(&curl_log, &zip_fixture, manifest_source),
|
||||
);
|
||||
|
||||
let unzip_script = temp.path().join("fake-unzip");
|
||||
write_executable(&unzip_script, &launcher_unzip_script(&resources_assets_path));
|
||||
write_executable(
|
||||
&unzip_script,
|
||||
&launcher_unzip_script(&resources_assets_path),
|
||||
);
|
||||
|
||||
Self {
|
||||
temp,
|
||||
@@ -159,7 +195,9 @@ impl TestHarness {
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_bootstrap(&self) -> bat_infrastructure::official_game_main_config::OfficialGameMainConfigBootstrap {
|
||||
fn fetch_bootstrap(
|
||||
&self,
|
||||
) -> bat_infrastructure::official_game_main_config::OfficialGameMainConfigBootstrap {
|
||||
OfficialGameMainConfigBootstrapService::with_commands(
|
||||
TEST_LAUNCHER_VERSION,
|
||||
&self.curl_script,
|
||||
@@ -170,11 +208,17 @@ impl TestHarness {
|
||||
}
|
||||
|
||||
fn fetcher(&self) -> OfficialResourcePullService {
|
||||
OfficialResourcePullService::with_curl_command(self.temp.path().join("official-pull"), &self.curl_script)
|
||||
OfficialResourcePullService::with_curl_command(
|
||||
self.temp.path().join("official-pull"),
|
||||
&self.curl_script,
|
||||
)
|
||||
}
|
||||
|
||||
fn pull_service(&self) -> OfficialResourcePullService {
|
||||
OfficialResourcePullService::with_curl_command(self.temp.path().join("pulled"), &self.curl_script)
|
||||
OfficialResourcePullService::with_curl_command(
|
||||
self.temp.path().join("pulled"),
|
||||
&self.curl_script,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +229,7 @@ fn write_executable(path: &Path, content: &str) {
|
||||
fs::set_permissions(path, permissions).unwrap();
|
||||
}
|
||||
|
||||
fn official_curl_script(curl_log: &Path, zip_fixture: &Path) -> String {
|
||||
fn official_curl_script(curl_log: &Path, zip_fixture: &Path, manifest_source: &str) -> String {
|
||||
format!(
|
||||
r#"#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -255,23 +299,23 @@ elif [[ "$url" == "{addressables_root}/Android_PatchPack/catalog_Android.hash" ]
|
||||
elif [[ "$url" == "{addressables_root}/TableBundles/TableCatalog.bytes" ]]; then
|
||||
emit_text "ExcelDB.db"
|
||||
elif [[ "$url" == "{addressables_root}/TableBundles/TableCatalog.hash" ]]; then
|
||||
emit_text "tablecatalog-hash"
|
||||
emit_text "{table_catalog_hash}"
|
||||
elif [[ "$url" == "{addressables_root}/Windows_PatchPack/BundlePackingInfo.bytes" ]]; then
|
||||
emit_text "FullPatch_000.zip"
|
||||
elif [[ "$url" == "{addressables_root}/Windows_PatchPack/BundlePackingInfo.hash" ]]; then
|
||||
emit_text "bundlepackinginfo-win-hash"
|
||||
emit_text "{windows_bundle_catalog_hash}"
|
||||
elif [[ "$url" == "{addressables_root}/MediaResources-Windows/Catalog/MediaCatalog.bytes" ]]; then
|
||||
emit_text "JP_Airi_Win.zip"
|
||||
elif [[ "$url" == "{addressables_root}/MediaResources-Windows/Catalog/MediaCatalog.hash" ]]; then
|
||||
emit_text "mediacatalog-win-hash"
|
||||
emit_text "{windows_media_catalog_hash}"
|
||||
elif [[ "$url" == "{addressables_root}/Android_PatchPack/BundlePackingInfo.bytes" ]]; then
|
||||
emit_text "FullPatch_001.zip"
|
||||
elif [[ "$url" == "{addressables_root}/Android_PatchPack/BundlePackingInfo.hash" ]]; then
|
||||
emit_text "bundlepackinginfo-android-hash"
|
||||
emit_text "{android_bundle_catalog_hash}"
|
||||
elif [[ "$url" == "{addressables_root}/MediaResources/Catalog/MediaCatalog.bytes" ]]; then
|
||||
emit_text "JP_Airi_Android.zip"
|
||||
elif [[ "$url" == "{addressables_root}/MediaResources/Catalog/MediaCatalog.hash" ]]; then
|
||||
emit_text "mediacatalog-android-hash"
|
||||
emit_text "{android_media_catalog_hash}"
|
||||
elif [[ -n "$output" && "$url" == "{addressables_root}/"* ]]; then
|
||||
filename="${{url##*/}}"
|
||||
if [[ "$filename" == *.zip ]]; then
|
||||
@@ -292,10 +336,15 @@ fi
|
||||
launcher_latest_file_path = TEST_LAUNCHER_LATEST_FILE_PATH,
|
||||
launcher_game_config_json_url = TEST_LAUNCHER_GAME_CONFIG_JSON_URL,
|
||||
launcher_manifest_url = TEST_LAUNCHER_MANIFEST_URL,
|
||||
launcher_manifest_source = TEST_LAUNCHER_MANIFEST_SOURCE,
|
||||
launcher_manifest_source = manifest_source,
|
||||
server_info_url = TEST_SERVER_INFO_URL,
|
||||
connection_group = TEST_CONNECTION_GROUP,
|
||||
addressables_root = TEST_ADDRESSABLES_ROOT
|
||||
addressables_root = TEST_ADDRESSABLES_ROOT,
|
||||
table_catalog_hash = xxhash32(b"ExcelDB.db"),
|
||||
windows_bundle_catalog_hash = xxhash32(b"FullPatch_000.zip"),
|
||||
windows_media_catalog_hash = xxhash32(b"JP_Airi_Win.zip"),
|
||||
android_bundle_catalog_hash = xxhash32(b"FullPatch_001.zip"),
|
||||
android_media_catalog_hash = xxhash32(b"JP_Airi_Android.zip"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user