Migrate to Turborepo (#1251)

This commit is contained in:
Evan Song
2024-07-04 21:46:29 -07:00
committed by GitHub
parent 6fa1acc461
commit 0f2ddb452c
811 changed files with 5623 additions and 7832 deletions

View File

@@ -0,0 +1,75 @@
use std::path::PathBuf;
use crate::{
event::{
emit::{emit_command, emit_warning},
CommandPayload,
},
util::io,
};
/// Handles external functions (such as through URL deep linkage)
/// Link is extracted value (link) in somewhat URL format, such as
/// subdomain1/subdomain2
/// (Does not include modrinth://)
pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
Ok(match sublink.split_once('/') {
// /mod/{id} - Installs a mod of mod id
Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() },
// /version/{id} - Installs a specific version of id
Some(("version", id)) => {
CommandPayload::InstallVersion { id: id.to_string() }
}
// /modpack/{id} - Installs a modpack of modpack id
Some(("modpack", id)) => {
CommandPayload::InstallModpack { id: id.to_string() }
}
_ => {
emit_warning(&format!(
"Invalid command, unrecognized path: {sublink}"
))
.await?;
return Err(crate::ErrorKind::InputError(format!(
"Invalid command, unrecognized path: {sublink}"
))
.into());
}
})
}
pub async fn parse_command(
command_string: &str,
) -> crate::Result<CommandPayload> {
tracing::debug!("Parsing command: {}", &command_string);
// modrinth://some-command
// This occurs when following a web redirect link
if let Some(sublink) = command_string.strip_prefix("modrinth://") {
Ok(handle_url(sublink).await?)
} else {
// We assume anything else is a filepath to an .mrpack file
let path = PathBuf::from(command_string);
let path = io::canonicalize(path)?;
if let Some(ext) = path.extension() {
if ext == "mrpack" {
return Ok(CommandPayload::RunMRPack { path });
}
}
emit_warning(&format!(
"Invalid command, unrecognized filetype: {}",
path.display()
))
.await?;
Err(crate::ErrorKind::InputError(format!(
"Invalid command, unrecognized filetype: {}",
path.display()
))
.into())
}
}
pub async fn parse_and_emit_command(command_string: &str) -> crate::Result<()> {
let command = parse_command(command_string).await?;
emit_command(command).await?;
Ok(())
}

View File

@@ -0,0 +1,174 @@
//! Authentication flow interface
use reqwest::Method;
use serde::Deserialize;
use std::path::PathBuf;
use crate::event::emit::{emit_loading, init_loading};
use crate::state::CredentialsStore;
use crate::util::fetch::{fetch_advanced, fetch_json};
use crate::util::io;
use crate::util::jre::extract_java_majorminor_version;
use crate::{
util::jre::{self, JavaVersion},
LoadingBarType, State,
};
// Searches for jres on the system given a java version (ex: 1.8, 1.17, 1.18)
// Allow higher allows for versions higher than the given version to be returned ('at least')
pub async fn find_filtered_jres(
java_version: Option<u32>,
) -> crate::Result<Vec<JavaVersion>> {
let jres = jre::get_all_jre().await?;
// Filter out JREs that are not 1.17 or higher
Ok(if let Some(java_version) = java_version {
jres.into_iter()
.filter(|jre| {
let jre_version = extract_java_majorminor_version(&jre.version);
if let Ok(jre_version) = jre_version {
jre_version.1 == java_version
} else {
false
}
})
.collect()
} else {
jres
})
}
#[theseus_macros::debug_pin]
pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
let state = State::get().await?;
let loading_bar = init_loading(
LoadingBarType::JavaDownload {
version: java_version,
},
100.0,
"Downloading java version",
)
.await?;
#[derive(Deserialize)]
struct Package {
pub download_url: String,
pub name: PathBuf,
}
emit_loading(&loading_bar, 0.0, Some("Fetching java version")).await?;
let packages = fetch_json::<Vec<Package>>(
Method::GET,
&format!(
"https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jre&page_size=1",
std::env::consts::ARCH, java_version, std::env::consts::OS
),
None,
None,
&state.fetch_semaphore,
&CredentialsStore(None),
).await?;
emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
if let Some(download) = packages.first() {
let file = fetch_advanced(
Method::GET,
&download.download_url,
None,
None,
None,
Some((&loading_bar, 80.0)),
&state.fetch_semaphore,
&CredentialsStore(None),
)
.await?;
let path = state.directories.java_versions_dir().await;
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read java zip".to_string(),
))
})?;
// removes the old installation of java
if let Some(file) = archive.file_names().next() {
if let Some(dir) = file.split('/').next() {
let path = path.join(dir);
if path.exists() {
io::remove_dir_all(path).await?;
}
}
}
emit_loading(&loading_bar, 0.0, Some("Extracting java")).await?;
archive.extract(&path).map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to extract java zip".to_string(),
))
})?;
emit_loading(&loading_bar, 10.0, Some("Done extracting java")).await?;
let mut base_path = path.join(
download
.name
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
);
#[cfg(target_os = "macos")]
{
base_path = base_path
.join(format!("zulu-{}.jre", java_version))
.join("Contents")
.join("Home")
.join("bin")
.join("java")
}
#[cfg(not(target_os = "macos"))]
{
base_path = base_path.join("bin").join(jre::JAVA_BIN)
}
Ok(base_path)
} else {
Err(crate::ErrorKind::LauncherError(format!(
"No Java Version found for Java version {}, OS {}, and Architecture {}",
java_version, std::env::consts::OS, std::env::consts::ARCH,
)).into())
}
}
// Validates JRE at a given at a given path
pub async fn check_jre(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
Ok(jre::check_java_at_filepath(&path).await)
}
// Test JRE at a given path
pub async fn test_jre(
path: PathBuf,
major_version: u32,
) -> crate::Result<bool> {
let jre = match jre::check_java_at_filepath(&path).await {
Some(jre) => jre,
None => return Ok(false),
};
let (major, _) = extract_java_majorminor_version(&jre.version)?;
Ok(major == major_version)
}
// Gets maximum memory in KiB.
pub async fn get_max_memory() -> crate::Result<u64> {
Ok(sys_info::mem_info()
.map_err(|_| {
crate::Error::from(crate::ErrorKind::LauncherError(
"Unable to get computer memory".to_string(),
))
})?
.total)
}

View File

