Skip to main content

mmd_anim_format/
format.rs

1use serde::Serialize;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
4#[serde(rename_all = "camelCase")]
5pub enum MmdFormatKind {
6    Pmd,
7    Pmx,
8    Vmd,
9    Vpd,
10    Pmm,
11    Nmd,
12    X,
13    Vac,
14    Unknown,
15}
16
17pub fn detect_mmd_format(data: &[u8], file_name: Option<&str>) -> MmdFormatKind {
18    if data.starts_with(b"PMX ") {
19        return MmdFormatKind::Pmx;
20    }
21    if data.starts_with(b"Pmd") {
22        return MmdFormatKind::Pmd;
23    }
24    if data.starts_with(b"Vocaloid Motion Data") {
25        return MmdFormatKind::Vmd;
26    }
27    if data.starts_with(b"Vocaloid Pose Data") {
28        return MmdFormatKind::Vpd;
29    }
30    if data.starts_with(b"Polygon Movie maker ") {
31        return MmdFormatKind::Pmm;
32    }
33    if data.starts_with(b"xof ") {
34        return MmdFormatKind::X;
35    }
36    if looks_like_nmd(data, file_name) {
37        return MmdFormatKind::Nmd;
38    }
39    match extension(file_name).as_deref() {
40        Some("x") => MmdFormatKind::X,
41        Some("vac") => MmdFormatKind::Vac,
42        Some("nmd") => MmdFormatKind::Nmd,
43        Some("pmd") => MmdFormatKind::Pmd,
44        Some("pmx") => MmdFormatKind::Pmx,
45        Some("vmd") => MmdFormatKind::Vmd,
46        Some("vpd") => MmdFormatKind::Vpd,
47        Some("pmm") => MmdFormatKind::Pmm,
48        _ => MmdFormatKind::Unknown,
49    }
50}
51
52fn extension(file_name: Option<&str>) -> Option<String> {
53    file_name?
54        .rsplit_once('.')
55        .map(|(_, ext)| ext.trim().to_ascii_lowercase())
56}
57
58fn looks_like_nmd(data: &[u8], file_name: Option<&str>) -> bool {
59    matches!(extension(file_name).as_deref(), Some("nmd"))
60        || data.starts_with(b"NMD")
61        || data.starts_with(b"Nanoem Motion Data")
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn detects_core_mmd_formats_from_magic_bytes() {
70        assert_eq!(detect_mmd_format(b"PMX test", None), MmdFormatKind::Pmx);
71        assert_eq!(detect_mmd_format(b"Pmd\x00", None), MmdFormatKind::Pmd);
72        assert_eq!(
73            detect_mmd_format(b"Vocaloid Motion Data 0002", None),
74            MmdFormatKind::Vmd
75        );
76        assert_eq!(
77            detect_mmd_format(b"Vocaloid Pose Data file", None),
78            MmdFormatKind::Vpd
79        );
80        assert_eq!(
81            detect_mmd_format(b"Polygon Movie maker 0002", None),
82            MmdFormatKind::Pmm
83        );
84        assert_eq!(
85            detect_mmd_format(b"xof 0303txt 0032", None),
86            MmdFormatKind::X
87        );
88        assert_eq!(
89            detect_mmd_format(b"Nanoem Motion Data", None),
90            MmdFormatKind::Nmd
91        );
92    }
93
94    #[test]
95    fn falls_back_to_case_insensitive_extension() {
96        assert_eq!(
97            detect_mmd_format(b"", Some("motion.NMD")),
98            MmdFormatKind::Nmd
99        );
100        assert_eq!(
101            detect_mmd_format(b"", Some("accessory.VAC")),
102            MmdFormatKind::Vac
103        );
104        assert_eq!(
105            detect_mmd_format(b"", Some("model.PMD")),
106            MmdFormatKind::Pmd
107        );
108        assert_eq!(
109            detect_mmd_format(b"", Some("unknown.bin")),
110            MmdFormatKind::Unknown
111        );
112    }
113}