Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08da3bec2c | ||
|
|
04f61833ea | ||
|
|
a60dc8bace | ||
|
|
7b8d9155fd | ||
|
|
ec0a5893c7 | ||
|
|
6bb6349c41 | ||
|
|
8ba512f45e | ||
|
|
0136d4a426 | ||
|
|
71b9cd8596 | ||
|
|
b6409869aa | ||
|
|
ff7c026bba | ||
|
|
f119d84439 | ||
|
|
4b5ec4f7f5 | ||
|
|
5ddf4eab9d | ||
|
|
839dabca0d | ||
|
|
319a4c56e8 | ||
|
|
0c9f36c292 | ||
|
|
bdb896b279 | ||
|
|
b435c8344b | ||
|
|
12dea62e3b | ||
|
|
192f9164d1 | ||
|
|
9fdbeb5650 | ||
|
|
95db57f9a1 | ||
|
|
ba4c08dcea | ||
|
|
e9f6c33021 | ||
|
|
234dae1972 | ||
|
|
ea0f8b7658 | ||
|
|
418e035def | ||
|
|
0079e3b5a6 | ||
|
|
ccc9765bf5 | ||
|
|
aff5ec53c8 | ||
|
|
7f7a00755c | ||
|
|
243dbf7c67 | ||
|
|
2a9a6480b5 | ||
|
|
ee36d5fb36 | ||
|
|
acf9d08527 | ||
|
|
4980c43d53 | ||
|
|
f36b0f3287 | ||
|
|
94e71041e4 | ||
|
|
3f269176da | ||
|
|
3786b49355 | ||
|
|
5bdc44159c | ||
|
|
0730f2da7d | ||
|
|
a9592e555b | ||
|
|
feea7e7975 | ||
|
|
f6df71dfb7 | ||
|
|
2e40762963 | ||
|
|
27e27eea3b | ||
|
|
008aa107a2 | ||
|
|
9d0691d5de | ||
|
|
28d5506758 | ||
|
|
97bed359e8 | ||
|
|
f4dd0eb1b5 | ||
|
|
f5fb6a70df | ||
|
|
1d357ca52a | ||
|
|
56e77b172b | ||
|
|
bfaf219d8d | ||
|
|
b8423a0760 | ||
|
|
de79b29de5 | ||
|
|
90cae4b6f7 | ||
|
|
88ad672c6e | ||
|
|
521e87e8c4 | ||
|
|
bb76fa8621 | ||
|
|
6c4e36ec08 | ||
|
|
34387f6c87 | ||
|
|
634571103b | ||
|
|
a709098214 | ||
|
|
b63a55cf02 | ||
|
|
5be05f0024 | ||
|
|
779fce78ab | ||
|
|
d7aafd49e9 | ||
|
|
93e2d8d1fb | ||
|
|
57698ae376 | ||
|
|
50285f0ff8 | ||
|
|
b58b282a35 | ||
|
|
72ec274d33 | ||
|
|
a69d9090c9 | ||
|
|
5b74ff4a6a | ||
|
|
7b2160537f | ||
|
|
2bc81ff974 | ||
|
|
151c976598 | ||
|
|
f92f09da53 | ||
|
|
764b5541d7 | ||
|
|
5f7e3bb850 | ||
|
|
67bc336216 | ||
|
|
8e4ebbbcb9 | ||
|
|
1b2e0b566a | ||
|
|
10a9a95862 | ||
|
|
fa50be4b24 | ||
|
|
4093ee43c1 | ||
|
|
4f6c6788d0 | ||
|
|
47437d280b | ||
|
|
e82257aa27 | ||
|
|
5991b03cd0 | ||
|
|
dc3df87384 | ||
|
|
a9e5233ae7 | ||
|
|
410c5fc0b7 | ||
|
|
e6cec74c96 | ||
|
|
c65f1acb1b | ||
|
|
f1b3cfae29 | ||
|
|
0614038175 | ||
|
|
66efe4ec0e | ||
|
|
0a7fb20aa9 | ||
|
|
709845f472 | ||
|
|
f90f25f033 | ||
|
|
08e46e6675 | ||
|
|
29e57c38e5 | ||
|
|
1ade24e88c | ||
|
|
b7e366e893 | ||
|
|
1179c01ddf | ||
|
|
765fa79ecd | ||
|
|
01771af1d7 | ||
|
|
5d1de303af | ||
|
|
4326563b9d | ||
|
|
bc48a858f0 | ||
|
|
d96f683944 |
No files matched your search
@@ -1,122 +0,0 @@
|
||||
name: Build Cross-Platform Launchers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
name: Build Linux Launcher
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev libgl1-mesa-dev
|
||||
|
||||
- name: Build Linux Launcher (Release)
|
||||
working-directory: launcher
|
||||
run: cargo build --release
|
||||
|
||||
- name: Prepare binary
|
||||
run: |
|
||||
cp launcher/target/release/launcher ./gow3-launcher
|
||||
chmod +x ./gow3-launcher
|
||||
|
||||
- name: Upload Linux Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gow3-launcher-linux
|
||||
path: ./gow3-launcher
|
||||
|
||||
build-windows:
|
||||
name: Build Windows Launcher
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Build Windows Launcher (Release)
|
||||
working-directory: launcher
|
||||
run: cargo build --release
|
||||
|
||||
- name: Prepare binary
|
||||
run: |
|
||||
Copy-Item launcher/target/release/launcher.exe -Destination ./gow3-launcher-win.exe
|
||||
|
||||
- name: Upload Windows Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gow3-launcher-windows
|
||||
path: ./gow3-launcher-win.exe
|
||||
|
||||
publish-release:
|
||||
name: Publish Release & Sync Windows Binary
|
||||
needs: [build-linux, build-windows]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Linux binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gow3-launcher-linux
|
||||
path: ./artifacts/
|
||||
|
||||
- name: Download Windows binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gow3-launcher-windows
|
||||
path: ./artifacts/
|
||||
|
||||
- name: Commit Windows launcher to repo root
|
||||
run: |
|
||||
cp ./artifacts/gow3-launcher-win.exe ./gow3-launcher-win.exe
|
||||
git config --local user.email "[email protected]"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add gow3-launcher-win.exe
|
||||
git diff --quiet && git diff --staged --quiet || (git commit -m "chore(build): sync gow3-launcher-win.exe to repo root [skip ci]" && git push origin main)
|
||||
|
||||
- name: Create or Update GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: latest
|
||||
name: "God of War III Native Launcher (Dual-Boot & Cross-Platform)"
|
||||
body: |
|
||||
## 🚀 God of War III Remastered - Native Launcher (Linux & Windows)
|
||||
|
||||
Aynı klasörde yan yana çalışabilen yerel ikili dosyalar:
|
||||
- 🐧 **Linux**: `gow3-launcher`
|
||||
- 🪟 **Windows / Dual-Boot**: `gow3-launcher-win.exe`
|
||||
|
||||
### ✨ Özellikler
|
||||
- **Dual-Boot Desteği:** Aynı klasörde hem Windows hem Linux için yan yana bağımsız launcher'lar!
|
||||
- **Modern Kayan Anahtarlar:** Klasik kutucuklar yerine animasyonlu Switch teması
|
||||
- **Kararlı 60 FPS / 120 FPS / 144 FPS / 240 FPS Pacing:** Fizik hızlanma sorunu olmadan akıcı oynanış
|
||||
- **Sinematikleri 'X' Tuşuyla Atlama Yaması:** Ara sahneleri kumandanızın X tuşuyla geçebilme seçeneği
|
||||
- **Sarı Doku Hatası Önleme Yaması (Instant Readbacks):** Karlı ve parlak sahnelerdeki renk bozulmasını engeller
|
||||
- **Dahili Donanım ve Ekran Hz Tespiti:** GPU, VRAM, RAM ve ekran yenileme hızına göre otomatik yapılandırma
|
||||
- **4 Farklı Dil Desteği:** Türkçe 🇹🇷, English 🇬🇧, Deutsch 🇩🇪, Español 🇪🇸
|
||||
- **%100 Rust Uyumlu Self-Updater:** Doğrudan GitHub üzerinden en son sürüme tek tıkla güncelleme
|
||||
files: |
|
||||
./artifacts/gow3-launcher
|
||||
./artifacts/gow3-launcher-win.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,154 @@
|
||||
name: Build & Release Single Executables (AppImage & Portable Exe)
|
||||
|
||||
on:
|
||||
# Otomatik release/tag build tetikleyicisi geçici olarak devre dışı bırakıldı
|
||||
# push:
|
||||
# tags:
|
||||
# - 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
name: Build Linux AppImage & Binary
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev libgl1-mesa-dev wget
|
||||
|
||||
- name: Build Linux Launcher (Release)
|
||||
working-directory: launcher
|
||||
run: cargo build --release
|
||||
|
||||
- name: Build AppImage
|
||||
run: |
|
||||
mkdir -p AppDir/usr/bin
|
||||
mkdir -p AppDir/usr/share/applications
|
||||
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
|
||||
cp launcher/target/release/launcher AppDir/usr/bin/gow3-launcher
|
||||
cp gow3.png AppDir/usr/share/icons/hicolor/256x256/apps/gow3.png
|
||||
cp gow3.png AppDir/gow3.png
|
||||
|
||||
cat << 'EOF' > AppDir/gow3-launcher.desktop
|
||||
[Desktop Entry]
|
||||
Name=God of War III Launcher
|
||||
Exec=gow3-launcher
|
||||
Icon=gow3
|
||||
Type=Application
|
||||
Categories=Game;
|
||||
Terminal=false
|
||||
EOF
|
||||
cp AppDir/gow3-launcher.desktop AppDir/usr/share/applications/
|
||||
|
||||
cat << 'EOF' > AppDir/AppRun
|
||||
#!/bin/bash
|
||||
HERE="$(dirname "$(readlink -f "${0}")")"
|
||||
export PATH="${HERE}/usr/bin:${PATH}"
|
||||
exec "${HERE}/usr/bin/gow3-launcher" "$@"
|
||||
EOF
|
||||
chmod +x AppDir/AppRun
|
||||
|
||||
wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool
|
||||
chmod +x appimagetool
|
||||
./appimagetool --appimage-extract
|
||||
ARCH=x86_64 ./squashfs-root/AppRun AppDir God_of_War_III_Launcher-x86_64.AppImage
|
||||
|
||||
cp launcher/target/release/launcher ./gow3-launcher
|
||||
|
||||
- name: Upload Linux Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-artifacts
|
||||
path: |
|
||||
God_of_War_III_Launcher-x86_64.AppImage
|
||||
gow3-launcher
|
||||
|
||||
build-windows:
|
||||
name: Build Windows Portable Exe
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Build Windows Launcher (Release)
|
||||
working-directory: launcher
|
||||
run: cargo build --release
|
||||
|
||||
- name: Prepare Windows Executables
|
||||
run: |
|
||||
Copy-Item launcher/target/release/launcher.exe -Destination ./gow3-launcher-win.exe
|
||||
Copy-Item launcher/target/release/launcher.exe -Destination ./God_of_War_III_Launcher-Windows.exe
|
||||
|
||||
- name: Upload Windows Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-artifacts
|
||||
path: |
|
||||
gow3-launcher-win.exe
|
||||
God_of_War_III_Launcher-Windows.exe
|
||||
|
||||
publish-release:
|
||||
name: Publish Release with Assets
|
||||
needs: [build-linux, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Linux artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: linux-artifacts
|
||||
path: ./dist/
|
||||
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-artifacts
|
||||
path: ./dist/
|
||||
|
||||
- name: Create or Update GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: "God of War III Launcher ${{ github.ref_name }}"
|
||||
draft: false
|
||||
prerelease: false
|
||||
make_latest: true
|
||||
body: |
|
||||
## 🚀 God of War III Remastered - Single Executable Release
|
||||
|
||||
Tek tıkla çalışan taşınabilir yürütülebilir dosyalar (Single Executables):
|
||||
|
||||
### 📦 İndirme Seçenekleri:
|
||||
- 🐧 **Linux (AppImage - Tek Dosya Kurulumsuz):** [`God_of_War_III_Launcher-x86_64.AppImage`](./God_of_War_III_Launcher-x86_64.AppImage)
|
||||
- *Kullanım:* İndirin, sağ tıklayıp özelliklerden "Çalıştırılabilir yap" (veya `chmod +x`) deyin ve çift tıklayın!
|
||||
- 🪟 **Windows (Portable Exe - Kurulumsuz):** [`God_of_War_III_Launcher-Windows.exe`](./God_of_War_III_Launcher-Windows.exe) / [`gow3-launcher-win.exe`](./gow3-launcher-win.exe)
|
||||
- 🐧 **Linux (Native ELF):** [`gow3-launcher`](./gow3-launcher)
|
||||
|
||||
### ✨ Yenilikler (${{ github.ref_name }})
|
||||
- **AppImage Desteği:** Linux üzerinde bağımlılık derdi olmadan tek tıkla çalışan resmi AppImage paketi.
|
||||
- **Windows Portable Exe:** Harici dosyalara ihtiyaç duymadan kendi simgesi ve arayüzüyle açılan taşınabilir Windows sürümü.
|
||||
- **Akıllı Konum & Donanım Tespiti:** Launcher artık doğrudan kendi bulunduğu dizini tanır, taşınabilir çalışır.
|
||||
- **Gelişmiş Oyun Arama:** Disklerde oyun bulunamadığında açıklayıcı modal uyarı penceresi ve anlık doğrulama.
|
||||
- **Otomatik Yama & FPS Ayarları:** 60/120/144/240 Hz pacing, sarı doku önleme ve sinematik atlama.
|
||||
files: |
|
||||
./dist/God_of_War_III_Launcher-x86_64.AppImage
|
||||
./dist/God_of_War_III_Launcher-Windows.exe
|
||||
./dist/gow3-launcher-win.exe
|
||||
./dist/gow3-launcher
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -10,6 +10,7 @@ user/
|
||||
out/
|
||||
GLCache/
|
||||
.runtime/
|
||||
mod-game-*/
|
||||
vkd3d-proton.cache
|
||||
fsr4_shaders/
|
||||
.local/
|
||||
@@ -18,6 +19,7 @@ __pycache__/
|
||||
# Logs & Configs generated during play
|
||||
*.log
|
||||
gow3_log.txt
|
||||
launcher_log.txt
|
||||
last_run.log
|
||||
gow3.ini
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -1,42 +1,55 @@
|
||||
# gow3_pclauncher
|
||||
Launcher for God Of War 3 PC on Linux
|
||||
# gow3_launcher
|
||||
Launcher for God Of War 3 PC on Linux and Windows
|
||||
|
||||
# ⚡ God of War® III Remastered - Linux Port & Native Launcher
|
||||
**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 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.
|
||||
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.
|
||||
|
||||
[](https://kernel.org)
|
||||
[](https://kernel.org)
|
||||
[](https://www.rust-lang.org)
|
||||
[](https://www.vulkan.org)
|
||||
[](LICENSE)
|
||||
[](LICENSE)
|
||||
Website: [https://screamangel.github.io/gow3-site/](https://screamangel.github.io/gow3-site/)
|
||||
|
||||
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
- 🦀 **Native Rust GUI Launcher (`gow3-launcher`):** Built with `eframe`/`egui`. Ultra-fast, lightweight (~17 MB standalone), with a sleek game-art interface and no heavy
|
||||
dependencies.
|
||||
- 🖥️ **Hardware Auto-Detection:** Automatically inspects GPU model, dedicated VRAM, system RAM, and display refresh rate (Hz) on launch.
|
||||
- 🦀 **Native Rust GUI Launcher (`gow3-launcher`):** Built with `eframe`/`egui`. Ultra-fast, lightweight, with a sleek game-art interface and no heavy dependencies.
|
||||
- 🖥️ **Hardware Auto-Detection:** Automatically inspects GPU model, CPU model, dedicated VRAM, system RAM, and display refresh rate (Hz) on launch.
|
||||
- ⏱️ **144Hz / High-Hz Delta-Time Sync:** Eliminates the infamous 2x speed bug on 144Hz+ gaming displays by syncing engine delta-time with target framerates.
|
||||
- 🎨 **Texture Glitch Fix (Instant Readbacks):** Prevents snowy, bright, or reflection textures from corrupting into golden-yellow artifacts.
|
||||
- 🎮 **Feral GameMode Integration:** Built-in toggle for `gamemoderun` to maximize CPU governor and GPU clocks during gameplay.
|
||||
- 🔍 **Smart Game Scanner & Completeness Check:** Scans drives to locate `eboot.bin` dumps, validates file integrity (`param.sfo`, elf size), and prevents launching corrupted
|
||||
directories.
|
||||
- 🚨 **Crash & Error Diagnostic Assistant:** Automatically analyzes `gow3_log.txt` on exit. If an unexpected crash occurs, it displays a plain-language diagnosis and recommended fix.
|
||||
- 🔄 **GitHub Upstream Sync & Auto-Updater:** Checks upstream ([R-V-L/gow3_pc](https://github.com/R-V-L/gow3_pc)) for updates and re-applies all your custom directory paths, 144Hz patches, and VRAM budgets seamlessly.
|
||||
- 🌐 **Multi-Language Support:** Available in **Turkish 🇹🇷**, **English 🇬🇧**, **German 🇩🇪**, and **Spanish 🇪🇸**.
|
||||
|
||||
- 🔄 **GitHub Upstream Sync & Auto-Updater:** Checks for updates in Upstream ([ScreamAngel/gow3_launcher](https://github.com/ScreamAngel/gow3_launcher)) and updates the launcher to the latest version.
|
||||
- 🌐 **Multi-Language Support:** Available in **Turkish 🇹🇷**, **English 🇬🇧**, **German 🇩🇪**, and **Spanish 🇪🇸**.
|
||||
|
||||
|
||||
|
||||
## 📋 System Requirements
|
||||
|
||||
| Component | Minimum | Recommended |
|
||||
|---|---|---|
|
||||
| **OS** | Linux (Arch, Ubuntu, Fedora, SteamOS, Debian) | Any modern 64-bit Linux distro |
|
||||
| **OS** | Linux (Arch, Ubuntu, Fedora, SteamOS, Debian) and Windows 10-11 | Any modern 64-bit OS |
|
||||
| **CPU** | 6-core x86_64 processor | 8-core / 16-thread modern CPU |
|
||||
| **GPU** | Vulkan 1.3 compatible (4 GB VRAM) | Vulkan 1.3+ (6 GB+ VRAM, Nvidia / AMD) |
|
||||
| **RAM** | 8 GB | 16 GB+ |
|
||||
| **Dependencies** | Python 3, Vulkan drivers | `gamemode` (optional) |
|
||||
| **Dependencies** | Python 3, Vulkan drivers | `gamemode` (optional) |
|
||||
|
||||
## Video
|
||||
|
||||
You can watch the video for details.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youtube.com/watch?v=-yGAEjdQGzE"><img width="1920" height="1080" alt="vlc_ie5PhePn2o" src="https://github.com/user-attachments/assets/190f531e-55b0-4a4a-b883-440cd615a18a" /></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -46,18 +59,47 @@ Launcher for God Of War 3 PC on Linux
|
||||
|
||||
**Option A: Clone with Git**
|
||||
```bash
|
||||
git clone https://github.com/ScreamAngel/gow3_pclauncher.git
|
||||
cd gow3_pclauncher
|
||||
git clone https://github.com/ScreamAngel/gow3_launcher.git
|
||||
cd gow3_launcher
|
||||
```
|
||||
- 🐧 **On Linux**: `./gow3-launcher`
|
||||
- 🪟 **On Windows**: `gow3-launcher-win.exe`
|
||||
|
||||
**Option B: Download Pre-compiled Binaries (GitHub Releases)**
|
||||
Download the executables directly from the GitHub Releases tab:
|
||||
- **Linux**: `gow3-launcher`
|
||||
- **Windows / Dual-Boot**: `gow3-launcher-win.exe`
|
||||
**Option B: Download from release tab or website**
|
||||
- Extract the downloaded file from the archive. Then, depending on your operating system, select and run the appropriate file ( “gow3-launcher1” for Linux or “gow3-launcher-win.exe” for Windows).
|
||||
|
||||
|
||||
The launcher is completely standalone: it automatically detects your hardware, verifies the game dump, configures settings files, and applies all required patches without requiring external bash scripts.
|
||||
|
||||
|
||||
### Sections of the launcher
|
||||
|
||||
**Home Screen**
|
||||
<img width="1901" height="1025" alt="image" src="https://github.com/user-attachments/assets/31385740-38a4-4ddd-bf5b-3d795930c51f" />
|
||||
|
||||
|
||||
|
||||
**Graphics Settings**
|
||||
<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" />
|
||||
|
||||
**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" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 2. Add Your Game Dump
|
||||
@@ -75,6 +117,9 @@ Launcher for God Of War 3 PC on Linux
|
||||
.\gow3-launcher-win.exe --play
|
||||
```
|
||||
|
||||
<img width="1920" height="1080" alt="gow3-probe_Hz51yYBACH" src="https://github.com/user-attachments/assets/651de8e7-15b9-454c-ab6d-a09dbd71f44e" />
|
||||
|
||||
|
||||
### 🔄 Dual-Boot Ready
|
||||
Both `gow3-launcher` and `gow3-launcher-win.exe` live side-by-side in the same directory. If you dual-boot between Linux and Windows, you can play on either operating system seamlessly while sharing the same game dumps and configurations!
|
||||
|
||||
@@ -95,6 +140,8 @@ Launcher for God Of War 3 PC on Linux
|
||||
Ensure **"Refresh Rate (Hz) Sync Patch"** is enabled and your target FPS matches your display.
|
||||
- **Game crashes during heavy combat or asset streaming?**
|
||||
Check if your GPU has at least 6GB VRAM. You can adjust the VRAM Garbage Collection Budget directly in the launcher.
|
||||
- **Does the Launcher have a keyboard configuration?**
|
||||
On the “Configuration & Controls” tab, you can assign any button to a function of your choice. DualShock 4 support is available.
|
||||
|
||||
|
||||
|
||||
@@ -104,23 +151,24 @@ Launcher for God Of War 3 PC on Linux
|
||||
|
||||
### Özellikler:
|
||||
1. **Tek ve Sade Rust Launcher:** Karmaşık `.sh` scriptleri kaldırılmış, tüm pipeline doğrudan `gow3-launcher` içine gömülmüştür.
|
||||
2. **Otomatik Donanım Algılama:** GPU, VRAM, RAM ve monitör yenileme hızınızı (Hz) anında tespit eder.
|
||||
2. **Otomatik Donanım Algılama:** GPU, CPU, VRAM, RAM ve monitör yenileme hızınızı (Hz) anında tespit eder.
|
||||
3. **144 Hz Düzeltmesi:** Yüksek yenileme hızlı monitörlerde oyunun hızlı akmasını (2x speed) engelleyen Delta-Time yaması.
|
||||
4. **Doku Bozulması Koruması:** Karlı sahnelerdeki sararma hatasını çözen Instant Readbacks yaması.
|
||||
5. **Feral GameMode:** Tek tıkla `gamemoderun` performans modu desteği.
|
||||
6. **Akıllı Hata Teşhisi:** Oyun kapanırsa log dosyasını analiz edip hatanın sebebini ve çözümünü arayüzde bildirir.
|
||||
7. **Steam / Steam Deck Desteği:** `--play` parametresi ile arayüze girmeden doğrudan oyunu başlatabilme.
|
||||
8. **Klavye Konfigürasyonu:** "Yapılandırma & Kontroller" sekmesinden tüm tuşlara klavyeden bir tuş atayabilirsiniz. Ayrıca Dualshock 4 desteği mevcuttur.
|
||||
|
||||
|
||||
|
||||
## 📜 Credits & Acknowledgments
|
||||
|
||||
- **Original Project:** https://github.com/FelipePrado0/gow3_pc
|
||||
- **Original Project:** [FelipePrado0](https://github.com/FelipePrado0/gow3_pc)
|
||||
- **Upstream Linux Port & Probe:** Developed by [R-V-L](https://github.com/R-V-L/gow3_pc) and community contributors.
|
||||
- **Launcher & Hardware Tuner:** Built with Rust, [egui/eframe](https://github.com/emilk/egui).
|
||||
|
||||
> **Disclaimer:** God of War is a registered trademark of Sony Interactive Entertainment LLC and Santa Monica Studio. This repository **does not contain any copyrighted game assets, ROMs, or binaries**. Users must supply their own legally obtained game files.
|
||||
|
||||
|
||||
#Licence
|
||||
## License
|
||||
GNU GPL v2 or later ([LICENSE](https://github.com/R-V-L/gow3_pc/blob/main/LICENSE)). Third-party components keep their own licenses.
|
||||
@@ -0,0 +1,417 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// God of War III Remastered - Linux & Windows Input Bridge & Mouse Injector
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <strings.h>
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
// Scancodes used for mouse buttons (F13 - F19)
|
||||
#define SCANCODE_MOUSE_LEFT 104 // F13
|
||||
#define SCANCODE_MOUSE_RIGHT 105 // F14
|
||||
#define SCANCODE_MOUSE_MIDDLE 106 // F15
|
||||
#define SCANCODE_MOUSE_SIDE1 107 // F16
|
||||
#define SCANCODE_MOUSE_SIDE2 108 // F17
|
||||
#define SCANCODE_MOUSE_WHEELUP 109 // F18
|
||||
#define SCANCODE_MOUSE_WHEELDN 110 // F19
|
||||
|
||||
typedef const bool* (*RealSDL_GetKeyboardState)(int *numkeys);
|
||||
typedef SDL_MouseButtonFlags (*RealSDL_GetMouseState)(float *x, float *y);
|
||||
typedef SDL_MouseButtonFlags (*RealSDL_GetGlobalMouseState)(float *x, float *y);
|
||||
typedef SDL_Window* (*RealSDL_GetKeyboardFocus)(void);
|
||||
typedef SDL_Window* (*RealSDL_GetMouseFocus)(void);
|
||||
typedef bool (*RealSDL_CursorVisible)(void);
|
||||
typedef bool (*RealSDL_HideCursor)(void);
|
||||
typedef bool (*RealSDL_ShowCursor)(void);
|
||||
typedef bool (*RealSDL_PollEvent)(SDL_Event *event);
|
||||
typedef SDL_Scancode (*RealSDL_GetScancodeFromName)(const char *name);
|
||||
typedef SDL_Surface* (*RealSDL_CreateSurface)(int width, int height, SDL_PixelFormat format);
|
||||
typedef void (*RealSDL_DestroySurface)(SDL_Surface *surface);
|
||||
typedef SDL_Cursor* (*RealSDL_CreateColorCursor)(SDL_Surface *surface, int hot_x, int hot_y);
|
||||
typedef bool (*RealSDL_SetCursor)(SDL_Cursor *cursor);
|
||||
typedef SDL_Window* (*RealSDL_CreateWindowWithProperties)(SDL_PropertiesID props);
|
||||
typedef SDL_Window* (*RealSDL_CreateWindow)(const char *title, int w, int h, SDL_WindowFlags flags);
|
||||
typedef bool (*RealSDL_ShowWindow)(SDL_Window *window);
|
||||
typedef bool (*RealSDL_HideWindow)(SDL_Window *window);
|
||||
typedef bool (*RealSDL_RaiseWindow)(SDL_Window *window);
|
||||
|
||||
static RealSDL_GetKeyboardState real_GetKeyboardState = NULL;
|
||||
static RealSDL_GetMouseState real_GetMouseState = NULL;
|
||||
static RealSDL_GetGlobalMouseState real_GetGlobalMouseState = NULL;
|
||||
static RealSDL_GetKeyboardFocus real_GetKeyboardFocus = NULL;
|
||||
static RealSDL_GetMouseFocus real_GetMouseFocus = NULL;
|
||||
static RealSDL_CursorVisible real_CursorVisible = NULL;
|
||||
static RealSDL_HideCursor real_HideCursor = NULL;
|
||||
static RealSDL_ShowCursor real_ShowCursor = NULL;
|
||||
static RealSDL_PollEvent real_PollEvent = NULL;
|
||||
static RealSDL_GetScancodeFromName real_GetScancodeFromName = NULL;
|
||||
static RealSDL_CreateSurface real_CreateSurface = NULL;
|
||||
static RealSDL_DestroySurface real_DestroySurface = NULL;
|
||||
static RealSDL_CreateColorCursor real_CreateColorCursor = NULL;
|
||||
static RealSDL_SetCursor real_SetCursor = NULL;
|
||||
static RealSDL_CreateWindowWithProperties real_CreateWindowWithProperties = NULL;
|
||||
static RealSDL_CreateWindow real_CreateWindow = NULL;
|
||||
static RealSDL_ShowWindow real_ShowWindow = NULL;
|
||||
static RealSDL_HideWindow real_HideWindow = NULL;
|
||||
static RealSDL_RaiseWindow real_RaiseWindow = NULL;
|
||||
|
||||
static int hide_cursor_enabled = -1;
|
||||
static int wait_warmup_enabled = -1;
|
||||
static volatile bool warmup_done = false;
|
||||
static SDL_Window* tracked_game_window = NULL;
|
||||
static volatile uint32_t tracked_mouse_buttons = 0;
|
||||
static volatile uint32_t latched_mouse_buttons = 0;
|
||||
static volatile int is_window_focused = 1;
|
||||
static volatile int wheel_up_ticks = 0;
|
||||
static volatile int wheel_dn_ticks = 0;
|
||||
static SDL_Cursor* transparent_cursor = NULL;
|
||||
static volatile bool cursor_is_hidden = false;
|
||||
|
||||
static void apply_cursor_hidden(void) {
|
||||
if (!hide_cursor_enabled) return;
|
||||
|
||||
if (!transparent_cursor && real_CreateSurface && real_CreateColorCursor && real_DestroySurface) {
|
||||
SDL_Surface* s = real_CreateSurface(1, 1, SDL_PIXELFORMAT_RGBA32);
|
||||
if (s) {
|
||||
uint32_t* pix = (uint32_t*)s->pixels;
|
||||
if (pix) *pix = 0; // 100% transparent pixel
|
||||
transparent_cursor = real_CreateColorCursor(s, 0, 0);
|
||||
real_DestroySurface(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (transparent_cursor && real_SetCursor) {
|
||||
real_SetCursor(transparent_cursor);
|
||||
}
|
||||
if (real_HideCursor) {
|
||||
real_HideCursor();
|
||||
}
|
||||
cursor_is_hidden = true;
|
||||
}
|
||||
|
||||
static void init_symbols(void) {
|
||||
if (!real_GetKeyboardState) {
|
||||
#ifdef _WIN32
|
||||
HMODULE h = GetModuleHandleA("SDL3.dll");
|
||||
if (!h) h = LoadLibraryA("SDL3.dll");
|
||||
if (h) {
|
||||
real_GetKeyboardState = (RealSDL_GetKeyboardState)GetProcAddress(h, "SDL_GetKeyboardState");
|
||||
real_GetMouseState = (RealSDL_GetMouseState)GetProcAddress(h, "SDL_GetMouseState");
|
||||
real_GetGlobalMouseState = (RealSDL_GetGlobalMouseState)GetProcAddress(h, "SDL_GetGlobalMouseState");
|
||||
real_GetKeyboardFocus = (RealSDL_GetKeyboardFocus)GetProcAddress(h, "SDL_GetKeyboardFocus");
|
||||
real_GetMouseFocus = (RealSDL_GetMouseFocus)GetProcAddress(h, "SDL_GetMouseFocus");
|
||||
real_CursorVisible = (RealSDL_CursorVisible)GetProcAddress(h, "SDL_CursorVisible");
|
||||
real_HideCursor = (RealSDL_HideCursor)GetProcAddress(h, "SDL_HideCursor");
|
||||
real_ShowCursor = (RealSDL_ShowCursor)GetProcAddress(h, "SDL_ShowCursor");
|
||||
real_PollEvent = (RealSDL_PollEvent)GetProcAddress(h, "SDL_PollEvent");
|
||||
real_GetScancodeFromName = (RealSDL_GetScancodeFromName)GetProcAddress(h, "SDL_GetScancodeFromName");
|
||||
real_CreateSurface = (RealSDL_CreateSurface)GetProcAddress(h, "SDL_CreateSurface");
|
||||
real_DestroySurface = (RealSDL_DestroySurface)GetProcAddress(h, "SDL_DestroySurface");
|
||||
real_CreateColorCursor = (RealSDL_CreateColorCursor)GetProcAddress(h, "SDL_CreateColorCursor");
|
||||
real_SetCursor = (RealSDL_SetCursor)GetProcAddress(h, "SDL_SetCursor");
|
||||
real_CreateWindowWithProperties = (RealSDL_CreateWindowWithProperties)GetProcAddress(h, "SDL_CreateWindowWithProperties");
|
||||
real_CreateWindow = (RealSDL_CreateWindow)GetProcAddress(h, "SDL_CreateWindow");
|
||||
real_ShowWindow = (RealSDL_ShowWindow)GetProcAddress(h, "SDL_ShowWindow");
|
||||
real_HideWindow = (RealSDL_HideWindow)GetProcAddress(h, "SDL_HideWindow");
|
||||
real_RaiseWindow = (RealSDL_RaiseWindow)GetProcAddress(h, "SDL_RaiseWindow");
|
||||
}
|
||||
#else
|
||||
real_GetKeyboardState = (RealSDL_GetKeyboardState)dlsym(RTLD_NEXT, "SDL_GetKeyboardState");
|
||||
if (!real_GetKeyboardState) {
|
||||
void* h = dlopen("libSDL3.so.0", RTLD_NOW | RTLD_GLOBAL);
|
||||
if (!h) h = dlopen("libSDL3.so", RTLD_NOW | RTLD_GLOBAL);
|
||||
if (h) {
|
||||
real_GetKeyboardState = (RealSDL_GetKeyboardState)dlsym(h, "SDL_GetKeyboardState");
|
||||
real_GetMouseState = (RealSDL_GetMouseState)dlsym(h, "SDL_GetMouseState");
|
||||
real_GetGlobalMouseState = (RealSDL_GetGlobalMouseState)dlsym(h, "SDL_GetGlobalMouseState");
|
||||
real_GetKeyboardFocus = (RealSDL_GetKeyboardFocus)dlsym(h, "SDL_GetKeyboardFocus");
|
||||
real_GetMouseFocus = (RealSDL_GetMouseFocus)dlsym(h, "SDL_GetMouseFocus");
|
||||
real_CursorVisible = (RealSDL_CursorVisible)dlsym(h, "SDL_CursorVisible");
|
||||
real_HideCursor = (RealSDL_HideCursor)dlsym(h, "SDL_HideCursor");
|
||||
real_ShowCursor = (RealSDL_ShowCursor)dlsym(h, "SDL_ShowCursor");
|
||||
real_PollEvent = (RealSDL_PollEvent)dlsym(h, "SDL_PollEvent");
|
||||
real_GetScancodeFromName = (RealSDL_GetScancodeFromName)dlsym(h, "SDL_GetScancodeFromName");
|
||||
real_CreateSurface = (RealSDL_CreateSurface)dlsym(h, "SDL_CreateSurface");
|
||||
real_DestroySurface = (RealSDL_DestroySurface)dlsym(h, "SDL_DestroySurface");
|
||||
real_CreateColorCursor = (RealSDL_CreateColorCursor)dlsym(h, "SDL_CreateColorCursor");
|
||||
real_SetCursor = (RealSDL_SetCursor)dlsym(h, "SDL_SetCursor");
|
||||
real_CreateWindowWithProperties = (RealSDL_CreateWindowWithProperties)dlsym(h, "SDL_CreateWindowWithProperties");
|
||||
real_CreateWindow = (RealSDL_CreateWindow)dlsym(h, "SDL_CreateWindow");
|
||||
real_ShowWindow = (RealSDL_ShowWindow)dlsym(h, "SDL_ShowWindow");
|
||||
real_HideWindow = (RealSDL_HideWindow)dlsym(h, "SDL_HideWindow");
|
||||
real_RaiseWindow = (RealSDL_RaiseWindow)dlsym(h, "SDL_RaiseWindow");
|
||||
}
|
||||
} else {
|
||||
real_GetMouseState = (RealSDL_GetMouseState)dlsym(RTLD_NEXT, "SDL_GetMouseState");
|
||||
real_GetGlobalMouseState = (RealSDL_GetGlobalMouseState)dlsym(RTLD_NEXT, "SDL_GetGlobalMouseState");
|
||||
real_GetKeyboardFocus = (RealSDL_GetKeyboardFocus)dlsym(RTLD_NEXT, "SDL_GetKeyboardFocus");
|
||||
real_GetMouseFocus = (RealSDL_GetMouseFocus)dlsym(RTLD_NEXT, "SDL_GetMouseFocus");
|
||||
real_CursorVisible = (RealSDL_CursorVisible)dlsym(RTLD_NEXT, "SDL_CursorVisible");
|
||||
real_HideCursor = (RealSDL_HideCursor)dlsym(RTLD_NEXT, "SDL_HideCursor");
|
||||
real_ShowCursor = (RealSDL_ShowCursor)dlsym(RTLD_NEXT, "SDL_ShowCursor");
|
||||
real_PollEvent = (RealSDL_PollEvent)dlsym(RTLD_NEXT, "SDL_PollEvent");
|
||||
real_GetScancodeFromName = (RealSDL_GetScancodeFromName)dlsym(RTLD_NEXT, "SDL_GetScancodeFromName");
|
||||
real_CreateSurface = (RealSDL_CreateSurface)dlsym(RTLD_NEXT, "SDL_CreateSurface");
|
||||
real_DestroySurface = (RealSDL_DestroySurface)dlsym(RTLD_NEXT, "SDL_DestroySurface");
|
||||
real_CreateColorCursor = (RealSDL_CreateColorCursor)dlsym(RTLD_NEXT, "SDL_CreateColorCursor");
|
||||
real_SetCursor = (RealSDL_SetCursor)dlsym(RTLD_NEXT, "SDL_SetCursor");
|
||||
real_CreateWindowWithProperties = (RealSDL_CreateWindowWithProperties)dlsym(RTLD_NEXT, "SDL_CreateWindowWithProperties");
|
||||
real_CreateWindow = (RealSDL_CreateWindow)dlsym(RTLD_NEXT, "SDL_CreateWindow");
|
||||
real_ShowWindow = (RealSDL_ShowWindow)dlsym(RTLD_NEXT, "SDL_ShowWindow");
|
||||
real_HideWindow = (RealSDL_HideWindow)dlsym(RTLD_NEXT, "SDL_HideWindow");
|
||||
real_RaiseWindow = (RealSDL_RaiseWindow)dlsym(RTLD_NEXT, "SDL_RaiseWindow");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (hide_cursor_enabled == -1) {
|
||||
const char *env = getenv("GOW3_HIDE_CURSOR");
|
||||
hide_cursor_enabled = (env && (env[0] == '1' || env[0] == 't' || env[0] == 'T')) ? 1 : 0;
|
||||
}
|
||||
|
||||
if (wait_warmup_enabled == -1) {
|
||||
const char *w_env = getenv("GOW3_WAIT_WARMUP");
|
||||
wait_warmup_enabled = (w_env && (w_env[0] == '1' || w_env[0] == 't' || w_env[0] == 'T')) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
SDL_Window* SDL_CreateWindowWithProperties(SDL_PropertiesID props) {
|
||||
init_symbols();
|
||||
SDL_Window* win = NULL;
|
||||
if (real_CreateWindowWithProperties) {
|
||||
win = real_CreateWindowWithProperties(props);
|
||||
}
|
||||
if (win) {
|
||||
tracked_game_window = win;
|
||||
apply_cursor_hidden();
|
||||
if (wait_warmup_enabled && !warmup_done) {
|
||||
if (real_HideWindow) {
|
||||
real_HideWindow(win);
|
||||
}
|
||||
}
|
||||
}
|
||||
return win;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
SDL_Window* SDL_CreateWindow(const char *title, int w, int h, SDL_WindowFlags flags) {
|
||||
init_symbols();
|
||||
SDL_Window* win = NULL;
|
||||
if (real_CreateWindow) {
|
||||
win = real_CreateWindow(title, w, h, flags);
|
||||
}
|
||||
if (win) {
|
||||
tracked_game_window = win;
|
||||
apply_cursor_hidden();
|
||||
if (wait_warmup_enabled && !warmup_done) {
|
||||
if (real_HideWindow) {
|
||||
real_HideWindow(win);
|
||||
}
|
||||
}
|
||||
}
|
||||
return win;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
bool SDL_ShowWindow(SDL_Window *window) {
|
||||
init_symbols();
|
||||
if (window) {
|
||||
tracked_game_window = window;
|
||||
}
|
||||
if (wait_warmup_enabled && !warmup_done) {
|
||||
// Keep window hidden until shader warmup is 100% complete
|
||||
if (real_HideWindow && window) {
|
||||
real_HideWindow(window);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (real_ShowWindow && window) {
|
||||
return real_ShowWindow(window);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
SDL_Scancode SDL_GetScancodeFromName(const char *name) {
|
||||
init_symbols();
|
||||
if (!name) return SDL_SCANCODE_UNKNOWN;
|
||||
|
||||
// Direct mapping for custom mouse buttons
|
||||
#ifdef _WIN32
|
||||
#define MATCH(s) (_stricmp(name, s) == 0)
|
||||
#else
|
||||
#define MATCH(s) (strcasecmp(name, s) == 0)
|
||||
#endif
|
||||
|
||||
if (MATCH("LeftButton") || MATCH("Mouse1") || MATCH("LeftClick")) {
|
||||
return (SDL_Scancode)SCANCODE_MOUSE_LEFT; // 104 (F13)
|
||||
}
|
||||
if (MATCH("RightButton") || MATCH("Mouse2") || MATCH("RightClick")) {
|
||||
return (SDL_Scancode)SCANCODE_MOUSE_RIGHT; // 105 (F14)
|
||||
}
|
||||
if (MATCH("MiddleButton") || MATCH("Mouse3") || MATCH("MiddleClick")) {
|
||||
return (SDL_Scancode)SCANCODE_MOUSE_MIDDLE; // 106 (F15)
|
||||
}
|
||||
if (MATCH("SideButtonBack") || MATCH("Mouse4") || MATCH("Side1")) {
|
||||
return (SDL_Scancode)SCANCODE_MOUSE_SIDE1; // 107 (F16)
|
||||
}
|
||||
if (MATCH("SideButtonForward") || MATCH("Mouse5") || MATCH("Side2")) {
|
||||
return (SDL_Scancode)SCANCODE_MOUSE_SIDE2; // 108 (F17)
|
||||
}
|
||||
if (MATCH("MouseWheelUp") || MATCH("WheelUp")) {
|
||||
return (SDL_Scancode)SCANCODE_MOUSE_WHEELUP; // 109 (F18)
|
||||
}
|
||||
if (MATCH("MouseWheelDown") || MATCH("WheelDown")) {
|
||||
return (SDL_Scancode)SCANCODE_MOUSE_WHEELDN; // 110 (F19)
|
||||
}
|
||||
|
||||
#undef MATCH
|
||||
|
||||
if (real_GetScancodeFromName) {
|
||||
return real_GetScancodeFromName(name);
|
||||
}
|
||||
return SDL_SCANCODE_UNKNOWN;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
const bool* SDL_GetKeyboardState(int *numkeys) {
|
||||
init_symbols();
|
||||
if (!real_GetKeyboardState) return NULL;
|
||||
|
||||
const bool* orig_state = real_GetKeyboardState(numkeys);
|
||||
if (!orig_state) return NULL;
|
||||
|
||||
bool* state = (bool*)orig_state;
|
||||
|
||||
// Query mouse buttons & inject into keyboard scancode array
|
||||
// tracked_mouse_buttons: continuously held buttons
|
||||
// latched_mouse_buttons: quick clicks between poll frames, guaranteed not to be lost
|
||||
uint32_t buttons = tracked_mouse_buttons | latched_mouse_buttons;
|
||||
latched_mouse_buttons = 0; // consumed by this frame
|
||||
|
||||
if (real_GetMouseState) {
|
||||
float mx = 0.0f, my = 0.0f;
|
||||
buttons |= real_GetMouseState(&mx, &my);
|
||||
}
|
||||
if (!buttons && real_GetGlobalMouseState && is_window_focused) {
|
||||
float mx = 0.0f, my = 0.0f;
|
||||
buttons |= real_GetGlobalMouseState(&mx, &my);
|
||||
}
|
||||
|
||||
state[SCANCODE_MOUSE_LEFT] = (buttons & SDL_BUTTON_LMASK) != 0;
|
||||
state[SCANCODE_MOUSE_RIGHT] = (buttons & SDL_BUTTON_RMASK) != 0;
|
||||
state[SCANCODE_MOUSE_MIDDLE] = (buttons & SDL_BUTTON_MMASK) != 0;
|
||||
state[SCANCODE_MOUSE_SIDE1] = (buttons & SDL_BUTTON_X1MASK) != 0;
|
||||
state[SCANCODE_MOUSE_SIDE2] = (buttons & SDL_BUTTON_X2MASK) != 0;
|
||||
|
||||
if (wheel_up_ticks > 0) {
|
||||
state[SCANCODE_MOUSE_WHEELUP] = true;
|
||||
wheel_up_ticks--;
|
||||
}
|
||||
if (wheel_dn_ticks > 0) {
|
||||
state[SCANCODE_MOUSE_WHEELDN] = true;
|
||||
wheel_dn_ticks--;
|
||||
}
|
||||
|
||||
return orig_state;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
bool SDL_PollEvent(SDL_Event *event) {
|
||||
init_symbols();
|
||||
if (!cursor_is_hidden) {
|
||||
apply_cursor_hidden();
|
||||
}
|
||||
if (!real_PollEvent) return false;
|
||||
|
||||
bool result = real_PollEvent(event);
|
||||
if (result && event) {
|
||||
uint32_t event_type = event->type;
|
||||
|
||||
// Window focus gained / shown
|
||||
if (event_type == SDL_EVENT_WINDOW_FOCUS_GAINED || event_type == 514 /* SDL_EVENT_WINDOW_SHOWN */) {
|
||||
is_window_focused = 1;
|
||||
apply_cursor_hidden();
|
||||
} else if (event_type == SDL_EVENT_WINDOW_FOCUS_LOST) {
|
||||
is_window_focused = 0;
|
||||
tracked_mouse_buttons = 0;
|
||||
latched_mouse_buttons = 0;
|
||||
}
|
||||
// Mouse button press / release
|
||||
else if (event_type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
is_window_focused = 1;
|
||||
uint8_t btn = event->button.button;
|
||||
if (btn >= 1 && btn <= 5) {
|
||||
uint32_t mask = (1u << (btn - 1));
|
||||
tracked_mouse_buttons |= mask;
|
||||
latched_mouse_buttons |= mask;
|
||||
}
|
||||
} else if (event_type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
uint8_t btn = event->button.button;
|
||||
if (btn >= 1 && btn <= 5) {
|
||||
tracked_mouse_buttons &= ~(1u << (btn - 1));
|
||||
}
|
||||
} else if (event_type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
if (event->wheel.y > 0.0f) {
|
||||
wheel_up_ticks = 2;
|
||||
} else if (event->wheel.y < 0.0f) {
|
||||
wheel_dn_ticks = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Override Gow3Overlay::SetLoading and ImGui::ProgressBar to completely eliminate
|
||||
// the engine's plain generic loading bar in fullscreen mode, and reveal the window
|
||||
// once shader warmup is 100% complete.
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
void _ZN11Gow3Overlay10SetLoadingEbjj(bool is_loading, unsigned int current, unsigned int total) {
|
||||
init_symbols();
|
||||
if (!is_loading || (total > 0 && current >= total)) {
|
||||
warmup_done = true;
|
||||
if (wait_warmup_enabled && tracked_game_window) {
|
||||
if (real_ShowWindow) {
|
||||
real_ShowWindow(tracked_game_window);
|
||||
}
|
||||
if (real_RaiseWindow) {
|
||||
real_RaiseWindow(tracked_game_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
void _ZN5ImGui11ProgressBarEfRK6ImVec2PKc(float fraction, const void* size_arg, const char* overlay) {
|
||||
(void)fraction;
|
||||
(void)size_arg;
|
||||
(void)overlay;
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ vsync=1
|
||||
deferred_readback=1
|
||||
stale_readback=1
|
||||
async_shaders=1
|
||||
frames_queued=1
|
||||
frames_queued=2
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 449 KiB |
@@ -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,14 +1671,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "launcher"
|
||||
version = "0.1.0"
|
||||
version = "2.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
"image",
|
||||
"libc",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"winres",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2443,7 +2332,7 @@ checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"color",
|
||||
"kurbo 0.13.1",
|
||||
"kurbo",
|
||||
"linebender_resource_handle",
|
||||
"smallvec",
|
||||
]
|
||||
@@ -2541,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"
|
||||
@@ -2596,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"
|
||||
@@ -2836,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"
|
||||
@@ -2877,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"
|
||||
@@ -2953,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"
|
||||
@@ -3132,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"
|
||||
@@ -3276,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"
|
||||
@@ -3416,7 +3180,6 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png 0.17.16",
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
@@ -3450,6 +3213,15 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
@@ -3568,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"
|
||||
@@ -3615,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"
|
||||
@@ -3703,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"
|
||||
@@ -3945,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"
|
||||
@@ -4414,6 +4108,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winres"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
||||
dependencies = [
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
@@ -4489,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"
|
||||
@@ -4665,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"
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
[package]
|
||||
name = "launcher"
|
||||
version = "0.1.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"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
|
||||
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 55 KiB |
@@ -0,0 +1,15 @@
|
||||
fn main() {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
if target_os == "windows" {
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("gow3.ico");
|
||||
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", "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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 184 KiB |
@@ -2,12 +2,210 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum GraphicPreset {
|
||||
#[default]
|
||||
Balanced, // Standart / Orijinal (1080p)
|
||||
UltraLow, // Ultra Düşük Donanım (480p)
|
||||
Low, // Düşük Sistem / Steam Deck (720p)
|
||||
SteamDeck,// Steam Deck 16:10 (800p)
|
||||
Ultra, // Yüksek / 2K - 4K
|
||||
Custom, // Özel (Kullanıcı Ayarlı)
|
||||
}
|
||||
|
||||
impl GraphicPreset {
|
||||
#[allow(dead_code)]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
GraphicPreset::Balanced => "balanced",
|
||||
GraphicPreset::UltraLow => "ultra_low",
|
||||
GraphicPreset::Low => "low",
|
||||
GraphicPreset::SteamDeck => "steam_deck",
|
||||
GraphicPreset::Ultra => "ultra",
|
||||
GraphicPreset::Custom => "custom",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum DisplayMode {
|
||||
#[default]
|
||||
Borderless,
|
||||
Fullscreen,
|
||||
Windowed,
|
||||
}
|
||||
|
||||
impl DisplayMode {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
DisplayMode::Borderless => "borderless",
|
||||
DisplayMode::Fullscreen => "fullscreen",
|
||||
DisplayMode::Windowed => "windowed",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_toml_mode(&self) -> (&'static str, bool) {
|
||||
match self {
|
||||
DisplayMode::Borderless => ("Borderless", true),
|
||||
DisplayMode::Fullscreen => ("Fullscreen", true),
|
||||
DisplayMode::Windowed => ("Windowed", false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct KeyboardBindings {
|
||||
// Face buttons
|
||||
pub triangle: String,
|
||||
pub circle: String,
|
||||
pub cross: String,
|
||||
pub square: String,
|
||||
|
||||
// Shoulder buttons
|
||||
pub l1: String,
|
||||
pub r1: String,
|
||||
pub l2: String,
|
||||
pub r2: String,
|
||||
pub l3: String,
|
||||
pub r3: String,
|
||||
|
||||
// System
|
||||
pub options: String,
|
||||
pub touchpad_center: String,
|
||||
|
||||
// D-Pad
|
||||
pub pad_up: String,
|
||||
pub pad_down: String,
|
||||
pub pad_left: String,
|
||||
pub pad_right: String,
|
||||
|
||||
// Left Stick (Movement)
|
||||
pub axis_left_x_minus: String, // Sol (A)
|
||||
pub axis_left_x_plus: String, // Sağ (D)
|
||||
pub axis_left_y_minus: String, // İleri (W)
|
||||
pub axis_left_y_plus: String, // Geri (S)
|
||||
|
||||
// Right Stick (Camera)
|
||||
pub axis_right_x_minus: String, // Sol (J)
|
||||
pub axis_right_x_plus: String, // Sağ (L)
|
||||
pub axis_right_y_minus: String, // Yukarı (I)
|
||||
pub axis_right_y_plus: String, // Aşağı (K)
|
||||
}
|
||||
|
||||
impl Default for KeyboardBindings {
|
||||
fn default() -> Self {
|
||||
Self::recommended()
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardBindings {
|
||||
/// 1. Önerilen Preset (Klavye):
|
||||
/// - Üçgen: U
|
||||
/// - Kare: T
|
||||
/// - Yuvarlak: J
|
||||
/// - X: G
|
||||
/// - Sol Stick: W, A, S, D
|
||||
/// - Sağ Stick: Numpad 8 (Yukarı), Numpad 2 (Aşağı), Numpad 4 (Sol), Numpad 6 (Sağ)
|
||||
/// - Options: Enter
|
||||
/// - Share veya Select: Backspace
|
||||
/// - DPAD: Ok Tuşları (Yukarı, Aşağı, Sol, Sağ)
|
||||
/// - L1/R1/L2/R2/L3/R3: Q, E, Z, X, C, V
|
||||
pub fn recommended() -> Self {
|
||||
Self {
|
||||
triangle: "u".to_string(),
|
||||
circle: "j".to_string(),
|
||||
cross: "g".to_string(),
|
||||
square: "t".to_string(),
|
||||
l1: "q".to_string(),
|
||||
r1: "e".to_string(),
|
||||
l2: "z".to_string(),
|
||||
r2: "x".to_string(),
|
||||
l3: "c".to_string(),
|
||||
r3: "v".to_string(),
|
||||
options: "enter".to_string(),
|
||||
touchpad_center: "backspace".to_string(),
|
||||
pad_up: "up".to_string(),
|
||||
pad_down: "down".to_string(),
|
||||
pad_left: "left".to_string(),
|
||||
pad_right: "right".to_string(),
|
||||
axis_left_x_minus: "a".to_string(),
|
||||
axis_left_x_plus: "d".to_string(),
|
||||
axis_left_y_minus: "w".to_string(),
|
||||
axis_left_y_plus: "s".to_string(),
|
||||
axis_right_x_minus: "kp4".to_string(),
|
||||
axis_right_x_plus: "kp6".to_string(),
|
||||
axis_right_y_minus: "kp8".to_string(),
|
||||
axis_right_y_plus: "kp2".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 2. İkinci Preset (Fare + Klavye):
|
||||
/// - Üçgen: Fare Sağ Tık (rightbutton)
|
||||
/// - Kare: Fare Sol Tık (leftbutton)
|
||||
/// - Yuvarlak: F
|
||||
/// - X: Space
|
||||
/// - Sol Stick: W, A, S, D
|
||||
/// - Sağ Stick: Numpad 8 (Yukarı), Numpad 2 (Aşağı), Numpad 4 (Sol), Numpad 6 (Sağ)
|
||||
/// - Options: Enter
|
||||
/// - Share veya Select: Backspace
|
||||
/// - DPAD: Ok Tuşları (Yukarı, Aşağı, Sol, Sağ)
|
||||
/// - L1/R1/L2/R2/L3/R3: Q, E, Z, X, C, V
|
||||
pub fn mouse_keyboard() -> Self {
|
||||
Self {
|
||||
triangle: "rightbutton".to_string(),
|
||||
circle: "f".to_string(),
|
||||
cross: "space".to_string(),
|
||||
square: "leftbutton".to_string(),
|
||||
l1: "q".to_string(),
|
||||
r1: "e".to_string(),
|
||||
l2: "z".to_string(),
|
||||
r2: "x".to_string(),
|
||||
l3: "c".to_string(),
|
||||
r3: "v".to_string(),
|
||||
options: "enter".to_string(),
|
||||
touchpad_center: "backspace".to_string(),
|
||||
pad_up: "up".to_string(),
|
||||
pad_down: "down".to_string(),
|
||||
pad_left: "left".to_string(),
|
||||
pad_right: "right".to_string(),
|
||||
axis_left_x_minus: "a".to_string(),
|
||||
axis_left_x_plus: "d".to_string(),
|
||||
axis_left_y_minus: "w".to_string(),
|
||||
axis_left_y_plus: "s".to_string(),
|
||||
axis_right_x_minus: "kp4".to_string(),
|
||||
axis_right_x_plus: "kp6".to_string(),
|
||||
axis_right_y_minus: "kp8".to_string(),
|
||||
axis_right_y_plus: "kp2".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CachedHardware {
|
||||
pub gpu_name: String,
|
||||
pub vram_mb: u64,
|
||||
pub ram_mb: u64,
|
||||
pub refresh_rate_hz: u32,
|
||||
#[serde(default = "default_cpu_name")]
|
||||
pub cpu_name: String,
|
||||
#[serde(default = "default_cpu_cores")]
|
||||
pub cpu_cores: usize,
|
||||
#[serde(default = "default_cpu_threads")]
|
||||
pub cpu_threads: usize,
|
||||
}
|
||||
|
||||
fn default_cpu_name() -> String {
|
||||
"Bilinmeyen CPU".to_string()
|
||||
}
|
||||
|
||||
fn default_cpu_cores() -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
fn default_cpu_threads() -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
impl Default for CachedHardware {
|
||||
@@ -17,6 +215,9 @@ impl Default for CachedHardware {
|
||||
vram_mb: 4096,
|
||||
ram_mb: 8192,
|
||||
refresh_rate_hz: 60,
|
||||
cpu_name: "Bilinmeyen CPU".to_string(),
|
||||
cpu_cores: 4,
|
||||
cpu_threads: 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +237,12 @@ pub struct LauncherConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub patch_readbacks: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub async_readbacks: bool,
|
||||
#[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,
|
||||
@@ -47,10 +254,60 @@ pub struct LauncherConfig {
|
||||
pub cached_hardware: CachedHardware,
|
||||
#[serde(default = "default_last_commit")]
|
||||
pub last_git_commit: String,
|
||||
#[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,
|
||||
#[serde(default = "default_true")]
|
||||
pub fsr_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub disable_motion_blur: bool,
|
||||
#[serde(default)]
|
||||
pub disable_dof: bool,
|
||||
#[serde(default)]
|
||||
pub graphic_preset: GraphicPreset,
|
||||
|
||||
// Display settings
|
||||
#[serde(default)]
|
||||
pub display_mode: DisplayMode,
|
||||
#[serde(default = "default_true")]
|
||||
pub vsync: bool,
|
||||
|
||||
// Controller settings
|
||||
#[serde(default)]
|
||||
pub background_controller_input: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub hide_mouse_cursor: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub is_motion_controls_enabled: bool,
|
||||
#[serde(default = "default_deadzone")]
|
||||
pub deadzone_left: u32,
|
||||
#[serde(default = "default_deadzone")]
|
||||
pub deadzone_right: u32,
|
||||
|
||||
// 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
|
||||
@@ -58,6 +315,13 @@ fn default_target_fps() -> u32 {
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_deadzone() -> u32 {
|
||||
2
|
||||
}
|
||||
fn default_vram_budget() -> u64 {
|
||||
6144
|
||||
}
|
||||
@@ -67,27 +331,201 @@ fn default_dmem() -> u64 {
|
||||
fn default_last_commit() -> String {
|
||||
"c5638d9956002b8a9f09a240028f6793ddf7b2f8".to_string()
|
||||
}
|
||||
fn default_launcher_version() -> String {
|
||||
"v2.1".to_string()
|
||||
}
|
||||
fn default_render_resolution() -> String {
|
||||
"native".to_string()
|
||||
}
|
||||
|
||||
impl Default for LauncherConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
first_launch_done: false,
|
||||
language: default_language(),
|
||||
launcher_version: default_launcher_version(),
|
||||
game_dir: String::new(),
|
||||
target_fps: default_target_fps(),
|
||||
patch_delta_time: true,
|
||||
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,
|
||||
disable_dof: false,
|
||||
graphic_preset: GraphicPreset::Balanced,
|
||||
display_mode: DisplayMode::default(),
|
||||
vsync: true,
|
||||
background_controller_input: false,
|
||||
hide_mouse_cursor: true,
|
||||
is_motion_controls_enabled: true,
|
||||
deadzone_left: default_deadzone(),
|
||||
deadzone_right: default_deadzone(),
|
||||
keyboard_bindings: KeyboardBindings::default(),
|
||||
discord_rpc_enabled: true,
|
||||
discord_client_id: default_discord_client_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LauncherConfig {
|
||||
pub fn apply_preset(&mut self, preset: GraphicPreset) {
|
||||
self.graphic_preset = preset;
|
||||
match preset {
|
||||
GraphicPreset::UltraLow => {
|
||||
self.render_resolution = "480p".to_string();
|
||||
self.fsr_enabled = false;
|
||||
self.disable_motion_blur = true;
|
||||
self.disable_dof = true;
|
||||
self.vram_gc_budget_mb = 1280;
|
||||
self.target_fps = 60;
|
||||
}
|
||||
GraphicPreset::Low => {
|
||||
self.render_resolution = "720p".to_string();
|
||||
self.fsr_enabled = true;
|
||||
self.disable_motion_blur = true;
|
||||
self.disable_dof = true;
|
||||
self.vram_gc_budget_mb = 2048;
|
||||
self.target_fps = 60;
|
||||
}
|
||||
GraphicPreset::SteamDeck => {
|
||||
self.render_resolution = "800p".to_string();
|
||||
self.fsr_enabled = true;
|
||||
self.disable_motion_blur = true;
|
||||
self.disable_dof = true;
|
||||
self.vram_gc_budget_mb = 2048;
|
||||
self.target_fps = 40;
|
||||
}
|
||||
GraphicPreset::Balanced => {
|
||||
self.render_resolution = "native".to_string();
|
||||
self.fsr_enabled = false;
|
||||
self.disable_motion_blur = false;
|
||||
self.disable_dof = false;
|
||||
self.vram_gc_budget_mb = 6144;
|
||||
self.target_fps = 60;
|
||||
}
|
||||
GraphicPreset::Ultra => {
|
||||
self.render_resolution = "1440p".to_string();
|
||||
self.fsr_enabled = false;
|
||||
self.disable_motion_blur = false;
|
||||
self.disable_dof = false;
|
||||
self.vram_gc_budget_mb = 8192;
|
||||
self.target_fps = 120;
|
||||
}
|
||||
GraphicPreset::Custom => {
|
||||
// Keep customized values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_preset_match(&mut self) {
|
||||
if self.render_resolution == "480p" && !self.fsr_enabled && self.disable_motion_blur && self.disable_dof {
|
||||
self.graphic_preset = GraphicPreset::UltraLow;
|
||||
} else if self.render_resolution == "720p" && self.fsr_enabled && self.disable_motion_blur && self.disable_dof {
|
||||
self.graphic_preset = GraphicPreset::Low;
|
||||
} else if self.render_resolution == "800p" && self.fsr_enabled && self.disable_motion_blur && self.disable_dof {
|
||||
self.graphic_preset = GraphicPreset::SteamDeck;
|
||||
} else if self.render_resolution == "native" && !self.fsr_enabled && !self.disable_motion_blur && !self.disable_dof {
|
||||
self.graphic_preset = GraphicPreset::Balanced;
|
||||
} else if (self.render_resolution == "1440p" || self.render_resolution == "4K") && !self.fsr_enabled && !self.disable_motion_blur && !self.disable_dof {
|
||||
self.graphic_preset = GraphicPreset::Ultra;
|
||||
} else {
|
||||
self.graphic_preset = GraphicPreset::Custom;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_resolution_dimensions(&self) -> (u32, u32) {
|
||||
match self.render_resolution.as_str() {
|
||||
"480p" => (854, 480),
|
||||
"720p" => (1280, 720),
|
||||
"800p" => (1280, 800),
|
||||
"1080p" => (1920, 1080),
|
||||
"1440p" => (2560, 1440),
|
||||
"4K" | "2160p" => (3840, 2160),
|
||||
_ => (1920, 1080),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_render_res_env_value(&self) -> Option<String> {
|
||||
if self.render_resolution == "native" {
|
||||
None
|
||||
} else {
|
||||
let (w, h) = self.get_resolution_dimensions();
|
||||
Some(format!("{}x{}", w, h))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_effective_vram_budget(&self) -> u64 {
|
||||
let vram = self.cached_hardware.vram_mb;
|
||||
if vram > 0 {
|
||||
// Guard against PCIe memory thrashing / swapping to system RAM over slow bus:
|
||||
// Desktop environment, window compositor (Wayland/X11) and GPU drivers consume ~600-1200MB of VRAM.
|
||||
// Game framebuffers, guest DMA buffers, swapchains and staging memory consume another ~1-1.5GB.
|
||||
// Texture GC budget must leave sufficient headroom while preventing premature texture purging (matches v1.8.2).
|
||||
let safe_ceiling = if vram <= 2048 {
|
||||
1280
|
||||
} else if vram <= 3072 {
|
||||
2048
|
||||
} else if vram <= 4096 {
|
||||
3072
|
||||
} else if vram <= 6144 {
|
||||
4608
|
||||
} else if vram <= 8192 {
|
||||
6144
|
||||
} else if vram <= 12288 {
|
||||
8192
|
||||
} else {
|
||||
vram.saturating_sub(1024)
|
||||
};
|
||||
self.vram_gc_budget_mb.min(safe_ceiling)
|
||||
} else {
|
||||
self.vram_gc_budget_mb
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_mouse_button_bound(&self) -> bool {
|
||||
let is_mouse = |s: &str| {
|
||||
let s_low = s.trim().to_lowercase();
|
||||
matches!(
|
||||
s_low.as_str(),
|
||||
"leftbutton" | "rightbutton" | "middlebutton" | "sidebuttonback" | "sidebuttonforward" | "mouse1" | "mouse2" | "mouse3" | "mouse4" | "mouse5" | "mousewheelup" | "mousewheeldown"
|
||||
)
|
||||
};
|
||||
let b = &self.keyboard_bindings;
|
||||
is_mouse(&b.triangle)
|
||||
|| is_mouse(&b.circle)
|
||||
|| is_mouse(&b.cross)
|
||||
|| is_mouse(&b.square)
|
||||
|| is_mouse(&b.l1)
|
||||
|| is_mouse(&b.r1)
|
||||
|| is_mouse(&b.l2)
|
||||
|| is_mouse(&b.r2)
|
||||
|| is_mouse(&b.l3)
|
||||
|| is_mouse(&b.r3)
|
||||
|| is_mouse(&b.options)
|
||||
|| is_mouse(&b.touchpad_center)
|
||||
|| is_mouse(&b.pad_up)
|
||||
|| is_mouse(&b.pad_down)
|
||||
|| is_mouse(&b.pad_left)
|
||||
|| is_mouse(&b.pad_right)
|
||||
|| is_mouse(&b.axis_left_x_minus)
|
||||
|| is_mouse(&b.axis_left_x_plus)
|
||||
|| is_mouse(&b.axis_left_y_minus)
|
||||
|| is_mouse(&b.axis_left_y_plus)
|
||||
|| is_mouse(&b.axis_right_x_minus)
|
||||
|| is_mouse(&b.axis_right_x_plus)
|
||||
|| is_mouse(&b.axis_right_y_minus)
|
||||
|| is_mouse(&b.axis_right_y_plus)
|
||||
}
|
||||
pub fn config_path(base_dir: &Path) -> PathBuf {
|
||||
base_dir.join("launcher_config.json")
|
||||
}
|
||||
@@ -96,7 +534,31 @@ impl LauncherConfig {
|
||||
let path = Self::config_path(base_dir);
|
||||
if path.exists() {
|
||||
if let Ok(content) = fs::read_to_string(&path) {
|
||||
if let Ok(cfg) = serde_json::from_str::<LauncherConfig>(&content) {
|
||||
if let Ok(mut cfg) = serde_json::from_str::<LauncherConfig>(&content) {
|
||||
let mut changed = false;
|
||||
// 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;
|
||||
}
|
||||
// If the user's config still has old default bindings,
|
||||
// automatically update to the new recommended preset.
|
||||
if (cfg.keyboard_bindings.triangle == "c" && cfg.keyboard_bindings.square == "v")
|
||||
|| (cfg.keyboard_bindings.triangle == "kp8" && cfg.keyboard_bindings.square == "kp4" && cfg.keyboard_bindings.cross == "kp2")
|
||||
|| (cfg.keyboard_bindings.triangle == "u" && cfg.keyboard_bindings.r1 == "i")
|
||||
|| (cfg.keyboard_bindings.triangle == "rightbutton" && cfg.keyboard_bindings.r1 == "r")
|
||||
{
|
||||
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);
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
@@ -106,9 +568,314 @@ 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 {
|
||||
let b = &self.keyboard_bindings;
|
||||
let to_sdl = |k: &str| -> String {
|
||||
let k_low = k.trim().to_lowercase();
|
||||
match k_low.as_str() {
|
||||
"enter" | "return" => "Return".to_string(),
|
||||
"space" => "Space".to_string(),
|
||||
"escape" | "esc" => "Escape".to_string(),
|
||||
"backspace" => "Backspace".to_string(),
|
||||
"tab" => "Tab".to_string(),
|
||||
"up" => "Up".to_string(),
|
||||
"down" => "Down".to_string(),
|
||||
"left" => "Left".to_string(),
|
||||
"right" => "Right".to_string(),
|
||||
s if s.starts_with("kp") && s.len() == 3 && s[2..].chars().all(|c| c.is_ascii_digit()) => {
|
||||
format!("Keypad {}", &s[2..])
|
||||
}
|
||||
"kp_plus" | "kpplus" => "Keypad +".to_string(),
|
||||
"kp_minus" | "kpminus" => "Keypad -".to_string(),
|
||||
"kp_multiply" | "kpasterisk" => "Keypad *".to_string(),
|
||||
"kp_divide" | "kpslash" => "Keypad /".to_string(),
|
||||
"kp_enter" | "kpenter" => "Keypad Enter".to_string(),
|
||||
"kp_period" | "kpperiod" => "Keypad .".to_string(),
|
||||
"leftbutton" | "mouse1" | "leftclick" => "F13".to_string(),
|
||||
"rightbutton" | "mouse2" | "rightclick" => "F14".to_string(),
|
||||
"middlebutton" | "mouse3" | "middleclick" => "F15".to_string(),
|
||||
"sidebuttonback" | "mouse4" | "side1" => "F16".to_string(),
|
||||
"sidebuttonforward" | "mouse5" | "side2" => "F17".to_string(),
|
||||
"mousewheelup" | "wheelup" => "F18".to_string(),
|
||||
"mousewheeldown" | "wheeldown" => "F19".to_string(),
|
||||
"shift" | "lshift" => "Left Shift".to_string(),
|
||||
"rshift" => "Right Shift".to_string(),
|
||||
"ctrl" | "lctrl" => "Left Ctrl".to_string(),
|
||||
"rctrl" => "Right Ctrl".to_string(),
|
||||
"alt" | "lalt" => "Left Alt".to_string(),
|
||||
"ralt" => "Right Alt".to_string(),
|
||||
s if s.len() == 1 => s.to_uppercase(),
|
||||
s => s.to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
format!(
|
||||
"triangle={},square={},circle={},cross={},l1={},r1={},l2={},r2={},l3={},r3={},options={},touchpad={},up={},down={},left={},right={}",
|
||||
to_sdl(&b.triangle),
|
||||
to_sdl(&b.square),
|
||||
to_sdl(&b.circle),
|
||||
to_sdl(&b.cross),
|
||||
to_sdl(&b.l1),
|
||||
to_sdl(&b.r1),
|
||||
to_sdl(&b.l2),
|
||||
to_sdl(&b.r2),
|
||||
to_sdl(&b.l3),
|
||||
to_sdl(&b.r3),
|
||||
to_sdl(&b.options),
|
||||
to_sdl(&b.touchpad_center),
|
||||
to_sdl(&b.pad_up),
|
||||
to_sdl(&b.pad_down),
|
||||
to_sdl(&b.pad_left),
|
||||
to_sdl(&b.pad_right),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_gow3_pad_map(&self) -> &'static str {
|
||||
"cross=a,circle=b,square=x,triangle=y,l1=leftshoulder,r1=rightshoulder,l3=leftstick,r3=rightstick,options=start,touchpad=back,up=dpup,down=dpdown,left=dpleft,right=dpright"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_to_sdl_scancode(key: &str) -> Option<u8> {
|
||||
let lower = key.trim().to_lowercase();
|
||||
match lower.as_str() {
|
||||
// Letters (SDL_SCANCODE_A = 4 .. SDL_SCANCODE_Z = 29)
|
||||
"a" => Some(4),
|
||||
"b" => Some(5),
|
||||
"c" => Some(6),
|
||||
"d" => Some(7),
|
||||
"e" => Some(8),
|
||||
"f" => Some(9),
|
||||
"g" => Some(10),
|
||||
"h" => Some(11),
|
||||
"i" => Some(12),
|
||||
"j" => Some(13),
|
||||
"k" => Some(14),
|
||||
"l" => Some(15),
|
||||
"m" => Some(16),
|
||||
"n" => Some(17),
|
||||
"o" => Some(18),
|
||||
"p" => Some(19),
|
||||
"q" => Some(20),
|
||||
"r" => Some(21),
|
||||
"s" => Some(22),
|
||||
"t" => Some(23),
|
||||
"u" => Some(24),
|
||||
"v" => Some(25),
|
||||
"w" => Some(26),
|
||||
"x" => Some(27),
|
||||
"y" => Some(28),
|
||||
"z" => Some(29),
|
||||
|
||||
// Number row (SDL_SCANCODE_1 = 30 .. SDL_SCANCODE_0 = 39)
|
||||
"1" => Some(30),
|
||||
"2" => Some(31),
|
||||
"3" => Some(32),
|
||||
"4" => Some(33),
|
||||
"5" => Some(34),
|
||||
"6" => Some(35),
|
||||
"7" => Some(36),
|
||||
"8" => Some(37),
|
||||
"9" => Some(38),
|
||||
"0" => Some(39),
|
||||
|
||||
// Controls
|
||||
"enter" | "return" => Some(40),
|
||||
"escape" | "esc" => Some(41),
|
||||
"backspace" => Some(42),
|
||||
"tab" => Some(43),
|
||||
"space" => Some(44),
|
||||
|
||||
// Function keys (SDL_SCANCODE_F1 = 58 ... F12 = 69)
|
||||
"f1" => Some(58),
|
||||
"f2" => Some(59),
|
||||
"f3" => Some(60),
|
||||
"f4" => Some(61),
|
||||
"f5" => Some(62),
|
||||
"f6" => Some(63),
|
||||
"f7" => Some(64),
|
||||
"f8" => Some(65),
|
||||
"f9" => Some(66),
|
||||
"f10" => Some(67),
|
||||
"f11" => Some(68),
|
||||
"f12" => Some(69),
|
||||
|
||||
// Navigation
|
||||
"insert" => Some(73),
|
||||
"home" => Some(74),
|
||||
"pageup" | "prior" => Some(75),
|
||||
"delete" => Some(76),
|
||||
"end" => Some(77),
|
||||
"pagedown" | "next" => Some(78),
|
||||
|
||||
// Arrow keys
|
||||
"right" => Some(79),
|
||||
"left" => Some(80),
|
||||
"down" => Some(81),
|
||||
"up" => Some(82),
|
||||
|
||||
// Numpad (SDL_SCANCODE_KP_1 = 89 .. SDL_SCANCODE_KP_0 = 98)
|
||||
"kp_divide" | "kpdivide" | "numdivide" | "kpslash" => Some(84),
|
||||
"kp_multiply" | "kpmultiply" | "nummultiply" | "kpasterisk" => Some(85),
|
||||
"kp_minus" | "kpminus" | "numminus" => Some(86),
|
||||
"kp_plus" | "kpplus" | "numplus" => Some(87),
|
||||
"kp_enter" | "kpenter" | "numenter" => Some(88),
|
||||
"kp1" | "num1" | "numpad1" | "numpad 1" => Some(89),
|
||||
"kp2" | "num2" | "numpad2" | "numpad 2" => Some(90),
|
||||
"kp3" | "num3" | "numpad3" | "numpad 3" => Some(91),
|
||||
"kp4" | "num4" | "numpad4" | "numpad 4" => Some(92),
|
||||
"kp5" | "num5" | "numpad5" | "numpad 5" => Some(93),
|
||||
"kp6" | "num6" | "numpad6" | "numpad 6" => Some(94),
|
||||
"kp7" | "num7" | "numpad7" | "numpad 7" => Some(95),
|
||||
"kp8" | "num8" | "numpad8" | "numpad 8" => Some(96),
|
||||
"kp9" | "num9" | "numpad9" | "numpad 9" => Some(97),
|
||||
"kp0" | "num0" | "numpad0" | "numpad 0" => Some(98),
|
||||
"kp_period" | "kpperiod" | "kp_decimal" | "kpdecimal" => Some(99),
|
||||
|
||||
// Modifiers
|
||||
"ctrl" | "lctrl" => Some(224),
|
||||
"shift" | "lshift" => Some(225),
|
||||
"alt" | "lalt" => Some(226),
|
||||
"rctrl" => Some(228),
|
||||
"rshift" => Some(229),
|
||||
"ralt" => Some(230),
|
||||
|
||||
// Mouse buttons (F13..F19)
|
||||
"leftbutton" | "mouse1" | "leftclick" => Some(104),
|
||||
"rightbutton" | "mouse2" | "rightclick" => Some(105),
|
||||
"middlebutton" | "mouse3" | "middleclick" => Some(106),
|
||||
"sidebuttonback" | "mouse4" | "side1" => Some(107),
|
||||
"sidebuttonforward" | "mouse5" | "side2" => Some(108),
|
||||
"mousewheelup" | "wheelup" => Some(109),
|
||||
"mousewheeldown" | "wheeldown" => Some(110),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_numpad_scancodes() {
|
||||
assert_eq!(key_to_sdl_scancode("kp0"), Some(98));
|
||||
assert_eq!(key_to_sdl_scancode("kp1"), Some(89));
|
||||
assert_eq!(key_to_sdl_scancode("kp2"), Some(90));
|
||||
assert_eq!(key_to_sdl_scancode("kp3"), Some(91));
|
||||
assert_eq!(key_to_sdl_scancode("kp4"), Some(92));
|
||||
assert_eq!(key_to_sdl_scancode("kp5"), Some(93));
|
||||
assert_eq!(key_to_sdl_scancode("kp6"), Some(94));
|
||||
assert_eq!(key_to_sdl_scancode("kp7"), Some(95));
|
||||
assert_eq!(key_to_sdl_scancode("kp8"), Some(96));
|
||||
assert_eq!(key_to_sdl_scancode("kp9"), Some(97));
|
||||
assert_eq!(key_to_sdl_scancode("kpplus"), Some(87));
|
||||
assert_eq!(key_to_sdl_scancode("kpminus"), Some(86));
|
||||
assert_eq!(key_to_sdl_scancode("kpasterisk"), Some(85));
|
||||
assert_eq!(key_to_sdl_scancode("kpslash"), Some(84));
|
||||
assert_eq!(key_to_sdl_scancode("kpenter"), Some(88));
|
||||
assert_eq!(key_to_sdl_scancode("kpperiod"), Some(99));
|
||||
assert_eq!(key_to_sdl_scancode("leftbutton"), Some(104));
|
||||
assert_eq!(key_to_sdl_scancode("rightbutton"), Some(105));
|
||||
assert_eq!(key_to_sdl_scancode("middlebutton"), Some(106));
|
||||
assert_eq!(key_to_sdl_scancode("sidebuttonback"), Some(107));
|
||||
assert_eq!(key_to_sdl_scancode("sidebuttonforward"), Some(108));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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]
|
||||
fn test_keyboard_presets() {
|
||||
let p1 = KeyboardBindings::recommended();
|
||||
assert_eq!(p1.triangle, "u");
|
||||
assert_eq!(p1.square, "t");
|
||||
assert_eq!(p1.circle, "j");
|
||||
assert_eq!(p1.cross, "g");
|
||||
assert_eq!(p1.axis_left_y_minus, "w");
|
||||
assert_eq!(p1.axis_left_x_minus, "a");
|
||||
assert_eq!(p1.axis_left_y_plus, "s");
|
||||
assert_eq!(p1.axis_left_x_plus, "d");
|
||||
assert_eq!(p1.axis_right_y_minus, "kp8");
|
||||
assert_eq!(p1.axis_right_y_plus, "kp2");
|
||||
assert_eq!(p1.axis_right_x_minus, "kp4");
|
||||
assert_eq!(p1.axis_right_x_plus, "kp6");
|
||||
assert_eq!(p1.l1, "q");
|
||||
assert_eq!(p1.r1, "e");
|
||||
assert_eq!(p1.l2, "z");
|
||||
assert_eq!(p1.r2, "x");
|
||||
assert_eq!(p1.l3, "c");
|
||||
assert_eq!(p1.r3, "v");
|
||||
assert_eq!(p1.options, "enter");
|
||||
assert_eq!(p1.touchpad_center, "backspace");
|
||||
assert_eq!(p1.pad_up, "up");
|
||||
assert_eq!(p1.pad_down, "down");
|
||||
assert_eq!(p1.pad_left, "left");
|
||||
assert_eq!(p1.pad_right, "right");
|
||||
|
||||
let p2 = KeyboardBindings::mouse_keyboard();
|
||||
assert_eq!(p2.triangle, "rightbutton");
|
||||
assert_eq!(p2.square, "leftbutton");
|
||||
assert_eq!(p2.circle, "f");
|
||||
assert_eq!(p2.cross, "space");
|
||||
assert_eq!(p2.l1, "q");
|
||||
assert_eq!(p2.r1, "e");
|
||||
assert_eq!(p2.l2, "z");
|
||||
assert_eq!(p2.r2, "x");
|
||||
assert_eq!(p2.l3, "c");
|
||||
assert_eq!(p2.r3, "v");
|
||||
assert_eq!(p2.axis_left_y_minus, "w");
|
||||
assert_eq!(p2.axis_left_x_minus, "a");
|
||||
assert_eq!(p2.axis_left_y_plus, "s");
|
||||
assert_eq!(p2.axis_left_x_plus, "d");
|
||||
assert_eq!(p2.axis_right_y_minus, "kp8");
|
||||
assert_eq!(p2.axis_right_y_plus, "kp2");
|
||||
assert_eq!(p2.axis_right_x_minus, "kp4");
|
||||
assert_eq!(p2.axis_right_x_plus, "kp6");
|
||||
assert_eq!(p2.options, "enter");
|
||||
assert_eq!(p2.touchpad_center, "backspace");
|
||||
assert_eq!(p2.pad_up, "up");
|
||||
assert_eq!(p2.pad_down, "down");
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,77 @@ pub fn analyze_post_game(base_dir: &Path, exit_code: Option<i32>) -> Option<Cras
|
||||
let tail_lines: Vec<&str> = log_content.lines().rev().take(100).collect();
|
||||
let tail_str = tail_lines.iter().rev().cloned().collect::<Vec<&str>>().join("\n");
|
||||
|
||||
// 1. Check for Out of Memory
|
||||
// 1. Check for Broken Vulkan Layers (lsfg-vk / Lossless Scaling)
|
||||
if tail_str.contains("lsfg-vk") && (tail_str.contains("shader DLL does not exist") || tail_str.contains("Failed to create swapchain") || tail_str.contains("ErrorInitializationFailed")) {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "💥 Hatalı Vulkan Katmanı Algılandı (lsfg-vk / Lossless Scaling)".to_string(),
|
||||
description: "Sisteminizde kurulu harici 'lsfg-vk' Vulkan katmanı, Lossless Scaling shader DLL dosyasını bulamadığı için ekran penceresini (swapchain) kilitledi ve oyunu çökertti.".to_string(),
|
||||
recommendation: "Launcher bu hatalı lsfg-vk katmanını otomatik engeller. Manuel olarak 'VK_LOADER_LAYERS_DISABLE=\"*lsfg*\"' ortam değişkenini kullanabilir veya '~/.config/lsfg-vk/conf.toml' içindeki Lossless.dll dosya yolunu düzeltebilirsiniz.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "lsfg-vk"),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Check for Incompatible XML Resolution Patch Segfault (0x1f0b3a)
|
||||
if tail_str.contains("0x1f0b3a") || tail_str.contains("0x2000003") {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "💥 Uyumsuz EBOOT Çözünürlük Yaması Algılandı".to_string(),
|
||||
description: "Oyunun eski XML tabanlı çözünürlük yaması eboot ikili kodunu bozduğu için motor başlatılırken çöktü.".to_string(),
|
||||
recommendation: "Launcher'ın yeni sürümü çözünürlük ölçeklemeyi GPU kütüphanesi (FSR 1.0) üzerinden donanımsal yapar. Launcher'ı güncelleyin.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "0x1f0b3a"),
|
||||
});
|
||||
}
|
||||
|
||||
// Check for Magic / Combat Pool Crash (0x2de2d2)
|
||||
if tail_str.contains("0x2de2d2") {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "💥 Büyü / Savaş Olay Havuzu Çökmesi Algılandı (0x2de2d2)".to_string(),
|
||||
description: "Büyü kullanırken veya yoğun savaş sahnelerinde oyun motorunun olay düğüm havuzu (128 eleman) tükendiği için boş bellek adresi (0x10) referans alınarak çöktü.".to_string(),
|
||||
recommendation: "v1.7 sürümündeki evrensel bellek havuzu genişletmesi ve NULL koruması bu sorunu çözer. Launcher'ı güncelleyip oyunu tekrar başlatın.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "0x2de2d2"),
|
||||
});
|
||||
}
|
||||
|
||||
// Check for Intro Transition / Memory Pool Crash (0x2c54b2)
|
||||
if tail_str.contains("0x2c54b2") {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "💥 Giriş İntrosu / Sahne Geçiş Çökmesi Algılandı (0x2c54b2)".to_string(),
|
||||
description: "Oyun motorunun olay havuzu (128 eleman) yüksek kare hızında veya introyu geçerken tükendiği için boş bellek adresi (0x10) referans alınarak çöktü.".to_string(),
|
||||
recommendation: "v1.7 sürümündeki 'Bug Fix - Intro Crash Fix' yaması havuzu 2048'e çıkarıp otomatik genişletme ve güvenli NULL kontrolü ekler. Launcher'ı güncelleyip oyunu tekrar başlatın.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "0x2c54b2"),
|
||||
});
|
||||
}
|
||||
|
||||
// Check for Vulkan Scheduler Device Lost (SubmitExecution / Assertion Failed)
|
||||
if tail_str.contains("vk_scheduler.cpp:465 SubmitExecution") || tail_str.contains("Device lost during submit") {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "💥 Vulkan Grafik Teslimat Hatası (Device Lost)".to_string(),
|
||||
description: "Vulkan grafik komutları GPU'ya teslim edilirken sürücü zaman aşımına uğradı veya ölçekleyici (Upscaler/FSR) çakışması yaşandı.".to_string(),
|
||||
recommendation: "v1.7 sürümünde Native 1080p ve çözünürlük ölçekleyici ortam değişkenleri düzeltilmiştir. Launcher'ı güncelleyip grafik profilini 'Dengeli (Native 1080p)' seçeneğine ayarlayarak oyunu başlatın.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "SubmitExecution"),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Check for Incomplete Haswell/Intel iGPU on hybrid laptops
|
||||
if tail_str.contains("Haswell Vulkan support is incomplete") || (tail_str.contains("MESA-INTEL") && tail_str.contains("DepthStencilAttachment")) {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "⚠️ Uyumsuz Dahili Intel GPU (Haswell / Eski iGPU)".to_string(),
|
||||
description: "Oyun, modern Vulkan özelliklerini desteklemeyen eski Intel Haswell dahili ekran kartında çalıştırılmış. Harici 2GB ekran kartınız (NVIDIA/AMD) devreye girmemiş.".to_string(),
|
||||
recommendation: "Launcher harici GPU'yu otomatik seçmeye çalışır. Manuel başlatmak için NVIDIA'da 'prime-run ./gow3-launcher' veya '__NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia ./gow3-launcher', AMD'de 'DRI_PRIME=1 ./gow3-launcher' komutunu kullanın.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "Haswell"),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Check for Swapchain Creation Failure
|
||||
if tail_str.contains("Failed to create swapchain") || tail_str.contains("vk_swapchain.cpp") {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "💥 Vulkan Swapchain (Görüntü Çıkışı) Başlatılamadı".to_string(),
|
||||
description: "Grafik kartı sürücüsü veya araya giren harici bir Vulkan katmanı oyunun pencere/ekran yüzeyini oluşturmasını engelledi.".to_string(),
|
||||
recommendation: "Harici Vulkan katmanlarını kapatın, GPU sürücülerinizi güncelleyin ve tam ekran modunu pencereli moda alarak tekrar deneyin.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "swapchain"),
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Check for Out of Memory
|
||||
if tail_str.contains("VK_ERROR_OUT_OF_DEVICE_MEMORY") || tail_str.contains("Out of memory") || tail_str.contains("dmem out of memory") {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "💥 Bellek / VRAM Yetersizliği Algılandı".to_string(),
|
||||
@@ -41,16 +111,51 @@ pub fn analyze_post_game(base_dir: &Path, exit_code: Option<i32>) -> Option<Cras
|
||||
});
|
||||
}
|
||||
|
||||
// Check for GPU PM4 / Liverpool Packet Overrun or Assertion
|
||||
if tail_str.contains("packet length exceeds remaining submission size")
|
||||
|| tail_str.contains("Unimplemented PM4")
|
||||
|| (tail_str.contains("liverpool.cpp") && tail_str.contains("Unreachable code"))
|
||||
{
|
||||
return Some(CrashDiagnosis {
|
||||
title: "💥 GPU Komut Akışı (PM4 / Liverpool) Aşım Hatası".to_string(),
|
||||
description: "GPU komut işlemcisine (Liverpool) iletilen PM4 paket uzunluğu tahsis edilen teslimat boyutunu aştı. Ara bellek kopyalaması (GOW3_COPY_GPU_BUFFERS) paket sınırlarında uyuşmazlığa yol açtı.".to_string(),
|
||||
recommendation: "Ara GPU bellek kopyalaması güvenli şekilde kapatılmıştır. Oyunu tekrar başlatabilirsiniz.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "liverpool.cpp"),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Check for Texture Readback Glitch
|
||||
if tail_str.contains("Readback failed") || tail_str.contains("stale_readback") || tail_str.contains("readbackLinearImages failed") {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "⚠️ Doku (Texture) Okuma Hatası Algılandı".to_string(),
|
||||
description: "GPU'dan sistem belleğine doku transferi sırasında gecikme veya veri bozulması yaşandı (Altın sarısı doku hatası belirtisi).".to_string(),
|
||||
recommendation: "Launcher üzerinde 'Sarı Doku Hatası Önleme (Instant Readbacks)' seçeneğinin AÇIK olduğundan emin olun.".to_string(),
|
||||
recommendation: "Launcher üzerinde 'Doku Bozulması ve Sararma Önleme (Instant Readbacks)' ve 'Asenkron Geri Okuma (Önerilen)' seçeneklerinin AÇIK olduğundan emin olun.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "readback"),
|
||||
});
|
||||
}
|
||||
|
||||
// NVIDIA Vulkan Driver Dynamic Rendering / VRAM Sınırı Çökmesi (libnvidia-glcore / EndRendering / 0x108)
|
||||
if (tail_str.contains("libnvidia-glcore") || tail_str.contains("EndRendering"))
|
||||
&& (tail_str.contains("0x108") || tail_str.contains("FlushBarriers") || tail_str.contains("DispatchRecord"))
|
||||
{
|
||||
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 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"),
|
||||
});
|
||||
}
|
||||
|
||||
// General NVIDIA driver crash
|
||||
if tail_str.contains("libnvidia-glcore") {
|
||||
return Some(CrashDiagnosis {
|
||||
title: "💥 NVIDIA Grafik Sürücüsü Hatası (libnvidia-glcore)".to_string(),
|
||||
description: "NVIDIA Vulkan sürücüsü grafik komutlarını işlerken beklenmedik bir hata ile kapandı.".to_string(),
|
||||
recommendation: "Ekran kartı sürücünüzün güncel olduğundan emin olun, VRAM GC bütçesini 4096-5120 MB seviyesine çekin ve arka plandaki diğer GPU uygulamalarını kapatın.".to_string(),
|
||||
log_snippet: extract_relevant_snippet(&tail_str, "libnvidia-glcore"),
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Check for Assertion Failure or SIGSEGV
|
||||
if tail_str.contains("Assertion failed") || tail_str.contains("SIGSEGV") || tail_str.contains("panic") || (exit_code.is_some() && exit_code != Some(0)) {
|
||||
let code_str = exit_code.map(|c| c.to_string()).unwrap_or_else(|| "Bilinmeyen".to_string());
|
||||
@@ -84,3 +189,45 @@ fn extract_relevant_snippet(log: &str, keyword: &str) -> String {
|
||||
snippet.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_diagnose_nvidia_endrendering_crash() {
|
||||
let temp_dir = std::env::temp_dir().join("gow3_crash_test_nvidia");
|
||||
let _ = fs::create_dir_all(&temp_dir);
|
||||
let log_content = r#"
|
||||
Host fault (signal 11) in /usr/lib/libnvidia-glcore.so.615.78.08+0xd60b72 (?), address 0x108
|
||||
#2 /usr/lib/libnvidia-glcore.so.615.78.08+0xd60b72 (?)
|
||||
#3 /usr/lib/libnvidia-glcore.so.615.78.08+0xd56693 (?)
|
||||
#4 .../gow3-linux/bin/lib/libgow3gpu.so+0x31ec70 (_ZN6Vulkan9Scheduler12EndRenderingEv)
|
||||
#5 .../gow3-linux/bin/lib/libgow3gpu.so+0x2fb5d4 (_ZN6Vulkan7Runtime13FlushBarriersEv)
|
||||
#6 .../gow3-linux/bin/lib/libgow3gpu.so+0x2d98e5 (_ZN6Vulkan10Rasterizer14DispatchRecordEPKNS_15ComputePipelineE)
|
||||
#7 .../gow3-linux/bin/lib/libgow3gpu.so+0x2f02a0 (_ZN6Vulkan10Rasterizer13RunDrawPacketEPvPKhj)
|
||||
"#;
|
||||
fs::write(temp_dir.join("gow3_log.txt"), log_content).unwrap();
|
||||
let diag = analyze_post_game(&temp_dir, Some(11)).expect("Should diagnose NVIDIA EndRendering crash");
|
||||
assert!(diag.title.contains("NVIDIA Vulkan Sürücü Çökmesi"));
|
||||
assert!(diag.recommendation.contains("5120 MB"));
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diagnose_liverpool_pm4_packet_crash() {
|
||||
let temp_dir = std::env::temp_dir().join("gow3_crash_test_pm4");
|
||||
let _ = fs::create_dir_all(&temp_dir);
|
||||
let log_content = r#"
|
||||
GPU [Lib.GnmDriver] <Error> liverpool.cpp:66 NextPacket: : packet length exceeds remaining submission size. Packet dword count=18, remaining submission dwords=8
|
||||
GPU [Debug] <Critical> liverpool.cpp:745 ProcessGraphics: Unreachable code!
|
||||
Unimplemented PM4 type 0, base reg: 0, size: 1
|
||||
STOP: GPU library assertion failed (see GPU log above)
|
||||
"#;
|
||||
fs::write(temp_dir.join("gow3_log.txt"), log_content).unwrap();
|
||||
let diag = analyze_post_game(&temp_dir, Some(1)).expect("Should diagnose PM4 packet overrun crash");
|
||||
assert!(diag.title.contains("GPU Komut Akışı (PM4 / Liverpool)"));
|
||||
assert!(diag.recommendation.contains("kapatılmıştır"));
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
static LOG_PATH: OnceLock<PathBuf> = OnceLock::new();
|
||||
static LOG_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Formats current SystemTime as UTC "YYYY-MM-DD HH:MM:SS" without external dependencies
|
||||
pub fn format_current_timestamp() -> String {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let total_secs = now.as_secs();
|
||||
let sec = (total_secs % 60) as u32;
|
||||
let min = ((total_secs / 60) % 60) as u32;
|
||||
let hour = ((total_secs / 3600) % 24) as u32;
|
||||
let days = (total_secs / 86400) as i64;
|
||||
|
||||
// Convert days since 1970-01-01 to year, month, day (Howard Hinnant civil algorithm)
|
||||
let z = days + 719468;
|
||||
let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
|
||||
let doe = (z - era * 146097) as u32;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let y = yoe as i64 + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = if m <= 2 { y + 1 } else { y };
|
||||
|
||||
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC", y, m, d, hour, min, sec)
|
||||
}
|
||||
|
||||
/// Initializes the global log file path in base_dir/launcher_log.txt and writes startup banner
|
||||
pub fn init_logger(base_dir: &Path) {
|
||||
let path = base_dir.join("launcher_log.txt");
|
||||
let _ = LOG_PATH.set(path.clone());
|
||||
|
||||
// Write session divider banner
|
||||
let sep = "================================================================================";
|
||||
write_log("INFO", sep);
|
||||
write_log(
|
||||
"INFO",
|
||||
&format!(
|
||||
"God of War III Launcher Log started (v2.0) | OS: {} | Arch: {}",
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH
|
||||
),
|
||||
);
|
||||
write_log("INFO", &format!("Base Directory: {}", base_dir.display()));
|
||||
write_log("INFO", sep);
|
||||
}
|
||||
|
||||
/// Appends a formatted log line to stdout/stderr and launcher_log.txt
|
||||
pub fn write_log(level: &str, message: &str) {
|
||||
let timestamp = format_current_timestamp();
|
||||
let formatted = format!("[{}] [{}] {}", timestamp, level, message);
|
||||
|
||||
if level == "ERROR" {
|
||||
eprintln!("{}", formatted);
|
||||
} else {
|
||||
println!("{}", formatted);
|
||||
}
|
||||
|
||||
if let Some(path) = LOG_PATH.get() {
|
||||
if let Ok(_guard) = LOG_MUTEX.lock() {
|
||||
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {
|
||||
let _ = writeln!(f, "{}", formatted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the current content of launcher_log.txt
|
||||
pub fn read_launcher_log(base_dir: &Path) -> String {
|
||||
let path = base_dir.join("launcher_log.txt");
|
||||
if path.is_file() {
|
||||
fs::read_to_string(&path).unwrap_or_else(|e| format!("Log dosyası okunamadı: {}", e))
|
||||
} else {
|
||||
"Launcher log dosyası henüz oluşturulmadı.".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens any file in the OS default text editor or viewer (e.g. Notepad, xdg-open)
|
||||
pub fn open_file_path(path: &Path) {
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let _ = std::process::Command::new("cmd")
|
||||
.args(["/C", "start", "", &path.to_string_lossy()])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = std::process::Command::new("xdg-open")
|
||||
.arg(path)
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the launcher_log.txt file in the OS default text editor (e.g. Notepad, xdg-open)
|
||||
pub fn open_log_in_editor(base_dir: &Path) {
|
||||
let log_file = base_dir.join("launcher_log.txt");
|
||||
if !log_file.exists() {
|
||||
let _ = fs::write(&log_file, "God of War III Launcher Log\n");
|
||||
}
|
||||
open_file_path(&log_file);
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_info {
|
||||
($($arg:tt)*) => {
|
||||
$crate::logger::write_log("INFO", &format!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_warn {
|
||||
($($arg:tt)*) => {
|
||||
$crate::logger::write_log("WARN", &format!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_error {
|
||||
($($arg:tt)*) => {
|
||||
$crate::logger::write_log("ERROR", &format!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_current_timestamp() {
|
||||
let ts = format_current_timestamp();
|
||||
assert!(ts.contains("UTC"));
|
||||
assert_eq!(ts.len(), 23); // "YYYY-MM-DD HH:MM:SS UTC"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_and_read_log() {
|
||||
let temp_dir = std::env::temp_dir().join("gow3_logger_test");
|
||||
let _ = fs::create_dir_all(&temp_dir);
|
||||
let test_file = temp_dir.join("launcher_log.txt");
|
||||
let _ = fs::remove_file(&test_file);
|
||||
|
||||
init_logger(&temp_dir);
|
||||
log_info!("Test info message 123");
|
||||
log_warn!("Test warning message 456");
|
||||
log_error!("Test error message 789");
|
||||
|
||||
let content = read_launcher_log(&temp_dir);
|
||||
assert!(content.contains("Test info message 123"));
|
||||
assert!(content.contains("Test warning message 456"));
|
||||
assert!(content.contains("Test error message 789"));
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
}
|
||||
@@ -2,66 +2,72 @@ mod app;
|
||||
mod config;
|
||||
mod crash_analyzer;
|
||||
mod i18n;
|
||||
pub mod logger;
|
||||
mod runner;
|
||||
mod scanner;
|
||||
mod system;
|
||||
mod tuner;
|
||||
mod updater;
|
||||
mod runner;
|
||||
mod shader_downloader;
|
||||
pub mod mods_manager;
|
||||
pub mod discord_rpc;
|
||||
pub mod win_input;
|
||||
|
||||
use app::LauncherApp;
|
||||
use eframe::egui;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
// Determine base_dir: If running inside 'launcher' folder, go up to parent, else use current dir
|
||||
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let base_dir = if current_dir.ends_with("launcher") {
|
||||
current_dir.parent().unwrap_or(¤t_dir).to_path_buf()
|
||||
// Determine base_dir:
|
||||
// 1. If running as an AppImage, use the directory where the .AppImage file is located
|
||||
// 2. If running as standalone executable, use the folder containing the binary
|
||||
// 3. Fallback to current working directory
|
||||
let base_dir = if let Ok(appimage) = std::env::var("APPIMAGE") {
|
||||
PathBuf::from(appimage).parent().unwrap_or(&PathBuf::from(".")).to_path_buf()
|
||||
} else if let Ok(exe_path) = std::env::current_exe() {
|
||||
let exe_dir = exe_path.parent().unwrap_or(&PathBuf::from(".")).to_path_buf();
|
||||
let cur = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
if cur.ends_with("launcher") {
|
||||
cur.parent().unwrap_or(&cur).to_path_buf()
|
||||
} else if exe_dir.ends_with("target/release") || exe_dir.ends_with("target/debug") {
|
||||
cur
|
||||
} else {
|
||||
exe_dir
|
||||
}
|
||||
} else {
|
||||
current_dir
|
||||
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
};
|
||||
|
||||
// Initialize launcher file logging
|
||||
logger::init_logger(&base_dir);
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.iter().any(|a| a == "--play" || a == "--start" || a == "-p") {
|
||||
log_info!("Command line argument '--play' detected. Launching game directly...");
|
||||
let config = config::LauncherConfig::load(&base_dir);
|
||||
let gamemode_avail = system::is_gamemode_available();
|
||||
let use_gamemode = gamemode_avail && config.use_gamemode;
|
||||
let _ = tuner::apply_settings(
|
||||
&base_dir,
|
||||
config.target_fps,
|
||||
config.patch_delta_time,
|
||||
config.patch_readbacks,
|
||||
config.vram_gc_budget_mb,
|
||||
config.dmem_mb,
|
||||
);
|
||||
match runner::launch_gow3(&base_dir, &config, use_gamemode) {
|
||||
Ok(code) => std::process::exit(code),
|
||||
let _ = tuner::apply_settings(&base_dir, &config);
|
||||
match runner::launch_gow3(&base_dir, &config, use_gamemode, None) {
|
||||
Ok(code) => {
|
||||
log_info!("Game process exited with status code: {}", code);
|
||||
std::process::exit(code);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error launching God of War III: {}", e);
|
||||
log_error!("Error launching God of War III via CLI: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Try load window icon
|
||||
let icon_path = base_dir.join("gow3.png");
|
||||
if icon_path.is_file() {
|
||||
if let Ok(img) = image::open(&icon_path) {
|
||||
let width = img.width();
|
||||
let height = img.height();
|
||||
let rgba = img.to_rgba8().into_raw();
|
||||
viewport = viewport.with_icon(egui::IconData {
|
||||
rgba,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
if let Some(icon) = load_window_icon(&base_dir) {
|
||||
viewport = viewport.with_icon(icon);
|
||||
}
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
@@ -75,7 +81,64 @@ fn main() -> eframe::Result<()> {
|
||||
native_options,
|
||||
Box::new(move |cc| {
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
|
||||
// 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(),
|
||||
std::sync::Arc::new(egui::FontData::from_static(include_bytes!("../assets/godofwar.ttf"))),
|
||||
);
|
||||
fonts.families.insert(
|
||||
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)))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn load_window_icon(base_dir: &std::path::Path) -> Option<egui::IconData> {
|
||||
// 1. Try to load gow3.png from base_dir, current_dir, exe_dir, or fallback to embedded asset
|
||||
let img = if let Ok(img) = image::open(base_dir.join("gow3.png")) {
|
||||
Some(img)
|
||||
} else if let Ok(cur) = std::env::current_dir() {
|
||||
if let Ok(img) = image::open(cur.join("gow3.png")) {
|
||||
Some(img)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.or_else(|| {
|
||||
if let Ok(exe_p) = std::env::current_exe() {
|
||||
if let Some(parent) = exe_p.parent() {
|
||||
if let Ok(img) = image::open(parent.join("gow3.png")) {
|
||||
return Some(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
image::load_from_memory(include_bytes!("../../gow3.png")).ok()
|
||||
})?;
|
||||
|
||||
// Downscale to 256x256 if larger:
|
||||
// Windows Win32 User32 CreateIcon fails on dimensions > 256, causing taskbar icon to revert to generic.
|
||||
// Scaling to 256x256 ensures smooth High-DPI rendering and 100% Win32 / Wayland / X11 taskbar compatibility.
|
||||
let (w, h) = (img.width(), img.height());
|
||||
let resized = if w > 256 || h > 256 || w != h {
|
||||
img.resize_exact(256, 256, image::imageops::FilterType::Lanczos3)
|
||||
} else {
|
||||
img
|
||||
};
|
||||
|
||||
let width = resized.width();
|
||||
let height = resized.height();
|
||||
let rgba = resized.to_rgba8().into_raw();
|
||||
Some(egui::IconData { rgba, width, height })
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,13 @@ pub fn is_valid_game_dir(path: &Path) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
let eboot = path.join("eboot.bin");
|
||||
if !eboot.is_file() {
|
||||
let eboot = if path.join("eboot.bin").is_file() {
|
||||
path.join("eboot.bin")
|
||||
} else if path.join("EBOOT.BIN").is_file() {
|
||||
path.join("EBOOT.BIN")
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Verify eboot.bin size (genuine God of War III eboot is > 1MB, typically ~4.5MB)
|
||||
if let Ok(meta) = fs::metadata(&eboot) {
|
||||
@@ -30,10 +33,17 @@ pub fn is_valid_game_dir(path: &Path) -> bool {
|
||||
}
|
||||
|
||||
// 2. Verify sce_sys/param.sfo exists and has data
|
||||
let sfo = path.join("sce_sys/param.sfo");
|
||||
if !sfo.is_file() {
|
||||
let sfo = if path.join("sce_sys/param.sfo").is_file() {
|
||||
path.join("sce_sys/param.sfo")
|
||||
} else if path.join("sce_sys/PARAM.SFO").is_file() {
|
||||
path.join("sce_sys/PARAM.SFO")
|
||||
} else if path.join("SCE_SYS/param.sfo").is_file() {
|
||||
path.join("SCE_SYS/param.sfo")
|
||||
} else if path.join("SCE_SYS/PARAM.SFO").is_file() {
|
||||
path.join("SCE_SYS/PARAM.SFO")
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if let Ok(meta) = fs::metadata(&sfo) {
|
||||
if meta.len() < 100 {
|
||||
return false;
|
||||
@@ -43,13 +53,13 @@ pub fn is_valid_game_dir(path: &Path) -> bool {
|
||||
}
|
||||
|
||||
// 3. Verify at least one essential game directory exists (sce_module, EXEC, or language packs)
|
||||
let has_modules = path.join("sce_module").is_dir();
|
||||
let has_exec = path.join("EXEC").is_dir();
|
||||
let has_modules = path.join("sce_module").is_dir() || path.join("SCE_MODULE").is_dir();
|
||||
let has_exec = path.join("EXEC").is_dir() || path.join("exec").is_dir();
|
||||
let has_lang = [
|
||||
"ENGLISH", "DUTCH", "FRENCH", "GERMAN", "ITALIAN", "POLISH", "PORTUGUESE", "RUSSIAN", "SPANISH", "data",
|
||||
]
|
||||
.iter()
|
||||
.any(|lang| path.join(lang).is_dir());
|
||||
.any(|lang| path.join(lang).is_dir() || path.join(lang.to_lowercase()).is_dir());
|
||||
|
||||
has_modules || has_exec || has_lang
|
||||
}
|
||||
@@ -58,6 +68,10 @@ pub fn scan_for_game() -> Vec<GameCandidate> {
|
||||
let mut candidates = Vec::new();
|
||||
let mut scan_roots = Vec::new();
|
||||
|
||||
if let Ok(cur) = std::env::current_dir() {
|
||||
scan_roots.push(cur);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(userprofile) = std::env::var("USERPROFILE") {
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
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/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`.
|
||||
pub fn resolve_cache_dir(base_dir: &Path) -> PathBuf {
|
||||
if base_dir.join("gow3-linux/user").is_dir() {
|
||||
base_dir.join("gow3-linux/user/cache")
|
||||
} else {
|
||||
base_dir.join("user/cache")
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts total pre-compiled shader and pipeline key files for God of War III.
|
||||
pub fn count_cached_shaders(base_dir: &Path) -> usize {
|
||||
let cache_dir = resolve_cache_dir(base_dir);
|
||||
let primary = cache_dir.join("CUSA01715");
|
||||
let fallback = cache_dir.join("CUSA01623");
|
||||
let target = if primary.is_dir() {
|
||||
primary
|
||||
} else if fallback.is_dir() {
|
||||
fallback
|
||||
} else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let mut count = 0;
|
||||
if let Ok(entries) = fs::read_dir(target) {
|
||||
for e in entries.flatten() {
|
||||
let p = e.path();
|
||||
if p.extension().map_or(false, |ext| ext == "spv" || ext == "meta" || ext == "key") {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// Finds a local pre-built shader archive if present in the repo or parent dirs.
|
||||
pub fn find_local_shader_archive(base_dir: &Path) -> Option<PathBuf> {
|
||||
let candidates = [
|
||||
base_dir.join("shaders/gow3-shader-cache.tar.xz"),
|
||||
base_dir.join("../shaders/gow3-shader-cache.tar.xz"),
|
||||
];
|
||||
for c in &candidates {
|
||||
if c.is_file() {
|
||||
return Some(c.clone());
|
||||
}
|
||||
}
|
||||
if let Some(parent) = base_dir.parent() {
|
||||
let p = parent.join("shaders/gow3-shader-cache.tar.xz");
|
||||
if p.is_file() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Downloads and extracts the official complete shader cache.
|
||||
/// Sets up cross-title compatibility for both CUSA01715 and CUSA01623.
|
||||
pub fn download_and_install_shaders(base_dir: &Path) -> Result<usize, String> {
|
||||
let cache_dir = resolve_cache_dir(base_dir);
|
||||
let _ = fs::create_dir_all(&cache_dir);
|
||||
|
||||
// 1. Check for local shader archive first (avoids unnecessary re-download)
|
||||
let archive_path = if let Some(local_path) = find_local_shader_archive(base_dir) {
|
||||
local_path
|
||||
} else {
|
||||
let temp_archive = cache_dir.join("gow3-shader-cache.tar.xz.tmp");
|
||||
let _ = fs::remove_file(&temp_archive);
|
||||
|
||||
http_download_with_fallback(SHADER_REPO_URL, SHADER_FALLBACK_URL, &temp_archive)?;
|
||||
temp_archive
|
||||
};
|
||||
|
||||
// 2. Extract archive into cache directory
|
||||
let extract_res = extract_tar_xz(&archive_path, &cache_dir);
|
||||
|
||||
// If a temporary download file was used, clean it up
|
||||
if archive_path.to_string_lossy().ends_with(".tmp") {
|
||||
let _ = fs::remove_file(&archive_path);
|
||||
}
|
||||
|
||||
extract_res?;
|
||||
|
||||
// 3. Ensure cross-compatibility between CUSA01715 (EU/TR) and CUSA01623 (US)
|
||||
link_regional_caches(&cache_dir);
|
||||
|
||||
let final_count = count_cached_shaders(base_dir);
|
||||
Ok(final_count)
|
||||
}
|
||||
|
||||
fn http_download(url: &str, dest: &Path) -> Result<(), String> {
|
||||
// 1. Try curl (available on Linux, macOS, and modern Windows 10+)
|
||||
if let Ok(out) = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: gow3-launcher", url, "-o", dest.to_str().unwrap_or("")])
|
||||
.output()
|
||||
{
|
||||
if out.status.success() && dest.is_file() {
|
||||
if let Ok(meta) = fs::metadata(dest) {
|
||||
if meta.len() > 1024 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to PowerShell on Windows
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let ps_code = format!(
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; \
|
||||
$wc = New-Object System.Net.WebClient; \
|
||||
$wc.Headers.Add('User-Agent', 'gow3-launcher'); \
|
||||
$wc.DownloadFile('{}', '{}')",
|
||||
url.replace('\'', "''"),
|
||||
dest.to_string_lossy().replace('\'', "''")
|
||||
);
|
||||
if let Ok(out) = Command::new("powershell")
|
||||
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps_code])
|
||||
.output()
|
||||
{
|
||||
if out.status.success() && dest.is_file() {
|
||||
if let Ok(meta) = fs::metadata(dest) {
|
||||
if meta.len() > 1024 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("Dosya indirilemedi: {}", url))
|
||||
}
|
||||
|
||||
fn http_download_with_fallback(primary_url: &str, fallback_url: &str, dest: &Path) -> Result<(), String> {
|
||||
if http_download(primary_url, dest).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
http_download(fallback_url, dest)
|
||||
}
|
||||
|
||||
fn extract_tar_xz(archive: &Path, dest_dir: &Path) -> Result<(), String> {
|
||||
let _ = fs::create_dir_all(dest_dir);
|
||||
|
||||
// 1. Try system tar (available on Linux, macOS, and modern Windows 10/11 tar.exe)
|
||||
if let Ok(st) = Command::new("tar")
|
||||
.args(["-xf", archive.to_str().unwrap_or(""), "-C", dest_dir.to_str().unwrap_or("")])
|
||||
.status()
|
||||
{
|
||||
if st.success() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try 7z fallback (if user has 7z installed)
|
||||
if let Ok(st) = Command::new("7z")
|
||||
.args([
|
||||
"x",
|
||||
archive.to_str().unwrap_or(""),
|
||||
&format!("-o{}", dest_dir.to_str().unwrap_or("")),
|
||||
"-y",
|
||||
])
|
||||
.status()
|
||||
{
|
||||
if st.success() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err("Shader arşivi açılamadı (tar veya 7z aracı bulunamadı).".to_string())
|
||||
}
|
||||
|
||||
/// Links CUSA01715 and CUSA01623 caches so both versions share the exact same shaders without duplication.
|
||||
pub fn link_regional_caches(cache_dir: &Path) {
|
||||
let cusa1715 = cache_dir.join("CUSA01715");
|
||||
let cusa1623 = cache_dir.join("CUSA01623");
|
||||
|
||||
if cusa1715.is_dir() && !cusa1623.exists() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let _ = std::os::unix::fs::symlink("CUSA01715", &cusa1623);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = Command::new("cmd")
|
||||
.args(["/C", "mklink", "/J", cusa1623.to_str().unwrap_or(""), cusa1715.to_str().unwrap_or("")])
|
||||
.output();
|
||||
}
|
||||
} else if cusa1623.is_dir() && !cusa1715.exists() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let _ = std::os::unix::fs::symlink("CUSA01623", &cusa1715);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = Command::new("cmd")
|
||||
.args(["/C", "mklink", "/J", cusa1715.to_str().unwrap_or(""), cusa1623.to_str().unwrap_or("")])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_count_cached_shaders() {
|
||||
let temp_dir = std::env::temp_dir().join("gow3_test_shader_count");
|
||||
let cache = temp_dir.join("user/cache/CUSA01715");
|
||||
let _ = fs::create_dir_all(&cache);
|
||||
|
||||
fs::write(cache.join("0x123.spv"), b"spv").unwrap();
|
||||
fs::write(cache.join("0x123.meta"), b"meta").unwrap();
|
||||
fs::write(cache.join("0x123.key"), b"key").unwrap();
|
||||
fs::write(cache.join("ignore.txt"), b"txt").unwrap();
|
||||
|
||||
let count = count_cached_shaders(&temp_dir);
|
||||
assert_eq!(count, 3);
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_link_regional_caches() {
|
||||
let temp_dir = std::env::temp_dir().join("gow3_test_shader_link");
|
||||
let cache = temp_dir.join("user/cache");
|
||||
let cusa1715 = cache.join("CUSA01715");
|
||||
let _ = fs::create_dir_all(&cusa1715);
|
||||
fs::write(cusa1715.join("test.spv"), b"spv").unwrap();
|
||||
|
||||
link_regional_caches(&cache);
|
||||
let cusa1623 = cache.join("CUSA01623");
|
||||
assert!(cusa1623.exists());
|
||||
|
||||
let count = count_cached_shaders(&temp_dir);
|
||||
assert_eq!(count, 1);
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,135 @@ use std::fs;
|
||||
use std::process::Command;
|
||||
use crate::config::CachedHardware;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CpuGrade {
|
||||
Weak, // Zayıf / Darboğaz Riski (Kırmızı)
|
||||
Medium, // Orta Seviye (40-60 FPS) (Sarı/Amber)
|
||||
Good, // Güçlü (60 FPS İdeal) (Açık Yeşil)
|
||||
Ultra, // Üst Seviye (60-144 FPS) (Parlak Yeşil)
|
||||
}
|
||||
|
||||
impl CpuGrade {
|
||||
pub fn badge_text(&self, lang: &str) -> &'static str {
|
||||
match self {
|
||||
CpuGrade::Weak => match lang {
|
||||
"tr" => "• Zayıf / Darboğaz Riski",
|
||||
"de" => "• Schwach / Bottleneck-Risiko",
|
||||
"es" => "• Débil / Riesgo Cuello de Botella",
|
||||
_ => "• Weak / Bottleneck Risk",
|
||||
},
|
||||
CpuGrade::Medium => match lang {
|
||||
"tr" => "• Orta Seviye (40-60 FPS)",
|
||||
"de" => "• Mittelstufe (40-60 FPS)",
|
||||
"es" => "• Nivel Medio (40-60 FPS)",
|
||||
_ => "• Mid-Range (40-60 FPS)",
|
||||
},
|
||||
CpuGrade::Good => match lang {
|
||||
"tr" => "• Güçlü (60 FPS İdeal)",
|
||||
"de" => "• Stark (60 FPS Ideal)",
|
||||
"es" => "• Fuerte (Ideal 60 FPS)",
|
||||
_ => "• Strong (60 FPS Ideal)",
|
||||
},
|
||||
CpuGrade::Ultra => match lang {
|
||||
"tr" => "• Üst Seviye (Kusursuz)",
|
||||
"de" => "• Spitzenklasse (Perfekt)",
|
||||
"es" => "• Gama Alta (Perfecto)",
|
||||
_ => "• High-End (Flawless)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rgb(&self) -> (u8, u8, u8) {
|
||||
match self {
|
||||
CpuGrade::Weak => (239, 83, 80),
|
||||
CpuGrade::Medium => (255, 213, 79),
|
||||
CpuGrade::Good => (130, 225, 140),
|
||||
CpuGrade::Ultra => (0, 230, 118),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn evaluate_cpu(name: &str, cores: usize, threads: usize) -> CpuGrade {
|
||||
let lower = name.to_lowercase();
|
||||
|
||||
// 1. Obsolete or severely underpowered architectures
|
||||
if lower.contains("celeron")
|
||||
|| lower.contains("pentium")
|
||||
|| lower.contains("core 2")
|
||||
|| lower.contains("core(tm)2")
|
||||
|| lower.contains("athlon")
|
||||
|| lower.contains("sempron")
|
||||
|| lower.contains("phenom")
|
||||
|| lower.contains("fx-")
|
||||
|| lower.contains("a10-")
|
||||
|| lower.contains("a8-")
|
||||
|| lower.contains("a6-")
|
||||
|| lower.contains("a4-")
|
||||
{
|
||||
return CpuGrade::Weak;
|
||||
}
|
||||
|
||||
// 2. Thread count constraints
|
||||
if threads <= 4 {
|
||||
// Any 2-core or 4-core/4-thread CPU (e.g. i5-4460, i5-6500, older i3) struggles with PS4 emulation threads
|
||||
return CpuGrade::Weak;
|
||||
}
|
||||
|
||||
// 3. Medium tier (4 cores / 8 threads or 6 cores / 6 threads)
|
||||
// Or older 4-core HEDT / 6-core Zen 1
|
||||
if threads <= 8 || cores <= 4 {
|
||||
return CpuGrade::Medium;
|
||||
}
|
||||
|
||||
// 4. Good vs Ultra
|
||||
if threads >= 16 || cores >= 8 {
|
||||
CpuGrade::Ultra
|
||||
} else if threads >= 12 || cores >= 6 {
|
||||
CpuGrade::Good
|
||||
} else {
|
||||
CpuGrade::Medium
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clean_cpu_name(name: &str) -> String {
|
||||
let mut cleaned = name
|
||||
.replace("(R)", "")
|
||||
.replace("(TM)", "")
|
||||
.replace(" CPU", "");
|
||||
|
||||
if let Some(pos) = cleaned.find("-Core") {
|
||||
if let Some(start) = cleaned[..pos].rfind(' ') {
|
||||
cleaned.drain(start..);
|
||||
}
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = cleaned.split_whitespace().collect();
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
pub fn detect_hardware() -> CachedHardware {
|
||||
let (gpu_name, vram_mb) = detect_gpu();
|
||||
let ram_mb = detect_ram();
|
||||
let refresh_rate_hz = detect_refresh_rate();
|
||||
let (cpu_name, cpu_cores, cpu_threads) = detect_cpu();
|
||||
|
||||
CachedHardware {
|
||||
gpu_name,
|
||||
vram_mb,
|
||||
ram_mb,
|
||||
refresh_rate_hz,
|
||||
cpu_name,
|
||||
cpu_cores,
|
||||
cpu_threads,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_cpu_threads() -> usize {
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(4)
|
||||
}
|
||||
|
||||
pub fn is_gamemode_available() -> bool {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -76,6 +192,47 @@ fn detect_gpu() -> (String, u64) {
|
||||
("Bilinmeyen GPU (Genel)".to_string(), 4096)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn detect_has_discrete_nvidia() -> bool {
|
||||
if let Ok(output) = Command::new("lspci").output() {
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if (line.contains("VGA compatible controller") || line.contains("3D controller"))
|
||||
&& line.to_lowercase().contains("nvidia")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn detect_has_discrete_amd() -> bool {
|
||||
if let Ok(output) = Command::new("lspci").output() {
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
let mut has_intel = false;
|
||||
let mut has_amd = false;
|
||||
for line in text.lines() {
|
||||
if line.contains("VGA compatible controller") || line.contains("3D controller") {
|
||||
let lower = line.to_lowercase();
|
||||
if lower.contains("intel") {
|
||||
has_intel = true;
|
||||
}
|
||||
if lower.contains("amd") || lower.contains("advanced micro devices") || lower.contains("ati") {
|
||||
has_amd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return has_intel && has_amd;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn glob_vram() -> Result<Option<(String, u64)>, std::io::Error> {
|
||||
for card_idx in 0..8 {
|
||||
@@ -177,19 +334,123 @@ fn detect_refresh_rate() -> u32 {
|
||||
rates.into_iter().max().unwrap_or(60)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn detect_cpu() -> (String, usize, usize) {
|
||||
let threads = detect_cpu_threads();
|
||||
let mut name = String::new();
|
||||
let mut cores = 0;
|
||||
|
||||
if let Ok(content) = fs::read_to_string("/proc/cpuinfo") {
|
||||
for line in content.lines() {
|
||||
if name.is_empty() && (line.starts_with("model name") || line.starts_with("Hardware")) {
|
||||
if let Some((_, n)) = line.split_once(':') {
|
||||
name = n.trim().to_string();
|
||||
}
|
||||
}
|
||||
if cores == 0 && line.starts_with("cpu cores") {
|
||||
if let Some((_, c)) = line.split_once(':') {
|
||||
if let Ok(val) = c.trim().parse::<usize>() {
|
||||
cores = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !name.is_empty() && cores > 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if name.is_empty() {
|
||||
if let Ok(output) = Command::new("lscpu").output() {
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if line.starts_with("Model name:") {
|
||||
if let Some((_, n)) = line.split_once(':') {
|
||||
name = n.trim().to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if name.is_empty() {
|
||||
name = "Linux x86_64 İşlemci".to_string();
|
||||
}
|
||||
|
||||
if cores == 0 {
|
||||
cores = if threads > 1 { threads / 2 } else { 1 };
|
||||
}
|
||||
|
||||
(name, cores, threads)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn detect_gpu() -> (String, u64) {
|
||||
// 1. Try nvidia-smi first (exact VRAM for NVIDIA GPUs, e.g. RTX 4070 Laptop 8GB)
|
||||
if let Ok(output) = Command::new("nvidia-smi")
|
||||
.args(["--query-gpu=name,memory.total", "--format=csv,noheader,nounits"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
if let Some(line) = text.lines().next() {
|
||||
let parts: Vec<&str> = line.split(',').collect();
|
||||
if parts.len() >= 2 {
|
||||
let name = parts[0].trim().to_string();
|
||||
let vram = parts[1].trim().parse::<u64>().unwrap_or(0);
|
||||
if vram > 0 {
|
||||
return (name, vram);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try Windows Registry for AMD / Intel / NVIDIA (64-bit QWORD qwMemorySize)
|
||||
let ps_script = r#"
|
||||
$g = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\00*' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.'HardwareInformation.qwMemorySize' -gt 0 } |
|
||||
Sort-Object 'HardwareInformation.qwMemorySize' -Descending |
|
||||
Select-Object -First 1;
|
||||
if ($g) {
|
||||
$name = if ($g.DriverDesc) { $g.DriverDesc } else { $g.'Device Description' };
|
||||
$vram = [int64]$g.'HardwareInformation.qwMemorySize';
|
||||
[PSCustomObject]@{ Name = $name; VRAM = $vram } | ConvertTo-Json -Compress
|
||||
}
|
||||
"#;
|
||||
if let Ok(output) = Command::new("powershell")
|
||||
.args(["-NoProfile", "-Command", ps_script])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(text.trim()) {
|
||||
let name = val["Name"].as_str().unwrap_or("Windows DirectX/Vulkan GPU").to_string();
|
||||
if let Some(vram_bytes) = val["VRAM"].as_u64() {
|
||||
let vram_mb = vram_bytes / (1024 * 1024);
|
||||
if vram_mb >= 1024 {
|
||||
return (name, vram_mb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to Win32_VideoController
|
||||
if let Ok(output) = Command::new("powershell")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"Get-CimInstance Win32_VideoController | Select-Object -First 1 -Property Name,AdapterRAM | ConvertTo-Json",
|
||||
"Get-CimInstance Win32_VideoController | Sort-Object AdapterRAM -Descending | Select-Object -First 1 -Property Name,AdapterRAM | ConvertTo-Json -Compress",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(text.trim()) {
|
||||
let name = val["Name"].as_str().unwrap_or("Windows DirectX/Vulkan GPU").to_string();
|
||||
let vram_bytes = val["AdapterRAM"].as_u64().unwrap_or(4 * 1024 * 1024 * 1024);
|
||||
let vram_mb = vram_bytes / (1024 * 1024);
|
||||
@@ -197,6 +458,7 @@ fn detect_gpu() -> (String, u64) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
("Windows DirectX/Vulkan GPU".to_string(), 4096)
|
||||
}
|
||||
|
||||
@@ -241,3 +503,95 @@ fn detect_refresh_rate() -> u32 {
|
||||
}
|
||||
60
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[allow(dead_code)]
|
||||
pub fn detect_has_discrete_nvidia() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[allow(dead_code)]
|
||||
pub fn detect_has_discrete_amd() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn detect_cpu() -> (String, usize, usize) {
|
||||
let threads = detect_cpu_threads();
|
||||
let mut name = "Windows x86_64 İşlemci".to_string();
|
||||
let mut cores = if threads > 1 { threads / 2 } else { 1 };
|
||||
|
||||
let ps_cmd = "Get-CimInstance Win32_Processor | Select-Object -First 1 -Property Name,NumberOfCores,NumberOfLogicalProcessors | ConvertTo-Json -Compress";
|
||||
if let Ok(output) = Command::new("powershell")
|
||||
.args(["-NoProfile", "-Command", ps_cmd])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(text.trim()) {
|
||||
if let Some(n) = val["Name"].as_str() {
|
||||
name = n.trim().to_string();
|
||||
}
|
||||
if let Some(c) = val["NumberOfCores"].as_u64() {
|
||||
cores = c as usize;
|
||||
}
|
||||
let detected_threads = val["NumberOfLogicalProcessors"].as_u64().unwrap_or(threads as u64) as usize;
|
||||
return (name, cores, detected_threads.max(threads));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(name, cores, threads)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_clean_cpu_name() {
|
||||
assert_eq!(
|
||||
clean_cpu_name("12th Gen Intel(R) Core(TM) i7-12700H"),
|
||||
"12th Gen Intel Core i7-12700H"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_cpu_name("AMD Ryzen 7 5800X 8-Core Processor"),
|
||||
"AMD Ryzen 7 5800X"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_cpu_name("Intel(R) Core(TM) i5-4460 CPU @ 3.20GHz"),
|
||||
"Intel Core i5-4460 @ 3.20GHz"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_cpu() {
|
||||
// Low-end / Bottleneck CPUs
|
||||
assert_eq!(evaluate_cpu("Intel Core i5-4460", 4, 4), CpuGrade::Weak);
|
||||
assert_eq!(evaluate_cpu("Intel Core i3-4130", 2, 4), CpuGrade::Weak);
|
||||
assert_eq!(evaluate_cpu("AMD FX-8350 Eight-Core", 8, 8), CpuGrade::Weak);
|
||||
assert_eq!(evaluate_cpu("Intel Pentium G4560", 2, 4), CpuGrade::Weak);
|
||||
|
||||
// Medium tier CPUs
|
||||
assert_eq!(evaluate_cpu("Intel Core i7-4790K", 4, 8), CpuGrade::Medium);
|
||||
assert_eq!(evaluate_cpu("AMD Ryzen 5 1600", 6, 6), CpuGrade::Medium);
|
||||
|
||||
// Good tier CPUs
|
||||
assert_eq!(evaluate_cpu("AMD Ryzen 5 5600", 6, 12), CpuGrade::Good);
|
||||
assert_eq!(evaluate_cpu("Intel Core i5-12400", 6, 12), CpuGrade::Good);
|
||||
|
||||
// Ultra tier CPUs
|
||||
assert_eq!(evaluate_cpu("12th Gen Intel Core i7-12700H", 14, 20), CpuGrade::Ultra);
|
||||
assert_eq!(evaluate_cpu("AMD Ryzen 7 7800X3D", 8, 16), CpuGrade::Ultra);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_cpu_host() {
|
||||
let (name, cores, threads) = detect_cpu();
|
||||
assert!(!name.is_empty());
|
||||
assert!(cores > 0);
|
||||
assert!(threads > 0);
|
||||
println!("Detected CPU: {} ({}C / {}T)", name, cores, threads);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,17 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn apply_settings(
|
||||
base_dir: &Path,
|
||||
target_hz: u32,
|
||||
patch_delta_time: bool,
|
||||
patch_readbacks: bool,
|
||||
vram_gc_budget_mb: u64,
|
||||
dmem_mb: u64,
|
||||
) -> Result<(), String> {
|
||||
if patch_delta_time {
|
||||
update_xml_patches(base_dir, target_hz)?;
|
||||
use crate::config::{key_to_sdl_scancode, DisplayMode, LauncherConfig};
|
||||
|
||||
pub fn apply_settings(base_dir: &Path, config: &LauncherConfig) -> Result<(), String> {
|
||||
if config.patch_delta_time {
|
||||
update_xml_patches(base_dir, config.target_fps)?;
|
||||
}
|
||||
update_run_sh(base_dir, target_hz, patch_readbacks, vram_gc_budget_mb, dmem_mb)?;
|
||||
update_gow3_ini(base_dir, target_hz, patch_readbacks, vram_gc_budget_mb)?;
|
||||
update_user_config_toml(base_dir, vram_gc_budget_mb)?;
|
||||
update_run_sh(base_dir, config)?;
|
||||
update_gow3_ini(base_dir, config)?;
|
||||
update_user_config_toml(base_dir, config)?;
|
||||
update_user_input_config(base_dir, config)?;
|
||||
let _ = patch_engine_analog_scancodes(base_dir, config);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -35,13 +32,18 @@ fn update_xml_patches(base_dir: &Path, target_hz: u32) -> Result<(), String> {
|
||||
for path in &patch_files {
|
||||
if path.is_file() {
|
||||
if let Ok(content) = fs::read_to_string(path) {
|
||||
// Update lines with Delta-Time or 120/144 FPS
|
||||
let mut new_lines = Vec::new();
|
||||
for line in content.lines() {
|
||||
if (line.contains("Delta-Time") || line.contains("120 FPS") || line.contains("144 FPS") || line.contains("60 FPS"))
|
||||
&& line.contains("value=\"")
|
||||
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"))
|
||||
&& 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..];
|
||||
@@ -60,13 +62,7 @@ fn update_xml_patches(base_dir: &Path, target_hz: u32) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_run_sh(
|
||||
base_dir: &Path,
|
||||
target_hz: u32,
|
||||
patch_readbacks: bool,
|
||||
vram_gc_budget_mb: u64,
|
||||
dmem_mb: u64,
|
||||
) -> Result<(), String> {
|
||||
fn update_run_sh(base_dir: &Path, config: &LauncherConfig) -> Result<(), String> {
|
||||
let run_sh = base_dir.join("run.sh");
|
||||
if !run_sh.is_file() {
|
||||
return Ok(());
|
||||
@@ -74,40 +70,162 @@ fn update_run_sh(
|
||||
|
||||
let mut content = fs::read_to_string(&run_sh).map_err(|e| e.to_string())?;
|
||||
|
||||
let patches_str = if target_hz != 60 {
|
||||
format!("Bug Fix - Texture Corruption Fix;Skip Any Video With X Button;Frame Rate Patch - {} FPS", target_hz)
|
||||
} else {
|
||||
"Bug Fix - Texture Corruption Fix;Skip Any Video With X Button".to_string()
|
||||
let mut patches = Vec::new();
|
||||
let res_patch = match config.render_resolution.as_str() {
|
||||
"480p" => Some("Resolution Patch - 480p"),
|
||||
"720p" => Some("Resolution Patch - 720p"),
|
||||
"800p" => Some("Resolution Patch - 800p"),
|
||||
"1440p" => Some("Resolution Patch - 1440p"),
|
||||
"1800p" => Some("Resolution Patch - 1800p"),
|
||||
"4K" => Some("Resolution Patch - 4K"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(p) = res_patch {
|
||||
patches.push(p.to_string());
|
||||
} else {
|
||||
patches.push("Bug Fix - Texture Corruption Fix".to_string());
|
||||
}
|
||||
patches.push("Bug Fix - Startup Black Screen".to_string());
|
||||
patches.push("Bug Fix - Intro Crash Fix".to_string());
|
||||
patches.push("Skip Any Video With X Button".to_string());
|
||||
if config.patch_delta_time {
|
||||
patches.push(format!("Frame Rate Patch - {} FPS", config.target_fps));
|
||||
}
|
||||
if config.disable_motion_blur {
|
||||
patches.push("Graphics - Disable Motion Blur".to_string());
|
||||
}
|
||||
if config.disable_dof {
|
||||
patches.push("Graphics - Disable Depth of Field".to_string());
|
||||
}
|
||||
let patches_str = patches.join(";");
|
||||
|
||||
// Update GOW3_GC_BUDGET_MB
|
||||
replace_or_insert_env(&mut content, "GOW3_GC_BUDGET_MB", &vram_gc_budget_mb.to_string());
|
||||
// Update GOW3_DMEM_POOL_MB
|
||||
replace_or_insert_env(&mut content, "GOW3_DMEM_POOL_MB", &dmem_mb.to_string());
|
||||
// Update GOW3_DEFAULT_ENGINE_FPS
|
||||
replace_or_insert_env(&mut content, "GOW3_DEFAULT_ENGINE_FPS", &target_hz.to_string());
|
||||
// Update GOW3_FORCE_READBACK
|
||||
replace_or_insert_env(&mut content, "GOW3_FORCE_READBACK", if patch_readbacks { "1" } else { "0" });
|
||||
// Update GOW3_READBACKS
|
||||
replace_or_insert_env(&mut content, "GOW3_READBACKS", if patch_readbacks { "1" } else { "0" });
|
||||
// Update GOW3_READBACK_LINEAR
|
||||
replace_or_insert_env(&mut content, "GOW3_READBACK_LINEAR", if patch_readbacks { "1" } else { "0" });
|
||||
// Update GOW3_FULL_BARRIERS
|
||||
if let Some(res_env) = config.get_render_res_env_value() {
|
||||
let (internal_w, internal_h) = config.get_resolution_dimensions();
|
||||
replace_or_insert_env(&mut content, "GOW3_RENDER_RES", &res_env);
|
||||
replace_or_insert_env(&mut content, "GOW3_INTERNAL_WIDTH", &internal_w.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_INTERNAL_HEIGHT", &internal_h.to_string());
|
||||
} else {
|
||||
remove_env_if_present(&mut content, "GOW3_RENDER_RES");
|
||||
remove_env_if_present(&mut content, "GOW3_INTERNAL_WIDTH");
|
||||
remove_env_if_present(&mut content, "GOW3_INTERNAL_HEIGHT");
|
||||
}
|
||||
|
||||
if config.fsr_enabled && config.render_resolution != "native" {
|
||||
replace_or_insert_env(&mut content, "GOW3_UPSCALER", "fsr");
|
||||
replace_or_insert_env(&mut content, "GOW3_FSR1", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_RCAS", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_FSR_SHARPNESS", "0.8");
|
||||
} else {
|
||||
replace_or_insert_env(&mut content, "GOW3_UPSCALER", "none");
|
||||
replace_or_insert_env(&mut content, "GOW3_FSR1", "0");
|
||||
replace_or_insert_env(&mut content, "GOW3_RCAS", "0");
|
||||
replace_or_insert_env(&mut content, "GOW3_FSR_SHARPNESS", "0.0");
|
||||
}
|
||||
replace_or_insert_env(&mut content, "GOW3_OBJECT_MOTION", if config.disable_motion_blur { "0" } else { "1" });
|
||||
|
||||
let effective_budget = config.get_effective_vram_budget();
|
||||
replace_or_insert_env(&mut content, "GOW3_GC_BUDGET_MB", &effective_budget.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_DMEM_POOL_MB", &config.dmem_mb.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_DMEM_MB", &config.dmem_mb.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_DEFAULT_ENGINE_FPS", &config.target_fps.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_FORCE_READBACK", if config.patch_readbacks { "1" } else { "0" });
|
||||
replace_or_insert_env(&mut content, "GOW3_READBACKS", if config.patch_readbacks { "1" } else { "0" });
|
||||
replace_or_insert_env(&mut content, "GOW3_READBACK_LINEAR", if config.patch_readbacks { "1" } else { "0" });
|
||||
replace_or_insert_env(&mut content, "GOW3_DEFERRED_READBACK", if config.async_readbacks { "1" } else { "0" });
|
||||
replace_or_insert_env(&mut content, "GOW3_STALE_READBACK", if config.async_readbacks { "1" } else { "0" });
|
||||
remove_env_if_present(&mut content, "GOW3_COPY_GPU_BUFFERS");
|
||||
|
||||
let cpu_threads = if config.cached_hardware.cpu_threads > 0 {
|
||||
config.cached_hardware.cpu_threads
|
||||
} else {
|
||||
crate::system::detect_cpu_threads()
|
||||
};
|
||||
let (prep_workers, copy_threads) = if cpu_threads <= 4 {
|
||||
(2, 1)
|
||||
} else if cpu_threads <= 8 {
|
||||
(4, 2)
|
||||
} else if cpu_threads <= 12 {
|
||||
(6, 4)
|
||||
} else {
|
||||
(8, 4)
|
||||
};
|
||||
replace_or_insert_env(&mut content, "GOW3_PREP_WORKERS", &prep_workers.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_COPY_THREADS", ©_threads.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_INTERP_INT_FIX", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_DIRECT_MEMORY_ACCESS", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_PIPELINE_CACHE", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_PIPELINE_BINARIES", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_PARALLEL_WARMUP", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_BACKGROUND_WARMUP", "0");
|
||||
replace_or_insert_env(&mut content, "GOW3_VK_RECORD_THREAD", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_PRESENT_THREAD", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_ASYNC_FENCES", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_ASYNC_SHADERS", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_FPS_LIMIT", &config.target_fps.to_string());
|
||||
|
||||
// Clean up any deprecated experimental env vars from earlier launches
|
||||
remove_env_if_present(&mut content, "GOW3_STAGING_KEEP_MB");
|
||||
remove_env_if_present(&mut content, "GOW3_COPY_MERGE_KB");
|
||||
remove_env_if_present(&mut content, "GOW3_HOT_PAGES");
|
||||
remove_env_if_present(&mut content, "GOW3_TEXTURE_HELPER");
|
||||
remove_env_if_present(&mut content, "GOW3_DRAW_PIPE");
|
||||
remove_env_if_present(&mut content, "__GL_YIELD");
|
||||
remove_env_if_present(&mut content, "GOW3_FRAMES_AHEAD");
|
||||
remove_env_if_present(&mut content, "radv_zero_vram");
|
||||
|
||||
// Driver Shader Disk Cache & Multithreaded Optimizations (10GB disk cache)
|
||||
replace_or_insert_env(&mut content, "__GL_SHADER_DISK_CACHE", "1");
|
||||
replace_or_insert_env(&mut content, "__GL_SHADER_DISK_CACHE_SIZE", "10737418240");
|
||||
replace_or_insert_env(&mut content, "__GL_SHADER_DISK_CACHE_SKIP_CLEANUP", "1");
|
||||
replace_or_insert_env(&mut content, "__GL_THREADED_OPTIMIZATIONS", "1");
|
||||
replace_or_insert_env(&mut content, "__GL_VRR_ALLOWED", "1");
|
||||
replace_or_insert_env(&mut content, "__GL_MaxFramesAllowed", "1");
|
||||
replace_or_insert_env(&mut content, "MESA_SHADER_CACHE_MAX_SIZE", "10G");
|
||||
replace_or_insert_env(&mut content, "GOW3_ASYNC_SHADERS", "1");
|
||||
replace_or_insert_env(&mut content, "GOW3_FPS_LIMIT", &config.target_fps.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_FULL_BARRIERS", "1");
|
||||
// Update GOW3_FORCE_PATCHES
|
||||
replace_or_insert_env(&mut content, "GOW3_FORCE_PATCHES", "1");
|
||||
// Update GOW3_PREBUILT
|
||||
replace_or_insert_env(&mut content, "GOW3_PREBUILT", "1");
|
||||
// Update GOW3_STREAM_MEMORY
|
||||
replace_or_insert_env(&mut content, "GOW3_STREAM_MEMORY", "cached");
|
||||
// Update GOW3_PATCHES
|
||||
replace_or_insert_env(&mut content, "GOW3_PATCHES", &patches_str);
|
||||
// Update GOW3_PATCHES_ONLY
|
||||
replace_or_insert_env(&mut content, "GOW3_PATCHES_ONLY", "1");
|
||||
// Update GOW3_DMEM_MB
|
||||
replace_or_insert_env(&mut content, "GOW3_DMEM_MB", &dmem_mb.to_string());
|
||||
// Update GOW3_VBLANK_HZ
|
||||
replace_or_insert_env(&mut content, "GOW3_VBLANK_HZ", &target_hz.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_DMEM_MB", &config.dmem_mb.to_string());
|
||||
replace_or_insert_env(&mut content, "GOW3_VBLANK_HZ", &config.target_fps.to_string());
|
||||
replace_or_insert_env(
|
||||
&mut content,
|
||||
"GOW3_FULLSCREEN",
|
||||
if config.display_mode != DisplayMode::Windowed { "1" } else { "0" },
|
||||
);
|
||||
let key_map = config.build_gow3_key_map();
|
||||
let pad_map = config.build_gow3_pad_map();
|
||||
replace_or_insert_env(&mut content, "GOW3_KEY_MAP", &key_map);
|
||||
replace_or_insert_env(&mut content, "GOW3_PAD_MAP", pad_map);
|
||||
replace_or_insert_env(
|
||||
&mut content,
|
||||
"GOW3_HIDE_CURSOR",
|
||||
if config.hide_mouse_cursor { "1" } else { "0" },
|
||||
);
|
||||
remove_env_if_present(&mut content, "LD_PRELOAD");
|
||||
if config.hide_mouse_cursor || config.is_mouse_button_bound() {
|
||||
let input_lib = base_dir.join("bin/lib/libgow3input.so");
|
||||
if input_lib.is_file() {
|
||||
let safe_lib = crate::runner::ensure_safe_preload_lib(base_dir).unwrap_or(input_lib);
|
||||
replace_or_insert_env(&mut content, "LD_PRELOAD", &safe_lib.to_string_lossy());
|
||||
}
|
||||
}
|
||||
replace_or_insert_env(
|
||||
&mut content,
|
||||
"SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS",
|
||||
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(())
|
||||
@@ -128,7 +246,6 @@ fn replace_or_insert_env(content: &mut String, var_name: &str, value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
// Insert before the last launch command if not present
|
||||
let insertion = format!("export {}=\"{}\"\n", var_name, value);
|
||||
if let Some(idx) = content.find("exec ") {
|
||||
content.insert_str(idx, &insertion);
|
||||
@@ -137,28 +254,79 @@ fn replace_or_insert_env(content: &mut String, var_name: &str, value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_gow3_ini(
|
||||
base_dir: &Path,
|
||||
target_hz: u32,
|
||||
patch_readbacks: bool,
|
||||
vram_gc_budget_mb: u64,
|
||||
) -> Result<(), String> {
|
||||
let ini_path = base_dir.join("gow3.ini");
|
||||
if !ini_path.is_file() {
|
||||
return Ok(());
|
||||
fn remove_env_if_present(content: &mut String, var_name: &str) {
|
||||
let pattern_quote = format!("export {}=\"", var_name);
|
||||
let pattern_noquote = format!("export {}=", var_name);
|
||||
|
||||
while let Some(idx) = content.find(&pattern_quote).or_else(|| content.find(&pattern_noquote)) {
|
||||
if let Some(end) = content[idx..].find('\n') {
|
||||
content.drain(idx..idx + end + 1);
|
||||
} else {
|
||||
content.truncate(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut content = fs::read_to_string(&ini_path).map_err(|e| e.to_string())?;
|
||||
fn update_gow3_ini(base_dir: &Path, config: &LauncherConfig) -> Result<(), String> {
|
||||
let ini_path = base_dir.join("gow3.ini");
|
||||
let mut content = if ini_path.is_file() {
|
||||
fs::read_to_string(&ini_path).unwrap_or_default()
|
||||
} else {
|
||||
"# God of War III Remastered settings (auto-tuned)\n\
|
||||
render_resolution=native\n\
|
||||
engine_fps=60\n\
|
||||
fps_limit=60\n\
|
||||
upscaler=off\n\
|
||||
fsr1=0\n\
|
||||
rcas=0\n\
|
||||
display_mode=windowed\n\
|
||||
vsync=1\n\
|
||||
deferred_readback=0\n\
|
||||
stale_readback=0\n\
|
||||
async_shaders=1\n\
|
||||
frames_queued=2\n\
|
||||
readbacks=true\n\
|
||||
readbackLinearImages=true\n\
|
||||
budget=6144\n".to_string()
|
||||
};
|
||||
|
||||
let readbacks_val = if patch_readbacks { "true" } else { "false" };
|
||||
let readbacks_val = if config.patch_readbacks { "true" } else { "false" };
|
||||
replace_ini_key(&mut content, "readbacks", readbacks_val);
|
||||
replace_ini_key(&mut content, "readbackLinearImages", readbacks_val);
|
||||
replace_ini_key(&mut content, "stale_readback", "0");
|
||||
replace_ini_key(&mut content, "budget", &vram_gc_budget_mb.to_string());
|
||||
replace_ini_key(&mut content, "engine_fps", &target_hz.to_string());
|
||||
replace_ini_key(&mut content, "fps_limit", &target_hz.to_string());
|
||||
replace_ini_key(&mut content, "frames_queued", "1");
|
||||
replace_ini_key(&mut content, "vsync", "1");
|
||||
replace_ini_key(&mut content, "deferred_readback", if config.async_readbacks { "1" } else { "0" });
|
||||
replace_ini_key(&mut content, "stale_readback", if config.async_readbacks { "1" } else { "0" });
|
||||
replace_ini_key(&mut content, "async_shaders", "1");
|
||||
if config.render_resolution == "native" {
|
||||
replace_ini_key(&mut content, "render_resolution", "native");
|
||||
replace_ini_key(&mut content, "internal_width", "1920");
|
||||
replace_ini_key(&mut content, "internal_height", "1080");
|
||||
replace_ini_key(&mut content, "upscaler", "off");
|
||||
replace_ini_key(&mut content, "fsr1", "0");
|
||||
replace_ini_key(&mut content, "rcas", "0");
|
||||
replace_ini_key(&mut content, "sharpen", "0");
|
||||
replace_ini_key(&mut content, "sharpness", "0.00");
|
||||
} else {
|
||||
let (internal_w, internal_h) = config.get_resolution_dimensions();
|
||||
replace_ini_key(&mut content, "render_resolution", &config.render_resolution);
|
||||
replace_ini_key(&mut content, "internal_width", &internal_w.to_string());
|
||||
replace_ini_key(&mut content, "internal_height", &internal_h.to_string());
|
||||
replace_ini_key(&mut content, "upscaler", if config.fsr_enabled { "fsr" } else { "off" });
|
||||
replace_ini_key(&mut content, "fsr1", if config.fsr_enabled { "1" } else { "0" });
|
||||
replace_ini_key(&mut content, "rcas", if config.fsr_enabled { "1" } else { "0" });
|
||||
replace_ini_key(&mut content, "sharpen", if config.fsr_enabled { "1" } else { "0" });
|
||||
replace_ini_key(&mut content, "sharpness", if config.fsr_enabled { "0.80" } else { "0.00" });
|
||||
}
|
||||
replace_ini_key(&mut content, "rcas_strength", "250");
|
||||
replace_ini_key(&mut content, "object_motion", if config.disable_motion_blur { "0" } else { "1" });
|
||||
|
||||
let effective_budget = config.get_effective_vram_budget();
|
||||
replace_ini_key(&mut content, "budget", &effective_budget.to_string());
|
||||
replace_ini_key(&mut content, "engine_fps", &config.target_fps.to_string());
|
||||
replace_ini_key(&mut content, "fps_limit", &config.target_fps.to_string());
|
||||
replace_ini_key(&mut content, "frames_queued", "2");
|
||||
replace_ini_key(&mut content, "vsync", if config.vsync { "1" } else { "0" });
|
||||
replace_ini_key(&mut content, "display_mode", config.display_mode.as_str());
|
||||
|
||||
fs::write(&ini_path, content).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
@@ -187,21 +355,532 @@ fn replace_ini_key(content: &mut String, key: &str, val: &str) {
|
||||
*content = lines.join("\n");
|
||||
}
|
||||
|
||||
fn update_user_config_toml(base_dir: &Path, vram_gc_budget_mb: u64) -> Result<(), String> {
|
||||
let toml_path = base_dir.join("user/config.toml");
|
||||
if !toml_path.is_file() {
|
||||
return Ok(());
|
||||
fn update_toml_key_in_section(content: &mut String, section: &str, key: &str, val: &str) {
|
||||
let section_header = format!("[{}]", section);
|
||||
let mut in_section = false;
|
||||
let mut key_replaced = false;
|
||||
let mut new_lines = Vec::new();
|
||||
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with('[') && trimmed.ends_with(']') {
|
||||
if in_section && !key_replaced {
|
||||
new_lines.push(format!("{} = {}", key, val));
|
||||
key_replaced = true;
|
||||
}
|
||||
in_section = trimmed == section_header;
|
||||
new_lines.push(line.to_string());
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_section {
|
||||
if let Some((k, _)) = trimmed.split_once('=') {
|
||||
if k.trim() == key {
|
||||
new_lines.push(format!("{} = {}", key, val));
|
||||
key_replaced = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new_lines.push(line.to_string());
|
||||
}
|
||||
|
||||
let mut content = fs::read_to_string(&toml_path).map_err(|e| e.to_string())?;
|
||||
if in_section && !key_replaced {
|
||||
new_lines.push(format!("{} = {}", key, val));
|
||||
} else if !key_replaced {
|
||||
new_lines.push(String::new());
|
||||
new_lines.push(section_header);
|
||||
new_lines.push(format!("{} = {}", key, val));
|
||||
}
|
||||
|
||||
*content = new_lines.join("\n");
|
||||
}
|
||||
|
||||
fn update_user_config_toml(base_dir: &Path, config: &LauncherConfig) -> Result<(), String> {
|
||||
let toml_path = base_dir.join("user/config.toml");
|
||||
let mut content = if toml_path.is_file() {
|
||||
fs::read_to_string(&toml_path).unwrap_or_default()
|
||||
} else {
|
||||
let user_dir = base_dir.join("user");
|
||||
let _ = fs::create_dir_all(&user_dir);
|
||||
"[General]\n\
|
||||
[GPU]\n\
|
||||
Fullscreen = false\n\
|
||||
FullscreenMode = \"Windowed\"\n\
|
||||
[Input]\n".to_string()
|
||||
};
|
||||
|
||||
// Update vulkan_gc_budget if present
|
||||
if let Some(idx) = content.find("vulkan_gc_budget =") {
|
||||
if let Some(end) = content[idx..].find('\n') {
|
||||
let prefix = &content[..idx];
|
||||
let suffix = &content[idx + end..];
|
||||
content = format!("{}vulkan_gc_budget = {}{}", prefix, vram_gc_budget_mb, suffix);
|
||||
let effective_budget = config.get_effective_vram_budget();
|
||||
content = format!("{}vulkan_gc_budget = {}{}", prefix, effective_budget, suffix);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure presentMode is removed so Vulkan uses default FIFO swapchain pacing (eliminating logo black screen blinks)
|
||||
if let Some(idx) = content.find("presentMode =") {
|
||||
if let Some(end) = content[idx..].find('\n') {
|
||||
content.replace_range(idx..=idx + end, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Update Display / GPU settings
|
||||
let (fullscreen_mode_str, fullscreen_bool) = config.display_mode.to_toml_mode();
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"Fullscreen",
|
||||
if fullscreen_bool { "true" } else { "false" },
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"FullscreenMode",
|
||||
&format!("\"{}\"", fullscreen_mode_str),
|
||||
);
|
||||
let (internal_w, internal_h) = match config.render_resolution.as_str() {
|
||||
"480p" => (854, 480),
|
||||
"720p" => (1280, 720),
|
||||
"800p" => (1280, 800),
|
||||
"1440p" => (2560, 1440),
|
||||
"1800p" => (3200, 1800),
|
||||
"4K" => (3840, 2160),
|
||||
_ => (1920, 1080),
|
||||
};
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"internalScreenWidth",
|
||||
&internal_w.to_string(),
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"internalScreenHeight",
|
||||
&internal_h.to_string(),
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"fsrEnabled",
|
||||
if config.fsr_enabled { "true" } else { "false" },
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"rcasEnabled",
|
||||
if config.fsr_enabled { "true" } else { "false" },
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"vblankFrequency",
|
||||
&config.target_fps.to_string(),
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"directMemoryAccess",
|
||||
"true",
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"readbacks",
|
||||
if config.patch_readbacks { "true" } else { "false" },
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"GPU",
|
||||
"readbackLinearImages",
|
||||
if config.patch_readbacks { "true" } else { "false" },
|
||||
);
|
||||
|
||||
// Update Vulkan settings
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"Vulkan",
|
||||
"pipelineCacheEnable",
|
||||
"true",
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"Vulkan",
|
||||
"pipelineCacheArchive",
|
||||
"true",
|
||||
);
|
||||
|
||||
// Update Input settings
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"Input",
|
||||
"backgroundControllerInput",
|
||||
if config.background_controller_input { "true" } else { "false" },
|
||||
);
|
||||
update_toml_key_in_section(
|
||||
&mut content,
|
||||
"Input",
|
||||
"isMotionControlsEnabled",
|
||||
if config.is_motion_controls_enabled { "true" } else { "false" },
|
||||
);
|
||||
|
||||
fs::write(&toml_path, content).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_user_input_config(base_dir: &Path, config: &LauncherConfig) -> Result<(), String> {
|
||||
let input_dir = base_dir.join("user/input_config");
|
||||
let _ = fs::create_dir_all(&input_dir);
|
||||
let ini_path = input_dir.join("default.ini");
|
||||
|
||||
let b = &config.keyboard_bindings;
|
||||
let content = format!(
|
||||
"# God of War III Remastered - Input Configuration\n\
|
||||
# Auto-generated by Launcher\n\n\
|
||||
# Keyboard bindings\n\n\
|
||||
triangle = {triangle}\n\
|
||||
circle = {circle}\n\
|
||||
cross = {cross}\n\
|
||||
square = {square}\n\n\
|
||||
l1 = {l1}\n\
|
||||
r1 = {r1}\n\
|
||||
l2 = {l2}\n\
|
||||
r2 = {r2}\n\
|
||||
l3 = {l3}\n\
|
||||
r3 = {r3}\n\n\
|
||||
options = {options}\n\
|
||||
touchpad_center = {touchpad}\n\n\
|
||||
pad_up = {pad_up}\n\
|
||||
pad_down = {pad_down}\n\
|
||||
pad_left = {pad_left}\n\
|
||||
pad_right = {pad_right}\n\n\
|
||||
axis_left_x_minus = {ax_lx_min}\n\
|
||||
axis_left_x_plus = {ax_lx_plus}\n\
|
||||
axis_left_y_minus = {ax_ly_min}\n\
|
||||
axis_left_y_plus = {ax_ly_plus}\n\n\
|
||||
axis_right_x_minus = {ax_rx_min}\n\
|
||||
axis_right_x_plus = {ax_rx_plus}\n\
|
||||
axis_right_y_minus = {ax_ry_min}\n\
|
||||
axis_right_y_plus = {ax_ry_plus}\n\n\
|
||||
# Controller bindings\n\n\
|
||||
triangle = triangle\n\
|
||||
cross = cross\n\
|
||||
square = square\n\
|
||||
circle = circle\n\n\
|
||||
l1 = l1\n\
|
||||
l2 = l2\n\
|
||||
l3 = l3\n\
|
||||
r1 = r1\n\
|
||||
r2 = r2\n\
|
||||
r3 = r3\n\n\
|
||||
options = options\n\
|
||||
touchpad_center = back\n\n\
|
||||
pad_up = pad_up\n\
|
||||
pad_down = pad_down\n\
|
||||
pad_left = pad_left\n\
|
||||
pad_right = pad_right\n\n\
|
||||
axis_left_x = axis_left_x\n\
|
||||
axis_left_y = axis_left_y\n\
|
||||
axis_right_x = axis_right_x\n\
|
||||
axis_right_y = axis_right_y\n\n\
|
||||
# Range of deadzones: 1 (almost none) to 127 (max)\n\
|
||||
analog_deadzone = leftjoystick, {deadzone_l}, 127\n\
|
||||
analog_deadzone = rightjoystick, {deadzone_r}, 127\n\n\
|
||||
override_controller_color = false, 0, 0, 255\n",
|
||||
triangle = b.triangle,
|
||||
circle = b.circle,
|
||||
cross = b.cross,
|
||||
square = b.square,
|
||||
l1 = b.l1,
|
||||
r1 = b.r1,
|
||||
l2 = b.l2,
|
||||
r2 = b.r2,
|
||||
l3 = b.l3,
|
||||
r3 = b.r3,
|
||||
options = b.options,
|
||||
touchpad = b.touchpad_center,
|
||||
pad_up = b.pad_up,
|
||||
pad_down = b.pad_down,
|
||||
pad_left = b.pad_left,
|
||||
pad_right = b.pad_right,
|
||||
ax_lx_min = b.axis_left_x_minus,
|
||||
ax_lx_plus = b.axis_left_x_plus,
|
||||
ax_ly_min = b.axis_left_y_minus,
|
||||
ax_ly_plus = b.axis_left_y_plus,
|
||||
ax_rx_min = b.axis_right_x_minus,
|
||||
ax_rx_plus = b.axis_right_x_plus,
|
||||
ax_ry_min = b.axis_right_y_minus,
|
||||
ax_ry_plus = b.axis_right_y_plus,
|
||||
deadzone_l = config.deadzone_left,
|
||||
deadzone_r = config.deadzone_right,
|
||||
);
|
||||
|
||||
fs::write(&ini_path, content).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Patches the 8 hardcoded analog-stick scancodes in gow3-probe (Linux) and gow3-probe.exe (Windows).
|
||||
///
|
||||
/// The engine binary uses SDL_GetKeyboardState and checks 8 hardcoded 1-byte scancodes:
|
||||
/// Left Stick: +X (right), -X (left), +Y (down), -Y (up)
|
||||
/// Right Stick: +X (right), -X (left), +Y (down), -Y (up)
|
||||
///
|
||||
/// These scancodes are found immediately after a known unique anchor byte sequence
|
||||
/// that appears exactly once in each binary.
|
||||
fn patch_engine_analog_scancodes(base_dir: &Path, config: &LauncherConfig) -> Result<(), String> {
|
||||
let b = &config.keyboard_bindings;
|
||||
|
||||
// Convert user key strings to SDL scancodes
|
||||
let sc_lx_plus = key_to_sdl_scancode(&b.axis_left_x_plus); // Left +X (move right → D default)
|
||||
let sc_lx_minus = key_to_sdl_scancode(&b.axis_left_x_minus); // Left -X (move left → A default)
|
||||
let sc_ly_plus = key_to_sdl_scancode(&b.axis_left_y_plus); // Left +Y (move down → S default)
|
||||
let sc_ly_minus = key_to_sdl_scancode(&b.axis_left_y_minus); // Left -Y (move up → W default)
|
||||
let sc_rx_plus = key_to_sdl_scancode(&b.axis_right_x_plus); // Right +X (cam right → Right Arrow default)
|
||||
let sc_rx_minus = key_to_sdl_scancode(&b.axis_right_x_minus); // Right -X (cam left → Left Arrow default)
|
||||
let sc_ry_plus = key_to_sdl_scancode(&b.axis_right_y_plus); // Right +Y (cam down → Down Arrow default)
|
||||
let sc_ry_minus = key_to_sdl_scancode(&b.axis_right_y_minus); // Right -Y (cam up → Up Arrow default)
|
||||
|
||||
// ─── Linux binary ──────────────────────────────────────────────────────────
|
||||
// Anchor (unique prefix just before the stick scancode block):
|
||||
// f6 c4 01 74 06 41 c6 44 24 08 ff f6 c4 02 74 06 41 c6 44 24 09 ff
|
||||
// Layout after anchor (offsets relative to first byte after anchor):
|
||||
// +0 : 0f b6 55 [LX+] +4 : 0f b6 45 [LX-]
|
||||
// +24: 0f b6 55 [LY+] +28: 0f b6 45 [LY-]
|
||||
// +48: 0f b6 55 [RX+] +52: 0f b6 45 [RX-]
|
||||
// +72: 0f b6 55 [RY+] +76: 0f b6 45 [RY-]
|
||||
let mut probe_candidates: Vec<PathBuf> = vec![
|
||||
base_dir.join("bin/gow3-probe"),
|
||||
base_dir.join("out/gow3-probe"),
|
||||
base_dir.join("gow3-probe"),
|
||||
];
|
||||
if !config.game_dir.is_empty() {
|
||||
let gd = Path::new(&config.game_dir);
|
||||
probe_candidates.push(gd.join("bin/gow3-probe"));
|
||||
probe_candidates.push(gd.join("out/gow3-probe"));
|
||||
probe_candidates.push(gd.join("gow3-probe"));
|
||||
}
|
||||
for probe_path in &probe_candidates {
|
||||
if probe_path.is_file() {
|
||||
patch_binary_linux(
|
||||
probe_path,
|
||||
sc_lx_plus, sc_lx_minus, sc_ly_plus, sc_ly_minus,
|
||||
sc_rx_plus, sc_rx_minus, sc_ry_plus, sc_ry_minus,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Windows binary ────────────────────────────────────────────────────────
|
||||
// Anchor:
|
||||
// a9 00 01 00 00 74 04 c6 46 08 ff a9 00 02 00 00 74 04 c6 46 09 ff
|
||||
// Layout after anchor:
|
||||
// +0 : 0f b6 47 [LX-] +4 : 0f b6 4f [LX+]
|
||||
// +36: 0f b6 4f [LY+] +40: 0f b6 47 [LY-]
|
||||
// +72: 0f b6 4f [RX+] +76: 0f b6 47 [RX-]
|
||||
// +108:0f b6 4f [RY+] +112: 0f b6 47 [RY-]
|
||||
let mut probe_exe_candidates: Vec<PathBuf> = vec![
|
||||
base_dir.join("bin/gow3-probe.exe"),
|
||||
base_dir.join("out/gow3-probe.exe"),
|
||||
base_dir.join("gow3-probe.exe"),
|
||||
base_dir.join("bin/gow3.exe"),
|
||||
base_dir.join("gow3.exe"),
|
||||
base_dir.join("God of War III.exe"),
|
||||
];
|
||||
if !config.game_dir.is_empty() {
|
||||
let gd = Path::new(&config.game_dir);
|
||||
probe_exe_candidates.push(gd.join("bin/gow3-probe.exe"));
|
||||
probe_exe_candidates.push(gd.join("out/gow3-probe.exe"));
|
||||
probe_exe_candidates.push(gd.join("gow3-probe.exe"));
|
||||
probe_exe_candidates.push(gd.join("bin/gow3.exe"));
|
||||
probe_exe_candidates.push(gd.join("gow3.exe"));
|
||||
probe_exe_candidates.push(gd.join("God of War III.exe"));
|
||||
}
|
||||
for probe_path in &probe_exe_candidates {
|
||||
if probe_path.is_file() {
|
||||
patch_binary_windows(
|
||||
probe_path,
|
||||
sc_lx_plus, sc_lx_minus, sc_ly_plus, sc_ly_minus,
|
||||
sc_rx_plus, sc_rx_minus, sc_ry_plus, sc_ry_minus,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn patch_binary_linux(
|
||||
path: &Path,
|
||||
sc_lx_plus: Option<u8>, sc_lx_minus: Option<u8>,
|
||||
sc_ly_plus: Option<u8>, sc_ly_minus: Option<u8>,
|
||||
sc_rx_plus: Option<u8>, sc_rx_minus: Option<u8>,
|
||||
sc_ry_plus: Option<u8>, sc_ry_minus: Option<u8>,
|
||||
) {
|
||||
let anchor: &[u8] = &[
|
||||
0xf6, 0xc4, 0x01, 0x74, 0x06, 0x41, 0xc6, 0x44, 0x24, 0x08, 0xff,
|
||||
0xf6, 0xc4, 0x02, 0x74, 0x06, 0x41, 0xc6, 0x44, 0x24, 0x09, 0xff,
|
||||
];
|
||||
let Ok(mut data) = fs::read(path) else { return };
|
||||
let Some(pos) = find_bytes(&data, anchor) else { return };
|
||||
let base = pos + anchor.len();
|
||||
|
||||
// Verify expected opcodes at each offset before patching
|
||||
if !check_opcode(&data, base, 0, &[0x0f, 0xb6, 0x55]) { return }
|
||||
if !check_opcode(&data, base, 4, &[0x0f, 0xb6, 0x45]) { return }
|
||||
if !check_opcode(&data, base, 24, &[0x0f, 0xb6, 0x55]) { return }
|
||||
if !check_opcode(&data, base, 28, &[0x0f, 0xb6, 0x45]) { return }
|
||||
if !check_opcode(&data, base, 48, &[0x0f, 0xb6, 0x55]) { return }
|
||||
if !check_opcode(&data, base, 52, &[0x0f, 0xb6, 0x45]) { return }
|
||||
if !check_opcode(&data, base, 72, &[0x0f, 0xb6, 0x55]) { return }
|
||||
if !check_opcode(&data, base, 76, &[0x0f, 0xb6, 0x45]) { return }
|
||||
|
||||
if let Some(v) = sc_lx_plus { data[base + 3] = v; }
|
||||
if let Some(v) = sc_lx_minus { data[base + 7] = v; }
|
||||
if let Some(v) = sc_ly_plus { data[base + 27] = v; }
|
||||
if let Some(v) = sc_ly_minus { data[base + 31] = v; }
|
||||
if let Some(v) = sc_rx_plus { data[base + 51] = v; }
|
||||
if let Some(v) = sc_rx_minus { data[base + 55] = v; }
|
||||
if let Some(v) = sc_ry_plus { data[base + 75] = v; }
|
||||
if let Some(v) = sc_ry_minus { data[base + 79] = v; }
|
||||
|
||||
if let Err(_) = fs::write(path, &data) {
|
||||
let tmp = path.with_extension("tmp_probe_patch");
|
||||
if fs::write(&tmp, &data).is_ok() {
|
||||
let _ = fs::rename(&tmp, path);
|
||||
}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o755));
|
||||
}
|
||||
println!("[Input Patch] Analog stick keys patched in {:?}", path);
|
||||
}
|
||||
|
||||
fn patch_binary_windows(
|
||||
path: &Path,
|
||||
sc_lx_plus: Option<u8>, sc_lx_minus: Option<u8>,
|
||||
sc_ly_plus: Option<u8>, sc_ly_minus: Option<u8>,
|
||||
sc_rx_plus: Option<u8>, sc_rx_minus: Option<u8>,
|
||||
sc_ry_plus: Option<u8>, sc_ry_minus: Option<u8>,
|
||||
) {
|
||||
let anchor: &[u8] = &[
|
||||
0xa9, 0x00, 0x01, 0x00, 0x00, 0x74, 0x04, 0xc6, 0x46, 0x08, 0xff,
|
||||
0xa9, 0x00, 0x02, 0x00, 0x00, 0x74, 0x04, 0xc6, 0x46, 0x09, 0xff,
|
||||
];
|
||||
let Ok(mut data) = fs::read(path) else { return };
|
||||
let Some(pos) = find_bytes(&data, anchor) else { return };
|
||||
let base = pos + anchor.len();
|
||||
|
||||
// Verify expected opcodes at each offset before patching
|
||||
if !check_opcode(&data, base, 0, &[0x0f, 0xb6, 0x47]) { return }
|
||||
if !check_opcode(&data, base, 4, &[0x0f, 0xb6, 0x4f]) { return }
|
||||
if !check_opcode(&data, base, 36, &[0x0f, 0xb6, 0x4f]) { return }
|
||||
if !check_opcode(&data, base, 40, &[0x0f, 0xb6, 0x47]) { return }
|
||||
if !check_opcode(&data, base, 72, &[0x0f, 0xb6, 0x4f]) { return }
|
||||
if !check_opcode(&data, base, 76, &[0x0f, 0xb6, 0x47]) { return }
|
||||
if !check_opcode(&data, base, 108, &[0x0f, 0xb6, 0x4f]) { return }
|
||||
if !check_opcode(&data, base, 112, &[0x0f, 0xb6, 0x47]) { return }
|
||||
|
||||
if let Some(v) = sc_lx_minus { data[base + 3] = v; }
|
||||
if let Some(v) = sc_lx_plus { data[base + 7] = v; }
|
||||
if let Some(v) = sc_ly_plus { data[base + 39] = v; }
|
||||
if let Some(v) = sc_ly_minus { data[base + 43] = v; }
|
||||
if let Some(v) = sc_rx_plus { data[base + 75] = v; }
|
||||
if let Some(v) = sc_rx_minus { data[base + 79] = v; }
|
||||
if let Some(v) = sc_ry_plus { data[base + 111] = v; }
|
||||
if let Some(v) = sc_ry_minus { data[base + 115] = v; }
|
||||
|
||||
if let Err(_) = fs::write(path, &data) {
|
||||
let tmp = path.with_extension("tmp_probe_patch");
|
||||
if fs::write(&tmp, &data).is_ok() {
|
||||
let _ = fs::rename(&tmp, path);
|
||||
}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o755));
|
||||
}
|
||||
println!("[Input Patch] Analog stick keys patched in {:?}", path);
|
||||
}
|
||||
|
||||
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||||
haystack.windows(needle.len()).position(|w| w == needle)
|
||||
}
|
||||
|
||||
fn check_opcode(data: &[u8], base: usize, offset: usize, expected: &[u8]) -> bool {
|
||||
let start = base + offset;
|
||||
if start + expected.len() > data.len() {
|
||||
return false;
|
||||
}
|
||||
&data[start..start + expected.len()] == expected
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_patch_analog_scancodes_synthetic() {
|
||||
// Construct a synthetic buffer with the Linux anchor and opcodes
|
||||
let anchor = [
|
||||
0xf6, 0xc4, 0x01, 0x74, 0x06, 0x41, 0xc6, 0x44, 0x24, 0x08, 0xff,
|
||||
0xf6, 0xc4, 0x02, 0x74, 0x06, 0x41, 0xc6, 0x44, 0x24, 0x09, 0xff,
|
||||
];
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&[0x90; 16]); // nop padding
|
||||
buf.extend_from_slice(&anchor);
|
||||
let base = buf.len();
|
||||
|
||||
// 8 instructions:
|
||||
// +0: 0f b6 55 07 (D)
|
||||
// +4: 0f b6 45 04 (A)
|
||||
// +8..+23: 16 dummy bytes
|
||||
// +24: 0f b6 55 16 (S)
|
||||
// +28: 0f b6 45 1a (W)
|
||||
// +32..+47: 16 dummy bytes
|
||||
// +48: 0f b6 55 4f (Right)
|
||||
// +52: 0f b6 45 50 (Left)
|
||||
// +56..+71: 16 dummy bytes
|
||||
// +72: 0f b6 55 51 (Down)
|
||||
// +76: 0f b6 45 52 (Up)
|
||||
buf.extend_from_slice(&[0x0f, 0xb6, 0x55, 0x07]);
|
||||
buf.extend_from_slice(&[0x0f, 0xb6, 0x45, 0x04]);
|
||||
buf.extend_from_slice(&[0x00; 16]);
|
||||
buf.extend_from_slice(&[0x0f, 0xb6, 0x55, 0x16]);
|
||||
buf.extend_from_slice(&[0x0f, 0xb6, 0x45, 0x1a]);
|
||||
buf.extend_from_slice(&[0x00; 16]);
|
||||
buf.extend_from_slice(&[0x0f, 0xb6, 0x55, 0x4f]);
|
||||
buf.extend_from_slice(&[0x0f, 0xb6, 0x45, 0x50]);
|
||||
buf.extend_from_slice(&[0x00; 16]);
|
||||
buf.extend_from_slice(&[0x0f, 0xb6, 0x55, 0x51]);
|
||||
buf.extend_from_slice(&[0x0f, 0xb6, 0x45, 0x52]);
|
||||
buf.extend_from_slice(&[0x90; 32]);
|
||||
|
||||
let tmp_file = std::env::temp_dir().join("test_gow3_probe_synthetic");
|
||||
fs::write(&tmp_file, &buf).unwrap();
|
||||
|
||||
// Test patching: Left stick unchanged, Right stick -> I(12), K(14), J(13), L(15)
|
||||
patch_binary_linux(
|
||||
&tmp_file,
|
||||
Some(7), Some(4), Some(22), Some(26),
|
||||
Some(15), // RX+ = L
|
||||
Some(13), // RX- = J
|
||||
Some(14), // RY+ = K
|
||||
Some(12), // RY- = I
|
||||
);
|
||||
|
||||
let patched = fs::read(&tmp_file).unwrap();
|
||||
let _ = fs::remove_file(&tmp_file);
|
||||
|
||||
assert_eq!(patched[base + 51], 15); // RX+ = L
|
||||
assert_eq!(patched[base + 55], 13); // RX- = J
|
||||
assert_eq!(patched[base + 75], 14); // RY+ = K
|
||||
assert_eq!(patched[base + 79], 12); // RY- = I
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,45 @@ use std::process::Command;
|
||||
use crate::config::LauncherConfig;
|
||||
use crate::tuner::apply_settings;
|
||||
|
||||
pub const GITHUB_REPO: &str = "ScreamAngel/gow3_pclauncher";
|
||||
pub const GITHUB_REPO_URL: &str = "https://github.com/ScreamAngel/gow3_pclauncher.git";
|
||||
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 {
|
||||
@@ -14,61 +51,308 @@ pub struct UpdateInfo {
|
||||
pub commit_message: String,
|
||||
pub commit_date: String,
|
||||
pub is_new: bool,
|
||||
pub version_tag: Option<String>,
|
||||
pub has_missing_files: bool,
|
||||
pub missing_files: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn check_github_update(current_commit: &str) -> Result<UpdateInfo, String> {
|
||||
// 1. Fetch remote commit SHA using git ls-remote (fast, no GitHub API rate limit)
|
||||
let output = Command::new("git")
|
||||
.args(["ls-remote", GITHUB_REPO_URL, "HEAD"])
|
||||
.output()
|
||||
.map_err(|e| format!("git ls-remote failed: {}", e))?;
|
||||
pub fn parse_version_numbers(v: &str) -> Vec<u32> {
|
||||
v.trim_start_matches(|c: char| c == 'v' || c == 'V')
|
||||
.split('.')
|
||||
.filter_map(|s| {
|
||||
let num_str: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
num_str.parse::<u32>().ok()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
return Err("GitHub deposuna ulaşılamadı veya internet bağlantısı yok.".to_string());
|
||||
pub fn compare_version_strings(v1: &str, v2: &str) -> std::cmp::Ordering {
|
||||
let p1 = parse_version_numbers(v1);
|
||||
let p2 = parse_version_numbers(v2);
|
||||
let max_len = p1.len().max(p2.len());
|
||||
for i in 0..max_len {
|
||||
let n1 = p1.get(i).copied().unwrap_or(0);
|
||||
let n2 = p2.get(i).copied().unwrap_or(0);
|
||||
match n1.cmp(&n2) {
|
||||
std::cmp::Ordering::Equal => continue,
|
||||
other => return other,
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
|
||||
pub fn detect_launcher_version(base_dir: &Path, config: &LauncherConfig) -> String {
|
||||
// 1. If in a git repository and git is installed, ask git for the exact tag
|
||||
if base_dir.join(".git").is_dir() && is_git_available() {
|
||||
if let Ok(output) = Command::new("git")
|
||||
.args(["describe", "--tags", "--abbrev=0"])
|
||||
.current_dir(base_dir)
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let tag = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !tag.is_empty() {
|
||||
let formatted = if tag.starts_with('v') || tag.starts_with('V') {
|
||||
tag
|
||||
} else {
|
||||
format!("v{}", tag)
|
||||
};
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
let remote_sha = text
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(|ln| ln.split_whitespace().next())
|
||||
.ok_or_else(|| "Uzak commit SHA değeri okunamadı.".to_string())?
|
||||
.to_string();
|
||||
// 2. If config has a recorded launcher_version and it's valid
|
||||
if !config.launcher_version.trim().is_empty() {
|
||||
let cv = config.launcher_version.trim();
|
||||
let formatted = if cv.starts_with('v') || cv.starts_with('V') {
|
||||
cv.to_string()
|
||||
} else {
|
||||
format!("v{}", cv)
|
||||
};
|
||||
if compare_version_strings(&formatted, BUILTIN_VERSION) >= std::cmp::Ordering::Equal {
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch commit details (message and date) via curl from GitHub API if available
|
||||
let mut commit_message = "En son optimizasyonlar ve güncellemeler".to_string();
|
||||
let mut commit_date = String::new();
|
||||
// 3. Fallback to built-in compiled version
|
||||
BUILTIN_VERSION.to_string()
|
||||
}
|
||||
|
||||
let api_url = format!("https://api.github.com/repos/{}/commits/HEAD", GITHUB_REPO);
|
||||
if let Ok(api_out) = Command::new("curl")
|
||||
.args([
|
||||
"-s",
|
||||
"-H",
|
||||
"User-Agent: gow3-launcher",
|
||||
&api_url,
|
||||
])
|
||||
fn is_git_available() -> bool {
|
||||
Command::new("git").arg("--version").output().map(|o| o.status.success()).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn http_get_string(url: &str) -> Option<String> {
|
||||
// 1. Try curl (installed on Linux, macOS, and modern Windows 10+)
|
||||
if let Ok(out) = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: gow3-launcher", url])
|
||||
.output()
|
||||
{
|
||||
if api_out.status.success() {
|
||||
let json_str = String::from_utf8_lossy(&api_out.stdout);
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&json_str) {
|
||||
if let Some(msg) = val["commit"]["message"].as_str() {
|
||||
if out.status.success() {
|
||||
let body = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
if !body.trim().is_empty() {
|
||||
return Some(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to PowerShell (native on all Windows installations)
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let ps_code = format!(
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; \
|
||||
$wc = New-Object System.Net.WebClient; \
|
||||
$wc.Headers.Add('User-Agent', 'gow3-launcher'); \
|
||||
$wc.DownloadString('{}')",
|
||||
url.replace('\'', "''")
|
||||
);
|
||||
if let Ok(out) = Command::new("powershell")
|
||||
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps_code])
|
||||
.output()
|
||||
{
|
||||
if out.status.success() {
|
||||
let body = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
if !body.trim().is_empty() {
|
||||
return Some(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn http_download_file(url: &str, dest: &Path) -> Result<(), String> {
|
||||
// 1. Try curl
|
||||
if let Ok(out) = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: gow3-launcher", url, "-o", dest.to_str().unwrap_or("")])
|
||||
.output()
|
||||
{
|
||||
if out.status.success() && dest.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to PowerShell on Windows
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let ps_code = format!(
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; \
|
||||
$wc = New-Object System.Net.WebClient; \
|
||||
$wc.Headers.Add('User-Agent', 'gow3-launcher'); \
|
||||
$wc.DownloadFile('{}', '{}')",
|
||||
url.replace('\'', "''"),
|
||||
dest.to_string_lossy().replace('\'', "''")
|
||||
);
|
||||
if let Ok(out) = Command::new("powershell")
|
||||
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps_code])
|
||||
.output()
|
||||
{
|
||||
if out.status.success() && dest.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Dosya indirilemedi.".to_string())
|
||||
}
|
||||
|
||||
pub fn check_installation_integrity(base_dir: &Path) -> Vec<String> {
|
||||
let mut missing = Vec::new();
|
||||
|
||||
// 1. Icon file
|
||||
if !base_dir.join("gow3.png").is_file() {
|
||||
missing.push("gow3.png".to_string());
|
||||
}
|
||||
|
||||
// 2. Patches XML
|
||||
let has_patch = base_dir.join("patches/God_of_War_III_Remastered.xml").is_file()
|
||||
|| base_dir.join(".runtime/patches/God_of_War_III_Remastered.xml").is_file();
|
||||
if !has_patch {
|
||||
missing.push("patches/God_of_War_III_Remastered.xml".to_string());
|
||||
}
|
||||
|
||||
// 3. Auto tune script
|
||||
let has_script = base_dir.join("scripts/auto_tune.py").is_file()
|
||||
|| base_dir.join(".runtime/scripts/auto_tune.py").is_file();
|
||||
if !has_script {
|
||||
missing.push("scripts/auto_tune.py".to_string());
|
||||
}
|
||||
|
||||
missing
|
||||
}
|
||||
|
||||
pub fn check_github_update(
|
||||
base_dir: &Path,
|
||||
_current_commit: &str,
|
||||
current_version: &str,
|
||||
) -> Result<UpdateInfo, String> {
|
||||
crate::log_info!("Checking updates for running launcher {} (repo: {})...", current_version, GITHUB_REPO);
|
||||
let release_api_url = format!("https://api.github.com/repos/{}/releases/latest", GITHUB_REPO);
|
||||
let commit_api_url = format!("https://api.github.com/repos/{}/commits/HEAD", GITHUB_REPO);
|
||||
|
||||
let mut remote_sha = String::new();
|
||||
let mut commit_message = "En son optimizasyonlar ve güncellemeler".to_string();
|
||||
let mut commit_date = String::new();
|
||||
let mut version_tag: Option<String> = None;
|
||||
|
||||
// 1. Fetch release tag and notes via GitHub Releases API
|
||||
if let Some(json_str) = http_get_string(&release_api_url) {
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&json_str) {
|
||||
if let Some(tag) = val["tag_name"].as_str() {
|
||||
let trimmed = tag.trim().to_string();
|
||||
if !trimmed.is_empty() {
|
||||
version_tag = Some(trimmed);
|
||||
}
|
||||
}
|
||||
if let Some(body) = val["body"].as_str().filter(|b| !b.trim().is_empty()) {
|
||||
commit_message = body.lines().next().unwrap_or(body).to_string();
|
||||
}
|
||||
if let Some(d) = val["published_at"].as_str() {
|
||||
commit_date = d.split('T').next().unwrap_or(d).to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch commit info via GitHub Commits API
|
||||
if let Some(json_str) = http_get_string(&commit_api_url) {
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&json_str) {
|
||||
if let Some(sha) = val["sha"].as_str() {
|
||||
remote_sha = sha.to_string();
|
||||
}
|
||||
if let Some(msg) = val["commit"]["message"].as_str() {
|
||||
if commit_message == "En son optimizasyonlar ve güncellemeler" {
|
||||
commit_message = msg.lines().next().unwrap_or(msg).to_string();
|
||||
}
|
||||
if let Some(d) = val["commit"]["committer"]["date"].as_str() {
|
||||
}
|
||||
if let Some(d) = val["commit"]["committer"]["date"].as_str() {
|
||||
if commit_date.is_empty() {
|
||||
commit_date = d.split('T').next().unwrap_or(d).to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_new = current_commit.trim().is_empty() || current_commit.trim() != remote_sha.trim();
|
||||
// 3. Fallback: only if GitHub API failed AND git is installed on system
|
||||
if remote_sha.is_empty() && is_git_available() {
|
||||
if let Ok(output) = Command::new("git")
|
||||
.args(["ls-remote", GITHUB_REPO_URL, "HEAD"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
if let Some(sha) = text.lines().next().and_then(|ln| ln.split_whitespace().next()) {
|
||||
remote_sha = sha.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if version_tag.is_none() {
|
||||
if let Ok(output) = Command::new("git")
|
||||
.args(["ls-remote", "--tags", GITHUB_REPO_URL])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
let mut highest_tag: Option<String> = None;
|
||||
for line in text.lines() {
|
||||
if let Some(ref_part) = line.split_whitespace().nth(1) {
|
||||
if let Some(tag) = ref_part.strip_prefix("refs/tags/") {
|
||||
if !tag.ends_with("^{}") && (tag.starts_with('v') || tag.starts_with('V')) {
|
||||
if let Some(ref h) = highest_tag {
|
||||
if compare_version_strings(tag, h) > std::cmp::Ordering::Equal {
|
||||
highest_tag = Some(tag.to_string());
|
||||
}
|
||||
} else {
|
||||
highest_tag = Some(tag.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
version_tag = highest_tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if remote_sha.is_empty() && version_tag.is_none() {
|
||||
return Err("GitHub deposuna ulaşılamadı. İnternet bağlantınızı kontrol edin veya daha sonra tekrar deneyin.".to_string());
|
||||
}
|
||||
|
||||
// Version Check:
|
||||
// Only true if the remote version tag is STRICTLY HIGHER than current running launcher version.
|
||||
let is_newer_version = if let Some(ref tag) = version_tag {
|
||||
compare_version_strings(tag, current_version) > std::cmp::Ordering::Equal
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let missing_files = check_installation_integrity(base_dir);
|
||||
let has_missing_files = !missing_files.is_empty();
|
||||
|
||||
// Do NOT flag update as needed if current launcher is already at or above the remote release tag!
|
||||
// Prevents redundant re-downloads when already running v1.7.
|
||||
let is_new = is_newer_version || has_missing_files;
|
||||
|
||||
crate::log_info!(
|
||||
"Update check complete: remote tag={:?}, is_newer_version={}, has_missing_files={} -> is_new={}",
|
||||
version_tag,
|
||||
is_newer_version,
|
||||
has_missing_files,
|
||||
is_new
|
||||
);
|
||||
|
||||
Ok(UpdateInfo {
|
||||
commit_sha: remote_sha,
|
||||
commit_message,
|
||||
commit_date,
|
||||
is_new,
|
||||
version_tag,
|
||||
has_missing_files,
|
||||
missing_files,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,21 +362,17 @@ pub fn apply_github_update(
|
||||
config: &mut LauncherConfig,
|
||||
) -> Result<String, String> {
|
||||
let binary_name = if cfg!(target_os = "windows") {
|
||||
if base_dir.join("gow3_launcher_win.exe").is_file() {
|
||||
"gow3_launcher_win.exe"
|
||||
} else {
|
||||
"gow3-launcher-win.exe"
|
||||
}
|
||||
"gow3-launcher-win.exe"
|
||||
} else {
|
||||
"gow3-launcher"
|
||||
};
|
||||
|
||||
let is_git_repo = base_dir.join(".git").is_dir();
|
||||
let is_git_repo = base_dir.join(".git").is_dir() && is_git_available();
|
||||
|
||||
if is_git_repo {
|
||||
// Path A: User cloned via git, perform git pull
|
||||
// Path A: User cloned via git and git is available on system
|
||||
let pull_output = Command::new("git")
|
||||
.args(["pull", "origin", "main"])
|
||||
.args(["pull", "--tags", "origin", "main"])
|
||||
.current_dir(base_dir)
|
||||
.output()
|
||||
.map_err(|e| format!("'git pull' çalıştırılamadı: {}", e))?;
|
||||
@@ -102,7 +382,6 @@ pub fn apply_github_update(
|
||||
return Err(format!("Git güncellemesi başarısız oldu: {}", err));
|
||||
}
|
||||
|
||||
// Ensure launcher binary is executable
|
||||
let launcher_bin = base_dir.join(binary_name);
|
||||
if launcher_bin.is_file() {
|
||||
#[cfg(unix)]
|
||||
@@ -112,7 +391,7 @@ pub fn apply_github_update(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Path B: Standalone folder without .git; download latest launcher binary and patch files
|
||||
// Path B: Standalone folder or git not installed; download latest launcher binary and patch files
|
||||
let raw_launcher_url = format!(
|
||||
"https://raw.githubusercontent.com/{}/main/{}",
|
||||
GITHUB_REPO, binary_name
|
||||
@@ -120,15 +399,8 @@ pub fn apply_github_update(
|
||||
let launcher_bin = base_dir.join(binary_name);
|
||||
let launcher_tmp = base_dir.join(format!("{}.new", binary_name));
|
||||
|
||||
let download = Command::new("curl")
|
||||
.args(["-fsSL", &raw_launcher_url, "-o", launcher_tmp.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| format!("Yeni launcher indirilemedi: {}", e))?;
|
||||
|
||||
if !download.status.success() {
|
||||
let _ = fs::remove_file(&launcher_tmp);
|
||||
return Err("GitHub üzerinden yeni launcher ikili dosyası indirilemedi.".to_string());
|
||||
}
|
||||
http_download_file(&raw_launcher_url, &launcher_tmp)
|
||||
.map_err(|e| format!("Yeni launcher indirilemedi ({}): {}", raw_launcher_url, e))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -143,7 +415,6 @@ pub fn apply_github_update(
|
||||
let _ = fs::rename(&launcher_bin, &launcher_old);
|
||||
}
|
||||
|
||||
// Atomically replace running binary
|
||||
fs::rename(&launcher_tmp, &launcher_bin)
|
||||
.map_err(|e| format!("Launcher güncellenemedi: {}", e))?;
|
||||
|
||||
@@ -155,23 +426,25 @@ pub fn apply_github_update(
|
||||
GITHUB_REPO
|
||||
);
|
||||
let patch_dest = patches_dir.join("God_of_War_III_Remastered.xml");
|
||||
let _ = Command::new("curl")
|
||||
.args(["-fsSL", &raw_patch_url, "-o", patch_dest.to_str().unwrap()])
|
||||
.output();
|
||||
let _ = http_download_file(&raw_patch_url, &patch_dest);
|
||||
|
||||
// Also fetch gow3.png if missing
|
||||
let icon_file = base_dir.join("gow3.png");
|
||||
if !icon_file.is_file() {
|
||||
let raw_icon_url = format!("https://raw.githubusercontent.com/{}/main/gow3.png", GITHUB_REPO);
|
||||
let _ = http_download_file(&raw_icon_url, &icon_file);
|
||||
}
|
||||
|
||||
// Also ensure runtime assets are unpacked
|
||||
let _ = crate::runner::ensure_runtime_assets(base_dir);
|
||||
}
|
||||
|
||||
// Re-apply optimizations and settings
|
||||
let _ = apply_settings(
|
||||
base_dir,
|
||||
config.target_fps,
|
||||
config.patch_delta_time,
|
||||
config.patch_readbacks,
|
||||
config.vram_gc_budget_mb,
|
||||
config.dmem_mb,
|
||||
);
|
||||
let _ = apply_settings(base_dir, config);
|
||||
|
||||
// Update last_git_commit in config
|
||||
config.last_git_commit = update.commit_sha.clone();
|
||||
if let Some(ref tag) = update.version_tag {
|
||||
config.launcher_version = tag.clone();
|
||||
}
|
||||
let _ = config.save(base_dir);
|
||||
|
||||
Ok("Launcher ve optimizasyonlar başarıyla güncellendi! Yeni sürümün geçerli olması için lütfen launcher'ı yeniden başlatın.".to_string())
|
||||
@@ -183,10 +456,35 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_check_github_update() {
|
||||
let res = check_github_update("test_dummy_sha");
|
||||
let res = check_github_update(Path::new(".."), "test_dummy_sha", "v0.1");
|
||||
if let Ok(info) = res {
|
||||
assert!(!info.commit_sha.is_empty());
|
||||
assert!(!info.commit_sha.is_empty() || info.version_tag.is_some());
|
||||
assert!(info.is_new);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
assert!(!info.is_new);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_compare() {
|
||||
assert_eq!(compare_version_strings("v1.3", "v1.4"), std::cmp::Ordering::Less);
|
||||
assert_eq!(compare_version_strings("v1.4", "v1.4"), std::cmp::Ordering::Equal);
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 250 B |
@@ -1,10 +1,13 @@
|
||||
{
|
||||
"first_launch_done": true,
|
||||
"language": "tr",
|
||||
"game_dir": "/home/screamangel/İndirilenler/GOW3 Emu Linux/CUSA01623",
|
||||
"target_fps": 60,
|
||||
"game_dir": "/home/screamangel/İndirilenler/GOW3 Emu Linux/CUSA01715",
|
||||
"target_fps": 144,
|
||||
"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,
|
||||
@@ -13,7 +16,62 @@
|
||||
"gpu_name": "NVIDIA GeForce RTX 4070 Laptop GPU",
|
||||
"vram_mb": 8188,
|
||||
"ram_mb": 15603,
|
||||
"refresh_rate_hz": 144
|
||||
"refresh_rate_hz": 144,
|
||||
"cpu_name": "12th Gen Intel(R) Core(TM) i7-12700H",
|
||||
"cpu_cores": 14,
|
||||
"cpu_threads": 20
|
||||
},
|
||||
"last_git_commit": "c5638d9956002b8a9f09a240028f6793ddf7b2f8"
|
||||
"last_git_commit": "0b4abe07acb711848d1b5d72402a9de8c0ad9844",
|
||||
"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,
|
||||
"disable_dof": false,
|
||||
"graphic_preset": "Balanced",
|
||||
"display_mode": "Borderless",
|
||||
"vsync": true,
|
||||
"background_controller_input": false,
|
||||
"hide_mouse_cursor": true,
|
||||
"is_motion_controls_enabled": true,
|
||||
"deadzone_left": 2,
|
||||
"deadzone_right": 2,
|
||||
"keyboard_bindings": {
|
||||
"triangle": "u",
|
||||
"circle": "j",
|
||||
"cross": "g",
|
||||
"square": "t",
|
||||
"l1": "q",
|
||||
"r1": "e",
|
||||
"l2": "z",
|
||||
"r2": "x",
|
||||
"l3": "c",
|
||||
"r3": "v",
|
||||
"options": "enter",
|
||||
"touchpad_center": "backspace",
|
||||
"pad_up": "up",
|
||||
"pad_down": "down",
|
||||
"pad_left": "left",
|
||||
"pad_right": "right",
|
||||
"axis_left_x_minus": "a",
|
||||
"axis_left_x_plus": "d",
|
||||
"axis_left_y_minus": "w",
|
||||
"axis_left_y_plus": "s",
|
||||
"axis_right_x_minus": "kp4",
|
||||
"axis_right_x_plus": "kp6",
|
||||
"axis_right_y_minus": "kp8",
|
||||
"axis_right_y_plus": "kp2"
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -17,59 +17,73 @@
|
||||
<Line Type="bytes" Address="0x007cd918" Value="00000090"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Bug Fix - Startup Black Screen" Note="Fixes the black screen at startup with the FPS counter running: the frame pacing index starts at 2 instead of 0, so the second frame no longer waits for a flip that only it can trigger." Author="SpartanPort" PatchVer="1.0" AppVer="01.02" AppElf="eboot.bin" isEnabled="true">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x018ba600" Value="02000000"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Bug Fix - Intro Crash Fix" Note="Universal fix for engine pool memory exhaustion crashes (such as intro transition crash at 0x2c54b2 and magic/combat crash at 0x2de2d2). Enables automatic slab growth for all engine pool allocators and adds defensive null checks." Author="gow3-launcher" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="true">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x006b36ae" Value="6a305eba00080000b9800000006a04415890"/>
|
||||
<Line Type="bytes" Address="0x006c54a7" Value="0f84d600000090"/>
|
||||
<Line Type="bytes" Address="0x006c6387" Value="0f84d600000090"/>
|
||||
<Line Type="bytes" Address="0x0058dca0" Value="4585ff750441b78090"/>
|
||||
<Line Type="bytes" Address="0x006de2c7" Value="0f84d600000090"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Resolution Patch - 480p" Note="Decreases game output resolution to 854x480 and increases Video Arena to 2.25 GiB. Requires setting DMEM to 2048 MB or more. Only leave one resolution patch enabled; do not enable it with Bug Fix - Texture Corruption Fix." Author="kvicken + cuesta4" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x005f0b3b" Value="56030000"/>
|
||||
<Line Type="bytes" Address="0x005f0b43" Value="e0010000"/>
|
||||
<Line Type="bytes" Address="0x005f0b4c" Value="5603"/>
|
||||
<Line Type="bytes" Address="0x005f0b53" Value="e001"/>
|
||||
<Line Type="bytes" Address="0x005643f3" Value="e0010000"/>
|
||||
<Line Type="bytes" Address="0x00564400" Value="56030000"/>
|
||||
<Line Type="bytes" Address="0x005ff769" Value="56030000"/>
|
||||
<Line Type="bytes" Address="0x005ff76f" Value="e0010000"/>
|
||||
<Line Type="bytes" Address="0x005f0c5b" Value="56030000"/>
|
||||
<Line Type="bytes" Address="0x005f0c63" Value="e0010000"/>
|
||||
<Line Type="bytes" Address="0x005f0c6c" Value="5603"/>
|
||||
<Line Type="bytes" Address="0x005f0c73" Value="e001"/>
|
||||
<Line Type="bytes" Address="0x005643e3" Value="e0010000"/>
|
||||
<Line Type="bytes" Address="0x005643f0" Value="56030000"/>
|
||||
<Line Type="bytes" Address="0x005ff889" Value="56030000"/>
|
||||
<Line Type="bytes" Address="0x005ff88f" Value="e0010000"/>
|
||||
<Line Type="bytes" Address="0x007cd918" Value="00000090"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Resolution Patch - 720p" Note="Decreases game output resolution to 1280x720 and increases Video Arena to 2.25 GiB. Requires setting DMEM to 2048 MB or more. Only leave one resolution patch enabled; do not enable it with Bug Fix - Texture Corruption Fix." Author="kvicken + cuesta4" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x005f0b3b" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005f0b43" Value="d0020000"/>
|
||||
<Line Type="bytes" Address="0x005f0b4c" Value="0005"/>
|
||||
<Line Type="bytes" Address="0x005f0b53" Value="d002"/>
|
||||
<Line Type="bytes" Address="0x005643f3" Value="d0020000"/>
|
||||
<Line Type="bytes" Address="0x00564400" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005ff769" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005ff76f" Value="d0020000"/>
|
||||
<Line Type="bytes" Address="0x005f0c5b" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005f0c63" Value="d0020000"/>
|
||||
<Line Type="bytes" Address="0x005f0c6c" Value="0005"/>
|
||||
<Line Type="bytes" Address="0x005f0c73" Value="d002"/>
|
||||
<Line Type="bytes" Address="0x005643e3" Value="d0020000"/>
|
||||
<Line Type="bytes" Address="0x005643f0" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005ff889" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005ff88f" Value="d0020000"/>
|
||||
<Line Type="bytes" Address="0x007cd918" Value="00000090"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Resolution Patch - 800p" Note="Sets the game output resolution to 1280x800 (16:10, the Steam Deck panel) and increases Video Arena to 2.25 GiB. Requires setting DMEM to 2048 MB or more. Only leave one resolution patch enabled; do not enable it with Bug Fix - Texture Corruption Fix." Author="kvicken + cuesta4 (1280x800 adapted for the Steam Deck)" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x005f0b3b" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005f0b43" Value="20030000"/>
|
||||
<Line Type="bytes" Address="0x005f0b4c" Value="0005"/>
|
||||
<Line Type="bytes" Address="0x005f0b53" Value="2003"/>
|
||||
<Line Type="bytes" Address="0x005643f3" Value="20030000"/>
|
||||
<Line Type="bytes" Address="0x00564400" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005ff769" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005ff76f" Value="20030000"/>
|
||||
<Line Type="bytes" Address="0x005f0c5b" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005f0c63" Value="20030000"/>
|
||||
<Line Type="bytes" Address="0x005f0c6c" Value="0005"/>
|
||||
<Line Type="bytes" Address="0x005f0c73" Value="2003"/>
|
||||
<Line Type="bytes" Address="0x005643e3" Value="20030000"/>
|
||||
<Line Type="bytes" Address="0x005643f0" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005ff889" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x005ff88f" Value="20030000"/>
|
||||
<Line Type="bytes" Address="0x007cd918" Value="00000090"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Resolution Patch - 1440p" Note="Increases game output resolution to 2560x1440 and Video Arena to 2.75 GiB. Requires setting DMEM to 3072 MB or more. Only leave one resolution patch enabled; do not enable it with Bug Fix - Texture Corruption Fix." Author="kvicken + cuesta4" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x005f0b3b" Value="000a0000"/>
|
||||
<Line Type="bytes" Address="0x005f0b43" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x005f0b4c" Value="000a"/>
|
||||
<Line Type="bytes" Address="0x005f0b53" Value="a005"/>
|
||||
<Line Type="bytes" Address="0x005643f3" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x00564400" Value="000a0000"/>
|
||||
<Line Type="bytes" Address="0x005ff769" Value="000a0000"/>
|
||||
<Line Type="bytes" Address="0x005ff76f" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x005f0c5b" Value="000a0000"/>
|
||||
<Line Type="bytes" Address="0x005f0c63" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x005f0c6c" Value="000a"/>
|
||||
<Line Type="bytes" Address="0x005f0c73" Value="a005"/>
|
||||
<Line Type="bytes" Address="0x005643e3" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x005643f0" Value="000a0000"/>
|
||||
<Line Type="bytes" Address="0x005ff889" Value="000a0000"/>
|
||||
<Line Type="bytes" Address="0x005ff88f" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x007c46b4" Value="000a0000"/>
|
||||
<Line Type="bytes" Address="0x007c46b8" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x0056bb82" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x0060953d" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x0056bb72" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x0060965d" Value="a0050000"/>
|
||||
<Line Type="bytes" Address="0x007f2a3c" Value="00050000"/>
|
||||
<Line Type="bytes" Address="0x007f2a40" Value="d0020000"/>
|
||||
<Line Type="bytes" Address="0x007cd918" Value="000000b0"/>
|
||||
@@ -77,18 +91,18 @@
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Resolution Patch - 1800p" Note="Increases game output resolution to 3200x1800 and Video Arena to 3.25 GiB. Requires setting DMEM to 4608 MB or more. Only leave one resolution patch enabled; do not enable it with Bug Fix - Texture Corruption Fix." Author="kvicken + cuesta4" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x005f0b3b" Value="800c0000"/>
|
||||
<Line Type="bytes" Address="0x005f0b43" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x005f0b4c" Value="800c"/>
|
||||
<Line Type="bytes" Address="0x005f0b53" Value="0807"/>
|
||||
<Line Type="bytes" Address="0x005643f3" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x00564400" Value="800c0000"/>
|
||||
<Line Type="bytes" Address="0x005ff769" Value="800c0000"/>
|
||||
<Line Type="bytes" Address="0x005ff76f" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x005f0c5b" Value="800c0000"/>
|
||||
<Line Type="bytes" Address="0x005f0c63" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x005f0c6c" Value="800c"/>
|
||||
<Line Type="bytes" Address="0x005f0c73" Value="0807"/>
|
||||
<Line Type="bytes" Address="0x005643e3" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x005643f0" Value="800c0000"/>
|
||||
<Line Type="bytes" Address="0x005ff889" Value="800c0000"/>
|
||||
<Line Type="bytes" Address="0x005ff88f" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x007c46b4" Value="800c0000"/>
|
||||
<Line Type="bytes" Address="0x007c46b8" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x0056bb82" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x0060953d" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x0056bb72" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x0060965d" Value="08070000"/>
|
||||
<Line Type="bytes" Address="0x007f2a3c" Value="40060000"/>
|
||||
<Line Type="bytes" Address="0x007f2a40" Value="84030000"/>
|
||||
<Line Type="bytes" Address="0x007cd918" Value="000000d0"/>
|
||||
@@ -96,39 +110,77 @@
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Resolution Patch - 4K" Note="Increases game output resolution to 3840x2160 and Video Arena to approximately 4 GB (3.75 GiB). Requires setting DMEM to 6144 MB or more. Only leave one resolution patch enabled; do not enable it with Bug Fix - Texture Corruption Fix." Author="kvicken + cuesta4" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x005f0b3b" Value="000f0000"/>
|
||||
<Line Type="bytes" Address="0x005f0b43" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x005f0b4c" Value="000f"/>
|
||||
<Line Type="bytes" Address="0x005f0b53" Value="7008"/>
|
||||
<Line Type="bytes" Address="0x005643f3" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x00564400" Value="000f0000"/>
|
||||
<Line Type="bytes" Address="0x005ff769" Value="000f0000"/>
|
||||
<Line Type="bytes" Address="0x005ff76f" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x005f0c5b" Value="000f0000"/>
|
||||
<Line Type="bytes" Address="0x005f0c63" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x005f0c6c" Value="000f"/>
|
||||
<Line Type="bytes" Address="0x005f0c73" Value="7008"/>
|
||||
<Line Type="bytes" Address="0x005643e3" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x005643f0" Value="000f0000"/>
|
||||
<Line Type="bytes" Address="0x005ff889" Value="000f0000"/>
|
||||
<Line Type="bytes" Address="0x005ff88f" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x007c46b4" Value="000f0000"/>
|
||||
<Line Type="bytes" Address="0x007c46b8" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x0056bb82" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x0060953d" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x0056bb72" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x0060965d" Value="70080000"/>
|
||||
<Line Type="bytes" Address="0x007f2a3c" Value="80070000"/>
|
||||
<Line Type="bytes" Address="0x007f2a40" Value="38040000"/>
|
||||
<Line Type="bytes" Address="0x007cd918" Value="000000f0"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 120 FPS" Note="Raises the engine maximum framerate from 59.94 to 120 FPS so the frametime clamp produces a 1/120 second gameplay delta. Corrects swing energy calculations to match the original 60 Hz simulation. Requires setting VBlank to 120 FPS." Author="kvicken" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 30 FPS" Note="Sets the engine maximum framerate to 30 FPS with correct 60 Hz swing energy simulation. Requires setting VBlank to 30 FPS." Author="gow3-launcher" PatchVer="1.2" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05a7c12600"/>
|
||||
<Line Type="bytes" Address="0x0075dbc4" Value="0000f042"/>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05e7c22600"/>
|
||||
<Line Type="bytes" Address="0x0075dd00" Value="0000f041"/>
|
||||
<Line Type="bytes" Address="0x0075dd04" Value="0000f041"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 144 FPS" Note="Raises the engine maximum framerate from 59.94 to 144 FPS so the frametime clamp produces a 1/144 second gameplay delta. Corrects swing energy calculations to match the original 60 Hz simulation. Requires setting VBlank to 144 FPS." Author="kvicken" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 40 FPS" Note="Sets the engine maximum framerate to 40 FPS (ideal for Steam Deck 40 Hz) with correct 60 Hz swing energy simulation. Requires setting VBlank to 40 FPS." Author="gow3-launcher" PatchVer="1.2" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05a7c12600"/>
|
||||
<Line Type="bytes" Address="0x0075dbc4" Value="00001043"/>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05e7c22600"/>
|
||||
<Line Type="bytes" Address="0x0075dd00" Value="00002042"/>
|
||||
<Line Type="bytes" Address="0x0075dd04" Value="00002042"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 240 FPS" Note="The 120 FPS patch with the engine maximum framerate at 240 FPS (frametime clamp of 1/240 second), adapted by Felipe Prado. Same swing energy correction. Requires setting VBlank to 240 FPS." Author="kvicken, gow3_pc" PatchVer="1.0" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 60 FPS" Note="Sets the engine maximum framerate to 60 FPS with correct 60 Hz swing energy simulation. Requires setting VBlank to 60 FPS." Author="gow3-launcher" PatchVer="1.2" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05a7c12600"/>
|
||||
<Line Type="bytes" Address="0x0075dbc4" Value="00007043"/>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05e7c22600"/>
|
||||
<Line Type="bytes" Address="0x0075dd00" Value="00007042"/>
|
||||
<Line Type="bytes" Address="0x0075dd04" Value="00007042"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 90 FPS" Note="Sets the engine maximum framerate to 90 FPS (ideal for Steam Deck OLED 90 Hz) with correct 60 Hz swing energy simulation. Requires setting VBlank to 90 FPS." Author="gow3-launcher" PatchVer="1.2" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05e7c22600"/>
|
||||
<Line Type="bytes" Address="0x0075dd00" Value="0000b442"/>
|
||||
<Line Type="bytes" Address="0x0075dd04" Value="0000b442"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 120 FPS" Note="Raises the engine maximum framerate from 59.94 to 120 FPS. Corrects swing energy calculations to match original 60 Hz simulation so game speed and physics stay normal. Requires setting VBlank to 120 FPS." Author="kvicken, gow3-launcher" PatchVer="1.2" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05e7c22600"/>
|
||||
<Line Type="bytes" Address="0x0075dd00" Value="0000f042"/>
|
||||
<Line Type="bytes" Address="0x0075dd04" Value="0000f042"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 144 FPS" Note="Raises the engine maximum framerate from 59.94 to 144 FPS. Corrects swing energy calculations to match original 60 Hz simulation. Requires setting VBlank to 144 FPS." Author="kvicken, gow3-launcher" PatchVer="1.2" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05e7c22600"/>
|
||||
<Line Type="bytes" Address="0x0075dd00" Value="00001043"/>
|
||||
<Line Type="bytes" Address="0x0075dd04" Value="00001043"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 165 FPS" Note="Raises the engine maximum framerate from 59.94 to 165 FPS. Corrects swing energy calculations to match original 60 Hz simulation. Requires setting VBlank to 165 FPS." Author="gow3-launcher" PatchVer="1.2" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05e7c22600"/>
|
||||
<Line Type="bytes" Address="0x0075dd00" Value="00002543"/>
|
||||
<Line Type="bytes" Address="0x0075dd04" Value="00002543"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Frame Rate Patch - 240 FPS" Note="The 120 FPS patch with the engine maximum framerate at 240 FPS, with swing energy correction. Requires setting VBlank to 240 FPS." Author="kvicken, Felipe Prado, gow3-launcher" PatchVer="1.2" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0048ff0a" Value="488d05e7c22600"/>
|
||||
<Line Type="bytes" Address="0x0075dd00" Value="00007043"/>
|
||||
<Line Type="bytes" Address="0x0075dd04" Value="00007043"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Skip Any Video With X Button" Note="Allows manual skipping of cinematics any cinematic with the X button. Thanks to illusion0001 aka illusionyy for the PS3 patch which made this very easy." Author="cuesta4" PatchVer="1.0" AppVer="01.02" AppElf="eboot.bin" isEnabled="true">
|
||||
@@ -137,4 +189,16 @@
|
||||
<Line Type="bytes" Address="0x0041b2e7" Value="b901000000909090"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Graphics - Disable Motion Blur" Note="Disables the camera and object motion blur render pass, improving performance on low-end systems and removing motion smearing." Author="gow3-launcher" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0056ae80" Value="c3"/>
|
||||
<Line Type="bytes" Address="0x0056b0e0" Value="c3"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Graphics - Disable Depth of Field" Note="Disables the Depth of Field (DoF) post-processing blur pass, increasing FPS on budget GPUs and keeping background details sharp." Author="gow3-launcher" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x00565ac0" Value="c3"/>
|
||||
<Line Type="bytes" Address="0x00566090" Value="c3"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
</Patch>
|
||||
@@ -118,8 +118,10 @@ def calculate_settings(specs, override_hz=None):
|
||||
gc_budget_mb = 4608
|
||||
elif vram_mb >= 3500:
|
||||
gc_budget_mb = 3072
|
||||
else:
|
||||
elif vram_mb >= 2500:
|
||||
gc_budget_mb = 2048
|
||||
else:
|
||||
gc_budget_mb = 1280
|
||||
|
||||
# Guest DMEM Pool (based on RAM)
|
||||
if ram_mb >= 24000:
|
||||
@@ -198,10 +200,7 @@ def update_run_sh(settings):
|
||||
dmem = settings["dmem_mb"]
|
||||
|
||||
# Target patches string
|
||||
if target_hz != 60:
|
||||
patches_str = f"Bug Fix - Texture Corruption Fix;Skip Any Video With X Button;Frame Rate Patch - {target_hz} FPS"
|
||||
else:
|
||||
patches_str = "Bug Fix - Texture Corruption Fix;Skip Any Video With X Button"
|
||||
patches_str = f"Bug Fix - Texture Corruption Fix;Bug Fix - Startup Black Screen;Bug Fix - Intro Crash Fix;Skip Any Video With X Button;Frame Rate Patch - {target_hz} FPS"
|
||||
|
||||
# Update or add GOW3_GC_BUDGET_MB near the top
|
||||
if re.search(r"^export GOW3_GC_BUDGET_MB=.*$", content, re.M):
|
||||
@@ -291,6 +290,9 @@ def update_user_config_toml(settings):
|
||||
content = re.sub(r"^readbacks\s*=\s*\w+", "readbacks = true", content, flags=re.M)
|
||||
content = re.sub(r"^readbackLinearImages\s*=\s*\w+", "readbackLinearImages = true", content, flags=re.M)
|
||||
content = re.sub(r"^copyGPUBuffers\s*=\s*\w+", "copyGPUBuffers = true", content, flags=re.M)
|
||||
content = re.sub(r"^directMemoryAccess\s*=\s*\w+", "directMemoryAccess = true", content, flags=re.M)
|
||||
content = re.sub(r"^pipelineCacheEnable\s*=\s*\w+", "pipelineCacheEnable = true", content, flags=re.M)
|
||||
content = re.sub(r"^pipelineCacheArchive\s*=\s*\w+", "pipelineCacheArchive = true", content, flags=re.M)
|
||||
|
||||
toml_path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ import json
|
||||
from pathlib import Path
|
||||
|
||||
RESOLUTIONS = ('native', '480p', '720p', '800p', '1440p', '1800p', '4K')
|
||||
ENGINE_FPS = (60, 120, 144, 240)
|
||||
DEFAULTS = {'render_resolution': 'native', 'engine_fps': '60', 'deferred_readback': '1',
|
||||
'stale_readback': '1', 'async_shaders': '1'}
|
||||
ENGINE_FPS = (30, 40, 60, 90, 120, 144, 165, 240)
|
||||
DEFAULTS = {'render_resolution': 'native', 'engine_fps': '60', 'deferred_readback': '0',
|
||||
'stale_readback': '0', 'async_shaders': '1'}
|
||||
RESOLUTION_PREFIX = 'Resolution Patch'
|
||||
FPS_PREFIX = 'Frame Rate Patch'
|
||||
TEXTURE_FIX = 'Bug Fix - Texture Corruption Fix'
|
||||
@@ -96,12 +96,16 @@ def patch_names(names, options, texture_fix_available=True):
|
||||
names = [n for n in names if n != TEXTURE_FIX] + [f'{RESOLUTION_PREFIX} - {resolution}']
|
||||
elif texture_fix_available and TEXTURE_FIX not in names:
|
||||
names.insert(0, TEXTURE_FIX)
|
||||
if options['engine_fps'] != '60':
|
||||
names.append(f'{FPS_PREFIX} - {options["engine_fps"]} FPS')
|
||||
fps = options.get('engine_fps', '60')
|
||||
names.append(f'{FPS_PREFIX} - {fps} FPS')
|
||||
return names
|
||||
|
||||
|
||||
def environment(options):
|
||||
return {'GOW3_DEFERRED_READBACK': options['deferred_readback'],
|
||||
'GOW3_STALE_READBACK': options['stale_readback'],
|
||||
'GOW3_ASYNC_SHADERS': options['async_shaders']}
|
||||
return {'GOW3_DEFERRED_READBACK': options.get('deferred_readback', '0'),
|
||||
'GOW3_STALE_READBACK': options.get('stale_readback', '0'),
|
||||
'GOW3_ASYNC_SHADERS': options.get('async_shaders', '1'),
|
||||
'GOW3_READBACKS': '1',
|
||||
'GOW3_READBACK_LINEAR': '1',
|
||||
'GOW3_FORCE_READBACK': '1',
|
||||
'GOW3_FPS_LIMIT': options.get('engine_fps', '60')}
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||