fix official auto discovery for current launcher manifest

This commit is contained in:
2026-07-06 23:02:52 +08:00
parent 5e76ea4ae3
commit e0d43fb994
7 changed files with 483 additions and 54 deletions
+163 -20
View File
@@ -6,8 +6,11 @@
use crate::official_launcher::launcher_package_url;
use crate::official_launcher::OfficialLauncherBootstrapService;
use crate::official_launcher::{YostarJpLauncherCdnConfig, YostarJpLauncherGameConfig};
use crate::official_launcher::{
YostarJpLauncherCdnConfig, YostarJpLauncherGameConfig, YostarJpLauncherRemoteManifest,
};
use bat_adapters::official::game_main_config::YostarJpGameMainConfig;
use bat_adapters::official::launcher::YostarJpLauncherManifestFile;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -24,7 +27,11 @@ pub struct OfficialGameMainConfigBootstrap {
pub manifest_url: String,
/// Remote manifest `source` path, if present.
pub manifest_source: Option<String>,
/// Official game ZIP URL built from the launcher CDN root.
/// Official URL used to obtain `resources.assets`.
///
/// Older launcher manifests point to a single game ZIP. Current manifests
/// may instead point to a directory source plus per-file entries; in that
/// case this is the direct `resources.assets` URL.
pub game_zip_url: String,
/// Number of files declared by the remote manifest.
pub manifest_file_count: usize,
@@ -76,23 +83,15 @@ impl OfficialGameMainConfigBootstrapService {
.filter(|value| !value.is_empty())
.ok_or_else(|| "Official launcher remote manifest has no source".to_string())?;
let cdn_config = self.launcher.fetch_cdn_config()?;
let package_path = select_game_package_path(&game_config, &manifest_source)?;
let game_zip_url = launcher_package_url(&cdn_config.primary_cdn, package_path)?;
let temp_dir = TempDir::new()
.map_err(|error| format!("Failed to create temporary directory: {error}"))?;
let archive_path = temp_dir.path().join("official-game.zip");
self.download_file_with_fallback(
&game_zip_url,
&cdn_config.back_up_cdn,
package_path,
&archive_path,
let (game_zip_url, resources_assets) = self.fetch_resources_assets(
&game_config,
&manifest,
&manifest_source,
&cdn_config,
temp_dir.path(),
)?;
let extract_root = temp_dir.path().join("extract");
fs::create_dir_all(&extract_root)
.map_err(|error| format!("Failed to create extract root: {error}"))?;
self.extract_archive(&archive_path, &extract_root)?;
let resources_assets = find_resources_assets(&extract_root)
.ok_or_else(|| "resources.assets not found in official launcher package".to_string())?;
let game_main_config = YostarJpGameMainConfig::from_resources_assets(resources_assets)?;
Ok(OfficialGameMainConfigBootstrap {
@@ -159,6 +158,50 @@ impl OfficialGameMainConfigBootstrapService {
}
}
fn fetch_resources_assets(
&self,
game_config: &YostarJpLauncherGameConfig,
manifest: &YostarJpLauncherRemoteManifest,
manifest_source: &str,
cdn_config: &YostarJpLauncherCdnConfig,
temp_root: &Path,
) -> Result<(String, PathBuf), String> {
match select_game_main_config_source(game_config, manifest_source, manifest)? {
GameMainConfigSource::Archive(package_path) => {
let game_zip_url = launcher_package_url(&cdn_config.primary_cdn, package_path)?;
let archive_path = temp_root.join("official-game.zip");
self.download_file_with_fallback(
&game_zip_url,
&cdn_config.back_up_cdn,
package_path,
&archive_path,
)?;
let extract_root = temp_root.join("extract");
fs::create_dir_all(&extract_root)
.map_err(|error| format!("Failed to create extract root: {error}"))?;
self.extract_archive(&archive_path, &extract_root)?;
let resources_assets = find_resources_assets(&extract_root).ok_or_else(|| {
"resources.assets not found in official launcher package".to_string()
})?;
Ok((game_zip_url, resources_assets))
}
GameMainConfigSource::ManifestFile { source_dir, file } => {
let relative_path = launcher_manifest_file_relative_path(source_dir, &file.path)?;
let resources_assets_url =
launcher_package_url(&cdn_config.primary_cdn, &relative_path)?;
let resources_assets = temp_root.join("resources.assets");
self.download_file_with_fallback(
&resources_assets_url,
&cdn_config.back_up_cdn,
&relative_path,
&resources_assets,
)?;
verify_manifest_file_size(&resources_assets, file)?;
Ok((resources_assets_url, resources_assets))
}
}
}
fn extract_archive(&self, archive_path: &Path, destination: &Path) -> Result<(), String> {
let output = Command::new(&self.unzip_command)
.arg("-q")
@@ -186,16 +229,39 @@ impl OfficialGameMainConfigBootstrapService {
}
}
fn select_game_package_path<'a>(
enum GameMainConfigSource<'a> {
Archive(&'a str),
ManifestFile {
source_dir: &'a str,
file: &'a YostarJpLauncherManifestFile,
},
}
fn select_game_main_config_source<'a>(
game_config: &'a YostarJpLauncherGameConfig,
manifest_source: &'a str,
) -> Result<&'a str, String> {
manifest: &'a YostarJpLauncherRemoteManifest,
) -> Result<GameMainConfigSource<'a>, String> {
if is_launcher_archive_path(manifest_source) {
return Ok(manifest_source);
return Ok(GameMainConfigSource::Archive(manifest_source));
}
if is_launcher_manifest_directory(manifest_source) {
if let Some(file) = manifest_resource_assets_file(manifest) {
return Ok(GameMainConfigSource::ManifestFile {
source_dir: manifest_source,
file,
});
}
return Err(format!(
"Official launcher manifest directory source has no BlueArchive_Data/resources.assets entry: {manifest_source}"
));
}
if is_launcher_archive_path(&game_config.game_latest_file_path) {
return Ok(&game_config.game_latest_file_path);
return Ok(GameMainConfigSource::Archive(
&game_config.game_latest_file_path,
));
}
Err(format!(
@@ -204,6 +270,16 @@ fn select_game_package_path<'a>(
))
}
fn manifest_resource_assets_file(
manifest: &YostarJpLauncherRemoteManifest,
) -> Option<&YostarJpLauncherManifestFile> {
manifest.files.iter().find(|file| {
normalize_manifest_file_path(&file.path)
.as_deref()
.is_some_and(|path| path.eq_ignore_ascii_case("BlueArchive_Data/resources.assets"))
})
}
fn is_launcher_archive_path(path: &str) -> bool {
!path.is_empty()
&& !path.starts_with('/')
@@ -216,6 +292,73 @@ fn is_launcher_archive_path(path: &str) -> bool {
})
}
fn is_launcher_manifest_directory(path: &str) -> bool {
normalize_manifest_source_dir(path).is_some()
}
fn launcher_manifest_file_relative_path(
source_dir: &str,
file_path: &str,
) -> Result<String, String> {
let source = normalize_manifest_source_dir(source_dir)
.ok_or_else(|| format!("Invalid official launcher manifest source: {source_dir}"))?;
let file = normalize_manifest_file_path(file_path)
.ok_or_else(|| format!("Invalid official launcher manifest file path: {file_path}"))?;
Ok(format!("{source}/{file}"))
}
fn normalize_manifest_source_dir(path: &str) -> Option<String> {
let path = path.trim().trim_start_matches('/');
if path.is_empty() || path.ends_with(".zip") {
return None;
}
normalize_safe_path(path)
}
fn normalize_manifest_file_path(path: &str) -> Option<String> {
let path = path.trim().trim_start_matches('/');
if path.is_empty() {
return None;
}
normalize_safe_path(path)
}
fn normalize_safe_path(path: &str) -> Option<String> {
if path.contains('\\') || path.contains('?') || path.contains('#') {
return None;
}
let mut segments = Vec::new();
for segment in path.split('/') {
if segment.is_empty() || segment == "." || segment == ".." {
return None;
}
segments.push(segment);
}
Some(segments.join("/"))
}
fn verify_manifest_file_size(
path: &Path,
file: &YostarJpLauncherManifestFile,
) -> Result<(), String> {
let expected_size = file.size.parse::<u64>().map_err(|error| {
format!(
"Official launcher manifest has invalid size for {}: {error}",
file.path
)
})?;
let actual_size = fs::metadata(path)
.map_err(|error| format!("Failed to stat downloaded {}: {error}", path.display()))?
.len();
if actual_size != expected_size {
return Err(format!(
"Downloaded official launcher file size mismatch for {}: expected {}, got {}",
file.path, expected_size, actual_size
));
}
Ok(())
}
fn find_resources_assets(root: &Path) -> Option<PathBuf> {
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {