feat: oneclient bundles

This commit is contained in:
LynithDev 2025-09-21 17:04:38 +02:00
parent aafffbacfa
commit 0a77a31846
17 changed files with 1069 additions and 271 deletions

View file

@ -1,6 +1,9 @@
use std::collections::HashMap;
use onelauncher_core::api::cluster::dao::ClusterId;
use onelauncher_core::api::packages::modpack::data::ModpackArchive;
use onelauncher_core::entity::clusters;
use onelauncher_core::entity::loader::GameLoader;
use onelauncher_core::error::LauncherResult;
use tauri::Runtime;
@ -11,6 +14,9 @@ pub trait OneClientApi {
#[taurpc(alias = "getClustersGroupedByMajor")]
async fn get_clusters_grouped_by_major() -> LauncherResult<HashMap<u32, Vec<clusters::Model>>>;
#[taurpc(alias = "getModpacksFor")]
async fn get_modpacks_for(cluster_id: ClusterId) -> LauncherResult<Vec<ModpackArchive>>;
}
#[taurpc::ipc_type]
@ -47,4 +53,27 @@ impl OneClientApi for OneClientApiImpl {
Ok(mapped)
}
async fn get_modpacks_for(self, cluster_id: ClusterId) -> LauncherResult<Vec<ModpackArchive>> {
struct ModpackEntry {
pub category: String,
pub version: String,
pub loader: GameLoader,
}
// const ENTRIES: &[ModpackEntry] = &[
// ModpackEntry {
// category: "Gameplay".into(),
// version: "1.0.0".into(),
// loader: GameLoader::Fabric,
// },
// ModpackEntry {
// category: "Graphics".into(),
// version: "1.1.0".into(),
// loader: GameLoader::Forge,
// },
// ];
Ok(vec![])
}
}

View file

@ -1,3 +1,9 @@
// === API ===
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
pub const DISCORD_CLIENT_ID: &str = "1274084193193955408";
pub const CURSEFORGE_API_KEY: &'static str =
"$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
pub const DISCORD_CLIENT_ID: &'static str = "1274084193193955408";
// === Meta ===
// Cannot contain trailing slash
pub const META_URL_BASE: &'static str =
"https://raw.githubusercontent.com/LynithDev/DataStorage/refs/heads/main/oneclient";

View file

@ -0,0 +1,135 @@
use std::collections::HashMap;
use onelauncher_core::api::packages::modpack::data::ModpackArchive;
use onelauncher_core::entity::loader::GameLoader;
use onelauncher_core::error::LauncherResult;
use onelauncher_core::send_error;
use onelauncher_core::store::Dirs;
use onelauncher_core::utils::{http, io};
use reqwest::Method;
use tokio::sync::{OnceCell, RwLock};
/// e.g.
/// ```json
/// {
/// "versions": {
/// "1.21.5": {
/// "fabric": ["/generated/hud-fabric-1.21.5.mrpack"],
/// "forge": ["/generated/hud-forge-1.21.5.mrpack"]
/// }
/// },
/// }
/// ```
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
struct BundleManifest {
pub versions: HashMap<String, HashMap<String, Vec<String>>>,
}
static BUNDLES_STATE: OnceCell<BundlesManager> = OnceCell::const_new();
#[derive(Debug)]
pub struct BundlesManager {
manifest: RwLock<BundleManifest>,
bundles: RwLock<HashMap<String, HashMap<GameLoader, Vec<ModpackArchive>>>>,
}
impl BundlesManager {
pub async fn get() -> &'static Self {
BUNDLES_STATE
.get_or_init(|| async {
let manifest = Self::fetch_cached().await;
Self {
manifest: RwLock::new(manifest),
bundles: RwLock::new(HashMap::new()),
}
})
.await
}
#[tracing::instrument]
pub async fn get_bundles_for(
&self,
mc_version: &str,
loader: onelauncher_core::entity::loader::GameLoader,
) -> LauncherResult<Vec<ModpackArchive>> {
let manifest = self.manifest.read().await;
let mut bundles_lock = self.bundles.write().await;
if let Some(entry) = bundles_lock.get(mc_version) {
if let Some(bundles) = entry.get(&loader) {
return Ok(bundles.clone());
}
}
let mut found = Vec::new();
// for (version, loaders) in &manifest.versions {
// if version != mc_version {
// continue;
// }
// let Some(paths) = loaders.get(&loader.get_format_name()) else {
// continue;
// };
// // we will be first checking the disk cache, if that fails we fetch from remote
// for path in paths {
// let url = format!("{}{}", crate::constants::META_URL_BASE, path);
// let res = http::request(Method::HEAD, &url, None).await?;
// // res.
// let name = path.split('/').last().unwrap_or(&format!("bundle-{}-{}-{}.mrpack"));
// }
// }
Ok(found.clone())
}
/// Fetches the bundles manifest from remote, falling back to a saved copy on disk if available
#[tracing::instrument]
pub async fn fetch_cached() -> BundleManifest {
let url = format!("{}/bundles.json", crate::constants::META_URL_BASE);
let manifest_path = Self::dir().await.join("bundles.json");
match http::fetch_json::<BundleManifest>(Method::GET, &url, None, None).await {
Ok(manifest) => {
if let Err(e) = io::write_json(&manifest_path, &manifest).await {
send_error!("failed to cache bundles manifest to disk: {e}");
}
manifest
}
Err(e) if manifest_path.exists() => {
tracing::debug!(
"falling back to cached bundles manifest, due to error fetching remote: {e}"
);
match io::read_json::<BundleManifest>(&manifest_path).await {
Ok(manifest) => manifest,
Err(e) => {
tracing::error!("failed to read cached bundles manifest: {e}");
BundleManifest::default()
}
}
}
Err(e) => {
tracing::error!("failed to fetch bundles manifest from remote: {e}");
BundleManifest::default()
}
}
}
/// returns the directory for everything bundle related
pub async fn dir() -> std::path::PathBuf {
Dirs::get_caches_dir()
.await
.expect("failed to get caches dir")
.join("oneclient")
.join("bundles")
}
}

View file

