game picker
This commit is contained in:
parent
7796045b13
commit
89f08ff580
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -984,6 +984,14 @@ dependencies = [
|
|||||||
"profiling",
|
"profiling",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "egui-modal"
|
||||||
|
version = "0.6.0"
|
||||||
|
dependencies = [
|
||||||
|
"eframe",
|
||||||
|
"egui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "egui-wgpu"
|
name = "egui-wgpu"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
@ -3245,10 +3253,12 @@ dependencies = [
|
|||||||
"dirs",
|
"dirs",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
|
"egui-modal",
|
||||||
"egui_alignments",
|
"egui_alignments",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"hex",
|
"hex",
|
||||||
"keyvalues-parser",
|
"keyvalues-parser",
|
||||||
|
"log",
|
||||||
"regex",
|
"regex",
|
||||||
"registry",
|
"registry",
|
||||||
"rfd",
|
"rfd",
|
||||||
|
@ -19,4 +19,8 @@ regex = "1.11.1"
|
|||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
egui_alignments = "0.3.4"
|
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
12
egui-modal/.gitignore
vendored
Normal 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
17
egui-modal/Cargo.toml
Normal 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
21
egui-modal/LICENSE
Normal 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
5
egui-modal/src/lib.rs
Normal 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
629
egui-modal/src/modal.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 crossbeam_channel::Receiver;
|
||||||
use egui::{mutex::Mutex};
|
use egui::{mutex::Mutex};
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
use crate::{config, omori_locator};
|
use crate::{config::{self, ConfigProvider}, omori_locator};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum UiState {
|
pub enum UiState {
|
||||||
Loading,
|
Loading,
|
||||||
KeyRequired(String),
|
KeyRequired(String),
|
||||||
|
PickGame(Result<PathBuf, String>, Vec<PathBuf>),
|
||||||
Error(String)
|
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 {
|
pub fn create(state_holder: &UiStateHolder, context: &egui::Context, ui_event_channel: &Receiver<UiEvent>) -> AppThread {
|
||||||
AppThread {
|
AppThread {
|
||||||
ui_state: state_holder.clone(),
|
ui_state: state_holder.clone(),
|
||||||
@ -57,6 +72,7 @@ impl AppThread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn real_main(&mut self) -> anyhow::Result<()> {
|
fn real_main(&mut self) -> anyhow::Result<()> {
|
||||||
|
debug!("I am ALIVE! HAHAHAHA!");
|
||||||
self.commit(UiState::Loading);
|
self.commit(UiState::Loading);
|
||||||
|
|
||||||
let mut config_provider = config::ConfigProvider::new()?;
|
let mut config_provider = config::ConfigProvider::new()?;
|
||||||
@ -71,6 +87,8 @@ impl AppThread {
|
|||||||
|
|
||||||
config_provider.set_key(&self.decryption_key)?;
|
config_provider.set_key(&self.decryption_key)?;
|
||||||
|
|
||||||
|
self.pick_game(&config_provider);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 anyhow::Context;
|
||||||
use serde::{de, Deserialize, Serialize};
|
use egui::ahash::HashMap;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
struct Config {
|
struct Config {
|
||||||
key: Option<Vec<u8>>
|
key: Option<Vec<u8>>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
recently_used_game_paths: HashMap<PathBuf, u64>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { key: Default::default() }
|
Self { key: Default::default(), recently_used_game_paths: Default::default() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,4 +65,19 @@ impl ConfigProvider {
|
|||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
61
src/main.rs
61
src/main.rs
@ -9,12 +9,22 @@ use crossbeam_channel::{Receiver, Sender};
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui::{mutex::{Mutex, RwLock}, Align, Layout, RichText, ThemePreference};
|
use egui::{mutex::{Mutex, RwLock}, Align, Layout, RichText, ThemePreference};
|
||||||
use egui_alignments::{center_horizontal, center_vertical, top_horizontal, Aligner};
|
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;
|
use sha2::Digest;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
env_logger::init();
|
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 {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_min_inner_size([640.0, 480.0]),
|
.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.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use log::{debug, info, trace};
|
||||||
use regex::bytes::Regex;
|
use regex::bytes::Regex;
|
||||||
use sha2::Digest;
|
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"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
fn get_steam_root() -> anyhow::Result<PathBuf> {
|
fn get_steam_root() -> anyhow::Result<PathBuf> {
|
||||||
@ -13,6 +14,8 @@ fn get_steam_root() -> anyhow::Result<PathBuf> {
|
|||||||
#[cfg(target_os="macos")]
|
#[cfg(target_os="macos")]
|
||||||
base.push("Library/Application Support/Steam");
|
base.push("Library/Application Support/Steam");
|
||||||
|
|
||||||
|
info!("Using {:?} as steam base", base);
|
||||||
|
|
||||||
Ok(base)
|
Ok(base)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,14 +32,20 @@ fn get_steam_root() -> anyhow::Result<PathBuf> {
|
|||||||
|
|
||||||
let mut base = PathBuf::from(value);
|
let mut base = PathBuf::from(value);
|
||||||
|
|
||||||
|
debug!("Using {:?} as steam base", base);
|
||||||
|
|
||||||
Ok(base)
|
Ok(base)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OMORI_STEAM_APPID: &str = "1150690";
|
||||||
|
|
||||||
pub fn get_omori_path() -> anyhow::Result<PathBuf> {
|
pub fn get_omori_path() -> anyhow::Result<PathBuf> {
|
||||||
let mut base = get_steam_root()?;
|
let mut base = get_steam_root()?;
|
||||||
|
|
||||||
base.push("steamapps");
|
base.push("steamapps");
|
||||||
base.push("libraryfolders.vdf");
|
base.push("libraryfolders.vdf");
|
||||||
|
|
||||||
|
info!("Searching {:?} for library folders with omori", base);
|
||||||
|
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
|
||||||
@ -50,7 +59,6 @@ pub fn get_omori_path() -> anyhow::Result<PathBuf> {
|
|||||||
|
|
||||||
let mut omori_path: Option<PathBuf> = None;
|
let mut omori_path: Option<PathBuf> = None;
|
||||||
for key in keys {
|
for key in keys {
|
||||||
println!("{:?}", key);
|
|
||||||
let entry = data.get(key).with_context(|| "Couldn't read specific steam library libraryfolders object.")?;
|
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(0).with_context(|| "Couldn't read specific steam library libraryfolders object.")?;
|
||||||
let entry = entry.get_obj().with_context(|| "libraryfolders parsing failed: Not an 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(0).with_context(|| "libraryfolders parsing failed: No path")?;
|
||||||
let path = path.get_str().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);
|
let mut base = PathBuf::from(path);
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[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");
|
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."))
|
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
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user