aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core/manifest.rs
blob: 2fea811d99d2b8d11f26851c29bf6e891ad7347d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::path::PathBuf;

use crate::core::game_version::GameVersion;

#[derive(Debug, Deserialize, Serialize)]
pub struct VersionManifest {
    pub latest: Latest,
    pub versions: Vec<Version>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Latest {
    pub release: String,
    pub snapshot: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Version {
    pub id: String,
    #[serde(rename = "type")]
    pub type_: String,
    pub url: String,
    pub time: String,
    #[serde(rename = "releaseTime")]
    pub release_time: String,
}

pub async fn fetch_version_manifest() -> Result<VersionManifest, Box<dyn Error + Send + Sync>> {
    let url = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
    let resp = reqwest::get(url).await?.json::<VersionManifest>().await?;
    Ok(resp)
}

/// Load a version JSON from the local versions directory.
///
/// This is used for loading both vanilla and modded versions that have been
/// previously downloaded or installed.
///
/// # Arguments
/// * `game_dir` - The .minecraft directory path
/// * `version_id` - The version ID to load
///
/// # Returns
/// The parsed `GameVersion` if found, or an error if not found.
pub async fn load_local_version(
    game_dir: &PathBuf,
    version_id: &str,
) -> Result<GameVersion, Box<dyn Error + Send + Sync>> {
    let json_path = game_dir
        .join("versions")
        .join(version_id)
        .join(format!("{}.json", version_id));

    if !json_path.exists() {
        return Err(format!("Version {} not found locally", version_id).into());
    }

    let content = tokio::fs::read_to_string(&json_path).await?;
    let version: GameVersion = serde_json::from_str(&content)?;
    Ok(version)
}

/// Fetch a version JSON from Mojang's servers.
///
/// # Arguments
/// * `version_id` - The version ID to fetch
///
/// # Returns
/// The parsed `GameVersion` from Mojang's API.
pub async fn fetch_vanilla_version(
    version_id: &str,
) -> Result<GameVersion, Box<dyn Error + Send + Sync>> {
    // First, get the manifest to find the version URL
    let manifest = fetch_version_manifest().await?;
    
    let version_entry = manifest
        .versions
        .iter()
        .find(|v| v.id == version_id)
        .ok_or_else(|| format!("Version {} not found in manifest", version_id))?;

    // Fetch the actual version JSON
    let resp = reqwest::get(&version_entry.url)
        .await?
        .json::<GameVersion>()
        .await?;
    
    Ok(resp)
}

/// Load a version, checking local first, then fetching from remote if needed.
///
/// For modded versions (those with `inheritsFrom`), this will also resolve
/// the inheritance chain.
///
/// # Arguments
/// * `game_dir` - The .minecraft directory path
/// * `version_id` - The version ID to load
///
/// # Returns
/// A fully resolved `GameVersion` ready for launching.
pub async fn load_version(
    game_dir: &PathBuf,
    version_id: &str,
) -> Result<GameVersion, Box<dyn Error + Send + Sync>> {
    // Try loading from local first
    let mut version = match load_local_version(game_dir, version_id).await {
        Ok(v) => v,
        Err(_) => {
            // Not found locally, try fetching from Mojang
            fetch_vanilla_version(version_id).await?
        }
    };

    // If this version inherits from another, resolve the inheritance iteratively
    while let Some(parent_id) = version.inherits_from.clone() {
        // Load the parent version
        let parent = match load_local_version(game_dir, &parent_id).await {
            Ok(v) => v,
            Err(_) => fetch_vanilla_version(&parent_id).await?,
        };
        
        // Merge child into parent
        version = crate::core::version_merge::merge_versions(version, parent);
    }

    Ok(version)
}

/// Save a version JSON to the local versions directory.
///
/// # Arguments
/// * `game_dir` - The .minecraft directory path
/// * `version` - The version to save
///
/// # Returns
/// The path where the JSON was saved.
pub async fn save_local_version(
    game_dir: &PathBuf,
    version: &GameVersion,
) -> Result<PathBuf, Box<dyn Error + Send + Sync>> {
    let version_dir = game_dir.join("versions").join(&version.id);
    tokio::fs::create_dir_all(&version_dir).await?;

    let json_path = version_dir.join(format!("{}.json", version.id));
    let content = serde_json::to_string_pretty(version)?;
    tokio::fs::write(&json_path, content).await?;

    Ok(json_path)
}

/// List all locally installed versions.
///
/// # Arguments
/// * `game_dir` - The .minecraft directory path
///
/// # Returns
/// A list of version IDs found in the versions directory.
pub async fn list_local_versions(
    game_dir: &PathBuf,
) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> {
    let versions_dir = game_dir.join("versions");
    let mut versions = Vec::new();

    if !versions_dir.exists() {
        return Ok(versions);
    }

    let mut entries = tokio::fs::read_dir(&versions_dir).await?;
    while let Some(entry) = entries.next_entry().await? {
        if entry.file_type().await?.is_dir() {
            let name = entry.file_name().to_string_lossy().to_string();
            let json_path = entry.path().join(format!("{}.json", name));
            if json_path.exists() {
                versions.push(name);
            }
        }
    }

    Ok(versions)
}