Compare commits

..
25 Commits
Author SHA1 Message Date
ScreamAngel 08da3bec2c docs: add project memory and architecture overview for persistent context 2026-11-07 16:22:19 +03:00
ScreamAngel 04f61833ea feat(discord-rpc): automatic system language detection and multi-language support (tr, en, de, es) 2026-11-07 16:14:19 +03:00
ScreamAngel a60dc8bace feat(discord-rpc): accurate real-time story chapters and boss fight presence 2026-11-07 15:17:38 +03:00
ScreamAngel 7b8d9155fd fix(linux): isolate Windows-specific viewport commands and restore clean fullscreen & borderless on Linux Hyprland 2026-11-07 14:57:50 +03:00
ScreamAngel ec0a5893c7 fix(windows): resolve mouse click scancodes and keep shader screen fullscreen behind game 2026-11-07 14:35:29 +03:00
ScreamAngel 6bb6349c41 fix(windows): resolve black screen launch bug and refine shader screen 10s background lifecycle 2026-11-07 14:05:47 +03:00
ScreamAngel 8ba512f45e feat(discord): official God of War III Remastered App ID, real-time chapter & boss fight detection, challenges & arena presence 2026-11-07 03:42:52 +03:00
ScreamAngel 0136d4a426 feat(branding): embed custom God of War icons in probe binaries, SDL window & launcher metadata 2026-11-07 03:12:36 +03:00
ScreamAngel 71b9cd8596 fix(windows): resolve FPS speed surge, mouse click bridge, mod support and shader window z-order 2026-11-07 02:45:46 +03:00
ScreamAngel b6409869aa feat: release v2.1 - integrate native Discord Rich Presence, activity reporting and custom Client ID support 2026-11-03 23:27:27 +03:00
ScreamAngel ff7c026bba fix: remove missing glyph tofu box before red category headers 2026-11-03 19:01:23 +03:00
ScreamAngel f119d84439 feat: release v2.0.3 - enhanced settings UI readability, smooth PS4 splash zoom with 20s auto-close, fix duplicate update icon 2026-11-03 18:50:13 +03:00
ScreamAngel 4b5ec4f7f5 feat: rebuild Windows executable, update to v2.0.2 for both Linux & Windows 2026-11-03 12:53:55 +03:00
ScreamAngel 5ddf4eab9d Update README.md 2026-11-03 01:51:16 +03:00
ScreamAngel 839dabca0d Update README.md 2026-11-03 01:50:20 +03:00
ScreamAngel 319a4c56e8 Add automatic version scan, reminder dismissal, and update button (v2.0.1) 2026-11-03 00:12:35 +03:00
ScreamAngel 0c9f36c292 fix(launcher): keep shader background screen open without closing (v2.0.1) 2026-11-02 23:58:25 +03:00
ScreamAngel bdb896b279 fix(launcher): hold splash screen until Sony intro video renders on screen (v2.0.1) 2026-11-02 23:40:29 +03:00
ScreamAngel b435c8344b Update README.md 2026-11-02 22:58:26 +03:00
ScreamAngel 12dea62e3b fix(mods, shaders): mod packaging/XML patch support, post-compile black screen hold, and PS4 splash toggle (v2.0.1) 2026-11-02 22:56:13 +03:00
ScreamAngel 192f9164d1 fix(shader): smooth synchronous 0-100% shader loading, hide yellow beam, bump v2.0.1 2026-11-02 21:02:18 +03:00
ScreamAngel 9fdbeb5650 feat: complete in-game mod manager (v2.0), colorful vector flags, and shader icon fix 2026-11-02 19:38:14 +03:00
ScreamAngel 95db57f9a1 feat(ui): settings system specs tab, vram gc combo, 240 fps, turkish flag, github release notes (v2.0) 2026-11-02 18:51:14 +03:00
ScreamAngel ba4c08dcea feat(ui): God of War Ascension console main menu, localized release notes, 4-tab settings (v2.0) 2026-11-02 18:11:00 +03:00
ScreamAngel e9f6c33021 feat(ui): major UI overhaul to wireframe layout with God of War font, sidebar navigation, and interactive animations (v2.0) 2026-11-02 17:36:14 +03:00
29 changed files with 6107 additions and 2099 deletions

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ user/
out/
GLCache/
.runtime/
mod-game-*/
vkd3d-proton.cache
fsr4_shaders/
.local/
+97
View File
@@ -0,0 +1,97 @@
# God of War III PC Launcher — Proje Hafızası ve Mimari Özeti
> **Versiyon:** v2.1
> **Depo:** [github.com/ScreamAngel/gow3_launcher](https://github.com/ScreamAngel/gow3_launcher) (`main` dalı)
> **Platform Desteği:** Linux (x86_64, Wayland/Hyprland & X11) ve Windows (x86_64 MSVC)
> **Tarih:** 07 Kasım 2026
---
## 1. Proje Mimarisi ve Çekirdek Dosyalar
* **Launcher Kaynak Kodu (`launcher/src/`):**
* `main.rs`: CLI başlatma argümanları (`--play`, `-p`), pencere ikonu yükleme ve eframe/egui giriş noktası.
* `app.rs`: Launcher ana GUI arayüzü, 6 sekme (Oyun, Grafik, Yapılandırma, Kontroller, Modlar, Sistem), shader derleme ekranı ve oyun süreci izleyicisi.
* `discord_rpc.rs`: Sıfır ek yükümlü, yerel Discord IPC istemcisi (Linux Unix Domain Socket & Windows Named Pipe), gerçek zamanlı bölüm & boss savaşı dedektörü, çoklu dil desteği (`RpcLanguage`).
* `win_input.rs`: Windows düşük seviye fare kancası (`WH_MOUSE_LL`), 1ms `timeBeginPeriod` zamanlayıcı çözünürlüğü ve oyun penceresini öne getirme (`SetForegroundWindow`).
* `runner.rs`: Mod bindirme (overlay) yöneticisi, Python/Rust mod montajı, oyun süreci (`gow3-probe`) çalıştırma ve log yönlendirme.
* `config.rs`: `launcher_config.json` yönetimi, donanım optimizasyonları ve otomatik sistem dili algılama (`default_language()`).
* `i18n.rs`: 4 dilli arayüz yerelleştirmesi (Türkçe, English, Deutsch, Español).
* `crash_analyzer.rs`: Oyun çöktüğünde `gow3_log.txt` çıktısını analiz edip kullanıcıya çözüm sunan hata teşhis motoru.
* **Dağıtılan İkili Dosyalar (Binaries):**
* Linux: `gow3-launcher`
* Windows: `gow3-launcher-win.exe`
* Alt dizin kopyaları: `gow3-linux/gow3-launcher`, `gow3-linux/gow3-launcher-win.exe`
---
## 2. Discord Rich Presence (RPC) Entegrasyonu
* **Resmi App ID:** `1440140183675867156` (*God of War III: Remastered*)
* **Resmi Varlıklar (Assets):**
* Kapak İkonu (Large Image): `https://cdn.discordapp.com/app-icons/1440140183675867156/2cbf4c7faf6444d474ff7b32b2025ec2.png`
* Rozet İkonu (Small Image): `https://raw.githubusercontent.com/ScreamAngel/gow3_launcher/main/gow3.png`
* **İletişim Kanalları:**
* **Linux:** `$XDG_RUNTIME_DIR/discord-ipc-0`..`9`, `/tmp/discord-ipc-0`..`9`, Flatpak, Snap ve Vesktop (`vesktop-ipc`).
* **Windows:** `\\.\pipe\discord-ipc-0`..`9` (Named Pipes). Olası donmaları engellemek için `PeekNamedPipe` ile asenkron kontrol mekanizması eklendi.
* **Gerçek Zamanlı Durum Tespiti:**
* `gow3_log.txt` dosyasından gerçek çalışma zamanı WAD yüklemeleri (`WAD_STATS: WAD <NAME>` ve `/WAD/PS4_LE/<NAME>.WAD`) taranır. Statik motor sözlüğü (`LoadWadSizeData`) **yoksayılır**.
* `user/savedata/*/CUSA01715/*.sce_sys/param.bin` SFO dosyasından anlık kayıt checkpoint'i ve zorluk derecesi çekilir.
* Ters kronolojik (newest-first) algoritma ile oyuncunun bulunduğu son bölge veya boss savaşı anlık belirlenir.
* **Otomatik Dil Desteği (tr, en, de, es):**
* İşletim sistemi dili (Linux'ta `$LANG`, Windows'ta `GetUserDefaultUILanguage`) veya launcher arayüz dili baz alınarak durum otomatik yerelleştirilir.
* **Bölüm & Boss Karşılıkları:**
* Poseidon Boss Savaşı: `Olympus Dağı (Poseidon Boss Fight)` / `Mount Olympus (Poseidon Boss Fight)`
* Hades Boss Savaşı: `Yeraltı Dünyası (Hades Boss Fight)` / `The Underworld (Hades Boss Fight)`
* Helios Boss Savaşı: `Olimpiya Şehri (Helios Boss Fight)` / `City of Olympia (Helios Boss Fight)`
* Hermes Kovalamacası: `Olimpiya Şehri (Hermes Kovalamacası)` / `City of Olympia (Hermes Chase)`
* Herkül Boss Savaşı: `Olympus Forumu (Hercules Boss Fight)` / `Forum of Olympus (Hercules Boss Fight)`
* Kronos Boss Savaşı: `Tartarus Çukuru (Cronos Boss Fight)` / `Pit of Tartarus (Cronos Boss Fight)`
* Hephaistos Boss Savaşı: `Demirhane (Hephaestus Boss Fight)` / `The Forge (Hephaestus Boss Fight)`
* Skorpius Boss Savaşı: `Olimpos Mağaraları (Skorpius Boss Fight)` / `Caverns of Olympus (Skorpius Boss Fight)`
* Zeus Boss Savaşı: `Olympus Tepesi (Zeus Boss Fight)` / `Summit of Olympus (Zeus Boss Fight)`
* Mücadeleler: `Olympus Mücadelesi 1-7` / `Challenge of Olympus 1-7`
* Savaş Arenası: `Savaş Arenası (Combat Arena)` / `Combat Arena`
* Menü: `Ana Menüde` / `In Main Menu`
* Shader Derleme: `Shader'lar Derleniyor (%X)` / `Compiling Shaders (X%)`
---
## 3. Çözülen Kritik Hatalar ve Teknik Notlar
1. **Windows Fare Tıklama Algılama Hatası:**
* Sorun: Windows'ta sol ve sağ tık çalışmıyordu.
* Çözüm: `win_input.rs` içindeki `WH_MOUSE_LL` kancasında PS4 SDL2 girdi haritasının beklediği F13/F14 sanal tarama kodları (`0x64` = Kare/Hafif Vuruş, `0x65` = Üçgen/Ağır Vuruş) düzeltildi.
2. **Windows Siyah Ekran / Mod Açılış Hatası:**
* Sorun: NTFS junction oluşturma yetki hatası yüzünden Windows'ta oyun siyah ekranda takılıyordu.
* Çözüm: `runner.rs` içinde Rust tabanlı doğrudan dosya kopyalama bindirmesi (`out/mod-game-rust-overlay`) uygulandı, yol formatı ve dosya doğrulaması eklendi.
3. **Shader Ekranı Küçülme / Kapanma / Arka Plana Düşme:**
* Sorun: Shader derleme ekranı açıldığında oyun arka plana düşüyor veya görev çubuğuna küçülüyordu.
* Çözüm: Oyun penceresi açıldığında shader ekranı 10 saniye boyunca arka planda tam ekran açık kalır, oyun penceresi öne getirilir (`bring_game_window_to_front`) ve süre bitiminde launcher normal moduna döner (asla küçülmez).
4. **Linux Hyprland / Wayland Tam Ekran Hatası:**
* Sorun: Windows için eklenen pencere boyutu komutları Linux Wayland'de oyunun kenarlıksız pencere moduna geçmesini bozuyordu.
* Çözüm: `Maximized(true)` ve ilgili `ViewportCommand` çağrıları `#[cfg(target_os = "windows")]` ile izole edildi, Linux'ta yerel `hyprctl` ve saf Wayland geometri protokolü korundu.
5. **Discord RPC'nin Sadece "Ana Menüde" Kalması:**
* Sorun: Motorun statik `LoadWadSizeData: WAD_Shell` satırı yüzünden log döngüsü erken çıkış veriyor ve boss/bölüm adımlarını atlıyordu.
* Çözüm: Statik sözlük filtrelendi, gerçek WAD yüklemeleri ters sırada önceliklendirildi.
---
## 4. Derleme ve Test Komutları
* **Birim Testleri (30 Test):**
```bash
cargo test --manifest-path launcher/Cargo.toml
```
* **Linux Release Derlemesi:**
```bash
cargo build --release --manifest-path launcher/Cargo.toml
cp -f launcher/target/release/launcher gow3-launcher
cp -f launcher/target/release/launcher gow3-linux/gow3-launcher
```
* **Windows Release Derlemesi (Cross-Compile):**
```bash
cargo xwin build --release --manifest-path launcher/Cargo.toml --target x86_64-pc-windows-msvc
cp -f launcher/target/x86_64-pc-windows-msvc/release/launcher.exe gow3-launcher-win.exe
cp -f launcher/target/x86_64-pc-windows-msvc/release/launcher.exe gow3-linux/gow3-launcher-win.exe
```
+18 -4
View File
@@ -1,6 +1,10 @@
# gow3_launcher
Launcher for God Of War 3 PC on Linux and Windows
**To ensure mod support works, install the launcher using a clean installation**
**Mod desteğinin düzgün çalışmasını sağlamak için, başlatıcıyı temiz bir kurulum yaparak yükleyin**
# ⚡ God of War® III Remastered - Linux-Windows Port & Native Launcher
An optimized Linux-Windows port and native graphical launcher for **God of War® III Remastered** (*CUSA01623 / CUSA01715*), featuring hardware auto-tuning, 144Hz Delta-Time synchronization, crash diagnostics, and upstream GitHub integration.
@@ -71,18 +75,28 @@ Launcher for God Of War 3 PC on Linux and Windows
### Sections of the launcher
**Home Screen**
<img width="2287" height="1241" alt="image" src="https://github.com/user-attachments/assets/937f6239-606f-439e-9924-f8df6549623e" />
<img width="1901" height="1025" alt="image" src="https://github.com/user-attachments/assets/31385740-38a4-4ddd-bf5b-3d795930c51f" />
**Graphics Settings**
<img width="2543" height="1388" alt="image" src="https://github.com/user-attachments/assets/ed939f69-93af-498a-a860-209bbb75bbd0" />
<img width="2284" height="1241" alt="image" src="https://github.com/user-attachments/assets/b69c4452-9e0b-4b9a-ac3e-e387d947bda4" />
**Mods Settings**
<img width="1901" height="1024" alt="image" src="https://github.com/user-attachments/assets/7a73fbbd-eb58-49a4-a052-b397da7bcb0a" />
**Keyboard Configuration**
<img width="3055" height="1675" alt="image" src="https://github.com/user-attachments/assets/fb06c6b1-2cf0-43a7-a212-aefe5082e087" />
**System Specifications**
<img width="1898" height="1024" alt="image" src="https://github.com/user-attachments/assets/2643f04c-02af-44e5-a2d0-c8f764688759" />
**Game Location**
<img width="1898" height="1025" alt="image" src="https://github.com/user-attachments/assets/aff6bba2-489c-47cf-8db4-5812dc705371" />
**Keyboard Configuration**
<img width="1901" height="1030" alt="image" src="https://github.com/user-attachments/assets/e245193f-3863-4a23-93ee-d4253a0a0d28" />
**Shader Settings**
<img width="1900" height="1029" alt="image" src="https://github.com/user-attachments/assets/6db7a6d5-4474-411d-aa81-defdfb65da6b" />
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+3 -332
View File
@@ -417,18 +417,6 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "bit-set"
version = "0.10.0"
@@ -637,12 +625,6 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "combine"
version = "4.6.8"
@@ -729,12 +711,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
name = "data-url"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -909,14 +885,12 @@ checksum = "05cbdc785020861553f230f23d330ba1fb72613b9d8e0e457f3bdc8ba6db6dcf"
dependencies = [
"ahash",
"egui",
"ehttp",
"enum-map",
"image",
"itertools",
"log",
"mime_guess2",
"profiling",
"resvg",
]
[[package]]
@@ -933,20 +907,6 @@ dependencies = [
"winit",
]
[[package]]
name = "ehttp"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2f1b93eb2e039aaff63ce07cca59bd1dca02f2ce30075a17b619d2c42f56efc"
dependencies = [
"document-features",
"js-sys",
"ureq",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "either"
version = "1.19.0"
@@ -1135,12 +1095,6 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]]
name = "foldhash"
version = "0.2.0"
@@ -1257,17 +1211,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -1291,16 +1234,6 @@ dependencies = [
"r-efi 6.0.0",
]
[[package]]
name = "gif"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gl_generator"
version = "0.14.0"
@@ -1485,22 +1418,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "icu_collections"
version = "2.3.0"
@@ -1613,31 +1530,12 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"moxcms",
"num-traits",
"png 0.18.1",
"png",
"tiff",
]
[[package]]
name = "image-webp"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
name = "imagesize"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]]
name = "indexmap"
version = "2.14.2"
@@ -1759,17 +1657,6 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kurbo"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
dependencies = [
"arrayvec",
"euclid",
"smallvec",
]
[[package]]
name = "kurbo"
version = "0.13.1"
@@ -1784,7 +1671,7 @@ dependencies = [
[[package]]
name = "launcher"
version = "1.9.0"
version = "2.1.0"
dependencies = [
"eframe",
"egui_extras",
@@ -2445,7 +2332,7 @@ checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6"
dependencies = [
"bytemuck",
"color",
"kurbo 0.13.1",
"kurbo",
"linebender_resource_handle",
"smallvec",
]
@@ -2543,12 +2430,6 @@ dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pin-project"
version = "1.1.13"
@@ -2598,19 +2479,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.9",
]
[[package]]
name = "png"
version = "0.18.1"
@@ -2838,20 +2706,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "resvg"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43"
dependencies = [
"log",
"pico-args",
"rgb",
"svgtypes",
"tiny-skia",
"usvg",
]
[[package]]
name = "rfd"
version = "0.17.2"
@@ -2879,35 +2733,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
dependencies = [
"bytemuck",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "roxmltree"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rustc-hash"
version = "1.1.0"
@@ -2955,41 +2780,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.23"
@@ -3134,15 +2924,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "simplecss"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
dependencies = [
"log",
]
[[package]]
name = "siphasher"
version = "1.0.4"
@@ -3278,25 +3059,6 @@ name = "strict-num"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
dependencies = [
"float-cmp",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "svgtypes"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
dependencies = [
"kurbo 0.11.3",
"siphasher",
]
[[package]]
name = "syn"
@@ -3418,7 +3180,6 @@ dependencies = [
"bytemuck",
"cfg-if",
"log",
"png 0.17.16",
"tiny-skia-path",
]
@@ -3579,41 +3340,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
dependencies = [
"base64 0.23.1",
"flate2",
"log",
"percent-encoding",
"rustls",
"rustls-pki-types",
"ureq-proto",
"utf8-zero",
"webpki-roots",
]
[[package]]
name = "ureq-proto"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
dependencies = [
"base64 0.23.1",
"http",
"httparse",
"log",
]
[[package]]
name = "url"
version = "2.5.8"
@@ -3626,34 +3352,6 @@ dependencies = [
"serde",
]
[[package]]
name = "usvg"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef"
dependencies = [
"base64 0.22.1",
"data-url",
"flate2",
"imagesize",
"kurbo 0.11.3",
"log",
"pico-args",
"roxmltree",
"simplecss",
"siphasher",
"strict-num",
"svgtypes",
"tiny-skia-path",
"xmlwriter",
]
[[package]]
name = "utf8-zero"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -3714,12 +3412,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
@@ -3956,15 +3648,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weezl"
version = "0.1.12"
@@ -4509,12 +4192,6 @@ version = "0.8.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xmlwriter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "yoke"
version = "0.8.3"
@@ -4685,12 +4362,6 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
[[package]]
name = "zerotrie"
version = "0.2.5"
+3 -3
View File
@@ -1,12 +1,12 @@
[package]
name = "launcher"
version = "1.9.0"
version = "2.1.0"
edition = "2024"
[dependencies]
eframe = "0.36.2"
egui_extras = { version = "0.36.2", features = ["all_loaders"] }
image = { version = "0.25.10", default-features = false, features = ["png"] }
egui_extras = { version = "0.36.2", features = ["image"] }
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"
+2 -2
View File
@@ -6,8 +6,8 @@ fn main() {
res.set("ProductName", "God of War III Remastered Launcher");
res.set("FileDescription", "God of War III Remastered PC Launcher");
res.set("LegalCopyright", "God of War III Remastered");
res.set("ProductVersion", "1.9.0.0");
res.set("FileVersion", "1.9.0.0");
res.set("ProductVersion", "2.1.0.0");
res.set("FileVersion", "2.1.0.0");
if let Err(e) = res.compile() {
eprintln!("Warning: failed to compile Windows icon resource: {}", e);
}
+2999 -1691
View File
File diff suppressed because it is too large. Load diff
+61 -8
View File
@@ -241,6 +241,8 @@ pub struct LauncherConfig {
#[serde(default)]
pub launch_while_compiling_shaders: bool,
#[serde(default = "default_true")]
pub show_shader_compile_rage_meter: bool,
#[serde(default = "default_true")]
pub skip_cinematics: bool,
#[serde(default = "default_true")]
pub use_gamemode: bool,
@@ -255,6 +257,10 @@ pub struct LauncherConfig {
#[serde(default = "default_launcher_version")]
pub launcher_version: String,
// Mods Management
#[serde(default)]
pub disabled_mods: Vec<String>,
// Graphics Settings
#[serde(default = "default_render_resolution")]
pub render_resolution: String,
@@ -288,10 +294,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
@@ -316,7 +332,7 @@ fn default_last_commit() -> String {
"c5638d9956002b8a9f09a240028f6793ddf7b2f8".to_string()
}
fn default_launcher_version() -> String {
"v1.9".to_string()
"v2.1".to_string()
}
fn default_render_resolution() -> String {
"native".to_string()
@@ -334,12 +350,14 @@ impl Default for LauncherConfig {
patch_readbacks: true,
async_readbacks: true,
launch_while_compiling_shaders: false,
show_shader_compile_rage_meter: true,
skip_cinematics: true,
use_gamemode: true,
vram_gc_budget_mb: default_vram_budget(),
dmem_mb: default_dmem(),
cached_hardware: CachedHardware::default(),
last_git_commit: default_last_commit(),
disabled_mods: Vec::new(),
render_resolution: default_render_resolution(),
fsr_enabled: false,
disable_motion_blur: false,
@@ -353,6 +371,8 @@ impl Default for LauncherConfig {
deadzone_left: default_deadzone(),
deadzone_right: default_deadzone(),
keyboard_bindings: KeyboardBindings::default(),
discord_rpc_enabled: true,
discord_client_id: default_discord_client_id(),
}
}
}
@@ -516,9 +536,9 @@ impl LauncherConfig {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(mut cfg) = serde_json::from_str::<LauncherConfig>(&content) {
let mut changed = false;
// Migrate from older versions: ensure v1.9 version string and enable recommended async_readbacks
if cfg.launcher_version != "v1.9" {
cfg.launcher_version = "v1.9".to_string();
// Migrate from older versions: ensure v2.0 version string and enable recommended async_readbacks
if cfg.launcher_version != "v2.0" {
cfg.launcher_version = "v2.0".to_string();
cfg.async_readbacks = true;
changed = true;
}
@@ -532,6 +552,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);
}
@@ -544,10 +568,23 @@ impl LauncherConfig {
default_cfg
}
pub fn save_mods_config(&self, base_dir: &Path) {
let mods_config_path = base_dir.join("mods_config.json");
let json = serde_json::json!({
"disabled": self.disabled_mods,
"order": []
});
if let Ok(content) = serde_json::to_string_pretty(&json) {
let _ = fs::write(&mods_config_path, content);
}
}
pub fn save(&self, base_dir: &Path) -> std::io::Result<()> {
let path = Self::config_path(base_dir);
let content = serde_json::to_string_pretty(self)?;
fs::write(path, content)
let res = fs::write(path, content);
self.save_mods_config(base_dir);
res
}
pub fn build_gow3_key_map(&self) -> String {
@@ -764,11 +801,13 @@ mod tests {
}
#[test]
fn test_launcher_version_is_v19() {
assert_eq!(default_launcher_version(), "v1.9");
fn test_launcher_version_is_v20() {
assert_eq!(default_launcher_version(), "v2.1");
let def = LauncherConfig::default();
assert!(def.async_readbacks);
assert!(def.patch_readbacks);
assert!(def.discord_rpc_enabled);
assert_eq!(def.discord_client_id, crate::discord_rpc::DISCORD_GOW3_APP_ID);
}
#[test]
@@ -825,4 +864,18 @@ mod tests {
assert_eq!(p2.pad_left, "left");
assert_eq!(p2.pad_right, "right");
}
#[test]
fn test_shader_rage_meter_config() {
let default_cfg = LauncherConfig::default();
assert!(default_cfg.show_shader_compile_rage_meter);
let json = r#"{"show_shader_compile_rage_meter": false}"#;
let deserialized: LauncherConfig = serde_json::from_str(json).unwrap();
assert!(!deserialized.show_shader_compile_rage_meter);
let empty_json = r#"{}"#;
let deserialized_empty: LauncherConfig = serde_json::from_str(empty_json).unwrap();
assert!(deserialized_empty.show_shader_compile_rage_meter);
}
}
+1 -1
View File
@@ -141,7 +141,7 @@ pub fn analyze_post_game(base_dir: &Path, exit_code: Option<i32>) -> Option<Cras
return Some(CrashDiagnosis {
title: "💥 NVIDIA Vulkan Sürücü Çökmesi (Dynamic Rendering / VRAM Sınırı)".to_string(),
description: "NVIDIA Vulkan sürücüsü (libnvidia-glcore), sahne bariyerleri boşaltılırken (FlushBarriers -> EndRendering) VRAM yetersizliği veya komut senkronizasyonu hatası nedeniyle çöktü (Hata adresi: 0x108). Bu durum özellikle 8 GB ekran kartlarında doku temizleme bütçesi aşırı yüksek seçildiğinde veya çoklu iş parçacığı çakışmasında meydana gelir.".to_string(),
recommendation: "1. Launcher üzerinden 'Doku Havuzu (VRAM GC)' ayarını 5120 MB veya 4096 MB olarak seçin (8 GB kartlar için güvenli sınır).\n2. Launcher v1.9 güncellemesiyle anlık geri okuma senkronizasyonu ve otomatik taşma koruması devrededir. Oyunu tekrar başlatabilirsiniz.".to_string(),
recommendation: "1. Launcher üzerinden 'Doku Havuzu (VRAM GC)' ayarını 5120 MB veya 4096 MB olarak seçin (8 GB kartlar için güvenli sınır).\n2. Launcher v2.0 güncellemesiyle anlık geri okuma senkronizasyonu ve otomatik taşma koruması devrededir. Oyunu tekrar başlatabilirsiniz.".to_string(),
log_snippet: extract_relevant_snippet(&tail_str, "libnvidia-glcore"),
});
}
File diff suppressed because it is too large. Load diff
+161
View File
@@ -9,6 +9,7 @@ pub enum Language {
Spanish,
}
#[allow(dead_code)]
impl Language {
pub fn all() -> &'static [Language] {
&[Language::Turkish, Language::English, Language::German, Language::Spanish]
@@ -55,6 +56,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Turkish, "tag_hardware") => "D O N A N I M V E S İ S T E M",
(Language::Turkish, "tag_game_location") => "O Y U N K O N U M U",
(Language::Turkish, "tag_patches") => "P E R F O R M A N S V E Y A M A L A R",
(Language::Turkish, "tag_mods") => "O Y U N M O D L A R I",
(Language::Turkish, "tag_display") => "E K R A N V E G R A F İ K",
(Language::Turkish, "tag_controller") => "K O N T R O L C Ü V E G A M E P A D",
(Language::Turkish, "tag_keyboard") => "K L A V Y E A T A M A L A R I",
@@ -112,6 +114,8 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Turkish, "async_readbacks_desc") => "CPU ile GPU senkronizasyonunu ayrıştırarak CPU'nun GPU render kuyruğunda beklemesini önler. Donma, kasma ve anlık FPS düşüşlerini ortadan kaldırır. Akıcı ve stabil bir oyun deneyimi için AÇIK tutulması önerilir.",
(Language::Turkish, "launch_while_compiling_shaders") => "Shader'lar Derlenirken Oyun Açılsın",
(Language::Turkish, "launch_while_compiling_shaders_desc") => "Açık: Oyun hemen açılır ve shader'lar arka planda derlenmeye devam eder. Kapalı: Shader'lar %100 derlenene kadar oyun başlamaz (Spartan Rage yükleme ekranı gösterilir, oyun içi takılmaları önler).",
(Language::Turkish, "show_shader_rage_meter") => "Shader Derleme Ekranı Görünümü (Spartan Rage Barı)",
(Language::Turkish, "show_shader_rage_meter_desc") => "Açık: Shader derleme sürecini Spartan Rage öfke barı ve boru hattı sayacı ile gösterir (Derleme %100 bitince oyun penceresi açılana kadar siyah ekranda bekler). Kapalı (PS4 Modu): Öfke barı ve sayaç gizlenir, oyun penceresi açılana kadar sadece animasyonlu PS4 açılış görseli (pic1.png) gösterilir.",
(Language::Turkish, "shader_cache_title") => "⚡ Resmi Shader Önbelleği (Steam Tarzı Pre-Caching)",
(Language::Turkish, "shader_cache_desc") => "Oyunun tüm haritaları, efektleri ve patron dövüşleri taranarak derlenmiş temiz ve güvenli 28.000+ shader önbelleğidir. Yeni bölgelere girerken veya büyüler kullanılırken oluşan tüm donma ve render takılmalarını sıfırlar.",
(Language::Turkish, "shader_download_btn") => "☁️ Tam Shader Paketini İndir / Yükle (17 MB)",
@@ -123,6 +127,38 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Turkish, "shader_success") => "✅ Shader paketi başarıyla kuruldu! ({} shader önbelleğe alındı)",
(Language::Turkish, "skip_cinematics") => "Sinematikleri 'X' Tuşuyla Atlama Yaması",
(Language::Turkish, "skip_cinematics_desc") => "Ara sahneleri kumandanızın X tuşuna basarak anında geçmenizi sağlar.",
(Language::Turkish, "nav_home") => "ANASAYFA",
(Language::Turkish, "nav_mods") => "MODLAR",
(Language::Turkish, "mods_title") => "God of War III Mod Yöneticisi",
(Language::Turkish, "mods_desc") => "Yüklenen karakter, kostüm ve doku modlarını buradan kolayca yönetebilirsiniz. Orijinal oyun dosyalarınıza dokunulmadan dinamik olarak oyuna aktarılır.",
(Language::Turkish, "mods_open_folder") => "Modlar Klasörünü Aç",
(Language::Turkish, "mods_refresh") => "Modları Yenile",
(Language::Turkish, "mods_count_summary") => "Yüklü Mod: {} | Aktif: {} | Devre Dışı: {}",
(Language::Turkish, "mods_empty_title") => "Henüz Yüklü Mod Bulunamadı",
(Language::Turkish, "mods_empty_desc") => "'Modlar Klasörünü Aç' butonuna basarak indirdiğiniz mod klasörünü buraya bırakın ve ardından 'Modları Yenile'ye tıklayın.",
(Language::Turkish, "mods_status_active") => "● Oyunda Devrede",
(Language::Turkish, "mods_status_disabled") => "○ Devre Dışı",
(Language::Turkish, "mods_author") => "Yazar",
(Language::Turkish, "mods_files_replaced") => "Değiştirilen Dosya",
(Language::Turkish, "nav_settings") => "AYARLAR",
(Language::Turkish, "nav_graphics") => "GRAFİKLER",
(Language::Turkish, "nav_game_location") => "OYUN KONUMU",
(Language::Turkish, "nav_controls") => "KONTROLLER",
(Language::Turkish, "nav_shaders") => "SHADER AYARI",
(Language::Turkish, "launcher_version_label") => "LAUNCHER SÜRÜMÜ",
(Language::Turkish, "lang_options_title") => "DİL SEÇENEKLERİ",
(Language::Turkish, "system_specs_title") => "SİSTEM ÖZELLİKLERİ",
(Language::Turkish, "play_btn_label") => "OYNA",
(Language::Turkish, "menu_play") => "OYNA",
(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.1)",
(Language::Turkish, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.1 YENİLİKLERİ:\n\n• 🎮 Gelişmiş Discord Rich Presence (Bölüm & Boss Fight Tespiti):\n - Resmi 'God of War III: Remastered' Discord App ID (1440140183675867156) ve yüksek çözünürlüklü kapak görseli entegre edildi.\n - Canlı Bölüm ve Chapter Takibi: 'Olympus Dağı', 'Yeraltı Dünyası', 'Labirent', 'Tartarus', 'Olimpiya Şehri', 'Alev Odası' vb.\n - Boss Karşılaşmaları: 'Olympus Dağı (Poseidon Boss Fight)', 'Yeraltı Dünyası (Hades Boss Fight)', 'Tartarus (Kronos Boss Fight)', 'Forum (Herkül Boss Fight)', 'Olimpos Zirvesi (Zeus Boss Fight)' ve diğerleri anlık olarak Discord profilinizde görünür.\n - Özel Modlar: 'Olimpos Mücadeleleri (Challenge of Olympus)' ve 'Savaş Arenası (Combat Arena)' durumu canlı paylaşılır.\n\n• ⚡ Sıfır Performans Kaybı & Geniş İstemci Desteği:\n - Windows Named Pipes, Linux yerel soketleri, Vesktop, Discord Flatpak ve Snap istemcilerini otomatik algılar.\n\n• 🛠️ Genel İyileştirmeler & Kararlılık Güncellemeleri.",
(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",
@@ -167,6 +203,11 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Turkish, "latest_commit") => "Son Commit:",
(Language::Turkish, "commit_date") => "Tarih:",
(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_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ığı:",
(Language::Turkish, "python_ready") => "Yüklü / Hazır",
(Language::Turkish, "python_missing") => "Bulunamadı",
@@ -281,6 +322,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::English, "tag_hardware") => "H A R D W A R E & S Y S T E M",
(Language::English, "tag_game_location") => "G A M E L O C A T I O N",
(Language::English, "tag_patches") => "P E R F O R M A N C E & P A T C H E S",
(Language::English, "tag_mods") => "G A M E M O D S",
(Language::English, "tag_display") => "D I S P L A Y & G R A P H I C S",
(Language::English, "tag_controller") => "C O N T R O L L E R & G A M E P A D",
(Language::English, "tag_keyboard") => "K E Y B O A R D B I N D I N G S",
@@ -338,6 +380,8 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::English, "async_readbacks_desc") => "Decouples CPU from GPU render fences, preventing the CPU from stalling on GPU draw calls. Eliminates FPS drops, micro-stutters, and pacing hitches. Recommended ON.",
(Language::English, "launch_while_compiling_shaders") => "Launch Game While Compiling Shaders",
(Language::English, "launch_while_compiling_shaders_desc") => "On: Game opens immediately while shaders compile in the background. Off: Game does not launch until shaders are 100% compiled (shows Spartan Rage bar, eliminates in-game stutter).",
(Language::English, "show_shader_rage_meter") => "Shader Compilation Display (Spartan Rage Bar)",
(Language::English, "show_shader_rage_meter_desc") => "On: Displays shader compilation progress with the Spartan Rage meter and pipeline counter (Holds black screen after 100% until game window is ready). Off (PS4 Mode): Hides the rage bar and counter, displaying only the animated PS4 splash artwork (pic1.png) until the game window is ready.",
(Language::English, "shader_cache_title") => "⚡ Official Shader Cache (Steam-style Pre-Caching)",
(Language::English, "shader_cache_desc") => "Official clean and safe pre-compiled shader cache covering all maps, cutscenes, effects, and boss fights. Completely eliminates in-game stuttering and render pauses.",
(Language::English, "shader_download_btn") => "☁️ Download & Install Full Shader Cache (17 MB)",
@@ -349,6 +393,38 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::English, "shader_success") => "✅ Shader cache installed successfully! ({} shaders cached)",
(Language::English, "skip_cinematics") => "Skip Cutscenes with 'X' Button",
(Language::English, "skip_cinematics_desc") => "Allows skipping any cutscene immediately by pressing the X button.",
(Language::English, "nav_home") => "HOME",
(Language::English, "nav_mods") => "MODS",
(Language::English, "mods_title") => "God of War III Mod Manager",
(Language::English, "mods_desc") => "Easily manage character, costume, and texture mods. Safely loaded into the game via non-destructive virtual overlay.",
(Language::English, "mods_open_folder") => "Open Mods Folder",
(Language::English, "mods_refresh") => "Refresh Mods",
(Language::English, "mods_count_summary") => "Installed: {} | Active: {} | Disabled: {}",
(Language::English, "mods_empty_title") => "No Mods Installed Yet",
(Language::English, "mods_empty_desc") => "Click 'Open Mods Folder', drop your mod directory inside, and click 'Refresh Mods'.",
(Language::English, "mods_status_active") => "● Active In-Game",
(Language::English, "mods_status_disabled") => "○ Disabled",
(Language::English, "mods_author") => "Author",
(Language::English, "mods_files_replaced") => "Replaced Files",
(Language::English, "nav_settings") => "SETTINGS",
(Language::English, "nav_graphics") => "GRAPHICS",
(Language::English, "nav_game_location") => "GAME LOCATION",
(Language::English, "nav_controls") => "CONTROLS",
(Language::English, "nav_shaders") => "SHADER SETTINGS",
(Language::English, "launcher_version_label") => "LAUNCHER VERSION",
(Language::English, "lang_options_title") => "LANGUAGE OPTIONS",
(Language::English, "system_specs_title") => "SYSTEM SPECIFICATIONS",
(Language::English, "play_btn_label") => "PLAY",
(Language::English, "menu_play") => "PLAY",
(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.1)",
(Language::English, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.1 CHANGELOG:\n\n• 🎮 Advanced Discord Rich Presence (Chapters & Boss Fights):\n - Official 'God of War III: Remastered' Discord App ID (1440140183675867156) and official artwork.\n - Real-time Chapter Tracking: 'Mount Olympus', 'Realm of Hades', 'The Labyrinth', 'Tartarus', 'City of Olympia', etc.\n - Boss Fight Detection: 'Olympus Dağı (Poseidon Boss Fight)', 'Yeraltı Dünyası (Hades Boss Fight)', 'Tartarus (Kronos Boss Fight)', 'Forum (Hercules Boss Fight)', 'Mount Olympus Summit (Zeus Boss Fight)', etc.\n - Special Modes: 'Challenge of Olympus' and 'Combat Arena' detected automatically.\n\n• ⚡ Broad Client Support:\n - Works with native Discord, Vesktop, Flatpak, Snap, and Windows Named Pipes.",
(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",
@@ -393,6 +469,11 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::English, "latest_commit") => "Latest Commit:",
(Language::English, "commit_date") => "Date:",
(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_now") => "🚀 Update Now",
(Language::English, "updating_in_progress") => "⏳ Downloading and installing update... Please wait...",
(Language::English, "python_dep") => "Python 3 Dependency:",
(Language::English, "python_ready") => "Installed / Ready",
(Language::English, "python_missing") => "Missing",
@@ -507,6 +588,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::German, "tag_hardware") => "H A R D W A R E & S Y S T E M",
(Language::German, "tag_game_location") => "S P I E L P F A D",
(Language::German, "tag_patches") => "L E I S T U N G & P A T C H E S",
(Language::German, "tag_mods") => "S P I E L - M O D S",
(Language::German, "tag_display") => "A N Z E I G E & G R A F I K",
(Language::German, "tag_controller") => "C O N T R O L L E R & G A M E P A D",
(Language::German, "tag_keyboard") => "T A S T A T U R B E L E G U N G",
@@ -564,6 +646,8 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::German, "async_readbacks_desc") => "Entkoppelt CPU- und GPU-Wartezeiten für Texturen. Verhindert FPS-Einbrüche und Mikroruckler für eine flüssige Darstellung. Empfohlen: EIN.",
(Language::German, "launch_while_compiling_shaders") => "Spiel während Shader-Kompilierung starten",
(Language::German, "launch_while_compiling_shaders_desc") => "Ein: Das Spiel startet sofort, Shader werden im Hintergrund kompiliert. Aus: Spiel startet erst nach 100%iger Shader-Kompilierung mit Spartan-Rage-Ladebildschirm.",
(Language::German, "show_shader_rage_meter") => "Shader-Kompilierungsanzeige (Spartan Rage Leiste)",
(Language::German, "show_shader_rage_meter_desc") => "Ein: Zeigt die Shader-Kompilierung mit der Spartan-Rage-Leiste und Pipeline-Zähler an (hält nach 100% einen schwarzen Bildschirm, bis das Spielfenster bereit ist). Aus (PS4-Modus): Blendet die Leiste aus und zeigt stattdessen das animierte PS4-Artwork (pic1.png).",
(Language::German, "shader_cache_title") => "⚡ Offizieller Shader-Cache (Steam-Style Pre-Caching)",
(Language::German, "shader_cache_desc") => "Offizieller, sauberer Shader-Cache mit über 28.000 kompilierten Shadern für alle Bereiche und Bosse. Beseitigt Mikroruckler und Render-Pausen komplett.",
(Language::German, "shader_download_btn") => "☁️ Vollständigen Shader-Cache herunterladen (17 MB)",
@@ -575,6 +659,38 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::German, "shader_success") => "✅ Shader-Cache erfolgreich installiert! ({} Shader im Cache)",
(Language::German, "skip_cinematics") => "Zwischensequenzen mit 'X'-Taste überspringen",
(Language::German, "skip_cinematics_desc") => "Ermöglicht das sofortige Überspringen von Zwischensequenzen mit der X-Taste.",
(Language::German, "nav_home") => "STARTSEITE",
(Language::German, "nav_mods") => "MODS",
(Language::German, "mods_title") => "God of War III Mod-Manager",
(Language::German, "mods_desc") => "Verwalten Sie Charakter-, Kostüm- und Textur-Mods. Sicheres Laden über ein virtuelles Dateisystem.",
(Language::German, "mods_open_folder") => "Mods-Ordner öffnen",
(Language::German, "mods_refresh") => "Mods aktualisieren",
(Language::German, "mods_count_summary") => "Installiert: {} | Aktiv: {} | Deaktiviert: {}",
(Language::German, "mods_empty_title") => "Noch keine Mods installiert",
(Language::German, "mods_empty_desc") => "Klicken Sie auf 'Mods-Ordner öffnen', fügen Sie Ihren Mod-Ordner ein und klicken Sie auf 'Mods aktualisieren'.",
(Language::German, "mods_status_active") => "● Im Spiel aktiv",
(Language::German, "mods_status_disabled") => "○ Deaktiviert",
(Language::German, "mods_author") => "Autor",
(Language::German, "mods_files_replaced") => "Ersetzte Dateien",
(Language::German, "nav_settings") => "EINSTELLUNGEN",
(Language::German, "nav_graphics") => "GRAFIK",
(Language::German, "nav_game_location") => "SPIELPFAD",
(Language::German, "nav_controls") => "STEUERUNG",
(Language::German, "nav_shaders") => "SHADER-EINSTELLUNGEN",
(Language::German, "launcher_version_label") => "LAUNCHER-VERSION",
(Language::German, "lang_options_title") => "SPRACHOPTIONEN",
(Language::German, "system_specs_title") => "SYSTEMSPEZIFIKATIONEN",
(Language::German, "play_btn_label") => "SPIELEN",
(Language::German, "menu_play") => "SPIELEN",
(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.1)",
(Language::German, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.1 NEUIGKEITEN:\n\n• 🎮 Discord Rich Presence Integration:\n - Zeigt 'Spielt God of War III Remastered' mit Kratos-Cover und Spielzeitzähler in deinem Discord-Profil an.\n - Automatische Statusmeldungen für Hauptmenü, Shader-Kompilierung und Spielverlauf.\n - In den Einstellungen ein-/ausschaltbar mit optionaler benutzerdefinierter Client-ID.\n\n• ⚡ Keine Performance-Einbußen & Nahtlose Verbindung.",
(Language::German, "discord_rpc_toggle") => "Discord Rich Presence (Spielstatus)",
(Language::German, "discord_rpc_desc") => "Zeigt 'Spielt God of War III Remastered' und die Spielzeit in deinem Discord-Profil an.",
(Language::German, "discord_client_id_label") => "Discord Anwendungs-ID (Client ID):",
(Language::German, "discord_reset_btn") => "Standard wiederherstellen",
(Language::German, "gamemode_toggle") => "Feral GameMode (gamemoderun)",
(Language::German, "gamemode_desc") => "Optimiert CPU-Governor und GPU für maximale Gaming-Leistung.",
(Language::German, "save_apply") => "Einstellungen speichern & anwenden",
@@ -617,6 +733,11 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::German, "latest_commit") => "Neuester Commit:",
(Language::German, "commit_date") => "Datum:",
(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_now") => "🚀 Jetzt aktualisieren",
(Language::German, "updating_in_progress") => "⏳ Update wird heruntergeladen und installiert... Bitte warten...",
(Language::German, "python_dep") => "Python 3 Abhängigkeit:",
(Language::German, "python_ready") => "Installiert / Bereit",
(Language::German, "python_missing") => "Nicht gefunden",
@@ -731,6 +852,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Spanish, "tag_hardware") => "H A R D W A R E Y S I S T E M A",
(Language::Spanish, "tag_game_location") => "U B I C A C I Ó N D E L J U E G O",
(Language::Spanish, "tag_patches") => "R E N D I M I E N T O Y P A R C H E S",
(Language::Spanish, "tag_mods") => "M O D S D E L J U E G O",
(Language::Spanish, "tag_display") => "P A N T A L L A Y G R Á F I C O S",
(Language::Spanish, "tag_controller") => "M A N D O Y G A M E P A D",
(Language::Spanish, "tag_keyboard") => "A S I G N A C I Ó N D E T E C L A S",
@@ -788,6 +910,8 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Spanish, "async_readbacks_desc") => "Desacopla la CPU de las esperas de la GPU para texturas. Elimina caídas de FPS y tirones para una experiencia fluida. Recomendado: ACTIVADO.",
(Language::Spanish, "launch_while_compiling_shaders") => "Iniciar Juego Mientras se Compilan los Shaders",
(Language::Spanish, "launch_while_compiling_shaders_desc") => "Activado: El juego se abre inmediatamente y compila en segundo plano. Desactivado: El juego espera a que los shaders estén al 100% para evitar tirones.",
(Language::Spanish, "show_shader_rage_meter") => "Visualización de Compilación de Shaders (Barra de Furia Espartana)",
(Language::Spanish, "show_shader_rage_meter_desc") => "Activado: Muestra el progreso de compilación con la barra de Furia Espartana y contador de pipelines (espera en pantalla negra al 100% hasta que la ventana del juego esté lista). Desactivado (Modo PS4): Oculta la barra y muestra solo el arte de inicio animado de PS4 (pic1.png).",
(Language::Spanish, "shader_cache_title") => "⚡ Caché Oficial de Shaders (Estilo Steam Pre-Caching)",
(Language::Spanish, "shader_cache_desc") => "Caché oficial limpia y segura con más de 28.000 shaders precompilados para todas las áreas y jefes. Elimina completamente tirones y congelaciones.",
(Language::Spanish, "shader_download_btn") => "☁️ Descargar e Instalar Caché Completa de Shaders (17 MB)",
@@ -799,6 +923,38 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Spanish, "shader_success") => "✅ ¡Caché de shaders instalada con éxito! ({} shaders en caché)",
(Language::Spanish, "skip_cinematics") => "Omitir Cinemáticas con el Botón 'X'",
(Language::Spanish, "skip_cinematics_desc") => "Permite saltar cualquier cinemática o vídeo pulsando el botón X del mando.",
(Language::Spanish, "nav_home") => "INICIO",
(Language::Spanish, "nav_mods") => "MODS",
(Language::Spanish, "mods_title") => "Gestor de Mods de God of War III",
(Language::Spanish, "mods_desc") => "Administra fácilmente mods de personajes, trajes y texturas. Se cargan de forma segura sin modificar archivos originales.",
(Language::Spanish, "mods_open_folder") => "Abrir Carpeta de Mods",
(Language::Spanish, "mods_refresh") => "Actualizar Mods",
(Language::Spanish, "mods_count_summary") => "Instalados: {} | Activos: {} | Desactivados: {}",
(Language::Spanish, "mods_empty_title") => "No hay mods instalados",
(Language::Spanish, "mods_empty_desc") => "Haz clic en 'Abrir Carpeta de Mods', coloca la carpeta de tu mod dentro y haz clic en 'Actualizar Mods'.",
(Language::Spanish, "mods_status_active") => "● Activo en el Juego",
(Language::Spanish, "mods_status_disabled") => "○ Desactivado",
(Language::Spanish, "mods_author") => "Autor",
(Language::Spanish, "mods_files_replaced") => "Archivos Reemplazados",
(Language::Spanish, "nav_settings") => "AJUSTES",
(Language::Spanish, "nav_graphics") => "GRÁFICOS",
(Language::Spanish, "nav_game_location") => "UBICACIÓN DEL JUEGO",
(Language::Spanish, "nav_controls") => "CONTROLES",
(Language::Spanish, "nav_shaders") => "AJUSTES DE SHADER",
(Language::Spanish, "launcher_version_label") => "VERSIÓN DEL LAUNCHER",
(Language::Spanish, "lang_options_title") => "OPCIONES DE IDIOMA",
(Language::Spanish, "system_specs_title") => "ESPECIFICACIONES DEL SISTEMA",
(Language::Spanish, "play_btn_label") => "JUGAR",
(Language::Spanish, "menu_play") => "JUGAR",
(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.1)",
(Language::Spanish, "release_notes_content") => "🔥 NOVEDADES DE GOD OF WAR III LAUNCHER v2.1:\n\n• 🎮 Integración con Discord Rich Presence:\n - Muestra 'Jugando a God of War III Remastered' con arte de Kratos y contador de tiempo en tu perfil de Discord.\n - Estados automáticos: En el Menú Principal, Compilando Shaders y Escalando el Monte Olimpo.\n - Se puede activar/desactivar en Ajustes con soporte para Client ID personalizado.\n\n• ⚡ Cero impacto en el rendimiento y conexión inteligente.",
(Language::Spanish, "discord_rpc_toggle") => "Discord Rich Presence (Estado de Juego)",
(Language::Spanish, "discord_rpc_desc") => "Muestra 'Jugando a God of War III Remastered' y el tiempo de juego en tu perfil de Discord.",
(Language::Spanish, "discord_client_id_label") => "ID de Aplicación de Discord (Client ID):",
(Language::Spanish, "discord_reset_btn") => "Restablecer por Defecto",
(Language::Spanish, "gamemode_toggle") => "Feral GameMode (gamemoderun)",
(Language::Spanish, "gamemode_desc") => "Optimiza la CPU y GPU para máximo rendimiento en juegos.",
(Language::Spanish, "save_apply") => "Guardar y Aplicar",
@@ -841,6 +997,11 @@ pub fn text(lang: Language, key: &str) -> &'static str {
(Language::Spanish, "latest_commit") => "Último Commit:",
(Language::Spanish, "commit_date") => "Fecha:",
(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_now") => "🚀 Actualizar ahora",
(Language::Spanish, "updating_in_progress") => "⏳ Descargando e instalando actualización... Por favor espere...",
(Language::Spanish, "python_dep") => "Dependencia Python 3:",
(Language::Spanish, "python_ready") => "Instalado / Listo",
(Language::Spanish, "python_missing") => "No encontrado",
+1 -1
View File
@@ -43,7 +43,7 @@ pub fn init_logger(base_dir: &Path) {
write_log(
"INFO",
&format!(
"God of War III Launcher Log started (v1.9) | OS: {} | Arch: {}",
"God of War III Launcher Log started (v2.0) | OS: {} | Arch: {}",
std::env::consts::OS,
std::env::consts::ARCH
),
+11 -4
View File
@@ -9,6 +9,9 @@ mod system;
mod tuner;
mod updater;
mod shader_downloader;
pub mod mods_manager;
pub mod discord_rpc;
pub mod win_input;
use app::LauncherApp;
use eframe::egui;
@@ -58,9 +61,9 @@ fn main() -> eframe::Result<()> {
}
let mut viewport = egui::ViewportBuilder::default()
.with_title("God of War® III Remastered - Launcher")
.with_inner_size([800.0, 720.0])
.with_min_inner_size([650.0, 550.0])
.with_title("God of War® III Remastered")
.with_inner_size([1024.0, 576.0])
.with_min_inner_size([854.0, 480.0])
.with_resizable(true);
if let Some(icon) = load_window_icon(&base_dir) {
@@ -79,7 +82,7 @@ fn main() -> eframe::Result<()> {
Box::new(move |cc| {
egui_extras::install_image_loaders(&cc.egui_ctx);
// Register God of War Font
// Register God of War Font and make it primary for Spartan style with unicode fallback
let mut fonts = egui::FontDefinitions::default();
fonts.font_data.insert(
"GodOfWar".to_owned(),
@@ -89,6 +92,10 @@ fn main() -> eframe::Result<()> {
egui::FontFamily::Name("GodOfWar".into()),
vec!["GodOfWar".to_owned()],
);
// Prepend GodOfWar to Proportional font family so headers and labels use it automatically
if let Some(prop) = fonts.families.get_mut(&egui::FontFamily::Proportional) {
prop.insert(0, "GodOfWar".to_owned());
}
cc.egui_ctx.set_fonts(fonts);
Ok(Box::new(LauncherApp::new(cc, base_dir_clone)))
+207
View File
@@ -0,0 +1,207 @@
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledMod {
pub folder_name: String,
pub title: String,
pub author: String,
pub version: String,
pub description: String,
pub file_count: usize,
pub sample_files: Vec<String>,
pub is_enabled: bool,
}
pub fn resolve_mods_dir(base_dir: &Path) -> PathBuf {
if base_dir.join("mods").is_dir() {
base_dir.join("mods")
} else if base_dir.join("gow3-linux/mods").is_dir() {
base_dir.join("gow3-linux/mods")
} else {
base_dir.join("mods")
}
}
pub fn open_mods_folder(base_dir: &Path) {
let dir = resolve_mods_dir(base_dir);
let _ = std::fs::create_dir_all(&dir);
#[cfg(target_os = "linux")]
{
let _ = std::process::Command::new("xdg-open").arg(&dir).spawn();
}
#[cfg(target_os = "windows")]
{
let _ = std::process::Command::new("explorer").arg(&dir).spawn();
}
}
pub fn scan_installed_mods(base_dir: &Path, disabled_mods: &[String]) -> Vec<InstalledMod> {
let mods_dir = resolve_mods_dir(base_dir);
if !mods_dir.is_dir() {
let _ = std::fs::create_dir_all(&mods_dir);
return Vec::new();
}
let mut list = Vec::new();
if let Ok(entries) = std::fs::read_dir(&mods_dir) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let folder_name = entry.file_name().to_string_lossy().to_string();
if folder_name.starts_with('.')
|| folder_name.eq_ignore_ascii_case("gow3-windows")
|| folder_name.eq_ignore_ascii_case("gow3-linux")
|| path.join("run.py").is_file()
{
continue;
}
let mut title = folder_name.clone();
let mut author = "Bilinmeyen Yazar".to_string();
let mut version = "v1.0".to_string();
let mut description = String::new();
// 1. Try reading mod.json
let json_path = path.join("mod.json");
if json_path.is_file() {
if let Ok(text) = std::fs::read_to_string(&json_path) {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&text) {
if let Some(n) = val["name"].as_str() {
title = n.to_string();
}
if let Some(a) = val["author"].as_str() {
author = a.to_string();
}
if let Some(v) = val["version"].as_str() {
version = v.to_string();
}
if let Some(d) = val["description"].as_str() {
description = d.to_string();
}
}
}
}
// 2. Try reading README.txt if description or author is still default
if description.is_empty() {
let readme_candidates = [
path.join("README.txt"),
path.join("readme.txt"),
path.join("README.md"),
path.join("readme.md"),
];
for r in &readme_candidates {
if r.is_file() {
if let Ok(text) = std::fs::read_to_string(r) {
for line in text.lines() {
let l = line.trim();
if let Some(stripped) = l.strip_prefix("Mod Name :") {
if title == folder_name {
title = stripped.trim().to_string();
}
} else if let Some(stripped) = l.strip_prefix("Author :") {
author = stripped.trim().to_string();
} else if let Some(stripped) = l.strip_prefix("Description :") {
description = stripped.trim().to_string();
} else if let Some(stripped) = l.strip_prefix("Version :") {
version = stripped.trim().to_string();
}
}
if description.is_empty() {
description = text.chars().take(220).collect();
}
break;
}
}
}
}
// 3. Scan mod files
let mut file_count = 0;
let mut sample_files = Vec::new();
scan_mod_files_recursive(&path, &mut file_count, &mut sample_files);
let is_enabled = !disabled_mods.contains(&folder_name);
list.push(InstalledMod {
folder_name,
title,
author,
version,
description,
file_count,
sample_files,
is_enabled,
});
}
}
list.sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase()));
list
}
fn scan_mod_files_recursive(dir: &Path, count: &mut usize, samples: &mut Vec<String>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
scan_mod_files_recursive(&p, count, samples);
} else if p.is_file() {
let name = entry.file_name().to_string_lossy().to_string();
let lower = name.to_lowercase();
if lower.ends_with(".txt")
|| lower.ends_with(".md")
|| lower.ends_with(".json")
|| lower.ends_with(".jpg")
|| lower.ends_with(".png")
|| lower.ends_with(".ini")
{
continue;
}
*count += 1;
if samples.len() < 3 {
samples.push(name);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scan_installed_mods() {
let base_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf();
let mods = scan_installed_mods(&base_dir, &[]);
assert!(mods.len() >= 2, "Should discover at least CJ and Golden Blades");
let cj = mods.iter().find(|m| m.folder_name.contains("CJ"));
assert!(cj.is_some(), "CJ mod should be found");
let cj_mod = cj.unwrap();
assert_eq!(cj_mod.file_count, 17);
assert!(cj_mod.is_enabled);
let gb = mods.iter().find(|m| m.folder_name.contains("Golden Blades"));
assert!(gb.is_some(), "Golden Blades mod should be found");
let gb_mod = gb.unwrap();
assert_eq!(gb_mod.file_count, 1);
assert_eq!(gb_mod.author, "Machi");
assert!(gb_mod.is_enabled);
}
#[test]
fn test_disabled_mods() {
let base_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf();
let disabled = vec!["Golden Blades".to_string()];
let mods = scan_installed_mods(&base_dir, &disabled);
let gb = mods.iter().find(|m| m.folder_name == "Golden Blades").unwrap();
assert!(!gb.is_enabled);
}
}
+438 -13
View File
@@ -70,9 +70,27 @@ pub fn ensure_runtime_assets(base_dir: &Path) -> Result<(PathBuf, PathBuf), Stri
let _ = fs::write(&patch_xml, PATCH_GOW3_XML);
}
ensure_game_icon_bmp(base_dir);
Ok((scripts_dir, patches_dir))
}
/// Ensures gow3.bmp is available for SDL3 window icon loading.
pub fn ensure_game_icon_bmp(base_dir: &Path) {
let bmp_path = base_dir.join("gow3.bmp");
if !bmp_path.is_file() {
let png_path = base_dir.join("gow3.png");
let img = if png_path.is_file() {
image::open(&png_path).ok()
} else {
image::load_from_memory(include_bytes!("../../gow3.png")).ok()
};
if let Some(img) = img {
let _ = img.save_with_format(&bmp_path, image::ImageFormat::Bmp);
}
}
}
/// Finds an available Python 3 interpreter (checking local runtime bundle first, then system PATH)
pub fn find_python(base_dir: &Path) -> Result<String, String> {
// 1. Check local runtime python folder (.runtime/python/python.exe or bin/python3)
@@ -313,6 +331,224 @@ fn find_file_recursive(dir: &Path, filename: &str) -> Option<PathBuf> {
None
}
fn collect_xml_files(dir: &Path, xml_paths: &mut Vec<PathBuf>) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext.eq_ignore_ascii_case("xml") {
xml_paths.push(path);
}
}
} else if path.is_dir() {
collect_xml_files(&path, xml_paths);
}
}
}
}
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");
@@ -747,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;
@@ -762,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;
}
@@ -848,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());
@@ -888,11 +1136,22 @@ pub fn launch_gow3(
let python = ensure_python(base_dir)?;
// 1. Run mods.py
let mods_dir = base_dir.join("mods");
let mods_dir = if base_dir.join("mods").is_dir() {
base_dir.join("mods")
} else if base_dir.join("gow3-linux/mods").is_dir() {
base_dir.join("gow3-linux/mods")
} else {
base_dir.join("mods")
};
if !mods_dir.exists() {
let _ = std::fs::create_dir_all(&mods_dir);
}
let _ = Command::new(&python)
config.save_mods_config(base_dir);
let mods_config_path = base_dir.join("mods_config.json");
let mut mods_cmd = Command::new(&python);
mods_cmd
.arg(scripts_path.join("mods.py"))
.arg(game_dir)
.arg("--out")
@@ -900,12 +1159,45 @@ pub fn launch_gow3(
.arg("--mods-dir")
.arg(&mods_dir)
.arg("--enabled")
.arg("1")
.arg("1");
if mods_config_path.exists() {
mods_cmd.arg("--config").arg(&mods_config_path);
}
let mut effective_app0 = game_dir.to_path_buf();
let mut mods_applied = false;
if let Ok(output) = mods_cmd
.env("PYTHONPATH", &scripts_path)
.current_dir(base_dir)
.stdout(Stdio::from(log_file.try_clone().unwrap()))
.stderr(Stdio::from(log_file_err.try_clone().unwrap()))
.status();
.output()
{
if let Ok(mut lf) = log_file.try_clone() {
use std::io::Write;
let _ = lf.write_all(&output.stdout);
let _ = lf.write_all(&output.stderr);
}
if output.status.success() {
let out_str = String::from_utf8_lossy(&output.stdout);
if let Some(last_line) = out_str.lines().filter(|l| !l.trim().is_empty()).last() {
let candidate = PathBuf::from(last_line.trim());
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)
@@ -1026,7 +1318,20 @@ pub fn launch_gow3(
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());
@@ -1036,6 +1341,54 @@ pub fn launch_gow3(
}
let patches_str = patch_list.join(";");
// Prepare active patches directory: copy built-in patch XML + XML patches from enabled mods
let active_patches_dir = out_dir.join("active_patches");
let _ = fs::remove_dir_all(&active_patches_dir);
let _ = fs::create_dir_all(&active_patches_dir);
// Copy built-in XML
let builtin_xml = patches_path.join("God_of_War_III_Remastered.xml");
if builtin_xml.is_file() {
let _ = fs::copy(&builtin_xml, active_patches_dir.join("God_of_War_III_Remastered.xml"));
}
// Collect and copy all XML patches from enabled mods
if mods_dir.is_dir() {
if let Ok(entries) = fs::read_dir(&mods_dir) {
for entry in entries.flatten() {
let mod_path = entry.path();
if !mod_path.is_dir() {
continue;
}
let mod_name = entry.file_name().to_string_lossy().to_string();
if mod_name.starts_with('.')
|| mod_name.eq_ignore_ascii_case("gow3-windows")
|| mod_name.eq_ignore_ascii_case("gow3-linux")
|| mod_path.join("run.py").is_file()
{
continue;
}
if config.disabled_mods.contains(&mod_name) {
continue;
}
let mut xml_files = Vec::new();
collect_xml_files(&mod_path, &mut xml_files);
for xml_file in xml_files {
let file_name = xml_file.file_name().unwrap_or_default().to_string_lossy();
let safe_mod: String = mod_name
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '_' })
.collect();
let dest = active_patches_dir.join(format!("{}_{}", safe_mod, file_name));
if fs::copy(&xml_file, &dest).is_ok() {
crate::log_info!("Mod XML patch applied: {:?}", xml_file);
}
}
}
}
}
let _ = Command::new(&python)
.arg(scripts_path.join("patches.py"))
.arg("--out")
@@ -1045,7 +1398,7 @@ pub fn launch_gow3(
.arg("--game-dir")
.arg(game_dir)
.arg("--patches-dir")
.arg(&patches_path)
.arg(&active_patches_dir)
.env("PYTHONPATH", &scripts_path)
.env("GOW3_FORCE_PATCHES", "1")
.env("GOW3_PATCHES_ONLY", "1")
@@ -1089,7 +1442,7 @@ pub fn launch_gow3(
.arg("--patches")
.arg(&patches_bin)
.arg("--app0")
.arg(game_dir)
.arg(&effective_app0)
.arg("--user")
.arg(&user_dir)
.arg("--timeout")
@@ -1253,18 +1606,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 {
@@ -1338,6 +1714,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);
@@ -1386,4 +1766,49 @@ mod tests {
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_collect_xml_files() {
let temp_dir = std::env::temp_dir().join("gow3_test_collect_xml");
let _ = fs::remove_dir_all(&temp_dir);
let sub = temp_dir.join("subdir/nested");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("patch1.xml"), "<Patch/>").unwrap();
fs::write(temp_dir.join("patch2.XML"), "<Patch/>").unwrap();
fs::write(temp_dir.join("readme.txt"), "info").unwrap();
let mut collected = Vec::new();
collect_xml_files(&temp_dir, &mut collected);
assert_eq!(collected.len(), 2);
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);
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ use std::process::Command;
pub const SHADER_REPO_URL: &str =
"https://raw.githubusercontent.com/ScreamAngel/gow3_launcher/main/shaders/gow3-shader-cache.tar.xz";
pub const SHADER_FALLBACK_URL: &str =
"https://github.com/ScreamAngel/gow3_launcher/releases/download/v1.9/gow3-shader-cache.tar.xz";
"https://github.com/ScreamAngel/gow3_launcher/releases/download/v2.0/gow3-shader-cache.tar.xz";
/// Resolves the shader cache directory for God of War III.
/// Handles both running from root repo or directly inside `gow3-linux`.
+13 -2
View File
@@ -34,13 +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..];
@@ -216,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(())
}
+49 -4
View File
@@ -5,10 +5,46 @@ use std::process::Command;
use crate::config::LauncherConfig;
use crate::tuner::apply_settings;
pub const BUILTIN_VERSION: &str = "v1.9";
pub const BUILTIN_VERSION: &str = "v2.1";
pub const GITHUB_REPO: &str = "ScreamAngel/gow3_launcher";
pub const GITHUB_REPO_URL: &str = "https://github.com/ScreamAngel/gow3_launcher.git";
#[derive(Debug, Clone)]
pub struct GithubReleaseNote {
pub tag_name: String,
pub title: String,
pub published_at: String,
pub body: String,
}
pub fn fetch_github_release_notes() -> Result<Vec<GithubReleaseNote>, String> {
let url = "https://api.github.com/repos/ScreamAngel/gow3_launcher/releases";
let body = http_get_string(url).ok_or_else(|| "GitHub Releases bağlantısı kurulamadı.".to_string())?;
let val: serde_json::Value = serde_json::from_str(&body).map_err(|e| format!("JSON parse hatası: {}", e))?;
let mut notes = Vec::new();
if let Some(arr) = val.as_array() {
for item in arr {
let tag = item["tag_name"].as_str().unwrap_or("").to_string();
let title = item["name"].as_str().unwrap_or("").to_string();
let published_at = item["published_at"].as_str().unwrap_or("").to_string();
let body_text = item["body"].as_str().unwrap_or("").to_string();
if !tag.is_empty() {
notes.push(GithubReleaseNote {
tag_name: tag,
title: if title.is_empty() { "Sürüm Notu".to_string() } else { title },
published_at,
body: body_text,
});
}
}
}
Ok(notes)
}
#[derive(Debug, Clone)]
pub struct UpdateInfo {
pub commit_sha: String,
@@ -426,10 +462,9 @@ mod tests {
assert!(info.is_new);
}
// Test with current version v1.9: should NOT report as an update
let res_current = check_github_update(Path::new(".."), "test_dummy_sha", "v1.9");
// Test with current version (v2.0.2): should NOT report as an update against current GitHub release (v2.0.1)
let res_current = check_github_update(Path::new(".."), "test_dummy_sha", BUILTIN_VERSION);
if let Ok(info) = res_current {
// Because remote release on GitHub is <= v1.9, and current is v1.9, and files are present, is_new must be false!
assert!(!info.is_new);
}
}
@@ -441,5 +476,15 @@ 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.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]
fn test_fetch_github_release_notes() {
if let Ok(notes) = fetch_github_release_notes() {
assert!(!notes.is_empty(), "Should fetch at least one release note from GitHub");
}
}
}
+345
View File
@@ -0,0 +1,345 @@
// 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;
}
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
}
}
#[cfg(not(target_os = "windows"))]
pub use stub::*;
+16 -1
View File
@@ -6,6 +6,8 @@
"patch_delta_time": true,
"patch_readbacks": true,
"async_readbacks": true,
"launch_while_compiling_shaders": false,
"show_shader_compile_rage_meter": false,
"skip_cinematics": true,
"use_gamemode": true,
"vram_gc_budget_mb": 6144,
@@ -20,7 +22,20 @@
"cpu_threads": 20
},
"last_git_commit": "0b4abe07acb711848d1b5d72402a9de8c0ad9844",
"launcher_version": "v1.8.5",
"launcher_version": "v2.1",
"discord_rpc_enabled": true,
"discord_client_id": "1124351711486689290",
"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"
],
"render_resolution": "native",
"fsr_enabled": false,
"disable_motion_blur": false,
+14
View File
@@ -0,0 +1,14 @@
{
"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"
],
"order": []
}
+121 -30
View File
@@ -19,24 +19,40 @@ def child(folder, name):
return folder / name
KNOWN_GOW3_DIRS = {
'exec', 'dutch', 'english', 'french', 'german',
'italian', 'polish', 'portuguese', 'russian', 'spanish'
}
def content_root(folder):
"""(root, prefix): where a mod's files are and the game path they go to, or None.
Accepted layouts: <mod>/dvdroot_ps4, <mod>/app0/dvdroot_ps4 and one wrapper folder around
either (an archive extracted into a folder of its name)."""
Accepted layouts: <mod>/dvdroot_ps4, <mod>/app0/dvdroot_ps4, <mod>/EXEC,
<mod>/Game/EXEC, XML patches, and wrapper folders around them."""
folder = Path(folder)
if not folder.is_dir():
return None
for wrapper in ('', 'app0'):
base = child(folder, wrapper) if wrapper else folder
dvdroot = child(base, 'dvdroot_ps4')
if dvdroot.is_dir():
return base, ''
entries = [e for e in folder.iterdir() if not e.name.startswith('.')]
folders = [e for e in entries if e.is_dir()]
if len(folders) == 1 and not [e for e in entries if e.is_file() and
e.suffix.casefold() not in ('.txt', '.md', '.jpg', '.png', '.ini')]:
return content_root(folders[0])
if folder.name.casefold() in ('gow3-windows', 'gow3-linux') or (folder / 'run.py').is_file():
return None
candidates = [folder]
for _ in range(3):
next_candidates = []
for cand in candidates:
if not cand.is_dir():
continue
if child(cand, 'dvdroot_ps4').is_dir():
return cand, ''
for d in KNOWN_GOW3_DIRS:
if child(cand, d).is_dir():
return cand, ''
if list(cand.glob('*.xml')) or child(cand, 'patches').is_dir():
return cand, ''
for sub in cand.iterdir():
if sub.is_dir() and not sub.name.startswith('.'):
next_candidates.append(sub)
candidates = next_candidates
return None
@@ -67,10 +83,12 @@ def selected(root, config):
def mod_files(folder):
"""(game path, source) of every file a mod replaces or adds under dvdroot_ps4."""
"""(game path, source) of every file a mod replaces or adds under the game root."""
layout = content_root(folder)
if not layout:
raise ValueError(f'{folder}: expected dvdroot_ps4 (or chr/, parts/, ...) inside the mod folder')
if list(Path(folder).rglob('*.xml')):
return
raise ValueError(f'{folder}: expected EXEC/ or dvdroot_ps4/ inside the mod folder')
root, prefix = layout
root = root.resolve()
for directory, folders, files in os.walk(root, followlinks=False):
@@ -81,14 +99,24 @@ def mod_files(folder):
for name in sorted(files):
source = directory / name
relative = Path(prefix) / source.relative_to(root) if prefix else source.relative_to(root)
# Readme/metadata stay outside the mounted game. This loader handles assets;
# executable patches use the existing patch compiler, with address validation.
if relative.parts[0].casefold() != 'dvdroot_ps4':
if relative.parts[0].casefold() in ('eboot.bin', 'sce_module', 'sce_sys'):
# Skip documentation / metadata at the mod root
if len(relative.parts) == 1:
if source.suffix.casefold() in ('.txt', '.md', '.json', '.jpg', '.png', '.ini'):
continue
# Strip packaging wrapper prefixes (app0, dvdroot_ps4, game) so files align with the game root
while len(relative.parts) > 1 and relative.parts[0].casefold() in ('app0', 'dvdroot_ps4', 'game'):
relative = Path(*relative.parts[1:])
# Skip patch XML files in mod folder (handled by patches.py)
if source.suffix.casefold() == '.xml':
continue
if relative.parts[0].casefold() in ('eboot.bin', 'sce_module', 'sce_sys'):
if relative.parts[0].casefold() in ('eboot.bin', 'sce_module'):
raise ValueError(f'Executable/system replacement is unsupported: {source}')
continue
if not source.is_file():
raise ValueError(f'Not a regular mod file: {source}')
continue
yield relative, source
@@ -123,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
@@ -131,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):
@@ -152,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):
@@ -174,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():
@@ -183,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 = []
@@ -201,6 +289,9 @@ def build_overlay(game, out, mods):
return game
out = Path(out).resolve()
out.mkdir(parents=True, exist_ok=True)
for old_dir in out.glob('mod-game-*'):
if old_dir.is_dir():
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
@@ -227,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
+2 -2
View File
@@ -134,7 +134,7 @@ def external_patches(directory, app_version, exclude, ids):
key is "<file name>/<patch name>" (the launcher's selection, patches.json)."""
found=[]
directory=Path(directory)
for path in sorted(directory.glob('*.xml')) if directory.is_dir() else []:
for path in sorted(directory.rglob('*.xml')) if directory.is_dir() else []:
if path.resolve()==Path(exclude).resolve(): continue # the built-in file (patches/ in a checkout)
try:
root=ET.parse(path).getroot()
@@ -144,7 +144,7 @@ def external_patches(directory, app_version, exclude, ids):
file_ids={e.text.strip() for e in root.iter('ID') if e.text}
if file_ids and not file_ids&ids: continue
for meta in root.iter('Metadata'):
if meta.get('AppVer')==app_version and meta.get('AppElf','eboot.bin')=='eboot.bin':
if (meta.get('AppVer')==app_version or meta.get('AppVer') in ('01.00', '01.02')) and meta.get('AppElf','eboot.bin')=='eboot.bin':
found.append((f'{path.name}/{meta.get("Name")}',path,meta))
return found