mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
feat: instances v2 (#6431)
* feat: base of instances v2 * feat: use old profiles with compat layer * prototype: instances v2 * fix: install_from using profile * fix: skins migration fix * fix: frontend still using profile path * fix: add update proj multiselect guard * fix: cargo fmt * fix: content missing fields * feat: break up app-lib/api/instance.rs * fix: check_content_updates mismatch * fix: updater modal cleanup w/new structure * feat: better update all handling * fix: remove preview_update_all * fix: feedback on bulk update + lint * fix: rem transitions * fix: change to jsonb * feat: app db backup after update * fix: lint * fix: sqlx prepare + use sqlx macros * fix: lint * fix: bugs * feat: defuck the installing process up * fix: bug of hell * fix: shear * fix: fmt * fix: install progress spacing + change mc/content/overrides to bytes * fix: lint * fix: prepr * fix: navtabs anim not working in app * fix: worlds.vue improvements + browse page fixes * feat: optimise queries + adapter fns * fix: lint * fix: lint * feat: shared modrinth-content-management crate (#6469) * feat: disable warnings setting * feat: add instances shortcuts (#6329) * Add modrinth://launch deep link to start a profile Support external profile launching via modrinth://launch/{profile_path} for integrations such as Stream Deck. * Change route to /launch/profile/{id} for future extensibility * fix: ensure profile path is url decoded * fix: URL-decode profile path from deep link * fix: use urlencoding crate for URL decoding * feat: implement app instance shortcuts * feat: change windows shortcut creation to use windows api instead * feat: implement creating a shortcut launching world/server * format * fmt * fix multiline inline tables * pnpm prepr * feat: move create shortcut to last item * refactor: split up shortcuts.rs for individual platforms * refactor: turn profile launch url into url type * use string literal and add safety comment * pt2 * refactor: rename anything that's profile into instance * update mac shortcut --------- Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com> --------- Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>
This commit is contained in:
co-authored by
DJCheesusReal
Truman Gao
parent
ef4044534f
commit
734720e11e
@@ -7,6 +7,8 @@ use crate::{
|
||||
},
|
||||
util::io,
|
||||
};
|
||||
use url::form_urlencoded;
|
||||
use urlencoding::decode;
|
||||
|
||||
/// Handles external functions (such as through URL deep linkage)
|
||||
/// Link is extracted value (link) in somewhat URL format, such as
|
||||
@@ -28,6 +30,53 @@ pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
|
||||
Some(("server", id)) => {
|
||||
CommandPayload::InstallServer { id: id.to_string() }
|
||||
}
|
||||
// /launch/instance/{id} - Launches an instance
|
||||
Some(("launch", rest)) if rest.starts_with("instance/") => {
|
||||
let raw = rest.trim_start_matches("instance/");
|
||||
let (raw, query) = raw.split_once('?').unwrap_or((raw, ""));
|
||||
let mut server = None;
|
||||
let mut singleplayer_world = None;
|
||||
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match &*key {
|
||||
"server" => server = Some(value.into_owned()),
|
||||
"singleplayer_world" => {
|
||||
singleplayer_world = Some(value.into_owned());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if server.is_some() && singleplayer_world.is_some() {
|
||||
emit_warning(
|
||||
"Invalid command, cannot launch both a server and a singleplayer world",
|
||||
)
|
||||
.await?;
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"Cannot launch both a server and a singleplayer world"
|
||||
.to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
match decode(raw) {
|
||||
Ok(decoded) => CommandPayload::LaunchInstance {
|
||||
id: decoded.to_string(),
|
||||
server,
|
||||
singleplayer_world,
|
||||
},
|
||||
Err(e) => {
|
||||
emit_warning(&format!(
|
||||
"Invalid UTF-8 in instance path: {e}"
|
||||
))
|
||||
.await?;
|
||||
return Err(crate::ErrorKind::InputError(format!(
|
||||
"Invalid UTF-8 in instance path: {e}"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
emit_warning(&format!(
|
||||
"Invalid command, unrecognized path: {sublink}"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Theseus instance management interface
|
||||
|
||||
mod content;
|
||||
mod export_mrpack;
|
||||
mod get;
|
||||
mod install;
|
||||
mod lifecycle;
|
||||
mod paths;
|
||||
mod projects;
|
||||
mod run;
|
||||
|
||||
pub use self::content::{
|
||||
get_content_items, get_dependencies_as_content_items,
|
||||
get_install_candidates, get_installed_project_ids,
|
||||
get_linked_modpack_content, get_linked_modpack_info, get_projects,
|
||||
list_content_sets, sync_content_files,
|
||||
};
|
||||
pub use self::export_mrpack::{
|
||||
create_mrpack_json, export_mrpack, get_pack_export_candidates,
|
||||
};
|
||||
pub use self::get::{get, get_many, list};
|
||||
pub use self::install::get_optimal_jre_key;
|
||||
pub(crate) use self::lifecycle::create;
|
||||
pub use self::lifecycle::{edit, edit_icon, remove};
|
||||
pub use self::paths::{get_full_path, get_mod_full_path};
|
||||
pub use self::projects::{
|
||||
InstallProjectWithDependenciesRequest, add_project_from_path,
|
||||
add_project_from_version, install_project_with_dependencies,
|
||||
remove_project, repair_managed_modrinth,
|
||||
switch_project_version_with_dependencies, toggle_disable_project,
|
||||
update_all_projects, update_managed_modrinth_version, update_project,
|
||||
};
|
||||
pub use self::run::{
|
||||
QuickPlayType, kill, run, try_update_playtime_by_instance_id,
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
use crate::state::{
|
||||
CacheBehaviour, ContentFile, ContentItem, ContentSet, Dependency,
|
||||
InstanceInstallCandidate, InstanceInstallTarget, LinkedModpackInfo,
|
||||
ProjectType, State,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn sync_content_files(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<Vec<crate::state::instances::InstanceFile>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::sync_content_files(instance_id, &state).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn list_content_sets(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<Vec<ContentSet>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::list_content_sets(instance_id, &state.pool).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_projects(
|
||||
instance_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
) -> crate::Result<DashMap<String, ContentFile>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::get_content_projects(
|
||||
instance_id,
|
||||
None,
|
||||
cache_behaviour,
|
||||
&state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_installed_project_ids(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::get_installed_project_ids_for_instance(
|
||||
instance_id,
|
||||
None,
|
||||
&state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_install_candidates(
|
||||
project_id: &str,
|
||||
project_type: ProjectType,
|
||||
targets: Vec<InstanceInstallTarget>,
|
||||
) -> crate::Result<Vec<InstanceInstallCandidate>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::get_instance_install_candidates(
|
||||
project_id,
|
||||
project_type,
|
||||
&targets,
|
||||
&state.pool,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_content_items(
|
||||
instance_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
) -> crate::Result<Vec<ContentItem>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::list_content(instance_id, None, cache_behaviour, &state).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_linked_modpack_content(
|
||||
instance_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
) -> crate::Result<Vec<ContentItem>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::list_linked_modpack_content(
|
||||
instance_id,
|
||||
None,
|
||||
cache_behaviour,
|
||||
&state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_dependencies_as_content_items(
|
||||
dependencies: Vec<Dependency>,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
) -> crate::Result<Vec<ContentItem>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::dependencies_to_content_items(
|
||||
&dependencies,
|
||||
cache_behaviour,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_linked_modpack_info(
|
||||
instance_id: &str,
|
||||
cache_behaviour: Option<CacheBehaviour>,
|
||||
) -> crate::Result<Option<LinkedModpackInfo>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::get_linked_modpack_info(
|
||||
instance_id,
|
||||
None,
|
||||
cache_behaviour,
|
||||
&state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
use super::content::get_projects;
|
||||
use super::get::get;
|
||||
use super::paths::get_full_path;
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::pack::install_from::{
|
||||
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
|
||||
};
|
||||
use crate::state::{
|
||||
CacheBehaviour, CachedEntry, InstanceMetadata, ModLoader, SideType, State,
|
||||
};
|
||||
use crate::util::io::{self, IOError};
|
||||
use async_zip::tokio::write::ZipFileWriter;
|
||||
use async_zip::{Compression, ZipEntryBuilder};
|
||||
use path_util::SafeRelativeUtf8UnixPathBuf;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::iter::FromIterator;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn export_mrpack(
|
||||
instance_id: &str,
|
||||
export_path: PathBuf,
|
||||
included_export_candidates: Vec<String>,
|
||||
version_id: Option<String>,
|
||||
description: Option<String>,
|
||||
_name: Option<String>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let _permit: tokio::sync::SemaphorePermit =
|
||||
state.io_semaphore.0.acquire().await?;
|
||||
let metadata = get(instance_id).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to export a nonexistent instance {instance_id}!"
|
||||
))
|
||||
})?;
|
||||
let included_export_candidates = included_export_candidates
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
if let Some(f) = PathBuf::from(x).file_name()
|
||||
&& f.to_string_lossy().starts_with(".DS_Store")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let instance_base_path = get_full_path(instance_id).await?;
|
||||
let mut file = File::create(&export_path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &export_path))?;
|
||||
let mut writer = ZipFileWriter::with_tokio(&mut file);
|
||||
let version_id = version_id.unwrap_or("1.0.0".to_string());
|
||||
let mut packfile =
|
||||
create_mrpack_json(&metadata, version_id, description).await?;
|
||||
let included_candidates_set = HashSet::<_>::from_iter(
|
||||
included_export_candidates.iter().map(|x| x.as_str()),
|
||||
);
|
||||
packfile
|
||||
.files
|
||||
.retain(|f| included_candidates_set.contains(f.path.as_str()));
|
||||
|
||||
let mut path_list = Vec::new();
|
||||
add_all_recursive_folder_paths(&instance_base_path, &mut path_list).await?;
|
||||
let loading_bar = init_loading(
|
||||
LoadingBarType::ZipExtract {
|
||||
instance_id: metadata.instance.id.clone(),
|
||||
instance_name: metadata.instance.name.clone(),
|
||||
},
|
||||
path_list.len() as f64,
|
||||
"Exporting instance to .mrpack",
|
||||
)
|
||||
.await?;
|
||||
|
||||
for path in path_list {
|
||||
emit_loading(&loading_bar, 1.0, None)?;
|
||||
let relative_path = pack_get_relative_path(&instance_base_path, &path)?;
|
||||
|
||||
if packfile.files.iter().any(|f| f.path == relative_path)
|
||||
|| !included_candidates_set
|
||||
.iter()
|
||||
.any(|x| relative_path.starts_with(&**x))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.is_file() {
|
||||
let mut file = File::open(&path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data).await.map_err(IOError::from)?;
|
||||
let builder = ZipEntryBuilder::new(
|
||||
format!("overrides/{relative_path}").into(),
|
||||
Compression::Deflate,
|
||||
);
|
||||
writer.write_entry_whole(builder, &data).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let data = serde_json::to_vec_pretty(&packfile)?;
|
||||
let builder = ZipEntryBuilder::new(
|
||||
"modrinth.index.json".to_string().into(),
|
||||
Compression::Deflate,
|
||||
);
|
||||
writer.write_entry_whole(builder, &data).await?;
|
||||
writer.close().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_pack_export_candidates(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<Vec<SafeRelativeUtf8UnixPathBuf>> {
|
||||
let mut path_list = Vec::new();
|
||||
let instance_base_dir = get_full_path(instance_id).await?;
|
||||
let mut read_dir = io::read_dir(&instance_base_dir).await?;
|
||||
while let Some(entry) = read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &instance_base_dir))?
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let mut read_dir = io::read_dir(&path).await?;
|
||||
while let Some(entry) = read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &instance_base_dir))?
|
||||
{
|
||||
path_list.push(pack_get_relative_path(
|
||||
&instance_base_dir,
|
||||
&entry.path(),
|
||||
)?);
|
||||
}
|
||||
} else {
|
||||
path_list.push(pack_get_relative_path(&instance_base_dir, &path)?);
|
||||
}
|
||||
}
|
||||
Ok(path_list)
|
||||
}
|
||||
|
||||
fn pack_get_relative_path(
|
||||
instance_path: &PathBuf,
|
||||
path: &PathBuf,
|
||||
) -> crate::Result<SafeRelativeUtf8UnixPathBuf> {
|
||||
Ok(SafeRelativeUtf8UnixPathBuf::try_from(
|
||||
path.strip_prefix(instance_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Path {path:?} does not correspond to an instance"
|
||||
))
|
||||
})?
|
||||
.components()
|
||||
.map(|c| c.as_os_str().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/"),
|
||||
)?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn create_mrpack_json(
|
||||
metadata: &InstanceMetadata,
|
||||
version_id: String,
|
||||
description: Option<String>,
|
||||
) -> crate::Result<PackFormat> {
|
||||
let mut dependencies = HashMap::new();
|
||||
match (
|
||||
metadata.applied_content_set.loader,
|
||||
metadata.applied_content_set.loader_version.clone(),
|
||||
) {
|
||||
(ModLoader::Forge, Some(v)) => {
|
||||
dependencies.insert(PackDependency::Forge, v)
|
||||
}
|
||||
(ModLoader::NeoForge, Some(v)) => {
|
||||
dependencies.insert(PackDependency::NeoForge, v)
|
||||
}
|
||||
(ModLoader::Fabric, Some(v)) => {
|
||||
dependencies.insert(PackDependency::FabricLoader, v)
|
||||
}
|
||||
(ModLoader::Quilt, Some(v)) => {
|
||||
dependencies.insert(PackDependency::QuiltLoader, v)
|
||||
}
|
||||
(ModLoader::Vanilla, _) => None,
|
||||
_ => {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Loader version mismatch".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
dependencies.insert(
|
||||
PackDependency::Minecraft,
|
||||
metadata.applied_content_set.game_version.clone(),
|
||||
);
|
||||
|
||||
let state = State::get().await?;
|
||||
let projects = get_projects(
|
||||
&metadata.instance.id,
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|(path, file)| match file.metadata {
|
||||
Some(metadata) => Some((path, metadata.version_id)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let versions = CachedEntry::get_version_many(
|
||||
&projects.iter().map(|x| &*x.1).collect::<Vec<_>>(),
|
||||
None,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?;
|
||||
let files = projects
|
||||
.into_iter()
|
||||
.filter_map(|(path, version_id)| {
|
||||
if let Some(version) = versions.iter().find(|x| x.id == version_id)
|
||||
{
|
||||
let mut env = HashMap::new();
|
||||
env.insert(EnvType::Client, SideType::Required);
|
||||
env.insert(EnvType::Server, SideType::Required);
|
||||
let Some(primary_file) = version.files.first() else {
|
||||
return Some(Err(crate::ErrorKind::OtherError(format!(
|
||||
"No primary file found for mod at: {path}"
|
||||
))
|
||||
.as_error()));
|
||||
};
|
||||
let file_size = primary_file.size;
|
||||
let downloads = vec![primary_file.url.clone()];
|
||||
let hashes = primary_file
|
||||
.hashes
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(h1, h2)| (PackFileHash::from(h1), h2))
|
||||
.collect();
|
||||
|
||||
Some(Ok(PackFile {
|
||||
path: match path.try_into() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
return Some(Err(crate::ErrorKind::OtherError(
|
||||
"Invalid file path in project".into(),
|
||||
)
|
||||
.as_error()));
|
||||
}
|
||||
},
|
||||
hashes,
|
||||
env: Some(env),
|
||||
downloads,
|
||||
file_size,
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<crate::Result<Vec<PackFile>>>()?;
|
||||
|
||||
Ok(PackFormat {
|
||||
game: "minecraft".to_string(),
|
||||
format_version: 1,
|
||||
version_id,
|
||||
name: metadata.instance.name.clone(),
|
||||
summary: description,
|
||||
files,
|
||||
dependencies,
|
||||
})
|
||||
}
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
async fn add_all_recursive_folder_paths(
|
||||
folder: &PathBuf,
|
||||
output: &mut Vec<PathBuf>,
|
||||
) -> crate::Result<()> {
|
||||
let mut read_dir = io::read_dir(folder).await?;
|
||||
while let Some(entry) = read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, folder))?
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
add_all_recursive_folder_paths(&path, output).await?;
|
||||
} else {
|
||||
output.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use crate::state::{InstanceMetadata, State};
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get(instance_id: &str) -> crate::Result<Option<InstanceMetadata>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::get_instance(instance_id, &state.pool).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_many(
|
||||
instance_ids: &[&str],
|
||||
) -> crate::Result<Vec<InstanceMetadata>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::get_instances_metadata(instance_ids, &state.pool).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn list() -> crate::Result<Vec<InstanceMetadata>> {
|
||||
let state = State::get().await?;
|
||||
crate::state::list_instances(&state.pool).await
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use crate::state::{JavaVersion, State};
|
||||
|
||||
pub async fn get_optimal_jre_key(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<Option<JavaVersion>> {
|
||||
let state = State::get().await?;
|
||||
let context =
|
||||
crate::state::instances::commands::get_instance_launch_context(
|
||||
instance_id,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to resolve a nonexistent instance {instance_id}!"
|
||||
))
|
||||
})?;
|
||||
let (minecraft, version_index) =
|
||||
crate::launcher::resolve_minecraft_manifest(
|
||||
&context.applied_content_set.game_version,
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
let version = &minecraft.versions[version_index];
|
||||
let loader_version = crate::launcher::get_loader_version_from_profile(
|
||||
&context.applied_content_set.game_version,
|
||||
context.applied_content_set.loader,
|
||||
context.applied_content_set.loader_version.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let version_info = crate::launcher::download::download_version_info(
|
||||
&state,
|
||||
version,
|
||||
loader_version.as_ref(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::launcher::get_java_version_from_launch_context(
|
||||
&context,
|
||||
&version_info,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
use crate::event::InstancePayloadType;
|
||||
use crate::event::emit::emit_instance;
|
||||
use crate::state::instances::adapters::sqlite::instance_rows;
|
||||
use crate::state::{
|
||||
CreateInstance, EditInstance, InstanceLink, InstanceMetadata, ModLoader,
|
||||
State,
|
||||
};
|
||||
use crate::util::io;
|
||||
use std::path::Path;
|
||||
|
||||
#[tracing::instrument]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn create(
|
||||
name: String,
|
||||
game_version: String,
|
||||
modloader: ModLoader,
|
||||
loader_version: Option<String>,
|
||||
icon_path: Option<String>,
|
||||
link: InstanceLink,
|
||||
) -> crate::Result<InstanceMetadata> {
|
||||
let state = State::get().await?;
|
||||
let instance = crate::state::create_instance(
|
||||
CreateInstance {
|
||||
name,
|
||||
path: None,
|
||||
game_version,
|
||||
loader: modloader,
|
||||
loader_version,
|
||||
icon_path,
|
||||
link,
|
||||
},
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let result = async {
|
||||
emit_instance(&instance.id, InstancePayloadType::Created).await?;
|
||||
|
||||
crate::state::get_instance(&instance.id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(
|
||||
"Created instance could not be loaded".to_string(),
|
||||
)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
let _ = crate::state::remove_instance(&instance.id, &state).await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn edit(
|
||||
instance_id: &str,
|
||||
patch: EditInstance,
|
||||
) -> crate::Result<InstanceMetadata> {
|
||||
let state = State::get().await?;
|
||||
crate::state::edit_instance(instance_id, patch, &state.pool).await?;
|
||||
|
||||
crate::state::get_instance(instance_id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string()).into()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn edit_icon(
|
||||
instance_id: &str,
|
||||
icon_path: Option<&Path>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let instance =
|
||||
instance_rows::get_instance_display_info(instance_id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
let icon_path = if let Some(icon) = icon_path {
|
||||
let bytes = io::read(icon).await?;
|
||||
let file = crate::util::fetch::write_cached_icon(
|
||||
&icon.to_string_lossy(),
|
||||
&state.directories.caches_dir(),
|
||||
bytes::Bytes::from(bytes),
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
Some(file.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
crate::state::edit_instance(
|
||||
instance_id,
|
||||
EditInstance {
|
||||
icon_path: Some(icon_path),
|
||||
..EditInstance::default()
|
||||
},
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
emit_instance(&instance.id, InstancePayloadType::Edited).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn remove(instance_id: &str) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let instance =
|
||||
instance_rows::get_instance_display_info(instance_id, &state.pool)
|
||||
.await?;
|
||||
crate::state::remove_instance(instance_id, &state).await?;
|
||||
|
||||
if let Some(instance) = instance {
|
||||
emit_instance(&instance.id, InstancePayloadType::Removed).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::state::State;
|
||||
use crate::util::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_full_path(instance_id: &str) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
let path =
|
||||
crate::state::instances::adapters::sqlite::instance_rows::get_instance_path_by_id(
|
||||
instance_id,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
|
||||
Ok(io::canonicalize(
|
||||
state.directories.instances_dir().join(path),
|
||||
)?)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_mod_full_path(
|
||||
instance_id: &str,
|
||||
project_path: &str,
|
||||
) -> crate::Result<PathBuf> {
|
||||
Ok(get_full_path(instance_id).await?.join(project_path))
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
use crate::event::emit::{emit_instance, emit_loading, init_loading};
|
||||
use crate::event::{InstancePayloadType, LoadingBarType};
|
||||
use crate::state::instances::adapters::sqlite::instance_rows;
|
||||
use crate::state::{ProjectType, State};
|
||||
use crate::util::fetch;
|
||||
use modrinth_content_management::{
|
||||
ContentType, ResolutionPreferences, ResolveContentPlan,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct InstallProjectWithDependenciesRequest {
|
||||
pub project_id: String,
|
||||
pub version_id: Option<String>,
|
||||
pub content_type: ContentType,
|
||||
#[serde(default)]
|
||||
pub selected: ResolutionPreferences,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn update_all_projects(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<HashMap<String, String>> {
|
||||
let state = State::get().await?;
|
||||
let instance = get_instance_display_info(instance_id, &state).await?;
|
||||
let loading_bar = init_loading(
|
||||
LoadingBarType::InstanceUpdate {
|
||||
instance_id: instance.id.clone(),
|
||||
instance_name: instance.name.clone(),
|
||||
},
|
||||
100.0,
|
||||
"Updating instance",
|
||||
)
|
||||
.await?;
|
||||
let map = crate::state::instances::commands::update_all_projects(
|
||||
instance_id,
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
emit_loading(&loading_bar, 100.0, Some("Updated instance"))?;
|
||||
emit_instance(&instance.id, InstancePayloadType::Edited).await?;
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn update_project(
|
||||
instance_id: &str,
|
||||
project_path: &str,
|
||||
skip_send_event: Option<bool>,
|
||||
) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
let path = crate::state::instances::commands::update_project(
|
||||
instance_id,
|
||||
project_path,
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !skip_send_event.unwrap_or(false) {
|
||||
emit_instance(instance_id, InstancePayloadType::Edited).await?;
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn add_project_from_version(
|
||||
instance_id: &str,
|
||||
version_id: &str,
|
||||
reason: fetch::DownloadReason,
|
||||
dependent_on_version_id: Option<String>,
|
||||
) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
let project_path =
|
||||
crate::state::instances::commands::add_project_from_version(
|
||||
instance_id,
|
||||
version_id,
|
||||
reason,
|
||||
dependent_on_version_id,
|
||||
crate::state::ContentSourceKind::Local,
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
emit_instance(instance_id, InstancePayloadType::Edited).await?;
|
||||
|
||||
Ok(project_path)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn install_project_with_dependencies(
|
||||
instance_id: &str,
|
||||
request: InstallProjectWithDependenciesRequest,
|
||||
) -> crate::Result<ResolveContentPlan> {
|
||||
let state = State::get().await?;
|
||||
let metadata = super::get::get(instance_id).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
let plan = crate::state::instances::commands::resolve_install_plan(
|
||||
instance_id,
|
||||
crate::state::instances::commands::InstanceInstallProjectRequest {
|
||||
project_id: request.project_id,
|
||||
version_id: request.version_id,
|
||||
content_type: request.content_type,
|
||||
selected: request.selected,
|
||||
},
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let instance_id = metadata.instance.id;
|
||||
let project_ids = plan_project_ids(&plan);
|
||||
let install_plan = plan.clone();
|
||||
tokio::spawn(async move {
|
||||
match crate::state::instances::commands::install_resolved_content_plan(
|
||||
&instance_id,
|
||||
&install_plan,
|
||||
&state,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if let Err(error) = emit_instance(
|
||||
&instance_id,
|
||||
InstancePayloadType::ContentInstallFinished {
|
||||
project_ids: project_ids.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to emit content install finished event: {error}"
|
||||
);
|
||||
}
|
||||
if let Err(error) =
|
||||
emit_instance(&instance_id, InstancePayloadType::Edited)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to emit instance edited event after content install: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
if let Err(emit_error) = emit_instance(
|
||||
&instance_id,
|
||||
InstancePayloadType::ContentInstallFailed {
|
||||
project_ids,
|
||||
message: error.to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to emit content install failed event: {emit_error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
fn plan_project_ids(plan: &ResolveContentPlan) -> Vec<String> {
|
||||
let mut project_ids = Vec::with_capacity(plan.dependencies.len() + 1);
|
||||
project_ids.push(plan.primary.project_id.clone());
|
||||
project_ids.extend(
|
||||
plan.dependencies
|
||||
.iter()
|
||||
.map(|dependency| dependency.project_id.clone()),
|
||||
);
|
||||
project_ids
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn switch_project_version_with_dependencies(
|
||||
instance_id: &str,
|
||||
project_path: &str,
|
||||
version_id: &str,
|
||||
) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
let metadata = super::get::get(instance_id).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
let path =
|
||||
crate::state::instances::commands::switch_project_version_with_dependencies(
|
||||
instance_id,
|
||||
project_path,
|
||||
version_id,
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
emit_instance(&metadata.instance.id, InstancePayloadType::Edited).await?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn add_project_from_path(
|
||||
instance_id: &str,
|
||||
path: &Path,
|
||||
project_type: Option<ProjectType>,
|
||||
) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
crate::state::instances::commands::add_project_from_path(
|
||||
instance_id,
|
||||
path,
|
||||
project_type,
|
||||
&state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn toggle_disable_project(
|
||||
instance_id: &str,
|
||||
project: &str,
|
||||
desired_enabled: Option<bool>,
|
||||
) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
let res = crate::state::instances::commands::toggle_disable_project(
|
||||
instance_id,
|
||||
project,
|
||||
desired_enabled,
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
emit_instance(instance_id, InstancePayloadType::Edited).await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_project(
|
||||
instance_id: &str,
|
||||
project: &str,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
crate::state::instances::commands::remove_project(
|
||||
instance_id,
|
||||
project,
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
emit_instance(instance_id, InstancePayloadType::Edited).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn update_managed_modrinth_version(
|
||||
instance_id: &str,
|
||||
version_id: &str,
|
||||
) -> crate::Result<crate::install::InstallJobSnapshot> {
|
||||
let state = State::get().await?;
|
||||
let metadata = crate::state::instances::commands::get_instance_metadata(
|
||||
instance_id,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
|
||||
let post_install_edit = match &metadata.link {
|
||||
crate::state::InstanceLink::ServerProjectModpack {
|
||||
server_project_id,
|
||||
content_project_id,
|
||||
..
|
||||
} => Some(crate::install::InstallPostInstallEdit {
|
||||
name: Some(metadata.instance.name.clone()),
|
||||
icon_path: Some(metadata.instance.icon_path.clone()),
|
||||
link: Some(crate::state::InstanceLink::ServerProjectModpack {
|
||||
server_project_id: server_project_id.clone(),
|
||||
content_project_id: content_project_id.clone(),
|
||||
content_version_id: version_id.to_string(),
|
||||
}),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let project_id = match &metadata.link {
|
||||
crate::state::InstanceLink::ModrinthModpack { project_id, .. } => {
|
||||
project_id.clone()
|
||||
}
|
||||
crate::state::InstanceLink::ServerProjectModpack {
|
||||
content_project_id,
|
||||
..
|
||||
} => content_project_id.clone(),
|
||||
_ => {
|
||||
return Err(unmanaged_pack_error(&metadata.instance.id).into());
|
||||
}
|
||||
};
|
||||
|
||||
crate::install::install_pack_to_existing_instance(
|
||||
metadata.instance.id,
|
||||
crate::api::pack::install_from::CreatePackLocation::FromVersionId {
|
||||
project_id,
|
||||
version_id: version_id.to_string(),
|
||||
title: metadata.instance.name.clone(),
|
||||
icon_url: None,
|
||||
},
|
||||
post_install_edit,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn repair_managed_modrinth(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<crate::install::InstallJobSnapshot> {
|
||||
let state = State::get().await?;
|
||||
let metadata = crate::state::instances::commands::get_instance_metadata(
|
||||
instance_id,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string())
|
||||
})?;
|
||||
|
||||
let post_install_edit = match &metadata.link {
|
||||
crate::state::InstanceLink::ServerProjectModpack { .. } => {
|
||||
Some(crate::install::InstallPostInstallEdit {
|
||||
name: Some(metadata.instance.name.clone()),
|
||||
icon_path: Some(metadata.instance.icon_path.clone()),
|
||||
link: Some(metadata.link.clone()),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let (project_id, version_id) = match &metadata.link {
|
||||
crate::state::InstanceLink::ModrinthModpack {
|
||||
project_id,
|
||||
version_id,
|
||||
} => (project_id.clone(), version_id.clone()),
|
||||
crate::state::InstanceLink::ServerProjectModpack {
|
||||
content_project_id,
|
||||
content_version_id,
|
||||
..
|
||||
} => (content_project_id.clone(), content_version_id.clone()),
|
||||
_ => {
|
||||
return Err(unmanaged_pack_error(&metadata.instance.id).into());
|
||||
}
|
||||
};
|
||||
|
||||
crate::install::install_pack_to_existing_instance(
|
||||
metadata.instance.id,
|
||||
crate::api::pack::install_from::CreatePackLocation::FromVersionId {
|
||||
project_id,
|
||||
version_id,
|
||||
title: metadata.instance.name.clone(),
|
||||
icon_url: None,
|
||||
},
|
||||
post_install_edit,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn unmanaged_pack_error(instance_id: &str) -> crate::ErrorKind {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Instance {instance_id} is not a managed Modrinth pack, or has been disconnected."
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_instance_display_info(
|
||||
instance_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<instance_rows::InstanceDisplayInfo> {
|
||||
instance_rows::get_instance_display_info(instance_id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError("Unknown instance".to_string()).into()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
use super::content::get_projects;
|
||||
use crate::server_address::ServerAddress;
|
||||
use crate::state::{
|
||||
Credentials, InstanceLink, ProcessMetadata, Settings, State,
|
||||
};
|
||||
use crate::util::fetch;
|
||||
use crate::util::io::IOError;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tokio::process::Command;
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum QuickPlayType {
|
||||
None,
|
||||
Singleplayer(String),
|
||||
Server(ServerAddress),
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn run(
|
||||
instance_id: &str,
|
||||
quick_play_type: QuickPlayType,
|
||||
) -> crate::Result<ProcessMetadata> {
|
||||
let state = State::get().await?;
|
||||
let default_account = Credentials::get_default_credential(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError.as_error())?;
|
||||
|
||||
run_credentials(instance_id, &default_account, quick_play_type).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(credentials))]
|
||||
async fn run_credentials(
|
||||
instance_id: &str,
|
||||
credentials: &Credentials,
|
||||
quick_play_type: QuickPlayType,
|
||||
) -> crate::Result<ProcessMetadata> {
|
||||
let state = State::get().await?;
|
||||
let settings = Settings::get(&state.pool).await?;
|
||||
let context =
|
||||
crate::state::instances::commands::get_instance_launch_context(
|
||||
instance_id,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to run a nonexistent instance {instance_id}!"
|
||||
))
|
||||
})?;
|
||||
|
||||
let pre_launch_hooks = context
|
||||
.launch_overrides
|
||||
.hooks
|
||||
.pre_launch
|
||||
.as_ref()
|
||||
.or(settings.hooks.pre_launch.as_ref())
|
||||
.filter(|hook_command| !hook_command.is_empty());
|
||||
if let Some(hook) = pre_launch_hooks {
|
||||
let mut cmd = shlex::split(hook)
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Invalid pre-launch command: {hook}",
|
||||
))
|
||||
})?
|
||||
.into_iter();
|
||||
|
||||
if let Some(command) = cmd.next() {
|
||||
let full_path = crate::util::io::canonicalize(
|
||||
state
|
||||
.directories
|
||||
.instances_dir()
|
||||
.join(&context.instance.path),
|
||||
)?;
|
||||
let result = Command::new(command)
|
||||
.args(cmd)
|
||||
.current_dir(&full_path)
|
||||
.spawn()
|
||||
.map_err(|e| IOError::with_path(e, &full_path))?
|
||||
.wait()
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
|
||||
if !result.success() {
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Non-zero exit code for pre-launch hook: {}",
|
||||
result.code().unwrap_or(-1)
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let java_args = context
|
||||
.launch_overrides
|
||||
.extra_launch_args
|
||||
.clone()
|
||||
.unwrap_or(settings.extra_launch_args);
|
||||
let wrapper = context
|
||||
.launch_overrides
|
||||
.hooks
|
||||
.wrapper
|
||||
.clone()
|
||||
.or(settings.hooks.wrapper)
|
||||
.filter(|hook_command| !hook_command.is_empty());
|
||||
let memory = context.launch_overrides.memory.unwrap_or(settings.memory);
|
||||
let resolution = context
|
||||
.launch_overrides
|
||||
.game_resolution
|
||||
.unwrap_or(settings.game_resolution);
|
||||
let env_args = context
|
||||
.launch_overrides
|
||||
.custom_env_vars
|
||||
.clone()
|
||||
.unwrap_or(settings.custom_env_vars);
|
||||
let post_exit_hook = context
|
||||
.launch_overrides
|
||||
.hooks
|
||||
.post_exit
|
||||
.clone()
|
||||
.or(settings.hooks.post_exit)
|
||||
.filter(|hook_command| !hook_command.is_empty());
|
||||
|
||||
let mut mc_set_options: Vec<(String, String)> = vec![];
|
||||
if let Some(fullscreen) = context.launch_overrides.force_fullscreen {
|
||||
mc_set_options.push(("fullscreen".to_string(), fullscreen.to_string()));
|
||||
} else if settings.force_fullscreen {
|
||||
mc_set_options.push(("fullscreen".to_string(), "true".to_string()));
|
||||
}
|
||||
|
||||
if let Some(project_id) = server_play_project_id(&context.link)
|
||||
&& !project_id.trim().is_empty()
|
||||
{
|
||||
let server_id = uuid::Uuid::new_v4().to_string();
|
||||
let join_result = fetch::INSECURE_REQWEST_CLIENT
|
||||
.post("https://sessionserver.mojang.com/session/minecraft/join")
|
||||
.json(&json!({
|
||||
"accessToken": &credentials.access_token,
|
||||
"selectedProfile": credentials.offline_profile.id.simple().to_string(),
|
||||
"serverId": &server_id,
|
||||
}))
|
||||
.timeout(Duration::from_secs(5))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match join_result {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
let result = fetch::post_json(
|
||||
concat!(
|
||||
env!("MODRINTH_API_BASE_URL"),
|
||||
"analytics/minecraft-server-play"
|
||||
),
|
||||
json!({
|
||||
"project_id": project_id,
|
||||
"username": &credentials.offline_profile.name,
|
||||
"server_id": &server_id,
|
||||
}),
|
||||
&state.api_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
info!(
|
||||
"Tracked server play for '{project_id}' in analytics"
|
||||
)
|
||||
}
|
||||
Err(err) => warn!("Failed to report server play: {err:?}"),
|
||||
}
|
||||
}
|
||||
Ok(resp) => warn!(
|
||||
"Failed to join Mojang session server: HTTP {}",
|
||||
resp.status()
|
||||
),
|
||||
Err(err) => warn!("Failed to join Mojang session server: {err:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
crate::minecraft_skins::flush_pending_skin_change().await?;
|
||||
crate::launcher::launch_minecraft(
|
||||
&java_args,
|
||||
&env_args,
|
||||
&mc_set_options,
|
||||
&wrapper,
|
||||
&memory,
|
||||
&resolution,
|
||||
credentials,
|
||||
post_exit_hook,
|
||||
&context,
|
||||
quick_play_type,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn server_play_project_id(link: &InstanceLink) -> Option<&String> {
|
||||
match link {
|
||||
InstanceLink::ServerProject { project_id }
|
||||
| InstanceLink::ServerProjectModpack {
|
||||
server_project_id: project_id,
|
||||
..
|
||||
} => Some(project_id),
|
||||
InstanceLink::Unmanaged
|
||||
| InstanceLink::ModrinthModpack { .. }
|
||||
| InstanceLink::ModrinthHosting { .. }
|
||||
| InstanceLink::ImportedModpack { .. }
|
||||
| InstanceLink::SharedInstance { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn kill(instance_id: &str) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let processes =
|
||||
crate::api::process::get_by_instance_id(instance_id).await?;
|
||||
|
||||
for process in processes {
|
||||
state.process_manager.kill(process.uuid).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn try_update_playtime_by_instance_id(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let context =
|
||||
crate::state::instances::commands::get_instance_launch_context(
|
||||
instance_id,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to update playtime for nonexistent instance {instance_id}!"
|
||||
))
|
||||
})?;
|
||||
let updated_recent_playtime = context.instance.recent_time_played;
|
||||
let res = if updated_recent_playtime > 0 {
|
||||
let modrinth_pack_version_id = match &context.link {
|
||||
InstanceLink::ModrinthModpack { version_id, .. }
|
||||
| InstanceLink::ServerProjectModpack {
|
||||
content_version_id: version_id,
|
||||
..
|
||||
}
|
||||
| InstanceLink::ImportedModpack {
|
||||
version_id: Some(version_id),
|
||||
..
|
||||
} => Some(version_id.clone()),
|
||||
InstanceLink::Unmanaged
|
||||
| InstanceLink::ServerProject { .. }
|
||||
| InstanceLink::ModrinthHosting { .. }
|
||||
| InstanceLink::ImportedModpack { .. }
|
||||
| InstanceLink::SharedInstance { .. } => None,
|
||||
};
|
||||
let playtime_update_json = json!({
|
||||
"seconds": updated_recent_playtime,
|
||||
"loader": context.applied_content_set.loader.as_str(),
|
||||
"game_version": &context.applied_content_set.game_version,
|
||||
"parent": modrinth_pack_version_id,
|
||||
});
|
||||
let mut hashmap: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
|
||||
for (_, project) in get_projects(instance_id, None).await? {
|
||||
if let Some(metadata) = project.metadata {
|
||||
hashmap
|
||||
.insert(metadata.version_id, playtime_update_json.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fetch::post_json(
|
||||
concat!(env!("MODRINTH_API_BASE_URL"), "analytics/playtime"),
|
||||
serde_json::to_value(hashmap)?,
|
||||
&state.api_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if res.is_ok() {
|
||||
crate::state::instances::commands::mark_instance_playtime_submitted(
|
||||
&context.instance.id,
|
||||
updated_recent_playtime,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
+170
-23
@@ -1,11 +1,19 @@
|
||||
//! Authentication flow interface
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::install::{
|
||||
InstallJavaStep, InstallPhaseDetails, InstallPhaseId, InstallProgress,
|
||||
InstallProgressReporter,
|
||||
};
|
||||
use crate::state::JavaVersion;
|
||||
use crate::util::fetch::{fetch_advanced, fetch_json};
|
||||
use crate::util::fetch::{
|
||||
FetchProgressFn, fetch_advanced, fetch_advanced_with_progress, fetch_json,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use sysinfo::{MemoryRefreshKind, RefreshKind};
|
||||
|
||||
use crate::util::io;
|
||||
@@ -52,16 +60,77 @@ pub async fn find_filtered_jres(
|
||||
}
|
||||
|
||||
pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
auto_install_java_with_loading(java_version, true).await
|
||||
}
|
||||
|
||||
pub async fn auto_install_java_with_loading(
|
||||
java_version: u32,
|
||||
show_loading: bool,
|
||||
) -> crate::Result<PathBuf> {
|
||||
auto_install_java_inner(java_version, show_loading, None).await
|
||||
}
|
||||
|
||||
pub async fn auto_install_java_with_reporter(
|
||||
java_version: u32,
|
||||
reporter: InstallProgressReporter,
|
||||
) -> crate::Result<PathBuf> {
|
||||
auto_install_java_inner(java_version, false, Some(reporter)).await
|
||||
}
|
||||
|
||||
const JAVA_INSTALL_STEPS: u64 = 4;
|
||||
const JAVA_DOWNLOAD_PROGRESS_MIN_BYTES: u64 = 256 * 1024;
|
||||
|
||||
async fn update_java_install_progress(
|
||||
reporter: Option<&InstallProgressReporter>,
|
||||
java_version: u32,
|
||||
step: InstallJavaStep,
|
||||
progress: Option<InstallProgress>,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(reporter) = reporter {
|
||||
reporter
|
||||
.update(
|
||||
InstallPhaseId::PreparingJava,
|
||||
progress,
|
||||
InstallPhaseDetails::Java {
|
||||
major_version: java_version,
|
||||
step,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn java_step_progress(current: u64) -> InstallProgress {
|
||||
InstallProgress {
|
||||
current,
|
||||
total: JAVA_INSTALL_STEPS,
|
||||
secondary: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn auto_install_java_inner(
|
||||
java_version: u32,
|
||||
show_loading: bool,
|
||||
reporter: Option<InstallProgressReporter>,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let loading_bar = init_loading(
|
||||
LoadingBarType::JavaDownload {
|
||||
version: java_version,
|
||||
},
|
||||
100.0,
|
||||
"Downloading java version",
|
||||
)
|
||||
.await?;
|
||||
let loading_bar = if show_loading {
|
||||
Some(
|
||||
init_loading(
|
||||
LoadingBarType::JavaDownload {
|
||||
version: java_version,
|
||||
},
|
||||
100.0,
|
||||
"Downloading java version",
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Package {
|
||||
@@ -69,7 +138,16 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
pub name: PathBuf,
|
||||
}
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Fetching java version"))?;
|
||||
if let Some(loading_bar) = &loading_bar {
|
||||
emit_loading(loading_bar, 0.0, Some("Fetching java version"))?;
|
||||
}
|
||||
update_java_install_progress(
|
||||
reporter.as_ref(),
|
||||
java_version,
|
||||
InstallJavaStep::FetchingMetadata,
|
||||
Some(java_step_progress(1)),
|
||||
)
|
||||
.await?;
|
||||
let packages = fetch_json::<Vec<Package>>(
|
||||
Method::GET,
|
||||
&format!(
|
||||
@@ -82,22 +160,80 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
&state.fetch_semaphore,
|
||||
&state.pool,
|
||||
).await?;
|
||||
emit_loading(&loading_bar, 10.0, Some("Downloading java version"))?;
|
||||
if let Some(loading_bar) = &loading_bar {
|
||||
emit_loading(loading_bar, 10.0, Some("Downloading java version"))?;
|
||||
}
|
||||
|
||||
if let Some(download) = packages.first() {
|
||||
let file = fetch_advanced(
|
||||
Method::GET,
|
||||
&download.download_url,
|
||||
update_java_install_progress(
|
||||
reporter.as_ref(),
|
||||
java_version,
|
||||
InstallJavaStep::Downloading,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some((&loading_bar, 80.0)),
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
let file = if reporter.is_some() {
|
||||
let mut last_reported_bytes = 0_u64;
|
||||
let download_reporter = reporter.clone();
|
||||
let mut progress = move |current: u64,
|
||||
total: u64|
|
||||
-> Pin<
|
||||
Box<dyn Future<Output = crate::Result<()>> + Send>,
|
||||
> {
|
||||
let min_delta =
|
||||
(total / 200).max(JAVA_DOWNLOAD_PROGRESS_MIN_BYTES);
|
||||
if current < total
|
||||
&& current.saturating_sub(last_reported_bytes) < min_delta
|
||||
{
|
||||
return Box::pin(async { Ok(()) });
|
||||
}
|
||||
|
||||
last_reported_bytes = current;
|
||||
let reporter = download_reporter.clone();
|
||||
Box::pin(async move {
|
||||
update_java_install_progress(
|
||||
reporter.as_ref(),
|
||||
java_version,
|
||||
InstallJavaStep::Downloading,
|
||||
Some(InstallProgress {
|
||||
current,
|
||||
total,
|
||||
secondary: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
})
|
||||
};
|
||||
|
||||
fetch_advanced_with_progress(
|
||||
Method::GET,
|
||||
&download.download_url,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
loading_bar.as_ref().map(|loading_bar| (loading_bar, 80.0)),
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&state.pool,
|
||||
Some(&mut progress as &mut FetchProgressFn<'_>),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
fetch_advanced(
|
||||
Method::GET,
|
||||
&download.download_url,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
loading_bar.as_ref().map(|loading_bar| (loading_bar, 80.0)),
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
let path = state.directories.java_versions_dir();
|
||||
|
||||
@@ -119,13 +255,24 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Extracting java"))?;
|
||||
if let Some(loading_bar) = &loading_bar {
|
||||
emit_loading(loading_bar, 0.0, Some("Extracting java"))?;
|
||||
}
|
||||
update_java_install_progress(
|
||||
reporter.as_ref(),
|
||||
java_version,
|
||||
InstallJavaStep::Extracting,
|
||||
Some(java_step_progress(3)),
|
||||
)
|
||||
.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"))?;
|
||||
if let Some(loading_bar) = &loading_bar {
|
||||
emit_loading(loading_bar, 10.0, Some("Done extracting java"))?;
|
||||
}
|
||||
let mut base_path = path.join(
|
||||
download
|
||||
.name
|
||||
|
||||
@@ -82,6 +82,32 @@ struct CompactedLog {
|
||||
stats: LogCompactionStats,
|
||||
}
|
||||
|
||||
async fn resolve_instance_path(
|
||||
instance: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<String> {
|
||||
sqlx::query_scalar!(
|
||||
"
|
||||
SELECT path
|
||||
FROM instances
|
||||
WHERE id = ? OR path = ?
|
||||
ORDER BY CASE WHEN id = ? THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
",
|
||||
instance,
|
||||
instance,
|
||||
instance,
|
||||
)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unknown instance id or path: {instance}"
|
||||
))
|
||||
.as_error()
|
||||
})
|
||||
}
|
||||
|
||||
fn split_line_ending(line: &str) -> (&str, &str) {
|
||||
if let Some(line) = line.strip_suffix("\r\n") {
|
||||
(line, "\r\n")
|
||||
@@ -211,7 +237,7 @@ impl Logs {
|
||||
async fn build(
|
||||
log_type: LogType,
|
||||
age: SystemTime,
|
||||
profile_subpath: &str,
|
||||
instance_path: &str,
|
||||
filename: String,
|
||||
clear_contents: Option<bool>,
|
||||
) -> crate::Result<Self> {
|
||||
@@ -225,12 +251,8 @@ impl Logs {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
get_output_by_filename(
|
||||
profile_subpath,
|
||||
log_type,
|
||||
&filename,
|
||||
)
|
||||
.await?,
|
||||
get_output_by_filename(instance_path, log_type, &filename)
|
||||
.await?,
|
||||
)
|
||||
},
|
||||
filename,
|
||||
@@ -240,17 +262,18 @@ impl Logs {
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs_from_type(
|
||||
profile_path: &str,
|
||||
instance_id: &str,
|
||||
log_type: LogType,
|
||||
clear_contents: Option<bool>,
|
||||
logs: &mut Vec<crate::Result<Logs>>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let instance_path = resolve_instance_path(instance_id, &state).await?;
|
||||
|
||||
let logs_folder = match log_type {
|
||||
LogType::InfoLog => state.directories.profile_logs_dir(profile_path),
|
||||
LogType::InfoLog => state.directories.instance_logs_dir(&instance_path),
|
||||
LogType::CrashReport => {
|
||||
state.directories.crash_reports_dir(profile_path)
|
||||
state.directories.crash_reports_dir(&instance_path)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -274,7 +297,7 @@ pub async fn get_logs_from_type(
|
||||
Logs::build(
|
||||
log_type,
|
||||
age,
|
||||
profile_path,
|
||||
&instance_path,
|
||||
file_name,
|
||||
clear_contents,
|
||||
)
|
||||
@@ -288,19 +311,19 @@ pub async fn get_logs_from_type(
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs(
|
||||
profile_path_id: &str,
|
||||
instance_id: &str,
|
||||
clear_contents: Option<bool>,
|
||||
) -> crate::Result<Vec<Logs>> {
|
||||
let mut logs = Vec::new();
|
||||
get_logs_from_type(
|
||||
profile_path_id,
|
||||
instance_id,
|
||||
LogType::InfoLog,
|
||||
clear_contents,
|
||||
&mut logs,
|
||||
)
|
||||
.await?;
|
||||
get_logs_from_type(
|
||||
profile_path_id,
|
||||
instance_id,
|
||||
LogType::CrashReport,
|
||||
clear_contents,
|
||||
&mut logs,
|
||||
@@ -314,16 +337,17 @@ pub async fn get_logs(
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs_by_filename(
|
||||
profile_path: &str,
|
||||
instance_id: &str,
|
||||
log_type: LogType,
|
||||
filename: String,
|
||||
) -> crate::Result<Logs> {
|
||||
let state = State::get().await?;
|
||||
let instance_path = resolve_instance_path(instance_id, &state).await?;
|
||||
|
||||
let path = match log_type {
|
||||
LogType::InfoLog => state.directories.profile_logs_dir(profile_path),
|
||||
LogType::InfoLog => state.directories.instance_logs_dir(&instance_path),
|
||||
LogType::CrashReport => {
|
||||
state.directories.crash_reports_dir(profile_path)
|
||||
state.directories.crash_reports_dir(&instance_path)
|
||||
}
|
||||
}
|
||||
.join(&filename);
|
||||
@@ -331,21 +355,21 @@ pub async fn get_logs_by_filename(
|
||||
let metadata = std::fs::metadata(&path)?;
|
||||
let age = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
|
||||
Logs::build(log_type, age, profile_path, filename, Some(true)).await
|
||||
Logs::build(log_type, age, &instance_path, filename, Some(true)).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_output_by_filename(
|
||||
profile_subpath: &str,
|
||||
instance_path: &str,
|
||||
log_type: LogType,
|
||||
file_name: &str,
|
||||
) -> crate::Result<CensoredString> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let logs_folder = match log_type {
|
||||
LogType::InfoLog => state.directories.profile_logs_dir(profile_subpath),
|
||||
LogType::InfoLog => state.directories.instance_logs_dir(instance_path),
|
||||
LogType::CrashReport => {
|
||||
state.directories.crash_reports_dir(profile_subpath)
|
||||
state.directories.crash_reports_dir(instance_path)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -386,10 +410,11 @@ pub async fn get_output_by_filename(
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs(profile_path_id: &str) -> crate::Result<()> {
|
||||
pub async fn delete_logs(instance_id: &str) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let instance_path = resolve_instance_path(instance_id, &state).await?;
|
||||
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_path_id);
|
||||
let logs_folder = state.directories.instance_logs_dir(&instance_path);
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
@@ -404,16 +429,17 @@ pub async fn delete_logs(profile_path_id: &str) -> crate::Result<()> {
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs_by_filename(
|
||||
profile_path_id: &str,
|
||||
instance_id: &str,
|
||||
log_type: LogType,
|
||||
filename: &str,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let instance_path = resolve_instance_path(instance_id, &state).await?;
|
||||
|
||||
let logs_folder = match log_type {
|
||||
LogType::InfoLog => state.directories.profile_logs_dir(profile_path_id),
|
||||
LogType::InfoLog => state.directories.instance_logs_dir(&instance_path),
|
||||
LogType::CrashReport => {
|
||||
state.directories.crash_reports_dir(profile_path_id)
|
||||
state.directories.crash_reports_dir(&instance_path)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -424,10 +450,10 @@ pub async fn delete_logs_by_filename(
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_live_log_buffer(
|
||||
profile_path: &str,
|
||||
instance_id: &str,
|
||||
) -> crate::Result<CensoredString> {
|
||||
let state = State::get().await?;
|
||||
let lines = crate::state::get_log_buffer(profile_path);
|
||||
let lines = crate::state::get_log_buffer(instance_id);
|
||||
let joined = lines.join("\n");
|
||||
let compacted = compact_duplicate_lines(&joined);
|
||||
|
||||
@@ -440,26 +466,27 @@ pub async fn get_live_log_buffer(
|
||||
Ok(CensoredString::censor(compacted.output, &credentials))
|
||||
}
|
||||
|
||||
pub fn clear_live_log_buffer(profile_path: &str) {
|
||||
crate::state::remove_log_buffer(profile_path);
|
||||
pub fn clear_live_log_buffer(instance_id: &str) {
|
||||
crate::state::remove_log_buffer(instance_id);
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_latest_log_cursor(
|
||||
profile_path: &str,
|
||||
instance_id: &str,
|
||||
cursor: u64, // 0 to start at beginning of file
|
||||
) -> crate::Result<LatestLogCursor> {
|
||||
get_generic_live_log_cursor(profile_path, "launcher_log.txt", cursor).await
|
||||
get_generic_live_log_cursor(instance_id, "launcher_log.txt", cursor).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_generic_live_log_cursor(
|
||||
profile_path_id: &str,
|
||||
instance_id: &str,
|
||||
log_file_name: &str,
|
||||
mut cursor: u64, // 0 to start at beginning of file
|
||||
) -> crate::Result<LatestLogCursor> {
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_path_id);
|
||||
let instance_path = resolve_instance_path(instance_id, &state).await?;
|
||||
let logs_folder = state.directories.instance_logs_dir(&instance_path);
|
||||
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)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
pub mod cache;
|
||||
pub mod friends;
|
||||
pub mod handler;
|
||||
pub mod instance;
|
||||
pub mod jre;
|
||||
pub mod logs;
|
||||
pub mod metadata;
|
||||
@@ -10,7 +11,6 @@ pub mod minecraft_skins;
|
||||
pub mod mr_auth;
|
||||
pub mod pack;
|
||||
pub mod process;
|
||||
pub mod profile;
|
||||
pub mod server_address;
|
||||
pub mod settings;
|
||||
pub mod tags;
|
||||
@@ -18,15 +18,21 @@ pub mod worlds;
|
||||
|
||||
pub mod data {
|
||||
pub use crate::state::{
|
||||
CacheBehaviour, CacheValueType, ContentItem, ContentItemOwner,
|
||||
ContentItemProject, ContentItemVersion, Credentials, Dependency,
|
||||
DirectoryInfo, Hooks, JavaVersion, LinkedData, LinkedModpackInfo,
|
||||
MemorySettings, ModLoader, ModrinthCredentials, Organization,
|
||||
OwnerType, ProcessMetadata, ProfileFile, Project, ProjectType,
|
||||
ProjectV3, SearchResult, SearchResults, SearchResultsV3, Settings,
|
||||
TeamMember, Theme, User, UserFriend, Version, WindowSize,
|
||||
AppliedContentSetPatch, CacheBehaviour, CacheValueType, ContentFile,
|
||||
ContentItem, ContentItemOwner, ContentItemProject, ContentItemVersion,
|
||||
CreateInstance, Credentials, Dependency, DirectoryInfo, EditInstance,
|
||||
Hooks, InstanceInstallCandidate, InstanceInstallTarget,
|
||||
InstanceLaunchOverridesPatch, InstanceLink, InstanceMetadata,
|
||||
JavaVersion, LinkedModpackInfo, MemorySettings, ModLoader,
|
||||
ModrinthCredentials, Organization, OwnerType, ProcessMetadata, Project,
|
||||
ProjectType, ProjectV3, SearchResult, SearchResults, SearchResultsV3,
|
||||
Settings, TeamMember, Theme, User, UserFriend, Version, WindowSize,
|
||||
};
|
||||
pub use ariadne::users::UserStatus;
|
||||
pub use modrinth_content_management::{
|
||||
ContentType, ResolutionPreferences, ResolveContentPlan,
|
||||
ResolveContentRequest,
|
||||
};
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
@@ -34,10 +40,9 @@ pub mod prelude {
|
||||
State,
|
||||
data::*,
|
||||
event::CommandPayload,
|
||||
jre, metadata, minecraft_auth, mr_auth, pack, process,
|
||||
profile::{self, Profile, create},
|
||||
settings,
|
||||
state::ReleaseChannel,
|
||||
install, instance, jre, metadata, minecraft_auth, mr_auth, pack,
|
||||
process, settings,
|
||||
state::{ReleaseChannel, db_backup::app_db_backup_dir},
|
||||
util::{
|
||||
io::{IOError, canonicalize},
|
||||
network::{is_network_metered, tcp_listen_any_loopback},
|
||||
|
||||
@@ -4,13 +4,17 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
State,
|
||||
install::{InstallPhaseDetails, InstallProgressReporter},
|
||||
pack::{
|
||||
self,
|
||||
import::{self, copy_dotminecraft},
|
||||
import::{self, finish_import},
|
||||
install_from::CreatePackDescription,
|
||||
},
|
||||
prelude::ModLoader,
|
||||
state::{LinkedData, ProfileInstallStage},
|
||||
state::{
|
||||
AppliedContentSetPatch, EditInstance, InstanceInstallStage,
|
||||
InstanceLink,
|
||||
},
|
||||
util::io,
|
||||
};
|
||||
|
||||
@@ -123,7 +127,9 @@ pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
|
||||
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: &str, // path to profile
|
||||
instance_id: &str,
|
||||
reporter: InstallProgressReporter,
|
||||
details: InstallPhaseDetails,
|
||||
) -> crate::Result<()> {
|
||||
let atlauncher_instance_path = atlauncher_base_path
|
||||
.join("instances")
|
||||
@@ -164,30 +170,34 @@ pub async fn import_atlauncher(
|
||||
override_title: Some(atinstance.launcher.name.clone()),
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
existing_loading_bar: None,
|
||||
profile_path: profile_path.to_string(),
|
||||
instance_id: instance_id.to_string(),
|
||||
source_filename: None,
|
||||
};
|
||||
|
||||
let backup_name = format!("ATLauncher-{instance_folder}");
|
||||
let minecraft_folder = atlauncher_instance_path;
|
||||
|
||||
import_atlauncher_unmanaged(
|
||||
profile_path,
|
||||
instance_id,
|
||||
minecraft_folder,
|
||||
backup_name,
|
||||
description,
|
||||
atinstance,
|
||||
reporter,
|
||||
details,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_atlauncher_unmanaged(
|
||||
profile_path: &str,
|
||||
instance_id: &str,
|
||||
minecraft_folder: PathBuf,
|
||||
backup_name: String,
|
||||
description: CreatePackDescription,
|
||||
atinstance: ATInstance,
|
||||
reporter: InstallProgressReporter,
|
||||
details: InstallPhaseDetails,
|
||||
) -> crate::Result<()> {
|
||||
let mod_loader = format!(
|
||||
"\"{}\"",
|
||||
@@ -213,53 +223,53 @@ async fn import_atlauncher_unmanaged(
|
||||
None
|
||||
};
|
||||
|
||||
// Set profile data to created default profile
|
||||
crate::api::profile::edit(profile_path, |prof| {
|
||||
prof.name = description
|
||||
.override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
|
||||
if let Some(ref project_id) = description.project_id
|
||||
&& let Some(ref version_id) = description.version_id
|
||||
{
|
||||
prof.linked_data = Some(LinkedData {
|
||||
let link = match (&description.project_id, &description.version_id) {
|
||||
(Some(project_id), Some(version_id)) => {
|
||||
Some(InstanceLink::ModrinthModpack {
|
||||
project_id: project_id.clone(),
|
||||
version_id: version_id.clone(),
|
||||
locked: true,
|
||||
})
|
||||
}
|
||||
|
||||
prof.icon_path = description
|
||||
.icon
|
||||
.clone()
|
||||
.map(|x| x.to_string_lossy().to_string());
|
||||
prof.game_version.clone_from(&game_version);
|
||||
prof.loader_version = loader_version.clone().map(|x| x.id);
|
||||
prof.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
_ => None,
|
||||
};
|
||||
crate::api::instance::edit(
|
||||
instance_id,
|
||||
EditInstance {
|
||||
install_stage: Some(InstanceInstallStage::PackInstalling),
|
||||
name: Some(
|
||||
description
|
||||
.override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string()),
|
||||
),
|
||||
icon_path: Some(
|
||||
description
|
||||
.icon
|
||||
.clone()
|
||||
.map(|x| x.to_string_lossy().to_string()),
|
||||
),
|
||||
link,
|
||||
content_set_patch: Some(AppliedContentSetPatch {
|
||||
source_kind: None,
|
||||
game_version: Some(game_version.clone()),
|
||||
protocol_version: Some(None),
|
||||
loader: Some(mod_loader),
|
||||
loader_version: Some(loader_version.clone().map(|x| x.id)),
|
||||
}),
|
||||
..EditInstance::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path,
|
||||
finish_import(
|
||||
instance_id,
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
reporter,
|
||||
details,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) = crate::api::profile::get(profile_path).await? {
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
State,
|
||||
install::{InstallPhaseDetails, InstallProgressReporter},
|
||||
prelude::ModLoader,
|
||||
state::ProfileInstallStage,
|
||||
state::{AppliedContentSetPatch, EditInstance, InstanceInstallStage},
|
||||
util::{
|
||||
fetch::{fetch, write_cached_icon},
|
||||
io,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{copy_dotminecraft, recache_icon};
|
||||
use super::{finish_import, recache_icon};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -49,7 +50,9 @@ pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
|
||||
|
||||
pub async fn import_curseforge(
|
||||
curseforge_instance_folder: PathBuf, // instance's folder
|
||||
profile_path: &str, // path to profile
|
||||
instance_id: &str,
|
||||
reporter: InstallProgressReporter,
|
||||
details: InstallPhaseDetails,
|
||||
) -> crate::Result<()> {
|
||||
// Load minecraftinstance.json
|
||||
let minecraft_instance = serde_json::from_str::<MinecraftInstance>(
|
||||
@@ -134,57 +137,64 @@ pub async fn import_curseforge(
|
||||
None
|
||||
};
|
||||
|
||||
// Set profile data to created default profile
|
||||
crate::api::profile::edit(profile_path, |prof| {
|
||||
prof.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
prof.icon_path =
|
||||
icon.clone().map(|x| x.to_string_lossy().to_string());
|
||||
prof.game_version.clone_from(&game_version);
|
||||
prof.loader_version = loader_version.clone().map(|x| x.id);
|
||||
prof.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
crate::api::instance::edit(
|
||||
instance_id,
|
||||
EditInstance {
|
||||
install_stage: Some(InstanceInstallStage::PackInstalling),
|
||||
name: Some(
|
||||
override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string()),
|
||||
),
|
||||
icon_path: Some(
|
||||
icon.clone().map(|x| x.to_string_lossy().to_string()),
|
||||
),
|
||||
content_set_patch: Some(AppliedContentSetPatch {
|
||||
source_kind: None,
|
||||
game_version: Some(game_version.clone()),
|
||||
protocol_version: Some(None),
|
||||
loader: Some(mod_loader),
|
||||
loader_version: Some(loader_version.clone().map(|x| x.id)),
|
||||
}),
|
||||
..EditInstance::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
// create a vanilla profile
|
||||
crate::api::profile::edit(profile_path, |prof| {
|
||||
prof.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.icon_path =
|
||||
icon.clone().map(|x| x.to_string_lossy().to_string());
|
||||
prof.game_version
|
||||
.clone_from(&minecraft_instance.game_version);
|
||||
prof.loader_version = None;
|
||||
prof.loader = ModLoader::Vanilla;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
crate::api::instance::edit(
|
||||
instance_id,
|
||||
EditInstance {
|
||||
name: Some(
|
||||
override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string()),
|
||||
),
|
||||
icon_path: Some(
|
||||
icon.clone().map(|x| x.to_string_lossy().to_string()),
|
||||
),
|
||||
content_set_patch: Some(AppliedContentSetPatch {
|
||||
source_kind: None,
|
||||
game_version: Some(minecraft_instance.game_version.clone()),
|
||||
protocol_version: Some(None),
|
||||
loader: Some(ModLoader::Vanilla),
|
||||
loader_version: Some(None),
|
||||
}),
|
||||
..EditInstance::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Copy in contained folders as overrides
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path,
|
||||
finish_import(
|
||||
instance_id,
|
||||
curseforge_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
reporter,
|
||||
details,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) = crate::api::profile::get(profile_path).await? {
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,9 +2,15 @@ use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{State, prelude::ModLoader, state::ProfileInstallStage, util::io};
|
||||
use crate::{
|
||||
State,
|
||||
install::{InstallPhaseDetails, InstallProgressReporter},
|
||||
prelude::ModLoader,
|
||||
state::{AppliedContentSetPatch, EditInstance, InstanceInstallStage},
|
||||
util::io,
|
||||
};
|
||||
|
||||
use super::{copy_dotminecraft, recache_icon};
|
||||
use super::{finish_import, recache_icon};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -36,7 +42,9 @@ pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
|
||||
|
||||
pub async fn import_gdlauncher(
|
||||
gdlauncher_instance_folder: PathBuf, // instance's folder
|
||||
profile_path: &str, // path to profile
|
||||
instance_id: &str,
|
||||
reporter: InstallProgressReporter,
|
||||
details: InstallPhaseDetails,
|
||||
) -> crate::Result<()> {
|
||||
// Load config.json
|
||||
let config = serde_json::from_str::<GDLauncherConfig>(
|
||||
@@ -81,39 +89,40 @@ pub async fn import_gdlauncher(
|
||||
None
|
||||
};
|
||||
|
||||
// Set profile data to created default profile
|
||||
crate::api::profile::edit(profile_path, |prof| {
|
||||
prof.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
prof.icon_path = icon.clone().map(|x| x.to_string_lossy().to_string());
|
||||
prof.game_version.clone_from(&game_version);
|
||||
prof.loader_version = loader_version.clone().map(|x| x.id);
|
||||
prof.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
crate::api::instance::edit(
|
||||
instance_id,
|
||||
EditInstance {
|
||||
install_stage: Some(InstanceInstallStage::PackInstalling),
|
||||
name: Some(
|
||||
override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string()),
|
||||
),
|
||||
icon_path: Some(
|
||||
icon.clone().map(|x| x.to_string_lossy().to_string()),
|
||||
),
|
||||
content_set_patch: Some(AppliedContentSetPatch {
|
||||
source_kind: None,
|
||||
game_version: Some(game_version.clone()),
|
||||
protocol_version: Some(None),
|
||||
loader: Some(mod_loader),
|
||||
loader_version: Some(loader_version.clone().map(|x| x.id)),
|
||||
}),
|
||||
..EditInstance::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Copy in contained folders as overrides
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path,
|
||||
finish_import(
|
||||
instance_id,
|
||||
gdlauncher_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
reporter,
|
||||
details,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) = crate::api::profile::get(profile_path).await? {
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ use serde::{Deserialize, Serialize, de};
|
||||
|
||||
use crate::{
|
||||
State,
|
||||
install::{InstallPhaseDetails, InstallProgressReporter},
|
||||
pack::{
|
||||
import::{self, copy_dotminecraft},
|
||||
import::{self, finish_import},
|
||||
install_from::{self, CreatePackDescription, PackDependency},
|
||||
},
|
||||
util::io,
|
||||
@@ -178,7 +179,9 @@ async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
|
||||
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: &str, // path to profile
|
||||
instance_id: &str,
|
||||
reporter: InstallProgressReporter,
|
||||
details: InstallPhaseDetails,
|
||||
) -> crate::Result<()> {
|
||||
let mmc_instance_path =
|
||||
mmc_base_path.join("instances").join(instance_folder);
|
||||
@@ -208,8 +211,8 @@ pub async fn import_mmc(
|
||||
override_title: instance_cfg.name,
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
existing_loading_bar: None,
|
||||
profile_path: profile_path.to_string(),
|
||||
instance_id: instance_id.to_string(),
|
||||
source_filename: None,
|
||||
};
|
||||
|
||||
let mut minecraft_folder = mmc_instance_path.join("minecraft");
|
||||
@@ -232,28 +235,30 @@ pub async fn import_mmc(
|
||||
|
||||
// Modrinth Managed Pack
|
||||
// Kept separate as we may in the future want to add special handling for modrinth managed packs
|
||||
import_mmc_unmanaged(profile_path, minecraft_folder, "Imported Modrinth Modpack".to_string(), description, mmc_pack).await?;
|
||||
import_mmc_unmanaged(instance_id, minecraft_folder, "Imported Modrinth Modpack".to_string(), description, mmc_pack, reporter, details).await?;
|
||||
}
|
||||
Some(MMCManagedPackType::Flame | MMCManagedPackType::ATLauncher) => {
|
||||
// For flame/atlauncher managed packs
|
||||
// Treat as unmanaged, but with 'minecraft' folder instead of '.minecraft'
|
||||
import_mmc_unmanaged(profile_path, minecraft_folder, "Imported Modpack".to_string(), description, mmc_pack).await?;
|
||||
import_mmc_unmanaged(instance_id, minecraft_folder, "Imported Modpack".to_string(), description, mmc_pack, reporter, details).await?;
|
||||
},
|
||||
Some(_) => {
|
||||
// For managed packs that aren't modrinth, flame, atlauncher
|
||||
// Treat as unmanaged
|
||||
import_mmc_unmanaged(profile_path, minecraft_folder, "ImportedModpack".to_string(), description, mmc_pack).await?;
|
||||
import_mmc_unmanaged(instance_id, minecraft_folder, "ImportedModpack".to_string(), description, mmc_pack, reporter, details).await?;
|
||||
},
|
||||
_ => return Err(crate::ErrorKind::InputError("Instance is managed, but managed pack type not specified in instance.cfg".to_string()).into())
|
||||
}
|
||||
} else {
|
||||
// Directly import unmanaged pack
|
||||
import_mmc_unmanaged(
|
||||
profile_path,
|
||||
instance_id,
|
||||
minecraft_folder,
|
||||
"Imported Modpack".to_string(),
|
||||
description,
|
||||
mmc_pack,
|
||||
reporter,
|
||||
details,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -261,11 +266,13 @@ pub async fn import_mmc(
|
||||
}
|
||||
|
||||
async fn import_mmc_unmanaged(
|
||||
profile_path: &str,
|
||||
instance_id: &str,
|
||||
minecraft_folder: PathBuf,
|
||||
backup_name: String,
|
||||
description: CreatePackDescription,
|
||||
mmc_pack: MMCPack,
|
||||
reporter: InstallProgressReporter,
|
||||
details: InstallPhaseDetails,
|
||||
) -> crate::Result<()> {
|
||||
// Pack dependencies stored in mmc-pack.json, we convert to .mrpack pack dependencies
|
||||
let dependencies = mmc_pack
|
||||
@@ -307,11 +314,11 @@ async fn import_mmc_unmanaged(
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sets profile information to be that loaded from mmc-pack.json and instance.cfg
|
||||
install_from::set_profile_information(
|
||||
profile_path.to_string(),
|
||||
install_from::set_instance_information(
|
||||
instance_id.to_string(),
|
||||
&description,
|
||||
&backup_name,
|
||||
None,
|
||||
&dependencies,
|
||||
false,
|
||||
)
|
||||
@@ -319,21 +326,13 @@ async fn import_mmc_unmanaged(
|
||||
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path,
|
||||
finish_import(
|
||||
instance_id,
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
reporter,
|
||||
details,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) = crate::api::profile::get(profile_path).await? {
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ use io::IOError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
event::{
|
||||
LoadingBarId,
|
||||
emit::{emit_loading, init_or_edit_loading},
|
||||
install::{
|
||||
InstallPhaseDetails, InstallPhaseId, InstallProgress,
|
||||
InstallProgressReporter,
|
||||
},
|
||||
util::{
|
||||
fetch::{self, IoSemaphore},
|
||||
@@ -114,23 +114,43 @@ pub async fn get_importable_instances(
|
||||
Ok(instances)
|
||||
}
|
||||
|
||||
// Import an instance from a launcher type and base path
|
||||
// Note: this *deletes* the submitted empty profile
|
||||
|
||||
// #[tracing::instrument]
|
||||
pub async fn import_instance(
|
||||
profile_path: &str, // This should be a blank profile
|
||||
pub(crate) async fn import_instance_with_reporter(
|
||||
instance_id: &str,
|
||||
launcher_type: ImportLauncherType,
|
||||
base_path: PathBuf,
|
||||
instance_folder: String,
|
||||
reporter: InstallProgressReporter,
|
||||
) -> crate::Result<()> {
|
||||
import_instance_inner(
|
||||
instance_id,
|
||||
launcher_type,
|
||||
base_path,
|
||||
instance_folder,
|
||||
reporter,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn import_instance_inner(
|
||||
instance_id: &str,
|
||||
launcher_type: ImportLauncherType,
|
||||
base_path: PathBuf,
|
||||
instance_folder: String,
|
||||
reporter: InstallProgressReporter,
|
||||
) -> crate::Result<()> {
|
||||
tracing::debug!("Importing instance from {instance_folder}");
|
||||
let details = InstallPhaseDetails::Import {
|
||||
launcher_type,
|
||||
instance_folder: instance_folder.clone(),
|
||||
};
|
||||
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, // path to profile
|
||||
instance_id,
|
||||
reporter.clone(),
|
||||
details.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -138,21 +158,27 @@ pub async fn import_instance(
|
||||
atlauncher::import_atlauncher(
|
||||
base_path, // path to atlauncher folder
|
||||
instance_folder, // instance folder in atlauncher
|
||||
profile_path, // path to profile
|
||||
instance_id,
|
||||
reporter.clone(),
|
||||
details.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
ImportLauncherType::GDLauncher => {
|
||||
gdlauncher::import_gdlauncher(
|
||||
base_path.join("instances").join(instance_folder), // path to gdlauncher folder
|
||||
profile_path, // path to profile
|
||||
instance_id,
|
||||
reporter.clone(),
|
||||
details.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
ImportLauncherType::Curseforge => {
|
||||
curseforge::import_curseforge(
|
||||
base_path.join("Instances").join(instance_folder), // path to curseforge folder
|
||||
profile_path, // path to profile
|
||||
instance_id,
|
||||
reporter.clone(),
|
||||
details.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -172,11 +198,12 @@ pub async fn import_instance(
|
||||
&& instances.contains(&instance_folder)
|
||||
{
|
||||
matched = true;
|
||||
Box::pin(import_instance(
|
||||
profile_path,
|
||||
Box::pin(import_instance_inner(
|
||||
instance_id,
|
||||
lt,
|
||||
base_path,
|
||||
instance_folder,
|
||||
reporter.clone(),
|
||||
))
|
||||
.await?;
|
||||
break;
|
||||
@@ -198,7 +225,7 @@ pub async fn import_instance(
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
tracing::warn!("Import failed: {:?}", e);
|
||||
let _ = crate::api::profile::remove(profile_path).await;
|
||||
let _ = crate::api::instance::remove(instance_id).await;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
@@ -339,33 +366,19 @@ pub async fn recache_icon(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn copy_dotminecraft(
|
||||
profile_path_id: &str,
|
||||
pub(crate) async fn copy_dotminecraft_with_reporter(
|
||||
instance_id: &str,
|
||||
dotminecraft: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
existing_loading_bar: Option<LoadingBarId>,
|
||||
) -> crate::Result<LoadingBarId> {
|
||||
// Get full path to profile
|
||||
let profile_path =
|
||||
crate::api::profile::get_full_path(profile_path_id).await?;
|
||||
|
||||
// Gets all subfiles recursively in src
|
||||
reporter: InstallProgressReporter,
|
||||
details: InstallPhaseDetails,
|
||||
) -> crate::Result<()> {
|
||||
let instance_path =
|
||||
crate::api::instance::get_full_path(instance_id).await?;
|
||||
let subfiles = get_all_subfiles(&dotminecraft, false).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 {
|
||||
for (index, src_child) in subfiles.into_iter().enumerate() {
|
||||
let dst_child =
|
||||
src_child.strip_prefix(&dotminecraft).map_err(|_| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
@@ -373,16 +386,51 @@ pub async fn copy_dotminecraft(
|
||||
&src_child.display()
|
||||
))
|
||||
})?;
|
||||
let dst_child = profile_path.join(dst_child);
|
||||
let dst_child = instance_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)?;
|
||||
reporter
|
||||
.update(
|
||||
InstallPhaseId::PreparingInstance,
|
||||
Some(InstallProgress {
|
||||
current: (index + 1) as u64,
|
||||
total: total_subfiles,
|
||||
secondary: None,
|
||||
}),
|
||||
details.clone(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(loading_bar)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn finish_import(
|
||||
instance_id: &str,
|
||||
dotminecraft: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
reporter: InstallProgressReporter,
|
||||
details: InstallPhaseDetails,
|
||||
) -> crate::Result<()> {
|
||||
copy_dotminecraft_with_reporter(
|
||||
instance_id,
|
||||
dotminecraft,
|
||||
io_semaphore,
|
||||
reporter.clone(),
|
||||
details,
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::launcher::install_minecraft_for_instance_id_with_reporter(
|
||||
instance_id,
|
||||
false,
|
||||
Some(reporter),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recursively get a list of all subfiles in src
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
use crate::State;
|
||||
use crate::api::profile;
|
||||
use crate::data::ModLoader;
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::event::{LoadingBarId, LoadingBarType};
|
||||
use crate::install::{
|
||||
InstallPhaseDetails, InstallPhaseId, InstallProgress,
|
||||
InstallProgressReporter,
|
||||
};
|
||||
use crate::state::{
|
||||
CacheBehaviour, CachedEntry, LinkedData, Profile, ProfileInstallStage,
|
||||
SideType,
|
||||
AppliedContentSetPatch, CacheBehaviour, CachedEntry, ContentSourceKind,
|
||||
EditInstance, InstanceInstallStage, InstanceLink, SideType,
|
||||
};
|
||||
use crate::util::fetch::{
|
||||
DownloadMeta, DownloadReason, fetch, fetch_advanced, sha1_file_async,
|
||||
write_cached_icon,
|
||||
DownloadMeta, DownloadReason, FetchProgressFn, fetch,
|
||||
fetch_advanced_with_progress, sha1_file_async, write_cached_icon,
|
||||
};
|
||||
use path_util::SafeRelativeUtf8UnixPathBuf;
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -102,30 +105,30 @@ pub enum CreatePackLocation {
|
||||
|
||||
#[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 struct CreatePackInstance {
|
||||
pub name: String, // the name of the instance and relative path
|
||||
pub game_version: String, // the game version of the instance
|
||||
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 icon: Option<PathBuf>, // the icon for the instance
|
||||
pub icon_url: Option<String>, // the URL icon for an instance during import
|
||||
pub link: Option<InstanceLink>,
|
||||
pub unknown_file: bool, // true when pack file isn't found on Modrinth via hash lookup
|
||||
pub skip_install_profile: Option<bool>,
|
||||
pub no_watch: Option<bool>,
|
||||
}
|
||||
|
||||
// default
|
||||
impl Default for CreatePackProfile {
|
||||
impl Default for CreatePackInstance {
|
||||
fn default() -> Self {
|
||||
CreatePackProfile {
|
||||
CreatePackInstance {
|
||||
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,
|
||||
link: None,
|
||||
unknown_file: false,
|
||||
skip_install_profile: Some(true),
|
||||
no_watch: Some(false),
|
||||
@@ -155,26 +158,25 @@ pub struct CreatePackDescription {
|
||||
pub override_title: Option<String>,
|
||||
pub project_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
pub existing_loading_bar: Option<LoadingBarId>,
|
||||
pub profile_path: String,
|
||||
pub instance_id: String,
|
||||
pub source_filename: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_profile_from_pack(
|
||||
pub async fn get_instance_from_pack(
|
||||
location: CreatePackLocation,
|
||||
) -> crate::Result<CreatePackProfile> {
|
||||
) -> crate::Result<CreatePackInstance> {
|
||||
match location {
|
||||
CreatePackLocation::FromVersionId {
|
||||
project_id,
|
||||
version_id,
|
||||
title,
|
||||
icon_url,
|
||||
} => Ok(CreatePackProfile {
|
||||
} => Ok(CreatePackInstance {
|
||||
name: title,
|
||||
icon_url,
|
||||
linked_data: Some(LinkedData {
|
||||
link: Some(InstanceLink::ModrinthModpack {
|
||||
project_id,
|
||||
version_id,
|
||||
locked: true,
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -212,7 +214,7 @@ pub async fn get_profile_from_pack(
|
||||
false
|
||||
};
|
||||
|
||||
Ok(CreatePackProfile {
|
||||
Ok(CreatePackInstance {
|
||||
name: file_name,
|
||||
unknown_file: !is_known_file,
|
||||
..Default::default()
|
||||
@@ -221,39 +223,20 @@ pub async fn get_profile_from_pack(
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
|
||||
pub async fn generate_pack_from_version_id(
|
||||
#[tracing::instrument(skip(reporter))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn generate_pack_from_version_id_with_reporter(
|
||||
project_id: String,
|
||||
version_id: String,
|
||||
title: String,
|
||||
icon_url: Option<String>,
|
||||
profile_path: String,
|
||||
|
||||
initialized_loading_bar: Option<LoadingBarId>,
|
||||
instance_id: String,
|
||||
reason: DownloadReason,
|
||||
reporter: InstallProgressReporter,
|
||||
) -> crate::Result<CreatePack> {
|
||||
let state = State::get().await?;
|
||||
let has_icon_url = icon_url.is_some();
|
||||
|
||||
let loading_bar = if let Some(bar) = initialized_loading_bar {
|
||||
emit_loading(&bar, 0.0, Some("Downloading pack file"))?;
|
||||
bar
|
||||
} else {
|
||||
init_loading(
|
||||
LoadingBarType::PackFileDownload {
|
||||
profile_path: profile_path.clone(),
|
||||
pack_name: title.clone(),
|
||||
icon: icon_url,
|
||||
pack_version: version_id.clone(),
|
||||
},
|
||||
100.0,
|
||||
"Downloading pack file",
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Fetching version"))?;
|
||||
let version = CachedEntry::get_version(
|
||||
&version_id,
|
||||
Some(CacheBehaviour::Bypass),
|
||||
@@ -266,9 +249,8 @@ pub async fn generate_pack_from_version_id(
|
||||
"Invalid version ID specified!".to_string(),
|
||||
)
|
||||
})?;
|
||||
emit_loading(&loading_bar, 10.0, None)?;
|
||||
|
||||
// Update profile with correct loader and game version from the API version metadata,
|
||||
// Update instance with correct loader and game version from the API version metadata,
|
||||
// so the UI shows accurate info while the pack file is still downloading.
|
||||
if let Some(game_version) = version.game_versions.first() {
|
||||
let loader = version
|
||||
@@ -277,12 +259,19 @@ pub async fn generate_pack_from_version_id(
|
||||
.map(|l| ModLoader::from_string(l))
|
||||
.unwrap_or(ModLoader::Vanilla);
|
||||
let game_version = game_version.clone();
|
||||
let profile_path_clone = profile_path.clone();
|
||||
profile::edit(&profile_path_clone, |prof| {
|
||||
prof.game_version.clone_from(&game_version);
|
||||
prof.loader = loader;
|
||||
async { Ok(()) }
|
||||
})
|
||||
crate::api::instance::edit(
|
||||
&instance_id,
|
||||
EditInstance {
|
||||
content_set_patch: Some(AppliedContentSetPatch {
|
||||
source_kind: None,
|
||||
game_version: Some(game_version),
|
||||
protocol_version: Some(None),
|
||||
loader: Some(loader),
|
||||
loader_version: None,
|
||||
}),
|
||||
..EditInstance::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -301,37 +290,73 @@ pub async fn generate_pack_from_version_id(
|
||||
)
|
||||
})?;
|
||||
|
||||
let profile =
|
||||
Profile::get(&profile_path, &state.pool)
|
||||
let metadata =
|
||||
crate::api::instance::get(&instance_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_path.to_string(),
|
||||
)
|
||||
.as_error()
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unknown instance {instance_id}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let download_meta = DownloadMeta {
|
||||
reason,
|
||||
game_version: profile.game_version.clone(),
|
||||
loader: profile.loader.as_str().to_string(),
|
||||
game_version: metadata.applied_content_set.game_version.clone(),
|
||||
loader: metadata.applied_content_set.loader.as_str().to_string(),
|
||||
dependent_on: Some(version_id.clone()),
|
||||
};
|
||||
|
||||
let file = fetch_advanced(
|
||||
let details = InstallPhaseDetails::Modpack {
|
||||
project_id: Some(project_id.clone()),
|
||||
version_id: Some(version_id.clone()),
|
||||
title: Some(title.clone()),
|
||||
};
|
||||
let mut last_reported_bytes = 0_u64;
|
||||
let mut progress =
|
||||
|current: u64,
|
||||
total: u64|
|
||||
-> Pin<Box<dyn Future<Output = crate::Result<()>> + Send>> {
|
||||
let min_delta = (total / 200).max(256 * 1024);
|
||||
if current < total
|
||||
&& current.saturating_sub(last_reported_bytes) < min_delta
|
||||
{
|
||||
return Box::pin(async { Ok(()) });
|
||||
}
|
||||
|
||||
last_reported_bytes = current;
|
||||
let reporter = reporter.clone();
|
||||
let details = details.clone();
|
||||
Box::pin(async move {
|
||||
reporter
|
||||
.update(
|
||||
InstallPhaseId::DownloadingPackFile,
|
||||
Some(InstallProgress {
|
||||
current,
|
||||
total,
|
||||
secondary: None,
|
||||
}),
|
||||
details,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
let progress = Some(&mut progress as &mut FetchProgressFn<'_>);
|
||||
|
||||
let file = fetch_advanced_with_progress(
|
||||
Method::GET,
|
||||
&url,
|
||||
hash.map(|x| &**x),
|
||||
None,
|
||||
None,
|
||||
Some(&download_meta),
|
||||
Some((&loading_bar, 70.0)),
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&state.pool,
|
||||
progress,
|
||||
)
|
||||
.await?;
|
||||
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata"))?;
|
||||
|
||||
let project = CachedEntry::get_project(
|
||||
&version.project_id,
|
||||
@@ -350,8 +375,7 @@ pub async fn generate_pack_from_version_id(
|
||||
// When installing to an existing profile (e.g. server projects),
|
||||
// icon_url is None and we preserve the profile's existing icon.
|
||||
let icon = if has_icon_url {
|
||||
emit_loading(&loading_bar, 10.0, Some("Retrieving icon"))?;
|
||||
let fetched = if let Some(icon_url) = project.icon_url {
|
||||
if let Some(icon_url) = project.icon_url {
|
||||
let state = State::get().await?;
|
||||
let icon_bytes = fetch(
|
||||
&icon_url,
|
||||
@@ -380,18 +404,18 @@ pub async fn generate_pack_from_version_id(
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
emit_loading(&loading_bar, 10.0, None)?;
|
||||
fetched
|
||||
}
|
||||
} else {
|
||||
emit_loading(&loading_bar, 20.0, None)?;
|
||||
None
|
||||
};
|
||||
|
||||
// Set the icon immediately so the UI shows it during download.
|
||||
if let Some(ref icon_path) = icon {
|
||||
let _ =
|
||||
profile::edit_icon(&profile_path, Some(icon_path.as_path())).await;
|
||||
let _ = crate::api::instance::edit_icon(
|
||||
&instance_id,
|
||||
Some(icon_path.as_path()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(CreatePack {
|
||||
@@ -401,8 +425,8 @@ pub async fn generate_pack_from_version_id(
|
||||
override_title: Some(title),
|
||||
project_id: Some(project_id),
|
||||
version_id: Some(version_id),
|
||||
existing_loading_bar: Some(loading_bar),
|
||||
profile_path,
|
||||
instance_id,
|
||||
source_filename: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -411,8 +435,11 @@ pub async fn generate_pack_from_version_id(
|
||||
|
||||
pub async fn generate_pack_from_file(
|
||||
path: PathBuf,
|
||||
profile_path: String,
|
||||
instance_id: String,
|
||||
) -> crate::Result<CreatePack> {
|
||||
let source_filename =
|
||||
path.file_name().map(|x| x.to_string_lossy().to_string());
|
||||
|
||||
Ok(CreatePack {
|
||||
file: CreatePackFile::Path(path),
|
||||
description: CreatePackDescription {
|
||||
@@ -420,20 +447,21 @@ pub async fn generate_pack_from_file(
|
||||
override_title: None,
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
existing_loading_bar: None,
|
||||
profile_path,
|
||||
instance_id,
|
||||
source_filename,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets generated profile attributes to the pack ones (using profile::edit)
|
||||
/// Sets generated instance attributes to the pack ones.
|
||||
/// This includes the pack name, icon, game version, loader version, and loader
|
||||
pub async fn set_profile_information(
|
||||
profile_path: String,
|
||||
pub async fn set_instance_information(
|
||||
instance_id: String,
|
||||
description: &CreatePackDescription,
|
||||
backup_name: &str,
|
||||
pack_version_id: Option<&str>,
|
||||
dependencies: &HashMap<PackDependency, String>,
|
||||
ignore_lock: bool, // do not change locked status
|
||||
_ignore_lock: bool,
|
||||
) -> crate::Result<()> {
|
||||
let mut game_version: Option<&String> = None;
|
||||
let mut mod_loader = None;
|
||||
@@ -479,40 +507,58 @@ pub async fn set_profile_information(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Sets values in profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.name = description
|
||||
.override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
|
||||
if let Some(ref project_id) = description.project_id
|
||||
&& let Some(ref version_id) = description.version_id
|
||||
{
|
||||
prof.linked_data = Some(LinkedData {
|
||||
let link = match (&description.project_id, &description.version_id) {
|
||||
(Some(project_id), Some(version_id)) => {
|
||||
Some(InstanceLink::ModrinthModpack {
|
||||
project_id: project_id.clone(),
|
||||
version_id: version_id.clone(),
|
||||
locked: if !ignore_lock {
|
||||
true
|
||||
} else {
|
||||
prof.linked_data.as_ref().is_none_or(|x| x.locked)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Only update the icon if the pack provides one.
|
||||
// When installing to an existing profile, icon is None
|
||||
// and we preserve the profile's existing icon.
|
||||
if let Some(ref icon) = description.icon {
|
||||
prof.icon_path = Some(icon.to_string_lossy().to_string());
|
||||
_ if description.source_filename.is_some() => {
|
||||
Some(InstanceLink::ImportedModpack {
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
name: Some(backup_name.to_string()),
|
||||
version_number: pack_version_id.map(ToString::to_string),
|
||||
filename: description.source_filename.clone(),
|
||||
})
|
||||
}
|
||||
prof.game_version.clone_from(game_version);
|
||||
prof.loader_version = loader_version.clone().map(|x| x.id);
|
||||
prof.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
_ => None,
|
||||
};
|
||||
let source_kind = match &link {
|
||||
Some(InstanceLink::ModrinthModpack { .. }) => {
|
||||
Some(ContentSourceKind::ModrinthModpack)
|
||||
}
|
||||
Some(InstanceLink::ImportedModpack { .. }) => {
|
||||
Some(ContentSourceKind::ImportedModpack)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
crate::api::instance::edit(
|
||||
&instance_id,
|
||||
EditInstance {
|
||||
install_stage: Some(InstanceInstallStage::PackInstalling),
|
||||
name: Some(
|
||||
description
|
||||
.override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string()),
|
||||
),
|
||||
icon_path: description
|
||||
.icon
|
||||
.as_ref()
|
||||
.map(|icon| Some(icon.to_string_lossy().to_string())),
|
||||
link,
|
||||
content_set_patch: Some(AppliedContentSetPatch {
|
||||
source_kind,
|
||||
game_version: Some(game_version.clone()),
|
||||
protocol_version: Some(None),
|
||||
loader: Some(mod_loader),
|
||||
loader_version: Some(loader_version.clone().map(|x| x.id)),
|
||||
}),
|
||||
..EditInstance::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::event::emit::{
|
||||
emit_loading, init_or_edit_loading, loading_try_for_each_concurrent,
|
||||
use crate::State;
|
||||
use crate::event::emit::loading_try_for_each_concurrent;
|
||||
use crate::install::{
|
||||
InstallPhaseDetails, InstallPhaseId, InstallProgress,
|
||||
InstallProgressReporter, InstallProgressSecondary,
|
||||
};
|
||||
use crate::pack::install_from::{
|
||||
EnvType, PackFile, PackFileHash, set_profile_information,
|
||||
EnvType, PackFile, PackFileHash, set_instance_information,
|
||||
};
|
||||
use crate::state::instances::ContentSourceKind;
|
||||
use crate::state::{
|
||||
CacheBehaviour, CachedEntry, Profile, ProfileInstallStage, SideType,
|
||||
cache_file_hash,
|
||||
CachedEntry, EditInstance, InstanceInstallStage, SideType, cache_file_hash,
|
||||
};
|
||||
use crate::util::fetch::{DownloadMeta, DownloadReason, fetch_mirrors, write};
|
||||
use crate::util::io;
|
||||
use crate::{State, profile};
|
||||
use async_zip::base::read::seek::ZipFileReader as SeekZipFileReader;
|
||||
use async_zip::base::read::{WithEntry, ZipEntryReader};
|
||||
use async_zip::tokio::read::fs::ZipFileReader as FsZipFileReader;
|
||||
use futures::StreamExt;
|
||||
use path_util::SafeRelativeUtf8UnixPathBuf;
|
||||
|
||||
use super::install_from::{
|
||||
CreatePack, CreatePackFile, CreatePackLocation, PackFormat,
|
||||
generate_pack_from_file, generate_pack_from_version_id,
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
use super::install_from::{CreatePack, CreatePackFile, PackFormat};
|
||||
use crate::data::ProjectType;
|
||||
use std::io::{Cursor, ErrorKind};
|
||||
use std::path::Path;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
type ExtractProgressFn<'a> = dyn FnMut(u64) -> Pin<Box<dyn Future<Output = crate::Result<()>> + Send + 'a>>
|
||||
+ Send
|
||||
+ 'a;
|
||||
|
||||
enum MrpackZipReader {
|
||||
Memory(async_zip::tokio::read::seek::ZipFileReader<Cursor<bytes::Bytes>>),
|
||||
// Local imports stay on disk so large .mrpacks do not have to fit in memory.
|
||||
@@ -100,6 +108,7 @@ impl MrpackZipReader {
|
||||
index: usize,
|
||||
path: &Path,
|
||||
semaphore: &crate::util::fetch::IoSemaphore,
|
||||
progress: Option<&mut ExtractProgressFn<'_>>,
|
||||
) -> crate::Result<(u64, String)> {
|
||||
match self {
|
||||
Self::Memory(reader) => {
|
||||
@@ -107,6 +116,7 @@ impl MrpackZipReader {
|
||||
reader.reader_with_entry(index).await?,
|
||||
path,
|
||||
semaphore,
|
||||
progress,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -115,6 +125,7 @@ impl MrpackZipReader {
|
||||
reader.reader_with_entry(index).await?,
|
||||
path,
|
||||
semaphore,
|
||||
progress,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -156,6 +167,7 @@ async fn extract_zip_entry<R>(
|
||||
mut reader: ZipEntryReader<'_, R, WithEntry<'_>>,
|
||||
path: &Path,
|
||||
semaphore: &crate::util::fetch::IoSemaphore,
|
||||
mut progress: Option<&mut ExtractProgressFn<'_>>,
|
||||
) -> crate::Result<(u64, String)>
|
||||
where
|
||||
R: futures_lite::io::AsyncBufRead + Unpin,
|
||||
@@ -197,6 +209,9 @@ where
|
||||
.map_err(|e| io::IOError::with_path(e, &temp_path))?;
|
||||
hasher.update(&buffer[..bytes_read]);
|
||||
size += bytes_read as u64;
|
||||
if let Some(progress) = progress.as_mut() {
|
||||
progress(bytes_read as u64).await?;
|
||||
}
|
||||
}
|
||||
|
||||
file.flush()
|
||||
@@ -216,75 +231,37 @@ where
|
||||
Ok((size, hasher.digest().to_string()))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
pub async fn install_zipped_mrpack(
|
||||
location: CreatePackLocation,
|
||||
profile_path: String,
|
||||
) -> crate::Result<String> {
|
||||
// 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,
|
||||
DownloadReason::Modpack,
|
||||
)
|
||||
.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,
|
||||
DownloadReason::Modpack,
|
||||
)
|
||||
.await;
|
||||
|
||||
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
|
||||
pub async fn install_zipped_mrpack_files(
|
||||
pub(crate) async fn install_zipped_mrpack_files_with_reporter(
|
||||
create_pack: CreatePack,
|
||||
ignore_lock: bool,
|
||||
reason: DownloadReason,
|
||||
reporter: InstallProgressReporter,
|
||||
) -> crate::Result<String> {
|
||||
let state = &State::get().await?;
|
||||
|
||||
let file = create_pack.file;
|
||||
let description = create_pack.description.clone(); // make a copy for profile edit function
|
||||
let description = create_pack.description.clone();
|
||||
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 instance_id = create_pack.description.instance_id;
|
||||
let mut icon_exists = icon.is_some();
|
||||
|
||||
let mut zip_reader = MrpackZipReader::new(&file).await?;
|
||||
let instance_full_path =
|
||||
crate::api::instance::get_full_path(&instance_id).await?;
|
||||
let modpack_details = InstallPhaseDetails::Modpack {
|
||||
project_id: project_id.clone(),
|
||||
version_id: version_id.clone(),
|
||||
title: description.override_title.clone(),
|
||||
};
|
||||
reporter
|
||||
.update(
|
||||
InstallPhaseId::ReadingPackManifest,
|
||||
None,
|
||||
modpack_details.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Extract index of modrinth.index.json
|
||||
let Some(manifest_idx) = zip_reader.file().entries().iter().position(|f| {
|
||||
@@ -299,7 +276,6 @@ pub async fn install_zipped_mrpack_files(
|
||||
manifest.push_str(&zip_reader.read_entry_to_string(manifest_idx).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(),
|
||||
@@ -307,6 +283,40 @@ pub async fn install_zipped_mrpack_files(
|
||||
.into());
|
||||
}
|
||||
|
||||
reporter
|
||||
.update(InstallPhaseId::ResolvingPack, None, modpack_details.clone())
|
||||
.await?;
|
||||
|
||||
if !icon_exists {
|
||||
let icon_entry =
|
||||
zip_reader.file().entries().iter().enumerate().find_map(
|
||||
|(index, entry)| {
|
||||
matches!(
|
||||
entry.filename().as_str(),
|
||||
Ok("icon.png"
|
||||
| "overrides/icon.png"
|
||||
| "client-overrides/icon.png")
|
||||
)
|
||||
.then_some(index)
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(icon_entry) = icon_entry {
|
||||
let icon_path = instance_full_path.join("icon.png");
|
||||
zip_reader
|
||||
.extract_entry(
|
||||
icon_entry,
|
||||
&icon_path,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
crate::api::instance::edit_icon(&instance_id, Some(&icon_path))
|
||||
.await?;
|
||||
icon_exists = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the modpack file hashes for later filtering of user-added content
|
||||
// Includes both manifest file hashes and computed hashes for override files
|
||||
if let Some(ref version_id) = version_id {
|
||||
@@ -370,66 +380,113 @@ pub async fn install_zipped_mrpack_files(
|
||||
);
|
||||
}
|
||||
|
||||
// Sets generated profile attributes to the pack ones (using profile::edit)
|
||||
set_profile_information(
|
||||
profile_path.clone(),
|
||||
set_instance_information(
|
||||
instance_id.clone(),
|
||||
&description,
|
||||
&pack.name,
|
||||
Some(&pack.version_id),
|
||||
&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.clone(),
|
||||
pack_name: pack.name.clone(),
|
||||
icon,
|
||||
pack_id: project_id.clone(),
|
||||
pack_version: version_id.clone(),
|
||||
},
|
||||
100.0,
|
||||
"Downloading modpack",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let profile =
|
||||
Profile::get(&profile_path, &state.pool)
|
||||
let metadata =
|
||||
crate::api::instance::get(&instance_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_path.to_string(),
|
||||
)
|
||||
.as_error()
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unknown instance {instance_id}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let instance_path = metadata.instance.path.clone();
|
||||
let download_meta = DownloadMeta {
|
||||
reason,
|
||||
game_version: profile.game_version.clone(),
|
||||
loader: profile.loader.as_str().to_string(),
|
||||
game_version: metadata.applied_content_set.game_version.clone(),
|
||||
loader: metadata.applied_content_set.loader.as_str().to_string(),
|
||||
dependent_on: version_id.clone(),
|
||||
};
|
||||
|
||||
let num_files = pack.files.len();
|
||||
let content_total_bytes = pack
|
||||
.files
|
||||
.iter()
|
||||
.map(|file| file.file_size as u64)
|
||||
.sum::<u64>();
|
||||
reporter
|
||||
.update(
|
||||
InstallPhaseId::DownloadingContent,
|
||||
Some(InstallProgress {
|
||||
current: 0,
|
||||
total: num_files as u64,
|
||||
secondary: (content_total_bytes > 0).then_some(
|
||||
InstallProgressSecondary {
|
||||
current: 0,
|
||||
total: content_total_bytes,
|
||||
},
|
||||
),
|
||||
}),
|
||||
modpack_details.clone(),
|
||||
)
|
||||
.await?;
|
||||
let content_progress = Arc::new(AtomicU64::new(0));
|
||||
let content_bytes_progress = Arc::new(AtomicU64::new(0));
|
||||
loading_try_for_each_concurrent(
|
||||
futures::stream::iter(pack.files).map(Ok::<PackFile, crate::Error>),
|
||||
None,
|
||||
Some(&loading_bar),
|
||||
None,
|
||||
70.0,
|
||||
num_files,
|
||||
None,
|
||||
|project| {
|
||||
let profile_path = profile_path.clone();
|
||||
let instance_id = instance_id.clone();
|
||||
let instance_path = instance_path.clone();
|
||||
let instance_full_path = instance_full_path.clone();
|
||||
let download_meta = download_meta.clone();
|
||||
let pack_version_id = version_id.clone();
|
||||
let reporter = reporter.clone();
|
||||
let modpack_details = modpack_details.clone();
|
||||
let content_progress = content_progress.clone();
|
||||
let content_bytes_progress = content_bytes_progress.clone();
|
||||
async move {
|
||||
let mark_downloaded = |file_size: u64| {
|
||||
let reporter = reporter.clone();
|
||||
let modpack_details = modpack_details.clone();
|
||||
let content_progress = content_progress.clone();
|
||||
let content_bytes_progress = content_bytes_progress.clone();
|
||||
async move {
|
||||
let current = content_progress
|
||||
.fetch_add(1, Ordering::Relaxed)
|
||||
+ 1;
|
||||
let current_bytes = content_bytes_progress
|
||||
.fetch_add(file_size, Ordering::Relaxed)
|
||||
+ file_size;
|
||||
reporter
|
||||
.update(
|
||||
InstallPhaseId::DownloadingContent,
|
||||
Some(InstallProgress {
|
||||
current,
|
||||
total: num_files as u64,
|
||||
secondary: (content_total_bytes > 0)
|
||||
.then_some(InstallProgressSecondary {
|
||||
current: current_bytes
|
||||
.min(content_total_bytes),
|
||||
total: content_total_bytes,
|
||||
}),
|
||||
}),
|
||||
modpack_details,
|
||||
)
|
||||
.await?;
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
};
|
||||
|
||||
//TODO: Future update: prompt user for optional files in a modpack
|
||||
if let Some(env) = project.env
|
||||
&& env
|
||||
.get(&EnvType::Client)
|
||||
.is_some_and(|x| x == &SideType::Unsupported)
|
||||
{
|
||||
mark_downloaded(project.file_size as u64).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -447,13 +504,11 @@ pub async fn install_zipped_mrpack_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let path = profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(project.path.as_str());
|
||||
let path = instance_full_path.join(project.path.as_str());
|
||||
|
||||
cache_file_hash(
|
||||
file.clone(),
|
||||
&profile_path,
|
||||
&instance_path,
|
||||
project.path.as_str(),
|
||||
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
||||
ProjectType::get_from_parent_folder(&path),
|
||||
@@ -464,14 +519,51 @@ pub async fn install_zipped_mrpack_files(
|
||||
|
||||
write(&path, &file, &state.io_semaphore).await?;
|
||||
|
||||
if let Some(project_type) =
|
||||
ProjectType::get_from_parent_folder(project.path.as_str())
|
||||
{
|
||||
let hash =
|
||||
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x);
|
||||
let file_info = if let Some(hash) = hash {
|
||||
CachedEntry::get_file_many(
|
||||
&[hash],
|
||||
None,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(hash) = hash {
|
||||
crate::state::instances::commands::record_project_file(
|
||||
&instance_id,
|
||||
project.path.as_str(),
|
||||
hash,
|
||||
project.file_size as u64,
|
||||
project_type,
|
||||
modpack_source_kind(pack_version_id.as_deref()),
|
||||
file_info
|
||||
.as_ref()
|
||||
.map(|file| file.project_id.as_str()),
|
||||
file_info
|
||||
.as_ref()
|
||||
.map(|file| file.version_id.as_str()),
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
mark_downloaded(project.file_size as u64).await?;
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Extracting overrides"))?;
|
||||
|
||||
let override_file_entries = zip_reader
|
||||
.file()
|
||||
.entries()
|
||||
@@ -485,9 +577,60 @@ pub async fn install_zipped_mrpack_files(
|
||||
.then(|| (index, file.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let override_file_entries_count = override_file_entries.len();
|
||||
let override_total_bytes = override_file_entries
|
||||
.iter()
|
||||
.map(|(_, file)| file.uncompressed_size())
|
||||
.sum::<u64>();
|
||||
let progress = (override_total_bytes > 0).then_some(InstallProgress {
|
||||
current: 0,
|
||||
total: override_total_bytes,
|
||||
secondary: None,
|
||||
});
|
||||
reporter
|
||||
.update(
|
||||
InstallPhaseId::ExtractingOverrides,
|
||||
progress,
|
||||
modpack_details.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for (i, (index, file)) in override_file_entries.into_iter().enumerate() {
|
||||
let extracted_override_bytes = Arc::new(AtomicU64::new(0));
|
||||
let mut last_reported_override_bytes = 0_u64;
|
||||
let reporter_for_overrides = reporter.clone();
|
||||
let details_for_overrides = modpack_details.clone();
|
||||
let mut report_override_progress = |bytes_read: u64| -> Pin<
|
||||
Box<dyn Future<Output = crate::Result<()>> + Send>,
|
||||
> {
|
||||
let current = extracted_override_bytes
|
||||
.fetch_add(bytes_read, Ordering::Relaxed)
|
||||
+ bytes_read;
|
||||
let min_delta = (override_total_bytes / 200).max(256 * 1024);
|
||||
if current < override_total_bytes
|
||||
&& current.saturating_sub(last_reported_override_bytes) < min_delta
|
||||
{
|
||||
return Box::pin(async { Ok(()) });
|
||||
}
|
||||
|
||||
last_reported_override_bytes = current;
|
||||
let reporter = reporter_for_overrides.clone();
|
||||
let details = details_for_overrides.clone();
|
||||
Box::pin(async move {
|
||||
reporter
|
||||
.update(
|
||||
InstallPhaseId::ExtractingOverrides,
|
||||
Some(InstallProgress {
|
||||
current: current.min(override_total_bytes),
|
||||
total: override_total_bytes,
|
||||
secondary: None,
|
||||
}),
|
||||
details,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
for (index, file) in override_file_entries {
|
||||
let relative_override_file_path =
|
||||
SafeRelativeUtf8UnixPathBuf::try_from(
|
||||
file.filename().as_str().unwrap().to_string(),
|
||||
@@ -501,18 +644,30 @@ pub async fn install_zipped_mrpack_files(
|
||||
))
|
||||
})?;
|
||||
|
||||
let path = profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(relative_override_file_path.as_str());
|
||||
let (size, hash) = zip_reader
|
||||
.extract_entry(index, &path, &state.io_semaphore)
|
||||
.await?;
|
||||
let path =
|
||||
instance_full_path.join(relative_override_file_path.as_str());
|
||||
let (size, hash) = if override_total_bytes > 0 {
|
||||
let progress =
|
||||
&mut report_override_progress as &mut ExtractProgressFn<'_>;
|
||||
zip_reader
|
||||
.extract_entry(
|
||||
index,
|
||||
&path,
|
||||
&state.io_semaphore,
|
||||
Some(progress),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
zip_reader
|
||||
.extract_entry(index, &path, &state.io_semaphore, None)
|
||||
.await?
|
||||
};
|
||||
|
||||
crate::state::cache_file_hash_metadata(
|
||||
&profile_path,
|
||||
&instance_path,
|
||||
relative_override_file_path.as_str(),
|
||||
size,
|
||||
hash,
|
||||
hash.clone(),
|
||||
ProjectType::get_from_parent_folder(
|
||||
relative_override_file_path.as_str(),
|
||||
),
|
||||
@@ -521,41 +676,54 @@ pub async fn install_zipped_mrpack_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
emit_loading(
|
||||
&loading_bar,
|
||||
30.0 / override_file_entries_count as f64,
|
||||
Some(&format!(
|
||||
"Extracting override {}/{override_file_entries_count}",
|
||||
i + 1
|
||||
)),
|
||||
)?;
|
||||
if let Some(project_type) = ProjectType::get_from_parent_folder(
|
||||
relative_override_file_path.as_str(),
|
||||
) {
|
||||
crate::state::instances::commands::record_project_file(
|
||||
&instance_id,
|
||||
relative_override_file_path.as_str(),
|
||||
&hash,
|
||||
size,
|
||||
project_type,
|
||||
modpack_source_kind(version_id.as_deref()),
|
||||
None,
|
||||
None,
|
||||
state,
|
||||
)
|
||||
.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::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join("icon.png");
|
||||
let potential_icon = instance_full_path.join("icon.png");
|
||||
if !icon_exists && potential_icon.exists() {
|
||||
profile::edit_icon(&profile_path, Some(&potential_icon)).await?;
|
||||
crate::api::instance::edit_icon(&instance_id, Some(&potential_icon))
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(profile_val) = profile::get(&profile_path).await? {
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
crate::launcher::install_minecraft_for_instance_id_with_reporter(
|
||||
&instance_id,
|
||||
false,
|
||||
Some(reporter),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok::<String, crate::Error>(profile_path.clone())
|
||||
Ok::<String, crate::Error>(instance_id.clone())
|
||||
}
|
||||
|
||||
fn modpack_source_kind(version_id: Option<&str>) -> ContentSourceKind {
|
||||
if version_id.is_some() {
|
||||
ContentSourceKind::ModrinthModpack
|
||||
} else {
|
||||
ContentSourceKind::ImportedModpack
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(mrpack_file))]
|
||||
|
||||
pub async fn remove_all_related_files(
|
||||
profile_path: String,
|
||||
instance_id: String,
|
||||
mrpack_file: CreatePackFile,
|
||||
) -> crate::Result<()> {
|
||||
// Updates can remove files from a locally imported or downloaded pack, so share the same reader path.
|
||||
@@ -581,17 +749,29 @@ pub async fn remove_all_related_files(
|
||||
.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(()) }
|
||||
})
|
||||
crate::api::instance::edit(
|
||||
&instance_id,
|
||||
EditInstance {
|
||||
install_stage: Some(InstanceInstallStage::PackInstalling),
|
||||
..EditInstance::default()
|
||||
},
|
||||
)
|
||||
.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 metadata =
|
||||
crate::api::instance::get(&instance_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unknown instance {instance_id}"
|
||||
))
|
||||
})?;
|
||||
let instance_full_path =
|
||||
crate::api::instance::get_full_path(&instance_id).await?;
|
||||
let all_hashes = pack
|
||||
.files
|
||||
.iter()
|
||||
@@ -612,34 +792,28 @@ pub async fn remove_all_related_files(
|
||||
.map(|p| p.project_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let profile = profile::get(&profile_path).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
})?;
|
||||
let profile_full_path = profile::get_full_path(&profile_path).await?;
|
||||
|
||||
for (file_path, project) in profile
|
||||
.get_projects(
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
for file in crate::state::instances::commands::list_project_files(
|
||||
&metadata.instance.id,
|
||||
&state,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if let Some(metadata) = &project.metadata
|
||||
&& to_remove.contains(&metadata.project_id)
|
||||
if let Some(project_id) = &file.project_id
|
||||
&& to_remove.contains(project_id)
|
||||
{
|
||||
match io::remove_file(profile_full_path.join(file_path)).await {
|
||||
Ok(_) => (),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => (),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
crate::state::instances::commands::remove_project(
|
||||
&metadata.instance.id,
|
||||
&file.relative_path,
|
||||
&state,
|
||||
)
|
||||
.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 {
|
||||
match io::remove_file(profile_full_path.join(file.path.as_str())).await
|
||||
match io::remove_file(instance_full_path.join(file.path.as_str())).await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => (),
|
||||
@@ -672,9 +846,7 @@ pub async fn remove_all_related_files(
|
||||
|
||||
// Remove this file if a corresponding one exists in the filesystem
|
||||
match io::remove_file(
|
||||
profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(relative_override_file_path.as_str()),
|
||||
instance_full_path.join(relative_override_file_path.as_str()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
use crate::state::ProcessMetadata;
|
||||
pub use crate::{
|
||||
State,
|
||||
state::{Hooks, MemorySettings, Profile, Settings, WindowSize},
|
||||
state::{Hooks, MemorySettings, Settings, WindowSize},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Gets the Profile paths of each *running* stored process in the state
|
||||
// Gets each running stored process in the state
|
||||
#[tracing::instrument]
|
||||
pub async fn get_all() -> crate::Result<Vec<ProcessMetadata>> {
|
||||
let state = State::get().await?;
|
||||
@@ -15,17 +15,50 @@ pub async fn get_all() -> crate::Result<Vec<ProcessMetadata>> {
|
||||
Ok(processes)
|
||||
}
|
||||
|
||||
// Gets the UUID of each stored process in the state by profile path
|
||||
pub async fn resolve_instance_id(instance: &str) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
resolve_instance_id_with_state(instance, &state)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unknown instance id or path: {instance}"
|
||||
))
|
||||
.as_error()
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_instance_id_with_state(
|
||||
instance: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<Option<String>> {
|
||||
sqlx::query_scalar!(
|
||||
"
|
||||
SELECT id
|
||||
FROM instances
|
||||
WHERE id = ? OR path = ?
|
||||
ORDER BY CASE WHEN id = ? THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
",
|
||||
instance,
|
||||
instance,
|
||||
instance,
|
||||
)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
// Gets the UUID of each stored process in the state by instance id
|
||||
#[tracing::instrument]
|
||||
pub async fn get_by_profile_path(
|
||||
profile_path: &str,
|
||||
pub async fn get_by_instance_id(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<Vec<ProcessMetadata>> {
|
||||
let state = State::get().await?;
|
||||
let processes = state
|
||||
.process_manager
|
||||
.get_all()
|
||||
.into_iter()
|
||||
.filter(|x| x.profile_path == profile_path)
|
||||
.filter(|x| x.instance_id == instance_id)
|
||||
.collect();
|
||||
Ok(processes)
|
||||
}
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
//! Theseus profile management interface
|
||||
use crate::launcher::get_loader_version_from_profile;
|
||||
use crate::settings::Hooks;
|
||||
use crate::state::{
|
||||
LauncherFeatureVersion, LinkedData, ProfileInstallStage, ReleaseChannel,
|
||||
};
|
||||
use crate::util::io::{self, canonicalize};
|
||||
use crate::{ErrorKind, pack, profile};
|
||||
pub use crate::{State, state::Profile};
|
||||
use crate::{
|
||||
event::{ProfilePayloadType, emit::emit_profile},
|
||||
prelude::ModLoader,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use std::path::PathBuf;
|
||||
use tracing::{info, trace};
|
||||
|
||||
// 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]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn profile_create(
|
||||
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_path: Option<String>, // the icon for the profile
|
||||
linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
|
||||
skip_install_profile: Option<bool>,
|
||||
) -> crate::Result<String> {
|
||||
trace!("Creating new profile. {}", name);
|
||||
let state = State::get().await?;
|
||||
|
||||
let mut path = profile::sanitize_profile_name(&name);
|
||||
let mut full_path = state.directories.profiles_dir().join(&path);
|
||||
if full_path.exists() {
|
||||
let mut new_path;
|
||||
let mut new_full_path;
|
||||
let mut which = 1;
|
||||
|
||||
loop {
|
||||
new_path = format!("{path} ({which})");
|
||||
new_full_path = state.directories.profiles_dir().join(&new_path);
|
||||
if !new_full_path.exists() {
|
||||
break;
|
||||
}
|
||||
which += 1;
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Folder collision: {}, renaming to: {}",
|
||||
full_path.display(),
|
||||
new_full_path.display()
|
||||
);
|
||||
|
||||
path = new_path;
|
||||
full_path = new_full_path;
|
||||
}
|
||||
io::create_dir_all(&full_path).await?;
|
||||
|
||||
info!(
|
||||
"Creating profile at path {}",
|
||||
&canonicalize(&full_path)?.display()
|
||||
);
|
||||
let loader = if modloader != ModLoader::Vanilla {
|
||||
get_loader_version_from_profile(
|
||||
&game_version,
|
||||
modloader,
|
||||
loader_version.as_deref(),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut profile = Profile {
|
||||
path: path.clone(),
|
||||
install_stage: ProfileInstallStage::NotInstalled,
|
||||
launcher_feature_version: LauncherFeatureVersion::MOST_RECENT,
|
||||
name,
|
||||
icon_path: None,
|
||||
game_version,
|
||||
protocol_version: None,
|
||||
loader: modloader,
|
||||
loader_version: loader.map(|x| x.id),
|
||||
groups: Vec::new(),
|
||||
linked_data,
|
||||
preferred_update_channel: ReleaseChannel::Release,
|
||||
created: Utc::now(),
|
||||
modified: Utc::now(),
|
||||
last_played: None,
|
||||
submitted_time_played: 0,
|
||||
recent_time_played: 0,
|
||||
java_path: None,
|
||||
extra_launch_args: None,
|
||||
custom_env_vars: None,
|
||||
memory: None,
|
||||
force_fullscreen: None,
|
||||
game_resolution: None,
|
||||
hooks: Hooks {
|
||||
pre_launch: None,
|
||||
wrapper: None,
|
||||
post_exit: None,
|
||||
},
|
||||
};
|
||||
|
||||
let result = async {
|
||||
if let Some(ref icon) = icon_path {
|
||||
let (bytes, file_name) = if icon.starts_with("https://")
|
||||
|| icon.starts_with("http://")
|
||||
{
|
||||
let fetched = crate::util::fetch::fetch(
|
||||
icon,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
let name =
|
||||
icon.rsplit('/').next().unwrap_or("icon").to_string();
|
||||
(fetched, name)
|
||||
} else {
|
||||
let data =
|
||||
io::read(state.directories.caches_dir().join(icon)).await?;
|
||||
(bytes::Bytes::from(data), icon.clone())
|
||||
};
|
||||
profile
|
||||
.set_icon(
|
||||
&state.directories.caches_dir(),
|
||||
&state.io_semaphore,
|
||||
bytes,
|
||||
&file_name,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
crate::state::fs_watcher::watch_profile(
|
||||
&profile.path,
|
||||
&state.file_watcher,
|
||||
&state.directories,
|
||||
)
|
||||
.await;
|
||||
|
||||
profile.upsert(&state.pool).await?;
|
||||
|
||||
emit_profile(&profile.path, ProfilePayloadType::Created).await?;
|
||||
|
||||
if !skip_install_profile.unwrap_or(false) {
|
||||
crate::launcher::install_minecraft(&profile, None, false).await?;
|
||||
}
|
||||
|
||||
Ok(profile.path)
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(profile) => Ok(profile),
|
||||
Err(err) => {
|
||||
let _ = profile::remove(&path).await;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn profile_create_from_duplicate(
|
||||
copy_from: &str,
|
||||
) -> crate::Result<String> {
|
||||
// Original profile
|
||||
let profile = profile::get(copy_from).await?.ok_or_else(|| {
|
||||
ErrorKind::UnmanagedProfileError(copy_from.to_string())
|
||||
})?;
|
||||
|
||||
let profile_path_id = profile_create(
|
||||
profile.name.clone(),
|
||||
profile.game_version.clone(),
|
||||
profile.loader,
|
||||
profile.loader_version.clone(),
|
||||
profile.icon_path.clone(),
|
||||
profile.linked_data.clone(),
|
||||
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,
|
||||
profile::get_full_path(copy_from).await?,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let duplicated_profile =
|
||||
profile::get(&profile_path_id).await?.ok_or_else(|| {
|
||||
ErrorKind::UnmanagedProfileError(profile_path_id.to_string())
|
||||
})?;
|
||||
|
||||
crate::launcher::install_minecraft(&duplicated_profile, Some(bar), false)
|
||||
.await?;
|
||||
|
||||
// emit profile edited
|
||||
emit_profile(&profile.path, ProfilePayloadType::Edited).await?;
|
||||
Ok(profile_path_id)
|
||||
}
|
||||
|
||||
#[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
@@ -1,242 +0,0 @@
|
||||
use crate::state::CacheBehaviour;
|
||||
use crate::util::fetch::DownloadReason;
|
||||
use crate::{
|
||||
LoadingBarType,
|
||||
event::{
|
||||
ProfilePayloadType,
|
||||
emit::{emit_profile, init_loading},
|
||||
},
|
||||
pack::{self, install_from::generate_pack_from_version_id},
|
||||
profile::get,
|
||||
state::ProfileInstallStage,
|
||||
};
|
||||
use futures::try_join;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Updates a managed modrinth pack to the version specified by new_version_id
|
||||
#[tracing::instrument]
|
||||
|
||||
pub async fn update_managed_modrinth_version(
|
||||
profile_path: &String,
|
||||
new_version_id: &String,
|
||||
) -> crate::Result<()> {
|
||||
let profile = get(profile_path).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let unmanaged_err = || {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Profile at {profile_path} is not a managed modrinth pack, or has been disconnected."
|
||||
))
|
||||
};
|
||||
|
||||
// Extract modrinth pack information, if appropriate
|
||||
let linked_data = profile.linked_data.as_ref().ok_or_else(unmanaged_err)?;
|
||||
|
||||
// Replace the pack with the new version
|
||||
replace_managed_modrinth(
|
||||
profile_path,
|
||||
&profile,
|
||||
&linked_data.project_id,
|
||||
&linked_data.version_id,
|
||||
Some(new_version_id),
|
||||
true, // switching versions should ignore the lock
|
||||
)
|
||||
.await?;
|
||||
|
||||
emit_profile(profile_path, ProfilePayloadType::Edited).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repair a managed modrinth pack by 'updating' it to the current version
|
||||
#[tracing::instrument]
|
||||
|
||||
pub async fn repair_managed_modrinth(profile_path: &str) -> crate::Result<()> {
|
||||
let profile = get(profile_path).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let unmanaged_err = || {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Profile at {profile_path} is not a managed modrinth pack, or has been disconnected."
|
||||
))
|
||||
};
|
||||
|
||||
// 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 state = crate::State::get().await?;
|
||||
let projects_map = profile
|
||||
.get_projects(
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for (file, _) in projects_map {
|
||||
crate::state::Profile::remove_project(&profile.path, &file).await?;
|
||||
}
|
||||
|
||||
// Extract modrinth pack information, if appropriate
|
||||
let linked_data = profile.linked_data.as_ref().ok_or_else(unmanaged_err)?;
|
||||
|
||||
// Replace the pack with the same version
|
||||
replace_managed_modrinth(
|
||||
profile_path,
|
||||
&profile,
|
||||
&linked_data.project_id,
|
||||
&linked_data.version_id,
|
||||
None,
|
||||
false, // do not ignore lock, as repairing can reset the lock
|
||||
)
|
||||
.await?;
|
||||
|
||||
emit_profile(profile_path, ProfilePayloadType::Edited).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))]
|
||||
|
||||
async fn replace_managed_modrinth(
|
||||
profile_path: &str,
|
||||
profile: &crate::state::Profile,
|
||||
project_id: &String,
|
||||
version_id: &String,
|
||||
new_version_id: Option<&String>,
|
||||
ignore_lock: bool,
|
||||
) -> crate::Result<()> {
|
||||
// get disabled project ids to re-disable after update
|
||||
let state = crate::State::get().await?;
|
||||
let disabled_project_ids = profile
|
||||
.get_projects(
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|(file_path, project)| {
|
||||
(file_path.ends_with(".disabled"))
|
||||
.then_some(project.metadata?.project_id)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
crate::profile::edit(profile_path, |profile| {
|
||||
profile.install_stage = ProfileInstallStage::MinecraftInstalling;
|
||||
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: crate::api::profile::get_full_path(profile_path)
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
pack_name: profile.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.name.clone(),
|
||||
None,
|
||||
profile_path.to_string(),
|
||||
Some(shared_loading_bar.clone()),
|
||||
DownloadReason::Update,
|
||||
),
|
||||
generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
new_version_id.clone(),
|
||||
profile.name.clone(),
|
||||
None,
|
||||
profile_path.to_string(),
|
||||
Some(shared_loading_bar),
|
||||
DownloadReason::Update,
|
||||
)
|
||||
)?
|
||||
} 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.name.clone(),
|
||||
None,
|
||||
profile_path.to_string(),
|
||||
None,
|
||||
DownloadReason::Update,
|
||||
)
|
||||
.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.to_string(),
|
||||
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,
|
||||
DownloadReason::Update,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// re-enable previously disabled project
|
||||
if !disabled_project_ids.is_empty()
|
||||
&& let Some(updated_profile) = get(profile_path).await?
|
||||
{
|
||||
for (file_path, project) in updated_profile
|
||||
.get_projects(
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if !file_path.ends_with(".disabled")
|
||||
&& let Some(metadata) = &project.metadata
|
||||
&& disabled_project_ids.contains(&metadata.project_id)
|
||||
{
|
||||
crate::state::Profile::toggle_disable_project(
|
||||
profile_path,
|
||||
&file_path,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Theseus profile management interface
|
||||
//! Theseus settings management interface
|
||||
|
||||
pub use crate::{
|
||||
State,
|
||||
state::{Hooks, MemorySettings, Profile, Settings, WindowSize},
|
||||
state::{Hooks, MemorySettings, Settings, WindowSize},
|
||||
};
|
||||
|
||||
/// Gets entire settings
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::data::ModLoader;
|
||||
use crate::instance::get_full_path;
|
||||
use crate::launcher::get_loader_version_from_profile;
|
||||
use crate::profile::get_full_path;
|
||||
use crate::server_address::{parse_server_address, resolve_server_address};
|
||||
use crate::state::attached_world_data::AttachedWorldData;
|
||||
use crate::state::{
|
||||
Profile, ProfileInstallStage, attached_world_data, server_join_log,
|
||||
InstanceInstallStage, attached_world_data, server_join_log,
|
||||
};
|
||||
use crate::util::protocol_version::OLD_PROTOCOL_VERSIONS;
|
||||
pub use crate::util::protocol_version::ProtocolVersion;
|
||||
@@ -36,8 +36,8 @@ use tokio_util::compat::FuturesAsyncWriteCompatExt;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct WorldWithProfile {
|
||||
pub profile: String,
|
||||
pub struct WorldWithInstance {
|
||||
pub instance_id: String,
|
||||
#[serde(flatten)]
|
||||
pub world: World,
|
||||
}
|
||||
@@ -191,33 +191,36 @@ impl From<ServerPackStatus> for Option<bool> {
|
||||
pub async fn get_recent_worlds(
|
||||
limit: usize,
|
||||
display_statuses: EnumSet<DisplayStatus>,
|
||||
) -> Result<Vec<WorldWithProfile>> {
|
||||
) -> Result<Vec<WorldWithInstance>> {
|
||||
let state = State::get().await?;
|
||||
let profiles_dir = state.directories.profiles_dir();
|
||||
let instances_dir = state.directories.instances_dir();
|
||||
|
||||
let mut profiles = Profile::get_all(&state.pool).await?;
|
||||
profiles.sort_by_key(|x| Reverse(x.last_played));
|
||||
let mut instances = crate::state::list_instances(&state.pool).await?;
|
||||
instances.sort_by_key(|x| Reverse(x.instance.last_played));
|
||||
|
||||
let mut result = Vec::with_capacity(limit);
|
||||
|
||||
let mut least_recent_time = None;
|
||||
for profile in profiles {
|
||||
if result.len() >= limit && profile.last_played < least_recent_time {
|
||||
for instance in instances {
|
||||
if result.len() >= limit
|
||||
&& instance.instance.last_played < least_recent_time
|
||||
{
|
||||
break;
|
||||
}
|
||||
let profile_path = &profile.path;
|
||||
let profile_dir = profiles_dir.join(profile_path);
|
||||
let profile_worlds =
|
||||
get_all_worlds_in_profile(profile_path, &profile_dir).await;
|
||||
if let Err(e) = profile_worlds {
|
||||
let instance_id = &instance.instance.id;
|
||||
let instance_path = &instance.instance.path;
|
||||
let instance_dir = instances_dir.join(instance_path);
|
||||
let instance_worlds =
|
||||
get_all_worlds_in_instance(instance_id, &instance_dir).await;
|
||||
if let Err(e) = instance_worlds {
|
||||
tracing::error!(
|
||||
"Failed to get worlds for profile {}: {}",
|
||||
profile_path,
|
||||
"Failed to get worlds for instance {}: {}",
|
||||
instance_id,
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for world in profile_worlds? {
|
||||
for world in instance_worlds? {
|
||||
let is_older = least_recent_time.is_none()
|
||||
|| world.last_played < least_recent_time;
|
||||
if result.len() >= limit && is_older {
|
||||
@@ -229,8 +232,8 @@ pub async fn get_recent_worlds(
|
||||
if is_older {
|
||||
least_recent_time = world.last_played;
|
||||
}
|
||||
result.push(WorldWithProfile {
|
||||
profile: profile_path.clone(),
|
||||
result.push(WorldWithInstance {
|
||||
instance_id: instance_id.clone(),
|
||||
world,
|
||||
});
|
||||
}
|
||||
@@ -246,23 +249,58 @@ pub async fn get_recent_worlds(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_profile_worlds(profile_path: &str) -> Result<Vec<World>> {
|
||||
get_all_worlds_in_profile(profile_path, &get_full_path(profile_path).await?)
|
||||
pub async fn get_instance_worlds(instance_id: &str) -> Result<Vec<World>> {
|
||||
get_all_worlds_in_instance(instance_id, &get_full_path(instance_id).await?)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_all_worlds_in_profile(
|
||||
profile_path: &str,
|
||||
profile_dir: &Path,
|
||||
async fn resolve_instance_id(instance: &str, state: &State) -> Result<String> {
|
||||
resolve_instance_identity(instance, state)
|
||||
.await
|
||||
.map(|(instance_id, _)| instance_id)
|
||||
}
|
||||
|
||||
async fn resolve_instance_identity(
|
||||
instance: &str,
|
||||
state: &State,
|
||||
) -> Result<(String, String)> {
|
||||
let row = sqlx::query!(
|
||||
"
|
||||
SELECT id, path
|
||||
FROM instances
|
||||
WHERE id = ? OR path = ?
|
||||
ORDER BY CASE WHEN id = ? THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
",
|
||||
instance,
|
||||
instance,
|
||||
instance,
|
||||
)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ErrorKind::InputError(format!(
|
||||
"Unknown instance id or path: {instance}"
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
Ok((row.id, row.path))
|
||||
}
|
||||
|
||||
async fn get_all_worlds_in_instance(
|
||||
instance_id: &str,
|
||||
instance_dir: &Path,
|
||||
) -> Result<Vec<World>> {
|
||||
let mut worlds = vec![];
|
||||
get_singleplayer_worlds_in_profile(profile_dir, &mut worlds).await?;
|
||||
get_server_worlds_in_profile(profile_path, profile_dir, &mut worlds)
|
||||
get_singleplayer_worlds_in_instance(instance_dir, &mut worlds).await?;
|
||||
let state = State::get().await?;
|
||||
|
||||
get_server_worlds_in_instance(instance_id, instance_dir, &mut worlds)
|
||||
.await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let attached_data =
|
||||
AttachedWorldData::get_all_for_instance(profile_path, &state.pool)
|
||||
AttachedWorldData::get_all_for_instance(instance_id, &state.pool)
|
||||
.await?;
|
||||
if !attached_data.is_empty() {
|
||||
for world in &mut worlds {
|
||||
@@ -277,7 +315,7 @@ async fn get_all_worlds_in_profile(
|
||||
Ok(worlds)
|
||||
}
|
||||
|
||||
async fn get_singleplayer_worlds_in_profile(
|
||||
async fn get_singleplayer_worlds_in_instance(
|
||||
instance_dir: &Path,
|
||||
worlds: &mut Vec<World>,
|
||||
) -> Result<()> {
|
||||
@@ -314,12 +352,14 @@ pub async fn get_singleplayer_world(
|
||||
world: &str,
|
||||
) -> Result<World> {
|
||||
let state = State::get().await?;
|
||||
let profile_path = state.directories.profiles_dir().join(instance);
|
||||
let (instance_id, instance_path) =
|
||||
resolve_instance_identity(instance, &state).await?;
|
||||
let instance_dir = state.directories.instances_dir().join(instance_path);
|
||||
let mut world =
|
||||
read_singleplayer_world(get_world_dir(&profile_path, world)).await?;
|
||||
read_singleplayer_world(get_world_dir(&instance_dir, world)).await?;
|
||||
|
||||
if let Some(data) = AttachedWorldData::get_for_world(
|
||||
instance,
|
||||
&instance_id,
|
||||
world.world_type(),
|
||||
world.world_id(),
|
||||
&state.pool,
|
||||
@@ -398,8 +438,8 @@ async fn read_singleplayer_world_maybe_locked(
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_server_worlds_in_profile(
|
||||
profile_path: &str,
|
||||
async fn get_server_worlds_in_instance(
|
||||
instance_id: &str,
|
||||
instance_dir: &Path,
|
||||
worlds: &mut Vec<World>,
|
||||
) -> Result<()> {
|
||||
@@ -409,7 +449,7 @@ async fn get_server_worlds_in_profile(
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let join_log = server_join_log::get_joins(profile_path, &state.pool)
|
||||
let join_log = server_join_log::get_joins(instance_id, &state.pool)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
@@ -467,8 +507,9 @@ pub async fn set_world_display_status(
|
||||
display_status: DisplayStatus,
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let instance_id = resolve_instance_id(instance, &state).await?;
|
||||
attached_world_data::set_display_status(
|
||||
instance,
|
||||
&instance_id,
|
||||
world_type,
|
||||
world_id,
|
||||
display_status,
|
||||
@@ -708,16 +749,19 @@ async fn try_get_world_session_lock(
|
||||
Ok(locked.then_some(file))
|
||||
}
|
||||
|
||||
pub async fn add_server_to_profile(
|
||||
profile_path: &Path,
|
||||
profile_path_id: &str,
|
||||
pub async fn add_server_to_instance(
|
||||
instance_id: &str,
|
||||
name: String,
|
||||
address: String,
|
||||
pack_status: ServerPackStatus,
|
||||
project_id: Option<String>,
|
||||
content_kind: Option<String>,
|
||||
) -> Result<usize> {
|
||||
let mut servers = servers_data::read(profile_path).await?;
|
||||
let state = State::get().await?;
|
||||
let (instance_id, instance_path) =
|
||||
resolve_instance_identity(instance_id, &state).await?;
|
||||
let instance_dir = state.directories.instances_dir().join(instance_path);
|
||||
let mut servers = servers_data::read(&instance_dir).await?;
|
||||
let insert_index = servers
|
||||
.iter()
|
||||
.position(|x| x.hidden)
|
||||
@@ -732,13 +776,12 @@ pub async fn add_server_to_profile(
|
||||
icon: None,
|
||||
},
|
||||
);
|
||||
servers_data::write(profile_path, &servers).await?;
|
||||
servers_data::write(&instance_dir, &servers).await?;
|
||||
|
||||
if project_id.is_some() || content_kind.is_some() {
|
||||
let state = State::get().await?;
|
||||
if let Some(project_id) = &project_id {
|
||||
attached_world_data::set_project_id(
|
||||
profile_path_id,
|
||||
&instance_id,
|
||||
WorldType::Server,
|
||||
&address,
|
||||
project_id,
|
||||
@@ -748,7 +791,7 @@ pub async fn add_server_to_profile(
|
||||
}
|
||||
if let Some(content_kind) = &content_kind {
|
||||
attached_world_data::set_content_kind(
|
||||
profile_path_id,
|
||||
&instance_id,
|
||||
WorldType::Server,
|
||||
&address,
|
||||
content_kind,
|
||||
@@ -761,14 +804,18 @@ pub async fn add_server_to_profile(
|
||||
Ok(insert_index)
|
||||
}
|
||||
|
||||
pub async fn edit_server_in_profile(
|
||||
profile_path: &Path,
|
||||
pub async fn edit_server_in_instance(
|
||||
instance_id: &str,
|
||||
index: usize,
|
||||
name: String,
|
||||
address: String,
|
||||
pack_status: ServerPackStatus,
|
||||
) -> Result<()> {
|
||||
let mut servers = servers_data::read(profile_path).await?;
|
||||
let state = State::get().await?;
|
||||
let (_, instance_path) =
|
||||
resolve_instance_identity(instance_id, &state).await?;
|
||||
let instance_dir = state.directories.instances_dir().join(instance_path);
|
||||
let mut servers = servers_data::read(&instance_dir).await?;
|
||||
let server =
|
||||
servers
|
||||
.get_mut(index)
|
||||
@@ -782,15 +829,19 @@ pub async fn edit_server_in_profile(
|
||||
server.name = name;
|
||||
server.ip = address;
|
||||
server.accept_textures = pack_status.into();
|
||||
servers_data::write(profile_path, &servers).await?;
|
||||
servers_data::write(&instance_dir, &servers).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_server_from_profile(
|
||||
profile_path: &Path,
|
||||
pub async fn remove_server_from_instance(
|
||||
instance_id: &str,
|
||||
index: usize,
|
||||
) -> Result<()> {
|
||||
let mut servers = servers_data::read(profile_path).await?;
|
||||
let state = State::get().await?;
|
||||
let (_, instance_path) =
|
||||
resolve_instance_identity(instance_id, &state).await?;
|
||||
let instance_dir = state.directories.instances_dir().join(instance_path);
|
||||
let mut servers = servers_data::read(&instance_dir).await?;
|
||||
if servers.get(index).as_ref().is_none_or(|x| x.hidden) {
|
||||
return Err(ErrorKind::InputError(format!(
|
||||
"No removable server at index {index}"
|
||||
@@ -798,7 +849,7 @@ pub async fn remove_server_from_profile(
|
||||
.into());
|
||||
}
|
||||
servers.remove(index);
|
||||
servers_data::write(profile_path, &servers).await?;
|
||||
servers_data::write(&instance_dir, &servers).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -863,23 +914,28 @@ mod servers_data {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_profile_protocol_version(
|
||||
profile: &str,
|
||||
pub async fn get_instance_protocol_version(
|
||||
instance_id: &str,
|
||||
) -> Result<Option<ProtocolVersion>> {
|
||||
let mut profile = super::profile::get(profile).await?.ok_or_else(|| {
|
||||
ErrorKind::UnmanagedProfileError(format!(
|
||||
"Could not find profile {profile}"
|
||||
))
|
||||
})?;
|
||||
if profile.install_stage != ProfileInstallStage::Installed {
|
||||
let metadata =
|
||||
crate::api::instance::get(instance_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ErrorKind::InputError(format!(
|
||||
"Could not find instance {instance_id}"
|
||||
))
|
||||
})?;
|
||||
if metadata.instance.install_stage != InstanceInstallStage::Installed {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(protocol_version) = profile.protocol_version {
|
||||
if let Some(protocol_version) =
|
||||
metadata.applied_content_set.protocol_version
|
||||
{
|
||||
return Ok(Some(ProtocolVersion::modern(protocol_version)));
|
||||
}
|
||||
if let Some(protocol_version) =
|
||||
OLD_PROTOCOL_VERSIONS.get(&profile.game_version)
|
||||
OLD_PROTOCOL_VERSIONS.get(&metadata.applied_content_set.game_version)
|
||||
{
|
||||
return Ok(Some(*protocol_version));
|
||||
}
|
||||
@@ -887,19 +943,21 @@ pub async fn get_profile_protocol_version(
|
||||
let state = State::get().await?;
|
||||
let (minecraft, version_index) =
|
||||
crate::launcher::resolve_minecraft_manifest(
|
||||
&profile.game_version,
|
||||
&metadata.applied_content_set.game_version,
|
||||
&state,
|
||||
)
|
||||
.await?;
|
||||
let version = &minecraft.versions[version_index];
|
||||
|
||||
let loader_version = get_loader_version_from_profile(
|
||||
&profile.game_version,
|
||||
profile.loader,
|
||||
profile.loader_version.as_deref(),
|
||||
&metadata.applied_content_set.game_version,
|
||||
metadata.applied_content_set.loader,
|
||||
metadata.applied_content_set.loader_version.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
if profile.loader != ModLoader::Vanilla && loader_version.is_none() {
|
||||
if metadata.applied_content_set.loader != ModLoader::Vanilla
|
||||
&& loader_version.is_none()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -920,8 +978,12 @@ pub async fn get_profile_protocol_version(
|
||||
|
||||
let version = launcher::read_protocol_version_from_jar(client_path).await?;
|
||||
if version.is_some() {
|
||||
profile.protocol_version = version;
|
||||
profile.upsert(&state.pool).await?;
|
||||
crate::state::instances::commands::set_applied_content_set_protocol_version(
|
||||
&metadata.instance.id,
|
||||
version,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(version.map(ProtocolVersion::modern))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user