Skip to main content

s3lightfixes/
lib.rs

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
46/// Displays a notification taking title and message as argument
47pub 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
63/// Saves the generated plugin to the requested output directory.
64///
65/// # Errors
66///
67/// Returns any filesystem error encountered while creating the output directory, resolving the
68/// fallback current directory, or writing the plugin file.
69pub 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}