mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 06:35:16 +08:00
fix official auto discovery for current launcher manifest
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user