@@ -0,0 +1,372 @@
use std::io::{Read, SeekFrom};
use std::time::SystemTime;
use futures::TryFutureExt;
use serde::{Deserialize, Serialize};
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncSeekExt},
};
use crate::{
prelude::{Credentials, DirectoryInfo},
util::io::{self, IOError},
{state::ProfilePathId, State},
};
#[derive(Serialize, Debug)]
pub struct Logs {
pub log_type: LogType,
pub filename: String,
pub age: u64,
pub output: Option<CensoredString>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
pub enum LogType {
InfoLog,
CrashReport,
}
#[derive(Serialize, Debug)]
pub struct LatestLogCursor {
pub cursor: u64,
pub output: CensoredString,
pub new_file: bool,
}
#[derive(Serialize, Debug)] // Not deserialize
#[serde(transparent)]
pub struct CensoredString(String);
impl CensoredString {
pub fn censor(mut s: String, credentials_set: &Vec<Credentials>) -> Self {
let username = whoami::username();
s = s
.replace(&format!("/{}/", username), "/{COMPUTER_USERNAME}/")
.replace(&format!("\\{}\\", username), "\\{COMPUTER_USERNAME}\\");
for credentials in credentials_set {
s = s
.replace(&credentials.access_token, "{MINECRAFT_ACCESS_TOKEN}")
.replace(&credentials.username, "{MINECRAFT_USERNAME}")
.replace(
&credentials.id.as_simple().to_string(),
"{MINECRAFT_UUID}",
)
.replace(
&credentials.id.as_hyphenated().to_string(),
"{MINECRAFT_UUID}",
);
}
Self(s)
}
}
impl Logs {
async fn build(
log_type: LogType,
age: SystemTime,
profile_subpath: &ProfilePathId,
filename: String,
clear_contents: Option<bool>,
) -> crate::Result<Self> {
Ok(Self {
log_type,
age: age
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs(),
output: if clear_contents.unwrap_or(false) {
None
} else {
Some(
get_output_by_filename(
profile_subpath,
log_type,
&filename,
)
.await?,
)
},
filename,
})
}
}
#[tracing::instrument]
pub async fn get_logs_from_type(
profile_path: &ProfilePathId,
log_type: LogType,
clear_contents: Option<bool>,
logs: &mut Vec<crate::Result<Logs>>,
) -> crate::Result<()> {
let logs_folder = match log_type {
LogType::InfoLog => {
DirectoryInfo::profile_logs_dir(profile_path).await?
}
LogType::CrashReport => {
DirectoryInfo::crash_reports_dir(profile_path).await?
}
};
if logs_folder.exists() {
for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &logs_folder))?
{
let entry: std::fs::DirEntry =
entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
let age = entry.metadata()?.created().unwrap_or_else(|_| SystemTime::UNIX_EPOCH);
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(file_name) = path.file_name() {
let file_name = file_name.to_string_lossy().to_string();
logs.push(
Logs::build(
log_type,
age,
&profile_path,
file_name,
clear_contents,
)
.await,
);
}
}
}
Ok(())
}
#[tracing::instrument]
pub async fn get_logs(
profile_path_id: ProfilePathId,
clear_contents: Option<bool>,
) -> crate::Result<Vec<Logs>> {
let profile_path = profile_path_id.profile_path().await?;
let mut logs = Vec::new();
get_logs_from_type(
&profile_path,
LogType::InfoLog,
clear_contents,
&mut logs,
)
.await?;
get_logs_from_type(
&profile_path,
LogType::CrashReport,
clear_contents,
&mut logs,
)
.await?;
let mut logs = logs.into_iter().collect::<crate::Result<Vec<Logs>>>()?;
logs.sort_by(|a, b| b.age.cmp(&a.age).then(b.filename.cmp(&a.filename)));
Ok(logs)
}
#[tracing::instrument]
pub async fn get_logs_by_filename(
profile_path_id: ProfilePathId,
log_type: LogType,
filename: String,
) -> crate::Result<Logs> {
let profile_path = profile_path_id.profile_path().await?;
let path = match log_type {
LogType::InfoLog => {
DirectoryInfo::profile_logs_dir(&profile_path).await
}
LogType::CrashReport => {
DirectoryInfo::crash_reports_dir(&profile_path).await
}
}?
.join(&filename);
let metadata = std::fs::metadata(&path)?;
let age = metadata.created().unwrap_or_else(|_| SystemTime::UNIX_EPOCH);
Logs::build(log_type, age, &profile_path, filename, Some(true)).await
}
#[tracing::instrument]
pub async fn get_output_by_filename(
profile_subpath: &ProfilePathId,
log_type: LogType,
file_name: &str,
) -> crate::Result<CensoredString> {
let state = State::get().await?;
let logs_folder = match log_type {
LogType::InfoLog => {
DirectoryInfo::profile_logs_dir(profile_subpath).await?
}
LogType::CrashReport => {
DirectoryInfo::crash_reports_dir(profile_subpath).await?
}
};
let path = logs_folder.join(file_name);
let credentials: Vec<Credentials> = state
.users
.read()
.await
.users
.clone()
.into_values()
.collect();
// Load .gz file into String
if let Some(ext) = path.extension() {
if ext == "gz" {
let file = std::fs::File::open(&path)
.map_err(|e| IOError::with_path(e, &path))?;
let mut contents = [0; 1024];
let mut result = String::new();
let mut gz =
flate2::read::GzDecoder::new(std::io::BufReader::new(file));
while gz
.read(&mut contents)
.map_err(|e| IOError::with_path(e, &path))?
> 0
{
result.push_str(&String::from_utf8_lossy(&contents));
contents = [0; 1024];
}
return Ok(CensoredString::censor(result, &credentials));
} else if ext == "log" || ext == "txt" {
let mut result = String::new();
let mut contents = [0; 1024];
let mut file = std::fs::File::open(&path)
.map_err(|e| IOError::with_path(e, &path))?;
// iteratively read the file to a String
while file
.read(&mut contents)
.map_err(|e| IOError::with_path(e, &path))?
> 0
{
result.push_str(&String::from_utf8_lossy(&contents));
contents = [0; 1024];
}
let result = CensoredString::censor(result, &credentials);
return Ok(result);
}
}
Err(crate::ErrorKind::OtherError(format!(
"File extension not supported: {}",
path.display()
))
.into())
}
#[tracing::instrument]
pub async fn delete_logs(profile_path_id: ProfilePathId) -> crate::Result<()> {
let profile_path = profile_path_id.profile_path().await?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &logs_folder))?
{
let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
let path = entry.path();
if path.is_dir() {
io::remove_dir_all(&path).await?;
}
}
Ok(())
}
#[tracing::instrument]
pub async fn delete_logs_by_filename(
profile_path_id: ProfilePathId,
log_type: LogType,
filename: &str,
) -> crate::Result<()> {
let profile_path = profile_path_id.profile_path().await?;
let logs_folder = match log_type {
LogType::InfoLog => {
DirectoryInfo::profile_logs_dir(&profile_path).await
}
LogType::CrashReport => {
DirectoryInfo::crash_reports_dir(&profile_path).await
}
}?;
let path = logs_folder.join(filename);
io::remove_dir_all(&path).await?;
Ok(())
}
#[tracing::instrument]
pub async fn get_latest_log_cursor(
profile_path: ProfilePathId,
cursor: u64, // 0 to start at beginning of file
) -> crate::Result<LatestLogCursor> {
get_generic_live_log_cursor(profile_path, "latest.log", cursor).await
}
#[tracing::instrument]
pub async fn get_generic_live_log_cursor(
profile_path_id: ProfilePathId,
log_file_name: &str,
mut cursor: u64, // 0 to start at beginning of file
) -> crate::Result<LatestLogCursor> {
let profile_path = profile_path_id.profile_path().await?;
let state = State::get().await?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
let path = logs_folder.join(log_file_name);
if !path.exists() {
// Allow silent failure if latest.log doesn't exist (as the instance may have been launched, but not yet created the file)
return Ok(LatestLogCursor {
cursor: 0,
new_file: false,
output: CensoredString("".to_string()),
});
}
let mut file = File::open(&path)
.await
.map_err(|e| IOError::with_path(e, &path))?;
let metadata = file
.metadata()
.await
.map_err(|e| IOError::with_path(e, &path))?;
let mut new_file = false;
if cursor > metadata.len() {
// Cursor is greater than file length, reset cursor to 0
// Likely cause is that the file was rotated while the log was being read
cursor = 0;
new_file = true;
}
let mut buffer = Vec::new();
file.seek(SeekFrom::Start(cursor))
.map_err(|e| IOError::with_path(e, &path))
.await?; // Seek to cursor
let bytes_read = file
.read_to_end(&mut buffer)
.map_err(|e| IOError::with_path(e, &path))
.await?; // Read to end of file
let output = String::from_utf8_lossy(&buffer).to_string(); // Convert to String
let cursor = cursor + bytes_read as u64; // Update cursor
let credentials: Vec<Credentials> = state
.users
.read()
.await
.users
.clone()
.into_values()
.collect();
let output = CensoredString::censor(output, &credentials);
Ok(LatestLogCursor {
cursor,
new_file,
output,
})
}

View File

@@ -0,0 +1,43 @@
use crate::State;
pub use daedalus::minecraft::VersionManifest;
pub use daedalus::modded::Manifest;
#[tracing::instrument]
pub async fn get_minecraft_versions() -> crate::Result<VersionManifest> {
let state = State::get().await?;
let tags = state.metadata.read().await.minecraft.clone();
Ok(tags)
}
#[tracing::instrument]
pub async fn get_fabric_versions() -> crate::Result<Manifest> {
let state = State::get().await?;
let tags = state.metadata.read().await.fabric.clone();
Ok(tags)
}
#[tracing::instrument]
pub async fn get_forge_versions() -> crate::Result<Manifest> {
let state = State::get().await?;
let tags = state.metadata.read().await.forge.clone();
Ok(tags)
}
#[tracing::instrument]
pub async fn get_quilt_versions() -> crate::Result<Manifest> {
let state = State::get().await?;
let tags = state.metadata.read().await.quilt.clone();
Ok(tags)
}
#[tracing::instrument]
pub async fn get_neoforge_versions() -> crate::Result<Manifest> {
let state = State::get().await?;
let tags = state.metadata.read().await.neoforge.clone();
Ok(tags)
}

View File

@@ -0,0 +1,76 @@
//! Authentication flow interface
use crate::state::{Credentials, MinecraftLoginFlow};
use crate::State;
#[tracing::instrument]
pub async fn begin_login() -> crate::Result<MinecraftLoginFlow> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.login_begin().await
}
#[tracing::instrument]
pub async fn finish_login(
code: &str,
flow: MinecraftLoginFlow,
) -> crate::Result<Credentials> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.login_finish(code, flow).await
}
#[tracing::instrument]
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.default_user)
}
#[tracing::instrument]
pub async fn set_default_user(user: uuid::Uuid) -> crate::Result<()> {
let user = get_user(user).await?;
let state = State::get().await?;
let mut users = state.users.write().await;
users.default_user = Some(user.id);
users.save().await?;
Ok(())
}
/// Remove a user account from the database
#[tracing::instrument]
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
let state = State::get().await?;
let mut users = state.users.write().await;
users.remove(user).await?;
Ok(())
}
/// Get a copy of the list of all user credentials
#[tracing::instrument]
pub async fn users() -> crate::Result<Vec<Credentials>> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.users.values().cloned().collect())
}
/// Get a specific user by user ID
/// Prefer to use 'refresh' instead of this function
#[tracing::instrument]
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
let state = State::get().await?;
let users = state.users.read().await;
let user = users
.users
.get(&user)
.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to get nonexistent user with ID {user}"
))
.as_error()
})?
.clone();
Ok(user)
}

View File

