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