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.to_string_lossy().contains(PLUGIN_NAME)
36        && plug_path.extension().is_some_and(|ext| {
37            matches!(
38                ext.to_ascii_lowercase().to_str().unwrap_or_default(),
39                "esp" | "esm" | "omwaddon" | "omwgame"
40            )
41        })
42}
43
44/// Displays a notification taking title and message as argument
45pub fn notification_box(title: &str, message: &str, no_notifications: bool) {
46    #[cfg(target_os = "android")]
47    println!("{message}");
48
49    #[cfg(not(target_os = "android"))]
50    if no_notifications {
51        println!("{message}");
52    } else {
53        let _ = native_dialog::DialogBuilder::message()
54            .set_title(title)
55            .set_text(message)
56            .alert()
57            .show();
58    }
59}
60
61/// Saves the generated plugin to the requested output directory.
62///
63/// # Errors
64///
65/// Returns any filesystem error encountered while creating the output directory, resolving the
66/// fallback current directory, or writing the plugin file.
67pub fn save_plugin(output_dir: &PathBuf, generated_plugin: &mut Plugin) -> io::Result<()> {
68    let mut plugin_path = output_dir.join(PLUGIN_NAME);
69
70    match metadata(output_dir) {
71        Ok(metadata) if !metadata.is_dir() => {
72            let cwd = current_dir()?;
73
74            eprintln!(
75                "WARNING: Couldn't use {} as an output directory, as it isn't a directory. Using the current working directory, {}, instead!",
76                output_dir.display(),
77                cwd.display()
78            );
79
80            plugin_path = cwd.join(PLUGIN_NAME);
81        }
82        Ok(_) => {}
83        Err(err) if err.kind() == io::ErrorKind::NotFound => {
84            create_dir_all(output_dir)?;
85        }
86        Err(err) => return Err(err),
87    }
88
89    generated_plugin.save_path(plugin_path)?;
90
91    Ok(())
92}
93
94pub fn to_io_error<E: std::fmt::Display>(err: E) -> std::io::Error {
95    std::io::Error::new(std::io::ErrorKind::InvalidData, err.to_string())
96}
97
98#[cfg(test)]
99mod tests {
100    use std::{
101        path::{Path, PathBuf},
102        sync::atomic::{AtomicU64, Ordering},
103    };
104
105    use super::*;
106
107    static NEXT_TEMP_FILE: AtomicU64 = AtomicU64::new(0);
108
109    struct TempFile {
110        path: PathBuf,
111    }
112
113    impl TempFile {
114        fn new(name: &str) -> Self {
115            let (stem, extension) = name
116                .rsplit_once('.')
117                .map_or((name, ""), |(stem, extension)| (stem, extension));
118            let extension = if extension.is_empty() {
119                String::new()
120            } else {
121                format!(".{extension}")
122            };
123            let path = std::env::temp_dir().join(format!(
124                "s3lightfixes-lib-{stem}-{}-{}{extension}",
125                std::process::id(),
126                NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed)
127            ));
128            std::fs::write(&path, []).unwrap();
129
130            Self { path }
131        }
132
133        fn with_exact_name_in_unique_dir(name: &str) -> Self {
134            let directory = std::env::temp_dir().join(format!(
135                "s3lightfixes-lib-dir-{}-{}",
136                std::process::id(),
137                NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed)
138            ));
139            std::fs::create_dir(&directory).unwrap();
140            let path = directory.join(name);
141            std::fs::write(&path, []).unwrap();
142
143            Self { path }
144        }
145
146        fn as_path(&self) -> &Path {
147            &self.path
148        }
149    }
150
151    impl Drop for TempFile {
152        fn drop(&mut self) {
153            let _ = std::fs::remove_file(&self.path);
154            if let Some(parent) = self.path.parent() {
155                let _ = std::fs::remove_dir(parent);
156            }
157        }
158    }
159
160    #[test]
161    fn is_fixable_plugin_accepts_supported_extensions_case_insensitively() {
162        for name in ["mod.esp", "mod.ESM", "mod.OmWaDdOn", "mod.omwgame"] {
163            let file = TempFile::new(name);
164
165            assert!(is_fixable_plugin(file.as_path()), "{name}");
166        }
167    }
168
169    #[test]
170    fn is_fixable_plugin_rejects_missing_files_unsupported_extensions_and_generated_plugin() {
171        let txt = TempFile::new("mod.txt");
172        let generated = TempFile::with_exact_name_in_unique_dir(PLUGIN_NAME);
173        let missing = std::env::temp_dir().join(format!(
174            "s3lightfixes-lib-missing-{}-{}",
175            std::process::id(),
176            NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed)
177        ));
178
179        assert!(!is_fixable_plugin(txt.as_path()));
180        assert!(!is_fixable_plugin(generated.as_path()));
181        assert!(!is_fixable_plugin(&missing));
182    }
183}