Skip to content

Commit

Permalink
stringtable: rapify
Browse files Browse the repository at this point in the history
  • Loading branch information
PabstMirror committed Dec 3, 2024
1 parent 018a338 commit a527196
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions bin/src/modules/stringtables/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{io::BufReader, sync::Arc};

use hemtt_stringtable::{
analyze::{lint_all, lint_check, lint_one},
rapify::convert_stringtable,
Project,
};
use hemtt_workspace::{
Expand Down Expand Up @@ -67,9 +68,12 @@ impl Module for Stringtables {
report.extend(lint_all(&stringtables, Some(ctx.config())));

for stringtable in stringtables {
report.extend(lint_one(&stringtable, Some(ctx.config())));
let codes = lint_one(&stringtable, Some(ctx.config()));
if codes.is_empty() {
convert_stringtable(&stringtable.0, &stringtable.1);
}
report.extend(codes);
}

Ok(report)
}
}
Expand Down
1 change: 1 addition & 0 deletions libs/stringtable/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ quick-xml = { version = "0.37.1", features = ["serialize"] }
serde = { workspace = true, features = ["derive"] }
toml = { workspace = true }
tracing = { workspace = true }
vfs = { workspace = true }

[dev-dependencies]
insta = { workspace = true }
1 change: 1 addition & 0 deletions libs/stringtable/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
pub mod analyze;
mod key;
mod package;
pub mod rapify;
mod totals;

pub use key::Key;
Expand Down
150 changes: 150 additions & 0 deletions libs/stringtable/src/rapify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use crate::{Key, Project};
use hemtt_workspace::WorkspacePath;
use tracing::warn;

static ALL_LANGUAGES: [&str; 14] = ["English", "Czech", "French", "Spanish", "Italian", "Polish", "Portuguese", "Russian", "German", "Korean", "Japanese", "Chinese", "Chinesesimp", "Turkish"];

#[derive(Default)]
struct XmlbLayout {
// 4 byte numbers, little end?, nul term strings
header: Vec<u8>, // Version
languages: Vec<u8>, // Language Count, [Languages]
offsets: Vec<u8>, // Offset Count, [Offsets]
keys: Vec<u8>, // Key Count, [Keys]
translations: Vec<Vec<u8>>, // [Translation Count, [Translations], ...]
}

/// # Panics
pub fn convert_stringtable(project: &Project, xml_path: &WorkspacePath) {
let result = rapify(project);

if result.is_some() {
// Create stringtable.bin
let xmlb_path = xml_path.with_extension("bin").expect("vfs error");
let mut xmlb_file = xmlb_path.create_file().expect("vfs error");

// Remove Original stringtable.xml
xml_path.vfs().remove_file().expect("vfs error");

let data = result.expect("data struct valid");
xmlb_file.write_all(&data.header).expect("IO Error");
xmlb_file.write_all(&data.languages).expect("IO Error");
xmlb_file.write_all(&data.offsets).expect("IO Error");
xmlb_file.write_all(&data.keys).expect("IO Error");
for translation_buffer in &data.translations {
xmlb_file.write_all(translation_buffer).expect("IO Error");
}
println!("pass");
} else {
warn!("fail");
}
}

/// Write string with null-termination
fn write_string(buffer: &mut Vec<u8>, input: &str) {
buffer.extend(input.as_bytes());
buffer.push(0);
}
fn write_int(buffer: &mut Vec<u8>, input: i32) {
buffer.extend(&input.to_le_bytes());
}

fn rapify(project: &Project) -> Option<XmlbLayout> {
let mut data: XmlbLayout = XmlbLayout::default();

// Restructure translations: flat for each language
let mut all_keys: Vec<String> = Vec::new();
let mut all_translations: Vec<Vec<String>> = Vec::new();

for _language in ALL_LANGUAGES {
all_translations.push(Vec::new());
}

for package in project.packages() {
for key in package.keys() {
all_keys.push(key.id().into());
// Make sure we can translate everything
if !get_translations(key, &mut all_translations) {
return None;
}
}
}

// Header
write_int(&mut data.header, 1_481_460_802); // aka XMLB in LE

// Languages
write_int(
&mut data.languages,
i32::try_from(ALL_LANGUAGES.len()).expect("overflow"),
);
for language in ALL_LANGUAGES {
write_string(&mut data.languages, language);
}

// compute offsets after finalizing languages

// Keys
write_int(
&mut data.keys,
i32::try_from(all_keys.len()).expect("overflow"),
);
for key in &all_keys {
write_string(&mut data.keys, key);
}

// Languages
for translations in all_translations {
debug_assert_eq!(translations.len(), all_keys.len());
let mut translation_buffer: Vec<u8> = Vec::new();
write_int(
&mut translation_buffer,
i32::try_from(translations.len()).expect("overflow"),
);
for string in translations {
write_string(&mut translation_buffer, &string);
}
data.translations.push(translation_buffer);
}

// Offsets
let offset_size_estimate = 4 + 4 * ALL_LANGUAGES.len();
let mut rolling_offset =
data.header.len() + data.languages.len() + offset_size_estimate + data.keys.len();

write_int(
&mut data.offsets,
i32::try_from(ALL_LANGUAGES.len()).expect("overflow"),
);
for translation_buffer in &data.translations {
write_int(
&mut data.offsets,
i32::try_from(rolling_offset).expect("overflow"),
);
rolling_offset += translation_buffer.len();
}

debug_assert_eq!(offset_size_estimate, data.offsets.len());

Some(data)
}

fn get_translations(key: &Key, all_translations: &mut [Vec<String>]) -> bool {
let tranlations = [key.english(), key.czech(), key.french(), key.spanish(), key.italian(), key.polish(), key.portuguese(), key.russian(), key.german(), key.korean(), key.japanese(), key.chinese(), key.chinesesimp(), key.turkish()];
debug_assert_eq!(tranlations.len(), ALL_LANGUAGES.len()); // needs to be synced

for (index, result) in tranlations.into_iter().enumerate() {
if let Some(native) = result {
all_translations[index].push(native.into());
} else if let Some(original) = key.original() {
all_translations[index].push(original.into());
} else if let Some(english) = key.english() {
all_translations[index].push(english.into());
} else {
// If we don't have some kind of default value to use, we should just not do the conversion
return false;
}
}

true
}

0 comments on commit a527196

Please sign in to comment.