modde_games/cyberpunk/
scanner.rs1use std::path::Path;
2
3use anyhow::Result;
4
5use crate::traits::{DiscoveredFile, DiscoveredMod, ModScanner, ModSource, ScanContext, walk_files_relative};
6use super::manifest::RedModManifest;
7
8pub struct CyberpunkScanner;
9
10pub static CYBERPUNK_SCANNER: CyberpunkScanner = CyberpunkScanner;
11
12const SCAN_DIRS: &[&str] = &[
14 "bin/x64/plugins/cyber_engine_tweaks/mods",
15 "r6/scripts",
16 "r6/tweaks",
17 "archive/pc/mod",
18 "mods",
19];
20
21impl ModScanner for CyberpunkScanner {
22 fn scan_directories(&self) -> &[&str] {
23 SCAN_DIRS
24 }
25
26 fn scan_filesystem(&self, ctx: &ScanContext<'_>) -> Result<Vec<DiscoveredMod>> {
27 let install = ctx.install_dir;
28 let mut mods = Vec::new();
29
30 scan_cet_mods(install, &mut mods)?;
31 scan_redscript_mods(install, &mut mods)?;
32 scan_tweakxl_mods(install, &mut mods)?;
33 scan_archive_mods(install, &mut mods)?;
34 scan_redmod_mods(install, &mut mods)?;
35
36 Ok(mods)
37 }
38
39 fn mod_id_footprint(&self, mod_id: &str) -> Option<modde_core::scanner::ModFootprint> {
48 use modde_core::scanner::ModFootprint;
49 if let Some(name) = mod_id.strip_prefix("cet/") {
50 Some(ModFootprint::Directory(format!(
51 "bin/x64/plugins/cyber_engine_tweaks/mods/{}/",
52 name.to_lowercase()
53 )))
54 } else if let Some(name) = mod_id.strip_prefix("reds/") {
55 Some(ModFootprint::Directory(format!(
56 "r6/scripts/{}/",
57 name.to_lowercase()
58 )))
59 } else if let Some(name) = mod_id.strip_prefix("tweak/") {
60 Some(ModFootprint::Directory(format!(
61 "r6/tweaks/{}/",
62 name.to_lowercase()
63 )))
64 } else if let Some(name) = mod_id.strip_prefix("redmod/") {
65 Some(ModFootprint::Directory(format!("mods/{}/", name.to_lowercase())))
66 } else if let Some(stem) = mod_id.strip_prefix("archive/") {
67 Some(ModFootprint::File(format!(
68 "archive/pc/mod/{}.archive",
69 stem.to_lowercase()
70 )))
71 } else {
72 None
73 }
74 }
75}
76
77fn scan_cet_mods(install: &Path, out: &mut Vec<DiscoveredMod>) -> Result<()> {
79 let cet_dir = install.join("bin/x64/plugins/cyber_engine_tweaks/mods");
80 if !cet_dir.is_dir() {
81 return Ok(());
82 }
83
84 for entry in std::fs::read_dir(&cet_dir)?.flatten() {
85 if !entry.path().is_dir() {
86 continue;
87 }
88 let name = entry.file_name().to_string_lossy().to_string();
89 let has_init = entry.path().join("init.lua").exists();
90 let files = walk_files_relative(install, &entry.path());
91
92 if files.is_empty() {
93 continue;
94 }
95
96 out.push(DiscoveredMod {
97 mod_id: format!("cet/{name}"),
98 display_name: name,
99 version: None,
100 files,
101 source: ModSource::Filesystem {
102 location: "cet".into(),
103 },
104 confidence: if has_init { 0.95 } else { 0.7 },
105 });
106 }
107 Ok(())
108}
109
110fn scan_redscript_mods(install: &Path, out: &mut Vec<DiscoveredMod>) -> Result<()> {
112 let scripts_dir = install.join("r6/scripts");
113 if !scripts_dir.is_dir() {
114 return Ok(());
115 }
116
117 for entry in std::fs::read_dir(&scripts_dir)?.flatten() {
118 if !entry.path().is_dir() {
119 continue;
120 }
121 let name = entry.file_name().to_string_lossy().to_string();
122 let files = walk_files_relative(install, &entry.path());
123
124 if files.is_empty() {
125 continue;
126 }
127
128 out.push(DiscoveredMod {
129 mod_id: format!("reds/{name}"),
130 display_name: name,
131 version: None,
132 files,
133 source: ModSource::Filesystem {
134 location: "r6/scripts".into(),
135 },
136 confidence: 0.9,
137 });
138 }
139 Ok(())
140}
141
142fn scan_tweakxl_mods(install: &Path, out: &mut Vec<DiscoveredMod>) -> Result<()> {
144 let tweaks_dir = install.join("r6/tweaks");
145 if !tweaks_dir.is_dir() {
146 return Ok(());
147 }
148
149 for entry in std::fs::read_dir(&tweaks_dir)?.flatten() {
150 if !entry.path().is_dir() {
151 continue;
152 }
153 let name = entry.file_name().to_string_lossy().to_string();
154 let files = walk_files_relative(install, &entry.path());
155
156 if files.is_empty() {
157 continue;
158 }
159
160 out.push(DiscoveredMod {
161 mod_id: format!("tweak/{name}"),
162 display_name: name,
163 version: None,
164 files,
165 source: ModSource::Filesystem {
166 location: "r6/tweaks".into(),
167 },
168 confidence: 0.9,
169 });
170 }
171 Ok(())
172}
173
174fn scan_archive_mods(install: &Path, out: &mut Vec<DiscoveredMod>) -> Result<()> {
176 let archive_dir = install.join("archive/pc/mod");
177 if !archive_dir.is_dir() {
178 return Ok(());
179 }
180
181 for entry in std::fs::read_dir(&archive_dir)?.flatten() {
182 let path = entry.path();
183 if path.is_dir() || path.extension().and_then(|e| e.to_str()) != Some("archive") {
184 continue;
185 }
186
187 let stem = path
188 .file_stem()
189 .and_then(|s| s.to_str())
190 .unwrap_or("unknown");
191 let size = path.metadata().map(|m| m.len()).unwrap_or(0);
192 let rel = path
193 .strip_prefix(install)
194 .unwrap_or(&path)
195 .to_string_lossy()
196 .replace('\\', "/");
197
198 out.push(DiscoveredMod {
199 mod_id: format!("archive/{stem}"),
200 display_name: stem.to_string(),
201 version: None,
202 files: vec![DiscoveredFile { rel_path: rel, size }],
203 source: ModSource::Filesystem {
204 location: "archive/pc/mod".into(),
205 },
206 confidence: 0.85,
207 });
208 }
209 Ok(())
210}
211
212fn scan_redmod_mods(install: &Path, out: &mut Vec<DiscoveredMod>) -> Result<()> {
214 let mods_dir = install.join("mods");
215 if !mods_dir.is_dir() {
216 return Ok(());
217 }
218
219 for entry in std::fs::read_dir(&mods_dir)?.flatten() {
220 if !entry.path().is_dir() {
221 continue;
222 }
223
224 let dir_name = entry.file_name().to_string_lossy().to_string();
225 let info_json = entry.path().join("info.json");
226
227 let (name, version) = if info_json.exists() {
228 match std::fs::read_to_string(&info_json).ok().and_then(|s| RedModManifest::parse(&s).ok()) {
229 Some(manifest) => (manifest.name, manifest.version),
230 None => (dir_name.clone(), None),
231 }
232 } else {
233 (dir_name.clone(), None)
234 };
235
236 let files = walk_files_relative(install, &entry.path());
237 if files.is_empty() {
238 continue;
239 }
240
241 out.push(DiscoveredMod {
242 mod_id: format!("redmod/{dir_name}"),
243 display_name: name,
244 version,
245 files,
246 source: ModSource::Filesystem {
247 location: "mods".into(),
248 },
249 confidence: if info_json.exists() { 0.95 } else { 0.8 },
250 });
251 }
252 Ok(())
253}