modde_games/generic/
loader.rs1use std::collections::HashSet;
6use std::fs;
7use std::path::PathBuf;
8
9use tracing::warn;
10
11use crate::generic::GenericGame;
12use crate::registry::{EngineFamily, GameRegistration, LauncherIds, SUPPORTED_GAME_IDS};
13use crate::traits::GamePlugin;
14
15use super::leak::str as leak_str;
16use super::spec::GameSpec;
17
18#[must_use]
19pub fn load_user_games() -> Vec<GameRegistration> {
20 let games_dir = modde_core::paths::modde_data_dir().join("games");
21 let Ok(entries) = fs::read_dir(&games_dir) else {
22 return Vec::new();
23 };
24
25 let mut paths: Vec<PathBuf> = entries.flatten().map(|entry| entry.path()).collect();
26 paths.sort_by(|a, b| a.to_string_lossy().cmp(&b.to_string_lossy()));
27
28 let mut seen_ids: HashSet<String> = SUPPORTED_GAME_IDS
29 .iter()
30 .map(|id| (*id).to_string())
31 .collect();
32 let mut registrations = Vec::new();
33
34 for path in paths {
35 if path.extension().and_then(|ext| ext.to_str()) != Some("toml") {
36 continue;
37 }
38
39 if path
40 .file_name()
41 .and_then(|name| name.to_str())
42 .is_some_and(|name| name.ends_with(".optiscaler.toml"))
43 {
44 continue;
45 }
46
47 let content = match fs::read_to_string(&path) {
48 Ok(content) => content,
49 Err(error) => {
50 warn!(path = %path.display(), error = %error, "skipping user game spec");
51 continue;
52 }
53 };
54
55 let spec = match toml::from_str::<GameSpec>(&content) {
56 Ok(spec) => spec,
57 Err(error) => {
58 warn!(path = %path.display(), error = %error, "skipping user game spec");
59 continue;
60 }
61 };
62
63 if let Err(error) = spec.validate() {
64 warn!(path = %path.display(), error = %error, "skipping user game spec");
65 continue;
66 }
67
68 if !seen_ids.insert(spec.id.clone()) {
69 warn!(
70 path = %path.display(),
71 game_id = %spec.id,
72 "skipping user game spec with duplicate id"
73 );
74 continue;
75 }
76
77 let game_id = leak_str(spec.id.clone());
78 let display_name = leak_str(spec.display_name.clone());
79 let nexus_domain = spec.nexus_domain.clone().map(leak_str);
80 let steam_app_id = spec.steam_app_id.clone().map(leak_str);
81 let steam_dir = spec.install_dir_name.clone().map(leak_str);
82
83 let plugin: &'static dyn GamePlugin = Box::leak(Box::new(GenericGame::from_spec(spec)));
84
85 registrations.push(GameRegistration {
86 game_id,
87 display_name,
88 engine: EngineFamily::Generic,
89 launcher: LauncherIds {
90 steam_app_id,
91 steam_dir,
92 heroic_gog_app_id: None,
93 heroic_epic_app_id: None,
94 },
95 wabbajack_names: &[],
96 nexus_domain,
97 nexus_game_id: None,
98 supports_save_profiles: false,
99 plugin,
100 scanner: None,
101 save_tracker: None,
102 collision_classifier: Some(crate::registry::generic_collision_classifier),
103 optiscaler_profiles: &[],
104 });
105 }
106
107 registrations
108}
109
110pub fn reload_user_games() {
111 crate::registry::reload_registry();
112}