beri cool

This commit is contained in:
Kyan 2025-11-06 22:11:29 +01:00
commit ef0a2ace43
4 changed files with 1397 additions and 0 deletions

1221
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

11
Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "metra"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.51", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_with = "3.15.1"
toml = "0.9.8"
unreal_asset = "0.1.16"

13
README.md Normal file
View file

@ -0,0 +1,13 @@
# Mercury Translator
Inject WTT .toml translations into WACCA
## Usage
```
Usage: metra --messagefolder <MESSAGEFOLDER> --translations <TRANSLATIONS>
Options:
-m, --messagefolder <MESSAGEFOLDER> Path to Message directory
-t, --translations <TRANSLATIONS> Path to directory containing translation files
-h, --help Print help
-V, --version Print version
```

152
src/main.rs Normal file
View file

@ -0,0 +1,152 @@
use std::{
collections::HashMap,
fs::{self, File},
io::{Cursor, Write},
process::{ExitCode, exit},
};
use clap::{Parser, command};
use unreal_asset::{
Asset, cast,
engine_version::EngineVersion,
exports::{Export, data_table_export::DataTableExport},
properties::{Property, PropertyDataTrait},
};
use serde::Deserialize;
use serde_with::{NoneAsEmptyString, serde_as};
// use std::collections::HashMap;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// Path to Message directory
#[arg(short, long)]
messagefolder: String,
/// Path to directory containing translation files
#[arg(short, long)]
translations: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct TranslationFile {
#[serde(flatten)]
pub translations: HashMap<String, TranslationEntry>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all(deserialize = "PascalCase"))]
pub struct TranslationEntry {
pub japanese_message: Option<TranslationValue>,
#[serde(alias = "EnglishMessageUSA")]
pub english_message_usa: Option<TranslationValue>,
#[serde(alias = "EnglishMessageSG")]
pub english_message_sg: Option<TranslationValue>,
#[serde(alias = "TraditionalChineseMessageTW")]
pub traditional_chinese_message_tw: Option<TranslationValue>,
#[serde(alias = "TraditionalChineseMessageHK")]
pub traditional_chinese_message_hk: Option<TranslationValue>,
pub simplified_chinese_message: Option<TranslationValue>,
pub korean_message: Option<TranslationValue>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum TranslationValue {
String(String),
Bool(bool),
}
fn convert_opt(opt: Option<TranslationValue>) -> Option<String> {
let opt = opt.clone();
opt.and_then(|v| match v {
TranslationValue::String(s) => Some(s),
TranslationValue::Bool(_) => None,
})
}
fn main() -> Result<ExitCode, String> {
let args = Args::parse();
let mut translationmap = HashMap::new();
let Ok(files) = fs::read_dir(&args.translations) else { println!("{}: Translation folder not found.", &args.translations); exit(2) };
for file in files {
let name = file.unwrap().file_name().into_string().unwrap();
if name.ends_with(".toml") {
let dat_str = fs::read_to_string(format!("{}/{}",&args.translations,name));
let config: TranslationFile = toml::from_str(&dat_str.unwrap()).expect("Bad Translation file");
translationmap.insert(name.replace(".toml", ""), config);
}
}
for t in translationmap {
let filename = t.0;
let Ok(file) = File::open(
format!("{}/{}.uasset",args.messagefolder,filename),
) else { println!("[{}] .uasset not found.",filename); continue };
let Ok(bulk_file) = File::open(
format!("{}/{}.uexp",args.messagefolder,filename),
) else { println!("[{}] .uexp not found.",filename); continue };
let mut asset = Asset::new(file, Some(bulk_file), EngineVersion::VER_UE4_19).unwrap();
let data_table_export: &mut DataTableExport =
cast!(Export, DataTableExport, &mut asset.asset_data.exports[0])
.expect("First export is not a DataTableExport");
let translations = t.1;
for entry in &mut data_table_export.table.data {
let entry_name = entry.name.get_content();
if translations.translations.get(&entry_name).is_none() {
continue;
}
let entryvalues = translations.translations.get(&entry_name).unwrap();
for property in &mut entry.value {
let property_name = property.get_name().get_content();
let tl = match property_name.as_str() {
"JapaneseMessage" => convert_opt(entryvalues.japanese_message.clone()),
"EnglishMessageUSA" => convert_opt(entryvalues.english_message_usa.clone()),
"EnglishMessageSG" => convert_opt(entryvalues.english_message_sg.clone()),
"TraditionalChineseMessageTW" => {
convert_opt(entryvalues.traditional_chinese_message_tw.clone())
}
"TraditionalChineseMessageHK" => {
convert_opt(entryvalues.traditional_chinese_message_hk.clone())
}
"SimplifiedChineseMessage" => {
convert_opt(entryvalues.simplified_chinese_message.clone())
}
"KoreanMessage" => convert_opt(entryvalues.korean_message.clone()),
_ => None,
};
if tl.is_some() {
let translation = tl.unwrap().replace("\\n", "\\n\\r");
println!("[{}] Wrote {} for {}",filename,property_name,entry_name);
if let Some(bool_prop) = cast!(Property, StrProperty, property) {
bool_prop.value = Some(translation)
}
}
}
}
let mut modified = Cursor::new(Vec::new());
let mut uexp_modified = Cursor::new(Vec::new());
asset
.write_data(&mut modified, Some(&mut uexp_modified))
.unwrap();
drop(asset);
let modified = modified.into_inner();
let uexp_modified = uexp_modified.into_inner();
let Ok(mut file) = File::create(
format!("{}/{}.uasset",args.messagefolder,filename),
) else { println!("[{}] failed to write to .uasset.",filename); continue };
let Ok(mut bulk_file) = File::create(
format!("{}/{}.uexp",args.messagefolder,filename),
) else { println!("[{}] failed to write to .uexp.",filename); continue };
file.write(&modified).unwrap();
bulk_file.write(&uexp_modified).unwrap();
}
Ok(ExitCode::from(0))
}