Skip to main content

hexomc_lib/install/
neoforge.rs

1use std::path::Path;
2use xml::reader::{EventReader, XmlEvent};
3
4use crate::{
5    error::{HexoError, Result},
6    install::{forge::install_forge_like, vanilla::LoaderType},
7};
8
9const NEOFORGE_MAVEN_BASE: &str =
10    "https://maven.neoforged.net/releases/net/neoforged/neoforge";
11const NEOFORGE_META_XML: &str =
12    "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml";
13
14/// NeoForge versions for the given MC version.
15///
16/// NeoForge version format: `{mc_minor}.{patch}` (e.g. MC 1.21.1 -> NeoForge 21.1.x).
17pub async fn get_neoforge_versions(mc_version: &str) -> Result<Vec<String>> {
18    let client = reqwest::Client::new();
19    let xml_text = client
20        .get(NEOFORGE_META_XML)
21        .send()
22        .await?
23        .text()
24        .await?;
25
26    // Derive the NeoForge version prefix from the MC version, e.g. "1.21.1" -> "21.1.".
27    let prefix = mc_version_to_neoforge_prefix(mc_version);
28
29    let versions = parse_maven_xml_versions(&xml_text)
30        .into_iter()
31        .filter(|v| v.starts_with(&prefix))
32        .collect();
33
34    Ok(versions)
35}
36
37/// Install NeoForge (same flow as Forge).
38pub async fn install_neoforge(
39    mc_version: &str,
40    neoforge_version: Option<&str>,
41    instance_name: &str,
42    java_path: &Path,
43    base_dir: &Path,
44) -> Result<()> {
45    let versions = get_neoforge_versions(mc_version).await?;
46    let nf_ver = if let Some(v) = neoforge_version {
47        v.to_string()
48    } else {
49        // List is ordered oldest -> newest.
50        versions
51            .into_iter()
52            .last()
53            .ok_or_else(|| HexoError::VersionNotFound(format!("NeoForge for {}", mc_version)))?
54    };
55
56    let installer_url = format!(
57        "{}/{}/neoforge-{}-installer.jar",
58        NEOFORGE_MAVEN_BASE, nf_ver, nf_ver
59    );
60
61    install_forge_like(
62        mc_version,
63        &nf_ver,
64        &installer_url,
65        instance_name,
66        java_path,
67        base_dir,
68        LoaderType::NeoForge,
69    )
70    .await
71}
72
73/// "1.21.1" -> "21.1.", "1.21" -> "21."
74fn mc_version_to_neoforge_prefix(mc_version: &str) -> String {
75    let parts: Vec<&str> = mc_version.split('.').collect();
76    if parts.len() >= 3 {
77        format!("{}.{}.", parts[1], parts[2])
78    } else if parts.len() == 2 {
79        format!("{}.", parts[1])
80    } else {
81        String::new()
82    }
83}
84
85fn parse_maven_xml_versions(xml_text: &str) -> Vec<String> {
86    let parser = EventReader::from_str(xml_text);
87    let mut in_version = false;
88    let mut versions = Vec::new();
89
90    for event in parser {
91        match event {
92            Ok(XmlEvent::StartElement { name, .. }) if name.local_name == "version" => {
93                in_version = true;
94            }
95            Ok(XmlEvent::Characters(text)) if in_version => {
96                versions.push(text);
97                in_version = false;
98            }
99            Ok(XmlEvent::EndElement { name }) if name.local_name == "version" => {
100                in_version = false;
101            }
102            _ => {}
103        }
104    }
105    versions
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn prefix_three_part_version() {
114        assert_eq!(mc_version_to_neoforge_prefix("1.21.1"), "21.1.");
115        assert_eq!(mc_version_to_neoforge_prefix("1.20.4"), "20.4.");
116    }
117
118    #[test]
119    fn prefix_two_part_version() {
120        assert_eq!(mc_version_to_neoforge_prefix("1.21"), "21.");
121    }
122
123    #[test]
124    fn prefix_invalid_version() {
125        assert_eq!(mc_version_to_neoforge_prefix("1"), "");
126    }
127
128    #[test]
129    fn parse_maven_xml() {
130        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
131<metadata>
132  <versioning>
133    <versions>
134      <version>21.1.10</version>
135      <version>21.1.11</version>
136      <version>21.4.0</version>
137    </versions>
138    <latest>21.4.0</latest>
139    <release>21.4.0</release>
140  </versioning>
141</metadata>"#;
142        let versions = parse_maven_xml_versions(xml);
143        assert_eq!(versions, vec!["21.1.10", "21.1.11", "21.4.0"]);
144    }
145
146    #[test]
147    fn filter_versions_by_prefix() {
148        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
149<metadata>
150  <versioning>
151    <versions>
152      <version>21.1.10</version>
153      <version>21.1.11</version>
154      <version>21.4.0</version>
155    </versions>
156  </versioning>
157</metadata>"#;
158        let prefix = mc_version_to_neoforge_prefix("1.21.1");
159        let versions: Vec<_> = parse_maven_xml_versions(xml)
160            .into_iter()
161            .filter(|v| v.starts_with(&prefix))
162            .collect();
163        assert_eq!(versions, vec!["21.1.10", "21.1.11"]);
164    }
165}