diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b38f6e9..251feca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,8 @@ Thanks for your interest in contributing to OWL Control! πŸ¦‰ ## Building from Source +**Note for Linux developers**: See [LINUX_DEV_SETUP.md](./tools/vm/LINUX_DEV_SETUP.md) for instructions on setting up a Windows VM for development and testing. + Using PowerShell or Command Prompt: 1. Install [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html). @@ -11,7 +13,7 @@ Using PowerShell or Command Prompt: 2. Clone the repo: ```powershell -git clone https://github.com/Wayfarer-Labs/owl-control.git +git clone https://github.com/Overworldai/owl-control.git cd owl-control ``` @@ -154,13 +156,11 @@ cargo run -p bump-version -- 1.1.1-rc1 After creating a release candidate: 1. Push the RC tag to trigger the automated build -2. Share the RC with testers in the Discord server -3. Gather feedback and fix any issues +2. Gather feedback and fix any issues 4. Once validated, create the final release ## Questions? If you have any questions or need help, feel free to: -- Open an issue on [GitHub Issues](https://github.com/Wayfarer-Labs/owl-control/issues) -- Join the discussion in the Discord server +- Open an issue on [GitHub Issues](https://github.com/Overworldai/owl-control/issues) diff --git a/Cargo.lock b/Cargo.lock index bfe3493..017c07c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,6 +480,20 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.16", + "instant", + "pin-project-lite", + "rand 0.8.5", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -628,7 +642,7 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" [[package]] name = "bump-version" -version = "1.5.0" +version = "1.6.0" dependencies = [ "clap", "color-eyre", @@ -732,7 +746,7 @@ dependencies = [ [[package]] name = "catppuccin-egui" version = "5.6.0" -source = "git+https://github.com/Wayfarer-Labs/catppuccin-egui.git?branch=disable-build-script#35115dd41a8deebf403bbdc8ee5b6f1d9e4a3a45" +source = "git+https://github.com/Overworldai/catppuccin-egui.git?branch=disable-build-script#35115dd41a8deebf403bbdc8ee5b6f1d9e4a3a45" dependencies = [ "egui", ] @@ -1099,8 +1113,8 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.62.2", - "windows-core 0.62.2", + "windows 0.61.3", + "windows-core 0.58.0", ] [[package]] @@ -1304,7 +1318,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -1535,7 +1549,7 @@ dependencies = [ [[package]] name = "egui_overlay" version = "0.9.0" -source = "git+https://github.com/Wayfarer-Labs/egui_overlay.git?branch=master#5ea1c91b4baac4b0fb808bc94486c881dc413a86" +source = "git+https://github.com/Overworldai/egui_overlay.git?branch=master#5ea1c91b4baac4b0fb808bc94486c881dc413a86" dependencies = [ "egui", "egui_render_three_d", @@ -1547,7 +1561,7 @@ dependencies = [ [[package]] name = "egui_render_glow" version = "0.9.1" -source = "git+https://github.com/Wayfarer-Labs/egui_overlay.git?branch=master#5ea1c91b4baac4b0fb808bc94486c881dc413a86" +source = "git+https://github.com/Overworldai/egui_overlay.git?branch=master#5ea1c91b4baac4b0fb808bc94486c881dc413a86" dependencies = [ "bytemuck", "egui", @@ -1563,7 +1577,7 @@ dependencies = [ [[package]] name = "egui_render_three_d" version = "0.9.0" -source = "git+https://github.com/Wayfarer-Labs/egui_overlay.git?branch=master#5ea1c91b4baac4b0fb808bc94486c881dc413a86" +source = "git+https://github.com/Overworldai/egui_overlay.git?branch=master#5ea1c91b4baac4b0fb808bc94486c881dc413a86" dependencies = [ "egui", "egui_render_glow", @@ -1584,7 +1598,7 @@ dependencies = [ [[package]] name = "egui_window_glfw_passthrough" version = "0.9.0" -source = "git+https://github.com/Wayfarer-Labs/egui_overlay.git?branch=master#5ea1c91b4baac4b0fb808bc94486c881dc413a86" +source = "git+https://github.com/Overworldai/egui_overlay.git?branch=master#5ea1c91b4baac4b0fb808bc94486c881dc413a86" dependencies = [ "egui", "glfw-passthrough", @@ -1752,7 +1766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -2328,7 +2342,7 @@ dependencies = [ "gobject-sys 0.21.2", "libc", "system-deps 7.0.7", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -2872,7 +2886,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.58.0", ] [[package]] @@ -3160,7 +3174,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -3374,8 +3388,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libobs" -version = "4.0.2+32.0.2" -source = "git+https://github.com/libobs-rs/libobs-rs.git#9406f52e1bc8a93b992be9b04725ff8ab54bf3c6" +version = "5.0.0+32.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eef3e1fb1b9265a01c927e1915cfa4c5760359d01a07291bad6e6ba61e34a39" dependencies = [ "bindgen", "pkg-config", @@ -3383,10 +3398,12 @@ dependencies = [ [[package]] name = "libobs-simple" -version = "5.0.6+32.0.2" -source = "git+https://github.com/libobs-rs/libobs-rs.git#9406f52e1bc8a93b992be9b04725ff8ab54bf3c6" +version = "8.0.1+32.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb0021c0d3497415b7e558605a74d2a297a46f8301bb3ca2564bcd11aee6181d" dependencies = [ "display-info", + "lazy_static", "libobs", "libobs-simple-macro", "libobs-window-helper", @@ -3395,12 +3412,15 @@ dependencies = [ "num-derive", "num-traits", "paste", + "tokio", + "windows 0.62.2", ] [[package]] name = "libobs-simple-macro" -version = "6.0.1" -source = "git+https://github.com/libobs-rs/libobs-rs.git#9406f52e1bc8a93b992be9b04725ff8ab54bf3c6" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3bdb09c0709df286ccd0fd07994d1fee8877e039fee6e3c21f043d90da76db" dependencies = [ "proc-macro2", "quote", @@ -3409,16 +3429,18 @@ dependencies = [ [[package]] name = "libobs-window-helper" -version = "0.2.1" -source = "git+https://github.com/libobs-rs/libobs-rs.git#9406f52e1bc8a93b992be9b04725ff8ab54bf3c6" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69207f23b413243491c3703c6c1c9434fd0ff46f158ef4d945d1dc8d889bad80" dependencies = [ "windows 0.62.2", ] [[package]] name = "libobs-wrapper" -version = "6.0.4+32.0.2" -source = "git+https://github.com/libobs-rs/libobs-rs.git#9406f52e1bc8a93b992be9b04725ff8ab54bf3c6" +version = "9.0.4+32.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c081d77e7aa29a0a72415e0ad2be91e4ee579bf69ccf834077d8206c87bbf6" dependencies = [ "chrono", "display-info", @@ -3913,7 +3935,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.106", @@ -4478,9 +4500,10 @@ dependencies = [ [[package]] name = "owl-control" -version = "1.5.0" +version = "1.6.0" dependencies = [ "async-trait", + "backoff", "catppuccin-egui", "chrono", "clap", @@ -5411,7 +5434,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -6201,7 +6224,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.0", + "windows-sys 0.60.2", ] [[package]] @@ -6777,7 +6800,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "update-games" -version = "1.5.0" +version = "1.6.0" dependencies = [ "constants", ] @@ -7395,7 +7418,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1530f98..726f94f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [workspace] resolver = "3" members = ["crates/*", "tools/*"] +exclude = ["tools/vm"] default-members = ["."] [workspace.package] -version = "1.5.0" +version = "1.6.0" [workspace.dependencies] input-capture = { path = "crates/input-capture" } @@ -44,12 +45,11 @@ wgpu = "27.0.0" obws = "0.14.0" async-trait = "0.1" -libobs-wrapper = { git = "https://github.com/libobs-rs/libobs-rs.git", features = [ +libobs-wrapper = { version = "9", features = [ "logging_crash_handler", - "enable_runtime", ], default-features = false } -libobs-simple = { git = "https://github.com/libobs-rs/libobs-rs.git" } -libobs-window-helper = { git = "https://github.com/libobs-rs/libobs-rs.git" } +libobs-simple = "8" +libobs-window-helper = "0.4" [package] name = "owl-control" @@ -89,14 +89,14 @@ egui_extras = { version = "0.33", features = ["all_loaders", "datepicker"] } egui-wgpu = "0.33" egui-winit = "0.33" egui_commonmark = { version = "0.22", features = ["macros"] } -# Wayfarer-owned forks for bumping egui version to 0.33. Will remove once updated. -egui_overlay = { git = "https://github.com/Wayfarer-Labs/egui_overlay.git", branch = "master", features = [ +# Overworld-owned forks for bumping egui version to 0.33. Will remove once updated. +egui_overlay = { git = "https://github.com/Overworldai/egui_overlay.git", branch = "master", features = [ "egui_default", "glfw_default", ] } -egui_window_glfw_passthrough = { git = "https://github.com/Wayfarer-Labs/egui_overlay.git", branch = "master" } -egui_render_three_d = { git = "https://github.com/Wayfarer-Labs/egui_overlay.git", branch = "master" } -catppuccin-egui = { git = "https://github.com/Wayfarer-Labs/catppuccin-egui.git", branch = "disable-build-script", default-features = false, features = [ +egui_window_glfw_passthrough = { git = "https://github.com/Overworldai/egui_overlay.git", branch = "master" } +egui_render_three_d = { git = "https://github.com/Overworldai/egui_overlay.git", branch = "master" } +catppuccin-egui = { git = "https://github.com/Overworldai/catppuccin-egui.git", branch = "disable-build-script", default-features = false, features = [ "egui33", ] } egui_virtual_list = "0.9.0" @@ -112,6 +112,7 @@ reqwest = { version = "0.12.23", features = ["json", "stream"] } tar = "0.4.44" opener = { version = "0.8", features = ["reveal"] } sha256 = "1.6.0" +backoff = { version = "0.4", features = ["tokio"] } rfd = "0.14" steamlocate.workspace = true diff --git a/GAMES.md b/GAMES.md index 0d20265..aa8595a 100644 --- a/GAMES.md +++ b/GAMES.md @@ -1,5 +1,10 @@ # Usage Guide +> [!CAUTION] +> **PUBLIC SUBMISSIONS ARE CLOSED.** We are not accepting new data submissions and will not pay for recordings. Our backend rejects submissions from users without a pre-established agreement. The guidelines below are for existing data collection partners only. +> +> Any public information indicating that we pay for submissions is out of date; if you can, we'd appreciate you letting the source know that the program has ended. + Follow these guidelines. You must be 18 or older! ❌ Cutscenes or menus. We need input data, not movies.
@@ -22,7 +27,6 @@ Follow these guidelines. You must be 18 or older! > [!IMPORTANT] > > - Singleplayer game recordings are preferred but multiplayer games are permitted. -> - We buy games from `Top-priority` for veteran contributors. > - We don't capture your microphone or anything outside the active game. > - MSI Afterburner or similar stats programs should be disabled and substituted with [Steam Overlay stats](https://help.steampowered.com/en/faqs/view/3462-CD4C-36BD5767) > - **Playing PvP games with anticheat is at your own risk.** Our key recording may trip anticheat systems, potentially resulting in bans or other penalties. @@ -36,7 +40,6 @@ Follow these guidelines. You must be 18 or older! > - Tampering with our software system.
> - Attempting to record pirated gameplay.
-Questions? Chat in the [Discord #owl-control channel](https://discord.gg/ZgCWTGYf4E)!
## The Vibe @@ -46,184 +49,317 @@ Secondly, we are also interested in games that display unique movement and envir In order to end up with a realistic model, we are generally not permitting games that came out before 2010, though there is some wiggle room for graphically advanced games. -## Games +## Unsupported Games -This is a whitelist: if you see your game here, you can play it, but you cannot play anything outside of this list. -Recordings of games not on this list will be rejected, and you will not be paid for your time. +The following games are excluded from recording. All other games are recordable. Games may be excluded because we have already collected sufficient data, or for other reasons. -We will not be adding any additional games to this list until the end of 2025. However, please note that we may remove games if we have captured sufficient data for them. +This list is automatically generated by `cargo run -p update-games`. Do not edit it directly. -- [A Story About My Uncle](https://store.steampowered.com/app/278360/A_Story_About_My_Uncle/) -- [Abyssus](https://store.steampowered.com/app/1721110/Abyssus/) -- [Alien: Isolation](https://store.steampowered.com/app/214490/Alien_Isolation/) -- [Amenti](https://store.steampowered.com/app/3292260/Amenti/) -- [Apex Legends](https://store.steampowered.com/app/1172470/Apex_Legends/) -- [ARMA 3](https://store.steampowered.com/app/107410/Arma_3/) -- [Atomic Heart](https://store.steampowered.com/app/668580/Atomic_Heart/) -- [Avowed](https://store.steampowered.com/app/2457220/Avowed/) -- [Back 4 Blood](https://store.steampowered.com/app/924970/Back_4_Blood/) -- [Battlefield 1](https://store.steampowered.com/app/1238840/Battlefield_1/) -- [Battlefield 3](https://store.steampowered.com/app/1238820/Battlefield_3/) -- [Battlefield 4](https://store.steampowered.com/app/1238860/Battlefield_4/) -- [Battlefield 6 / REDSEC](https://store.steampowered.com/app/2807960/Battlefield_6/) -- [Battlefield Hardline](https://store.steampowered.com/app/1238880/Battlefield_Hardline/) -- [Battlefield V](https://store.steampowered.com/app/1238810/Battlefield_V/) -- [BioShock Infinite](https://store.steampowered.com/app/8870/BioShock_Infinite/) -- [Blacktail](https://store.steampowered.com/app/1532690/BLACKTAIL/) -- [Blair Witch](https://store.steampowered.com/app/1092660/Blair_Witch/) -- [Borderlands 1](https://store.steampowered.com/app/729040/Borderlands_Game_of_the_Year_Enhanced/) -- [Borderlands 2](https://store.steampowered.com/app/49520/Borderlands_2/) -- [Borderlands 3](https://store.steampowered.com/app/397540/Borderlands_3/) -- [Borderlands 4](https://store.steampowered.com/app/1285190/Borderlands_4) -- [Borderlands: The Pre-Sequel](https://store.steampowered.com/app/261640/Borderlands_The_PreSequel/) -- [BPM: Bullets Per Minute](https://store.steampowered.com/app/1286350/BPM_BULLETS_PER_MINUTE/) -- [Call of Duty: Advanced Warfare](https://store.steampowered.com/app/209650/Call_of_Duty_Advanced_Warfare) -- [Call of Duty: Black Ops 6](https://store.steampowered.com/app/2933620/Call_of_Duty_Black_Ops_6/) -- [Call of Duty: Black Ops 7](https://store.steampowered.com/app/3606480/Call_of_Duty_Black_Ops_7/) -- [Call of Duty: Black Ops Cold War](https://store.steampowered.com/app/1985810/Call_of_Duty_Black_Ops_Cold_War/) -- [Call of Duty: Black Ops III](https://store.steampowered.com/app/311210/Call_of_Duty_Black_Ops_III/) -- [Call of Duty: Infinite Warfare](https://store.steampowered.com/app/292730/Call_of_Duty_Infinite_Warfare/) -- [Call of Duty: Modern Warfare (2019)](https://store.steampowered.com/app/2000950/Call_of_Duty_Modern_Warfare/) -- [Call of Duty: Modern Warfare II (2022)](https://store.steampowered.com/app/3595230/Call_of_Duty_Modern_Warfare_II/) -- [Call of Duty: Modern Warfare III (2023)](https://store.steampowered.com/app/3595270/Call_of_Duty_Modern_Warfare_III/) -- [Call of Duty: Vanguard](https://store.steampowered.com/app/1985820/Call_of_Duty_Vanguard/) -- [Call of Duty: WWII](https://store.steampowered.com/app/476600/Call_of_Duty_WWII/) -- [Close to the Sun](https://store.steampowered.com/app/968870/Close_to_the_Sun/) -- [Conundrum](https://store.steampowered.com/app/1744140/Conundrum/) -- [Crysis 1 Remastered](https://store.steampowered.com/app/1715130/Crysis_Remastered/) -- [Crysis 2](https://store.steampowered.com/app/108800/Crysis_2__Maximum_Edition/) -- [Crysis 3](https://store.steampowered.com/app/2096610/Crysis_3_Remastered/) -- [Dark Hours](https://store.steampowered.com/app/2208570/Dark_Hours/) -- [Dead Island 2](https://store.steampowered.com/app/934700/Dead_Island_2/) -- [Deadzone: Rogue](https://store.steampowered.com/app/3228590/Deadzone_Rogue) -- [Deathloop](https://store.steampowered.com/app/1252330/DEATHLOOP/) -- [Delta Force](https://store.steampowered.com/app/2507950/Delta_Force/) -- [Dishonored](https://store.steampowered.com/app/205100/Dishonored/) -- [Dishonored 2](https://store.steampowered.com/app/403640/Dishonored_2/) -- [Dishonored: Death of the Outsider](https://store.steampowered.com/app/614570/Dishonored_Death_of_the_Outsider/) -- [DOOM 2016](https://store.steampowered.com/app/379720/DOOM/) -- [DOOM Eternal](https://store.steampowered.com/app/782330/DOOM_Eternal/) -- [Earthfall](https://store.steampowered.com/app/415590/Earthfall/) -- [ELDERBORN](https://store.steampowered.com/app/727850/ELDERBORN/) -- [Enlisted](https://store.steampowered.com/app/2051620/Enlisted/) -- [Escape from Tarkov](https://store.steampowered.com/app/3932890/Escape_from_Tarkov/) -- [Everyone's Gone to the Rapture](https://store.steampowered.com/app/417880/Everybodys_Gone_to_the_Rapture/) -- [Fallout 4](https://store.steampowered.com/app/377160/Fallout_4/) -- [Far Cry 3](https://store.steampowered.com/app/220240/Far_Cry_3/) -- [Far Cry 3: Blood Dragon](https://store.steampowered.com/app/233270/Far_Cry_3__Blood_Dragon/) -- [Far Cry 4](https://store.steampowered.com/app/298110/Far_Cry_4/) -- [Far Cry 5](https://store.steampowered.com/app/552520/Far_Cry_5/) -- [Far Cry 6](https://store.steampowered.com/app/2369390/Far_Cry_6/) -- [Far Cry: New Dawn](https://store.steampowered.com/app/939960/Far_Cry_New_Dawn/) -- [Far Cry: Primal](https://store.steampowered.com/app/371660/Far_Cry_Primal/) -- [Firewatch](https://store.steampowered.com/app/383870/Firewatch/) -- [Fobia - St. Dinfna Hotel](https://store.steampowered.com/app/1298140/Fobia__St_Dinfna_Hotel/) -- [Generation Zero](https://store.steampowered.com/app/704270/Generation_Zero/) -- [Ghost Watchers](https://store.steampowered.com/app/1850740/Ghost_Watchers/) -- [Ghostrunner](https://store.steampowered.com/app/1139900/Ghostrunner/) -- [Ghostrunner 2](https://store.steampowered.com/app/2144740/Ghostrunner_2) -- [Ghostwire: Tokyo](https://store.steampowered.com/app/1475810/Ghostwire_Tokyo/) -- [Green Hell](https://store.steampowered.com/app/815370/Green_Hell/) -- [GTFO](https://store.steampowered.com/app/493520/GTFO/) -- [Halo 2 Anniversary (MCC)](https://store.steampowered.com/app/976730/Halo_The_Master_Chief_Collection/) -- [Halo 3 (MCC)](https://store.steampowered.com/app/976730/Halo_The_Master_Chief_Collection/) -- [Halo: Combat Evolved Anniversary (MCC)](https://store.steampowered.com/app/976730/Halo_The_Master_Chief_Collection/) -- [Halo: Infinite](https://store.steampowered.com/app/1240440/Halo_Infinite/) -- [Hard Reset Redux](https://store.steampowered.com/app/407810/Hard_Reset_Redux/) -- [Hardspace: Shipbreaker](https://store.steampowered.com/app/1161580/Hardspace_Shipbreaker) -- [Hell Let Loose](https://store.steampowered.com/app/686810/Hell_Let_Loose/) -- [High on Life](https://store.steampowered.com/app/1583230/High_On_Life) -- [Home Sweet Home](https://store.steampowered.com/app/617160/Home_Sweet_Home/) -- [Home Sweet Home 2](https://store.steampowered.com/app/1098940/Home_Sweet_Home_EP2/) -- [ICARUS](https://store.steampowered.com/app/1149460/ICARUS/) -- [Immortals of Aveum](https://store.steampowered.com/app/2009100/Immortals_of_Aveum/) -- [In Sound Mind](https://store.steampowered.com/app/1119980/In_Sound_Mind/) -- [Indiana Jones and the Great Circle](https://store.steampowered.com/app/2677660/Indiana_Jones_and_the_Great_Circle/) -- [Journey to the Savage Planet](https://store.steampowered.com/app/973810/Journey_To_The_Savage_Planet/) -- [Keep Digging](https://store.steampowered.com/app/3585800/Keep_Digging) -- [Killing Floor 2](https://store.steampowered.com/app/232090/Killing_Floor_2/) -- [Kingdom Come: Deliverance II](https://store.steampowered.com/app/1771300/Kingdom_Come_Deliverance_II/) -- [Layers of Fear](https://store.steampowered.com/app/391720/Layers_of_Fear/) -- [Layers of Fear 2](https://store.steampowered.com/app/1029890/Layers_of_Fear_2_2019) -- [Madison](https://store.steampowered.com/app/1670870/MADiSON/) -- [Medieval Dynasty](https://store.steampowered.com/app/1129580/Medieval_Dynasty/) -- [METAL EDEN](https://store.steampowered.com/app/990380/METAL_EDEN/) -- [Metro Exodus](https://store.steampowered.com/app/412020/Metro_Exodus/) -- [Metro: Last Light (delisted, see Redux)](https://store.steampowered.com/app/43160/Metro_Last_Light/) -- [Metro: Last Light Redux](https://store.steampowered.com/app/287390/Metro_Last_Light_Redux/) -- [Mirror's Edge](https://store.steampowered.com/app/17410/Mirrors_Edge/) -- [Mirror's Edge Catalyst](https://store.steampowered.com/app/1233570/Mirrors_Edge_Catalyst/) -- [MORDHAU](https://store.steampowered.com/app/629760/MORDHAU/) -- [Neon White](https://store.steampowered.com/app/1533420/Neon_White/) -- [No Man's Sky](https://store.steampowered.com/app/275850/No_Mans_Sky) -- [Observer: System Redux](https://store.steampowered.com/app/1386900/Observer_System_Redux/) -- [Outer Wilds](https://store.steampowered.com/app/753640/Outer_Wilds/) -- [Outlast 2](https://store.steampowered.com/app/414700/Outlast_2/) -- [Painkiller 2025](https://store.steampowered.com/app/2300120/Painkiller/) -- [Painkiller Hell & Damnation](https://store.steampowered.com/app/214870/Painkiller_Hell__Damnation/) -- [Panicore](https://store.steampowered.com/app/2695940/PANICORE/) -- [PAYDAY 2](https://store.steampowered.com/app/218620/PAYDAY_2/) -- [PAYDAY 3](https://store.steampowered.com/app/1272080/PAYDAY_3/) -- [Poppy Playtime](https://store.steampowered.com/app/1721470/Poppy_Playtime/) -- [Prey (2017)](https://store.steampowered.com/app/480490/Prey/) -- [Q.U.B.E.](https://store.steampowered.com/app/1564220/QUBE_10th_Anniversary/) -- [Q.U.B.E. 2](https://store.steampowered.com/app/359100/QUBE_2/) -- [RAGE 2](https://store.steampowered.com/app/548570/RAGE_2/) -- [Ready or Not](https://store.steampowered.com/app/1144200/Ready_or_Not/) -- [Resident Evil 7: Biohazard](https://store.steampowered.com/app/418370/RESIDENT_EVIL_7_biohazard/) -- [Resident Evil Village](https://store.steampowered.com/app/1196590/Resident_Evil_Village/) -- [Riven](https://store.steampowered.com/app/1712350/Riven/) -- [Salt 2](https://store.steampowered.com/app/1574900/Salt_2_Shores_of_Gold/) -- [SCP: 5K](https://store.steampowered.com/app/872670/SCP_5K/) -- [Sea of Thieves](https://store.steampowered.com/app/1172620/Sea_of_Thieves_2025_Edition/) -- [Severed Steel](https://store.steampowered.com/app/1227690/Severed_Steel/) -- [Shadow Warrior 2](https://store.steampowered.com/app/324800/Shadow_Warrior_2/) -- [Shadow Warrior 3](https://store.steampowered.com/app/1036890/Shadow_Warrior_3_Definitive_Edition) -- [Slime Rancher](https://store.steampowered.com/app/433340/Slime_Rancher/) -- [Slime Rancher 2](https://store.steampowered.com/app/1657630/Slime_Rancher_2/) -- [Soma](https://store.steampowered.com/app/282140/SOMA/) -- [Sons of the Forest](https://store.steampowered.com/app/1326470/Sons_Of_The_Forest) -- [Splitgate](https://store.steampowered.com/app/677620/Splitgate/) -- [Squad](https://store.steampowered.com/app/393380/Squad/) -- [Subnautica](https://store.steampowered.com/app/264710/Subnautica/) -- [Superliminal](https://store.steampowered.com/app/1049410/Superliminal/) -- [Tacoma](https://store.steampowered.com/app/343860/Tacoma/) -- [Teardown](https://store.steampowered.com/app/1167630/Teardown) -- [The Beast Inside](https://store.steampowered.com/app/792300/The_Beast_Inside/) -- [The Darkness II](https://store.steampowered.com/app/67370/The_Darkness_II/) -- [The Elder Scrolls IV: Oblivion Remastered](https://store.steampowered.com/app/2623190/The_Elder_Scrolls_IV_Oblivion_Remastered/) -- [The Elder Scrolls V: Skyrim](https://store.steampowered.com/app/489830/The_Elder_Scrolls_V_Skyrim_Special_Edition/) -- [The Forest](https://store.steampowered.com/app/242760/The_Forest/) -- [The Lightkeeper](https://store.steampowered.com/app/3612850/The_Lightkeeper/) -- [The Long Dark](https://store.steampowered.com/app/305620/The_Long_Dark/) -- [The Outer Worlds](https://store.steampowered.com/app/578650/The_Outer_Worlds/) -- [The Outer Worlds 2](https://store.steampowered.com/app/1449110/The_Outer_Worlds_2/) -- [The Outlast Trials](https://store.steampowered.com/app/1304930/The_Outlast_Trials) -- [The Stanley Parable](https://store.steampowered.com/app/221910/The_Stanley_Parable/) -- [The Stanley Parable: Ultra Deluxe](https://store.steampowered.com/app/1703340/The_Stanley_Parable_Ultra_Deluxe/) -- [The Talos Principle](https://store.steampowered.com/app/257510/The_Talos_Principle/) -- [The Talos Principle 2](https://store.steampowered.com/app/835960/The_Talos_Principle_2/) -- [The Vanishing of Ethan Carter](https://store.steampowered.com/app/258520/The_Vanishing_of_Ethan_Carter) -- [The Witness](https://store.steampowered.com/app/210970/The_Witness/) -- [Thief](https://store.steampowered.com/app/239160/Thief/) -- [Thief Simulator](https://store.steampowered.com/app/704850/Thief_Simulator/) -- [Tiny Tina's Wonderlands](https://store.steampowered.com/app/1286680/Tiny_Tinas_Wonderlands/) -- [Titanfall 2](https://store.steampowered.com/app/1237970/Titanfall_2/) -- [Trepang2](https://store.steampowered.com/app/1164940/Trepang2/) -- [Visage](https://store.steampowered.com/app/594330/Visage/) -- [Viscera Cleanup Detail](https://store.steampowered.com/app/246900/Viscera_Cleanup_Detail/) -- [VOIDBREAKER](https://store.steampowered.com/app/2615540/VOIDBREAKER/) -- [Voidtrain](https://store.steampowered.com/app/1159690/Voidtrain/) -- [VOIN](https://store.steampowered.com/app/2464530/VOIN/) -- [Warhammer 40,000: Darktide](https://store.steampowered.com/app/1361210/Warhammer_40000_Darktide/) -- [Warhammer: Vermintide 2](https://store.steampowered.com/app/552500/Warhammer_Vermintide_2/) -- [What Remains of Edith Finch](https://store.steampowered.com/app/501300/What_Remains_of_Edith_Finch/) -- [Witchfire](https://store.steampowered.com/app/3156770/Witchfire/) -- [Wolfenstein: The New Colossus](https://store.steampowered.com/app/612880/Wolfenstein_II_The_New_Colossus/) -- [Wolfenstein: The New Order](https://store.steampowered.com/app/201810/Wolfenstein_The_New_Order/) -- [Wolfenstein: The Old Blood](https://store.steampowered.com/app/350080/Wolfenstein_The_Old_Blood/) -- [Wolfenstein: Youngblood](https://store.steampowered.com/app/1056960/Wolfenstein_Youngblood/) -- [Zero Hour](https://store.steampowered.com/app/1359090/Zero_Hour/) -- [Ziggurat 2](https://store.steampowered.com/app/1159560/Ziggurat_2/) +### Sufficient Data Collected + +- A Story About My Uncle +- Abyssus +- Alien: Isolation +- American Truck Simulator +- Amnesia: A Machine for Pigs +- Amnesia: Rebirth +- Among the Sleep +- Apex Legends +- ARC Raiders +- Arena Breakout: Infinite +- ARK: Survival Ascended +- ARK: Survival Evolved +- Assassin's Creed Syndicate +- Assetto Corsa Competizione +- Atomic Heart +- Avatar: Frontiers of Pandora +- Avowed +- Back 4 Blood +- Battle Shapers +- Battlefield 1 +- Battlefield 3 +- Battlefield 4 +- Battlefield 6 / REDSEC +- Battlefield Hardline +- Battlefield V +- BDS Unknown UE Game +- BeamNG.drive +- BioShock +- BioShock Infinite +- BioShock Remastered +- Black Mesa +- Blair Witch +- Blood: Fresh Supply +- Borderlands 1 +- Borderlands 2 +- Borderlands 3 +- Borderlands 4 +- Borderlands: The Pre-Sequel +- BPM: Bullets Per Minute +- Bus Flipper Simulator +- Call of Duty 4: Modern Warfare +- Call of Duty: Black Ops 4 +- Call of Duty: Black Ops 6 +- Call of Duty: Black Ops Cold War +- Call of Duty: Black Ops II +- Call of Duty: Black Ops III +- Call of Duty: Black Ops III (Custom Client) +- Call of Duty: Modern Warfare (2019) +- Call of Duty: Modern Warfare 2 (2009) +- Call of Duty: Modern Warfare II (2022) +- Call of Duty: Modern Warfare III (2023) +- Call of Duty: WWII +- Car Mechanic Simulator 2018 +- Car Mechanic Simulator 2021 +- Chivalry: Medieval Warfare +- Close to the Sun +- Clustertruck +- Condemned: Criminal Origins +- Conundrum +- Cooking Simulator +- Counter-Strike 2 +- Counter-Strike: Source +- Crab Game +- Cry of Fear +- Crysis 1 Remastered +- Crysis 2 +- Crysis 3 +- CUFFBUST +- Cyberpunk 2077 +- Dark and Darker +- DayZ +- Dead Island 2 +- Deadlock +- Dear Esther +- Deathloop +- Deceit 2 +- Deep Rock Galactic +- Delta Force +- Deus Ex: Human Revolution +- Deus Ex: Mankind Divided +- DEVOUR +- Dinocop +- Dishonored +- Dishonored 2 +- Dishonored: Death of the Outsider +- Divinity: Original Sin +- DOOM 2016 +- DOOM Eternal +- Drive Beyond Horizons +- Dying Light +- Dying Light: The Beast +- eFootball +- ELDERBORN +- Enlisted +- Euro Truck Simulator 2 +- Exit 8 +- F.E.A.R. +- F.E.A.R. Extraction Point +- Fallout 3 +- Fallout 4 +- Fallout: New Vegas +- Far Cry +- Far Cry 2 +- Far Cry 3 +- Far Cry 3: Blood Dragon +- Far Cry 4 +- Far Cry 5 +- Far Cry 6 +- Far Cry: New Dawn +- Far Cry: Primal +- Firewatch +- Fishing Planet +- Fortnite +- Forza Horizon 4 +- Forza Horizon 5 +- FragPunk +- Garry's Mod +- Generation Zero +- Ghostrunner +- Ghostrunner 2 +- Ghostwire: Tokyo +- Gone Home +- Green Hell +- GTA III +- GTA IV +- GTA V +- GTA V Enhanced +- GTA: San Andreas +- GTA: Vice City +- GTFO +- Gunfire Reborn +- Gym Manager +- Half Sword +- Half-Life +- Half-Life 2 + Mods +- Halo 2 Anniversary (MCC) +- Halo: Infinite +- Hard Reset Redux +- Hell Let Loose +- High on Life +- HITMAN World of Assassination +- House Builder +- House Builder 2 +- House Flipper +- House Flipper 2 +- I Am Your Beast +- ICARUS +- Immortals of Aveum +- Indiana Jones and the Great Circle +- Internet Cafe Simulator +- Internet Cafe Simulator 2 +- Internet Cafe Simulator 2025 +- Journey +- Journey to the Savage Planet +- Jump Space +- Just Cause 2 +- Just Cause 3 +- Just Cause 4 +- Keep Digging +- Killing Floor +- Killing Floor 2 +- Killing Floor 3 +- Kingdom Come: Deliverance +- Layers of Fear (2023) +- Layers of Fear 2 +- League of Legends +- Left 4 Dead 2 +- Lethal Company +- Liftoff: FPV Drone Racing +- Liftoff: Micro Drones +- Manifold Garden +- Medieval Dynasty +- Metro 2033 +- Metro Exodus +- Metro: Last Light (delisted, see Redux) +- Minecraft +- Mirror's Edge +- Mirror's Edge Catalyst +- MiSide +- Momentum Mod +- MORDHAU +- Muck +- Mycopunk +- Neighbours from Hell +- Neon White +- No Man's Sky +- Outer Wilds +- Outlast +- Outlast 2 +- Overwatch 2 +- Pacific Drive +- Paint the Town Red +- Palworld +- Panicore +- PAYDAY 2 +- PAYDAY 3 +- PC Building Simulator +- PEAK +- Peaks of Yore +- Phasmophobia +- Planet Crafter +- Poppy Playtime +- Portal 2 +- PowerWash Simulator +- PowerWash Simulator 2 +- Prey (2017) +- Prison Escape Simulator +- Q.U.B.E. 2 +- R.E.P.O. +- RAGE 2 +- Rainbow Six Siege +- Ranch Simulator +- Ready or Not +- Red Dead Redemption 2 +- Remnant 2 +- Resident Evil 7: Biohazard +- Resident Evil Village +- Risk of Rain 2 +- Roboquest +- Rust +- RV There Yet? +- S.T.A.L.K.E.R. 2: Heart of Chornobyl +- Satisfactory +- Schedule I +- SCP: Nine-Tailed Fox +- SCP: Secret Laboratory +- SCUM +- Sea of Thieves +- Serious Sam +- Severed Steel +- Shadow Warrior +- Shadow Warrior 2 +- Shadow Warrior 3 +- Shady Knight +- Shark Attack Deathmatch 2 +- Slime Rancher +- Slime Rancher 2 +- Soma +- Sons of the Forest +- Species: Unknown +- Splitgate +- Static Dread +- Stranded Deep +- Stray +- Subnautica +- SUPERHOT +- SUPERHOT: MIND CONTROL DELETE +- Superliminal +- Supermarket Together +- SWAT 4 +- Tacoma +- Tales of Escape +- Team Fortress 2 +- Teardown +- The Beginner's Guide +- The Crew 2 +- The Darkness II +- The Elder Scrolls IV: Oblivion Remastered +- The Elder Scrolls V: Skyrim +- The Finals +- The Forest +- The Long Dark +- The Outer Worlds +- The Outer Worlds 2 +- The Outlast Trials +- The Stanley Parable +- The Stanley Parable: Ultra Deluxe +- The Talos Principle +- The Vanishing of Ethan Carter +- The Voidness +- The Witness +- Thief +- Thief Simulator +- Thief Simulator 2 +- Tiny Tina's Wonderlands +- Titanfall 2 +- Totally Unrealistic Shooter +- Trepang2 +- ULTRAKILL +- Valorant +- Vampire: The Masquerade - Bloodlines 2 +- Viewfinder +- Viscera Cleanup Detail +- Void Bastards +- VOIDBREAKER +- Voidtrain +- Warhammer 40,000: Darktide +- Warhammer: Vermintide 2 +- We Who Are About To Die +- What Remains of Edith Finch +- White Knuckle +- Wild Bastards +- Wolfenstein: The New Colossus +- Wolfenstein: The New Order +- Wolfenstein: The Old Blood +- Wolfenstein: Youngblood +- Zero Hour +- Ziggurat 2 + +### Other + +- Destiny 2 (Recorded footage is all-black.) +- Roblox (Recorded footage is all-black.) +- Split Fiction (Split-screen games are unsupported.) diff --git a/LICENSE b/LICENSE index a103e77..2819896 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Wayfarer Labs +Copyright (c) 2026 Overworld Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4c90129..b2e406d 100644 --- a/README.md +++ b/README.md @@ -11,26 +11,35 @@ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +> [!CAUTION] +> **PUBLIC SUBMISSIONS ARE CLOSED.** We are not accepting new data submissions and will not pay for recordings. Our backend rejects submissions from users without a pre-established agreement. The setup instructions below are for existing data collection partners only. +> +> Any public information indicating that we pay for submissions is out of date; if you can, we'd appreciate you letting the source know that the program has ended. + OWL Control is a high-performance Windows app that captures control data from games. These datasets are fundamental to training world models that power sophisticated robots and simulations. ## About We carefully log keyboard, mouse and gamepad inputs from the active game to a file synced with a mini video of the game. No other windows or control input is recorded. Any other window or input - including any microphone or camera - is not captured. -OWL Control is fully open-source, so anyone can verify its inner workings by reading the code or feeding this page's link to your favorite AI language model. The software is developed and enriched by a vibrant [community on Discord](https://discord.gg/ZgCWTGYf4E), and anyone is allowed to [contribute to the project](./CONTRIBUTING.md) +OWL Control is fully open-source, so anyone can verify its inner workings by reading the code or feeding this page's link to your favorite AI language model. Anyone is allowed to [contribute to the project](./CONTRIBUTING.md) ## System Requirements - Windows device capable of running games at 60 FPS. - Keyboard, mouse, trackball, trackpad, Wired/Wireless XBOX or Wired PS5 gamepads. PS4 controllers may be used with DS4Windows. - A reliable internet connection. Uploading may take a long time. -- Computer games! We provide [eligible games](./GAMES.md) to veteran players! +- Computer games! See the [eligible games list](./GAMES.md). ## Setup +> [!NOTE] +> The following setup steps are only relevant if you have a pre-existing data collection agreement with Overworld. Public submissions are not accepted. + Watch the [Walkthrough Video](https://vimeo.com/1134400699) or follow the steps below: -- [Download OWL Control installer](https://github.com/Wayfarer-Labs/owl-control/releases/latest). +- [Download OWL Control installer](https://github.com/Overworldai/owl-control/releases/latest). - Run the installer. - Launch the app from your desktop or Start menu. - Check the bottom right corner of your screen for the turquoise OWL control icon. The app may already be open. @@ -54,25 +63,13 @@ Watch the [Walkthrough Video](https://vimeo.com/1134400699) or follow the steps - If your game runs slowly while recording, lower settings in `Video Encoder`, or lower the game detail or resolution. - Recordings will be tracked in the app. Recordings ready to be uploaded are marked in yellow. - Recordings may be too short or not have enough activity to submit. These recordings are marked with red and tagged invalid. - - A message why they can't be accepted will appear. This information can be useful to share in [our Discord server](https://discord.gg/ZgCWTGYf4E). + - A message why they can't be accepted will appear. - You can review your recordings by clicking its number. A window will open showing the folder contents. - Non-video files in this folder can be opened in Notepad or other text editors. - Location of the entire recordings folder can be changed with the `Move` button to the right of `Upload Manager` - Upload recordings by hitting the `Upload Recordings` button. - If your connection is slow, try checking `Optimize for unreliable recordings`. -> [!TIP] -> -> For a LIMITED TIME, we are compensating per hour for game data under these circumstances: -> -> - You must be 18 years old, or older. -> - You must play a game on [the games list](./GAMES.md) -> - You must only play PvE or Co-op PvE (ie: gameplay against in-game, non-human opponents). We do not allow PvP recordings. -> - You must upload least 20 hours of footage. -> - You must record active and human play. No camping, bots, idling, etc. -> - You must use [Wise](https://wise.com/) or [Fiverr](https://www.fiverr.com/). We do not use other payment providers. If you need help setting up ask [our Discord community](https://discord.gg/ZgCWTGYf4E). -> - Due to legal and technical restrictions, residents of MENA countries, .PK, or .VN must use Fiverr. - ## Troubleshooting Software known to interfere with OWL Control: @@ -81,11 +78,11 @@ Software known to interfere with OWL Control: - RivaTuner Statistics Server - Often installed with MSI Afterburner. Sometimes causes conflicts. - Antivirus Software - OWL Control is NOT malware. If you experience problems, you are safe to lower antivirus on OWL Control while problem solving. -If you run into other difficulties, write down what happened and take screenshots using [Windows' snipper tool](https://support.microsoft.com/en-us/windows/use-snipping-tool-to-capture-screenshots-00246869-1843-655f-f220-97299b865f6b), then [speak to us on our Discord server](https://discord.gg/ZgCWTGYf4E) or [open an issue on GitHub](https://github.com/Wayfarer-Labs/owl-control/issues). +If you run into other difficulties, write down what happened and take screenshots using [Windows' snipper tool](https://support.microsoft.com/en-us/windows/use-snipping-tool-to-capture-screenshots-00246869-1843-655f-f220-97299b865f6b), then [open an issue on GitHub](https://github.com/Overworldai/owl-control/issues). > [!NOTE] > -> You may get an `.invalid` recording that is marked as Too Long, is longer than 10 minutes, or larger than 150-200MB. If this happens, Please [speak to us on our Discord server](https://discord.gg/ZgCWTGYf4E) or [open an issue on GitHub](https://github.com/Wayfarer-Labs/owl-control/issues). +> You may get an `.invalid` recording that is marked as Too Long, is longer than 10 minutes, or larger than 150-200MB. If this happens, please [open an issue on GitHub](https://github.com/Overworldai/owl-control/issues). ## Contributing to AI Research @@ -106,15 +103,14 @@ If you're interested in the technical details or want to contribute, please visi | Need Help? | Where to Go | | :--------------------: | :------------------------------------------------------------------------------------------- | -| πŸ› **Issues or Bugs?** | Report them on our [GitHub Issues](https://github.com/Wayfarer-Labs/owl-control/issues) page | -| ❓ **Questions?** | Visit our [GitHub Issues](https://github.com/Wayfarer-Labs/owl-control/issues) page | -| **πŸ’¬Discord** | [Discord Community](https://discord.gg/dX4HW9Pt7Z) | +| πŸ› **Issues or Bugs?** | Report them on our [GitHub Issues](https://github.com/Overworldai/owl-control/issues) page | +| ❓ **Questions?** | Visit our [GitHub Issues](https://github.com/Overworldai/owl-control/issues) page |
-# OWL Control is a project by [Wayfarer Labs](https://wayfarerlabs.ai) +# OWL Control is a project by [Overworld](https://wayfarerlabs.ai) Building open datasets for AI research
-2025 Wayfarer Labs
+2025 Overworld
Trademarks `` copyright respective owners where indicated .
diff --git a/crates/constants/src/lib.rs b/crates/constants/src/lib.rs index d2280e1..3b523de 100644 --- a/crates/constants/src/lib.rs +++ b/crates/constants/src/lib.rs @@ -1,7 +1,7 @@ use std::time::Duration; pub mod encoding; -pub mod supported_games; +pub mod unsupported_games; pub const FPS: u32 = 60; pub const RECORDING_WIDTH: u32 = 1280; @@ -58,7 +58,7 @@ pub const PLAY_TIME_SAVE_INTERVAL: Duration = if PLAY_TIME_TESTING { }; /// GitHub organization -pub const GH_ORG: &str = "Wayfarer-Labs"; +pub const GH_ORG: &str = "Overworldai"; /// GitHub repository pub const GH_REPO: &str = "owl-control"; diff --git a/crates/constants/src/supported_games.json b/crates/constants/src/supported_games.json index faea6be..f173222 100644 --- a/crates/constants/src/supported_games.json +++ b/crates/constants/src/supported_games.json @@ -1,1296 +1,301 @@ [ - { - "game": "A Story About My Uncle", - "url": "https://store.steampowered.com/app/278360/A_Story_About_My_Uncle/", - "binaries": [ - "asamu-win32-shipping" - ] - }, { "game": "Abyssus", "url": "https://store.steampowered.com/app/1721110/Abyssus/", - "binaries": [ - "abyssus", - "rgame" - ] - }, - { - "game": "Alien: Isolation", - "url": "https://store.steampowered.com/app/214490/Alien_Isolation/", - "binaries": [ - "ai" - ] + "binaries": ["abyssus", "rgame"] }, { "game": "Amenti", "url": "https://store.steampowered.com/app/3292260/Amenti/", - "binaries": [ - "amenti" - ] - }, - { - "game": "Apex Legends", - "url": "https://store.steampowered.com/app/1172470/Apex_Legends/", - "binaries": [ - "r5apex", - "r5apex_dx12" - ] + "binaries": ["amenti"] }, { "game": "ARMA 3", "url": "https://store.steampowered.com/app/107410/Arma_3/", - "binaries": [ - "arma3", - "arma3_x64" - ] - }, - { - "game": "Atomic Heart", - "url": "https://store.steampowered.com/app/668580/Atomic_Heart/", - "binaries": [ - "atomicheart", - "atomicheart-win64-shipping" - ] - }, - { - "game": "Avowed", - "url": "https://store.steampowered.com/app/2457220/Avowed/", - "binaries": [ - "avowed", - "avowed-win64-shipping" - ] - }, - { - "game": "Back 4 Blood", - "url": "https://store.steampowered.com/app/924970/Back_4_Blood/", - "binaries": [ - "back4blood" - ] - }, - { - "game": "Battlefield 1", - "url": "https://store.steampowered.com/app/1238840/Battlefield_1/", - "binaries": [ - "bf1" - ] - }, - { - "game": "Battlefield 3", - "url": "https://store.steampowered.com/app/1238820/Battlefield_3/", - "binaries": [ - "bf3" - ] - }, - { - "game": "Battlefield 4", - "url": "https://store.steampowered.com/app/1238860/Battlefield_4/", - "binaries": [ - "bf4" - ] - }, - { - "game": "Battlefield 6 / REDSEC", - "url": "https://store.steampowered.com/app/2807960/Battlefield_6/", - "binaries": [ - "bf6" - ] + "binaries": ["arma3", "arma3_x64"] }, { "game": "Battlefield Hardline", "url": "https://store.steampowered.com/app/1238880/Battlefield_Hardline/", - "binaries": [ - "bfh" - ] - }, - { - "game": "Battlefield V", - "url": "https://store.steampowered.com/app/1238810/Battlefield_V/", - "binaries": [ - "bfv" - ] - }, - { - "game": "BioShock Infinite", - "url": "https://store.steampowered.com/app/8870/BioShock_Infinite/", - "binaries": [ - "bioshockinfinite", - "shippingpc-xgame" - ] + "binaries": ["bfh"] }, { "game": "Blacktail", "url": "https://store.steampowered.com/app/1532690/BLACKTAIL/", - "binaries": [ - "blacktail", - "blacktail-win64-shipping" - ] + "binaries": ["blacktail", "blacktail-win64-shipping"] }, { "game": "Blair Witch", "url": "https://store.steampowered.com/app/1092660/Blair_Witch/", - "binaries": [ - "blairwitch" - ] - }, - { - "game": "Borderlands 1", - "url": "https://store.steampowered.com/app/729040/Borderlands_Game_of_the_Year_Enhanced/", - "binaries": [ - "borderlands", - "borderlandsgoty" - ] - }, - { - "game": "Borderlands 2", - "url": "https://store.steampowered.com/app/49520/Borderlands_2/", - "binaries": [ - "borderlands2" - ] - }, - { - "game": "Borderlands 3", - "url": "https://store.steampowered.com/app/397540/Borderlands_3/", - "binaries": [ - "borderlands3" - ] - }, - { - "game": "Borderlands 4", - "url": "https://store.steampowered.com/app/1285190/Borderlands_4", - "binaries": [ - "borderlands4" - ] - }, - { - "game": "Borderlands: The Pre-Sequel", - "url": "https://store.steampowered.com/app/261640/Borderlands_The_PreSequel/", - "binaries": [ - "borderlandspresequel" - ] - }, - { - "game": "BPM: Bullets Per Minute", - "url": "https://store.steampowered.com/app/1286350/BPM_BULLETS_PER_MINUTE/", - "binaries": [ - "bpmgame", - "bpmgame-win64-shipping" - ] + "binaries": ["blairwitch"] }, { "game": "Call of Duty: Advanced Warfare", "url": "https://store.steampowered.com/app/209650/Call_of_Duty_Advanced_Warfare", - "binaries": [ - "s1_sp64_ship", - "s1_mp64_ship" - ] - }, - { - "game": "Call of Duty: Black Ops 6", - "url": "https://store.steampowered.com/app/2933620/Call_of_Duty_Black_Ops_6/", - "binaries": [ - "sp24-cod", - "mp24-cod", - "cod", - "cod24-cod" - ] - }, - { - "game": "Call of Duty: Black Ops 7", - "url": "https://store.steampowered.com/app/3606480/Call_of_Duty_Black_Ops_7/", - "binaries": [ - "cod" - ] - }, - { - "game": "Call of Duty: Black Ops Cold War", - "url": "https://store.steampowered.com/app/1985810/Call_of_Duty_Black_Ops_Cold_War/", - "binaries": [ - "blackopscoldwar" - ] - }, - { - "game": "Call of Duty: Black Ops III", - "url": "https://store.steampowered.com/app/311210/Call_of_Duty_Black_Ops_III/", - "binaries": [ - "blackops3" - ] + "binaries": ["s1_sp64_ship", "s1_mp64_ship"] }, { "game": "Call of Duty: Infinite Warfare", "url": "https://store.steampowered.com/app/292730/Call_of_Duty_Infinite_Warfare/", - "binaries": [ - "iw7_ship" - ] - }, - { - "game": "Call of Duty: Modern Warfare (2019)", - "url": "https://store.steampowered.com/app/2000950/Call_of_Duty_Modern_Warfare/", - "binaries": [ - "modernwarfare" - ] - }, - { - "game": "Call of Duty: Modern Warfare II (2022)", - "url": "https://store.steampowered.com/app/3595230/Call_of_Duty_Modern_Warfare_II/", - "binaries": [ - "cod", - "sp22-cod" - ] - }, - { - "game": "Call of Duty: Modern Warfare III (2023)", - "url": "https://store.steampowered.com/app/3595270/Call_of_Duty_Modern_Warfare_III/", - "binaries": [ - "cod23-cod", - "sp23-cod" - ] + "binaries": ["iw7_ship"] }, { "game": "Call of Duty: Vanguard", "url": "https://store.steampowered.com/app/1985820/Call_of_Duty_Vanguard/", - "binaries": [ - "vanguard" - ] + "binaries": ["vanguard"] }, { "game": "Call of Duty: WWII", "url": "https://store.steampowered.com/app/476600/Call_of_Duty_WWII/", - "binaries": [ - "s2_sp64_ship", - "s2_mp64_ship" - ] + "binaries": ["s2_sp64_ship", "s2_mp64_ship"] }, { "game": "Close to the Sun", "url": "https://store.steampowered.com/app/968870/Close_to_the_Sun/", - "binaries": [ - "ctts-win64-shipping" - ] + "binaries": ["ctts-win64-shipping"] }, { "game": "Conundrum", "url": "https://store.steampowered.com/app/1744140/Conundrum/", - "binaries": [ - "trygame-win32-shipping", - "conundrum" - ] - }, - { - "game": "Crysis 1 Remastered", - "url": "https://store.steampowered.com/app/1715130/Crysis_Remastered/", - "binaries": [ - "crysis", - "crysis64", - "crysisremastered" - ] - }, - { - "game": "Crysis 2", - "url": "https://store.steampowered.com/app/108800/Crysis_2__Maximum_Edition/", - "binaries": [ - "crysis2" - ] - }, - { - "game": "Crysis 3", - "url": "https://store.steampowered.com/app/2096610/Crysis_3_Remastered/", - "binaries": [ - "crysis3" - ] + "binaries": ["trygame-win32-shipping", "conundrum"] }, { "game": "Dark Hours", "url": "https://store.steampowered.com/app/2208570/Dark_Hours/", - "binaries": [ - "dark hours", - "darkhours-win64-shipping" - ] - }, - { - "game": "Dead Island 2", - "url": "https://store.steampowered.com/app/934700/Dead_Island_2/", - "binaries": [ - "deadisland2-win64-shipping", - "deadisland", - "deadisland-win64-shipping" - ] + "binaries": ["dark hours", "darkhours-win64-shipping"] }, { "game": "Deadzone: Rogue", "url": "https://store.steampowered.com/app/3228590/Deadzone_Rogue", - "binaries": [ - "deadzonesteam" - ] - }, - { - "game": "Deathloop", - "url": "https://store.steampowered.com/app/1252330/DEATHLOOP/", - "binaries": [ - "deathloop" - ] - }, - { - "game": "Delta Force", - "url": "https://store.steampowered.com/app/2507950/Delta_Force/", - "binaries": [ - "deltaforceclient-win64-shipping", - "deltaforceclient", - "bhdclient" - ] - }, - { - "game": "Dishonored 2", - "url": "https://store.steampowered.com/app/403640/Dishonored_2/", - "binaries": [ - "dishonored2", - "dishonored2_x64" - ] - }, - { - "game": "Dishonored: Death of the Outsider", - "url": "https://store.steampowered.com/app/614570/Dishonored_Death_of_the_Outsider/", - "binaries": [ - "dishonored_do" - ] - }, - { - "game": "Dishonored", - "url": "https://store.steampowered.com/app/205100/Dishonored/", - "binaries": [ - "dishonored" - ] - }, - { - "game": "DOOM 2016", - "url": "https://store.steampowered.com/app/379720/DOOM/", - "binaries": [ - "doomx64", - "doomx64vk" - ] - }, - { - "game": "DOOM Eternal", - "url": "https://store.steampowered.com/app/782330/DOOM_Eternal/", - "binaries": [ - "doometernalx64vk" - ] + "binaries": ["deadzonesteam"] }, { "game": "Earthfall", "url": "https://store.steampowered.com/app/415590/Earthfall/", - "binaries": [ - "earthfall" - ] - }, - { - "game": "ELDERBORN", - "url": "https://store.steampowered.com/app/727850/ELDERBORN/", - "binaries": [ - "elderborn" - ] - }, - { - "game": "Enlisted", - "url": "https://store.steampowered.com/app/2051620/Enlisted/", - "binaries": [ - "enlisted", - "enlisted-min-cpu" - ] + "binaries": ["earthfall"] }, { "game": "Escape from Tarkov", "url": "https://store.steampowered.com/app/3932890/Escape_from_Tarkov/", - "binaries": [ - "escapefromtarkov", - "escapefromtarkov_be" - ] + "binaries": ["escapefromtarkov", "escapefromtarkov_be"] }, { "game": "Everyone's Gone to the Rapture", "url": "https://store.steampowered.com/app/417880/Everybodys_Gone_to_the_Rapture/", - "binaries": [ - "rapture_release" - ] - }, - { - "game": "Fallout 4", - "url": "https://store.steampowered.com/app/377160/Fallout_4/", - "binaries": [ - "fallout4" - ] - }, - { - "game": "Far Cry 3: Blood Dragon", - "url": "https://store.steampowered.com/app/233270/Far_Cry_3__Blood_Dragon/", - "binaries": [ - "fc3_blooddragon", - "fc3_blooddragon_d3d11" - ] - }, - { - "game": "Far Cry 3", - "url": "https://store.steampowered.com/app/220240/Far_Cry_3/", - "binaries": [ - "farcry3", - "farcry3_d3d11" - ] - }, - { - "game": "Far Cry 4", - "url": "https://store.steampowered.com/app/298110/Far_Cry_4/", - "binaries": [ - "farcry4" - ] - }, - { - "game": "Far Cry 5", - "url": "https://store.steampowered.com/app/552520/Far_Cry_5/", - "binaries": [ - "farcry5" - ] - }, - { - "game": "Far Cry 6", - "url": "https://store.steampowered.com/app/2369390/Far_Cry_6/", - "binaries": [ - "farcry6" - ] - }, - { - "game": "Far Cry: New Dawn", - "url": "https://store.steampowered.com/app/939960/Far_Cry_New_Dawn/", - "binaries": [ - "farcrynewdawn" - ] - }, - { - "game": "Far Cry: Primal", - "url": "https://store.steampowered.com/app/371660/Far_Cry_Primal/", - "binaries": [ - "fcprimal" - ] - }, - { - "game": "Firewatch", - "url": "https://store.steampowered.com/app/383870/Firewatch/", - "binaries": [ - "firewatch" - ] + "binaries": ["rapture_release"] }, { "game": "Fobia - St. Dinfna Hotel", "url": "https://store.steampowered.com/app/1298140/Fobia__St_Dinfna_Hotel/", - "binaries": [ - "fobia-win64-shipping" - ] - }, - { - "game": "Generation Zero", - "url": "https://store.steampowered.com/app/704270/Generation_Zero/", - "binaries": [ - "generationzero_f" - ] + "binaries": ["fobia-win64-shipping"] }, { "game": "Ghost Watchers", "url": "https://store.steampowered.com/app/1850740/Ghost_Watchers/", - "binaries": [ - "ghost watchers" - ] - }, - { - "game": "Ghostrunner 2", - "url": "https://store.steampowered.com/app/2144740/Ghostrunner_2", - "binaries": [ - "ghostrunner2", - "ghostrunner2-win64-shipping" - ] - }, - { - "game": "Ghostrunner", - "url": "https://store.steampowered.com/app/1139900/Ghostrunner/", - "binaries": [ - "ghostrunner", - "ghostrunner-win64-shipping" - ] - }, - { - "game": "Ghostwire: Tokyo", - "url": "https://store.steampowered.com/app/1475810/Ghostwire_Tokyo/", - "binaries": [ - "gwt" - ] - }, - { - "game": "Green Hell", - "url": "https://store.steampowered.com/app/815370/Green_Hell/", - "binaries": [ - "gh" - ] - }, - { - "game": "GTFO", - "url": "https://store.steampowered.com/app/493520/GTFO/", - "binaries": [ - "gtfo" - ] - }, - { - "game": "Halo 2 Anniversary (MCC)", - "url": "https://store.steampowered.com/app/976730/Halo_The_Master_Chief_Collection/", - "binaries": [ - "mcc-win64-shipping", - "mccwinstore-win64-shipping" - ] - }, - { - "game": "Halo 3 (MCC)", - "url": "https://store.steampowered.com/app/976730/Halo_The_Master_Chief_Collection/", - "binaries": [ - "mcc-win64-shipping", - "mccwinstore-win64-shipping" - ] - }, - { - "game": "Halo: Combat Evolved Anniversary (MCC)", - "url": "https://store.steampowered.com/app/976730/Halo_The_Master_Chief_Collection/", - "binaries": [ - "mcc-win64-shipping", - "mccwinstore-win64-shipping" - ] + "binaries": ["ghost watchers"] }, { "game": "Halo: Infinite", "url": "https://store.steampowered.com/app/1240440/Halo_Infinite/", - "binaries": [ - "haloinfinite" - ] + "binaries": ["haloinfinite"] }, { "game": "Hard Reset Redux", "url": "https://store.steampowered.com/app/407810/Hard_Reset_Redux/", - "binaries": [ - "hr.win32", - "hr.x64", - "hr" - ] + "binaries": ["hr.win32", "hr.x64", "hr"] }, { "game": "Hardspace: Shipbreaker", "url": "https://store.steampowered.com/app/1161580/Hardspace_Shipbreaker", - "binaries": [ - "shipbreaker" - ] - }, - { - "game": "Hell Let Loose", - "url": "https://store.steampowered.com/app/686810/Hell_Let_Loose/", - "binaries": [ - "hll", - "hll-win64-shipping", - "hllepicgamesstore" - ] - }, - { - "game": "High on Life", - "url": "https://store.steampowered.com/app/1583230/High_On_Life", - "binaries": [ - "oregon", - "oregon-win64-shipping" - ] - }, - { - "game": "Home Sweet Home 2", - "url": "https://store.steampowered.com/app/1098940/Home_Sweet_Home_EP2/", - "binaries": [ - "homesweethome2", - "homesweethome2-win64-shipping" - ] - }, - { - "game": "Home Sweet Home", - "url": "https://store.steampowered.com/app/617160/Home_Sweet_Home/", - "binaries": [ - "homesweethome", - "homesweethome-win32-shipping" - ] - }, - { - "game": "ICARUS", - "url": "https://store.steampowered.com/app/1149460/ICARUS/", - "binaries": [ - "icarus", - "icarus-win64-shipping" - ] - }, - { - "game": "Immortals of Aveum", - "url": "https://store.steampowered.com/app/2009100/Immortals_of_Aveum/", - "binaries": [ - "immortalsofaveum", - "immortalsofaveum-win64-shipping" - ] - }, - { - "game": "In Sound Mind", - "url": "https://store.steampowered.com/app/1119980/In_Sound_Mind/", - "binaries": [ - "in sound mind" - ] - }, - { - "game": "Indiana Jones and the Great Circle", - "url": "https://store.steampowered.com/app/2677660/Indiana_Jones_and_the_Great_Circle/", - "binaries": [ - "thegreatcircle" - ] - }, - { - "game": "Journey to the Savage Planet", - "url": "https://store.steampowered.com/app/973810/Journey_To_The_Savage_Planet/", - "binaries": [ - "towers", - "towers-win64-shipping" - ] - }, - { - "game": "Keep Digging", - "url": "https://store.steampowered.com/app/3585800/Keep_Digging", - "binaries": [ - "keepdigging", - "keepdigging-win64-shipping" - ] - }, - { - "game": "Killing Floor 2", - "url": "https://store.steampowered.com/app/232090/Killing_Floor_2/", - "binaries": [ - "kfgame" - ] - }, - { - "game": "Kingdom Come: Deliverance II", - "url": "https://store.steampowered.com/app/1771300/Kingdom_Come_Deliverance_II/", - "binaries": [ - "kingdomcome" - ] - }, - { - "game": "Layers of Fear 2", - "url": "https://store.steampowered.com/app/1029890/Layers_of_Fear_2_2019", - "binaries": [ - "lof2", - "lof2-win64-shipping" - ] - }, - { - "game": "Layers of Fear", - "url": "https://store.steampowered.com/app/391720/Layers_of_Fear/", - "binaries": [ - "layers of fear", - "layers of fearsub" - ] - }, - { - "game": "Madison", - "url": "https://store.steampowered.com/app/1670870/MADiSON/", - "binaries": [ - "madison" - ] - }, - { - "game": "Medieval Dynasty", - "url": "https://store.steampowered.com/app/1129580/Medieval_Dynasty/", - "binaries": [ - "medieval_dynasty", - "medieval_dynasty-win64-shipping" - ] + "binaries": ["shipbreaker"] }, { - "game": "METAL EDEN", - "url": "https://store.steampowered.com/app/990380/METAL_EDEN/", - "binaries": [ - "metaleden", - "metaleden-win64-shipping" - ] + "game": "Hell Let Loose", + "url": "https://store.steampowered.com/app/686810/Hell_Let_Loose/", + "binaries": ["hll", "hll-win64-shipping", "hllepicgamesstore"] }, { - "game": "Metro Exodus", - "url": "https://store.steampowered.com/app/412020/Metro_Exodus/", - "binaries": [ - "metroexodus" - ] + "game": "Home Sweet Home 2", + "url": "https://store.steampowered.com/app/1098940/Home_Sweet_Home_EP2/", + "binaries": ["homesweethome2", "homesweethome2-win64-shipping"] }, { - "game": "Metro: Last Light (delisted, see Redux)", - "url": "https://store.steampowered.com/app/43160/Metro_Last_Light/", - "binaries": [ - "metroll" - ] + "game": "Home Sweet Home", + "url": "https://store.steampowered.com/app/617160/Home_Sweet_Home/", + "binaries": ["homesweethome", "homesweethome-win32-shipping"] }, { - "game": "Metro: Last Light Redux", - "url": "https://store.steampowered.com/app/287390/Metro_Last_Light_Redux/", - "binaries": [ - "metro" - ] + "game": "Immortals of Aveum", + "url": "https://store.steampowered.com/app/2009100/Immortals_of_Aveum/", + "binaries": ["immortalsofaveum", "immortalsofaveum-win64-shipping"] }, { - "game": "Mirror's Edge Catalyst", - "url": "https://store.steampowered.com/app/1233570/Mirrors_Edge_Catalyst/", - "binaries": [ - "mirrorsedgecatalyst" - ] + "game": "In Sound Mind", + "url": "https://store.steampowered.com/app/1119980/In_Sound_Mind/", + "binaries": ["in sound mind"] }, { - "game": "Mirror's Edge", - "url": "https://store.steampowered.com/app/17410/Mirrors_Edge/", - "binaries": [ - "mirrorsedge" - ] + "game": "Layers of Fear 2", + "url": "https://store.steampowered.com/app/1029890/Layers_of_Fear_2_2019", + "binaries": ["lof2", "lof2-win64-shipping"] }, { - "game": "MORDHAU", - "url": "https://store.steampowered.com/app/629760/MORDHAU/", - "binaries": [ - "mordhau", - "mordhau-win64-shipping" - ] + "game": "Layers of Fear", + "url": "https://store.steampowered.com/app/391720/Layers_of_Fear/", + "binaries": ["layers of fear", "layers of fearsub"] }, { - "game": "Neon White", - "url": "https://store.steampowered.com/app/1533420/Neon_White/", - "binaries": [ - "neon white" - ] + "game": "Madison", + "url": "https://store.steampowered.com/app/1670870/MADiSON/", + "binaries": ["madison"] }, { - "game": "No Man's Sky", - "url": "https://store.steampowered.com/app/275850/No_Mans_Sky", - "binaries": [ - "nms" - ] + "game": "METAL EDEN", + "url": "https://store.steampowered.com/app/990380/METAL_EDEN/", + "binaries": ["metaleden", "metaleden-win64-shipping"] }, { "game": "Observer: System Redux", "url": "https://store.steampowered.com/app/1386900/Observer_System_Redux/", - "binaries": [ - "observersystemredux" - ] - }, - { - "game": "Outer Wilds", - "url": "https://store.steampowered.com/app/753640/Outer_Wilds/", - "binaries": [ - "outerwilds" - ] + "binaries": ["observersystemredux"] }, { "game": "The Outer Worlds 2", "url": "https://store.steampowered.com/app/1449110/The_Outer_Worlds_2/", - "binaries": [ - "theouterworlds2", - "theouterworlds2-win64-shipping" - ] - }, - { - "game": "The Outer Worlds", - "url": "https://store.steampowered.com/app/578650/The_Outer_Worlds/", - "binaries": [ - "theouterworlds", - "indiana-win64-shipping", - "indianaepicgamestore", - "indianawindowsstore" - ] - }, - { - "game": "Outlast 2", - "url": "https://store.steampowered.com/app/414700/Outlast_2/", - "binaries": [ - "outlast2" - ] + "binaries": ["theouterworlds2", "theouterworlds2-win64-shipping"] }, { "game": "Painkiller 2025", "url": "https://store.steampowered.com/app/2300120/Painkiller/", - "binaries": [ - "painkiller", - "painkiller-win64-shipping" - ] + "binaries": ["painkiller", "painkiller-win64-shipping"] }, { "game": "Painkiller Hell & Damnation", "url": "https://store.steampowered.com/app/214870/Painkiller_Hell__Damnation/", - "binaries": [ - "pkhdgame-win32-shipping" - ] + "binaries": ["pkhdgame-win32-shipping"] }, { "game": "Panicore", "url": "https://store.steampowered.com/app/2695940/PANICORE/", - "binaries": [ - "panicore", - "panicore-win64-shipping" - ] - }, - { - "game": "PAYDAY 2", - "url": "https://store.steampowered.com/app/218620/PAYDAY_2/", - "binaries": [ - "payday2_win32_release" - ] + "binaries": ["panicore", "panicore-win64-shipping"] }, { "game": "PAYDAY 3", "url": "https://store.steampowered.com/app/1272080/PAYDAY_3/", - "binaries": [ - "payday3client", - "payday3client-win64-shipping" - ] - }, - { - "game": "Poppy Playtime", - "url": "https://store.steampowered.com/app/1721470/Poppy_Playtime/", - "binaries": [ - "poppy_playtime", - "poppy_playtime-win64-shipping", - "ch4_pro" - ] - }, - { - "game": "Prey (2017)", - "url": "https://store.steampowered.com/app/480490/Prey/", - "binaries": [ - "prey" - ] - }, - { - "game": "Q.U.B.E. 2", - "url": "https://store.steampowered.com/app/359100/QUBE_2/", - "binaries": [ - "qube", - "qube-win64-shipping" - ] - }, - { - "game": "Q.U.B.E.", - "url": "https://store.steampowered.com/app/1564220/QUBE_10th_Anniversary/", - "binaries": [ - "qube", - "qube-win64-shipping" - ] - }, - { - "game": "RAGE 2", - "url": "https://store.steampowered.com/app/548570/RAGE_2/", - "binaries": [ - "rage2" - ] + "binaries": ["payday3client", "payday3client-win64-shipping"] }, { "game": "Ready or Not", "url": "https://store.steampowered.com/app/1144200/Ready_or_Not/", "binaries": [ "readyornot", - "readyornotsteam-win64-shipping" - ] - }, - { - "game": "Resident Evil 7: Biohazard", - "url": "https://store.steampowered.com/app/418370/RESIDENT_EVIL_7_biohazard/", - "binaries": [ - "re7" - ] - }, - { - "game": "Resident Evil Village", - "url": "https://store.steampowered.com/app/1196590/Resident_Evil_Village/", - "binaries": [ - "re8" + "readyornotsteam-win64-shipping", + "readyornotxboxpc-wingdk-shipping" ] }, { "game": "Riven", "url": "https://store.steampowered.com/app/1712350/Riven/", - "binaries": [ - "riven", - "riven-win64-shipping" - ] + "binaries": ["riven", "riven-win64-shipping"] }, { "game": "Salt 2", "url": "https://store.steampowered.com/app/1574900/Salt_2_Shores_of_Gold/", - "binaries": [ - "salt2" - ] + "binaries": ["salt2"] }, { "game": "SCP: 5K", "url": "https://store.steampowered.com/app/872670/SCP_5K/", - "binaries": [ - "pandemic" - ] - }, - { - "game": "Sea of Thieves", - "url": "https://store.steampowered.com/app/1172620/Sea_of_Thieves_2025_Edition/", - "binaries": [ - "sotgame", - "seaofthieves" - ] - }, - { - "game": "Severed Steel", - "url": "https://store.steampowered.com/app/1227690/Severed_Steel/", - "binaries": [ - "severedsteel", - "thankyouverycool-win64-shipping" - ] - }, - { - "game": "Shadow Warrior 2", - "url": "https://store.steampowered.com/app/324800/Shadow_Warrior_2/", - "binaries": [ - "shadowwarrior2" - ] + "binaries": ["pandemic"] }, { "game": "Shadow Warrior 3", "url": "https://store.steampowered.com/app/1036890/Shadow_Warrior_3_Definitive_Edition", - "binaries": [ - "sw3" - ] - }, - { - "game": "Slime Rancher 2", - "url": "https://store.steampowered.com/app/1657630/Slime_Rancher_2/", - "binaries": [ - "slimerancher2" - ] - }, - { - "game": "Slime Rancher", - "url": "https://store.steampowered.com/app/433340/Slime_Rancher/", - "binaries": [ - "slimerancher" - ] + "binaries": ["sw3"] }, { "game": "Soma", "url": "https://store.steampowered.com/app/282140/SOMA/", - "binaries": [ - "soma", - "soma_nosteam" - ] - }, - { - "game": "Sons of the Forest", - "url": "https://store.steampowered.com/app/1326470/Sons_Of_The_Forest", - "binaries": [ - "sonsoftheforest" - ] - }, - { - "game": "Splitgate", - "url": "https://store.steampowered.com/app/677620/Splitgate/", - "binaries": [ - "portalwars", - "portalwars-win64-shipping" - ] + "binaries": ["soma", "soma_nosteam"] }, { "game": "Squad", "url": "https://store.steampowered.com/app/393380/Squad/", - "binaries": [ - "squadgame" - ] - }, - { - "game": "Subnautica", - "url": "https://store.steampowered.com/app/264710/Subnautica/", - "binaries": [ - "subnautica", - "subnautica32" - ] - }, - { - "game": "Superliminal", - "url": "https://store.steampowered.com/app/1049410/Superliminal/", - "binaries": [ - "superliminalsteam", - "superliminal" - ] + "binaries": ["squadgame"] }, { "game": "Tacoma", "url": "https://store.steampowered.com/app/343860/Tacoma/", - "binaries": [ - "tacoma" - ] - }, - { - "game": "Teardown", - "url": "https://store.steampowered.com/app/1167630/Teardown", - "binaries": [ - "teardown" - ] + "binaries": ["tacoma"] }, { "game": "The Beast Inside", "url": "https://store.steampowered.com/app/792300/The_Beast_Inside/", - "binaries": [ - "thebeastinside", - "thebeastinside-win64-shipping" - ] + "binaries": ["thebeastinside", "thebeastinside-win64-shipping"] }, { "game": "The Darkness II", "url": "https://store.steampowered.com/app/67370/The_Darkness_II/", - "binaries": [ - "darknessii" - ] - }, - { - "game": "The Elder Scrolls IV: Oblivion Remastered", - "url": "https://store.steampowered.com/app/2623190/The_Elder_Scrolls_IV_Oblivion_Remastered/", - "binaries": [ - "oblivionremastered", - "oblivionremastered-win64-shipping" - ] - }, - { - "game": "The Elder Scrolls V: Skyrim", - "url": "https://store.steampowered.com/app/489830/The_Elder_Scrolls_V_Skyrim_Special_Edition/", - "binaries": [ - "skyrimse", - "tesv" - ] - }, - { - "game": "The Forest", - "url": "https://store.steampowered.com/app/242760/The_Forest/", - "binaries": [ - "theforest", - "theforest32" - ] + "binaries": ["darknessii"] }, { "game": "The Lightkeeper", "url": "https://store.steampowered.com/app/3612850/The_Lightkeeper/", - "binaries": [ - "thelightkeeper", - "thelightkeeper-win64-shipping" - ] - }, - { - "game": "The Long Dark", - "url": "https://store.steampowered.com/app/305620/The_Long_Dark/", - "binaries": [ - "tld" - ] - }, - { - "game": "The Outlast Trials", - "url": "https://store.steampowered.com/app/1304930/The_Outlast_Trials", - "binaries": [ - "totclient", - "totclient-win64-shipping" - ] - }, - { - "game": "The Stanley Parable: Ultra Deluxe", - "url": "https://store.steampowered.com/app/1703340/The_Stanley_Parable_Ultra_Deluxe/", - "binaries": [ - "the stanley parable ultra deluxe" - ] + "binaries": ["thelightkeeper", "thelightkeeper-win64-shipping"] }, { "game": "The Stanley Parable", "url": "https://store.steampowered.com/app/221910/The_Stanley_Parable/", - "binaries": [ - "stanley" - ] + "binaries": ["stanley"] }, { "game": "The Talos Principle 2", "url": "https://store.steampowered.com/app/835960/The_Talos_Principle_2/", - "binaries": [ - "talos2", - "talos2-win64-shipping" - ] - }, - { - "game": "The Talos Principle", - "url": "https://store.steampowered.com/app/257510/The_Talos_Principle/", - "binaries": [ - "talos", - "talos_unrestricted" - ] - }, - { - "game": "The Vanishing of Ethan Carter", - "url": "https://store.steampowered.com/app/258520/The_Vanishing_of_Ethan_Carter", - "binaries": [ - "astronautsgame-win32-shipping", - "astronautsgame-win64-shipping", - "ethancarter" - ] + "binaries": ["talos2", "talos2-win64-shipping"] }, { "game": "The Witness", "url": "https://store.steampowered.com/app/210970/The_Witness/", - "binaries": [ - "witness64_d3d11", - "witness_d3d11" - ] - }, - { - "game": "Thief Simulator", - "url": "https://store.steampowered.com/app/704850/Thief_Simulator/", - "binaries": [ - "thief simulator", - "thief" - ] - }, - { - "game": "Thief", - "url": "https://store.steampowered.com/app/239160/Thief/", - "binaries": [ - "shipping-thiefgame" - ] - }, - { - "game": "Tiny Tina's Wonderlands", - "url": "https://store.steampowered.com/app/1286680/Tiny_Tinas_Wonderlands/", - "binaries": [ - "wonderlands" - ] - }, - { - "game": "Titanfall 2", - "url": "https://store.steampowered.com/app/1237970/Titanfall_2/", - "binaries": [ - "titanfall2" - ] + "binaries": ["witness64_d3d11", "witness_d3d11"] }, { "game": "Trepang2", "url": "https://store.steampowered.com/app/1164940/Trepang2/", - "binaries": [ - "cppfps-win64-shipping" - ] + "binaries": ["cppfps-win64-shipping"] }, { "game": "Visage", "url": "https://store.steampowered.com/app/594330/Visage/", - "binaries": [ - "visage", - "visage-win64-shipping" - ] - }, - { - "game": "Viscera Cleanup Detail", - "url": "https://store.steampowered.com/app/246900/Viscera_Cleanup_Detail/", - "binaries": [ - "rust", - "rustugc", - "rustclientprocess", - "udk" - ] + "binaries": ["visage", "visage-win64-shipping"] }, { "game": "VOIDBREAKER", "url": "https://store.steampowered.com/app/2615540/VOIDBREAKER/", - "binaries": [ - "voidbreaker", - "voidbreaker-win64-shipping" - ] - }, - { - "game": "Voidtrain", - "url": "https://store.steampowered.com/app/1159690/Voidtrain/", - "binaries": [ - "voidtrain", - "voidtrain-win64-shipping" - ] + "binaries": ["voidbreaker", "voidbreaker-win64-shipping"] }, { "game": "VOIN", "url": "https://store.steampowered.com/app/2464530/VOIN/", - "binaries": [ - "voin", - "voin-win64-shipping" - ] - }, - { - "game": "Warhammer 40,000: Darktide", - "url": "https://store.steampowered.com/app/1361210/Warhammer_40000_Darktide/", - "binaries": [ - "darktide" - ] - }, - { - "game": "Warhammer: Vermintide 2", - "url": "https://store.steampowered.com/app/552500/Warhammer_Vermintide_2/", - "binaries": [ - "vermintide2", - "vermintide2_dx12" - ] + "binaries": ["voin", "voin-win64-shipping"] }, { "game": "What Remains of Edith Finch", "url": "https://store.steampowered.com/app/501300/What_Remains_of_Edith_Finch/", - "binaries": [ - "finchgame" - ] + "binaries": ["finchgame"] }, { "game": "Witchfire", "url": "https://store.steampowered.com/app/3156770/Witchfire/", - "binaries": [ - "witchfire" - ] - }, - { - "game": "Wolfenstein: The New Colossus", - "url": "https://store.steampowered.com/app/612880/Wolfenstein_II_The_New_Colossus/", - "binaries": [ - "newcolossus_x64vk" - ] - }, - { - "game": "Wolfenstein: The New Order", - "url": "https://store.steampowered.com/app/201810/Wolfenstein_The_New_Order/", - "binaries": [ - "wolfneworder_x64" - ] - }, - { - "game": "Wolfenstein: The Old Blood", - "url": "https://store.steampowered.com/app/350080/Wolfenstein_The_Old_Blood/", - "binaries": [ - "wolfoldblood_x64" - ] + "binaries": ["witchfire"] }, { "game": "Wolfenstein: Youngblood", "url": "https://store.steampowered.com/app/1056960/Wolfenstein_Youngblood/", - "binaries": [ - "youngblood_x64vk" - ] - }, - { - "game": "Zero Hour", - "url": "https://store.steampowered.com/app/1359090/Zero_Hour/", - "binaries": [ - "zero hour" - ] + "binaries": ["youngblood_x64vk"] }, { "game": "Ziggurat 2", "url": "https://store.steampowered.com/app/1159560/Ziggurat_2/", - "binaries": [ - "ziggurat2" - ] + "binaries": ["ziggurat2"] } -] \ No newline at end of file +] diff --git a/crates/constants/src/supported_games.rs b/crates/constants/src/supported_games.rs deleted file mode 100644 index 2fc7bf9..0000000 --- a/crates/constants/src/supported_games.rs +++ /dev/null @@ -1,128 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SupportedGame { - pub game: String, - pub url: String, - pub binaries: Vec, - pub steam_app_id: Option, - pub installed: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SupportedGames { - pub games: Vec, -} - -impl SupportedGames { - pub fn load_from_str(s: &str) -> serde_json::Result { - /// Internal struct for JSON deserialization - #[derive(Debug, Clone, Serialize, Deserialize)] - struct RawSupportedGame { - game: String, - url: String, - binaries: Vec, - } - - let raw_games: Vec = serde_json::from_str(s)?; - let installed_app_ids = detect_installed_app_ids(); - - let mut games: Vec = raw_games - .into_iter() - .map(|raw| { - let steam_app_id = extract_steam_app_id(&raw.url); - let installed = steam_app_id.is_some_and(|id| installed_app_ids.contains(&id)); - SupportedGame { - game: raw.game, - url: raw.url, - binaries: raw.binaries, - steam_app_id, - installed, - } - }) - .collect(); - - // Add test app in debug builds - if cfg!(debug_assertions) { - games.push(SupportedGame { - game: "Owl Control Test App".to_string(), - url: "https://store.steampowered.com/app/534380/Dying_Light_2_Stay_Human_Reloaded_Edition/".to_string(), - binaries: vec!["test-app".to_string()], - steam_app_id: Some(534380), - installed: false, - }); - } - - Ok(Self { games }) - } - - /// Do not use this unless you're sure you don't need a more up-to-date version. - pub fn load_from_embedded() -> Self { - Self::load_from_str(include_str!("supported_games.json")) - .expect("Failed to load supported games from embedded data") - } - - pub fn sort(&mut self) { - self.games - .sort_by(|a, b| a.game.to_lowercase().cmp(&b.game.to_lowercase())); - } - - pub fn get(&self, game_exe_without_ext: &str) -> Option<&SupportedGame> { - let game_exe_without_ext = game_exe_without_ext.to_lowercase(); - self.games.iter().find(|g| { - g.binaries.iter().any(|b| { - let b_lower = b.to_lowercase(); - // Exact match or exe has a suffix (e.g., _dx12, -win64-shipping), or epic games store variant - game_exe_without_ext == b_lower - || game_exe_without_ext.starts_with(&format!("{b_lower}_")) - || game_exe_without_ext.starts_with(&format!("{b_lower}-")) - || game_exe_without_ext.starts_with(&format!("{b_lower}epicgamesstore")) - }) - }) - } - - pub fn installed(&self) -> impl Iterator { - self.games.iter().filter(|g| g.installed) - } - - pub fn uninstalled(&self) -> impl Iterator { - self.games.iter().filter(|g| !g.installed) - } -} - -fn extract_steam_app_id(url: &str) -> Option { - // Parse "https://store.steampowered.com/app/278360/..." -> Some(278360) - url.strip_prefix("https://store.steampowered.com/app/")? - .split('/') - .next()? - .parse() - .ok() -} - -fn detect_installed_app_ids() -> Vec { - let Ok(steam_dir) = steamlocate::SteamDir::locate() else { - tracing::warn!("Steam installation not found"); - return vec![]; - }; - - let Ok(libraries) = steam_dir.libraries() else { - tracing::warn!("Failed to read Steam libraries"); - return vec![]; - }; - - let mut installed = vec![]; - for lib in libraries { - let Ok(library) = lib else { - tracing::warn!("Failed to read Steam library"); - continue; - }; - for app in library.apps() { - let Ok(app) = app else { - tracing::warn!("Failed to read app"); - continue; - }; - installed.push(app.app_id); - } - } - installed -} diff --git a/crates/constants/src/unsupported_games.json b/crates/constants/src/unsupported_games.json index 3badece..da17b6c 100644 --- a/crates/constants/src/unsupported_games.json +++ b/crates/constants/src/unsupported_games.json @@ -1,9 +1,4 @@ [ - { - "name": "COMMENT: This file is kept for backwards compatibility. It will be removed in the near-future.", - "binaries": ["comment-not-a-real-executable"], - "reason": "EnoughData" - }, { "name": "Fortnite", "binaries": ["fortnite", "fortniteclient-win64-shipping"], @@ -21,7 +16,7 @@ }, { "name": "The Finals", - "binaries": ["discovery-e.exe"], + "binaries": ["discovery-e"], "reason": "EnoughData" }, { @@ -111,90 +106,94 @@ }, { "name": "PowerWash Simulator 2", - "binaries": ["PowerWash Simulator 2.exe"], + "binaries": ["PowerWash Simulator 2"], "reason": "EnoughData" }, { "name": "PowerWash Simulator", - "binaries": ["PowerWashSimulator.exe"], + "binaries": ["PowerWashSimulator"], "reason": "EnoughData" }, { "name": "Cyberpunk 2077", - "binaries": ["Cyberpunk2077.exe"], + "binaries": ["Cyberpunk2077"], "reason": "EnoughData" }, { "name": "Half-Life 2 + Mods", - "binaries": ["hl2.exe"], + "binaries": ["hl2"], "reason": "EnoughData" }, { "name": "Left 4 Dead 2", - "binaries": ["left4dead2.exe"], + "binaries": ["left4dead2"], "reason": "EnoughData" }, { "name": "Satisfactory", - "binaries": ["FactoryGameSteam-Win64-Shipping.exe"], + "binaries": ["FactoryGameSteam-Win64-Shipping"], "reason": "EnoughData" }, { "name": "Portal 2", - "binaries": ["portal2.exe"], + "binaries": ["portal2"], "reason": "EnoughData" }, { "name": "Dying Light", - "binaries": ["DyingLightGame_x64_rwdi.exe"], + "binaries": ["DyingLightGame_x64_rwdi"], "reason": "EnoughData" }, { "name": "Red Dead Redemption 2", - "binaries": ["RDR2.exe"], + "binaries": ["RDR2"], "reason": "EnoughData" }, { "name": "Dying Light: The Beast", - "binaries": ["DyingLightGame_TheBeast_x64_rwdi.exe"], + "binaries": ["DyingLightGame_TheBeast_x64_rwdi"], "reason": "EnoughData" }, { "name": "House Flipper", - "binaries": ["HouseFlipper.exe"], + "binaries": ["HouseFlipper"], "reason": "EnoughData" }, { "name": "House Flipper 2", - "binaries": ["HouseFlipper2.exe"], + "binaries": ["HouseFlipper2"], "reason": "EnoughData" }, { "name": "Kingdom Come: Deliverance", - "binaries": ["KingdomCome.exe"], + "binaries": ["KingdomCome"], "reason": "EnoughData" }, { "name": "Deep Rock Galactic", - "binaries": ["FSD-Win64-Shipping.exe"], + "binaries": ["FSD-Win64-Shipping"], "reason": "EnoughData" }, { "name": "Gunfire Reborn", - "binaries": ["Gunfire Reborn.exe"], + "binaries": ["Gunfire Reborn"], "reason": "EnoughData" }, { "name": "Planet Crafter", - "binaries": ["Planet Crafter.exe"], + "binaries": ["Planet Crafter"], "reason": "EnoughData" }, { "name": "Deus Ex: Human Revolution", - "binaries": ["DXHRDC.exe"], + "binaries": ["DXHRDC"], + "reason": "EnoughData" + }, + { + "name": "Metro 2033", + "binaries": ["metro"], "reason": "EnoughData" }, - { "name": "Metro 2033", "binaries": ["metro.exe"], "reason": "EnoughData" }, { "name": "Roblox", "binaries": ["robloxstudiobeta", "robloxplayerbeta"], @@ -290,5 +289,1576 @@ "name": "Visual Studio Code", "binaries": ["code", "codium"], "reason": "NotAGame" + }, + { + "name": "Call of Duty: Black Ops 6", + "binaries": ["cod"], + "reason": "EnoughData" + }, + { + "name": "Sons of the Forest", + "binaries": ["sonsoftheforest"], + "reason": "EnoughData" + }, + { + "name": "The Forest", + "binaries": ["theforest"], + "reason": "EnoughData" + }, + { + "name": "The Elder Scrolls V: Skyrim", + "binaries": ["skyrimse"], + "reason": "EnoughData" + }, + { + "name": "Sea of Thieves", + "binaries": ["sotgame"], + "reason": "EnoughData" + }, + { + "name": "Borderlands 2", + "binaries": ["borderlands2"], + "reason": "EnoughData" + }, + { + "name": "Far Cry 5", + "binaries": ["farcry5"], + "reason": "EnoughData" + }, + { + "name": "Far Cry 6", + "binaries": ["farcry6"], + "reason": "EnoughData" + }, + { + "name": "Dead Island 2", + "binaries": ["deadisland-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Splitgate", + "binaries": ["portalwars-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Killing Floor 2", + "binaries": ["kfgame"], + "reason": "EnoughData" + }, + { + "name": "Ghostwire: Tokyo", + "binaries": ["gwt"], + "reason": "EnoughData" + }, + { + "name": "Far Cry 3", + "binaries": ["farcry3_d3d11"], + "reason": "EnoughData" + }, + { + "name": "Warhammer: Vermintide 2", + "binaries": ["vermintide2_dx12"], + "reason": "EnoughData" + }, + { + "name": "Apex Legends", + "binaries": ["r5apex_dx12"], + "reason": "EnoughData" + }, + { + "name": "Medieval Dynasty", + "binaries": ["medieval_dynasty-wingdk-shipping"], + "reason": "EnoughData" + }, + { + "name": "Battlefield 6 / REDSEC", + "binaries": ["bf6"], + "reason": "EnoughData" + }, + { + "name": "Delta Force", + "binaries": ["deltaforceclient-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Slime Rancher 2", + "binaries": ["slimerancher2"], + "reason": "EnoughData" + }, + { + "name": "Slime Rancher", + "binaries": ["slimerancher"], + "reason": "EnoughData" + }, + { + "name": "PAYDAY 2", + "binaries": ["payday2_win32_release"], + "reason": "EnoughData" + }, + { + "name": "Keep Digging", + "binaries": ["keepdigging-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Dishonored", + "binaries": ["dishonored"], + "reason": "EnoughData" + }, + { + "name": "DOOM Eternal", + "binaries": ["doometernalx64vk"], + "reason": "EnoughData" + }, + { + "name": "Far Cry 4", + "binaries": ["farcry4"], + "reason": "EnoughData" + }, + { + "name": "The Outlast Trials", + "binaries": ["totclient-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Borderlands 3", + "binaries": ["borderlands3"], + "reason": "EnoughData" + }, + { + "name": "Borderlands: The Pre-Sequel", + "binaries": ["borderlandspresequel"], + "reason": "EnoughData" + }, + { + "name": "No Man's Sky", + "binaries": ["nms"], + "reason": "EnoughData" + }, + { + "name": "Halo 2 Anniversary (MCC)", + "binaries": ["mcc-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Wolfenstein: The New Order", + "binaries": ["wolfneworder_x64"], + "reason": "EnoughData" + }, + { + "name": "Severed Steel", + "binaries": ["thankyouverycool-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "GTFO", + "binaries": ["gtfo"], + "reason": "EnoughData" + }, + { + "name": "Medieval Dynasty", + "binaries": ["medieval_dynasty-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Prey (2017)", + "binaries": ["prey"], + "reason": "EnoughData" + }, + { + "name": "Green Hell", + "binaries": ["gh"], + "reason": "EnoughData" + }, + { + "name": "The Talos Principle", + "binaries": ["talos"], + "reason": "EnoughData" + }, + { + "name": "Firewatch", + "binaries": ["firewatch"], + "reason": "EnoughData" + }, + { + "name": "Resident Evil Village", + "binaries": ["re8"], + "reason": "EnoughData" + }, + { + "name": "Warhammer 40,000: Darktide", + "binaries": ["darktide"], + "reason": "EnoughData" + }, + { + "name": "Viscera Cleanup Detail", + "binaries": ["udk"], + "reason": "EnoughData" + }, + { + "name": "Ghostrunner", + "binaries": ["ghostrunner-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "A Story About My Uncle", + "binaries": ["asamu-win32-shipping"], + "reason": "EnoughData" + }, + { + "name": "Titanfall 2", + "binaries": ["titanfall2"], + "reason": "EnoughData" + }, + { + "name": "Shadow Warrior 2", + "binaries": ["shadowwarrior2"], + "reason": "EnoughData" + }, + { + "name": "Thief Simulator", + "binaries": ["thief"], + "reason": "EnoughData" + }, + { + "name": "VOIDBREAKER", + "binaries": ["voidbreaker-wingdk-shipping", "voidbreaker-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "DOOM 2016", + "binaries": ["doomx64"], + "reason": "EnoughData" + }, + { + "name": "Subnautica", + "binaries": ["subnautica"], + "reason": "EnoughData" + }, + { + "name": "Fallout 4", + "binaries": ["fallout4"], + "reason": "EnoughData" + }, + { + "name": "Wolfenstein: Youngblood", + "binaries": ["youngblood_x64vk"], + "reason": "EnoughData" + }, + { + "name": "Tiny Tina's Wonderlands", + "binaries": ["wonderlands"], + "reason": "EnoughData" + }, + { + "name": "Far Cry: New Dawn", + "binaries": ["farcrynewdawn"], + "reason": "EnoughData" + }, + { + "name": "Resident Evil 7: Biohazard", + "binaries": ["re7"], + "reason": "EnoughData" + }, + { + "name": "High on Life", + "binaries": ["oregon-wingdk-shipping"], + "reason": "EnoughData" + }, + { + "name": "Thief", + "binaries": ["shipping-thiefgame"], + "reason": "EnoughData" + }, + { + "name": "ICARUS", + "binaries": ["icarus-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "RAGE 2", + "binaries": ["rage2"], + "reason": "EnoughData" + }, + { + "name": "Superliminal", + "binaries": ["superliminal"], + "reason": "EnoughData" + }, + { + "name": "Dishonored 2", + "binaries": ["dishonored2"], + "reason": "EnoughData" + }, + { + "name": "Blair Witch", + "binaries": ["blairwitch-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Mirror's Edge", + "binaries": ["mirrorsedge"], + "reason": "EnoughData" + }, + { + "name": "Voidtrain", + "binaries": ["voidtrain-wingdk-shipping"], + "reason": "EnoughData" + }, + { + "name": "Panicore", + "binaries": ["panicore-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Battlefield 1", + "binaries": ["bf1"], + "reason": "EnoughData" + }, + { + "name": "Borderlands 4", + "binaries": ["borderlands4"], + "reason": "EnoughData" + }, + { + "name": "MORDHAU", + "binaries": ["mordhau-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Generation Zero", + "binaries": ["generationzero_f"], + "reason": "EnoughData" + }, + { + "name": "Metro Exodus", + "binaries": ["metroexodus"], + "reason": "EnoughData" + }, + { + "name": "Halo: Infinite", + "binaries": ["haloinfinite"], + "reason": "EnoughData" + }, + { + "name": "High on Life", + "binaries": ["oregon-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Ghostrunner 2", + "binaries": ["ghostrunner2-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Battlefield 4", + "binaries": ["bf4"], + "reason": "EnoughData" + }, + { + "name": "Battlefield V", + "binaries": ["bfv"], + "reason": "EnoughData" + }, + { + "name": "Alien: Isolation", + "binaries": ["ai"], + "reason": "EnoughData" + }, + { + "name": "Far Cry 3", + "binaries": ["farcry3"], + "reason": "EnoughData" + }, + { + "name": "Far Cry 3: Blood Dragon", + "binaries": ["fc3_blooddragon_d3d11"], + "reason": "EnoughData" + }, + { + "name": "Crysis 1 Remastered", + "binaries": ["crysisremastered"], + "reason": "EnoughData" + }, + { + "name": "Dishonored: Death of the Outsider", + "binaries": ["dishonored_do"], + "reason": "EnoughData" + }, + { + "name": "The Witness", + "binaries": ["witness64_d3d11"], + "reason": "EnoughData" + }, + { + "name": "Crysis 2", + "binaries": ["crysis2"], + "reason": "EnoughData" + }, + { + "name": "Poppy Playtime", + "binaries": ["poppy_playtime-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Tacoma", + "binaries": ["tacoma"], + "reason": "EnoughData" + }, + { + "name": "Wolfenstein: The New Colossus", + "binaries": ["newcolossus_x64vk"], + "reason": "EnoughData" + }, + { + "name": "Wolfenstein: The Old Blood", + "binaries": ["wolfoldblood_x64"], + "reason": "EnoughData" + }, + { + "name": "BioShock Infinite", + "binaries": ["bioshockinfinite"], + "reason": "EnoughData" + }, + { + "name": "Zero Hour", + "binaries": ["zero hour"], + "reason": "EnoughData" + }, + { + "name": "Deathloop", + "binaries": ["deathloop"], + "reason": "EnoughData" + }, + { + "name": "What Remains of Edith Finch", + "binaries": ["finchgame", "finchgame-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Enlisted", + "binaries": ["enlisted"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: WWII", + "binaries": ["s2_sp64_ship", "s2_mp64_ship"], + "reason": "EnoughData" + }, + { + "name": "Mirror's Edge Catalyst", + "binaries": ["mirrorsedgecatalyst"], + "reason": "EnoughData" + }, + { + "name": "Ziggurat 2", + "binaries": ["ziggurat2"], + "reason": "EnoughData" + }, + { + "name": "The Darkness II", + "binaries": ["darknessii"], + "reason": "EnoughData" + }, + { + "name": "Back 4 Blood", + "binaries": ["back4blood"], + "reason": "EnoughData" + }, + { + "name": "Far Cry: Primal", + "binaries": ["fcprimal"], + "reason": "EnoughData" + }, + { + "name": "Ready or Not", + "binaries": ["readyornotsteam-win64-shipping", "readyornot-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Outlast 2", + "binaries": ["outlast2"], + "reason": "EnoughData" + }, + { + "name": "Indiana Jones and the Great Circle", + "binaries": ["thegreatcircle"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Black Ops Cold War", + "binaries": ["blackopscoldwar"], + "reason": "EnoughData" + }, + { + "name": "Borderlands 1", + "binaries": ["borderlandsgoty"], + "reason": "EnoughData" + }, + { + "name": "The Long Dark", + "binaries": ["tld"], + "reason": "EnoughData" + }, + { + "name": "Avowed", + "binaries": ["avowed-wingdk-shipping"], + "reason": "EnoughData" + }, + { + "name": "BioShock Infinite", + "binaries": ["shippingpc-xgame"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Black Ops 6", + "binaries": ["cod24-cod"], + "reason": "EnoughData" + }, + { + "name": "Neon White", + "binaries": ["neon white"], + "reason": "EnoughData" + }, + { + "name": "Dead Island 2", + "binaries": ["deadisland-wingdk-shipping"], + "reason": "EnoughData" + }, + { + "name": "Atomic Heart", + "binaries": ["atomicheart-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Abyssus", + "binaries": ["rgame-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "ELDERBORN", + "binaries": ["elderborn"], + "reason": "EnoughData" + }, + { + "name": "Journey to the Savage Planet", + "binaries": ["towers-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "The Stanley Parable: Ultra Deluxe", + "binaries": ["the stanley parable ultra deluxe"], + "reason": "EnoughData" + }, + { + "name": "Hard Reset Redux", + "binaries": ["hr.x64"], + "reason": "EnoughData" + }, + { + "name": "The Outer Worlds", + "binaries": ["indianaepicgamestore-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Layers of Fear 2", + "binaries": ["lof2-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "BPM: Bullets Per Minute", + "binaries": ["bpmgame-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Hell Let Loose", + "binaries": ["hllepicgamesstore-win64-shipping", "hll-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Outer Wilds", + "binaries": ["outerwilds"], + "reason": "EnoughData" + }, + { + "name": "Teardown", + "binaries": ["teardown"], + "reason": "EnoughData" + }, + { + "name": "Trepang2", + "binaries": ["cppfps-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Battlefield 3", + "binaries": ["bf3"], + "reason": "EnoughData" + }, + { + "name": "The Stanley Parable", + "binaries": ["stanley"], + "reason": "EnoughData" + }, + { + "name": "The Outer Worlds 2", + "binaries": [ + "theouterworlds2-wingdk-shipping", + "theouterworlds2-win64-shipping" + ], + "reason": "EnoughData" + }, + { + "name": "PAYDAY 3", + "binaries": ["payday3client-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Immortals of Aveum", + "binaries": ["immortalsofaveum-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Conundrum", + "binaries": ["conundrum-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Q.U.B.E. 2", + "binaries": ["qube-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Battlefield Hardline", + "binaries": ["bfh"], + "reason": "EnoughData" + }, + { + "name": "Crysis 3", + "binaries": ["crysis3remastered"], + "reason": "EnoughData" + }, + { + "name": "DOOM 2016", + "binaries": ["doomx64vk"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Black Ops III", + "binaries": ["blackops3"], + "reason": "EnoughData" + }, + { + "name": "Soma", + "binaries": ["soma"], + "reason": "EnoughData" + }, + { + "name": "Shadow Warrior 3", + "binaries": ["sw3"], + "reason": "EnoughData" + }, + { + "name": "Close to the Sun", + "binaries": ["ctts-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "The Vanishing of Ethan Carter", + "binaries": ["astronautsgame-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "The Elder Scrolls IV: Oblivion Remastered", + "binaries": ["oblivionremastered-wingdk-shipping"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Modern Warfare II (2022)", + "binaries": ["sp22-cod"], + "reason": "EnoughData" + }, + { + "name": "Warhammer: Vermintide 2", + "binaries": ["vermintide2"], + "reason": "EnoughData" + }, + { + "name": "The Outer Worlds", + "binaries": ["indiana-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Dishonored: Death of the Outsider", + "binaries": ["dishonored_do_x64"], + "reason": "EnoughData" + }, + { + "name": "Superliminal", + "binaries": ["superliminalsteam"], + "reason": "EnoughData" + }, + { + "name": "The Elder Scrolls IV: Oblivion Remastered", + "binaries": ["oblivionremastered-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Modern Warfare (2019)", + "binaries": ["modernwarfare"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Modern Warfare III (2023)", + "binaries": ["cod23-cod"], + "reason": "EnoughData" + }, + { + "name": "The Outer Worlds", + "binaries": ["indianawindowsstore-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Atomic Heart", + "binaries": ["atomicheart-wingdk-shipping"], + "reason": "EnoughData" + }, + { + "name": "Crysis 3", + "binaries": ["crysis3"], + "reason": "EnoughData" + }, + { + "name": "Q.U.B.E. 2", + "binaries": ["qube_remastered-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Metro: Last Light (delisted, see Redux)", + "binaries": ["metroll"], + "reason": "EnoughData" + }, + { + "name": "The Vanishing of Ethan Carter", + "binaries": ["ethancarter-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Poppy Playtime", + "binaries": ["playtime_chapter3-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Crysis 1 Remastered", + "binaries": ["crysis"], + "reason": "EnoughData" + }, + { + "name": "The Elder Scrolls V: Skyrim", + "binaries": ["tesv"], + "reason": "EnoughData" + }, + { + "name": "Avowed", + "binaries": ["avowed-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Halo 2 Anniversary (MCC)", + "binaries": ["mccwinstore-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Borderlands 1", + "binaries": ["borderlands"], + "reason": "EnoughData" + }, + { + "name": "Enlisted", + "binaries": ["enlisted-gdk"], + "reason": "EnoughData" + }, + { + "name": "Poppy Playtime", + "binaries": ["playtime_prototype4-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Modern Warfare III (2023)", + "binaries": ["sp23-cod"], + "reason": "EnoughData" + }, + { + "name": "Poppy Playtime", + "binaries": ["ch4_pro-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Crysis 2", + "binaries": ["crysis2remastered"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Black Ops 6", + "binaries": ["sp24-cod"], + "reason": "EnoughData" + }, + { + "name": "Crysis 1 Remastered", + "binaries": ["crysis64"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Black Ops 6", + "binaries": ["cod_sp"], + "reason": "EnoughData" + }, + { + "name": "Q.U.B.E. 2", + "binaries": ["qube"], + "reason": "EnoughData" + }, + { + "name": "Battlefield 4", + "binaries": ["bf4_offline"], + "reason": "EnoughData" + }, + { + "name": "Delta Force", + "binaries": ["bhdclient-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Modern Warfare II (2022)", + "binaries": ["cod22-cod"], + "reason": "EnoughData" + }, + { + "name": "Battlefield 4", + "binaries": ["bf4_x86"], + "reason": "EnoughData" + }, + { + "name": "ARC Raiders", + "binaries": ["pioneergame"], + "reason": "EnoughData" + }, + { + "name": "ARK: Survival Ascended", + "binaries": ["arkascended"], + "reason": "EnoughData" + }, + { + "name": "ARK: Survival Evolved", + "binaries": ["shootergame"], + "reason": "EnoughData" + }, + { + "name": "American Truck Simulator", + "binaries": ["amtrucks"], + "reason": "EnoughData" + }, + { + "name": "Amnesia: A Machine for Pigs", + "binaries": ["aamfp"], + "reason": "EnoughData" + }, + { + "name": "Amnesia: Rebirth", + "binaries": ["amnesiarebirth"], + "reason": "EnoughData" + }, + { + "name": "Among the Sleep", + "binaries": ["among the sleep"], + "reason": "EnoughData" + }, + { + "name": "AnyDesk", + "binaries": ["anydesk"], + "reason": "NotAGame" + }, + { + "name": "Assassin's Creed Syndicate", + "binaries": ["acs"], + "reason": "EnoughData" + }, + { + "name": "Assetto Corsa Competizione", + "binaries": ["ac2-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Avatar: Frontiers of Pandora", + "binaries": ["afop"], + "reason": "EnoughData" + }, + { + "name": "BDS Unknown UE Game", + "binaries": ["bds-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Battle Shapers", + "binaries": ["battleshapers"], + "reason": "EnoughData" + }, + { + "name": "BeamNG.drive", + "binaries": ["beamng.drive.x64"], + "reason": "EnoughData" + }, + { + "name": "BioShock", + "binaries": ["bioshock"], + "reason": "EnoughData" + }, + { + "name": "BioShock Remastered", + "binaries": ["bioshockhd"], + "reason": "EnoughData" + }, + { + "name": "Black Mesa", + "binaries": ["bms"], + "reason": "EnoughData" + }, + { + "name": "Blood: Fresh Supply", + "binaries": ["anuket_x64"], + "reason": "EnoughData" + }, + { + "name": "BlueStacks HD Player", + "binaries": ["hd-player"], + "reason": "NotAGame" + }, + { + "name": "Brave", + "binaries": ["brave"], + "reason": "NotAGame" + }, + { + "name": "Bus Flipper Simulator", + "binaries": ["busflippergame-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "CUFFBUST", + "binaries": ["cuffbust-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty 4: Modern Warfare", + "binaries": ["iw3sp"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Black Ops 4", + "binaries": ["blackops4"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Black Ops II", + "binaries": ["t6zm"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Black Ops III (Custom Client)", + "binaries": ["boiii (2)"], + "reason": "EnoughData" + }, + { + "name": "Call of Duty: Modern Warfare 2 (2009)", + "binaries": ["iw4sp"], + "reason": "EnoughData" + }, + { + "name": "Car Mechanic Simulator 2018", + "binaries": ["cms2018"], + "reason": "EnoughData" + }, + { + "name": "Car Mechanic Simulator 2021", + "binaries": [ + "car mechanic simulator 2021", + "car mechanic simulator 2021 demo" + ], + "reason": "EnoughData" + }, + { + "name": "Chivalry: Medieval Warfare", + "binaries": ["cmw"], + "reason": "EnoughData" + }, + { + "name": "Clustertruck", + "binaries": ["clustertruck"], + "reason": "EnoughData" + }, + { + "name": "Condemned: Criminal Origins", + "binaries": ["condemned"], + "reason": "EnoughData" + }, + { + "name": "Cooking Simulator", + "binaries": ["cookingsim"], + "reason": "EnoughData" + }, + { + "name": "Corsair iCUE", + "binaries": ["icue"], + "reason": "NotAGame" + }, + { + "name": "Crab Game", + "binaries": ["crab game"], + "reason": "EnoughData" + }, + { + "name": "Cry of Fear", + "binaries": ["cof"], + "reason": "EnoughData" + }, + { + "name": "DEVOUR", + "binaries": ["devour"], + "reason": "EnoughData" + }, + { + "name": "Dark and Darker", + "binaries": ["dungeoncrawler"], + "reason": "EnoughData" + }, + { + "name": "Deadlock", + "binaries": ["deadlock"], + "reason": "EnoughData" + }, + { + "name": "Dear Esther", + "binaries": ["dearesther"], + "reason": "EnoughData" + }, + { + "name": "Deceit 2", + "binaries": ["deceit2game-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Deep Rock Galactic", + "binaries": ["fsd-wingdk-shipping"], + "reason": "EnoughData" + }, + { + "name": "Deus Ex: Mankind Divided", + "binaries": ["dxmd"], + "reason": "EnoughData" + }, + { + "name": "Dinocop", + "binaries": ["dinocop"], + "reason": "EnoughData" + }, + { + "name": "Divinity: Original Sin", + "binaries": ["eocapp"], + "reason": "EnoughData" + }, + { + "name": "Drive Beyond Horizons", + "binaries": ["drivebeyondhorizons-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Dying Light", + "binaries": ["dyinglightgame"], + "reason": "EnoughData" + }, + { + "name": "Exit 8", + "binaries": ["exit8-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "F.E.A.R.", + "binaries": ["fear"], + "reason": "EnoughData" + }, + { + "name": "F.E.A.R. Extraction Point", + "binaries": ["fearxp"], + "reason": "EnoughData" + }, + { + "name": "Fallout 3", + "binaries": ["fallout3"], + "reason": "EnoughData" + }, + { + "name": "Fallout: New Vegas", + "binaries": ["falloutnv"], + "reason": "EnoughData" + }, + { + "name": "Fishing Planet", + "binaries": ["fishingplanet"], + "reason": "EnoughData" + }, + { + "name": "Forza Horizon 4", + "binaries": ["forzahorizon4"], + "reason": "EnoughData" + }, + { + "name": "Forza Horizon 5", + "binaries": ["forzahorizon5"], + "reason": "EnoughData" + }, + { + "name": "FragPunk", + "binaries": ["fragpunk"], + "reason": "EnoughData" + }, + { + "name": "Garry's Mod", + "binaries": ["gmod"], + "reason": "EnoughData" + }, + { + "name": "GeForce NOW", + "binaries": ["geforcenow"], + "reason": "NotAGame" + }, + { + "name": "Gone Home", + "binaries": ["gonehome", "gonehome32"], + "reason": "EnoughData" + }, + { + "name": "Gym Manager", + "binaries": ["gymmanager"], + "reason": "EnoughData" + }, + { + "name": "HITMAN World of Assassination", + "binaries": ["hitman3"], + "reason": "EnoughData" + }, + { + "name": "Half Sword", + "binaries": ["halfswordue5-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Half-Life", + "binaries": ["hl"], + "reason": "EnoughData" + }, + { + "name": "House Builder", + "binaries": ["housebuilder"], + "reason": "EnoughData" + }, + { + "name": "House Builder 2", + "binaries": ["housebuilder2"], + "reason": "EnoughData" + }, + { + "name": "I Am Your Beast", + "binaries": ["i am your beast"], + "reason": "EnoughData" + }, + { + "name": "Internet Cafe Simulator", + "binaries": ["internet cafe simulator"], + "reason": "EnoughData" + }, + { + "name": "Internet Cafe Simulator 2", + "binaries": ["internet cafe simulator 2"], + "reason": "EnoughData" + }, + { + "name": "Internet Cafe Simulator 2025", + "binaries": ["internet cafe simulator 2025"], + "reason": "EnoughData" + }, + { + "name": "Journey", + "binaries": ["journey"], + "reason": "EnoughData" + }, + { + "name": "Jump Space", + "binaries": ["jump space"], + "reason": "EnoughData" + }, + { + "name": "Just Cause 2", + "binaries": ["justcause2"], + "reason": "EnoughData" + }, + { + "name": "Just Cause 3", + "binaries": ["justcause3"], + "reason": "EnoughData" + }, + { + "name": "Just Cause 4", + "binaries": ["justcause4"], + "reason": "EnoughData" + }, + { + "name": "Killing Floor", + "binaries": ["killingfloor"], + "reason": "EnoughData" + }, + { + "name": "Killing Floor 3", + "binaries": ["nightfallclient-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Layers of Fear (2023)", + "binaries": ["layersoffear-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "League of Legends", + "binaries": ["league of legends"], + "reason": "EnoughData" + }, + { + "name": "Lethal Company", + "binaries": ["lethal company"], + "reason": "EnoughData" + }, + { + "name": "Liftoff: FPV Drone Racing", + "binaries": ["liftoff"], + "reason": "EnoughData" + }, + { + "name": "Liftoff: Micro Drones", + "binaries": ["liftoff micro drones"], + "reason": "EnoughData" + }, + { + "name": "Manifold Garden", + "binaries": ["manifoldgarden"], + "reason": "EnoughData" + }, + { + "name": "MiSide", + "binaries": ["miside"], + "reason": "EnoughData" + }, + { + "name": "Momentum Mod", + "binaries": ["momentum"], + "reason": "EnoughData" + }, + { + "name": "Muck", + "binaries": ["muck"], + "reason": "EnoughData" + }, + { + "name": "Mycopunk", + "binaries": ["mycopunk"], + "reason": "EnoughData" + }, + { + "name": "Neighbours from Hell", + "binaries": ["neighbours from hell 3d"], + "reason": "EnoughData" + }, + { + "name": "OWL Recorder", + "binaries": ["owl-recorder"], + "reason": "NotAGame" + }, + { + "name": "Outlast", + "binaries": ["olgame"], + "reason": "EnoughData" + }, + { + "name": "PC Building Simulator", + "binaries": ["pcbs"], + "reason": "EnoughData" + }, + { + "name": "PEAK", + "binaries": ["peak"], + "reason": "EnoughData" + }, + { + "name": "Pacific Drive", + "binaries": ["pendriverpro-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Paint the Town Red", + "binaries": ["paintthetownred"], + "reason": "EnoughData" + }, + { + "name": "Palworld", + "binaries": ["palworld-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Peaks of Yore", + "binaries": ["peaks of yore"], + "reason": "EnoughData" + }, + { + "name": "Phasmophobia", + "binaries": ["phasmophobia"], + "reason": "EnoughData" + }, + { + "name": "PowerWash Simulator 2", + "binaries": ["powerwash simulator 2 demo"], + "reason": "EnoughData" + }, + { + "name": "Prison Escape Simulator", + "binaries": ["prison escape simulator"], + "reason": "EnoughData" + }, + { + "name": "R.E.P.O.", + "binaries": ["repo"], + "reason": "EnoughData" + }, + { + "name": "RV There Yet?", + "binaries": ["ride-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Ranch Simulator", + "binaries": ["ranch_simulator-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Remnant 2", + "binaries": ["remnant2-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Risk of Rain 2", + "binaries": ["risk of rain 2"], + "reason": "EnoughData" + }, + { + "name": "Roboquest", + "binaries": ["roboquest-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Rust", + "binaries": ["rustclient"], + "reason": "EnoughData" + }, + { + "name": "S.T.A.L.K.E.R. 2: Heart of Chornobyl", + "binaries": ["stalker2-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "SCP: Nine-Tailed Fox", + "binaries": ["scp nine-tailed fox"], + "reason": "EnoughData" + }, + { + "name": "SCP: Secret Laboratory", + "binaries": ["scpsl"], + "reason": "EnoughData" + }, + { + "name": "SCUM", + "binaries": ["scum"], + "reason": "EnoughData" + }, + { + "name": "SUPERHOT", + "binaries": ["sh"], + "reason": "EnoughData" + }, + { + "name": "SUPERHOT: MIND CONTROL DELETE", + "binaries": ["shmcd"], + "reason": "EnoughData" + }, + { + "name": "SWAT 4", + "binaries": ["swat4"], + "reason": "EnoughData" + }, + { + "name": "Satisfactory", + "binaries": ["factorygameegs-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Schedule I", + "binaries": ["schedule i"], + "reason": "EnoughData" + }, + { + "name": "Serious Sam", + "binaries": ["serioussam"], + "reason": "EnoughData" + }, + { + "name": "Shadow Warrior", + "binaries": ["sw.x64"], + "reason": "EnoughData" + }, + { + "name": "Shady Knight", + "binaries": ["shady knight"], + "reason": "EnoughData" + }, + { + "name": "Shark Attack Deathmatch 2", + "binaries": ["shark attack deathmatch 2"], + "reason": "EnoughData" + }, + { + "name": "Species: Unknown", + "binaries": ["speciesunknown-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Static Dread", + "binaries": ["static dread"], + "reason": "EnoughData" + }, + { + "name": "Stranded Deep", + "binaries": ["stranded_deep"], + "reason": "EnoughData" + }, + { + "name": "Stray", + "binaries": ["stray-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Superliminal", + "binaries": ["superliminalgog"], + "reason": "EnoughData" + }, + { + "name": "Supermarket Together", + "binaries": ["supermarket together"], + "reason": "EnoughData" + }, + { + "name": "Tales of Escape", + "binaries": ["talesofescape"], + "reason": "EnoughData" + }, + { + "name": "The Beginner's Guide", + "binaries": ["beginnersguide"], + "reason": "EnoughData" + }, + { + "name": "The Crew 2", + "binaries": ["thecrew2"], + "reason": "EnoughData" + }, + { + "name": "The Finals", + "binaries": ["discovery-d"], + "reason": "EnoughData" + }, + { + "name": "The Voidness", + "binaries": ["the voidness"], + "reason": "EnoughData" + }, + { + "name": "Thief Simulator 2", + "binaries": ["thief simulator 2"], + "reason": "EnoughData" + }, + { + "name": "Totally Unrealistic Shooter", + "binaries": ["tus"], + "reason": "EnoughData" + }, + { + "name": "ULTRAKILL", + "binaries": ["ultrakill"], + "reason": "EnoughData" + }, + { + "name": "Unreal Editor", + "binaries": ["unrealeditor"], + "reason": "NotAGame" + }, + { + "name": "Vampire: The Masquerade - Bloodlines 2", + "binaries": ["bloodlines2-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "Viewfinder", + "binaries": ["viewfinder"], + "reason": "EnoughData" + }, + { + "name": "Viscera Cleanup Detail", + "binaries": ["viscera cleanup detail"], + "reason": "EnoughData" + }, + { + "name": "Void Bastards", + "binaries": ["void bastards"], + "reason": "EnoughData" + }, + { + "name": "We Who Are About To Die", + "binaries": ["wwaatd-win64-shipping"], + "reason": "EnoughData" + }, + { + "name": "WhatsApp", + "binaries": ["whatsapp.root"], + "reason": "NotAGame" + }, + { + "name": "White Knuckle", + "binaries": ["white knuckle"], + "reason": "EnoughData" + }, + { + "name": "Wild Bastards", + "binaries": ["wildbastards"], + "reason": "EnoughData" + }, + { + "name": "Windows Explorer", + "binaries": ["explorer"], + "reason": "NotAGame" + }, + { + "name": "Windows Search", + "binaries": ["searchhost"], + "reason": "NotAGame" + }, + { + "name": "eFootball", + "binaries": ["efootball"], + "reason": "EnoughData" } ] diff --git a/crates/constants/src/unsupported_games.rs b/crates/constants/src/unsupported_games.rs new file mode 100644 index 0000000..f58f552 --- /dev/null +++ b/crates/constants/src/unsupported_games.rs @@ -0,0 +1,99 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum UnsupportedReason { + EnoughData, + NotAGame, + Other(String), +} + +impl fmt::Display for UnsupportedReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UnsupportedReason::EnoughData => { + write!(f, "We have collected enough data for this game.") + } + UnsupportedReason::NotAGame => write!(f, "This is not a game."), + UnsupportedReason::Other(s) => write!(f, "{s}"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct UnsupportedGame { + pub name: String, + pub binaries: Vec, + pub reason: UnsupportedReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnsupportedGames { + pub games: Vec, +} + +impl UnsupportedGames { + pub fn load_from_str(s: &str) -> serde_json::Result { + let games: Vec = serde_json::from_str(s)?; + Ok(Self { games }) + } + + /// Do not use this unless you're sure you don't need a more up-to-date version. + pub fn load_from_embedded() -> Self { + Self::load_from_str(include_str!("unsupported_games.json")) + .expect("Failed to load unsupported games from embedded data") + } + + pub fn get(&self, game_exe_without_ext: &str) -> Option<&UnsupportedGame> { + let game_exe_without_ext = game_exe_without_ext.to_lowercase(); + self.games.iter().find(|g| { + g.binaries.iter().any(|b| { + let b_lower = b.to_lowercase(); + // Exact match or exe has a suffix (e.g., _dx12, -win64-shipping), or epic games store variant + game_exe_without_ext == b_lower + || game_exe_without_ext.starts_with(&format!("{b_lower}_")) + || game_exe_without_ext.starts_with(&format!("{b_lower}-")) + || game_exe_without_ext.starts_with(&format!("{b_lower}epicgamesstore")) + }) + }) + } +} + +pub struct InstalledGame { + pub name: String, + pub steam_app_id: u32, +} + +pub fn detect_installed_games() -> Vec { + let Ok(steam_dir) = steamlocate::SteamDir::locate() else { + tracing::warn!("Steam installation not found"); + return vec![]; + }; + + let Ok(libraries) = steam_dir.libraries() else { + tracing::warn!("Failed to read Steam libraries"); + return vec![]; + }; + + let mut installed = vec![]; + for lib in libraries { + let Ok(library) = lib else { + tracing::warn!("Failed to read Steam library"); + continue; + }; + for app in library.apps() { + let Ok(app) = app else { + tracing::warn!("Failed to read app"); + continue; + }; + if let Some(name) = &app.name { + installed.push(InstalledGame { + name: name.clone(), + steam_app_id: app.app_id, + }); + } + } + } + installed +} diff --git a/src/app_state.rs b/src/app_state.rs index 9a5ed74..048155b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -2,12 +2,12 @@ use std::{ path::PathBuf, sync::{ Arc, OnceLock, RwLock, - atomic::{AtomicBool, AtomicUsize}, + atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize}, }, time::{Duration, Instant}, }; -use constants::{encoding::VideoEncoderType, supported_games::SupportedGames}; +use constants::{encoding::VideoEncoderType, unsupported_games::UnsupportedGames}; use egui_wgpu::wgpu; use tokio::sync::{broadcast, mpsc}; @@ -33,9 +33,35 @@ pub struct AppState { pub is_out_of_date: AtomicBool, pub play_time_state: RwLock, pub last_foregrounded_game: RwLock>, - pub supported_games: RwLock, + /// The exe name (e.g. "game.exe") of the last application that was recognised as recordable. + /// Used by the games settings UI to offer per-game configuration. + pub last_recordable_game: RwLock>, + pub unsupported_games: RwLock, + /// Offline mode state + pub offline: OfflineState, +} + +/// State for offline mode and backoff retry logic +pub struct OfflineState { /// Flag for offline mode - skips API server calls when enabled - pub offline_mode: AtomicBool, + pub mode: AtomicBool, + /// Whether offline backoff retry is currently active + pub backoff_active: AtomicBool, + /// Timestamp (as seconds since UNIX epoch) of when the next offline retry will occur + pub next_retry_time: AtomicU64, + /// Current retry count for offline backoff (used to display in UI) + pub retry_count: AtomicU32, +} + +impl Default for OfflineState { + fn default() -> Self { + Self { + mode: AtomicBool::new(false), + backoff_active: AtomicBool::new(false), + next_retry_time: AtomicU64::new(0), + retry_count: AtomicU32::new(0), + } + } } impl AppState { pub fn new( @@ -60,8 +86,9 @@ impl AppState { is_out_of_date: AtomicBool::new(false), play_time_state: RwLock::new(PlayTimeTracker::load()), last_foregrounded_game: RwLock::new(None), - supported_games: RwLock::new(SupportedGames::load_from_embedded()), - offline_mode: AtomicBool::new(false), + last_recordable_game: RwLock::new(None), + unsupported_games: RwLock::new(UnsupportedGames::load_from_embedded()), + offline: OfflineState::default(), }; tracing::debug!("AppState::new() complete"); state @@ -143,7 +170,7 @@ pub enum AsyncRequest { PauseUpload, OpenDataDump, OpenLog, - UpdateSupportedGames(SupportedGames), + UpdateUnsupportedGames(UnsupportedGames), LoadUploadStats, LoadLocalRecordings, DeleteAllInvalidRecordings, @@ -171,6 +198,10 @@ pub enum AsyncRequest { enabled: bool, offline_reason: Option, }, + /// Attempt to go online with backoff - starts backoff if not active, or retries if active + OfflineBackoffAttempt, + /// Cancel the offline mode backoff retry loop + CancelOfflineBackoff, } /// A message sent to the UI thread, usually in response to some action taken in another thread diff --git a/src/config.rs b/src/config.rs index b510043..94fee39 100644 --- a/src/config.rs +++ b/src/config.rs @@ -306,7 +306,7 @@ impl EncoderSettings { VideoEncoderType::Amf => self.amf.apply_to_data_updater(updater), VideoEncoderType::Qsv => self.qsv.apply_to_data_updater(updater), }; - updater.update()?; + updater.apply()?; Ok(data) } diff --git a/src/record/obs_embedded_recorder.rs b/src/record/obs_embedded_recorder.rs index ec0c8c4..2759685 100644 --- a/src/record/obs_embedded_recorder.rs +++ b/src/record/obs_embedded_recorder.rs @@ -16,17 +16,18 @@ use constants::{FPS, RECORDING_HEIGHT, RECORDING_WIDTH, encoding::VideoEncoderTy use windows::Win32::Foundation::HWND; use libobs_simple::sources::{ - ObsObjectUpdater, ObsSourceBuilder, + ObsEitherSource, ObsObjectUpdater, ObsSourceBuilder, windows::{ - GameCaptureSourceBuilder, GameCaptureSourceUpdater, ObsGameCaptureMode, - WindowCaptureSourceBuilder, WindowCaptureSourceUpdater, WindowInfo, + GameCaptureSource, GameCaptureSourceBuilder, ObsGameCaptureMode, ObsHookableSourceTrait, + WindowCaptureSource, WindowCaptureSourceBuilder, WindowInfo, }, }; use libobs_wrapper::{ context::ObsContext, data::{ - ObsDataGetters as _, - output::ObsOutputRef, + ObsDataGetters as _, ObsDataSetters, + object::ObsObjectTrait, + output::{ObsOutputRef, ObsOutputTrait}, video::{ObsVideoInfo, ObsVideoInfoBuilder}, }, encoders::{ @@ -34,10 +35,9 @@ use libobs_wrapper::{ }, enums::ObsScaleType, logger::ObsLogger, - scenes::ObsSceneRef, - sources::ObsSourceRef, + scenes::{ObsSceneItemRef, ObsSceneRef, SceneItemExtSceneTrait, SceneItemTrait}, unsafe_send::SendableComp, - utils::{AudioEncoderInfo, ObsPath, OutputInfo, VideoEncoderInfo, traits::ObsUpdatable}, + utils::{AudioEncoderInfo, ObsPath, OutputInfo, VideoEncoderInfo}, }; use crate::{ @@ -292,7 +292,7 @@ struct RecorderState { adapter_index: usize, skipped_frames: Arc>>, output: ObsOutputRef, - source: Option, + scene_item: Option>>, last_encoder_settings: Option, was_hooked: Arc, last_video_encoder_type: Option, @@ -377,7 +377,7 @@ impl RecorderState { adapter_index, skipped_frames, output, - source: None, + scene_item: None, last_encoder_settings: None, was_hooked: Arc::new(AtomicBool::new(false)), last_video_encoder_type: None, @@ -408,7 +408,7 @@ impl RecorderState { scene } else { tracing::info!("Creating new scene"); - self.obs_context.scene(OWL_SCENE_NAME)? + self.obs_context.scene(OWL_SCENE_NAME, Some(0))? }; self.obs_context @@ -417,21 +417,18 @@ impl RecorderState { let source_creation_state = SourceCreationState { use_window_capture: request.game_config.use_window_capture, }; - let source = prepare_source( + let scene_item = prepare_source( &mut self.obs_context, &request.game_exe, request.hwnd.0, &mut scene, - self.source.take(), + self.scene_item.take(), &source_creation_state, self.last_source_creation_state.as_ref(), )?; - // Register the source - scene.set_to_channel(0)?; - // Ensure the source takes up the entire scene - scene.fit_source_to_screen(&source)?; + scene_item.fit_source_to_screen()?; // Register the video encoder with encoder-specific settings let video_encoder_data = self.obs_context.data()?; @@ -486,18 +483,19 @@ impl RecorderState { // output let mut start_signal_rx = self .output - .signal_manager() + .signals() .on_start() .context("failed to register output on_start signal")?; let mut stop_signal_rx = self .output - .signal_manager() + .signals() .on_stop() .context("failed to register output on_stop signal")?; // source - let mut hook_signal_rx = source - .signal_manager() + let mut hook_signal_rx = scene_item + .inner_source() + .source_specific_signals() .on_hooked() .context("failed to register source on_hooked signal")?; @@ -571,7 +569,7 @@ impl RecorderState { self.output.start()?; - self.source = Some(source); + self.scene_item = Some(scene_item); self.last_application = Some((request.game_exe.clone(), request.hwnd)); self.last_source_creation_state = Some(source_creation_state); self.is_recording = true; @@ -637,9 +635,9 @@ impl RecorderState { { tracing::warn!("Game no longer open, removing source"); if let Some(mut scene) = self.obs_context.get_scene(OWL_SCENE_NAME)? - && let Some(source) = self.source.take() + && let Some(scene_item) = self.scene_item.take() { - scene.remove_source(&source)?; + scene.remove_scene_item(scene_item)?; self.last_application = None; } } @@ -708,23 +706,25 @@ fn prepare_source( game_exe: &str, hwnd: HWND, scene: &mut ObsSceneRef, - mut last_source: Option, + mut last_scene_item: Option< + ObsSceneItemRef>, + >, state: &SourceCreationState, last_state: Option<&SourceCreationState>, -) -> Result { +) -> Result>> { let capture_audio = true; // Check if source creation state changed - if so, we can't reuse the old source if let Some(last) = last_state && last != state - && last_source.is_some() + && last_scene_item.is_some() { tracing::info!( "Source creation state changed ({last:?} -> {state:?}), discarding old source", ); - if let Some(source) = last_source.take() { + if let Some(scene_item) = last_scene_item.take() { tracing::info!("Removing old source"); - dbg!(scene.remove_source(&source))?; + dbg!(scene.remove_scene_item(scene_item))?; tracing::info!("Old source removed"); } } @@ -737,23 +737,34 @@ fn prepare_source( // capture full screen. if this is set to true there's black borders around the window capture. let client_area = false; - if let Some(mut source) = last_source.take() { - tracing::info!("Reusing existing window capture source"); - source - .create_updater::()? - .set_window(&window) - .set_capture_audio(capture_audio)? - .set_client_area(client_area) - .update()?; - Ok(source) - } else { + if let Some(mut scene_item) = last_scene_item.take() { + if let ObsEitherSource::Left(window_source) = scene_item.inner_source_mut() { + tracing::info!("Reusing existing window capture source"); + window_source + .create_updater()? + .set_window(&window) + .set_capture_audio(capture_audio)? + .set_client_area(client_area) + .update()?; + return Ok(scene_item); + } else { + // Source type mismatch - remove old source and create new one below + tracing::info!("Source type mismatch (expected window capture), recreating"); + scene.remove_scene_item(scene_item)?; + } + } + + { tracing::info!("Creating new window capture source"); - obs_context + let window_source = obs_context .source_builder::(OWL_WINDOW_CAPTURE_NAME)? .set_window(&window) .set_capture_audio(capture_audio)? .set_client_area(client_area) - .add_to_scene(scene) + .build()?; + + let source = ObsEitherSource::Left(window_source); + scene.add_source(source) } } else { let window = find_game_capture_window(Some(game_exe), hwnd)?; @@ -766,16 +777,24 @@ fn prepare_source( let capture_mode = ObsGameCaptureMode::CaptureSpecificWindow; - if let Some(mut source) = last_source.take() { - tracing::info!("Reusing existing game capture source"); - source - .create_updater::()? - .set_capture_mode(capture_mode) - .set_window_raw(window.obs_id.as_str()) - .set_capture_audio(capture_audio)? - .update()?; - Ok(source) - } else { + if let Some(mut scene_item) = last_scene_item.take() { + if let ObsEitherSource::Right(game_source) = scene_item.inner_source_mut() { + tracing::info!("Reusing existing game capture source"); + game_source + .create_updater()? + .set_capture_mode(capture_mode) + .set_window_raw(window.obs_id.as_str()) + .set_capture_audio(capture_audio)? + .update()?; + return Ok(scene_item); + } else { + // Source type mismatch - remove old source and create new one below + tracing::info!("Source type mismatch (expected game capture), recreating"); + scene.remove_scene_item(scene_item)?; + } + } + + { tracing::info!("Creating new game capture source"); if GameCaptureSourceBuilder::is_window_in_use_by_other_instance(window.pid)? { @@ -785,12 +804,16 @@ fn prepare_source( ); } - obs_context + let source = obs_context .source_builder::(OWL_GAME_CAPTURE_NAME)? .set_capture_mode(capture_mode) .set_window(&window) .set_capture_audio(capture_audio)? - .add_to_scene(scene) + .build()?; + + let source = ObsEitherSource::Right(source); + + scene.add_source(source) } }; diff --git a/src/record/recorder.rs b/src/record/recorder.rs index 3b96073..4654ae2 100644 --- a/src/record/recorder.rs +++ b/src/record/recorder.rs @@ -24,7 +24,9 @@ use crate::{ recording::{Recording, RecordingParams}, }, }; -use constants::{MIN_FREE_SPACE_MB, encoding::VideoEncoderType, supported_games::SupportedGames}; +use constants::{ + MIN_FREE_SPACE_MB, encoding::VideoEncoderType, unsupported_games::UnsupportedGames, +}; #[async_trait::async_trait(?Send)] pub trait VideoRecorder { @@ -125,7 +127,7 @@ impl Recorder { pub async fn start( &mut self, input_capture: &InputCapture, - supported_games: &SupportedGames, + unsupported_games: &UnsupportedGames, ) -> Result<()> { if self.recording.is_some() { return Ok(()); @@ -175,8 +177,8 @@ impl Recorder { .next() .unwrap_or(&game_exe) .to_lowercase(); - if supported_games.get(&game_exe_without_extension).is_none() { - bail!("{game_exe} is not supported!"); + if let Some(unsupported) = unsupported_games.get(&game_exe_without_extension) { + bail!("{game_exe} is not supported: {}", unsupported.reason); } if let Err(error) = is_process_game_shaped(pid) { diff --git a/src/tokio_thread.rs b/src/tokio_thread.rs index f93dcdb..dd9a1cf 100644 --- a/src/tokio_thread.rs +++ b/src/tokio_thread.rs @@ -12,17 +12,20 @@ use crate::{ upload, util::version::is_version_newer, }; +use backoff::{ExponentialBackoff, backoff::Backoff}; use std::{ collections::HashMap, io::Cursor, path::PathBuf, - sync::Arc, + sync::{Arc, atomic::Ordering}, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; use color_eyre::{Result, eyre::Context}; -use constants::{GH_ORG, GH_REPO, MAX_FOOTAGE, MAX_IDLE_DURATION, supported_games::SupportedGames}; +use constants::{ + GH_ORG, GH_REPO, MAX_FOOTAGE, MAX_IDLE_DURATION, unsupported_games::UnsupportedGames, +}; use game_process::does_process_exist; use input_capture::{Event, InputCapture}; use rodio::{Decoder, Sink, Source}; @@ -136,6 +139,10 @@ async fn main( actively_recording_window: None, }; + // Offline backoff state + let mut offline_backoff: Option = None; + let mut offline_backoff_handle: Option> = None; + // Initial async requests to GitHub/server tracing::debug!("Spawning startup requests task"); tokio::spawn(startup_requests(app_state.clone())); @@ -184,44 +191,38 @@ async fn main( }; match e { AsyncRequest::ValidateApiKey { api_key } => { - // Check if offline mode is enabled - if app_state.offline_mode.load(std::sync::atomic::Ordering::SeqCst) { - tracing::info!("Offline mode enabled, skipping API key validation"); - app_state.async_request_tx.send(AsyncRequest::SetOfflineMode { enabled: true, offline_reason: None }).await.ok(); - } else { - let response = api_client.validate_api_key(&api_key).await; - tracing::info!("Received response from API key validation: {response:?}"); - - match response { - Err(e) if e.is_network_error() => { - // Network error or server unavailable (502/503/504) - switch to offline mode - tracing::warn!("API server unavailable, switching to offline mode: {e}"); - app_state.async_request_tx.send(AsyncRequest::SetOfflineMode { enabled: true, offline_reason: Some(e.to_string()) }).await.ok(); - } - Err(e) => { - // API key validation failed - don't switch to offline mode - tracing::warn!("API key validation failed: {e}"); - app_state - .ui_update_tx - .send(UiUpdate::UpdateUserId(Err(e.to_string()))) - .ok(); - } - Ok(user_id) => { - valid_api_key_and_user_id = Some((api_key.clone(), user_id.clone())); - app_state - .ui_update_tx - .send(UiUpdate::UpdateUserId(Ok(user_id))) - .ok(); - - app_state.async_request_tx.send(AsyncRequest::LoadUploadStats).await.ok(); - } + let response = api_client.validate_api_key(&api_key).await; + tracing::info!("Received response from API key validation: {response:?}"); + + match response { + Err(e) if e.is_network_error() => { + // Network error or server unavailable (502/503/504) - switch to offline mode + tracing::warn!("API server unavailable, switching to offline mode: {e}"); + app_state.async_request_tx.send(AsyncRequest::SetOfflineMode { enabled: true, offline_reason: Some(e.to_string()) }).await.ok(); + } + Err(e) => { + // API key validation failed - don't switch to offline mode + tracing::warn!("API key validation failed: {e}"); + app_state + .ui_update_tx + .send(UiUpdate::UpdateUserId(Err(e.to_string()))) + .ok(); + } + Ok(user_id) => { + valid_api_key_and_user_id = Some((api_key.clone(), user_id.clone())); + app_state + .ui_update_tx + .send(UiUpdate::UpdateUserId(Ok(user_id))) + .ok(); + + app_state.async_request_tx.send(AsyncRequest::LoadUploadStats).await.ok(); } - // no matter if offline or online, local recordings should be loaded - app_state.async_request_tx.send(AsyncRequest::LoadLocalRecordings).await.ok(); } + // no matter if offline or online, local recordings should be loaded + app_state.async_request_tx.send(AsyncRequest::LoadLocalRecordings).await.ok(); } AsyncRequest::UploadData => { - if app_state.offline_mode.load(std::sync::atomic::Ordering::SeqCst) { + if app_state.offline.mode.load(Ordering::SeqCst) { tracing::info!("Offline mode enabled, skipping upload"); app_state .ui_update_tx @@ -232,11 +233,11 @@ async fn main( } } AsyncRequest::PauseUpload => { - app_state.upload_pause_flag.store(true, std::sync::atomic::Ordering::SeqCst); + app_state.upload_pause_flag.store(true, Ordering::SeqCst); // Clear the auto-upload queue when pausing let prev_queue_count = app_state .auto_upload_queue_count - .load(std::sync::atomic::Ordering::SeqCst); + .load(Ordering::SeqCst); tracing::info!( "Upload pause requested, auto-upload queue cleared (was {prev_queue_count} recordings)" ); @@ -256,18 +257,17 @@ async fn main( AsyncRequest::OpenFolder(path) => { opener::open(&path).ok(); } - AsyncRequest::UpdateSupportedGames(new_games) => { - let mut supported_games = app_state.supported_games.write().unwrap(); - let old_game_count = supported_games.games.len(); - *supported_games = new_games; + AsyncRequest::UpdateUnsupportedGames(new_games) => { + let mut unsupported_games = app_state.unsupported_games.write().unwrap(); + let old_game_count = unsupported_games.games.len(); + *unsupported_games = new_games; tracing::info!( - "Updated supported games: {old_game_count} -> {} total, {} installed", - supported_games.games.len(), - supported_games.installed().count() + "Updated unsupported games: {old_game_count} -> {} total", + unsupported_games.games.len(), ); } AsyncRequest::LoadUploadStats => { - if app_state.offline_mode.load(std::sync::atomic::Ordering::SeqCst) { + if app_state.offline.mode.load(Ordering::SeqCst) { tracing::info!("Offline mode enabled, skipping upload stats load"); // Don't send any update - UI will show no upload stats } else { @@ -444,7 +444,7 @@ async fn main( // Subtract the number of recordings that were just uploaded from the queue let prev_count = app_state .auto_upload_queue_count - .load(std::sync::atomic::Ordering::SeqCst); + .load(Ordering::SeqCst); let new_count = prev_count.saturating_sub(uploaded_count); tracing::info!( @@ -472,7 +472,7 @@ async fn main( AsyncRequest::ClearAutoUploadQueue => { let prev_count = app_state .auto_upload_queue_count - .load(std::sync::atomic::Ordering::SeqCst); + .load(Ordering::SeqCst); tracing::info!( "Auto-upload queue cleared (was {} recordings)", prev_count @@ -481,16 +481,171 @@ async fn main( } AsyncRequest::SetOfflineMode { enabled, offline_reason } => { tracing::info!("Setting offline mode to {}", enabled); - app_state.offline_mode.store(enabled, std::sync::atomic::Ordering::SeqCst); - let mut offline_str = "Offline".to_string(); + app_state.offline.mode.store(enabled, Ordering::SeqCst); + + match (enabled, &offline_reason) { + (true, Some(reason)) => { + tracing::info!("Offline mode enabled: {}", reason); + app_state.ui_update_tx.send(UiUpdate::UpdateUserId(Ok(format!("Offline ({reason})")))).ok(); + // trigger backoff attempts since offline mode enabled with error + app_state.async_request_tx.send(AsyncRequest::OfflineBackoffAttempt).await.ok(); + }, + (true, None) => { + tracing::info!("Offline mode enabled by user without error"); + app_state.ui_update_tx.send(UiUpdate::UpdateUserId(Ok("Offline".to_string()))).ok(); + }, + (false, _) => { + tracing::info!("Offline mode disabled, going online"); + let api_key = app_state.config.read().unwrap().credentials.api_key.clone(); + app_state.ui_update_tx.send(UiUpdate::UpdateUserId(Ok("Authenticating...".to_string()))).ok(); + app_state.async_request_tx.send(AsyncRequest::CancelOfflineBackoff).await.ok(); + app_state.async_request_tx.send(AsyncRequest::ValidateApiKey { api_key }).await.ok(); + // Load data now that we're online + app_state.async_request_tx.send(AsyncRequest::LoadUploadStats).await.ok(); + app_state.async_request_tx.send(AsyncRequest::LoadLocalRecordings).await.ok(); + }, + } + } + AsyncRequest::CancelOfflineBackoff => { + tracing::info!("Cancelling offline backoff retry loop"); + if let Some(handle) = offline_backoff_handle.take() { + handle.abort(); + } + offline_backoff = None; + app_state.offline.backoff_active.store(false, Ordering::SeqCst); + app_state.offline.retry_count.store(0, Ordering::SeqCst); + app_state.offline.next_retry_time.store(0, Ordering::SeqCst); + } + AsyncRequest::OfflineBackoffAttempt => { + let backoff_active = app_state.offline.backoff_active.load(Ordering::SeqCst); + let offline_mode = app_state.offline.mode.load(Ordering::SeqCst); + + match (backoff_active, offline_mode) { + // Not offline - nothing to do + (_, false) => {} + + // Offline but backoff not started - initialize backoff and schedule first retry + (false, true) => { + tracing::info!("Starting offline backoff retry loop"); + + // Create new backoff with ~2.5 min initial, doubling, max 60 min + // but never stops since max_elapsed_time is None. At max every hour + // it will retry. + let mut backoff = ExponentialBackoff { + initial_interval: Duration::from_secs(150), + current_interval: Duration::from_secs(150), // Must match initial_interval + max_interval: Duration::from_secs(3600), + max_elapsed_time: None, + multiplier: 2.0, + randomization_factor: 0.1, + ..Default::default() + }; + + // Get first interval and schedule retry + if let Some(delay) = backoff.next_backoff() { + let next_retry_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + + delay.as_secs(); + + app_state.offline.backoff_active.store(true, Ordering::SeqCst); + app_state.offline.next_retry_time.store(next_retry_time, Ordering::SeqCst); + app_state.offline.retry_count.store(0, Ordering::SeqCst); + + offline_backoff = Some(backoff); + + // Cancel any existing handle + if let Some(handle) = offline_backoff_handle.take() { + handle.abort(); + } - if let Some(reason) = offline_reason { - offline_str.push_str(&format!(" ({reason})")); + // Schedule the retry + tracing::info!("Scheduling offline retry in {:?}", delay); + offline_backoff_handle = Some(tokio::spawn({ + let tx = app_state.async_request_tx.clone(); + async move { + tokio::time::sleep(delay).await; + tx.send(AsyncRequest::OfflineBackoffAttempt).await.ok(); + } + })); + } + } + + // Backoff active and still offline - attempt API validation + (true, true) => { + let retry_count = app_state.offline.retry_count.load(Ordering::SeqCst); + tracing::info!("Offline backoff retry #{} - attempting API validation", retry_count + 1); + let api_key = app_state.config.read().unwrap().credentials.api_key.clone(); + // Attempt validation + let response = api_client.validate_api_key(&api_key).await; + match response { + Ok(user_id) => { + // Successful server response, cancel backoff and go online + tracing::info!("Offline backoff retry succeeded, going online"); + app_state.offline.mode.store(false, Ordering::SeqCst); + valid_api_key_and_user_id = Some((api_key.clone(), user_id.clone())); + app_state.ui_update_tx.send(UiUpdate::UpdateUserId(Ok(user_id))).ok(); + + // Cancel backoff + app_state.async_request_tx.send(AsyncRequest::SetOfflineMode { enabled: false, offline_reason: None }).await.ok(); + } + Err(e) if e.is_network_error() => { + // Still offline, schedule next retry + tracing::warn!("Offline backoff retry #{} failed (network error): {}", retry_count + 1, e); + + let new_retry_count = retry_count + 1; + app_state.offline.retry_count.store(new_retry_count, Ordering::SeqCst); + + // Get next backoff delay (None if max_elapsed_time exceeded) + let next_delay = offline_backoff.as_mut().and_then(|b| b.next_backoff()); + + if let Some(delay) = next_delay { + let next_retry_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + + delay.as_secs(); + app_state.offline.next_retry_time.store(next_retry_time, Ordering::SeqCst); + + // Schedule next retry + tracing::info!("Scheduling next offline retry in {:?}", delay); + offline_backoff_handle = Some(tokio::spawn({ + let tx = app_state.async_request_tx.clone(); + async move { + tokio::time::sleep(delay).await; + tx.send(AsyncRequest::OfflineBackoffAttempt).await.ok(); + } + })); + } else { + // Backoff exhausted (max_elapsed_time reached) - stop retrying + // This should never happen since we set max_elapsed_time to None, but just + // in case in the future we change that behaviour we don't get footgunned + tracing::warn!("Offline backoff exhausted, stopping retries"); + offline_backoff = None; + app_state.offline.backoff_active.store(false, Ordering::SeqCst); + app_state.offline.retry_count.store(0, Ordering::SeqCst); + app_state.offline.next_retry_time.store(0, Ordering::SeqCst); + } + } + Err(e) => { + // Non-network error (e.g., invalid API key) - stop backoff + tracing::warn!("Offline backoff retry got non-network error, stopping: {}", e); + + // Cancel backoff but stay offline + if let Some(handle) = offline_backoff_handle.take() { + handle.abort(); + } + offline_backoff = None; + app_state.offline.backoff_active.store(false, Ordering::SeqCst); + app_state.offline.retry_count.store(0, Ordering::SeqCst); + app_state.offline.next_retry_time.store(0, Ordering::SeqCst); + + app_state.ui_update_tx.send(UiUpdate::UpdateUserId(Err(e.to_string()))).ok(); + } + } + } } - app_state - .ui_update_tx - .send(UiUpdate::UpdateUserId(Ok(offline_str))) - .ok(); } } }, @@ -503,7 +658,14 @@ async fn main( tracing::error!(e=?e, "Failed to flush input events"); } // Check foregrounded game - *app_state.last_foregrounded_game.write().unwrap() = get_foregrounded_game(&app_state.supported_games.read().unwrap(), &state.recorder); + let foregrounded = get_foregrounded_game(&app_state.unsupported_games.read().unwrap(), &state.recorder); + if let Some(ref fg) = foregrounded + && fg.is_recordable() + && fg.exe_name.is_some() + { + *app_state.last_recordable_game.write().unwrap() = fg.exe_name.clone(); + } + *app_state.last_foregrounded_game.write().unwrap() = foregrounded; // Tick state machine state.tick().await; // Periodically force the UI to rerender so that it will process events, even if not visible @@ -563,11 +725,7 @@ impl State { self.last_active = Instant::now(); if let Err(e) = match (&self.recording_state, e.key_press_keycode()) { (RecordingState::Idle, key) if key == start_key => { - if self - .app_state - .is_out_of_date - .load(std::sync::atomic::Ordering::SeqCst) - { + if self.app_state.is_out_of_date.load(Ordering::SeqCst) { error_message_box(concat!( "You are using an outdated version of OWL Control. ", "Please update to the latest version to continue.\n\n", @@ -727,11 +885,11 @@ impl State { (RecordingState::Idle | RecordingState::Paused { .. }, RecordingState::Recording) => { // Start recording from Idle or Paused state let honk = self.app_state.config.read().unwrap().preferences.honk; - let supported_games = self.app_state.supported_games.read().unwrap().clone(); + let unsupported_games = self.app_state.unsupported_games.read().unwrap().clone(); start_recording_safely( &mut self.recorder, &self.input_capture, - &supported_games, + &unsupported_games, Some((&self.sink, honk, &self.app_state)), &mut self.cue_cache, ) @@ -843,7 +1001,7 @@ impl State { // Restart the currently active recording // Here we intentionally set honk to false, we don't want audio cue to occur // on an intended recording restart and confuse the user - let supported_games = self.app_state.supported_games.read().unwrap().clone(); + let unsupported_games = self.app_state.unsupported_games.read().unwrap().clone(); stop_recording_with_notification( &mut self.recorder, &self.input_capture, @@ -854,7 +1012,7 @@ impl State { start_recording_safely( &mut self.recorder, &self.input_capture, - &supported_games, + &unsupported_games, Some((&self.sink, false, &self.app_state)), &mut self.cue_cache, ) @@ -885,17 +1043,14 @@ impl State { return; } - let upload_in_progress = self - .app_state - .upload_in_progress - .load(std::sync::atomic::Ordering::SeqCst); + let upload_in_progress = self.app_state.upload_in_progress.load(Ordering::SeqCst); if upload_in_progress { // Upload already in progress, queue this one let current_count = self .app_state .auto_upload_queue_count - .load(std::sync::atomic::Ordering::SeqCst); + .load(Ordering::SeqCst); let new_count = current_count + 1; tracing::info!( "Auto-upload: upload in progress, queued recording (queue count: {})", @@ -922,11 +1077,11 @@ impl State { async fn start_recording_safely( recorder: &mut Recorder, input_capture: &InputCapture, - supported_games: &SupportedGames, + unsupported_games: &UnsupportedGames, notification_state: Option<(&Sink, bool, &AppState)>, cue_cache: &mut HashMap>, ) -> Result<()> { - if let Err(e) = recorder.start(input_capture, supported_games).await { + if let Err(e) = recorder.start(input_capture, unsupported_games).await { tracing::error!(e=?e, "Failed to start recording"); error_message_box(&e.to_string()); recorder.stop(input_capture).await.ok(); @@ -1023,7 +1178,7 @@ fn play_cue( fn set_auto_upload_queue_count(app_state: &AppState, count: usize) { app_state .auto_upload_queue_count - .store(count, std::sync::atomic::Ordering::SeqCst); + .store(count, Ordering::SeqCst); app_state .ui_update_tx .send(UiUpdate::UpdateAutoUploadQueueCount(count)) @@ -1047,21 +1202,19 @@ fn is_window_focused(hwnd: HWND) -> bool { } fn get_foregrounded_game( - supported_games: &SupportedGames, + unsupported_games: &UnsupportedGames, recorder: &Recorder, ) -> Option { let (exe_name, _, hwnd) = crate::record::get_foregrounded_game().ok().flatten()?; - // Check if game is supported let exe_without_ext = std::path::Path::new(&exe_name) .file_stem() .and_then(|s| s.to_str()) .unwrap_or(&exe_name) .to_lowercase(); - let supported_game = supported_games.get(&exe_without_ext.clone()); - let unsupported_reason = if supported_game.is_none() { - Some("Not on the games list.".to_string()) + let unsupported_reason = if let Some(unsupported) = unsupported_games.get(&exe_without_ext) { + Some(unsupported.reason.to_string()) } else if !recorder.is_window_capturable(hwnd) { Some( "Recorder cannot capture this window. Try running OWL Control in admin mode." @@ -1203,20 +1356,20 @@ async fn move_recordings_folder(app_state: Arc, from: PathBuf, to: Pat async fn startup_requests(app_state: Arc) { if cfg!(debug_assertions) { - tracing::info!("Skipping fetch of supported games in dev/debug build"); + tracing::info!("Skipping fetch of unsupported games in dev/debug build"); } else { tokio::spawn({ let async_request_tx = app_state.async_request_tx.clone(); async move { - match get_supported_games().await { + match get_unsupported_games().await { Ok(games) => { async_request_tx - .send(AsyncRequest::UpdateSupportedGames(games)) + .send(AsyncRequest::UpdateUnsupportedGames(games)) .await .ok(); } Err(e) => { - tracing::error!(e=?e, "Failed to get supported games from GitHub"); + tracing::error!(e=?e, "Failed to get unsupported games from GitHub"); } } } @@ -1230,20 +1383,15 @@ async fn startup_requests(app_state: Arc) { }); } -async fn get_supported_games() -> Result { - let text = reqwest::get(format!("https://raw.githubusercontent.com/{GH_ORG}/{GH_REPO}/refs/heads/main/crates/constants/src/supported_games.json")) +async fn get_unsupported_games() -> Result { + let text = reqwest::get(format!("https://raw.githubusercontent.com/{GH_ORG}/{GH_REPO}/refs/heads/main/crates/constants/src/unsupported_games.json")) .await - .context("Failed to request supported games from GitHub")? + .context("Failed to request unsupported games from GitHub")? .text() .await - .context("Failed to get text of supported games from GitHub")?; + .context("Failed to get text of unsupported games from GitHub")?; - // Use spawn_blocking since load_from_str now does Steam detection (blocking I/O) - tokio::task::spawn_blocking(move || { - SupportedGames::load_from_str(&text).context("Failed to parse supported games from GitHub") - }) - .await - .unwrap() + UnsupportedGames::load_from_str(&text).context("Failed to parse unsupported games from GitHub") } async fn check_for_updates(app_state: Arc) -> Result<()> { @@ -1309,9 +1457,7 @@ async fn check_for_updates(app_state: Arc) -> Result<()> { })) .ok(); - app_state - .is_out_of_date - .store(true, std::sync::atomic::Ordering::SeqCst); + app_state.is_out_of_date.store(true, Ordering::SeqCst); } Ok(()) diff --git a/src/ui/consent.md b/src/ui/consent.md index 1f1f5d5..f8bfbcd 100644 --- a/src/ui/consent.md +++ b/src/ui/consent.md @@ -1,8 +1,8 @@ -# Research Study: Wayfarer Labs Interactive Game Data Pilot +# Research Study: Overworld Interactive Game Data Pilot Principal Investigators: Louis Castricato, Shahbuland Matiana -Contact: louis@wayfarerlabs.ai, shahbuland@wayfarerlabs.ai +Contact: louis@overworld.ai, shahbuland@overworld.ai ## Purpose of Study @@ -47,7 +47,7 @@ Your participation is entirely voluntary. You may: ## Questions or Concerns -For questions about this research, contact shahbuland@wayfarerlabs.ai +For questions about this research, contact shahbuland@overworld.ai ## Content Policy & Legal Terms diff --git a/src/ui/views/main/mod.rs b/src/ui/views/main/mod.rs index ae2443c..1fc4877 100644 --- a/src/ui/views/main/mod.rs +++ b/src/ui/views/main/mod.rs @@ -154,7 +154,7 @@ impl App { }); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.hyperlink_to( - RichText::new("Wayfarer Labs") + RichText::new("Overworld") .italics() .color(Color32::LIGHT_BLUE), "https://wayfarerlabs.ai/", @@ -187,12 +187,16 @@ impl App { ); // Games Window - windows::games::window( - ctx, - &mut self.main_view_state.games_window, - &self.app_state.supported_games.read().unwrap(), - &mut self.local_preferences, - ); + { + let last_recordable = self.app_state.last_recordable_game.read().unwrap().clone(); + windows::games::window( + ctx, + &mut self.main_view_state.games_window, + &self.app_state.unsupported_games.read().unwrap(), + &mut self.local_preferences, + last_recordable.as_deref(), + ); + } } } @@ -200,45 +204,70 @@ fn account_section(ui: &mut Ui, app: &mut App) { ui.horizontal(|ui| { ui.label(RichText::new("Account").size(18.0).strong()); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - let offline_mode = app.app_state.offline_mode.load(Ordering::SeqCst); + let offline_mode = app.app_state.offline.mode.load(Ordering::SeqCst); let upload_in_progress = app.app_state.upload_in_progress.load(Ordering::SeqCst); + let backoff_active = app.app_state.offline.backoff_active.load(Ordering::SeqCst); - let (icon, color, tooltip) = if upload_in_progress { + let (icon, color, tooltip, is_disabled) = if upload_in_progress { ( "πŸ“‘", Color32::from_rgb(128, 128, 128), - "Cannot toggle offline mode while upload is in progress", + "Cannot toggle offline mode while upload is in progress".to_string(), + true, + ) + } else if backoff_active { + // Calculate time remaining until next retry + let next_retry_time = app.app_state.offline.next_retry_time.load(Ordering::SeqCst); + let retry_count = app.app_state.offline.retry_count.load(Ordering::SeqCst); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let time_remaining = if next_retry_time > now { + let secs = next_retry_time - now; + format!("{}m {}s", secs / 60, secs % 60) + } else { + "retrying...".to_string() + }; + + ( + "πŸ“‘", + Color32::from_rgb(200, 150, 50), // Orange for backoff + format!( + "Offline - auto-retry in {} (attempt #{})", + time_remaining, + retry_count + 1 + ), + true, // Disabled during backoff to prevent server spam ) } else if offline_mode { ( "πŸ“‘", Color32::from_rgb(180, 80, 80), - "Offline mode (click to go online)", + "Offline mode (click to go online)".to_string(), + false, ) } else { ( "πŸ“‘", Color32::from_rgb(100, 180, 100), - "Online mode (click to go offline)", + "Online mode (click to go offline)".to_string(), + false, ) }; let button = Button::new(RichText::new(icon).size(16.0).color(color)).frame(false); - let response = ui.add_enabled(!upload_in_progress, button); + let response = ui.add_enabled(!is_disabled, button); if response - .on_hover_text(tooltip) - .on_disabled_hover_text(tooltip) + .on_hover_text(&tooltip) + .on_disabled_hover_text(&tooltip) .clicked() { - app.app_state - .offline_mode - .store(!offline_mode, Ordering::SeqCst); - - // Trigger API key validation when toggling offline mode - // This allows switching between online and offline modes app.app_state .async_request_tx - .blocking_send(AsyncRequest::ValidateApiKey { - api_key: app.local_credentials.api_key.clone(), + .blocking_send(AsyncRequest::SetOfflineMode { + enabled: !offline_mode, + offline_reason: None, }) .ok(); } diff --git a/src/ui/views/main/upload_manager.rs b/src/ui/views/main/upload_manager.rs index f26784b..9bac45e 100644 --- a/src/ui/views/main/upload_manager.rs +++ b/src/ui/views/main/upload_manager.rs @@ -430,7 +430,8 @@ pub fn view( // Unreliable Connection Setting ui.add_space(5.0); let offline_mode = app_state - .offline_mode + .offline + .mode .load(std::sync::atomic::Ordering::SeqCst); ui.add_enabled_ui(!offline_mode, |ui| { ui.horizontal(|ui| { diff --git a/src/ui/views/main/windows/games.rs b/src/ui/views/main/windows/games.rs index cfcd5a4..a439130 100644 --- a/src/ui/views/main/windows/games.rs +++ b/src/ui/views/main/windows/games.rs @@ -1,8 +1,8 @@ use crate::config::{GameConfig, Preferences}; -use constants::supported_games::{SupportedGame, SupportedGames}; +use constants::unsupported_games::{InstalledGame, UnsupportedGames, detect_installed_games}; use egui::{ - Align, Align2, Button, CollapsingHeader, Color32, Context, CursorIcon, Frame, Label, Layout, - RichText, ScrollArea, Sense, Ui, Vec2, Window, vec2, + Align, Align2, Button, Color32, Context, CursorIcon, Frame, Label, Layout, RichText, + ScrollArea, Sense, Ui, Vec2, Window, vec2, }; const FONTSIZE: f32 = 13.0; @@ -13,10 +13,10 @@ const DEFAULT_HEIGHT: f32 = 600.0; pub struct GamesWindowState { pub open: bool, pub installed_list: egui_virtual_list::VirtualList, - pub uninstalled_list: egui_virtual_list::VirtualList, /// Currently open game settings window (stores the game name and primary exe) pub game_settings_open: Option, } + /// Identifies which game's settings window is open #[derive(Clone)] pub struct GameSettingsTarget { @@ -27,8 +27,9 @@ pub struct GameSettingsTarget { pub fn window( ctx: &Context, state: &mut GamesWindowState, - supported_games: &SupportedGames, + unsupported_games: &UnsupportedGames, preferences: &mut Preferences, + last_recordable_game: Option<&str>, ) { // Always render the game settings window if it's open game_settings_window(ctx, &mut state.game_settings_open, preferences); @@ -37,76 +38,68 @@ pub fn window( return; } - let (installed, uninstalled): (Vec<_>, Vec<_>) = - supported_games.games.iter().partition(|g| g.installed); + let installed = detect_installed_games(); + + // Filter out games whose names match entries in UnsupportedGames (case-insensitive) + let supported_installed: Vec<_> = installed + .into_iter() + .filter(|game| { + !unsupported_games + .games + .iter() + .any(|ug| ug.name.to_lowercase() == game.name.to_lowercase()) + }) + .collect(); let mut should_close = false; - let mut open_settings: Option = None; egui::Window::new("Games") .default_size([DEFAULT_WIDTH, DEFAULT_HEIGHT]) .resizable(true) .open(&mut state.open) .show(ctx, |ui| { - ScrollArea::vertical().show(ui, |ui| { - // Installed games section - if !installed.is_empty() { - CollapsingHeader::new(RichText::new("Installed via Steam").size(14.0).strong()) - .default_open(true) - .show(ui, |ui| { - state.installed_list.ui_custom_layout( - ui, - installed.len(), - |ui, index| { - if let Some(game) = installed.get(index) { - let result = game_entry(ui, game, preferences); - if result.launched { - should_close = true; - } - if result.open_settings { - open_settings = Some(GameSettingsTarget { - game_name: game.game.clone(), - binaries: game.binaries.clone(), - }); - } - 1 - } else { - 0 - } - }, - ); - }); + // Show a settings button for the last recordable game + if let Some(exe_name) = last_recordable_game { + let exe_without_ext = std::path::Path::new(exe_name) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or(exe_name); + + let has_custom_settings = preferences.games.contains_key(exe_without_ext); + let settings_label = if has_custom_settings { + format!("Configure {exe_name} (custom settings active)") + } else { + format!("Configure {exe_name}") + }; + + if ui.button(&settings_label).clicked() { + state.game_settings_open = Some(GameSettingsTarget { + game_name: exe_name.to_string(), + binaries: vec![exe_without_ext.to_lowercase()], + }); } - // Uninstalled games section - if !uninstalled.is_empty() { - CollapsingHeader::new( - RichText::new("Not installed via Steam").size(14.0).strong(), - ) - .default_open(true) - .show(ui, |ui| { - state.uninstalled_list.ui_custom_layout( - ui, - uninstalled.len(), - |ui, index| { - if let Some(game) = uninstalled.get(index) { - let result = game_entry(ui, game, preferences); - if result.launched { - should_close = true; - } - if result.open_settings { - open_settings = Some(GameSettingsTarget { - game_name: game.game.clone(), - binaries: game.binaries.clone(), - }); - } - 1 - } else { - 0 + ui.separator(); + } + + ScrollArea::vertical().show(ui, |ui| { + if supported_installed.is_empty() { + ui.label("No supported installed Steam games found."); + } else { + state.installed_list.ui_custom_layout( + ui, + supported_installed.len(), + |ui, index| { + if let Some(game) = supported_installed.get(index) { + if game_entry(ui, game) { + should_close = true; } - }, - ); - }); + 1 + } else { + 0 + } + }, + ); } }); }); @@ -114,32 +107,14 @@ pub fn window( if should_close { state.open = false; } - - if let Some(target) = open_settings { - state.game_settings_open = Some(target); - } -} - -struct GameEntryResult { - launched: bool, - open_settings: bool, } -fn game_entry(ui: &mut Ui, game: &SupportedGame, preferences: &Preferences) -> GameEntryResult { - let alpha = if game.installed { 1.0 } else { 0.7 }; - let mut result = GameEntryResult { - launched: false, - open_settings: false, - }; - - // Check if any binary has custom settings - let has_custom_settings = game - .binaries - .iter() - .any(|exe| preferences.games.contains_key(exe)); +/// Returns true if the game was launched (to close the window). +fn game_entry(ui: &mut Ui, game: &InstalledGame) -> bool { + let mut launched = false; Frame::new() - .fill(ui.visuals().faint_bg_color.gamma_multiply(alpha)) + .fill(ui.visuals().faint_bg_color) .inner_margin(4.0) .corner_radius(4.0) .show(ui, |ui| { @@ -149,9 +124,9 @@ fn game_entry(ui: &mut Ui, game: &SupportedGame, preferences: &Preferences) -> G let game_response = ui .add( Label::new( - RichText::new(&game.game) + RichText::new(&game.name) .size(FONTSIZE) - .color(ui.visuals().text_color().gamma_multiply(alpha)) + .color(ui.visuals().text_color()) .underline(), ) .sense(Sense::click()), @@ -159,52 +134,32 @@ fn game_entry(ui: &mut Ui, game: &SupportedGame, preferences: &Preferences) -> G .on_hover_cursor(CursorIcon::PointingHand) .on_hover_text("Open Steam store page"); if game_response.clicked() { - opener::open_browser(&game.url).ok(); + let url = format!("https://store.steampowered.com/app/{}/", game.steam_app_id); + opener::open_browser(&url).ok(); } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - // Launch button for installed games - if game.installed - && let Some(app_id) = game.steam_app_id - { - let response = ui - .add_sized( - vec2(60.0, 20.0), - Button::new( - RichText::new("Launch") - .size(FONTSIZE * 0.85) - .color(Color32::WHITE), - ) - .fill(Color32::from_rgb(60, 120, 180)), - ) - .on_hover_text("Launch game via Steam"); - if response.clicked() { - let steam_launch_url = format!("steam://rungameid/{app_id}"); - opener::open(&steam_launch_url).ok(); - result.launched = true; - } - } - - // Settings button - let settings_icon = if has_custom_settings { "βš™*" } else { "βš™" }; - let settings_response = ui + let response = ui .add_sized( - vec2(30.0, 20.0), + vec2(60.0, 20.0), Button::new( - RichText::new(settings_icon) + RichText::new("Launch") .size(FONTSIZE * 0.85) - .color(ui.visuals().text_color().gamma_multiply(alpha)), - ), + .color(Color32::WHITE), + ) + .fill(Color32::from_rgb(60, 120, 180)), ) - .on_hover_text("Game-specific settings"); - if settings_response.clicked() { - result.open_settings = true; + .on_hover_text("Launch game via Steam"); + if response.clicked() { + let steam_launch_url = format!("steam://rungameid/{}", game.steam_app_id); + opener::open(&steam_launch_url).ok(); + launched = true; } }); }); }); - result + launched } fn game_settings_window( diff --git a/src/upload/upload_tar.rs b/src/upload/upload_tar.rs index 5000f31..73ea0cc 100644 --- a/src/upload/upload_tar.rs +++ b/src/upload/upload_tar.rs @@ -1,8 +1,10 @@ use std::{ sync::{Arc, Mutex}, - time::{SystemTime, UNIX_EPOCH}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; +use backoff::{Error as BackoffError, ExponentialBackoff, future::retry_notify}; + use futures::TryStreamExt as _; use tokio::io::{AsyncReadExt as _, AsyncSeekExt as _}; @@ -39,7 +41,7 @@ pub enum UploadTarError { FailedToUploadChunk { chunk_number: u64, total_chunks: u64, - max_retries: u32, + retries: u32, error: UploadSingleChunkError, }, FailedToCompleteMultipartUpload(String), @@ -67,12 +69,12 @@ impl std::fmt::Display for UploadTarError { UploadTarError::FailedToUploadChunk { chunk_number, total_chunks, - max_retries, + retries, error, } => { write!( f, - "Failed to upload chunk {chunk_number}/{total_chunks} after {max_retries} attempts: {error:?}" + "Failed to upload chunk {chunk_number}/{total_chunks} after {retries} retries: {error:?}" ) } UploadTarError::FailedToCompleteMultipartUpload(message) => { @@ -281,69 +283,72 @@ pub async fn run( let chunk_data = buffer[..chunk_size].to_vec(); let chunk_hash = sha256::digest(&chunk_data); - const MAX_RETRIES: u32 = 5; - - for attempt in 1..=MAX_RETRIES { - // Store bytes_uploaded before attempting the chunk - let bytes_before_chunk = progress_sender.lock().unwrap().bytes_uploaded(); - - let chunk = Chunk { - data: &chunk_data, - hash: &chunk_hash, - number: chunk_number, - }; - - match upload_single_chunk( - chunk, - &api_client, - api_token, - &upload_id, - progress_sender.clone(), - &client, - ) - .await - { - Ok(etag) => { - progress_sender.lock().unwrap().send(); - - // Update progress state with new chunk and save to file - guard.paused_mut().mutate_upload_progress(|progress| { - progress - .chunk_etags - .push(CompleteMultipartUploadChunk { chunk_number, etag }); - }); - - tracing::info!( - "Uploaded chunk {chunk_number}/{total_chunks} for upload_id {upload_id}" - ); - break; // Success, move to next chunk - } - Err(error) => { - // Reset bytes_uploaded to what it was before the chunk attempt - { - let mut progress_sender = progress_sender.lock().unwrap(); - progress_sender.set_bytes_uploaded(bytes_before_chunk); - } - - tracing::warn!( - "Failed to upload chunk {chunk_number}/{total_chunks} (attempt {attempt}/{MAX_RETRIES}): {error:?}" - ); - - if attempt == MAX_RETRIES { - return Err(UploadTarError::FailedToUploadChunk { - chunk_number, - total_chunks, - max_retries: MAX_RETRIES, - error, - }); - } - - // Optional: add a small delay before retrying - tokio::time::sleep(std::time::Duration::from_millis(500 * attempt as u64)) - .await; - } - } - } + // Store bytes_uploaded before attempting the chunk + let bytes_before_chunk = progress_sender.lock().unwrap().bytes_uploaded(); + let mut retries = 0u32; + // Should be about 5-6 retries + let backoff = ExponentialBackoff { + initial_interval: Duration::from_millis(500), + max_interval: Duration::from_secs(8), + max_elapsed_time: Some(Duration::from_secs(16)), + multiplier: 2.0, + randomization_factor: 0.25, + ..Default::default() + }; + + let etag = retry_notify( + backoff, + || async { + // Reset progress before each attempt + progress_sender + .lock() + .unwrap() + .set_bytes_uploaded(bytes_before_chunk); + + let chunk = Chunk { + data: &chunk_data, + hash: &chunk_hash, + number: chunk_number, + }; + + upload_single_chunk( + chunk, + &api_client, + api_token, + &upload_id, + progress_sender.clone(), + &client, + ) + .await + .map_err(BackoffError::transient) + }, + |err, dur| { + retries += 1; + tracing::warn!( + "Failed to upload chunk {chunk_number}/{total_chunks}, retrying in {dur:?}: {err:?}" + ); + }, + ) + .await + .map_err(|e| UploadTarError::FailedToUploadChunk { + chunk_number, + total_chunks, + retries, + error: e, + })?; + + progress_sender.lock().unwrap().send(); + + // Update progress state with new chunk and save to file + guard.paused_mut().mutate_upload_progress(|progress| { + progress + .chunk_etags + .push(CompleteMultipartUploadChunk { chunk_number, etag }); + }); + + tracing::info!( + "Uploaded chunk {chunk_number}/{total_chunks} for upload_id {upload_id}" + ); } } let completion_result = match api_client diff --git a/tools/update-games/src/main.rs b/tools/update-games/src/main.rs index b38276b..c9b009e 100644 --- a/tools/update-games/src/main.rs +++ b/tools/update-games/src/main.rs @@ -1,12 +1,20 @@ -use constants::supported_games::SupportedGames; +use constants::unsupported_games::{UnsupportedGames, UnsupportedReason}; +use std::collections::BTreeMap; use std::fs; +fn reason_heading(reason: &UnsupportedReason) -> Option<&'static str> { + match reason { + UnsupportedReason::EnoughData => Some("Sufficient Data Collected"), + UnsupportedReason::NotAGame => None, // hidden + UnsupportedReason::Other(_) => Some("Other"), + } +} + fn main() { let md_path = "GAMES.md"; let marker_start = "