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
44pub 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
61pub 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}