hexomc_lib/install/
neoforge.rs1use 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
14pub 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 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
37pub 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 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
73fn 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}