lunar_magic_wrapper/
lib.rs

1//! This crate provides a lightweight wrapper around [Lunar Magic](http://fusoya.eludevisibility.org/lm/index.html)'s
2//! command line functions.
3//! It supports all available command line functions as of Lunar Magic 3.40.
4//!
5//! Note that this crate requires [wine](https://www.winehq.org/) to be installed, accessible as a command and working on operating systems other than Windows.
6//!
7//! Paths passed to functions can be any type that can be turned into `AsRef<Path>`, e.g., the following will
8//! all work equally well:
9//!
10//!```
11//! # use lunar_magic_wrapper::Wrapper;
12//! # use std::path::Path;
13//! # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
14//! // `&str` works fine
15//! let output = lm_wrapper.export_gfx("C:/hacks/my_project/my_hack.smc");
16//!
17//! // So does a `Path`
18//! let output = lm_wrapper.export_gfx(Path::new("C:/hacks/my_project/my_hack.smc"));
19//!
20//! // So does a `String`
21//! let output = lm_wrapper.export_gfx(String::from("C:/hacks/my_project/my_hack.smc"));
22//!
23//! // And so on, but note that separators should be your operating system's separators, i.e. `\` should be used on Windows.
24//! ```
25
26use std::{
27    error::Error,
28    fmt::{self, Display},
29    fs::File,
30    io::{BufRead, BufReader, ErrorKind},
31    path::{Path, PathBuf},
32    process::Command,
33};
34
35use bitflags::bitflags;
36use tempfile::tempdir;
37
38/// A wrapper around Lunar Magic's command line functionality.
39///
40/// Up to date as of Lunar Magic 3.40.
41#[derive(Debug)]
42pub struct Wrapper {
43    lunar_magic_path: PathBuf,
44}
45
46/// Contains errors raised as a result of an operation using
47/// a [Wrapper].
48#[derive(Debug)]
49pub enum WrapperErr {
50    /// Raised when no Lunar Magic is found at the path given to the wrapper.
51    LunarMagicMissing { command: String },
52
53    /// Raised when an operation by Lunar Magic fails.
54    Operation {
55        code: Option<i32>,
56        command: String,
57        output: Vec<String>,
58    },
59
60    /// Raised when the underlying command couldn't be executed by the OS.
61    FailedToExecute {
62        command: String,
63        error_kind: ErrorKind,
64    },
65
66    /// Raised when no temp file for logging Lunar Magic's output was found.
67    NoTempFile { command: String },
68
69    /// Raised when no temporary directory to keep the Lunar Magic log
70    /// file could be created.
71    NoTempDir { command: String },
72
73    #[cfg(not(target_os = "windows"))]
74    NoWine,
75}
76
77impl Display for WrapperErr {
78    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79        let err_msg =
80            match self {
81                WrapperErr::LunarMagicMissing { command } => {
82                    format!(
83                        "Lunar Magic not found while performing operation '{}'",
84                        command
85                    )
86                }
87                WrapperErr::Operation {
88                    code,
89                    command,
90                    output,
91                } => {
92                    if let Some(code) = code {
93                        format!(
94                        "Lunar Magic returned with error code {} while performing operation '{}' \
95                        with the following output:\n\t{}",
96                        code, command, output.join("\n\t")
97                    )
98                    } else {
99                        format!(
100                            "Lunar Magic failed while performing operation '{}' \
101                        with the following output:\n\t{}",
102                            command,
103                            output.join("\n\t")
104                        )
105                    }
106                }
107                WrapperErr::FailedToExecute {
108                    command,
109                    error_kind,
110                } => {
111                    format!(
112                        "Failed to execute Lunar Magic while attempting to perform \
113                    operation '{}', error kind was {}",
114                        command, error_kind
115                    )
116                }
117                WrapperErr::NoTempDir { command } => {
118                    format!(
119                        "Failed to create temporary folder while attempting to perform \
120                    operation '{}'",
121                        command
122                    )
123                }
124                WrapperErr::NoTempFile { command } => {
125                    format!(
126                        "Failed to read temporary log file while attempting to perform \
127                    operation '{}'",
128                        command
129                    )
130                }
131                #[cfg(not(target_os = "windows"))]
132                WrapperErr::NoWine => {
133                    format!("Wine is not installed, cannot perform operation")
134                }
135            };
136
137        write!(f, "{}", err_msg)
138    }
139}
140
141impl Error for WrapperErr {
142    fn source(&self) -> Option<&(dyn Error + 'static)> {
143        None
144    }
145}
146
147/// Contains all valid ROM sizes that can be used with
148/// [Wrapper::expand_rom].
149#[derive(Debug)]
150pub enum RomSize {
151    _2mb,
152    _3mb,
153    _4mb,
154    _6mbSa1,
155    _8mbSa1,
156}
157
158impl Display for RomSize {
159    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
160        let string = match self {
161            RomSize::_2mb => "2MB",
162            RomSize::_3mb => "3MB",
163            RomSize::_4mb => "4MB",
164            RomSize::_6mbSa1 => "6MB_SA1",
165            RomSize::_8mbSa1 => "8MB_SA1",
166        };
167
168        write!(f, "{}", string)
169    }
170}
171
172/// Result of invoking an operation through a [Wrapper].
173///
174/// Contains the text output of Lunar Magic if the operation succeeded
175/// or a [WrapperErr] otherwise.
176pub type ResultL = Result<Vec<String>, WrapperErr>;
177
178/// Contains all valid ROM compression formats that can be used with
179/// [Wrapper::change_compression].
180#[derive(Debug)]
181pub enum CompressionFormat {
182    LcLz2Orig,
183    LcLz2Speed,
184    LcLz3,
185}
186
187impl Display for CompressionFormat {
188    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
189        let string = match self {
190            CompressionFormat::LcLz2Orig => "LC_LZ2_Orig",
191            CompressionFormat::LcLz2Speed => "LC_LZ2_Speed",
192            CompressionFormat::LcLz3 => "LC_LZ3",
193        };
194
195        write!(f, "{}", string)
196    }
197}
198
199bitflags! {
200    /// Contains currently available flags when importing
201    /// multiple levels into the ROM using [Wrapper::import_mult_levels].
202    pub struct LevelImportFlag: u32 {
203        const None = 0b0000;
204        const ClearSecondaryExits = 0b0001;
205    }
206
207    /// Contains currently available flags when importing
208    /// multiple levels into the ROM using [Wrapper::export_mult_levels].
209    pub struct LevelExportFlag: u32 {
210        const None = 0b0000;
211        const OnlyModifiedLevels = 0b0001;
212    }
213}
214
215impl Wrapper {
216    /// Returns a new [Wrapper].
217    ///
218    /// Note that there doesn't necessarily have to be a
219    /// Lunar Magic executable at the passed path at time
220    /// of creation. It only needs to exist once you try
221    /// to actually use the wrapper to perform operations.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use lunar_magic_wrapper::Wrapper;
227    ///
228    /// let lm_wrapper = Wrapper::new("C:/programs/LunarMagic/lunar_magic.exe");
229    /// ```
230    pub fn new<P: Into<PathBuf>>(path: P) -> Self {
231        Wrapper {
232            lunar_magic_path: path.into(),
233        }
234    }
235
236    /// Exports Graphics from the passed ROM and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
237    ///
238    /// # Examples
239    ///
240    /// ```
241    /// # use lunar_magic_wrapper::Wrapper;
242    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
243    /// let output = lm_wrapper.export_gfx("C:/hacks/my_project/my_hack.smc");
244    /// ```
245    pub fn export_gfx<P: AsRef<Path>>(&self, rom_path: P) -> ResultL {
246        self.run_command(&format!(
247            "-ExportGFX {}",
248            rom_path.as_ref().to_string_lossy()
249        ))
250    }
251
252    /// Exports ExGraphics from the passed ROM and returns Lunar Magic's
253    /// text output or a [WrapperErr] if something went wrong.
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// # use lunar_magic_wrapper::Wrapper;
259    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
260    /// let output = lm_wrapper.export_exgfx("C:/hacks/my_project/my_hack.smc");
261    /// ```
262    pub fn export_exgfx<P: AsRef<Path>>(&self, rom_path: P) -> ResultL {
263        self.run_command(&format!(
264            "-ExportExGFX {}",
265            rom_path.as_ref().to_string_lossy()
266        ))
267    }
268
269    /// Imports Graphics into the passed ROM and returns Lunar Magic's
270    /// text output or a [WrapperErr] if something went wrong.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// # use lunar_magic_wrapper::Wrapper;
276    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
277    /// let output = lm_wrapper.import_gfx("C:/hacks/my_project/my_hack.smc");
278    /// ```
279    pub fn import_gfx<P: AsRef<Path>>(&self, rom_path: P) -> ResultL {
280        self.run_command(&format!(
281            "-ImportGFX {}",
282            rom_path.as_ref().to_string_lossy()
283        ))
284    }
285
286    /// Imports ExGraphics into the passed ROM and returns Lunar Magic's
287    /// text output or a [WrapperErr] if something went wrong.
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// # use lunar_magic_wrapper::Wrapper;
293    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
294    /// let output = lm_wrapper.import_exgfx("C:/hacks/my_project/my_hack.smc");
295    /// ```
296    pub fn import_exgfx<P: AsRef<Path>>(&self, rom_path: P) -> ResultL {
297        self.run_command(&format!(
298            "-ImportExGFX {}",
299            rom_path.as_ref().to_string_lossy()
300        ))
301    }
302
303    /// Imports all graphics into the passed ROM and returns Lunar Magic's
304    /// text output or a [WrapperErr] if something went wrong.
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// # use lunar_magic_wrapper::Wrapper;
310    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
311    /// let output = lm_wrapper.import_all_graphics("C:/hacks/my_project/my_hack.smc");
312    /// ```
313    pub fn import_all_graphics<P: AsRef<Path>>(&self, rom_path: P) -> ResultL {
314        self.run_command(&format!(
315            "-ImportAllGraphics {}",
316            rom_path.as_ref().to_string_lossy()
317        ))
318    }
319
320    /// Exports the specified level number as an MWL at the specified location from the passed ROM
321    /// and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
322    ///
323    /// # Examples
324    ///
325    /// ```
326    /// # use lunar_magic_wrapper::Wrapper;
327    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
328    /// let output = lm_wrapper.export_level(
329    ///     "C:/hacks/my_project/my_hack.smc",
330    ///     "C:/hacks/my_project/levels/level 105.mwl",
331    ///     105
332    /// );
333    /// ```
334    pub fn export_level<P, M>(&self, rom_path: P, mwl_path: M, level_number: u16) -> ResultL
335    where
336        P: AsRef<Path>,
337        M: AsRef<Path>,
338    {
339        self.run_command(&format!(
340            "-ExportLevel {} {} {}",
341            rom_path.as_ref().to_string_lossy(),
342            mwl_path.as_ref().to_string_lossy(),
343            level_number
344        ))
345    }
346
347    /// Imports the specified MWL file as the (optionally) specified level number
348    /// into the passed ROM and returns Lunar Magic's text output or
349    /// a [WrapperErr] if something went wrong.
350    ///
351    /// # Examples
352    ///
353    /// Without specifying a level number:
354    /// ```
355    /// # use lunar_magic_wrapper::Wrapper;
356    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
357    /// let output = lm_wrapper.import_level(
358    ///     "C:/hacks/my_project/my_hack.smc",
359    ///     "C:/hacks/my_project/levels/level 105.mwl",
360    ///     None
361    /// );
362    /// ```
363    ///
364    /// With specifying a level number:
365    /// ```
366    /// # use lunar_magic_wrapper::Wrapper;
367    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
368    /// let output = lm_wrapper.import_level(
369    ///     "C:/hacks/my_project/my_hack.smc",
370    ///     "C:/hacks/my_project/levels/level 105.mwl",
371    ///     Some(107)
372    /// );
373    /// ```
374    pub fn import_level<P, M>(&self, rom_path: P, mwl_path: M, level_number: Option<u16>) -> ResultL
375    where
376        P: AsRef<Path>,
377        M: AsRef<Path>,
378    {
379        if let Some(level_number) = level_number {
380            self.run_command(&format!(
381                "-ImportLevel {} {} {}",
382                rom_path.as_ref().to_string_lossy(),
383                mwl_path.as_ref().to_string_lossy(),
384                level_number
385            ))
386        } else {
387            self.run_command(&format!(
388                "-ImportLevel {} {}",
389                rom_path.as_ref().to_string_lossy(),
390                mwl_path.as_ref().to_string_lossy()
391            ))
392        }
393    }
394
395    /// Imports the specified map16 file into the passed ROM at the (optionally)
396    /// specified X, Y map16 location using the tileset of the specified level
397    /// and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
398    ///
399    /// # Examples
400    ///
401    /// Without a location:
402    /// ```
403    /// # use lunar_magic_wrapper::*;
404    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
405    /// let output = lm_wrapper.import_map16(
406    ///     "C:/hacks/my_project/my_hack.smc",
407    ///     "C:/hacks/my_project/resources/tiles.map16",
408    ///     105,
409    ///     None
410    /// );
411    /// ```
412    ///
413    /// With a location:
414    /// ```
415    /// # use lunar_magic_wrapper::*;
416    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
417    /// let output = lm_wrapper.import_map16(
418    ///     "C:/hacks/my_project/my_hack.smc",
419    ///     "C:/hacks/my_project/resources/tiles.map16",
420    ///     105,
421    ///     Some((0x02, 0x40))
422    /// );
423    /// ```
424    pub fn import_map16<P, M>(
425        &self,
426        rom_path: P,
427        map16_path: M,
428        level_number: u16,
429        location: Option<(u32, u32)>,
430    ) -> ResultL
431    where
432        P: AsRef<Path>,
433        M: AsRef<Path>,
434    {
435        if let Some((x, y)) = location {
436            self.run_command(&format!(
437                "-ImportMap16 {} {} {} {},{}",
438                rom_path.as_ref().to_string_lossy(),
439                map16_path.as_ref().to_string_lossy(),
440                level_number,
441                x,
442                y
443            ))
444        } else {
445            self.run_command(&format!(
446                "-ImportMap16 {} {} {}",
447                rom_path.as_ref().to_string_lossy(),
448                map16_path.as_ref().to_string_lossy(),
449                level_number
450            ))
451        }
452    }
453
454    /// Imports the passed custom palette file into the specified level in the passed
455    /// ROM and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
456    ///
457    /// # Examples
458    /// ```
459    /// # use lunar_magic_wrapper::*;
460    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
461    /// let output = lm_wrapper.import_custom_palette(
462    ///     "C:/hacks/my_project/my_hack.smc",
463    ///     "C:/hacks/my_project/resources/my_palette.pal",
464    ///     105
465    /// );
466    /// ```
467    pub fn import_custom_palette<P: AsRef<Path>, Q: AsRef<Path>>(
468        &self,
469        rom_path: P,
470        palette_path: Q,
471        level_number: u16,
472    ) -> ResultL {
473        self.run_command(&format!(
474            "-ImportCustomPalette {} {} {}",
475            rom_path.as_ref().to_string_lossy(),
476            palette_path.as_ref().to_string_lossy(),
477            level_number
478        ))
479    }
480
481    /// Exports shared palette from the passed ROM to the specified output path
482    /// and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
483    ///
484    /// # Examples
485    /// ```
486    /// # use lunar_magic_wrapper::*;
487    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
488    /// let output = lm_wrapper.export_shared_palette(
489    ///     "C:/hacks/my_project/my_hack.smc",
490    ///     "C:/hacks/my_project/resources/shared.pal"
491    /// );
492    /// ```
493    pub fn export_shared_palette<P, Q>(&self, rom_path: P, palette_path: Q) -> ResultL
494    where
495        P: AsRef<Path>,
496        Q: AsRef<Path>,
497    {
498        self.run_command(&format!(
499            "-ExportSharedPalette {} {}",
500            rom_path.as_ref().to_string_lossy(),
501            palette_path.as_ref().to_string_lossy()
502        ))
503    }
504
505    /// Imports passed shared palette into the passed ROM
506    /// and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
507    ///
508    /// # Examples
509    /// ```
510    /// # use lunar_magic_wrapper::*;
511    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
512    /// let output = lm_wrapper.import_shared_palette(
513    ///     "C:/hacks/my_project/my_hack.smc",
514    ///     "C:/hacks/my_project/resources/shared.pal"
515    /// );
516    /// ```
517    pub fn import_shared_palette<P, Q>(&self, rom_path: P, palette_path: Q) -> ResultL
518    where
519        P: AsRef<Path>,
520        Q: AsRef<Path>,
521    {
522        self.run_command(&format!(
523            "-ImportSharedPalette {} {}",
524            rom_path.as_ref().to_string_lossy(),
525            palette_path.as_ref().to_string_lossy()
526        ))
527    }
528
529    /// Exports all map16 data from the passed ROM to the specified output path
530    /// and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
531    ///
532    /// # Examples
533    /// ```
534    /// # use lunar_magic_wrapper::*;
535    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
536    /// let output = lm_wrapper.export_all_map16(
537    ///     "C:/hacks/my_project/my_hack.smc",
538    ///     "C:/hacks/my_project/resources/all.map16"
539    /// );
540    /// ```
541    pub fn export_all_map16<P, M>(&self, rom_path: P, map16_path: M) -> ResultL
542    where
543        P: AsRef<Path>,
544        M: AsRef<Path>,
545    {
546        self.run_command(&format!(
547            "-ExportAllMap16 {} {}",
548            rom_path.as_ref().to_string_lossy(),
549            map16_path.as_ref().to_string_lossy()
550        ))
551    }
552
553    /// Imports the passed all map16 file into the passed ROM
554    /// and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
555    ///
556    /// # Examples
557    /// ```
558    /// # use lunar_magic_wrapper::*;
559    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
560    /// let output = lm_wrapper.import_all_map16(
561    ///     "C:/hacks/my_project/my_hack.smc",
562    ///     "C:/hacks/my_project/resources/all.map16"
563    /// );
564    /// ```
565    pub fn import_all_map16<P, M>(&self, rom_path: P, map16_path: M) -> ResultL
566    where
567        P: AsRef<Path>,
568        M: AsRef<Path>,
569    {
570        self.run_command(&format!(
571            "-ImportAllMap16 {} {}",
572            rom_path.as_ref().to_string_lossy(),
573            map16_path.as_ref().to_string_lossy()
574        ))
575    }
576
577    /// Exports multiple levels from the passed ROM to the specified
578    /// location using the (optionally) specified flags and returns
579    /// Lunar Magic's text output or a [WrapperErr] if something went wrong.
580    ///
581    /// Flags can be specified using the [LevelExportFlag] enum.
582    /// Note that if flags are omitted, Lunar Magic will use its
583    /// default settings for them.
584    ///
585    /// # Examples
586    ///
587    /// Without flags:
588    /// ```
589    /// # use lunar_magic_wrapper::*;
590    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
591    /// // MWL files will be prefixed with "level ", i.e. "level 105.mwl", etc.
592    /// // and be contained in `C:/hacks/my_project/resources/levels`
593    /// let output = lm_wrapper.export_mult_levels(
594    ///     "C:/hacks/my_project/my_hack.smc",
595    ///     "C:/hacks/my_project/resources/levels/level ",
596    ///     None
597    /// );
598    /// ```
599    ///
600    /// With flags:
601    /// ```
602    /// # use lunar_magic_wrapper::*;
603    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
604    /// // MWL files will be prefixed with "level ", i.e. "level 105.mwl", etc.
605    /// // and be contained in `C:/hacks/my_project/resources/levels`
606    /// let output = lm_wrapper.export_mult_levels(
607    ///     "C:/hacks/my_project/my_hack.smc",
608    ///     "C:/hacks/my_project/resources/levels/level ",
609    ///     Some(LevelExportFlag::None)
610    /// );
611    /// ```
612    pub fn export_mult_levels<P: AsRef<Path>, M: AsRef<Path>>(
613        &self,
614        rom_path: P,
615        mwl_path: M,
616        flags: Option<LevelExportFlag>,
617    ) -> ResultL {
618        if let Some(flags) = flags {
619            self.run_command(&format!(
620                "-ExportMultLevels {} {} {}",
621                rom_path.as_ref().to_string_lossy(),
622                mwl_path.as_ref().to_string_lossy(),
623                flags.bits()
624            ))
625        } else {
626            self.run_command(&format!(
627                "-ExportMultLevels {} {}",
628                rom_path.as_ref().to_string_lossy(),
629                mwl_path.as_ref().to_string_lossy()
630            ))
631        }
632    }
633
634    /// Imports multiple levels into the passed ROM from the specified
635    /// location using the (optionally) specified flags and returns
636    /// Lunar Magic's text output or a [WrapperErr] if something went wrong.
637    ///
638    /// Flags can be specified using the [LevelImportFlag] enum.
639    /// Note that if flags are omitted, Lunar Magic will use its
640    /// default settings for them.
641    ///
642    /// # Examples
643    ///
644    /// Without flags:
645    /// ```
646    /// # use lunar_magic_wrapper::*;
647    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
648    /// let output = lm_wrapper.import_mult_levels(
649    ///     "C:/hacks/my_project/my_hack.smc",
650    ///     "C:/hacks/my_project/resources/levels",
651    ///     None
652    /// );
653    /// ```
654    ///
655    /// With flags:
656    /// ```
657    /// # use lunar_magic_wrapper::*;
658    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
659    /// let output = lm_wrapper.import_mult_levels(
660    ///     "C:/hacks/my_project/my_hack.smc",
661    ///     "C:/hacks/my_project/resources/levels",
662    ///     Some(LevelImportFlag::None)
663    /// );
664    /// ```
665    pub fn import_mult_levels<P: AsRef<Path>, L: AsRef<Path>>(
666        &self,
667        rom_path: P,
668        level_directory: L,
669        flags: Option<LevelImportFlag>,
670    ) -> ResultL {
671        if let Some(flags) = flags {
672            self.run_command(&format!(
673                "-ImportMultLevels {} {} {}",
674                rom_path.as_ref().to_string_lossy(),
675                level_directory.as_ref().to_string_lossy(),
676                flags.bits()
677            ))
678        } else {
679            self.run_command(&format!(
680                "-ImportMultLevels {} {}",
681                rom_path.as_ref().to_string_lossy(),
682                level_directory.as_ref().to_string_lossy()
683            ))
684        }
685    }
686
687    /// Expands the passed ROM to the specified size
688    /// and returns Lunar Magic's text output or an
689    /// [WrapperErr] if something went wrong.
690    ///
691    /// # Examples
692    /// ```
693    /// # use lunar_magic_wrapper::*;
694    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
695    /// let output = lm_wrapper.expand_rom(
696    ///     "C:/hacks/my_project/my_hack.smc",
697    ///     RomSize::_4mb
698    /// );
699    /// ```
700    pub fn expand_rom<P: AsRef<Path>>(&self, rom_path: P, rom_size: RomSize) -> ResultL {
701        self.run_command(&format!(
702            "-ExpandROM {} {}",
703            rom_path.as_ref().to_string_lossy(),
704            rom_size
705        ))
706    }
707
708    /// Changes the compression of the passed ROM to the specified format
709    /// and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
710    ///
711    /// # Examples
712    /// ```
713    /// # use lunar_magic_wrapper::*;
714    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
715    /// let output = lm_wrapper.change_compression(
716    ///     "C:/hacks/my_project/my_hack.smc",
717    ///     CompressionFormat::LcLz2Speed
718    /// );
719    /// ```
720    pub fn change_compression<P: AsRef<Path>>(
721        &self,
722        rom_path: P,
723        compression_format: CompressionFormat,
724    ) -> ResultL {
725        self.run_command(&format!(
726            "-ChangeCompression {} {}",
727            rom_path.as_ref().to_string_lossy(),
728            compression_format
729        ))
730    }
731
732    /// Transfers level global ExAnimation data from source ROM to destination ROM and
733    /// return Lunar Magic's text output or a [WrapperErr] if something went wrong.
734    ///
735    /// # Examples
736    /// ```
737    /// # use lunar_magic_wrapper::*;
738    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
739    /// let output = lm_wrapper.transfer_level_global_exanim(
740    ///     "C:/hacks/my_project/destination.smc",
741    ///     "C:/hacks/my_project/source.smc"
742    /// );
743    /// ```
744    pub fn transfer_level_global_exanim<D, S>(&self, dest_rom_path: D, src_rom_path: S) -> ResultL
745    where
746        D: AsRef<Path>,
747        S: AsRef<Path>,
748    {
749        self.run_command(&format!(
750            "-TransferLevelGlobalExAnim {} {}",
751            dest_rom_path.as_ref().to_string_lossy(),
752            src_rom_path.as_ref().to_string_lossy()
753        ))
754    }
755
756    /// Transfers overworld data from source ROM to destination ROM and
757    /// return Lunar Magic's text output or a [WrapperErr] if something went wrong.
758    ///
759    /// # Examples
760    /// ```
761    /// # use lunar_magic_wrapper::*;
762    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
763    /// let output = lm_wrapper.transfer_overworld(
764    ///     "C:/hacks/my_project/destination.smc",
765    ///     "C:/hacks/my_project/source.smc"
766    /// );
767    /// ```
768    pub fn transfer_overworld<D, S>(&self, dest_rom_path: D, src_rom_path: S) -> ResultL
769    where
770        D: AsRef<Path>,
771        S: AsRef<Path>,
772    {
773        self.run_command(&format!(
774            "-TransferOverworld {} {}",
775            dest_rom_path.as_ref().to_string_lossy(),
776            src_rom_path.as_ref().to_string_lossy()
777        ))
778    }
779
780    /// Transfers title screen data from source ROM to destination ROM and
781    /// return Lunar Magic's text output or a [WrapperErr] if something went wrong.
782    ///
783    /// # Examples
784    /// ```
785    /// # use lunar_magic_wrapper::*;
786    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
787    /// let output = lm_wrapper.transfer_title_screen(
788    ///     "C:/hacks/my_project/destination.smc",
789    ///     "C:/hacks/my_project/source.smc"
790    /// );
791    /// ```
792    pub fn transfer_title_screen<D, S>(&self, dest_rom_path: D, src_rom_path: S) -> ResultL
793    where
794        D: AsRef<Path>,
795        S: AsRef<Path>,
796    {
797        self.run_command(&format!(
798            "-TransferTitleScreen {} {}",
799            dest_rom_path.as_ref().to_string_lossy(),
800            src_rom_path.as_ref().to_string_lossy()
801        ))
802    }
803
804    /// Transfers credit data from source ROM to destination ROM and
805    /// return Lunar Magic's text output or a [WrapperErr] if something went wrong.
806    ///
807    /// # Examples
808    /// ```
809    /// # use lunar_magic_wrapper::*;
810    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
811    /// let output = lm_wrapper.transfer_credits(
812    ///     "C:/hacks/my_project/destination.smc",
813    ///     "C:/hacks/my_project/source.smc"
814    /// );
815    /// ```
816    pub fn transfer_credits<D, S>(&self, dest_rom_path: D, src_rom_path: S) -> ResultL
817    where
818        D: AsRef<Path>,
819        S: AsRef<Path>,
820    {
821        self.run_command(&format!(
822            "-TransferCredits {} {}",
823            dest_rom_path.as_ref().to_string_lossy(),
824            src_rom_path.as_ref().to_string_lossy()
825        ))
826    }
827
828    /// Exports title screen movement data from the passed ROM to the specified location
829    /// and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
830    ///
831    /// # Examples
832    /// ```
833    /// # use lunar_magic_wrapper::*;
834    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
835    /// let output = lm_wrapper.export_title_moves(
836    ///     "C:/hacks/my_project/my_hack.smc",
837    ///     "C:/hacks/my_project/resources/title_screen_movement.zst"
838    /// );
839    /// ```
840    pub fn export_title_moves<D, S>(&self, rom_path: D, title_moves_path: S) -> ResultL
841    where
842        D: AsRef<Path>,
843        S: AsRef<Path>,
844    {
845        self.run_command(&format!(
846            "-ExportTitleMoves {} {}",
847            rom_path.as_ref().to_string_lossy(),
848            title_moves_path.as_ref().to_string_lossy()
849        ))
850    }
851
852    /// Imports title screen movement data into the passed ROM from the specified location
853    /// and returns Lunar Magic's text output or a [WrapperErr] if something went wrong.
854    ///
855    /// # Examples
856    /// ```
857    /// # use lunar_magic_wrapper::*;
858    /// # let lm_wrapper = Wrapper::new("C:/lunar_magic.exe");
859    /// let output = lm_wrapper.import_title_moves(
860    ///     "C:/hacks/my_project/my_hack.smc",
861    ///     "C:/hacks/my_project/resources/title_screen_movement.zst"
862    /// );
863    /// ```
864    pub fn import_title_moves<P, T>(&self, rom_path: P, title_moves_path: T) -> ResultL
865    where
866        P: AsRef<Path>,
867        T: AsRef<Path>,
868    {
869        self.run_command(&format!(
870            "-ImportTitleMoves {} {}",
871            rom_path.as_ref().to_string_lossy(),
872            title_moves_path.as_ref().to_string_lossy()
873        ))
874    }
875
876    fn run_command(&self, command_string: &str) -> ResultL {
877        if !self.lunar_magic_path.exists() {
878            return Err(WrapperErr::LunarMagicMissing {
879                command: format!(
880                    "{} {}",
881                    self.lunar_magic_path.to_string_lossy(),
882                    command_string
883                ),
884            });
885        }
886
887        self.run_and_log(command_string)
888    }
889
890    #[cfg(target_os = "windows")]
891    fn build_command(main_command: &str, log_file_path: &Path) -> Command {
892        let args = format!("{} > {}", &main_command, log_file_path.to_string_lossy());
893
894        // Unfortunately, Lunar Magic writes directly to the console rather than to
895        // standard output/error and the only way I've found to suppress and get
896        // its output is to pipe it into a file with >, which I think I can only
897        // really manage by running via cmd here
898        let mut cmd = Command::new("cmd");
899        cmd.args(["/C", &args]);
900
901        cmd
902    }
903
904    #[cfg(not(target_os = "windows"))]
905    fn build_command(main_command: &str, log_file_path: &PathBuf) -> Command {
906        let args = format!("{} > {}", &main_command, log_file_path.to_string_lossy());
907
908        let mut cmd = Command::new("wine");
909        cmd.args(["cmd", "/C", &args]);
910
911        cmd
912    }
913
914    fn run_and_log(&self, command_string: &str) -> ResultL {
915        let main_command = format!(
916            "{} {}",
917            self.lunar_magic_path.to_string_lossy(),
918            command_string,
919        );
920
921        if let Ok(log_dir) = tempdir() {
922            let log_file_path = log_dir.path().join("lunar_magic.log");
923
924            let mut cmd = Self::build_command(&main_command, &log_file_path);
925            let output = cmd.output();
926
927            if let Ok(result) = output {
928                if let Ok(log_file) = File::open(log_file_path) {
929                    let lines = BufReader::new(log_file).lines();
930                    let output = lines.map(|l| l.expect("Failed to read line")).collect();
931
932                    if !result.status.success() {
933                        Err(WrapperErr::Operation {
934                            code: result.status.code(),
935                            command: main_command,
936                            output,
937                        })
938                    } else {
939                        Ok(output)
940                    }
941                } else {
942                    Err(WrapperErr::NoTempFile {
943                        command: main_command,
944                    })
945                }
946            } else {
947                let kind = output.err().unwrap().kind();
948
949                #[cfg(not(target_os = "windows"))]
950                if kind == ErrorKind::NotFound {
951                    return Err(WrapperErr::NoWine);
952                }
953
954                Err(WrapperErr::FailedToExecute {
955                    command: main_command,
956                    error_kind: kind,
957                })
958            }
959        } else {
960            Err(WrapperErr::NoTempDir {
961                command: main_command,
962            })
963        }
964    }
965}