@ -0,0 +1,113 @@
use std::path::Path;
use onelauncher_core::entity::clusters::Model as ClusterModel;
use onelauncher_core::entity::loader::GameLoader;
use onelauncher_core::error::LauncherResult;
use onelauncher_core::send_error;
use onelauncher_core::utils::http::fetch_json;
use reqwest::Method;
use serde::Deserialize;
///
/// e.g.
/// ```json
/// {
/// "clusters": [
/// {
/// "major_version": 21,
/// "entries": [
/// {
/// "minor_version": 5,
/// "loader": "fabric"
/// },
/// {
/// "minor_version": 5,
/// "loader": "forge"
/// }
/// ]
/// },
/// {
/// "major_version": 20,
/// "entries": [
/// {
/// "minor_version": 5,
/// "loader": "fabric"
/// }
/// ]
/// }
/// ]
/// }
/// ```
#[derive(Deserialize)]
struct OnlineClusterManifest {
clusters: Vec<OnlineCluster>,
}
#[derive(Deserialize)]
struct OnlineCluster {
major_version: u8,
entries: Vec<OnlineClusterEntry>,
}
#[derive(Deserialize)]
struct OnlineClusterEntry {
minor_version: u8,
loader: GameLoader,
}
pub async fn init_clusters() -> LauncherResult<()> {
let manifest = match fetch_json::<OnlineClusterManifest>(
Method::GET,
&format!("{}/versions/versions.json", crate::constants::META_URL_BASE),
None,
None,
)
.await
{
Ok(m) => m,
Err(e) => {
send_error!("failed to fetch clusters manifest: {}", e);
return Err(e);
}
};
for cluster in manifest.clusters {
for entry in cluster.entries {
let mc_version = format!("1.{}.{}", cluster.major_version, entry.minor_version);
if let Err(e) = create_cluster_if_not_exist(&mc_version, entry.loader, None).await {
send_error!("failed to create cluster for {}: {}", mc_version, e);
}
}
}
Ok(())
}
fn cluster_name(mc_version: &str, loader: &GameLoader) -> String {
format!("{mc_version} {loader}")
}
async fn create_cluster_if_not_exist(
mc_version: &str,
mc_loader: GameLoader,
mc_loader_version: Option<&str>,
) -> LauncherResult<Option<ClusterModel>> {
let name = cluster_name(mc_version, &mc_loader);
let cluster =
onelauncher_core::api::cluster::dao::get_cluster_by_folder_name(Path::new(&name)).await?;
if cluster.is_some() {
return Ok(cluster);
}
onelauncher_core::api::cluster::create_cluster(
&name,
mc_version,
mc_loader,
mc_loader_version,
None,
)
.await
.map(Some)
}

View file

@ -1,74 +1,6 @@
use onelauncher_core::entity::clusters::Model as ClusterModel;
use onelauncher_core::entity::icon::Icon;
use onelauncher_core::entity::loader::GameLoader;
use onelauncher_core::error::LauncherResult;
use onelauncher_core::send_error;
pub mod bundles;
pub mod clusters;
pub async fn initialize_oneclient() {
init_clusters().await;
}
struct OnlineCluster {
mc_major_version: u8,
mc_minor_versions: Vec<u8>,
mc_loaders: Vec<GameLoader>,
}
async fn init_clusters() {
// TODO: use online metadata stuff
let online_clusters: Vec<OnlineCluster> = vec![
OnlineCluster {
mc_major_version: 21,
mc_minor_versions: vec![1, 2, 4, 7],
mc_loaders: vec![GameLoader::Fabric, GameLoader::Forge],
},
OnlineCluster {
mc_major_version: 8,
mc_minor_versions: vec![9],
mc_loaders: vec![GameLoader::Forge],
},
];
for cluster in online_clusters {
for minor_version in cluster.mc_minor_versions {
let mc_version = format!("1.{}.{}", cluster.mc_major_version, minor_version);
for loader in &cluster.mc_loaders {
if let Err(e) = create_cluster_if_not_exist(
&format!("{mc_version} {loader}"),
&mc_version,
*loader,
None,
None,
)
.await
{
send_error!("failed to create cluster for {}: {}", mc_version, e);
}
}
}
}
}
async fn create_cluster_if_not_exist(
name: &str,
mc_version: &str,
mc_loader: GameLoader,
mc_loader_version: Option<&str>,
icon_url: Option<Icon>,
) -> LauncherResult<Option<ClusterModel>> {
let clusters = onelauncher_core::api::cluster::dao::get_clusters_by_version(mc_version).await?;
if clusters.len() > 1 {
return Ok(None);
}
onelauncher_core::api::cluster::create_cluster(
name,
mc_version,
mc_loader,
mc_loader_version,
icon_url,
)
.await
.map(Some)
clusters::init_clusters().await;
}

View file

