G4Patcher 0.14.0

A code injection patcher tool for Pokémon Gen 4 ROM hacks
use crate::constants::{PATCH_DIRECTIVE, PREASSEMBLE_DIRECTIVE};
use crate::filedialog::{get_patch_path, get_project_path};
use crate::io::Error;
use crate::run_armips;
use crate::synthoverlay_utils::handle_synthoverlay;
use crate::usage_checks::{determine_game_version, is_patch_compatible, needs_synthoverlay};
use eframe::egui;
use eframe::egui::{Color32, RichText};
use log::{debug, error, info, warn, Level, LevelFilter, Log, Metadata, Record};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::{fs, io};

pub struct GuiLogger {
    log_buffer: Mutex<Vec<LogEntry>>,
}
#[derive(Clone)]
pub struct LogEntry {
    pub level: Level,
    pub message: String,
}
static LOGGER: OnceLock<GuiLogger> = OnceLock::new();
impl Log for GuiLogger {
    fn enabled(&self, _: &Metadata) -> bool {
        true
    }
    fn log(&self, record: &Record) {
        if self.enabled(record.metadata()) {
            let mut buffer = self.log_buffer.lock().unwrap();
            buffer.push(LogEntry {
                level: record.level(),
                message: format!("{}", record.args()),
            });
        }
    }
    fn flush(&self) {}
}

impl GuiLogger {
    pub fn get_logs() -> Vec<LogEntry> {
        LOGGER
            .get()
            .map(|l| l.log_buffer.lock().unwrap().clone())
            .unwrap_or_default()
    }
    pub fn init(max_level: LevelFilter) {
        LOGGER
            .set(Self {
                log_buffer: Mutex::new(Vec::new()),
            })
            .ok();
        log::set_logger(LOGGER.get().unwrap()).unwrap();
        log::set_max_level(max_level);
    }
}

pub struct G4PatcherApp {
    project_path: Option<String>,
    patch: Option<String>,
    game_version: Option<String>,
    exe_dir: PathBuf,
}

impl Default for G4PatcherApp {
    fn default() -> Self {
        Self {
            project_path: None,
            patch: None,
            game_version: None,
            exe_dir: std::env::current_exe()
                .ok()
                .and_then(|p| p.parent().map(Path::to_path_buf))
                .unwrap_or_else(|| PathBuf::from(".")),
        }
    }
}

impl eframe::App for G4PatcherApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("Pokemon Gen 4 Code Injection Patcher");
            ui.horizontal(|ui| {
                if ui.button("Select ROM Folder").clicked() {
                    if let Some(path) = get_project_path() {
                        self.project_path = Some(path.display().to_string());
                        self.game_version = match determine_game_version(&path.display().to_string()) {
                            Ok(version) => {
                                info!("Game version: {version}");
                                Some(version.to_string())
                            },
                            Err(e) => {
                                error!("Error determining game version: {e}\nPlease ensure you are selecting the ROM folder, usually called 'romname_DSPRE_contents.'");
                                self.project_path = None;
                                None
                            }
                        };
                    } else {self.project_path = None;}
                }
                ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP).with_main_wrap(true), |ui| {
                    ui.label(RichText::new(self.project_path.clone()
                        .unwrap_or_else(|| "No ROM folder selected".to_string())));
                });
            });
            ui.horizontal(|ui| {
                let selectpatch_enabled = self.project_path.is_some();
                if ui.add_enabled(selectpatch_enabled, egui::Button::new("Select Patch")).clicked() {
                    if let Some(file) = get_patch_path(&self.exe_dir) {
                        self.patch = Some(file.display().to_string());
                        if !is_patch_compatible(&self.patch.clone().unwrap(), &self.project_path.clone().unwrap()) {
                            warn!("This patch is not compatible with this ROM, please select a compatible patch.");
                            self.patch = None;
                        }
                    } else {self.patch = None;}
                }
                ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP).with_main_wrap(true), |ui| {
                    ui.label(RichText::new(self.patch.clone()
                        .unwrap_or_else(|| "No patch selected".to_string())));
                });
            });
            ui.horizontal(|ui| {
                let apply_enabled = self.project_path.is_some() && self.patch.is_some();
                if ui.add_enabled(apply_enabled, egui::Button::new("Apply Patch")).clicked() {
                    if needs_synthoverlay(&self.patch.clone().unwrap()) {
                        if let Err(e) = run_armips(&self.patch.clone().unwrap(),
                                                   &self.project_path.clone().unwrap(),
                                                   &self.exe_dir,
                                                   PREASSEMBLE_DIRECTIVE) {
                            error!("Failed to assemble the patch: {e}");
                        }
                        let patch_size = fs::metadata(format!("{}/temp.bin", self.project_path.clone().unwrap()))
                            .map_err(|e| Error::new(io::ErrorKind::NotFound, format!("Failed to read temp.bin: {}", e))).unwrap()
                            .len() as usize;
                        info!("Patch size: {patch_size} bytes");
                        fs::remove_file(format!("{}/temp.bin", self.project_path.clone().unwrap()))
                            .map_err(|e| Error::new(io::ErrorKind::NotFound, format!("Failed to delete temp.bin: {e}"))).unwrap();
                        match handle_synthoverlay(&self.patch.clone().unwrap(), &self.project_path.clone().unwrap(), &self.game_version.clone().unwrap(), patch_size){
                            Ok(()) => debug!("SynthOverlay handled successfully."),
                            Err(e) => {
                                warn!("Failed to handle synthOverlay: {e}");
                                return
                            }
                        }
                    }
                    if matches!(run_armips(&self.patch.clone().unwrap(), &self.project_path.clone().unwrap(), &self.exe_dir, PATCH_DIRECTIVE), Ok(())) {
                        debug!("armips ran successfully.");
                        info!("\nPatch applied! You can now repack your ROM.\n");
                    } else {
                        error!("there was an error running armips, please contact the developer with the log below.");
                    }
                    
                }
            });
            ui.separator();
            ui.collapsing("Limitations", |ui| {
                ui.label("- Does not check if patch is already applied (may duplicate).");
                ui.label("- Does not verify overlay compression (ensure hook overlay is uncompressed).");
            });
            ui.label("Make sure to read the documentation for the patch you are applying!");
            ui.separator();
            ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
                ui.group(|ui| {
                    ui.label("Log:");
                    egui::ScrollArea::vertical()
                        .max_height(ui.available_height())
                        .stick_to_bottom(true)
                        .show(ui, |ui| {
                            for entry in GuiLogger::get_logs() {
                                let color = match entry.level {
                                    Level::Error => Color32::LIGHT_RED,
                                    Level::Warn => Color32::YELLOW,
                                    Level::Info => Color32::WHITE,
                                    Level::Debug => Color32::LIGHT_BLUE,
                                    Level::Trace => Color32::GREEN,
                                };
                                ui.label(RichText::new(format!("[{}] {}", entry.level, entry.message))
                                    .color(color));
                            }
                        });
                });
            });
        });
    }
}