mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 06:28:00 +08:00
432 lines
14 KiB
Rust
432 lines
14 KiB
Rust
use bat_adapters::official::inventory::{
|
|
YostarJpPlatformCatalogInventory, YostarJpPlatformDownloadInventory,
|
|
};
|
|
use bat_adapters::official::yostar_jp::{
|
|
server_info_url, PatchPlatform, YostarJpResourceDiscoveryPlan, YostarJpResourceEndpoint,
|
|
YostarJpResourceEndpointKind, YostarJpServerInfo,
|
|
};
|
|
use bat_infrastructure::{
|
|
build_official_pull_plan_for_platform_inventory,
|
|
build_official_pull_plan_from_platform_inventory, default_official_platforms,
|
|
OfficialGameMainConfigBootstrapService, OfficialResourcePullService,
|
|
};
|
|
use std::collections::HashMap;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Debug)]
|
|
struct CliArgs {
|
|
server_info_source: Option<ServerInfoSource>,
|
|
connection_group: Option<String>,
|
|
app_version: Option<String>,
|
|
launcher_version: String,
|
|
launcher_bootstrap: bool,
|
|
platforms: Option<Vec<PatchPlatform>>,
|
|
output_root: PathBuf,
|
|
curl_command: PathBuf,
|
|
dry_run: bool,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum ServerInfoSource {
|
|
LocalPath(PathBuf),
|
|
OfficialFile(String),
|
|
}
|
|
|
|
fn main() -> anyhow::Result<()> {
|
|
let args = parse_args()?;
|
|
let default_platforms = default_official_platforms();
|
|
let platforms = args
|
|
.platforms
|
|
.as_deref()
|
|
.unwrap_or(default_platforms.as_slice());
|
|
let needs_official_bootstrap = args.launcher_bootstrap
|
|
|| args.server_info_source.is_none()
|
|
|| args.connection_group.is_none()
|
|
|| args.app_version.is_none();
|
|
let official_bootstrap = if needs_official_bootstrap {
|
|
Some(
|
|
OfficialGameMainConfigBootstrapService::with_commands(
|
|
&args.launcher_version,
|
|
args.curl_command.to_string_lossy().to_string(),
|
|
"unzip",
|
|
)
|
|
.fetch_bootstrap()
|
|
.map_err(anyhow::Error::msg)?,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
let app_version = args
|
|
.app_version
|
|
.clone()
|
|
.or_else(|| {
|
|
official_bootstrap
|
|
.as_ref()
|
|
.map(|bootstrap| bootstrap.game_config.game_latest_version.clone())
|
|
})
|
|
.ok_or_else(|| {
|
|
anyhow::anyhow!("missing --app-version and official bootstrap failed")
|
|
})?;
|
|
let connection_group = args
|
|
.connection_group
|
|
.clone()
|
|
.or_else(|| {
|
|
official_bootstrap
|
|
.as_ref()
|
|
.and_then(|bootstrap| bootstrap.game_main_config.default_connection_group.clone())
|
|
})
|
|
.ok_or_else(|| {
|
|
anyhow::anyhow!(
|
|
"missing --connection-group and official GameMainConfig has no default_connection_group"
|
|
)
|
|
})?;
|
|
|
|
let fetcher =
|
|
OfficialResourcePullService::with_curl_command(&args.output_root, &args.curl_command);
|
|
|
|
if args.launcher_bootstrap {
|
|
let bootstrap = official_bootstrap.as_ref().ok_or_else(|| {
|
|
anyhow::anyhow!("launcher bootstrap requested but official bootstrap is unavailable")
|
|
})?;
|
|
println!(
|
|
"launcher_api_game_latest_version={}",
|
|
bootstrap.game_config.game_latest_version
|
|
);
|
|
println!(
|
|
"launcher_api_game_latest_file_path={}",
|
|
bootstrap.game_config.game_latest_file_path
|
|
);
|
|
println!(
|
|
"launcher_api_game_lowest_version={}",
|
|
bootstrap
|
|
.game_config
|
|
.game_lowest_version
|
|
.as_deref()
|
|
.unwrap_or("<none>")
|
|
);
|
|
println!(
|
|
"launcher_api_game_start_exe_name={}",
|
|
bootstrap
|
|
.game_config
|
|
.game_start_exe_name
|
|
.as_deref()
|
|
.unwrap_or("<none>")
|
|
);
|
|
println!(
|
|
"launcher_api_game_start_params={:?}",
|
|
bootstrap.game_config.game_start_params
|
|
);
|
|
println!("launcher_api_primary_cdn={}", bootstrap.cdn_config.primary_cdn);
|
|
println!("launcher_api_backup_cdn={}", bootstrap.cdn_config.back_up_cdn);
|
|
println!("launcher_api_manifest_url={}", bootstrap.manifest_url);
|
|
println!(
|
|
"launcher_api_manifest_source={}",
|
|
bootstrap.manifest_source.as_deref().unwrap_or("<none>")
|
|
);
|
|
println!(
|
|
"launcher_api_manifest_file_count={}",
|
|
bootstrap.manifest_file_count
|
|
);
|
|
println!(
|
|
"launcher_api_game_main_config_server_info_url={}",
|
|
bootstrap
|
|
.game_main_config
|
|
.server_info_data_url
|
|
.as_deref()
|
|
.unwrap_or("<none>")
|
|
);
|
|
println!(
|
|
"launcher_api_game_main_config_default_connection_group={}",
|
|
bootstrap
|
|
.game_main_config
|
|
.default_connection_group
|
|
.as_deref()
|
|
.unwrap_or("<none>")
|
|
);
|
|
println!();
|
|
}
|
|
|
|
let server_info_source = args
|
|
.server_info_source
|
|
.as_ref();
|
|
|
|
let server_info_bytes = if let Some(source) = server_info_source {
|
|
load_server_info(source, &fetcher)?
|
|
} else {
|
|
let bootstrap = official_bootstrap.as_ref().ok_or_else(|| {
|
|
anyhow::anyhow!("official bootstrap is required when no --server-info-file or --server-info-path is provided")
|
|
})?;
|
|
let url = bootstrap
|
|
.game_main_config
|
|
.server_info_data_url
|
|
.as_deref()
|
|
.ok_or_else(|| anyhow::anyhow!("official GameMainConfig has no ServerInfoDataUrl"))?;
|
|
fetcher.fetch_bytes(url).map_err(anyhow::Error::msg)?
|
|
};
|
|
let server_info = YostarJpServerInfo::from_slice(&server_info_bytes)?;
|
|
let discovery = server_info
|
|
.discovery_plan(&connection_group, &app_version, platforms)
|
|
.map_err(anyhow::Error::msg)?;
|
|
let seed_catalogs = fetch_seed_catalogs(&fetcher, &discovery)?;
|
|
let inventory = build_inventory_from_seed_catalogs(&seed_catalogs, platforms)?;
|
|
let plan = if args.platforms.is_some() {
|
|
build_official_pull_plan_for_platform_inventory(discovery, inventory, platforms)
|
|
} else {
|
|
build_official_pull_plan_from_platform_inventory(discovery, inventory)
|
|
};
|
|
let all_urls = plan.all_urls().map_err(anyhow::Error::msg)?;
|
|
|
|
println!("connection_group={}", plan.discovery.connection_group_name);
|
|
println!("app_version={}", plan.discovery.app_version);
|
|
println!(
|
|
"bundle_version={}",
|
|
plan.discovery.bundle_version.as_deref().unwrap_or("<none>")
|
|
);
|
|
println!("addressables_root={}", plan.discovery.addressables_root);
|
|
println!("platforms={:?}", plan.platforms);
|
|
println!("discovery_url_count={}", plan.discovery_urls().len());
|
|
println!(
|
|
"content_url_count={}",
|
|
plan.content_urls().map_err(anyhow::Error::msg)?.len()
|
|
);
|
|
println!("total_url_count={}", all_urls.len());
|
|
println!("output_root={}", args.output_root.display());
|
|
println!("dry_run={}", args.dry_run);
|
|
println!();
|
|
|
|
for url in &all_urls {
|
|
println!("{url}");
|
|
}
|
|
|
|
if args.dry_run {
|
|
return Ok(());
|
|
}
|
|
|
|
let downloader =
|
|
OfficialResourcePullService::with_curl_command(args.output_root, args.curl_command);
|
|
let report = downloader.pull(&plan).map_err(anyhow::Error::msg)?;
|
|
eprintln!("downloaded_count={}", report.items.len());
|
|
eprintln!("downloaded_bytes={}", report.total_bytes());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn load_server_info(
|
|
source: &ServerInfoSource,
|
|
fetcher: &OfficialResourcePullService,
|
|
) -> anyhow::Result<Vec<u8>> {
|
|
match source {
|
|
ServerInfoSource::LocalPath(path) => Ok(fs::read(path)?),
|
|
ServerInfoSource::OfficialFile(file_name) => {
|
|
let url = server_info_url(file_name).map_err(anyhow::Error::msg)?;
|
|
fetcher.fetch_bytes(&url).map_err(anyhow::Error::msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fetch_seed_catalogs(
|
|
fetcher: &OfficialResourcePullService,
|
|
discovery: &YostarJpResourceDiscoveryPlan,
|
|
) -> anyhow::Result<SeedCatalogs> {
|
|
let mut catalogs = SeedCatalogs::default();
|
|
|
|
for endpoint in &discovery.endpoints {
|
|
if !matches!(
|
|
endpoint.kind,
|
|
YostarJpResourceEndpointKind::TableCatalog
|
|
| YostarJpResourceEndpointKind::BundlePackingInfo
|
|
| YostarJpResourceEndpointKind::MediaCatalog
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
let bytes = fetcher
|
|
.fetch_bytes(&endpoint.url)
|
|
.map_err(anyhow::Error::msg)?;
|
|
catalogs.insert(endpoint, bytes)?;
|
|
}
|
|
|
|
Ok(catalogs)
|
|
}
|
|
|
|
fn build_inventory_from_seed_catalogs(
|
|
catalogs: &SeedCatalogs,
|
|
platforms: &[PatchPlatform],
|
|
) -> anyhow::Result<YostarJpPlatformDownloadInventory> {
|
|
let table_catalog = catalogs
|
|
.table_catalog
|
|
.as_deref()
|
|
.ok_or_else(|| anyhow::anyhow!("official discovery did not include TableCatalog.bytes"))?;
|
|
let mut platform_catalogs = Vec::new();
|
|
|
|
for platform in platforms {
|
|
let bundle_packing_info = catalogs.bundle_packing_infos.get(platform).ok_or_else(|| {
|
|
anyhow::anyhow!(
|
|
"official discovery did not include {} BundlePackingInfo.bytes",
|
|
platform.as_str()
|
|
)
|
|
})?;
|
|
let media_catalog = catalogs.media_catalogs.get(platform).ok_or_else(|| {
|
|
anyhow::anyhow!(
|
|
"official discovery did not include {} MediaCatalog.bytes",
|
|
platform.as_str()
|
|
)
|
|
})?;
|
|
|
|
platform_catalogs.push(YostarJpPlatformCatalogInventory::from_catalog_bytes(
|
|
*platform,
|
|
bundle_packing_info,
|
|
media_catalog,
|
|
));
|
|
}
|
|
|
|
Ok(YostarJpPlatformDownloadInventory::from_catalog_bytes(
|
|
table_catalog,
|
|
platform_catalogs,
|
|
))
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct SeedCatalogs {
|
|
table_catalog: Option<Vec<u8>>,
|
|
bundle_packing_infos: HashMap<PatchPlatform, Vec<u8>>,
|
|
media_catalogs: HashMap<PatchPlatform, Vec<u8>>,
|
|
}
|
|
|
|
impl SeedCatalogs {
|
|
fn insert(
|
|
&mut self,
|
|
endpoint: &YostarJpResourceEndpoint,
|
|
bytes: Vec<u8>,
|
|
) -> anyhow::Result<()> {
|
|
match endpoint.kind {
|
|
YostarJpResourceEndpointKind::TableCatalog => {
|
|
self.table_catalog = Some(bytes);
|
|
}
|
|
YostarJpResourceEndpointKind::BundlePackingInfo => {
|
|
let platform = required_platform(endpoint)?;
|
|
self.bundle_packing_infos.insert(platform, bytes);
|
|
}
|
|
YostarJpResourceEndpointKind::MediaCatalog => {
|
|
let platform = required_platform(endpoint)?;
|
|
self.media_catalogs.insert(platform, bytes);
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn required_platform(endpoint: &YostarJpResourceEndpoint) -> anyhow::Result<PatchPlatform> {
|
|
endpoint.platform.ok_or_else(|| {
|
|
anyhow::anyhow!(
|
|
"discovery endpoint {:?} has no platform: {}",
|
|
endpoint.kind,
|
|
endpoint.url
|
|
)
|
|
})
|
|
}
|
|
|
|
fn parse_args() -> anyhow::Result<CliArgs> {
|
|
let raw_args = env::args().collect::<Vec<_>>();
|
|
let mut args = raw_args.into_iter();
|
|
let binary = args
|
|
.next()
|
|
.unwrap_or_else(|| "official_pull_plan".to_string());
|
|
let mut parsed = CliArgs {
|
|
server_info_source: None,
|
|
connection_group: None,
|
|
app_version: None,
|
|
launcher_version: "1.7.2".to_string(),
|
|
launcher_bootstrap: false,
|
|
platforms: None,
|
|
output_root: PathBuf::from("official_pull_output"),
|
|
curl_command: PathBuf::from("curl"),
|
|
dry_run: false,
|
|
};
|
|
|
|
while let Some(flag) = args.next() {
|
|
match flag.as_str() {
|
|
"--server-info-file" => {
|
|
parsed.server_info_source = Some(ServerInfoSource::OfficialFile(
|
|
next_option_value(&mut args, &flag)?,
|
|
));
|
|
}
|
|
"--server-info-path" => {
|
|
parsed.server_info_source = Some(ServerInfoSource::LocalPath(PathBuf::from(
|
|
next_option_value(&mut args, &flag)?,
|
|
)));
|
|
}
|
|
"--connection-group" => {
|
|
parsed.connection_group = Some(next_option_value(&mut args, &flag)?);
|
|
}
|
|
"--app-version" => {
|
|
parsed.app_version = Some(next_option_value(&mut args, &flag)?);
|
|
}
|
|
"--launcher-version" => {
|
|
parsed.launcher_version = next_option_value(&mut args, &flag)?;
|
|
}
|
|
"--launcher-bootstrap" => {
|
|
parsed.launcher_bootstrap = true;
|
|
}
|
|
"--platforms" => {
|
|
let value = next_option_value(&mut args, &flag)?;
|
|
parsed.platforms = Some(parse_platforms(&value).map_err(anyhow::Error::msg)?);
|
|
}
|
|
"--output" => {
|
|
parsed.output_root = PathBuf::from(next_option_value(&mut args, &flag)?);
|
|
}
|
|
"--curl" => {
|
|
parsed.curl_command = PathBuf::from(next_option_value(&mut args, &flag)?);
|
|
}
|
|
"--dry-run" => {
|
|
parsed.dry_run = true;
|
|
}
|
|
"--help" | "-h" => {
|
|
print_usage(&binary);
|
|
std::process::exit(0);
|
|
}
|
|
_ => return Err(anyhow::anyhow!("unknown argument: {flag}")),
|
|
}
|
|
}
|
|
|
|
Ok(parsed)
|
|
}
|
|
|
|
fn next_option_value(
|
|
args: &mut impl Iterator<Item = String>,
|
|
flag: &str,
|
|
) -> anyhow::Result<String> {
|
|
args.next()
|
|
.ok_or_else(|| anyhow::anyhow!("missing value for {flag}"))
|
|
}
|
|
|
|
fn print_usage(binary: &str) {
|
|
eprintln!(
|
|
"usage: {binary} [--server-info-file <official-json-name> | --server-info-path <local-json>] \
|
|
[--app-version 1.70.0] \
|
|
[--launcher-bootstrap] [--launcher-version 1.7.2] [--connection-group <name>] \
|
|
[--platforms Windows,Android] [--output official_pull_output] [--curl curl] [--dry-run]"
|
|
);
|
|
}
|
|
|
|
fn parse_platforms(value: &str) -> Result<Vec<PatchPlatform>, String> {
|
|
value
|
|
.split(',')
|
|
.map(str::trim)
|
|
.filter(|part| !part.is_empty())
|
|
.map(parse_platform)
|
|
.collect()
|
|
}
|
|
|
|
fn parse_platform(value: &str) -> Result<PatchPlatform, String> {
|
|
match value.to_ascii_lowercase().as_str() {
|
|
"windows" | "win" => Ok(PatchPlatform::Windows),
|
|
"android" => Ok(PatchPlatform::Android),
|
|
_ => Err(format!("unsupported platform: {value}")),
|
|
}
|
|
}
|