1use std::path::{Path, PathBuf};
6
7use smallvec::SmallVec;
8
9use modde_core::collision::{CollisionClassifier, CollisionSeverity};
10
11use crate::traits::{ContentCategory, ModSafety};
12
13#[derive(Debug, Clone, Copy)]
15pub struct ContentPolicy {
16 pub save_breaking_ext: &'static [&'static str],
17 pub cosmetic_ext: &'static [&'static str],
18 pub save_breaking_dirs: &'static [&'static str],
19 pub categories: &'static [(&'static str, ContentCategory)],
20}
21
22impl ContentPolicy {
23 #[must_use]
25 pub fn classify_extension(self, ext: &str) -> ContentCategory {
26 self.categories
27 .iter()
28 .find_map(|(candidate, category)| (*candidate == ext).then_some(*category))
29 .unwrap_or(ContentCategory::Other)
30 }
31
32 #[must_use]
34 pub fn classify_mod(self, mod_dir: &Path) -> ModSafety {
35 if !mod_dir.exists() {
36 return ModSafety::Unknown;
37 }
38
39 let mut has_any_file = false;
40 let mut has_cosmetic = false;
41 let mut stack = vec![mod_dir.to_path_buf()];
42
43 while let Some(dir) = stack.pop() {
44 let entries = match std::fs::read_dir(&dir) {
45 Ok(entries) => entries,
46 Err(_) => continue,
47 };
48
49 for entry in entries.flatten() {
50 let path = entry.path();
51 if path.is_dir() {
52 if !self.save_breaking_dirs.is_empty() {
53 let rel = path.strip_prefix(mod_dir).unwrap_or(&path);
54 let rel = rel.to_string_lossy().to_lowercase().replace('\\', "/");
55 if self
56 .save_breaking_dirs
57 .iter()
58 .any(|pattern| rel.contains(pattern))
59 {
60 return ModSafety::SaveBreaking;
61 }
62 }
63 stack.push(path);
64 continue;
65 }
66
67 has_any_file = true;
68 let Some(ext) = path.extension().and_then(|e| e.to_str()) else {
69 continue;
70 };
71 let ext = ext.to_lowercase();
72 if self.save_breaking_ext.contains(&ext.as_str()) {
73 return ModSafety::SaveBreaking;
74 }
75 if self.cosmetic_ext.contains(&ext.as_str()) {
76 has_cosmetic = true;
77 }
78 }
79 }
80
81 if has_cosmetic && has_any_file {
82 ModSafety::SaveSafe
83 } else {
84 ModSafety::Unknown
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy)]
93pub struct BareLayoutPolicy {
94 pub root_dirs: &'static [&'static str],
95 pub root_file_exts: &'static [&'static str],
96 pub case_insensitive_dirs: bool,
97}
98
99impl BareLayoutPolicy {
100 #[must_use]
103 pub fn recognizes(self, extracted_dir: &Path) -> bool {
104 let Ok(entries) = std::fs::read_dir(extracted_dir) else {
105 return false;
106 };
107
108 for entry in entries.flatten() {
109 let path = entry.path();
110 if path.is_dir() {
111 let name = entry.file_name().to_string_lossy().to_string();
112 if self.case_insensitive_dirs {
113 let name = name.to_lowercase();
114 if self.root_dirs.iter().any(|candidate| *candidate == name) {
115 return true;
116 }
117 } else if self.root_dirs.iter().any(|candidate| *candidate == name) {
118 return true;
119 }
120 } else if path.is_file()
121 && let Some(ext) = path.extension().and_then(|e| e.to_str())
122 {
123 let ext = ext.to_lowercase();
124 if self.root_file_exts.contains(&ext.as_str()) {
125 return true;
126 }
127 }
128 }
129
130 false
131 }
132}
133
134#[derive(Debug, Clone, Copy)]
136pub enum StagingDllSearch {
137 DirectChildDirs,
139 NestedModsBinX64,
141}
142
143#[derive(Debug, Clone, Copy)]
146pub struct DllOverridePolicy {
147 pub proxy_dlls: &'static [&'static str],
148 pub staging_search: StagingDllSearch,
149}
150
151impl DllOverridePolicy {
152 #[must_use]
154 pub fn from_executable_dir(self, executable_dir: &Path) -> SmallVec<[String; 4]> {
155 let mut out = SmallVec::new();
156 for &name in self.proxy_dlls {
157 if executable_dir.join(format!("{name}.dll")).exists() {
158 out.push(name.to_string());
159 }
160 }
161 out
162 }
163
164 #[must_use]
166 pub fn from_staging(self, staging: &Path) -> SmallVec<[String; 4]> {
167 match self.staging_search {
168 StagingDllSearch::DirectChildDirs => self.scan_direct_child_dirs(staging),
169 StagingDllSearch::NestedModsBinX64 => self.scan_nested_mods_bin_x64(staging),
170 }
171 }
172
173 fn push_dll_name(self, out: &mut SmallVec<[String; 4]>, file_name: &std::ffi::OsStr) {
174 let name = file_name.to_string_lossy().to_lowercase();
175 if let Some(stem) = name.strip_suffix(".dll")
176 && self.proxy_dlls.contains(&stem)
177 && !out.iter().any(|item| item == stem)
178 {
179 out.push(stem.to_string());
180 }
181 }
182
183 fn scan_direct_child_dirs(self, staging: &Path) -> SmallVec<[String; 4]> {
184 let mut out = SmallVec::new();
185 let Ok(entries) = std::fs::read_dir(staging) else {
186 return out;
187 };
188 for entry in entries.flatten() {
189 if !entry.file_type().is_ok_and(|t| t.is_dir()) {
190 continue;
191 }
192 let Ok(inner) = std::fs::read_dir(entry.path()) else {
193 continue;
194 };
195 for file in inner.flatten() {
196 self.push_dll_name(&mut out, &file.file_name());
197 }
198 }
199 out
200 }
201
202 fn scan_nested_mods_bin_x64(self, staging: &Path) -> SmallVec<[String; 4]> {
203 let mut out = SmallVec::new();
204 let mods_dir = staging.join("mods");
205 if !mods_dir.is_dir() {
206 return out;
207 }
208
209 for entry in std::fs::read_dir(&mods_dir).into_iter().flatten().flatten() {
210 if !entry.file_type().is_ok_and(|t| t.is_dir()) {
211 continue;
212 }
213 let mod_bin_x64 = entry.path().join("bin/x64");
214 if !mod_bin_x64.is_dir() {
215 continue;
216 }
217 for file in std::fs::read_dir(&mod_bin_x64)
218 .into_iter()
219 .flatten()
220 .flatten()
221 {
222 self.push_dll_name(&mut out, &file.file_name());
223 }
224 }
225
226 out
227 }
228}
229
230#[derive(Debug, Clone, Copy)]
232pub enum ModDirectoryLayout {
233 Relative(&'static str),
235 Ue4PaksMods { project_name: &'static str },
237}
238
239impl ModDirectoryLayout {
240 #[must_use]
242 pub fn resolve(self, install: &Path) -> PathBuf {
243 match self {
244 Self::Relative(path) => install.join(path),
245 Self::Ue4PaksMods { project_name } => install
246 .join(project_name)
247 .join("Content")
248 .join("Paks")
249 .join("~mods"),
250 }
251 }
252}
253
254#[derive(Debug, Clone, Copy)]
257pub struct CollisionPolicy {
258 pub archive_extensions: &'static [&'static str],
259 pub severities: &'static [(&'static str, CollisionSeverity)],
260}
261
262impl CollisionPolicy {
263 #[must_use]
265 pub fn classify_severity(self, file_path: &str) -> CollisionSeverity {
266 let ext = file_path.rsplit('.').next().unwrap_or("").to_lowercase();
267 self.severities
268 .iter()
269 .find_map(|(candidate, severity)| (*candidate == ext).then_some(*severity))
270 .unwrap_or(CollisionSeverity::Unknown)
271 }
272}
273
274#[derive(Debug, Clone, Copy)]
277pub struct PolicyCollisionClassifier {
278 pub policy: CollisionPolicy,
279}
280
281impl CollisionClassifier for PolicyCollisionClassifier {
282 fn index_archive(&self, _archive_path: &Path) -> anyhow::Result<Vec<(String, u64)>> {
283 Ok(Vec::new())
284 }
285
286 fn classify_severity(&self, file_path: &str) -> CollisionSeverity {
287 self.policy.classify_severity(file_path)
288 }
289
290 fn archive_extensions(&self) -> &[&str] {
291 self.policy.archive_extensions
292 }
293}