feat: release v2.1 - integrate native Discord Rich Presence, activity reporting and custom Client ID support

This commit is contained in:
ScreamAngel committed 2026-11-03 23:27:27 +03:00
1 parent ff7c026bba
commit b6409869aa
11 files changed
+517 -17

No files matched your search

BIN
View File
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1671,7 +1671,7 @@ dependencies = [
[[package]]
name = "launcher"
version = "2.0.3"
version = "2.1.0"
dependencies = [
"eframe",
"egui_extras",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "launcher"
version = "2.0.3"
version = "2.1.0"
edition = "2024"
[dependencies]
+56
View File
@@ -1206,6 +1206,9 @@ pub struct LauncherApp {
// Installed Game Mods
installed_mods: Vec<crate::mods_manager::InstalledMod>,
// 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::*;
+16 -2
View File
@@ -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]
+411
View File
@@ -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<Vec<u8>>;
/// Activity metadata sent to Discord
#[derive(Debug, Clone)]
pub struct DiscordActivity {
pub details: String,
pub state: String,
pub start_timestamp: Option<i64>,
pub large_image: Option<String>,
pub large_text: Option<String>,
pub small_image: Option<String>,
pub small_text: Option<String>,
}
pub enum DiscordRpcMsg {
SetActivity(DiscordActivity),
ClearActivity,
UpdateConfig {
enabled: bool,
client_id: String,
},
Shutdown,
}
#[derive(Clone)]
pub struct DiscordRpc {
tx: Sender<DiscordRpcMsg>,
}
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<f32>) {
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<DiscordRpcMsg>, mut enabled: bool, mut client_id: String) {
let mut stream: Option<IpcStream> = None;
let mut current_activity: Option<DiscordActivity> = 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, &current_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, &current_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<IpcStream> {
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<IpcStream> {
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<IpcStream> {
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<IpcStream> {
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<W: Write>(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<W: Write>(writer: &mut W, nonce: u64) -> std::io::Result<()> {
let payload = build_clear_payload(nonce);
send_frame(writer, 1, &payload)
}
fn send_frame<W: Write>(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();
}
}
+24 -8
View File
@@ -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",
+1
View File
@@ -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;
+4 -4
View File
@@ -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]
+3 -1
View File
@@ -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",