Skip to main content

minecraft_java_rs_core/launcher/
game_data.rs

1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    error::LaunchError,
7    models::{java::JavaFileItem, loader::LoaderType, minecraft::{AssetItem, MinecraftVersionJson}},
8};
9
10// ── GameData ──────────────────────────────────────────────────────────────────
11
12#[derive(Debug, Clone, Deserialize, Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct GameData {
15    pub minecraft_json: MinecraftVersionJson,
16    /// Loader profile merged on top of the base version JSON (raw JSON value
17    /// to avoid coupling game_data to a specific loader type).
18    #[serde(default)]
19    pub minecraft_loader: Option<serde_json::Value>,
20    pub minecraft_version: String,
21    pub minecraft_java: JavaInfo,
22    /// Extra library JARs added by the mod loader; included in the classpath.
23    #[serde(default)]
24    pub loader_libraries: Vec<AssetItem>,
25    /// Main class override set by the mod loader.
26    #[serde(default)]
27    pub loader_main_class: Option<String>,
28    /// Loader version id, e.g. `"1.20.4-forge-47.4.20"`.
29    #[serde(default)]
30    pub loader_version_id: Option<String>,
31    /// Which loader type was installed (used for Forge-specific JVM flags).
32    #[serde(default)]
33    pub loader_type: Option<LoaderType>,
34    /// Extra plain-string game args contributed by the loader
35    /// (from `minecraftArguments` or `arguments.game` in the loader JSON).
36    #[serde(default)]
37    pub loader_extra_game_args: Vec<String>,
38    /// Extra JVM args from the loader version JSON (`arguments.jvm`), pre-resolved.
39    #[serde(default)]
40    pub loader_extra_jvm_args: Vec<String>,
41}
42
43#[derive(Debug, Clone, Deserialize, Serialize)]
44pub struct JavaInfo {
45    pub files: Vec<JavaFileItem>,
46    pub path: String,
47}
48
49// ── Persistence ───────────────────────────────────────────────────────────────
50
51const GAME_DATA_FILE: &str = "gameData.json";
52
53pub async fn load_game_data(dir: &Path) -> Result<GameData, LaunchError> {
54    let path = dir.join(GAME_DATA_FILE);
55    let raw = tokio::fs::read_to_string(&path).await?;
56    let data = serde_json::from_str(&raw)?;
57    Ok(data)
58}
59
60pub async fn save_game_data(dir: &Path, data: &GameData) -> Result<(), LaunchError> {
61    tokio::fs::create_dir_all(dir).await?;
62    let json = serde_json::to_string_pretty(data)?;
63    tokio::fs::write(dir.join(GAME_DATA_FILE), json).await?;
64    Ok(())
65}
66
67// ── Tests ─────────────────────────────────────────────────────────────────────
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use tempfile::TempDir;
73
74    fn make_game_data() -> GameData {
75        use crate::models::minecraft::{
76            AssetIndexRef, DownloadArtifact, MinecraftVersionJson, VersionDownloads,
77        };
78        GameData {
79            minecraft_version: "1.20.4".into(),
80            minecraft_loader: None,
81            minecraft_java: JavaInfo { files: vec![], path: "/usr/bin/java".into() },
82            loader_libraries: vec![],
83            loader_main_class: None,
84            loader_version_id: None,
85            loader_type: None,
86            loader_extra_game_args: vec![],
87            loader_extra_jvm_args: vec![],
88            minecraft_json: MinecraftVersionJson {
89                id: "1.20.4".into(),
90                version_type: "release".into(),
91                assets: Some("16".into()),
92                asset_index: Some(AssetIndexRef {
93                    id: "16".into(),
94                    url: "https://example.com/16.json".into(),
95                    sha1: "abc123".into(),
96                    size: 0,
97                    total_size: None,
98                }),
99                downloads: Some(VersionDownloads {
100                    client: DownloadArtifact {
101                        url: "https://example.com/client.jar".into(),
102                        sha1: "def456".into(),
103                        size: 0,
104                    },
105                    server: None,
106                    client_mappings: None,
107                    server_mappings: None,
108                }),
109                libraries: vec![],
110                arguments: None,
111                minecraft_arguments: None,
112                java_version: None,
113                main_class: Some("net.minecraft.client.main.Main".into()),
114                has_natives: false,
115            },
116        }
117    }
118
119    #[tokio::test]
120    async fn round_trip_game_data() {
121        let dir = TempDir::new().unwrap();
122        let original = make_game_data();
123        save_game_data(dir.path(), &original).await.unwrap();
124        let loaded = load_game_data(dir.path()).await.unwrap();
125        assert_eq!(loaded.minecraft_version, "1.20.4");
126        assert_eq!(loaded.minecraft_java.path, "/usr/bin/java");
127        assert!(loaded.minecraft_loader.is_none());
128    }
129
130    #[tokio::test]
131    async fn load_missing_file_returns_error() {
132        let dir = TempDir::new().unwrap();
133        let result = load_game_data(dir.path()).await;
134        assert!(result.is_err());
135    }
136
137    #[tokio::test]
138    async fn save_creates_missing_dir() {
139        let dir = TempDir::new().unwrap();
140        let nested = dir.path().join("a").join("b").join("c");
141        let data = make_game_data();
142        save_game_data(&nested, &data).await.unwrap();
143        assert!(nested.join("gameData.json").exists());
144    }
145}