@@ -0,0 +1,40 @@
//! API for interacting with Theseus
pub mod handler;
pub mod jre;
pub mod logs;
pub mod metadata;
pub mod minecraft_auth;
pub mod mr_auth;
pub mod pack;
pub mod process;
pub mod profile;
pub mod safety;
pub mod settings;
pub mod tags;
pub mod data {
pub use crate::state::{
Credentials, DirectoryInfo, Hooks, JavaSettings, LinkedData,
MemorySettings, ModLoader, ModrinthCredentials,
ModrinthCredentialsResult, ModrinthProject, ModrinthTeamMember,
ModrinthUser, ModrinthVersion, ProfileMetadata, ProjectMetadata,
Settings, Theme, WindowSize,
};
}
pub mod prelude {
pub use crate::{
data::*,
event::CommandPayload,
jre, metadata, minecraft_auth, pack, process,
profile::{self, create, Profile},
settings,
state::JavaGlobals,
state::{Dependency, ProfilePathId, ProjectPathId},
util::{
io::{canonicalize, IOError},
jre::JavaVersion,
},
State,
};
}

View File

@@ -0,0 +1,144 @@
use crate::state::{
ModrinthAuthFlow, ModrinthCredentials, ModrinthCredentialsResult,
};
use crate::ErrorKind;
#[tracing::instrument]
pub async fn authenticate_begin_flow(provider: &str) -> crate::Result<String> {
let state = crate::State::get().await?;
// Don't start an uncompleteable new flow if there's an existing locked one
let mut write: tokio::sync::RwLockWriteGuard<'_, Option<ModrinthAuthFlow>> =
state.modrinth_auth_flow.write().await;
let mut flow = ModrinthAuthFlow::new(provider).await?;
let url = flow.prepare_login_url().await?;
*write = Some(flow);
Ok(url)
}
#[tracing::instrument]
pub async fn authenticate_await_complete_flow(
) -> crate::Result<ModrinthCredentialsResult> {
let state = crate::State::get().await?;
let mut write = state.modrinth_auth_flow.write().await;
if let Some(ref mut flow) = *write {
let creds = flow.extract_credentials(&state.fetch_semaphore).await?;
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
let mut write = state.credentials.write().await;
write.login(creds.clone()).await?;
}
Ok(creds)
} else {
Err(ErrorKind::OtherError(
"No active Modrinth authenication flow!".to_string(),
)
.into())
}
}
#[tracing::instrument]
pub async fn cancel_flow() -> crate::Result<()> {
let state = crate::State::get().await?;
let mut write = state.modrinth_auth_flow.write().await;
if let Some(ref mut flow) = *write {
flow.close().await?;
}
*write = None;
Ok(())
}
pub async fn login_password(
username: &str,
password: &str,
challenge: &str,
) -> crate::Result<ModrinthCredentialsResult> {
let state = crate::State::get().await?;
let creds = crate::state::login_password(
username,
password,
challenge,
&state.fetch_semaphore,
)
.await?;
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
let mut write = state.credentials.write().await;
write.login(creds.clone()).await?;
}
Ok(creds)
}
#[tracing::instrument]
pub async fn login_2fa(
code: &str,
flow: &str,
) -> crate::Result<ModrinthCredentials> {
let state = crate::State::get().await?;
let creds =
crate::state::login_2fa(code, flow, &state.fetch_semaphore).await?;
let mut write = state.credentials.write().await;
write.login(creds.clone()).await?;
Ok(creds)
}
#[tracing::instrument]
pub async fn create_account(
username: &str,
email: &str,
password: &str,
challenge: &str,
sign_up_newsletter: bool,
) -> crate::Result<ModrinthCredentials> {
let state = crate::State::get().await?;
let creds = crate::state::create_account(
username,
email,
password,
challenge,
sign_up_newsletter,
&state.fetch_semaphore,
)
.await?;
let mut write = state.credentials.write().await;
write.login(creds.clone()).await?;
Ok(creds)
}
#[tracing::instrument]
pub async fn refresh() -> crate::Result<()> {
let state = crate::State::get().await?;
let mut write = state.credentials.write().await;
crate::state::refresh_credentials(&mut write, &state.fetch_semaphore)
.await?;
Ok(())
}
#[tracing::instrument]
pub async fn logout() -> crate::Result<()> {
let state = crate::State::get().await?;
let mut write = state.credentials.write().await;
write.logout().await?;
Ok(())
}
#[tracing::instrument]
pub async fn get_credentials() -> crate::Result<Option<ModrinthCredentials>> {
let state = crate::State::get().await?;
let read = state.credentials.read().await;
Ok(read.0.clone())
}

View File

