diff --git a/.rustfmt.toml b/.rustfmt.toml index 63ac28ed..632534eb 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -6,7 +6,7 @@ reorder_imports = true imports_granularity = "Module" reorder_modules = true remove_nested_parens = true -edition = "2021" +edition = "2024" merge_derives = true use_try_shorthand = false use_field_init_shorthand = false diff --git a/Cargo.lock b/Cargo.lock index 1cfe1d1f..14939e2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2989,6 +2989,7 @@ name = "onelauncher_entity" version = "0.2.0-alpha.2" dependencies = [ "chrono", + "interpulse", "onelauncher_macro", "sea-orm", "serde", diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index f919111e..fd9c1e35 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -21,7 +21,7 @@ tauri = [ "specta", "gui" ] -specta = [ "dep:specta", "onelauncher_entity/specta" ] +specta = [ "dep:specta", "interpulse/specta", "onelauncher_entity/specta" ] cli = [ "dep:indicatif" ] gui = [] diff --git a/packages/core/examples/dirs.rs b/packages/core/examples/dirs.rs index 237ea680..81ed53a4 100644 --- a/packages/core/examples/dirs.rs +++ b/packages/core/examples/dirs.rs @@ -4,8 +4,8 @@ use onelauncher_core::store::Dirs; async fn main() { let dirs = Dirs::get().await.expect("couldn't initialize dirs"); - println!(" Base dir : {:?}", dirs.base_dir()); - println!("Launcher logs dir : {:?}", dirs.launcher_logs_dir()); - println!(" Metadata dir : {:?}", dirs.metadata_dir()); - println!(" DB file : {:?}", dirs.db_file()); + println!(" Base dir : {}", dirs.base_dir().display()); + println!("Launcher logs dir : {}", dirs.launcher_logs_dir().display()); + println!(" Metadata dir : {}", dirs.metadata_dir().display()); + println!(" DB file : {}", dirs.db_file().display()); } \ No newline at end of file diff --git a/packages/core/examples/metadata.rs b/packages/core/examples/metadata.rs new file mode 100644 index 00000000..51161b64 --- /dev/null +++ b/packages/core/examples/metadata.rs @@ -0,0 +1,20 @@ +use onelauncher_core::{api::proxy::ProxyDynamic, error::LauncherResult, initialize_core, store::{CoreOptions, State}}; +use onelauncher_entity::loader::GameLoader; + +#[tokio::main] +async fn main() -> LauncherResult<()> { + initialize_core(CoreOptions::default(), ProxyDynamic::default()).await?; + + let state = State::get().await?; + let metadata = &mut state.metadata.write().await; + + println!("Initialized: {}", metadata.initialized()); + println!("Minecraft: {:?}", metadata.get_vanilla(GameLoader::Vanilla).await.is_ok()); + println!("Forge: {:?}", metadata.get_modded(GameLoader::Forge).await.is_ok()); + println!("Initialized: {}", metadata.initialized()); + println!("NeoForge: {:?}", metadata.get_modded(GameLoader::NeoForge).await.is_ok()); + println!("Fabric: {:?}", metadata.get_modded(GameLoader::Fabric).await.is_ok()); + println!("Quilt: {:?}", metadata.get_modded(GameLoader::Quilt).await.is_ok()); + + Ok(()) +} \ No newline at end of file diff --git a/packages/core/src/api/cluster/sync.rs b/packages/core/src/api/cluster/sync.rs index 23fe6a98..4c94c997 100644 --- a/packages/core/src/api/cluster/sync.rs +++ b/packages/core/src/api/cluster/sync.rs @@ -65,7 +65,7 @@ pub async fn sync_clusters() -> LauncherResult> { // We have a directory that is not in the database if let Err(err) = sync_from_fs_to_db(&path).await { tracing::error!("failed to sync cluster from fs to db: {}", err); - }; + } } Ok(missing_ids) diff --git a/packages/core/src/api/ingress.rs b/packages/core/src/api/ingress.rs index ee6f10f5..a455eef5 100644 --- a/packages/core/src/api/ingress.rs +++ b/packages/core/src/api/ingress.rs @@ -30,6 +30,11 @@ pub async fn init_ingress_opt( #[tracing::instrument] pub async fn send_ingress(id: impl AsRef + std::fmt::Debug, increment: f64) -> LauncherResult<()> { + if !State::initialized() { + tracing::debug!("attempted to send ingress when state is not initialized"); + return Ok(()) + } + let state = State::get().await?; let processor = &state.ingress_processor; diff --git a/packages/core/src/api/proxy/mod.rs b/packages/core/src/api/proxy/mod.rs index 1b4e1b18..9b7f52ef 100644 --- a/packages/core/src/api/proxy/mod.rs +++ b/packages/core/src/api/proxy/mod.rs @@ -4,6 +4,8 @@ pub mod message; #[cfg(feature = "cli")] mod proxy_cli; mod proxy_empty; +use std::fmt::Debug; + #[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri; #[cfg(feature = "cli")] pub use proxy_cli::ProxyCli; pub use proxy_empty::ProxyEmpty; @@ -12,7 +14,7 @@ pub use proxy_empty::ProxyEmpty; #[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty; #[async_trait::async_trait] -pub trait LauncherProxy: Send + Sync { +pub trait LauncherProxy: Send + Sync + Debug { async fn send_ingress(&self, ingress: crate::store::ingress::IngressPayload) -> crate::LauncherResult<()>; async fn send_message(&self, message: message::MessagePayload) -> crate::LauncherResult<()>; @@ -22,4 +24,3 @@ pub trait LauncherProxy: Send + Sync { #[cfg(feature = "gui")] fn show_main_window(&self) -> crate::LauncherResult<()>; } - diff --git a/packages/core/src/api/proxy/proxy_cli.rs b/packages/core/src/api/proxy/proxy_cli.rs index 28a3e2f7..3bb8b311 100644 --- a/packages/core/src/api/proxy/proxy_cli.rs +++ b/packages/core/src/api/proxy/proxy_cli.rs @@ -8,7 +8,7 @@ use crate::{constants::CLI_TOTAL_INGRESS, store::ingress::IngressPayload, Launch use super::LauncherProxy; -#[derive(Default)] +#[derive(Default, Debug)] pub struct ProxyCli { ingress_feeds: RwLock>, } diff --git a/packages/core/src/api/proxy/proxy_empty.rs b/packages/core/src/api/proxy/proxy_empty.rs index 218e4ebf..e74d887f 100644 --- a/packages/core/src/api/proxy/proxy_empty.rs +++ b/packages/core/src/api/proxy/proxy_empty.rs @@ -2,7 +2,7 @@ use crate::{store::ingress::IngressPayload, LauncherResult}; use super::LauncherProxy; -#[derive(Default)] +#[derive(Default, Debug)] pub struct ProxyEmpty; impl ProxyEmpty { diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index aed20bdc..eb95c3e1 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -13,6 +13,8 @@ pub enum LauncherError { #[error(transparent)] DiscordError(#[from] crate::store::discord::DiscordError), #[error(transparent)] + MetadataError(#[from] crate::store::metadata::MetadataError), + #[error(transparent)] DaoError(#[from] DaoError), #[error(transparent)] diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5b7394c5..bcce3e2c 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,5 +1,7 @@ +#![feature(let_chains)] + use api::proxy::LauncherProxy; -use store::{proxy::ProxyState, Core, CoreOptions, Dirs, State}; +use store::{proxy::ProxyState, semaphore::SemaphoreStore, Core, CoreOptions, Dirs, State}; use error::LauncherResult; use logger::start_logger; @@ -21,8 +23,11 @@ pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherP Dirs::get().await?; start_logger().await; + SemaphoreStore::get().await; ProxyState::initialize(proxy_backend).await?; - State::get().await?; + let _ = State::get().await?; + + tracing::info!("Core initialized successfully"); Ok(()) } diff --git a/packages/core/src/logger.rs b/packages/core/src/logger.rs index c1a1e835..b755b535 100644 --- a/packages/core/src/logger.rs +++ b/packages/core/src/logger.rs @@ -2,7 +2,7 @@ // This will log to the console, and will not log to a file #[cfg(debug_assertions)] pub async fn start_logger() { - use tracing_subscriber::{prelude::*, util::SubscriberInitExt}; + use tracing_subscriber::{fmt::format::FmtSpan, prelude::*, util::SubscriberInitExt}; let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(format!("{}=info", env!("CARGO_PKG_NAME")))); @@ -15,6 +15,7 @@ pub async fn start_logger() { .with_line_number(true) .with_level(true) .with_thread_names(true) + .with_span_events(FmtSpan::FULL) .pretty() ) .with(filter) diff --git a/packages/core/src/store/metadata.rs b/packages/core/src/store/metadata.rs new file mode 100644 index 00000000..e6260f97 --- /dev/null +++ b/packages/core/src/store/metadata.rs @@ -0,0 +1,229 @@ +use std::str::FromStr; + +use interpulse::api::minecraft::VersionManifest as VanillaManifest; +use interpulse::api::modded::Manifest as ModdedManifest; +use onelauncher_entity::loader::GameLoader; +use reqwest::Method; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +use crate::constants; +use crate::error::LauncherResult; +use crate::utils::http::fetch_json; +use crate::utils::io; + +use super::Dirs; + +#[allow(clippy::unsafe_derive_deserialize)] +#[derive(Debug, Default)] +pub struct Metadata { + initialized: bool, + inner: MetadataInner, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +struct MetadataInner { + minecraft: Option, + forge: Option, + neo: Option, // TODO(metadata): change to neoforge + fabric: Option, + quilt: Option, + // legacyfabric: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum MetadataError { + #[error("failed to fetch metadata")] + FetchError, + #[error("loader {0} does not use a modded manifest")] + NotModdedManifest(GameLoader), + #[error("loader {0} does not use a vanilla manifest")] + NotVanillaManifest(GameLoader), + #[error("failed to parse metadata")] + ParseError(#[from] serde_json::Error), +} + +impl Metadata { + #[must_use] + pub const fn initialized(&self) -> bool { + self.initialized + } + + #[must_use] + pub fn initialize() -> Self { + Self::default() + } + + + pub async fn get_vanilla(&mut self, loader: GameLoader) -> LauncherResult<&VanillaManifest> { + if loader.is_mod_loader() { + return Err(MetadataError::NotVanillaManifest(loader).into()); + } + + if !self.initialized() { + self.load().await?; + } + + match loader { + GameLoader::Vanilla => self.inner.minecraft.as_ref(), + _ => None, + }.ok_or_else(|| MetadataError::FetchError.into()) + } + + pub async fn get_modded(&mut self, loader: GameLoader) -> LauncherResult<&ModdedManifest> { + if !loader.is_mod_loader() { + return Err(MetadataError::NotModdedManifest(loader).into()); + } + + if !self.initialized() { + self.load().await?; + } + + match loader { + GameLoader::Forge => self.inner.forge.as_ref(), + GameLoader::NeoForge => self.inner.neo.as_ref(), + GameLoader::Fabric => self.inner.fabric.as_ref(), + GameLoader::Quilt => self.inner.quilt.as_ref(), + _ => None, + }.ok_or_else(|| MetadataError::FetchError.into()) + } + + + async fn load(&mut self) -> LauncherResult<()> { + let path = Dirs::get_caches_dir().await?.join("metadata.json"); + let mut save_file = false; + + let mut metadata = Self::default(); + + if let Ok(metadata_json) = io::read_json::(&path).await { + metadata.inner = metadata_json; + + if metadata.refetch_errored().await > 0 { + save_file = true; + } + } else { + metadata.fetch_all().await; + save_file = true; + } + + if save_file { + io::create_dir_all(&path.parent().expect("couldn't get metadata.json parent")).await?; + io::write_json(&path, &metadata.inner).await?; + } + + *self = metadata; + self.initialized = true; + + Ok(()) + } + + /// Fetches the all missing metadata and returns the amount that were a success + #[tracing::instrument(skip_all)] + pub async fn refetch_errored(&mut self) -> u8 { + let mut changed: u8 = 0; + + macro_rules! check { + ($var:tt, $is_modded:tt) => { + if self.inner.$var.is_none() { + if let Ok(loader) = GameLoader::from_str(stringify!($var)) { + match check!(_fetch, loader, $is_modded) { + Ok(data) => { + self.inner.$var = Some(data); + changed += 1; + } + Err(err) => { + tracing::error!("failed to fetch manifest for {}: {}", loader, err); + } + } + }; + } + }; + + (_fetch, $loader:expr, false) => { + fetch_vanilla_manifest($loader).await + }; + + (_fetch, $loader:expr, true) => { + fetch_modded_manifest($loader).await + }; + } + + check!(minecraft, false); + check!(forge, true); + check!(neo, true); + check!(fabric, true); + check!(quilt, true); + + changed + } + + /// Force fetches all the manifests and sets them in the current instance + #[tracing::instrument(skip_all)] + pub async fn fetch_all(&mut self) { + macro_rules! fetch_manifest { + ($loader:expr, false) => { + fetch_vanilla_manifest($loader).await + }; + + ($loader:expr, true) => { + fetch_modded_manifest($loader).await + }; + } + + macro_rules! task { + ($(($var:tt, $is_modded:tt)),*) => { + let ($($var),*) = tokio::join!( + $( + tokio::spawn(async move { + if let Ok(loader) = GameLoader::from_str(stringify!(minecraft)) { + fetch_manifest!(loader, $is_modded).map_err(|e| $crate::send_error!("couldn't fetch {loader} manifest: {e}")).ok() + } else { + tracing::error!("failed to fetch {} manifest", stringify!($var)); + None + } + }), + )* + ); + + $( + if let Ok(data) = $var { + self.inner.$var = data; + } + )* + }; + } + + task! { + // (name, is_modded) + (minecraft, false), + (forge, true), + (neo, true), + (fabric, true), + (quilt, true) + }; + } +} + +pub async fn fetch_vanilla_manifest(loader: GameLoader) -> LauncherResult { + fetch_manifest(loader).await +} + +pub async fn fetch_modded_manifest(loader: GameLoader) -> LauncherResult { + fetch_manifest(loader).await +} + +pub async fn fetch_manifest(loader: GameLoader) -> LauncherResult { + fetch_json::( + Method::GET, + format!( + "{}/{}/v{}/manifest.json", + constants::METADATA_API_URL, + loader.get_format_name(), + loader.get_format_version() + ) + .as_str(), + None, + None, + ) + .await +} diff --git a/packages/core/src/store/mod.rs b/packages/core/src/store/mod.rs index 0181f555..d05025c5 100644 --- a/packages/core/src/store/mod.rs +++ b/packages/core/src/store/mod.rs @@ -2,6 +2,8 @@ pub(super) mod db; pub mod discord; pub mod ingress; pub mod proxy; +pub mod semaphore; +pub mod metadata; mod core_options; mod dirs; mod settings; diff --git a/packages/core/src/store/proxy.rs b/packages/core/src/store/proxy.rs index 6a787c80..7a449173 100644 --- a/packages/core/src/store/proxy.rs +++ b/packages/core/src/store/proxy.rs @@ -20,6 +20,7 @@ impl Deref for ProxyState { } impl ProxyState { + #[tracing::instrument(level = "debug")] pub async fn initialize(proxy: impl LauncherProxy + 'static) -> LauncherResult> { PROXY_STATE .get_or_try_init(|| async { diff --git a/packages/core/src/store/semaphore.rs b/packages/core/src/store/semaphore.rs new file mode 100644 index 00000000..03a92b76 --- /dev/null +++ b/packages/core/src/store/semaphore.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; + +use tokio::sync::{OnceCell, Semaphore}; + +static STORE_STATE: OnceCell> = OnceCell::const_new(); + +pub struct SemaphoreStore { + fetch: Arc +} + +impl SemaphoreStore { + pub async fn get() -> Arc { + STORE_STATE.get_or_init(|| async { Self::initialize() }).await.clone() + } + + #[tracing::instrument] + fn initialize() -> Arc { + Arc::new(Self { + fetch: Arc::new(Semaphore::new(10)), // Set the limit to 10 + }) + } + + pub async fn fetch() -> Arc { + Self::get().await.fetch.clone() + } +} \ No newline at end of file diff --git a/packages/core/src/store/state.rs b/packages/core/src/store/state.rs index 1edfceeb..1738244d 100644 --- a/packages/core/src/store/state.rs +++ b/packages/core/src/store/state.rs @@ -3,42 +3,32 @@ use std::sync::Arc; use sea_orm::DatabaseConnection; use tokio::sync::{OnceCell, RwLock}; -use crate::{utils::http::{self, FetchSemaphore}, LauncherResult}; +use crate::LauncherResult; -use super::{discord::DiscordRPC, ingress::IngressProcessor, Settings}; +use super::Settings; +use super::discord::DiscordRPC; +use super::ingress::IngressProcessor; +use super::metadata::Metadata; /// The static [`OnceCell>`] for storing the global runtime launcher state. -static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); +static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); pub struct State { pub ingress_processor: IngressProcessor, pub settings: RwLock, pub db: DatabaseConnection, + pub metadata: RwLock, pub rpc: Option, - - pub client: Arc, - pub(crate) fetch_semaphore: FetchSemaphore, } impl State { /// Get the current global launcher state (or initialize it) - pub async fn get() -> LauncherResult>> { - Ok(Arc::new( - LAUNCHER_STATE - .get_or_try_init(Self::initialize) - .await? - .read() - .await, - )) - } - - /// Get and lock writing to the current global launcher state (or initalize it) - pub async fn get_and_write() -> LauncherResult> { + pub async fn get() -> LauncherResult> { Ok(LAUNCHER_STATE .get_or_try_init(Self::initialize) .await? - .write() - .await) + .clone() + ) } pub fn initialized() -> bool { @@ -46,30 +36,26 @@ impl State { } #[tracing::instrument] - async fn initialize() -> LauncherResult> { + async fn initialize() -> LauncherResult> { let ingress_processor = IngressProcessor::new(); let settings = Settings::new().await; let db = super::db::create_pool().await?; + let metadata = Metadata::initialize(); let rpc = match DiscordRPC::initialize() { Ok(rpc) => Some(rpc), Err(err) => { tracing::warn!("{}", err); None - }, + } }; - let reqwest_client = http::create_client()?; - let fetch_semaphore = FetchSemaphore::new(settings.max_concurrent_requests); - - Ok(RwLock::new(Self { + Ok(Arc::new(Self { ingress_processor, settings: RwLock::new(settings), db, + metadata: RwLock::new(metadata), rpc, - - client: Arc::new(reqwest_client), - fetch_semaphore, })) } -} \ No newline at end of file +} diff --git a/packages/core/src/utils/http.rs b/packages/core/src/utils/http.rs index 350f4584..9fdc62bb 100644 --- a/packages/core/src/utils/http.rs +++ b/packages/core/src/utils/http.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::Path; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use bytes::Bytes; use reqwest::Method; @@ -13,7 +13,8 @@ use tokio_stream::StreamExt; use crate::api::ingress::send_ingress; use crate::error::LauncherResult; use crate::store::ingress::IngressRef; -use crate::store::{Core, State}; +use crate::store::semaphore::SemaphoreStore; +use crate::store::Core; use super::crypto::{CryptoError, HashAlgorithm}; use super::io; @@ -28,8 +29,8 @@ impl FetchSemaphore { } } -pub fn create_client() -> LauncherResult { - Ok(reqwest::ClientBuilder::new() +static REQWEST_CLIENT: LazyLock = LazyLock::new(|| { + reqwest::ClientBuilder::new() .tcp_keepalive(Some(std::time::Duration::from_secs(15))) .use_rustls_tls() .default_headers({ @@ -44,10 +45,12 @@ pub fn create_client() -> LauncherResult { headers.insert(reqwest::header::USER_AGENT, header); headers }) - .build()?) -} + .build() + .expect("failed to build reqwest client!") +}); -#[tracing::instrument(skip(body, headers, ingress_ref))] +#[tracing::instrument(level = "debug", skip(body, headers, ingress_ref))] +#[onelauncher_macro::pin] pub async fn fetch_advanced( method: Method, url: &str, @@ -56,9 +59,9 @@ pub async fn fetch_advanced( headers: Option>, ingress_ref: Option<&IngressRef<'_>>, ) -> LauncherResult { - let state = State::get().await?; - let _permit = state.fetch_semaphore.0.acquire().await?; - let client = &state.client; + let semaphore = SemaphoreStore::fetch().await; + let _permit = semaphore.acquire().await?; + let client = &REQWEST_CLIENT; for attempt in 0..Core::get().fetch_attempts { let mut req = client.request(method.clone(), url); diff --git a/packages/core/src/utils/io.rs b/packages/core/src/utils/io.rs index 2a8bf79b..dc0d70c5 100644 --- a/packages/core/src/utils/io.rs +++ b/packages/core/src/utils/io.rs @@ -1,4 +1,5 @@ use async_compression::tokio::bufread::GzipDecoder; +use serde::{de::DeserializeOwned, Serialize}; use tempfile::TempDir; use tokio::io::AsyncReadExt; @@ -18,6 +19,8 @@ pub enum IOError { ZipError(#[from] zip::result::ZipError), #[error(transparent)] IOError(#[from] std::io::Error), + #[error("json deserialization error: {0}")] + DeserializeError(#[from] serde_json::Error), } impl> From<(P, std::io::Error)> for IOError { @@ -119,7 +122,7 @@ pub async fn read_to_string(path: impl AsRef) -> Result) -> Result, IOError> { let path = path.as_ref(); tokio::fs::read(path) @@ -130,6 +133,11 @@ pub async fn read(path: impl AsRef) -> Result, IOError> }) } +/// Asynchronously read a file as JSON and return the deserialized object +pub async fn read_json(path: impl AsRef) -> Result { + Ok(serde_json::from_slice(&read(path).await?)?) +} + /// Asynchrously write to a tempfile that is then transferred to an official [`AsRef`]. pub async fn write( path: impl AsRef, @@ -144,6 +152,14 @@ pub async fn write( }) } +/// Asynchronously write json to a file, creating it if it does not exist. +pub async fn write_json( + path: impl AsRef, + data: T +) -> Result<(), IOError> { + write(path, serde_json::to_vec(&data)?).await +} + /// Renames a file or directory to a new name, replacing the original file if `to` already exists. pub async fn rename( from: impl AsRef, diff --git a/packages/entity/Cargo.toml b/packages/entity/Cargo.toml index 87b612fb..0a435b67 100644 --- a/packages/entity/Cargo.toml +++ b/packages/entity/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" [features] default = [] -specta = [ "dep:specta" ] +specta = [ "dep:specta", "interpulse/specta" ] [dependencies] onelauncher_macro = { workspace = true } @@ -26,4 +26,5 @@ serde_json = { workspace = true } chrono = { workspace = true } uuid = { workspace = true } url = { workspace = true } +interpulse = { workspace = true } specta = { workspace = true, optional = true } diff --git a/packages/entity/src/domain/loader.rs b/packages/entity/src/domain/loader.rs index aebe6129..3b0619bf 100644 --- a/packages/entity/src/domain/loader.rs +++ b/packages/entity/src/domain/loader.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "lowercase")] #[sea_orm(rs_type = "u8", db_type = "Integer")] pub enum GameLoader { - #[default] Vanilla = 0, + #[default] Unknown = 1, Forge = 2, NeoForge = 3, @@ -25,6 +25,25 @@ impl GameLoader { matches!(self, Self::Forge | Self::NeoForge | Self::Quilt | Self::Fabric | Self::LegacyFabric) } + pub const fn get_format_version(&self) -> usize { + match self { + Self::Vanilla | Self::Unknown => interpulse::api::minecraft::CURRENT_FORMAT_VERSION, + Self::Forge => interpulse::api::modded::CURRENT_FORGE_FORMAT_VERSION, + Self::NeoForge => interpulse::api::modded::CURRENT_NEOFORGE_FORMAT_VERSION, + Self::Quilt => interpulse::api::modded::CURRENT_QUILT_FORMAT_VERSION, + Self::Fabric => interpulse::api::modded::CURRENT_FABRIC_FORMAT_VERSION, + Self::LegacyFabric => interpulse::api::modded::CURRENT_LEGACY_FABRIC_FORMAT_VERSION, + } + } + + pub fn get_format_name(&self) -> String { + match self { + Self::Vanilla | Self::Unknown => String::from("minecraft"), + Self::NeoForge => String::from("neo"), // TODO(metadata): change to neoforge + _ => self.to_string().to_lowercase().replace(['_', '.', ' '], "") + } + } + } impl From for GameLoader { diff --git a/packages/macro/src/lib.rs b/packages/macro/src/lib.rs index 081a24de..e30805fd 100644 --- a/packages/macro/src/lib.rs +++ b/packages/macro/src/lib.rs @@ -1,5 +1,5 @@ -use quote::quote; -use syn::{meta::ParseNestedMeta, parse_macro_input, Item, Result}; +mod specta; +mod pin; /// 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) @@ -10,43 +10,11 @@ use syn::{meta::ParseNestedMeta, parse_macro_input, Item, Result}; /// ``` #[proc_macro_attribute] pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { - let mut attrs = SpectaAttributes::default(); - let parser = syn::meta::parser(|meta| attrs.parse(meta)); - parse_macro_input!(attr with parser); - - let input = parse_macro_input!(item as Item); - - let specta_event_type = if attrs.with_event { - quote! { - #[cfg_attr(feature = "tauri", derive(tauri_specta::Event))] - } - } else { - quote! {} - }; - - let specta_type = quote! { - #[cfg_attr(feature = "specta", derive(specta::Type))] - }; - - let expanded = quote! { - #specta_event_type - #specta_type - #input - }; - - expanded.into() + specta::specta(attr, item) } -#[derive(Default)] -struct SpectaAttributes { - with_event: bool, +/// This macro Box::Pin a function +#[proc_macro_attribute] +pub fn pin(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { + pin::pin(attr, item) } - -impl SpectaAttributes { - fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> { - if meta.path.is_ident("with_event") { - self.with_event = true; - } - Ok(()) - } -} \ No newline at end of file diff --git a/packages/macro/src/pin.rs b/packages/macro/src/pin.rs new file mode 100644 index 00000000..ac1ae23a --- /dev/null +++ b/packages/macro/src/pin.rs @@ -0,0 +1,22 @@ +use quote::quote; +use syn::{parse_macro_input, ItemFn}; + +pub fn pin(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(item as ItemFn); + + let attrs = &input.attrs; + let vis = &input.vis; + let sig = &input.sig; + let body = &input.block; + + let result = quote! { + #(#attrs)* + #vis #sig { + Box::pin(async move { + #body + }).await + } + }; + + result.into() +} \ No newline at end of file diff --git a/packages/macro/src/specta.rs b/packages/macro/src/specta.rs new file mode 100644 index 00000000..de124454 --- /dev/null +++ b/packages/macro/src/specta.rs @@ -0,0 +1,44 @@ +use quote::quote; +use syn::{meta::ParseNestedMeta, parse_macro_input, Item, Result}; + +pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let mut attrs = SpectaAttributes::default(); + let parser = syn::meta::parser(|meta| attrs.parse(meta)); + parse_macro_input!(attr with parser); + + let input = parse_macro_input!(item as Item); + + let specta_event_type = if attrs.with_event { + quote! { + #[cfg_attr(feature = "tauri", derive(tauri_specta::Event))] + } + } else { + quote! {} + }; + + let specta_type = quote! { + #[cfg_attr(feature = "specta", derive(specta::Type))] + }; + + let expanded = quote! { + #specta_event_type + #specta_type + #input + }; + + expanded.into() +} + +#[derive(Default)] +struct SpectaAttributes { + with_event: bool, +} + +impl SpectaAttributes { + fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> { + if meta.path.is_ident("with_event") { + self.with_event = true; + } + Ok(()) + } +} \ No newline at end of file