mod omori_locator; mod app_logic; mod config; mod mod_builder; use std::{process::exit, sync::Arc, thread, time::SystemTime, default::Default}; use app_logic::{AppThread, UiEvent, UiState, UiStateHolder}; use crossbeam_channel::{Receiver, Sender}; use eframe::egui; use egui::{mutex::Mutex, Align, Layout, RichText, TextStyle, ThemePreference}; use egui_alignments::{center_vertical, top_horizontal}; use egui_extras::{Column, TableBuilder}; use egui_modal::Modal; use mod_builder::ModConfiguration; use sha2::Digest; fn main() -> anyhow::Result<()> { env_logger::init(); #[cfg(target_os = "windows")] log::trace!("How do you even do serious work on ValorantOS??"); #[cfg(target_os = "macos")] log::trace!("Mac user, rich fuck, send me $500"); #[cfg(target_os = "linux")] log::trace!("Linux user... Please touch grass for once"); let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_min_inner_size([640.0, 480.0]) .with_inner_size([640.0, 480.0]), ..Default::default() }; let app_state = UiStateHolder { state: Arc::new(Mutex::new(UiState::Loading)) }; let (sender, receiver): (Sender, Receiver) = crossbeam_channel::unbounded(); eframe::run_native("TundleBool", options, Box::new(|cc| { cc.egui_ctx.set_theme(ThemePreference::System); cc.egui_ctx.style_mut(|style| { style.interaction.selectable_labels = false; }); let mut app_thread = AppThread::create(&app_state, &cc.egui_ctx, &receiver); thread::spawn(move || { app_thread.main(); }); Ok(Box::::new(Application::create(sender, app_state))) })).unwrap(); Ok(()) } struct Application { state: UiStateHolder, sender: Sender, key_input: String, configuration: ModConfiguration, did_fill_configuration: bool } impl Application { fn create(sender: Sender, state: UiStateHolder) -> Application { Application { sender, state, key_input: "".to_string(), configuration: Default::default(), did_fill_configuration: false } } } const GAME_KEY_HASH: &[u8; 32] = include_bytes!("keyhash"); impl eframe::App for Application { fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { exit(0); // TODO: Prompt the user to maybe please consider not actually exiting the app while it's working } fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { let state = self.state.state.lock(); let state = state.clone(); ctx.request_repaint_after_secs(10.0); match state { UiState::Loading => { egui::CentralPanel::default().show(ctx, |ui| { center_vertical(ui,|ui| { ui.label(RichText::new("Loading").size(32.0)); }); }); }, UiState::Error(reason) => { egui::TopBottomPanel::top("error_title_bar").show(ctx, |ui| { ui.with_layout(Layout::top_down(Align::Center), |ui| { ui.label(RichText::new("An error has occured :(").size(32.0)); }); }); egui::CentralPanel::default().show(ctx, |ui| { center_vertical(ui,|ui| { ui.label(RichText::new(reason).size(32.0)); }); }); }, UiState::KeyRequired(reason) => { let mut is_valid = true; let hash = sha2::Sha256::digest(&self.key_input.as_bytes()); for i in 0..32 { if hash[i] != GAME_KEY_HASH[i] { is_valid = false; } } egui::TopBottomPanel::top("key_prompt_title_bar").show(ctx, |ui| { ui.with_layout(Layout::top_down(Align::Center), |ui| { ui.label(RichText::new("Decryption key required").size(32.0)); ui.label(RichText::new(format!("Reason: {reason}")).size(24.0)); }); }); egui::CentralPanel::default().show(ctx, |ui| { top_horizontal(ui, |ui| { ui.horizontal(|ui| { ui.label("Decryption key: "); ui.text_edit_singleline(&mut self.key_input); if is_valid { ui.label("Valid"); } else { ui.label(RichText::new("Invalid").color(ui.visuals().error_fg_color)); } }); }); }); egui::TopBottomPanel::bottom("key_prompt_button_bar").show(ctx, |ui| { if !is_valid { ui.disable(); } ui.with_layout(Layout::top_down(Align::Max), |ui| { if ui.button("Continue").clicked() { self.sender.send(UiEvent::SetKey(self.key_input.as_bytes().to_vec())).expect("Failed to send"); } }) }); }, UiState::PickGame(steam, others) => { let invalid_path_modal = Modal::new(ctx, "invalid_path_modal"); invalid_path_modal.show(|ui| { invalid_path_modal.title(ui, "Invalid path"); invalid_path_modal.frame(ui, |ui| { invalid_path_modal.body(ui, "Please pick a correct installation of OMORI."); }); invalid_path_modal.buttons(ui, |ui| { invalid_path_modal.button(ui, "OK"); }); }); egui::TopBottomPanel::top("game_prompt_title_bar").show(ctx, |ui| { ui.with_layout(Layout::top_down(Align::Center), |ui| { ui.label(RichText::new("Select your base game").size(32.0)); }); }); egui::CentralPanel::default().show(ctx, |ui| { ui.columns(2, |columns| { columns[0].vertical(|ui| { ui.label(RichText::new("Steam Installation").size(24.0)); match steam { Ok(path) => { ui.label(RichText::new(format!("Found game at:\n{}", path.display())).italics()); ui.separator(); if ui.button(RichText::new("Use Steam version").size(16.0)).clicked() { self.sender.send(UiEvent::UseSteamPath).expect("Failed to send"); } }, Err(e) => { ui.label(RichText::new(format!("Failed to find game:\n{}", e)).color(ui.visuals().error_fg_color)); } } }); columns[1].vertical(|ui| { ui.label(RichText::new("Custom").size(24.0)); if ui.button(RichText::new("Pick game location").size(16.0)).clicked() { match rfd::FileDialog::new().pick_folder() { Some(path) => { if omori_locator::validate_omori_installation(&path) { self.sender.send(UiEvent::UsePath(path)).expect("Failed to send"); } else { invalid_path_modal.open(); } }, None => {} } } ui.separator(); if others.len() > 0 { ui.label("History of game locations"); TableBuilder::new(ui) .striped(true) .column(Column::remainder()) .column(Column::auto().at_least(100.0)) .sense(egui::Sense::hover() | egui::Sense::click()) .body(|body| { body.rows(20.0, others.len(), |mut row| { let item = &others[row.index()]; let dt = (item.1 as i64) - (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).expect("Failed to get the time").as_secs() as i64); let dt = chrono::Duration::from(chrono::TimeDelta::seconds(dt as i64)); row.col(|ui| { ui.with_layout(Layout::right_to_left(Align::Center).with_main_align(Align::Min).with_main_justify(true), |ui| { ui.label(format!("{}", item.0.display())); }); }); row.col(|ui| { ui.with_layout(Layout::right_to_left(Align::Center).with_main_align(Align::Max), |ui| { ui.label(format!("{}", chrono_humanize::HumanTime::from(dt))); }); }); if row.response().clicked() { if omori_locator::validate_omori_installation(&item.0) { self.sender.send(UiEvent::UsePath(item.0.clone())).expect("Failed to send"); } else { invalid_path_modal.open(); } } if row.response().hovered() { ctx.set_cursor_icon(egui::CursorIcon::PointingHand); } }); }); } else { ui.label("Once you use a custom location, it will be remembered here."); } }); }); }); }, UiState::PickPlaytest(playtest_history) => { let invalid_path_modal = Modal::new(ctx, "invalid_playtest_modal"); invalid_path_modal.show(|ui| { invalid_path_modal.title(ui, "Invalid path"); invalid_path_modal.frame(ui, |ui| { invalid_path_modal.body(ui, "Please pick a valid, Oneloader-generated playtest."); }); invalid_path_modal.buttons(ui, |ui| { invalid_path_modal.button(ui, "OK"); }); }); egui::TopBottomPanel::top("playtest_prompt_title_bar").show(ctx, |ui| { ui.with_layout(Layout::top_down(Align::Center), |ui| { ui.label(RichText::new("Select your playtest").size(32.0)); }); }); egui::CentralPanel::default().show(ctx, |ui| { ui.with_layout(Layout::right_to_left(Align::Min).with_main_justify(true).with_main_align(Align::Center), |ui| { if ui.button(RichText::new("Pick playtest").size(16.0)).clicked() { match rfd::FileDialog::new().pick_folder() { Some(path) => { if omori_locator::validate_playtest(&path) { self.sender.send(UiEvent::UsePath(path)).expect("Failed to send"); } else { invalid_path_modal.open(); } }, None => {} } } }); if playtest_history.len() > 0 { ui.label("History of playtests"); ui.separator(); TableBuilder::new(ui) .striped(true) .column(Column::remainder()) .column(Column::auto().at_least(100.0)) .sense(egui::Sense::hover() | egui::Sense::click()) .body(|body| { body.rows(20.0, playtest_history.len(), |mut row| { let item = &playtest_history[row.index()]; let dt = (item.1 as i64) - (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).expect("Failed to get the time").as_secs() as i64); let dt = chrono::Duration::from(chrono::TimeDelta::seconds(dt as i64)); row.col(|ui| { ui.with_layout(Layout::right_to_left(Align::Center).with_main_align(Align::Min).with_main_justify(true), |ui| { ui.label(format!("{}", item.0.display())); }); }); row.col(|ui| { ui.with_layout(Layout::right_to_left(Align::Center).with_main_align(Align::Max), |ui| { ui.label(format!("{}", chrono_humanize::HumanTime::from(dt))); }); }); if row.response().clicked() { if omori_locator::validate_playtest(&item.0) { self.sender.send(UiEvent::UsePath(item.0.clone())).expect("Failed to send"); } else { invalid_path_modal.open(); } } if row.response().hovered() { ctx.set_cursor_icon(egui::CursorIcon::PointingHand); } }); }); } else { ui.label("Any playtests you've used before will be remembered here."); } }); }, UiState::Configure(initial_configuration) => { if !self.did_fill_configuration { self.did_fill_configuration = true; self.configuration = initial_configuration; } let mut font = TextStyle::Body.resolve(&ctx.style()); font.size = 16.0; egui::TopBottomPanel::top("configure_title_bar").show(ctx, |ui| { ui.with_layout(Layout::top_down(Align::Center), |ui| { ui.label(RichText::new("Configure your mod").size(32.0)); }); }); egui::CentralPanel::default().show(ctx, |ui| { top_horizontal(ui, |ui| { TableBuilder::new(ui) .column(Column::auto().at_least(100.0)) .column(Column::exact(300.0)) .body(|mut body| { body.row(24.0, |mut row| { row.col(|ui| { ui.label(RichText::new("Mod ID").size(16.0)); }); row.col(|ui| { ui.add(egui::TextEdit::singleline(&mut self.configuration.mod_id).font(font.clone())); }); }); body.row(24.0, |mut row| { row.col(|ui| { ui.label(RichText::new("Mod Name").size(16.0)); }); row.col(|ui| { ui.add(egui::TextEdit::singleline(&mut self.configuration.mod_name).font(font.clone())); }); }); body.row(24.0, |mut row| { row.col(|ui| { ui.label(RichText::new("Mod Description").size(16.0)); }); row.col(|ui| { ui.add(egui::TextEdit::singleline(&mut self.configuration.mod_description).font(font.clone())); }); }); body.row(24.0, |mut row| { row.col(|ui| { ui.label(RichText::new("Mod Version").size(16.0)); }); row.col(|ui| { ui.add(egui::TextEdit::singleline(&mut self.configuration.mod_version).font(font.clone())); }); }); }); }); ui.separator(); top_horizontal(ui, |ui| { ui.vertical(|ui| { ui.add(egui::Checkbox::new(&mut self.configuration.include_audio, RichText::new("Include /audio").size(16.0))); ui.add(egui::Checkbox::new(&mut self.configuration.include_data, RichText::new("Include /data").size(16.0))); ui.add(egui::Checkbox::new(&mut self.configuration.include_fonts, RichText::new("Include /fonts").size(16.0))); ui.add(egui::Checkbox::new(&mut self.configuration.include_icon, RichText::new("Include /icon").size(16.0))); ui.add(egui::Checkbox::new(&mut self.configuration.include_img, RichText::new("Include /img").size(16.0))); ui.add(egui::Checkbox::new(&mut self.configuration.include_languages, RichText::new("Include /languages").size(16.0))); ui.add(egui::Checkbox::new(&mut self.configuration.include_maps, RichText::new("Include /maps").size(16.0))); ui.add(egui::Checkbox::new(&mut self.configuration.include_movies, RichText::new("Include /movies").size(16.0))); }); }); // TODO: Implement performance settings here }); egui::TopBottomPanel::bottom("configuration_start").show(ctx, |ui| { let mut is_valid = self.configuration.include_audio || self.configuration.include_data || self.configuration.include_fonts || self.configuration.include_icon || self.configuration.include_img || self.configuration.include_languages || self.configuration.include_maps || self.configuration.include_movies || self.configuration.include_plugins; if self.configuration.mod_description.len() < 2 { is_valid = false; } if self.configuration.mod_id.len() < 2 { is_valid = false; } if self.configuration.mod_name.len() < 2 { is_valid = false; } if self.configuration.mod_version.len() < 2 { is_valid = false; } ui.with_layout(Layout::top_down(Align::Max), |ui| { if !is_valid { ui.disable(); } if ui.button("Continue").clicked() { self.sender.send(UiEvent::UseConfiguration(self.configuration.clone())).expect("Failed to send"); } }) }); } } } }