@ -24,6 +24,8 @@ export type DirectoryError = { type: "BaseDir"; data: string }
export type DiscordError = { type: "MissingClientId"; data: string } | { type: "ConnectError"; data: string }
export type ExternalPackage = { name: string; url: string; sha1: string; size: number; package_type: PackageType }
export type Filters = { game_versions: string[] | null; loaders: GameLoader[] | null; categories: PackageCategories | null; package_type: PackageType | null }
export type GameLoader = "vanilla" | "forge" | "neoforge" | "quilt" | "fabric" | "legacyfabric"
@ -94,6 +96,16 @@ refresh_token: string;
*/
expires: string }
export type ModpackArchive = { manifest: ModpackManifest; path: string; format: ModpackFormat }
export type ModpackFile = { enabled: boolean; kind: ModpackFileKind }
export type ModpackFileKind = { Managed: [ManagedPackage, ManagedVersion] } | { External: ExternalPackage }
export type ModpackFormat = "CurseForge" | "MrPack" | "PolyMrPack"
export type ModpackManifest = { name: string; version: string; loader: GameLoader; loader_version: string; mc_version: string; files: ModpackFile[] }
export type MojangPlayerProfile = { uuid: string; username: string; is_slim: boolean; skin_url: string | null; cape_url: string | null }
export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] }
@ -232,11 +244,8 @@ export type VersionType =
*/
"old_beta"
const ARGS_MAP = { 'core':'{"getUser":["uuid"],"readSettings":[],"searchPackages":["provider","query"],"writeSettings":["setting"],"createCluster":["options"],"fetchMinecraftProfile":["uuid"],"getLoadersForVersion":["mc_version"],"launchCluster":["id","uuid"],"getGlobalProfile":[],"openMsaLogin":[],"open":["input"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"isClusterRunning":["cluster_id"],"getGameVersions":[],"getMultiplePackages":["provider","slugs"],"setDefaultUser":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getLogs":["id"],"getRunningProcesses":[],"getUsersFromAuthor":["provider","author"],"getClusterById":["id"],"getScreenshots":["id"],"getProfileOrDefault":["name"],"getPackageBody":["provider","body"],"getRunningProcessesByClusterId":["cluster_id"],"updateClusterProfile":["name","profile"],"getPackage":["provider","slug"],"removeUser":["uuid"],"updateClusterById":["id","request"],"getWorlds":["id"],"getLogByName":["id","name"],"killProcess":["pid"],"getClusters":[],"removeCluster":["id"],"getDefaultUser":["fallback"],"getUsers":[]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}' }
export type Router = { 'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'core': { getClusters: () => Promise<ClusterModel[]>,
const ARGS_MAP = { 'oneclient':'{"getModpacksFor":["cluster_id"],"getClustersGroupedByMajor":[],"openDevTools":[]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'core':'{"getUsersFromAuthor":["provider","author"],"getUsers":[],"launchCluster":["id","uuid"],"fetchMinecraftProfile":["uuid"],"searchPackages":["provider","query"],"getLoadersForVersion":["mc_version"],"open":["input"],"removeCluster":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getGlobalProfile":[],"getLogByName":["id","name"],"getClusterById":["id"],"getRunningProcesses":[],"getClusters":[],"getDefaultUser":["fallback"],"readSettings":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"createCluster":["options"],"killProcess":["pid"],"getProfileOrDefault":["name"],"getUser":["uuid"],"getPackage":["provider","slug"],"updateClusterById":["id","request"],"writeSettings":["setting"],"setDefaultUser":["uuid"],"getLogs":["id"],"getWorlds":["id"],"openMsaLogin":[],"getRunningProcessesByClusterId":["cluster_id"],"removeUser":["uuid"],"getMultiplePackages":["provider","slugs"],"updateClusterProfile":["name","profile"],"getPackageBody":["provider","body"],"getScreenshots":["id"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"getGameVersions":[]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}' }
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
getClusterById: (id: number) => Promise<ClusterModel | null>,
removeCluster: (id: number) => Promise<null>,
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
@ -270,12 +279,17 @@ getMultiplePackages: (provider: Provider, slugs: string[]) => Promise<ManagedPac
getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: number, limit: number) => Promise<Paginated<ManagedVersion>>,
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise<PackageModel>,
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]>,
installModpack: (modpack: ModpackArchive, clusterId: number) => Promise<null>,
fetchMinecraftProfile: (uuid: string) => Promise<MojangPlayerProfile>,
open: (input: string) => Promise<null> },
'oneclient': { openDevTools: () => Promise<void>,
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
openCluster: (folderName: string) => Promise<null> } };
openCluster: (folderName: string) => Promise<null> },
'oneclient': { openDevTools: () => Promise<void>,
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getModpacksFor: (clusterId: number) => Promise<ModpackArchive[]> } };
export type { InferCommandOutput }

View file

@ -1,4 +1,5 @@
export * from './AccountAvatar';
export * from './BrowserPackageItem';
export * from './GameBackground';
export * from './Loader';
export * from './LogViewer';

View file

@ -0,0 +1,14 @@
import type { OverlayProps } from './Overlay';
import { Overlay } from './Overlay';
export interface PolyBundleModalProps extends OverlayProps {
}
export function PolyBundleModal() {
return (
<Overlay.Dialog>
<Overlay.Title>test</Overlay.Title>
</Overlay.Dialog>
);
}

View file

@ -1,6 +1,5 @@
import type { Filters, PackageCategories, PackageType, Provider, Sort } from '@/bindings.gen';
import { LoaderSuspense, SheetPage } from '@/components';
import { PackageGrid } from '@/components/PackageItem';
import { LoaderSuspense, PackageGrid } from '@/components';
import { bindings } from '@/main';
import { browserCategories, categoryNameFromId } from '@/utils/browser';
import { useCommandSuspense, usePagination } from '@onelauncher/common';

View file

