use config::{default_save_filename, load_config, save_config, Config}; use eframe::egui; use infos::{Difficulty, LauncherInfo, WadInfo}; use native_dialog::{MessageDialog, MessageType}; use std::path::PathBuf; use std::process::Command; pub mod config; pub mod infos; fn main() -> Result<(), eframe::Error> { let gui_options = eframe::NativeOptions { ..Default::default() }; eframe::run_native( "Rust Doom Launcher", gui_options, Box::new(|_cc| Box::::default()), ) } struct RustDoomLauncher { all_launchers: Vec, all_iwads: Vec, all_pwads: Vec, selected_launcher: Option, selected_iwad: Option, selected_pwads: Vec, name: String, config_filename: PathBuf, config_file_loaded: bool, warp_string: String, difficulty: Difficulty, fast_monsters: bool, respawning_monsters: bool, display_command: bool, } // Great name, I know enum MyErrors { NoLauncher, NoIwad, } impl Default for RustDoomLauncher { fn default() -> Self { Self { all_launchers: Vec::new(), all_iwads: Vec::new(), all_pwads: Vec::new(), selected_launcher: None, selected_iwad: None, selected_pwads: Vec::new(), name: "".to_string(), config_filename: default_save_filename(), config_file_loaded: false, warp_string: String::new(), difficulty: Difficulty::None, fast_monsters: false, respawning_monsters: false, display_command: false, } } } impl RustDoomLauncher { // There must be a better way than this - maybe for the RustDoomLauncher to // have a config thing fn get_config_file_stuff(&mut self) -> () { let config = load_config(&self.config_filename).unwrap(); if let Some(iwads) = config.iwads { for iwad in iwads { self.all_iwads.push(iwad.clone()); } } if let Some(pwads) = config.pwads { for pwad in pwads { self.all_pwads.push(pwad.clone()); } } if let Some(launchers) = config.launchers { for launcher in launchers { self.all_launchers.push(launcher.clone()); } } } fn get_launcher_and_command(&self, show_dialogs: bool) -> Result<(&str, Vec<&str>), MyErrors> { let launcher = match self.selected_launcher { Some(l) => self.all_launchers.get(l).unwrap(), None => { if show_dialogs { MessageDialog::new() .set_type(MessageType::Error) .set_title("I can't let you do that") .set_text("You didn't pick a launcher!\nPlease do that") .show_alert() .unwrap(); } return Err(MyErrors::NoLauncher); } }; let iwad = match self.selected_iwad { Some(i) => self.all_iwads.get(i).unwrap(), None => { if show_dialogs { MessageDialog::new() .set_type(MessageType::Error) .set_title("I can't let you do that") .set_text("You didn't pick an IWAD!\nPlease do that") .show_alert() .unwrap(); } return Err(MyErrors::NoIwad); } }; let mut command = vec!["-iwad", iwad.path.to_str().unwrap()]; for pwad_index in &self.selected_pwads { command.push("-file"); let pwad = self.all_pwads.get(*pwad_index).unwrap(); command.push(pwad.path.to_str().unwrap()); } if self.difficulty != Difficulty::None { command.push("-skill"); command.push(self.difficulty.flag_number()); } if !self.warp_string.is_empty() { command.push("-warp"); command.push(&self.warp_string); } if self.respawning_monsters { command.push("-respawn"); } Ok((launcher.path.to_str().unwrap(), command)) } fn form_command_string(&self) -> String { match self.get_launcher_and_command(false) { Ok((launch, comms)) => { let mut command_string = String::new(); // Feels like a bit of a hack, but it works I think command_string.push_str(&Cow::from(launch).to_string()); for c in comms { command_string.push_str(" \'"); command_string.push_str(c); command_string.push('\'') } command_string } Err(MyErrors::NoLauncher) => "Error: No launcher".to_string(), Err(MyErrors::NoIwad) => "Error: No IWAD".to_string(), } } fn launch_doom(&self) -> () { if let Ok((launcher, command)) = self.get_launcher_and_command(true) { // Note to self, don't hit launch button when running from emacs Command::new(launcher).args(command).spawn(); } } } impl eframe::App for RustDoomLauncher { fn on_close_event(&mut self) -> bool { let config = Config { iwads: Some(self.all_iwads.clone()), pwads: Some(self.all_pwads.clone()), launchers: Some(self.all_launchers.clone()), }; save_config(&self.config_filename, &config).unwrap(); true } fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { if self.config_file_loaded == false { self.get_config_file_stuff(); self.config_file_loaded = true; } egui::containers::panel::CentralPanel::default().show(ctx, |ui| { ui.horizontal(|ui| { ui.heading("RustDoomLauncher"); if ui.button("PLAY SOME DOOM!").clicked() { self.launch_doom(); } }); ui.horizontal_wrapped(|ui| { ui.vertical(|ui| { ui.label("Launchers"); for (pos, launcher) in self.all_launchers.iter().enumerate() { if ui .add(egui::SelectableLabel::new( self.selected_launcher.is_some() && *self.selected_launcher.as_ref().unwrap() == pos, &launcher.name, )) .clicked() { self.selected_launcher = Some(pos); } } }); ui.separator(); ui.vertical(|ui| { ui.set_min_width(100.0); ui.label("IWADs"); for (pos, iwad) in self.all_iwads.iter().enumerate() { if ui .add(egui::SelectableLabel::new( self.selected_iwad.is_some() && *self.selected_iwad.as_ref().unwrap() == pos, &iwad.name, )) .clicked() { self.selected_iwad = Some(pos); } } }); ui.separator(); ui.vertical(|ui| { ui.set_min_width(100.0); ui.label("PWADs and Mods"); for (pos, pwad) in self.all_pwads.iter().enumerate() { if ui .add(egui::SelectableLabel::new( self.selected_pwads.contains(&pos), &pwad.name, )) .clicked() { self.selected_pwads.push(pos); } } }); ui.separator(); ui.vertical(|ui| { ui.label("Add WADs etc"); ui.set_max_width(180.0); ui.horizontal(|ui| { let name_label = ui.label("Name:"); ui.text_edit_singleline(&mut self.name) .labelled_by(name_label.id); }); // It kind of feels like the right place to use a closure - unsure whether I need // to pass both of these vaules in, or what the story is here. let get_name = |path: &PathBuf, name: &String| { if self.name.is_empty() { // Check these unwraps perhaps? Unsure whether the FileDialog can actually // return something that isn't a file path.file_name().unwrap().to_str().unwrap().to_string() } else { name.clone() } }; if ui.button("Add Launcher").clicked() { if let Some(path) = rfd::FileDialog::new().pick_file() { self.all_launchers.push(LauncherInfo { name: get_name(&path, &self.name), path, }); } } if ui.button("Add IWAD").clicked() { if let Some(path) = rfd::FileDialog::new().pick_file() { self.all_iwads.push(WadInfo { name: get_name(&path, &self.name), path, }); } } if ui.button("Add PWAD or Mod").clicked() { if let Some(path) = rfd::FileDialog::new().pick_file() { self.all_pwads.push(WadInfo { name: get_name(&path, &self.name), path, }); } } }); }); ui.separator(); ui.horizontal_wrapped(|ui| { ui.horizontal(|ui| { ui.set_max_width(180.0); let warp_label = ui.label("warp"); ui.text_edit_singleline(&mut self.warp_string) .labelled_by(warp_label.id); }); ui.separator(); ui.horizontal(|ui| { ui.label("difficulty"); eframe::egui::ComboBox::new("difficulty", "") .selected_text(format!("{}", self.difficulty)) .show_ui(ui, |ui| { for diff in Difficulty::iterator() { ui.selectable_value(&mut self.difficulty, *diff, diff.to_string()); } }); }); ui.separator(); ui.horizontal(|ui| { ui.label("fast"); ui.checkbox(&mut self.fast_monsters, ""); }); ui.separator(); ui.horizontal(|ui| { ui.label("respawn"); ui.checkbox(&mut self.respawning_monsters, ""); }); }); ui.separator(); ui.horizontal(|ui| { ui.label("Command"); ui.checkbox(&mut self.display_command, ""); }); ui.horizontal_wrapped(|ui| { // I don't actually think using SelectableLabel is correct here - // but it'll at least do the highlighting when hovered nicely if let Some(l) = self.selected_launcher { if let Some(iwad) = self.all_launchers.get(l) { if ui .add(egui::SelectableLabel::new(false, &iwad.name)) .clicked() { self.selected_launcher = None; } } else { self.selected_launcher = None; } } else { ui.label("Select a launcher plz"); } if let Some(i) = self.selected_iwad { if let Some(iwad) = self.all_iwads.get(i) { if ui .add(egui::SelectableLabel::new(false, &iwad.name)) .clicked() { self.selected_iwad = None; } } else { self.selected_iwad = None; } } else { ui.label("Select an iwad plz"); } // This feels ver much more C-like, but I think it works? // Probably should have a little bit more checking around the unwrap calls let mut pos = 0_usize; while pos < self.selected_pwads.len() { if ui .add(egui::SelectableLabel::new( false, &self .all_pwads .get(*self.selected_pwads.get(pos).unwrap()) .unwrap() .name, )) .clicked() { self.selected_pwads.remove(pos); continue; } pos += 1; } }); if self.display_command { ui.horizontal(|ui| { let mut text = self.form_command_string(); let window_size = frame.info().window_info.size; ui.add( egui::TextEdit::multiline(&mut text).desired_width(window_size[0] * 0.9), ); }); } }); } }