1use std::{
2 env::current_dir,
3 fs::{create_dir_all, metadata},
4 io,
5 path::{Path, PathBuf},
6};
7
8pub use openmw_config::OpenMWConfiguration;
9pub use tes3::esp::Plugin;
10
11mod app;
12pub use app::run;
13
14pub mod default;
15
16pub mod light_args;
17pub use light_args::LightArgs;
18
19mod light_config;
20pub use light_config::LightConfig;
21
22mod light_override;
23pub use light_override::{CustomCellAmbient, CustomLightData};
24
25mod light_processing;
26pub use light_processing::{light_to_hsv, process_light};
27
28pub const DEFAULT_CONFIG_NAME: &str = "lightconfig.toml";
29pub const LOG_NAME: &str = "lightconfig.log";
30pub const PLUGIN_NAME: &str = "S3LightFixes.omwaddon";
31
32#[must_use]
33pub fn is_fixable_plugin(plug_path: &Path) -> bool {
34 metadata(plug_path).is_ok()
35 && !plug_path
36 .file_stem()
37 .is_some_and(|stem| stem.eq_ignore_ascii_case("S3LightFixes"))
38 && plug_path.extension().is_some_and(|ext| {
39 matches!(
40 ext.to_ascii_lowercase().to_str().unwrap_or_default(),
41 "esp" | "esm" | "omwaddon" | "omwgame"
42 )
43 })
44}
45
46pub fn notification_box(title: &str, message: &str, no_notifications: bool) {
48 #[cfg(target_os = "android")]
49 println!("{message}");
50
51 #[cfg(not(target_os = "android"))]
52 if no_notifications {
53 println!("{message}");
54 } else {
55 let _ = native_dialog::DialogBuilder::message()
56 .set_title(title)
57 .set_text(message)
58 .alert()
59 .show();
60 }
61}
62
63pub fn save_plugin(output_dir: &PathBuf, generated_plugin: &mut Plugin) -> io::Result<()> {
70 let mut plugin_path = output_dir.join(PLUGIN_NAME);
71
72 match metadata(output_dir) {
73 Ok(metadata) if !metadata.is_dir() => {
74 let cwd = current_dir()?;
75
76 eprintln!(
77 "WARNING: Couldn't use {} as an output directory, as it isn't a directory. Using the current working directory, {}, instead!",
78 output_dir.display(),
79 cwd.display()
80 );
81
82 plugin_path = cwd.join(PLUGIN_NAME);
83 }
84 Ok(_) => {}
85 Err(err) if err.kind() == io::ErrorKind::NotFound => {
86 create_dir_all(output_dir)?;
87 }
88 Err(err) => return Err(err),
89 }
90
91 generated_plugin.save_path(plugin_path)?;
92
93 Ok(())
94}
95
96pub fn to_io_error<E: std::fmt::Display>(err: E) -> std::io::Error {
97 std::io::Error::new(std::io::ErrorKind::InvalidData, err.to_string())
98}
99
100#[cfg(test)]
101mod tests {
102 use std::{
103 path::{Path, PathBuf},
104 sync::atomic::{AtomicU64, Ordering},
105 };
106
107 use super::*;
108
109 static NEXT_TEMP_FILE: AtomicU64 = AtomicU64::new(0);
110
111 struct TempFile {
112 path: PathBuf,
113 }
114
115 impl TempFile {
116 fn new(name: &str) -> Self {
117 let (stem, extension) = name
118 .rsplit_once('.')
119 .map_or((name, ""), |(stem, extension)| (stem, extension));
120 let extension = if extension.is_empty() {
121 String::new()
122 } else {
123 format!(".{extension}")
124 };
125 let path = std::env::temp_dir().join(format!(
126 "s3lightfixes-lib-{stem}-{}-{}{extension}",
127 std::process::id(),
128 NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed)
129 ));
130 std::fs::write(&path, []).unwrap();
131
132 Self { path }
133 }
134
135 fn with_exact_name_in_unique_dir(name: &str) -> Self {
136 let directory = std::env::temp_dir().join(format!(
137 "s3lightfixes-lib-dir-{}-{}",
138 std::process::id(),
139 NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed)
140 ));
141 std::fs::create_dir(&directory).unwrap();
142 let path = directory.join(name);
143 std::fs::write(&path, []).unwrap();
144
145 Self { path }
146 }
147
148 fn as_path(&self) -> &Path {
149 &self.path
150 }
151 }
152
153 impl Drop for TempFile {
154 fn drop(&mut self) {
155 let _ = std::fs::remove_file(&self.path);
156 if let Some(parent) = self.path.parent() {
157 let _ = std::fs::remove_dir(parent);
158 }
159 }
160 }
161
162 #[test]
163 fn is_fixable_plugin_accepts_supported_extensions_case_insensitively() {
164 for name in ["mod.esp", "mod.ESM", "mod.OmWaDdOn", "mod.omwgame"] {
165 let file = TempFile::new(name);
166
167 assert!(is_fixable_plugin(file.as_path()), "{name}");
168 }
169 }
170
171 #[test]
172 fn is_fixable_plugin_rejects_missing_files_unsupported_extensions_and_generated_plugin() {
173 let txt = TempFile::new("mod.txt");
174 let generated = TempFile::with_exact_name_in_unique_dir(PLUGIN_NAME);
175 let missing = std::env::temp_dir().join(format!(
176 "s3lightfixes-lib-missing-{}-{}",
177 std::process::id(),
178 NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed)
179 ));
180
181 assert!(!is_fixable_plugin(txt.as_path()));
182 assert!(!is_fixable_plugin(generated.as_path()));
183 assert!(!is_fixable_plugin(&missing));
184 }
185
186 #[test]
187 fn is_fixable_plugin_rejects_renamed_output_with_esp_extension() {
188 let esp = TempFile::with_exact_name_in_unique_dir("S3LightFixes.esp");
189 let upper_esp = TempFile::with_exact_name_in_unique_dir("S3LIGHTFIXES.ESP");
190 let omwaddon = TempFile::with_exact_name_in_unique_dir("S3LightFixes.omwaddon");
191 let omwgame = TempFile::with_exact_name_in_unique_dir("S3LightFixes.omwgame");
192 let esm = TempFile::with_exact_name_in_unique_dir("S3LightFixes.esm");
193
194 assert!(
195 !is_fixable_plugin(esp.as_path()),
196 "S3LightFixes.esp must be rejected"
197 );
198 assert!(
199 !is_fixable_plugin(upper_esp.as_path()),
200 "S3LIGHTFIXES.ESP must be rejected"
201 );
202 assert!(
203 !is_fixable_plugin(omwaddon.as_path()),
204 "S3LightFixes.omwaddon must be rejected"
205 );
206 assert!(
207 !is_fixable_plugin(omwgame.as_path()),
208 "S3LightFixes.omwgame must be rejected"
209 );
210 assert!(
211 !is_fixable_plugin(esm.as_path()),
212 "S3LightFixes.esm must be rejected"
213 );
214 }
215}