diff --git a/gow3-launcher b/gow3-launcher index dfe6c3a..8514081 100755 Binary files a/gow3-launcher and b/gow3-launcher differ diff --git a/gow3-launcher-win.exe b/gow3-launcher-win.exe index ba572b8..2e9d896 100755 Binary files a/gow3-launcher-win.exe and b/gow3-launcher-win.exe differ diff --git a/launcher/Cargo.lock b/launcher/Cargo.lock index 6261f02..e10ae19 100644 --- a/launcher/Cargo.lock +++ b/launcher/Cargo.lock @@ -1671,7 +1671,7 @@ dependencies = [ [[package]] name = "launcher" -version = "2.0.3" +version = "2.1.0" dependencies = [ "eframe", "egui_extras", diff --git a/launcher/Cargo.toml b/launcher/Cargo.toml index 046e7f9..b77ab26 100644 --- a/launcher/Cargo.toml +++ b/launcher/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "launcher" -version = "2.0.3" +version = "2.1.0" edition = "2024" [dependencies] diff --git a/launcher/src/app.rs b/launcher/src/app.rs index a0014cc..5d182ce 100644 --- a/launcher/src/app.rs +++ b/launcher/src/app.rs @@ -1206,6 +1206,9 @@ pub struct LauncherApp { // Installed Game Mods installed_mods: Vec, + + // Discord Rich Presence + pub discord_rpc: crate::discord_rpc::DiscordRpc, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1275,6 +1278,9 @@ impl LauncherApp { let has_engine = crate::runner::find_game_engine(&base_dir).is_some(); let installed_mods = crate::mods_manager::scan_installed_mods(&base_dir, &config.disabled_mods); + let discord_rpc = crate::discord_rpc::DiscordRpc::new(config.discord_rpc_enabled, config.discord_client_id.clone()); + discord_rpc.set_in_menu(&config.language); + let mut app = Self { base_dir: base_dir.clone(), config, @@ -1335,6 +1341,7 @@ impl LauncherApp { shader_loading: None, launcher_version, installed_mods, + discord_rpc, }; crate::log_info!("Launcher UI initialized successfully. Version: {}", app.launcher_version); @@ -1514,6 +1521,7 @@ impl LauncherApp { } fn apply_all_optimizations(&mut self) { + self.discord_rpc.update_config(self.config.discord_rpc_enabled, self.config.discord_client_id.clone()); let res = apply_settings(&self.base_dir, &self.config); match res { @@ -1679,7 +1687,11 @@ impl LauncherApp { if self.config.launch_while_compiling_shaders { // "Shader'lar derlenirken oyun açılsın": Fast launch immediately while compiling in the background self.shader_loading = None; + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0); + let is_modded = self.installed_mods.iter().any(|m| m.is_enabled); + self.discord_rpc.set_in_game(now, self.config.target_fps, is_modded); } else { + self.discord_rpc.set_compiling_shaders(Some(0.0)); // Pre-warm all shaders to 100% with the full Spartan Rage bar before game window is displayed ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true)); ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel(egui::WindowLevel::AlwaysOnTop)); @@ -3073,6 +3085,39 @@ impl LauncherApp { }); ui.label(egui::RichText::new(self.t("hide_mouse_cursor_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5)); + ui.add_space(8.0); + // 9. Discord Rich Presence + ui.horizontal(|ui| { + if toggle_switch(ui, &mut self.config.discord_rpc_enabled).changed() { + self.discord_rpc.update_config(self.config.discord_rpc_enabled, self.config.discord_client_id.clone()); + } + let lbl = self.t("discord_rpc_toggle"); + if ui.add(egui::Label::new(egui::RichText::new(lbl).strong().color(egui::Color32::WHITE).size(14.5)).sense(egui::Sense::click())).clicked() { + self.config.discord_rpc_enabled = !self.config.discord_rpc_enabled; + self.discord_rpc.update_config(self.config.discord_rpc_enabled, self.config.discord_client_id.clone()); + } + }); + ui.label(egui::RichText::new(self.t("discord_rpc_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5)); + + if self.config.discord_rpc_enabled { + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new(self.t("discord_client_id_label")).size(12.0).color(egui::Color32::from_rgb(180, 185, 200))); + let id_edit = ui.add( + egui::TextEdit::singleline(&mut self.config.discord_client_id) + .desired_width(190.0) + .font(egui::TextStyle::Monospace), + ); + if id_edit.changed() { + self.discord_rpc.update_config(self.config.discord_rpc_enabled, self.config.discord_client_id.clone()); + } + if ui.button(self.t("discord_reset_btn")).clicked() { + self.config.discord_client_id = "1124351711486689290".to_string(); + self.discord_rpc.update_config(self.config.discord_rpc_enabled, self.config.discord_client_id.clone()); + } + }); + } + ui.add_space(12.0); ui.separator(); ui.horizontal(|ui| { @@ -4147,6 +4192,7 @@ impl eframe::App for LauncherApp { if let Ok(res) = rx.try_recv() { self.is_game_running = false; self.game_exit_rx = None; + self.discord_rpc.set_in_menu(&self.config.language); if self.shader_loading.is_some() { ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel(egui::WindowLevel::Normal)); ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false)); @@ -4382,6 +4428,10 @@ impl eframe::App for LauncherApp { } // Drop AlwaysOnTop to Normal so the SDL2 game window can focus in front ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel(egui::WindowLevel::Normal)); + + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0); + let is_modded = self.installed_mods.iter().any(|m| m.is_enabled); + self.discord_rpc.set_in_game(now, self.config.target_fps, is_modded); } // Oyun start aldıktan 20 saniye sonra açılış/shader ekranı otomatik olarak kapanır (arkada ekstra olarak durmaz) @@ -5373,6 +5423,12 @@ impl eframe::App for LauncherApp { } } +impl Drop for LauncherApp { + fn drop(&mut self) { + self.discord_rpc.shutdown(); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/launcher/src/config.rs b/launcher/src/config.rs index 7fa569b..3073e20 100644 --- a/launcher/src/config.rs +++ b/launcher/src/config.rs @@ -294,6 +294,16 @@ pub struct LauncherConfig { // Keyboard bindings #[serde(default)] pub keyboard_bindings: KeyboardBindings, + + // Discord Rich Presence + #[serde(default = "default_true")] + pub discord_rpc_enabled: bool, + #[serde(default = "default_discord_client_id")] + pub discord_client_id: String, +} + +fn default_discord_client_id() -> String { + "1124351711486689290".to_string() } fn default_language() -> String { @@ -322,7 +332,7 @@ fn default_last_commit() -> String { "c5638d9956002b8a9f09a240028f6793ddf7b2f8".to_string() } fn default_launcher_version() -> String { - "v2.0.3".to_string() + "v2.1".to_string() } fn default_render_resolution() -> String { "native".to_string() @@ -361,6 +371,8 @@ impl Default for LauncherConfig { deadzone_left: default_deadzone(), deadzone_right: default_deadzone(), keyboard_bindings: KeyboardBindings::default(), + discord_rpc_enabled: true, + discord_client_id: default_discord_client_id(), } } } @@ -786,10 +798,12 @@ mod tests { #[test] fn test_launcher_version_is_v20() { - assert_eq!(default_launcher_version(), "v2.0.3"); + assert_eq!(default_launcher_version(), "v2.1"); let def = LauncherConfig::default(); assert!(def.async_readbacks); assert!(def.patch_readbacks); + assert!(def.discord_rpc_enabled); + assert_eq!(def.discord_client_id, "1124351711486689290"); } #[test] diff --git a/launcher/src/discord_rpc.rs b/launcher/src/discord_rpc.rs new file mode 100644 index 0000000..9b055b1 --- /dev/null +++ b/launcher/src/discord_rpc.rs @@ -0,0 +1,411 @@ +//! Discord Rich Presence (RPC) Implementation for God of War III Launcher +//! +//! Provides zero-overhead, native local IPC communication with the desktop Discord client +//! on both Linux (Unix Domain Sockets) and Windows (Named Pipes). +//! +//! Never blocks the main GUI thread; runs silently on a background worker thread. + +use std::io::Write; +#[cfg(unix)] +use std::path::PathBuf; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::thread; +use std::time::{Duration, Instant}; + +#[cfg(unix)] +use std::io::Read; + +#[cfg(unix)] +type IpcStream = std::os::unix::net::UnixStream; + +#[cfg(windows)] +type IpcStream = std::fs::File; + +#[cfg(not(any(unix, windows)))] +type IpcStream = std::io::Cursor>; + +/// Activity metadata sent to Discord +#[derive(Debug, Clone)] +pub struct DiscordActivity { + pub details: String, + pub state: String, + pub start_timestamp: Option, + pub large_image: Option, + pub large_text: Option, + pub small_image: Option, + pub small_text: Option, +} + +pub enum DiscordRpcMsg { + SetActivity(DiscordActivity), + ClearActivity, + UpdateConfig { + enabled: bool, + client_id: String, + }, + Shutdown, +} + +#[derive(Clone)] +pub struct DiscordRpc { + tx: Sender, +} + +impl DiscordRpc { + pub fn new(enabled: bool, client_id: String) -> Self { + let (tx, rx) = channel(); + + thread::Builder::new() + .name("discord-rpc-worker".to_string()) + .spawn(move || { + run_rpc_worker(rx, enabled, client_id); + }) + .expect("Failed to spawn discord-rpc-worker thread"); + + Self { tx } + } + + pub fn set_in_menu(&self, lang: &str) { + let (state, details) = match lang { + "tr" => ("Ana Menüde", "God of War III Remastered"), + "de" => ("Im Hauptmenü", "God of War III Remastered"), + "es" => ("En el Menú Principal", "God of War III Remastered"), + _ => ("In Main Menu", "God of War III Remastered"), + }; + + let act = DiscordActivity { + details: details.to_string(), + state: state.to_string(), + start_timestamp: None, + large_image: Some("1124351711486689290".to_string()), + large_text: Some("God of War III Remastered".to_string()), + small_image: None, + small_text: None, + }; + let _ = self.tx.send(DiscordRpcMsg::SetActivity(act)); + } + + pub fn set_compiling_shaders(&self, percent: Option) { + let state = if let Some(pct) = percent { + format!("Shader'lar Derleniyor (%{:.0})", pct) + } else { + "Shader'lar Derleniyor...".to_string() + }; + + let act = DiscordActivity { + details: "God of War III Remastered".to_string(), + state, + start_timestamp: None, + large_image: Some("1124351711486689290".to_string()), + large_text: Some("Vulkan Pipeline Shader Cache".to_string()), + small_image: None, + small_text: None, + }; + let _ = self.tx.send(DiscordRpcMsg::SetActivity(act)); + } + + pub fn set_in_game(&self, start_unix_secs: i64, target_fps: u32, is_modded: bool) { + let fps_tag = if target_fps > 0 { + format!("{} FPS", target_fps) + } else { + "Uncapped FPS".to_string() + }; + + let mod_tag = if is_modded { " | Modlu" } else { "" }; + let state = format!("Olimpos'a Meydan Okuyor ({}{})", fps_tag, mod_tag); + + let act = DiscordActivity { + details: "God of War III Remastered".to_string(), + state, + start_timestamp: Some(start_unix_secs), + large_image: Some("1124351711486689290".to_string()), + large_text: Some("God of War III Remastered".to_string()), + small_image: Some("playstation".to_string()), + small_text: Some("PS4 Remastered on PC".to_string()), + }; + let _ = self.tx.send(DiscordRpcMsg::SetActivity(act)); + } + + pub fn clear(&self) { + let _ = self.tx.send(DiscordRpcMsg::ClearActivity); + } + + pub fn update_config(&self, enabled: bool, client_id: String) { + let _ = self.tx.send(DiscordRpcMsg::UpdateConfig { enabled, client_id }); + } + + pub fn shutdown(&self) { + let _ = self.tx.send(DiscordRpcMsg::Shutdown); + } +} + +fn run_rpc_worker(rx: Receiver, mut enabled: bool, mut client_id: String) { + let mut stream: Option = None; + let mut current_activity: Option = None; + let mut last_connect_attempt = Instant::now() - Duration::from_secs(10); + let mut nonce: u64 = 1; + + loop { + // 1. Process any pending messages with a 500ms timeout + match rx.recv_timeout(Duration::from_millis(500)) { + Ok(msg) => match msg { + DiscordRpcMsg::SetActivity(act) => { + current_activity = Some(act.clone()); + if enabled { + if stream.is_none() { + stream = try_connect_and_handshake(&client_id); + } + if let Some(ref mut s) = stream { + nonce += 1; + if send_activity_payload(s, &act, nonce).is_err() { + stream = None; + } + } + } + } + DiscordRpcMsg::ClearActivity => { + current_activity = None; + if let Some(ref mut s) = stream { + nonce += 1; + let _ = send_clear_payload(s, nonce); + } + } + DiscordRpcMsg::UpdateConfig { enabled: new_enabled, client_id: new_id } => { + let id_changed = client_id != new_id; + enabled = new_enabled; + client_id = new_id; + + if id_changed || !enabled { + if let Some(ref mut s) = stream { + nonce += 1; + let _ = send_clear_payload(s, nonce); + } + stream = None; + } + + if enabled && stream.is_none() { + stream = try_connect_and_handshake(&client_id); + if let (Some(s), Some(act)) = (&mut stream, ¤t_activity) { + nonce += 1; + let _ = send_activity_payload(s, act, nonce); + } + } + } + DiscordRpcMsg::Shutdown => { + if let Some(ref mut s) = stream { + nonce += 1; + let _ = send_clear_payload(s, nonce); + } + break; + } + }, + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + // Periodic maintenance check: if enabled and we have an activity but no active connection, + // try to reconnect every 10 seconds (in case user opened Discord after launcher started) + if enabled && current_activity.is_some() && stream.is_none() { + if last_connect_attempt.elapsed() >= Duration::from_secs(10) { + last_connect_attempt = Instant::now(); + stream = try_connect_and_handshake(&client_id); + if let (Some(s), Some(act)) = (&mut stream, ¤t_activity) { + nonce += 1; + let _ = send_activity_payload(s, act, nonce); + } + } + } + } + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { + // Parent dropped channel, exit cleanly + break; + } + } + } +} + +fn try_connect_and_handshake(client_id: &str) -> Option { + let mut stream = try_connect_ipc()?; + + // Send Handshake packet (Opcode 0) + let handshake_payload = serde_json::json!({ + "v": 1, + "client_id": client_id + }).to_string(); + + if send_frame(&mut stream, 0, &handshake_payload).is_err() { + return None; + } + + #[cfg(unix)] + { + // Read Discord dispatch response to verify connection and clear buffer + let mut header = [0u8; 8]; + if stream.read_exact(&mut header).is_ok() { + let length = u32::from_le_bytes([header[4], header[5], header[6], header[7]]) as usize; + if length > 0 && length < 65536 { + let mut buf = vec![0u8; length]; + let _ = stream.read_exact(&mut buf); + } + } + } + + Some(stream) +} + +#[cfg(unix)] +fn try_connect_ipc() -> Option { + let mut search_dirs = Vec::new(); + + if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { + let p = PathBuf::from(&runtime_dir); + search_dirs.push(p.clone()); + search_dirs.push(p.join("app/com.discordapp.Discord")); + search_dirs.push(p.join("snap.discord")); + } + + search_dirs.push(PathBuf::from("/tmp")); + + for dir in &search_dirs { + for i in 0..10 { + let candidate = dir.join(format!("discord-ipc-{}", i)); + if candidate.exists() { + if let Ok(stream) = std::os::unix::net::UnixStream::connect(&candidate) { + let _ = stream.set_read_timeout(Some(Duration::from_millis(300))); + let _ = stream.set_write_timeout(Some(Duration::from_millis(500))); + return Some(stream); + } + } + } + } + None +} + +#[cfg(windows)] +fn try_connect_ipc() -> Option { + for i in 0..10 { + let pipe_name = format!(r"\\.\pipe\discord-ipc-{}", i); + if let Ok(file) = std::fs::OpenOptions::new().read(true).write(true).open(&pipe_name) { + return Some(file); + } + } + None +} + +#[cfg(not(any(unix, windows)))] +fn try_connect_ipc() -> Option { + None +} + +pub fn build_activity_payload(activity: &DiscordActivity, nonce: u64) -> String { + let mut activity_obj = serde_json::Map::new(); + activity_obj.insert("details".to_string(), serde_json::Value::String(activity.details.clone())); + activity_obj.insert("state".to_string(), serde_json::Value::String(activity.state.clone())); + + if let Some(start) = activity.start_timestamp { + let mut timestamps = serde_json::Map::new(); + timestamps.insert("start".to_string(), serde_json::Value::Number(start.into())); + activity_obj.insert("timestamps".to_string(), serde_json::Value::Object(timestamps)); + } + + let mut assets = serde_json::Map::new(); + if let Some(ref img) = activity.large_image { + assets.insert("large_image".to_string(), serde_json::Value::String(img.clone())); + } + if let Some(ref txt) = activity.large_text { + assets.insert("large_text".to_string(), serde_json::Value::String(txt.clone())); + } + if let Some(ref img) = activity.small_image { + assets.insert("small_image".to_string(), serde_json::Value::String(img.clone())); + } + if let Some(ref txt) = activity.small_text { + assets.insert("small_text".to_string(), serde_json::Value::String(txt.clone())); + } + + if !assets.is_empty() { + activity_obj.insert("assets".to_string(), serde_json::Value::Object(assets)); + } + + serde_json::json!({ + "cmd": "SET_ACTIVITY", + "args": { + "pid": std::process::id(), + "activity": activity_obj + }, + "nonce": format!("{}", nonce) + }) + .to_string() +} + +pub fn build_clear_payload(nonce: u64) -> String { + serde_json::json!({ + "cmd": "SET_ACTIVITY", + "args": { + "pid": std::process::id(), + "activity": null + }, + "nonce": format!("{}", nonce) + }) + .to_string() +} + +fn send_activity_payload(writer: &mut W, act: &DiscordActivity, nonce: u64) -> std::io::Result<()> { + let payload = build_activity_payload(act, nonce); + send_frame(writer, 1, &payload) +} + +fn send_clear_payload(writer: &mut W, nonce: u64) -> std::io::Result<()> { + let payload = build_clear_payload(nonce); + send_frame(writer, 1, &payload) +} + +fn send_frame(writer: &mut W, opcode: u32, payload: &str) -> std::io::Result<()> { + let payload_bytes = payload.as_bytes(); + let length = payload_bytes.len() as u32; + + writer.write_all(&opcode.to_le_bytes())?; + writer.write_all(&length.to_le_bytes())?; + writer.write_all(payload_bytes)?; + writer.flush()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_activity_payload_formatting() { + let act = DiscordActivity { + details: "God of War III Remastered".to_string(), + state: "Olimpos'a Meydan Okuyor (60 FPS)".to_string(), + start_timestamp: Some(1700000000), + large_image: Some("1124351711486689290".to_string()), + large_text: Some("God of War III".to_string()), + small_image: Some("playstation".to_string()), + small_text: Some("PS4".to_string()), + }; + + let json_str = build_activity_payload(&act, 42); + assert!(json_str.contains("SET_ACTIVITY")); + assert!(json_str.contains("Olimpos'a Meydan Okuyor")); + assert!(json_str.contains("1700000000")); + assert!(json_str.contains("\"nonce\":\"42\"")); + } + + #[test] + fn test_clear_payload_formatting() { + let json_str = build_clear_payload(99); + assert!(json_str.contains("SET_ACTIVITY")); + assert!(json_str.contains("\"activity\":null")); + assert!(json_str.contains("\"nonce\":\"99\"")); + } + + #[test] + fn test_discord_rpc_handle_channels() { + let rpc = DiscordRpc::new(false, "1124351711486689290".to_string()); + rpc.set_in_menu("tr"); + rpc.set_compiling_shaders(Some(50.0)); + rpc.set_in_game(1700000000, 60, true); + rpc.clear(); + rpc.update_config(true, "1124351711486689290".to_string()); + rpc.shutdown(); + } +} diff --git a/launcher/src/i18n.rs b/launcher/src/i18n.rs index 2131216..d524b4f 100644 --- a/launcher/src/i18n.rs +++ b/launcher/src/i18n.rs @@ -153,8 +153,12 @@ pub fn text(lang: Language, key: &str) -> &'static str { (Language::Turkish, "menu_latest_news") => "GÜNCELLEME NOTLARI", (Language::Turkish, "menu_options") => "AYARLAR", (Language::Turkish, "menu_back") => "GERİ DÖN", - (Language::Turkish, "release_notes_title") => "God of War III Launcher - Sürüm Notları (v2.0.3)", - (Language::Turkish, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.0.3 YENİLİKLERİ:\n\n• 👁️ Okunabilir & Belirgin Ayarlar Arayüzü:\n - Ayarlar ekranındaki beyaz ayar yazıları ve açıklamaların yazı boyutu büyütüldü; göz yormayan net görünüm sağlandı.\n - Kırmızı kategori başlıkları büyük ve belirgin puntolarla vurgulandı; hangi ayarın nerede olduğu anında anlaşılır hale getirildi.\n\n• 🎬 Pürüzsüz PS4 Açılış Ekranı (60-144 FPS) & 20s Otomatik Kapanma:\n - PS4 tarzı açılış görselindeki takılma/1 FPS hissi giderildi; ekrana doğru pürüzsüz ve sinematik yakınlaşma animasyonu entegre edildi.\n - Açılış ekranı oyun başladıktan 20 saniye sonra arkada gereksiz yer kaplamadan otomatik olarak kapanır.\n\n• 🛠️ Görsel İyileştirmeler:\n - Güncelleme butonundaki çift ikon hatası düzeltildi (tek '🔄' ikonu).", + (Language::Turkish, "release_notes_title") => "God of War III Launcher - Sürüm Notları (v2.1)", + (Language::Turkish, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.1 YENİLİKLERİ:\n\n• 🎮 Discord Rich Presence (Oyun Durumu Entegrasyonu):\n - Discord profilinizde 'God of War III Remastered Oynuyor' durumu, Kratos görseli ve canlı oynama süresi sayacı gösterilir.\n - Menüdeyken 'Ana Menüde', derleme sırasında 'Shader'lar Derleniyor' ve oyunda 'Olimpos Dağı'na Tırmanıyor' durumları otomatik güncellenir.\n - Ayarlar menüsünden tek tıkla açılıp kapatılabilir; istenirse özel Discord Client ID girilebilir.\n\n• ⚡ Sıfır Performans Kaybı & Akıllı Bağlantı:\n - Arka planda yerel soket üzerinden sıfır gecikmeyle çalışır; Discord açık değilse sessizce bekler, sistemi asla yormaz.\n\n• 🛠️ Genel İyileştirmeler & Stabilite Artışı.", + (Language::Turkish, "discord_rpc_toggle") => "Discord Rich Presence (Oynuyor Durumu)", + (Language::Turkish, "discord_rpc_desc") => "Discord profilinizde 'God of War III Remastered Oynuyor' durumunu ve oynama sürenizi canlı gösterir.", + (Language::Turkish, "discord_client_id_label") => "Discord Uygulama Kimliği (Client ID):", + (Language::Turkish, "discord_reset_btn") => "Varsayılana Sıfırla", (Language::Turkish, "gamemode_toggle") => "Feral GameMode (gamemoderun)", (Language::Turkish, "gamemode_desc") => "CPU zamanlayıcısını ve GPU performansını oyun için en yüksek seviyeye ayarlar.", (Language::Turkish, "save_apply") => "Ayarları Kaydet ve Uygula", @@ -415,8 +419,12 @@ pub fn text(lang: Language, key: &str) -> &'static str { (Language::English, "menu_latest_news") => "LATEST NEWS", (Language::English, "menu_options") => "OPTIONS", (Language::English, "menu_back") => "BACK", - (Language::English, "release_notes_title") => "God of War III Launcher - Release Notes (v2.0.3)", - (Language::English, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.0.3 CHANGELOG:\n\n• 👁️ Readable & Distinct Settings UI:\n - Increased text font sizes in the Settings menu for maximum readability without eye strain.\n - Highlighted red category headings with larger fonts and high contrast styling.\n\n• 🎬 Smooth PS4 Cinematic Splash Screen & 20s Auto-Close:\n - Replaced stuttery animation with ultra-smooth cinematic push-in zoom (60-144+ FPS).\n - The splash screen now automatically closes 20 seconds after game launch to save background resources.\n\n• 🛠️ UI Fixes:\n - Fixed duplicate update icon on the update button.", + (Language::English, "release_notes_title") => "God of War III Launcher - Release Notes (v2.1)", + (Language::English, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.1 CHANGELOG:\n\n• 🎮 Discord Rich Presence Integration:\n - Live 'Playing God of War III Remastered' status with Kratos artwork and playtime counter on your Discord profile.\n - Automatically reports 'In Main Menu', 'Compiling Shaders', and 'Scaling Mount Olympus' game states.\n - Can be toggled on/off in Settings with optional custom Discord Client ID support.\n\n• ⚡ Zero Performance Impact & Smart Reconnect:\n - Lightweight local IPC socket integration with zero CPU overhead.\n\n• 🛠️ General Polish & Stability Improvements.", + (Language::English, "discord_rpc_toggle") => "Discord Rich Presence (Activity Status)", + (Language::English, "discord_rpc_desc") => "Displays 'Playing God of War III Remastered' and elapsed play time on your Discord profile.", + (Language::English, "discord_client_id_label") => "Discord Application ID (Client ID):", + (Language::English, "discord_reset_btn") => "Reset Default", (Language::English, "gamemode_toggle") => "Feral GameMode (gamemoderun)", (Language::English, "gamemode_desc") => "Configures CPU governor and GPU clocks for optimal gaming performance.", (Language::English, "save_apply") => "Save & Apply Settings", @@ -677,8 +685,12 @@ pub fn text(lang: Language, key: &str) -> &'static str { (Language::German, "menu_latest_news") => "NEUIGKEITEN", (Language::German, "menu_options") => "OPTIONEN", (Language::German, "menu_back") => "ZURÜCK", - (Language::German, "release_notes_title") => "God of War III Launcher - Versionshinweise (v2.0.3)", - (Language::German, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.0.3 NEUIGKEITEN:\n\n• 👁️ Bessere Lesbarkeit im Einstellungsmenü:\n - Größere Schriftgrößen für Texte und Beschreibungen für ermüdungsfreies Lesen.\n - Deutlich hervorgehobene rote Kategorie-Überschriften in gut lesbarer Größe.\n\n• 🎬 Fließender PS4-Ladebildschirm & Automatisches Schließen nach 20s:\n - Butterweiche Zoom-Animation ohne Ruckeln (60-144+ FPS).\n - Schließt sich automatisch 20 Sekunden nach dem Spielstart.\n\n• 🛠️ Update-Button-Symbol korrigiert (doppeltes Icon entfernt).", + (Language::German, "release_notes_title") => "God of War III Launcher - Versionshinweise (v2.1)", + (Language::German, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.1 NEUIGKEITEN:\n\n• 🎮 Discord Rich Presence Integration:\n - Zeigt 'Spielt God of War III Remastered' mit Kratos-Cover und Spielzeitzähler in deinem Discord-Profil an.\n - Automatische Statusmeldungen für Hauptmenü, Shader-Kompilierung und Spielverlauf.\n - In den Einstellungen ein-/ausschaltbar mit optionaler benutzerdefinierter Client-ID.\n\n• ⚡ Keine Performance-Einbußen & Nahtlose Verbindung.", + (Language::German, "discord_rpc_toggle") => "Discord Rich Presence (Spielstatus)", + (Language::German, "discord_rpc_desc") => "Zeigt 'Spielt God of War III Remastered' und die Spielzeit in deinem Discord-Profil an.", + (Language::German, "discord_client_id_label") => "Discord Anwendungs-ID (Client ID):", + (Language::German, "discord_reset_btn") => "Standard wiederherstellen", (Language::German, "gamemode_toggle") => "Feral GameMode (gamemoderun)", (Language::German, "gamemode_desc") => "Optimiert CPU-Governor und GPU für maximale Gaming-Leistung.", (Language::German, "save_apply") => "Einstellungen speichern & anwenden", @@ -937,8 +949,12 @@ pub fn text(lang: Language, key: &str) -> &'static str { (Language::Spanish, "menu_latest_news") => "ÚLTIMAS NOTICIAS", (Language::Spanish, "menu_options") => "OPCIONES", (Language::Spanish, "menu_back") => "VOLVER", - (Language::Spanish, "release_notes_title") => "God of War III Launcher - Notas de la Versión (v2.0.3)", - (Language::Spanish, "release_notes_content") => "🔥 NOVEDADES DE GOD OF WAR III LAUNCHER v2.0.3:\n\n• 👁️ Mejor Legibilidad en el Menú de Ajustes:\n - Aumentado el tamaño de texto y descripciones para una lectura cómoda.\n - Encabezados de categoría en rojo más grandes y visibles.\n\n• 🎬 Pantalla de Inicio Cinemática PS4 Fluida y Auto-Cierre en 20s:\n - Animación de zoom suave y cinemática (60-144+ FPS).\n - Se cierra automáticamente 20 segundos después de iniciar el juego.\n\n• 🛠️ Corrección del botón de actualización (eliminado icono duplicado).", + (Language::Spanish, "release_notes_title") => "God of War III Launcher - Notas de la Versión (v2.1)", + (Language::Spanish, "release_notes_content") => "🔥 NOVEDADES DE GOD OF WAR III LAUNCHER v2.1:\n\n• 🎮 Integración con Discord Rich Presence:\n - Muestra 'Jugando a God of War III Remastered' con arte de Kratos y contador de tiempo en tu perfil de Discord.\n - Estados automáticos: En el Menú Principal, Compilando Shaders y Escalando el Monte Olimpo.\n - Se puede activar/desactivar en Ajustes con soporte para Client ID personalizado.\n\n• ⚡ Cero impacto en el rendimiento y conexión inteligente.", + (Language::Spanish, "discord_rpc_toggle") => "Discord Rich Presence (Estado de Juego)", + (Language::Spanish, "discord_rpc_desc") => "Muestra 'Jugando a God of War III Remastered' y el tiempo de juego en tu perfil de Discord.", + (Language::Spanish, "discord_client_id_label") => "ID de Aplicación de Discord (Client ID):", + (Language::Spanish, "discord_reset_btn") => "Restablecer por Defecto", (Language::Spanish, "gamemode_toggle") => "Feral GameMode (gamemoderun)", (Language::Spanish, "gamemode_desc") => "Optimiza la CPU y GPU para máximo rendimiento en juegos.", (Language::Spanish, "save_apply") => "Guardar y Aplicar", diff --git a/launcher/src/main.rs b/launcher/src/main.rs index 239d037..17e3f64 100644 --- a/launcher/src/main.rs +++ b/launcher/src/main.rs @@ -10,6 +10,7 @@ mod tuner; mod updater; mod shader_downloader; pub mod mods_manager; +pub mod discord_rpc; use app::LauncherApp; use eframe::egui; diff --git a/launcher/src/updater.rs b/launcher/src/updater.rs index 653e578..58c7c1c 100644 --- a/launcher/src/updater.rs +++ b/launcher/src/updater.rs @@ -5,7 +5,7 @@ use std::process::Command; use crate::config::LauncherConfig; use crate::tuner::apply_settings; -pub const BUILTIN_VERSION: &str = "v2.0.3"; +pub const BUILTIN_VERSION: &str = "v2.1"; pub const GITHUB_REPO: &str = "ScreamAngel/gow3_launcher"; pub const GITHUB_REPO_URL: &str = "https://github.com/ScreamAngel/gow3_launcher.git"; @@ -476,9 +476,9 @@ mod tests { assert_eq!(compare_version_strings("v1.5", "v1.4"), std::cmp::Ordering::Greater); assert_eq!(compare_version_strings("v1.10", "v1.9"), std::cmp::Ordering::Greater); assert_eq!(compare_version_strings("1.4", "v1.4"), std::cmp::Ordering::Equal); - assert_eq!(compare_version_strings("v2.0.4", "v2.0.3"), std::cmp::Ordering::Greater); - assert_eq!(compare_version_strings("v2.0.3", "v2.0.3"), std::cmp::Ordering::Equal); - assert_eq!(compare_version_strings("v2.0.2", "v2.0.3"), std::cmp::Ordering::Less); + assert_eq!(compare_version_strings("v2.2", "v2.1"), std::cmp::Ordering::Greater); + assert_eq!(compare_version_strings("v2.1", "v2.1"), std::cmp::Ordering::Equal); + assert_eq!(compare_version_strings("v2.0.3", "v2.1"), std::cmp::Ordering::Less); } #[test] diff --git a/launcher_config.json b/launcher_config.json index 1a23690..3c5a43a 100644 --- a/launcher_config.json +++ b/launcher_config.json @@ -22,7 +22,9 @@ "cpu_threads": 20 }, "last_git_commit": "0b4abe07acb711848d1b5d72402a9de8c0ad9844", - "launcher_version": "v2.0.3", + "launcher_version": "v2.1", + "discord_rpc_enabled": true, + "discord_client_id": "1124351711486689290", "disabled_mods": [ "(OPTIONAL) God Armor", "Blades of Athena Damage Boost",