use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct ItemNameEntry {
pub np_key: String,
pub category: String,
pub uuid: String,
pub display_name: String,
}
pub fn extract_from_binary(data: &[u8]) -> Vec<ItemNameEntry> {
let content = match crate::NcsContent::parse(data) {
Some(c) => c,
None => return Vec::new(),
};
if content.type_name() != "inv_name_part" {
return Vec::new();
}
let mut entries = Vec::new();
let strings = &content.strings;
let mut i = 0;
while i + 1 < strings.len() {
let np_key = &strings[i];
let guid_str = &strings[i + 1];
if !np_key.starts_with("np_") && !np_key.starts_with("NP_") {
i += 1;
continue;
}
if let Some(entry) = parse_guid_entry(np_key, guid_str) {
entries.push(entry);
i += 2;
} else {
i += 1;
}
}
entries
}
fn parse_guid_entry(np_key: &str, guid_str: &str) -> Option<ItemNameEntry> {
let parts: Vec<&str> = guid_str.splitn(3, ", ").collect();
if parts.len() != 3 {
return None;
}
let uuid = parts[1];
if uuid.len() != 32 || !uuid.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
let display_name = parts[2].trim();
if display_name.is_empty() {
return None;
}
Some(ItemNameEntry {
np_key: np_key.to_lowercase(),
category: parts[0].to_string(),
uuid: uuid.to_string(),
display_name: display_name.to_string(),
})
}
pub fn extract_from_directory(ncs_dir: &Path) -> Vec<ItemNameEntry> {
let mut best: Vec<ItemNameEntry> = Vec::new();
for entry in walkdir::WalkDir::new(ncs_dir)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
let fname = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default();
if !fname.starts_with("inv_name_part") {
continue;
}
if let Ok(data) = std::fs::read(path) {
let entries = extract_from_binary(&data);
if entries.len() > best.len() {
best = entries;
}
}
}
best
}
pub fn build_name_map(entries: &[ItemNameEntry]) -> HashMap<String, String> {
let mut by_key: HashMap<String, Vec<&ItemNameEntry>> = HashMap::new();
for entry in entries {
by_key.entry(entry.np_key.clone()).or_default().push(entry);
}
let mut map = HashMap::new();
for (key, variants) in by_key {
if variants.len() == 1 {
map.insert(key, variants[0].display_name.clone());
} else {
let best = pick_best_variant(&variants);
map.insert(key, best.display_name.clone());
}
}
map
}
fn pick_best_variant<'a>(variants: &[&'a ItemNameEntry]) -> &'a ItemNameEntry {
const DEFAULT_UUID: &str = "641B14834BAE08173BD6AAACEDAB0310";
let variant_prefixes = ["upgraded ", "big encore ", "badass ", "vile "];
let mut best = variants[0];
let mut best_score: i32 = i32::MIN;
for &v in variants {
let mut score: i32 = 0;
let name_lower = v.display_name.to_lowercase();
if v.uuid == DEFAULT_UUID {
score += 100;
}
if variant_prefixes.iter().any(|p| name_lower.starts_with(p)) {
score -= 50;
}
score -= v.display_name.len() as i32;
if score > best_score {
best_score = score;
best = v;
}
}
best
}
pub fn write_tsv(entries: &[ItemNameEntry], path: &Path) -> std::io::Result<()> {
use std::io::Write;
let map = build_name_map(entries);
let mut pairs: Vec<_> = map.into_iter().collect();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
let mut f = std::fs::File::create(path)?;
for (key, name) in &pairs {
writeln!(f, "{}\t{}", key, name)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_guid_entry() {
let entry = parse_guid_entry(
"np_anarchy",
"Uni_Inv_TED_SG_Anarchy, 641B14834BAE08173BD6AAACEDAB0310, Anarchy",
)
.unwrap();
assert_eq!(entry.np_key, "np_anarchy");
assert_eq!(entry.category, "Uni_Inv_TED_SG_Anarchy");
assert_eq!(entry.display_name, "Anarchy");
}
#[test]
fn test_parse_guid_entry_invalid() {
assert!(parse_guid_entry("np_test", "not a guid").is_none());
assert!(parse_guid_entry("np_test", "Cat, SHORT, Name").is_none());
}
#[test]
fn test_pick_best_variant_prefers_default_uuid() {
let default = ItemNameEntry {
np_key: "np_test".into(),
category: "Uni_Test".into(),
uuid: "641B14834BAE08173BD6AAACEDAB0310".into(),
display_name: "Anarchy".into(),
};
let variant = ItemNameEntry {
np_key: "np_test".into(),
category: "Uni_Test".into(),
uuid: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1".into(),
display_name: "Forsaken Chaos".into(),
};
let best = pick_best_variant(&[&variant, &default]);
assert_eq!(best.display_name, "Anarchy");
}
}