1use std::collections::BTreeSet;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use crate::content_files::{
8 ArchiveImportRequest, ContentFileImportRequest, import_archives, import_content_files,
9};
10use crate::events::ImportEvent;
11use crate::fallback_keys::MORROWIND_FALLBACK_KEYS;
12use crate::openmw_cfg::{load_resolved_cfg, normalize_cfg};
13use crate::parser::{insert_multimap, parse_ini_bytes_with_warnings, set_single_value};
14use crate::{Game, ImportError, ImportWarning, MultiMap, TextEncoding};
15
16#[derive(Debug, Clone)]
17#[allow(clippy::struct_excessive_bools)]
18pub struct ImportOptions {
19 pub game: Game,
20 pub import_game_files: bool,
21 pub import_fonts: bool,
22 pub import_archives: bool,
23 pub data_dirs: Vec<PathBuf>,
24 pub data_dir_base: Option<PathBuf>,
25 pub write_resolved_data_dirs: bool,
26 pub data_local: Option<PathBuf>,
27 pub resources: Option<PathBuf>,
28 pub user_data: Option<PathBuf>,
29 pub cfg_dir: Option<PathBuf>,
30 pub encoding: Option<TextEncoding>,
31 pub verbose: bool,
32}
33
34impl Default for ImportOptions {
35 fn default() -> Self {
36 Self {
37 game: Game::Morrowind,
38 import_game_files: false,
39 import_fonts: false,
40 import_archives: true,
41 data_dirs: Vec::new(),
42 data_dir_base: None,
43 write_resolved_data_dirs: false,
44 data_local: None,
45 resources: None,
46 user_data: None,
47 cfg_dir: None,
48 encoding: None,
49 verbose: false,
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ImportResult {
56 pub cfg: MultiMap,
57 pub warnings: Vec<ImportWarning>,
58 pub events: Vec<ImportEvent>,
59 pub changed_keys: BTreeSet<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct ImportReport {
64 pub warnings: Vec<ImportWarning>,
65 pub events: Vec<ImportEvent>,
66 pub changed_keys: BTreeSet<String>,
67}
68
69#[derive(Debug, Clone)]
70pub struct IniImporter {
71 options: ImportOptions,
72}
73
74impl IniImporter {
75 #[must_use]
76 pub fn new(options: ImportOptions) -> Self {
77 Self { options }
78 }
79
80 pub fn import_paths(
86 &self,
87 ini_path: &Path,
88 cfg_path: &Path,
89 ) -> Result<ImportResult, ImportError> {
90 self.import_optional_cfg_path(ini_path, Some(cfg_path))
91 }
92
93 pub fn import_optional_cfg_path(
99 &self,
100 ini_path: &Path,
101 cfg_path: Option<&Path>,
102 ) -> Result<ImportResult, ImportError> {
103 let mut cfg = match cfg_path {
104 Some(path) => load_resolved_cfg(path)?,
105 _ => MultiMap::new(),
106 };
107 let cfg_dir = cfg_path.and_then(cfg_parent_dir);
108
109 let mut changed_keys = BTreeSet::new();
110 let encoding = self.effective_encoding(&cfg)?;
111 if self.options.encoding.is_some() || !cfg.contains_key("encoding") {
112 changed_keys.insert("encoding".to_owned());
113 }
114 set_single_value(&mut cfg, "encoding", encoding.as_label().to_owned());
115
116 let ini_bytes = read_bytes(ini_path)?;
117 let parsed_ini = parse_ini_bytes_with_warnings(&ini_bytes, encoding);
118 let mut report = self.import_maps_with_cfg_dir(
119 &mut cfg,
120 &parsed_ini.entries,
121 ini_path,
122 cfg_dir.as_deref(),
123 )?;
124 report.warnings.splice(0..0, parsed_ini.warnings);
125 changed_keys.extend(report.changed_keys);
126 Ok(ImportResult {
127 cfg,
128 warnings: report.warnings,
129 events: report.events,
130 changed_keys,
131 })
132 }
133
134 pub fn import_maps(
140 &self,
141 cfg: &mut MultiMap,
142 ini: &MultiMap,
143 ini_path: &Path,
144 ) -> Result<ImportReport, ImportError> {
145 self.import_maps_with_cfg_dir(cfg, ini, ini_path, self.options.cfg_dir.as_deref())
146 }
147
148 fn import_maps_with_cfg_dir(
149 &self,
150 cfg: &mut MultiMap,
151 ini: &MultiMap,
152 ini_path: &Path,
153 cfg_dir: Option<&Path>,
154 ) -> Result<ImportReport, ImportError> {
155 let mut warnings = Vec::new();
156 let mut events = Vec::new();
157 let mut changed_keys = BTreeSet::new();
158 let mut search_cfg = normalize_cfg(cfg, cfg_dir)?;
159 let mut imported_cfg = cfg.clone();
160
161 if merge(&mut imported_cfg, ini) {
162 changed_keys.insert("no-sound".to_owned());
163 }
164 if merge_fallback(&mut imported_cfg, ini, self.options.import_fonts) {
165 changed_keys.insert("fallback".to_owned());
166 }
167
168 if self.options.import_game_files {
169 let imported_content = import_content_files(ContentFileImportRequest {
170 ini,
171 cfg: &search_cfg,
172 ini_path,
173 cfg_dir,
174 explicit_data_dirs: &self.options.data_dirs,
175 explicit_data_dir_base: self.options.data_dir_base.as_deref(),
176 write_resolved_data_dirs: self.options.write_resolved_data_dirs,
177 verbose: self.options.verbose,
178 })?;
179 for data_dir in imported_content.data_dirs {
180 changed_keys.insert("data".to_owned());
181 insert_multimap(&mut imported_cfg, "data".to_owned(), data_dir.cfg_value);
182 insert_multimap(
183 &mut search_cfg,
184 "data".to_owned(),
185 data_dir.path.to_string_lossy().into_owned(),
186 );
187 }
188 imported_cfg.insert("content".to_owned(), imported_content.content);
189 changed_keys.insert("content".to_owned());
190 events.extend(imported_content.events);
191 warnings.extend(imported_content.warnings);
192 }
193
194 if self.options.import_archives {
195 let imported_archives = import_archives(ArchiveImportRequest {
196 ini,
197 cfg: &search_cfg,
198 ini_path,
199 cfg_dir,
200 explicit_data_dirs: &self.options.data_dirs,
201 explicit_data_dir_base: self.options.data_dir_base.as_deref(),
202 write_resolved_data_dirs: self.options.write_resolved_data_dirs,
203 verbose: self.options.verbose,
204 })?;
205 for data_dir in imported_archives.data_dirs {
206 changed_keys.insert("data".to_owned());
207 insert_multimap(&mut imported_cfg, "data".to_owned(), data_dir.cfg_value);
208 }
209 imported_cfg.insert("fallback-archive".to_owned(), imported_archives.archives);
210 changed_keys.insert("fallback-archive".to_owned());
211 events.extend(imported_archives.events);
212 }
213
214 self.apply_singleton_path_overrides(&mut imported_cfg, &mut changed_keys);
215
216 *cfg = imported_cfg;
217 Ok(ImportReport {
218 warnings,
219 events,
220 changed_keys,
221 })
222 }
223
224 fn effective_encoding(&self, cfg: &MultiMap) -> Result<TextEncoding, ImportError> {
225 if let Some(encoding) = self.options.encoding {
226 return Ok(encoding);
227 }
228
229 if let Some(value) = cfg.get("encoding").and_then(|values| values.last()) {
230 return TextEncoding::parse(value);
231 }
232
233 Ok(TextEncoding::Win1252)
234 }
235
236 fn apply_singleton_path_overrides(
237 &self,
238 cfg: &mut MultiMap,
239 changed_keys: &mut BTreeSet<String>,
240 ) {
241 set_path_override(
242 cfg,
243 changed_keys,
244 "data-local",
245 self.options.data_local.as_deref(),
246 );
247 set_path_override(
248 cfg,
249 changed_keys,
250 "resources",
251 self.options.resources.as_deref(),
252 );
253 set_path_override(
254 cfg,
255 changed_keys,
256 "user-data",
257 self.options.user_data.as_deref(),
258 );
259 }
260}
261
262fn merge(cfg: &mut MultiMap, ini: &MultiMap) -> bool {
263 if let Some(values) = ini.get("General:Disable Audio")
264 && let Some(value) = values.last()
265 {
266 cfg.insert("no-sound".to_owned(), vec![value.clone()]);
267 return true;
268 }
269 false
270}
271
272fn merge_fallback(cfg: &mut MultiMap, ini: &MultiMap, import_fonts: bool) -> bool {
273 let mut imported = Vec::new();
274 for key in MORROWIND_FALLBACK_KEYS {
275 if !import_fonts && matches!(*key, "Fonts:Font 0" | "Fonts:Font 1" | "Fonts:Font 2") {
276 continue;
277 }
278 if let Some(values) = ini.get(*key) {
279 for value in values {
280 let fallback_key = key.replace([' ', ':'], "_");
281 imported.push(format!("{fallback_key},{value}"));
282 }
283 }
284 }
285
286 if imported.is_empty() {
287 return false;
288 }
289
290 cfg.insert("fallback".to_owned(), imported);
291 true
292}
293
294fn set_path_override(
295 cfg: &mut MultiMap,
296 changed_keys: &mut BTreeSet<String>,
297 key: &str,
298 path: Option<&Path>,
299) {
300 if let Some(path) = path {
301 set_single_value(cfg, key, path.to_string_lossy().into_owned());
302 changed_keys.insert(key.to_owned());
303 }
304}
305
306fn cfg_parent_dir(path: &Path) -> Option<PathBuf> {
307 path.parent().map(Path::to_owned)
308}
309
310fn read_bytes(path: &Path) -> Result<Vec<u8>, ImportError> {
311 fs::read(path).map_err(|source| ImportError::Io {
312 path: path.to_owned(),
313 source,
314 })
315}