game picker

This commit is contained in:
Rph :3 2025-04-16 23:23:17 +02:00
parent 7796045b13
commit 89f08ff580
No known key found for this signature in database
11 changed files with 872 additions and 11 deletions

10
Cargo.lock generated
View File

@ -984,6 +984,14 @@ dependencies = [
"profiling",
]
[[package]]
name = "egui-modal"
version = "0.6.0"
dependencies = [
"eframe",
"egui",
]
[[package]]
name = "egui-wgpu"
version = "0.31.1"
@ -3245,10 +3253,12 @@ dependencies = [
"dirs",
"eframe",
"egui",
"egui-modal",
"egui_alignments",
"env_logger",
"hex",
"keyvalues-parser",
"log",
"regex",
"registry",
"rfd",

View File

@ -19,4 +19,8 @@ regex = "1.11.1"
sha2 = "0.10.8"
hex = "0.4.3"
egui_alignments = "0.3.4"
log = "0.4.27"
egui-modal = { path = "./egui-modal" }
[workspace]
members = ["egui-modal"]

12
egui-modal/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
Cargo.lock

17
egui-modal/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "egui-modal"
version = "0.6.0"
edition = "2021"
license = "MIT"
description = "a modal library for egui"
repository = "https://github.com/n00kii/egui-modal"
readme = "README.md"
authors = ["n00kii"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
egui = { version = "0.31.1", default-features = false }
[dev-dependencies]
eframe = "0.31.1"

21
egui-modal/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 n00kii
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
egui-modal/src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
#![warn(missing_docs)]
//! egui-modal
//! Modal library for [`egui`]
mod modal;
pub use modal::*;

629
egui-modal/src/modal.rs Normal file
View File

@ -0,0 +1,629 @@
use egui::{
emath::{Align, Align2}, epaint::{Color32, Pos2}, Area, Button, Context, CornerRadius, Id, Layout, Response, RichText, Sense, Ui, WidgetText, Window
};
const ERROR_ICON_COLOR: Color32 = Color32::from_rgb(200, 90, 90);
const INFO_ICON_COLOR: Color32 = Color32::from_rgb(150, 200, 210);
const WARNING_ICON_COLOR: Color32 = Color32::from_rgb(230, 220, 140);
const SUCCESS_ICON_COLOR: Color32 = Color32::from_rgb(140, 230, 140);
const CAUTION_BUTTON_FILL: Color32 = Color32::from_rgb(87, 38, 34);
const SUGGESTED_BUTTON_FILL: Color32 = Color32::from_rgb(33, 54, 84);
const CAUTION_BUTTON_TEXT_COLOR: Color32 = Color32::from_rgb(242, 148, 148);
const SUGGESTED_BUTTON_TEXT_COLOR: Color32 = Color32::from_rgb(141, 182, 242);
const OVERLAY_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 200);
/// The different styles a modal button can take.
pub enum ModalButtonStyle {
/// A normal [`egui`] button
None,
/// A button highlighted blue
Suggested,
/// A button highlighted red
Caution,
}
/// An icon. If used, it will be shown next to the body of
/// the modal.
#[derive(Clone, Default, PartialEq)]
pub enum Icon {
#[default]
/// An info icon
Info,
/// A warning icon
Warning,
/// A success icon
Success,
/// An error icon
Error,
/// A custom icon. The first field in the tuple is
/// the text of the icon, and the second field is the
/// color.
Custom((String, Color32)),
}
impl std::fmt::Display for Icon {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Icon::Info => write!(f, ""),
Icon::Warning => write!(f, ""),
Icon::Success => write!(f, ""),
Icon::Error => write!(f, ""),
Icon::Custom((icon_text, _)) => write!(f, "{icon_text}"),
}
}
}
#[derive(Clone, Default)]
struct DialogData {
title: Option<String>,
body: Option<String>,
icon: Option<Icon>,
}
/// Used for constructing and opening a modal dialog. This can be used
/// to both set the title/body/icon of the modal and open it as a one-time call
/// (as opposed to a continous call in the update loop) at the same time.
/// Make sure to call `DialogBuilder::open` to actually open the dialog.
#[must_use = "use `DialogBuilder::open`"]
pub struct DialogBuilder {
data: DialogData,
modal_id: Id,
ctx: Context,
}
#[derive(Clone)]
enum ModalType {
Modal,
Dialog(DialogData),
}
#[derive(Clone)]
/// Information about the current state of the modal. (Pretty empty
/// right now but may be expanded upon in the future.)
struct ModalState {
is_open: bool,
was_outside_clicked: bool,
modal_type: ModalType,
last_frame_height: Option<f32>,
}
#[derive(Clone, Debug)]
/// Contains styling parameters for the modal, like body margin
/// and button colors.
pub struct ModalStyle {
/// The margin around the modal body. Only applies if using
/// [`.body()`]
pub body_margin: f32,
/// The margin around the container of the icon and body. Only
/// applies if using [`.frame()`]
pub frame_margin: f32,
/// The margin around the container of the icon. Only applies
/// if using [`.icon()`].
pub icon_margin: f32,
/// The size of any icons used in the modal
pub icon_size: f32,
/// The color of the overlay that dims the background
pub overlay_color: Color32,
/// The fill color for the caution button style
pub caution_button_fill: Color32,
/// The fill color for the suggested button style
pub suggested_button_fill: Color32,
/// The text color for the caution button style
pub caution_button_text_color: Color32,
/// The text color for the suggested button style
pub suggested_button_text_color: Color32,
/// The text of the acknowledgement button for dialogs
pub dialog_ok_text: String,
/// The color of the info icon
pub info_icon_color: Color32,
/// The color of the warning icon
pub warning_icon_color: Color32,
/// The color of the success icon
pub success_icon_color: Color32,
/// The color of the error icon
pub error_icon_color: Color32,
/// The default width of the modal
pub default_width: Option<f32>,
/// The default height of the modal
pub default_height: Option<f32>,
/// The alignment of text inside the body
pub body_alignment: Align,
}
impl ModalState {
fn load(ctx: &Context, id: Id) -> Self {
ctx.data_mut(|d| d.get_temp(id).unwrap_or_default())
}
fn save(self, ctx: &Context, id: Id) {
ctx.data_mut(|d| d.insert_temp(id, self))
}
}
impl Default for ModalState {
fn default() -> Self {
Self {
was_outside_clicked: false,
is_open: false,
modal_type: ModalType::Modal,
last_frame_height: None,
}
}
}
impl Default for ModalStyle {
fn default() -> Self {
Self {
body_margin: 5.,
icon_margin: 7.,
frame_margin: 2.,
icon_size: 30.,
overlay_color: OVERLAY_COLOR,
caution_button_fill: CAUTION_BUTTON_FILL,
suggested_button_fill: SUGGESTED_BUTTON_FILL,
caution_button_text_color: CAUTION_BUTTON_TEXT_COLOR,
suggested_button_text_color: SUGGESTED_BUTTON_TEXT_COLOR,
dialog_ok_text: "ok".to_string(),
info_icon_color: INFO_ICON_COLOR,
warning_icon_color: WARNING_ICON_COLOR,
success_icon_color: SUCCESS_ICON_COLOR,
error_icon_color: ERROR_ICON_COLOR,
default_height: None,
default_width: None,
body_alignment: Align::Min,
}
}
}
/// A [`Modal`] is created using [`Modal::new()`]. Make sure to use a `let` binding when
/// using [`Modal::new()`] to ensure you can call things like [`Modal::open()`] later on.
/// ```
/// let modal = Modal::new(ctx, "my_modal");
/// modal.show(|ui| {
/// ui.label("Hello world!")
/// });
/// if ui.button("modal").clicked() {
/// modal.open();
/// }
/// ```
/// Helper functions are also available to use that help apply margins based on the modal's
/// [`ModalStyle`]. They are not necessary to use, but may help reduce boilerplate.
/// ```
/// let other_modal = Modal::new(ctx, "another_modal");
/// other_modal.show(|ui| {
/// other_modal.frame(ui, |ui| {
/// other_modal.body(ui, "Hello again, world!");
/// });
/// other_modal.buttons(ui, |ui| {
/// other_modal.button(ui, "Close");
/// });
/// });
/// if ui.button("open the other modal").clicked() {
/// other_modal.open();
/// }
/// ```
pub struct Modal {
close_on_outside_click: bool,
style: ModalStyle,
ctx: Context,
id: Id,
window_id: Id,
}
fn ui_with_margin<R>(ui: &mut Ui, margin: f32, add_contents: impl FnOnce(&mut Ui) -> R) {
egui::Frame::NONE
.inner_margin(margin)
.show(ui, |ui| add_contents(ui));
}
impl Modal {
/// Creates a new [`Modal`]. Can use constructor functions like [`Modal::with_style`]
/// to modify upon creation.
pub fn new(ctx: &Context, id_source: impl std::fmt::Display) -> Self {
let self_id = Id::new(id_source.to_string());
Self {
window_id: self_id.with("window"),
id: self_id,
style: ModalStyle::default(),
ctx: ctx.clone(),
close_on_outside_click: false,
}
}
fn set_open_state(&self, is_open: bool) {
let mut modal_state = ModalState::load(&self.ctx, self.id);
modal_state.is_open = is_open;
modal_state.save(&self.ctx, self.id)
}
fn set_outside_clicked(&self, was_clicked: bool) {
let mut modal_state = ModalState::load(&self.ctx, self.id);
modal_state.was_outside_clicked = was_clicked;
modal_state.save(&self.ctx, self.id)
}
/// Was the outer overlay clicked this frame?
pub fn was_outside_clicked(&self) -> bool {
let modal_state = ModalState::load(&self.ctx, self.id);
modal_state.was_outside_clicked
}
/// Is the modal currently open?
pub fn is_open(&self) -> bool {
let modal_state = ModalState::load(&self.ctx, self.id);
modal_state.is_open
}
/// Open the modal; make it visible. The modal prevents user input to other parts of the
/// application.
///
/// ⚠️ WARNING ⚠️: This function requires a write lock to the [`egui::Context`]. Using it within
/// closures within functions like [`egui::Ui::input_mut`] will result in a deadlock. [Tracking issue](https://github.com/n00kii/egui-modal/issues/15)
pub fn open(&self) {
self.set_open_state(true)
}
/// Close the modal so that it is no longer visible, allowing input to flow back into
/// the application.
///
/// ⚠️ WARNING ⚠️: This function requires a write lock to the [`egui::Context`]. Using it within
/// closures within functions like [`egui::Ui::input_mut`] will result in a deadlock. [Tracking issue](https://github.com/n00kii/egui-modal/issues/15)
pub fn close(&self) {
self.set_open_state(false)
}
/// If set to `true`, the modal will close itself if the user clicks outside on the modal window
/// (onto the overlay).
pub fn with_close_on_outside_click(mut self, do_close_on_click_ouside: bool) -> Self {
self.close_on_outside_click = do_close_on_click_ouside;
self
}
/// Change the [`ModalStyle`] of the modal upon creation.
pub fn with_style(mut self, style: &ModalStyle) -> Self {
self.style = style.clone();
self
}
/// Helper function for styling the title of the modal.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.title(ui, "my title");
/// });
/// ```
pub fn title(&self, ui: &mut Ui, text: impl Into<RichText>) {
let text: RichText = text.into();
ui.vertical_centered(|ui| {
ui.heading(text);
});
ui.separator();
}
/// Helper function for styling the icon of the modal.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.frame(ui, |ui| {
/// modal.icon(ui, Icon::Info);
/// });
/// });
/// ```
pub fn icon(&self, ui: &mut Ui, icon: Icon) {
let color = match icon {
Icon::Info => self.style.info_icon_color,
Icon::Warning => self.style.warning_icon_color,
Icon::Success => self.style.success_icon_color,
Icon::Error => self.style.error_icon_color,
Icon::Custom((_, color)) => color,
};
let text = RichText::new(icon.to_string())
.color(color)
.size(self.style.icon_size);
ui_with_margin(ui, self.style.icon_margin, |ui| {
ui.add(egui::Label::new(text));
});
}
/// Helper function for styling the container the of body and icon.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.title(ui, "my title");
/// modal.frame(ui, |ui| {
/// // inner modal contents go here
/// });
/// modal.buttons(ui, |ui| {
/// // button contents go here
/// });
/// });
/// ```
pub fn frame<R>(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) {
let last_frame_height = ModalState::load(&self.ctx, self.id)
.last_frame_height
.unwrap_or_default();
let default_height = self.style.default_height.unwrap_or_default();
let space_height = ((default_height - last_frame_height) * 0.5).max(0.);
ui.with_layout(
Layout::top_down(Align::Center).with_cross_align(Align::Center),
|ui| {
ui_with_margin(ui, self.style.frame_margin, |ui| {
if space_height > 0. {
ui.add_space(space_height);
add_contents(ui);
ui.add_space(space_height);
} else {
add_contents(ui);
}
})
},
);
}
/// Helper function that should be used when using a body and icon together.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.frame(ui, |ui| {
/// modal.body_and_icon(ui, "my modal body", Icon::Warning);
/// });
/// });
/// ```
pub fn body_and_icon(&self, ui: &mut Ui, text: impl Into<WidgetText>, icon: Icon) {
egui::Grid::new(self.id).num_columns(2).show(ui, |ui| {
self.icon(ui, icon);
self.body(ui, text);
});
}
/// Helper function for styling the body of the modal.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.frame(ui, |ui| {
/// modal.body(ui, "my modal body");
/// });
/// });
/// ```
pub fn body(&self, ui: &mut Ui, text: impl Into<WidgetText>) {
let text: WidgetText = text.into();
ui.with_layout(Layout::top_down(self.style.body_alignment), |ui| {
ui_with_margin(ui, self.style.body_margin, |ui| {
ui.label(text);
})
});
}
/// Helper function for styling the button container of the modal.
/// ```
/// let modal = Modal::new(ctx, "modal");
/// modal.show(|ui| {
/// modal.buttons(ui, |ui| {
/// modal.button(ui, "my modal button");
/// });
/// });
/// ```
pub fn buttons<R>(&self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) {
ui.separator();
ui.with_layout(Layout::right_to_left(Align::Min), add_contents);
}
/// Helper function for creating a normal button for the modal.
/// Automatically closes the modal on click.
pub fn button(&self, ui: &mut Ui, text: impl Into<WidgetText>) -> Response {
self.styled_button(ui, text, ModalButtonStyle::None)
}
/// Helper function for creating a "cautioned" button for the modal.
/// Automatically closes the modal on click.
pub fn caution_button(&self, ui: &mut Ui, text: impl Into<WidgetText>) -> Response {
self.styled_button(ui, text, ModalButtonStyle::Caution)
}
/// Helper function for creating a "suggested" button for the modal.
/// Automatically closes the modal on click.
pub fn suggested_button(&self, ui: &mut Ui, text: impl Into<WidgetText>) -> Response {
self.styled_button(ui, text, ModalButtonStyle::Suggested)
}
fn styled_button(
&self,
ui: &mut Ui,
text: impl Into<WidgetText>,
button_style: ModalButtonStyle,
) -> Response {
let button = match button_style {
ModalButtonStyle::Suggested => {
let text: WidgetText = text.into().color(self.style.suggested_button_text_color);
Button::new(text).fill(self.style.suggested_button_fill)
}
ModalButtonStyle::Caution => {
let text: WidgetText = text.into().color(self.style.caution_button_text_color);
Button::new(text).fill(self.style.caution_button_fill)
}
ModalButtonStyle::None => Button::new(text.into()),
};
let response = ui.add(button);
if response.clicked() {
self.close()
}
response
}
/// The ui contained in this function will be shown within the modal window. The modal will only actually show
/// when [`Modal::open`] is used.
pub fn show<R>(&self, add_contents: impl FnOnce(&mut Ui) -> R) {
let mut modal_state = ModalState::load(&self.ctx, self.id);
self.set_outside_clicked(false);
if modal_state.is_open {
let ctx_clone = self.ctx.clone();
let area_resp = Area::new(self.id)
.interactable(true)
.fixed_pos(Pos2::ZERO)
.show(&self.ctx, |ui: &mut Ui| {
let screen_rect = ui.ctx().input(|i| i.screen_rect);
let area_response = ui.allocate_response(screen_rect.size(), Sense::click());
// let current_focus = area_response.ctx.memory().focus().clone();
// let top_layer = area_response.ctx.memory().layer_ids().last();
// if let Some(focus) = current_focus {
// area_response.ctx.memory().surrender_focus(focus)
// }
if area_response.clicked() {
self.set_outside_clicked(true);
if self.close_on_outside_click {
self.close();
}
}
ui.painter()
.rect_filled(screen_rect, CornerRadius::ZERO, self.style.overlay_color);
});
ctx_clone.move_to_top(area_resp.response.layer_id);
// the below lines of code addresses a weird problem where if the default_height changes, egui doesnt respond unless
// it's a different window id
let mut window_id = self
.style
.default_width
.map_or(self.window_id, |w| self.window_id.with(w.to_string()));
window_id = self
.style
.default_height
.map_or(window_id, |h| window_id.with(h.to_string()));
let mut window = Window::new("")
.id(window_id)
.open(&mut modal_state.is_open)
.title_bar(false)
.anchor(Align2::CENTER_CENTER, [0., 0.])
.resizable(false);
let recalculating_height =
self.style.default_height.is_some() && modal_state.last_frame_height.is_none();
if let Some(default_height) = self.style.default_height {
window = window.default_height(default_height);
}
if let Some(default_width) = self.style.default_width {
window = window.default_width(default_width);
}
let response = window.show(&ctx_clone, add_contents);
if let Some(inner_response) = response {
ctx_clone.move_to_top(inner_response.response.layer_id);
if recalculating_height {
let mut modal_state = ModalState::load(&self.ctx, self.id);
modal_state.last_frame_height = Some(inner_response.response.rect.height());
modal_state.save(&self.ctx, self.id);
}
}
}
}
/// Open the modal as a dialog. This is a shorthand way of defining a [`Modal::show`] once,
/// for example, if a function returns an `Error`. This should be used in conjunction with
/// [`Modal::show_dialog`].
#[deprecated(since = "0.3.0", note = "use `Modal::dialog`")]
pub fn open_dialog(
&self,
title: Option<impl std::fmt::Display>,
body: Option<impl std::fmt::Display>,
icon: Option<Icon>,
) {
let modal_data = DialogData {
title: title.map(|s| s.to_string()),
body: body.map(|s| s.to_string()),
icon,
};
let mut modal_state = ModalState::load(&self.ctx, self.id);
modal_state.modal_type = ModalType::Dialog(modal_data);
modal_state.is_open = true;
modal_state.save(&self.ctx, self.id);
}
/// Create a `DialogBuilder` for this modal. Make sure to use `DialogBuilder::open`
/// to open the dialog.
pub fn dialog(&self) -> DialogBuilder {
DialogBuilder {
data: DialogData::default(),
modal_id: self.id.clone(),
ctx: self.ctx.clone(),
}
}
/// Needed in order to use [`Modal::dialog`]. Make sure this is called every frame, as
/// it renders the necessary ui when using a modal as a dialog.
pub fn show_dialog(&mut self) {
let modal_state = ModalState::load(&self.ctx, self.id);
if let ModalType::Dialog(modal_data) = modal_state.modal_type {
self.close_on_outside_click = true;
self.show(|ui| {
if let Some(title) = modal_data.title {
self.title(ui, title)
}
self.frame(ui, |ui| {
if modal_data.body.is_none() {
if let Some(icon) = modal_data.icon {
self.icon(ui, icon)
}
} else if modal_data.icon.is_none() {
if let Some(body) = modal_data.body {
self.body(ui, body)
}
} else if modal_data.icon.is_some() && modal_data.icon.is_some() {
self.body_and_icon(ui, modal_data.body.unwrap(), modal_data.icon.unwrap())
}
});
self.buttons(ui, |ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
self.button(ui, &self.style.dialog_ok_text)
})
})
});
}
}
}
impl DialogBuilder {
/// Construct this dialog with the given title.
pub fn with_title(mut self, title: impl std::fmt::Display) -> Self {
self.data.title = Some(title.to_string());
self
}
/// Construct this dialog with the given body.
pub fn with_body(mut self, body: impl std::fmt::Display) -> Self {
self.data.body = Some(body.to_string());
self
}
/// Construct this dialog with the given icon.
pub fn with_icon(mut self, icon: Icon) -> Self {
self.data.icon = Some(icon);
self
}
/// Open the dialog.
///
/// ⚠️ WARNING ⚠️: This function requires a write lock to the [`egui::Context`]. Using it within
/// closures within functions like [`egui::Ui::input_mut`] will result in a deadlock. [Tracking issue](https://github.com/n00kii/egui-modal/issues/15)
pub fn open(self) {
let mut modal_state = ModalState::load(&self.ctx, self.modal_id);
modal_state.modal_type = ModalType::Dialog(self.data);
modal_state.is_open = true;
modal_state.save(&self.ctx, self.modal_id);
}
}

