aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-14 16:37:23 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-14 16:38:35 +0800
commite861e52d43e93478690c3a20903639f7773b8ce8 (patch)
tree6b785406754e7e11a5d816508c8364d9fc5fbd68 /src-tauri/src
parent9a5e827274979b16f74d513a948b64b4edf15203 (diff)
downloadDropOut-e861e52d43e93478690c3a20903639f7773b8ce8.tar.gz
DropOut-e861e52d43e93478690c3a20903639f7773b8ce8.zip
feat: add Fabric and Forge loader support modules with version fetching and installation functionality
Diffstat (limited to 'src-tauri/src')
-rw-r--r--src-tauri/src/core/fabric.rs269
-rw-r--r--src-tauri/src/core/forge.rs333
2 files changed, 602 insertions, 0 deletions
diff --git a/src-tauri/src/core/fabric.rs b/src-tauri/src/core/fabric.rs
new file mode 100644
index 0000000..8fc960f
--- /dev/null
+++ b/src-tauri/src/core/fabric.rs
@@ -0,0 +1,269 @@
+//! Fabric Loader support module.
+//!
+//! This module provides functionality to:
+//! - Fetch available Fabric loader versions from the Fabric Meta API
+//! - Generate version JSON files for Fabric-enabled Minecraft versions
+//! - Install Fabric loader for a specific Minecraft version
+
+use serde::{Deserialize, Serialize};
+use std::error::Error;
+use std::path::PathBuf;
+
+const FABRIC_META_URL: &str = "https://meta.fabricmc.net/v2";
+
+/// Represents a Fabric loader version from the Meta API.
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct FabricLoaderVersion {
+ pub separator: String,
+ pub build: i32,
+ pub maven: String,
+ pub version: String,
+ pub stable: bool,
+}
+
+/// Represents a Fabric intermediary mapping version.
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct FabricIntermediaryVersion {
+ pub maven: String,
+ pub version: String,
+ pub stable: bool,
+}
+
+/// Represents a combined loader + intermediary version entry.
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct FabricLoaderEntry {
+ pub loader: FabricLoaderVersion,
+ pub intermediary: FabricIntermediaryVersion,
+ #[serde(rename = "launcherMeta")]
+ pub launcher_meta: FabricLauncherMeta,
+}
+
+/// Launcher metadata from Fabric Meta API.
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct FabricLauncherMeta {
+ pub version: i32,
+ pub libraries: FabricLibraries,
+ #[serde(rename = "mainClass")]
+ pub main_class: FabricMainClass,
+}
+
+/// Libraries required by Fabric loader.
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct FabricLibraries {
+ pub client: Vec<FabricLibrary>,
+ pub common: Vec<FabricLibrary>,
+ pub server: Vec<FabricLibrary>,
+}
+
+/// A single Fabric library dependency.
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct FabricLibrary {
+ pub name: String,
+ pub url: Option<String>,
+}
+
+/// Main class configuration for Fabric.
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct FabricMainClass {
+ pub client: String,
+ pub server: String,
+}
+
+/// Represents a Minecraft version supported by Fabric.
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct FabricGameVersion {
+ pub version: String,
+ pub stable: bool,
+}
+
+/// Information about an installed Fabric version.
+#[derive(Debug, Serialize, Clone)]
+pub struct InstalledFabricVersion {
+ pub id: String,
+ pub minecraft_version: String,
+ pub loader_version: String,
+ pub path: PathBuf,
+}
+
+/// Fetch all Minecraft versions supported by Fabric.
+///
+/// # Returns
+/// A list of game versions that have Fabric intermediary mappings available.
+pub async fn fetch_supported_game_versions() -> Result<Vec<FabricGameVersion>, Box<dyn Error + Send + Sync>> {
+ let url = format!("{}/versions/game", FABRIC_META_URL);
+ let resp = reqwest::get(&url)
+ .await?
+ .json::<Vec<FabricGameVersion>>()
+ .await?;
+ Ok(resp)
+}
+
+/// Fetch all available Fabric loader versions.
+///
+/// # Returns
+/// A list of all Fabric loader versions, ordered by build number (newest first).
+pub async fn fetch_loader_versions() -> Result<Vec<FabricLoaderVersion>, Box<dyn Error + Send + Sync>> {
+ let url = format!("{}/versions/loader", FABRIC_META_URL);
+ let resp = reqwest::get(&url)
+ .await?
+ .json::<Vec<FabricLoaderVersion>>()
+ .await?;
+ Ok(resp)
+}
+
+/// Fetch Fabric loader versions available for a specific Minecraft version.
+///
+/// # Arguments
+/// * `game_version` - The Minecraft version (e.g., "1.20.4")
+///
+/// # Returns
+/// A list of loader entries with full metadata for the specified game version.
+pub async fn fetch_loaders_for_game_version(
+ game_version: &str,
+) -> Result<Vec<FabricLoaderEntry>, Box<dyn Error + Send + Sync>> {
+ let url = format!("{}/versions/loader/{}", FABRIC_META_URL, game_version);
+ let resp = reqwest::get(&url)
+ .await?
+ .json::<Vec<FabricLoaderEntry>>()
+ .await?;
+ Ok(resp)
+}
+
+/// Fetch the version JSON profile for a specific Fabric loader + game version combination.
+///
+/// # Arguments
+/// * `game_version` - The Minecraft version (e.g., "1.20.4")
+/// * `loader_version` - The Fabric loader version (e.g., "0.15.6")
+///
+/// # Returns
+/// The raw version JSON as a `serde_json::Value` that can be saved to the versions directory.
+pub async fn fetch_version_profile(
+ game_version: &str,
+ loader_version: &str,
+) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> {
+ let url = format!(
+ "{}/versions/loader/{}/{}/profile/json",
+ FABRIC_META_URL, game_version, loader_version
+ );
+ let resp = reqwest::get(&url).await?.json::<serde_json::Value>().await?;
+ Ok(resp)
+}
+
+/// Generate the version ID for a Fabric installation.
+///
+/// # Arguments
+/// * `game_version` - The Minecraft version
+/// * `loader_version` - The Fabric loader version
+///
+/// # Returns
+/// The version ID string (e.g., "fabric-loader-0.15.6-1.20.4")
+pub fn generate_version_id(game_version: &str, loader_version: &str) -> String {
+ format!("fabric-loader-{}-{}", loader_version, game_version)
+}
+
+/// Install Fabric loader for a specific Minecraft version.
+///
+/// This creates the version JSON file in the versions directory.
+/// The actual library downloads happen during game launch.
+///
+/// # Arguments
+/// * `game_dir` - The .minecraft directory path
+/// * `game_version` - The Minecraft version (e.g., "1.20.4")
+/// * `loader_version` - The Fabric loader version (e.g., "0.15.6")
+///
+/// # Returns
+/// Information about the installed version.
+pub async fn install_fabric(
+ game_dir: &PathBuf,
+ game_version: &str,
+ loader_version: &str,
+) -> Result<InstalledFabricVersion, Box<dyn Error + Send + Sync>> {
+ // Fetch the version profile from Fabric Meta
+ let profile = fetch_version_profile(game_version, loader_version).await?;
+
+ // Get the version ID from the profile or generate it
+ let version_id = profile
+ .get("id")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string())
+ .unwrap_or_else(|| generate_version_id(game_version, loader_version));
+
+ // Create the version directory
+ let version_dir = game_dir.join("versions").join(&version_id);
+ tokio::fs::create_dir_all(&version_dir).await?;
+
+ // Write the version JSON
+ let json_path = version_dir.join(format!("{}.json", version_id));
+ let json_content = serde_json::to_string_pretty(&profile)?;
+ tokio::fs::write(&json_path, json_content).await?;
+
+ Ok(InstalledFabricVersion {
+ id: version_id,
+ minecraft_version: game_version.to_string(),
+ loader_version: loader_version.to_string(),
+ path: json_path,
+ })
+}
+
+/// Check if Fabric is installed for a specific version combination.
+///
+/// # Arguments
+/// * `game_dir` - The .minecraft directory path
+/// * `game_version` - The Minecraft version
+/// * `loader_version` - The Fabric loader version
+///
+/// # Returns
+/// `true` if the version JSON exists, `false` otherwise.
+pub fn is_fabric_installed(game_dir: &PathBuf, game_version: &str, loader_version: &str) -> bool {
+ let version_id = generate_version_id(game_version, loader_version);
+ let json_path = game_dir
+ .join("versions")
+ .join(&version_id)
+ .join(format!("{}.json", version_id));
+ json_path.exists()
+}
+
+/// List all installed Fabric versions in the game directory.
+///
+/// # Arguments
+/// * `game_dir` - The .minecraft directory path
+///
+/// # Returns
+/// A list of installed Fabric version IDs.
+pub async fn list_installed_fabric_versions(
+ game_dir: &PathBuf,
+) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> {
+ let versions_dir = game_dir.join("versions");
+ let mut installed = Vec::new();
+
+ if !versions_dir.exists() {
+ return Ok(installed);
+ }
+
+ let mut entries = tokio::fs::read_dir(&versions_dir).await?;
+ while let Some(entry) = entries.next_entry().await? {
+ let name = entry.file_name().to_string_lossy().to_string();
+ if name.starts_with("fabric-loader-") {
+ // Verify the JSON file exists
+ let json_path = entry.path().join(format!("{}.json", name));
+ if json_path.exists() {
+ installed.push(name);
+ }
+ }
+ }
+
+ Ok(installed)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_generate_version_id() {
+ assert_eq!(
+ generate_version_id("1.20.4", "0.15.6"),
+ "fabric-loader-0.15.6-1.20.4"
+ );
+ }
+}
diff --git a/src-tauri/src/core/forge.rs b/src-tauri/src/core/forge.rs
new file mode 100644
index 0000000..7b951ff
--- /dev/null
+++ b/src-tauri/src/core/forge.rs
@@ -0,0 +1,333 @@
+//! Forge Loader support module.
+//!
+//! This module provides functionality to:
+//! - Fetch available Forge versions from the Forge promotions API
+//! - Install Forge loader for a specific Minecraft version
+//!
+//! Note: Forge installation is more complex than Fabric, especially for versions 1.13+.
+//! This implementation focuses on the basic JSON generation approach.
+//! For full Forge 1.13+ support, processor execution would need to be implemented.
+
+use serde::{Deserialize, Serialize};
+use std::error::Error;
+use std::path::PathBuf;
+
+const FORGE_PROMOTIONS_URL: &str =
+ "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json";
+const FORGE_MAVEN_URL: &str = "https://maven.minecraftforge.net/";
+
+/// Represents a Forge version entry.
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct ForgeVersion {
+ pub version: String,
+ pub minecraft_version: String,
+ #[serde(default)]
+ pub recommended: bool,
+ #[serde(default)]
+ pub latest: bool,
+}
+
+/// Forge promotions response from the API.
+#[derive(Debug, Deserialize)]
+struct ForgePromotions {
+ promos: std::collections::HashMap<String, String>,
+}
+
+/// Information about an installed Forge version.
+#[derive(Debug, Serialize, Clone)]
+pub struct InstalledForgeVersion {
+ pub id: String,
+ pub minecraft_version: String,
+ pub forge_version: String,
+ pub path: PathBuf,
+}
+
+/// Fetch all Minecraft versions supported by Forge.
+///
+/// # Returns
+/// A list of Minecraft version strings that have Forge available.
+pub async fn fetch_supported_game_versions() -> Result<Vec<String>, Box<dyn Error + Send + Sync>> {
+ let promos = fetch_promotions().await?;
+
+ let mut versions: Vec<String> = promos
+ .promos
+ .keys()
+ .filter_map(|key| {
+ // Keys are like "1.20.4-latest", "1.20.4-recommended"
+ let parts: Vec<&str> = key.split('-').collect();
+ if parts.len() >= 2 {
+ Some(parts[0].to_string())
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ // Deduplicate and sort
+ versions.sort();
+ versions.dedup();
+ versions.reverse(); // Newest first
+
+ Ok(versions)
+}
+
+/// Fetch Forge promotions data.
+async fn fetch_promotions() -> Result<ForgePromotions, Box<dyn Error + Send + Sync>> {
+ let resp = reqwest::get(FORGE_PROMOTIONS_URL)
+ .await?
+ .json::<ForgePromotions>()
+ .await?;
+ Ok(resp)
+}
+
+/// Fetch available Forge versions for a specific Minecraft version.
+///
+/// # Arguments
+/// * `game_version` - The Minecraft version (e.g., "1.20.4")
+///
+/// # Returns
+/// A list of Forge versions available for the specified game version.
+pub async fn fetch_forge_versions(
+ game_version: &str,
+) -> Result<Vec<ForgeVersion>, Box<dyn Error + Send + Sync>> {
+ let promos = fetch_promotions().await?;
+ let mut versions = Vec::new();
+
+ // Look for both latest and recommended
+ let latest_key = format!("{}-latest", game_version);
+ let recommended_key = format!("{}-recommended", game_version);
+
+ if let Some(latest) = promos.promos.get(&latest_key) {
+ versions.push(ForgeVersion {
+ version: latest.clone(),
+ minecraft_version: game_version.to_string(),
+ recommended: false,
+ latest: true,
+ });
+ }
+
+ if let Some(recommended) = promos.promos.get(&recommended_key) {
+ // Don't duplicate if recommended == latest
+ if !versions.iter().any(|v| v.version == *recommended) {
+ versions.push(ForgeVersion {
+ version: recommended.clone(),
+ minecraft_version: game_version.to_string(),
+ recommended: true,
+ latest: false,
+ });
+ } else {
+ // Mark the existing one as both
+ if let Some(v) = versions.iter_mut().find(|v| v.version == *recommended) {
+ v.recommended = true;
+ }
+ }
+ }
+
+ Ok(versions)
+}
+
+/// Generate the version ID for a Forge installation.
+///
+/// # Arguments
+/// * `game_version` - The Minecraft version
+/// * `forge_version` - The Forge version
+///
+/// # Returns
+/// The version ID string (e.g., "1.20.4-forge-49.0.38")
+pub fn generate_version_id(game_version: &str, forge_version: &str) -> String {
+ format!("{}-forge-{}", game_version, forge_version)
+}
+
+/// Install Forge for a specific Minecraft version.
+///
+/// Note: This creates a basic version JSON. For Forge 1.13+, the full installation
+/// requires running the Forge installer processors, which is not yet implemented.
+/// This basic implementation works for legacy Forge versions (<1.13) and creates
+/// the structure needed for modern Forge (libraries will need to be downloaded
+/// separately).
+///
+/// # Arguments
+/// * `game_dir` - The .minecraft directory path
+/// * `game_version` - The Minecraft version (e.g., "1.20.4")
+/// * `forge_version` - The Forge version (e.g., "49.0.38")
+///
+/// # Returns
+/// Information about the installed version.
+pub async fn install_forge(
+ game_dir: &PathBuf,
+ game_version: &str,
+ forge_version: &str,
+) -> Result<InstalledForgeVersion, Box<dyn Error + Send + Sync>> {
+ let version_id = generate_version_id(game_version, forge_version);
+
+ // Create basic version JSON structure
+ // Note: This is a simplified version. Full Forge installation requires
+ // downloading the installer and running processors.
+ let version_json = create_forge_version_json(game_version, forge_version)?;
+
+ // Create the version directory
+ let version_dir = game_dir.join("versions").join(&version_id);
+ tokio::fs::create_dir_all(&version_dir).await?;
+
+ // Write the version JSON
+ let json_path = version_dir.join(format!("{}.json", version_id));
+ let json_content = serde_json::to_string_pretty(&version_json)?;
+ tokio::fs::write(&json_path, json_content).await?;
+
+ Ok(InstalledForgeVersion {
+ id: version_id,
+ minecraft_version: game_version.to_string(),
+ forge_version: forge_version.to_string(),
+ path: json_path,
+ })
+}
+
+/// Create a basic Forge version JSON.
+///
+/// This creates a minimal version JSON that inherits from vanilla and adds
+/// the Forge libraries. For full functionality with Forge 1.13+, the installer
+/// would need to be run to patch the game.
+fn create_forge_version_json(
+ game_version: &str,
+ forge_version: &str,
+) -> Result<serde_json::Value, Box<dyn Error + Send + Sync>> {
+ let version_id = generate_version_id(game_version, forge_version);
+ let forge_maven_coord = format!(
+ "net.minecraftforge:forge:{}-{}",
+ game_version, forge_version
+ );
+
+ // Determine main class based on version
+ // Forge 1.13+ uses different launchers
+ let (main_class, libraries) = if is_modern_forge(game_version) {
+ // Modern Forge (1.13+) uses cpw.mods.bootstraplauncher
+ (
+ "cpw.mods.bootstraplauncher.BootstrapLauncher".to_string(),
+ vec![
+ create_library_entry(&forge_maven_coord, Some(FORGE_MAVEN_URL)),
+ create_library_entry(
+ &format!("net.minecraftforge:forge:{}-{}:universal", game_version, forge_version),
+ Some(FORGE_MAVEN_URL),
+ ),
+ ],
+ )
+ } else {
+ // Legacy Forge uses LaunchWrapper
+ (
+ "net.minecraft.launchwrapper.Launch".to_string(),
+ vec![
+ create_library_entry(&forge_maven_coord, Some(FORGE_MAVEN_URL)),
+ create_library_entry("net.minecraft:launchwrapper:1.12", None),
+ ],
+ )
+ };
+
+ let json = serde_json::json!({
+ "id": version_id,
+ "inheritsFrom": game_version,
+ "type": "release",
+ "mainClass": main_class,
+ "libraries": libraries,
+ "arguments": {
+ "game": [],
+ "jvm": []
+ }
+ });
+
+ Ok(json)
+}
+
+/// Create a library entry for the version JSON.
+fn create_library_entry(name: &str, maven_url: Option<&str>) -> serde_json::Value {
+ let mut entry = serde_json::json!({
+ "name": name
+ });
+
+ if let Some(url) = maven_url {
+ entry["url"] = serde_json::Value::String(url.to_string());
+ }
+
+ entry
+}
+
+/// Check if the Minecraft version uses modern Forge (1.13+).
+fn is_modern_forge(game_version: &str) -> bool {
+ let parts: Vec<&str> = game_version.split('.').collect();
+ if parts.len() >= 2 {
+ if let (Ok(major), Ok(minor)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
+ return major > 1 || (major == 1 && minor >= 13);
+ }
+ }
+ false
+}
+
+/// Check if Forge is installed for a specific version combination.
+///
+/// # Arguments
+/// * `game_dir` - The .minecraft directory path
+/// * `game_version` - The Minecraft version
+/// * `forge_version` - The Forge version
+///
+/// # Returns
+/// `true` if the version JSON exists, `false` otherwise.
+pub fn is_forge_installed(game_dir: &PathBuf, game_version: &str, forge_version: &str) -> bool {
+ let version_id = generate_version_id(game_version, forge_version);
+ let json_path = game_dir
+ .join("versions")
+ .join(&version_id)
+ .join(format!("{}.json", version_id));
+ json_path.exists()
+}
+
+/// List all installed Forge versions in the game directory.
+///
+/// # Arguments
+/// * `game_dir` - The .minecraft directory path
+///
+/// # Returns
+/// A list of installed Forge version IDs.
+pub async fn list_installed_forge_versions(
+ game_dir: &PathBuf,
+) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> {
+ let versions_dir = game_dir.join("versions");
+ let mut installed = Vec::new();
+
+ if !versions_dir.exists() {
+ return Ok(installed);
+ }
+
+ let mut entries = tokio::fs::read_dir(&versions_dir).await?;
+ while let Some(entry) = entries.next_entry().await? {
+ let name = entry.file_name().to_string_lossy().to_string();
+ if name.contains("-forge-") {
+ // Verify the JSON file exists
+ let json_path = entry.path().join(format!("{}.json", name));
+ if json_path.exists() {
+ installed.push(name);
+ }
+ }
+ }
+
+ Ok(installed)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_generate_version_id() {
+ assert_eq!(
+ generate_version_id("1.20.4", "49.0.38"),
+ "1.20.4-forge-49.0.38"
+ );
+ }
+
+ #[test]
+ fn test_is_modern_forge() {
+ assert!(!is_modern_forge("1.12.2"));
+ assert!(is_modern_forge("1.13"));
+ assert!(is_modern_forge("1.20.4"));
+ assert!(is_modern_forge("1.21"));
+ }
+}