feat: unified package categories + curseforge implementation

This commit is contained in:
LynithDev 2025-08-02 16:51:28 +02:00
parent 554e51d543
commit 6841051b7a
25 changed files with 1932 additions and 268 deletions

View file

@ -27,6 +27,10 @@
"rust-analyzer.diagnostics.disabled": [
"macro-error" // TODO: Remove this once taurpc upgrades to latest specta
],
"rust-analyzer.assist.preferSelf": true,
"rust-analyzer.procMacro.ignored": {
"async-trait": ["async_trait"]
},
"rust-analyzer.check.command": "check",
"rust-analyzer.diagnostics.experimental.enable": false,
"rust-analyzer.linkedProjects": [],

5
Cargo.lock generated
View file

@ -3438,6 +3438,7 @@ dependencies = [
"sea-orm",
"serde",
"serde_json_path_to_error",
"serde_repr",
"serde_with",
"sha1",
"sha2",
@ -5034,9 +5035,9 @@ dependencies = [
[[package]]
name = "serde_repr"
version = "0.1.19"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",

View file

@ -90,6 +90,7 @@ serde_with = { version = "=3.12.0", features = [
"json",
"macros"
] }
serde_repr = { version = "=0.1.20" }
toml = { version = "0.8" }
url = { version = "2.5" }
uuid = { version = "1.16", features = [

View file

@ -14,19 +14,17 @@ authors = { workspace = true }
path = "src/lib.rs"
[features]
default = [ "tauri" ]
default = [ ]
tauri = [
"dep:tauri",
"dep:taurpc",
"specta",
"gui",
"onelauncher_macro/tauri",
]
specta = [
"dep:specta",
"interpulse/specta",
"onelauncher_entity/specta",
"onelauncher_macro/specta",
]
cli = [ "dep:indicatif" ]
gui = []
@ -51,6 +49,7 @@ tracing-appender = { workspace = true }
serde = { workspace = true }
serde_json = { package = "serde_json_path_to_error", path = "../serde_json_path_to_error" }
serde_with = { workspace = true }
serde_repr = { workspace = true }
# utils
sea-orm = { workspace = true }

View file

@ -0,0 +1,227 @@
use onelauncher_entity::package::PackageType;
use serde::{Deserialize, Serialize};
use crate::api::packages::provider::ProviderExt;
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageCategories {
Mod(Vec<PackageModCategory>),
ResourcePack(Vec<PackageResourcePackCategory>),
Shader(Vec<PackageShaderCategory>),
DataPack(Vec<PackageModCategory>),
ModPack(Vec<PackageModPackCategory>),
}
pub trait ToProviderCategory<Out, Provider>
where
Provider: ProviderExt,
{
fn as_mod_out(category: &PackageModCategory) -> Out;
fn to_mod(value: &Out) -> Option<PackageModCategory>;
fn as_resource_pack_out(category: &PackageResourcePackCategory) -> Out;
fn to_resource_pack(value: &Out) -> Option<PackageResourcePackCategory>;
fn as_shader_out(category: &PackageShaderCategory) -> Out;
fn to_shader(value: &Out) -> Option<PackageShaderCategory>;
fn as_mod_pack_out(category: &PackageModPackCategory) -> Out;
fn to_mod_pack(value: &Out) -> Option<PackageModPackCategory>;
fn as_data_pack_out(category: &PackageModCategory) -> Out {
Self::as_mod_out(category)
}
fn to_data_pack(value: &Out) -> Option<PackageModCategory> {
Self::to_mod(value)
}
fn as_out(categories: &PackageCategories) -> Vec<Out> {
match categories {
PackageCategories::Mod(categories) => {
categories.into_iter().map(Self::as_mod_out).collect()
}
PackageCategories::ResourcePack(categories) => categories
.into_iter()
.map(Self::as_resource_pack_out)
.collect(),
PackageCategories::Shader(categories) => {
categories.into_iter().map(Self::as_shader_out).collect()
}
PackageCategories::DataPack(categories) => {
categories.into_iter().map(Self::as_data_pack_out).collect()
}
PackageCategories::ModPack(categories) => {
categories.into_iter().map(Self::as_mod_pack_out).collect()
}
}
}
fn to_list(package_type: &PackageType, values: &Vec<Out>) -> PackageCategories {
match package_type {
&PackageType::Mod => {
PackageCategories::Mod(values.iter().filter_map(|v| Self::to_mod(v)).collect())
}
&PackageType::ResourcePack => PackageCategories::ResourcePack(
values
.iter()
.filter_map(|v| Self::to_resource_pack(v))
.collect(),
),
&PackageType::Shader => PackageCategories::Shader(
values.iter().filter_map(|v| Self::to_shader(v)).collect(),
),
&PackageType::DataPack => PackageCategories::DataPack(
values
.iter()
.filter_map(|v| Self::to_data_pack(v))
.collect(),
),
&PackageType::ModPack => PackageCategories::ModPack(
values.iter().filter_map(|v| Self::to_mod_pack(v)).collect(),
),
}
}
}
macro_rules! define_category_mapping {
(
impl $trait_name:ident <$out:ty, $provider:ty> for $struct_name:ty {
$(
$enum_type:ty as $fn_name:ident => $reverse_fn:ident {
$($variant:ident => $value:expr),* $(,)?
}
)*
}
) => {
impl $trait_name<$out, $provider> for $struct_name {
$(
fn $fn_name(category: &$enum_type) -> $out {
match category {
$(
<$enum_type>::$variant => $value,
)*
}
}
)*
$(
fn $reverse_fn(value: &$out) -> Option<$enum_type> {
match value {
$(
v if v == &$value => Some(<$enum_type>::$variant),
)*
_ => None,
}
}
)*
}
};
}
pub(super) use define_category_mapping;
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageModCategory {
Adventure,
Library,
Equipment,
Patches,
Cosmetic,
Food,
Magic,
Information,
Misc,
Performance,
Redstone,
ServerUtil,
Storage,
Technology,
Farming,
Automation,
Transport,
Utility,
QoL,
WorldGen,
Mobs,
Economy,
Social,
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageResourcePackCategory {
X8,
X16,
X32,
X48,
X64,
X128,
X256,
X512,
VanillaLike,
Utility,
Tweaks,
Themed,
Simplistic,
Realistic,
Modded,
Decoration,
Cursed,
Combat,
Audio,
Blocks,
CoreShaders,
Gui,
Fonts,
Equipment,
Environment,
Entities,
Items,
Locale,
Models,
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageShaderCategory {
VanillaLike,
SemiRealistic,
Realistic,
Fantasy,
Cursed,
Cartoon,
Bloom,
Atmosphere,
Reflections,
Shadows,
PBR,
PathTracing,
Foliage,
ColoredLightning,
Potato,
Low,
Medium,
High,
Ultra,
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageModPackCategory {
Technology,
Quests,
Optimization,
Multiplayer,
Magic,
LightWeight,
Combat,
Challenging,
Adventure,
}

View file

@ -4,6 +4,8 @@ use onelauncher_entity::package::{PackageType, Provider};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use crate::api::packages::categories::PackageCategories;
// Divisible by 4 and 3
pub const DEFAULT_LIMIT: usize = 24;
@ -13,8 +15,8 @@ pub const DEFAULT_LIMIT: usize = 24;
pub struct Filters {
pub game_versions: Option<Vec<String>>,
pub loaders: Option<Vec<GameLoader>>,
pub categories: Option<Vec<String>>,
pub package_types: Option<Vec<PackageType>>,
pub categories: Option<PackageCategories>,
pub package_type: Option<PackageType>,
}
#[onelauncher_macro::specta]
@ -53,28 +55,26 @@ pub struct SearchQuery {
}
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SearchResult {
pub project_id: String,
pub project_type: String,
pub package_type: PackageType,
pub slug: String,
pub author: String,
pub title: String,
pub description: String,
pub categories: Vec<String>,
pub display_categories: Vec<String>,
pub versions: Vec<String>,
pub categories: PackageCategories,
pub mc_versions: Vec<String>,
pub downloads: usize,
pub icon_url: String,
pub date_created: DateTime<Utc>,
pub date_modified: DateTime<Utc>,
pub latest_version: String,
pub license: String,
pub license: Option<String>,
pub client_side: PackageSide,
pub server_side: PackageSide,
/// List of URLs to images
pub gallery: Vec<String>,
// pub color: i64,
}
#[onelauncher_macro::specta]
@ -87,7 +87,9 @@ pub struct ManagedPackage {
pub package_type: PackageType,
pub name: String,
pub short_desc: String,
pub body: String,
pub body: ManagedPackageBody,
/// Won't have all versions for some providers, like CurseForge.
/// Try making a request to get the versions if needed
pub version_ids: Vec<String>,
pub mc_versions: Vec<String>,
#[serde_as(as = "serde_with::VecSkipError<_>")]
@ -97,15 +99,36 @@ pub struct ManagedPackage {
pub updated: DateTime<Utc>,
pub client: PackageSide,
pub server: PackageSide,
pub categories: Vec<String>,
pub categories: PackageCategories,
pub license: Option<PackageLicense>,
pub author: PackageAuthor,
pub links: PackageLinks,
pub status: PackageStatus,
pub downloads: usize,
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub gallery: Vec<PackageGallery>,
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ManagedPackageBody {
Url(String),
Raw(String),
}
impl std::fmt::Display for ManagedPackageBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
ManagedPackageBody::Url(_) => "Url",
ManagedPackageBody::Raw(_) => "Raw",
}
)
}
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManagedUser {
@ -173,6 +196,25 @@ pub enum PackageAuthor {
Users(Vec<ManagedUser>),
}
impl std::fmt::Display for PackageAuthor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Team { team_id, org_id } => {
if let Some(org_id) = org_id {
format!("Team({} - {})", team_id, org_id)
} else {
format!("Team({})", team_id)
}
}
Self::Users(users) => format!("Users({})", users.len()),
}
)
}
}
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]

View file

@ -1,9 +1,10 @@
use data::{ManagedPackage, ManagedVersion};
use onelauncher_entity::icon::Icon;
use onelauncher_entity::package::PackageType;
use onelauncher_entity::package::{PackageType, Provider};
use onelauncher_entity::{clusters, packages};
use reqwest::Method;
use crate::api::packages::data::{ManagedPackageBody, PackageAuthor};
use crate::error::{LauncherError, LauncherResult};
use crate::store::Dirs;
use crate::store::ingress::SubIngress;
@ -11,13 +12,11 @@ use crate::utils::crypto::HashAlgorithm;
use crate::utils::io::IOError;
use crate::utils::{http, icon, io};
pub mod categories;
pub mod dao;
pub mod data;
pub mod provider;
#[cfg(test)]
mod tests;
#[onelauncher_macro::error]
#[derive(Debug, thiserror::Error)]
pub enum PackageError {
@ -27,6 +26,12 @@ pub enum PackageError {
IsModPack,
#[error(transparent)]
Incompatible(#[from] IncompatiblePackageType),
#[error("missing API key for provider '{0}'")]
MissingApiKey(Provider),
#[error("unsupported package body type '{0}'")]
UnsupportedBodyType(ManagedPackageBody),
#[error("unsupported author type '{0}'")]
UnsupportedAuthorType(PackageAuthor),
}
#[onelauncher_macro::error]
@ -200,5 +205,5 @@ fn join_package_file(
version_id: &str,
file_name: &str,
) -> std::path::PathBuf {
path.as_ref().join(format!("{version_id}-{file_name}"))
path.as_ref().join(version_id).join(file_name)
}

View file

@ -0,0 +1,103 @@
use crate::api::packages::categories::{
PackageModCategory, PackageModPackCategory, PackageResourcePackCategory, PackageShaderCategory,
ToProviderCategory, define_category_mapping,
};
use crate::api::packages::provider::CurseForgeProviderImpl;
pub struct CurseForgeCategories;
define_category_mapping! {
impl ToProviderCategory<u32, CurseForgeProviderImpl> for CurseForgeCategories {
PackageModCategory as as_mod_out => to_mod {
Adventure => 0,
Library => 0,
Equipment => 0,
Patches => 0,
Cosmetic => 0,
Food => 0,
Magic => 0,
Information => 0,
Misc => 0,
Performance => 0,
Redstone => 0,
ServerUtil => 0,
Storage => 0,
Technology => 0,
Farming => 0,
Automation => 0,
Transport => 0,
Utility => 0,
QoL => 0,
WorldGen => 0,
Mobs => 0,
Economy => 0,
Social => 0,
}
PackageResourcePackCategory as as_resource_pack_out => to_resource_pack {
X8 => 0,
X16 => 0,
X32 => 0,
X48 => 0,
X64 => 0,
X128 => 0,
X256 => 0,
X512 => 0,
VanillaLike => 0,
Utility => 0,
Tweaks => 0,
Themed => 0,
Simplistic => 0,
Realistic => 0,
Modded => 0,
Decoration => 0,
Cursed => 0,
Combat => 0,
Audio => 0,
Blocks => 0,
CoreShaders => 0,
Gui => 0,
Fonts => 0,
Equipment => 0,
Environment => 0,
Entities => 0,
Items => 0,
Locale => 0,
Models => 0,
}
PackageShaderCategory as as_shader_out => to_shader {
VanillaLike => 0,
SemiRealistic => 0,
Realistic => 0,
Fantasy => 0,
Cursed => 0,
Cartoon => 0,
Bloom => 0,
Atmosphere => 0,
Reflections => 0,
Shadows => 0,
PBR => 0,
PathTracing => 0,
Foliage => 0,
ColoredLightning => 0,
Potato => 0,
Low => 0,
Medium => 0,
High => 0,
Ultra => 0,
}
PackageModPackCategory as as_mod_pack_out => to_mod_pack {
Adventure => 0,
Technology => 0,
Quests => 0,
Optimization => 0,
Multiplayer => 0,
Magic => 0,
LightWeight => 0,
Combat => 0,
Challenging => 0,
}
}
}

View file

@ -0,0 +1,889 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::package::{PackageType, Provider};
use reqwest::Method;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_with::serde_as;
use url::Url;
use crate::api::packages::PackageError;
use crate::api::packages::categories::ToProviderCategory;
use crate::api::packages::data::{
ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion, ManagedVersionDependency,
ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageGallery, PackageLinks,
PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult,
};
use crate::api::packages::provider::ProviderExt;
use crate::api::packages::provider::curseforge::categories::CurseForgeCategories;
use crate::error::LauncherResult;
use crate::store::Core;
use crate::utils::http;
use crate::utils::pagination::Paginated;
#[derive(Default)]
pub struct CurseForgeProviderImpl;
mod categories;
#[async_trait::async_trait]
impl ProviderExt for CurseForgeProviderImpl {
async fn search(&self, filter: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
let results: CFPaginatedData<Vec<CFPackage>> = fetch_url_builder(
"/mods/search",
Some(|url: &mut Url| {
let mut params = url.query_pairs_mut();
// Always required parameters
params.append_pair("gameId", &crate::constants::CURSEFORGE_GAME_ID.to_string());
let package_type: CFPackageType = filter
.filters
.as_ref()
.and_then(|f| f.package_type.clone())
.unwrap_or_default()
.into();
params.append_pair("classId", &(package_type as u32).to_string());
params.append_pair("pageSize", &filter.limit.unwrap_or(10).to_string());
params.append_pair("index", &filter.offset.unwrap_or(0).to_string());
params.append_pair("sortField", &(SearchSort::Popularity as u8).to_string());
params.append_pair("sortOrder", "desc");
// Optional: search query
if let Some(query) = &filter.query {
params.append_pair("searchFilter", query);
}
if let Some(filters) = &filter.filters {
if let Some(game_versions) = &filters.game_versions {
// Need to create a list e.g. ["1.16.5", "1.17.1"]
let game_versions = format!(
"[{}]",
game_versions
.iter()
.map(|v| format!("\"{}\"", v))
.collect::<Vec<String>>()
.join(",")
);
params.append_pair("gameVersions", game_versions.as_str());
}
if let Some(loaders) = &filters.loaders {
// Need to create a list e.g. [1, 2, 3]
let loaders = format!(
"[{}]",
loaders
.iter()
.map(|l| (CFLoader::from(l.clone()) as u32).to_string())
.collect::<Vec<String>>()
.join(",")
);
params.append_pair("modLoaderTypes", loaders.as_str());
}
if let Some(categories) = &filters.categories {
let categories = CurseForgeCategories::as_out(categories);
if !categories.is_empty() {
// Need to create a list e.g. [6, 12, 4471]
let categories = format!(
"[{}]",
categories
.iter()
.map(u32::to_string)
.collect::<Vec<String>>()
.join(",")
);
params.append_pair("categoryIds", categories.as_str());
}
}
}
}),
)
.await?;
Ok(Paginated {
total: results.pagination.total_count,
offset: results.pagination.index,
limit: results.pagination.page_size,
items: results.data.into_iter().map(SearchResult::from).collect(),
})
}
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage> {
Ok(fetch::<CFData<CFPackage>>(&format!("/mods/{slug}"))
.await?
.data
.into())
}
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>> {
let body = serde_json::json!({
"modIds": slugs.iter().filter_map(|s| s.parse::<u32>().ok()).collect::<Vec<u32>>(),
});
let results: CFData<Vec<CFPackage>> = fetch_advanced(
Method::POST,
"/mods",
None::<fn(&mut Url)>,
None,
Some(body),
)
.await?;
Ok(results.data.into_iter().map(ManagedPackage::from).collect())
}
async fn get_versions_by_hashes(
&self,
hashes: &[String],
) -> LauncherResult<HashMap<String, ManagedVersion>> {
let body = serde_json::json!({
"fingerprints": hashes.into_iter().filter_map(|hash| hash.parse::<u32>().ok()).collect::<Vec<u32>>()
});
#[derive(Deserialize)]
struct FingerprintMatch {
// id: u32,
file: CFModFile,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Response {
exact_matches: Vec<FingerprintMatch>,
// exact_fingerprints: Vec<u32>
}
let results: CFData<Response> = fetch_advanced(
Method::POST,
&format!(
"/mods/fingerprints/{}",
crate::constants::CURSEFORGE_GAME_ID
),
None::<fn(&mut Url)>,
None,
Some(body),
)
.await?;
Ok(results
.data
.exact_matches
.into_iter()
.map(|file| {
(
file.file.file_fingerprint.to_string(),
ManagedVersion::from(file.file),
)
})
.collect())
}
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>> {
let results = self.get_versions_by_hashes(&[hash.to_string()]).await?;
Ok(results.values().next().cloned())
}
async fn get_users_from_author(
&self,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>> {
match author {
PackageAuthor::Users(users) => Ok(users),
_ => Err(PackageError::UnsupportedAuthorType(author).into()),
}
}
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
// todo!("CurseForge get users not implemented yet")
// }
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
// todo!("CurseForge get user not implemented yet")
// }
async fn get_versions_paginated(
&self,
slug: &str,
mc_version: Option<String>,
loaders: Option<GameLoader>,
offset: usize,
limit: usize,
) -> LauncherResult<Paginated<ManagedVersion>> {
let results: CFPaginatedData<Vec<CFModFile>> = fetch_url_builder(
&format!("/mods/{slug}/files"),
Some(|url: &mut Url| {
let mut params = url.query_pairs_mut();
if let Some(mc_version) = mc_version {
params.append_pair("gameVersion", &mc_version);
}
if let Some(loader) = loaders {
params
.append_pair("modLoaderType", &(CFLoader::from(loader) as u8).to_string());
}
let page = offset / limit;
params.append_pair("pageSize", &limit.to_string());
params.append_pair("index", &page.to_string());
}),
)
.await?;
Ok(Paginated {
total: results.pagination.total_count,
offset: results.pagination.index,
limit: results.pagination.page_size,
items: results.data.into_iter().map(ManagedVersion::from).collect(),
})
}
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>> {
let json_body = serde_json::json!({
"fileIds": slugs.iter().filter_map(|v| v.parse::<u32>().ok()).collect::<Vec<u32>>()
});
let results: CFData<Vec<CFModFile>> = fetch_advanced(
Method::POST,
"/mods/files",
None::<fn(&mut Url)>,
None,
Some(json_body),
)
.await?;
Ok(results.data.into_iter().map(ManagedVersion::from).collect())
}
async fn get_body(&self, body: &ManagedPackageBody) -> LauncherResult<String> {
match body {
ManagedPackageBody::Raw(raw) => Ok(raw.clone()),
ManagedPackageBody::Url(url) => {
let body: String = fetch(url).await?;
Ok(body)
}
}
}
}
// MARK: fetch helpers
async fn fetch_advanced<T: DeserializeOwned, F: FnOnce(&mut Url)>(
method: Method,
url: &str,
url_builder: Option<F>,
headers: Option<HashMap<&str, &str>>,
json_body: Option<serde_json::Value>,
) -> LauncherResult<T> {
let core = Core::get();
let key = core
.curseforge_api_key
.as_ref()
.ok_or(PackageError::MissingApiKey(
onelauncher_entity::package::Provider::CurseForge,
))?;
let mut headers = headers.unwrap_or(HashMap::<&str, &str>::new());
headers.insert("x-api-key", key);
let url = &mut Url::parse(format!("{}{}", crate::constants::CURSEFORGE_API_URL, url).as_str())?;
if let Some(url_builder) = url_builder {
url_builder(url);
}
http::fetch_json_advanced(method, url.as_str(), json_body, Some(headers), None, None).await
}
pub async fn fetch_url_builder<T: DeserializeOwned, F: FnOnce(&mut Url)>(
url: &str,
url_builder: Option<F>,
) -> LauncherResult<T> {
fetch_advanced::<T, F>(Method::GET, url, url_builder, None, None).await
}
async fn fetch<T: DeserializeOwned>(url: &str) -> LauncherResult<T> {
fetch_url_builder::<T, fn(&mut Url)>(url, None).await
}
// MARK: data structs
/// Mapping of Curseforge "classes" to their respective category ids
#[derive(Debug, Default, Serialize_repr, Deserialize_repr)]
#[repr(u32)]
pub enum CFPackageType {
#[default]
Mods = 6,
ModPack = 4471,
DataPack = 6945,
ResourcePack = 12,
ShaderPack = 6552,
}
impl From<CFPackageType> for PackageType {
fn from(package_type: CFPackageType) -> Self {
match package_type {
CFPackageType::Mods => Self::Mod,
CFPackageType::ModPack => Self::ModPack,
CFPackageType::DataPack => Self::DataPack,
CFPackageType::ResourcePack => Self::ResourcePack,
CFPackageType::ShaderPack => Self::Shader,
}
}
}
impl From<PackageType> for CFPackageType {
fn from(package_type: PackageType) -> Self {
match package_type {
PackageType::Mod => Self::Mods,
PackageType::ModPack => Self::ModPack,
PackageType::DataPack => Self::DataPack,
PackageType::ResourcePack => Self::ResourcePack,
PackageType::Shader => Self::ShaderPack,
}
}
}
/// Mapping of Curseforge supported loaders to their respective id
#[derive(Debug, Default, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum CFLoader {
#[default]
Any = 0,
Forge = 1,
Cauldron = 2,
LiteLoader = 3,
Fabric = 4,
Quilt = 5,
NeoForge = 6,
}
impl From<String> for CFLoader {
fn from(loader: String) -> Self {
match loader.to_lowercase().as_str() {
"forge" => CFLoader::Forge,
"cauldron" => CFLoader::Cauldron,
"liteloader" => CFLoader::LiteLoader,
"fabric" => CFLoader::Fabric,
"quilt" => CFLoader::Quilt,
"neoforge" => CFLoader::NeoForge,
_ => CFLoader::Any,
}
}
}
impl From<GameLoader> for CFLoader {
fn from(loader: GameLoader) -> Self {
match loader {
GameLoader::Forge => CFLoader::Forge,
GameLoader::Fabric | GameLoader::LegacyFabric => CFLoader::Fabric,
GameLoader::Quilt => CFLoader::Quilt,
GameLoader::NeoForge => CFLoader::NeoForge,
_ => CFLoader::Any,
}
}
}
impl From<CFLoader> for GameLoader {
fn from(loader: CFLoader) -> Self {
match loader {
CFLoader::Forge => Self::Forge,
CFLoader::Fabric => Self::Fabric,
CFLoader::Quilt => Self::Quilt,
CFLoader::NeoForge => Self::NeoForge,
_ => GameLoader::Vanilla,
}
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde_as]
#[serde(rename_all = "camelCase")]
pub struct CFPackage {
pub id: usize,
pub game_id: usize,
#[serde(alias = "classId")]
pub package_type: CFPackageType,
pub name: String,
pub slug: String,
pub links: CFLinks,
#[serde(default)]
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub social_links: Vec<CFSocialLink>,
pub summary: String,
pub status: CFStatus,
pub download_count: usize,
#[serde(default)]
pub is_featured: bool,
pub categories: Vec<Category>,
pub authors: Vec<CFAuthor>,
pub logo: Option<CFModAsset>,
pub screenshots: Vec<CFModAsset>,
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub latest_files_indexes: Vec<CFLatestFileIndex>,
pub date_created: DateTime<Utc>,
pub date_modified: DateTime<Utc>,
pub date_released: DateTime<Utc>,
pub rating: Option<f32>,
}
impl From<CFPackage> for ManagedPackage {
fn from(value: CFPackage) -> Self {
let package_type: PackageType = value.package_type.into();
let mut loaders: Vec<GameLoader> = Vec::new();
let mut mc_versions: Vec<String> = Vec::new();
let mut latest_version_ids: Vec<String> = Vec::new();
for index in value.latest_files_indexes {
mc_versions.push(index.game_version);
latest_version_ids.push(index.file_id.to_string());
if let Some(loader) = index
.mod_loader
.and_then(|loader| GameLoader::try_from(loader).ok())
{
loaders.push(loader);
}
}
Self {
provider: Provider::CurseForge,
id: value.id.to_string(),
slug: value.slug,
name: value.name,
short_desc: value.summary,
body: ManagedPackageBody::Url(format!("/v1/mods/{}/description", value.id)),
version_ids: latest_version_ids,
mc_versions,
loaders,
icon_url: value.logo.map(|logo| logo.url),
created: value.date_created,
updated: value.date_modified,
client: PackageSide::Unknown, // TODO: determine client side
server: PackageSide::Unknown, // TODO: determine server side
categories: CurseForgeCategories::to_list(
&package_type,
&value.categories.into_iter().map(|c| c.id).collect(),
),
package_type,
license: None,
author: PackageAuthor::Users(
value
.authors
.into_iter()
.map(|author| ManagedUser {
id: author.id.to_string(),
username: author.name,
url: Some(author.url),
is_organization_user: false,
avatar_url: author.avatar_url,
bio: None,
role: None,
})
.collect(),
),
links: PackageLinks {
source: value.links.source_url,
issues: value.links.issues_url,
wiki: value.links.wiki_url,
donation: None, // we love curseforge
discord: value
.social_links
.iter()
.find(|l| l.link_type == CFSocialLinkType::Discord)
.map(|l| l.url.clone()),
website: value
.social_links
.iter()
.find(|l| l.link_type == CFSocialLinkType::Website)
.map(|l| l.url.clone()),
..Default::default()
},
status: value.status.into(),
downloads: value.download_count,
gallery: value.screenshots.into_iter().map(Into::into).collect(),
}
}
}
impl From<CFPackage> for SearchResult {
fn from(value: CFPackage) -> Self {
let mc_versions: Vec<String> = value
.latest_files_indexes
.into_iter()
.map(|index| index.game_version)
.collect();
let package_type: PackageType = value.package_type.into();
Self {
title: value.name,
slug: value.slug,
project_id: value.id.to_string(),
author: value
.authors
.first()
.and_then(|a| Some(a.name.clone()))
.unwrap_or(String::from("Unknown")),
description: value.summary,
client_side: PackageSide::Unknown,
server_side: PackageSide::Unknown,
downloads: value.download_count,
icon_url: value.logo.map_or(String::new(), |l| l.url),
categories: CurseForgeCategories::to_list(
&package_type,
&value.categories.into_iter().map(|c| c.id).collect(),
),
package_type,
mc_versions,
date_created: value.date_created,
date_modified: value.date_modified,
gallery: value
.screenshots
.into_iter()
.map(|asset| asset.url)
.collect(),
latest_version: "".to_string(), // TODO: figure out how to get this
license: None,
}
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CFLatestFileIndex {
pub game_version: String,
pub file_id: usize,
#[serde(rename = "filename")]
pub file_name: String,
// pub release_type: ModFileReleaseType,
pub mod_loader: Option<CFLoader>,
}
#[derive(Default, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(u64)]
pub enum CFStatus {
#[default]
Unknown = 0,
New = 1,
Approved = 4,
Rejected = 5,
Deleted = 9,
}
impl From<CFStatus> for PackageStatus {
fn from(status: CFStatus) -> Self {
match status {
CFStatus::Unknown => Self::Active,
CFStatus::New => Self::Active,
CFStatus::Approved => Self::Active,
CFStatus::Rejected => Self::Abandoned,
CFStatus::Deleted => Self::Abandoned,
}
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CFLinks {
pub website_url: Option<String>,
pub wiki_url: Option<String>,
pub issues_url: Option<String>,
pub source_url: Option<String>,
}
// undocumented ??
#[derive(Default, Debug, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(u64)]
pub enum CFSocialLinkType {
None = 0,
Mastodon = 1,
Discord = 2,
Website = 3,
Facebook = 4,
Twitter = 5,
Instagram = 6,
Patreon = 7,
Twitch = 8,
Reddit = 9,
Youtube = 10,
TikTok = 11,
Pinterest = 12,
Github = 13,
BlueSky = 14,
#[default]
#[serde(other)]
Unknown,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CFSocialLink {
#[serde(rename = "type")]
pub link_type: CFSocialLinkType,
pub url: String,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Category {
pub id: u32,
// pub game_id: u32,
// pub name: String,
pub slug: String,
// pub url: String,
// pub icon_url: String,
// pub date_modified: String,
// pub parent_category_id: u32,
// pub display_index: u32,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CFAuthor {
pub id: u32,
pub name: String,
pub url: String,
pub avatar_url: Option<String>,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CFModAsset {
pub id: u32,
pub mod_id: u32,
pub title: String,
pub description: String,
pub thumbnail_url: String,
pub url: String,
}
impl From<CFModAsset> for PackageGallery {
fn from(screenshot: CFModAsset) -> Self {
Self {
url: screenshot.url,
thumbnail_url: screenshot.thumbnail_url,
title: Some(screenshot.title),
description: Some(screenshot.description),
featured: None,
}
}
}
#[derive(Default, Serialize_repr, Deserialize_repr, Clone, Debug)]
#[repr(u8)]
pub enum ModFileReleaseType {
#[default]
Release = 1,
Beta = 2,
Alpha = 3,
}
impl From<ModFileReleaseType> for PackageReleaseType {
fn from(value: ModFileReleaseType) -> Self {
match value {
ModFileReleaseType::Release => Self::Release,
ModFileReleaseType::Beta => Self::Beta,
ModFileReleaseType::Alpha => Self::Alpha,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CFModFile {
pub id: u32,
pub game_id: u32,
pub mod_id: u32,
#[serde(default)]
pub is_available: bool,
pub display_name: String,
pub file_name: String,
pub release_type: ModFileReleaseType,
// pub file_status: u32,
pub hashes: Vec<Hash>,
pub file_date: DateTime<Utc>,
pub file_length: usize,
pub download_count: usize,
pub file_size_on_disk: Option<usize>,
pub download_url: String,
pub game_versions: Vec<String>,
pub sortable_game_versions: Vec<SortableGameVersion>,
pub dependencies: Vec<CFDependency>,
// pub expose_as_alternative: bool,
// pub parent_project_file_id: u32,
// pub alternate_file_id: u32,
// pub is_server_pack: bool,
// pub server_pack_file_id: u32,
// pub is_early_access_content: bool,
// pub early_access_end_date: String,
pub file_fingerprint: u32,
// pub modules: Vec<Module>,
}
impl From<CFModFile> for ManagedVersion {
fn from(value: CFModFile) -> ManagedVersion {
let mut hashes = HashMap::new();
for hash in value.hashes {
hashes.insert(hash.algo.name(), hash.value);
}
let mut mc_versions = Vec::new();
let mut loaders = Vec::new();
for version in value.sortable_game_versions {
if version.game_version.is_empty() {
// This is most likely a loader
loaders.push(CFLoader::from(version.game_version_name));
} else {
mc_versions.push(version.game_version);
}
}
let mut files = Vec::new();
files.push(ManagedVersionFile {
url: value.download_url,
file_name: value.file_name.clone(),
primary: true,
size: value.file_size_on_disk.unwrap_or(value.file_length),
sha1: hashes.get("sha1").cloned().unwrap_or_default(),
});
ManagedVersion {
project_id: value.mod_id.to_string(),
version_id: value.id.to_string(),
display_name: value.display_name.clone(),
loaders: loaders.into_iter().map(Into::into).collect(),
mc_versions,
changelog: None,
display_version: value.file_name,
release_type: value.release_type.into(),
downloads: value.download_count,
published: value.file_date,
dependencies: value.dependencies.into_iter().map(Into::into).collect(),
files,
}
}
}
#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
#[repr(u8)]
pub enum HashAlgorithm {
Sha1 = 1,
MD5 = 2,
}
impl HashAlgorithm {
pub fn name(&self) -> String {
match self {
Self::Sha1 => "sha1",
Self::MD5 => "md5",
}
.to_string()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hash {
pub value: String,
pub algo: HashAlgorithm,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SortableGameVersion {
pub game_version_name: String,
pub game_version_padded: String,
pub game_version: String,
pub game_version_release_date: String,
pub game_version_type_id: u32,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CFDependency {
pub mod_id: usize,
pub relation_type: CFRelationType,
}
impl From<CFDependency> for ManagedVersionDependency {
fn from(value: CFDependency) -> Self {
Self {
project_id: Some(value.mod_id.to_string()),
dependency_type: value.relation_type.into(),
file_name: None,
version_id: None,
}
}
}
#[derive(Default, Debug, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum CFRelationType {
Embedded = 1,
#[default]
Optional = 2,
Required = 3,
Tool = 4,
Incompatible = 5,
Include = 6,
}
impl From<CFRelationType> for PackageDependencyType {
fn from(value: CFRelationType) -> Self {
match value {
CFRelationType::Embedded => Self::Embedded,
CFRelationType::Optional => Self::Optional,
CFRelationType::Required => Self::Required,
CFRelationType::Tool => Self::Optional,
CFRelationType::Incompatible => Self::Incompatible,
CFRelationType::Include => Self::Embedded,
}
}
}
// https://docs.curseforge.com/rest-api/?shell#tocS_ModsSearchSortField
#[allow(dead_code)]
enum SearchSort {
Featured = 1,
Popularity = 2,
LastUpdated = 3,
Name = 4,
Author = 5,
TotalDownloads = 6,
Rating = 12,
}
#[derive(Default, Debug, Serialize, Deserialize)]
struct CFData<T> {
data: T,
}
#[derive(Default, Debug, Serialize, Deserialize)]
struct CFPaginatedData<T> {
pagination: Pagination,
data: T,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Pagination {
pub index: usize,
pub page_size: usize,
pub total_count: usize,
}

View file

@ -1,15 +1,25 @@
use std::collections::HashMap;
use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::package::Provider;
use crate::api::packages::data::SearchResult;
use crate::api::packages::PackageError;
use crate::api::packages::data::{ManagedPackageBody, SearchResult};
use crate::error::LauncherResult;
use crate::utils::pagination::Paginated;
use super::data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery};
mod modrinth;
#[cfg(test)]
mod tests;
mod curseforge;
mod modrinth;
// mod skyclient;
pub use curseforge::CurseForgeProviderImpl;
pub use modrinth::ModrinthProviderImpl;
// pub use skyclient::SkyClientProviderImpl;
#[async_trait::async_trait]
pub trait ProviderExt {
@ -19,8 +29,8 @@ pub trait ProviderExt {
async fn get_versions_by_hashes(
&self,
hashes: &[String],
) -> LauncherResult<Vec<ManagedVersion>>;
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<ManagedVersion>;
) -> LauncherResult<HashMap<String, ManagedVersion>>;
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>>;
// async fn get_org_projects(&self, slug: &str) -> LauncherResult<Vec<ManagedPackage>>;
@ -28,107 +38,96 @@ pub trait ProviderExt {
&self,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>>;
async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>>;
async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser>;
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>>;
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser>;
async fn get_versions_paginated(
&self,
slug: &str,
mc_versions: Option<Vec<String>>,
loaders: Option<Vec<GameLoader>>,
mc_version: Option<String>,
loader: Option<GameLoader>,
offset: usize,
limit: usize,
) -> LauncherResult<Paginated<ManagedVersion>>;
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>>;
async fn get_body(&self, body: &ManagedPackageBody) -> LauncherResult<String> {
match body {
ManagedPackageBody::Raw(raw) => Ok(raw.clone()),
_ => Err(PackageError::UnsupportedBodyType(body.clone()).into()),
}
}
}
/// Cleans up code duplication by delegating method calls to the appropriate provider implementation.
macro_rules! delegate {
($self:expr, $method:ident($($arg:expr),*)) => {
match $self {
Provider::Modrinth => ModrinthProviderImpl.$method($($arg),*).await,
Provider::CurseForge => CurseForgeProviderImpl.$method($($arg),*).await,
// Provider::SkyClient => SkyClientProviderImpl.$method($($arg),*).await,
_ => todo!("skyclient not implement")
}
};
}
/// Wrapper around all supported providers into a single enum.
#[async_trait::async_trait]
impl ProviderExt for Provider {
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
match self {
Self::Modrinth => ModrinthProviderImpl.search(query).await,
_ => todo!("unimplemented provider"),
}
delegate!(self, search(query))
}
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage> {
match self {
Self::Modrinth => ModrinthProviderImpl.get(slug).await,
_ => todo!("unimplemented provider"),
}
delegate!(self, get(slug))
}
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>> {
match self {
Self::Modrinth => ModrinthProviderImpl.get_multiple(slugs).await,
_ => todo!("unimplemented provider"),
}
delegate!(self, get_multiple(slugs))
}
async fn get_versions_by_hashes(
&self,
hashes: &[String],
) -> LauncherResult<Vec<ManagedVersion>> {
match self {
Self::Modrinth => ModrinthProviderImpl.get_versions_by_hashes(hashes).await,
_ => todo!("unimplemented provider"),
}
) -> LauncherResult<HashMap<String, ManagedVersion>> {
delegate!(self, get_versions_by_hashes(hashes))
}
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<ManagedVersion> {
match self {
Self::Modrinth => ModrinthProviderImpl.get_version_by_hash(hash).await,
_ => todo!("unimplemented provider"),
}
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>> {
delegate!(self, get_version_by_hash(hash))
}
async fn get_users_from_author(
&self,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>> {
match self {
Self::Modrinth => ModrinthProviderImpl.get_users_from_author(author).await,
_ => todo!("unimplemented provider"),
}
delegate!(self, get_users_from_author(author))
}
async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
match self {
Self::Modrinth => ModrinthProviderImpl.get_users(slugs).await,
_ => todo!("unimplemented provider"),
}
}
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
// delegate!(self, get_users(slugs))
// }
async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
match self {
Self::Modrinth => ModrinthProviderImpl.get_user(slug).await,
_ => todo!("unimplemented provider"),
}
}
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
// delegate!(self, get_user(slug))
// }
async fn get_versions_paginated(
&self,
slug: &str,
mc_versions: Option<Vec<String>>,
loaders: Option<Vec<GameLoader>>,
mc_version: Option<String>,
loader: Option<GameLoader>,
offset: usize,
limit: usize,
) -> LauncherResult<Paginated<ManagedVersion>> {
match self {
Self::Modrinth => {
ModrinthProviderImpl
.get_versions_paginated(slug, mc_versions, loaders, offset, limit)
.await
}
_ => todo!("unimplemented provider"),
}
delegate!(
self,
get_versions_paginated(slug, mc_version, loader, offset, limit)
)
}
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>> {
match self {
Self::Modrinth => ModrinthProviderImpl.get_versions(slugs).await,
_ => todo!("unimplemented provider"),
}
delegate!(self, get_versions(slugs))
}
}

View file

@ -0,0 +1,103 @@
use crate::api::packages::categories::{
PackageModCategory, PackageModPackCategory, PackageResourcePackCategory, PackageShaderCategory,
ToProviderCategory, define_category_mapping,
};
use crate::api::packages::provider::ModrinthProviderImpl;
pub struct ModrinthCategories;
define_category_mapping! {
impl ToProviderCategory<String, ModrinthProviderImpl> for ModrinthCategories {
PackageModCategory as as_mod_out => to_mod {
Adventure => String::from("adventure"),
Library => String::from("library"),
Equipment => String::from("equipment"),
Patches => String::from("optimization"),
Cosmetic => String::from("decoration"),
Food => String::from("food"),
Magic => String::from("magic"),
Information => String::from("utility"),
Misc => String::from("utility"),
Performance => String::from("optimization"),
Redstone => String::from("technology"),
ServerUtil => String::from("management"),
Storage => String::from("storage"),
Technology => String::from("technology"),
Farming => String::from("food"),
Automation => String::from("technology"),
Transport => String::from("transportation"),
Utility => String::from("utility"),
QoL => String::from("utility"),
WorldGen => String::from("worldgen"),
Mobs => String::from("mobs"),
Economy => String::from("economy"),
Social => String::from("social"),
}
PackageResourcePackCategory as as_resource_pack_out => to_resource_pack {
X8 => String::from("8x-"),
X16 => String::from("16x"),
X32 => String::from("32x"),
X48 => String::from("48x"),
X64 => String::from("64x"),
X128 => String::from("128x"),
X256 => String::from("256x"),
X512 => String::from("512x+"),
VanillaLike => String::from("vanilla-like"),
Utility => String::from("utility"),
Tweaks => String::from("tweaks"),
Themed => String::from("themed"),
Simplistic => String::from("simplistic"),
Realistic => String::from("realistic"),
Modded => String::from("modded"),
Decoration => String::from("decoration"),
Cursed => String::from("cursed"),
Combat => String::from("combat"),
Audio => String::from("audio"),
Blocks => String::from("blocks"),
CoreShaders => String::from("core-shaders"),
Gui => String::from("gui"),
Fonts => String::from("fonts"),
Equipment => String::from("equipment"),
Environment => String::from("environment"),
Entities => String::from("entities"),
Items => String::from("items"),
Locale => String::from("locale"),
Models => String::from("models"),
}
PackageShaderCategory as as_shader_out => to_shader {
VanillaLike => String::from("vanilla-like"),
SemiRealistic => String::from("semi-realistic"),
Realistic => String::from("realistic"),
Fantasy => String::from("fantasy"),
Cursed => String::from("cursed"),
Cartoon => String::from("cartoon"),
Bloom => String::from("bloom"),
Atmosphere => String::from("atmosphere"),
Reflections => String::from("reflections"),
Shadows => String::from("shadows"),
PBR => String::from("pbr"),
PathTracing => String::from("path-tracing"),
Foliage => String::from("foliage"),
ColoredLightning => String::from("colored-lightning"),
Potato => String::from("potato"),
Low => String::from("low"),
Medium => String::from("medium"),
High => String::from("high"),
Ultra => String::from("screenshot"),
}
PackageModPackCategory as as_mod_pack_out => to_mod_pack {
Adventure => String::from("adventure"),
Technology => String::from("technology"),
Quests => String::from("quests"),
Optimization => String::from("optimization"),
Multiplayer => String::from("multiplayer"),
Magic => String::from("magic"),
LightWeight => String::from("lightweight"),
Combat => String::from("combat"),
Challenging => String::from("challenging"),
}
}
}

View file

@ -1,10 +1,14 @@
use std::collections::HashMap;
use super::ProviderExt;
use crate::api::packages::categories::ToProviderCategory;
use crate::api::packages::data::{
DEFAULT_LIMIT, ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency,
ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageGallery,
PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery,
SearchResult,
DEFAULT_LIMIT, ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion,
ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType,
PackageDonationUrl, PackageGallery, PackageLicense, PackageLinks, PackageReleaseType,
PackageSide, PackageStatus, SearchQuery, SearchResult,
};
use crate::api::packages::provider::modrinth::categories::ModrinthCategories;
use crate::error::LauncherResult;
use crate::utils::http;
use crate::utils::pagination::Paginated;
@ -16,6 +20,8 @@ use serde::Deserialize;
use serde_with::serde_as;
use url::Url;
mod categories;
macro_rules! url {
($($arg:tt)*) => {
format!("{}/v2{}", crate::constants::MODRINTH_API_URL, format!($($arg)*)).as_str()
@ -31,13 +37,6 @@ macro_rules! url_v3 {
#[derive(Default)]
pub struct ModrinthProviderImpl;
impl ModrinthProviderImpl {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[async_trait::async_trait]
impl ProviderExt for ModrinthProviderImpl {
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
@ -54,39 +53,33 @@ impl ProviderExt for ModrinthProviderImpl {
let mut builder = FacetBuilder::builder();
if let Some(categories) = &filters.categories {
for category in categories {
builder.and(Facet(
"categories".to_string(),
FacetOperation::Eq,
category.to_string(),
));
for category in ModrinthCategories::as_out(categories) {
builder.and(Facet::new("categories", FacetOperation::Eq, category));
}
}
if let Some(mc_versions) = &filters.game_versions {
for mc_version in mc_versions {
builder.and(Facet(
"versions".to_string(),
builder.and(Facet::new(
"versions",
FacetOperation::Eq,
mc_version.to_string(),
mc_version.clone(),
));
}
}
if let Some(package_types) = &filters.package_types {
for package_type in package_types {
builder.and(Facet(
"project_type".to_string(),
if let Some(package_type) = &filters.package_type {
builder.and(Facet::new(
"project_type",
FacetOperation::Eq,
package_type_to_string(package_type),
package_type.to_string(),
));
}
}
if let Some(loaders) = &filters.loaders {
for loader in loaders {
builder.and(Facet(
"categories".to_string(),
builder.and(Facet::new(
"categories",
FacetOperation::Eq,
loader.to_string(),
));
@ -152,33 +145,34 @@ impl ProviderExt for ModrinthProviderImpl {
async fn get_versions_by_hashes(
&self,
hashes: &[String],
) -> LauncherResult<Vec<ManagedVersion>> {
) -> LauncherResult<HashMap<String, ManagedVersion>> {
let body = serde_json::json!({
"hashes": hashes,
"algorithm": "sha1"
});
Ok(http::fetch_json::<Vec<ModrinthVersion>>(
let results = http::fetch_json::<HashMap<String, ModrinthVersion>>(
Method::POST,
url!("/version_files"),
Some(body),
None,
)
.await?
.into_iter()
.map(Into::into)
.collect())
.await?;
Ok(results.into_iter().map(|(k, v)| (k, v.into())).collect())
}
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<ManagedVersion> {
Ok(http::fetch_json::<ModrinthVersion>(
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>> {
Ok(Some(
http::fetch_json::<ModrinthVersion>(
Method::GET,
url!("/version_file/{hash}"),
None,
None,
)
.await?
.into())
.into(),
))
}
// async fn get_org_projects(&self, slug: &str) -> LauncherResult<Vec<ManagedPackage>> {
@ -252,45 +246,45 @@ impl ProviderExt for ModrinthProviderImpl {
Ok(users)
}
async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
Ok(http::fetch_json::<Vec<ModrinthUser>>(
Method::GET,
url!("/users?ids=[{}]", &serde_json::to_string(&slugs)?),
None,
None,
)
.await?
.into_iter()
.map(Into::into)
.collect())
}
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
// Ok(http::fetch_json::<Vec<ModrinthUser>>(
// Method::GET,
// url!("/users?ids=[{}]", &serde_json::to_string(&slugs)?),
// None,
// None,
// )
// .await?
// .into_iter()
// .map(Into::into)
// .collect())
// }
async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
Ok(
http::fetch_json::<ModrinthUser>(Method::GET, url!("/user/{slug}"), None, None)
.await?
.into(),
)
}
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
// Ok(
// http::fetch_json::<ModrinthUser>(Method::GET, url!("/user/{slug}"), None, None)
// .await?
// .into(),
// )
// }
async fn get_versions_paginated(
&self,
slug: &str,
mc_versions: Option<Vec<String>>,
loaders: Option<Vec<GameLoader>>,
mc_version: Option<String>,
loader: Option<GameLoader>,
offset: usize,
limit: usize,
) -> LauncherResult<Paginated<ManagedVersion>> {
let mut url = Url::parse(url!("/project/{slug}/version"))?;
if let Some(mc_versions) = mc_versions {
if let Some(mc_version) = mc_version {
url.query_pairs_mut()
.append_pair("game_versions", &serde_json::to_string(&mc_versions)?);
.append_pair("game_versions", &format!("[\"{}\"]", mc_version));
}
if let Some(loaders) = loaders {
if let Some(loader) = loader {
url.query_pairs_mut()
.append_pair("loaders", &serde_json::to_string(&loaders)?);
.append_pair("loaders", &format!("[\"{}\"]", loader.to_string()));
}
// url.query_pairs_mut()
@ -335,15 +329,50 @@ impl ProviderExt for ModrinthProviderImpl {
}
}
fn package_type_to_string(package_type: &PackageType) -> String {
match package_type {
PackageType::Mod => "mod",
PackageType::ResourcePack => "resourcepack",
PackageType::Shader => "shader",
PackageType::DataPack => "datapack",
PackageType::ModPack => "modpack",
#[derive(Deserialize)]
struct ModrinthSearchResult {
pub project_id: String,
pub project_type: PackageType,
pub slug: String,
pub author: String,
pub title: String,
pub description: String,
pub categories: Vec<String>,
pub mc_versions: Vec<String>,
pub downloads: usize,
pub icon_url: String,
pub date_created: DateTime<Utc>,
pub date_modified: DateTime<Utc>,
pub latest_version: String,
pub license: Option<String>,
pub client_side: PackageSide,
pub server_side: PackageSide,
/// List of URLs to images
pub gallery: Vec<String>,
}
impl From<ModrinthSearchResult> for SearchResult {
fn from(value: ModrinthSearchResult) -> Self {
Self {
categories: ModrinthCategories::to_list(&value.project_type, &value.categories),
package_type: value.project_type,
project_id: value.project_id,
slug: value.slug,
author: value.author,
title: value.title,
description: value.description,
mc_versions: value.mc_versions,
downloads: value.downloads,
icon_url: value.icon_url,
date_created: value.date_created,
date_modified: value.date_modified,
latest_version: value.latest_version,
license: value.license,
client_side: value.client_side,
server_side: value.server_side,
gallery: value.gallery,
}
}
.to_string()
}
#[serde_as]
@ -407,10 +436,9 @@ impl From<ModrinthPackage> for ManagedPackage {
provider: Provider::Modrinth,
id: value.id,
slug: value.slug,
package_type: value.project_type,
name: value.title,
short_desc: value.description,
body: value.body,
body: ManagedPackageBody::Raw(value.body),
version_ids: value.versions,
mc_versions: value.game_versions,
loaders: value.loaders,
@ -419,7 +447,8 @@ impl From<ModrinthPackage> for ManagedPackage {
updated: value.updated,
client: value.client_side,
server: value.server_side,
categories: value.categories,
categories: ModrinthCategories::to_list(&value.project_type, &value.categories),
package_type: value.project_type,
license: value.license,
author: PackageAuthor::Team {
team_id: value.team,
@ -444,20 +473,25 @@ impl From<ModrinthPackage> for ManagedPackage {
}
}
#[serde_as]
#[derive(Deserialize)]
struct ModrinthVersion {
pub name: String,
pub version_number: String,
pub changelog: Option<String>,
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub dependencies: Vec<ModrinthDependency>,
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub game_versions: Vec<String>,
pub version_type: PackageReleaseType,
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub loaders: Vec<GameLoader>,
// pub featured: bool,
pub id: String,
pub project_id: String,
pub date_published: DateTime<Utc>,
pub downloads: usize,
#[serde_as(as = "serde_with::VecSkipError<_>")]
pub files: Vec<ModrinthFile>,
}
@ -611,6 +645,12 @@ impl std::fmt::Display for FacetOperation {
struct Facet(pub String, pub FacetOperation, pub String);
impl Facet {
pub fn new(key: &str, operation: FacetOperation, value: String) -> Self {
Self(key.to_string(), operation, value)
}
}
impl std::fmt::Display for Facet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(format!("{}{}{}", self.0, self.1, self.2).as_str())

View file

@ -0,0 +1,69 @@
use std::collections::HashMap;
use onelauncher_entity::loader::GameLoader;
use crate::api::packages::data::{
ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery, SearchResult,
};
use crate::api::packages::provider::ProviderExt;
use crate::error::LauncherResult;
use crate::utils::pagination::Paginated;
#[derive(Default)]
pub struct SkyClientProviderImpl;
#[async_trait::async_trait]
impl ProviderExt for SkyClientProviderImpl {
async fn search(&self, query: &SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
todo!("SkyClient search not implemented yet")
}
async fn get(&self, slug: &str) -> LauncherResult<ManagedPackage> {
todo!("SkyClient get package not implemented yet")
}
async fn get_multiple(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedPackage>> {
todo!("SkyClient get multiple packages not implemented yet")
}
async fn get_versions_by_hashes(
&self,
hashes: &[String],
) -> LauncherResult<HashMap<String, ManagedVersion>> {
todo!("SkyClient get versions by hashes not implemented yet")
}
async fn get_version_by_hash(&self, hash: &str) -> LauncherResult<Option<ManagedVersion>> {
todo!("SkyClient get version by hash not implemented yet")
}
async fn get_users_from_author(
&self,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>> {
todo!("SkyClient get users from author not implemented yet")
}
// async fn get_users(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedUser>> {
// todo!("SkyClient get users not implemented yet")
// }
// async fn get_user(&self, slug: &str) -> LauncherResult<ManagedUser> {
// todo!("SkyClient get user not implemented yet")
// }
async fn get_versions_paginated(
&self,
slug: &str,
mc_version: Option<String>,
loader: Option<GameLoader>,
offset: usize,
limit: usize,
) -> LauncherResult<Paginated<ManagedVersion>> {
todo!("SkyClient get versions paginated not implemented yet")
}
async fn get_versions(&self, slugs: &[String]) -> LauncherResult<Vec<ManagedVersion>> {
todo!("SkyClient get versions not implemented yet")
}
}

View file

@ -0,0 +1,211 @@
use std::sync::LazyLock;
use onelauncher_entity::package::{PackageType, Provider};
use crate::api::packages;
use crate::api::packages::provider::ProviderExt;
use crate::error::LauncherResult;
use crate::store::{Core, CoreOptions, Dirs};
macro_rules! init {
() => {{
Core::initialize(CoreOptions::default()).await?;
if Core::get().curseforge_api_key.is_none() {
eprintln!("CURSEFORGE_API_KEY is not set, skipping tests that require it.");
false
} else {
true
}
}};
}
struct Entry {
slug: &'static str,
id: &'static str,
version_id: &'static str,
package_type: PackageType,
}
static DATA: LazyLock<Vec<(Provider, Vec<Entry>)>> = LazyLock::new(|| {
vec![
(
Provider::CurseForge,
vec![
Entry {
slug: "oneconfig",
id: "1148006",
version_id: "5938441",
package_type: PackageType::Mod,
},
Entry {
slug: "complementary-reimagined",
id: "627557",
version_id: "6515577",
package_type: PackageType::Shader,
},
Entry {
slug: "tschipcrafts-dynamic-lights",
id: "831385",
version_id: "6725286",
package_type: PackageType::DataPack,
},
Entry {
slug: "fresh-animations",
id: "453763",
version_id: "6528594",
package_type: PackageType::ResourcePack,
},
Entry {
slug: "all-the-mods-10",
id: "925200",
version_id: "6826503",
package_type: PackageType::ModPack,
},
],
),
(
Provider::Modrinth,
vec![
Entry {
slug: "oneconfig",
id: "AibBIVmj",
version_id: "YofF8Rpk",
package_type: PackageType::Mod,
},
Entry {
slug: "complementary-reimagined",
id: "HVnmMxH1",
version_id: "sAAjYvFB",
package_type: PackageType::Shader,
},
Entry {
slug: "veinminer",
id: "OhduvhIc",
version_id: "gZ4v72II",
package_type: PackageType::Mod,
},
Entry {
slug: "fresh-animations",
id: "50dA9Sha",
version_id: "9LtDLleW",
package_type: PackageType::ResourcePack,
},
Entry {
slug: "fabulously-optimized",
id: "1KVo5zza",
version_id: "U9MwqSo1",
package_type: PackageType::ModPack,
},
],
),
]
});
#[tokio::test]
pub async fn test_get() -> LauncherResult<()> {
let cf = init!();
for (provider, entries) in DATA.iter() {
if provider == &Provider::CurseForge && !cf {
continue;
}
for entry in entries {
let res = provider.get(entry.id).await?;
assert_eq!(res.slug, entry.slug);
assert_eq!(res.id, entry.id);
assert_eq!(res.package_type, entry.package_type);
}
}
Ok(())
}
#[tokio::test]
pub async fn test_get_multiple() -> LauncherResult<()> {
let cf = init!();
for (provider, entries) in DATA.iter() {
if provider == &Provider::CurseForge && !cf {
continue;
}
let ids = entries
.iter()
.map(|e| e.id.to_string())
.collect::<Vec<String>>();
let res = provider.get_multiple(&ids).await?;
assert_eq!(res.len(), entries.len());
}
Ok(())
}
#[tokio::test]
pub async fn test_get_versions() -> LauncherResult<()> {
let cf = init!();
for (provider, entries) in DATA.iter() {
if provider == &Provider::CurseForge && !cf {
continue;
}
let slugs = entries
.iter()
.map(|e| e.version_id.to_string())
.collect::<Vec<String>>();
let res = provider
.get_versions(&slugs.iter().map(|s| s.to_string()).collect::<Vec<String>>())
.await?;
assert_eq!(res.len(), slugs.len());
}
Ok(())
}
#[tokio::test]
pub async fn test_download_packages() -> LauncherResult<()> {
let cf = init!();
for (provider, entries) in DATA.iter() {
if provider == &Provider::CurseForge && !cf {
continue;
}
for entry in entries {
if entry.package_type == PackageType::ModPack {
continue; // TODO: Skip modpacks for now
}
let pkg = provider.get(entry.id).await?;
let ver_id = entry.version_id.to_string();
let versions = provider.get_versions(&[ver_id.clone()]).await?;
let ver = versions.first().expect("No version found");
let db_model = packages::download_package(&pkg, ver, None).await?;
assert!(db_model.hash == ver.files.first().unwrap().sha1);
let dir = Dirs::get_package_dir(
&db_model.package_type,
&db_model.provider,
&db_model.package_id,
)
.await?;
let dest = dir.join(packages::join_package_file(
&dir,
&db_model.version_id,
&db_model.file_name,
));
assert!(dest.exists(), "File does not exist: {dest:?}");
}
}
Ok(())
}
// TODO: Add more tests

View file

@ -1,73 +0,0 @@
use onelauncher_entity::package::Provider;
use crate::api::packages::provider::ProviderExt;
use crate::api::packages::{self};
use crate::error::LauncherResult;
use crate::store::{Core, CoreOptions, Dirs};
#[tokio::test]
pub async fn test_get_provider() -> LauncherResult<()> {
Core::initialize(CoreOptions::default()).await?;
let provider = Provider::Modrinth;
let res = provider.get("oneconfig").await?;
assert_eq!(res.id, "AibBIVmj");
Ok(())
}
#[tokio::test]
pub async fn test_get_multiple() -> LauncherResult<()> {
Core::initialize(CoreOptions::default()).await?;
let provider = Provider::Modrinth;
let res = provider
.get_multiple(&["oneconfig".to_string(), "chatting".to_string()])
.await?;
assert_eq!(res.len(), 2);
Ok(())
}
#[tokio::test]
pub async fn test_get_versions() -> LauncherResult<()> {
Core::initialize(CoreOptions::default()).await?;
let provider = Provider::Modrinth;
let res = provider
.get_versions(&["ZvlCAdEF".to_string(), "GQANlg7p".to_string()])
.await?;
assert_eq!(res.len(), 2);
Ok(())
}
#[tokio::test]
pub async fn test_download_chatting() -> LauncherResult<()> {
Core::initialize(CoreOptions::default()).await?;
let provider = Provider::Modrinth;
let pkg = provider.get("chatting").await?;
let ver_id = pkg.version_ids.first().expect("No version found");
let versions = provider.get_versions(&[ver_id.clone()]).await?;
let ver = versions.first().expect("No version found");
let db_model = packages::download_package(&pkg, ver, None).await;
assert!(db_model.is_ok_and(|m| m.display_name == pkg.name));
let dir = Dirs::get_package_dir(&pkg.package_type, &provider, &pkg.id).await?;
let dest = dir.join(&ver.files.first().unwrap().file_name);
assert!(dest.exists(), "File does not exist: {dest:?}");
Ok(())
}
// TODO: Add more tests

View file

@ -118,14 +118,14 @@ pub trait TauriLauncherApi {
async fn get_package_versions(
provider: Provider,
slug: String,
mc_versions: Option<Vec<String>>,
loaders: Option<Vec<GameLoader>>,
mc_version: Option<String>,
loader: Option<GameLoader>,
offset: usize,
limit: usize,
) -> LauncherResult<Paginated<ManagedVersion>>;
#[taurpc(alias = "getPackageUser")]
async fn get_package_user(provider: Provider, slug: String) -> LauncherResult<ManagedUser>;
// #[taurpc(alias = "getPackageUser")]
// async fn get_package_user(provider: Provider, slug: String) -> LauncherResult<ManagedUser>;
#[taurpc(alias = "downloadPackage")]
async fn download_package(
@ -485,24 +485,16 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
self,
provider: Provider,
slug: String,
mc_versions: Option<Vec<String>>,
loaders: Option<Vec<GameLoader>>,
mc_version: Option<String>,
loader: Option<GameLoader>,
offset: usize,
limit: usize,
) -> LauncherResult<Paginated<ManagedVersion>> {
provider
.get_versions_paginated(&slug, mc_versions, loaders, offset, limit)
.get_versions_paginated(&slug, mc_version, loader, offset, limit)
.await
}
async fn get_package_user(
self,
provider: Provider,
slug: String,
) -> LauncherResult<ManagedUser> {
provider.get_user(&slug).await
}
async fn download_package(
self,
provider: Provider,

View file

@ -20,7 +20,7 @@ pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
/// The Modrinth API base url.
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com";
/// The CurseForge API base url.
pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com";
pub const CURSEFORGE_API_URL: &str = "https://api.curseforge.com/v1";
/// The Minecraft game ID on CurseForge.
pub const CURSEFORGE_GAME_ID: u32 = 432;
/// Our metadata API base url.

View file

@ -26,6 +26,7 @@ pub struct CoreOptions {
}
impl Default for CoreOptions {
#[cfg(not(test))]
fn default() -> Self {
Self {
discord_client_id: None,
@ -40,6 +41,26 @@ impl Default for CoreOptions {
logger_filter: None,
}
}
#[cfg(test)]
fn default() -> Self {
Self {
discord_client_id: std::env::var("DISCORD_CLIENT_ID").ok(),
launcher_name: String::from("Launcher"),
launcher_version: String::from(env!("CARGO_PKG_VERSION")),
launcher_website: String::from("https://polyfrost.org/"),
fetch_attempts: 3,
msa_client_id: std::env::var("MSA_CLIENT_ID")
.ok()
.unwrap_or_else(|| String::from("00000000402b5328")),
msa_redirect_uri: std::env::var("MSA_REDIRECT_URI")
.ok()
.unwrap_or_else(|| String::from("https://login.live.com/oauth20_desktop.srf")),
curseforge_api_key: std::env::var("CURSEFORGE_API_KEY").ok(),
logger_span_formatting: None,
logger_filter: None,
}
}
}
impl Core {

View file

@ -113,8 +113,8 @@ pub struct Ingress {
pub last_sent: f64,
}
// #[onelauncher_macro::specta]
#[derive(Serialize, Deserialize, specta::Type, Debug, Clone)]
#[onelauncher_macro::specta]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum IngressType {
Download { file_name: String },
JavaPrepare,

View file

@ -18,7 +18,6 @@ path = "src/lib.rs"
default = []
specta = [
"dep:specta",
"onelauncher_macro/specta",
"interpulse/specta"
]

View file

@ -6,10 +6,8 @@ use sea_orm::DeriveValueType;
use sea_orm::sea_query::Nullable;
use serde::{Deserialize, Serialize};
// #[onelauncher_macro::specta]
#[derive(
Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType, specta::Type,
)]
#[onelauncher_macro::specta]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)]
pub struct Icon(String);
impl Nullable for Icon {

View file

@ -4,10 +4,13 @@ use sea_orm::{DeriveActiveEnum, EnumIter};
use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
#[derive(
Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum,
)]
#[serde(rename_all = "lowercase")]
#[sea_orm(rs_type = "u8", db_type = "Integer")]
pub enum PackageType {
#[default]
Mod = 0,
ResourcePack = 1,
Shader = 2,
@ -29,6 +32,32 @@ impl PackageType {
}
}
impl From<String> for PackageType {
fn from(value: String) -> Self {
match value.to_lowercase().as_str() {
"mod" => Self::Mod,
"resourcepack" => Self::ResourcePack,
"shader" => Self::Shader,
"datapack" => Self::DataPack,
"modpack" => Self::ModPack,
_ => Self::Mod, // default case
}
}
}
impl ToString for PackageType {
fn to_string(&self) -> String {
match self {
&Self::Mod => "mod",
&Self::ResourcePack => "resourcepack",
&Self::Shader => "shader",
&Self::DataPack => "datapack",
&Self::ModPack => "modpack",
}
.to_string()
}
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "u8", db_type = "Integer")]

View file

@ -17,8 +17,6 @@ proc-macro = true
[features]
default = []
tauri = [ "specta" ]
specta = []
[dependencies]
syn = { workspace = true }

View file

@ -16,9 +16,16 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
for variant in &data_enum.variants {
let v_ident = &variant.ident;
let cfg_attrs = &variant
.attrs
.iter()
.filter(|attr| attr.path().is_ident("cfg"))
.collect::<Vec<_>>();
match &variant.fields {
Fields::Unit => {
serialize_arms.push(quote! {
#(#cfg_attrs)*
Self::#v_ident => {
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
s.serialize_field("type", stringify!(#v_ident))?;
@ -43,6 +50,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
if has_from && !has_skip {
serialize_arms.push(quote! {
#(#cfg_attrs)*
Self::#v_ident(inner) => {
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
s.serialize_field("type", stringify!(#v_ident))?;
@ -52,6 +60,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
});
} else {
serialize_arms.push(quote! {
#(#cfg_attrs)*
Self::#v_ident(_) => {
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
s.serialize_field("type", stringify!(#v_ident))?;
@ -62,6 +71,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
} else {
serialize_arms.push(quote! {
#(#cfg_attrs)*
Self::#v_ident(..) => {
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
s.serialize_field("type", stringify!(#v_ident))?;
@ -73,6 +83,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
Fields::Named(_) => {
serialize_arms.push(quote! {
#(#cfg_attrs)*
Self::#v_ident { .. } => {
let mut s = serializer.serialize_struct(stringify!(#ident), 2)?;
s.serialize_field("type", stringify!(#v_ident))?;
@ -99,7 +110,7 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
};
let output = quote! {
#[cfg_attr(feature = "specta", derive(onelauncher_macro::SerializedError))]
#[derive(onelauncher_macro::SerializedError)]
#input
#serialize_impl
@ -160,6 +171,7 @@ pub fn error_derive(input: TokenStream) -> TokenStream {
let enum_name_str = enum_name.to_string();
let output = quote! {
#[cfg(feature = "specta")]
#[derive(Debug, specta::Type, serde::Serialize)]
#[specta(remote = #enum_name)]
#[specta(tag = "type", content = "data")]

View file

@ -1,10 +1,6 @@
#[cfg(feature = "specta")]
mod specta;
#[cfg(feature = "tauri")]
mod error;
mod pin;
mod specta;
/// This macro is used to derive the `specta::Type` trait for the annotated item.
/// It's mainly meant for entity-generation with sea-orm-cli (it doesn't like commas in attributes)
@ -23,7 +19,6 @@ mod pin;
/// #[cfg_attr(feature = "specta", specta(rename = #rename))]
/// ```
#[proc_macro_attribute]
#[cfg(feature = "specta")]
pub fn specta(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,