feat: Customizable core options

This commit is contained in:
LynithDev 2025-04-03 13:02:48 +02:00
parent bb2d33bef0
commit 11d2ff43ed
11 changed files with 129 additions and 50 deletions

View file

@ -1,8 +1,12 @@
use onelauncher_core::{api::proxy::proxy_empty::ProxyEmpty, error::LauncherResult, initialize_core, store::State}; use discord_rich_presence::activity::Timestamps;
use onelauncher_core::{api::proxy::proxy_empty::ProxyEmpty, error::LauncherResult, initialize_core, store::{CoreOptions, State}};
#[tokio::main] #[tokio::main]
async fn main() -> LauncherResult<()> { async fn main() -> LauncherResult<()> {
initialize_core(ProxyEmpty::new()).await?; initialize_core(CoreOptions {
discord_client_id: Some(String::from("1274084193193955408")),
..Default::default()
}, ProxyEmpty::new()).await?;
let state = State::get().await?; let state = State::get().await?;
let Some(rpc) = &state.rpc else { let Some(rpc) = &state.rpc else {
@ -10,11 +14,15 @@ async fn main() -> LauncherResult<()> {
return Ok(()); return Ok(());
}; };
rpc.set_message("Hello, Discord!").await; rpc.set_message("Hello, Discord!", None).await;
println!("Set Discord message"); println!("Set Discord message");
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
rpc.set_message("This user is testing Discord RPC!").await; rpc.set_message("New timestamp.", Some(Timestamps::new())).await;
println!("Set Discord message");
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
rpc.set_message("Reverted back to the old timestamp!", None).await;
println!("Set Discord message"); println!("Set Discord message");
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;

View file

@ -1,8 +1,8 @@
use onelauncher_core::{api::{java, proxy::proxy_empty::ProxyEmpty}, error::LauncherResult, initialize_core}; use onelauncher_core::{api::{java, proxy::proxy_empty::ProxyEmpty}, error::LauncherResult, initialize_core, store::CoreOptions};
#[tokio::main] #[tokio::main]
async fn main() -> LauncherResult<()> { async fn main() -> LauncherResult<()> {
initialize_core(ProxyEmpty::new()).await?; initialize_core(CoreOptions::default(), ProxyEmpty::new()).await?;
let java = java::locate_java().await?; let java = java::locate_java().await?;

View file

@ -2,19 +2,7 @@
//! //!
//! Public constant variables and strings that are used throughout the launcher. //! Public constant variables and strings that are used throughout the launcher.
// =========== Core Metadata ===========
/// The name of the launcher.
pub const NAME: &str = match option_env!("LAUNCHER_NAME") {
Some(name) => name,
None => "OneLauncher",
};
/// The version of the launcher (from `../Cargo.toml` env).
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
// =========== Default Ingress =========== // =========== Default Ingress ===========
/// The amount of attempts to fetch a file before a request fails.
pub const FETCH_ATTEMPTS: usize = 3;
/// The total ingress capacity of the CLI, provided to [`indicatif`]. /// The total ingress capacity of the CLI, provided to [`indicatif`].
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
pub const CLI_TOTAL_INGRESS: u64 = 1000; pub const CLI_TOTAL_INGRESS: u64 = 1000;
@ -22,27 +10,17 @@ pub const CLI_TOTAL_INGRESS: u64 = 1000;
pub const DUMMY_REPLACE_NEWLINE: &str = "\n"; pub const DUMMY_REPLACE_NEWLINE: &str = "\n";
// =========== Authentication =========== // =========== Authentication ===========
/// Our Discord RPC client ID.
pub const DISCORD_RPC_CLIENT_ID: &str = "1274084193193955408";
/// Our Microsoft client ID. /// Our Microsoft client ID.
pub const MICROSOFT_CLIENT_ID: &str = "9eac3a4e-8cdd-43ef-863e-49cd601b1f03"; pub const MICROSOFT_CLIENT_ID: &str = "9eac3a4e-8cdd-43ef-863e-49cd601b1f03";
/// Mojang/Microsoft client ID.
pub const MINECRAFT_CLIENT_ID: &str = "00000000402b5328";
/// Mojang/Microsoft login redirect URI.
pub const MINECRAFT_REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
/// Mojang/Microsoft login xboxlive scopes to get tokens. /// Mojang/Microsoft login xboxlive scopes to get tokens.
pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL"; pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
// =========== API =========== // =========== API ===========
// !!! URLS must NOT have a trailing slash. !!! // !!! URLS must NOT have a trailing slash. !!!
/// The Modrinth API base url. /// The Modrinth API base url.
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2"; pub const MODRINTH_API_URL: &str = "https://api.modrinth.com";
/// The Modrinth V3 API base url. Used for things like fetching information about organizations.
pub const MODRINTH_V3_API_URL: &str = "https://api.modrinth.com/v3";
/// The Curseforge API base url. /// 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";
/// The Curseforge API key. Reserved for use in Polyfrost projects only. Do not use in other projects without permission from the Polyfrost team.
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
/// The Minecraft game ID on Curseforge. /// The Minecraft game ID on Curseforge.
pub const CURSEFORGE_GAME_ID: u32 = 432; pub const CURSEFORGE_GAME_ID: u32 = 432;
/// Our metadata API base url. /// Our metadata API base url.

View file

@ -10,9 +10,8 @@ pub enum LauncherError {
JavaError(#[from] crate::api::java::JavaError), JavaError(#[from] crate::api::java::JavaError),
#[error(transparent)] #[error(transparent)]
CryptoError(#[from] crate::utils::crypto::CryptoError), CryptoError(#[from] crate::utils::crypto::CryptoError),
#[error(transparent)]
#[error("failed to connect to discord rpc socket")] DiscordError(#[from] crate::store::discord::DiscordError),
DiscordError,
#[error(transparent)] #[error(transparent)]
SerdeError(#[from] serde_json::Error), SerdeError(#[from] serde_json::Error),

View file

@ -1,5 +1,5 @@
use api::proxy::LauncherProxy; use api::proxy::LauncherProxy;
use store::{proxy::ProxyState, Dirs, State}; use store::{proxy::ProxyState, Core, CoreOptions, Dirs, State};
use error::LauncherResult; use error::LauncherResult;
use logger::start_logger; use logger::start_logger;
@ -14,7 +14,8 @@ mod logger;
pub use onelauncher_entity as entity; pub use onelauncher_entity as entity;
pub use onelauncher_migration as migration; pub use onelauncher_migration as migration;
pub async fn initialize_core(proxy_backend: impl LauncherProxy + 'static) -> LauncherResult<()> { pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherProxy + 'static) -> LauncherResult<()> {
Core::initialize(options).await?;
Dirs::get().await?; Dirs::get().await?;
start_logger().await; start_logger().await;
@ -22,4 +23,4 @@ pub async fn initialize_core(proxy_backend: impl LauncherProxy + 'static) -> Lau
State::get().await?; State::get().await?;
Ok(()) Ok(())
} }

View file

@ -0,0 +1,68 @@
use std::{ops::Deref, sync::Arc};
use tokio::sync::OnceCell;
use crate::error::{LauncherError, LauncherResult};
static CORE_STATE: OnceCell<Arc<Core>> = OnceCell::const_new();
pub struct Core(CoreOptions);
/// Used for "customizable constants" for the core
pub struct CoreOptions {
pub discord_client_id: Option<String>,
pub launcher_name: String,
pub launcher_version: String,
pub launcher_website: String,
pub fetch_attempts: u8,
/// Default client id is the same as the one used by the official launcher
pub msa_client_id: String,
pub msa_redirect_uri: String,
pub curseforge_api_key: Option<String>,
}
impl Default for CoreOptions {
fn default() -> Self {
Self {
discord_client_id: None,
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: String::from("00000000402b5328"),
msa_redirect_uri: String::from("https://login.live.com/oauth20_desktop.srf"),
curseforge_api_key: None,
}
}
}
impl Core {
#[must_use]
pub fn new(options: CoreOptions) -> Arc<Self> {
Arc::new(Self(options))
}
pub async fn initialize(options: CoreOptions) -> LauncherResult<()> {
CORE_STATE.get_or_try_init(|| async move {
Ok::<_, LauncherError>(Self::new(options))
}).await?;
Ok(())
}
pub fn get() -> Arc<Self> {
CORE_STATE
.get()
.expect("core was not initialized")
.clone()
}
}
impl Deref for Core {
type Target = CoreOptions;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View file

@ -2,7 +2,9 @@ use std::path::PathBuf;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use crate::{constants, utils, LauncherResult}; use crate::{utils, LauncherResult};
use super::Core;
/// The static [`OnceCell<RwLock<Dirs>>`] for storing the global directory state. /// The static [`OnceCell<RwLock<Dirs>>`] for storing the global directory state.
/// Should be initialized as soon as possible (preferably before logging) /// Should be initialized as soon as possible (preferably before logging)
@ -21,7 +23,7 @@ impl Dirs {
fn initialize() -> LauncherResult<Self> { fn initialize() -> LauncherResult<Self> {
let base_dir = utils::io::env_path("LAUNCHER_DIR") let base_dir = utils::io::env_path("LAUNCHER_DIR")
.or_else(|| Some(dirs::data_dir()?.join(constants::NAME))) .or_else(|| Some(dirs::data_dir()?.join(Core::get().launcher_name.clone())))
.ok_or(DirectoryError::BaseDir)?; .ok_or(DirectoryError::BaseDir)?;
Ok(Self { Ok(Self {

View file

@ -1,33 +1,45 @@
use std::sync::{atomic::AtomicBool, Arc}; use std::sync::{atomic::AtomicBool, Arc};
use discord_rich_presence::{activity::{Activity, Assets, Button}, DiscordIpc, DiscordIpcClient}; use discord_rich_presence::{activity::{Activity, Assets, Button, Timestamps}, DiscordIpc, DiscordIpcClient};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{constants, error::{LauncherError, LauncherResult}}; use crate::error::LauncherResult;
use super::Core;
pub struct DiscordRPC { pub struct DiscordRPC {
started_at: i64,
client: Arc<RwLock<DiscordIpcClient>>, client: Arc<RwLock<DiscordIpcClient>>,
connected: Arc<AtomicBool>, connected: Arc<AtomicBool>,
} }
impl DiscordRPC { impl DiscordRPC {
pub fn initialize() -> LauncherResult<Self> { pub fn initialize() -> LauncherResult<Self> {
let mut client = DiscordIpcClient::new(constants::DISCORD_RPC_CLIENT_ID) let client_id = Core::get().discord_client_id.clone()
.map_err(|_| LauncherError::DiscordError)?; .ok_or(DiscordError::MissingClientId)?;
let mut client = DiscordIpcClient::new(client_id.as_str())
.map_err(|_| DiscordError::ConnectError)?;
let connected = client.connect().is_ok(); let connected = client.connect().is_ok();
Ok(Self { Ok(Self {
started_at: chrono::Utc::now().timestamp(),
client: Arc::new(RwLock::new(client)), client: Arc::new(RwLock::new(client)),
connected: Arc::new(AtomicBool::new(connected)), connected: Arc::new(AtomicBool::new(connected)),
}) })
} }
pub async fn set_message(&self, msg: &str) -> bool { pub async fn set_message(&self, msg: &str, timestamp: Option<Timestamps>) -> bool {
self.set_activity( self.set_activity(
Activity::new() Activity::new()
.state(msg) .state(msg)
.buttons(vec![Button::new("Website", "https://polyfrost.org/")]) .buttons(
vec![Button::new("Website", "https://polyfrost.org/")]
)
.timestamps(
timestamp.unwrap_or_else(|| Timestamps::new().start(self.started_at))
)
.assets( .assets(
Assets::new() Assets::new()
.large_image("onelauncher_logo_512") .large_image("onelauncher_logo_512")
@ -92,4 +104,12 @@ impl DiscordRPC {
fn set_connected(&self, connected: bool) { fn set_connected(&self, connected: bool) {
self.connected.store(connected, std::sync::atomic::Ordering::Relaxed); self.connected.store(connected, std::sync::atomic::Ordering::Relaxed);
} }
}
#[derive(Debug, thiserror::Error)]
pub enum DiscordError {
#[error("Discord client ID is missing")]
MissingClientId,
#[error("couldn't connect to Discord IPC")]
ConnectError,
} }

View file

@ -3,10 +3,12 @@ pub mod discord;
pub mod ingress; pub mod ingress;
pub mod proxy; pub mod proxy;
pub mod java; pub mod java;
mod core_options;
mod dirs; mod dirs;
mod settings; mod settings;
mod state; mod state;
pub use dirs::*; pub use dirs::*;
pub use settings::*; pub use settings::*;
pub use state::*; pub use state::*;
pub use core_options::*;

View file

@ -50,7 +50,7 @@ impl State {
let ingress_processor = IngressProcessor::new(); let ingress_processor = IngressProcessor::new();
let settings = Settings::new().await; let settings = Settings::new().await;
let db = super::db::create_pool().await?; let db = super::db::create_pool().await?;
let rpc = match DiscordRPC::initialize() { let rpc = match DiscordRPC::initialize() {
Ok(rpc) => Some(rpc), Ok(rpc) => Some(rpc),
Err(err) => { Err(err) => {

View file

@ -8,7 +8,7 @@ use serde::Deserialize;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use crate::{api::ingress::send_ingress, constants, error::LauncherResult, store::{ingress::IngressId, State}}; use crate::{api::ingress::send_ingress, error::LauncherResult, store::{ingress::IngressId, Core, State}};
use super::{crypto::{CryptoError, HashAlgorithm}, io}; use super::{crypto::{CryptoError, HashAlgorithm}, io};
@ -29,9 +29,10 @@ pub fn create_client() -> LauncherResult<reqwest::Client> {
.default_headers({ .default_headers({
let mut headers = reqwest::header::HeaderMap::new(); let mut headers = reqwest::header::HeaderMap::new();
let header = reqwest::header::HeaderValue::from_str(&format!( let header = reqwest::header::HeaderValue::from_str(&format!(
"{}/{} (https://polyfrost.org)", "{}/{} ({})",
constants::NAME, Core::get().launcher_name,
constants::VERSION, Core::get().launcher_version,
Core::get().launcher_website,
)) ))
.expect("failed to build reqwest headers!"); .expect("failed to build reqwest headers!");
headers.insert(reqwest::header::USER_AGENT, header); headers.insert(reqwest::header::USER_AGENT, header);
@ -53,7 +54,7 @@ pub async fn fetch_advanced(
let _permit = state.fetch_semaphore.0.acquire().await?; let _permit = state.fetch_semaphore.0.acquire().await?;
let client = &state.client; let client = &state.client;
for attempt in 0..constants::FETCH_ATTEMPTS { for attempt in 0..Core::get().fetch_attempts {
let mut req = client.request(method.clone(), url); let mut req = client.request(method.clone(), url);
if let Some(body) = body.clone() { if let Some(body) = body.clone() {