Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf8fd8fb6c | ||
|
|
7ae4948793 | ||
|
|
9147960de5 | ||
|
|
3a96e9845b | ||
|
|
3992455718 | ||
|
|
08da3bec2c | ||
|
|
04f61833ea | ||
|
|
a60dc8bace | ||
|
|
7b8d9155fd | ||
|
|
ec0a5893c7 | ||
|
|
6bb6349c41 | ||
|
|
8ba512f45e | ||
|
|
0136d4a426 | ||
|
|
71b9cd8596 | ||
|
|
b6409869aa | ||
|
|
ff7c026bba | ||
|
|
f119d84439 |
No files matched your search
@@ -10,6 +10,7 @@ user/
|
||||
out/
|
||||
GLCache/
|
||||
.runtime/
|
||||
mod-game-*/
|
||||
vkd3d-proton.cache
|
||||
fsr4_shaders/
|
||||
.local/
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# God of War III PC Launcher — Proje Hafızası ve Mimari Özeti
|
||||
|
||||
> **Versiyon:** v2.1
|
||||
> **Depo:** [github.com/ScreamAngel/gow3_launcher](https://github.com/ScreamAngel/gow3_launcher) (`main` dalı)
|
||||
> **Platform Desteği:** Linux (x86_64, Wayland/Hyprland & X11) ve Windows (x86_64 MSVC)
|
||||
> **Tarih:** 07 Kasım 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. Proje Mimarisi ve Çekirdek Dosyalar
|
||||
|
||||
* **Launcher Kaynak Kodu (`launcher/src/`):**
|
||||
* `main.rs`: CLI başlatma argümanları (`--play`, `-p`), pencere ikonu yükleme ve eframe/egui giriş noktası.
|
||||
* `app.rs`: Launcher ana GUI arayüzü, 6 sekme (Oyun, Grafik, Yapılandırma, Kontroller, Modlar, Sistem), shader derleme ekranı ve oyun süreci izleyicisi.
|
||||
* `discord_rpc.rs`: Sıfır ek yükümlü, yerel Discord IPC istemcisi (Linux Unix Domain Socket & Windows Named Pipe), gerçek zamanlı bölüm & boss savaşı dedektörü, çoklu dil desteği (`RpcLanguage`).
|
||||
* `win_input.rs`: Windows düşük seviye fare kancası (`WH_MOUSE_LL`), 1ms `timeBeginPeriod` zamanlayıcı çözünürlüğü ve oyun penceresini öne getirme (`SetForegroundWindow`).
|
||||
* `runner.rs`: Mod bindirme (overlay) yöneticisi, Python/Rust mod montajı, oyun süreci (`gow3-probe`) çalıştırma ve log yönlendirme.
|
||||
* `config.rs`: `launcher_config.json` yönetimi, donanım optimizasyonları ve otomatik sistem dili algılama (`default_language()`).
|
||||
* `i18n.rs`: 4 dilli arayüz yerelleştirmesi (Türkçe, English, Deutsch, Español).
|
||||
* `crash_analyzer.rs`: Oyun çöktüğünde `gow3_log.txt` çıktısını analiz edip kullanıcıya çözüm sunan hata teşhis motoru.
|
||||
* **Dağıtılan İkili Dosyalar (Binaries):**
|
||||
* Linux: `gow3-launcher`
|
||||
* Windows: `gow3-launcher-win.exe`
|
||||
* Alt dizin kopyaları: `gow3-linux/gow3-launcher`, `gow3-linux/gow3-launcher-win.exe`
|
||||
|
||||
---
|
||||
|
||||
## 2. Discord Rich Presence (RPC) Entegrasyonu
|
||||
|
||||
* **Resmi App ID:** `1440140183675867156` (*God of War III: Remastered*)
|
||||
* **Resmi Varlıklar (Assets):**
|
||||
* Kapak İkonu (Large Image): `https://cdn.discordapp.com/app-icons/1440140183675867156/2cbf4c7faf6444d474ff7b32b2025ec2.png`
|
||||
* Rozet İkonu (Small Image): `https://raw.githubusercontent.com/ScreamAngel/gow3_launcher/main/gow3.png`
|
||||
* **İletişim Kanalları:**
|
||||
* **Linux:** `$XDG_RUNTIME_DIR/discord-ipc-0`..`9`, `/tmp/discord-ipc-0`..`9`, Flatpak, Snap ve Vesktop (`vesktop-ipc`).
|
||||
* **Windows:** `\\.\pipe\discord-ipc-0`..`9` (Named Pipes). Olası donmaları engellemek için `PeekNamedPipe` ile asenkron kontrol mekanizması eklendi.
|
||||
* **Gerçek Zamanlı Durum Tespiti:**
|
||||
* `gow3_log.txt` dosyasından gerçek çalışma zamanı WAD yüklemeleri (`WAD_STATS: WAD <NAME>` ve `/WAD/PS4_LE/<NAME>.WAD`) taranır. Statik motor sözlüğü (`LoadWadSizeData`) **yoksayılır**.
|
||||
* `user/savedata/*/CUSA01715/*.sce_sys/param.bin` SFO dosyasından anlık kayıt checkpoint'i ve zorluk derecesi çekilir.
|
||||
* Ters kronolojik (newest-first) algoritma ile oyuncunun bulunduğu son bölge veya boss savaşı anlık belirlenir.
|
||||
* **Otomatik Dil Desteği (tr, en, de, es):**
|
||||
* İşletim sistemi dili (Linux'ta `$LANG`, Windows'ta `GetUserDefaultUILanguage`) veya launcher arayüz dili baz alınarak durum otomatik yerelleştirilir.
|
||||
* **Bölüm & Boss Karşılıkları:**
|
||||
* Poseidon Boss Savaşı: `Olympus Dağı (Poseidon Boss Fight)` / `Mount Olympus (Poseidon Boss Fight)`
|
||||
* Hades Boss Savaşı: `Yeraltı Dünyası (Hades Boss Fight)` / `The Underworld (Hades Boss Fight)`
|
||||
* Helios Boss Savaşı: `Olimpiya Şehri (Helios Boss Fight)` / `City of Olympia (Helios Boss Fight)`
|
||||
* Hermes Kovalamacası: `Olimpiya Şehri (Hermes Kovalamacası)` / `City of Olympia (Hermes Chase)`
|
||||
* Herkül Boss Savaşı: `Olympus Forumu (Hercules Boss Fight)` / `Forum of Olympus (Hercules Boss Fight)`
|
||||
* Kronos Boss Savaşı: `Tartarus Çukuru (Cronos Boss Fight)` / `Pit of Tartarus (Cronos Boss Fight)`
|
||||
* Hephaistos Boss Savaşı: `Demirhane (Hephaestus Boss Fight)` / `The Forge (Hephaestus Boss Fight)`
|
||||
* Skorpius Boss Savaşı: `Olimpos Mağaraları (Skorpius Boss Fight)` / `Caverns of Olympus (Skorpius Boss Fight)`
|
||||
* Zeus Boss Savaşı: `Olympus Tepesi (Zeus Boss Fight)` / `Summit of Olympus (Zeus Boss Fight)`
|
||||
* Mücadeleler: `Olympus Mücadelesi 1-7` / `Challenge of Olympus 1-7`
|
||||
* Savaş Arenası: `Savaş Arenası (Combat Arena)` / `Combat Arena`
|
||||
* Menü: `Ana Menüde` / `In Main Menu`
|
||||
* Shader Derleme: `Shader'lar Derleniyor (%X)` / `Compiling Shaders (X%)`
|
||||
|
||||
---
|
||||
|
||||
## 3. Çözülen Kritik Hatalar ve Teknik Notlar
|
||||
|
||||
1. **Windows Fare Tıklama Algılama Hatası:**
|
||||
* Sorun: Windows'ta sol ve sağ tık çalışmıyordu.
|
||||
* Çözüm: `win_input.rs` içindeki `WH_MOUSE_LL` kancasında PS4 SDL2 girdi haritasının beklediği F13/F14 sanal tarama kodları (`0x64` = Kare/Hafif Vuruş, `0x65` = Üçgen/Ağır Vuruş) düzeltildi.
|
||||
2. **Windows Siyah Ekran / Mod Açılış Hatası:**
|
||||
* Sorun: NTFS junction oluşturma yetki hatası yüzünden Windows'ta oyun siyah ekranda takılıyordu.
|
||||
* Çözüm: `runner.rs` içinde Rust tabanlı doğrudan dosya kopyalama bindirmesi (`out/mod-game-rust-overlay`) uygulandı, yol formatı ve dosya doğrulaması eklendi.
|
||||
3. **Shader Ekranı Küçülme / Kapanma / Arka Plana Düşme:**
|
||||
* Sorun: Shader derleme ekranı açıldığında oyun arka plana düşüyor veya görev çubuğuna küçülüyordu.
|
||||
* Çözüm: Oyun penceresi açıldığında shader ekranı 10 saniye boyunca arka planda tam ekran açık kalır, oyun penceresi öne getirilir (`bring_game_window_to_front`) ve süre bitiminde launcher normal moduna döner (asla küçülmez).
|
||||
4. **Linux Hyprland / Wayland Tam Ekran Hatası:**
|
||||
* Sorun: Windows için eklenen pencere boyutu komutları Linux Wayland'de oyunun kenarlıksız pencere moduna geçmesini bozuyordu.
|
||||
* Çözüm: `Maximized(true)` ve ilgili `ViewportCommand` çağrıları `#[cfg(target_os = "windows")]` ile izole edildi, Linux'ta yerel `hyprctl` ve saf Wayland geometri protokolü korundu.
|
||||
5. **Discord RPC'nin Sadece "Ana Menüde" Kalması:**
|
||||
* Sorun: Motorun statik `LoadWadSizeData: WAD_Shell` satırı yüzünden log döngüsü erken çıkış veriyor ve boss/bölüm adımlarını atlıyordu.
|
||||
* Çözüm: Statik sözlük filtrelendi, gerçek WAD yüklemeleri ters sırada önceliklendirildi.
|
||||
|
||||
---
|
||||
|
||||
## 4. Derleme ve Test Komutları
|
||||
|
||||
* **Birim Testleri (30 Test):**
|
||||
```bash
|
||||
cargo test --manifest-path launcher/Cargo.toml
|
||||
```
|
||||
* **Linux Release Derlemesi:**
|
||||
```bash
|
||||
cargo build --release --manifest-path launcher/Cargo.toml
|
||||
cp -f launcher/target/release/launcher gow3-launcher
|
||||
cp -f launcher/target/release/launcher gow3-linux/gow3-launcher
|
||||
```
|
||||
* **Windows Release Derlemesi (Cross-Compile):**
|
||||
```bash
|
||||
cargo xwin build --release --manifest-path launcher/Cargo.toml --target x86_64-pc-windows-msvc
|
||||
cp -f launcher/target/x86_64-pc-windows-msvc/release/launcher.exe gow3-launcher-win.exe
|
||||
cp -f launcher/target/x86_64-pc-windows-msvc/release/launcher.exe gow3-linux/gow3-launcher-win.exe
|
||||
```
|
||||
@@ -415,3 +415,22 @@ void _ZN5ImGui11ProgressBarEfRK6ImVec2PKc(float fraction, const void* size_arg,
|
||||
(void)overlay;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
|
||||
(void)hinstDLL;
|
||||
(void)lpReserved;
|
||||
if (fdwReason == DLL_PROCESS_ATTACH) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
__attribute__((constructor))
|
||||
static void gow3_runtime_preinit(void) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1671,7 +1671,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "launcher"
|
||||
version = "2.0.2"
|
||||
version = "2.2.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "launcher"
|
||||
version = "2.0.2"
|
||||
version = "2.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.36.2"
|
||||
egui_extras = { version = "0.36.2", features = ["image"] }
|
||||
image = { version = "0.25.10", default-features = false, features = ["png"] }
|
||||
image = { version = "0.25.10", default-features = false, features = ["png", "bmp"] }
|
||||
rfd = "0.17.2"
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.154"
|
||||
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -6,8 +6,8 @@ fn main() {
|
||||
res.set("ProductName", "God of War III Remastered Launcher");
|
||||
res.set("FileDescription", "God of War III Remastered PC Launcher");
|
||||
res.set("LegalCopyright", "God of War III Remastered");
|
||||
res.set("ProductVersion", "1.9.0.0");
|
||||
res.set("FileVersion", "1.9.0.0");
|
||||
res.set("ProductVersion", "2.1.0.0");
|
||||
res.set("FileVersion", "2.1.0.0");
|
||||
if let Err(e) = res.compile() {
|
||||
eprintln!("Warning: failed to compile Windows icon resource: {}", e);
|
||||
}
|
||||
|
||||
@@ -245,6 +245,8 @@ pub struct LauncherConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub skip_cinematics: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub bonus_play_fix: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub use_gamemode: bool,
|
||||
#[serde(default = "default_vram_budget")]
|
||||
pub vram_gc_budget_mb: u64,
|
||||
@@ -294,10 +296,20 @@ pub struct LauncherConfig {
|
||||
// Keyboard bindings
|
||||
#[serde(default)]
|
||||
pub keyboard_bindings: KeyboardBindings,
|
||||
|
||||
// Discord Rich Presence
|
||||
#[serde(default = "default_true")]
|
||||
pub discord_rpc_enabled: bool,
|
||||
#[serde(default = "default_discord_client_id")]
|
||||
pub discord_client_id: String,
|
||||
}
|
||||
|
||||
fn default_discord_client_id() -> String {
|
||||
crate::discord_rpc::DISCORD_GOW3_APP_ID.to_string()
|
||||
}
|
||||
|
||||
fn default_language() -> String {
|
||||
"tr".to_string()
|
||||
crate::discord_rpc::detect_system_language().to_string()
|
||||
}
|
||||
fn default_target_fps() -> u32 {
|
||||
60
|
||||
@@ -322,7 +334,7 @@ fn default_last_commit() -> String {
|
||||
"c5638d9956002b8a9f09a240028f6793ddf7b2f8".to_string()
|
||||
}
|
||||
fn default_launcher_version() -> String {
|
||||
"v2.0.2".to_string()
|
||||
"v2.2".to_string()
|
||||
}
|
||||
fn default_render_resolution() -> String {
|
||||
"native".to_string()
|
||||
@@ -342,6 +354,7 @@ impl Default for LauncherConfig {
|
||||
launch_while_compiling_shaders: false,
|
||||
show_shader_compile_rage_meter: true,
|
||||
skip_cinematics: true,
|
||||
bonus_play_fix: true,
|
||||
use_gamemode: true,
|
||||
vram_gc_budget_mb: default_vram_budget(),
|
||||
dmem_mb: default_dmem(),
|
||||
@@ -361,6 +374,8 @@ impl Default for LauncherConfig {
|
||||
deadzone_left: default_deadzone(),
|
||||
deadzone_right: default_deadzone(),
|
||||
keyboard_bindings: KeyboardBindings::default(),
|
||||
discord_rpc_enabled: true,
|
||||
discord_client_id: default_discord_client_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -540,6 +555,10 @@ impl LauncherConfig {
|
||||
cfg.keyboard_bindings = KeyboardBindings::recommended();
|
||||
changed = true;
|
||||
}
|
||||
if cfg.discord_client_id != crate::discord_rpc::DISCORD_GOW3_APP_ID {
|
||||
cfg.discord_client_id = crate::discord_rpc::DISCORD_GOW3_APP_ID.to_string();
|
||||
changed = true;
|
||||
}
|
||||
if changed {
|
||||
let _ = cfg.save(base_dir);
|
||||
}
|
||||
@@ -785,11 +804,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_launcher_version_is_v20() {
|
||||
assert_eq!(default_launcher_version(), "v2.0.2");
|
||||
fn test_launcher_version_is_v22() {
|
||||
assert_eq!(default_launcher_version(), "v2.2");
|
||||
let def = LauncherConfig::default();
|
||||
assert!(def.async_readbacks);
|
||||
assert!(def.patch_readbacks);
|
||||
assert!(def.bonus_play_fix);
|
||||
assert!(def.discord_rpc_enabled);
|
||||
assert_eq!(def.discord_client_id, crate::discord_rpc::DISCORD_GOW3_APP_ID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -150,11 +150,18 @@ pub fn text(lang: Language, key: &str) -> &'static str {
|
||||
(Language::Turkish, "system_specs_title") => "SİSTEM ÖZELLİKLERİ",
|
||||
(Language::Turkish, "play_btn_label") => "OYNA",
|
||||
(Language::Turkish, "menu_play") => "OYNA",
|
||||
(Language::Turkish, "menu_trophies") => "KUPALAR",
|
||||
(Language::Turkish, "menu_latest_news") => "GÜNCELLEME NOTLARI",
|
||||
(Language::Turkish, "menu_options") => "AYARLAR",
|
||||
(Language::Turkish, "menu_back") => "GERİ DÖN",
|
||||
(Language::Turkish, "release_notes_title") => "God of War III Launcher - Sürüm Notları (v2.0.2)",
|
||||
(Language::Turkish, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.0.2 YENİLİKLERİ:\n\n• 🪟 Windows & Linux Eş Zamanlı Tam Sürüm (v2.0.2):\n - Windows sürümü (gow3-launcher-win.exe) modern konsol arayüzü ve mod yöneticisiyle tamamen güncellendi.\n - Linux ve Windows ikili dosyaları birebir senkronize edildi.\n\n• 🔄 Otomatik Güncelleme Tarama & Sonra Hatırlat:\n - Başlangıçta sessiz GitHub sürüm denetimi ve otomatik güncelleme bildirim penceresi.\n - 'Sonra Hatırlat' ve 'Şimdi Güncelle' seçenekleri ile oturum bazlı akıllı hatırlatma.\n - Ana menü ve ayarlara dinamik 'Güncelle' butonu.\n\n• 🛡️ Shader & Mod Yönetimi:\n - %100 kesintisiz Spartan Rage dolumu ve sarı çubuksuz temiz sinematik ekran.\n - Gelişmiş oyun içi mod desteği (CJ Mod, Golden Blades vb.).",
|
||||
(Language::Turkish, "bonus_play_fix_toggle") => "Bonus Play QTE & Düşman Doğma Düzeltmesi",
|
||||
(Language::Turkish, "bonus_play_fix_desc") => "Tanrısal Eşyalar (Godly Possessions / Hephaistos'un Yüzüğü) aktifken QTE sekanslarının erken tetiklenip düşman dalgalarını (arena kilitlenmelerini) ve tuş göstergelerini engellemesini önler.",
|
||||
(Language::Turkish, "release_notes_title") => "God of War III Launcher - Sürüm Notları (v2.2)",
|
||||
(Language::Turkish, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.2 YENİLİKLERİ:\n\n• 🏆 Gerçek Zamanlı Kupa (Trophy) ve PS4 Bildirim Sistemi:\n - Resmi 36 kupanın tamamı, yüksek çözünürlüklü ikonları ve Türkçe/İngilizce açıklamalarıyla entegre edildi.\n - PS4 Stili Sağ Üst Bildirim Hapı (Toast Notification): Kupa kazanıldığında sağdan kayarak gelir, 4 saniye ekranda kalır ve kararır.\n - Orijinal PS4 Kupa Kazanma Sesi (Trophy Pop SFX) milisaniyesinde çalar.\n - Sıralı Kuyruk Yönetimi (FIFO): Üst üste binen kupaları sıralı gösterir.\n - Kupa Merkezi (Dashboard): İlerleme çubuğu, %100 Platin Ustalık Mührü, filtreleme, kupa arama ve gizli kupa açma desteği.\n\n• 🗡️ Bonus Play & Tanrısal Eşyalar (Godly Possessions) Düzeltmesi:\n - Hephaistos'un Yüzüğü ve diğer bonus eşyalar aktifken QTE'lerin erken tetiklenip düşman dalgalarını engellemesi ve arena kilitlenmeleri düzeltildi.\n\n• 🎮 Kararlılık ve Performans İyileştirmeleri.",
|
||||
(Language::Turkish, "discord_rpc_toggle") => "Discord Rich Presence (Oynuyor Durumu)",
|
||||
(Language::Turkish, "discord_rpc_desc") => "Discord profilinizde 'God of War III: Remastered', bulunduğunuz bölüm, boss savaşları ve oynama sürenizi canlı gösterir.",
|
||||
(Language::Turkish, "discord_client_id_label") => "Discord Uygulama Kimliği (Client ID):",
|
||||
(Language::Turkish, "discord_reset_btn") => "Varsayılana Sıfırla",
|
||||
(Language::Turkish, "gamemode_toggle") => "Feral GameMode (gamemoderun)",
|
||||
(Language::Turkish, "gamemode_desc") => "CPU zamanlayıcısını ve GPU performansını oyun için en yüksek seviyeye ayarlar.",
|
||||
(Language::Turkish, "save_apply") => "Ayarları Kaydet ve Uygula",
|
||||
@@ -201,7 +208,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
|
||||
(Language::Turkish, "view_on_github") => "GitHub'da Görüntüle",
|
||||
(Language::Turkish, "remind_later") => "⏰ Sonra Hatırlat",
|
||||
(Language::Turkish, "update_available_badge") => "🚀 Güncelleme Mevcut!",
|
||||
(Language::Turkish, "btn_update") => "🔄 Güncelle",
|
||||
(Language::Turkish, "btn_update") => "Güncelle",
|
||||
(Language::Turkish, "btn_update_now") => "🚀 Şimdi Güncelle",
|
||||
(Language::Turkish, "updating_in_progress") => "⏳ Güncelleme indiriliyor ve kuruluyor... Lütfen bekleyin...",
|
||||
(Language::Turkish, "python_dep") => "Python 3 Bağımlılığı:",
|
||||
@@ -412,11 +419,18 @@ pub fn text(lang: Language, key: &str) -> &'static str {
|
||||
(Language::English, "system_specs_title") => "SYSTEM SPECIFICATIONS",
|
||||
(Language::English, "play_btn_label") => "PLAY",
|
||||
(Language::English, "menu_play") => "PLAY",
|
||||
(Language::English, "menu_trophies") => "TROPHIES",
|
||||
(Language::English, "menu_latest_news") => "LATEST NEWS",
|
||||
(Language::English, "menu_options") => "OPTIONS",
|
||||
(Language::English, "menu_back") => "BACK",
|
||||
(Language::English, "release_notes_title") => "God of War III Launcher - Release Notes (v2.0.2)",
|
||||
(Language::English, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.0.2 CHANGELOG:\n\n• 🪟 Synchronized Windows & Linux Release (v2.0.2):\n - Windows version (gow3-launcher-win.exe) rebuilt with full console UI and mod manager.\n - Linux and Windows binaries are fully in sync.\n\n• 🔄 Auto Version Checking & Remind Later:\n - Startup background GitHub release checks with automatic update prompt.\n - 'Remind Later' and 'Update Now' session-aware dismissals.\n - Dynamic 'Update' buttons in header and settings.\n\n• 🛡️ Shader & Mod Management:\n - Continuous 100% Spartan Rage fill without freezes or yellow beam.\n - In-game mod manager (CJ Mod, Golden Blades, etc.).",
|
||||
(Language::English, "bonus_play_fix_toggle") => "Bonus Play QTE & Enemy Spawning Fix",
|
||||
(Language::English, "bonus_play_fix_desc") => "Prevents Godly Possession bonus items (specifically Hephaestus' Ring) from auto-skipping QTEs prematurely, preventing missing enemy wave spawns and missing QTE prompts.",
|
||||
(Language::English, "release_notes_title") => "God of War III Launcher - Release Notes (v2.2)",
|
||||
(Language::English, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.2 CHANGELOG:\n\n• 🏆 Real-Time Trophy System & PS4 Notification Integration:\n - All 36 trophies tracked with official badges, unlock conditions, and local persistence (trophies_state.json).\n - PS4-Style Pill Toast Notification in the top-right corner with smooth slide-in, 4s hold, and soft fade-out.\n - Authentic PS4 Trophy Pop SFX played at the millisecond of unlocking.\n - Sequential Notification Queue (FIFO) for multiple unlocks.\n - Full Trophy Showcase & Dashboard: Progress bar, dynamic percentage, 100% Platinum Mastery Badge, search & filter.\n\n• 🗡️ Bonus Play & Godly Possessions Fix:\n - Fixed missing enemies and QTE softlocks when equipping Hephaestus' Ring and bonus items.\n\n• ⚡ Stability & performance enhancements.",
|
||||
(Language::English, "discord_rpc_toggle") => "Discord Rich Presence (Activity Status)",
|
||||
(Language::English, "discord_rpc_desc") => "Displays 'God of War III: Remastered', your active chapter, boss fights, and elapsed play time on Discord.",
|
||||
(Language::English, "discord_client_id_label") => "Discord Application ID (Client ID):",
|
||||
(Language::English, "discord_reset_btn") => "Reset Default",
|
||||
(Language::English, "gamemode_toggle") => "Feral GameMode (gamemoderun)",
|
||||
(Language::English, "gamemode_desc") => "Configures CPU governor and GPU clocks for optimal gaming performance.",
|
||||
(Language::English, "save_apply") => "Save & Apply Settings",
|
||||
@@ -463,7 +477,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
|
||||
(Language::English, "view_on_github") => "View on GitHub",
|
||||
(Language::English, "remind_later") => "⏰ Remind Me Later",
|
||||
(Language::English, "update_available_badge") => "🚀 Update Available!",
|
||||
(Language::English, "btn_update") => "🔄 Update",
|
||||
(Language::English, "btn_update") => "Update",
|
||||
(Language::English, "btn_update_now") => "🚀 Update Now",
|
||||
(Language::English, "updating_in_progress") => "⏳ Downloading and installing update... Please wait...",
|
||||
(Language::English, "python_dep") => "Python 3 Dependency:",
|
||||
@@ -674,11 +688,18 @@ pub fn text(lang: Language, key: &str) -> &'static str {
|
||||
(Language::German, "system_specs_title") => "SYSTEMSPEZIFIKATIONEN",
|
||||
(Language::German, "play_btn_label") => "SPIELEN",
|
||||
(Language::German, "menu_play") => "SPIELEN",
|
||||
(Language::German, "menu_trophies") => "TROPHÄEN",
|
||||
(Language::German, "menu_latest_news") => "NEUIGKEITEN",
|
||||
(Language::German, "menu_options") => "OPTIONEN",
|
||||
(Language::German, "menu_back") => "ZURÜCK",
|
||||
(Language::German, "release_notes_title") => "God of War III Launcher - Versionshinweise (v2.0.2)",
|
||||
(Language::German, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.0.2 NEUIGKEITEN:\n\n• 🪟 Synchronisierte Windows- & Linux-Version (v2.0.2):\n - Windows-Version (gow3-launcher-win.exe) mit Konsolen-UI und Mod-Manager neu erstellt.\n\n• 🔄 Automatische Versionsprüfung & Später erinnern:\n - Automatische GitHub-Aktualisierungsprüfung beim Start.\n - Intelligente 'Später erinnern' und 'Jetzt aktualisieren' Funktionen.\n\n• 🛡️ Shader & Mod-Verwaltung optimiert.",
|
||||
(Language::German, "bonus_play_fix_toggle") => "Bonus Play QTE & Gegner-Spawn-Korrektur",
|
||||
(Language::German, "bonus_play_fix_desc") => "Verhindert, dass göttliche Besitztümer (Hephaistos' Ring) QTEs vorzeitig überspringen und dadurch das Erscheinen von Gegnerwellen blockieren.",
|
||||
(Language::German, "release_notes_title") => "God of War III Launcher - Versionshinweise (v2.2)",
|
||||
(Language::German, "release_notes_content") => "🔥 GOD OF WAR III LAUNCHER v2.2 NEUIGKEITEN:\n\n• 🏆 Echzeit-Trophäensystem & PS4-Benachrichtigung:\n - Alle 36 Trophäen mit Originalgrafiken, Benachrichtigungskapsel und PS4-Soundeffekt.\n - Trophäen-Showcase mit Fortschrittsanzeige und 100%-Platin-Abzeichen.\n\n• 🗡️ Bonus-Gegenstände Bugfix (QTE & Gegner-Spawn-Korrektur).",
|
||||
(Language::German, "discord_rpc_toggle") => "Discord Rich Presence (Spielstatus)",
|
||||
(Language::German, "discord_rpc_desc") => "Zeigt 'Spielt God of War III Remastered' und die Spielzeit in deinem Discord-Profil an.",
|
||||
(Language::German, "discord_client_id_label") => "Discord Anwendungs-ID (Client ID):",
|
||||
(Language::German, "discord_reset_btn") => "Standard wiederherstellen",
|
||||
(Language::German, "gamemode_toggle") => "Feral GameMode (gamemoderun)",
|
||||
(Language::German, "gamemode_desc") => "Optimiert CPU-Governor und GPU für maximale Gaming-Leistung.",
|
||||
(Language::German, "save_apply") => "Einstellungen speichern & anwenden",
|
||||
@@ -723,7 +744,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
|
||||
(Language::German, "view_on_github") => "Auf GitHub ansehen",
|
||||
(Language::German, "remind_later") => "⏰ Später erinnern",
|
||||
(Language::German, "update_available_badge") => "🚀 Update verfügbar!",
|
||||
(Language::German, "btn_update") => "🔄 Aktualisieren",
|
||||
(Language::German, "btn_update") => "Aktualisieren",
|
||||
(Language::German, "btn_update_now") => "🚀 Jetzt aktualisieren",
|
||||
(Language::German, "updating_in_progress") => "⏳ Update wird heruntergeladen und installiert... Bitte warten...",
|
||||
(Language::German, "python_dep") => "Python 3 Abhängigkeit:",
|
||||
@@ -934,11 +955,18 @@ pub fn text(lang: Language, key: &str) -> &'static str {
|
||||
(Language::Spanish, "system_specs_title") => "ESPECIFICACIONES DEL SISTEMA",
|
||||
(Language::Spanish, "play_btn_label") => "JUGAR",
|
||||
(Language::Spanish, "menu_play") => "JUGAR",
|
||||
(Language::Spanish, "menu_trophies") => "TROFEOS",
|
||||
(Language::Spanish, "menu_latest_news") => "ÚLTIMAS NOTICIAS",
|
||||
(Language::Spanish, "menu_options") => "OPCIONES",
|
||||
(Language::Spanish, "menu_back") => "VOLVER",
|
||||
(Language::Spanish, "release_notes_title") => "God of War III Launcher - Notas de la Versión (v2.0.2)",
|
||||
(Language::Spanish, "release_notes_content") => "🔥 NOVEDADES DE GOD OF WAR III LAUNCHER v2.0.2:\n\n• 🪟 Versión sincronizada para Windows y Linux (v2.0.2):\n - Versión de Windows (gow3-launcher-win.exe) reconstruida con interfaz de consola y gestor de mods.\n\n• 🔄 Detección automática de actualizaciones & Recordar más tarde:\n - Comprobación automática en segundo plano al iniciar.\n - Botones inteligentes de 'Recordar más tarde' y 'Actualizar ahora'.\n\n• 🛡️ Gestión avanzada de Shaders y Mods.",
|
||||
(Language::Spanish, "bonus_play_fix_toggle") => "Corrección de QTE y Enemigos en Bonus Play",
|
||||
(Language::Spanish, "bonus_play_fix_desc") => "Evita que las Posesiones Divinas (Anillo de Hefesto) omitan los QTEs prematuramente y bloqueen la aparición de oleadas de enemigos.",
|
||||
(Language::Spanish, "release_notes_title") => "God of War III Launcher - Notas de la Versión (v2.2)",
|
||||
(Language::Spanish, "release_notes_content") => "🔥 NOVEDADES DE GOD OF WAR III LAUNCHER v2.2:\n\n• 🏆 Sistema de Trofeos en Tiempo Real y Notificaciones PS4:\n - Los 36 trofeos oficiales con insignias en alta resolución y efectos de sonido de PS4.\n - Notificaciones flotantes estilo cápsula PS4 y panel de trofeos con medalla de maestría al 100%.\n\n• 🗡️ Corrección de errores en objetos de Bonus Play (QTE y generación de enemigos).",
|
||||
(Language::Spanish, "discord_rpc_toggle") => "Discord Rich Presence (Estado de Juego)",
|
||||
(Language::Spanish, "discord_rpc_desc") => "Muestra 'Jugando a God of War III Remastered' y el tiempo de juego en tu perfil de Discord.",
|
||||
(Language::Spanish, "discord_client_id_label") => "ID de Aplicación de Discord (Client ID):",
|
||||
(Language::Spanish, "discord_reset_btn") => "Restablecer por Defecto",
|
||||
(Language::Spanish, "gamemode_toggle") => "Feral GameMode (gamemoderun)",
|
||||
(Language::Spanish, "gamemode_desc") => "Optimiza la CPU y GPU para máximo rendimiento en juegos.",
|
||||
(Language::Spanish, "save_apply") => "Guardar y Aplicar",
|
||||
@@ -983,7 +1011,7 @@ pub fn text(lang: Language, key: &str) -> &'static str {
|
||||
(Language::Spanish, "view_on_github") => "Ver en GitHub",
|
||||
(Language::Spanish, "remind_later") => "⏰ Recordármelo más tarde",
|
||||
(Language::Spanish, "update_available_badge") => "🚀 ¡Actualización disponible!",
|
||||
(Language::Spanish, "btn_update") => "🔄 Actualizar",
|
||||
(Language::Spanish, "btn_update") => "Actualizar",
|
||||
(Language::Spanish, "btn_update_now") => "🚀 Actualizar ahora",
|
||||
(Language::Spanish, "updating_in_progress") => "⏳ Descargando e instalando actualización... Por favor espere...",
|
||||
(Language::Spanish, "python_dep") => "Dependencia Python 3:",
|
||||
|
||||
@@ -10,6 +10,9 @@ mod tuner;
|
||||
mod updater;
|
||||
mod shader_downloader;
|
||||
pub mod mods_manager;
|
||||
pub mod discord_rpc;
|
||||
pub mod win_input;
|
||||
pub mod trophy;
|
||||
|
||||
use app::LauncherApp;
|
||||
use eframe::egui;
|
||||
@@ -79,6 +82,7 @@ fn main() -> eframe::Result<()> {
|
||||
native_options,
|
||||
Box::new(move |cc| {
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
cc.egui_ctx.set_embed_viewports(false);
|
||||
|
||||
// Register God of War Font and make it primary for Spartan style with unicode fallback
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
|
||||
@@ -70,9 +70,27 @@ pub fn ensure_runtime_assets(base_dir: &Path) -> Result<(PathBuf, PathBuf), Stri
|
||||
let _ = fs::write(&patch_xml, PATCH_GOW3_XML);
|
||||
}
|
||||
|
||||
ensure_game_icon_bmp(base_dir);
|
||||
|
||||
Ok((scripts_dir, patches_dir))
|
||||
}
|
||||
|
||||
/// Ensures gow3.bmp is available for SDL3 window icon loading.
|
||||
pub fn ensure_game_icon_bmp(base_dir: &Path) {
|
||||
let bmp_path = base_dir.join("gow3.bmp");
|
||||
if !bmp_path.is_file() {
|
||||
let png_path = base_dir.join("gow3.png");
|
||||
let img = if png_path.is_file() {
|
||||
image::open(&png_path).ok()
|
||||
} else {
|
||||
image::load_from_memory(include_bytes!("../../gow3.png")).ok()
|
||||
};
|
||||
if let Some(img) = img {
|
||||
let _ = img.save_with_format(&bmp_path, image::ImageFormat::Bmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds an available Python 3 interpreter (checking local runtime bundle first, then system PATH)
|
||||
pub fn find_python(base_dir: &Path) -> Result<String, String> {
|
||||
// 1. Check local runtime python folder (.runtime/python/python.exe or bin/python3)
|
||||
@@ -330,6 +348,207 @@ fn collect_xml_files(dir: &Path, xml_paths: &mut Vec<PathBuf>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_rust_mod_overlay(
|
||||
game_dir: &Path,
|
||||
out_dir: &Path,
|
||||
mods_dir: &Path,
|
||||
config: &LauncherConfig,
|
||||
) -> Option<PathBuf> {
|
||||
if !game_dir.is_dir() || !mods_dir.is_dir() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut enabled_mods = Vec::new();
|
||||
if let Ok(entries) = fs::read_dir(mods_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with('.')
|
||||
|| name.eq_ignore_ascii_case("gow3-windows")
|
||||
|| name.eq_ignore_ascii_case("gow3-linux")
|
||||
|| path.join("run.py").is_file()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !config.disabled_mods.contains(&name) {
|
||||
enabled_mods.push((name, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enabled_mods.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
enabled_mods.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let overlay_dir = out_dir.join("mod-game-rust-overlay");
|
||||
let _ = fs::remove_dir_all(&overlay_dir);
|
||||
if fs::create_dir_all(&overlay_dir).is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
fn link_or_copy_dir(target: &Path, link: &Path) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if std::os::unix::fs::symlink(target, link).is_ok() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if std::os::windows::fs::symlink_dir(target, link).is_ok() {
|
||||
return true;
|
||||
}
|
||||
use std::os::windows::process::CommandExt;
|
||||
let status = Command::new("cmd")
|
||||
.args(["/C", "mklink", "/J"])
|
||||
.arg(link)
|
||||
.arg(target)
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||
.status();
|
||||
if let Ok(st) = status {
|
||||
if st.success() && link.is_dir() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: create directory and link/copy contents
|
||||
if fs::create_dir_all(link).is_ok() {
|
||||
if let Ok(entries) = fs::read_dir(target) {
|
||||
for entry in entries.flatten() {
|
||||
let entry_target = entry.path();
|
||||
let entry_link = link.join(entry.file_name());
|
||||
if entry_target.is_dir() {
|
||||
let _ = link_or_copy_dir(&entry_target, &entry_link);
|
||||
} else {
|
||||
let _ = link_or_copy_file(&entry_target, &entry_link);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn link_or_copy_file(target: &Path, link: &Path) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if std::os::unix::fs::symlink(target, link).is_ok() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if fs::hard_link(target, link).is_ok() {
|
||||
return true;
|
||||
}
|
||||
fs::copy(target, link).is_ok()
|
||||
}
|
||||
|
||||
// Link or copy base game root entries into overlay
|
||||
if let Ok(entries) = fs::read_dir(game_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let target = entry.path();
|
||||
let link = overlay_dir.join(entry.file_name());
|
||||
if target.is_dir() {
|
||||
let _ = link_or_copy_dir(&target, &link);
|
||||
} else {
|
||||
let _ = link_or_copy_file(&target, &link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_content_root(mod_path: &Path) -> Option<PathBuf> {
|
||||
let candidates = [
|
||||
mod_path.join("dvdroot_ps4"),
|
||||
mod_path.join("app0/dvdroot_ps4"),
|
||||
mod_path.join("Game/EXEC"),
|
||||
mod_path.join("EXEC"),
|
||||
];
|
||||
for c in candidates {
|
||||
if c.is_dir() {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn apply_mod_dir(src: &Path, dst: &Path, base_game_src: &Path, count: &mut usize) {
|
||||
if let Ok(entries) = fs::read_dir(src) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let name = entry.file_name();
|
||||
let dest_path = dst.join(&name);
|
||||
let base_child = base_game_src.join(&name);
|
||||
|
||||
if path.is_dir() {
|
||||
let is_link = dest_path.is_symlink();
|
||||
if is_link {
|
||||
let _ = fs::remove_file(&dest_path);
|
||||
#[cfg(windows)]
|
||||
let _ = fs::remove_dir(&dest_path);
|
||||
let _ = fs::create_dir_all(&dest_path);
|
||||
if base_child.is_dir() {
|
||||
if let Ok(base_entries) = fs::read_dir(&base_child) {
|
||||
for be in base_entries.flatten() {
|
||||
let bet = be.path();
|
||||
let bel = dest_path.join(be.file_name());
|
||||
if bet.is_dir() {
|
||||
let _ = link_or_copy_dir(&bet, &bel);
|
||||
} else {
|
||||
let _ = link_or_copy_file(&bet, &bel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !dest_path.is_dir() {
|
||||
let _ = fs::create_dir_all(&dest_path);
|
||||
}
|
||||
|
||||
apply_mod_dir(&path, &dest_path, &base_child, count);
|
||||
} else if path.is_file() {
|
||||
if path.extension().map(|e| e.eq_ignore_ascii_case("xml")).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
if dest_path.is_symlink() || dest_path.is_file() {
|
||||
let _ = fs::remove_file(&dest_path);
|
||||
}
|
||||
if fs::copy(&path, &dest_path).is_ok() {
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut applied = 0;
|
||||
for (_name, path) in enabled_mods {
|
||||
if let Some(root) = find_content_root(&path) {
|
||||
apply_mod_dir(&root, &overlay_dir, game_dir, &mut applied);
|
||||
}
|
||||
}
|
||||
|
||||
// Safety validation: Ensure eboot.bin and EXEC directory are present in overlay.
|
||||
// If missing, NEVER return a broken directory to avoid black screen on game launch!
|
||||
if !overlay_dir.join("eboot.bin").is_file() || !overlay_dir.join("EXEC").is_dir() {
|
||||
crate::log_error!(
|
||||
"Rust mod overlay validation failed (missing eboot.bin or EXEC). Falling back to original game_dir."
|
||||
);
|
||||
let _ = fs::remove_dir_all(&overlay_dir);
|
||||
return None;
|
||||
}
|
||||
|
||||
if applied > 0 {
|
||||
crate::log_info!("Rust mod overlay successfully built with {} modified files", applied);
|
||||
Some(overlay_dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn download_windows_engine(base_dir: &Path, is_alternative: bool) -> Result<String, String> {
|
||||
let dest_bin = base_dir.join("bin");
|
||||
@@ -764,6 +983,7 @@ pub fn adjust_windows_windowed_mode(target_pid: u32) {
|
||||
|
||||
unsafe extern "system" {
|
||||
fn GetWindowThreadProcessId(hwnd: *mut c_void, lpdwProcessId: *mut u32) -> u32;
|
||||
fn GetWindowTextA(hwnd: *mut c_void, lp_string: *mut u8, n_max_count: i32) -> i32;
|
||||
fn EnumWindows(lpEnumFunc: unsafe extern "system" fn(*mut c_void, isize) -> i32, lParam: isize) -> i32;
|
||||
fn IsWindowVisible(hwnd: *mut c_void) -> i32;
|
||||
fn GetWindowLongPtrA(hWnd: *mut c_void, nIndex: i32) -> isize;
|
||||
@@ -779,7 +999,15 @@ pub fn adjust_windows_windowed_mode(target_pid: u32) {
|
||||
let ctx = &mut *(lparam as *mut EnumContext);
|
||||
let mut proc_id = 0u32;
|
||||
GetWindowThreadProcessId(hwnd, &mut proc_id);
|
||||
if proc_id == ctx.pid && IsWindowVisible(hwnd) != 0 {
|
||||
if proc_id != std::process::id() && proc_id == ctx.pid && IsWindowVisible(hwnd) != 0 {
|
||||
let mut title_buf = [0u8; 128];
|
||||
let len = GetWindowTextA(hwnd, title_buf.as_mut_ptr(), 127);
|
||||
if len > 0 {
|
||||
let title = String::from_utf8_lossy(&title_buf[..len as usize]);
|
||||
if title.contains("Launcher") || title.contains("gow3-launcher") {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
ctx.hwnd = hwnd;
|
||||
return 0;
|
||||
}
|
||||
@@ -865,6 +1093,9 @@ pub fn launch_gow3(
|
||||
running_pid: Option<Arc<AtomicU32>>,
|
||||
) -> Result<i32, String> {
|
||||
crate::log_info!("Initiating God of War III launch sequence...");
|
||||
// Synchronize configuration files (gow3.ini, user/config.toml, etc.) before launch
|
||||
let _ = crate::tuner::apply_settings(base_dir, config);
|
||||
|
||||
let game_dir = Path::new(&config.game_dir);
|
||||
if !game_dir.join("eboot.bin").is_file() {
|
||||
let msg = format!("Oyun klasöründe ('{}') 'eboot.bin' bulunamadı!", game_dir.display());
|
||||
@@ -935,6 +1166,7 @@ pub fn launch_gow3(
|
||||
}
|
||||
|
||||
let mut effective_app0 = game_dir.to_path_buf();
|
||||
let mut mods_applied = false;
|
||||
|
||||
if let Ok(output) = mods_cmd
|
||||
.env("PYTHONPATH", &scripts_path)
|
||||
@@ -953,11 +1185,20 @@ pub fn launch_gow3(
|
||||
if candidate.is_dir() {
|
||||
crate::log_info!("Mods active. Mounting overlay directory: {:?}", candidate);
|
||||
effective_app0 = candidate;
|
||||
mods_applied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mods_applied && mods_dir.is_dir() {
|
||||
if let Some(rust_overlay) = build_rust_mod_overlay(game_dir, &out_dir, &mods_dir, config) {
|
||||
crate::log_info!("Rust mod overlay active (fallback). Mounting overlay: {:?}", rust_overlay);
|
||||
effective_app0 = rust_overlay;
|
||||
mods_applied = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Run prepare.py (ELF parsing and analysis)
|
||||
let status = Command::new(&python)
|
||||
.arg(scripts_path.join("prepare.py"))
|
||||
@@ -1072,12 +1313,30 @@ pub fn launch_gow3(
|
||||
|
||||
patch_list.push("Bug Fix - Startup Black Screen".to_string());
|
||||
patch_list.push("Bug Fix - Intro Crash Fix".to_string());
|
||||
patch_list.push("Bug Fix - Trophy Unlock Fix".to_string());
|
||||
|
||||
if config.bonus_play_fix {
|
||||
patch_list.push("Bug Fix - Bonus Play QTE and Enemy Spawning Fix".to_string());
|
||||
}
|
||||
|
||||
if config.skip_cinematics {
|
||||
patch_list.push("Skip Any Video With X Button".to_string());
|
||||
}
|
||||
if config.patch_delta_time {
|
||||
patch_list.push(format!("Frame Rate Patch - {} FPS", config.target_fps));
|
||||
let fps_patch = match config.target_fps {
|
||||
30 => Some("Frame Rate Patch - 30 FPS"),
|
||||
40 => Some("Frame Rate Patch - 40 FPS"),
|
||||
60 => Some("Frame Rate Patch - 60 FPS"),
|
||||
90 => Some("Frame Rate Patch - 90 FPS"),
|
||||
120 => Some("Frame Rate Patch - 120 FPS"),
|
||||
144 => Some("Frame Rate Patch - 144 FPS"),
|
||||
165 => Some("Frame Rate Patch - 165 FPS"),
|
||||
240 => Some("Frame Rate Patch - 240 FPS"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(p) = fps_patch {
|
||||
patch_list.push(p.to_string());
|
||||
}
|
||||
}
|
||||
if config.disable_motion_blur {
|
||||
patch_list.push("Graphics - Disable Motion Blur".to_string());
|
||||
@@ -1087,15 +1346,25 @@ pub fn launch_gow3(
|
||||
}
|
||||
let patches_str = patch_list.join(";");
|
||||
|
||||
// Prepare active patches directory: copy built-in patch XML + XML patches from enabled mods
|
||||
// Prepare active patches directory: copy custom XML patches (excluding built-in) + XML patches from enabled mods
|
||||
let active_patches_dir = out_dir.join("active_patches");
|
||||
let _ = fs::remove_dir_all(&active_patches_dir);
|
||||
let _ = fs::create_dir_all(&active_patches_dir);
|
||||
|
||||
// Copy built-in XML
|
||||
let builtin_xml = patches_path.join("God_of_War_III_Remastered.xml");
|
||||
if builtin_xml.is_file() {
|
||||
let _ = fs::copy(&builtin_xml, active_patches_dir.join("God_of_War_III_Remastered.xml"));
|
||||
// Copy any custom XML patches from patches_path (excluding built-in God_of_War_III_Remastered.xml)
|
||||
if patches_path.is_dir() {
|
||||
if let Ok(entries) = fs::read_dir(&patches_path) {
|
||||
for entry in entries.flatten() {
|
||||
let p = entry.path();
|
||||
if p.is_file() && p.extension().map_or(false, |ext| ext.eq_ignore_ascii_case("xml")) {
|
||||
if let Some(file_name) = p.file_name() {
|
||||
if !file_name.eq_ignore_ascii_case("God_of_War_III_Remastered.xml") {
|
||||
let _ = fs::copy(&p, active_patches_dir.join(file_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect and copy all XML patches from enabled mods
|
||||
@@ -1352,18 +1621,41 @@ pub fn launch_gow3(
|
||||
cmd.env("GOW3_PRESENT_THREAD", "1");
|
||||
cmd.env("GOW3_ASYNC_FENCES", "1");
|
||||
cmd.env("GOW3_ASYNC_SHADERS", "1");
|
||||
cmd.env("GOW3_FPS_LIMIT", config.target_fps.to_string());
|
||||
|
||||
let target_fps_str = if config.target_fps > 0 {
|
||||
config.target_fps.to_string()
|
||||
} else {
|
||||
"0".to_string()
|
||||
};
|
||||
cmd.env("GOW3_FPS_LIMIT", &target_fps_str);
|
||||
cmd.env("GOW3_ENGINE_FPS", if config.target_fps > 0 { &target_fps_str } else { "60" });
|
||||
cmd.env("GOW3_DEFAULT_ENGINE_FPS", if config.target_fps > 0 { &target_fps_str } else { "60" });
|
||||
cmd.env("GOW3_VBLANK_HZ", if config.target_fps > 0 { &target_fps_str } else { "60" });
|
||||
cmd.env("GOW3_VSYNC", if config.vsync { "1" } else { "0" });
|
||||
|
||||
let effective_budget = config.get_effective_vram_budget();
|
||||
cmd.env("GOW3_GC_BUDGET_MB", effective_budget.to_string());
|
||||
cmd.env("GOW3_DMEM_POOL_MB", config.dmem_mb.to_string());
|
||||
cmd.env("GOW3_DMEM_MB", config.dmem_mb.to_string());
|
||||
cmd.env("GOW3_DEFAULT_ENGINE_FPS", config.target_fps.to_string());
|
||||
cmd.env("GOW3_VBLANK_HZ", config.target_fps.to_string());
|
||||
cmd.env("GOW3_PATCHES", &patches_str);
|
||||
cmd.env("GOW3_PATCHES_ONLY", "1");
|
||||
cmd.env("GOW3_CONFIG", &config_ini);
|
||||
cmd.env("GOW3_GAME_DIR", game_dir);
|
||||
if mods_applied {
|
||||
cmd.env("GOW3_MODS_DIR", &effective_app0);
|
||||
} else {
|
||||
cmd.env_remove("GOW3_MODS_DIR");
|
||||
}
|
||||
cmd.env("GOW3_MOD_TRACE", "1");
|
||||
|
||||
// Native SDL3 window & taskbar icon for gow3-probe
|
||||
let icon_bmp = base_dir.join("gow3.bmp");
|
||||
let icon_png = base_dir.join("gow3.png");
|
||||
if icon_bmp.is_file() {
|
||||
cmd.env("GOW3_WINDOW_ICON", &icon_bmp);
|
||||
} else if icon_png.is_file() {
|
||||
cmd.env("GOW3_WINDOW_ICON", &icon_png);
|
||||
}
|
||||
cmd.env(
|
||||
"GOW3_FULLSCREEN",
|
||||
if config.display_mode != crate::config::DisplayMode::Windowed {
|
||||
@@ -1437,6 +1729,10 @@ pub fn launch_gow3(
|
||||
pid_holder.store(child_pid, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// Windows Input Bridge: Install low-level mouse hook & 1ms timer resolution while the game is running
|
||||
#[cfg(target_os = "windows")]
|
||||
let _win_input_guard = crate::win_input::start_mouse_hook(child_pid, config.hide_mouse_cursor);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if config.display_mode == crate::config::DisplayMode::Windowed {
|
||||
adjust_windows_windowed_mode(child_pid);
|
||||
@@ -1502,4 +1798,32 @@ mod tests {
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_rust_mod_overlay() {
|
||||
let temp_dir = std::env::temp_dir().join("gow3_test_rust_mod_overlay");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
|
||||
let game_dir = temp_dir.join("CUSA01715");
|
||||
let out_dir = temp_dir.join("out");
|
||||
let mods_dir = temp_dir.join("mods");
|
||||
|
||||
fs::create_dir_all(game_dir.join("EXEC/WAD")).unwrap();
|
||||
fs::write(game_dir.join("EXEC/WAD/ORIG.WAD"), b"ORIG").unwrap();
|
||||
fs::write(game_dir.join("eboot.bin"), b"EBOOT").unwrap();
|
||||
|
||||
let mod_a = mods_dir.join("ModA/dvdroot_ps4/EXEC/WAD");
|
||||
fs::create_dir_all(&mod_a).unwrap();
|
||||
fs::write(mod_a.join("ORIG.WAD"), b"MODDED").unwrap();
|
||||
|
||||
let config = LauncherConfig::default();
|
||||
let overlay = build_rust_mod_overlay(&game_dir, &out_dir, &mods_dir, &config);
|
||||
assert!(overlay.is_some());
|
||||
let ov = overlay.unwrap();
|
||||
let modded_file = ov.join("EXEC/WAD/ORIG.WAD");
|
||||
assert!(modded_file.is_file());
|
||||
assert_eq!(fs::read(&modded_file).unwrap(), b"MODDED");
|
||||
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
}
|
||||
@@ -34,15 +34,16 @@ fn update_xml_patches(base_dir: &Path, target_hz: u32) -> Result<(), String> {
|
||||
if let Ok(content) = fs::read_to_string(path) {
|
||||
let mut new_lines = Vec::new();
|
||||
for line in content.lines() {
|
||||
let low = line.to_lowercase();
|
||||
if (line.contains("Delta-Time")
|
||||
|| line.contains("120 FPS")
|
||||
|| line.contains("144 FPS")
|
||||
|| line.contains("165 FPS")
|
||||
|| line.contains("240 FPS")
|
||||
|| line.contains("60 FPS"))
|
||||
&& line.contains("value=\"")
|
||||
&& low.contains("value=\"")
|
||||
{
|
||||
if let Some(start) = line.find("value=\"") {
|
||||
if let Some(start) = low.find("value=\"") {
|
||||
let prefix = &line[..start + 7];
|
||||
if let Some(end) = line[start + 7..].find('\"') {
|
||||
let suffix = &line[start + 7 + end..];
|
||||
@@ -218,6 +219,14 @@ fn update_run_sh(base_dir: &Path, config: &LauncherConfig) -> Result<(), String>
|
||||
if config.background_controller_input { "1" } else { "0" },
|
||||
);
|
||||
|
||||
let icon_bmp = base_dir.join("gow3.bmp");
|
||||
let icon_png = base_dir.join("gow3.png");
|
||||
if icon_bmp.is_file() {
|
||||
replace_or_insert_env(&mut content, "GOW3_WINDOW_ICON", &icon_bmp.to_string_lossy());
|
||||
} else if icon_png.is_file() {
|
||||
replace_or_insert_env(&mut content, "GOW3_WINDOW_ICON", &icon_png.to_string_lossy());
|
||||
}
|
||||
|
||||
fs::write(&run_sh, content).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::process::Command;
|
||||
use crate::config::LauncherConfig;
|
||||
use crate::tuner::apply_settings;
|
||||
|
||||
pub const BUILTIN_VERSION: &str = "v2.0.2";
|
||||
pub const BUILTIN_VERSION: &str = "v2.2";
|
||||
pub const GITHUB_REPO: &str = "ScreamAngel/gow3_launcher";
|
||||
pub const GITHUB_REPO_URL: &str = "https://github.com/ScreamAngel/gow3_launcher.git";
|
||||
|
||||
@@ -476,9 +476,9 @@ mod tests {
|
||||
assert_eq!(compare_version_strings("v1.5", "v1.4"), std::cmp::Ordering::Greater);
|
||||
assert_eq!(compare_version_strings("v1.10", "v1.9"), std::cmp::Ordering::Greater);
|
||||
assert_eq!(compare_version_strings("1.4", "v1.4"), std::cmp::Ordering::Equal);
|
||||
assert_eq!(compare_version_strings("v2.0.3", "v2.0.2"), std::cmp::Ordering::Greater);
|
||||
assert_eq!(compare_version_strings("v2.0.2", "v2.0.2"), std::cmp::Ordering::Equal);
|
||||
assert_eq!(compare_version_strings("v2.0.1", "v2.0.2"), std::cmp::Ordering::Less);
|
||||
assert_eq!(compare_version_strings("v2.2", "v2.1"), std::cmp::Ordering::Greater);
|
||||
assert_eq!(compare_version_strings("v2.1", "v2.1"), std::cmp::Ordering::Equal);
|
||||
assert_eq!(compare_version_strings("v2.0.3", "v2.1"), std::cmp::Ordering::Less);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// God of War III Remastered - Windows Input Bridge & Window Manager
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod imp {
|
||||
use std::ffi::c_void;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
static HOOK_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
static GAME_PID: AtomicU32 = AtomicU32::new(0);
|
||||
static HIDE_CURSOR_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
static HOOK_HANDLE: std::sync::atomic::AtomicIsize = std::sync::atomic::AtomicIsize::new(0);
|
||||
|
||||
type HWND = *mut c_void;
|
||||
type HHOOK = isize;
|
||||
type HINSTANCE = isize;
|
||||
type LRESULT = isize;
|
||||
type WPARAM = usize;
|
||||
type LPARAM = isize;
|
||||
type HOOKPROC = unsafe extern "system" fn(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT;
|
||||
|
||||
const WH_MOUSE_LL: i32 = 14;
|
||||
const WM_LBUTTONDOWN: usize = 0x0201;
|
||||
const WM_LBUTTONUP: usize = 0x0202;
|
||||
const WM_RBUTTONDOWN: usize = 0x0204;
|
||||
const WM_RBUTTONUP: usize = 0x0205;
|
||||
const WM_MBUTTONDOWN: usize = 0x0207;
|
||||
const WM_MBUTTONUP: usize = 0x0208;
|
||||
const WM_MOUSEWHEEL: usize = 0x020A;
|
||||
const WM_XBUTTONDOWN: usize = 0x020B;
|
||||
const WM_XBUTTONUP: usize = 0x020C;
|
||||
const WM_QUIT: u32 = 0x0012;
|
||||
|
||||
const VK_F13: u8 = 0x7C;
|
||||
const VK_F14: u8 = 0x7D;
|
||||
const VK_F15: u8 = 0x7E;
|
||||
const VK_F16: u8 = 0x7F;
|
||||
const VK_F17: u8 = 0x80;
|
||||
const VK_F18: u8 = 0x81;
|
||||
const VK_F19: u8 = 0x82;
|
||||
|
||||
// Direct Windows hardware scan codes for F13-F19 mapped by SDL3's windows_scancode_table:
|
||||
// 0x64 -> SDL_SCANCODE_F13 (Square / Light Attack)
|
||||
// 0x65 -> SDL_SCANCODE_F14 (Triangle / Heavy Attack)
|
||||
// 0x66 -> SDL_SCANCODE_F15 (Circle / Grab)
|
||||
// 0x67 -> SDL_SCANCODE_F16 (Cross / Jump)
|
||||
// 0x68 -> SDL_SCANCODE_F17
|
||||
// 0x69 -> SDL_SCANCODE_F18
|
||||
// 0x6A -> SDL_SCANCODE_F19
|
||||
const SCAN_F13: u8 = 0x64;
|
||||
const SCAN_F14: u8 = 0x65;
|
||||
const SCAN_F15: u8 = 0x66;
|
||||
const SCAN_F16: u8 = 0x67;
|
||||
const SCAN_F17: u8 = 0x68;
|
||||
const SCAN_F18: u8 = 0x69;
|
||||
const SCAN_F19: u8 = 0x6A;
|
||||
|
||||
const KEYEVENTF_KEYUP: u32 = 0x0002;
|
||||
const SW_SHOW: i32 = 5;
|
||||
const GA_ROOT: u32 = 2;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct POINT {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct MSLLHOOKSTRUCT {
|
||||
pt: POINT,
|
||||
mouse_data: u32,
|
||||
flags: u32,
|
||||
time: u32,
|
||||
extra_info: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct MSG {
|
||||
hwnd: HWND,
|
||||
message: u32,
|
||||
w_param: WPARAM,
|
||||
l_param: LPARAM,
|
||||
time: u32,
|
||||
pt: POINT,
|
||||
}
|
||||
|
||||
#[link(name = "user32")]
|
||||
#[link(name = "winmm")]
|
||||
unsafe extern "system" {
|
||||
fn SetWindowsHookExA(id_hook: i32, lpfn: HOOKPROC, h_mod: HINSTANCE, dw_thread_id: u32) -> HHOOK;
|
||||
fn UnhookWindowsHookEx(hhk: HHOOK) -> i32;
|
||||
fn CallNextHookEx(hhk: HHOOK, n_code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT;
|
||||
fn GetForegroundWindow() -> HWND;
|
||||
fn GetWindowThreadProcessId(hwnd: HWND, lpdw_process_id: *mut u32) -> u32;
|
||||
fn GetWindowTextA(hwnd: HWND, lp_string: *mut u8, n_max_count: i32) -> i32;
|
||||
fn keybd_event(b_vk: u8, b_scan: u8, dw_flags: u32, dw_extra_info: usize);
|
||||
fn GetMessageA(lp_msg: *mut MSG, hwnd: HWND, w_msg_filter_min: u32, w_msg_filter_max: u32) -> i32;
|
||||
fn PostThreadMessageA(id_thread: u32, msg: u32, w_param: WPARAM, l_param: LPARAM) -> i32;
|
||||
fn GetCurrentThreadId() -> u32;
|
||||
fn ShowWindow(hwnd: HWND, n_cmd_show: i32) -> i32;
|
||||
fn SetForegroundWindow(hwnd: HWND) -> i32;
|
||||
fn BringWindowToTop(hwnd: HWND) -> i32;
|
||||
fn EnumWindows(lp_enum_func: unsafe extern "system" fn(hwnd: HWND, lparam: LPARAM) -> i32, lparam: LPARAM) -> i32;
|
||||
fn IsWindowVisible(hwnd: HWND) -> i32;
|
||||
fn timeBeginPeriod(u_period: u32) -> u32;
|
||||
fn timeEndPeriod(u_period: u32) -> u32;
|
||||
fn WindowFromPoint(point: POINT) -> HWND;
|
||||
fn GetAncestor(hwnd: HWND, ga_flags: u32) -> HWND;
|
||||
fn AttachThreadInput(id_attach: u32, id_attach_to: u32, f_attach: i32) -> i32;
|
||||
fn PlaySoundA(psz_sound: *const u8, hmod: HINSTANCE, fdw_sound: u32) -> i32;
|
||||
}
|
||||
|
||||
pub fn play_sound_memory(bytes: &[u8]) {
|
||||
unsafe {
|
||||
// SND_ASYNC = 0x0001, SND_NODEFAULT = 0x0002, SND_MEMORY = 0x0004
|
||||
PlaySoundA(bytes.as_ptr(), 0, 0x0001 | 0x0002 | 0x0004);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn is_game_window(hwnd: HWND) -> bool {
|
||||
if hwnd.is_null() {
|
||||
return false;
|
||||
}
|
||||
let my_pid = std::process::id();
|
||||
let expected_pid = GAME_PID.load(Ordering::SeqCst);
|
||||
let mut win_pid: u32 = 0;
|
||||
unsafe {
|
||||
GetWindowThreadProcessId(hwnd, &mut win_pid);
|
||||
}
|
||||
if win_pid == my_pid {
|
||||
return false; // Asla launcher penceresiyle eşleşme!
|
||||
}
|
||||
if expected_pid > 0 && win_pid == expected_pid {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut title_buf = [0u8; 128];
|
||||
let len = unsafe { GetWindowTextA(hwnd, title_buf.as_mut_ptr(), 127) };
|
||||
if len > 0 {
|
||||
let title = String::from_utf8_lossy(&title_buf[..len as usize]);
|
||||
if title.contains("Launcher") || title.contains("gow3-launcher") {
|
||||
return false;
|
||||
}
|
||||
if title.contains("God of War") || title.contains("gow3") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
unsafe extern "system" fn low_level_mouse_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
if code >= 0 {
|
||||
unsafe {
|
||||
let fg = GetForegroundWindow();
|
||||
let p_mouse = &*(lparam as *const MSLLHOOKSTRUCT);
|
||||
let hwnd_pt = WindowFromPoint(p_mouse.pt);
|
||||
let is_fg = is_game_window(fg);
|
||||
let is_pt = is_game_window(hwnd_pt) || is_game_window(GetAncestor(hwnd_pt, GA_ROOT));
|
||||
|
||||
if is_fg || is_pt {
|
||||
match wparam {
|
||||
WM_LBUTTONDOWN => {
|
||||
keybd_event(VK_F13, SCAN_F13, 0, 0);
|
||||
}
|
||||
WM_LBUTTONUP => {
|
||||
keybd_event(VK_F13, SCAN_F13, KEYEVENTF_KEYUP, 0);
|
||||
}
|
||||
WM_RBUTTONDOWN => {
|
||||
keybd_event(VK_F14, SCAN_F14, 0, 0);
|
||||
}
|
||||
WM_RBUTTONUP => {
|
||||
keybd_event(VK_F14, SCAN_F14, KEYEVENTF_KEYUP, 0);
|
||||
}
|
||||
WM_MBUTTONDOWN => {
|
||||
keybd_event(VK_F15, SCAN_F15, 0, 0);
|
||||
}
|
||||
WM_MBUTTONUP => {
|
||||
keybd_event(VK_F15, SCAN_F15, KEYEVENTF_KEYUP, 0);
|
||||
}
|
||||
WM_XBUTTONDOWN => {
|
||||
let xbtn = (p_mouse.mouse_data >> 16) as u16;
|
||||
if xbtn == 1 {
|
||||
keybd_event(VK_F16, SCAN_F16, 0, 0);
|
||||
} else if xbtn == 2 {
|
||||
keybd_event(VK_F17, SCAN_F17, 0, 0);
|
||||
}
|
||||
}
|
||||
WM_XBUTTONUP => {
|
||||
let xbtn = (p_mouse.mouse_data >> 16) as u16;
|
||||
if xbtn == 1 {
|
||||
keybd_event(VK_F16, SCAN_F16, KEYEVENTF_KEYUP, 0);
|
||||
} else if xbtn == 2 {
|
||||
keybd_event(VK_F17, SCAN_F17, KEYEVENTF_KEYUP, 0);
|
||||
}
|
||||
}
|
||||
WM_MOUSEWHEEL => {
|
||||
let delta = (p_mouse.mouse_data >> 16) as i16;
|
||||
if delta > 0 {
|
||||
keybd_event(VK_F18, SCAN_F18, 0, 0);
|
||||
keybd_event(VK_F18, SCAN_F18, KEYEVENTF_KEYUP, 0);
|
||||
} else if delta < 0 {
|
||||
keybd_event(VK_F19, SCAN_F19, 0, 0);
|
||||
keybd_event(VK_F19, SCAN_F19, KEYEVENTF_KEYUP, 0);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hhk = HOOK_HANDLE.load(Ordering::SeqCst);
|
||||
unsafe { CallNextHookEx(hhk, code, wparam, lparam) }
|
||||
}
|
||||
|
||||
struct EnumParam {
|
||||
target_pid: u32,
|
||||
found_hwnd: HWND,
|
||||
}
|
||||
|
||||
unsafe extern "system" fn enum_win_proc(hwnd: HWND, lparam: LPARAM) -> i32 {
|
||||
let p = unsafe { &mut *(lparam as *mut EnumParam) };
|
||||
let mut pid: u32 = 0;
|
||||
unsafe {
|
||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||
if pid == std::process::id() {
|
||||
return 1; // Launcher penceresini asla hedef alma!
|
||||
}
|
||||
if IsWindowVisible(hwnd) != 0 {
|
||||
if p.target_pid > 0 && pid == p.target_pid {
|
||||
p.found_hwnd = hwnd;
|
||||
return 0; // Hedef oyun PID'sine ait görünür pencere bulundu!
|
||||
} else if p.target_pid == 0 && is_game_window(hwnd) {
|
||||
p.found_hwnd = hwnd;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
1 // continue
|
||||
}
|
||||
|
||||
pub fn find_game_hwnd(pid: u32) -> Option<HWND> {
|
||||
let mut param = EnumParam {
|
||||
target_pid: pid,
|
||||
found_hwnd: std::ptr::null_mut(),
|
||||
};
|
||||
unsafe {
|
||||
EnumWindows(enum_win_proc, &mut param as *mut _ as LPARAM);
|
||||
}
|
||||
if !param.found_hwnd.is_null() {
|
||||
Some(param.found_hwnd)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bring_game_window_to_front(pid: u32) {
|
||||
if let Some(hwnd) = find_game_hwnd(pid) {
|
||||
unsafe {
|
||||
let cur_fg = GetForegroundWindow();
|
||||
let cur_thread = GetCurrentThreadId();
|
||||
let mut fg_pid = 0u32;
|
||||
let fg_thread = GetWindowThreadProcessId(cur_fg, &mut fg_pid);
|
||||
if fg_thread != 0 && cur_thread != fg_thread {
|
||||
AttachThreadInput(cur_thread, fg_thread, 1);
|
||||
}
|
||||
ShowWindow(hwnd, SW_SHOW);
|
||||
BringWindowToTop(hwnd);
|
||||
SetForegroundWindow(hwnd);
|
||||
if fg_thread != 0 && cur_thread != fg_thread {
|
||||
AttachThreadInput(cur_thread, fg_thread, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WinInputGuard {
|
||||
thread_id: u32,
|
||||
}
|
||||
|
||||
impl Drop for WinInputGuard {
|
||||
fn drop(&mut self) {
|
||||
HOOK_RUNNING.store(false, Ordering::SeqCst);
|
||||
if self.thread_id != 0 {
|
||||
unsafe {
|
||||
PostThreadMessageA(self.thread_id, WM_QUIT, 0, 0);
|
||||
timeEndPeriod(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_mouse_hook(pid: u32, hide_cursor: bool) -> WinInputGuard {
|
||||
GAME_PID.store(pid, Ordering::SeqCst);
|
||||
HIDE_CURSOR_ENABLED.store(hide_cursor, Ordering::SeqCst);
|
||||
HOOK_RUNNING.store(true, Ordering::SeqCst);
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
thread::spawn(move || {
|
||||
unsafe {
|
||||
timeBeginPeriod(1);
|
||||
|
||||
let tid = GetCurrentThreadId();
|
||||
let hook = SetWindowsHookExA(WH_MOUSE_LL, low_level_mouse_proc, 0, 0);
|
||||
HOOK_HANDLE.store(hook, Ordering::SeqCst);
|
||||
let _ = tx.send(tid);
|
||||
|
||||
if hook != 0 {
|
||||
let mut msg: MSG = std::mem::zeroed();
|
||||
while HOOK_RUNNING.load(Ordering::SeqCst) && GetMessageA(&mut msg, std::ptr::null_mut(), 0, 0) > 0 {
|
||||
// Hook message loop
|
||||
}
|
||||
UnhookWindowsHookEx(hook);
|
||||
HOOK_HANDLE.store(0, Ordering::SeqCst);
|
||||
}
|
||||
timeEndPeriod(1);
|
||||
}
|
||||
});
|
||||
|
||||
let thread_id = rx.recv_timeout(Duration::from_millis(500)).unwrap_or(0);
|
||||
WinInputGuard { thread_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use imp::*;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod stub {
|
||||
pub struct WinInputGuard;
|
||||
pub fn start_mouse_hook(_pid: u32, _hide_cursor: bool) -> WinInputGuard {
|
||||
WinInputGuard
|
||||
}
|
||||
pub fn bring_game_window_to_front(pid: u32) {
|
||||
if pid > 0 {
|
||||
let _ = std::process::Command::new("hyprctl")
|
||||
.args(["dispatch", "focuswindow", &format!("pid:{}", pid)])
|
||||
.output();
|
||||
let _ = std::process::Command::new("xdotool")
|
||||
.args(["search", "--pid", &pid.to_string(), "windowactivate"])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
pub fn find_game_hwnd(_pid: u32) -> Option<isize> {
|
||||
None
|
||||
}
|
||||
pub fn play_sound_memory(_bytes: &[u8]) {}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::*;
|
||||
@@ -9,6 +9,7 @@
|
||||
"launch_while_compiling_shaders": false,
|
||||
"show_shader_compile_rage_meter": false,
|
||||
"skip_cinematics": true,
|
||||
"bonus_play_fix": true,
|
||||
"use_gamemode": true,
|
||||
"vram_gc_budget_mb": 6144,
|
||||
"dmem_mb": 9216,
|
||||
@@ -22,17 +23,16 @@
|
||||
"cpu_threads": 20
|
||||
},
|
||||
"last_git_commit": "0b4abe07acb711848d1b5d72402a9de8c0ad9844",
|
||||
"launcher_version": "v2.0.2",
|
||||
"launcher_version": "v2.2",
|
||||
"disabled_mods": [
|
||||
"(OPTIONAL) God Armor",
|
||||
"Blades of Athena Damage Boost",
|
||||
"CJ (Mod by Gamer JP)",
|
||||
"game",
|
||||
"Game",
|
||||
"god armor By Richarito v2",
|
||||
"Golden Blades",
|
||||
"DiamondSword v1.1 Author YT @MustopuhGGI",
|
||||
"Skeleton Kratos Skin"
|
||||
"Skeleton Kratos Skin",
|
||||
"Golden Blades",
|
||||
"CJ (Mod by Gamer JP)"
|
||||
],
|
||||
"render_resolution": "native",
|
||||
"fsr_enabled": false,
|
||||
@@ -47,10 +47,10 @@
|
||||
"deadzone_left": 2,
|
||||
"deadzone_right": 2,
|
||||
"keyboard_bindings": {
|
||||
"triangle": "u",
|
||||
"circle": "j",
|
||||
"cross": "g",
|
||||
"square": "t",
|
||||
"triangle": "mouse2",
|
||||
"circle": "f",
|
||||
"cross": "space",
|
||||
"square": "mouse1",
|
||||
"l1": "q",
|
||||
"r1": "e",
|
||||
"l2": "z",
|
||||
@@ -71,5 +71,7 @@
|
||||
"axis_right_x_plus": "kp6",
|
||||
"axis_right_y_minus": "kp8",
|
||||
"axis_right_y_plus": "kp2"
|
||||
}
|
||||
},
|
||||
"discord_rpc_enabled": true,
|
||||
"discord_client_id": "1440140183675867156"
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"disabled": [
|
||||
"(OPTIONAL) God Armor",
|
||||
"Blades of Athena Damage Boost",
|
||||
"CJ (Mod by Gamer JP)",
|
||||
"game",
|
||||
"Game",
|
||||
"god armor By Richarito v2",
|
||||
"Golden Blades",
|
||||
"DiamondSword v1.1 Author YT @MustopuhGGI",
|
||||
"Skeleton Kratos Skin"
|
||||
"Skeleton Kratos Skin",
|
||||
"Golden Blades",
|
||||
"CJ (Mod by Gamer JP)"
|
||||
],
|
||||
"order": []
|
||||
}
|
||||
@@ -31,6 +31,12 @@
|
||||
<Line Type="bytes" Address="0x006de2c7" Value="0f84d600000090"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Bug Fix - Bonus Play QTE and Enemy Spawning Fix" Note="Fixes a critical scripting desynchronization in God of War III Bonus Play when Godly Possessions (specifically Hephaestus' Ring) are active. Prevents auto-QTE from prematurely skipping context-sensitive animation death callbacks, ensuring enemy wave spawns and QTE triggers progress without arena softlocks." Author="gow3-launcher" PatchVer="1.0" AppVer="01.02" AppElf="eboot.bin" isEnabled="true">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0041b2a3" Value="7542"/>
|
||||
<Line Type="bytes" Address="0x0041b2e7" Value="b901000000909090"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Resolution Patch - 480p" Note="Decreases game output resolution to 854x480 and increases Video Arena to 2.25 GiB. Requires setting DMEM to 2048 MB or more. Only leave one resolution patch enabled; do not enable it with Bug Fix - Texture Corruption Fix." Author="kvicken + cuesta4" PatchVer="1.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="false">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x005f0c5b" Value="56030000"/>
|
||||
@@ -201,4 +207,14 @@
|
||||
<Line Type="bytes" Address="0x00566090" Value="c3"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
<Metadata Title="God of War III Remastered" Name="Bug Fix - Trophy Unlock Fix" Note="Disables Bonus Play trophy restriction and bounds checks in eboot.bin, enabling all 35 trophies to unlock seamlessly." Author="gow3-launcher" PatchVer="2.1" AppVer="01.02" AppElf="eboot.bin" isEnabled="true">
|
||||
<PatchList>
|
||||
<Line Type="bytes" Address="0x0041ef88" Value="909090909090"/>
|
||||
<Line Type="bytes" Address="0x0041efce" Value="909090909090"/>
|
||||
<Line Type="bytes" Address="0x0041f709" Value="9090"/>
|
||||
<Line Type="bytes" Address="0x0041f73a" Value="9090"/>
|
||||
<Line Type="bytes" Address="0x004252e9" Value="9090"/>
|
||||
<Line Type="bytes" Address="0x00425330" Value="9090"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
</Patch>
|
||||
@@ -151,6 +151,16 @@ LINKED = {}
|
||||
|
||||
|
||||
def make_link(link, target):
|
||||
if link.is_symlink() or link.is_file():
|
||||
try:
|
||||
link.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
elif link.is_dir():
|
||||
try:
|
||||
remove_link(link)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
link.symlink_to(target, target_is_directory=target.is_dir())
|
||||
return
|
||||
@@ -159,13 +169,19 @@ def make_link(link, target):
|
||||
raise
|
||||
if target.is_dir():
|
||||
import _winapi
|
||||
_winapi.CreateJunction(str(target), str(link))
|
||||
try:
|
||||
_winapi.CreateJunction(str(target), str(link))
|
||||
except OSError:
|
||||
# Fallback if junction creation fails across volumes or permissions
|
||||
shutil.copytree(target, link, dirs_exist_ok=True)
|
||||
else:
|
||||
try:
|
||||
os.link(target, link)
|
||||
except OSError:
|
||||
shutil.copy2(target, link)
|
||||
LINKED[str(link)] = target
|
||||
p_str = str(link)
|
||||
LINKED[p_str] = target
|
||||
LINKED[p_str.casefold()] = target
|
||||
|
||||
|
||||
def symlinks_work(directory):
|
||||
@@ -180,19 +196,52 @@ def symlinks_work(directory):
|
||||
|
||||
|
||||
def is_link(path):
|
||||
return path.is_symlink() or str(path) in LINKED or (hasattr(path, 'is_junction') and path.is_junction())
|
||||
p_str = str(path)
|
||||
if path.is_symlink():
|
||||
return True
|
||||
if p_str in LINKED or p_str.casefold() in LINKED:
|
||||
return True
|
||||
if hasattr(path, 'is_junction') and path.is_junction():
|
||||
return True
|
||||
if os.name == 'nt':
|
||||
try:
|
||||
import stat
|
||||
mode = os.lstat(path).st_mode
|
||||
if stat.S_ISLNK(mode):
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def link_target(path):
|
||||
return LINKED.get(str(path)) or path.resolve(strict=True)
|
||||
p_str = str(path)
|
||||
return LINKED.get(p_str) or LINKED.get(p_str.casefold()) or path.resolve(strict=True)
|
||||
|
||||
|
||||
def remove_link(path):
|
||||
if hasattr(path, 'is_junction') and path.is_junction():
|
||||
os.rmdir(path)
|
||||
else:
|
||||
path.unlink()
|
||||
LINKED.pop(str(path), None)
|
||||
p_str = str(path)
|
||||
try:
|
||||
if path.is_symlink():
|
||||
path.unlink()
|
||||
elif hasattr(path, 'is_junction') and path.is_junction():
|
||||
os.rmdir(path)
|
||||
elif path.is_dir():
|
||||
os.rmdir(path)
|
||||
else:
|
||||
path.unlink()
|
||||
except OSError:
|
||||
try:
|
||||
if path.is_symlink():
|
||||
path.unlink()
|
||||
elif os.path.isdir(path):
|
||||
os.rmdir(path)
|
||||
else:
|
||||
path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
LINKED.pop(p_str, None)
|
||||
LINKED.pop(p_str.casefold(), None)
|
||||
|
||||
|
||||
def expand(directory):
|
||||
@@ -202,7 +251,7 @@ def expand(directory):
|
||||
if not target.is_dir():
|
||||
raise ValueError(f'File/directory conflict at {directory.name}')
|
||||
remove_link(directory)
|
||||
directory.mkdir()
|
||||
directory.mkdir(exist_ok=True)
|
||||
for entry in target.iterdir():
|
||||
make_link(directory / entry.name, entry)
|
||||
elif directory.exists() and not directory.is_dir():
|
||||
@@ -211,6 +260,17 @@ def expand(directory):
|
||||
directory.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def on_rm_error(func, path, exc_info):
|
||||
try:
|
||||
os.chmod(path, 0o777)
|
||||
if os.path.isdir(path):
|
||||
os.rmdir(path)
|
||||
else:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def build_overlay(game, out, mods):
|
||||
game = Path(game).resolve(strict=True)
|
||||
replacements = []
|
||||
@@ -231,7 +291,7 @@ def build_overlay(game, out, mods):
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
for old_dir in out.glob('mod-game-*'):
|
||||
if old_dir.is_dir():
|
||||
shutil.rmtree(old_dir, ignore_errors=True)
|
||||
shutil.rmtree(old_dir, onerror=on_rm_error)
|
||||
if os.name == 'nt' and not symlinks_work(out) and out.drive.casefold() != game.drive.casefold():
|
||||
# Hard links only reach files on the same volume: the overlay goes beside the game.
|
||||
out = game.parent
|
||||
@@ -258,7 +318,7 @@ def build_overlay(game, out, mods):
|
||||
print(f'Mods: {replaced} game files replaced, {added} added', file=sys.stderr)
|
||||
return overlay
|
||||
except BaseException:
|
||||
shutil.rmtree(overlay)
|
||||
shutil.rmtree(overlay, onerror=on_rm_error)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"title_id": "CUSA01715",
|
||||
"last_updated": 1794235113,
|
||||
"trophies": {
|
||||
"0": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"1": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"2": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"3": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"4": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"5": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"6": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"7": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"8": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"9": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"10": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"11": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"12": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"13": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"14": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"15": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"16": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"17": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"18": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"19": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"20": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"21": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"22": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"23": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"24": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"25": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"26": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"27": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"28": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"29": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"30": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"31": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"32": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"33": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
},
|
||||
"34": {
|
||||
"unlocked": false,
|
||||
"unlocked_at": null,
|
||||
"unlocked_at_formatted": null
|
||||
}
|
||||
}
|
||||
}
|
||||