@@ -0,0 +1,266 @@
use std::{collections::HashMap, path::PathBuf};
use serde::{Deserialize, Serialize};
use crate::{
pack::{
self,
import::{self, copy_dotminecraft},
install_from::CreatePackDescription,
},
prelude::{ModLoader, Profile, 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>,
}
#[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 client_side: Option<String>,
pub server_side: Option<String>,
pub categories: Vec<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 =
io::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);
if let Err(e) = instance {
tracing::warn!(
"Could not parse instance.json at {}: {}",
instance_folder.display(),
e
);
false
} else {
true
}
}
#[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,
)
.await?;
Ok(())
}
async fn import_atlauncher_unmanaged(
profile_path: ProfilePathId,
minecraft_folder: PathBuf,
backup_name: String,
description: CreatePackDescription,
atinstance: ATInstance,
) -> 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(),
locked: Some(
description.project_id.is_some()
&& description.version_id.is_some(),
),
});
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)
let state = State::get().await?;
let loading_bar = copy_dotminecraft(
profile_path.clone(),
minecraft_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await?
{
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
{
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(
&profile_val.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
}
State::sync().await?;
}
Ok(())
}

View File

@@ -0,0 +1,198 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::prelude::Profile;
use crate::state::CredentialsStore;
use crate::{
prelude::{ModLoader, ProfilePathId},
state::ProfileInstallStage,
util::{
fetch::{fetch, write_cached_icon},
io,
},
State,
};
use super::{copy_dotminecraft, recache_icon};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MinecraftInstance {
pub name: Option<String>,
pub base_mod_loader: Option<MinecraftInstanceModLoader>,
pub profile_image_path: Option<PathBuf>,
pub installed_modpack: Option<InstalledModpack>,
pub game_version: String, // Minecraft game version. Non-prioritized, use this if Vanilla
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct MinecraftInstanceModLoader {
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InstalledModpack {
pub thumbnail_url: Option<String>,
}
// Check if folder has a minecraftinstance.json that parses
pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
let minecraftinstance: String =
io::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<()> {
// 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())
);
let state = State::get().await?;
// Recache Curseforge Icon if it exists
let mut icon = None;
if let Some(icon_path) = minecraft_instance.profile_image_path.clone() {
icon = recache_icon(icon_path).await?;
} else if let Some(InstalledModpack {
thumbnail_url: Some(thumbnail_url),
}) = minecraft_instance.installed_modpack.clone()
{
let icon_bytes = fetch(
&thumbnail_url,
None,
&state.fetch_semaphore,
&CredentialsStore(None),
)
.await?;
let filename = thumbnail_url.rsplit('/').last();
if let Some(filename) = filename {
icon = Some(
write_cached_icon(
filename,
&state.directories.caches_dir(),
icon_bytes,
&state.io_semaphore,
)
.await?,
);
}
}
// base mod loader is always None for vanilla
if let Some(instance_mod_loader) = minecraft_instance.base_mod_loader {
let game_version = minecraft_instance.game_version;
// CF allows Forge, Fabric, and Vanilla
let mut mod_loader = None;
let mut loader_version = None;
match instance_mod_loader.name.split('-').collect::<Vec<&str>>()[..] {
["forge", version] => {
mod_loader = Some(ModLoader::Forge);
loader_version = Some(version.to_string());
}
["fabric", version, _game_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 {
// create 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
let state = State::get().await?;
let loading_bar = copy_dotminecraft(
profile_path.clone(),
curseforge_instance_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await?
{
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
{
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(
&profile_val.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
}
State::sync().await?;
}
Ok(())
}

View File

@@ -0,0 +1,134 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::{
prelude::{ModLoader, Profile, 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 =
io::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
let state = State::get().await?;
let loading_bar = copy_dotminecraft(
profile_path.clone(),
gdlauncher_instance_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await?
{
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
{
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(
&profile_val.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
}
State::sync().await?;
}
Ok(())
}

View File

@@ -0,0 +1,344 @@
use std::path::{Path, PathBuf};
use serde::{de, Deserialize, Serialize};
use crate::{
pack::{
import::{self, copy_dotminecraft},
install_from::{self, CreatePackDescription, PackDependency},
},
prelude::{Profile, 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>,
#[serde(rename = "ManagedPackID")]
pub managed_pack_id: Option<String>,
pub managed_pack_type: Option<MMCManagedPackType>,
#[serde(rename = "ManagedPackVersionID")]
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>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[serde(untagged)]
enum MMCLauncherEnum {
General(MMCLauncherGeneral),
Instance(MMCLauncher),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct MMCLauncherGeneral {
pub general: MMCLauncher,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct MMCLauncher {
instance_dir: 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 io::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()
}
#[tracing::instrument]
pub async fn get_instances_subpath(config: PathBuf) -> Option<String> {
let launcher = io::read_to_string(&config).await.ok()?;
let launcher: MMCLauncherEnum = serde_ini::from_str(&launcher).ok()?;
match launcher {
MMCLauncherEnum::General(p) => Some(p.general.instance_dir),
MMCLauncherEnum::Instance(p) => Some(p.instance_dir),
}
}
// Loading the INI (instance.cfg) file
async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
let instance_cfg: String = 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,
false,
)
.await?;
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
let state = State::get().await?;
let loading_bar = copy_dotminecraft(
profile_path.clone(),
minecraft_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await?
{
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
{
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(
&profile_val.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
}
State::sync().await?;
}
Ok(())
}

View File

@@ -0,0 +1,320 @@
use std::{
fmt,
path::{Path, PathBuf},
};
use io::IOError;
use serde::{Deserialize, Serialize};
use crate::{
event::{
emit::{emit_loading, init_or_edit_loading},
LoadingBarId,
},
prelude::ProfilePathId,
state::Profiles,
util::{
fetch::{self, IoSemaphore},
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,
}
// impl display
impl fmt::Display for ImportLauncherType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImportLauncherType::MultiMC => write!(f, "MultiMC"),
ImportLauncherType::PrismLauncher => write!(f, "PrismLauncher"),
ImportLauncherType::ATLauncher => write!(f, "ATLauncher"),
ImportLauncherType::GDLauncher => write!(f, "GDLauncher"),
ImportLauncherType::Curseforge => write!(f, "Curseforge"),
ImportLauncherType::Unknown => write!(f, "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_subfolder = match launcher_type {
ImportLauncherType::GDLauncher | ImportLauncherType::ATLauncher => {
"instances".to_string()
}
ImportLauncherType::Curseforge => "Instances".to_string(),
ImportLauncherType::MultiMC => {
mmc::get_instances_subpath(base_path.clone().join("multimc.cfg"))
.await
.unwrap_or_else(|| "instances".to_string())
}
ImportLauncherType::PrismLauncher => mmc::get_instances_subpath(
base_path.clone().join("prismlauncher.cfg"),
)
.await
.unwrap_or_else(|| "instances".to_string()),
ImportLauncherType::Unknown => {
return Err(crate::ErrorKind::InputError(
"Launcher type Unknown".to_string(),
)
.into())
}
};
let instances_folder = base_path.join(&instances_subfolder);
let mut instances = Vec::new();
let mut dir = io::read_dir(&instances_folder).await.map_err(| _ | {
crate::ErrorKind::InputError(format!(
"Invalid {launcher_type} launcher path, could not find '{instances_subfolder}' subfolder."
))
})?;
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
// Note: this *deletes* the submitted empty profile
#[theseus_macros::debug_pin]
#[tracing::instrument]
pub async fn import_instance(
profile_path: ProfilePathId, // This should be a blank profile
launcher_type: ImportLauncherType,
base_path: PathBuf,
instance_folder: String,
) -> crate::Result<()> {
tracing::debug!("Importing instance from {instance_folder}");
let res = 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.clone(), // path to profile
)
.await
}
ImportLauncherType::ATLauncher => {
atlauncher::import_atlauncher(
base_path, // path to atlauncher folder
instance_folder, // instance folder in atlauncher
profile_path.clone(), // path to profile
)
.await
}
ImportLauncherType::GDLauncher => {
gdlauncher::import_gdlauncher(
base_path.join("instances").join(instance_folder), // path to gdlauncher folder
profile_path.clone(), // path to profile
)
.await
}
ImportLauncherType::Curseforge => {
curseforge::import_curseforge(
base_path.join("Instances").join(instance_folder), // path to curseforge folder
profile_path.clone(), // path to profile
)
.await
}
ImportLauncherType::Unknown => {
return Err(crate::ErrorKind::InputError(
"Launcher type Unknown".to_string(),
)
.into());
}
};
// If import failed, delete the profile
match res {
Ok(_) => {}
Err(e) => {
tracing::warn!("Import failed: {:?}", e);
let _ = crate::api::profile::remove(&profile_path).await;
return Err(e);
}
}
// Check existing managed packs for potential updates
tokio::task::spawn(Profiles::update_modrinth_versions());
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)
}
}
pub async fn copy_dotminecraft(
profile_path_id: ProfilePathId,
dotminecraft: PathBuf,
io_semaphore: &IoSemaphore,
existing_loading_bar: Option<LoadingBarId>,
) -> crate::Result<LoadingBarId> {
// Get full path to profile
let profile_path = profile_path_id.get_full_path().await?;
// Gets all subfiles recursively in src
let subfiles = get_all_subfiles(&dotminecraft).await?;
let total_subfiles = subfiles.len() as u64;
let loading_bar = init_or_edit_loading(
existing_loading_bar,
crate::LoadingBarType::CopyProfile {
import_location: dotminecraft.clone(),
profile_name: profile_path_id.to_string(),
},
total_subfiles as f64,
"Copying files in profile",
)
.await?;
// Copy each file
for src_child in subfiles {
let dst_child =
src_child.strip_prefix(&dotminecraft).map_err(|_| {
crate::ErrorKind::InputError(format!(
"Invalid file: {}",
&src_child.display()
))
})?;
let dst_child = profile_path.join(dst_child);
// sleep for cpu for 1 millisecond
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
fetch::copy(&src_child, &dst_child, io_semaphore).await?;
emit_loading(&loading_bar, 1.0, None).await?;
}
Ok(loading_bar)
}
/// Recursively get a list of all subfiles in src
/// uses async recursion
#[theseus_macros::debug_pin]
#[async_recursion::async_recursion]
#[tracing::instrument]
pub async fn get_all_subfiles(src: &Path) -> crate::Result<Vec<PathBuf>> {
if !src.is_dir() {
return Ok(vec![src.to_path_buf()]);
}
let mut files = Vec::new();
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();
files.append(&mut get_all_subfiles(&src_child).await?);
}
Ok(files)
}

View File

@@ -0,0 +1,419 @@
use crate::config::MODRINTH_API_URL;
use crate::data::ModLoader;
use crate::event::emit::{emit_loading, init_loading};
use crate::event::{LoadingBarId, LoadingBarType};
use crate::prelude::ProfilePathId;
use crate::state::{
LinkedData, ModrinthProject, ModrinthVersion, ProfileInstallStage, SideType,
};
use crate::util::fetch::{
fetch, fetch_advanced, fetch_json, write_cached_icon,
};
use crate::util::io;
use crate::{InnerProjectPathUnix, State};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PackFormat {
pub game: String,
pub format_version: i32,
pub version_id: String,
pub name: String,
pub summary: Option<String>,
pub files: Vec<PackFile>,
pub dependencies: HashMap<PackDependency, String>,
}
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PackFile {
pub path: InnerProjectPathUnix,
pub hashes: HashMap<PackFileHash, String>,
pub env: Option<HashMap<EnvType, SideType>>,
pub downloads: Vec<String>,
pub file_size: u32,
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase", from = "String")]
pub enum PackFileHash {
Sha1,
Sha512,
Unknown(String),
}
impl From<String> for PackFileHash {
fn from(s: String) -> Self {
return match s.as_str() {
"sha1" => PackFileHash::Sha1,
"sha512" => PackFileHash::Sha512,
_ => PackFileHash::Unknown(s),
};
}
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase")]
pub enum EnvType {
Client,
Server,
}
#[derive(Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq, Debug)]
pub enum PackDependency {
#[serde(rename = "forge")]
Forge,
#[serde(rename = "neoforge")]
#[serde(alias = "neo-forge")]
NeoForge,
#[serde(rename = "fabric-loader")]
FabricLoader,
#[serde(rename = "quilt-loader")]
QuiltLoader,
#[serde(rename = "minecraft")]
Minecraft,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum CreatePackLocation {
// Create a pack from a modrinth version ID (such as a modpack)
FromVersionId {
project_id: String,
version_id: String,
title: 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 {
path: PathBuf,
},
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatePackProfile {
pub name: String, // the name of the profile, and relative path
pub game_version: String, // the game version of the profile
pub modloader: ModLoader, // the modloader to use
pub loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest
pub icon: Option<PathBuf>, // the icon for the profile
pub icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES)
pub linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
pub skip_install_profile: Option<bool>,
pub no_watch: Option<bool>,
}
// default
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),
no_watch: Some(false),
}
}
}
#[derive(Clone)]
pub struct CreatePack {
pub file: bytes::Bytes,
pub description: CreatePackDescription,
}
#[derive(Clone, Debug)]
pub struct CreatePackDescription {
pub icon: Option<PathBuf>,
pub override_title: Option<String>,
pub project_id: Option<String>,
pub version_id: Option<String>,
pub existing_loading_bar: Option<LoadingBarId>,
pub profile_path: ProfilePathId,
}
pub fn get_profile_from_pack(
location: CreatePackLocation,
) -> CreatePackProfile {
match location {
CreatePackLocation::FromVersionId {
project_id,
version_id,
title,
icon_url,
} => CreatePackProfile {
name: title,
icon_url,
linked_data: Some(LinkedData {
project_id: Some(project_id),
version_id: Some(version_id),
locked: Some(true),
}),
..Default::default()
},
CreatePackLocation::FromFile { path } => {
let file_name = path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
CreatePackProfile {
name: file_name,
..Default::default()
}
}
}
}
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn generate_pack_from_version_id(
project_id: String,
version_id: String,
title: String,
icon_url: Option<String>,
profile_path: ProfilePathId,
// Existing loading bar. Unlike when existing_loading_bar is used, this one is pre-initialized with PackFileDownload
// For example, you might use this if multiple packs are being downloaded at once and you want to use the same loading bar
initialized_loading_bar: Option<LoadingBarId>,
) -> crate::Result<CreatePack> {
let state = State::get().await?;
let loading_bar = if let Some(bar) = initialized_loading_bar {
emit_loading(&bar, 0.0, Some("Downloading pack file")).await?;
bar
} else {
init_loading(
LoadingBarType::PackFileDownload {
profile_path: profile_path.get_full_path().await?,
pack_name: title,
icon: icon_url,
pack_version: version_id.clone(),
},
100.0,
"Downloading pack file",
)
.await?
};
emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?;
let creds = state.credentials.read().await;
let version: ModrinthVersion = fetch_json(
Method::GET,
&format!("{}version/{}", MODRINTH_API_URL, version_id),
None,
None,
&state.fetch_semaphore,
&creds,
)
.await?;
emit_loading(&loading_bar, 10.0, None).await?;
let (url, hash) =
if let Some(file) = version.files.iter().find(|x| x.primary) {
Some((file.url.clone(), file.hashes.get("sha1")))
} else {
version
.files
.first()
.map(|file| (file.url.clone(), file.hashes.get("sha1")))
}
.ok_or_else(|| {
crate::ErrorKind::InputError(
"Specified version has no files".to_string(),
)
})?;
let file = fetch_advanced(
Method::GET,
&url,
hash.map(|x| &**x),
None,
None,
Some((&loading_bar, 70.0)),
&state.fetch_semaphore,
&creds,
)
.await?;
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?;
let project: ModrinthProject = fetch_json(
Method::GET,
&format!("{}project/{}", MODRINTH_API_URL, version.project_id),
None,
None,
&state.fetch_semaphore,
&creds,
)
.await?;
emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?;
let icon = if let Some(icon_url) = project.icon_url {
let state = State::get().await?;
let icon_bytes =
fetch(&icon_url, None, &state.fetch_semaphore, &creds).await?;
drop(creds);
let filename = icon_url.rsplit('/').next();
if let Some(filename) = filename {
Some(
write_cached_icon(
filename,
&state.directories.caches_dir(),
icon_bytes,
&state.io_semaphore,
)
.await?,
)
} else {
None
}
} else {
None
};
emit_loading(&loading_bar, 10.0, None).await?;
Ok(CreatePack {
file,
description: CreatePackDescription {
icon,
override_title: None,
project_id: Some(project_id),
version_id: Some(version_id),
existing_loading_bar: Some(loading_bar),
profile_path,
},
})
}
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn generate_pack_from_file(
path: PathBuf,
profile_path: ProfilePathId,
) -> crate::Result<CreatePack> {
let file = io::read(&path).await?;
Ok(CreatePack {
file: bytes::Bytes::from(file),
description: CreatePackDescription {
icon: None,
override_title: None,
project_id: None,
version_id: None,
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>,
ignore_lock: bool, // do not change locked status
) -> 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::NeoForge => {
mod_loader = Some(ModLoader::NeoForge);
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;
let project_id = description.project_id.clone();
let version_id = description.version_id.clone();
prof.metadata.linked_data = if project_id.is_some()
&& version_id.is_some()
{
Some(LinkedData {
project_id,
version_id,
locked: if !ignore_lock {
Some(true)
} else {
prof.metadata.linked_data.as_ref().and_then(|x| x.locked)
},
})
} else {
None
};
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

@@ -0,0 +1,430 @@
use crate::config::MODRINTH_API_URL;
use crate::event::emit::{
emit_loading, init_or_edit_loading, loading_try_for_each_concurrent,
};
use crate::event::LoadingBarType;
use crate::pack::install_from::{
set_profile_information, EnvType, PackFile, PackFileHash,
};
use crate::prelude::{ModrinthVersion, ProfilePathId, ProjectMetadata};
use crate::state::{ProfileInstallStage, Profiles, SideType};
use crate::util::fetch::{fetch_json, fetch_mirrors, write};
use crate::util::io;
use crate::{profile, State};
use async_zip::base::read::seek::ZipFileReader;
use reqwest::Method;
use serde_json::json;
use std::collections::HashMap;
use std::io::Cursor;
use std::path::{Component, PathBuf};
use super::install_from::{
generate_pack_from_file, generate_pack_from_version_id, CreatePack,
CreatePackLocation, PackFormat,
};
/// Install a pack
/// Wrapper around install_pack_files that generates a pack creation description, and
/// attempts to install the pack files. If it fails, it will remove the profile (fail safely)
/// Install a modpack from a mrpack file (a modrinth .zip format)
#[theseus_macros::debug_pin]
pub async fn install_zipped_mrpack(
location: CreatePackLocation,
profile_path: ProfilePathId,
) -> crate::Result<ProfilePathId> {
// Get file from description
let create_pack: CreatePack = match location {
CreatePackLocation::FromVersionId {
project_id,
version_id,
title,
icon_url,
} => {
generate_pack_from_version_id(
project_id,
version_id,
title,
icon_url,
profile_path.clone(),
None,
)
.await?
}
CreatePackLocation::FromFile { path } => {
generate_pack_from_file(path, profile_path.clone()).await?
}
};
// Install pack files, and if it fails, fail safely by removing the profile
let result = install_zipped_mrpack_files(create_pack, false).await;
// Check existing managed packs for potential updates
tokio::task::spawn(Profiles::update_modrinth_versions());
match result {
Ok(profile) => Ok(profile),
Err(err) => {
let _ = crate::api::profile::remove(&profile_path).await;
Err(err)
}
}
}
/// Install all pack files from a description
/// Does not remove the profile if it fails
#[theseus_macros::debug_pin]
pub async fn install_zipped_mrpack_files(
create_pack: CreatePack,
ignore_lock: bool,
) -> crate::Result<ProfilePathId> {
let state = &State::get().await?;
let file = create_pack.file;
let description = create_pack.description.clone(); // make a copy for profile edit function
let icon = create_pack.description.icon;
let project_id = create_pack.description.project_id;
let version_id = create_pack.description.version_id;
let existing_loading_bar = create_pack.description.existing_loading_bar;
let profile_path = create_pack.description.profile_path;
let icon_exists = icon.is_some();
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
// Create zip reader around file
let mut zip_reader =
ZipFileReader::with_tokio(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read input modpack zip".to_string(),
))
})?;
// Extract index of modrinth.index.json
let zip_index_option = zip_reader.file().entries().iter().position(|f| {
f.filename().as_str().unwrap_or_default() == "modrinth.index.json"
});
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let mut reader = zip_reader.reader_with_entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" {
return Err(crate::ErrorKind::InputError(
"Pack does not support Minecraft".to_string(),
)
.into());
}
// Sets generated profile attributes to the pack ones (using profile::edit)
set_profile_information(
profile_path.clone(),
&description,
&pack.name,
&pack.dependencies,
ignore_lock,
)
.await?;
let profile_path = profile_path.clone();
let loading_bar = init_or_edit_loading(
existing_loading_bar,
LoadingBarType::PackDownload {
profile_path: profile_path.get_full_path().await?.clone(),
pack_name: pack.name.clone(),
icon,
pack_id: project_id,
pack_version: version_id,
},
100.0,
"Downloading modpack",
)
.await?;
let num_files = pack.files.len();
use futures::StreamExt;
loading_try_for_each_concurrent(
futures::stream::iter(pack.files.into_iter())
.map(Ok::<PackFile, crate::Error>),
None,
Some(&loading_bar),
70.0,
num_files,
None,
|project| {
let profile_path = profile_path.clone();
async move {
//TODO: Future update: prompt user for optional files in a modpack
if let Some(env) = project.env {
if env
.get(&EnvType::Client)
.map(|x| x == &SideType::Unsupported)
.unwrap_or(false)
{
return Ok(());
}
}
let creds = state.credentials.read().await;
let file = fetch_mirrors(
&project
.downloads
.iter()
.map(|x| &**x)
.collect::<Vec<&str>>(),
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
&state.fetch_semaphore,
&creds,
)
.await?;
drop(creds);
let project_path = project.path.to_string();
let path =
std::path::Path::new(&project_path).components().next();
if let Some(path) = path {
match path {
Component::CurDir | Component::Normal(_) => {
let path = profile_path
.get_full_path()
.await?
.join(&project_path);
write(&path, &file, &state.io_semaphore)
.await?;
}
_ => {}
};
}
Ok(())
}
},
)
.await?;
emit_loading(&loading_bar, 0.0, Some("Extracting overrides")).await?;
let mut total_len = 0;
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap();
let filename = file.filename().as_str().unwrap_or_default();
if (filename.starts_with("overrides")
|| filename.starts_with("client-overrides"))
&& !filename.ends_with('/')
{
total_len += 1;
}
}
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap();
let filename = file.filename().as_str().unwrap_or_default();
let file_path = PathBuf::from(filename);
if (filename.starts_with("overrides")
|| filename.starts_with("client-overrides"))
&& !filename.ends_with('/')
{
// Reads the file into the 'content' variable
let mut content = Vec::new();
let mut reader = zip_reader.reader_with_entry(index).await?;
reader.read_to_end_checked(&mut content).await?;
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
if new_path.file_name().is_some() {
write(
&profile_path.get_full_path().await?.join(new_path),
&content,
&state.io_semaphore,
)
.await?;
}
emit_loading(
&loading_bar,
30.0 / total_len as f64,
Some(&format!(
"Extracting override {}/{}",
index, total_len
)),
)
.await?;
}
}
// If the icon doesn't exist, we expect icon.png to be a potential icon.
// If it doesn't exist, and an override to icon.png exists, cache and use that
let potential_icon =
profile_path.get_full_path().await?.join("icon.png");
if !icon_exists && potential_icon.exists() {
profile::edit_icon(&profile_path, Some(&potential_icon)).await?;
}
if let Some(profile_val) = profile::get(&profile_path, None).await? {
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
State::sync().await?;
}
Ok::<ProfilePathId, crate::Error>(profile_path.clone())
} else {
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)))
}
}
#[tracing::instrument(skip(mrpack_file))]
#[theseus_macros::debug_pin]
pub async fn remove_all_related_files(
profile_path: ProfilePathId,
mrpack_file: bytes::Bytes,
) -> crate::Result<()> {
let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file);
// Create zip reader around file
let mut zip_reader =
ZipFileReader::with_tokio(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read input modpack zip".to_string(),
))
})?;
// Extract index of modrinth.index.json
let zip_index_option = zip_reader.file().entries().iter().position(|f| {
f.filename().as_str().unwrap_or_default() == "modrinth.index.json"
});
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let mut reader = zip_reader.reader_with_entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" {
return Err(crate::ErrorKind::InputError(
"Pack does not support Minecraft".to_string(),
)
.into());
}
// Set install stage to installing, and do not change it back (as files are being removed and are not being reinstalled here)
crate::api::profile::edit(&profile_path, |prof| {
prof.install_stage = ProfileInstallStage::PackInstalling;
async { Ok(()) }
})
.await?;
// First, remove all modrinth projects by their version hashes
// Remove all modrinth projects by their version hashes
// We need to do a fetch to get the project ids from Modrinth
let state = State::get().await?;
let all_hashes = pack
.files
.iter()
.filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha512)?.clone()))
.collect::<Vec<_>>();
let creds = state.credentials.read().await;
// First, get project info by hash
let files_url = format!("{}version_files", MODRINTH_API_URL);
let hash_projects = fetch_json::<HashMap<String, ModrinthVersion>>(
Method::POST,
&files_url,
None,
Some(json!({
"hashes": all_hashes,
"algorithm": "sha512",
})),
&state.fetch_semaphore,
&creds,
)
.await?;
let to_remove = hash_projects
.into_values()
.map(|p| p.project_id)
.collect::<Vec<_>>();
let profile =
profile::get(&profile_path, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(
profile_path.to_string(),
)
})?;
for (project_id, project) in &profile.projects {
if let ProjectMetadata::Modrinth { project, .. } = &project.metadata
{
if to_remove.contains(&project.id) {
let path = profile
.get_profile_full_path()
.await?
.join(project_id.0.clone());
if path.exists() {
io::remove_file(&path).await?;
}
}
}
}
// Iterate over all Modrinth project file paths in the json, and remove them
// (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized)
for file in pack.files {
let path: PathBuf = profile_path
.get_full_path()
.await?
.join(file.path.to_string());
if path.exists() {
io::remove_file(&path).await?;
}
}
// Iterate over each 'overrides' file and remove it
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap();
let filename = file.filename().as_str().unwrap_or_default();
let file_path = PathBuf::from(filename);
if (filename.starts_with("overrides")
|| filename.starts_with("client-overrides"))
&& !filename.ends_with('/')
{
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
// Remove this file if a corresponding one exists in the filesystem
let existing_file =
profile_path.get_full_path().await?.join(&new_path);
if existing_file.exists() {
io::remove_file(&existing_file).await?;
}
}
}
Ok(())
} else {
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)))
}
}

