diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index fe9c973f..784618a2 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -313,13 +313,8 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'debug':'{"getBuildTimestamp":[],"getPackageVersion":[],"getPlatform":[],"isInDev":[],"getArch":[],"getFamily":[],"openDevTools":[],"getGitCommitHash":[],"getType":[],"getLocale":[],"getOsVersion":[]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'oneclient':'{"getVersions":[],"getBundlesFor":["cluster_id"],"extractBundleOverrides":["bundle_path","cluster_id"],"installUpdate":[],"updateBundlePackages":["cluster_id"],"isBundleSyncing":[],"refreshArt":["path"],"cacheArt":["path"],"checkForUpdate":[],"getClustersGroupedByMajor":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"]}', 'core':'{"createSettingsProfile":["name"],"getPackageBody":["provider","body"],"getUsers":[],"launchCluster":["id","uuid","search_for_java"],"getUsersFromAuthor":["provider","author"],"syncCluster":["cluster_id"],"getLinkedPackages":["cluster_id"],"changeSkin":["access_token","skin_url","skin_variant"],"getMultiplePackages":["provider","slugs"],"removeCluster":["id"],"togglePackage":["cluster_id","package_hash"],"installModpack":["modpack","cluster_id"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"fetchMinecraftProfile":["uuid"],"getScreenshots":["id"],"updateClusterProfile":["name","profile"],"getRunningProcessesByClusterId":["cluster_id"],"getProfileOrDefault":["name"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getProcessLogTail":["id","max_lines"],"getClusterById":["id"],"isClusterRunning":["cluster_id"],"open":["input"],"killProcess":["pid"],"fetchLoggedInProfile":["access_token"],"openMsaLogin":[],"writeSettings":["setting"],"changeCape":["access_token","cape_uuid"],"createCluster":["options"],"setClusterStage":["id","stage"],"searchPackages":["provider","query"],"setDiscordRPCMessage":["message"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"removePackage":["cluster_id","package_hash"],"getClusters":[],"getWorlds":["id"],"getDefaultUser":["fallback"],"getPackage":["provider","slug"],"setDefaultUser":["uuid"],"getRunningProcesses":[],"getGlobalProfile":[],"updateClusterById":["id","request"],"refreshAccounts":[],"getGameVersions":[],"getUser":["uuid"],"convertUsernameUUID":["username_uuid"],"refreshAccount":["uuid"],"removeCape":["access_token"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"removeUser":["uuid"],"getLogs":["id"],"getLogByName":["id","name"],"getLoadersForVersion":["mc_version"],"readSettings":[]}' } -export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, -'debug': { openDevTools: () => Promise, +const ARGS_MAP = { 'oneclient':'{"checkForUpdate":[],"installUpdate":[],"downloadPackageFromBundle":["package","cluster_id","bundle_name","skip_compatibility"],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"],"getVersions":[],"isBundleSyncing":[],"cacheArt":["path"],"updateBundlePackages":["cluster_id"],"refreshArt":["path"],"extractBundleOverrides":["bundle_path","cluster_id"]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'debug':'{"getBuildTimestamp":[],"getLocale":[],"openDevTools":[],"isInDev":[],"getFamily":[],"getArch":[],"getOsVersion":[],"getPlatform":[],"getGitCommitHash":[],"getPackageVersion":[],"getType":[]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'core':'{"getDefaultUser":["fallback"],"getUser":["uuid"],"removeCluster":["id"],"launchCluster":["id","uuid","search_for_java"],"searchPackages":["provider","query"],"getPackageBody":["provider","body"],"getRunningProcessesByClusterId":["cluster_id"],"setDefaultUser":["uuid"],"getProfileOrDefault":["name"],"refreshAccount":["uuid"],"readSettings":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUsersFromAuthor":["provider","author"],"removePackage":["cluster_id","package_hash"],"installModpack":["modpack","cluster_id"],"getLoadersForVersion":["mc_version"],"changeSkin":["access_token","skin_url","skin_variant"],"open":["input"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"getClusterById":["id"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getUsers":[],"fetchMinecraftProfile":["uuid"],"setClusterStage":["id","stage"],"changeCape":["access_token","cape_uuid"],"getMultiplePackages":["provider","slugs"],"isClusterRunning":["cluster_id"],"openMsaLogin":[],"getClusters":[],"getLogByName":["id","name"],"killProcess":["pid"],"getScreenshots":["id"],"getLinkedPackages":["cluster_id"],"updateClusterProfile":["name","profile"],"getPackage":["provider","slug"],"fetchLoggedInProfile":["access_token"],"removeUser":["uuid"],"getProcessLogTail":["id","max_lines"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"createCluster":["options"],"createSettingsProfile":["name"],"getRunningProcesses":[],"updateClusterById":["id","request"],"setDiscordRPCMessage":["message"],"togglePackage":["cluster_id","package_hash"],"refreshAccounts":[],"getWorlds":["id"],"writeSettings":["setting"],"getLogs":["id"],"removeCape":["access_token"],"convertUsernameUUID":["username_uuid"],"getGameVersions":[],"syncCluster":["cluster_id"],"getGlobalProfile":[]}' } +export type Router = { 'debug': { openDevTools: () => Promise, isInDev: () => Promise, getArch: () => Promise, getFamily: () => Promise, @@ -330,6 +325,11 @@ getOsVersion: () => Promise, getGitCommitHash: () => Promise, getBuildTimestamp: () => Promise, getPackageVersion: () => Promise }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise }, 'oneclient': { getClustersGroupedByMajor: () => Promise>, getBundlesFor: (clusterId: number) => Promise, getVersions: () => Promise, diff --git a/apps/oneclient/frontend/src/routes/__root.tsx b/apps/oneclient/frontend/src/routes/__root.tsx index 769e0282..41868d87 100644 --- a/apps/oneclient/frontend/src/routes/__root.tsx +++ b/apps/oneclient/frontend/src/routes/__root.tsx @@ -35,7 +35,7 @@ const ResolvedPathNames: Record = { '/app': 'Viewing Homepage', '/app/account': 'Viewing Account', '/app/account/skins': 'Viewing Skin Manager', - '/app/cluster': 'Viewing Clusters', + '/app/cluster': 'Viewing Versions', '/app/cluster/browser': 'Viewing {clusterName}\'s mods', '/app/cluster/browser/package': 'Browsing {packageName}', '/app/cluster/logs': 'Viewing {clusterName}\'s logs', @@ -51,7 +51,7 @@ const ResolvedPathNames: Record = { '/app/settings/minecraft': 'Viewing Settings', '/app/settings/changelog': 'Viewing Settings', '/app/accounts': 'Viewing Accounts', - '/app/clusters': 'Viewing Clusters', + '/app/clusters': 'Viewing Versions', '/onboarding': 'Preparing OneClient', '/onboarding/account': 'Preparing OneClient', '/onboarding/finished': 'Preparing OneClient', diff --git a/packages/core/src/api/game/launch.rs b/packages/core/src/api/game/launch.rs index 5f6fdfbe..1535b620 100644 --- a/packages/core/src/api/game/launch.rs +++ b/packages/core/src/api/game/launch.rs @@ -196,13 +196,16 @@ pub async fn launch_minecraft( let censors = log::create_censors(&creds); - if let Some(discord) = &state.rpc { - discord - .set_message( - &format!("Playing {}", cluster.name), - Some(Timestamps::new().start(Utc::now().timestamp())), - ) - .await; + if state.ensure_rpc().await { + let rpc = state.rpc.read().await; + if let Some(discord) = rpc.as_ref() { + discord + .set_message( + &format!("Playing {}", cluster.name), + Some(Timestamps::new().start(Utc::now().timestamp())), + ) + .await; + } } state diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index 9fbfe55a..a9bd987f 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -945,12 +945,15 @@ impl TauriLauncherApi for TauriLauncherApiImpl { // region: discord RPC async fn set_discord_rpc_message(self, message: String) -> LauncherResult<()> { let state = State::get().await?; - if let Some(discord) = &state.rpc { - if !state.settings.read().await.discord_enabled { - discord.clear_activity().await; - return Ok(()); + if state.ensure_rpc().await { + let rpc = state.rpc.read().await; + if let Some(discord) = rpc.as_ref() { + if !state.settings.read().await.discord_enabled { + discord.clear_activity().await; + return Ok(()); + } + discord.set_message(&message, None).await; } - discord.set_message(&message, None).await; } Ok(()) diff --git a/packages/core/src/store/discord.rs b/packages/core/src/store/discord.rs index e27ace7e..77e63631 100644 --- a/packages/core/src/store/discord.rs +++ b/packages/core/src/store/discord.rs @@ -27,6 +27,12 @@ impl DiscordRPC { let connected = client.connect().is_ok(); + if connected { + tracing::info!("Discord RPC connected successfully"); + } else { + tracing::warn!("Discord RPC failed to connect (Discord may not be running)"); + } + Ok(Self { started_at: chrono::Utc::now().timestamp(), client: Arc::new(RwLock::new(client)), @@ -50,13 +56,17 @@ impl DiscordRPC { } pub async fn set_activity(&self, activity: Activity<'_>) -> bool { - if !self.is_connected() { + // If we know we're disconnected, try to reconnect first + if !self.is_connected() && !self.reconnect().await { return false; } let mut client = self.client.write().await; if client.set_activity(activity).is_err() { + // Connection dropped — mark as disconnected so next call will reconnect + self.set_connected(false); + tracing::warn!("Discord RPC set_activity failed; connection may have dropped"); return false; } @@ -71,6 +81,7 @@ impl DiscordRPC { let mut client = self.client.write().await; if client.clear_activity().is_err() { + self.set_connected(false); return false; } @@ -78,11 +89,19 @@ impl DiscordRPC { } pub async fn reconnect(&self) -> bool { + tracing::info!("Attempting Discord RPC reconnect..."); let mut client = self.client.write().await; let connected = client.reconnect().is_ok(); self.set_connected(connected); + + if connected { + tracing::info!("Discord RPC reconnected successfully"); + } else { + tracing::warn!("Discord RPC reconnect failed"); + } + connected } diff --git a/packages/core/src/store/state.rs b/packages/core/src/store/state.rs index e6c422e3..62dae686 100644 --- a/packages/core/src/store/state.rs +++ b/packages/core/src/store/state.rs @@ -23,7 +23,9 @@ pub struct State { pub credentials: RwLock, pub metadata: RwLock, pub processes: ProcessStore, - pub rpc: Option, + /// Discord RPC client. Wrapped in `RwLock` so it can be lazily initialized + /// after startup if Discord wasn't running when the launcher launched. + pub rpc: RwLock>, } impl State { @@ -64,7 +66,31 @@ impl State { credentials: RwLock::new(credentials), metadata: RwLock::new(metadata), processes, - rpc, + rpc: RwLock::new(rpc), })) } + + /// Get or lazily initialize the Discord RPC. + /// + /// If RPC was `None` at startup (Discord wasn't running), this will attempt + /// to initialize it now. Returns `true` if RPC is available after the call. + pub async fn ensure_rpc(&self) -> bool { + // Fast path: already have a client + if self.rpc.read().await.is_some() { + return true; + } + + // Try to initialize for the first time + match DiscordRPC::initialize() { + Ok(rpc) => { + *self.rpc.write().await = Some(rpc); + tracing::info!("Discord RPC lazily initialized"); + true + } + Err(err) => { + tracing::debug!("Discord RPC lazy init failed: {err}"); + false + } + } + } }