Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9147960de5 | ||
|
|
3a96e9845b | ||
|
|
3992455718 | ||
|
|
08da3bec2c | ||
|
|
04f61833ea | ||
|
|
a60dc8bace | ||
|
|
7b8d9155fd | ||
|
|
ec0a5893c7 | ||
|
|
6bb6349c41 | ||
|
|
8ba512f45e | ||
|
|
0136d4a426 | ||
|
|
71b9cd8596 | ||
|
|
b6409869aa | ||
|
|
ff7c026bba | ||
|
|
f119d84439 |
No files matched your search
@@ -10,6 +10,7 @@ user/
|
||||
out/
|
||||
GLCache/
|
||||
.runtime/
|
||||
mod-game-*/
|
||||
vkd3d-proton.cache
|
||||
fsr4_shaders/
|
||||
.local/
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1671,7 +1671,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "launcher"
|
||||
version = "2.0.2"
|
||||
version = "2.2.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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::*;
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||