mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 08:54:55 +08:00
bat-rust / Build and test Rust (push) Successful in 3m47s
daemon 子进程现会透传下载并发配置,下载进度按已完成数量单调上报。MediaCatalog 改为使用官方相对路径生成全局媒体 URL,覆盖 GameData、Prologue 等目录并补齐 jpg 资源,避免叶子文件名误拼媒体根目录导致 403。同步更新测试、smoke 断言和运行文档。
697 lines
23 KiB
Rust
697 lines
23 KiB
Rust
//! Official JP download inventory extraction.
|
|
|
|
use super::yostar_jp::{verified_official_platforms, PatchPlatform, YostarJpResourceRoot};
|
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
|
|
|
/// Download inventory extracted from the official JP catalog bytes.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct YostarJpDownloadInventory {
|
|
/// Patch-pack zip names from `BundlePackingInfo.bytes`.
|
|
pub bundle_patch_pack_names: Vec<String>,
|
|
/// Table file names from `TableCatalog.bytes`.
|
|
pub table_file_names: Vec<String>,
|
|
/// Media file relative paths from `MediaCatalog.bytes`.
|
|
pub media_file_names: Vec<String>,
|
|
}
|
|
|
|
/// Platform-specific catalog inventory extracted from official JP bytes.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct YostarJpPlatformCatalogInventory {
|
|
/// Platform this inventory belongs to.
|
|
pub platform: PatchPlatform,
|
|
/// Patch-pack zip names from this platform's `BundlePackingInfo.bytes`.
|
|
pub bundle_patch_pack_names: Vec<String>,
|
|
/// Media file relative paths from this platform's `MediaCatalog.bytes`.
|
|
pub media_file_names: Vec<String>,
|
|
}
|
|
|
|
impl YostarJpPlatformCatalogInventory {
|
|
/// Extracts one platform inventory from official JP catalog bytes.
|
|
pub fn from_catalog_bytes(
|
|
platform: PatchPlatform,
|
|
bundle_packing_info: &[u8],
|
|
media_catalog: &[u8],
|
|
) -> Self {
|
|
Self {
|
|
platform,
|
|
bundle_patch_pack_names: extract_full_patch_pack_names(bundle_packing_info),
|
|
media_file_names: extract_media_file_paths(media_catalog),
|
|
}
|
|
}
|
|
|
|
/// Returns this platform's official bundle patch-pack URLs.
|
|
pub fn bundle_patch_pack_urls(
|
|
&self,
|
|
root: &YostarJpResourceRoot,
|
|
) -> Result<Vec<String>, String> {
|
|
self.bundle_patch_pack_names
|
|
.iter()
|
|
.map(|name| root.bundle_patch_pack(self.platform, name))
|
|
.collect()
|
|
}
|
|
|
|
/// Returns this platform's official media URLs.
|
|
pub fn media_file_urls(&self, root: &YostarJpResourceRoot) -> Result<Vec<String>, String> {
|
|
self.media_file_names
|
|
.iter()
|
|
.map(|name| root.media_file(self.platform, name))
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Multi-platform download inventory with shared table files and platform
|
|
/// specific patch-pack/media catalogs.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct YostarJpPlatformDownloadInventory {
|
|
/// Table file names from the shared `TableCatalog.bytes`.
|
|
pub table_file_names: Vec<String>,
|
|
/// Per-platform bundle/media inventories.
|
|
pub platform_catalogs: Vec<YostarJpPlatformCatalogInventory>,
|
|
}
|
|
|
|
impl YostarJpPlatformDownloadInventory {
|
|
/// Extracts a multi-platform inventory from official JP catalog bytes.
|
|
pub fn from_catalog_bytes(
|
|
table_catalog: &[u8],
|
|
platform_catalogs: Vec<YostarJpPlatformCatalogInventory>,
|
|
) -> Self {
|
|
Self {
|
|
table_file_names: extract_table_file_names(table_catalog),
|
|
platform_catalogs: merge_platform_catalogs(platform_catalogs),
|
|
}
|
|
}
|
|
|
|
/// Builds a platform inventory by reusing a legacy shared inventory for
|
|
/// each requested platform.
|
|
pub fn from_shared_inventory(
|
|
inventory: YostarJpDownloadInventory,
|
|
platforms: &[PatchPlatform],
|
|
) -> Self {
|
|
let platform_catalogs = unique_platforms(platforms)
|
|
.into_iter()
|
|
.map(|platform| YostarJpPlatformCatalogInventory {
|
|
platform,
|
|
bundle_patch_pack_names: inventory.bundle_patch_pack_names.clone(),
|
|
media_file_names: inventory.media_file_names.clone(),
|
|
})
|
|
.collect();
|
|
|
|
Self {
|
|
table_file_names: inventory.table_file_names,
|
|
platform_catalogs,
|
|
}
|
|
}
|
|
|
|
/// Returns the official table file URLs.
|
|
pub fn table_file_urls(&self, root: &YostarJpResourceRoot) -> Result<Vec<String>, String> {
|
|
self.table_file_names
|
|
.iter()
|
|
.map(|name| root.table_bundle(name))
|
|
.collect()
|
|
}
|
|
|
|
/// Returns the complete direct-download URL set for multiple platforms.
|
|
///
|
|
/// Table bundles are emitted once. Patch-pack and media files are emitted
|
|
/// from the matching platform catalog only, which avoids creating invalid
|
|
/// cross-platform URL combinations.
|
|
pub fn direct_download_urls_for_platforms(
|
|
&self,
|
|
root: &YostarJpResourceRoot,
|
|
platforms: &[PatchPlatform],
|
|
) -> Result<Vec<String>, String> {
|
|
let mut urls = Vec::new();
|
|
let mut seen = HashSet::new();
|
|
|
|
append_unique_urls(&mut urls, &mut seen, self.table_file_urls(root)?);
|
|
|
|
for platform in unique_platforms(platforms) {
|
|
let catalog = self.platform_catalog(platform).ok_or_else(|| {
|
|
format!(
|
|
"Missing official catalog inventory for platform: {}",
|
|
platform.as_str()
|
|
)
|
|
})?;
|
|
|
|
append_unique_urls(&mut urls, &mut seen, catalog.bundle_patch_pack_urls(root)?);
|
|
append_unique_urls(&mut urls, &mut seen, catalog.media_file_urls(root)?);
|
|
}
|
|
|
|
Ok(urls)
|
|
}
|
|
|
|
/// Returns the complete direct-download URL set for all verified official
|
|
/// JP platforms.
|
|
pub fn direct_download_urls_for_verified_platforms(
|
|
&self,
|
|
root: &YostarJpResourceRoot,
|
|
) -> Result<Vec<String>, String> {
|
|
self.direct_download_urls_for_platforms(root, &verified_official_platforms())
|
|
}
|
|
|
|
fn platform_catalog(
|
|
&self,
|
|
platform: PatchPlatform,
|
|
) -> Option<&YostarJpPlatformCatalogInventory> {
|
|
self.platform_catalogs
|
|
.iter()
|
|
.find(|catalog| catalog.platform == platform)
|
|
}
|
|
}
|
|
|
|
impl YostarJpDownloadInventory {
|
|
/// Extracts an inventory from the three official JP catalog bytes.
|
|
pub fn from_catalog_bytes(
|
|
bundle_packing_info: &[u8],
|
|
table_catalog: &[u8],
|
|
media_catalog: &[u8],
|
|
) -> Self {
|
|
Self {
|
|
bundle_patch_pack_names: extract_full_patch_pack_names(bundle_packing_info),
|
|
table_file_names: extract_table_file_names(table_catalog),
|
|
media_file_names: extract_media_file_paths(media_catalog),
|
|
}
|
|
}
|
|
|
|
/// Returns the official bundle patch-pack URLs for a platform.
|
|
pub fn bundle_patch_pack_urls(
|
|
&self,
|
|
root: &YostarJpResourceRoot,
|
|
platform: PatchPlatform,
|
|
) -> Result<Vec<String>, String> {
|
|
self.bundle_patch_pack_names
|
|
.iter()
|
|
.map(|name| root.bundle_patch_pack(platform, name))
|
|
.collect()
|
|
}
|
|
|
|
/// Returns the official table file URLs.
|
|
pub fn table_file_urls(&self, root: &YostarJpResourceRoot) -> Result<Vec<String>, String> {
|
|
self.table_file_names
|
|
.iter()
|
|
.map(|name| root.table_bundle(name))
|
|
.collect()
|
|
}
|
|
|
|
/// Returns the official media file URLs for a platform.
|
|
pub fn media_file_urls(
|
|
&self,
|
|
root: &YostarJpResourceRoot,
|
|
platform: PatchPlatform,
|
|
) -> Result<Vec<String>, String> {
|
|
self.media_file_names
|
|
.iter()
|
|
.map(|name| root.media_file(platform, name))
|
|
.collect()
|
|
}
|
|
|
|
/// Returns the complete direct-download URL set for a platform.
|
|
pub fn direct_download_urls(
|
|
&self,
|
|
root: &YostarJpResourceRoot,
|
|
platform: PatchPlatform,
|
|
) -> Result<Vec<String>, String> {
|
|
let mut urls = self.bundle_patch_pack_urls(root, platform)?;
|
|
urls.extend(self.table_file_urls(root)?);
|
|
urls.extend(self.media_file_urls(root, platform)?);
|
|
Ok(urls)
|
|
}
|
|
|
|
/// Returns the complete direct-download URL set for multiple platforms.
|
|
///
|
|
/// Table bundles are emitted once. Platform-specific URLs are emitted in
|
|
/// platform order and deduplicated by URL so shared Android media paths
|
|
/// only appear once.
|
|
pub fn direct_download_urls_for_platforms(
|
|
&self,
|
|
root: &YostarJpResourceRoot,
|
|
platforms: &[PatchPlatform],
|
|
) -> Result<Vec<String>, String> {
|
|
let mut urls = Vec::new();
|
|
let mut seen = HashSet::new();
|
|
|
|
append_unique_urls(&mut urls, &mut seen, self.table_file_urls(root)?);
|
|
|
|
for platform in unique_platforms(platforms) {
|
|
append_unique_urls(
|
|
&mut urls,
|
|
&mut seen,
|
|
self.bundle_patch_pack_urls(root, platform)?,
|
|
);
|
|
append_unique_urls(&mut urls, &mut seen, self.media_file_urls(root, platform)?);
|
|
}
|
|
|
|
Ok(urls)
|
|
}
|
|
|
|
/// Returns the complete direct-download URL set for all verified official
|
|
/// JP platforms.
|
|
pub fn direct_download_urls_for_verified_platforms(
|
|
&self,
|
|
root: &YostarJpResourceRoot,
|
|
) -> Result<Vec<String>, String> {
|
|
self.direct_download_urls_for_platforms(root, &verified_official_platforms())
|
|
}
|
|
}
|
|
|
|
fn merge_platform_catalogs(
|
|
catalogs: Vec<YostarJpPlatformCatalogInventory>,
|
|
) -> Vec<YostarJpPlatformCatalogInventory> {
|
|
let mut merged = Vec::<YostarJpPlatformCatalogInventory>::new();
|
|
|
|
for catalog in catalogs {
|
|
if let Some(existing) = merged
|
|
.iter_mut()
|
|
.find(|existing| existing.platform == catalog.platform)
|
|
{
|
|
merge_names(
|
|
&mut existing.bundle_patch_pack_names,
|
|
catalog.bundle_patch_pack_names,
|
|
);
|
|
merge_names(&mut existing.media_file_names, catalog.media_file_names);
|
|
} else {
|
|
merged.push(catalog);
|
|
}
|
|
}
|
|
|
|
merged.sort_by_key(|catalog| catalog.platform);
|
|
merged
|
|
}
|
|
|
|
fn merge_names(existing: &mut Vec<String>, names: Vec<String>) {
|
|
let mut merged = existing.iter().cloned().collect::<BTreeSet<_>>();
|
|
merged.extend(names);
|
|
*existing = merged.into_iter().collect();
|
|
}
|
|
|
|
fn extract_full_patch_pack_names(data: &[u8]) -> Vec<String> {
|
|
extract_file_names(data, &["zip"]) // only .zip names survive here
|
|
.into_iter()
|
|
.filter(|name| is_full_patch_pack_name(name))
|
|
.collect()
|
|
}
|
|
|
|
fn extract_table_file_names(data: &[u8]) -> Vec<String> {
|
|
let mut counts = BTreeMap::<String, usize>::new();
|
|
|
|
// Real TableCatalog bytes list top-level downloadable table files twice,
|
|
// while package-internal stage/resource .zip names normally appear once
|
|
// next to rawdata paths and are not direct TableBundles URLs.
|
|
for string in extract_printable_strings(data, 4) {
|
|
for name in candidate_file_names(&string, &["db", "zip"]) {
|
|
*counts.entry(name).or_default() += 1;
|
|
}
|
|
}
|
|
|
|
counts
|
|
.into_iter()
|
|
.filter_map(|(name, count)| (count >= 2).then_some(name))
|
|
.collect()
|
|
}
|
|
|
|
fn extract_file_names(data: &[u8], extensions: &[&str]) -> Vec<String> {
|
|
let mut names = BTreeSet::new();
|
|
|
|
for string in extract_printable_strings(data, 4) {
|
|
for name in candidate_file_names(&string, extensions) {
|
|
names.insert(name);
|
|
}
|
|
}
|
|
|
|
names.into_iter().collect()
|
|
}
|
|
|
|
fn extract_media_file_paths(data: &[u8]) -> Vec<String> {
|
|
let mut paths = BTreeSet::new();
|
|
|
|
for string in extract_printable_strings(data, 4) {
|
|
for path in
|
|
candidate_relative_paths(&string, &["zip", "mp4", "png", "jpg", "jpeg", "ogg", "wav"])
|
|
{
|
|
paths.insert(path);
|
|
}
|
|
}
|
|
|
|
paths.into_iter().collect()
|
|
}
|
|
|
|
fn extract_printable_strings(data: &[u8], min_len: usize) -> Vec<String> {
|
|
let mut strings = Vec::new();
|
|
let mut current = Vec::new();
|
|
|
|
for &byte in data {
|
|
if byte.is_ascii_graphic() || byte == b' ' {
|
|
current.push(byte);
|
|
} else if current.len() >= min_len {
|
|
strings.push(String::from_utf8_lossy(¤t).into_owned());
|
|
current.clear();
|
|
} else {
|
|
current.clear();
|
|
}
|
|
}
|
|
|
|
if current.len() >= min_len {
|
|
strings.push(String::from_utf8_lossy(¤t).into_owned());
|
|
}
|
|
|
|
strings
|
|
}
|
|
|
|
fn candidate_file_names(value: &str, extensions: &[&str]) -> Vec<String> {
|
|
let mut names = Vec::new();
|
|
let bytes = value.as_bytes();
|
|
|
|
for extension in extensions {
|
|
let suffix = format!(".{extension}");
|
|
let mut search_from = 0;
|
|
|
|
while let Some(relative_index) = value[search_from..].find(&suffix) {
|
|
let extension_start = search_from + relative_index;
|
|
let start = filename_start(bytes, extension_start);
|
|
let end = extension_start + suffix.len();
|
|
let candidate = &value[start..end];
|
|
let candidate = candidate.rsplit(['/', '\\']).next().unwrap_or(candidate);
|
|
|
|
if is_plausible_file_name(candidate) {
|
|
names.push(candidate.to_string());
|
|
}
|
|
|
|
search_from = end;
|
|
}
|
|
}
|
|
|
|
names
|
|
}
|
|
|
|
fn candidate_relative_paths(value: &str, extensions: &[&str]) -> Vec<String> {
|
|
let mut paths = Vec::new();
|
|
let bytes = value.as_bytes();
|
|
|
|
for extension in extensions {
|
|
let suffix = format!(".{extension}");
|
|
let mut search_from = 0;
|
|
|
|
while let Some(relative_index) = value[search_from..].find(&suffix) {
|
|
let extension_start = search_from + relative_index;
|
|
let start = filename_start(bytes, extension_start);
|
|
let end = extension_start + suffix.len();
|
|
let candidate = &value[start..end];
|
|
let candidate = candidate.replace('\\', "/");
|
|
|
|
if is_plausible_relative_path(&candidate) {
|
|
paths.push(candidate);
|
|
}
|
|
|
|
search_from = end;
|
|
}
|
|
}
|
|
|
|
paths
|
|
}
|
|
|
|
fn filename_start(bytes: &[u8], mut index: usize) -> usize {
|
|
while index > 0 {
|
|
let byte = bytes[index - 1];
|
|
if byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b'/' | b'\\') {
|
|
index -= 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
index
|
|
}
|
|
|
|
fn is_full_patch_pack_name(name: &str) -> bool {
|
|
let bytes = name.as_bytes();
|
|
if bytes.len() != 17 || !name.starts_with("FullPatch_") || !name.ends_with(".zip") {
|
|
return false;
|
|
}
|
|
|
|
bytes[10..13].iter().all(|byte| byte.is_ascii_digit())
|
|
}
|
|
|
|
fn is_plausible_file_name(name: &str) -> bool {
|
|
!name.is_empty()
|
|
&& !name.contains('/')
|
|
&& !name.contains('\\')
|
|
&& !name.contains("..")
|
|
&& !name.contains(':')
|
|
&& !name.contains('=')
|
|
&& name
|
|
.chars()
|
|
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
|
|
}
|
|
|
|
fn is_plausible_relative_path(path: &str) -> bool {
|
|
if path.is_empty()
|
|
|| path.starts_with('/')
|
|
|| path.starts_with('.')
|
|
|| path.contains("..")
|
|
|| path.contains(':')
|
|
|| path.contains('=')
|
|
|| !path.contains('/')
|
|
{
|
|
return false;
|
|
}
|
|
|
|
path.split('/').all(|segment| {
|
|
!segment.is_empty()
|
|
&& segment != "."
|
|
&& segment != ".."
|
|
&& segment
|
|
.chars()
|
|
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
|
|
})
|
|
}
|
|
|
|
fn unique_platforms(platforms: &[PatchPlatform]) -> Vec<PatchPlatform> {
|
|
platforms
|
|
.iter()
|
|
.copied()
|
|
.fold(Vec::new(), |mut unique, platform| {
|
|
if !unique.contains(&platform) {
|
|
unique.push(platform);
|
|
}
|
|
unique
|
|
})
|
|
}
|
|
|
|
fn append_unique_urls(urls: &mut Vec<String>, seen: &mut HashSet<String>, next_urls: Vec<String>) {
|
|
for url in next_urls {
|
|
if seen.insert(url.clone()) {
|
|
urls.push(url);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::official::yostar_jp::YostarJpResourceRoot;
|
|
|
|
const ROOT: &str = "r93_dctuo3tcd029wwxnvb55";
|
|
|
|
#[test]
|
|
fn extracts_download_names_from_synthetic_bytes() {
|
|
let inventory = YostarJpDownloadInventory::from_catalog_bytes(
|
|
b"prefix FullPatch_000.zip noise FullPatch_114.zip suffix",
|
|
b"GameData\\Table\\ExcelDB.db\0ExcelDB.db\0rawdata/table/excel/ignored.bytes\0Battle.zip\0Battle.zip8",
|
|
b"audio/voc_jp/jp_airi/jp_airi\0GameData\\Audio\\VOC_JP\\JP_Airi.zip8\0audio/voc_jp/jp_akane/jp_akane\0GameData\\Audio\\VOC_JP\\JP_Akane.zip",
|
|
);
|
|
|
|
assert_eq!(
|
|
inventory.bundle_patch_pack_names,
|
|
vec![
|
|
"FullPatch_000.zip".to_string(),
|
|
"FullPatch_114.zip".to_string()
|
|
]
|
|
);
|
|
assert_eq!(
|
|
inventory.table_file_names,
|
|
vec!["Battle.zip".to_string(), "ExcelDB.db".to_string(),]
|
|
);
|
|
assert_eq!(
|
|
inventory.media_file_names,
|
|
vec![
|
|
"GameData/Audio/VOC_JP/JP_Airi.zip".to_string(),
|
|
"GameData/Audio/VOC_JP/JP_Akane.zip".to_string(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_only_top_level_table_files_from_table_catalog() {
|
|
let inventory = YostarJpPlatformDownloadInventory::from_catalog_bytes(
|
|
b"ExcelDB.db\0ExcelDB.db\0TablePatchPack_Prologue_GroundStage_1.zip\0TablePatchPack_Prologue_GroundStage_1.zip|1011101_01_s1_01_mainstreet_p01_d.zip\0rawdata/ground/stage/bytes/1011101_01_s1_01_mainstreet_p01_d.bytes",
|
|
Vec::new(),
|
|
);
|
|
|
|
assert_eq!(
|
|
inventory.table_file_names,
|
|
vec![
|
|
"ExcelDB.db".to_string(),
|
|
"TablePatchPack_Prologue_GroundStage_1.zip".to_string()
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn builds_direct_download_urls() {
|
|
let inventory = YostarJpDownloadInventory::from_catalog_bytes(
|
|
b"FullPatch_000.zip FullPatch_001.zip",
|
|
b"ExcelDB.db ExcelDB.db Battle.zip Battle.zip",
|
|
b"GameData\\Audio\\VOC_JP\\JP_Airi.zip GameData\\Audio\\VOC_JP\\JP_Akane.zip",
|
|
);
|
|
let root = YostarJpResourceRoot::from_root_token(ROOT).unwrap();
|
|
|
|
let urls = inventory
|
|
.direct_download_urls(&root, PatchPlatform::Windows)
|
|
.unwrap();
|
|
|
|
assert_eq!(urls[0], "https://prod-clientpatch.bluearchiveyostar.com/r93_dctuo3tcd029wwxnvb55/Windows_PatchPack/FullPatch_000.zip");
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/TableBundles/ExcelDB.db")));
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/MediaResources-Windows/GameData/Audio/VOC_JP/JP_Airi.zip")));
|
|
}
|
|
|
|
#[test]
|
|
fn builds_direct_download_urls_for_verified_platforms() {
|
|
let inventory = YostarJpDownloadInventory::from_catalog_bytes(
|
|
b"FullPatch_000.zip",
|
|
b"ExcelDB.db ExcelDB.db",
|
|
b"GameData\\Audio\\VOC_JP\\JP_Airi.zip",
|
|
);
|
|
let root = YostarJpResourceRoot::from_root_token(ROOT).unwrap();
|
|
|
|
let urls = inventory
|
|
.direct_download_urls_for_verified_platforms(&root)
|
|
.unwrap();
|
|
|
|
assert_eq!(urls.len(), 5);
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.contains("/Windows_PatchPack/FullPatch_000.zip")));
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.contains("/Android_PatchPack/FullPatch_000.zip")));
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/TableBundles/ExcelDB.db")));
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/MediaResources-Windows/GameData/Audio/VOC_JP/JP_Airi.zip")));
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/MediaResources/GameData/Audio/VOC_JP/JP_Airi.zip")));
|
|
}
|
|
|
|
#[test]
|
|
fn builds_platform_specific_download_urls_without_cross_mixing_catalogs() {
|
|
let inventory = YostarJpPlatformDownloadInventory::from_catalog_bytes(
|
|
b"ExcelDB.db ExcelDB.db",
|
|
vec![
|
|
YostarJpPlatformCatalogInventory::from_catalog_bytes(
|
|
PatchPlatform::Windows,
|
|
b"FullPatch_000.zip",
|
|
b"GameData\\Audio\\VOC_JP\\JP_Airi_Win.zip",
|
|
),
|
|
YostarJpPlatformCatalogInventory::from_catalog_bytes(
|
|
PatchPlatform::Android,
|
|
b"FullPatch_001.zip",
|
|
b"GameData\\Audio\\VOC_JP\\JP_Airi_Android.zip",
|
|
),
|
|
],
|
|
);
|
|
let root = YostarJpResourceRoot::from_root_token(ROOT).unwrap();
|
|
|
|
let urls = inventory
|
|
.direct_download_urls_for_verified_platforms(&root)
|
|
.unwrap();
|
|
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/Windows_PatchPack/FullPatch_000.zip")));
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/Android_PatchPack/FullPatch_001.zip")));
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url
|
|
.ends_with("/MediaResources-Windows/GameData/Audio/VOC_JP/JP_Airi_Win.zip")));
|
|
assert!(urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/MediaResources/GameData/Audio/VOC_JP/JP_Airi_Android.zip")));
|
|
assert!(!urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/Android_PatchPack/FullPatch_000.zip")));
|
|
assert!(!urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/Windows_PatchPack/FullPatch_001.zip")));
|
|
}
|
|
|
|
#[test]
|
|
fn media_catalog_uses_download_relative_path_not_leaf_name() {
|
|
let inventory = YostarJpPlatformDownloadInventory::from_catalog_bytes(
|
|
b"ExcelDB.db ExcelDB.db",
|
|
vec![YostarJpPlatformCatalogInventory::from_catalog_bytes(
|
|
PatchPlatform::Windows,
|
|
b"FullPatch_000.zip",
|
|
b"scenario/event/10000_title_sound\0Prologue\\Scenario\\Event\\10000_Title_Sound.ogg\0 10000_Title_Sound.ogg",
|
|
)],
|
|
);
|
|
let root = YostarJpResourceRoot::from_root_token(ROOT).unwrap();
|
|
|
|
let urls = inventory
|
|
.direct_download_urls_for_platforms(&root, &[PatchPlatform::Windows])
|
|
.unwrap();
|
|
|
|
assert!(urls.iter().any(|url| {
|
|
url.ends_with("/MediaResources-Windows/Prologue/Scenario/Event/10000_Title_Sound.ogg")
|
|
}));
|
|
assert!(!urls
|
|
.iter()
|
|
.any(|url| url.ends_with("/MediaResources-Windows/10000_Title_Sound.ogg")));
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires BAT_REAL_OFFICIAL_BUNDLE_PACKING_INFO, BAT_REAL_OFFICIAL_TABLE_CATALOG, BAT_REAL_OFFICIAL_MEDIA_CATALOG"]
|
|
fn extracts_realistic_counts_from_official_shape() {
|
|
let bundle_packing_info =
|
|
std::fs::read(std::env::var("BAT_REAL_OFFICIAL_BUNDLE_PACKING_INFO").unwrap()).unwrap();
|
|
let table_catalog =
|
|
std::fs::read(std::env::var("BAT_REAL_OFFICIAL_TABLE_CATALOG").unwrap()).unwrap();
|
|
let media_catalog =
|
|
std::fs::read(std::env::var("BAT_REAL_OFFICIAL_MEDIA_CATALOG").unwrap()).unwrap();
|
|
|
|
let inventory = YostarJpDownloadInventory::from_catalog_bytes(
|
|
&bundle_packing_info,
|
|
&table_catalog,
|
|
&media_catalog,
|
|
);
|
|
|
|
assert_eq!(inventory.bundle_patch_pack_names.len(), 142);
|
|
assert!(inventory.table_file_names.len() < 1000);
|
|
assert!(inventory.media_file_names.len() >= 4000);
|
|
assert!(inventory
|
|
.bundle_patch_pack_names
|
|
.iter()
|
|
.any(|name| name == "FullPatch_000.zip"));
|
|
assert!(inventory
|
|
.table_file_names
|
|
.iter()
|
|
.any(|name| name == "ExcelDB.db"));
|
|
assert!(inventory
|
|
.media_file_names
|
|
.iter()
|
|
.any(|name| name == "GameData/Audio/VOC_JP/JP_Airi.zip"));
|
|
assert!(inventory
|
|
.media_file_names
|
|
.iter()
|
|
.any(|name| name.ends_with(".jpg")));
|
|
}
|
|
}
|