Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,980 changes: 1,589 additions & 1,391 deletions src-tauri/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ backon = { version = "^1.6.0", default-features = false, features = ["tokio-slee
# Generic
void = "^1"
directories = "^6.0.0"
dirs = "^5.0"
once_cell = "^1.16.0"

# FS libs
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/app/gui/commands/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ pub(crate) async fn run_client(
client,
client_account,
skip_advertisement,
vanilla_integration: options.start_options.installation,
};

thread::spawn(move || {
Expand Down
73 changes: 73 additions & 0 deletions src-tauri/src/app/gui/commands/minecraft_installation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* This file is part of LiquidLauncher (https://github.com/CCBlueX/LiquidLauncher)
*
* Copyright (c) 2015 - 2024 CCBlueX
*
* LiquidLauncher is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiquidLauncher is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LiquidLauncher. If not, see <https://www.gnu.org/licenses/>.
*/

use std::path::PathBuf;
use serde::Serialize;

#[derive(Serialize)]
pub struct MinecraftInstallation {
pub path: String,
pub saves_count: usize,
pub resource_packs_count: usize,
pub shader_packs_count: usize,
}

fn get_vanilla_minecraft_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
dirs::data_dir().map(|p| p.join(".minecraft"))
}
#[cfg(target_os = "macos")]
{
dirs::home_dir().map(|p| p.join("Library/Application Support/minecraft"))
}
#[cfg(target_os = "linux")]
{
dirs::home_dir().map(|p| p.join(".minecraft"))
}
}

fn count_entries(path: &PathBuf) -> usize {
path.read_dir().map(|r| r.count()).unwrap_or(0)
}

#[tauri::command]
pub(crate) async fn get_minecraft_installation(custom_path: Option<String>) -> Result<Option<MinecraftInstallation>, String> {
let mc_dir = if let Some(ref path) = custom_path {
if !path.is_empty() {
Some(PathBuf::from(path))
} else {
get_vanilla_minecraft_dir()
}
} else {
get_vanilla_minecraft_dir()
};

match mc_dir {
Some(path) if path.exists() => {
Ok(Some(MinecraftInstallation {
path: path.to_string_lossy().to_string(),
saves_count: count_entries(&path.join("saves")),
resource_packs_count: count_entries(&path.join("resourcepacks")),
shader_packs_count: count_entries(&path.join("shaderpacks")),
}))
}
_ => Ok(None),
}
}
2 changes: 2 additions & 0 deletions src-tauri/src/app/gui/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ pub(crate) mod auth;
pub(crate) mod client;
pub(crate) mod data;
pub(crate) mod system;
pub(crate) mod minecraft_installation;

pub(crate) use auth::*;
pub(crate) use client::*;
pub(crate) use data::*;
pub(crate) use system::*;
pub(crate) use minecraft_installation::*;
3 changes: 2 additions & 1 deletion src-tauri/src/app/gui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ pub fn gui_main() {
get_launcher_version,
get_custom_mods,
install_custom_mod,
delete_custom_mod
delete_custom_mod,
get_minecraft_installation
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
15 changes: 15 additions & 0 deletions src-tauri/src/app/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ pub(crate) struct StartOptions {
pub jvm_args: Option<Vec<String>>,
#[serde(rename = "memory", default = "default_memory")]
pub memory: u64,
#[serde(rename = "installation", default)]
pub installation: MinecraftInstallationOptions,
}

#[derive(Clone, Serialize, Deserialize, Default)]
pub struct MinecraftInstallationOptions {
#[serde(rename = "customPath", default)]
pub custom_path: String,
#[serde(rename = "useVanillaSaves", default)]
pub use_vanilla_saves: bool,
#[serde(rename = "useVanillaResourcePacks", default)]
pub use_vanilla_resource_packs: bool,
#[serde(rename = "useVanillaShaderPacks", default)]
pub use_vanilla_shader_packs: bool,
}

#[derive(Serialize, Deserialize)]
Expand Down Expand Up @@ -117,6 +131,7 @@ impl Default for StartOptions {
custom_data_path: String::new(),
jvm_args: None,
memory: 4096,
installation: MinecraftInstallationOptions::default(),
}
}
}
Expand Down
98 changes: 96 additions & 2 deletions src-tauri/src/minecraft/launcher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
*/

use std::collections::HashSet;
use std::path::Path;
use std::fs;
use std::path::{Path, PathBuf};

use std::process::exit;

use anyhow::{bail, Context, Result};

use path_absolutize::Absolutize;
use tokio::io;
use tracing::*;

use crate::app::client_api::{Client, LaunchManifest};
Expand All @@ -37,7 +39,7 @@ use crate::{
utils::{OS, OS_VERSION},
LAUNCHER_VERSION,
};

use crate::app::options::MinecraftInstallationOptions;
use self::assets::setup_assets;
use self::client_jar::setup_client_jar;
use self::jre::load_jre;
Expand Down Expand Up @@ -103,6 +105,9 @@ pub async fn launch<D: Send + Sync>(
let assets_folder = join_and_mkdir!(data, "assets");
let game_dir = join_and_mkdir_vec!(data, vec!["gameDir", &*manifest.build.branch]);

// Setup vanilla integration symlinks
setup_installation_link(&game_dir, &launching_parameter.vanilla_integration, &launcher_data)?;

let java_bin = load_jre(
&runtimes_folder,
&manifest,
Expand Down Expand Up @@ -276,6 +281,7 @@ pub struct StartParameter {
pub client: Client,
pub client_account: Option<ClientAccount>,
pub skip_advertisement: bool,
pub vanilla_integration: MinecraftInstallationOptions,
}

fn process_templates<F: Fn(&mut String, &str) -> Result<()>>(
Expand Down Expand Up @@ -325,3 +331,91 @@ fn process_templates<F: Fn(&mut String, &str) -> Result<()>>(

Ok(output)
}

fn setup_installation_link<D: Send + Sync>(
game_dir: &Path,
minecraft_installation: &MinecraftInstallationOptions,
launcher_data: &LauncherData<D>,
) -> Result<()> {
let vanilla_dir = if !minecraft_installation.custom_path.is_empty() {
let custom = PathBuf::from(&minecraft_installation.custom_path);
if custom.exists() { Some(custom) } else { None }
} else {
get_vanilla_minecraft_dir().filter(|p| p.exists())
};

let vanilla_dir = match vanilla_dir {
Some(p) => p,
None => return Ok(()),
};

let links = [
("saves", minecraft_installation.use_vanilla_saves),
("resourcepacks", minecraft_installation.use_vanilla_resource_packs),
("shaderpacks", minecraft_installation.use_vanilla_shader_packs),
];

for (folder, enabled) in links {
let target = game_dir.join(folder);
let source = vanilla_dir.join(folder);

if enabled && source.exists() {
if target.exists() {
if target.is_symlink() {
continue;
}

// To prevent losing our saves, resource packs or shader packs,
// we need to copy the content of the target folder to the source folder.
copy_dir_all(&target, &source).ok();
fs::remove_dir_all(&target).ok();
}

#[cfg(unix)]
std::os::unix::fs::symlink(&source, &target)?;

#[cfg(windows)]
std::os::windows::fs::symlink_dir(&source, &target)?;

launcher_data.log(&format!("Linked vanilla {} folder", folder));
} else if !enabled && target.is_symlink() {
fs::remove_file(&target).ok();
fs::create_dir_all(&target)?;
launcher_data.log(&format!("Unlinked vanilla {} folder", folder));
}
}

Ok(())
}

fn get_vanilla_minecraft_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
dirs::data_dir().map(|p| p.join(".minecraft"))
}
#[cfg(target_os = "macos")]
{
dirs::home_dir().map(|p| p.join("Library/Application Support/minecraft"))
}
#[cfg(target_os = "linux")]
{
dirs::home_dir().map(|p| p.join(".minecraft"))
}
}

// Source - https://stackoverflow.com/a/65192210
// Posted by Simon Buchan, modified by community. See post 'Timeline' for change history
// Retrieved 2026-08-21, License - CC BY-SA 4.0
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
fs::create_dir_all(&dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
57 changes: 57 additions & 0 deletions src/lib/main/settings/MinecraftSettings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script>
import ToggleSetting from "../../settings/ToggleSetting.svelte";
import DirectorySelectorSetting from "../../settings/DirectorySelectorSetting.svelte";
import {onMount} from "svelte";
import {invoke} from "@tauri-apps/api/core";
import Description from "../../settings/Description.svelte";

export let options;

let installation = null;

async function getInstallation() {
try {
installation = await invoke("get_minecraft_installation", {
customPath: options.start.installation.customPath || null
});
} catch (e) {
console.error("Failed to get vanilla status:", e);
}
}

onMount(getInstallation);
$: if (options.start.installation.customPath !== undefined) {
getInstallation();
}
</script>

<Description description="This will allow you to use worlds, resource packs, and shader packs from another installation of Minecraft." />

<DirectorySelectorSetting
title="Minecraft Directory"
placeholder={installation?.path || "Auto-detect"}
bind:value={options.start.installation.customPath}
windowTitle="Select Minecraft directory"
/>

{#if installation}
<ToggleSetting
title="Link worlds ({installation.saves_count})"
disabled={false}
bind:value={options.start.installation.useVanillaSaves}
/>

<ToggleSetting
title="Link resource packs ({installation.resource_packs_count})"
disabled={false}
bind:value={options.start.installation.useVanillaResourcePacks}
/>

<ToggleSetting
title="Link shader packs ({installation.shader_packs_count})"
disabled={false}
bind:value={options.start.installation.useVanillaShaderPacks}
/>
{:else}
<Description description="No Minecraft vanilla installation found."/>
{/if}
7 changes: 6 additions & 1 deletion src/lib/main/settings/Settings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import PremiumSettings from "./PremiumSettings.svelte";
import SettingsContainer from "../../settings/SettingsContainer.svelte";
import Tabs from "../../settings/tab/Tabs.svelte";
import MinecraftSettings from "./MinecraftSettings.svelte";

export let client;
export let options;
Expand All @@ -17,7 +18,7 @@
on:hideSettings={() => dispatch('hide')}
>
<Tabs
tabs={["General", "Premium"]}
tabs={["General", "Minecraft", "Premium"]}
bind:activeTab={activeSettingsTab}
slot="tabs"
/>
Expand All @@ -26,6 +27,10 @@
<GeneralSettings
bind:options
/>
{:else if activeSettingsTab === "Minecraft"}
<MinecraftSettings
bind:options
/>
{:else if activeSettingsTab === "Premium"}
<PremiumSettings
{client}
Expand Down