View File

@ -1,14 +1,16 @@
use std::{sync::Arc, thread, time::Duration};
use std::{f32::consts::E, path::PathBuf, sync::Arc, thread, time::Duration};
use crossbeam_channel::Receiver;
use egui::{mutex::Mutex};
use log::debug;
use crate::{config, omori_locator};
use crate::{config::{self, ConfigProvider}, omori_locator};
#[derive(Clone)]
pub enum UiState {
Loading,
KeyRequired(String),
PickGame(Result<PathBuf, String>, Vec<PathBuf>),
Error(String)
}
@ -47,6 +49,19 @@ impl AppThread {
}
}
fn pick_game(&mut self, provider: &ConfigProvider) -> anyhow::Result<()> {
let steam_location = match omori_locator::get_omori_path() {
Ok(l) => Ok(l),
Err(e) => Err(String::from(format!("{:#}", e)))
};
let location_history = provider.get_game_paths()?;
self.commit(UiState::PickGame(steam_location.clone(), location_history.clone()));
Ok(())
}
pub fn create(state_holder: &UiStateHolder, context: &egui::Context, ui_event_channel: &Receiver<UiEvent>) -> AppThread {
AppThread {
ui_state: state_holder.clone(),
@ -57,6 +72,7 @@ impl AppThread {
}
fn real_main(&mut self) -> anyhow::Result<()> {
debug!("I am ALIVE! HAHAHAHA!");
self.commit(UiState::Loading);
let mut config_provider = config::ConfigProvider::new()?;
@ -71,6 +87,8 @@ impl AppThread {
config_provider.set_key(&self.decryption_key)?;
self.pick_game(&config_provider);
Ok(())
}

View File

@ -1,16 +1,21 @@
use std::{fs::{self, File}, path::PathBuf};
use std::{fs::{self, File}, path::PathBuf, time::{Instant, SystemTime}};
use anyhow::Context;
use serde::{de, Deserialize, Serialize};
use egui::ahash::HashMap;
use serde::{Deserialize, Serialize};
use serde;
#[derive(Deserialize, Serialize, Debug)]
struct Config {
key: Option<Vec<u8>>
key: Option<Vec<u8>>,
#[serde(default)]
recently_used_game_paths: HashMap<PathBuf, u64>
}
impl Default for Config {
fn default() -> Self {
Self { key: Default::default() }
Self { key: Default::default(), recently_used_game_paths: Default::default() }
}
}
@ -60,4 +65,19 @@ impl ConfigProvider {
Ok(())
}
pub fn get_game_paths(&self) -> anyhow::Result<Vec<PathBuf>> {
let mut paths: Vec<(&PathBuf, &u64)> = self.config.recently_used_game_paths.iter().collect();
paths.sort_by(|a, b| a.1.cmp(b.1));
Ok(paths.iter().map(|v| v.0.clone()).collect())
}
pub fn set_game_path_used(&mut self, which: &PathBuf) -> anyhow::Result<()> {
let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs();
self.config.recently_used_game_paths.insert(which.clone(), current_time);
self.commit()?;
Ok(())
}
}

View File

@ -9,12 +9,22 @@ use crossbeam_channel::{Receiver, Sender};
use eframe::egui;
use egui::{mutex::{Mutex, RwLock}, Align, Layout, RichText, ThemePreference};
use egui_alignments::{center_horizontal, center_vertical, top_horizontal, Aligner};
use omori_locator::{get_omori_key, get_omori_path};
use egui_modal::Modal;
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]),
@ -128,6 +138,55 @@ impl eframe::App for Application {
}
})
});
},
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("key_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();
ui.button(RichText::new("Use Steam version").size(16.0));
},
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() {
rfd::FileDialog::new().pick_folder();
invalid_path_modal.open();
}
ui.separator();
if others.len() > 0 {
} else {
ui.label("Once you use a custom location, it will be remembered here.");
}
});
});
});
}
}
}

