diff --git a/GAMES.md b/GAMES.md index 782d507..aa8595a 100644 --- a/GAMES.md +++ b/GAMES.md @@ -49,71 +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. -- [Abyssus](https://store.steampowered.com/app/1721110/Abyssus/) -- [Amenti](https://store.steampowered.com/app/3292260/Amenti/) -- [ARMA 3](https://store.steampowered.com/app/107410/Arma_3/) -- [Battlefield Hardline](https://store.steampowered.com/app/1238880/Battlefield_Hardline/) -- [Blacktail](https://store.steampowered.com/app/1532690/BLACKTAIL/) -- [Blair Witch](https://store.steampowered.com/app/1092660/Blair_Witch/) -- [Call of Duty: Advanced Warfare](https://store.steampowered.com/app/209650/Call_of_Duty_Advanced_Warfare) -- [Call of Duty: Infinite Warfare](https://store.steampowered.com/app/292730/Call_of_Duty_Infinite_Warfare/) -- [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/) -- [Dark Hours](https://store.steampowered.com/app/2208570/Dark_Hours/) -- [Deadzone: Rogue](https://store.steampowered.com/app/3228590/Deadzone_Rogue) -- [Earthfall](https://store.steampowered.com/app/415590/Earthfall/) -- [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/) -- [Fobia - St. Dinfna Hotel](https://store.steampowered.com/app/1298140/Fobia__St_Dinfna_Hotel/) -- [Ghost Watchers](https://store.steampowered.com/app/1850740/Ghost_Watchers/) -- [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/) -- [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/) -- [Immortals of Aveum](https://store.steampowered.com/app/2009100/Immortals_of_Aveum/) -- [In Sound Mind](https://store.steampowered.com/app/1119980/In_Sound_Mind/) -- [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/) -- [METAL EDEN](https://store.steampowered.com/app/990380/METAL_EDEN/) -- [Observer: System Redux](https://store.steampowered.com/app/1386900/Observer_System_Redux/) -- [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 3](https://store.steampowered.com/app/1272080/PAYDAY_3/) -- [Ready or Not](https://store.steampowered.com/app/1144200/Ready_or_Not/) -- [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/) -- [Shadow Warrior 3](https://store.steampowered.com/app/1036890/Shadow_Warrior_3_Definitive_Edition) -- [Soma](https://store.steampowered.com/app/282140/SOMA/) -- [Squad](https://store.steampowered.com/app/393380/Squad/) -- [Tacoma](https://store.steampowered.com/app/343860/Tacoma/) -- [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 Lightkeeper](https://store.steampowered.com/app/3612850/The_Lightkeeper/) -- [The Outer Worlds 2](https://store.steampowered.com/app/1449110/The_Outer_Worlds_2/) -- [The Stanley Parable](https://store.steampowered.com/app/221910/The_Stanley_Parable/) -- [The Talos Principle 2](https://store.steampowered.com/app/835960/The_Talos_Principle_2/) -- [The Witness](https://store.steampowered.com/app/210970/The_Witness/) -- [Trepang2](https://store.steampowered.com/app/1164940/Trepang2/) -- [Visage](https://store.steampowered.com/app/594330/Visage/) -- [VOIDBREAKER](https://store.steampowered.com/app/2615540/VOIDBREAKER/) -- [VOIN](https://store.steampowered.com/app/2464530/VOIN/) -- [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: Youngblood](https://store.steampowered.com/app/1056960/Wolfenstein_Youngblood/) -- [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/crates/constants/src/lib.rs b/crates/constants/src/lib.rs index 073ee09..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; 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..c110e6a 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"], @@ -194,7 +189,11 @@ "binaries": ["DXHRDC.exe"], "reason": "EnoughData" }, - { "name": "Metro 2033", "binaries": ["metro.exe"], "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 ed3ee32..048155b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -7,7 +7,7 @@ use std::{ 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,7 +33,10 @@ 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, } @@ -83,7 +86,8 @@ 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()), + last_recordable_game: RwLock::new(None), + unsupported_games: RwLock::new(UnsupportedGames::load_from_embedded()), offline: OfflineState::default(), }; tracing::debug!("AppState::new() complete"); @@ -166,7 +170,7 @@ pub enum AsyncRequest { PauseUpload, OpenDataDump, OpenLog, - UpdateSupportedGames(SupportedGames), + UpdateUnsupportedGames(UnsupportedGames), LoadUploadStats, LoadLocalRecordings, DeleteAllInvalidRecordings, 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 d391c72..dd9a1cf 100644 --- a/src/tokio_thread.rs +++ b/src/tokio_thread.rs @@ -23,7 +23,9 @@ use std::{ 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}; @@ -255,14 +257,13 @@ 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 => { @@ -657,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 @@ -877,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, ) @@ -993,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, @@ -1004,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, ) @@ -1069,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(); @@ -1194,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." @@ -1350,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"); } } } @@ -1377,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<()> { diff --git a/src/ui/views/main/mod.rs b/src/ui/views/main/mod.rs index ca3b9b4..1fc4877 100644 --- a/src/ui/views/main/mod.rs +++ b/src/ui/views/main/mod.rs @@ -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(), + ); + } } } 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/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 = "