View File

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

View File

@@ -0,0 +1,130 @@
//! Theseus process management interface
use uuid::Uuid;
use crate::state::{MinecraftChild, ProfilePathId};
pub use crate::{
state::{
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
},
State,
};
// Gets whether a child process stored in the state by UUID has finished
#[tracing::instrument]
pub async fn has_finished_by_uuid(uuid: Uuid) -> crate::Result<bool> {
Ok(get_exit_status_by_uuid(uuid).await?.is_some())
}
// Gets the exit status of a child process stored in the state by UUID
#[tracing::instrument]
pub async fn get_exit_status_by_uuid(
uuid: Uuid,
) -> crate::Result<Option<i32>> {
let state = State::get().await?;
let children = state.children.read().await;
children.exit_status(uuid).await
}
// Gets the UUID of each stored process in the state
#[tracing::instrument]
pub async fn get_all_uuids() -> crate::Result<Vec<Uuid>> {
let state = State::get().await?;
let children = state.children.read().await;
Ok(children.keys())
}
// Gets the UUID of each *running* stored process in the state
#[tracing::instrument]
pub async fn get_all_running_uuids() -> crate::Result<Vec<Uuid>> {
let state = State::get().await?;
let children = state.children.read().await;
children.running_keys().await
}
// Gets the Profile paths of each *running* stored process in the state
#[tracing::instrument]
pub async fn get_all_running_profile_paths() -> crate::Result<Vec<ProfilePathId>>
{
let state = State::get().await?;
let children = state.children.read().await;
children.running_profile_paths().await
}
// Gets the Profiles (cloned) of each *running* stored process in the state
#[tracing::instrument]
pub async fn get_all_running_profiles() -> crate::Result<Vec<Profile>> {
let state = State::get().await?;
let children = state.children.read().await;
children.running_profiles().await
}
// Gets the UUID of each stored process in the state by profile path
#[tracing::instrument]
pub async fn get_uuids_by_profile_path(
profile_path: ProfilePathId,
) -> crate::Result<Vec<Uuid>> {
let state = State::get().await?;
let children = state.children.read().await;
children.running_keys_with_profile(profile_path).await
}
// Kill a child process stored in the state by UUID, as a string
#[tracing::instrument]
pub async fn kill_by_uuid(uuid: Uuid) -> crate::Result<()> {
let state = State::get().await?;
let children = state.children.read().await;
if let Some(mchild) = children.get(uuid) {
let mut mchild = mchild.write().await;
kill(&mut mchild).await
} else {
// No error returned for already finished process
Ok(())
}
}
// Wait for a child process stored in the state by UUID
#[tracing::instrument]
pub async fn wait_for_by_uuid(uuid: Uuid) -> crate::Result<()> {
let state = State::get().await?;
let children = state.children.read().await;
// No error returned for already killed process
if let Some(mchild) = children.get(uuid) {
let mut mchild = mchild.write().await;
wait_for(&mut mchild).await
} else {
// No error returned for already finished process
Ok(())
}
}
// Kill a running child process directly
#[tracing::instrument(skip(running))]
pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> {
running.current_child.write().await.kill().await?;
Ok(())
}
// Await on the completion of a child process directly
#[tracing::instrument(skip(running))]
pub async fn wait_for(running: &mut MinecraftChild) -> crate::Result<()> {
// We do not wait on the Child directly, but wait on the thread manager.
// This way we can still run all cleanup hook functions that happen after.
running
.manager
.take()
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Process manager already completed or missing for process {}",
running.uuid
))
})?
.await?
.map_err(|err| {
crate::ErrorKind::LauncherError(format!(
"Error running minecraft: {err}"
))
})?;
Ok(())
}

