mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-23 07:55:15 +08:00
chore: establish development baseline
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
//! Unity Addressables Catalog Driver
|
||||
//!
|
||||
//! 解析 Unity Addressables 的 catalog_Remote.json 文件
|
||||
|
||||
use super::driver::{GenericManifest, ManifestDriver, ManifestFormat, ManifestMetadata};
|
||||
use async_trait::async_trait;
|
||||
use bat_core::domain::{ResourceEntry, ResourceType};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Addressables Catalog Driver
|
||||
pub struct AddressablesCatalogDriver;
|
||||
|
||||
impl AddressablesCatalogDriver {
|
||||
/// 创建新的 Driver 实例
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AddressablesCatalogDriver {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ManifestDriver for AddressablesCatalogDriver {
|
||||
fn name(&self) -> &str {
|
||||
"Unity Addressables Catalog"
|
||||
}
|
||||
|
||||
fn format_version(&self) -> &str {
|
||||
"1.0"
|
||||
}
|
||||
|
||||
fn can_parse(&self, raw_data: &[u8]) -> bool {
|
||||
// 检测是否是 Addressables Catalog
|
||||
// 1. 必须是有效的 JSON
|
||||
// 2. 必须包含 m_LocatorId 字段
|
||||
// 3. m_LocatorId 值应该是 "AddressablesMainContentCatalog"
|
||||
|
||||
let text = match std::str::from_utf8(raw_data) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let json: Value = match serde_json::from_str(text) {
|
||||
Ok(j) => j,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let locator_id = match json.get("m_LocatorId") {
|
||||
Some(id) => id,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let id_str = match locator_id.as_str() {
|
||||
Some(s) => s,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
id_str.contains("AddressablesMainContentCatalog") || id_str.contains("Addressables")
|
||||
}
|
||||
|
||||
async fn parse(&self, raw_data: &[u8]) -> Result<GenericManifest, String> {
|
||||
// 解析 JSON
|
||||
let text = std::str::from_utf8(raw_data)
|
||||
.map_err(|e| format!("Invalid UTF-8: {}", e))?;
|
||||
|
||||
let json: Value = serde_json::from_str(text)
|
||||
.map_err(|e| format!("Invalid JSON: {}", e))?;
|
||||
|
||||
// 提取元数据
|
||||
let locator_id = json.get("m_LocatorId")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// 提取 CDN 前缀
|
||||
let mut cdn_prefixes = Vec::new();
|
||||
if let Some(prefixes) = json.get("m_InternalIdPrefixes").and_then(|v| v.as_array()) {
|
||||
for prefix in prefixes {
|
||||
if let Some(s) = prefix.as_str() {
|
||||
cdn_prefixes.push(s.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提取资源列表(基础版本 - 仅从 m_InternalIds 提取)
|
||||
let mut resources = Vec::new();
|
||||
if let Some(internal_ids) = json.get("m_InternalIds").and_then(|v| v.as_array()) {
|
||||
for (index, id) in internal_ids.iter().enumerate() {
|
||||
if let Some(path) = id.as_str() {
|
||||
// 跳过空路径
|
||||
if path.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断资源类型
|
||||
let resource_type = if path.ends_with(".bundle") {
|
||||
ResourceType::AssetBundle
|
||||
} else if path.contains("catalog") {
|
||||
ResourceType::Manifest
|
||||
} else {
|
||||
ResourceType::Other
|
||||
};
|
||||
|
||||
resources.push(ResourceEntry {
|
||||
path: path.to_string(),
|
||||
hash: format!("addressable_{}", index), // 临时 Hash
|
||||
size: 0, // 大小未知
|
||||
resource_type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 解析 m_KeyDataString、m_EntryDataString 等压缩字段
|
||||
// 这些字段使用了自定义压缩格式,需要实现解压缩算法
|
||||
// 参考:Unity Addressables 源代码
|
||||
|
||||
let metadata = ManifestMetadata {
|
||||
locator_id,
|
||||
cdn_prefixes,
|
||||
extra: HashMap::new(),
|
||||
};
|
||||
|
||||
Ok(GenericManifest {
|
||||
format: ManifestFormat::AddressablesCatalog,
|
||||
resources,
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_can_parse_valid_catalog() {
|
||||
let driver = AddressablesCatalogDriver::new();
|
||||
|
||||
let valid_json = r#"{
|
||||
"m_LocatorId": "AddressablesMainContentCatalog",
|
||||
"m_InternalIds": []
|
||||
}"#;
|
||||
|
||||
assert!(driver.can_parse(valid_json.as_bytes()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_can_parse_invalid() {
|
||||
let driver = AddressablesCatalogDriver::new();
|
||||
|
||||
let invalid_json = r#"{"some": "other"}"#;
|
||||
assert!(!driver.can_parse(invalid_json.as_bytes()));
|
||||
|
||||
let not_json = b"not json at all";
|
||||
assert!(!driver.can_parse(not_json));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_parse_simple_catalog() {
|
||||
let driver = AddressablesCatalogDriver::new();
|
||||
|
||||
let catalog_json = r#"{
|
||||
"m_LocatorId": "AddressablesMainContentCatalog",
|
||||
"m_InternalIdPrefixes": [],
|
||||
"m_InternalIds": [
|
||||
"academy.bundle",
|
||||
"character.bundle",
|
||||
"catalog.json"
|
||||
]
|
||||
}"#;
|
||||
|
||||
let result = driver.parse(catalog_json.as_bytes()).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let manifest = result.unwrap();
|
||||
assert_eq!(manifest.format, ManifestFormat::AddressablesCatalog);
|
||||
assert_eq!(manifest.resources.len(), 3);
|
||||
assert_eq!(manifest.resources[0].resource_type, ResourceType::AssetBundle);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user