Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stringtable: rapify #842

Merged
merged 7 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
161 changes: 161 additions & 0 deletions libs/stringtable/src/rapify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
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 package_inner in package.containers() { // ugh
for key in package_inner.keys() {
all_keys.push(key.id().into());
// Make sure we can translate everything
if !get_translations(key, &mut all_translations) {
return None;
}
}
}
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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BI's binarizer places an empty string here.
But for HEMTT its definitely better if it complains, but it should say where the missing string is? Probably the stringtable linter already does that?

}
}

true
}
Loading