View File

@@ -0,0 +1,315 @@
//! Theseus profile management interface
use crate::pack::install_from::CreatePackProfile;
use crate::prelude::ProfilePathId;
use crate::state::LinkedData;
use crate::util::io::{self, canonicalize};
use crate::{
event::{emit::emit_profile, ProfilePayloadType},
prelude::ModLoader,
};
use crate::{pack, profile, ErrorKind};
pub use crate::{
state::{JavaSettings, Profile},
State,
};
use daedalus::modded::LoaderVersion;
use std::path::PathBuf;
use tracing::{info, trace};
use uuid::Uuid;
// Creates a profile of a given name and adds it to the in-memory state
// Returns relative filepath as ProfilePathId which can be used to access it in the State
#[tracing::instrument]
#[theseus_macros::debug_pin]
#[allow(clippy::too_many_arguments)]
pub async fn profile_create(
mut name: String, // the name of the profile, and relative path
game_version: String, // the game version of the profile
modloader: ModLoader, // the modloader to use
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest
icon: Option<PathBuf>, // the icon for the profile
icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES)
linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
skip_install_profile: Option<bool>,
no_watch: Option<bool>,
) -> crate::Result<ProfilePathId> {
name = profile::sanitize_profile_name(&name);
trace!("Creating new profile. {}", name);
let state = State::get().await?;
let uuid = Uuid::new_v4();
let mut path = state.directories.profiles_dir().await.join(&name);
if path.exists() {
let mut new_name;
let mut new_path;
let mut which = 1;
loop {
new_name = format!("{name} ({which})");
new_path = state.directories.profiles_dir().await.join(&new_name);
if !new_path.exists() {
break;
}
which += 1;
}
tracing::debug!(
"Folder collision: {}, renaming to: {}",
path.display(),
new_path.display()
);
path = new_path;
name = new_name;
}
io::create_dir_all(&path).await?;
info!(
"Creating profile at path {}",
&canonicalize(&path)?.display()
);
let loader = if modloader != ModLoader::Vanilla {
get_loader_version_from_loader(
game_version.clone(),
modloader,
loader_version,
)
.await?
} else {
None
};
let mut profile = Profile::new(uuid, name, game_version).await?;
let result = async {
if let Some(ref icon) = icon {
let bytes =
io::read(state.directories.caches_dir().join(icon)).await?;
profile
.set_icon(
&state.directories.caches_dir(),
&state.io_semaphore,
bytes::Bytes::from(bytes),
&icon.to_string_lossy(),
)
.await?;
}
profile.metadata.icon_url = icon_url;
if let Some(loader_version) = loader {
profile.metadata.loader = modloader;
profile.metadata.loader_version = Some(loader_version);
}
profile.metadata.linked_data = linked_data;
if let Some(linked_data) = &mut profile.metadata.linked_data {
linked_data.locked = Some(
linked_data.project_id.is_some()
&& linked_data.version_id.is_some(),
);
}
emit_profile(
uuid,
&profile.profile_id(),
&profile.metadata.name,
ProfilePayloadType::Created,
)
.await?;
{
let mut profiles = state.profiles.write().await;
profiles
.insert(profile.clone(), no_watch.unwrap_or_default())
.await?;
}
if !skip_install_profile.unwrap_or(false) {
crate::launcher::install_minecraft(&profile, None, false).await?;
}
State::sync().await?;
Ok(profile.profile_id())
}
.await;
match result {
Ok(profile) => Ok(profile),
Err(err) => {
let _ = crate::api::profile::remove(&profile.profile_id()).await;
Err(err)
}
}
}
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,
profile.no_watch,
)
.await
}
pub async fn profile_create_from_duplicate(
copy_from: ProfilePathId,
) -> crate::Result<ProfilePathId> {
// Original profile
let profile = profile::get(&copy_from, None).await?.ok_or_else(|| {
ErrorKind::UnmanagedProfileError(copy_from.to_string())
})?;
let profile_path_id = profile_create(
profile.metadata.name.clone(),
profile.metadata.game_version.clone(),
profile.metadata.loader,
profile.metadata.loader_version.clone().map(|it| it.id),
profile.metadata.icon.clone(),
profile.metadata.icon_url.clone(),
profile.metadata.linked_data.clone(),
Some(true),
Some(true),
)
.await?;
// Copy it over using the import system (essentially importing from the same profile)
let state = State::get().await?;
let bar = pack::import::copy_dotminecraft(
profile_path_id.clone(),
copy_from.get_full_path().await?,
&state.io_semaphore,
None,
)
.await?;
let duplicated_profile =
profile::get(&profile_path_id, None).await?.ok_or_else(|| {
ErrorKind::UnmanagedProfileError(profile_path_id.to_string())
})?;
crate::launcher::install_minecraft(&duplicated_profile, Some(bar), false)
.await?;
{
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(
&profile.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
}
// emit profile edited
emit_profile(
profile.uuid,
&profile.profile_id(),
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
State::sync().await?;
Ok(profile_path_id)
}
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub(crate) async fn get_loader_version_from_loader(
game_version: String,
loader: ModLoader,
loader_version: Option<String>,
) -> crate::Result<Option<LoaderVersion>> {
let state = State::get().await?;
let metadata = state.metadata.read().await;
let version = loader_version.unwrap_or_else(|| "latest".to_string());
let filter = |it: &LoaderVersion| match version.as_str() {
"latest" => true,
"stable" => it.stable,
id => {
it.id == *id
|| format!("{}-{}", game_version, id) == it.id
|| format!("{}-{}-{}", game_version, id, game_version) == it.id
}
};
let loader_data = match loader {
ModLoader::Forge => &metadata.forge,
ModLoader::Fabric => &metadata.fabric,
ModLoader::Quilt => &metadata.quilt,
ModLoader::NeoForge => &metadata.neoforge,
_ => {
return Err(
ProfileCreationError::NoManifest(loader.to_string()).into()
)
}
};
let loaders = &loader_data
.game_versions
.iter()
.find(|it| {
it.id
.replace(daedalus::modded::DUMMY_REPLACE_STRING, &game_version)
== game_version
})
.ok_or_else(|| {
ProfileCreationError::ModloaderUnsupported(
loader.to_string(),
game_version.clone(),
)
})?
.loaders;
let loader_version = loaders
.iter()
.find(|&x| filter(x))
.cloned()
.or(
// If stable was searched for but not found, return latest by default
if version == "stable" {
loaders.iter().next().cloned()
} else {
None
},
)
.ok_or_else(|| {
ProfileCreationError::InvalidVersionModloader(
version,
loader.to_string(),
)
})?;
Ok(Some(loader_version))
}
#[derive(thiserror::Error, Debug)]
pub enum ProfileCreationError {
#[error("Profile .json exists: {0}")]
ProfileExistsError(PathBuf),
#[error("Modloader {0} unsupported for Minecraft version {1}")]
ModloaderUnsupported(String, String),
#[error("Invalid version {0} for modloader {1}")]
InvalidVersionModloader(String, String),
#[error("Could not get manifest for loader {0}. This is a bug in the GUI")]
NoManifest(String),
#[error("Could not get State.")]
NoState,
#[error("Attempted to create project in something other than a folder.")]
NotFolder,
#[error("You are trying to create a profile in a non-empty directory")]
NotEmptyFolder,
#[error("IO error: {0}")]
IOError(#[from] std::io::Error),
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,233 @@
use crate::{
event::{
emit::{emit_profile, init_loading, loading_try_for_each_concurrent},
ProfilePayloadType,
},
pack::{self, install_from::generate_pack_from_version_id},
prelude::{ProfilePathId, ProjectPathId},
profile::get,
state::{ProfileInstallStage, Project},
LoadingBarType, State,
};
use futures::try_join;
/// Updates a managed modrinth pack to the version specified by new_version_id
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn update_managed_modrinth_version(
profile_path: &ProfilePathId,
new_version_id: &String,
) -> crate::Result<()> {
let profile = get(profile_path, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
.as_error()
})?;
let unmanaged_err = || {
crate::ErrorKind::InputError(
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
)
};
// Extract modrinth pack information, if appropriate
let linked_data = profile
.metadata
.linked_data
.as_ref()
.ok_or_else(unmanaged_err)?;
let project_id: &String =
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?;
let version_id =
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
// Replace the pack with the new version
replace_managed_modrinth(
profile_path,
&profile,
project_id,
version_id,
Some(new_version_id),
true, // switching versions should ignore the lock
)
.await?;
emit_profile(
profile.uuid,
profile_path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
State::sync().await?;
Ok(())
}
/// Repair a managed modrinth pack by 'updating' it to the current version
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn repair_managed_modrinth(
profile_path: &ProfilePathId,
) -> crate::Result<()> {
let profile = get(profile_path, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
.as_error()
})?;
let unmanaged_err = || {
crate::ErrorKind::InputError(
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
)
};
// For repairing specifically, first we remove all installed projects (to ensure we do remove ones that aren't in the pack)
// We do a project removal followed by removing everything in the .mrpack, to ensure we only
// remove relevant projects and not things like save files
let projects_map = profile.projects.clone();
let stream = futures::stream::iter(
projects_map
.into_iter()
.map(Ok::<(ProjectPathId, Project), crate::Error>),
);
loading_try_for_each_concurrent(
stream,
None,
None,
0.0,
0,
None,
|(project_id, _)| {
let profile = profile.clone();
async move {
profile.remove_project(&project_id, Some(true)).await?;
Ok(())
}
},
)
.await?;
// Extract modrinth pack information, if appropriate
let linked_data = profile
.metadata
.linked_data
.as_ref()
.ok_or_else(unmanaged_err)?;
let project_id: &String =
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?;
let version_id =
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
// Replace the pack with the same version
replace_managed_modrinth(
profile_path,
&profile,
project_id,
version_id,
None,
false, // do not ignore lock, as repairing can reset the lock
)
.await?;
emit_profile(
profile.uuid,
profile_path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
State::sync().await?;
Ok(())
}
/// Replace a managed modrinth pack with a new version
/// If new_version_id is None, the pack is 'reinstalled' in-place
#[tracing::instrument(skip(profile))]
#[theseus_macros::debug_pin]
async fn replace_managed_modrinth(
profile_path: &ProfilePathId,
profile: &crate::state::Profile,
project_id: &String,
version_id: &String,
new_version_id: Option<&String>,
ignore_lock: bool,
) -> crate::Result<()> {
crate::profile::edit(profile_path, |profile| {
profile.install_stage = ProfileInstallStage::Installing;
async { Ok(()) }
})
.await?;
// Fetch .mrpacks for both old and new versions
// TODO: this will need to be updated if we revert the hacky pack method we needed for compiler speed
let (old_pack_creator, new_pack_creator) =
if let Some(new_version_id) = new_version_id {
let shared_loading_bar = init_loading(
LoadingBarType::PackFileDownload {
profile_path: profile_path.get_full_path().await?,
pack_name: profile.metadata.name.clone(),
icon: None,
pack_version: version_id.clone(),
},
200.0, // These two downloads will share the same loading bar
"Downloading pack file",
)
.await?;
// download in parallel, then join.
try_join!(
generate_pack_from_version_id(
project_id.clone(),
version_id.clone(),
profile.metadata.name.clone(),
None,
profile_path.clone(),
Some(shared_loading_bar.clone())
),
generate_pack_from_version_id(
project_id.clone(),
new_version_id.clone(),
profile.metadata.name.clone(),
None,
profile_path.clone(),
Some(shared_loading_bar)
)
)?
} else {
// If new_version_id is None, we don't need to download the new pack, so we clone the old one
let mut old_pack_creator = generate_pack_from_version_id(
project_id.clone(),
version_id.clone(),
profile.metadata.name.clone(),
None,
profile_path.clone(),
None,
)
.await?;
old_pack_creator.description.existing_loading_bar = None;
(old_pack_creator.clone(), old_pack_creator)
};
// Removal - remove all files that were added by the old pack
// - remove all installed projects
// - remove all overrides
pack::install_mrpack::remove_all_related_files(
profile_path.clone(),
old_pack_creator.file,
)
.await?;
// Reinstallation - install all files that are added by the new pack
// - install all projects
// - install all overrides
// - edits the profile to update the new data
// - (functionals almost identically to rteinstalling the pack 'in-place')
pack::install_mrpack::install_zipped_mrpack_files(
new_pack_creator,
ignore_lock,
)
.await?;
Ok(())
}

View File

@@ -0,0 +1,5 @@
use crate::state::{ProcessType, SafeProcesses};
pub async fn check_safe_loading_bars() -> crate::Result<bool> {
SafeProcesses::is_complete(ProcessType::LoadingBar).await
}

View File

@@ -0,0 +1,246 @@
//! Theseus profile management interface
use std::path::{Path, PathBuf};
use tokio::fs;
use io::IOError;
use tokio::sync::RwLock;
use crate::{
event::emit::{emit_loading, init_loading},
prelude::DirectoryInfo,
state::{self, Profiles},
util::{fetch, io},
};
pub use crate::{
state::{
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
},
State,
};
/// Gets entire settings
#[tracing::instrument]
pub async fn get() -> crate::Result<Settings> {
let state = State::get().await?;
let settings = state.settings.read().await;
Ok(settings.clone())
}
/// Sets entire settings
#[tracing::instrument]
pub async fn set(settings: Settings) -> crate::Result<()> {
let state = State::get().await?;
if settings.loaded_config_dir
!= state.settings.read().await.loaded_config_dir
{
return Err(crate::ErrorKind::OtherError(
"Cannot change config directory as setting".to_string(),
)
.as_error());
}
let (reset_io, reset_fetch) = async {
let read = state.settings.read().await;
(
settings.max_concurrent_writes != read.max_concurrent_writes,
settings.max_concurrent_downloads != read.max_concurrent_downloads,
)
}
.await;
let updated_discord_rpc = {
let read = state.settings.read().await;
settings.disable_discord_rpc != read.disable_discord_rpc
};
{
*state.settings.write().await = settings;
}
if updated_discord_rpc {
state.discord_rpc.clear_to_default(true).await?;
}
if reset_io {
state.reset_io_semaphore().await;
}
if reset_fetch {
state.reset_fetch_semaphore().await;
}
State::sync().await?;
Ok(())
}
/// Sets the new config dir, the location of all Theseus data except for the settings.json and caches
/// Takes control of the entire state and blocks until completion
pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
tracing::trace!("Changing config dir to: {}", new_config_dir.display());
if !new_config_dir.is_dir() {
return Err(crate::ErrorKind::FSError(format!(
"New config dir is not a folder: {}",
new_config_dir.display()
))
.as_error());
}
if !is_dir_writeable(new_config_dir.clone()).await? {
return Err(crate::ErrorKind::FSError(format!(
"New config dir is not writeable: {}",
new_config_dir.display()
))
.as_error());
}
let loading_bar = init_loading(
crate::LoadingBarType::ConfigChange {
new_path: new_config_dir.clone(),
},
100.0,
"Changing configuration directory",
)
.await?;
tracing::trace!("Changing config dir, taking control of the state");
// Take control of the state
let mut state_write = State::get_write().await?;
let old_config_dir =
state_write.directories.config_dir.read().await.clone();
// Reset file watcher
tracing::trace!("Reset file watcher");
let file_watcher = state::init_watcher().await?;
state_write.file_watcher = RwLock::new(file_watcher);
// Getting files to be moved
let mut config_entries = io::read_dir(&old_config_dir).await?;
let across_drives = is_different_drive(&old_config_dir, &new_config_dir);
let mut entries = vec![];
let mut deletable_entries = vec![];
while let Some(entry) = config_entries
.next_entry()
.await
.map_err(|e| IOError::with_path(e, &old_config_dir))?
{
let entry_path = entry.path();
if let Some(file_name) = entry_path.file_name() {
// We are only moving the profiles and metadata folders
if file_name == state::PROFILES_FOLDER_NAME
|| file_name == state::METADATA_FOLDER_NAME
{
if across_drives {
entries.extend(
crate::pack::import::get_all_subfiles(&entry_path)
.await?,
);
deletable_entries.push(entry_path.clone());
} else {
entries.push(entry_path.clone());
}
}
}
}
tracing::trace!("Moving files");
let semaphore = &state_write.io_semaphore;
let num_entries = entries.len() as f64;
for entry_path in entries {
let relative_path = entry_path.strip_prefix(&old_config_dir)?;
let new_path = new_config_dir.join(relative_path);
if across_drives {
fetch::copy(&entry_path, &new_path, semaphore).await?;
} else {
io::rename(entry_path.clone(), new_path.clone()).await?;
}
emit_loading(&loading_bar, 80.0 * (1.0 / num_entries), None).await?;
}
tracing::trace!("Setting configuration setting");
// Set load config dir setting
let settings = {
let mut settings = state_write.settings.write().await;
settings.loaded_config_dir = Some(new_config_dir.clone());
// Some java paths are hardcoded to within our config dir, so we need to update them
tracing::trace!("Updating java keys");
for key in settings.java_globals.keys() {
if let Some(java) = settings.java_globals.get_mut(&key) {
// If the path is within the old config dir path, update it to the new config dir
if let Ok(relative_path) = PathBuf::from(java.path.clone())
.strip_prefix(&old_config_dir)
{
java.path = new_config_dir
.join(relative_path)
.to_string_lossy()
.to_string();
}
}
}
tracing::trace!("Syncing settings");
settings
.sync(&state_write.directories.settings_file())
.await?;
settings.clone()
};
tracing::trace!("Reinitializing directory");
// Set new state information
state_write.directories = DirectoryInfo::init(&settings)?;
// Delete entries that were from a different drive
let deletable_entries_len = deletable_entries.len();
if deletable_entries_len > 0 {
tracing::trace!("Deleting old files");
}
for entry in deletable_entries {
io::remove_dir_all(entry).await?;
emit_loading(
&loading_bar,
10.0 * (1.0 / deletable_entries_len as f64),
None,
)
.await?;
}
// Reset file watcher
tracing::trace!("Reset file watcher");
let mut file_watcher = state::init_watcher().await?;
// Reset profiles (for filepaths, file watcher, etc)
state_write.profiles = RwLock::new(
Profiles::init(&state_write.directories, &mut file_watcher).await?,
);
state_write.file_watcher = RwLock::new(file_watcher);
emit_loading(&loading_bar, 10.0, None).await?;
tracing::info!(
"Successfully switched config folder to: {}",
new_config_dir.display()
);
Ok(())
}
// Function to check if two paths are on different drives/roots
fn is_different_drive(path1: &Path, path2: &Path) -> bool {
let root1 = path1.components().next();
let root2 = path2.components().next();
root1 != root2
}
pub async fn is_dir_writeable(new_config_dir: PathBuf) -> crate::Result<bool> {
let temp_path = new_config_dir.join(".tmp");
match fs::write(temp_path.clone(), "test").await {
Ok(_) => {
fs::remove_file(temp_path).await?;
Ok(true)
}
Err(e) => {
tracing::error!("Error writing to new config dir: {}", e);
Ok(false)
}
}
}

View File

@@ -0,0 +1,60 @@
//! Theseus tag management interface
pub use crate::{
state::{Category, DonationPlatform, GameVersion, Loader, Tags},
State,
};
// Get bundled set of tags
#[tracing::instrument]
pub async fn get_tag_bundle() -> crate::Result<Tags> {
let state = State::get().await?;
let tags = state.tags.read().await;
Ok(tags.get_tag_bundle())
}
/// Get category tags
#[tracing::instrument]
pub async fn get_category_tags() -> crate::Result<Vec<Category>> {
let state = State::get().await?;
let tags = state.tags.read().await;
Ok(tags.get_categories())
}
/// Get report type tags
#[tracing::instrument]
pub async fn get_report_type_tags() -> crate::Result<Vec<String>> {
let state = State::get().await?;
let tags = state.tags.read().await;
Ok(tags.get_report_types())
}
/// Get loader tags
#[tracing::instrument]
pub async fn get_loader_tags() -> crate::Result<Vec<Loader>> {
let state = State::get().await?;
let tags = state.tags.read().await;
Ok(tags.get_loaders())
}
/// Get game version tags
#[tracing::instrument]
pub async fn get_game_version_tags() -> crate::Result<Vec<GameVersion>> {
let state = State::get().await?;
let tags = state.tags.read().await;
Ok(tags.get_game_versions())
}
/// Get donation platform tags
#[tracing::instrument]
pub async fn get_donation_platform_tags() -> crate::Result<Vec<DonationPlatform>>
{
let state = State::get().await?;
let tags = state.tags.read().await;
Ok(tags.get_donation_platforms())
}