Compare commits

...
15 Commits
Author SHA1 Message Date
ScreamAngel 9147960de5 fix(trophy): align PS4 35-trophy mapping, fix log tailer startup offset, and bypass bonus play anti-cheat 2026-11-09 17:42:38 +03:00
ScreamAngel 3a96e9845b fix(trophy): resolve trophy unlock bounds check, emulator count, and auto-sync savegame progress
- Fix sceNpTrophyGetTrophyUnlockState in gow3-probe & gow3-probe.exe returning count=0
- Add 'Bug Fix - Trophy Unlock Fix' XML patch in eboot.bin to return count=64 and bypass jae checks
- Add unbuffered stdout/stderr constructor to libgow3input.so for instant log tailing
- Add retroactive savegame trophy sync for previously defeated bosses (Poseidon, Hades, Helios, etc.)
2026-11-09 16:55:52 +03:00
ScreamAngel 3992455718 feat(trophy): real-time PS4 trophy engine, toast notifications, trophy dashboard, bonus play bug fix (v2.2) 2026-11-09 14:34:34 +03:00
ScreamAngel 08da3bec2c docs: add project memory and architecture overview for persistent context 2026-11-07 16:22:19 +03:00
ScreamAngel 04f61833ea feat(discord-rpc): automatic system language detection and multi-language support (tr, en, de, es) 2026-11-07 16:14:19 +03:00
ScreamAngel a60dc8bace feat(discord-rpc): accurate real-time story chapters and boss fight presence 2026-11-07 15:17:38 +03:00
ScreamAngel 7b8d9155fd fix(linux): isolate Windows-specific viewport commands and restore clean fullscreen & borderless on Linux Hyprland 2026-11-07 14:57:50 +03:00
ScreamAngel ec0a5893c7 fix(windows): resolve mouse click scancodes and keep shader screen fullscreen behind game 2026-11-07 14:35:29 +03:00
ScreamAngel 6bb6349c41 fix(windows): resolve black screen launch bug and refine shader screen 10s background lifecycle 2026-11-07 14:05:47 +03:00
ScreamAngel 8ba512f45e feat(discord): official God of War III Remastered App ID, real-time chapter & boss fight detection, challenges & arena presence 2026-11-07 03:42:52 +03:00
ScreamAngel 0136d4a426 feat(branding): embed custom God of War icons in probe binaries, SDL window & launcher metadata 2026-11-07 03:12:36 +03:00
ScreamAngel 71b9cd8596 fix(windows): resolve FPS speed surge, mouse click bridge, mod support and shader window z-order 2026-11-07 02:45:46 +03:00
ScreamAngel b6409869aa feat: release v2.1 - integrate native Discord Rich Presence, activity reporting and custom Client ID support 2026-11-03 23:27:27 +03:00
ScreamAngel ff7c026bba fix: remove missing glyph tofu box before red category headers 2026-11-03 19:01:23 +03:00
ScreamAngel f119d84439 feat: release v2.0.3 - enhanced settings UI readability, smooth PS4 splash zoom with 20s auto-close, fix duplicate update icon 2026-11-03 18:50:13 +03:00
70 changed files with 4697 additions and 173 deletions

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ user/
out/
GLCache/
.runtime/
mod-game-*/
vkd3d-proton.cache
fsr4_shaders/
.local/
+97
View File
@@ -0,0 +1,97 @@
# God of War III PC Launcher — Proje Hafızası ve Mimari Özeti
> **Versiyon:** v2.1
> **Depo:** [github.com/ScreamAngel/gow3_launcher](https://github.com/ScreamAngel/gow3_launcher) (`main` dalı)
> **Platform Desteği:** Linux (x86_64, Wayland/Hyprland & X11) ve Windows (x86_64 MSVC)
> **Tarih:** 07 Kasım 2026
---
## 1. Proje Mimarisi ve Çekirdek Dosyalar
* **Launcher Kaynak Kodu (`launcher/src/`):**
* `main.rs`: CLI başlatma argümanları (`--play`, `-p`), pencere ikonu yükleme ve eframe/egui giriş noktası.
* `app.rs`: Launcher ana GUI arayüzü, 6 sekme (Oyun, Grafik, Yapılandırma, Kontroller, Modlar, Sistem), shader derleme ekranı ve oyun süreci izleyicisi.
* `discord_rpc.rs`: Sıfır ek yükümlü, yerel Discord IPC istemcisi (Linux Unix Domain Socket & Windows Named Pipe), gerçek zamanlı bölüm & boss savaşı dedektörü, çoklu dil desteği (`RpcLanguage`).
* `win_input.rs`: Windows düşük seviye fare kancası (`WH_MOUSE_LL`), 1ms `timeBeginPeriod` zamanlayıcı çözünürlüğü ve oyun penceresini öne getirme (`SetForegroundWindow`).
* `runner.rs`: Mod bindirme (overlay) yöneticisi, Python/Rust mod montajı, oyun süreci (`gow3-probe`) çalıştırma ve log yönlendirme.
* `config.rs`: `launcher_config.json` yönetimi, donanım optimizasyonları ve otomatik sistem dili algılama (`default_language()`).
* `i18n.rs`: 4 dilli arayüz yerelleştirmesi (Türkçe, English, Deutsch, Español).
* `crash_analyzer.rs`: Oyun çöktüğünde `gow3_log.txt` çıktısını analiz edip kullanıcıya çözüm sunan hata teşhis motoru.
* **Dağıtılan İkili Dosyalar (Binaries):**
* Linux: `gow3-launcher`
* Windows: `gow3-launcher-win.exe`
* Alt dizin kopyaları: `gow3-linux/gow3-launcher`, `gow3-linux/gow3-launcher-win.exe`
---
## 2. Discord Rich Presence (RPC) Entegrasyonu
* **Resmi App ID:** `1440140183675867156` (*God of War III: Remastered*)
* **Resmi Varlıklar (Assets):**
* Kapak İkonu (Large Image): `https://cdn.discordapp.com/app-icons/1440140183675867156/2cbf4c7faf6444d474ff7b32b2025ec2.png`
* Rozet İkonu (Small Image): `https://raw.githubusercontent.com/ScreamAngel/gow3_launcher/main/gow3.png`
* **İletişim Kanalları:**
* **Linux:** `$XDG_RUNTIME_DIR/discord-ipc-0`..`9`, `/tmp/discord-ipc-0`..`9`, Flatpak, Snap ve Vesktop (`vesktop-ipc`).
* **Windows:** `\\.\pipe\discord-ipc-0`..`9` (Named Pipes). Olası donmaları engellemek için `PeekNamedPipe` ile asenkron kontrol mekanizması eklendi.
* **Gerçek Zamanlı Durum Tespiti:**
* `gow3_log.txt` dosyasından gerçek çalışma zamanı WAD yüklemeleri (`WAD_STATS: WAD <NAME>` ve `/WAD/PS4_LE/<NAME>.WAD`) taranır. Statik motor sözlüğü (`LoadWadSizeData`) **yoksayılır**.
* `user/savedata/*/CUSA01715/*.sce_sys/param.bin` SFO dosyasından anlık kayıt checkpoint'i ve zorluk derecesi çekilir.
* Ters kronolojik (newest-first) algoritma ile oyuncunun bulunduğu son bölge veya boss savaşı anlık belirlenir.
* **Otomatik Dil Desteği (tr, en, de, es):**
* İşletim sistemi dili (Linux'ta `$LANG`, Windows'ta `GetUserDefaultUILanguage`) veya launcher arayüz dili baz alınarak durum otomatik yerelleştirilir.
* **Bölüm & Boss Karşılıkları:**
* Poseidon Boss Savaşı: `Olympus Dağı (Poseidon Boss Fight)` / `Mount Olympus (Poseidon Boss Fight)`
* Hades Boss Savaşı: `Yeraltı Dünyası (Hades Boss Fight)` / `The Underworld (Hades Boss Fight)`
* Helios Boss Savaşı: `Olimpiya Şehri (Helios Boss Fight)` / `City of Olympia (Helios Boss Fight)`
* Hermes Kovalamacası: `Olimpiya Şehri (Hermes Kovalamacası)` / `City of Olympia (Hermes Chase)`
* Herkül Boss Savaşı: `Olympus Forumu (Hercules Boss Fight)` / `Forum of Olympus (Hercules Boss Fight)`
* Kronos Boss Savaşı: `Tartarus Çukuru (Cronos Boss Fight)` / `Pit of Tartarus (Cronos Boss Fight)`
* Hephaistos Boss Savaşı: `Demirhane (Hephaestus Boss Fight)` / `The Forge (Hephaestus Boss Fight)`
* Skorpius Boss Savaşı: `Olimpos Mağaraları (Skorpius Boss Fight)` / `Caverns of Olympus (Skorpius Boss Fight)`
* Zeus Boss Savaşı: `Olympus Tepesi (Zeus Boss Fight)` / `Summit of Olympus (Zeus Boss Fight)`
* Mücadeleler: `Olympus Mücadelesi 1-7` / `Challenge of Olympus 1-7`
* Savaş Arenası: `Savaş Arenası (Combat Arena)` / `Combat Arena`
* Menü: `Ana Menüde` / `In Main Menu`
* Shader Derleme: `Shader'lar Derleniyor (%X)` / `Compiling Shaders (X%)`
---
## 3. Çözülen Kritik Hatalar ve Teknik Notlar
1. **Windows Fare Tıklama Algılama Hatası:**
* Sorun: Windows'ta sol ve sağ tık çalışmıyordu.
* Çözüm: `win_input.rs` içindeki `WH_MOUSE_LL` kancasında PS4 SDL2 girdi haritasının beklediği F13/F14 sanal tarama kodları (`0x64` = Kare/Hafif Vuruş, `0x65` = Üçgen/Ağır Vuruş) düzeltildi.
2. **Windows Siyah Ekran / Mod Açılış Hatası:**
* Sorun: NTFS junction oluşturma yetki hatası yüzünden Windows'ta oyun siyah ekranda takılıyordu.
* Çözüm: `runner.rs` içinde Rust tabanlı doğrudan dosya kopyalama bindirmesi (`out/mod-game-rust-overlay`) uygulandı, yol formatı ve dosya doğrulaması eklendi.
3. **Shader Ekranı Küçülme / Kapanma / Arka Plana Düşme:**
* Sorun: Shader derleme ekranı açıldığında oyun arka plana düşüyor veya görev çubuğuna küçülüyordu.
* Çözüm: Oyun penceresi açıldığında shader ekranı 10 saniye boyunca arka planda tam ekran açık kalır, oyun penceresi öne getirilir (`bring_game_window_to_front`) ve süre bitiminde launcher normal moduna döner (asla küçülmez).
4. **Linux Hyprland / Wayland Tam Ekran Hatası:**
* Sorun: Windows için eklenen pencere boyutu komutları Linux Wayland'de oyunun kenarlıksız pencere moduna geçmesini bozuyordu.
* Çözüm: `Maximized(true)` ve ilgili `ViewportCommand` çağrıları `#[cfg(target_os = "windows")]` ile izole edildi, Linux'ta yerel `hyprctl` ve saf Wayland geometri protokolü korundu.
5. **Discord RPC'nin Sadece "Ana Menüde" Kalması:**
* Sorun: Motorun statik `LoadWadSizeData: WAD_Shell` satırı yüzünden log döngüsü erken çıkış veriyor ve boss/bölüm adımlarını atlıyordu.
* Çözüm: Statik sözlük filtrelendi, gerçek WAD yüklemeleri ters sırada önceliklendirildi.
---
## 4. Derleme ve Test Komutları
* **Birim Testleri (30 Test):**
```bash
cargo test --manifest-path launcher/Cargo.toml
```
* **Linux Release Derlemesi:**
```bash
cargo build --release --manifest-path launcher/Cargo.toml
cp -f launcher/target/release/launcher gow3-launcher
cp -f launcher/target/release/launcher gow3-linux/gow3-launcher
```
* **Windows Release Derlemesi (Cross-Compile):**
```bash
cargo xwin build --release --manifest-path launcher/Cargo.toml --target x86_64-pc-windows-msvc
cp -f launcher/target/x86_64-pc-windows-msvc/release/launcher.exe gow3-launcher-win.exe
cp -f launcher/target/x86_64-pc-windows-msvc/release/launcher.exe gow3-linux/gow3-launcher-win.exe
```
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+19
View File
@@ -415,3 +415,22 @@ void _ZN5ImGui11ProgressBarEfRK6ImVec2PKc(float fraction, const void* size_arg,
(void)overlay;
}
#ifdef _WIN32
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
(void)hinstDLL;
(void)lpReserved;
if (fdwReason == DLL_PROCESS_ATTACH) {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
return TRUE;
}
#else
__attribute__((constructor))
static void gow3_runtime_preinit(void) {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
#endif
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+1 -1
View File
@@ -1671,7 +1671,7 @@ dependencies = [
[[package]]
name = "launcher"
version = "2.0.2"
version = "2.2.0"
dependencies = [
"eframe",
"egui_extras",
+2 -2
View File
@@ -1,12 +1,12 @@
[package]
name = "launcher"
version = "2.0.2"
version = "2.2.0"
edition = "2024"
[dependencies]
eframe = "0.36.2"
egui_extras = { version = "0.36.2", features = ["image"] }
image = { version = "0.25.10", default-features = false, features = ["png"] }
image = { version = "0.25.10", default-features = false, features = ["png", "bmp"] }
rfd = "0.17.2"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.154"
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.
+2 -2
View File
@@ -6,8 +6,8 @@ fn main() {
res.set("ProductName", "God of War III Remastered Launcher");
res.set("FileDescription", "God of War III Remastered PC Launcher");
res.set("LegalCopyright", "God of War III Remastered");
res.set("ProductVersion", "1.9.0.0");
res.set("FileVersion", "1.9.0.0");
res.set("ProductVersion", "2.1.0.0");
res.set("FileVersion", "2.1.0.0");
if let Err(e) = res.compile() {
eprintln!("Warning: failed to compile Windows icon resource: {}", e);
}
+323 -115
View File
@@ -18,6 +18,7 @@ pub enum ViewMode {
#[default]
MainMenu,
Settings,
Trophies,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -839,14 +840,14 @@ fn render_button_feedback(
}
fn render_section_tag(ui: &mut egui::Ui, tag: &str) {
ui.add_space(4.0);
ui.add_space(8.0);
ui.label(
egui::RichText::new(tag)
.size(11.0)
.color(egui::Color32::from_rgb(239, 68, 68))
egui::RichText::new(tag.to_uppercase())
.size(16.0)
.color(egui::Color32::from_rgb(255, 95, 95))
.strong(),
);
ui.add_space(1.0);
ui.add_space(4.0);
}
fn render_stat_tile(
@@ -871,12 +872,12 @@ fn render_stat_tile(
.corner_radius(egui::CornerRadius::same(4))
.inner_margin(egui::Margin::symmetric(5, 2))
.show(ui, |ui| {
ui.label(egui::RichText::new(tag).color(tag_color).size(10.0).strong());
ui.label(egui::RichText::new(tag).color(tag_color).size(11.0).strong());
});
ui.label(egui::RichText::new(title).color(egui::Color32::from_rgb(165, 170, 185)).size(11.0));
ui.label(egui::RichText::new(title).color(egui::Color32::from_rgb(175, 180, 195)).size(12.5));
});
ui.add_space(2.0);
ui.label(egui::RichText::new(value).color(egui::Color32::WHITE).strong().size(13.0));
ui.label(egui::RichText::new(value).color(egui::Color32::WHITE).strong().size(15.0));
if let Some((b_text, b_color)) = badge {
ui.add_space(3.0);
egui::Frame::new()
@@ -885,7 +886,7 @@ fn render_stat_tile(
.corner_radius(egui::CornerRadius::same(4))
.inner_margin(egui::Margin::symmetric(6, 2))
.show(ui, |ui| {
ui.label(egui::RichText::new(b_text).color(b_color).size(10.0).strong());
ui.label(egui::RichText::new(b_text).color(b_color).size(11.5).strong());
});
}
});
@@ -1004,8 +1005,8 @@ fn render_binding_row(
) {
ui.horizontal(|ui| {
paint_ps_badge(ui, target);
ui.add_space(2.0);
ui.label(egui::RichText::new(label).size(12.5).color(egui::Color32::from_rgb(230, 230, 235)));
ui.add_space(4.0);
ui.label(egui::RichText::new(label).size(14.5).color(egui::Color32::from_rgb(240, 240, 245)).strong());
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let is_listening = listening_for == Some(target);
@@ -1033,7 +1034,7 @@ fn render_binding_row(
};
let btn = egui::Button::new(
egui::RichText::new(btn_text).strong().size(12.0).color(text_color),
egui::RichText::new(btn_text).strong().size(13.5).color(text_color),
)
.min_size(egui::vec2(145.0, 28.0))
.fill(bg_color)
@@ -1206,6 +1207,13 @@ pub struct LauncherApp {
// Installed Game Mods
installed_mods: Vec<crate::mods_manager::InstalledMod>,
// Discord Rich Presence
pub discord_rpc: crate::discord_rpc::DiscordRpc,
// PS4 Trophy System
pub trophy_manager: Arc<std::sync::Mutex<crate::trophy::TrophyManager>>,
pub trophy_watcher: Option<crate::trophy::TrophyWatcherService>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1275,6 +1283,16 @@ 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);
discord_rpc.set_in_menu(&config.language);
let trophy_manager = Arc::new(std::sync::Mutex::new(crate::trophy::TrophyManager::new(&base_dir)));
let trophy_watcher = Some(crate::trophy::TrophyWatcherService::start(
base_dir.clone(),
trophy_manager.clone(),
config.language.clone(),
));
let mut app = Self {
base_dir: base_dir.clone(),
config,
@@ -1335,6 +1353,9 @@ impl LauncherApp {
shader_loading: None,
launcher_version,
installed_mods,
discord_rpc,
trophy_manager,
trophy_watcher,
};
crate::log_info!("Launcher UI initialized successfully. Version: {}", app.launcher_version);
@@ -1514,6 +1535,7 @@ impl LauncherApp {
}
fn apply_all_optimizations(&mut self) {
self.discord_rpc.update_config(self.config.discord_rpc_enabled);
let res = apply_settings(&self.base_dir, &self.config);
match res {
@@ -1679,9 +1701,15 @@ 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(self.base_dir.clone(), now, self.config.target_fps, is_modded, &self.config.language);
} else {
self.discord_rpc.set_compiling_shaders(Some(0.0), &self.config.language);
// 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));
#[cfg(target_os = "windows")]
ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel(egui::WindowLevel::AlwaysOnTop));
let initial_total = if self.cached_shader_count > 0 && self.cached_shader_count < 5000 {
@@ -1898,9 +1926,12 @@ impl LauncherApp {
1.0
};
let alpha = (elapsed * 2.2).min(1.0);
let pulse = ((time * 0.9).sin() * 0.006) as f32; // subtle breathing
// Continuous, buttery-smooth cinematic push-in zoom towards camera
let zoom_progress = (elapsed / 20.0).clamp(0.0, 1.0);
let zoom_ease = zoom_progress * (2.0 - zoom_progress); // smooth quad ease-out
let scale_factor = 1.0 + zoom_ease * 0.095;
let center = screen_rect.center();
let zoomed_size = screen_rect.size() * (1.0 + pulse);
let zoomed_size = screen_rect.size() * scale_factor;
let draw_rect = egui::Rect::from_center_size(center, zoomed_size);
painter.image(
@@ -2133,7 +2164,7 @@ impl LauncherApp {
);
ui.painter().rect_filled(top_vignette, 0.0, egui::Color32::from_rgba_unmultiplied(0, 0, 0, 130));
let bottom_vignette_height = 145.0;
let bottom_vignette_height = 168.0;
let bottom_vignette = egui::Rect::from_min_max(
egui::pos2(screen_rect.min.x, screen_rect.max.y - bottom_vignette_height),
screen_rect.max,
@@ -2218,6 +2249,11 @@ impl LauncherApp {
self.current_lang = *lang;
self.config.language = lang.code().to_string();
let _ = self.config.save(&self.base_dir);
self.discord_rpc.update_language(lang.code());
self.discord_rpc.set_in_menu(lang.code());
if let Some(ref watcher) = self.trophy_watcher {
watcher.set_language(lang.code().to_string());
}
}
}
});
@@ -2446,12 +2482,12 @@ impl LauncherApp {
}
}
// 7. Bottom-Left: Console Action Menu Items (Row 1: Latest News, Row 2: Mods, Row 3: Options)
// 7. Bottom-Left: Console Action Menu Items (Row 1: Latest News, Row 2: Trophies, Row 3: Mods, Row 4: Options)
let bl_x = screen_rect.min.x + 28.0;
let bl_y = screen_rect.max.y - 124.0;
let bl_y = screen_rect.max.y - 156.0;
let bl_rect = egui::Rect::from_min_size(
egui::pos2(bl_x, bl_y),
egui::vec2(280.0, 114.0),
egui::vec2(280.0, 146.0),
);
ui.scope_builder(egui::UiBuilder::new().max_rect(bl_rect), |ui| {
ui.vertical(|ui| {
@@ -2470,7 +2506,20 @@ impl LauncherApp {
self.show_release_notes_modal = true;
}
// Row 2: 📦 MODLAR / MODS
// Row 2: 🏆 KUPALAR / TROPHIES
if render_console_action_button(
ui,
"btn_trophies",
"🏆",
self.t("menu_trophies"),
14.5,
false,
) {
self.view_mode = ViewMode::Trophies;
self.view_transition_start = std::time::Instant::now();
}
// Row 3: 📦 MODLAR / MODS
if render_console_action_button(
ui,
"btn_mods",
@@ -2484,7 +2533,7 @@ impl LauncherApp {
self.view_transition_start = std::time::Instant::now();
}
// Row 3: ⚙️ AYARLAR / OPTIONS
// Row 4: ⚙️ AYARLAR / OPTIONS
if render_console_action_button(
ui,
"btn_options",
@@ -2662,6 +2711,10 @@ impl LauncherApp {
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| {
// Ayarlar ekranında göz yormayan, okunaklı net metin boyutları
ui.style_mut().text_styles.insert(egui::TextStyle::Body, egui::FontId::proportional(14.5));
ui.style_mut().text_styles.insert(egui::TextStyle::Button, egui::FontId::proportional(14.0));
// Fade / smooth slide transition
let elapsed = self.view_transition_start.elapsed().as_secs_f32();
let t = (elapsed / 0.18).min(1.0);
@@ -2753,6 +2806,36 @@ impl LauncherApp {
});
}
pub fn render_trophies_screen(&mut self, ui: &mut egui::Ui, _ctx: &egui::Context) {
let screen_rect = ui.max_rect();
// 1. Fullscreen Wallpaper (pic1.png)
if let Some(ref bg) = self.bg_texture {
let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
ui.painter().image(bg.id(), screen_rect, uv, egui::Color32::from_white_alpha(90));
}
// 2. Dark Frosted Backdrop
ui.painter().rect_filled(
screen_rect,
0.0,
egui::Color32::from_rgba_unmultiplied(10, 11, 16, 238),
);
let mut back_clicked = false;
let lang = self.current_lang.code().to_string();
ui.add_space(8.0);
if let Ok(mut mgr) = self.trophy_manager.lock() {
crate::trophy::render_trophy_dashboard(ui, &mut mgr, &lang, &mut back_clicked);
}
if back_clicked {
self.view_mode = ViewMode::MainMenu;
self.view_transition_start = std::time::Instant::now();
}
}
// ==========================================
// SCREEN: SİSTEM ÖZELLİKLERİ (SYSTEM SPECS)
// ==========================================
@@ -2957,7 +3040,7 @@ impl LauncherApp {
// 1. Target FPS
ui.horizontal(|ui| {
ui.label(egui::RichText::new(self.t("target_fps")).strong().color(egui::Color32::WHITE));
ui.label(egui::RichText::new(self.t("target_fps")).strong().color(egui::Color32::WHITE).size(14.5));
let fps_str = match self.config.target_fps {
30 => "30 FPS (Konsol Kilidi & Düşük Sistem - Maksimum Kararlılık)".to_string(),
40 => "40 FPS (Steam Deck 40Hz)".to_string(),
@@ -2984,87 +3067,129 @@ impl LauncherApp {
ui.selectable_value(&mut self.config.target_fps, 0, "Sınırsız (Uncapped)");
});
});
ui.label(egui::RichText::new("Oyunun hedef kare hızı sınırı. Delta-Time ve 60Hz fizik motor koruması sayesinde 240 FPS'e kadar oyun hızlanmadan ve sapıtmadan ultra akıcı çalışır.").color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new("Oyunun hedef kare hızı sınırı. Delta-Time ve 60Hz fizik motor koruması sayesinde 240 FPS'e kadar oyun hızlanmadan ve sapıtmadan ultra akıcı çalışır.").color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 2. Skip Cinematics with X
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.skip_cinematics);
let lbl = self.t("skip_cinematics");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.skip_cinematics = !self.config.skip_cinematics;
}
});
ui.label(egui::RichText::new(self.t("skip_cinematics_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("skip_cinematics_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 3. Feral GameMode
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.use_gamemode);
let lbl = self.t("gamemode_toggle");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.use_gamemode = !self.config.use_gamemode;
}
if !self.has_gamemode {
ui.label(egui::RichText::new("(Sistemde Gamemode Bulunamadı)").color(egui::Color32::YELLOW).size(10.5));
ui.label(egui::RichText::new("(Sistemde Gamemode Bulunamadı)").color(egui::Color32::YELLOW).size(11.0));
}
});
ui.label(egui::RichText::new(self.t("gamemode_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("gamemode_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 4. Delta Time Speed Patch
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.patch_delta_time);
let lbl = "Delta Time Senkronizasyon Yaması (60-144 FPS)";
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.patch_delta_time = !self.config.patch_delta_time;
}
});
ui.label(egui::RichText::new("Yüksek kare hızlarında (60-144 FPS) oyun hızının iki katına çıkmasını engelleyerek normal hızda çalışmasını sağlar.").color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new("Yüksek kare hızlarında (60-144 FPS) oyun hızının iki katına çıkmasını engelleyerek normal hızda çalışmasını sağlar.").color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 5. Disable Motion Blur
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.disable_motion_blur);
let lbl = self.t("disable_motion_blur");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.disable_motion_blur = !self.config.disable_motion_blur;
}
});
ui.label(egui::RichText::new(self.t("disable_motion_blur_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("disable_motion_blur_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 6. Disable Depth of Field
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.disable_dof);
let lbl = self.t("disable_dof");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.disable_dof = !self.config.disable_dof;
}
});
ui.label(egui::RichText::new(self.t("disable_dof_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("disable_dof_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 7. Background Controller Input
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.background_controller_input);
let lbl = self.t("bg_input");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.background_controller_input = !self.config.background_controller_input;
}
});
ui.label(egui::RichText::new(self.t("bg_input_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("bg_input_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 8. Hide Mouse Cursor
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.hide_mouse_cursor);
let lbl = self.t("hide_mouse_cursor");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.hide_mouse_cursor = !self.config.hide_mouse_cursor;
}
});
ui.label(egui::RichText::new(self.t("hide_mouse_cursor_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
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);
}
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);
}
});
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("🔒 Discord App: God of War III: Remastered (1440140183675867156)")
.size(12.5)
.strong()
.color(egui::Color32::from_rgb(160, 220, 180)),
);
});
ui.label(
egui::RichText::new("Bölümler, Poseidon/Hades/Zeus boss savaşları, mücadeleler ve arena durumu otomatik paylaşılır.")
.size(11.5)
.color(egui::Color32::from_rgb(170, 175, 195)),
);
}
ui.add_space(8.0);
// 10. Bonus Play QTE and Enemy Spawning Fix
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.bonus_play_fix);
let lbl = self.t("bonus_play_fix_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.bonus_play_fix = !self.config.bonus_play_fix;
}
});
ui.label(egui::RichText::new(self.t("bonus_play_fix_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(12.0);
ui.separator();
@@ -3095,8 +3220,8 @@ impl LauncherApp {
);
ui.label(
egui::RichText::new(self.t("mods_desc"))
.size(11.5)
.color(egui::Color32::from_rgb(175, 185, 205)),
.size(12.5)
.color(egui::Color32::from_rgb(190, 195, 210)),
);
ui.separator();
@@ -3154,7 +3279,7 @@ impl LauncherApp {
.replacen("{}", &disabled_count.to_string(), 1),
)
.color(egui::Color32::from_rgb(160, 215, 255))
.size(12.0)
.size(13.0)
.strong(),
);
});
@@ -3175,14 +3300,14 @@ impl LauncherApp {
ui.label(
egui::RichText::new(format!("📦 {}", self.t("mods_empty_title")))
.color(egui::Color32::from_rgb(255, 215, 120))
.size(14.0)
.size(14.5)
.strong(),
);
ui.add_space(4.0);
ui.label(
egui::RichText::new(self.t("mods_empty_desc"))
.color(egui::Color32::from_rgb(175, 180, 195))
.size(11.5),
.color(egui::Color32::from_rgb(190, 195, 210))
.size(12.5),
);
});
});
@@ -3229,12 +3354,12 @@ impl LauncherApp {
let title_resp = ui.add(
egui::Label::new(
egui::RichText::new(format!("{} {}", icon, m.title))
.size(14.5)
.size(15.0)
.strong()
.color(if is_active {
egui::Color32::WHITE
} else {
egui::Color32::from_rgb(150, 155, 165)
egui::Color32::from_rgb(160, 165, 175)
}),
)
.sense(egui::Sense::click()),
@@ -3248,23 +3373,23 @@ impl LauncherApp {
ui.label(
egui::RichText::new(format!("[{}]", m.version))
.size(11.0)
.color(egui::Color32::from_rgb(140, 165, 200)),
.size(11.5)
.color(egui::Color32::from_rgb(150, 175, 210)),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if is_active {
ui.label(
egui::RichText::new(self.t("mods_status_active"))
.size(11.5)
.size(12.5)
.color(egui::Color32::from_rgb(134, 239, 172))
.strong(),
);
} else {
ui.label(
egui::RichText::new(self.t("mods_status_disabled"))
.size(11.5)
.color(egui::Color32::from_rgb(180, 100, 100)),
.size(12.5)
.color(egui::Color32::from_rgb(200, 110, 110)),
);
}
});
@@ -3275,8 +3400,8 @@ impl LauncherApp {
ui.add_space(3.0);
ui.label(
egui::RichText::new(&m.description)
.size(11.0)
.color(egui::Color32::from_rgb(175, 185, 200)),
.size(12.5)
.color(egui::Color32::from_rgb(190, 195, 210)),
);
}
@@ -3285,7 +3410,7 @@ impl LauncherApp {
ui.horizontal(|ui| {
ui.label(
egui::RichText::new(format!("👤 {}: {}", self.t("mods_author"), m.author))
.size(10.5)
.size(11.5)
.color(egui::Color32::from_rgb(255, 215, 120)),
);
@@ -3304,8 +3429,8 @@ impl LauncherApp {
m.file_count,
samples_str
))
.size(10.5)
.color(egui::Color32::from_rgb(150, 200, 255)),
.size(11.5)
.color(egui::Color32::from_rgb(160, 210, 255)),
);
});
});
@@ -3359,19 +3484,20 @@ impl LauncherApp {
card_frame.show(ui, |ui| {
ui.heading(
egui::RichText::new(format!("⚙️ {}", self.t("nav_graphics")))
.size(16.0)
.color(egui::Color32::from_rgb(150, 205, 255)),
.size(17.5)
.color(egui::Color32::from_rgb(150, 205, 255))
.strong(),
);
ui.label(
egui::RichText::new(self.t("graphics_subtitle"))
.size(11.5)
.color(egui::Color32::from_rgb(175, 185, 205)),
.size(13.0)
.color(egui::Color32::from_rgb(185, 195, 215)),
);
ui.separator();
// 1. Hazır Grafik Profili
ui.horizontal(|ui| {
ui.label(egui::RichText::new(self.t("graphic_preset")).strong().color(egui::Color32::WHITE));
ui.label(egui::RichText::new(self.t("graphic_preset")).strong().color(egui::Color32::WHITE).size(14.5));
let current_preset_text = match self.config.graphic_preset {
crate::config::GraphicPreset::UltraLow => self.t("preset_ultra_low"),
crate::config::GraphicPreset::Low => self.t("preset_low"),
@@ -3404,12 +3530,12 @@ impl LauncherApp {
}
});
});
ui.label(egui::RichText::new(self.t("preset_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("preset_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 2. Render Resolution
ui.horizontal(|ui| {
ui.label(egui::RichText::new(self.t("render_resolution")).strong().color(egui::Color32::WHITE));
ui.label(egui::RichText::new(self.t("render_resolution")).strong().color(egui::Color32::WHITE).size(14.5));
let res_text = match self.config.render_resolution.as_str() {
"480p" => "480p (854x480 - Ultra Düşük Donanım / Yüksek FPS)",
"720p" => "720p (1280x720 - Düşük Sistem & Steam Deck)",
@@ -3449,46 +3575,46 @@ impl LauncherApp {
}
});
});
ui.label(egui::RichText::new(self.t("res_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("res_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 3. AMD FSR
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.fsr_enabled);
let lbl = self.t("fsr_toggle");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.fsr_enabled = !self.config.fsr_enabled;
self.config.check_preset_match();
}
});
ui.label(egui::RichText::new(self.t("fsr_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("fsr_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 4. Texture Corruption & Yellow Tint Fix (patch_readbacks)
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.patch_readbacks);
let lbl = self.t("readback_patch");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.patch_readbacks = !self.config.patch_readbacks;
}
});
ui.label(egui::RichText::new(self.t("readback_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("readback_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 5. Asynchronous Readbacks (async_readbacks)
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.async_readbacks);
let lbl = self.t("async_readbacks");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.async_readbacks = !self.config.async_readbacks;
}
});
ui.label(egui::RichText::new(self.t("async_readbacks_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("async_readbacks_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 6. VRAM GC Budget
ui.horizontal(|ui| {
ui.label(egui::RichText::new(self.t("vram_budget")).strong().color(egui::Color32::WHITE));
ui.label(egui::RichText::new(self.t("vram_budget")).strong().color(egui::Color32::WHITE).size(14.5));
let vram_text = match self.config.vram_gc_budget_mb {
1280 => "1280 MB (2 GB GPU / Düşük Sistem - PCIe Taşma Korumalı)".to_string(),
2048 => "2048 MB (3 GB GPU / Steam Deck)".to_string(),
@@ -3511,20 +3637,20 @@ impl LauncherApp {
ui.selectable_value(&mut self.config.vram_gc_budget_mb, 8192, "8192 MB (12-16 GB+ GPU / Yüksek)");
});
});
ui.label(egui::RichText::new(self.t("vram_budget_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("vram_budget_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
let effective = self.config.get_effective_vram_budget();
if effective != self.config.vram_gc_budget_mb && self.config.cached_hardware.vram_mb > 0 {
ui.label(egui::RichText::new(format!(
"🛡️ Donanım Koruması Devrede: Ekran kartınızın kapasitesine ({} MB) göre aktif VRAM bütçesi {} MB olarak güvenli şekilde sınırlandırıldı.",
self.config.cached_hardware.vram_mb, effective
)).color(egui::Color32::from_rgb(102, 225, 106)).size(11.0));
)).color(egui::Color32::from_rgb(102, 225, 106)).size(12.0));
}
ui.add_space(8.0);
// 7. Display Mode & VSync
ui.horizontal(|ui| {
ui.label(egui::RichText::new(self.t("display_mode")).strong());
ui.label(egui::RichText::new(self.t("display_mode")).strong().color(egui::Color32::WHITE).size(14.5));
let mode_borderless = self.t("display_borderless");
let mode_fullscreen = self.t("display_fullscreen");
let mode_windowed = self.t("display_windowed");
@@ -3544,10 +3670,22 @@ impl LauncherApp {
ui.add_space(16.0);
toggle_switch(ui, &mut self.config.vsync);
let vsync_lbl = self.t("vsync");
if ui.add(egui::Label::new(egui::RichText::new(vsync_lbl).strong()).sense(egui::Sense::click())).clicked() {
if ui.add(egui::Label::new(egui::RichText::new(vsync_lbl).strong().color(egui::Color32::WHITE).size(14.5)).sense(egui::Sense::click())).clicked() {
self.config.vsync = !self.config.vsync;
}
});
ui.label(egui::RichText::new(self.t("display_mode_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// 8. Hide Mouse Cursor
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.hide_mouse_cursor);
let lbl = self.t("hide_mouse_cursor");
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.hide_mouse_cursor = !self.config.hide_mouse_cursor;
}
});
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(12.0);
ui.separator();
@@ -3584,7 +3722,7 @@ impl LauncherApp {
} else {
&self.config.game_dir
};
ui.label(egui::RichText::new(dir_display).monospace());
ui.label(egui::RichText::new(dir_display).monospace().size(13.5).color(egui::Color32::WHITE));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if render_button_feedback(ui, "btn_browse", self.t("manual_select"), egui::vec2(120.0, 30.0), false).clicked() {
@@ -3607,12 +3745,14 @@ impl LauncherApp {
ui.label(
egui::RichText::new(self.t("game_valid"))
.color(egui::Color32::from_rgb(105, 225, 110))
.size(13.0)
.strong(),
);
} else {
ui.label(
egui::RichText::new(self.t("game_invalid"))
.color(egui::Color32::from_rgb(245, 90, 85))
.size(13.0)
.strong(),
);
}
@@ -3630,6 +3770,7 @@ impl LauncherApp {
ui.label(
egui::RichText::new(self.t("scan_not_found_inline"))
.color(egui::Color32::from_rgb(255, 140, 140))
.size(13.0)
.strong(),
);
});
@@ -3650,14 +3791,14 @@ impl LauncherApp {
// 1. Python
ui.horizontal(|ui| {
ui.label(egui::RichText::new("🐍 Python 3 Ortamı:").strong());
ui.label(egui::RichText::new("🐍 Python 3 Ortamı:").strong().color(egui::Color32::WHITE).size(14.5));
if self.has_python {
ui.label(egui::RichText::new("✅ Sistemde Hazır").color(egui::Color32::from_rgb(105, 225, 110)).strong());
ui.label(egui::RichText::new("✅ Sistemde Hazır").color(egui::Color32::from_rgb(105, 225, 110)).size(13.5).strong());
} else if self.is_downloading_python {
ui.spinner();
ui.label(egui::RichText::new("İndiriliyor...").color(egui::Color32::YELLOW));
ui.label(egui::RichText::new("İndiriliyor...").color(egui::Color32::YELLOW).size(13.0));
} else {
ui.label(egui::RichText::new("⚠️ Eksik").color(egui::Color32::from_rgb(245, 90, 85)).strong());
ui.label(egui::RichText::new("⚠️ Eksik").color(egui::Color32::from_rgb(245, 90, 85)).size(13.5).strong());
if render_button_feedback(ui, "btn_py_dl", "Python Ortamını İndir", egui::vec2(160.0, 26.0), false).clicked() {
self.start_python_download();
}
@@ -3667,14 +3808,14 @@ impl LauncherApp {
ui.add_space(6.0);
// 2. Engine
ui.horizontal(|ui| {
ui.label(egui::RichText::new("⚙️ GOW3 Port Motoru:").strong());
ui.label(egui::RichText::new("⚙️ GOW3 Port Motoru:").strong().color(egui::Color32::WHITE).size(14.5));
if self.has_engine {
ui.label(egui::RichText::new("✅ Motor Hazır").color(egui::Color32::from_rgb(105, 225, 110)).strong());
ui.label(egui::RichText::new("✅ Motor Hazır").color(egui::Color32::from_rgb(105, 225, 110)).size(13.5).strong());
} else if self.is_downloading_engine {
ui.spinner();
ui.label(egui::RichText::new("İndiriliyor...").color(egui::Color32::YELLOW));
ui.label(egui::RichText::new("İndiriliyor...").color(egui::Color32::YELLOW).size(13.0));
} else {
ui.label(egui::RichText::new("⚠️ Eksik").color(egui::Color32::from_rgb(245, 90, 85)).strong());
ui.label(egui::RichText::new("⚠️ Eksik").color(egui::Color32::from_rgb(245, 90, 85)).size(13.5).strong());
if render_button_feedback(ui, "btn_eng_dl", "Motoru İndir", egui::vec2(140.0, 26.0), false).clicked() {
self.start_engine_download(false);
}
@@ -3711,25 +3852,25 @@ impl LauncherApp {
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.is_motion_controls_enabled);
let motion_lbl = self.t("motion_controls");
if ui.add(egui::Label::new(egui::RichText::new(motion_lbl).strong()).sense(egui::Sense::click())).clicked() {
if ui.add(egui::Label::new(egui::RichText::new(motion_lbl).strong().color(egui::Color32::WHITE).size(14.5)).sense(egui::Sense::click())).clicked() {
self.config.is_motion_controls_enabled = !self.config.is_motion_controls_enabled;
}
});
ui.label(egui::RichText::new(self.t("motion_controls_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("motion_controls_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(8.0);
// Analog stick deadzones
ui.columns(2, |cols| {
cols[0].vertical(|ui| {
ui.label(egui::RichText::new(self.t("deadzone_left")).strong());
ui.label(egui::RichText::new(self.t("deadzone_left")).strong().color(egui::Color32::WHITE).size(14.0));
ui.add(egui::Slider::new(&mut self.config.deadzone_left, 1..=40).text(""));
});
cols[1].vertical(|ui| {
ui.label(egui::RichText::new(self.t("deadzone_right")).strong());
ui.label(egui::RichText::new(self.t("deadzone_right")).strong().color(egui::Color32::WHITE).size(14.0));
ui.add(egui::Slider::new(&mut self.config.deadzone_right, 1..=40).text(""));
});
});
ui.label(egui::RichText::new(self.t("deadzone_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("deadzone_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
});
ui.add_space(10.0);
@@ -3844,8 +3985,8 @@ impl LauncherApp {
);
ui.label(
egui::RichText::new(self.t("shader_cache_desc"))
.size(11.5)
.color(egui::Color32::from_rgb(175, 185, 205)),
.size(12.5)
.color(egui::Color32::from_rgb(190, 195, 210)),
);
ui.separator();
@@ -3884,7 +4025,7 @@ impl LauncherApp {
egui::Color32::from_rgb(254, 240, 138)
})
.strong()
.size(13.0),
.size(13.5),
);
});
});
@@ -3896,7 +4037,7 @@ impl LauncherApp {
ui.horizontal(|ui| {
ui.spinner();
let status = self.shader_install_status.as_deref().unwrap_or(self.t("shader_downloading"));
ui.label(egui::RichText::new(status).color(egui::Color32::from_rgb(255, 225, 120)).strong());
ui.label(egui::RichText::new(status).color(egui::Color32::from_rgb(255, 225, 120)).size(13.0).strong());
});
} else {
let dl_btn_text = if has_full_cache {
@@ -3937,7 +4078,7 @@ impl LauncherApp {
draw_rect.center(),
egui::Align2::CENTER_CENTER,
dl_btn_text,
egui::FontId::proportional(13.0),
egui::FontId::proportional(13.5),
egui::Color32::WHITE,
);
@@ -3954,11 +4095,11 @@ impl LauncherApp {
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.launch_while_compiling_shaders);
let lbl = self.t("launch_while_compiling_shaders");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.launch_while_compiling_shaders = !self.config.launch_while_compiling_shaders;
}
});
ui.label(egui::RichText::new(self.t("launch_while_compiling_shaders_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("launch_while_compiling_shaders_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(10.0);
@@ -3966,11 +4107,11 @@ impl LauncherApp {
ui.horizontal(|ui| {
toggle_switch(ui, &mut self.config.show_shader_compile_rage_meter);
let lbl = self.t("show_shader_rage_meter");
if ui.add(egui::Label::new(egui::RichText::new(lbl).strong()).sense(egui::Sense::click())).clicked() {
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.show_shader_compile_rage_meter = !self.config.show_shader_compile_rage_meter;
}
});
ui.label(egui::RichText::new(self.t("show_shader_rage_meter_desc")).color(egui::Color32::LIGHT_GRAY).size(11.0));
ui.label(egui::RichText::new(self.t("show_shader_rage_meter_desc")).color(egui::Color32::from_rgb(190, 195, 210)).size(12.5));
ui.add_space(14.0);
// SHADER SCREEN PREVIEW INFO
@@ -3989,14 +4130,14 @@ impl LauncherApp {
ui.vertical(|ui| {
ui.label(
egui::RichText::new("⚔️ Spartan Rage Shader Yükleme Ekranı (Aktif)")
.size(13.0)
.size(13.5)
.color(egui::Color32::from_rgb(255, 215, 120))
.strong(),
);
ui.label(
egui::RichText::new("Oyun başlatılırken shader'lar önceden derlenir ve God of War III Spartan Rage barı yukarı doğru dolar. Derleme %100 bittiğinde oyun penceresi açılana kadar siyah ekranda bekler.")
.size(11.0)
.color(egui::Color32::from_rgb(200, 205, 220)),
.size(12.5)
.color(egui::Color32::from_rgb(205, 210, 225)),
);
});
} else {
@@ -4007,14 +4148,14 @@ impl LauncherApp {
ui.vertical(|ui| {
ui.label(
egui::RichText::new("🎮 PS4 Konsol Tarzı Açılış Ekranı (pic1.png)")
.size(13.0)
.size(13.5)
.color(egui::Color32::from_rgb(130, 205, 255))
.strong(),
);
ui.label(
egui::RichText::new("Shader derleme barları ve sayaçlar gizlenir. Oyun penceresi açılana kadar ekranda tam ekran nefes alan animasyonlu PS4 resmi (pic1.png) görüntülenir.")
.size(11.0)
.color(egui::Color32::from_rgb(200, 205, 220)),
egui::RichText::new("Shader derleme barları ve sayaçlar gizlenir. Oyun penceresi açılana kadar ekranda tam ekran pürüzsüz yaklaşan animasyonlu PS4 resmi (pic1.png) görüntülenir.")
.size(12.5)
.color(egui::Color32::from_rgb(205, 210, 225)),
);
});
}
@@ -4124,6 +4265,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));
@@ -4294,13 +4436,11 @@ impl eframe::App for LauncherApp {
// --- 7. SHADER LOADING SCREEN PROGRESS & ANIMATION ---
if let Some(ref mut sl) = self.shader_loading {
if sl.phase == ShaderLoadingPhase::Compiling {
ctx.request_repaint();
} else {
ctx.request_repaint_after(std::time::Duration::from_millis(500));
}
// Kesintisiz yüksek kare hızında (60-144+ FPS) pürüzsüz sinematik animasyon
ctx.request_repaint();
// Poll progress updates from monitor thread
let mut game_ready_now = false;
while let Ok(msg) = sl.rx.try_recv() {
if msg.total > 0 {
sl.total_shaders = msg.total;
@@ -4318,11 +4458,21 @@ impl eframe::App for LauncherApp {
sl.completed_at = Some(std::time::Instant::now());
}
}
if msg.game_window_ready {
game_ready_now = true;
}
}
let elapsed = sl.start_time.elapsed().as_secs_f32();
let pid = self.running_pid.load(Ordering::SeqCst);
// Auto-detect game window on Windows / Linux if rendered
if !game_ready_now && pid > 0 && elapsed > 2.0 {
if crate::win_input::find_game_hwnd(pid).is_some() {
game_ready_now = true;
}
}
// Auto-complete triggers if game engine is up and running or after warmup phase
if (pid > 0 && elapsed > 5.5) || elapsed > 8.5 {
sl.is_complete = true;
@@ -4362,22 +4512,57 @@ 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(self.base_dir.clone(), now, self.config.target_fps, is_modded, &self.config.language);
}
// Shader derleme ekranı kapanmaz, arkaplanda açık kalmaya devam eder (asla çökmüş gibi hissettirmez).
// Derleme bittiği için sistem zorlanmaz (500ms repaint).
// Oyun kapandığında veya çöktüğünde game_exit_rx otomatik olarak launcher ekranını normale döndürür.
// Oyun penceresi açıldığında oyun ekranını en öne getir ve launcher'ı Normal seviyeye çek
// Shader ekranı hemen kapanmaz veya görev çubuğuna küçülmez! Oyunun arkasında 10 saniye açık kalır.
if game_ready_now && sl.game_ready_at.is_none() {
sl.game_ready_at = Some(std::time::Instant::now());
ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel(egui::WindowLevel::Normal));
crate::win_input::bring_game_window_to_front(pid);
}
// Safety: allow user to press Escape to dismiss shader loading screen early
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
// Oyun açıldıktan 10 saniye sonra (veya algılanamazsa güvenlik zaman aşımı 25 saniye sonra)
// shader ekranı arka planda kapanıp standart launcher arayüzüne döner (görev çubuğuna küçülmez!)
let should_close = if let Some(ready_at) = sl.game_ready_at {
ready_at.elapsed().as_secs_f32() >= 10.0
} else {
elapsed >= 25.0
};
if should_close {
ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel(egui::WindowLevel::Normal));
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
#[cfg(target_os = "windows")]
{
ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(false));
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(1024.0, 576.0)));
}
self.shader_loading = None;
} else if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
// Güvenlik: Kullanıcı ESC tuşuna basarak ekranı erken kapatabilir
ctx.send_viewport_cmd(egui::ViewportCommand::WindowLevel(egui::WindowLevel::Normal));
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
#[cfg(target_os = "windows")]
{
ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(false));
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(1024.0, 576.0)));
}
self.shader_loading = None;
}
}
// If shader loading screen is active, render it full screen and return!
if self.shader_loading.is_some() {
#[cfg(target_os = "windows")]
{
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(true));
ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
}
self.render_shader_loading_screen(ui, &ctx);
return;
}
@@ -4418,10 +4603,11 @@ impl eframe::App for LauncherApp {
s.spacing.scroll.foreground_color = true;
});
// --- MAIN VIEW ROUTING: CONSOLE MAIN MENU OR SETTINGS SCREEN ---
// --- MAIN VIEW ROUTING: CONSOLE MAIN MENU, SETTINGS SCREEN OR TROPHIES ---
match self.view_mode {
ViewMode::MainMenu => self.render_console_main_menu(ui, &ctx),
ViewMode::Settings => self.render_settings_screen(ui, &ctx),
ViewMode::Trophies => self.render_trophies_screen(ui, &ctx),
}
// Dim background if any modal dialog is currently active
@@ -5349,6 +5535,28 @@ impl eframe::App for LauncherApp {
});
});
}
// --- PS4 STYLE TROPHY TOAST NOTIFICATION RENDERER ---
let toast_to_render = if let Ok(mut mgr) = self.trophy_manager.lock() {
mgr.advance_queue(self.current_lang.code());
mgr.active_toast.clone()
} else {
None
};
if let Some(ref toast) = toast_to_render {
crate::trophy::render_ps4_toast(ui, toast, self.current_lang.code(), false);
ctx.request_repaint();
}
}
}
impl Drop for LauncherApp {
fn drop(&mut self) {
self.discord_rpc.shutdown();
if let Some(ref watcher) = self.trophy_watcher {
watcher.stop();
}
}
}
+26 -4
View File
@@ -245,6 +245,8 @@ pub struct LauncherConfig {
#[serde(default = "default_true")]
pub skip_cinematics: bool,
#[serde(default = "default_true")]
pub bonus_play_fix: bool,
#[serde(default = "default_true")]
pub use_gamemode: bool,
#[serde(default = "default_vram_budget")]
pub vram_gc_budget_mb: u64,
@@ -294,10 +296,20 @@ 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 {
crate::discord_rpc::DISCORD_GOW3_APP_ID.to_string()
}
fn default_language() -> String {
"tr".to_string()
crate::discord_rpc::detect_system_language().to_string()
}
fn default_target_fps() -> u32 {
60
@@ -322,7 +334,7 @@ fn default_last_commit() -> String {
"c5638d9956002b8a9f09a240028f6793ddf7b2f8".to_string()
}
fn default_launcher_version() -> String {
"v2.0.2".to_string()
"v2.2".to_string()
}
fn default_render_resolution() -> String {
"native".to_string()
@@ -342,6 +354,7 @@ impl Default for LauncherConfig {
launch_while_compiling_shaders: false,
show_shader_compile_rage_meter: true,
skip_cinematics: true,
bonus_play_fix: true,
use_gamemode: true,
vram_gc_budget_mb: default_vram_budget(),
dmem_mb: default_dmem(),
@@ -361,6 +374,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(),
}
}
}
@@ -540,6 +555,10 @@ impl LauncherConfig {
cfg.keyboard_bindings = KeyboardBindings::recommended();
changed = true;
}
if cfg.discord_client_id != crate::discord_rpc::DISCORD_GOW3_APP_ID {
cfg.discord_client_id = crate::discord_rpc::DISCORD_GOW3_APP_ID.to_string();
changed = true;
}
if changed {
let _ = cfg.save(base_dir);
}
@@ -785,11 +804,14 @@ mod tests {
}
#[test]
fn test_launcher_version_is_v20() {
assert_eq!(default_launcher_version(), "v2.0.2");
fn test_launcher_version_is_v22() {
assert_eq!(default_launcher_version(), "v2.2");
let def = LauncherConfig::default();
assert!(def.async_readbacks);
assert!(def.patch_readbacks);
assert!(def.bonus_play_fix);
assert!(def.discord_rpc_enabled);
assert_eq!(def.discord_client_id, crate::discord_rpc::DISCORD_GOW3_APP_ID);
}
#[test]
File diff suppressed because it is too large. Load diff
+40 -12
View File
@@ -150,11 +150,18 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Turkish, "system_specs_title") => "SİSTEM ÖZELLİKLERİ",
(Language::Turkish, "play_btn_label") => "OYNA",
(Language::Turkish, "menu_play") => "OYNA",
(Language::Turkish, "menu_trophies") => "KUPALAR",
(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.2)",
(Language::Turkish, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.0.2 YENİLİKLERİ:\n\n• 🪟 Windows & Linux Eş Zamanlı Tam Sürüm (v2.0.2):\n - Windows sürümü (gow3-launcher-win.exe) modern konsol arayüzü ve mod yöneticisiyle tamamen güncellendi.\n - Linux ve Windows ikili dosyaları birebir senkronize edildi.\n\n• 🔄 Otomatik Güncelleme Tarama & Sonra Hatırlat:\n - Başlangıçta sessiz GitHub sürüm denetimi ve otomatik güncelleme bildirim penceresi.\n - 'Sonra Hatırlat' ve 'Şimdi Güncelle' seçenekleri ile oturum bazlı akıllı hatırlatma.\n - Ana menü ve ayarlara dinamik 'Güncelle' butonu.\n\n• 🛡️ Shader & Mod Yönetimi:\n - %100 kesintisiz Spartan Rage dolumu ve sarı çubuksuz temiz sinematik ekran.\n - Gelişmiş oyun içi mod desteği (CJ Mod, Golden Blades vb.).",
(Language::Turkish, "bonus_play_fix_toggle") => "Bonus Play QTE & Düşman Doğma Düzeltmesi",
(Language::Turkish, "bonus_play_fix_desc") => "Tanrısal Eşyalar (Godly Possessions / Hephaistos'un Yüzüğü) aktifken QTE sekanslarının erken tetiklenip düşman dalgalarını (arena kilitlenmelerini) ve tuş göstergelerini engellemesini önler.",
(Language::Turkish, "release_notes_title") => "God of War III Launcher - Sürüm Notları (v2.2)",
(Language::Turkish, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.2 YENİLİKLERİ:\n\n• 🏆 Gerçek Zamanlı Kupa (Trophy) ve PS4 Bildirim Sistemi:\n - Resmi 36 kupanın tamamı, yüksek çözünürlüklü ikonları ve Türkçe/İngilizce açıklamalarıyla entegre edildi.\n - PS4 Stili Sağ Üst Bildirim Hapı (Toast Notification): Kupa kazanıldığında sağdan kayarak gelir, 4 saniye ekranda kalır ve kararır.\n - Orijinal PS4 Kupa Kazanma Sesi (Trophy Pop SFX) milisaniyesinde çalar.\n - Sıralı Kuyruk Yönetimi (FIFO): Üst üste binen kupaları sıralı gösterir.\n - Kupa Merkezi (Dashboard): İlerleme çubuğu, %100 Platin Ustalık Mührü, filtreleme, kupa arama ve gizli kupa açma desteği.\n\n• 🗡️ Bonus Play & Tanrısal Eşyalar (Godly Possessions) Düzeltmesi:\n - Hephaistos'un Yüzüğü ve diğer bonus eşyalar aktifken QTE'lerin erken tetiklenip düşman dalgalarını engellemesi ve arena kilitlenmeleri düzeltildi.\n\n• 🎮 Kararlılık ve Performans İyileştirmeleri.",
(Language::Turkish, "discord_rpc_toggle") => "Discord Rich Presence (Oynuyor Durumu)",
(Language::Turkish, "discord_rpc_desc") => "Discord profilinizde 'God of War III: Remastered', bulunduğunuz bölüm, boss savaşları 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",
@@ -201,7 +208,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Turkish, "view_on_github") => "GitHub'da Görüntüle",
(Language::Turkish, "remind_later") => "⏰ Sonra Hatırlat",
(Language::Turkish, "update_available_badge") => "🚀 Güncelleme Mevcut!",
(Language::Turkish, "btn_update") => "🔄 Güncelle",
(Language::Turkish, "btn_update") => "Güncelle",
(Language::Turkish, "btn_update_now") => "🚀 Şimdi Güncelle",
(Language::Turkish, "updating_in_progress") => "⏳ Güncelleme indiriliyor ve kuruluyor... Lütfen bekleyin...",
(Language::Turkish, "python_dep") => "Python 3 Bağımlılığı:",
@@ -412,11 +419,18 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::English, "system_specs_title") => "SYSTEM SPECIFICATIONS",
(Language::English, "play_btn_label") => "PLAY",
(Language::English, "menu_play") => "PLAY",
(Language::English, "menu_trophies") => "TROPHIES",
(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.2)",
(Language::English, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.0.2 CHANGELOG:\n\n• 🪟 Synchronized Windows & Linux Release (v2.0.2):\n - Windows version (gow3-launcher-win.exe) rebuilt with full console UI and mod manager.\n - Linux and Windows binaries are fully in sync.\n\n• 🔄 Auto Version Checking & Remind Later:\n - Startup background GitHub release checks with automatic update prompt.\n - 'Remind Later' and 'Update Now' session-aware dismissals.\n - Dynamic 'Update' buttons in header and settings.\n\n• 🛡️ Shader & Mod Management:\n - Continuous 100% Spartan Rage fill without freezes or yellow beam.\n - In-game mod manager (CJ Mod, Golden Blades, etc.).",
(Language::English, "bonus_play_fix_toggle") => "Bonus Play QTE & Enemy Spawning Fix",
(Language::English, "bonus_play_fix_desc") => "Prevents Godly Possession bonus items (specifically Hephaestus' Ring) from auto-skipping QTEs prematurely, preventing missing enemy wave spawns and missing QTE prompts.",
(Language::English, "release_notes_title") => "God of War III Launcher - Release Notes (v2.2)",
(Language::English, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.2 CHANGELOG:\n\n• 🏆 Real-Time Trophy System & PS4 Notification Integration:\n - All 36 trophies tracked with official badges, unlock conditions, and local persistence (trophies_state.json).\n - PS4-Style Pill Toast Notification in the top-right corner with smooth slide-in, 4s hold, and soft fade-out.\n - Authentic PS4 Trophy Pop SFX played at the millisecond of unlocking.\n - Sequential Notification Queue (FIFO) for multiple unlocks.\n - Full Trophy Showcase & Dashboard: Progress bar, dynamic percentage, 100% Platinum Mastery Badge, search & filter.\n\n• 🗡️ Bonus Play & Godly Possessions Fix:\n - Fixed missing enemies and QTE softlocks when equipping Hephaestus' Ring and bonus items.\n\n• ⚡ Stability & performance enhancements.",
(Language::English, "discord_rpc_toggle") => "Discord Rich Presence (Activity Status)",
(Language::English, "discord_rpc_desc") => "Displays 'God of War III: Remastered', your active chapter, boss fights, and elapsed play time on Discord.",
(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",
@@ -463,7 +477,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::English, "view_on_github") => "View on GitHub",
(Language::English, "remind_later") => "⏰ Remind Me Later",
(Language::English, "update_available_badge") => "🚀 Update Available!",
(Language::English, "btn_update") => "🔄 Update",
(Language::English, "btn_update") => "Update",
(Language::English, "btn_update_now") => "🚀 Update Now",
(Language::English, "updating_in_progress") => "⏳ Downloading and installing update... Please wait...",
(Language::English, "python_dep") => "Python 3 Dependency:",
@@ -674,11 +688,18 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::German, "system_specs_title") => "SYSTEMSPEZIFIKATIONEN",
(Language::German, "play_btn_label") => "SPIELEN",
(Language::German, "menu_play") => "SPIELEN",
(Language::German, "menu_trophies") => "TROPHÄEN",
(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.2)",
(Language::German, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.0.2 NEUIGKEITEN:\n\n• 🪟 Synchronisierte Windows- & Linux-Version (v2.0.2):\n - Windows-Version (gow3-launcher-win.exe) mit Konsolen-UI und Mod-Manager neu erstellt.\n\n• 🔄 Automatische Versionsprüfung & Später erinnern:\n - Automatische GitHub-Aktualisierungsprüfung beim Start.\n - Intelligente 'Später erinnern' und 'Jetzt aktualisieren' Funktionen.\n\n• 🛡️ Shader & Mod-Verwaltung optimiert.",
(Language::German, "bonus_play_fix_toggle") => "Bonus Play QTE & Gegner-Spawn-Korrektur",
(Language::German, "bonus_play_fix_desc") => "Verhindert, dass göttliche Besitztümer (Hephaistos' Ring) QTEs vorzeitig überspringen und dadurch das Erscheinen von Gegnerwellen blockieren.",
(Language::German, "release_notes_title") => "God of War III Launcher - Versionshinweise (v2.2)",
(Language::German, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.2 NEUIGKEITEN:\n\n• 🏆 Echzeit-Trophäensystem & PS4-Benachrichtigung:\n - Alle 36 Trophäen mit Originalgrafiken, Benachrichtigungskapsel und PS4-Soundeffekt.\n - Trophäen-Showcase mit Fortschrittsanzeige und 100%-Platin-Abzeichen.\n\n• 🗡️ Bonus-Gegenstände Bugfix (QTE & Gegner-Spawn-Korrektur).",
(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",
@@ -723,7 +744,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::German, "view_on_github") => "Auf GitHub ansehen",
(Language::German, "remind_later") => "⏰ Später erinnern",
(Language::German, "update_available_badge") => "🚀 Update verfügbar!",
(Language::German, "btn_update") => "🔄 Aktualisieren",
(Language::German, "btn_update") => "Aktualisieren",
(Language::German, "btn_update_now") => "🚀 Jetzt aktualisieren",
(Language::German, "updating_in_progress") => "⏳ Update wird heruntergeladen und installiert... Bitte warten...",
(Language::German, "python_dep") => "Python 3 Abhängigkeit:",
@@ -934,11 +955,18 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Spanish, "system_specs_title") => "ESPECIFICACIONES DEL SISTEMA",
(Language::Spanish, "play_btn_label") => "JUGAR",
(Language::Spanish, "menu_play") => "JUGAR",
(Language::Spanish, "menu_trophies") => "TROFEOS",
(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.2)",
(Language::Spanish, "release_notes_content") => "🔥 NOVEDADES DE GOD OF WAR III LAUNCHER v2.0.2:\n\n• 🪟 Versión sincronizada para Windows y Linux (v2.0.2):\n - Versión de Windows (gow3-launcher-win.exe) reconstruida con interfaz de consola y gestor de mods.\n\n• 🔄 Detección automática de actualizaciones & Recordar más tarde:\n - Comprobación automática en segundo plano al iniciar.\n - Botones inteligentes de 'Recordar más tarde' y 'Actualizar ahora'.\n\n• 🛡️ Gestión avanzada de Shaders y Mods.",
(Language::Spanish, "bonus_play_fix_toggle") => "Corrección de QTE y Enemigos en Bonus Play",
(Language::Spanish, "bonus_play_fix_desc") => "Evita que las Posesiones Divinas (Anillo de Hefesto) omitan los QTEs prematuramente y bloqueen la aparición de oleadas de enemigos.",
(Language::Spanish, "release_notes_title") => "God of War III Launcher - Notas de la Versión (v2.2)",
(Language::Spanish, "release_notes_content") => "🔥 NOVEDADES DE GOD OF WAR III LAUNCHER v2.2:\n\n• 🏆 Sistema de Trofeos en Tiempo Real y Notificaciones PS4:\n - Los 36 trofeos oficiales con insignias en alta resolución y efectos de sonido de PS4.\n - Notificaciones flotantes estilo cápsula PS4 y panel de trofeos con medalla de maestría al 100%.\n\n• 🗡️ Corrección de errores en objetos de Bonus Play (QTE y generación de enemigos).",
(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",
@@ -983,7 +1011,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Spanish, "view_on_github") => "Ver en GitHub",
(Language::Spanish, "remind_later") => "⏰ Recordármelo más tarde",
(Language::Spanish, "update_available_badge") => "🚀 ¡Actualización disponible!",
(Language::Spanish, "btn_update") => "🔄 Actualizar",
(Language::Spanish, "btn_update") => "Actualizar",
(Language::Spanish, "btn_update_now") => "🚀 Actualizar ahora",
(Language::Spanish, "updating_in_progress") => "⏳ Descargando e instalando actualización... Por favor espere...",
(Language::Spanish, "python_dep") => "Dependencia Python 3:",
+3
View File
@@ -10,6 +10,9 @@ mod tuner;
mod updater;
mod shader_downloader;
pub mod mods_manager;
pub mod discord_rpc;
pub mod win_input;
pub mod trophy;
use app::LauncherApp;
use eframe::egui;
+319 -5
View File
@@ -70,9 +70,27 @@ pub fn ensure_runtime_assets(base_dir: &Path) -> Result<(PathBuf, PathBuf), Stri
let _ = fs::write(&patch_xml, PATCH_GOW3_XML);
}
ensure_game_icon_bmp(base_dir);
Ok((scripts_dir, patches_dir))
}
/// Ensures gow3.bmp is available for SDL3 window icon loading.
pub fn ensure_game_icon_bmp(base_dir: &Path) {
let bmp_path = base_dir.join("gow3.bmp");
if !bmp_path.is_file() {
let png_path = base_dir.join("gow3.png");
let img = if png_path.is_file() {
image::open(&png_path).ok()
} else {
image::load_from_memory(include_bytes!("../../gow3.png")).ok()
};
if let Some(img) = img {
let _ = img.save_with_format(&bmp_path, image::ImageFormat::Bmp);
}
}
}
/// Finds an available Python 3 interpreter (checking local runtime bundle first, then system PATH)
pub fn find_python(base_dir: &Path) -> Result<String, String> {
// 1. Check local runtime python folder (.runtime/python/python.exe or bin/python3)
@@ -330,6 +348,207 @@ fn collect_xml_files(dir: &Path, xml_paths: &mut Vec<PathBuf>) {
}
}
pub fn build_rust_mod_overlay(
game_dir: &Path,
out_dir: &Path,
mods_dir: &Path,
config: &LauncherConfig,
) -> Option<PathBuf> {
if !game_dir.is_dir() || !mods_dir.is_dir() {
return None;
}
let mut enabled_mods = Vec::new();
if let Ok(entries) = fs::read_dir(mods_dir) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.')
|| name.eq_ignore_ascii_case("gow3-windows")
|| name.eq_ignore_ascii_case("gow3-linux")
|| path.join("run.py").is_file()
{
continue;
}
if !config.disabled_mods.contains(&name) {
enabled_mods.push((name, path));
}
}
}
if enabled_mods.is_empty() {
return None;
}
enabled_mods.sort_by(|a, b| a.0.cmp(&b.0));
let overlay_dir = out_dir.join("mod-game-rust-overlay");
let _ = fs::remove_dir_all(&overlay_dir);
if fs::create_dir_all(&overlay_dir).is_err() {
return None;
}
fn link_or_copy_dir(target: &Path, link: &Path) -> bool {
#[cfg(unix)]
{
if std::os::unix::fs::symlink(target, link).is_ok() {
return true;
}
}
#[cfg(windows)]
{
if std::os::windows::fs::symlink_dir(target, link).is_ok() {
return true;
}
use std::os::windows::process::CommandExt;
let status = Command::new("cmd")
.args(["/C", "mklink", "/J"])
.arg(link)
.arg(target)
.creation_flags(0x08000000) // CREATE_NO_WINDOW
.status();
if let Ok(st) = status {
if st.success() && link.is_dir() {
return true;
}
}
}
// Fallback: create directory and link/copy contents
if fs::create_dir_all(link).is_ok() {
if let Ok(entries) = fs::read_dir(target) {
for entry in entries.flatten() {
let entry_target = entry.path();
let entry_link = link.join(entry.file_name());
if entry_target.is_dir() {
let _ = link_or_copy_dir(&entry_target, &entry_link);
} else {
let _ = link_or_copy_file(&entry_target, &entry_link);
}
}
return true;
}
}
false
}
fn link_or_copy_file(target: &Path, link: &Path) -> bool {
#[cfg(unix)]
{
if std::os::unix::fs::symlink(target, link).is_ok() {
return true;
}
}
if fs::hard_link(target, link).is_ok() {
return true;
}
fs::copy(target, link).is_ok()
}
// Link or copy base game root entries into overlay
if let Ok(entries) = fs::read_dir(game_dir) {
for entry in entries.flatten() {
let target = entry.path();
let link = overlay_dir.join(entry.file_name());
if target.is_dir() {
let _ = link_or_copy_dir(&target, &link);
} else {
let _ = link_or_copy_file(&target, &link);
}
}
}
fn find_content_root(mod_path: &Path) -> Option<PathBuf> {
let candidates = [
mod_path.join("dvdroot_ps4"),
mod_path.join("app0/dvdroot_ps4"),
mod_path.join("Game/EXEC"),
mod_path.join("EXEC"),
];
for c in candidates {
if c.is_dir() {
return Some(c);
}
}
None
}
fn apply_mod_dir(src: &Path, dst: &Path, base_game_src: &Path, count: &mut usize) {
if let Ok(entries) = fs::read_dir(src) {
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let dest_path = dst.join(&name);
let base_child = base_game_src.join(&name);
if path.is_dir() {
let is_link = dest_path.is_symlink();
if is_link {
let _ = fs::remove_file(&dest_path);
#[cfg(windows)]
let _ = fs::remove_dir(&dest_path);
let _ = fs::create_dir_all(&dest_path);
if base_child.is_dir() {
if let Ok(base_entries) = fs::read_dir(&base_child) {
for be in base_entries.flatten() {
let bet = be.path();
let bel = dest_path.join(be.file_name());
if bet.is_dir() {
let _ = link_or_copy_dir(&bet, &bel);
} else {
let _ = link_or_copy_file(&bet, &bel);
}
}
}
}
} else if !dest_path.is_dir() {
let _ = fs::create_dir_all(&dest_path);
}
apply_mod_dir(&path, &dest_path, &base_child, count);
} else if path.is_file() {
if path.extension().map(|e| e.eq_ignore_ascii_case("xml")).unwrap_or(false) {
continue;
}
if dest_path.is_symlink() || dest_path.is_file() {
let _ = fs::remove_file(&dest_path);
}
if fs::copy(&path, &dest_path).is_ok() {
*count += 1;
}
}
}
}
}
let mut applied = 0;
for (_name, path) in enabled_mods {
if let Some(root) = find_content_root(&path) {
apply_mod_dir(&root, &overlay_dir, game_dir, &mut applied);
}
}
// Safety validation: Ensure eboot.bin and EXEC directory are present in overlay.
// If missing, NEVER return a broken directory to avoid black screen on game launch!
if !overlay_dir.join("eboot.bin").is_file() || !overlay_dir.join("EXEC").is_dir() {
crate::log_error!(
"Rust mod overlay validation failed (missing eboot.bin or EXEC). Falling back to original game_dir."
);
let _ = fs::remove_dir_all(&overlay_dir);
return None;
}
if applied > 0 {
crate::log_info!("Rust mod overlay successfully built with {} modified files", applied);
Some(overlay_dir)
} else {
None
}
}
#[allow(dead_code)]
pub fn download_windows_engine(base_dir: &Path, is_alternative: bool) -> Result<String, String> {
let dest_bin = base_dir.join("bin");
@@ -764,6 +983,7 @@ pub fn adjust_windows_windowed_mode(target_pid: u32) {
unsafe extern "system" {
fn GetWindowThreadProcessId(hwnd: *mut c_void, lpdwProcessId: *mut u32) -> u32;
fn GetWindowTextA(hwnd: *mut c_void, lp_string: *mut u8, n_max_count: i32) -> i32;
fn EnumWindows(lpEnumFunc: unsafe extern "system" fn(*mut c_void, isize) -> i32, lParam: isize) -> i32;
fn IsWindowVisible(hwnd: *mut c_void) -> i32;
fn GetWindowLongPtrA(hWnd: *mut c_void, nIndex: i32) -> isize;
@@ -779,7 +999,15 @@ pub fn adjust_windows_windowed_mode(target_pid: u32) {
let ctx = &mut *(lparam as *mut EnumContext);
let mut proc_id = 0u32;
GetWindowThreadProcessId(hwnd, &mut proc_id);
if proc_id == ctx.pid && IsWindowVisible(hwnd) != 0 {
if proc_id != std::process::id() && proc_id == ctx.pid && IsWindowVisible(hwnd) != 0 {
let mut title_buf = [0u8; 128];
let len = GetWindowTextA(hwnd, title_buf.as_mut_ptr(), 127);
if len > 0 {
let title = String::from_utf8_lossy(&title_buf[..len as usize]);
if title.contains("Launcher") || title.contains("gow3-launcher") {
return 1;
}
}
ctx.hwnd = hwnd;
return 0;
}
@@ -865,6 +1093,9 @@ pub fn launch_gow3(
running_pid: Option<Arc<AtomicU32>>,
) -> Result<i32, String> {
crate::log_info!("Initiating God of War III launch sequence...");
// Synchronize configuration files (gow3.ini, user/config.toml, etc.) before launch
let _ = crate::tuner::apply_settings(base_dir, config);
let game_dir = Path::new(&config.game_dir);
if !game_dir.join("eboot.bin").is_file() {
let msg = format!("Oyun klasöründe ('{}') 'eboot.bin' bulunamadı!", game_dir.display());
@@ -935,6 +1166,7 @@ pub fn launch_gow3(
}
let mut effective_app0 = game_dir.to_path_buf();
let mut mods_applied = false;
if let Ok(output) = mods_cmd
.env("PYTHONPATH", &scripts_path)
@@ -953,11 +1185,20 @@ pub fn launch_gow3(
if candidate.is_dir() {
crate::log_info!("Mods active. Mounting overlay directory: {:?}", candidate);
effective_app0 = candidate;
mods_applied = true;
}
}
}
}
if !mods_applied && mods_dir.is_dir() {
if let Some(rust_overlay) = build_rust_mod_overlay(game_dir, &out_dir, &mods_dir, config) {
crate::log_info!("Rust mod overlay active (fallback). Mounting overlay: {:?}", rust_overlay);
effective_app0 = rust_overlay;
mods_applied = true;
}
}
// 2. Run prepare.py (ELF parsing and analysis)
let status = Command::new(&python)
.arg(scripts_path.join("prepare.py"))
@@ -1072,12 +1313,30 @@ pub fn launch_gow3(
patch_list.push("Bug Fix - Startup Black Screen".to_string());
patch_list.push("Bug Fix - Intro Crash Fix".to_string());
patch_list.push("Bug Fix - Trophy Unlock Fix".to_string());
if config.bonus_play_fix {
patch_list.push("Bug Fix - Bonus Play QTE and Enemy Spawning Fix".to_string());
}
if config.skip_cinematics {
patch_list.push("Skip Any Video With X Button".to_string());
}
if config.patch_delta_time {
patch_list.push(format!("Frame Rate Patch - {} FPS", config.target_fps));
let fps_patch = match config.target_fps {
30 => Some("Frame Rate Patch - 30 FPS"),
40 => Some("Frame Rate Patch - 40 FPS"),
60 => Some("Frame Rate Patch - 60 FPS"),
90 => Some("Frame Rate Patch - 90 FPS"),
120 => Some("Frame Rate Patch - 120 FPS"),
144 => Some("Frame Rate Patch - 144 FPS"),
165 => Some("Frame Rate Patch - 165 FPS"),
240 => Some("Frame Rate Patch - 240 FPS"),
_ => None,
};
if let Some(p) = fps_patch {
patch_list.push(p.to_string());
}
}
if config.disable_motion_blur {
patch_list.push("Graphics - Disable Motion Blur".to_string());
@@ -1352,18 +1611,41 @@ pub fn launch_gow3(
cmd.env("GOW3_PRESENT_THREAD", "1");
cmd.env("GOW3_ASYNC_FENCES", "1");
cmd.env("GOW3_ASYNC_SHADERS", "1");
cmd.env("GOW3_FPS_LIMIT", config.target_fps.to_string());
let target_fps_str = if config.target_fps > 0 {
config.target_fps.to_string()
} else {
"0".to_string()
};
cmd.env("GOW3_FPS_LIMIT", &target_fps_str);
cmd.env("GOW3_ENGINE_FPS", if config.target_fps > 0 { &target_fps_str } else { "60" });
cmd.env("GOW3_DEFAULT_ENGINE_FPS", if config.target_fps > 0 { &target_fps_str } else { "60" });
cmd.env("GOW3_VBLANK_HZ", if config.target_fps > 0 { &target_fps_str } else { "60" });
cmd.env("GOW3_VSYNC", if config.vsync { "1" } else { "0" });
let effective_budget = config.get_effective_vram_budget();
cmd.env("GOW3_GC_BUDGET_MB", effective_budget.to_string());
cmd.env("GOW3_DMEM_POOL_MB", config.dmem_mb.to_string());
cmd.env("GOW3_DMEM_MB", config.dmem_mb.to_string());
cmd.env("GOW3_DEFAULT_ENGINE_FPS", config.target_fps.to_string());
cmd.env("GOW3_VBLANK_HZ", config.target_fps.to_string());
cmd.env("GOW3_PATCHES", &patches_str);
cmd.env("GOW3_PATCHES_ONLY", "1");
cmd.env("GOW3_CONFIG", &config_ini);
cmd.env("GOW3_GAME_DIR", game_dir);
if mods_applied {
cmd.env("GOW3_MODS_DIR", &effective_app0);
} else {
cmd.env_remove("GOW3_MODS_DIR");
}
cmd.env("GOW3_MOD_TRACE", "1");
// Native SDL3 window & taskbar icon for gow3-probe
let icon_bmp = base_dir.join("gow3.bmp");
let icon_png = base_dir.join("gow3.png");
if icon_bmp.is_file() {
cmd.env("GOW3_WINDOW_ICON", &icon_bmp);
} else if icon_png.is_file() {
cmd.env("GOW3_WINDOW_ICON", &icon_png);
}
cmd.env(
"GOW3_FULLSCREEN",
if config.display_mode != crate::config::DisplayMode::Windowed {
@@ -1437,6 +1719,10 @@ pub fn launch_gow3(
pid_holder.store(child_pid, Ordering::SeqCst);
}
// Windows Input Bridge: Install low-level mouse hook & 1ms timer resolution while the game is running
#[cfg(target_os = "windows")]
let _win_input_guard = crate::win_input::start_mouse_hook(child_pid, config.hide_mouse_cursor);
#[cfg(target_os = "windows")]
if config.display_mode == crate::config::DisplayMode::Windowed {
adjust_windows_windowed_mode(child_pid);
@@ -1502,4 +1788,32 @@ mod tests {
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_build_rust_mod_overlay() {
let temp_dir = std::env::temp_dir().join("gow3_test_rust_mod_overlay");
let _ = fs::remove_dir_all(&temp_dir);
let game_dir = temp_dir.join("CUSA01715");
let out_dir = temp_dir.join("out");
let mods_dir = temp_dir.join("mods");
fs::create_dir_all(game_dir.join("EXEC/WAD")).unwrap();
fs::write(game_dir.join("EXEC/WAD/ORIG.WAD"), b"ORIG").unwrap();
fs::write(game_dir.join("eboot.bin"), b"EBOOT").unwrap();
let mod_a = mods_dir.join("ModA/dvdroot_ps4/EXEC/WAD");
fs::create_dir_all(&mod_a).unwrap();
fs::write(mod_a.join("ORIG.WAD"), b"MODDED").unwrap();
let config = LauncherConfig::default();
let overlay = build_rust_mod_overlay(&game_dir, &out_dir, &mods_dir, &config);
assert!(overlay.is_some());
let ov = overlay.unwrap();
let modded_file = ov.join("EXEC/WAD/ORIG.WAD");
assert!(modded_file.is_file());
assert_eq!(fs::read(&modded_file).unwrap(), b"MODDED");
let _ = fs::remove_dir_all(&temp_dir);
}
}
File diff suppressed because it is too large. Load diff
+11 -2
View File
@@ -34,15 +34,16 @@ fn update_xml_patches(base_dir: &Path, target_hz: u32) -> Result<(), String> {
if let Ok(content) = fs::read_to_string(path) {
let mut new_lines = Vec::new();
for line in content.lines() {
let low = line.to_lowercase();
if (line.contains("Delta-Time")
|| line.contains("120 FPS")
|| line.contains("144 FPS")
|| line.contains("165 FPS")
|| line.contains("240 FPS")
|| line.contains("60 FPS"))
&& line.contains("value=\"")
&& low.contains("value=\"")
{
if let Some(start) = line.find("value=\"") {
if let Some(start) = low.find("value=\"") {
let prefix = &line[..start + 7];
if let Some(end) = line[start + 7..].find('\"') {
let suffix = &line[start + 7 + end..];
@@ -218,6 +219,14 @@ fn update_run_sh(base_dir: &Path, config: &LauncherConfig) -> Result<(), String>
if config.background_controller_input { "1" } else { "0" },
);
let icon_bmp = base_dir.join("gow3.bmp");
let icon_png = base_dir.join("gow3.png");
if icon_bmp.is_file() {
replace_or_insert_env(&mut content, "GOW3_WINDOW_ICON", &icon_bmp.to_string_lossy());
} else if icon_png.is_file() {
replace_or_insert_env(&mut content, "GOW3_WINDOW_ICON", &icon_png.to_string_lossy());
}
fs::write(&run_sh, content).map_err(|e| e.to_string())?;
Ok(())
}
+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.2";
pub const BUILTIN_VERSION: &str = "v2.2";
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.3", "v2.0.2"), std::cmp::Ordering::Greater);
assert_eq!(compare_version_strings("v2.0.2", "v2.0.2"), std::cmp::Ordering::Equal);
assert_eq!(compare_version_strings("v2.0.1", "v2.0.2"), 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]
+354
View File
@@ -0,0 +1,354 @@
// SPDX-License-Identifier: MIT
// God of War III Remastered - Windows Input Bridge & Window Manager
#[cfg(target_os = "windows")]
mod imp {
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::thread;
use std::time::Duration;
static HOOK_RUNNING: AtomicBool = AtomicBool::new(false);
static GAME_PID: AtomicU32 = AtomicU32::new(0);
static HIDE_CURSOR_ENABLED: AtomicBool = AtomicBool::new(false);
static HOOK_HANDLE: std::sync::atomic::AtomicIsize = std::sync::atomic::AtomicIsize::new(0);
type HWND = *mut c_void;
type HHOOK = isize;
type HINSTANCE = isize;
type LRESULT = isize;
type WPARAM = usize;
type LPARAM = isize;
type HOOKPROC = unsafe extern "system" fn(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT;
const WH_MOUSE_LL: i32 = 14;
const WM_LBUTTONDOWN: usize = 0x0201;
const WM_LBUTTONUP: usize = 0x0202;
const WM_RBUTTONDOWN: usize = 0x0204;
const WM_RBUTTONUP: usize = 0x0205;
const WM_MBUTTONDOWN: usize = 0x0207;
const WM_MBUTTONUP: usize = 0x0208;
const WM_MOUSEWHEEL: usize = 0x020A;
const WM_XBUTTONDOWN: usize = 0x020B;
const WM_XBUTTONUP: usize = 0x020C;
const WM_QUIT: u32 = 0x0012;
const VK_F13: u8 = 0x7C;
const VK_F14: u8 = 0x7D;
const VK_F15: u8 = 0x7E;
const VK_F16: u8 = 0x7F;
const VK_F17: u8 = 0x80;
const VK_F18: u8 = 0x81;
const VK_F19: u8 = 0x82;
// Direct Windows hardware scan codes for F13-F19 mapped by SDL3's windows_scancode_table:
// 0x64 -> SDL_SCANCODE_F13 (Square / Light Attack)
// 0x65 -> SDL_SCANCODE_F14 (Triangle / Heavy Attack)
// 0x66 -> SDL_SCANCODE_F15 (Circle / Grab)
// 0x67 -> SDL_SCANCODE_F16 (Cross / Jump)
// 0x68 -> SDL_SCANCODE_F17
// 0x69 -> SDL_SCANCODE_F18
// 0x6A -> SDL_SCANCODE_F19
const SCAN_F13: u8 = 0x64;
const SCAN_F14: u8 = 0x65;
const SCAN_F15: u8 = 0x66;
const SCAN_F16: u8 = 0x67;
const SCAN_F17: u8 = 0x68;
const SCAN_F18: u8 = 0x69;
const SCAN_F19: u8 = 0x6A;
const KEYEVENTF_KEYUP: u32 = 0x0002;
const SW_SHOW: i32 = 5;
const GA_ROOT: u32 = 2;
#[repr(C)]
#[derive(Clone, Copy)]
struct POINT {
x: i32,
y: i32,
}
#[repr(C)]
struct MSLLHOOKSTRUCT {
pt: POINT,
mouse_data: u32,
flags: u32,
time: u32,
extra_info: usize,
}
#[repr(C)]
struct MSG {
hwnd: HWND,
message: u32,
w_param: WPARAM,
l_param: LPARAM,
time: u32,
pt: POINT,
}
#[link(name = "user32")]
#[link(name = "winmm")]
unsafe extern "system" {
fn SetWindowsHookExA(id_hook: i32, lpfn: HOOKPROC, h_mod: HINSTANCE, dw_thread_id: u32) -> HHOOK;
fn UnhookWindowsHookEx(hhk: HHOOK) -> i32;
fn CallNextHookEx(hhk: HHOOK, n_code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT;
fn GetForegroundWindow() -> HWND;
fn GetWindowThreadProcessId(hwnd: HWND, lpdw_process_id: *mut u32) -> u32;
fn GetWindowTextA(hwnd: HWND, lp_string: *mut u8, n_max_count: i32) -> i32;
fn keybd_event(b_vk: u8, b_scan: u8, dw_flags: u32, dw_extra_info: usize);
fn GetMessageA(lp_msg: *mut MSG, hwnd: HWND, w_msg_filter_min: u32, w_msg_filter_max: u32) -> i32;
fn PostThreadMessageA(id_thread: u32, msg: u32, w_param: WPARAM, l_param: LPARAM) -> i32;
fn GetCurrentThreadId() -> u32;
fn ShowWindow(hwnd: HWND, n_cmd_show: i32) -> i32;
fn SetForegroundWindow(hwnd: HWND) -> i32;
fn BringWindowToTop(hwnd: HWND) -> i32;
fn EnumWindows(lp_enum_func: unsafe extern "system" fn(hwnd: HWND, lparam: LPARAM) -> i32, lparam: LPARAM) -> i32;
fn IsWindowVisible(hwnd: HWND) -> i32;
fn timeBeginPeriod(u_period: u32) -> u32;
fn timeEndPeriod(u_period: u32) -> u32;
fn WindowFromPoint(point: POINT) -> HWND;
fn GetAncestor(hwnd: HWND, ga_flags: u32) -> HWND;
fn AttachThreadInput(id_attach: u32, id_attach_to: u32, f_attach: i32) -> i32;
fn PlaySoundA(psz_sound: *const u8, hmod: HINSTANCE, fdw_sound: u32) -> i32;
}
pub fn play_sound_memory(bytes: &[u8]) {
unsafe {
// SND_ASYNC = 0x0001, SND_NODEFAULT = 0x0002, SND_MEMORY = 0x0004
PlaySoundA(bytes.as_ptr(), 0, 0x0001 | 0x0002 | 0x0004);
}
}
unsafe fn is_game_window(hwnd: HWND) -> bool {
if hwnd.is_null() {
return false;
}
let my_pid = std::process::id();
let expected_pid = GAME_PID.load(Ordering::SeqCst);
let mut win_pid: u32 = 0;
unsafe {
GetWindowThreadProcessId(hwnd, &mut win_pid);
}
if win_pid == my_pid {
return false; // Asla launcher penceresiyle eşleşme!
}
if expected_pid > 0 && win_pid == expected_pid {
return true;
}
let mut title_buf = [0u8; 128];
let len = unsafe { GetWindowTextA(hwnd, title_buf.as_mut_ptr(), 127) };
if len > 0 {
let title = String::from_utf8_lossy(&title_buf[..len as usize]);
if title.contains("Launcher") || title.contains("gow3-launcher") {
return false;
}
if title.contains("God of War") || title.contains("gow3") {
return true;
}
}
false
}
unsafe extern "system" fn low_level_mouse_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
if code >= 0 {
unsafe {
let fg = GetForegroundWindow();
let p_mouse = &*(lparam as *const MSLLHOOKSTRUCT);
let hwnd_pt = WindowFromPoint(p_mouse.pt);
let is_fg = is_game_window(fg);
let is_pt = is_game_window(hwnd_pt) || is_game_window(GetAncestor(hwnd_pt, GA_ROOT));
if is_fg || is_pt {
match wparam {
WM_LBUTTONDOWN => {
keybd_event(VK_F13, SCAN_F13, 0, 0);
}
WM_LBUTTONUP => {
keybd_event(VK_F13, SCAN_F13, KEYEVENTF_KEYUP, 0);
}
WM_RBUTTONDOWN => {
keybd_event(VK_F14, SCAN_F14, 0, 0);
}
WM_RBUTTONUP => {
keybd_event(VK_F14, SCAN_F14, KEYEVENTF_KEYUP, 0);
}
WM_MBUTTONDOWN => {
keybd_event(VK_F15, SCAN_F15, 0, 0);
}
WM_MBUTTONUP => {
keybd_event(VK_F15, SCAN_F15, KEYEVENTF_KEYUP, 0);
}
WM_XBUTTONDOWN => {
let xbtn = (p_mouse.mouse_data >> 16) as u16;
if xbtn == 1 {
keybd_event(VK_F16, SCAN_F16, 0, 0);
} else if xbtn == 2 {
keybd_event(VK_F17, SCAN_F17, 0, 0);
}
}
WM_XBUTTONUP => {
let xbtn = (p_mouse.mouse_data >> 16) as u16;
if xbtn == 1 {
keybd_event(VK_F16, SCAN_F16, KEYEVENTF_KEYUP, 0);
} else if xbtn == 2 {
keybd_event(VK_F17, SCAN_F17, KEYEVENTF_KEYUP, 0);
}
}
WM_MOUSEWHEEL => {
let delta = (p_mouse.mouse_data >> 16) as i16;
if delta > 0 {
keybd_event(VK_F18, SCAN_F18, 0, 0);
keybd_event(VK_F18, SCAN_F18, KEYEVENTF_KEYUP, 0);
} else if delta < 0 {
keybd_event(VK_F19, SCAN_F19, 0, 0);
keybd_event(VK_F19, SCAN_F19, KEYEVENTF_KEYUP, 0);
}
}
_ => {}
}
}
}
}
let hhk = HOOK_HANDLE.load(Ordering::SeqCst);
unsafe { CallNextHookEx(hhk, code, wparam, lparam) }
}
struct EnumParam {
target_pid: u32,
found_hwnd: HWND,
}
unsafe extern "system" fn enum_win_proc(hwnd: HWND, lparam: LPARAM) -> i32 {
let p = unsafe { &mut *(lparam as *mut EnumParam) };
let mut pid: u32 = 0;
unsafe {
GetWindowThreadProcessId(hwnd, &mut pid);
if pid == std::process::id() {
return 1; // Launcher penceresini asla hedef alma!
}
if IsWindowVisible(hwnd) != 0 {
if p.target_pid > 0 && pid == p.target_pid {
p.found_hwnd = hwnd;
return 0; // Hedef oyun PID'sine ait görünür pencere bulundu!
} else if p.target_pid == 0 && is_game_window(hwnd) {
p.found_hwnd = hwnd;
return 0;
}
}
}
1 // continue
}
pub fn find_game_hwnd(pid: u32) -> Option<HWND> {
let mut param = EnumParam {
target_pid: pid,
found_hwnd: std::ptr::null_mut(),
};
unsafe {
EnumWindows(enum_win_proc, &mut param as *mut _ as LPARAM);
}
if !param.found_hwnd.is_null() {
Some(param.found_hwnd)
} else {
None
}
}
pub fn bring_game_window_to_front(pid: u32) {
if let Some(hwnd) = find_game_hwnd(pid) {
unsafe {
let cur_fg = GetForegroundWindow();
let cur_thread = GetCurrentThreadId();
let mut fg_pid = 0u32;
let fg_thread = GetWindowThreadProcessId(cur_fg, &mut fg_pid);
if fg_thread != 0 && cur_thread != fg_thread {
AttachThreadInput(cur_thread, fg_thread, 1);
}
ShowWindow(hwnd, SW_SHOW);
BringWindowToTop(hwnd);
SetForegroundWindow(hwnd);
if fg_thread != 0 && cur_thread != fg_thread {
AttachThreadInput(cur_thread, fg_thread, 0);
}
}
}
}
pub struct WinInputGuard {
thread_id: u32,
}
impl Drop for WinInputGuard {
fn drop(&mut self) {
HOOK_RUNNING.store(false, Ordering::SeqCst);
if self.thread_id != 0 {
unsafe {
PostThreadMessageA(self.thread_id, WM_QUIT, 0, 0);
timeEndPeriod(1);
}
}
}
}
pub fn start_mouse_hook(pid: u32, hide_cursor: bool) -> WinInputGuard {
GAME_PID.store(pid, Ordering::SeqCst);
HIDE_CURSOR_ENABLED.store(hide_cursor, Ordering::SeqCst);
HOOK_RUNNING.store(true, Ordering::SeqCst);
let (tx, rx) = std::sync::mpsc::channel();
thread::spawn(move || {
unsafe {
timeBeginPeriod(1);
let tid = GetCurrentThreadId();
let hook = SetWindowsHookExA(WH_MOUSE_LL, low_level_mouse_proc, 0, 0);
HOOK_HANDLE.store(hook, Ordering::SeqCst);
let _ = tx.send(tid);
if hook != 0 {
let mut msg: MSG = std::mem::zeroed();
while HOOK_RUNNING.load(Ordering::SeqCst) && GetMessageA(&mut msg, std::ptr::null_mut(), 0, 0) > 0 {
// Hook message loop
}
UnhookWindowsHookEx(hook);
HOOK_HANDLE.store(0, Ordering::SeqCst);
}
timeEndPeriod(1);
}
});
let thread_id = rx.recv_timeout(Duration::from_millis(500)).unwrap_or(0);
WinInputGuard { thread_id }
}
}
#[cfg(target_os = "windows")]
pub use imp::*;
#[cfg(not(target_os = "windows"))]
mod stub {
pub struct WinInputGuard;
pub fn start_mouse_hook(_pid: u32, _hide_cursor: bool) -> WinInputGuard {
WinInputGuard
}
pub fn bring_game_window_to_front(pid: u32) {
if pid > 0 {
let _ = std::process::Command::new("hyprctl")
.args(["dispatch", "focuswindow", &format!("pid:{}", pid)])
.output();
let _ = std::process::Command::new("xdotool")
.args(["search", "--pid", &pid.to_string(), "windowactivate"])
.output();
}
}
pub fn find_game_hwnd(_pid: u32) -> Option<isize> {
None
}
pub fn play_sound_memory(_bytes: &[u8]) {}
}
#[cfg(not(target_os = "windows"))]
pub use stub::*;
+12 -10
View File
@@ -9,6 +9,7 @@
"launch_while_compiling_shaders": false,
"show_shader_compile_rage_meter": false,
"skip_cinematics": true,
"bonus_play_fix": true,
"use_gamemode": true,
"vram_gc_budget_mb": 6144,
"dmem_mb": 9216,
@@ -22,17 +23,16 @@
"cpu_threads": 20
},
"last_git_commit": "0b4abe07acb711848d1b5d72402a9de8c0ad9844",
"launcher_version": "v2.0.2",
"launcher_version": "v2.2",
"disabled_mods": [
"(OPTIONAL) God Armor",
"Blades of Athena Damage Boost",
"CJ (Mod by Gamer JP)",
"game",
"Game",
"god armor By Richarito v2",
"Golden Blades",
"DiamondSword v1.1 Author YT @MustopuhGGI",
"Skeleton Kratos Skin"
"Skeleton Kratos Skin",
"Golden Blades",
"CJ (Mod by Gamer JP)"
],
"render_resolution": "native",
"fsr_enabled": false,
@@ -47,10 +47,10 @@
"deadzone_left": 2,
"deadzone_right": 2,
"keyboard_bindings": {
"triangle": "u",
"circle": "j",
"cross": "g",
"square": "t",
"triangle": "mouse2",
"circle": "f",
"cross": "space",
"square": "mouse1",
"l1": "q",
"r1": "e",
"l2": "z",
@@ -71,5 +71,7 @@
"axis_right_x_plus": "kp6",
"axis_right_y_minus": "kp8",
"axis_right_y_plus": "kp2"
}
},
"discord_rpc_enabled": true,
"discord_client_id": "1440140183675867156"
}
+3 -4
View File
@@ -1,14 +1,13 @@
{
"disabled": [
"(OPTIONAL) God Armor",
"Blades of Athena Damage Boost",
"CJ (Mod by Gamer JP)",
"game",
"Game",
"god armor By Richarito v2",
"Golden Blades",
"DiamondSword v1.1 Author YT @MustopuhGGI",
"Skeleton Kratos Skin"
"Skeleton Kratos Skin",
"Golden Blades",
"CJ (Mod by Gamer JP)"
],
"order": []
}
+17
View File
@@ -31,6 +31,12 @@
<Line Type="bytes" Address="0x006de2c7" Value="0f84d600000090"/>
</PatchList>
</Metadata>
<Metadata Title="God of War III Remastered" Name="Bug Fix - Bonus Play QTE and Enemy Spawning Fix" Note="Fixes a critical scripting desynchronization in God of War III Bonus Play when Godly Possessions (specifically Hephaestus' Ring) are active. Prevents auto-QTE from prematurely skipping context-sensitive animation death callbacks, ensuring enemy wave spawns and QTE triggers progress without arena softlocks." Author="gow3-launcher" PatchVer="1.0" AppVer="01.02" AppElf="eboot.bin" isEnabled="true">
<PatchList>
<Line Type="bytes" Address="0x0041b2a3" Value="7542"/>
<Line Type="bytes" Address="0x0041b2e7" Value="b901000000909090"/>
</PatchList>
</Metadata>
<Metadata Title="God of War III Remastered" Name="Resolution Patch - 480p" Note="Decreases game output resolution to 854x480 and increases Video Arena to 2.25 GiB. Requires setting DMEM to 2048 MB or more. Only leave one resolution patch enabled; do not enable it with Bug Fix - Texture Corruption Fix." Author="kvicken + cuesta4" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
<PatchList>
<Line Type="bytes" Address="0x005f0c5b" Value="56030000"/>
@@ -201,4 +207,15 @@
<Line Type="bytes" Address="0x00566090" Value="c3"/>
</PatchList>
</Metadata>
<Metadata Title="God of War III Remastered" Name="Bug Fix - Trophy Unlock Fix" Note="Fixes emulator trophy count bounds check and disables Bonus Play trophy restriction in eboot.bin, enabling all 35 trophies to unlock seamlessly." Author="gow3-launcher" PatchVer="2.0" AppVer="01.02" AppElf="eboot.bin" isEnabled="true">
<PatchList>
<Line Type="bytes" Address="0x005f4510" Value="b840000000c3"/>
<Line Type="bytes" Address="0x00422f88" Value="909090909090"/>
<Line Type="bytes" Address="0x00422fce" Value="909090909090"/>
<Line Type="bytes" Address="0x00423709" Value="9090"/>
<Line Type="bytes" Address="0x0042373a" Value="9090"/>
<Line Type="bytes" Address="0x004292e9" Value="9090"/>
<Line Type="bytes" Address="0x00429330" Value="9090"/>
</PatchList>
</Metadata>
</Patch>
+72 -12
View File
@@ -151,6 +151,16 @@ LINKED = {}
def make_link(link, target):
if link.is_symlink() or link.is_file():
try:
link.unlink()
except OSError:
pass
elif link.is_dir():
try:
remove_link(link)
except OSError:
pass
try:
link.symlink_to(target, target_is_directory=target.is_dir())
return
@@ -159,13 +169,19 @@ def make_link(link, target):
raise
if target.is_dir():
import _winapi
_winapi.CreateJunction(str(target), str(link))
try:
_winapi.CreateJunction(str(target), str(link))
except OSError:
# Fallback if junction creation fails across volumes or permissions
shutil.copytree(target, link, dirs_exist_ok=True)
else:
try:
os.link(target, link)
except OSError:
shutil.copy2(target, link)
LINKED[str(link)] = target
p_str = str(link)
LINKED[p_str] = target
LINKED[p_str.casefold()] = target
def symlinks_work(directory):
@@ -180,19 +196,52 @@ def symlinks_work(directory):
def is_link(path):
return path.is_symlink() or str(path) in LINKED or (hasattr(path, 'is_junction') and path.is_junction())
p_str = str(path)
if path.is_symlink():
return True
if p_str in LINKED or p_str.casefold() in LINKED:
return True
if hasattr(path, 'is_junction') and path.is_junction():
return True
if os.name == 'nt':
try:
import stat
mode = os.lstat(path).st_mode
if stat.S_ISLNK(mode):
return True
except OSError:
pass
return False
def link_target(path):
return LINKED.get(str(path)) or path.resolve(strict=True)
p_str = str(path)
return LINKED.get(p_str) or LINKED.get(p_str.casefold()) or path.resolve(strict=True)
def remove_link(path):
if hasattr(path, 'is_junction') and path.is_junction():
os.rmdir(path)
else:
path.unlink()
LINKED.pop(str(path), None)
p_str = str(path)
try:
if path.is_symlink():
path.unlink()
elif hasattr(path, 'is_junction') and path.is_junction():
os.rmdir(path)
elif path.is_dir():
os.rmdir(path)
else:
path.unlink()
except OSError:
try:
if path.is_symlink():
path.unlink()
elif os.path.isdir(path):
os.rmdir(path)
else:
path.unlink()
except OSError:
pass
LINKED.pop(p_str, None)
LINKED.pop(p_str.casefold(), None)
def expand(directory):
@@ -202,7 +251,7 @@ def expand(directory):
if not target.is_dir():
raise ValueError(f'File/directory conflict at {directory.name}')
remove_link(directory)
directory.mkdir()
directory.mkdir(exist_ok=True)
for entry in target.iterdir():
make_link(directory / entry.name, entry)
elif directory.exists() and not directory.is_dir():
@@ -211,6 +260,17 @@ def expand(directory):
directory.mkdir(exist_ok=True)
def on_rm_error(func, path, exc_info):
try:
os.chmod(path, 0o777)
if os.path.isdir(path):
os.rmdir(path)
else:
os.unlink(path)
except OSError:
pass
def build_overlay(game, out, mods):
game = Path(game).resolve(strict=True)
replacements = []
@@ -231,7 +291,7 @@ def build_overlay(game, out, mods):
out.mkdir(parents=True, exist_ok=True)
for old_dir in out.glob('mod-game-*'):
if old_dir.is_dir():
shutil.rmtree(old_dir, ignore_errors=True)
shutil.rmtree(old_dir, onerror=on_rm_error)
if os.name == 'nt' and not symlinks_work(out) and out.drive.casefold() != game.drive.casefold():
# Hard links only reach files on the same volume: the overlay goes beside the game.
out = game.parent
@@ -258,7 +318,7 @@ def build_overlay(game, out, mods):
print(f'Mods: {replaced} game files replaced, {added} added', file=sys.stderr)
return overlay
except BaseException:
shutil.rmtree(overlay)
shutil.rmtree(overlay, onerror=on_rm_error)
raise
+181
View File
@@ -0,0 +1,181 @@
{
"title_id": "CUSA01715",
"last_updated": 1794235113,
"trophies": {
"0": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"1": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"2": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"3": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"4": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"5": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"6": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"7": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"8": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"9": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"10": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"11": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"12": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"13": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"14": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"15": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"16": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"17": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"18": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"19": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"20": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"21": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"22": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"23": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"24": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"25": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"26": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"27": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"28": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"29": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"30": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"31": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"32": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"33": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
},
"34": {
"unlocked": false,
"unlocked_at": null,
"unlocked_at_formatted": null
}
}
}