fromsoftware_shared/
game_version.rs1use pelite::pe64::{Pe, PeView};
2use thiserror::Error;
3
4pub const LANG_ID_EN: u16 = 0x0009;
5pub const LANG_ID_JP: u16 = 0x0011;
6
7#[derive(Debug, Error)]
8pub enum DetectError {
9 #[error("Executable doesn't contain version metadata")]
10 MissingVersionMetadata,
11 #[error("Executable doesn't contain language metadata")]
12 MissingLanguageMetadata,
13 #[error("Executable doesn't contain product name metadata")]
14 MissingProductName,
15 #[error("Expected executable name to be \"{expected}\", was \"{actual}\"")]
16 WrongProduct {
17 expected: &'static str,
18 actual: String,
19 },
20 #[error(
21 "Expected executable language ID to be {LANG_ID_EN:#04x} or {LANG_ID_JP:#04x}, was {0:#04x}"
22 )]
23 UnsupportedLanguage(u16),
24 #[error("Unsupported game version {0}")]
25 UnsupportedVersion(String),
26}
27
28pub trait GameVersion: Sized {
29 const NAME: &'static str;
30
31 fn from_lang_version(lang_id: u16, version: &str) -> Option<Self>;
32
33 fn detect(module: &PeView) -> Result<Self, DetectError> {
34 let resources = module.resources().unwrap();
35 let info = resources.version_info().unwrap();
36
37 let product_version = info
38 .fixed()
39 .ok_or(DetectError::MissingVersionMetadata)?
40 .dwProductVersion;
41 let version = format!(
42 "{}.{}.{}.{}",
43 product_version.Major,
44 product_version.Minor,
45 product_version.Patch,
46 product_version.Build,
47 );
48
49 let language = *info
50 .translation()
51 .first()
52 .ok_or(DetectError::MissingLanguageMetadata)?;
53 let mut product_name: Option<String> = None;
54 info.strings(language, |k, v| {
55 if k == "ProductName" {
56 product_name = Some(v.to_string());
57 }
58 });
59
60 let product = product_name.ok_or(DetectError::MissingProductName)?;
61 if normalize(&product) != Self::NAME {
62 return Err(DetectError::WrongProduct {
63 expected: Self::NAME,
64 actual: product,
65 });
66 }
67
68 let lang_id = language.lang_id & 0x03FF;
69 if lang_id != LANG_ID_EN && lang_id != LANG_ID_JP {
70 return Err(DetectError::UnsupportedLanguage(lang_id));
71 }
72
73 Self::from_lang_version(lang_id, &version).ok_or(DetectError::UnsupportedVersion(version))
74 }
75}
76
77fn normalize(product: &str) -> String {
78 product
79 .chars()
80 .filter(|c| c.is_alphanumeric() || c.is_whitespace())
81 .collect::<String>()
82 .to_lowercase()
83}