Skip to main content

fromsoftware_shared/
game_version.rs

1use 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}