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]
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 Some(rpc) = &state.rpc else {
@ -10,11 +14,15 @@ async fn main() -> LauncherResult<()> {
return Ok(());
};
rpc.set_message("Hello, Discord!").await;
rpc.set_message("Hello, Discord!", None).await;
println!("Set Discord message");
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");
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]
async fn main() -> LauncherResult<()> {
initialize_core(ProxyEmpty::new()).await?;
initialize_core(CoreOptions::default(), ProxyEmpty::new()).await?;
let java = java::locate_java().await?;

View file

@ -2,19 +2,7 @@
//!
//! 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 ===========
/// 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`].
#[cfg(feature = "cli")]
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";
// =========== Authentication ===========
/// Our Discord RPC client ID.
pub const DISCORD_RPC_CLIENT_ID: &str = "1274084193193955408";
/// Our Microsoft client ID.
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.
pub const MINECRAFT_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
// =========== API ===========
// !!! URLS must NOT have a trailing slash. !!!
/// The Modrinth API base url.
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2";
/// 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";
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";
/// 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.
pub const CURSEFORGE_GAME_ID: u32 = 432;
/// Our metadata API base url.

View file

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

View file

@ -1,5 +1,5 @@
use api::proxy::LauncherProxy;
use store::{proxy::ProxyState, Dirs, State};
use store::{proxy::ProxyState, Core, CoreOptions, Dirs, State};
use error::LauncherResult;
use logger::start_logger;
@ -14,7 +14,8 @@ mod logger;
pub use onelauncher_entity as entity;
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?;
start_logger().await;
@ -22,4 +23,4 @@ pub async fn initialize_core(proxy_backend: impl LauncherProxy + 'static) -> Lau
State::get().await?;
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 crate::{constants, utils, LauncherResult};
use crate::{utils, LauncherResult};
use super::Core;
/// The static [`OnceCell<RwLock<Dirs>>`] for storing the global directory state.
/// Should be initialized as soon as possible (preferably before logging)
@ -21,7 +23,7 @@ impl Dirs {
fn initialize() -> LauncherResult<Self> {
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(Self {

View file

@ -1,33 +1,45 @@
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 crate::{constants, error::{LauncherError, LauncherResult}};
use crate::error::LauncherResult;
use super::Core;
pub struct DiscordRPC {
started_at: i64,
client: Arc<RwLock<DiscordIpcClient>>,
connected: Arc<AtomicBool>,
}
impl DiscordRPC {
pub fn initialize() -> LauncherResult<Self> {
let mut client = DiscordIpcClient::new(constants::DISCORD_RPC_CLIENT_ID)
.map_err(|_| LauncherError::DiscordError)?;
let client_id = Core::get().discord_client_id.clone()
.ok_or(DiscordError::MissingClientId)?;
let mut client = DiscordIpcClient::new(client_id.as_str())
.map_err(|_| DiscordError::ConnectError)?;
let connected = client.connect().is_ok();
Ok(Self {
started_at: chrono::Utc::now().timestamp(),
client: Arc::new(RwLock::new(client)),
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(
Activity::new()
.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::new()
.large_image("onelauncher_logo_512")
@ -92,4 +104,12 @@ impl DiscordRPC {
fn set_connected(&self, connected: bool) {
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 proxy;
pub mod java;
mod core_options;
mod dirs;
mod settings;
mod state;
pub use dirs::*;
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 settings = Settings::new().await;
let db = super::db::create_pool().await?;
let rpc = match DiscordRPC::initialize() {
Ok(rpc) => Some(rpc),
Err(err) => {

View file

@ -8,7 +8,7 @@ use serde::Deserialize;
use tokio::sync::Semaphore;
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};
@ -29,9 +29,10 @@ pub fn create_client() -> LauncherResult<reqwest::Client> {
.default_headers({
let mut headers = reqwest::header::HeaderMap::new();
let header = reqwest::header::HeaderValue::from_str(&format!(
"{}/{} (https://polyfrost.org)",
constants::NAME,
constants::VERSION,
"{}/{} ({})",
Core::get().launcher_name,
Core::get().launcher_version,
Core::get().launcher_website,
))
.expect("failed to build reqwest headers!");
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 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);
if let Some(body) = body.clone() {