View File

@ -1,7 +1,8 @@
use anyhow::Context;
use log::{debug, info, trace};
use regex::bytes::Regex;
use sha2::Digest;
use std::{fs::File, io::Read, path::PathBuf};
use std::{fs::{self, File}, io::Read, path::PathBuf};
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn get_steam_root() -> anyhow::Result<PathBuf> {
@ -13,6 +14,8 @@ fn get_steam_root() -> anyhow::Result<PathBuf> {
#[cfg(target_os="macos")]
base.push("Library/Application Support/Steam");
info!("Using {:?} as steam base", base);
Ok(base)
}
@ -29,14 +32,20 @@ fn get_steam_root() -> anyhow::Result<PathBuf> {
let mut base = PathBuf::from(value);
debug!("Using {:?} as steam base", base);
Ok(base)
}
const OMORI_STEAM_APPID: &str = "1150690";
pub fn get_omori_path() -> anyhow::Result<PathBuf> {
let mut base = get_steam_root()?;
base.push("steamapps");
base.push("libraryfolders.vdf");
info!("Searching {:?} for library folders with omori", base);
let mut buffer = Vec::new();
@ -50,7 +59,6 @@ pub fn get_omori_path() -> anyhow::Result<PathBuf> {
let mut omori_path: Option<PathBuf> = None;
for key in keys {
println!("{:?}", key);
let entry = data.get(key).with_context(|| "Couldn't read specific steam library libraryfolders object.")?;
let entry = entry.get(0).with_context(|| "Couldn't read specific steam library libraryfolders object.")?;
let entry = entry.get_obj().with_context(|| "libraryfolders parsing failed: Not an object")?;
@ -64,7 +72,7 @@ pub fn get_omori_path() -> anyhow::Result<PathBuf> {
let path = path.get(0).with_context(|| "libraryfolders parsing failed: No path")?;
let path = path.get_str().with_context(|| "libraryfolders parsing failed: No path")?;
if apps.iter().find(|x| (**x).eq("1150690")).is_some() {
if apps.iter().find(|x| (**x).eq(OMORI_STEAM_APPID)).is_some() {
let mut base = PathBuf::from(path);
#[cfg(not(target_os = "macos"))]
@ -91,7 +99,13 @@ pub fn get_omori_path() -> anyhow::Result<PathBuf> {
}
omori_path.with_context(|| "Failed to find omori")
let path = omori_path.with_context(|| "Failed to find omori")?;
debug!("{:?} seems to be it.", path);
if validate_omori_installation(&path) {
Ok(path)
} else {
Err(anyhow::anyhow!("The steam installation of OMORI is not valid"))
}
}
const GAME_KEY_HASH: &[u8; 32] = include_bytes!("keyhash");
@ -127,4 +141,56 @@ pub fn get_omori_key() -> anyhow::Result<Vec<u8>> {
Err(anyhow::anyhow!("Couldn't find any valid decryption key for OMORI in your Steam installation."))
}
const EXPECTED_PATHS: [&str; 32] = [
"index.html",
"editor.json",
"audio/bgm/AMB_forest.rpgmvo",
"audio/me/cutscene_omori_saves_player.rpgmvo",
"data/Actors.KEL",
"data/System.KEL",
"data/Atlas.PLUTO",
"data/Troops.KEL",
"fonts/OMORI_GAME.ttf",
"icon/icon.png",
"img/animations/e_calm_down.rpgmvp",
"img/atlases/battleATLAS.rpgmvp",
"img/battlebacks1/battleback_dw_humphrey.rpgmvp", // best area
"img/characters/!FA_OBJECTS_2.rpgmvp",
"img/characters/FA_AUBREY.rpgmvp",
"img/enemies/!battle_snow_bunny.rpgmvp",
"img/faces/01_OMORI_BATTLE.rpgmvp",
"img/overlays/fog.rpgmvp",
"img/parallaxes/!BS_faces.rpgmvp",
"img/pictures/basil_something.rpgmvp",
"img/slotmachine/bet_line_2.rpgmvp",
"img/sv_actors/!battle_aubrey.rpgmvp",
"img/system/bar_gradients.rpgmvp",
"img/system/Window.png",
"img/tilesets/BS_Beach.rpgmvp",
"img/transition/starburst_2.rpgmvp",
"js/rpg_core.js",
"js/plugins/Omori BASE.OMORI",
"languages/en/10_cutscenes_fakeknifefight.HERO",
"maps/FA_SUNSET_INTERIOR2.AUBREY",
"movies/secret_ending.webm",
"movies/Bad End Credits Draft 4.webm"
];
pub fn validate_omori_installation(base: &PathBuf) -> bool {
info!("Validating {:?}", base);
for path in EXPECTED_PATHS {
trace!("Checking {path}");
let mut real = base.clone();
real.push(path);
if let Ok(r) = fs::exists(&real) {
if !r { return false; }
} else { return false; }
}
info!("Validation passed");
true
}