mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 01:15:14 +08:00
feat: track official versions and import resources
Persist official version-state, extend CAS/ResourceRepository import classification, and add offline catalog and failure regression fixtures. Fixes #13 Fixes #12 Fixes #8
This commit is contained in:
@@ -187,12 +187,32 @@ impl AddressablesCatalogDriver {
|
||||
}
|
||||
|
||||
fn resource_type_for_path(path: &str) -> ResourceType {
|
||||
if path.contains("TableBundles/") || path.ends_with(".bytes") {
|
||||
ResourceType::TableBundle
|
||||
} else if path.ends_with(".bundle") {
|
||||
ResourceType::AssetBundle
|
||||
} else if path.contains("catalog") {
|
||||
let normalized = path.replace('\\', "/").to_ascii_lowercase();
|
||||
if normalized.contains("catalog") || normalized.ends_with(".hash") {
|
||||
ResourceType::Manifest
|
||||
} else if normalized.contains("tablebundles/") {
|
||||
ResourceType::TableBundle
|
||||
} else if normalized.contains("mediaresources/")
|
||||
|| normalized.contains("mediaresources-")
|
||||
|| matches!(
|
||||
normalized.rsplit('.').next(),
|
||||
Some(
|
||||
"mp3" | "mp4" | "ogg" | "wav" | "png" | "jpg" | "jpeg" | "webp" | "acb" | "awb"
|
||||
)
|
||||
)
|
||||
{
|
||||
ResourceType::Media
|
||||
} else if normalized.ends_with(".bundle") {
|
||||
ResourceType::AssetBundle
|
||||
} else if normalized.contains("textassets/")
|
||||
|| matches!(
|
||||
normalized.rsplit('.').next(),
|
||||
Some("txt" | "csv" | "xml" | "yaml" | "yml")
|
||||
)
|
||||
{
|
||||
ResourceType::TextAsset
|
||||
} else if normalized.ends_with(".bytes") {
|
||||
ResourceType::TableBundle
|
||||
} else {
|
||||
ResourceType::Other
|
||||
}
|
||||
@@ -210,6 +230,16 @@ impl AddressablesCatalogDriver {
|
||||
return ResourceType::Manifest;
|
||||
}
|
||||
|
||||
if resource_type_name.is_some_and(|name| name.contains("TextAsset")) {
|
||||
return ResourceType::TextAsset;
|
||||
}
|
||||
|
||||
if resource_type_name.is_some_and(|name| {
|
||||
name.contains("AudioClip") || name.contains("VideoClip") || name.contains("Texture2D")
|
||||
}) {
|
||||
return ResourceType::Media;
|
||||
}
|
||||
|
||||
Self::resource_type_for_path(path)
|
||||
}
|
||||
|
||||
@@ -1017,4 +1047,24 @@ mod tests {
|
||||
ResourceType::TableBundle
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_parse_text_and_media_resource_types() {
|
||||
let driver = AddressablesCatalogDriver::new();
|
||||
|
||||
let catalog_json = r#"{
|
||||
"m_LocatorId": "AddressablesMainContentCatalog",
|
||||
"m_Entries": [
|
||||
{"internal_id": "TextAssets/dialogue.csv"},
|
||||
{"internal_id": "MediaResources-Windows/voice/academy.acb"},
|
||||
{"internal_id": "MediaResources-Windows/movie/opening.mp4"}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let manifest = driver.parse(catalog_json.as_bytes()).await.unwrap();
|
||||
|
||||
assert_eq!(manifest.resources[0].resource_type, ResourceType::TextAsset);
|
||||
assert_eq!(manifest.resources[1].resource_type, ResourceType::Media);
|
||||
assert_eq!(manifest.resources[2].resource_type, ResourceType::Media);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ fn parse_obfuscated_game_main_config(bytes: &[u8]) -> Result<YostarJpGameMainCon
|
||||
}
|
||||
|
||||
fn decrypt_inferred_ascii_json(bytes: &[u8]) -> Result<String, String> {
|
||||
if bytes.len() % 2 != 0 {
|
||||
if !bytes.len().is_multiple_of(2) {
|
||||
return Err("Obfuscated GameMainConfig byte length is not UTF-16LE aligned".to_string());
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ fn xor_bytes(bytes: &mut [u8], key: &[u8]) {
|
||||
}
|
||||
|
||||
fn utf16le_to_string(bytes: &[u8]) -> Result<String, String> {
|
||||
if bytes.len() % 2 != 0 {
|
||||
if !bytes.len().is_multiple_of(2) {
|
||||
return Err("GameMainConfig decrypted byte length is not UTF-16LE aligned".into());
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ mod tests {
|
||||
|
||||
fn encrypt_utf16le_xor(value: &str, key: &[u8]) -> Vec<u8> {
|
||||
let mut utf16 = utf16le_bytes(value);
|
||||
xor_bytes(&mut utf16, &key);
|
||||
xor_bytes(&mut utf16, key);
|
||||
utf16
|
||||
}
|
||||
|
||||
|
||||
@@ -965,8 +965,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let delta = current.diff(Some(&previous));
|
||||
assert!(delta.connection_group_changed == false);
|
||||
assert!(delta.app_version_changed == false);
|
||||
assert!(!delta.connection_group_changed);
|
||||
assert!(!delta.app_version_changed);
|
||||
assert!(delta.bundle_version_changed);
|
||||
assert!(delta.addressables_root_changed);
|
||||
assert!(delta.root_token_changed);
|
||||
|
||||
@@ -78,7 +78,7 @@ impl UnitySerializedFile {
|
||||
)?);
|
||||
}
|
||||
|
||||
let big_id_enabled = if version >= 11 && version < 14 {
|
||||
let big_id_enabled = if (11..14).contains(&version) {
|
||||
reader.read_i32("big_id_enabled")?
|
||||
} else {
|
||||
0
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use bat_adapters::manifest::ManifestDriverRegistry;
|
||||
use bat_core::domain::ResourceType;
|
||||
|
||||
#[tokio::test]
|
||||
async fn parses_current_catalog_fixture_with_resource_categories() {
|
||||
let manifest = ManifestDriverRegistry::with_defaults()
|
||||
.parse(include_bytes!(
|
||||
"fixtures/addressables_regression/current_catalog.json"
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(manifest.resources.len(), 4);
|
||||
assert_eq!(
|
||||
manifest.resources[0].resource_type,
|
||||
ResourceType::TableBundle
|
||||
);
|
||||
assert_eq!(manifest.resources[1].resource_type, ResourceType::Media);
|
||||
assert_eq!(manifest.resources[2].resource_type, ResourceType::TextAsset);
|
||||
assert_eq!(
|
||||
manifest.resources[3].dependencies,
|
||||
vec!["shared_dependencies.bundle".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
manifest.metadata.cdn_prefixes,
|
||||
vec!["https://fixture.invalid/current/".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parses_previous_catalog_fixture_using_internal_ids() {
|
||||
let manifest = ManifestDriverRegistry::with_defaults()
|
||||
.parse(include_bytes!(
|
||||
"fixtures/addressables_regression/previous_catalog.json"
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(manifest.resources.len(), 3);
|
||||
assert_eq!(
|
||||
manifest.resources[0].resource_type,
|
||||
ResourceType::AssetBundle
|
||||
);
|
||||
assert_eq!(manifest.resources[1].resource_type, ResourceType::Manifest);
|
||||
assert_eq!(
|
||||
manifest.resources[2].resource_type,
|
||||
ResourceType::TableBundle
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parses_catalog_structure_change_with_alias_fields() {
|
||||
let manifest = ManifestDriverRegistry::with_defaults()
|
||||
.parse(include_bytes!(
|
||||
"fixtures/addressables_regression/structure_changed_catalog.json"
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(manifest.resources.len(), 2);
|
||||
assert_eq!(manifest.resources[0].resource_type, ResourceType::Media);
|
||||
assert_eq!(
|
||||
manifest.resources[0].dependencies,
|
||||
vec!["shared_assets_current.bundle".to_string()]
|
||||
);
|
||||
assert_eq!(manifest.resources[1].resource_type, ResourceType::TextAsset);
|
||||
assert_eq!(manifest.resources[1].address.as_deref(), Some("lesson"));
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"m_LocatorId": "AddressablesMainContentCatalog",
|
||||
"m_InternalIdPrefixes": [
|
||||
"https://fixture.invalid/current/"
|
||||
],
|
||||
"m_Entries": [
|
||||
{
|
||||
"internal_id": "TableBundles/ExcelDB.db",
|
||||
"hash": "current-table-hash",
|
||||
"size": 4096,
|
||||
"address": "ExcelDB",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"internal_id": "MediaResources-Windows/voice/title.acb",
|
||||
"hash": "current-media-hash",
|
||||
"size": 2048,
|
||||
"address": "title",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"internal_id": "TextAssets/dialogue.csv",
|
||||
"hash": "current-text-hash",
|
||||
"size": 128,
|
||||
"address": "dialogue",
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"internal_id": "shared_assets_current.bundle",
|
||||
"hash": "current-bundle-hash",
|
||||
"size": 8192,
|
||||
"address": "shared_assets_current",
|
||||
"dependencies": [
|
||||
"shared_dependencies.bundle"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"m_LocatorId": "AddressablesMainContentCatalog",
|
||||
"m_InternalIdPrefixes": [],
|
||||
"m_InternalIds": [
|
||||
"shared_assets_previous.bundle",
|
||||
"catalog_previous.json",
|
||||
"TableBundles/ExcelDB.db"
|
||||
]
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"m_LocatorId": "AddressablesMainContentCatalog",
|
||||
"m_InternalIdPrefixes": [],
|
||||
"m_InternalIds": [
|
||||
"fallback.bundle"
|
||||
],
|
||||
"m_Entries": [
|
||||
{
|
||||
"InternalId": "MediaResources-Android/voice/title.awb",
|
||||
"Hash": "changed-media-hash",
|
||||
"Size": 65536,
|
||||
"Address": "title-android",
|
||||
"m_Dependencies": [
|
||||
"shared_assets_current.bundle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Path": "TextAssets/lesson.json",
|
||||
"Hash": "changed-text-hash",
|
||||
"Size": 512,
|
||||
"Key": "lesson"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user