Skip to main content
LiquidLauncher allows you to install and manage custom mods alongside the default mod configuration.

Mod Structure

Mods in LiquidLauncher follow a structured format:
src-tauri/src/app/client_api.rs
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LoaderMod {
    #[serde(default)]
    pub required: bool,
    #[serde(default)]
    #[serde(alias = "default")]
    pub enabled: bool,
    pub name: String,
    pub source: ModSource,
}
  • required: Whether the mod is mandatory for the build
  • enabled: Whether the mod is currently active
  • name: Display name of the mod
  • source: How to obtain the mod (SkipAd, Repository, or Local)

Mod Sources

Mods can come from three different sources:
#[serde(rename = "skip")]
SkipAd {
    artifact_name: String,
    url: String,
    #[serde(default)]
    extract: bool,
}

Source Types Explained

SkipAd

Downloads from external URLs with optional ad skip

Repository

Downloads from Maven repositories using artifact coordinates

Local

Uses locally installed custom mods

Fetching Available Mods

The API provides a list of compatible mods for each Minecraft version:
src-tauri/src/app/client_api.rs
/// Request list of downloadable mods for mc_version and used subsystem
pub async fn fetch_mods(&self, mc_version: &str, subsystem: &str) -> Result<Vec<LoaderMod>> {
    self.request_from_endpoint(
        API_V1, 
        &format!("version/mods/{}/{}", mc_version, subsystem)
    )
    .await
}

Request Mods Command

src-tauri/src/app/gui/commands/client.rs
#[tauri::command]
pub(crate) async fn request_mods(
    client: Client,
    mc_version: &str,
    subsystem: &str,
) -> Result<Vec<LoaderMod>, String> {
    let mods = (|| async { client.fetch_mods(&mc_version, &subsystem).await })
        .retry(ExponentialBuilder::default())
        .notify(|err, dur| {
            warn!("Failed to request mods. Retrying in {:?}. Error: {}", dur, err);
        })
        .await
        .map_err(|e| format!("unable to request mods: {:?}", e))?;

    Ok(mods)
}
Mods are version-specific. The launcher ensures compatibility with your selected Minecraft version.

Installing Custom Mods

You can install custom JAR files as mods:

Install Process

1

Select Mod File

Choose a .jar file from your filesystem
2

Call Install Command

src-tauri/src/app/gui/commands/client.rs
#[tauri::command]
pub(crate) async fn install_custom_mod(
    branch: &str,
    mc_version: &str,
    path: PathBuf,
) -> Result<(), String> {
    let data = LAUNCHER_DIRECTORY.data_dir();
    let mod_cache_path = data
        .join("custom_mods")
        .join(format!("{}-{}", branch, mc_version));

    if !mod_cache_path.exists() {
        fs::create_dir_all(&mod_cache_path).await.unwrap();
    }

    if let Some(file_name) = path.file_name() {
        let dest_path = mod_cache_path.join(file_name.to_str().unwrap());

        fs::copy(path, dest_path)
            .await
            .map_err(|e| format!("unable to copy custom mod: {:?}", e))?;
        return Ok(());
    }

    Err("unable to copy custom mod: invalid path".to_string())
}
3

Mod is Copied

The mod is copied to the custom mods directory for the specific branch and Minecraft version

Storage Structure

Custom mods are stored in a version-specific directory:
~/.liquidlauncher/data/custom_mods/
├── nextgen-1.20.1/
│   ├── MyCustomMod.jar
│   └── AnotherMod.jar
├── nextgen-1.19.4/
│   └── LegacyMod.jar
└── legacy-1.12.2/
    └── OldMod.jar
Mods are isolated by branch and Minecraft version to prevent compatibility issues.

Retrieving Custom Mods

The launcher can list all installed custom mods:
src-tauri/src/app/gui/commands/client.rs
#[tauri::command]
pub(crate) async fn get_custom_mods(
    branch: &str,
    mc_version: &str,
) -> Result<Vec<LoaderMod>, String> {
    let data = LAUNCHER_DIRECTORY.data_dir();
    let mod_cache_path = data
        .join("custom_mods")
        .join(format!("{}-{}", branch, mc_version));

    if !mod_cache_path.exists() {
        return Ok(vec![]);
    }

    let mut mods = vec![];
    let mut mods_read = fs::read_dir(&mod_cache_path)
        .await
        .map_err(|e| format!("unable to read custom mods: {:?}", e))?;

    while let Some(entry) = mods_read
        .next_entry()
        .await
        .map_err(|e| format!("unable to read custom mods: {:?}", e))?
    {
        let file_type = entry
            .file_type()
            .await
            .map_err(|e| format!("unable to read custom mods: {:?}", e))?;
        let file_name = entry.file_name().to_str().unwrap().to_string();

        if file_type.is_file() && file_name.ends_with(".jar") {
            // todo: pull name from JAR manifest
            let file_name_without_extension = file_name.replace(".jar", "");

            mods.push(LoaderMod {
                required: false,
                enabled: true,
                name: file_name_without_extension,
                source: ModSource::Local { file_name },
            });
        }
    }

    Ok(mods)
}
  • All custom mods are non-required (can be disabled)
  • Default state is enabled
  • Name is derived from filename (without .jar extension)
  • Source is set to Local with the original filename

