minecraft_java_rs_core/game/
version.rs1use crate::error::LaunchError;
2use crate::launcher::options::LaunchOptions;
3use crate::models::minecraft::{MinecraftVersionJson, MojangVersionManifest};
4use crate::net::http::fetch_json;
5
6const MANIFEST_URL: &str =
7 "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json";
8
9pub async fn get_version_json(
18 options: &LaunchOptions,
19 client: &reqwest::Client,
20) -> Result<MinecraftVersionJson, LaunchError> {
21 fetch_version_json(MANIFEST_URL, options, client).await
22}
23
24pub(crate) async fn fetch_version_json(
27 manifest_base: &str,
28 options: &LaunchOptions,
29 client: &reqwest::Client,
30) -> Result<MinecraftVersionJson, LaunchError> {
31 let ts = std::time::SystemTime::now()
33 .duration_since(std::time::UNIX_EPOCH)
34 .map(|d| d.as_millis())
35 .unwrap_or(0);
36 let manifest_url = format!("{manifest_base}?_t={ts}");
37
38 let manifest: MojangVersionManifest = fetch_json(client, &manifest_url)
39 .await
40 .map_err(LaunchError::InvalidData)?;
41
42 let version = resolve_alias(&options.version, &manifest);
43
44 let entry = manifest
45 .versions
46 .iter()
47 .find(|v| v.id == version)
48 .ok_or_else(|| LaunchError::VersionNotFound(version.clone()))?;
49
50 let mut version_json: MinecraftVersionJson = fetch_json(client, &entry.url)
51 .await
52 .map_err(LaunchError::InvalidData)?;
53
54 if is_linux_arm() {
55 crate::game::lwjgl_native::process_json(&mut version_json)?;
56 }
57
58 Ok(version_json)
59}
60
61fn resolve_alias(version: &str, manifest: &MojangVersionManifest) -> String {
63 match version {
64 "latest_release" | "r" | "lr" => manifest.latest.release.clone(),
65 "latest_snapshot" | "s" | "ls" => manifest.latest.snapshot.clone(),
66 other => other.to_string(),
67 }
68}
69
70fn is_linux_arm() -> bool {
75 std::env::consts::OS == "linux"
76 && matches!(std::env::consts::ARCH, "aarch64" | "arm")
77}
78
79#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::models::minecraft::{LatestVersions, MojangVersionManifest, VersionEntry};
85
86 fn manifest(release: &str, snapshot: &str) -> MojangVersionManifest {
87 MojangVersionManifest {
88 latest: LatestVersions {
89 release: release.to_string(),
90 snapshot: snapshot.to_string(),
91 },
92 versions: vec![
93 make_entry("1.20.4"),
94 make_entry("24w14a"),
95 ],
96 }
97 }
98
99 fn make_entry(id: &str) -> VersionEntry {
100 VersionEntry {
101 id: id.to_string(),
102 version_type: "release".to_string(),
103 url: format!("https://example.com/{id}.json"),
104 time: String::new(),
105 release_time: String::new(),
106 }
107 }
108
109 #[test]
110 fn alias_latest_release() {
111 let m = manifest("1.20.4", "24w14a");
112 assert_eq!(resolve_alias("latest_release", &m), "1.20.4");
113 assert_eq!(resolve_alias("r", &m), "1.20.4");
114 assert_eq!(resolve_alias("lr", &m), "1.20.4");
115 }
116
117 #[test]
118 fn alias_latest_snapshot() {
119 let m = manifest("1.20.4", "24w14a");
120 assert_eq!(resolve_alias("latest_snapshot", &m), "24w14a");
121 assert_eq!(resolve_alias("s", &m), "24w14a");
122 assert_eq!(resolve_alias("ls", &m), "24w14a");
123 }
124
125 #[test]
126 fn concrete_version_passes_through() {
127 let m = manifest("1.20.4", "24w14a");
128 assert_eq!(resolve_alias("1.19.4", &m), "1.19.4");
129 }
130
131 const MOCK_MANIFEST_TEMPLATE: &str = r#"{
136 "latest": { "release": "1.20.4", "snapshot": "1.20.4" },
137 "versions": [
138 {
139 "id": "1.20.4",
140 "type": "release",
141 "url": "VERSION_URL",
142 "time": "2024-01-22T10:00:00+00:00",
143 "releaseTime": "2024-01-22T10:00:00+00:00"
144 }
145 ]
146 }"#;
147
148 const MOCK_VERSION_JSON: &str = r#"{
149 "id": "1.20.4",
150 "type": "release",
151 "assets": "16",
152 "libraries": [],
153 "mainClass": "net.minecraft.client.main.Main",
154 "assetIndex": {
155 "id": "16",
156 "sha1": "abc123",
157 "size": 100,
158 "url": "https://resources.example.com/16.json"
159 },
160 "downloads": {
161 "client": {
162 "sha1": "def456",
163 "size": 1000,
164 "url": "https://resources.example.com/client.jar"
165 }
166 }
167 }"#;
168
169 fn mock_options(version: &str) -> LaunchOptions {
170 use crate::launcher::options::{JavaOptions, LoaderConfig, MemoryConfig, ScreenConfig};
171 use crate::models::minecraft::Authenticator;
172 LaunchOptions {
173 path: std::path::PathBuf::from("/tmp/mc-test"),
174 version: version.to_string(),
175 authenticator: Authenticator {
176 access_token: String::new(),
177 name: "TestUser".into(),
178 uuid: "test-uuid".into(),
179 xbox_account: None,
180 user_properties: None,
181 client_id: None,
182 client_token: None,
183 },
184 timeout_secs: 5,
185 download_concurrency: 1,
186 verify_concurrency: 4,
187 memory: MemoryConfig::default(),
188 java: JavaOptions::default(),
189 loader: LoaderConfig::default(),
190 screen: ScreenConfig::default(),
191 verify: false,
192 game_args: vec![],
193 jvm_args: vec![],
194 instance: None,
195 url: None,
196 mcp: None,
197 intel_enabled_mac: false,
198 bypass_offline: false,
199 skip_bundle_check: false,
200 }
201 }
202
203 #[tokio::test]
204 async fn fetch_version_json_from_mock_server_concrete_version() {
205 use wiremock::matchers::{method, path};
206 use wiremock::{Mock, MockServer, ResponseTemplate};
207
208 let server = MockServer::start().await;
209
210 let version_url = format!("{}/versions/1.20.4.json", server.uri());
212 let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
213 Mock::given(method("GET"))
214 .and(path("/manifest.json"))
215 .respond_with(
216 ResponseTemplate::new(200)
217 .insert_header("content-type", "application/json")
218 .set_body_string(manifest_body),
219 )
220 .mount(&server)
221 .await;
222
223 Mock::given(method("GET"))
225 .and(path("/versions/1.20.4.json"))
226 .respond_with(
227 ResponseTemplate::new(200)
228 .insert_header("content-type", "application/json")
229 .set_body_string(MOCK_VERSION_JSON),
230 )
231 .mount(&server)
232 .await;
233
234 let manifest_url = format!("{}/manifest.json", server.uri());
235 let client = reqwest::Client::new();
236 let options = mock_options("1.20.4");
237
238 let result = fetch_version_json(&manifest_url, &options, &client)
239 .await
240 .unwrap();
241
242 assert_eq!(result.id, "1.20.4");
243 assert_eq!(result.version_type, "release");
244 assert_eq!(result.main_class.as_deref(), Some("net.minecraft.client.main.Main"));
245 assert!(result.libraries.is_empty());
246 }
247
248 #[tokio::test]
249 async fn fetch_version_json_resolves_latest_release_alias() {
250 use wiremock::matchers::{method, path};
251 use wiremock::{Mock, MockServer, ResponseTemplate};
252
253 let server = MockServer::start().await;
254
255 let version_url = format!("{}/versions/1.20.4.json", server.uri());
256 let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
257 Mock::given(method("GET"))
258 .and(path("/manifest.json"))
259 .respond_with(
260 ResponseTemplate::new(200)
261 .insert_header("content-type", "application/json")
262 .set_body_string(manifest_body),
263 )
264 .mount(&server)
265 .await;
266 Mock::given(method("GET"))
267 .and(path("/versions/1.20.4.json"))
268 .respond_with(
269 ResponseTemplate::new(200)
270 .insert_header("content-type", "application/json")
271 .set_body_string(MOCK_VERSION_JSON),
272 )
273 .mount(&server)
274 .await;
275
276 let manifest_url = format!("{}/manifest.json", server.uri());
277 let client = reqwest::Client::new();
278 let options = mock_options("latest_release");
280
281 let result = fetch_version_json(&manifest_url, &options, &client)
282 .await
283 .unwrap();
284
285 assert_eq!(result.id, "1.20.4");
286 }
287
288 #[tokio::test]
289 async fn fetch_version_json_returns_error_for_missing_version() {
290 use wiremock::matchers::{method, path};
291 use wiremock::{Mock, MockServer, ResponseTemplate};
292
293 let server = MockServer::start().await;
294
295 let version_url = format!("{}/versions/1.20.4.json", server.uri());
296 let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
297 Mock::given(method("GET"))
298 .and(path("/manifest.json"))
299 .respond_with(
300 ResponseTemplate::new(200)
301 .insert_header("content-type", "application/json")
302 .set_body_string(manifest_body),
303 )
304 .mount(&server)
305 .await;
306
307 let manifest_url = format!("{}/manifest.json", server.uri());
308 let client = reqwest::Client::new();
309 let options = mock_options("99.99.99");
310
311 let result = fetch_version_json(&manifest_url, &options, &client).await;
312
313 assert!(matches!(result, Err(LaunchError::VersionNotFound(_))));
314 }
315
316 #[tokio::test]
317 async fn fetch_version_json_returns_http_error_on_500() {
318 use wiremock::matchers::{method, path};
319 use wiremock::{Mock, MockServer, ResponseTemplate};
320
321 let server = MockServer::start().await;
322 Mock::given(method("GET"))
323 .and(path("/manifest.json"))
324 .respond_with(ResponseTemplate::new(500))
325 .mount(&server)
326 .await;
327
328 let manifest_url = format!("{}/manifest.json", server.uri());
329 let client = reqwest::Client::new();
330 let options = mock_options("1.20.4");
331
332 let result = fetch_version_json(&manifest_url, &options, &client).await;
333 assert!(result.is_err());
334 }
335}