beri cool
This commit is contained in:
commit
ef0a2ace43
4 changed files with 1397 additions and 0 deletions
1221
Cargo.lock
generated
Normal file
1221
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal 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
13
README.md
Normal 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
152
src/main.rs
Normal 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))
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue