aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core/version_merge.rs
blob: 098d2717970785bcebe5a3d1f75463b81a08d68e (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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
//! Version merging utilities for mod loaders.
//!
//! Mod loaders like Fabric and Forge create "partial" version JSON files that
//! inherit from vanilla Minecraft versions via the `inheritsFrom` field.
//! This module provides functionality to merge these partial versions with
//! their parent versions to create a complete, launchable version profile.

use crate::core::game_version::{Arguments, GameVersion};
use std::error::Error;

/// Merge a child version (mod loader) with its parent version (vanilla).
///
/// The merging follows these rules:
/// 1. Child's `mainClass` overrides parent's
/// 2. Child's libraries are prepended to parent's (mod loader classes take priority)
/// 3. Arguments are merged (child's additions come after parent's)
/// 4. Parent provides `downloads`, `assetIndex`, `javaVersion` if child doesn't have them
///
/// # Arguments
/// * `child` - The mod loader version (e.g., Fabric)
/// * `parent` - The vanilla Minecraft version
///
/// # Returns
/// A merged `GameVersion` that can be used for launching.
pub fn merge_versions(child: GameVersion, parent: GameVersion) -> GameVersion {
    // Libraries: child libraries first (mod loader takes priority in classpath)
    let mut merged_libraries = child.libraries;
    merged_libraries.extend(parent.libraries);

    // Arguments: merge both game and JVM arguments
    let merged_arguments = merge_arguments(child.arguments, parent.arguments);

    GameVersion {
        id: child.id,
        // Use child's downloads if present, otherwise parent's
        downloads: child.downloads.or(parent.downloads),
        // Use child's asset_index if present, otherwise parent's
        asset_index: child.asset_index.or(parent.asset_index),
        libraries: merged_libraries,
        // Child's main class always takes priority (this is the mod loader entry point)
        main_class: child.main_class,
        // Prefer child's minecraft_arguments, fall back to parent's
        minecraft_arguments: child.minecraft_arguments.or(parent.minecraft_arguments),
        arguments: merged_arguments,
        // Use child's java_version if specified, otherwise parent's
        java_version: child.java_version.or(parent.java_version),
        // Clear inheritsFrom since we've now merged
        inherits_from: None,
        // Use child's assets field if present, otherwise parent's
        assets: child.assets.or(parent.assets),
        // Use parent's version type if child doesn't specify
        version_type: child.version_type.or(parent.version_type),
    }
}

/// Merge argument objects from child and parent versions.
///
/// Both game and JVM arguments are merged, with parent arguments coming first
/// and child arguments appended (child can add additional arguments).
fn merge_arguments(child: Option<Arguments>, parent: Option<Arguments>) -> Option<Arguments> {
    match (child, parent) {
        (None, None) => None,
        (Some(c), None) => Some(c),
        (None, Some(p)) => Some(p),
        (Some(c), Some(p)) => Some(Arguments {
            game: merge_json_arrays(p.game, c.game),
            jvm: merge_json_arrays(p.jvm, c.jvm),
        }),
    }
}

/// Merge two JSON arrays (used for arguments).
///
/// Parent array comes first, child array is appended.
fn merge_json_arrays(
    parent: Option<serde_json::Value>,
    child: Option<serde_json::Value>,
) -> Option<serde_json::Value> {
    match (parent, child) {
        (None, None) => None,
        (Some(p), None) => Some(p),
        (None, Some(c)) => Some(c),
        (Some(p), Some(c)) => {
            if let (serde_json::Value::Array(mut p_arr), serde_json::Value::Array(c_arr)) =
                (p.clone(), c.clone())
            {
                p_arr.extend(c_arr);
                Some(serde_json::Value::Array(p_arr))
            } else {
                // If they're not arrays, prefer child
                Some(c)
            }
        }
    }
}

/// Check if a version requires inheritance resolution.
///
/// # Arguments
/// * `version` - The version to check
///
/// # Returns
/// `true` if the version has an `inheritsFrom` field that needs resolution.
#[allow(dead_code)]
pub fn needs_inheritance_resolution(version: &GameVersion) -> bool {
    version.inherits_from.is_some()
}

/// Recursively resolve version inheritance.
///
/// This function resolves the entire inheritance chain by loading parent versions
/// and merging them until a version without `inheritsFrom` is found.
///
/// # Arguments
/// * `version` - The starting version (e.g., a Fabric version)
/// * `version_loader` - A function that loads a version by ID
///
/// # Returns
/// A fully merged `GameVersion` with all inheritance resolved.
#[allow(dead_code)]
pub async fn resolve_inheritance<F, Fut>(
    version: GameVersion,
    version_loader: F,
) -> Result<GameVersion, Box<dyn Error + Send + Sync>>
where
    F: Fn(String) -> Fut,
    Fut: std::future::Future<Output = Result<GameVersion, Box<dyn Error + Send + Sync>>>,
{
    let mut current = version;

    // Keep resolving until we have no more inheritance
    while let Some(parent_id) = current.inherits_from.clone() {
        let parent = version_loader(parent_id).await?;
        current = merge_versions(current, parent);
    }

    Ok(current)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::game_version::{DownloadArtifact, Downloads, Library};

    fn create_test_library(name: &str) -> Library {
        Library {
            name: name.to_string(),
            downloads: None,
            rules: None,
            natives: None,
            url: None,
        }
    }

    #[test]
    fn test_merge_libraries_order() {
        let child = GameVersion {
            id: "fabric-1.20.4".to_string(),
            downloads: None,
            asset_index: None,
            libraries: vec![create_test_library("fabric:loader:1.0")],
            main_class: "net.fabricmc.loader.launch.knot.KnotClient".to_string(),
            minecraft_arguments: None,
            arguments: None,
            java_version: None,
            inherits_from: Some("1.20.4".to_string()),
            assets: None,
            version_type: None,
        };

        let parent = GameVersion {
            id: "1.20.4".to_string(),
            downloads: Some(Downloads {
                client: DownloadArtifact {
                    sha1: Some("abc".to_string()),
                    size: Some(1000),
                    url: "https://example.com/client.jar".to_string(),
                    path: None,
                },
                server: None,
            }),
            asset_index: None,
            libraries: vec![create_test_library("net.minecraft:client:1.20.4")],
            main_class: "net.minecraft.client.main.Main".to_string(),
            minecraft_arguments: None,
            arguments: None,
            java_version: None,
            inherits_from: None,
            assets: None,
            version_type: Some("release".to_string()),
        };

        let merged = merge_versions(child, parent);

        // Child libraries should come first
        assert_eq!(merged.libraries.len(), 2);
        assert_eq!(merged.libraries[0].name, "fabric:loader:1.0");
        assert_eq!(merged.libraries[1].name, "net.minecraft:client:1.20.4");

        // Child main class should override
        assert_eq!(
            merged.main_class,
            "net.fabricmc.loader.launch.knot.KnotClient"
        );

        // Parent downloads should be used
        assert!(merged.downloads.is_some());

        // inheritsFrom should be cleared
        assert!(merged.inherits_from.is_none());
    }

    #[test]
    fn test_needs_inheritance_resolution() {
        let with_inheritance = GameVersion {
            id: "test".to_string(),
            downloads: None,
            asset_index: None,
            libraries: vec![],
            main_class: "Main".to_string(),
            minecraft_arguments: None,
            arguments: None,
            java_version: None,
            inherits_from: Some("1.20.4".to_string()),
            assets: None,
            version_type: None,
        };

        let without_inheritance = GameVersion {
            id: "test".to_string(),
            downloads: None,
            asset_index: None,
            libraries: vec![],
            main_class: "Main".to_string(),
            minecraft_arguments: None,
            arguments: None,
            java_version: None,
            inherits_from: None,
            assets: None,
            version_type: None,
        };

        assert!(needs_inheritance_resolution(&with_inheritance));
        assert!(!needs_inheritance_resolution(&without_inheritance));
    }
}