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:
Wyatt Verchere 2023-07-22 12:37:19 -07:00 committed by GitHub
parent 3fa33dc241
commit 70aaf6eef9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1440 additions and 127 deletions

View File

@ -56,4 +56,5 @@
"rust-analyzer.linkedProjects": [ "rust-analyzer.linkedProjects": [
"./theseus/Cargo.toml" "./theseus/Cargo.toml"
], ],
"rust-analyzer.showUnlinkedFileNotification": false,
} }

24
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View File

@ -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(())
}

View File

@ -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)
} }

View File

@ -1,2 +1,3 @@
pub mod install; pub mod import;
pub mod install_from; pub mod install_from;
pub mod install_mrpack;

View File

@ -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(

View File

@ -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),

View File

@ -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

View File

@ -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>,

View 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))
}

View File

@ -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;

View File

@ -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]

View File

@ -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())

View 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 })
}