mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
feat(sync): 接入解析缓存与汉化发布前置
补齐官方 release 解析缓存、TextUnit 明细索引、资源变更集、Crowdin handoff 预留、ResourceRepository 导入元数据和 localized release patch 前置链路。 同时开放文件级 patch.apply 与 UnityFS TextAsset/string/semantic field patch CLI/RPC 入口,并保留官方原版资源与汉化产物双目录发布状态。 验证:cargo test -p bat-assetbundle --locked;cargo clippy -p bat-assetbundle --all-targets --locked -- -D warnings;cargo test -p bat-infrastructure --locked。
This commit is contained in:
@@ -13,6 +13,7 @@ serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
lz4 = "1.28"
|
||||
lzma-rs = "0.3"
|
||||
md-5 = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.4"
|
||||
|
||||
@@ -9,14 +9,26 @@
|
||||
|
||||
pub mod error;
|
||||
pub mod parser;
|
||||
pub mod patch;
|
||||
pub mod serialized;
|
||||
pub mod text;
|
||||
pub mod types;
|
||||
|
||||
pub use error::{AssetBundleError, Result};
|
||||
pub use parser::{compression_from_flags, Parser, UnityFsParser};
|
||||
pub use patch::{
|
||||
patch_unityfs_field, patch_unityfs_string_field, patch_unityfs_text_asset, FieldPatch,
|
||||
StringFieldPatch, TextAssetPatch,
|
||||
};
|
||||
pub use serialized::{
|
||||
UnitySerializedFile, UnitySerializedObject, UnitySerializedTextAsset, UnitySerializedType,
|
||||
UnityTypeTreeNode,
|
||||
UnityManagedReferenceMetadata, UnityManagedReferenceRecord, UnitySerializedField,
|
||||
UnitySerializedFieldReplacement, UnitySerializedFile, UnitySerializedObject,
|
||||
UnitySerializedReplacementValue, UnitySerializedTextAsset, UnitySerializedType,
|
||||
UnitySerializedValue, UnityTypeTreeNode,
|
||||
};
|
||||
pub use text::{
|
||||
text_units_to_jsonl, TextUnit, TextUnitExtractionError, TextUnitExtractionReport,
|
||||
TextUnitExtractor,
|
||||
};
|
||||
pub use types::{
|
||||
AssetType, ParsedAssetBundle, RawAssetBundle, UnityFsBlockInfo, UnityFsBundle,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,949 @@
|
||||
//! Text extraction from parsed Unity serialized objects.
|
||||
|
||||
use crate::serialized::{
|
||||
managed_reference_metadata_from_fields, UnityManagedReferenceMetadata,
|
||||
UnityManagedReferenceRecord, UnitySerializedField, UnitySerializedValue,
|
||||
};
|
||||
use crate::types::ParsedAssetBundle;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// One text unit used by translation, glossary and patch pipelines.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TextUnit {
|
||||
/// Original source text.
|
||||
pub source_text: String,
|
||||
/// Logical AssetBundle path, when provided by the caller.
|
||||
pub bundle_path: Option<String>,
|
||||
/// ZIP/archive entry containing the bundle, when known.
|
||||
pub archive_entry: Option<String>,
|
||||
/// Unity serialized file path.
|
||||
pub serialized_file: Option<String>,
|
||||
/// Unity object path ID.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path_id: Option<i64>,
|
||||
/// Unity class ID, for example `49` for `TextAsset`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub class_id: Option<i32>,
|
||||
/// TypeTree field path. `TextAsset` is used for a whole TextAsset payload.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub field_path: Option<String>,
|
||||
/// Byte offset relative to the beginning of the Unity object payload.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub field_offset: Option<usize>,
|
||||
/// Number of bytes consumed by this field, including alignment padding.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub field_byte_size: Option<usize>,
|
||||
/// Unity version associated with the source.
|
||||
pub version: String,
|
||||
/// Stable context for format, asset name and extraction details.
|
||||
pub context: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
/// Non-fatal diagnostic generated while extracting text units.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TextUnitExtractionError {
|
||||
/// Serialized file containing the failed object.
|
||||
pub serialized_file: Option<String>,
|
||||
/// Object path ID, when known.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path_id: Option<i64>,
|
||||
/// Unity class ID, when known.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub class_id: Option<i32>,
|
||||
/// TypeTree field path, when known.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub field_path: Option<String>,
|
||||
/// Byte offset relative to the beginning of the Unity object payload.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub offset: Option<usize>,
|
||||
/// Human-readable error.
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
/// Result of extracting text units from one parsed bundle.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TextUnitExtractionReport {
|
||||
/// Extracted text units in deterministic traversal order.
|
||||
pub units: Vec<TextUnit>,
|
||||
/// Non-fatal object-level errors.
|
||||
pub errors: Vec<TextUnitExtractionError>,
|
||||
/// TextAsset payloads that were binary or invalid UTF-8.
|
||||
pub skipped_binary_text_assets: usize,
|
||||
}
|
||||
|
||||
/// Extracts translation-ready text from Unity bundle data.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct TextUnitExtractor;
|
||||
|
||||
impl TextUnitExtractor {
|
||||
/// Creates an extractor.
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Extracts TextAsset and TypeTree string fields from a parsed bundle.
|
||||
pub fn extract_bundle(
|
||||
&self,
|
||||
bundle: &ParsedAssetBundle,
|
||||
bundle_path: Option<&str>,
|
||||
) -> TextUnitExtractionReport {
|
||||
self.extract_bundle_with_context(bundle, bundle_path, None)
|
||||
}
|
||||
|
||||
/// Extracts text with both logical bundle and archive-entry context.
|
||||
pub fn extract_bundle_with_context(
|
||||
&self,
|
||||
bundle: &ParsedAssetBundle,
|
||||
bundle_path: Option<&str>,
|
||||
archive_entry: Option<&str>,
|
||||
) -> TextUnitExtractionReport {
|
||||
let mut report = TextUnitExtractionReport {
|
||||
units: Vec::new(),
|
||||
errors: Vec::new(),
|
||||
skipped_binary_text_assets: 0,
|
||||
};
|
||||
|
||||
for asset in &bundle.text_assets {
|
||||
if let Some((format, text)) = decode_text_payload(&asset.bytes) {
|
||||
let mut context = BTreeMap::new();
|
||||
context.insert("format".to_string(), format.to_string());
|
||||
context.insert("asset_name".to_string(), asset.name.clone());
|
||||
context.insert("source_kind".to_string(), "TextAsset".to_string());
|
||||
report.units.push(TextUnit {
|
||||
source_text: text,
|
||||
bundle_path: bundle_path.map(ToOwned::to_owned),
|
||||
archive_entry: archive_entry.map(ToOwned::to_owned),
|
||||
serialized_file: asset.source_path.clone(),
|
||||
path_id: Some(asset.path_id),
|
||||
class_id: Some(49),
|
||||
field_path: Some("TextAsset".to_string()),
|
||||
field_offset: None,
|
||||
field_byte_size: None,
|
||||
version: bundle.unity_version.clone(),
|
||||
context,
|
||||
});
|
||||
} else {
|
||||
report.skipped_binary_text_assets += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for serialized_file in &bundle.serialized_files {
|
||||
for object in &serialized_file.objects {
|
||||
if object.class_id == 49 || !serialized_file.object_has_type_tree(object) {
|
||||
continue;
|
||||
}
|
||||
let fields = match serialized_file.fields_for_object_entry(object) {
|
||||
Ok(fields) => fields,
|
||||
Err(error) => {
|
||||
report.errors.push(TextUnitExtractionError {
|
||||
serialized_file: serialized_file.source_path.clone(),
|
||||
path_id: Some(object.path_id),
|
||||
class_id: Some(object.class_id),
|
||||
field_path: None,
|
||||
offset: None,
|
||||
error: error.to_string(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let context = FieldTextContext {
|
||||
serialized_file_path: serialized_file.source_path.as_deref(),
|
||||
path_id: object.path_id,
|
||||
class_id: object.class_id,
|
||||
version: &bundle.unity_version,
|
||||
bundle_path,
|
||||
archive_entry,
|
||||
managed_reference: None,
|
||||
};
|
||||
for field in fields {
|
||||
collect_field_text(&mut report.units, &context, &field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
report
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes text units as one stable JSON object per line.
|
||||
pub fn text_units_to_jsonl(units: &[TextUnit]) -> Result<String, serde_json::Error> {
|
||||
let mut output = String::new();
|
||||
for unit in units {
|
||||
output.push_str(&serde_json::to_string(unit)?);
|
||||
output.push('\n');
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FieldTextContext<'a> {
|
||||
serialized_file_path: Option<&'a str>,
|
||||
path_id: i64,
|
||||
class_id: i32,
|
||||
version: &'a str,
|
||||
bundle_path: Option<&'a str>,
|
||||
archive_entry: Option<&'a str>,
|
||||
managed_reference: Option<UnityManagedReferenceMetadata>,
|
||||
}
|
||||
|
||||
impl<'a> FieldTextContext<'a> {
|
||||
fn with_managed_reference(&self, metadata: Option<&UnityManagedReferenceMetadata>) -> Self {
|
||||
Self {
|
||||
serialized_file_path: self.serialized_file_path,
|
||||
path_id: self.path_id,
|
||||
class_id: self.class_id,
|
||||
version: self.version,
|
||||
bundle_path: self.bundle_path,
|
||||
archive_entry: self.archive_entry,
|
||||
managed_reference: metadata.cloned().or_else(|| self.managed_reference.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_field_text<'a>(
|
||||
units: &mut Vec<TextUnit>,
|
||||
context: &FieldTextContext<'a>,
|
||||
field: &'a UnitySerializedField,
|
||||
) {
|
||||
match &field.value {
|
||||
UnitySerializedValue::String(text) if !text.is_empty() => {
|
||||
let mut unit_context = BTreeMap::new();
|
||||
unit_context.insert("format".to_string(), "plain".to_string());
|
||||
unit_context.insert(
|
||||
"source_kind".to_string(),
|
||||
if context.managed_reference.is_some() {
|
||||
"ManagedReferenceField"
|
||||
} else {
|
||||
"TypeTreeField"
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
unit_context.insert("type_name".to_string(), field.type_name.clone());
|
||||
if let Some(metadata) = &context.managed_reference {
|
||||
insert_managed_reference_context(&mut unit_context, metadata);
|
||||
}
|
||||
units.push(TextUnit {
|
||||
source_text: text.clone(),
|
||||
bundle_path: context.bundle_path.map(ToOwned::to_owned),
|
||||
archive_entry: context.archive_entry.map(ToOwned::to_owned),
|
||||
serialized_file: context.serialized_file_path.map(ToOwned::to_owned),
|
||||
path_id: Some(context.path_id),
|
||||
class_id: Some(context.class_id),
|
||||
field_path: Some(field.path.clone()),
|
||||
field_offset: Some(field.offset),
|
||||
field_byte_size: Some(field.byte_size),
|
||||
version: context.version.to_string(),
|
||||
context: unit_context,
|
||||
});
|
||||
}
|
||||
UnitySerializedValue::Object(fields) => {
|
||||
for field in fields {
|
||||
collect_field_text(units, context, field);
|
||||
}
|
||||
}
|
||||
UnitySerializedValue::ManagedReference {
|
||||
metadata, fields, ..
|
||||
} => {
|
||||
let managed_context = context.with_managed_reference(metadata.as_ref());
|
||||
for field in fields {
|
||||
collect_managed_reference_child_text(units, &managed_context, field);
|
||||
}
|
||||
}
|
||||
UnitySerializedValue::ManagedReferenceRegistry { references, fields } => {
|
||||
if references.is_empty() {
|
||||
let fallback_metadata = managed_reference_metadata_from_fields(fields);
|
||||
for field in fields {
|
||||
let sibling_metadata =
|
||||
managed_reference_metadata_from_sibling_fields(fields, field);
|
||||
let managed_context = context.with_managed_reference(
|
||||
sibling_metadata.as_ref().or(fallback_metadata.as_ref()),
|
||||
);
|
||||
collect_managed_reference_child_text(units, &managed_context, field);
|
||||
}
|
||||
} else {
|
||||
for reference in references {
|
||||
collect_managed_reference_record_text(units, context, reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
UnitySerializedValue::Array(values) | UnitySerializedValue::Map(values) => {
|
||||
for field in values {
|
||||
collect_field_text(units, context, field);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_managed_reference_record_text<'a>(
|
||||
units: &mut Vec<TextUnit>,
|
||||
context: &FieldTextContext<'a>,
|
||||
reference: &'a UnityManagedReferenceRecord,
|
||||
) {
|
||||
let managed_context = context.with_managed_reference(Some(&reference.metadata));
|
||||
for field in &reference.fields {
|
||||
collect_field_text(units, &managed_context, field);
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_managed_reference_child_text<'a>(
|
||||
units: &mut Vec<TextUnit>,
|
||||
context: &FieldTextContext<'a>,
|
||||
field: &'a UnitySerializedField,
|
||||
) {
|
||||
let key = normalized_text_metadata_key(&field.name);
|
||||
if is_managed_reference_metadata_text_key(&key) {
|
||||
return;
|
||||
}
|
||||
if is_managed_reference_payload_text_key(&key) {
|
||||
if let Some(children) = serialized_field_children(field) {
|
||||
for child in children {
|
||||
collect_field_text(units, context, child);
|
||||
}
|
||||
} else {
|
||||
collect_field_text(units, context, field);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(children) = serialized_field_children(field) {
|
||||
if let Some(metadata) = managed_reference_metadata_from_fields(children) {
|
||||
let managed_context = context.with_managed_reference(Some(&metadata));
|
||||
for child in children {
|
||||
collect_managed_reference_child_text(units, &managed_context, child);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
collect_field_text(units, context, field);
|
||||
}
|
||||
|
||||
fn managed_reference_metadata_from_sibling_fields(
|
||||
fields: &[UnitySerializedField],
|
||||
field: &UnitySerializedField,
|
||||
) -> Option<UnityManagedReferenceMetadata> {
|
||||
let record_prefix = managed_reference_record_path_prefix(&field.path)?;
|
||||
let grouped_fields = fields
|
||||
.iter()
|
||||
.filter(|candidate| {
|
||||
candidate.path == record_prefix
|
||||
|| candidate
|
||||
.path
|
||||
.strip_prefix(record_prefix)
|
||||
.is_some_and(|suffix| suffix.starts_with('.'))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
managed_reference_metadata_from_fields(&grouped_fields)
|
||||
}
|
||||
|
||||
fn managed_reference_record_path_prefix(path: &str) -> Option<&str> {
|
||||
if let Some(index_end) = path.rfind(']') {
|
||||
return Some(&path[..=index_end]);
|
||||
}
|
||||
path.rsplit_once('.')
|
||||
.map(|(parent, _)| parent)
|
||||
.filter(|parent| !parent.is_empty())
|
||||
}
|
||||
|
||||
fn insert_managed_reference_context(
|
||||
context: &mut BTreeMap<String, String>,
|
||||
metadata: &UnityManagedReferenceMetadata,
|
||||
) {
|
||||
if let Some(reference_id) = metadata.reference_id {
|
||||
context.insert("managed_reference_id".to_string(), reference_id.to_string());
|
||||
}
|
||||
if let Some(value) = &metadata.full_type_name {
|
||||
context.insert(
|
||||
"managed_reference_full_type_name".to_string(),
|
||||
value.clone(),
|
||||
);
|
||||
}
|
||||
if let Some(value) = &metadata.type_name {
|
||||
context.insert("managed_reference_type".to_string(), value.clone());
|
||||
}
|
||||
if let Some(value) = &metadata.namespace {
|
||||
context.insert("managed_reference_namespace".to_string(), value.clone());
|
||||
}
|
||||
if let Some(value) = &metadata.assembly_name {
|
||||
context.insert("managed_reference_assembly".to_string(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_field_children(field: &UnitySerializedField) -> Option<&[UnitySerializedField]> {
|
||||
match &field.value {
|
||||
UnitySerializedValue::Object(fields)
|
||||
| UnitySerializedValue::Array(fields)
|
||||
| UnitySerializedValue::Map(fields)
|
||||
| UnitySerializedValue::ManagedReference { fields, .. }
|
||||
| UnitySerializedValue::ManagedReferenceRegistry { fields, .. } => Some(fields),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_text_metadata_key(name: &str) -> String {
|
||||
name.strip_prefix("m_")
|
||||
.unwrap_or(name)
|
||||
.chars()
|
||||
.filter(|character| character.is_ascii_alphanumeric())
|
||||
.flat_map(char::to_lowercase)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_managed_reference_metadata_text_key(key: &str) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
"rid"
|
||||
| "id"
|
||||
| "identifier"
|
||||
| "refid"
|
||||
| "referenceid"
|
||||
| "managedreferenceid"
|
||||
| "managedreferenceids"
|
||||
| "managedreferencesid"
|
||||
| "managedreferencesids"
|
||||
| "serializedreferenceid"
|
||||
| "serializedreferenceids"
|
||||
| "refids"
|
||||
| "type"
|
||||
| "typeid"
|
||||
| "typeinfo"
|
||||
| "typename"
|
||||
| "fullname"
|
||||
| "fulltypename"
|
||||
| "class"
|
||||
| "classname"
|
||||
| "managedreferenceclassname"
|
||||
| "serializedreferenceclassname"
|
||||
| "klass"
|
||||
| "managedtype"
|
||||
| "managedreferencetype"
|
||||
| "managedreferencefullname"
|
||||
| "managedreferencefulltypename"
|
||||
| "serializedreferencetype"
|
||||
| "serializedreferencefullname"
|
||||
| "serializedreferencefulltypename"
|
||||
| "assemblyqualifiedname"
|
||||
| "ns"
|
||||
| "namespace"
|
||||
| "namespacename"
|
||||
| "managedreferencenamespace"
|
||||
| "managedreferencenamespacename"
|
||||
| "serializedreferencenamespace"
|
||||
| "serializedreferencenamespacename"
|
||||
| "asm"
|
||||
| "asmname"
|
||||
| "assembly"
|
||||
| "assemblyname"
|
||||
| "managedreferenceassembly"
|
||||
| "managedreferenceassemblyname"
|
||||
| "serializedreferenceassembly"
|
||||
| "serializedreferenceassemblyname"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_managed_reference_payload_text_key(key: &str) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
"data"
|
||||
| "payload"
|
||||
| "value"
|
||||
| "object"
|
||||
| "instance"
|
||||
| "managedreferencepayload"
|
||||
| "referencepayload"
|
||||
| "serializedreferencepayload"
|
||||
| "managedreferencevalue"
|
||||
| "referencevalue"
|
||||
| "serializedreferencevalue"
|
||||
| "managedreferenceobject"
|
||||
| "referenceobject"
|
||||
| "serializedreferenceobject"
|
||||
| "managedreferencedata"
|
||||
| "referencedata"
|
||||
| "serializeddata"
|
||||
| "serializedreferencedata"
|
||||
)
|
||||
}
|
||||
|
||||
fn decode_text_payload(bytes: &[u8]) -> Option<(&'static str, String)> {
|
||||
let bytes = bytes.strip_prefix(&[0xef, 0xbb, 0xbf]).unwrap_or(bytes);
|
||||
if bytes.contains(&0) {
|
||||
return None;
|
||||
}
|
||||
let text = std::str::from_utf8(bytes).ok()?.to_string();
|
||||
if text.trim().is_empty()
|
||||
|| text
|
||||
.chars()
|
||||
.any(|character| character.is_control() && !matches!(character, '\n' | '\r' | '\t'))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let trimmed = text.trim();
|
||||
if serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
|
||||
return Some(("json", text));
|
||||
}
|
||||
if text.lines().any(|line| line.contains('\t')) {
|
||||
return Some(("tsv", text));
|
||||
}
|
||||
if text.lines().count() > 1 && text.lines().any(|line| line.contains(',')) {
|
||||
return Some(("csv", text));
|
||||
}
|
||||
Some(("plain", text))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::serialized::UnitySerializedTextAsset;
|
||||
use crate::types::{
|
||||
ParsedAssetBundle, UnityFsBlockInfo, UnityFsDirectoryInfo, UnityFsHeader,
|
||||
UnitySerializedParseError,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn extracts_textasset_and_writes_jsonl() {
|
||||
let bundle = ParsedAssetBundle {
|
||||
unity_version: "2021.3.56f2".to_string(),
|
||||
assets: vec!["CAB-test".to_string()],
|
||||
raw_data: Vec::new(),
|
||||
unityfs_header: Some(UnityFsHeader {
|
||||
format_version: 8,
|
||||
target_version: "5.x.x".to_string(),
|
||||
unity_version: "2021.3.56f2".to_string(),
|
||||
total_size: 0,
|
||||
compressed_blocks_info_size: 0,
|
||||
uncompressed_blocks_info_size: 0,
|
||||
flags: 0,
|
||||
}),
|
||||
blocks: vec![UnityFsBlockInfo {
|
||||
uncompressed_size: 0,
|
||||
compressed_size: 0,
|
||||
flags: 0,
|
||||
compression: crate::types::UnityFsCompression::None,
|
||||
}],
|
||||
directories: vec![UnityFsDirectoryInfo {
|
||||
offset: 0,
|
||||
size: 0,
|
||||
flags: 0,
|
||||
path: "CAB-test".to_string(),
|
||||
}],
|
||||
files: Vec::new(),
|
||||
serialized_files: Vec::new(),
|
||||
text_assets: vec![UnitySerializedTextAsset {
|
||||
source_path: Some("CAB-test".to_string()),
|
||||
path_id: 1,
|
||||
name: "dialogue.json".to_string(),
|
||||
bytes: br#"{"text":"hello"}"#.to_vec(),
|
||||
}],
|
||||
serialized_parse_errors: Vec::<UnitySerializedParseError>::new(),
|
||||
};
|
||||
|
||||
let report = TextUnitExtractor::new().extract_bundle_with_context(
|
||||
&bundle,
|
||||
Some("dialogue.bundle"),
|
||||
Some("assets/dialogue.bundle"),
|
||||
);
|
||||
|
||||
assert_eq!(report.units.len(), 1);
|
||||
assert_eq!(
|
||||
report.units[0].archive_entry.as_deref(),
|
||||
Some("assets/dialogue.bundle")
|
||||
);
|
||||
assert_eq!(
|
||||
report.units[0].context.get("format"),
|
||||
Some(&"json".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
text_units_to_jsonl(&report.units).unwrap(),
|
||||
format!("{}\n", serde_json::to_string(&report.units[0]).unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_managed_reference_payload_without_metadata_strings() {
|
||||
let payload_field = string_field(
|
||||
"m_ManagedReferences.references[0].data.message",
|
||||
"message",
|
||||
"こんにちは",
|
||||
);
|
||||
let metadata_field = string_field(
|
||||
"m_ManagedReferences.references[0].managedReferenceFullTypeName",
|
||||
"managedReferenceFullTypeName",
|
||||
"Game BA.Text.ScenarioLine",
|
||||
);
|
||||
let registry_field = UnitySerializedField {
|
||||
path: "m_ManagedReferences".to_string(),
|
||||
name: "m_ManagedReferences".to_string(),
|
||||
type_name: "ManagedReferenceRegistry".to_string(),
|
||||
offset: 0,
|
||||
byte_size: 64,
|
||||
type_tree_node_index: None,
|
||||
value: UnitySerializedValue::ManagedReferenceRegistry {
|
||||
references: vec![UnityManagedReferenceRecord {
|
||||
metadata: UnityManagedReferenceMetadata {
|
||||
reference_id: Some(42),
|
||||
full_type_name: Some("Game BA.Text.ScenarioLine".to_string()),
|
||||
type_name: Some("ScenarioLine".to_string()),
|
||||
namespace: Some("BA.Text".to_string()),
|
||||
assembly_name: Some("Game".to_string()),
|
||||
},
|
||||
fields: vec![payload_field.clone()],
|
||||
}],
|
||||
fields: vec![metadata_field],
|
||||
},
|
||||
};
|
||||
let context = FieldTextContext {
|
||||
serialized_file_path: Some("CAB-test"),
|
||||
path_id: 7,
|
||||
class_id: 114,
|
||||
version: "2021.3.56f2",
|
||||
bundle_path: Some("scenario.bundle"),
|
||||
archive_entry: None,
|
||||
managed_reference: None,
|
||||
};
|
||||
let mut units = Vec::new();
|
||||
|
||||
collect_field_text(&mut units, &context, ®istry_field);
|
||||
|
||||
assert_eq!(units.len(), 1);
|
||||
assert_eq!(units[0].source_text, "こんにちは");
|
||||
assert_eq!(
|
||||
units[0].field_path.as_deref(),
|
||||
Some("m_ManagedReferences.references[0].data.message")
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("source_kind"),
|
||||
Some(&"ManagedReferenceField".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_id"),
|
||||
Some(&"42".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_full_type_name"),
|
||||
Some(&"Game BA.Text.ScenarioLine".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_type"),
|
||||
Some(&"ScenarioLine".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_namespace"),
|
||||
Some(&"BA.Text".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_assembly"),
|
||||
Some(&"Game".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_fallback_managed_reference_payload_alias_without_metadata_strings() {
|
||||
let payload_field = string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceData.message",
|
||||
"message",
|
||||
"こんにちは",
|
||||
);
|
||||
let registry_field = UnitySerializedField {
|
||||
path: "m_ManagedReferences".to_string(),
|
||||
name: "m_ManagedReferences".to_string(),
|
||||
type_name: "ManagedReferencesRegistry".to_string(),
|
||||
offset: 0,
|
||||
byte_size: 96,
|
||||
type_tree_node_index: None,
|
||||
value: UnitySerializedValue::ManagedReferenceRegistry {
|
||||
references: Vec::new(),
|
||||
fields: vec![
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceClassName",
|
||||
"managedReferenceClassName",
|
||||
"ScenarioLine",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceNamespaceName",
|
||||
"managedReferenceNamespaceName",
|
||||
"BA.Text",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceAssemblyName",
|
||||
"managedReferenceAssemblyName",
|
||||
"Game",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].serializedReferenceFullTypeName",
|
||||
"serializedReferenceFullTypeName",
|
||||
"Game BA.Text.ScenarioLine",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].typeInfo",
|
||||
"typeInfo",
|
||||
"Game BA.Text.ScenarioLine",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].typeID.className",
|
||||
"className",
|
||||
"ScenarioLine",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].typeID.namespaceName",
|
||||
"namespaceName",
|
||||
"BA.Text",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].typeID.asmName",
|
||||
"asmName",
|
||||
"Game",
|
||||
),
|
||||
UnitySerializedField {
|
||||
path: "m_ManagedReferences.RefIds[0].managedReferenceData".to_string(),
|
||||
name: "managedReferenceData".to_string(),
|
||||
type_name: "managedReference".to_string(),
|
||||
offset: 64,
|
||||
byte_size: 24,
|
||||
type_tree_node_index: None,
|
||||
value: UnitySerializedValue::Object(vec![payload_field]),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
let context = FieldTextContext {
|
||||
serialized_file_path: Some("CAB-test"),
|
||||
path_id: 7,
|
||||
class_id: 114,
|
||||
version: "2021.3.56f2",
|
||||
bundle_path: Some("scenario.bundle"),
|
||||
archive_entry: None,
|
||||
managed_reference: None,
|
||||
};
|
||||
let mut units = Vec::new();
|
||||
|
||||
collect_field_text(&mut units, &context, ®istry_field);
|
||||
|
||||
assert_eq!(units.len(), 1);
|
||||
assert_eq!(units[0].source_text, "こんにちは");
|
||||
assert_eq!(
|
||||
units[0].field_path.as_deref(),
|
||||
Some("m_ManagedReferences.RefIds[0].managedReferenceData.message")
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("source_kind"),
|
||||
Some(&"ManagedReferenceField".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_type"),
|
||||
Some(&"ScenarioLine".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_namespace"),
|
||||
Some(&"BA.Text".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_assembly"),
|
||||
Some(&"Game".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_full_type_name"),
|
||||
Some(&"Game BA.Text.ScenarioLine".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_fallback_managed_reference_payload_family_alias_with_context() {
|
||||
let payload_field = string_field(
|
||||
"m_ManagedReferences.RefIds[0].serializedReferencePayload.message",
|
||||
"message",
|
||||
"こんにちは",
|
||||
);
|
||||
let registry_field = UnitySerializedField {
|
||||
path: "m_ManagedReferences".to_string(),
|
||||
name: "m_ManagedReferences".to_string(),
|
||||
type_name: "ManagedReferencesRegistry".to_string(),
|
||||
offset: 0,
|
||||
byte_size: 96,
|
||||
type_tree_node_index: None,
|
||||
value: UnitySerializedValue::ManagedReferenceRegistry {
|
||||
references: Vec::new(),
|
||||
fields: vec![
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceClassName",
|
||||
"managedReferenceClassName",
|
||||
"ScenarioLine",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceNamespaceName",
|
||||
"managedReferenceNamespaceName",
|
||||
"BA.Text",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceAssemblyName",
|
||||
"managedReferenceAssemblyName",
|
||||
"Game",
|
||||
),
|
||||
UnitySerializedField {
|
||||
path: "m_ManagedReferences.RefIds[0].serializedReferencePayload"
|
||||
.to_string(),
|
||||
name: "serializedReferencePayload".to_string(),
|
||||
type_name: "managedReference".to_string(),
|
||||
offset: 64,
|
||||
byte_size: 24,
|
||||
type_tree_node_index: None,
|
||||
value: UnitySerializedValue::Object(vec![payload_field]),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
let context = FieldTextContext {
|
||||
serialized_file_path: Some("CAB-test"),
|
||||
path_id: 7,
|
||||
class_id: 114,
|
||||
version: "2021.3.56f2",
|
||||
bundle_path: Some("scenario.bundle"),
|
||||
archive_entry: None,
|
||||
managed_reference: None,
|
||||
};
|
||||
let mut units = Vec::new();
|
||||
|
||||
collect_field_text(&mut units, &context, ®istry_field);
|
||||
|
||||
assert_eq!(units.len(), 1);
|
||||
assert_eq!(units[0].source_text, "こんにちは");
|
||||
assert_eq!(
|
||||
units[0].field_path.as_deref(),
|
||||
Some("m_ManagedReferences.RefIds[0].serializedReferencePayload.message")
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("source_kind"),
|
||||
Some(&"ManagedReferenceField".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_type"),
|
||||
Some(&"ScenarioLine".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_namespace"),
|
||||
Some(&"BA.Text".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_assembly"),
|
||||
Some(&"Game".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_fallback_managed_reference_sibling_records_with_separate_context() {
|
||||
let registry_field = UnitySerializedField {
|
||||
path: "m_ManagedReferences".to_string(),
|
||||
name: "m_ManagedReferences".to_string(),
|
||||
type_name: "ManagedReferencesRegistry".to_string(),
|
||||
offset: 0,
|
||||
byte_size: 160,
|
||||
type_tree_node_index: None,
|
||||
value: UnitySerializedValue::ManagedReferenceRegistry {
|
||||
references: Vec::new(),
|
||||
fields: vec![
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceClassName",
|
||||
"managedReferenceClassName",
|
||||
"ScenarioLine",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceNamespaceName",
|
||||
"managedReferenceNamespaceName",
|
||||
"BA.Text",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceAssemblyName",
|
||||
"managedReferenceAssemblyName",
|
||||
"Game",
|
||||
),
|
||||
UnitySerializedField {
|
||||
path: "m_ManagedReferences.RefIds[0].managedReferenceData".to_string(),
|
||||
name: "managedReferenceData".to_string(),
|
||||
type_name: "managedReference".to_string(),
|
||||
offset: 64,
|
||||
byte_size: 24,
|
||||
type_tree_node_index: None,
|
||||
value: UnitySerializedValue::Object(vec![string_field(
|
||||
"m_ManagedReferences.RefIds[0].managedReferenceData.message",
|
||||
"message",
|
||||
"こんにちは",
|
||||
)]),
|
||||
},
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[1].managedReferenceClassName",
|
||||
"managedReferenceClassName",
|
||||
"ChoiceLine",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[1].managedReferenceNamespaceName",
|
||||
"managedReferenceNamespaceName",
|
||||
"BA.Text",
|
||||
),
|
||||
string_field(
|
||||
"m_ManagedReferences.RefIds[1].managedReferenceAssemblyName",
|
||||
"managedReferenceAssemblyName",
|
||||
"Game",
|
||||
),
|
||||
UnitySerializedField {
|
||||
path: "m_ManagedReferences.RefIds[1].referencePayload".to_string(),
|
||||
name: "referencePayload".to_string(),
|
||||
type_name: "managedReference".to_string(),
|
||||
offset: 120,
|
||||
byte_size: 24,
|
||||
type_tree_node_index: None,
|
||||
value: UnitySerializedValue::Object(vec![string_field(
|
||||
"m_ManagedReferences.RefIds[1].referencePayload.message",
|
||||
"message",
|
||||
"選択肢",
|
||||
)]),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
let context = FieldTextContext {
|
||||
serialized_file_path: Some("CAB-test"),
|
||||
path_id: 7,
|
||||
class_id: 114,
|
||||
version: "2021.3.56f2",
|
||||
bundle_path: Some("scenario.bundle"),
|
||||
archive_entry: None,
|
||||
managed_reference: None,
|
||||
};
|
||||
let mut units = Vec::new();
|
||||
|
||||
collect_field_text(&mut units, &context, ®istry_field);
|
||||
|
||||
assert_eq!(units.len(), 2);
|
||||
assert_eq!(
|
||||
units[0].field_path.as_deref(),
|
||||
Some("m_ManagedReferences.RefIds[0].managedReferenceData.message")
|
||||
);
|
||||
assert_eq!(units[0].source_text, "こんにちは");
|
||||
assert_eq!(
|
||||
units[0].context.get("managed_reference_type"),
|
||||
Some(&"ScenarioLine".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
units[1].field_path.as_deref(),
|
||||
Some("m_ManagedReferences.RefIds[1].referencePayload.message")
|
||||
);
|
||||
assert_eq!(units[1].source_text, "選択肢");
|
||||
assert_eq!(
|
||||
units[1].context.get("managed_reference_type"),
|
||||
Some(&"ChoiceLine".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
fn string_field(path: &str, name: &str, value: &str) -> UnitySerializedField {
|
||||
UnitySerializedField {
|
||||
path: path.to_string(),
|
||||
name: name.to_string(),
|
||||
type_name: "string".to_string(),
|
||||
offset: 0,
|
||||
byte_size: value.len() + 4,
|
||||
type_tree_node_index: None,
|
||||
value: UnitySerializedValue::String(value.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user