Deleting Custom Mods

Remove installed custom mods:
src-tauri/src/app/gui/commands/client.rs
#[tauri::command]
pub(crate) async fn delete_custom_mod(
    branch: &str,
    mc_version: &str,
    mod_name: &str,
) -> Result<(), String> {
    let data = LAUNCHER_DIRECTORY.data_dir();
    let mod_cache_path = data
        .join("custom_mods")
        .join(format!("{}-{}", branch, mc_version));

    if !mod_cache_path.exists() {
        return Ok(());
    }

    let mod_path = mod_cache_path.join(mod_name);

    if mod_path.exists() {
        fs::remove_file(mod_path)
            .await
            .map_err(|e| format!("unable to delete custom mod: {:?}", e))?;
    }

    Ok(())
}
Deleting a mod only removes it from the launcher’s storage. Your original file remains untouched.

Mod Path Resolution

Mods resolve to different paths based on their source:
src-tauri/src/app/client_api.rs
impl ModSource {
    pub fn get_path(&self) -> Result<String> {
        Ok(match self {
            ModSource::SkipAd { artifact_name, .. } => {
                format!("{}.jar", artifact_name)
            }
            ModSource::Repository { artifact, .. } => {
                get_maven_artifact_path(artifact)?
            }
            ModSource::Local { file_name } => {
                file_name.clone()
            }
        })
    }
}
  • SkipAd: liquidbounce.jar
  • Repository: com/example/mymod/1.0.0/mymod-1.0.0.jar
  • Local: MyCustomMod.jar

Launching with Mods

When launching the game, mods are passed to the prelauncher:
src-tauri/src/app/gui/commands/client.rs
pub(crate) async fn run_client(
    client: Client,
    build_id: u32,
    options: Options,
    mods: Vec<LoaderMod>,
    window: Window,
    app_state: tauri::State<'_, AppState>,
) -> Result<(), String> {
    // ...
    let launch_manifest = client.fetch_launch_manifest(build_id).await?;
    
    prelauncher::launch(
        launch_manifest, 
        parameters, 
        mods,  // Custom mods merged with default mods
        launcher_data
    ).await
    // ...
}

Mod Merging

The prelauncher combines default mods from the manifest with custom mods:
src-tauri/src/minecraft/prelauncher.rs
pub async fn launch(
    launch_manifest: LaunchManifest,
    parameters: StartParameter,
    additional_mods: Vec<LoaderMod>,
    launcher_data: LauncherData<'_, Box<ShareableWindow>>,
) -> Result<()> {
    // Merge launch_manifest.mods with additional_mods
    // ...
}
Custom mods are downloaded and installed alongside the default mods before launching.

Skip File Resolution

For mods using SkipAd source, the launcher resolves download links:
src-tauri/src/app/client_api.rs
/// Resolve direct download link from skip file pid
pub async fn resolve_skip_file(
    &self,
    client_account: &ClientAccount,
    pid: &str,
) -> Result<SkipFileResolve> {
    self.request_with_client_account(
        &format!("file/resolve/{}", pid), 
        client_account
    )
    .await
}

/// Uses [self.url] to create a direct download link for a file with the given pid
pub fn get_direct_download_link(&self, pid: &str) -> String {
    format!("{}/{}/file/{}", self.url, API_V3, pid)
}
  1. Extract PID: Get the file identifier from the skip URL
  2. Resolve: Call API to get direct download link
  3. Download: Fetch the mod file
  4. Extract (optional): Unzip if extract: true

Repository Downloads

Mods from Maven repositories are downloaded using artifact coordinates:
src-tauri/src/app/client_api.rs
#[derive(Deserialize)]
pub struct LaunchManifest {
    pub repositories: BTreeMap<String, String>,
    pub mods: Vec<LoaderMod>,
    // ...
}
Example repository configuration:
{
  "repositories": {
    "central": "https://repo.maven.apache.org/maven2",
    "fabric": "https://maven.fabricmc.net",
    "kotlin": "https://maven.kotlinlang.org"
  }
}

Future Improvements

Planned enhancements for mod management:

JAR Manifest Parsing

Extract mod name and metadata from JAR manifest
// todo: pull name from JAR manifest

Mod Validation

Verify mod compatibility and signatures

Mod Updates

Check for and install mod updates

Mod Dependencies

Automatic dependency resolution

Version Selection

Each version has its own mod list

Auto-Updates

Default mods update with builds

Build docs developers (and LLMs) love