diff --git a/packages/oneclient_app/src/hooks/mod.rs b/packages/oneclient_app/src/hooks/mod.rs index dd5b607e..3d8e13c8 100644 --- a/packages/oneclient_app/src/hooks/mod.rs +++ b/packages/oneclient_app/src/hooks/mod.rs @@ -19,6 +19,7 @@ pub use queries::{ ClusterAction, invalidate_cluster_queries, invalidate_profile_queries, use_cluster_mutation, AddOfflineAccountKeys, RefreshAccountKeys, RemoveAccountKeys, SetDefaultAccountKeys, UseRefreshAccount, UseRemoveAccount, UseSetDefaultAccount, accounts_have_microsoft, + login_code_already_handled, mutation_error, mutation_is_pending, pick_version_metadata, try_account, try_accounts, try_default_account, try_game_profile, use_account, use_accounts, use_add_microsoft_account, use_add_offline_account, use_begin_microsoft_login, use_bundle_updates, diff --git a/packages/oneclient_app/src/hooks/queries/auth.rs b/packages/oneclient_app/src/hooks/queries/auth.rs index 63fff5d9..dfed2d9b 100644 --- a/packages/oneclient_app/src/hooks/queries/auth.rs +++ b/packages/oneclient_app/src/hooks/queries/auth.rs @@ -1,4 +1,6 @@ +use std::sync::Mutex; + use freya::query::{ Mutation, MutationCapability, MutationStateData, QueriesStorage, Query, QueryCapability, QueryStateData, UseMutation, UseQuery, use_mutation, use_query, @@ -7,6 +9,17 @@ use oneclient_core::LauncherError; use oneclient_core::auth::{self, AccountKind, MinecraftAccount, MinecraftLogin}; use uuid::Uuid; +static HANDLED_LOGIN_CODE: Mutex> = Mutex::new(None); + +pub fn login_code_already_handled(user_code: &str) -> bool { + let mut handled = HANDLED_LOGIN_CODE.lock().unwrap(); + if handled.as_deref() == Some(user_code) { + return true; + } + *handled = Some(user_code.to_string()); + false +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ListAccountsKeys; diff --git a/packages/oneclient_app/src/hooks/queries/mod.rs b/packages/oneclient_app/src/hooks/queries/mod.rs index 6c7b9fdd..784506e9 100644 --- a/packages/oneclient_app/src/hooks/queries/mod.rs +++ b/packages/oneclient_app/src/hooks/queries/mod.rs @@ -57,6 +57,7 @@ pub use versions::{use_versions, use_loader_versions, loader_versions}; pub use auth::{ AddOfflineAccountKeys, RefreshAccountKeys, RemoveAccountKeys, SetDefaultAccountKeys, UseRefreshAccount, UseRemoveAccount, UseSetDefaultAccount, accounts_have_microsoft, + login_code_already_handled, mutation_error, mutation_is_pending, try_account, try_accounts, try_default_account, use_account, use_accounts, use_add_microsoft_account, use_add_offline_account, use_begin_microsoft_login, use_current_account, use_default_account, diff --git a/packages/oneclient_app/src/layout/app_shell.rs b/packages/oneclient_app/src/layout/app_shell.rs index ce8b7aac..e727b02f 100644 --- a/packages/oneclient_app/src/layout/app_shell.rs +++ b/packages/oneclient_app/src/layout/app_shell.rs @@ -492,7 +492,7 @@ half4 main(float2 fragCoord) { } "#; -fn gradient_overlay_radial() -> impl IntoElement { +pub(crate) fn gradient_overlay_radial() -> impl IntoElement { let effect = use_hook(|| { freya::engine::prelude::RuntimeEffect::make_for_shader(VIGNETTE_SPOTLIGHT_SHADER, None) .expect("Failed to compile vignette shader") diff --git a/packages/oneclient_app/src/layout/mod.rs b/packages/oneclient_app/src/layout/mod.rs index 209e4a4c..bab99f5a 100644 --- a/packages/oneclient_app/src/layout/mod.rs +++ b/packages/oneclient_app/src/layout/mod.rs @@ -7,6 +7,7 @@ mod settings_shell; pub use animated_outlet::AnimatedAppOutlet; pub use app_shell::{AppShell, HOME_BACKGROUND_ASSET}; +pub(crate) use app_shell::gradient_overlay_radial; pub use cluster_shell::{ClusterShell, cluster_content}; pub use onboarding_shell::OnboardingShell; pub use root_layout::RootLayout; diff --git a/packages/oneclient_app/src/view/app/accounts.rs b/packages/oneclient_app/src/view/app/accounts.rs index 582395b0..da8e86fe 100644 --- a/packages/oneclient_app/src/view/app/accounts.rs +++ b/packages/oneclient_app/src/view/app/accounts.rs @@ -1,5 +1,3 @@ -use std::sync::Mutex; - use chrono::Utc; use freya::prelude::*; use freya::query::{MutationCapability, MutationStateData, UseMutation}; @@ -11,15 +9,14 @@ use crate::components::{ }; use crate::hooks::{ AddOfflineAccountKeys, RefreshAccountKeys, RemoveAccountKeys, SetDefaultAccountKeys, - accounts_have_microsoft, try_accounts, try_default_account, use_accounts, - use_add_offline_account, use_begin_microsoft_login, use_current_account, + accounts_have_microsoft, login_code_already_handled, try_accounts, try_default_account, + use_accounts, use_add_offline_account, use_begin_microsoft_login, use_current_account, use_finish_microsoft_login, use_refresh_account, use_remove_account, use_set_default_account, }; use crate::platform; use crate::theme::colors; use crate::ui::border_all_color; -static HANDLED_LOGIN_CODE: Mutex> = Mutex::new(None); #[derive(PartialEq)] pub struct Accounts; @@ -48,12 +45,8 @@ impl Component for Accounts { _ => None, }; let Some(login) = login else { return }; - { - let mut handled = HANDLED_LOGIN_CODE.lock().unwrap(); - if handled.as_deref() == Some(login.user_code.as_str()) { - return; - } - *handled = Some(login.user_code.clone()); + if login_code_already_handled(&login.user_code) { + return; } handled_code.set(Some(login.user_code.clone())); finish.mutate(login.clone()); diff --git a/packages/oneclient_app/src/view/onboarding/account.rs b/packages/oneclient_app/src/view/onboarding/account.rs index ffce6cde..0c9027ea 100644 --- a/packages/oneclient_app/src/view/onboarding/account.rs +++ b/packages/oneclient_app/src/view/onboarding/account.rs @@ -1,5 +1,3 @@ -use std::sync::Mutex; - use freya::prelude::*; use freya::query::{MutationCapability, MutationStateData, UseMutation}; use freya::text_edit::Clipboard; @@ -7,8 +5,8 @@ use oneclient_core::auth::{MinecraftAccount, MinecraftLogin}; use crate::components::{Avatar, Button, Icon, IconType, OverlayPopup}; use crate::hooks::{ - try_default_account, use_begin_microsoft_login, use_current_account, - use_finish_microsoft_login, + login_code_already_handled, try_default_account, use_begin_microsoft_login, + use_current_account, use_finish_microsoft_login, }; use crate::platform; use crate::routes::Route; @@ -18,8 +16,6 @@ use crate::view::onboarding::{ onboarding_illustration, onboarding_nav, onboarding_page, step_heading, }; -static HANDLED_LOGIN_CODE: Mutex> = Mutex::new(None); - #[derive(PartialEq)] pub struct OnboardingAccount; @@ -38,12 +34,8 @@ impl Component for OnboardingAccount { _ => None, }; let Some(login) = login else { return }; - { - let mut handled = HANDLED_LOGIN_CODE.lock().unwrap(); - if handled.as_deref() == Some(login.user_code.as_str()) { - return; - } - *handled = Some(login.user_code.clone()); + if login_code_already_handled(&login.user_code) { + return; } finish.mutate(login.clone()); pending_login.set(Some(login)); diff --git a/packages/oneclient_app/src/view/onboarding/downloading/backdrop.rs b/packages/oneclient_app/src/view/onboarding/downloading/backdrop.rs index 63f3931d..efce9c88 100644 --- a/packages/oneclient_app/src/view/onboarding/downloading/backdrop.rs +++ b/packages/oneclient_app/src/view/onboarding/downloading/backdrop.rs @@ -7,10 +7,18 @@ use std::time::Duration; use oneclient_core::clusters::Cluster; use crate::components::DynamicArt; +use crate::hooks::use_settings_snapshot; +use crate::layout::gradient_overlay_radial; use crate::theme::colors; const BACKDROP_INTERVAL_SECS: u64 = 12; +const PARALLAX_SCALE: f32 = 1.10; +const PARALLAX_STRENGTH: f32 = 0.008; +const PARALLAX_STEP: f32 = 0.12; +const PARALLAX_SETTLE: f32 = 0.002; +const PARALLAX_FRAME: Duration = Duration::from_millis(33); + #[derive(PartialEq)] pub struct LoadingBackdrop { pub clusters: Vec, @@ -23,7 +31,7 @@ impl Component for LoadingBackdrop { let mut index = use_state(|| 0usize); let stop = use_hook(|| Arc::new(AtomicBool::new(false))); - + use_drop({ let stop = stop.clone(); move || stop.store(true, Ordering::Relaxed) @@ -53,6 +61,52 @@ impl Component for LoadingBackdrop { let current = *index.read() % count; + let parallax_enabled = use_settings_snapshot().settings.dynamic_background_enabled; + + let mut size = use_state(|| (0f32, 0f32)); + let mut target = use_state(|| (0f32, 0f32)); + let mut pan = use_state(|| (0f32, 0f32)); + + let mut anim_task = use_state(|| None::); + use_side_effect_with_deps(¶llax_enabled, move |enabled| { + if *enabled { + let handle = spawn(async move { + loop { + let (tx, ty) = *target.peek(); + let (cx, cy) = *pan.peek(); + + if (tx - cx).abs() > PARALLAX_SETTLE || (ty - cy).abs() > PARALLAX_SETTLE { + pan.set(( + cx + (tx - cx) * PARALLAX_STEP, + cy + (ty - cy) * PARALLAX_STEP, + )); + } + + tokio::time::sleep(PARALLAX_FRAME).await; + } + }); + anim_task.set(Some(handle.owned())); + } else { + anim_task.set(None); + target.set((0.0, 0.0)); + pan.set((0.0, 0.0)); + } + }); + + let (cx, cy) = if parallax_enabled { + *pan.read() + } else { + (0.0, 0.0) + }; + let (sw, sh) = *size.read(); + let pan_x = cx * sw * PARALLAX_STRENGTH; + let pan_y = cy * sh * PARALLAX_STRENGTH; + + let art = match clusters.get(current) { + Some(cluster) => DynamicArt::for_cluster(cluster).max_edge(1280), + None => DynamicArt::fallback().max_edge(1280), + }; + let fade = use_animation_with_dependencies(¤t, |conf, _| { conf.on_creation(OnCreation::Run); conf.on_change(OnChange::Rerun); @@ -62,26 +116,6 @@ impl Component for LoadingBackdrop { .function(Function::Cubic) }); - let art: Element = match clusters.get(current) { - Some(cluster) => rect() - .width(Size::fill()) - .height(Size::fill()) - .opacity(fade.get().value()) - .child( - rect() - .width(Size::fill()) - .height(Size::fill()) - .child(DynamicArt::for_cluster(cluster).max_edge(1280)), - ) - .into_element(), - - None => rect() - .width(Size::fill()) - .height(Size::fill()) - .background(colors::page()) - .into_element(), - }; - rect() .position(Position::new_absolute().top(0.).left(0.)) .width(Size::fill()) @@ -89,41 +123,44 @@ impl Component for LoadingBackdrop { .overflow(Overflow::Clip) .interactive(false) .background(colors::page()) + .on_sized(move |e: Event| { + let (nw, nh) = (e.area.width(), e.area.height()); + let (pw, ph) = *size.peek(); + if (pw - nw).abs() > 0.5 || (ph - nh).abs() > 0.5 { + size.set((nw, nh)); + } + }) + .maybe(parallax_enabled, |el| { + el.on_capture_global_pointer_move(move |e: Event| { + let (sw, sh) = *size.peek(); + if sw > 0.0 && sh > 0.0 { + let loc = e.global_location(); + target.set(( + (loc.x as f32 / sw) * 2.0 - 1.0, + (loc.y as f32 / sh) * 2.0 - 1.0, + )); + } + }) + }) .child( rect() + .position(Position::new_absolute().top(0.).left(0.)) .width(Size::fill()) .height(Size::fill()) - .position(Position::new_absolute().top(0.).left(0.)) + .layer(Layer::Relative(0)) + .scale(PARALLAX_SCALE) + .offset_x(pan_x) + .offset_y(pan_y) + .opacity(fade.get().value()) .child(art), ) .child( rect() + .position(Position::new_absolute().top(0.).left(0.)) .width(Size::fill()) .height(Size::fill()) - .position(Position::new_absolute().top(0.).left(0.)) - .layer(Layer::Relative(8)) - .background( - LinearGradient::new() - .angle(270.) - .stop((Color::BLACK.with_a(235), 0.)) - .stop((Color::BLACK.with_a(235), 32.)) - .stop((Color::BLACK.with_a(120), 48.)) - .stop((Color::BLACK.with_a(10), 100.)), - ), - ) - .child( - rect() - .width(Size::fill()) - .height(Size::fill()) - .position(Position::new_absolute().top(0.).left(0.)) - .layer(Layer::Relative(8)) - .background( - LinearGradient::new() - .angle(0.) - .stop((Color::BLACK.with_a(0), 55.)) - .stop((Color::BLACK.with_a(160), 100.)), - ), + .layer(Layer::Relative(1)) + .child(gradient_overlay_radial()), ) } } - diff --git a/packages/oneclient_app/src/view/onboarding/downloading/mod.rs b/packages/oneclient_app/src/view/onboarding/downloading/mod.rs index 93f8adf6..732d316e 100644 --- a/packages/oneclient_app/src/view/onboarding/downloading/mod.rs +++ b/packages/oneclient_app/src/view/onboarding/downloading/mod.rs @@ -21,9 +21,11 @@ use crate::theme::colors; mod backdrop; mod progress; mod summary; +mod tips; pub use backdrop::LoadingBackdrop; use progress::{ProgressView, failure_panel, progress_panel}; use summary::summary_view; +use tips::{tip_panel, use_onboarding_tip}; const FADE_DURATION_MS: u64 = 700; const MAX_TASK_ROWS: usize = 6; @@ -154,6 +156,14 @@ struct DownloadStage { label: String, } +enum InstallUiEvent { + Progress(usize, usize), + Stage(DownloadStage), + Activity(String), + TotalEstimate(u64), + Finished(Vec), +} + #[derive(Clone, Copy)] struct InstallHandles { progress: State<(usize, usize)>, @@ -257,7 +267,7 @@ impl Component for OnboardingDownloading { let heading = if has_failures && is_complete { "Download issues found" } else if predownload { - "Downloading your content" + "Downloading..." } else { "Finishing up..." }; @@ -277,6 +287,8 @@ impl Component for OnboardingDownloading { 1.0 }; + let tip = use_onboarding_tip(); + if !*confirmed.read() { let items = onboarding_bundles_items(&bundles_query).unwrap_or_default(); let selected = selected_state.read().clone(); @@ -365,7 +377,8 @@ impl Component for OnboardingDownloading { predownload, running: is_running, })) - .child(rect().width(Size::flex(1.0))), + .child(rect().width(Size::flex(1.0))) + .child(tip_panel(tip)), ) .into_element() } @@ -455,6 +468,11 @@ fn run_install_batch(plans: Vec, predownload: bool, handles: Instal let total = plans.len(); progress.set((0, total)); + let (notif_tx, mut notif_rx) = mpsc::unbounded_channel::(); + let notifier = NotificationService::new(notif_tx); + + let (ui_tx, mut ui_rx) = mpsc::unbounded_channel::(); + spawn(async move { let mut prev = 0u64; let mut speed = 0f64; @@ -482,23 +500,52 @@ fn run_install_batch(plans: Vec, predownload: bool, handles: Instal }); spawn(async move { - let mut failed = Vec::new(); - - let (tx, mut rx) = mpsc::unbounded_channel::(); - let notifier = NotificationService::new(tx); - - spawn(async move { - while let Some(notification) = rx.recv().await { + let mut local = GroupedAgg::default(); + loop { + let Some(first) = notif_rx.recv().await else { + break; + }; + if let Notification::GroupedProgress(event) = first { + apply_grouped(&mut local, event); + } + // Drain everything already queued in one pass. + while let Ok(notification) = notif_rx.try_recv() { if let Notification::GroupedProgress(event) = notification { - let mut next = agg.peek().clone(); - apply_grouped(&mut next, event); - agg.set(next); + apply_grouped(&mut local, event); } } - }); + agg.set(local.clone()); + tokio::time::sleep(Duration::from_millis(50)).await; + } + }); + + spawn(async move { + while let Some(event) = ui_rx.recv().await { + match event { + InstallUiEvent::Progress(done, total) => progress.set((done, total)), + InstallUiEvent::Stage(next) => stage.set(next), + InstallUiEvent::Activity(text) => activity.set(text), + InstallUiEvent::TotalEstimate(bytes) => total_estimate.set(bytes), + InstallUiEvent::Finished(failed) => { + agg.set(GroupedAgg::default()); + activity.set("Wrapping up...".to_string()); + invalidate_cluster_queries().await; + failures.set(failed); + running.set(false); + complete.set(true); + break; + } + } + } + }); + + tokio::spawn(async move { + let mut failed = Vec::new(); if predownload { - activity.set("Calculating download size...".to_string()); + let _ = ui_tx.send(InstallUiEvent::Activity( + "Calculating download size...".to_string(), + )); if let Ok(state) = oneclient_core::LauncherState::get() { let mut sum = 0u64; for plan in &plans { @@ -515,19 +562,19 @@ fn run_install_batch(plans: Vec, predownload: bool, handles: Instal "download size estimate failed: {err}" ), } - total_estimate.set(sum); + let _ = ui_tx.send(InstallUiEvent::TotalEstimate(sum)); } } } for (index, plan) in plans.into_iter().enumerate() { - stage.set(DownloadStage { + let _ = ui_tx.send(InstallUiEvent::Stage(DownloadStage { index, total, label: plan.mc_version.clone(), - }); + })); - if let Err(reason) = install_one(&plan, predownload, ¬ifier, activity).await { + if let Err(reason) = install_one(&plan, predownload, ¬ifier, &ui_tx).await { tracing::error!( cluster_id = plan.cluster_id, mc_version = %plan.mc_version, @@ -535,17 +582,11 @@ fn run_install_batch(plans: Vec, predownload: bool, handles: Instal ); failed.push(InstallFailure { plan, reason }); } - progress.set((index + 1, total)); + let _ = ui_tx.send(InstallUiEvent::Progress(index + 1, total)); } drop(notifier); - agg.set(GroupedAgg::default()); - activity.set("Wrapping up...".to_string()); - - invalidate_cluster_queries().await; - failures.set(failed); - running.set(false); - complete.set(true); + let _ = ui_tx.send(InstallUiEvent::Finished(failed)); }); } @@ -553,12 +594,14 @@ async fn install_one( plan: &ClusterPlan, predownload: bool, notifier: &NotificationService, - mut activity: State, + ui_tx: &mpsc::UnboundedSender, ) -> Result<(), String> { let state = oneclient_core::LauncherState::get().map_err(|err| err.to_string())?; if !plan.overrides.is_empty() { - activity.set("Saving your package choices...".to_string()); + let _ = ui_tx.send(InstallUiEvent::Activity( + "Saving your package choices...".to_string(), + )); } for (bundle_name, package_id) in &plan.overrides { oneclient_core::set_bundle_package_enabled( @@ -579,7 +622,10 @@ async fn install_one( let session = GroupedProgressSession::start(notifier, format!("Downloading {}", plan.mc_version)); - activity.set(format!("Downloading Minecraft {}...", plan.mc_version)); + let _ = ui_tx.send(InstallUiEvent::Activity(format!( + "Downloading Minecraft {}...", + plan.mc_version + ))); let prepared = oneclient_core::ClusterManager::prepare( &state, @@ -592,10 +638,10 @@ async fn install_one( .await; let bundles_result = if prepared.is_ok() { - activity.set(format!( + let _ = ui_tx.send(InstallUiEvent::Activity(format!( "Installing mods & content for {}...", plan.mc_version - )); + ))); oneclient_core::install_cluster_bundles( plan.cluster_id, state.bundles.as_ref(), diff --git a/packages/oneclient_app/src/view/onboarding/downloading/tips.rs b/packages/oneclient_app/src/view/onboarding/downloading/tips.rs new file mode 100644 index 00000000..8025652d --- /dev/null +++ b/packages/oneclient_app/src/view/onboarding/downloading/tips.rs @@ -0,0 +1,113 @@ +use super::*; + +use crate::theme::colors; + +const ONBOARDING_TIPS_BACKUP: &[&str] = &[ + "418 I'm a teapot", + "Floors 8 through 10 of the Catacombs were originally planned for release, but portions of it were later merged into Floor 7 and its Master Mode counterpart.", + "At one point, Feather Client was willing to pay $300 USD to add Skytils, a popular SkyBlock mod.", + "Grass is a plant with narrow leaves growing from the base. [...]\nGrasses are monocotyledon, herbaceous plants.", + "In Canada, Santa's postal code is H0H 0H0.", + "Guvf vf abg na vzcbegnag fragrapr.", + "Vs lbh qrpvcure guvf, lbh fubhyq yvfgra gb Mrgihr'f zhfvp.", + "Try to do something creative! It could be music, art, coding...\nYou won't know how much you like it until you try it, so give it a shot :)", + "At one point in Minecraft, there was a rare chance of Snow Golems naturally spawning.", + "W[h]yvest", + "RUMOUR. Open your ears; 9r\"5j5&?OWTY Z0d", + "Make sure to drink water! You never know if you might be dehydrated.", + "People asking you to \"test their mod\" are likely trying to hack you.\nThe only ones you can trust are ones widely trusted by the community.", + "Have a great day!", + "You can sleep through a thunderstorm, even during the day", +]; + +const FUNFACTS_URL: &str = + "https://raw.githubusercontent.com/Polyfrost/DataStorage/main/oneclient/funfacts.txt"; + +fn normalize_line_breaks(value: &str) -> String { + value.replace("/n", "\n").replace("\\n", "\n") +} + +fn parse_funfacts(text: &str) -> Vec { + text.lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(normalize_line_breaks) + .collect() +} + +/// Loads onboarding tips (remote with backup fallback) and returns a single +/// stable random tip for the lifetime of the component. +pub(super) fn use_onboarding_tip() -> String { + let mut tips = use_state(|| { + ONBOARDING_TIPS_BACKUP + .iter() + .map(|tip| normalize_line_breaks(tip)) + .collect::>() + }); + + use_hook(move || { + spawn(async move { + let Ok(state) = oneclient_core::LauncherState::get() else { + return; + }; + match state.services.requester.http().get(FUNFACTS_URL).send().await { + Ok(res) if res.status().is_success() => match res.text().await { + Ok(text) => { + let parsed = parse_funfacts(&text); + if !parsed.is_empty() { + tips.set(parsed); + } + } + Err(err) => { + tracing::warn!("onboarding tips: reading body failed: {err}"); + } + }, + Ok(res) => { + tracing::warn!("onboarding tips: unexpected status {}", res.status()); + } + Err(err) => { + tracing::warn!("onboarding tips: request failed, using backup facts: {err}"); + } + } + }); + }); + + let seed = use_hook(|| { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.subsec_nanos() as usize) + .unwrap_or(0) + }); + + let list = tips.read(); + if list.is_empty() { + String::new() + } else { + list[seed % list.len()].clone() + } +} + +pub(super) fn tip_panel(tip: String) -> impl IntoElement { + rect() + .vertical() + .width(Size::px(320.)) + .spacing(4.) + .cross_align(Alignment::End) + .child( + label() + .text("TIP") + .width(Size::fill()) + .font_size(11.) + .font_weight(FontWeight::SEMI_BOLD) + .text_align(TextAlign::Right) + .color(colors::fg_secondary()), + ) + .child( + label() + .text(tip) + .width(Size::fill()) + .font_size(13.) + .text_align(TextAlign::Right) + .color(colors::fg_secondary()), + ) +} diff --git a/packages/oneclient_core/src/constants.rs b/packages/oneclient_core/src/constants.rs index a7e7b824..ee2411db 100644 --- a/packages/oneclient_core/src/constants.rs +++ b/packages/oneclient_core/src/constants.rs @@ -1,8 +1,7 @@ pub const DUMMY_REPLACE_NEWLINE: &str = "\n"; -// pub const MICROSOFT_CLIENT_ID: &str = "9eac3a4e-8cdd-43ef-863e-49cd601b1f03"; -pub const MICROSOFT_CLIENT_ID: &str = "c36a9fb6-4f2a-41ff-90bd-ae7cc92031eb"; +pub const MICROSOFT_CLIENT_ID: &str = "9419b7ee-1448-4d1b-b52a-550d8f36ab56"; pub const MINECRAFT_SCOPES: &str = "XboxLive.SignIn XboxLive.offline_access"; pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe"; pub const DISCORD_CLIENT_ID: &str = "1426999264633946334";