Files
tundlebool/src/main.rs
2025-04-18 22:15:32 +02:00

435 lines
21 KiB
Rust

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<UiEvent>, Receiver<UiEvent>) = 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::<Application>::new(Application::create(sender, app_state)))
})).unwrap();
Ok(())
}
struct Application {
state: UiStateHolder,
sender: Sender<UiEvent>,
key_input: String,
configuration: ModConfiguration,
did_fill_configuration: bool
}
impl Application {
fn create(sender: Sender<UiEvent>, 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");
}
})
});
}
}
}
}