@ -1,7 +1,18 @@
use std::path::PathBuf;
use onelauncher_entity::loader::GameLoader;
use serde::{Deserialize, Serialize};
use crate::api::packages::data::{ExternalPackage, ManagedPackage, ManagedVersion};
use crate::api::packages::modpack::ModpackFormat;
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModpackArchive {
pub manifest: ModpackManifest,
pub path: PathBuf,
pub format: ModpackFormat,
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -16,7 +27,14 @@ pub struct ModpackManifest {
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ModpackFile {
pub struct ModpackFile {
pub enabled: bool,
pub kind: ModpackFileKind,
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ModpackFileKind {
Managed((ManagedPackage, ManagedVersion)),
External(ExternalPackage),
}

View file

@ -4,26 +4,47 @@ use std::sync::Arc;
use onelauncher_entity::package::PackageType;
use onelauncher_entity::{clusters, packages};
use sea_orm::ActiveValue::Set;
use serde::{Deserialize, Serialize};
use crate::api::packages::PackageError;
use crate::api::packages::modpack::data::ModpackArchive;
use crate::api::packages::modpack::mrpack::MrPackFormatImpl;
use crate::api::packages::modpack::polymrpack::PolyMrPackFormatImpl;
use crate::api::{self};
use crate::error::LauncherResult;
use crate::store::ingress::SubIngress;
pub mod data;
mod mrpack;
mod polymrpack;
#[cfg(test)]
mod tests;
#[onelauncher_macro::specta]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ModpackFormat {
CurseForge,
MrPack,
PolyMrPack,
}
const FORMAT_PIPELINE_PATH: &[LoaderFn<PathBuf>] = &[
PolyMrPackFormatImpl::from_file, // has to be first because of it being a **VALID** mrpack with extra stuff
MrPackFormatImpl::from_file,
];
const FORMAT_PIPELINE_BYTES: &[LoaderFn<Arc<Vec<u8>>>] = &[
PolyMrPackFormatImpl::from_bytes, // has to be first because of it being a **VALID** mrpack with extra stuff
MrPackFormatImpl::from_bytes,
];
type LoaderFn<Arg> = fn(
Arg,
) -> std::pin::Pin<
Box<dyn Future<Output = LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>> + Send>,
>;
#[async_trait::async_trait]
pub trait ModpackFormatExt {
async fn from_file(
@ -37,6 +58,15 @@ pub trait ModpackFormatExt {
) -> LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>
where
Self: Sized;
async fn install_modpack_archive(
modpack_archive: &ModpackArchive,
cluster: &clusters::Model,
skip_compatibility: Option<bool>,
ingress: Option<SubIngress<'_>>,
) -> LauncherResult<()>
where
Self: Sized;
}
#[async_trait::async_trait]
@ -52,17 +82,9 @@ pub trait InstallableModpackFormatExt: Send + Sync + std::any::Any {
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any + Send + Sync>;
}
type LoaderFn<Arg> = fn(
Arg,
) -> std::pin::Pin<
Box<dyn Future<Output = LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>> + Send>,
>;
impl ModpackFormat {
async fn from_file(path: PathBuf) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
let pipeline: Vec<LoaderFn<PathBuf>> = vec![MrPackFormatImpl::from_file];
for stage in pipeline {
pub async fn from_file(path: PathBuf) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
for stage in FORMAT_PIPELINE_PATH {
if let Some(format) = stage(path.clone()).await? {
return Ok(format);
}
@ -71,12 +93,10 @@ impl ModpackFormat {
Err(PackageError::UnsupportedModpackFormat.into())
}
async fn from_bytes(
pub async fn from_bytes(
bytes: Arc<Vec<u8>>,
) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
let pipeline: Vec<LoaderFn<Arc<Vec<u8>>>> = vec![MrPackFormatImpl::from_bytes];
for stage in pipeline {
for stage in FORMAT_PIPELINE_BYTES {
if let Some(format) = stage(Arc::clone(&bytes)).await? {
return Ok(format);
}
@ -84,6 +104,31 @@ impl ModpackFormat {
Err(PackageError::UnsupportedModpackFormat.into())
}
pub async fn install_modpack_archive(
&self,
modpack_archive: &ModpackArchive,
cluster: &clusters::Model,
skip_compatibility: Option<bool>,
ingress: Option<SubIngress<'_>>,
) -> LauncherResult<()> {
match self {
ModpackFormat::CurseForge => unimplemented!(),
ModpackFormat::MrPack => MrPackFormatImpl::install_modpack_archive(
modpack_archive,
cluster,
skip_compatibility,
ingress,
),
ModpackFormat::PolyMrPack => PolyMrPackFormatImpl::install_modpack_archive(
modpack_archive,
cluster,
skip_compatibility,
ingress,
),
}
.await
}
}
/// Installs a modpack to a cluster.

View file

@ -10,8 +10,10 @@ use serde::Deserialize;
use tokio::sync::OnceCell;
use crate::api::cluster::ClusterError;
use crate::api::packages::data::{ExternalPackage, PackageSide};
use crate::api::packages::modpack::data::{ModpackFile, ModpackManifest};
use crate::api::packages::data::{ExternalPackage, ManagedVersion, PackageSide};
use crate::api::packages::modpack::data::{
ModpackArchive, ModpackFile, ModpackFileKind, ModpackManifest,
};
use crate::api::packages::modpack::{InstallableModpackFormatExt, ModpackFormatExt};
use crate::api::packages::provider::ProviderExt;
use crate::api::{self};
@ -21,17 +23,15 @@ use crate::utils::DatabaseModelExt;
use crate::utils::io::{self, IOError};
pub struct MrPackFormatImpl {
archive: Option<PathBuf>,
raw_manifest: MrPackManifest,
manifest: OnceCell<ModpackManifest>,
mc_version: String,
loader: GameLoader,
loader_version: String,
pub(super) archive: Option<PathBuf>,
pub(super) raw_manifest: MrPackManifest,
pub(super) manifest: OnceCell<ModpackManifest>,
pub(super) mc_version: String,
pub(super) loader: GameLoader,
pub(super) loader_version: String,
}
const MODRINTH_URL_PREFIX: &str = "https://cdn.modrinth.com/";
// TODO: move from_file fetching to manifest function
pub(super) const MODRINTH_URL_PREFIX: &str = "https://cdn.modrinth.com/";
#[async_trait::async_trait]
impl ModpackFormatExt for MrPackFormatImpl {
@ -112,6 +112,23 @@ impl ModpackFormatExt for MrPackFormatImpl {
loader_version: loader_version.unwrap(),
})))
}
async fn install_modpack_archive(
modpack_archive: &ModpackArchive,
cluster: &clusters::Model,
skip_compatibility: Option<bool>,
ingress: Option<SubIngress<'_>>,
) -> LauncherResult<()>
where
Self: Sized,
{
let ModpackArchive { manifest, path, .. } = modpack_archive;
download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress).await?;
copy_overrides_folder(cluster, &path, &ingress).await?;
Ok(())
}
}
#[async_trait::async_trait]
@ -128,79 +145,7 @@ impl InstallableModpackFormatExt for MrPackFormatImpl {
return Ok(manifest);
}
let mut project_ids_to_fetch: Vec<String> = Vec::new();
let mut version_ids_to_fetch: Vec<String> = Vec::new();
let mut files: Vec<ModpackFile> = Vec::new();
for file in &self.raw_manifest.files {
let name = file
.path
.split('/')
.last()
.unwrap_or(&file.path)
.to_string();
if let Some(url) = file
.downloads
.iter()
.find(|url| url.starts_with(MODRINTH_URL_PREFIX))
{
// https://cdn.modrinth.com/data/<project_id>/versions/<version_id>/<file_name>
let paths = url[MODRINTH_URL_PREFIX.len()..]
.split('/')
.collect::<Vec<_>>();
if paths.len() >= 4 {
let project_id = paths[1];
let version_id = paths[3];
project_ids_to_fetch.push(project_id.to_string());
version_ids_to_fetch.push(version_id.to_string())
} else {
tracing::error!("invalid modrinth file URL: '{}'", url);
}
} else {
let download_url = file
.downloads
.first()
.cloned()
.ok_or_else(|| {
tracing::warn!("mrpack file '{}' does not contain a download URL", name)
})
.unwrap_or(String::new());
// the path usually contains the folder name such as "mods" or "resourcepacks"
// so we can use it to determine the package type
let package_type = file
.path
.split('/')
.next()
.and_then(|s| PackageType::try_from(s).ok())
.unwrap_or(PackageType::Mod);
files.push(ModpackFile::External(ExternalPackage {
name,
url: download_url,
sha1: file.hashes.sha1.clone(),
size: file.file_size,
package_type,
}));
}
}
let managed_packages = Provider::Modrinth
.get_multiple(&project_ids_to_fetch)
.await?;
let managed_versions = Provider::Modrinth
.get_versions(&version_ids_to_fetch)
.await?;
files.extend(
managed_packages
.into_iter()
.zip(managed_versions.into_iter())
.map(|(package, version)| ModpackFile::Managed((package, version))),
);
let files = to_modpack_files(&self.raw_manifest.files).await?;
let manifest = ModpackManifest {
name: self.raw_manifest.name.clone(),
@ -225,91 +170,217 @@ impl InstallableModpackFormatExt for MrPackFormatImpl {
) -> LauncherResult<()> {
let manifest = self.manifest().await?;
if cluster.mc_version != manifest.mc_version {
return Err(ClusterError::MismatchedVersion(
manifest.mc_version.clone(),
cluster.mc_version.clone(),
)
.into());
}
download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress).await?;
if cluster.mc_loader != manifest.loader {
return Err(ClusterError::MismatchedLoader(
manifest.loader.clone(),
cluster.mc_loader.clone(),
)
.into());
}
// TODO: Implement loader version checking
// if cluster.mc_loader_version.is_some_and(|v| v )
let mut errors = Vec::new();
let mut packages_to_link = Vec::new();
// TODO: Ingress
for file in manifest.files.iter() {
match file {
ModpackFile::Managed((package, version)) => {
match api::packages::download_package(package, version, None, None).await {
Ok(model) => packages_to_link.push(model),
Err(e) => errors.push(e),
}
}
ModpackFile::External(package) => {
if let Err(e) = api::packages::download_external_package(
package,
cluster,
None,
skip_compatibility,
None,
)
.await
{
errors.push(e);
}
}
}
}
let linked = api::packages::link_many_packages_to_cluster(
&packages_to_link,
cluster,
skip_compatibility,
)
.await?;
if linked < packages_to_link.len() as u64 {
tracing::warn!("not all packages could be linked to the cluster, some errors occurred");
}
if let Some(archive) = &self.archive {
tracing::debug!(
"extracting overrides from modpack archive: {}",
archive.display()
);
let dest = cluster.path().await?;
io::extract_zip_filtered(
archive,
dest,
Some(|entry: &async_zip::StoredZipEntry| {
entry
.filename()
.as_str()
.is_ok_and(|s| s.starts_with("overrides/"))
}),
Some(|name: &str| name.trim_start_matches("overrides/").to_string()),
)
.await?;
if let Some(path) = self.archive.as_ref() {
copy_overrides_folder(cluster, path, &ingress).await?;
}
Ok(())
}
}
pub(super) async fn download_and_link_packages(
cluster: &clusters::Model,
manifest: &ModpackManifest,
skip_compatibility: Option<bool>,
_ingress: &Option<SubIngress<'_>>,
) -> LauncherResult<()> {
if cluster.mc_version != manifest.mc_version {
return Err(ClusterError::MismatchedVersion(
manifest.mc_version.clone(),
cluster.mc_version.clone(),
)
.into());
}
if cluster.mc_loader != manifest.loader {
return Err(ClusterError::MismatchedLoader(
manifest.loader.clone(),
cluster.mc_loader.clone(),
)
.into());
}
// TODO: Implement loader version checking
// if cluster.mc_loader_version.is_some_and(|v| v )
let mut errors = Vec::new();
let mut packages_to_link = Vec::new();
// TODO: Ingress
for file in manifest.files.iter() {
match &file.kind {
ModpackFileKind::Managed((package, version)) => {
match api::packages::download_package(package, version, None, None).await {
Ok(model) => packages_to_link.push(model),
Err(e) => errors.push(e),
}
}
ModpackFileKind::External(package) => {
if let Err(e) = api::packages::download_external_package(
package,
cluster,
None,
skip_compatibility,
None,
)
.await
{
errors.push(e);
}
}
}
}
let linked = api::packages::link_many_packages_to_cluster(
&packages_to_link,
cluster,
skip_compatibility,
)
.await?;
if linked < packages_to_link.len() as u64 {
tracing::warn!("not all packages could be linked to the cluster, some errors occurred");
}
Ok(())
}
pub(super) async fn copy_overrides_folder(
cluster: &clusters::Model,
archive_path: &PathBuf,
_ingress: &Option<SubIngress<'_>>,
) -> LauncherResult<()> {
tracing::debug!(
"extracting overrides from modpack archive: {}",
archive_path.display()
);
let dest = cluster.path().await?;
io::extract_zip_filtered(
archive_path,
dest,
Some(|entry: &async_zip::StoredZipEntry| {
entry
.filename()
.as_str()
.is_ok_and(|s| s.starts_with("overrides/"))
}),
Some(|name: &str| name.trim_start_matches("overrides/").to_string()),
)
.await?;
Ok(())
}
async fn to_modpack_files(mrpack_files: &Vec<MrPackFile>) -> LauncherResult<Vec<ModpackFile>> {
struct ToFetch {
project_id: String,
version_id: String,
}
let mut to_fetch: Vec<ToFetch> = Vec::new();
let mut files: Vec<ModpackFile> = Vec::new();
for file in mrpack_files {
let name = file
.path
.split('/')
.last()
.unwrap_or(&file.path)
.to_string();
if let Some(url) = file
.downloads
.iter()
.find(|url| url.starts_with(MODRINTH_URL_PREFIX))
{
// https://cdn.modrinth.com/data/<project_id>/versions/<version_id>/<file_name>
let paths = url[MODRINTH_URL_PREFIX.len()..]
.split('/')
.collect::<Vec<_>>();
if paths.len() >= 4 {
let project_id = paths[1];
let version_id = paths[3];
to_fetch.push(ToFetch {
project_id: project_id.to_string(),
version_id: version_id.to_string(),
});
} else {
tracing::error!("invalid modrinth file URL: '{}'", url);
}
} else {
let download_url = file
.downloads
.first()
.cloned()
.ok_or_else(|| {
tracing::warn!("mrpack file '{}' does not contain a download URL", name)
})
.unwrap_or(String::new());
// the path usually contains the folder name such as "mods" or "resourcepacks"
// so we can use it to determine the package type
let package_type = file
.path
.split('/')
.next()
.and_then(|s| PackageType::try_from(s).ok())
.unwrap_or(PackageType::Mod);
files.push(ModpackFile {
kind: ModpackFileKind::External(ExternalPackage {
name,
url: download_url,
sha1: file.hashes.sha1.clone(),
size: file.file_size,
package_type,
}),
enabled: true,
});
}
}
let managed_packages = Provider::Modrinth
.get_multiple(
&to_fetch
.iter()
.map(|f| f.project_id.clone())
.collect::<Vec<_>>(),
)
.await?;
let managed_versions = Provider::Modrinth
.get_versions(
&to_fetch
.iter()
.map(|f| f.version_id.clone())
.collect::<Vec<_>>(),
)
.await?;
let mut version_map = managed_versions
.into_iter()
.map(|v| (v.project_id.clone(), v))
.collect::<HashMap<String, ManagedVersion>>();
for fetched_pkg in managed_packages {
if let Some(version) = version_map.remove(&fetched_pkg.id) {
files.push(ModpackFile {
kind: ModpackFileKind::Managed((fetched_pkg, version)),
enabled: true,
});
} else {
tracing::error!("no version found for managed package '{}'", fetched_pkg.id);
}
}
Ok(files)
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MrPackManifest {
pub(super) struct MrPackManifest {
pub format_version: usize,
pub version_id: String,
pub name: String,
@ -319,7 +390,7 @@ struct MrPackManifest {
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MrPackFile {
pub(super) struct MrPackFile {
pub path: String,
pub hashes: MrPackFileHash,
#[serde(default)]
@ -330,14 +401,14 @@ struct MrPackFile {
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MrPackFileHash {
pub(super) struct MrPackFileHash {
pub sha1: String,
// pub sha512: String,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MrPackFileEnv {
pub(super) struct MrPackFileEnv {
pub client: PackageSide,
pub server: PackageSide,
}

View file

@ -0,0 +1,322 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use onelauncher_entity::clusters;
use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::package::{PackageType, Provider};
use serde::Deserialize;
use tokio::sync::OnceCell;
use crate::api::packages::data::{ExternalPackage, ManagedVersion};
use crate::api::packages::modpack::data::{
ModpackArchive, ModpackFile, ModpackFileKind, ModpackManifest,
};
use crate::api::packages::modpack::mrpack::MrPackFile;
use crate::api::packages::modpack::{InstallableModpackFormatExt, ModpackFormatExt};
use crate::api::packages::provider::ProviderExt;
use crate::error::LauncherResult;
use crate::store::ingress::SubIngress;
use crate::utils::io::{self, IOError};
pub struct PolyMrPackFormatImpl {
pub(super) archive: Option<PathBuf>,
pub(super) raw_manifest: PolyMrPackManifest,
pub(super) manifest: OnceCell<ModpackManifest>,
pub(super) mc_version: String,
pub(super) loader: GameLoader,
pub(super) loader_version: String,
}
#[async_trait::async_trait]
impl ModpackFormatExt for PolyMrPackFormatImpl {
async fn from_file(
path: std::path::PathBuf,
) -> LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>
where
Self: Sized,
{
let zip_file = tokio::fs::File::open(path.clone())
.await
.map_err(IOError::from)?;
let buf_reader = tokio::io::BufReader::new(zip_file);
let manifest_file = io::try_read_zip_entry_bytes(buf_reader, "modrinth.index.json").await?;
let Some(this) = Self::from_bytes(Arc::new(manifest_file)).await? else {
return Ok(None);
};
let mut this = this
.as_any()
.downcast::<Self>()
.expect("downcast failed for MrPackFormatImpl");
this.archive = Some(path);
Ok(Some(this))
}
async fn from_bytes(
bytes: Arc<Vec<u8>>,
) -> LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>
where
Self: Sized,
{
let serialized: PolyMrPackManifest = match serde_json::from_slice(&bytes) {
Ok(manifest) => manifest,
Err(e) => {
tracing::debug!("failed to deserialize modpack as polymrpack: {}", e);
return Ok(None);
}
};
let mut mc_version: Option<String> = None;
let mut loader: Option<GameLoader> = None;
let mut loader_version: Option<String> = None;
for (key, value) in serialized.dependencies.iter() {
if key == "minecraft" {
mc_version = Some(value.clone());
} else {
loader = GameLoader::from_str(key).ok();
loader_version = Some(value.clone());
}
}
if mc_version.is_none() {
tracing::error!("polymrpack manifest does not contain a minecraft version");
return Ok(None);
}
if loader.is_none() {
tracing::error!("polymrpack manifest does not contain a valid game loader");
return Ok(None);
}
if loader_version.is_none() {
tracing::error!("polymrpack manifest does not contain a valid loader version");
return Ok(None);
}
Ok(Some(Box::new(Self {
archive: None,
raw_manifest: serialized,
manifest: OnceCell::new(),
mc_version: mc_version.unwrap(),
loader: loader.unwrap(),
loader_version: loader_version.unwrap(),
})))
}
async fn install_modpack_archive(
modpack_archive: &ModpackArchive,
cluster: &clusters::Model,
skip_compatibility: Option<bool>,
ingress: Option<SubIngress<'_>>,
) -> LauncherResult<()>
where
Self: Sized,
{
let ModpackArchive { manifest, path, .. } = modpack_archive;
super::mrpack::download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress)
.await?;
super::mrpack::copy_overrides_folder(cluster, &path, &ingress).await?;
Ok(())
}
}
#[async_trait::async_trait]
impl InstallableModpackFormatExt for PolyMrPackFormatImpl {
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any + Send + Sync>
where
Self: Sized,
{
self
}
async fn manifest(&self) -> LauncherResult<&ModpackManifest> {
if let Some(manifest) = self.manifest.get() {
return Ok(manifest);
}
let files = to_modpack_files(&self.raw_manifest.files).await?;
let manifest = ModpackManifest {
name: self.raw_manifest.name.clone(),
version: self.raw_manifest.version_id.clone(),
mc_version: self.mc_version.clone(),
loader: self.loader.clone(),
loader_version: self.loader_version.clone(),
files,
};
self.manifest
.set(manifest)
.expect("failed to cache inner modpack manifest");
Ok(self.manifest.get().unwrap())
}
async fn install_to(
&self,
cluster: &clusters::Model,
skip_compatibility: Option<bool>,
ingress: Option<SubIngress<'_>>,
) -> LauncherResult<()> {
let manifest = self.manifest().await?;
super::mrpack::download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress)
.await?;
if let Some(path) = self.archive.as_ref() {
super::mrpack::copy_overrides_folder(cluster, path, &ingress).await?;
}
Ok(())
}
}
async fn to_modpack_files(mrpack_files: &Vec<PolyMrPackFile>) -> LauncherResult<Vec<ModpackFile>> {
struct FetchedPackage {
version_id: String,
enabled: bool,
}
let mut to_fetch: HashMap<String, FetchedPackage> = HashMap::new();
let mut files: Vec<ModpackFile> = Vec::new();
for file in mrpack_files {
if file.hidden {
continue;
}
let name = file
.base
.path
.split('/')
.last()
.unwrap_or(&file.base.path)
.to_string();
if let Some(url) = file
.base
.downloads
.iter()
.find(|url| url.starts_with(super::mrpack::MODRINTH_URL_PREFIX))
{
// https://cdn.modrinth.com/data/<project_id>/versions/<version_id>/<file_name>
let paths = url[super::mrpack::MODRINTH_URL_PREFIX.len()..]
.split('/')
.collect::<Vec<_>>();
if paths.len() >= 4 {
let project_id = paths[1];
let version_id = paths[3];
to_fetch.insert(
project_id.to_string(),
FetchedPackage {
version_id: version_id.to_string(),
enabled: file.enabled,
},
);
} else {
tracing::error!("invalid modrinth file URL: '{}'", url);
}
} else {
let download_url = file
.base
.downloads
.first()
.cloned()
.ok_or_else(|| {
tracing::warn!("mrpack file '{}' does not contain a download URL", name)
})
.unwrap_or(String::new());
// the path usually contains the folder name such as "mods" or "resourcepacks"
// so we can use it to determine the package type
let package_type = file
.base
.path
.split('/')
.next()
.and_then(|s| PackageType::try_from(s).ok())
.unwrap_or(PackageType::Mod);
files.push(ModpackFile {
enabled: file.enabled,
kind: ModpackFileKind::External(ExternalPackage {
name,
url: download_url,
sha1: file.base.hashes.sha1.clone(),
size: file.base.file_size,
package_type,
}),
});
}
}
let managed_packages = Provider::Modrinth
.get_multiple(&to_fetch.keys().cloned().collect::<Vec<_>>())
.await?;
let managed_versions = Provider::Modrinth
.get_versions(
&to_fetch
.values()
.map(|f| f.version_id.clone())
.collect::<Vec<_>>(),
)
.await?;
let mut version_map = managed_versions
.into_iter()
.map(|v| (v.project_id.clone(), v))
.collect::<HashMap<String, ManagedVersion>>();
for fetched_pkg in managed_packages {
if let Some(version) = version_map.remove(&fetched_pkg.id) {
let enabled = to_fetch
.get(&fetched_pkg.id)
.map(|f| f.enabled)
.unwrap_or(true);
files.push(ModpackFile {
enabled,
kind: ModpackFileKind::Managed((fetched_pkg, version)),
});
} else {
tracing::error!("no version found for managed package '{}'", fetched_pkg.id);
}
}
Ok(files)
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct PolyMrPackManifest {
pub id: String,
pub category: String,
pub enabled: bool,
#[serde(rename = "polyFormat")]
pub poly_format_version: i32,
pub update_url: Option<String>,
pub format_version: usize,
pub version_id: String,
pub name: String,
pub files: Vec<PolyMrPackFile>,
pub dependencies: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct PolyMrPackFile {
#[serde(flatten)]
pub base: MrPackFile,
pub enabled: bool,
pub hidden: bool,
}

View file

@ -2,6 +2,7 @@ use crate::api::packages::data::{
ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery,
SearchResult,
};
use crate::api::packages::modpack::data::ModpackArchive;
use crate::api::packages::provider::ProviderExt;
use crate::store::processes::Process;
use crate::store::{Settings, State};
@ -26,6 +27,7 @@ use onelauncher_entity::prelude::model::*;
#[taurpc::procedures(path = "core")]
pub trait TauriLauncherApi {
// MARK: API: clusters
// region: clusters
#[taurpc(alias = "getClusters")]
async fn get_clusters() -> LauncherResult<Vec<Cluster>>;
@ -55,8 +57,10 @@ pub trait TauriLauncherApi {
#[taurpc(alias = "getLogByName")]
async fn get_log_by_name(id: ClusterId, name: String) -> LauncherResult<Option<String>>;
// endregion: clusters
// MARK: API: processes
// region: processes
#[taurpc(alias = "getRunningProcesses")]
async fn get_running_processes() -> LauncherResult<Vec<Process>>;
@ -70,8 +74,10 @@ pub trait TauriLauncherApi {
#[taurpc(alias = "killProcess")]
async fn kill_process(pid: u32) -> LauncherResult<()>;
// endregion: processes
// MARK: API: profiles
// region: profiles
#[taurpc(alias = "getProfileOrDefault")]
async fn get_profile_or_default(name: Option<String>) -> LauncherResult<SettingsProfile>;
@ -83,15 +89,19 @@ pub trait TauriLauncherApi {
name: String,
profile: ProfileUpdate,
) -> LauncherResult<SettingsProfile>;
// endregion: profiles
// MARK: API: metadata
// region: metadata
#[taurpc(alias = "getGameVersions")]
async fn get_game_versions() -> LauncherResult<Vec<Version>>;
#[taurpc(alias = "getLoadersForVersion")]
async fn get_loaders_for_version(mc_version: String) -> LauncherResult<Vec<GameLoader>>;
// endregion: metadata
// MARK: API: users
// region: users
#[taurpc(alias = "getUsers")]
async fn get_users() -> LauncherResult<Vec<MinecraftCredentials>>;
@ -113,15 +123,19 @@ pub trait TauriLauncherApi {
async fn open_msa_login<R: Runtime>(
app_handle: AppHandle<R>,
) -> LauncherResult<Option<MinecraftCredentials>>;
// endregion: users
// MARK: API: settings
// region: settings
#[taurpc(alias = "readSettings")]
async fn read_settings() -> LauncherResult<Settings>;
#[taurpc(alias = "writeSettings")]
async fn write_settings(setting: Settings) -> LauncherResult<()>;
// endregion: settings
// MARK: API: packages
// MARK: API: package
// region: package
#[taurpc(alias = "searchPackages")]
async fn search_packages(
provider: Provider,
@ -170,6 +184,13 @@ pub trait TauriLauncherApi {
provider: Provider,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>>;
// endregion: package
// MARK: API: modpack
// region: modpack
#[taurpc(alias = "installModpack")]
async fn install_modpack(modpack: ModpackArchive, cluster_id: ClusterId) -> LauncherResult<()>;
// endregion: modpack
// MARK: API: minecraft
#[taurpc(alias = "fetchMinecraftProfile")]
@ -214,6 +235,7 @@ pub struct TauriLauncherApiImpl;
#[taurpc::resolvers]
impl TauriLauncherApi for TauriLauncherApiImpl {
// MARK: Impl: clusters
// region: clusters
async fn get_clusters(self) -> LauncherResult<Vec<Cluster>> {
api::cluster::dao::get_all_clusters().await
}
@ -340,8 +362,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
api::cluster::content::get_log_by_name(&cluster, &name).await
}
// endregion: clusters
// MARK: Impl: processes
// region: processes
async fn get_running_processes(self) -> LauncherResult<Vec<Process>> {
api::processes::get_running_processes().await
}
@ -360,8 +384,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
async fn kill_process(self, pid: u32) -> LauncherResult<()> {
api::processes::kill_process(pid).await
}
// endregion: processes
// MARK: Impl: profiles
// region: profiles
async fn get_global_profile(self) -> SettingsProfile {
api::setting_profiles::get_global_profile().await
}
@ -418,8 +444,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(profile)
}
// endregion: profiles
// MARK: Impl: metadata
// region: metadata
async fn get_loaders_for_version(self, mc_version: String) -> LauncherResult<Vec<GameLoader>> {
api::game::metadata::get_loaders_for_version(&mc_version).await
}
@ -427,8 +455,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
async fn get_game_versions(self) -> LauncherResult<Vec<Version>> {
api::game::metadata::get_game_versions().await
}
// endregion: metadata
// MARK: Impl: users
// region: users
async fn get_users(self) -> LauncherResult<Vec<MinecraftCredentials>> {
api::credentials::get_users().await
}
@ -516,7 +546,17 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(None)
}
async fn get_users_from_author(
self,
provider: Provider,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>> {
provider.get_users_from_author(author).await
}
// endregion: users
// MARK: Impl: settings
// region: settings
async fn read_settings(self) -> LauncherResult<Settings> {
let state = State::get().await?;
let settings = state.settings.read().await;
@ -534,8 +574,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(())
}
// endregion: settings
// MARK: Impl: packages
// MARK: Impl: package
// region: package
async fn search_packages(
self,
provider: Provider,
@ -604,26 +646,41 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(model)
}
// endregion: package
async fn get_users_from_author(
// MARK: Impl: modpack
// region: modpack
async fn install_modpack(
self,
provider: Provider,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>> {
provider.get_users_from_author(author).await
modpack: ModpackArchive,
cluster_id: ClusterId,
) -> LauncherResult<()> {
let cluster = api::cluster::dao::get_cluster_by_id(cluster_id)
.await?
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", cluster_id))?;
modpack
.format
.install_modpack_archive(&modpack, &cluster, None, None)
.await
}
// endregion: modpack
// MARK: Impl: minecraft
// region: minecraft
async fn fetch_player_profile(
self,
uuid: String,
) -> LauncherResult<crate::utils::minecraft::MojangPlayerProfile> {
crate::utils::minecraft::fetch_player_profile(&uuid).await
}
// endregion: minecraft
// MARK: Impl: Other
// region: other
async fn open(self, input: String) -> LauncherResult<()> {
opener::open(input)?;
Ok(())
}
// endregion: other
}

View file

@ -11,7 +11,7 @@ use tokio::sync::Semaphore;
use tokio_stream::StreamExt;
use crate::api::ingress::IngressSendExt;
use crate::error::LauncherResult;
use crate::error::{LauncherError, LauncherResult};
use crate::store::Core;
use crate::store::ingress::{SubIngress, SubIngressExt};
use crate::store::semaphore::SemaphoreStore;
@ -49,6 +49,48 @@ pub(crate) static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
.expect("failed to build reqwest client!")
});
pub async fn request(
method: Method,
url: &str,
builder: Option<impl Fn(reqwest::RequestBuilder) -> reqwest::Result<reqwest::Request>>,
) -> LauncherResult<reqwest::Response> {
let semaphore = SemaphoreStore::fetch().await;
let permit = semaphore.acquire().await;
let client = &REQWEST_CLIENT;
let mut attempt = 0;
let res = loop {
attempt += 1;
let req = client.request(method.clone(), url);
let req = match builder {
Some(ref builder) => builder(req),
None => req.build(),
}
.map_err(LauncherError::from)?;
match client.execute(req).await {
Err(err) => {
if attempt <= Core::get().fetch_attempts {
tracing::error!(
"error occurred whilst fetching on attempt {attempt}: {err} retrying request..."
);
continue;
}
return Err(err.into());
}
Ok(res) => break res,
}
};
// Drop the fetch permit as we are no longer fetching
drop(permit);
Ok(res)
}
#[tracing::instrument(level = "debug", skip(ingress))]
#[onelauncher_macro::pin]
pub async fn fetch_advanced(

View file

@ -1,5 +1,5 @@
[toolchain]
channel = "nightly-2025-07-23"
channel = "nightly-2025-09-12"
profile = "minimal"
components = [
"cargo",