Profile imports (#348)
* temporary switch * draft; working unmanaged + modrinth * working update * added checkerg * fixed io merge issue * Added api handling * tidying up * reverted playground changes * fixed js issue * fixed merge issues
This commit is contained in:
parent
3fa33dc241
commit
70aaf6eef9
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -56,4 +56,5 @@
|
|||||||
"rust-analyzer.linkedProjects": [
|
"rust-analyzer.linkedProjects": [
|
||||||
"./theseus/Cargo.toml"
|
"./theseus/Cargo.toml"
|
||||||
],
|
],
|
||||||
|
"rust-analyzer.showUnlinkedFileNotification": false,
|
||||||
}
|
}
|
||||||
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -3528,6 +3528,12 @@ dependencies = [
|
|||||||
"winreg 0.10.1",
|
"winreg 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "result"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfd"
|
name = "rfd"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@ -3827,6 +3833,17 @@ dependencies = [
|
|||||||
"syn 2.0.22",
|
"syn 2.0.22",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_ini"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb236687e2bb073a7521c021949be944641e671b8505a94069ca37b656c81139"
|
||||||
|
dependencies = [
|
||||||
|
"result",
|
||||||
|
"serde",
|
||||||
|
"void",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.99"
|
version = "1.0.99"
|
||||||
@ -4599,6 +4616,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_ini",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1 0.6.1",
|
"sha1 0.6.1",
|
||||||
"sha2 0.9.9",
|
"sha2 0.9.9",
|
||||||
@ -5237,6 +5255,12 @@ version = "0.0.13"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314"
|
checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "void"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vswhom"
|
name = "vswhom"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@ -12,6 +12,7 @@ theseus_macros = { path = "../theseus_macros" }
|
|||||||
bytes = "1"
|
bytes = "1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
serde_ini = "0.2.0"
|
||||||
toml = "0.7.3"
|
toml = "0.7.3"
|
||||||
sha1 = { version = "0.6.1", features = ["std"]}
|
sha1 = { version = "0.6.1", features = ["std"]}
|
||||||
sha2 = "0.9.9"
|
sha2 = "0.9.9"
|
||||||
|
|||||||
244
theseus/src/api/pack/import/atlauncher.rs
Normal file
244
theseus/src/api/pack/import/atlauncher.rs
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
event::LoadingBarId,
|
||||||
|
pack::{
|
||||||
|
self,
|
||||||
|
import::{self, copy_dotminecraft},
|
||||||
|
install_from::CreatePackDescription,
|
||||||
|
},
|
||||||
|
prelude::{ModLoader, ProfilePathId},
|
||||||
|
state::{LinkedData, ProfileInstallStage},
|
||||||
|
util::io,
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ATInstance {
|
||||||
|
pub id: String, // minecraft version id ie: 1.12.1, not a name
|
||||||
|
pub launcher: ATLauncher,
|
||||||
|
pub java_version: ATJavaVersion,
|
||||||
|
}
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ATLauncher {
|
||||||
|
pub name: String,
|
||||||
|
pub pack: String,
|
||||||
|
pub version: String, // ie: 1.6
|
||||||
|
pub loader_version: ATLauncherLoaderVersion,
|
||||||
|
|
||||||
|
pub modrinth_project: Option<ATLauncherModrinthProject>,
|
||||||
|
pub modrinth_version: Option<ATLauncherModrinthVersion>,
|
||||||
|
pub modrinth_manifest: Option<pack::install_from::PackFormat>,
|
||||||
|
|
||||||
|
pub mods: Vec<ATLauncherMod>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ATJavaVersion {
|
||||||
|
pub major_version: u8,
|
||||||
|
pub component: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ATLauncherLoaderVersion {
|
||||||
|
pub r#type: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct ATLauncherModrinthProject {
|
||||||
|
pub id: String,
|
||||||
|
pub slug: String,
|
||||||
|
pub project_type: String,
|
||||||
|
pub team: String,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub body: String,
|
||||||
|
pub client_side: Option<String>,
|
||||||
|
pub server_side: Option<String>,
|
||||||
|
pub categories: Vec<String>,
|
||||||
|
pub icon_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct ATLauncherModrinthVersion {
|
||||||
|
pub id: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub version_number: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ATLauncherModrinthVersionFile {
|
||||||
|
pub hashes: HashMap<String, String>,
|
||||||
|
pub url: String,
|
||||||
|
pub filename: String,
|
||||||
|
pub primary: bool,
|
||||||
|
pub size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ATLauncherModrinthVersionDependency {
|
||||||
|
pub project_id: Option<String>,
|
||||||
|
pub version_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ATLauncherMod {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub file: String,
|
||||||
|
|
||||||
|
pub modrinth_project: Option<ATLauncherModrinthProject>,
|
||||||
|
pub modrinth_version: Option<ATLauncherModrinthVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if folder has a instance.json that parses
|
||||||
|
pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
|
||||||
|
let instance: String =
|
||||||
|
fs::read_to_string(&instance_folder.join("instance.json"))
|
||||||
|
.await
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let instance: Result<ATInstance, serde_json::Error> =
|
||||||
|
serde_json::from_str::<ATInstance>(&instance);
|
||||||
|
instance.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
#[theseus_macros::debug_pin]
|
||||||
|
pub async fn import_atlauncher(
|
||||||
|
atlauncher_base_path: PathBuf, // path to base atlauncher folder
|
||||||
|
instance_folder: String, // instance folder in atlauncher_base_path
|
||||||
|
profile_path: ProfilePathId, // path to profile
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let atlauncher_instance_path = atlauncher_base_path
|
||||||
|
.join("instances")
|
||||||
|
.join(instance_folder.clone());
|
||||||
|
|
||||||
|
// Load instance.json
|
||||||
|
let atinstance: String =
|
||||||
|
io::read_to_string(&atlauncher_instance_path.join("instance.json"))
|
||||||
|
.await?;
|
||||||
|
let atinstance: ATInstance =
|
||||||
|
serde_json::from_str::<ATInstance>(&atinstance)?;
|
||||||
|
|
||||||
|
// Icon path should be {instance_folder}/instance.png if it exists,
|
||||||
|
// Second possibility is ATLauncher/configs/images/{safe_pack_name}.png (safe pack name is alphanumeric lowercase)
|
||||||
|
let icon_path_primary = atlauncher_instance_path.join("instance.png");
|
||||||
|
let safe_pack_name = atinstance
|
||||||
|
.launcher
|
||||||
|
.pack
|
||||||
|
.replace(|c: char| !c.is_alphanumeric(), "")
|
||||||
|
.to_lowercase();
|
||||||
|
let icon_path_secondary = atlauncher_base_path
|
||||||
|
.join("configs")
|
||||||
|
.join("images")
|
||||||
|
.join(safe_pack_name + ".png");
|
||||||
|
let icon = match (icon_path_primary.exists(), icon_path_secondary.exists())
|
||||||
|
{
|
||||||
|
(true, _) => import::recache_icon(icon_path_primary).await?,
|
||||||
|
(_, true) => import::recache_icon(icon_path_secondary).await?,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create description from instance.cfg
|
||||||
|
let description = CreatePackDescription {
|
||||||
|
icon,
|
||||||
|
override_title: Some(atinstance.launcher.name.clone()),
|
||||||
|
project_id: None,
|
||||||
|
version_id: None,
|
||||||
|
existing_loading_bar: None,
|
||||||
|
profile_path: profile_path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let backup_name = format!("ATLauncher-{}", instance_folder);
|
||||||
|
let minecraft_folder = atlauncher_instance_path;
|
||||||
|
|
||||||
|
import_atlauncher_unmanaged(
|
||||||
|
profile_path,
|
||||||
|
minecraft_folder,
|
||||||
|
backup_name,
|
||||||
|
description,
|
||||||
|
atinstance,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn import_atlauncher_unmanaged(
|
||||||
|
profile_path: ProfilePathId,
|
||||||
|
minecraft_folder: PathBuf,
|
||||||
|
backup_name: String,
|
||||||
|
description: CreatePackDescription,
|
||||||
|
atinstance: ATInstance,
|
||||||
|
existing_loading_bar: Option<LoadingBarId>,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let mod_loader = format!(
|
||||||
|
"\"{}\"",
|
||||||
|
atinstance.launcher.loader_version.r#type.to_lowercase()
|
||||||
|
);
|
||||||
|
let mod_loader: ModLoader = serde_json::from_str::<ModLoader>(&mod_loader)
|
||||||
|
.map_err(|_| {
|
||||||
|
crate::ErrorKind::InputError(format!(
|
||||||
|
"Could not parse mod loader type: {}",
|
||||||
|
mod_loader
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let game_version = atinstance.id;
|
||||||
|
|
||||||
|
let loader_version = if mod_loader != ModLoader::Vanilla {
|
||||||
|
crate::profile_create::get_loader_version_from_loader(
|
||||||
|
game_version.clone(),
|
||||||
|
mod_loader,
|
||||||
|
Some(atinstance.launcher.loader_version.version.clone()),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set profile data to created default profile
|
||||||
|
crate::api::profile::edit(&profile_path, |prof| {
|
||||||
|
prof.metadata.name = description
|
||||||
|
.override_title
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| backup_name.to_string());
|
||||||
|
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||||
|
prof.metadata.linked_data = Some(LinkedData {
|
||||||
|
project_id: description.project_id.clone(),
|
||||||
|
version_id: description.version_id.clone(),
|
||||||
|
});
|
||||||
|
prof.metadata.icon = description.icon.clone();
|
||||||
|
prof.metadata.game_version = game_version.clone();
|
||||||
|
prof.metadata.loader_version = loader_version.clone();
|
||||||
|
prof.metadata.loader = mod_loader;
|
||||||
|
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||||
|
copy_dotminecraft(profile_path.clone(), minecraft_folder).await?;
|
||||||
|
|
||||||
|
if let Some(profile_val) =
|
||||||
|
crate::api::profile::get(&profile_path, None).await?
|
||||||
|
{
|
||||||
|
crate::launcher::install_minecraft(&profile_val, existing_loading_bar)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
State::sync().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
161
theseus/src/api/pack/import/curseforge.rs
Normal file
161
theseus/src/api/pack/import/curseforge.rs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
prelude::{ModLoader, ProfilePathId},
|
||||||
|
state::ProfileInstallStage,
|
||||||
|
util::io,
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::copy_dotminecraft;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FlameManifest {
|
||||||
|
pub manifest_version: u8,
|
||||||
|
pub name: String,
|
||||||
|
pub minecraft: FlameMinecraft,
|
||||||
|
}
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FlameMinecraft {
|
||||||
|
pub version: String,
|
||||||
|
pub mod_loaders: Vec<FlameModLoader>,
|
||||||
|
}
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FlameModLoader {
|
||||||
|
pub id: String,
|
||||||
|
pub primary: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MinecraftInstance {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub game_version: String, // Minecraft game version. Non-prioritized, use this if Vanilla
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if folder has a minecraftinstance.json that parses
|
||||||
|
pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
|
||||||
|
let minecraftinstance: String =
|
||||||
|
fs::read_to_string(&instance_folder.join("minecraftinstance.json"))
|
||||||
|
.await
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let minecraftinstance: Result<MinecraftInstance, serde_json::Error> =
|
||||||
|
serde_json::from_str::<MinecraftInstance>(&minecraftinstance);
|
||||||
|
minecraftinstance.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_curseforge(
|
||||||
|
curseforge_instance_folder: PathBuf, // instance's folder
|
||||||
|
profile_path: ProfilePathId, // path to profile
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
// TODO: recache curseforge instance icon
|
||||||
|
let icon: Option<PathBuf> = None;
|
||||||
|
|
||||||
|
// Load minecraftinstance.json
|
||||||
|
let minecraft_instance: String = io::read_to_string(
|
||||||
|
&curseforge_instance_folder.join("minecraftinstance.json"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let minecraft_instance: MinecraftInstance =
|
||||||
|
serde_json::from_str::<MinecraftInstance>(&minecraft_instance)?;
|
||||||
|
let override_title: Option<String> = minecraft_instance.name.clone();
|
||||||
|
let backup_name = format!(
|
||||||
|
"Curseforge-{}",
|
||||||
|
curseforge_instance_folder
|
||||||
|
.file_name()
|
||||||
|
.map(|a| a.to_string_lossy().to_string())
|
||||||
|
.unwrap_or("Unknown".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Curseforge vanilla profile may not have a manifest.json, so we allow it to not exist
|
||||||
|
if curseforge_instance_folder.join("manifest.json").exists() {
|
||||||
|
// Load manifest.json
|
||||||
|
let cf_manifest: String = io::read_to_string(
|
||||||
|
&curseforge_instance_folder.join("manifest.json"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let cf_manifest: FlameManifest =
|
||||||
|
serde_json::from_str::<FlameManifest>(&cf_manifest)?;
|
||||||
|
|
||||||
|
let game_version = cf_manifest.minecraft.version;
|
||||||
|
|
||||||
|
// CF allows Forge, Fabric, and Vanilla
|
||||||
|
let mut mod_loader = None;
|
||||||
|
let mut loader_version = None;
|
||||||
|
for loader in cf_manifest.minecraft.mod_loaders {
|
||||||
|
match loader.id.split_once('-') {
|
||||||
|
Some(("forge", version)) => {
|
||||||
|
mod_loader = Some(ModLoader::Forge);
|
||||||
|
loader_version = Some(version.to_string());
|
||||||
|
}
|
||||||
|
Some(("fabric", version)) => {
|
||||||
|
mod_loader = Some(ModLoader::Fabric);
|
||||||
|
loader_version = Some(version.to_string());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla);
|
||||||
|
|
||||||
|
let loader_version = if mod_loader != ModLoader::Vanilla {
|
||||||
|
crate::profile_create::get_loader_version_from_loader(
|
||||||
|
game_version.clone(),
|
||||||
|
mod_loader,
|
||||||
|
loader_version,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set profile data to created default profile
|
||||||
|
crate::api::profile::edit(&profile_path, |prof| {
|
||||||
|
prof.metadata.name = override_title
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| backup_name.to_string());
|
||||||
|
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||||
|
prof.metadata.icon = icon.clone();
|
||||||
|
prof.metadata.game_version = game_version.clone();
|
||||||
|
prof.metadata.loader_version = loader_version.clone();
|
||||||
|
prof.metadata.loader = mod_loader;
|
||||||
|
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
// If no manifest is found, it's a vanilla profile
|
||||||
|
crate::api::profile::edit(&profile_path, |prof| {
|
||||||
|
prof.metadata.name = override_title
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| backup_name.to_string());
|
||||||
|
prof.metadata.icon = icon.clone();
|
||||||
|
prof.metadata.game_version =
|
||||||
|
minecraft_instance.game_version.clone();
|
||||||
|
prof.metadata.loader_version = None;
|
||||||
|
prof.metadata.loader = ModLoader::Vanilla;
|
||||||
|
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy in contained folders as overrides
|
||||||
|
copy_dotminecraft(profile_path.clone(), curseforge_instance_folder).await?;
|
||||||
|
|
||||||
|
if let Some(profile_val) =
|
||||||
|
crate::api::profile::get(&profile_path, None).await?
|
||||||
|
{
|
||||||
|
crate::launcher::install_minecraft(&profile_val, None).await?;
|
||||||
|
|
||||||
|
State::sync().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
115
theseus/src/api/pack/import/gdlauncher.rs
Normal file
115
theseus/src/api/pack/import/gdlauncher.rs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
prelude::{ModLoader, ProfilePathId},
|
||||||
|
state::ProfileInstallStage,
|
||||||
|
util::io,
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{copy_dotminecraft, recache_icon};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GDLauncherConfig {
|
||||||
|
pub background: Option<String>,
|
||||||
|
pub loader: GDLauncherLoader,
|
||||||
|
// pub mods: Vec<GDLauncherMod>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GDLauncherLoader {
|
||||||
|
pub loader_type: ModLoader,
|
||||||
|
pub loader_version: Option<String>,
|
||||||
|
pub mc_version: String,
|
||||||
|
pub source: Option<String>,
|
||||||
|
pub source_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if folder has a config.json that parses
|
||||||
|
pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
|
||||||
|
let config: String =
|
||||||
|
fs::read_to_string(&instance_folder.join("config.json"))
|
||||||
|
.await
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let config: Result<GDLauncherConfig, serde_json::Error> =
|
||||||
|
serde_json::from_str::<GDLauncherConfig>(&config);
|
||||||
|
config.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn import_gdlauncher(
|
||||||
|
gdlauncher_instance_folder: PathBuf, // instance's folder
|
||||||
|
profile_path: ProfilePathId, // path to profile
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
// Load config.json
|
||||||
|
let config: String =
|
||||||
|
io::read_to_string(&gdlauncher_instance_folder.join("config.json"))
|
||||||
|
.await?;
|
||||||
|
let config: GDLauncherConfig =
|
||||||
|
serde_json::from_str::<GDLauncherConfig>(&config)?;
|
||||||
|
let override_title: Option<String> = config.loader.source_name.clone();
|
||||||
|
let backup_name = format!(
|
||||||
|
"GDLauncher-{}",
|
||||||
|
gdlauncher_instance_folder
|
||||||
|
.file_name()
|
||||||
|
.map(|a| a.to_string_lossy().to_string())
|
||||||
|
.unwrap_or("Unknown".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-cache icon
|
||||||
|
let icon = config
|
||||||
|
.background
|
||||||
|
.clone()
|
||||||
|
.map(|b| gdlauncher_instance_folder.join(b));
|
||||||
|
let icon = if let Some(icon) = icon {
|
||||||
|
recache_icon(icon).await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let game_version = config.loader.mc_version;
|
||||||
|
let mod_loader = config.loader.loader_type;
|
||||||
|
let loader_version = config.loader.loader_version;
|
||||||
|
|
||||||
|
let loader_version = if mod_loader != ModLoader::Vanilla {
|
||||||
|
crate::profile_create::get_loader_version_from_loader(
|
||||||
|
game_version.clone(),
|
||||||
|
mod_loader,
|
||||||
|
loader_version,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set profile data to created default profile
|
||||||
|
crate::api::profile::edit(&profile_path, |prof| {
|
||||||
|
prof.metadata.name = override_title
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| backup_name.to_string());
|
||||||
|
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||||
|
prof.metadata.icon = icon.clone();
|
||||||
|
prof.metadata.game_version = game_version.clone();
|
||||||
|
prof.metadata.loader_version = loader_version.clone();
|
||||||
|
prof.metadata.loader = mod_loader;
|
||||||
|
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Copy in contained folders as overrides
|
||||||
|
copy_dotminecraft(profile_path.clone(), gdlauncher_instance_folder).await?;
|
||||||
|
|
||||||
|
if let Some(profile_val) =
|
||||||
|
crate::api::profile::get(&profile_path, None).await?
|
||||||
|
{
|
||||||
|
crate::launcher::install_minecraft(&profile_val, None).await?;
|
||||||
|
|
||||||
|
State::sync().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
292
theseus/src/api/pack/import/mmc.rs
Normal file
292
theseus/src/api/pack/import/mmc.rs
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::{de, Deserialize, Serialize};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
pack::{
|
||||||
|
import::{self, copy_dotminecraft},
|
||||||
|
install_from::{self, CreatePackDescription, PackDependency},
|
||||||
|
},
|
||||||
|
prelude::ProfilePathId,
|
||||||
|
util::io,
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
|
||||||
|
// instance.cfg
|
||||||
|
// https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/MinecraftInstance.cpp
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum MMCInstanceEnum {
|
||||||
|
General(MMCInstanceGeneral),
|
||||||
|
Instance(MMCInstance),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
struct MMCInstanceGeneral {
|
||||||
|
pub general: MMCInstance,
|
||||||
|
}
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct MMCInstance {
|
||||||
|
pub java_path: Option<String>,
|
||||||
|
pub jvm_args: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(deserialize_with = "deserialize_optional_bool")]
|
||||||
|
pub managed_pack: Option<bool>,
|
||||||
|
|
||||||
|
pub managed_pack_id: Option<String>,
|
||||||
|
pub managed_pack_type: Option<MMCManagedPackType>,
|
||||||
|
pub managed_pack_version_id: Option<String>,
|
||||||
|
pub managed_pack_version_name: Option<String>,
|
||||||
|
|
||||||
|
#[serde(rename = "iconKey")]
|
||||||
|
pub icon_key: Option<String>,
|
||||||
|
#[serde(rename = "name")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// serde_ini reads 'true' and 'false' as strings, so we need to convert them to booleans
|
||||||
|
fn deserialize_optional_bool<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<Option<bool>, D::Error>
|
||||||
|
where
|
||||||
|
D: de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = Option::<String>::deserialize(deserializer)?;
|
||||||
|
match s {
|
||||||
|
Some(string) => match string.as_str() {
|
||||||
|
"true" => Ok(Some(true)),
|
||||||
|
"false" => Ok(Some(false)),
|
||||||
|
_ => Err(de::Error::custom("expected 'true' or 'false'")),
|
||||||
|
},
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum MMCManagedPackType {
|
||||||
|
Modrinth,
|
||||||
|
Flame,
|
||||||
|
ATLauncher,
|
||||||
|
#[serde(other)]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
// mmc-pack.json
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MMCPack {
|
||||||
|
components: Vec<MMCComponent>,
|
||||||
|
format_version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/Component.h
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MMCComponent {
|
||||||
|
pub uid: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub version: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub dependency_only: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub important: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub disabled: bool,
|
||||||
|
|
||||||
|
pub cached_name: Option<String>,
|
||||||
|
pub cached_version: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub cached_requires: Vec<MMCComponentRequirement>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cached_conflicts: Vec<MMCComponentRequirement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MMCComponentRequirement {
|
||||||
|
pub uid: String,
|
||||||
|
pub equals_version: Option<String>,
|
||||||
|
pub suggests: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if if its a folder, and the folder contains instance.cfg and mmc-pack.json, and they both parse
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool {
|
||||||
|
let instance_cfg = instance_folder.join("instance.cfg");
|
||||||
|
let mmc_pack = instance_folder.join("mmc-pack.json");
|
||||||
|
|
||||||
|
let mmc_pack = match fs::read_to_string(&mmc_pack).await {
|
||||||
|
Ok(mmc_pack) => mmc_pack,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
load_instance_cfg(&instance_cfg).await.is_ok()
|
||||||
|
&& serde_json::from_str::<MMCPack>(&mmc_pack).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading the INI (instance.cfg) file
|
||||||
|
async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
|
||||||
|
let instance_cfg = io::read_to_string(file_path).await?;
|
||||||
|
let instance_cfg_enum: MMCInstanceEnum =
|
||||||
|
serde_ini::from_str::<MMCInstanceEnum>(&instance_cfg)?;
|
||||||
|
match instance_cfg_enum {
|
||||||
|
MMCInstanceEnum::General(instance_cfg) => Ok(instance_cfg.general),
|
||||||
|
MMCInstanceEnum::Instance(instance_cfg) => Ok(instance_cfg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
#[theseus_macros::debug_pin]
|
||||||
|
pub async fn import_mmc(
|
||||||
|
mmc_base_path: PathBuf, // path to base mmc folder
|
||||||
|
instance_folder: String, // instance folder in mmc_base_path
|
||||||
|
profile_path: ProfilePathId, // path to profile
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let mmc_instance_path = mmc_base_path
|
||||||
|
.join("instances")
|
||||||
|
.join(instance_folder.clone());
|
||||||
|
|
||||||
|
let mmc_pack =
|
||||||
|
io::read_to_string(&mmc_instance_path.join("mmc-pack.json")).await?;
|
||||||
|
let mmc_pack: MMCPack = serde_json::from_str::<MMCPack>(&mmc_pack)?;
|
||||||
|
|
||||||
|
let instance_cfg =
|
||||||
|
load_instance_cfg(&mmc_instance_path.join("instance.cfg")).await?;
|
||||||
|
|
||||||
|
// Re-cache icon
|
||||||
|
let icon = if let Some(icon_key) = instance_cfg.icon_key {
|
||||||
|
let icon_path = mmc_base_path.join("icons").join(icon_key);
|
||||||
|
import::recache_icon(icon_path).await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create description from instance.cfg
|
||||||
|
let description = CreatePackDescription {
|
||||||
|
icon,
|
||||||
|
override_title: instance_cfg.name,
|
||||||
|
project_id: instance_cfg.managed_pack_id,
|
||||||
|
version_id: instance_cfg.managed_pack_version_id,
|
||||||
|
existing_loading_bar: None,
|
||||||
|
profile_path: profile_path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Managed pack
|
||||||
|
let backup_name = "Imported Modpack".to_string();
|
||||||
|
|
||||||
|
if instance_cfg.managed_pack.unwrap_or(false) {
|
||||||
|
match instance_cfg.managed_pack_type {
|
||||||
|
Some(MMCManagedPackType::Modrinth) => {
|
||||||
|
// Modrinth Managed Pack
|
||||||
|
// Kept separate as we may in the future want to add special handling for modrinth managed packs
|
||||||
|
let backup_name = "Imported Modrinth Modpack".to_string();
|
||||||
|
let minecraft_folder = mmc_base_path.join("instances").join(instance_folder).join(".minecraft");
|
||||||
|
import_mmc_unmanaged(profile_path, minecraft_folder, backup_name, description, mmc_pack).await?;
|
||||||
|
}
|
||||||
|
Some(MMCManagedPackType::Flame) | Some(MMCManagedPackType::ATLauncher) => {
|
||||||
|
// For flame/atlauncher managed packs
|
||||||
|
// Treat as unmanaged, but with 'minecraft' folder instead of '.minecraft'
|
||||||
|
let minecraft_folder = mmc_base_path.join("instances").join(instance_folder).join("minecraft");
|
||||||
|
import_mmc_unmanaged(profile_path, minecraft_folder, backup_name, description, mmc_pack).await?;
|
||||||
|
},
|
||||||
|
Some(_) => {
|
||||||
|
// For managed packs that aren't modrinth, flame, atlauncher
|
||||||
|
// Treat as unmanaged
|
||||||
|
let backup_name = "ImportedModpack".to_string();
|
||||||
|
let minecraft_folder = mmc_base_path.join("instances").join(instance_folder).join(".minecraft");
|
||||||
|
import_mmc_unmanaged(profile_path, minecraft_folder, backup_name, description, mmc_pack).await?;
|
||||||
|
},
|
||||||
|
_ => return Err(crate::ErrorKind::InputError({
|
||||||
|
"Instance is managed, but managed pack type not specified in instance.cfg".to_string()
|
||||||
|
}).into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Direclty import unmanaged pack
|
||||||
|
let backup_name = "Imported Modpack".to_string();
|
||||||
|
let minecraft_folder = mmc_base_path
|
||||||
|
.join("instances")
|
||||||
|
.join(instance_folder)
|
||||||
|
.join(".minecraft");
|
||||||
|
import_mmc_unmanaged(
|
||||||
|
profile_path,
|
||||||
|
minecraft_folder,
|
||||||
|
backup_name,
|
||||||
|
description,
|
||||||
|
mmc_pack,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn import_mmc_unmanaged(
|
||||||
|
profile_path: ProfilePathId,
|
||||||
|
minecraft_folder: PathBuf,
|
||||||
|
backup_name: String,
|
||||||
|
description: CreatePackDescription,
|
||||||
|
mmc_pack: MMCPack,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
// Pack dependencies stored in mmc-pack.json, we convert to .mrpack pack dependencies
|
||||||
|
let dependencies = mmc_pack
|
||||||
|
.components
|
||||||
|
.iter()
|
||||||
|
.filter_map(|component| {
|
||||||
|
if component.uid.starts_with("net.fabricmc.fabric-loader") {
|
||||||
|
return Some((
|
||||||
|
PackDependency::FabricLoader,
|
||||||
|
component.version.clone().unwrap_or_default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if component.uid.starts_with("net.minecraftforge") {
|
||||||
|
return Some((
|
||||||
|
PackDependency::Forge,
|
||||||
|
component.version.clone().unwrap_or_default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if component.uid.starts_with("org.quiltmc.quilt-loader") {
|
||||||
|
return Some((
|
||||||
|
PackDependency::QuiltLoader,
|
||||||
|
component.version.clone().unwrap_or_default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if component.uid.starts_with("net.minecraft") {
|
||||||
|
return Some((
|
||||||
|
PackDependency::Minecraft,
|
||||||
|
component.version.clone().unwrap_or_default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sets profile information to be that loaded from mmc-pack.json and instance.cfg
|
||||||
|
install_from::set_profile_information(
|
||||||
|
profile_path.clone(),
|
||||||
|
&description,
|
||||||
|
&backup_name,
|
||||||
|
&dependencies,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||||
|
copy_dotminecraft(profile_path.clone(), minecraft_folder).await?;
|
||||||
|
|
||||||
|
if let Some(profile_val) =
|
||||||
|
crate::api::profile::get(&profile_path, None).await?
|
||||||
|
{
|
||||||
|
crate::launcher::install_minecraft(&profile_val, None).await?;
|
||||||
|
|
||||||
|
State::sync().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
268
theseus/src/api/pack/import/mod.rs
Normal file
268
theseus/src/api/pack/import/mod.rs
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use io::IOError;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
prelude::ProfilePathId,
|
||||||
|
util::{fetch, io},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod atlauncher;
|
||||||
|
pub mod curseforge;
|
||||||
|
pub mod gdlauncher;
|
||||||
|
pub mod mmc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum ImportLauncherType {
|
||||||
|
MultiMC,
|
||||||
|
PrismLauncher,
|
||||||
|
ATLauncher,
|
||||||
|
GDLauncher,
|
||||||
|
Curseforge,
|
||||||
|
#[serde(other)]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a list of importable instances from a launcher type and base path, by iterating through the folder and checking
|
||||||
|
pub async fn get_importable_instances(
|
||||||
|
launcher_type: ImportLauncherType,
|
||||||
|
base_path: PathBuf,
|
||||||
|
) -> crate::Result<Vec<String>> {
|
||||||
|
// Some launchers have a different folder structure for instances
|
||||||
|
let instances_folder = match launcher_type {
|
||||||
|
ImportLauncherType::GDLauncher
|
||||||
|
| ImportLauncherType::MultiMC
|
||||||
|
| ImportLauncherType::PrismLauncher
|
||||||
|
| ImportLauncherType::ATLauncher => base_path.join("instances"),
|
||||||
|
ImportLauncherType::Curseforge => base_path.join("Instances"),
|
||||||
|
ImportLauncherType::Unknown => {
|
||||||
|
return Err(crate::ErrorKind::InputError(
|
||||||
|
"Launcher type Unknown".to_string(),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut instances = Vec::new();
|
||||||
|
let mut dir = io::read_dir(&instances_folder).await?;
|
||||||
|
while let Some(entry) = dir
|
||||||
|
.next_entry()
|
||||||
|
.await
|
||||||
|
.map_err(|e| IOError::with_path(e, &instances_folder))?
|
||||||
|
{
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
// Check instance is valid of this launcher type
|
||||||
|
if is_valid_importable_instance(path.clone(), launcher_type).await {
|
||||||
|
let name = path.file_name();
|
||||||
|
if let Some(name) = name {
|
||||||
|
instances.push(name.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import an instance from a launcher type and base path
|
||||||
|
#[theseus_macros::debug_pin]
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn import_instance(
|
||||||
|
profile_path: ProfilePathId,
|
||||||
|
launcher_type: ImportLauncherType,
|
||||||
|
base_path: PathBuf,
|
||||||
|
instance_folder: String,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
tracing::debug!("Importing instance from {instance_folder}");
|
||||||
|
match launcher_type {
|
||||||
|
ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => {
|
||||||
|
mmc::import_mmc(
|
||||||
|
base_path, // path to base mmc folder
|
||||||
|
instance_folder, // instance folder in mmc_base_path
|
||||||
|
profile_path, // path to profile
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
ImportLauncherType::ATLauncher => {
|
||||||
|
atlauncher::import_atlauncher(
|
||||||
|
base_path, // path to atlauncher folder
|
||||||
|
instance_folder, // instance folder in atlauncher
|
||||||
|
profile_path, // path to profile
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
ImportLauncherType::GDLauncher => {
|
||||||
|
gdlauncher::import_gdlauncher(
|
||||||
|
base_path.join("instances").join(instance_folder), // path to gdlauncher folder
|
||||||
|
profile_path, // path to profile
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
ImportLauncherType::Curseforge => {
|
||||||
|
curseforge::import_curseforge(
|
||||||
|
base_path.join("Instances").join(instance_folder), // path to curseforge folder
|
||||||
|
profile_path, // path to profile
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
ImportLauncherType::Unknown => {
|
||||||
|
return Err(crate::ErrorKind::InputError(
|
||||||
|
"Launcher type Unknown".to_string(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::debug!("Completed import.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default path for the given launcher type
|
||||||
|
/// None if it can't be found or doesn't exist
|
||||||
|
pub fn get_default_launcher_path(
|
||||||
|
r#type: ImportLauncherType,
|
||||||
|
) -> Option<PathBuf> {
|
||||||
|
let path = match r#type {
|
||||||
|
ImportLauncherType::MultiMC => None, // multimc data is *in* app dir
|
||||||
|
ImportLauncherType::PrismLauncher => {
|
||||||
|
Some(dirs::data_dir()?.join("PrismLauncher"))
|
||||||
|
}
|
||||||
|
ImportLauncherType::ATLauncher => {
|
||||||
|
Some(dirs::data_dir()?.join("ATLauncher"))
|
||||||
|
}
|
||||||
|
ImportLauncherType::GDLauncher => {
|
||||||
|
Some(dirs::data_dir()?.join("gdlauncher_next"))
|
||||||
|
}
|
||||||
|
ImportLauncherType::Curseforge => {
|
||||||
|
Some(dirs::home_dir()?.join("curseforge").join("minecraft"))
|
||||||
|
}
|
||||||
|
ImportLauncherType::Unknown => None,
|
||||||
|
};
|
||||||
|
let path = path?;
|
||||||
|
if path.exists() {
|
||||||
|
Some(path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if this PathBuf is a valid instance for the given launcher type
|
||||||
|
#[theseus_macros::debug_pin]
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn is_valid_importable_instance(
|
||||||
|
instance_path: PathBuf,
|
||||||
|
r#type: ImportLauncherType,
|
||||||
|
) -> bool {
|
||||||
|
match r#type {
|
||||||
|
ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => {
|
||||||
|
mmc::is_valid_mmc(instance_path).await
|
||||||
|
}
|
||||||
|
ImportLauncherType::ATLauncher => {
|
||||||
|
atlauncher::is_valid_atlauncher(instance_path).await
|
||||||
|
}
|
||||||
|
ImportLauncherType::GDLauncher => {
|
||||||
|
gdlauncher::is_valid_gdlauncher(instance_path).await
|
||||||
|
}
|
||||||
|
ImportLauncherType::Curseforge => {
|
||||||
|
curseforge::is_valid_curseforge(instance_path).await
|
||||||
|
}
|
||||||
|
ImportLauncherType::Unknown => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Caches an image file in the filesystem into the cache directory, and returns the path to the cached file.
|
||||||
|
#[theseus_macros::debug_pin]
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn recache_icon(
|
||||||
|
icon_path: PathBuf,
|
||||||
|
) -> crate::Result<Option<PathBuf>> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
|
||||||
|
let bytes = tokio::fs::read(&icon_path).await;
|
||||||
|
if let Ok(bytes) = bytes {
|
||||||
|
let bytes = bytes::Bytes::from(bytes);
|
||||||
|
let cache_dir = &state.directories.caches_dir();
|
||||||
|
let semaphore = &state.io_semaphore;
|
||||||
|
Ok(Some(
|
||||||
|
fetch::write_cached_icon(
|
||||||
|
&icon_path.to_string_lossy(),
|
||||||
|
cache_dir,
|
||||||
|
bytes,
|
||||||
|
semaphore,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// could not find icon (for instance, prism default icon, etc)
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn copy_dotminecraft(
|
||||||
|
profile_path: ProfilePathId,
|
||||||
|
dotminecraft: PathBuf,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
// Get full path to profile
|
||||||
|
let profile_path = profile_path.get_full_path().await?;
|
||||||
|
|
||||||
|
// std fs copy every file in dotminecraft to profile_path
|
||||||
|
let mut dir = io::read_dir(&dotminecraft).await?;
|
||||||
|
while let Some(entry) = dir
|
||||||
|
.next_entry()
|
||||||
|
.await
|
||||||
|
.map_err(|e| IOError::with_path(e, &dotminecraft))?
|
||||||
|
{
|
||||||
|
let path = entry.path();
|
||||||
|
copy_dir_to(
|
||||||
|
&path,
|
||||||
|
&profile_path.join(path.file_name().ok_or_else(|| {
|
||||||
|
crate::ErrorKind::InputError(format!(
|
||||||
|
"Invalid file: {}",
|
||||||
|
&path.display()
|
||||||
|
))
|
||||||
|
})?),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively fs::copy every file in src to dest
|
||||||
|
/// uses async recursion
|
||||||
|
#[theseus_macros::debug_pin]
|
||||||
|
#[async_recursion::async_recursion]
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn copy_dir_to(src: &Path, dst: &Path) -> crate::Result<()> {
|
||||||
|
if !src.is_dir() {
|
||||||
|
io::copy(src, dst).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the destination directory
|
||||||
|
io::create_dir_all(&dst).await?;
|
||||||
|
|
||||||
|
// Iterate over the directory
|
||||||
|
let mut dir = io::read_dir(&src).await?;
|
||||||
|
while let Some(child) = dir
|
||||||
|
.next_entry()
|
||||||
|
.await
|
||||||
|
.map_err(|e| IOError::with_path(e, src))?
|
||||||
|
{
|
||||||
|
let src_child = child.path();
|
||||||
|
let dst_child = dst.join(src_child.file_name().ok_or_else(|| {
|
||||||
|
crate::ErrorKind::InputError(format!(
|
||||||
|
"Invalid file: {}",
|
||||||
|
&src_child.display()
|
||||||
|
))
|
||||||
|
})?);
|
||||||
|
|
||||||
|
if src_child.is_dir() {
|
||||||
|
// Recurse into sub-directory
|
||||||
|
copy_dir_to(&src_child, &dst_child).await?;
|
||||||
|
} else {
|
||||||
|
// Copy file
|
||||||
|
io::copy(&src_child, &dst_child).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -2,8 +2,9 @@ use crate::config::MODRINTH_API_URL;
|
|||||||
use crate::data::ModLoader;
|
use crate::data::ModLoader;
|
||||||
use crate::event::emit::{emit_loading, init_loading};
|
use crate::event::emit::{emit_loading, init_loading};
|
||||||
use crate::event::{LoadingBarId, LoadingBarType};
|
use crate::event::{LoadingBarId, LoadingBarType};
|
||||||
|
use crate::prelude::ProfilePathId;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
LinkedData, ModrinthProject, ModrinthVersion, ProfilePathId, SideType,
|
LinkedData, ModrinthProject, ModrinthVersion, ProfileInstallStage, SideType,
|
||||||
};
|
};
|
||||||
use crate::util::fetch::{
|
use crate::util::fetch::{
|
||||||
fetch, fetch_advanced, fetch_json, write_cached_icon,
|
fetch, fetch_advanced, fetch_json, write_cached_icon,
|
||||||
@ -64,7 +65,7 @@ pub enum EnvType {
|
|||||||
Server,
|
Server,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Debug)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum PackDependency {
|
pub enum PackDependency {
|
||||||
Forge,
|
Forge,
|
||||||
@ -76,12 +77,14 @@ pub enum PackDependency {
|
|||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase", tag = "type")]
|
#[serde(rename_all = "camelCase", tag = "type")]
|
||||||
pub enum CreatePackLocation {
|
pub enum CreatePackLocation {
|
||||||
|
// Create a pack from a modrinth version ID (such as a modpack)
|
||||||
FromVersionId {
|
FromVersionId {
|
||||||
project_id: String,
|
project_id: String,
|
||||||
version_id: String,
|
version_id: String,
|
||||||
title: String,
|
title: String,
|
||||||
icon_url: Option<String>,
|
icon_url: Option<String>,
|
||||||
},
|
},
|
||||||
|
// Create a pack from a file (such as an .mrpack for installing from a file, or a folder name for importing)
|
||||||
FromFile {
|
FromFile {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
@ -100,9 +103,29 @@ pub struct CreatePackProfile {
|
|||||||
pub skip_install_profile: Option<bool>,
|
pub skip_install_profile: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
// default
|
||||||
pub struct CreatePackDescription {
|
impl Default for CreatePackProfile {
|
||||||
|
fn default() -> Self {
|
||||||
|
CreatePackProfile {
|
||||||
|
name: "Untitled".to_string(),
|
||||||
|
game_version: "1.19.4".to_string(),
|
||||||
|
modloader: ModLoader::Vanilla,
|
||||||
|
loader_version: None,
|
||||||
|
icon: None,
|
||||||
|
icon_url: None,
|
||||||
|
linked_data: None,
|
||||||
|
skip_install_profile: Some(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreatePack {
|
||||||
pub file: bytes::Bytes,
|
pub file: bytes::Bytes,
|
||||||
|
pub description: CreatePackDescription,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CreatePackDescription {
|
||||||
pub icon: Option<PathBuf>,
|
pub icon: Option<PathBuf>,
|
||||||
pub override_title: Option<String>,
|
pub override_title: Option<String>,
|
||||||
pub project_id: Option<String>,
|
pub project_id: Option<String>,
|
||||||
@ -122,16 +145,12 @@ pub fn get_profile_from_pack(
|
|||||||
icon_url,
|
icon_url,
|
||||||
} => CreatePackProfile {
|
} => CreatePackProfile {
|
||||||
name: title,
|
name: title,
|
||||||
game_version: "1.19.4".to_string(),
|
|
||||||
modloader: ModLoader::Vanilla,
|
|
||||||
loader_version: None,
|
|
||||||
icon: None,
|
|
||||||
icon_url,
|
icon_url,
|
||||||
linked_data: Some(LinkedData {
|
linked_data: Some(LinkedData {
|
||||||
project_id: Some(project_id),
|
project_id: Some(project_id),
|
||||||
version_id: Some(version_id),
|
version_id: Some(version_id),
|
||||||
}),
|
}),
|
||||||
skip_install_profile: Some(true),
|
..Default::default()
|
||||||
},
|
},
|
||||||
CreatePackLocation::FromFile { path } => {
|
CreatePackLocation::FromFile { path } => {
|
||||||
let file_name = path
|
let file_name = path
|
||||||
@ -142,13 +161,7 @@ pub fn get_profile_from_pack(
|
|||||||
|
|
||||||
CreatePackProfile {
|
CreatePackProfile {
|
||||||
name: file_name,
|
name: file_name,
|
||||||
game_version: "1.19.4".to_string(),
|
..Default::default()
|
||||||
modloader: ModLoader::Vanilla,
|
|
||||||
loader_version: None,
|
|
||||||
icon: None,
|
|
||||||
icon_url: None,
|
|
||||||
linked_data: None,
|
|
||||||
skip_install_profile: Some(true),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,7 +175,7 @@ pub async fn generate_pack_from_version_id(
|
|||||||
title: String,
|
title: String,
|
||||||
icon_url: Option<String>,
|
icon_url: Option<String>,
|
||||||
profile_path: ProfilePathId,
|
profile_path: ProfilePathId,
|
||||||
) -> crate::Result<CreatePackDescription> {
|
) -> crate::Result<CreatePack> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
|
|
||||||
let loading_bar = init_loading(
|
let loading_bar = init_loading(
|
||||||
@ -249,14 +262,16 @@ pub async fn generate_pack_from_version_id(
|
|||||||
};
|
};
|
||||||
emit_loading(&loading_bar, 10.0, None).await?;
|
emit_loading(&loading_bar, 10.0, None).await?;
|
||||||
|
|
||||||
Ok(CreatePackDescription {
|
Ok(CreatePack {
|
||||||
file,
|
file,
|
||||||
icon,
|
description: CreatePackDescription {
|
||||||
override_title: None,
|
icon,
|
||||||
project_id: Some(project_id),
|
override_title: None,
|
||||||
version_id: Some(version_id),
|
project_id: Some(project_id),
|
||||||
existing_loading_bar: Some(loading_bar),
|
version_id: Some(version_id),
|
||||||
profile_path,
|
existing_loading_bar: Some(loading_bar),
|
||||||
|
profile_path,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,15 +280,90 @@ pub async fn generate_pack_from_version_id(
|
|||||||
pub async fn generate_pack_from_file(
|
pub async fn generate_pack_from_file(
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
profile_path: ProfilePathId,
|
profile_path: ProfilePathId,
|
||||||
) -> crate::Result<CreatePackDescription> {
|
) -> crate::Result<CreatePack> {
|
||||||
let file = io::read(&path).await?;
|
let file = io::read(&path).await?;
|
||||||
Ok(CreatePackDescription {
|
Ok(CreatePack {
|
||||||
file: bytes::Bytes::from(file),
|
file: bytes::Bytes::from(file),
|
||||||
icon: None,
|
description: CreatePackDescription {
|
||||||
override_title: None,
|
icon: None,
|
||||||
project_id: None,
|
override_title: None,
|
||||||
version_id: None,
|
project_id: None,
|
||||||
existing_loading_bar: None,
|
version_id: None,
|
||||||
profile_path,
|
existing_loading_bar: None,
|
||||||
|
profile_path,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets generated profile attributes to the pack ones (using profile::edit)
|
||||||
|
/// This includes the pack name, icon, game version, loader version, and loader
|
||||||
|
#[theseus_macros::debug_pin]
|
||||||
|
pub async fn set_profile_information(
|
||||||
|
profile_path: ProfilePathId,
|
||||||
|
description: &CreatePackDescription,
|
||||||
|
backup_name: &str,
|
||||||
|
dependencies: &HashMap<PackDependency, String>,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let mut game_version: Option<&String> = None;
|
||||||
|
let mut mod_loader = None;
|
||||||
|
let mut loader_version = None;
|
||||||
|
|
||||||
|
for (key, value) in dependencies {
|
||||||
|
match key {
|
||||||
|
PackDependency::Forge => {
|
||||||
|
mod_loader = Some(ModLoader::Forge);
|
||||||
|
loader_version = Some(value);
|
||||||
|
}
|
||||||
|
PackDependency::FabricLoader => {
|
||||||
|
mod_loader = Some(ModLoader::Fabric);
|
||||||
|
loader_version = Some(value);
|
||||||
|
}
|
||||||
|
PackDependency::QuiltLoader => {
|
||||||
|
mod_loader = Some(ModLoader::Quilt);
|
||||||
|
loader_version = Some(value);
|
||||||
|
}
|
||||||
|
PackDependency::Minecraft => game_version = Some(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let game_version = if let Some(game_version) = game_version {
|
||||||
|
game_version
|
||||||
|
} else {
|
||||||
|
return Err(crate::ErrorKind::InputError(
|
||||||
|
"Pack did not specify Minecraft version".to_string(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla);
|
||||||
|
let loader_version = if mod_loader != ModLoader::Vanilla {
|
||||||
|
crate::profile_create::get_loader_version_from_loader(
|
||||||
|
game_version.clone(),
|
||||||
|
mod_loader,
|
||||||
|
loader_version.cloned(),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
// Sets values in profile
|
||||||
|
crate::api::profile::edit(&profile_path, |prof| {
|
||||||
|
prof.metadata.name = description
|
||||||
|
.override_title
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| backup_name.to_string());
|
||||||
|
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||||
|
prof.metadata.linked_data = Some(LinkedData {
|
||||||
|
project_id: description.project_id.clone(),
|
||||||
|
version_id: description.version_id.clone(),
|
||||||
|
});
|
||||||
|
prof.metadata.icon = description.icon.clone();
|
||||||
|
prof.metadata.game_version = game_version.clone();
|
||||||
|
prof.metadata.loader_version = loader_version.clone();
|
||||||
|
prof.metadata.loader = mod_loader;
|
||||||
|
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
use crate::data::ModLoader;
|
|
||||||
use crate::event::emit::{
|
use crate::event::emit::{
|
||||||
emit_loading, init_or_edit_loading, loading_try_for_each_concurrent,
|
emit_loading, init_or_edit_loading, loading_try_for_each_concurrent,
|
||||||
};
|
};
|
||||||
use crate::event::LoadingBarType;
|
use crate::event::LoadingBarType;
|
||||||
use crate::pack::install_from::{EnvType, PackFile, PackFileHash};
|
use crate::pack::install_from::{
|
||||||
use crate::state::{LinkedData, ProfileInstallStage, ProfilePathId, SideType};
|
set_profile_information, EnvType, PackFile, PackFileHash,
|
||||||
|
};
|
||||||
|
use crate::prelude::ProfilePathId;
|
||||||
|
use crate::state::SideType;
|
||||||
use crate::util::fetch::{fetch_mirrors, write};
|
use crate::util::fetch::{fetch_mirrors, write};
|
||||||
use crate::State;
|
use crate::State;
|
||||||
use async_zip::tokio::read::seek::ZipFileReader;
|
use async_zip::tokio::read::seek::ZipFileReader;
|
||||||
@ -13,17 +15,18 @@ use std::io::Cursor;
|
|||||||
use std::path::{Component, PathBuf};
|
use std::path::{Component, PathBuf};
|
||||||
|
|
||||||
use super::install_from::{
|
use super::install_from::{
|
||||||
generate_pack_from_file, generate_pack_from_version_id,
|
generate_pack_from_file, generate_pack_from_version_id, CreatePack,
|
||||||
CreatePackDescription, CreatePackLocation, PackDependency, PackFormat,
|
CreatePackLocation, PackFormat,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Install a modpack from a mrpack file (a modrinth .zip format)
|
||||||
#[theseus_macros::debug_pin]
|
#[theseus_macros::debug_pin]
|
||||||
pub async fn install_pack(
|
pub async fn install_zipped_mrpack(
|
||||||
location: CreatePackLocation,
|
location: CreatePackLocation,
|
||||||
profile_path: ProfilePathId,
|
profile: ProfilePathId,
|
||||||
) -> crate::Result<ProfilePathId> {
|
) -> crate::Result<ProfilePathId> {
|
||||||
// Get file from description
|
// Get file from description
|
||||||
let description: CreatePackDescription = match location {
|
let create_pack: CreatePack = match location {
|
||||||
CreatePackLocation::FromVersionId {
|
CreatePackLocation::FromVersionId {
|
||||||
project_id,
|
project_id,
|
||||||
version_id,
|
version_id,
|
||||||
@ -31,26 +34,22 @@ pub async fn install_pack(
|
|||||||
icon_url,
|
icon_url,
|
||||||
} => {
|
} => {
|
||||||
generate_pack_from_version_id(
|
generate_pack_from_version_id(
|
||||||
project_id,
|
project_id, version_id, title, icon_url, profile,
|
||||||
version_id,
|
|
||||||
title,
|
|
||||||
icon_url,
|
|
||||||
profile_path,
|
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
CreatePackLocation::FromFile { path } => {
|
CreatePackLocation::FromFile { path } => {
|
||||||
generate_pack_from_file(path, profile_path).await?
|
generate_pack_from_file(path, profile).await?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let file = description.file;
|
let file = create_pack.file;
|
||||||
let icon = description.icon;
|
let description = create_pack.description.clone(); // make a copy for profile edit function
|
||||||
let override_title = description.override_title;
|
let icon = create_pack.description.icon;
|
||||||
let project_id = description.project_id;
|
let project_id = create_pack.description.project_id;
|
||||||
let version_id = description.version_id;
|
let version_id = create_pack.description.version_id;
|
||||||
let existing_loading_bar = description.existing_loading_bar;
|
let existing_loading_bar = create_pack.description.existing_loading_bar;
|
||||||
let profile_path = description.profile_path;
|
let profile = create_pack.description.profile_path;
|
||||||
|
|
||||||
let state = &State::get().await?;
|
let state = &State::get().await?;
|
||||||
|
|
||||||
@ -92,69 +91,22 @@ pub async fn install_pack(
|
|||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut game_version = None;
|
// Sets generated profile attributes to the pack ones (using profile::edit)
|
||||||
let mut mod_loader = None;
|
set_profile_information(
|
||||||
let mut loader_version = None;
|
profile.clone(),
|
||||||
for (key, value) in &pack.dependencies {
|
&description,
|
||||||
match key {
|
&pack.name,
|
||||||
PackDependency::Forge => {
|
&pack.dependencies,
|
||||||
mod_loader = Some(ModLoader::Forge);
|
)
|
||||||
loader_version = Some(value);
|
|
||||||
}
|
|
||||||
PackDependency::FabricLoader => {
|
|
||||||
mod_loader = Some(ModLoader::Fabric);
|
|
||||||
loader_version = Some(value);
|
|
||||||
}
|
|
||||||
PackDependency::QuiltLoader => {
|
|
||||||
mod_loader = Some(ModLoader::Quilt);
|
|
||||||
loader_version = Some(value);
|
|
||||||
}
|
|
||||||
PackDependency::Minecraft => game_version = Some(value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let game_version = if let Some(game_version) = game_version {
|
|
||||||
game_version
|
|
||||||
} else {
|
|
||||||
return Err(crate::ErrorKind::InputError(
|
|
||||||
"Pack did not specify Minecraft version".to_string(),
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
};
|
|
||||||
|
|
||||||
let loader_version =
|
|
||||||
crate::profile_create::get_loader_version_from_loader(
|
|
||||||
game_version.clone(),
|
|
||||||
mod_loader.unwrap_or(ModLoader::Vanilla),
|
|
||||||
loader_version.cloned(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
crate::api::profile::edit(&profile_path, |prof| {
|
|
||||||
prof.metadata.name =
|
|
||||||
override_title.clone().unwrap_or_else(|| pack.name.clone());
|
|
||||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
|
||||||
prof.metadata.linked_data = Some(LinkedData {
|
|
||||||
project_id: project_id.clone(),
|
|
||||||
version_id: version_id.clone(),
|
|
||||||
});
|
|
||||||
prof.metadata.icon = icon.clone();
|
|
||||||
prof.metadata.game_version = game_version.clone();
|
|
||||||
prof.metadata.loader_version = loader_version.clone();
|
|
||||||
prof.metadata.loader = mod_loader.unwrap_or(ModLoader::Vanilla);
|
|
||||||
|
|
||||||
async { Ok(()) }
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let profile_path = profile_path.clone();
|
let profile_full_path = profile.get_full_path().await?;
|
||||||
|
let profile = profile.clone();
|
||||||
let result = async {
|
let result = async {
|
||||||
let loading_bar = init_or_edit_loading(
|
let loading_bar = init_or_edit_loading(
|
||||||
existing_loading_bar,
|
existing_loading_bar,
|
||||||
LoadingBarType::PackDownload {
|
LoadingBarType::PackDownload {
|
||||||
profile_path: profile_path
|
profile_path: profile_full_path.clone(),
|
||||||
.get_full_path()
|
|
||||||
.await?
|
|
||||||
.clone(),
|
|
||||||
pack_name: pack.name.clone(),
|
pack_name: pack.name.clone(),
|
||||||
icon,
|
icon,
|
||||||
pack_id: project_id,
|
pack_id: project_id,
|
||||||
@ -176,7 +128,7 @@ pub async fn install_pack(
|
|||||||
num_files,
|
num_files,
|
||||||
None,
|
None,
|
||||||
|project| {
|
|project| {
|
||||||
let profile_path = profile_path.clone();
|
let profile_full_path = profile_full_path.clone();
|
||||||
async move {
|
async move {
|
||||||
//TODO: Future update: prompt user for optional files in a modpack
|
//TODO: Future update: prompt user for optional files in a modpack
|
||||||
if let Some(env) = project.env {
|
if let Some(env) = project.env {
|
||||||
@ -210,9 +162,7 @@ pub async fn install_pack(
|
|||||||
match path {
|
match path {
|
||||||
Component::CurDir
|
Component::CurDir
|
||||||
| Component::Normal(_) => {
|
| Component::Normal(_) => {
|
||||||
let path = profile_path
|
let path = profile_full_path
|
||||||
.get_full_path()
|
|
||||||
.await?
|
|
||||||
.join(project.path);
|
.join(project.path);
|
||||||
write(
|
write(
|
||||||
&path,
|
&path,
|
||||||
@ -275,10 +225,7 @@ pub async fn install_pack(
|
|||||||
|
|
||||||
if new_path.file_name().is_some() {
|
if new_path.file_name().is_some() {
|
||||||
write(
|
write(
|
||||||
&profile_path
|
&profile_full_path.join(new_path),
|
||||||
.get_full_path()
|
|
||||||
.await?
|
|
||||||
.join(new_path),
|
|
||||||
&content,
|
&content,
|
||||||
&state.io_semaphore,
|
&state.io_semaphore,
|
||||||
)
|
)
|
||||||
@ -298,7 +245,7 @@ pub async fn install_pack(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(profile_val) =
|
if let Some(profile_val) =
|
||||||
crate::api::profile::get(&profile_path, None).await?
|
crate::api::profile::get(&profile, None).await?
|
||||||
{
|
{
|
||||||
crate::launcher::install_minecraft(
|
crate::launcher::install_minecraft(
|
||||||
&profile_val,
|
&profile_val,
|
||||||
@ -309,14 +256,14 @@ pub async fn install_pack(
|
|||||||
State::sync().await?;
|
State::sync().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<ProfilePathId, crate::Error>(profile_path.clone())
|
Ok::<ProfilePathId, crate::Error>(profile.clone())
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(profile) => Ok(profile),
|
Ok(profile) => Ok(profile),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let _ = crate::api::profile::remove(&profile_path).await;
|
let _ = crate::api::profile::remove(&profile).await;
|
||||||
|
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
@ -332,7 +279,7 @@ pub async fn install_pack(
|
|||||||
match result {
|
match result {
|
||||||
Ok(profile) => Ok(profile),
|
Ok(profile) => Ok(profile),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let _ = crate::api::profile::remove(&profile_path).await;
|
let _ = crate::api::profile::remove(&profile).await;
|
||||||
|
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
@ -1,2 +1,3 @@
|
|||||||
pub mod install;
|
pub mod import;
|
||||||
pub mod install_from;
|
pub mod install_from;
|
||||||
|
pub mod install_mrpack;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
//! Theseus profile management interface
|
//! Theseus profile management interface
|
||||||
use crate::state::{LinkedData, ProfilePathId};
|
use crate::pack::install_from::CreatePackProfile;
|
||||||
|
use crate::prelude::ProfilePathId;
|
||||||
|
use crate::state::LinkedData;
|
||||||
use crate::util::io::{self, canonicalize};
|
use crate::util::io::{self, canonicalize};
|
||||||
use crate::{
|
use crate::{
|
||||||
event::{emit::emit_profile, ProfilePayloadType},
|
event::{emit::emit_profile, ProfilePayloadType},
|
||||||
@ -128,6 +130,22 @@ pub async fn profile_create(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn profile_create_from_creator(
|
||||||
|
profile: CreatePackProfile,
|
||||||
|
) -> crate::Result<ProfilePathId> {
|
||||||
|
profile_create(
|
||||||
|
profile.name,
|
||||||
|
profile.game_version,
|
||||||
|
profile.modloader,
|
||||||
|
profile.loader_version,
|
||||||
|
profile.icon,
|
||||||
|
profile.icon_url,
|
||||||
|
profile.linked_data,
|
||||||
|
profile.skip_install_profile,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
#[theseus_macros::debug_pin]
|
#[theseus_macros::debug_pin]
|
||||||
pub(crate) async fn get_loader_version_from_loader(
|
pub(crate) async fn get_loader_version_from_loader(
|
||||||
|
|||||||
@ -7,6 +7,9 @@ pub enum ErrorKind {
|
|||||||
#[error("Filesystem error: {0}")]
|
#[error("Filesystem error: {0}")]
|
||||||
FSError(String),
|
FSError(String),
|
||||||
|
|
||||||
|
#[error("Serialization error (INI): {0}")]
|
||||||
|
INIError(#[from] serde_ini::de::Error),
|
||||||
|
|
||||||
#[error("Serialization error (JSON): {0}")]
|
#[error("Serialization error (JSON): {0}")]
|
||||||
JSONError(#[from] serde_json::Error),
|
JSONError(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,7 @@ pub struct LoadingBar {
|
|||||||
pub cli_progress_bar: indicatif::ProgressBar,
|
pub cli_progress_bar: indicatif::ProgressBar,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug, Clone)]
|
||||||
pub struct LoadingBarId(Uuid);
|
pub struct LoadingBarId(Uuid);
|
||||||
|
|
||||||
// When Loading bar id is dropped, we should remove it from the hashmap
|
// When Loading bar id is dropped, we should remove it from the hashmap
|
||||||
|
|||||||
@ -135,6 +135,21 @@ pub async fn rename(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy
|
||||||
|
pub async fn copy(
|
||||||
|
from: impl AsRef<std::path::Path>,
|
||||||
|
to: impl AsRef<std::path::Path>,
|
||||||
|
) -> Result<u64, IOError> {
|
||||||
|
let from = from.as_ref();
|
||||||
|
let to = to.as_ref();
|
||||||
|
tokio::fs::copy(from, to)
|
||||||
|
.await
|
||||||
|
.map_err(|e| IOError::IOPathError {
|
||||||
|
source: e,
|
||||||
|
path: from.to_string_lossy().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// remove file
|
// remove file
|
||||||
pub async fn remove_file(
|
pub async fn remove_file(
|
||||||
path: impl AsRef<std::path::Path>,
|
path: impl AsRef<std::path::Path>,
|
||||||
|
|||||||
70
theseus_gui/src-tauri/src/api/import.rs
Normal file
70
theseus_gui/src-tauri/src/api/import.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::api::Result;
|
||||||
|
use theseus::pack::import::ImportLauncherType;
|
||||||
|
|
||||||
|
use theseus::pack::import;
|
||||||
|
use theseus::prelude::ProfilePathId;
|
||||||
|
|
||||||
|
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||||
|
tauri::plugin::Builder::new("import")
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
import_get_importable_instances,
|
||||||
|
import_import_instance,
|
||||||
|
import_is_valid_importable_instance,
|
||||||
|
import_get_default_launcher_path,
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a list of importable instances from a launcher type and base path
|
||||||
|
/// eg: get_importable_instances(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"))
|
||||||
|
/// returns ["Instance 1", "Instance 2"]
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_get_importable_instances(
|
||||||
|
launcher_type: ImportLauncherType,
|
||||||
|
base_path: PathBuf,
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
Ok(import::get_importable_instances(launcher_type, base_path).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import an instance from a launcher type and base path
|
||||||
|
/// eg: import_instance(ImportLauncherType::MultiMC, PathBuf::from("C:/MultiMC"), "Instance 1")
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_import_instance(
|
||||||
|
profile_path: ProfilePathId,
|
||||||
|
launcher_type: ImportLauncherType,
|
||||||
|
base_path: PathBuf,
|
||||||
|
instance_folder: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
import::import_instance(
|
||||||
|
profile_path,
|
||||||
|
launcher_type,
|
||||||
|
base_path,
|
||||||
|
instance_folder,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if this instance is valid for importing, given a certain launcher type
|
||||||
|
/// eg: is_valid_importable_instance(PathBuf::from("C:/MultiMC/Instance 1"), ImportLauncherType::MultiMC)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_is_valid_importable_instance(
|
||||||
|
instance_folder: PathBuf,
|
||||||
|
launcher_type: ImportLauncherType,
|
||||||
|
) -> Result<bool> {
|
||||||
|
Ok(
|
||||||
|
import::is_valid_importable_instance(instance_folder, launcher_type)
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default path for the given launcher type
|
||||||
|
/// None if it can't be found or doesn't exist
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_get_default_launcher_path(
|
||||||
|
launcher_type: ImportLauncherType,
|
||||||
|
) -> Result<Option<PathBuf>> {
|
||||||
|
Ok(import::get_default_launcher_path(launcher_type))
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ use serde::{Serialize, Serializer};
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod import;
|
||||||
pub mod jre;
|
pub mod jre;
|
||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
|
|||||||
@ -2,8 +2,8 @@ use crate::api::Result;
|
|||||||
|
|
||||||
use theseus::{
|
use theseus::{
|
||||||
pack::{
|
pack::{
|
||||||
install::install_pack,
|
|
||||||
install_from::{CreatePackLocation, CreatePackProfile},
|
install_from::{CreatePackLocation, CreatePackProfile},
|
||||||
|
install_mrpack::install_zipped_mrpack,
|
||||||
},
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
@ -22,7 +22,7 @@ pub async fn pack_install(
|
|||||||
location: CreatePackLocation,
|
location: CreatePackLocation,
|
||||||
profile: ProfilePathId,
|
profile: ProfilePathId,
|
||||||
) -> Result<ProfilePathId> {
|
) -> Result<ProfilePathId> {
|
||||||
Ok(install_pack(location, profile).await?)
|
Ok(install_zipped_mrpack(location, profile).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@ -118,6 +118,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
let builder = builder
|
let builder = builder
|
||||||
.plugin(api::auth::init())
|
.plugin(api::auth::init())
|
||||||
|
.plugin(api::import::init())
|
||||||
.plugin(api::logs::init())
|
.plugin(api::logs::init())
|
||||||
.plugin(api::jre::init())
|
.plugin(api::jre::init())
|
||||||
.plugin(api::metadata::init())
|
.plugin(api::metadata::init())
|
||||||
|
|||||||
61
theseus_gui/src/helpers/import.js
Normal file
61
theseus_gui/src/helpers/import.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* All theseus API calls return serialized values (both return values and errors);
|
||||||
|
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||||
|
* and deserialized into a usable JS object.
|
||||||
|
*/
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
import { create } from './profile'
|
||||||
|
|
||||||
|
/*
|
||||||
|
API for importing instances from other launchers
|
||||||
|
launcherType can be one of the following:
|
||||||
|
- MultiMC
|
||||||
|
- GDLauncher
|
||||||
|
- ATLauncher
|
||||||
|
- Curseforge
|
||||||
|
- PrismLauncher
|
||||||
|
- Unknown (shouldn't be used, but is used internally if the launcher type isn't recognized)
|
||||||
|
|
||||||
|
For each launcher type, we can get a guess of the default path for the launcher, and a list of importable instances
|
||||||
|
For most launchers, this will be the application's data directory, with two exceptions:
|
||||||
|
- MultiMC: this goes to the app directory (wherever the app is)
|
||||||
|
- Curseforge: this goes to the 'minecraft' subdirectory of the data directory, as Curseforge has multiple games
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Gets a list of importable instances from a launcher type and base path
|
||||||
|
/// eg: get_importable_instances("MultiMC", "C:/MultiMC")
|
||||||
|
/// returns ["Instance 1", "Instance 2"]
|
||||||
|
export async function get_importable_instances(launcherType, basePath) {
|
||||||
|
return await invoke('plugin:import|import_get_importable_instances', { launcherType, basePath })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import an instance from a launcher type and base path
|
||||||
|
/// eg: import_instance("profile-name-to-go-to", "MultiMC", "C:/MultiMC", "Instance 1")
|
||||||
|
export async function import_instance(launcherType, basePath, instanceFolder) {
|
||||||
|
// create a basic, empty instance (most properties will be filled in by the import process)
|
||||||
|
const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null)
|
||||||
|
|
||||||
|
return await invoke('plugin:import|import_import_instance', {
|
||||||
|
profilePath,
|
||||||
|
launcherType,
|
||||||
|
basePath,
|
||||||
|
instanceFolder,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if this instance is valid for importing, given a certain launcher type
|
||||||
|
/// eg: is_valid_importable_instance("C:/MultiMC/Instance 1", "MultiMC")
|
||||||
|
export async function is_valid_importable_instance(instanceFolder, launcherType) {
|
||||||
|
return await invoke('plugin:import|import_is_valid_importable_instance', {
|
||||||
|
instanceFolder,
|
||||||
|
launcherType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the default path for the given launcher type
|
||||||
|
/// null if it can't be found or doesn't exist
|
||||||
|
/// eg: get_default_launcher_path("MultiMC")
|
||||||
|
export async function get_default_launcher_path(launcherType) {
|
||||||
|
return await invoke('plugin:import|import_get_default_launcher_path', { launcherType })
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user