some more UI

This commit is contained in:
Rph :3 2025-04-18 00:15:28 +02:00
parent 165a9fc139
commit 4d6e23980e
No known key found for this signature in database
8 changed files with 413 additions and 25 deletions

53
Cargo.lock generated
View File

@ -737,6 +737,12 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.3"
@ -1433,6 +1439,16 @@ dependencies = [
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "gif"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "gl_generator"
version = "0.14.0"
@ -1774,9 +1790,24 @@ checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"num-traits",
"png",
"tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]]
@ -2727,6 +2758,12 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.30.0"
@ -3416,6 +3453,7 @@ dependencies = [
"egui_extras",
"env_logger",
"hex",
"image",
"keyvalues-parser",
"log",
"regex",
@ -4626,6 +4664,21 @@ dependencies = [
"syn",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-jpeg"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
dependencies = [
"zune-core",
]
[[package]]
name = "zvariant"
version = "4.2.0"

View File

@ -24,6 +24,18 @@ egui-modal = { path = "./egui-modal" }
chrono-humanize = "0.2.3"
chrono = "0.4.40"
egui_extras = "0.31.1"
image = { version = "0.25.6", default-features = false, features = ["gif", "jpeg", "png", "webp", "bmp"] }
[workspace]
members = ["egui-modal"]
[profile.special]
inherits = "release"
opt-level = 3
strip = true
lto = "fat"
debug-assertions = false
overflow-checks = false
panic = "abort"
incremental = false
debug = false

View File

@ -15,7 +15,7 @@
inherit overlays;
};
native-deps = with pkgs; [
pkg-config pipewire lld clang libclang alsa-lib openssl zlib libxkbcommon libGL wayland mangohud
pkg-config pipewire lld clang libclang alsa-lib openssl zlib libxkbcommon libGL wayland mangohud upx
];
in
pkgs.mkShell {

View File

@ -1,16 +1,18 @@
use std::{f32::consts::E, path::PathBuf, sync::Arc, thread, time::Duration};
use std::{path::PathBuf, sync::Arc};
use crossbeam_channel::Receiver;
use egui::{mutex::Mutex};
use egui::mutex::Mutex;
use log::{debug, info};
use crate::{config::{self, ConfigProvider}, omori_locator};
use crate::{config::{self, ConfigProvider}, mod_builder::ModConfiguration, omori_locator};
#[derive(Clone)]
pub enum UiState {
Loading,
KeyRequired(String),
PickGame(Result<PathBuf, String>, Vec<(PathBuf, u64)>),
PickPlaytest(Vec<(PathBuf, u64)>),
Configure(ModConfiguration),
Error(String)
}
@ -22,7 +24,8 @@ pub struct UiStateHolder {
pub enum UiEvent {
SetKey(Vec<u8>),
UsePath(PathBuf),
UseSteamPath
UseSteamPath,
UseConfiguration(ModConfiguration)
}
pub struct AppThread {
@ -36,9 +39,11 @@ impl AppThread {
fn commit(&self, s: UiState) {
let mut state = self.ui_state.state.lock();
*state = s;
drop(state);
self.context.request_repaint();
}
fn get_key(&mut self, reason: String) -> anyhow::Result<Vec<u8>> {
fn get_key(&self, reason: String) -> anyhow::Result<Vec<u8>> {
self.commit(UiState::KeyRequired(reason));
loop {
match self.ui_event_channel.recv()? {
@ -51,7 +56,7 @@ impl AppThread {
}
}
fn pick_game(&mut self, provider: &mut ConfigProvider) -> anyhow::Result<PathBuf> {
fn pick_game(&self, provider: &mut ConfigProvider) -> anyhow::Result<PathBuf> {
let steam_location = match omori_locator::get_omori_path() {
Ok(l) => Ok(l),
Err(e) => Err(String::from(format!("{:#}", e)))
@ -77,6 +82,38 @@ impl AppThread {
}
}
fn pick_playtest(&self, provider: &mut ConfigProvider) -> anyhow::Result<PathBuf> {
let playtest_history = provider.get_playtest_paths()?;
self.commit(UiState::PickPlaytest(playtest_history.clone()));
loop {
match self.ui_event_channel.recv()? {
UiEvent::UsePath(path) => {
provider.set_playtest_path_used(&path)?;
self.commit(UiState::Loading);
return Ok(path);
},
_ => {}
}
}
}
fn configure_bundling(&self, provider: &mut ConfigProvider, path: &PathBuf) -> anyhow::Result<ModConfiguration> {
let configuration = provider.get_configuration_for_playtest(path);
self.commit(UiState::Configure(configuration));
loop {
match self.ui_event_channel.recv()? {
UiEvent::UseConfiguration(config) => {
provider.set_configuration_for_playtest(&path, &config)?;
self.commit(UiState::Loading);
return Ok(config);
},
_ => {}
}
}
}
pub fn create(state_holder: &UiStateHolder, context: &egui::Context, ui_event_channel: &Receiver<UiEvent>) -> AppThread {
AppThread {
ui_state: state_holder.clone(),
@ -105,6 +142,12 @@ impl AppThread {
let game_path = self.pick_game(&mut config_provider)?;
info!("Will use {:?}", game_path);
let playtest_path = self.pick_playtest(&mut config_provider)?;
info!("Playtest {:?}", playtest_path);
let config = self.configure_bundling(&mut config_provider, &playtest_path)?;
info!("Config {:?}", config);
Ok(())
}

View File

@ -1,21 +1,34 @@
use std::{fs::{self, File}, path::PathBuf, time::{Instant, SystemTime}};
use std::{fs::{self, File}, path::PathBuf, time::SystemTime};
use anyhow::Context;
use egui::ahash::HashMap;
use serde::{Deserialize, Serialize};
use serde;
use crate::{mod_builder::ModConfiguration, omori_locator};
#[derive(Deserialize, Serialize, Debug)]
struct Config {
key: Option<Vec<u8>>,
#[serde(default)]
recently_used_game_paths: HashMap<PathBuf, u64>
recently_used_game_paths: HashMap<PathBuf, u64>,
#[serde(default)]
recently_used_playtest_paths: HashMap<PathBuf, u64>,
#[serde(default)]
configuration_history: HashMap<PathBuf, ModConfiguration>
}
impl Default for Config {
fn default() -> Self {
Self { key: Default::default(), recently_used_game_paths: Default::default() }
Self {
key: Default::default(),
recently_used_game_paths: Default::default(),
recently_used_playtest_paths: Default::default(),
configuration_history: Default::default()
}
}
}
@ -70,12 +83,61 @@ impl ConfigProvider {
let mut paths: Vec<(&PathBuf, &u64)> = self.config.recently_used_game_paths.iter().collect();
paths.sort_by(|a, b| a.1.cmp(b.1).reverse());
Ok(paths.iter().map(|v| (v.0.clone(), *v.1)).collect())
Ok(paths
.iter()
.filter(|p| omori_locator::validate_omori_installation(p.0))
.map(|v| (v.0.clone(), *v.1))
.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.config.recently_used_game_paths = self.config.recently_used_game_paths
.clone()
.into_iter()
.filter(|p| omori_locator::validate_omori_installation(&p.0))
.collect();
self.commit()?;
Ok(())
}
pub fn get_playtest_paths(&self) -> anyhow::Result<Vec<(PathBuf, u64)>> {
let mut paths: Vec<(&PathBuf, &u64)> = self.config.recently_used_playtest_paths.iter().collect();
paths.sort_by(|a, b| a.1.cmp(b.1).reverse());
Ok(paths
.iter()
.filter(|p| omori_locator::validate_playtest(p.0))
.map(|v| (v.0.clone(), *v.1))
.collect()
)
}
pub fn set_playtest_path_used(&mut self, which: &PathBuf) -> anyhow::Result<()> {
let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs();
self.config.recently_used_playtest_paths.insert(which.clone(), current_time);
self.config.recently_used_playtest_paths = self.config.recently_used_playtest_paths
.clone()
.into_iter()
.filter(|p| omori_locator::validate_playtest(&p.0))
.collect();
self.commit()?;
Ok(())
}
pub fn get_configuration_for_playtest(&self, which: &PathBuf) -> ModConfiguration {
match self.config.configuration_history.get(which) {
Some(v) => v.clone(),
None => Default::default()
}
}
pub fn set_configuration_for_playtest(&mut self, which: &PathBuf, config: &ModConfiguration) -> anyhow::Result<()> {
self.config.configuration_history.insert(which.clone(), config.clone());
self.commit()?;
Ok(())

View File

@ -1,16 +1,18 @@
mod omori_locator;
mod app_logic;
mod config;
mod mod_builder;
use std::{sync::Arc, thread, time::{Duration, SystemTime}};
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, RwLock}, Align, Layout, RichText, ThemePreference};
use egui_alignments::{center_horizontal, center_vertical, top_horizontal, Aligner};
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<()> {
@ -30,6 +32,7 @@ fn main() -> anyhow::Result<()> {
viewport: egui::ViewportBuilder::default()
.with_min_inner_size([640.0, 480.0]),
..Default::default()
};
let app_state = UiStateHolder {
@ -60,7 +63,9 @@ fn main() -> anyhow::Result<()> {
struct Application {
state: UiStateHolder,
sender: Sender<UiEvent>,
key_input: String
key_input: String,
configuration: ModConfiguration,
did_fill_configuration: bool
}
impl Application {
@ -68,7 +73,9 @@ impl Application {
Application {
sender,
state,
key_input: "".to_string()
key_input: "".to_string(),
configuration: Default::default(),
did_fill_configuration: false
}
}
}
@ -76,10 +83,15 @@ impl Application {
const GAME_KEY_HASH: &[u8; 32] = include_bytes!("keyhash");
impl eframe::App for Application {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
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| {
@ -154,7 +166,7 @@ impl eframe::App for Application {
invalid_path_modal.button(ui, "OK");
});
});
egui::TopBottomPanel::top("key_prompt_title_bar").show(ctx, |ui| {
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));
});
@ -200,7 +212,8 @@ impl eframe::App for Application {
TableBuilder::new(ui)
.striped(true)
.column(Column::remainder())
.column(Column::auto())
.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()];
@ -211,13 +224,26 @@ impl eframe::App for Application {
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 {
@ -226,7 +252,144 @@ impl eframe::App for Application {
});
});
});
},
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();
});
}
}
}
}

19
src/mod_builder.rs Normal file
View File

@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ModConfiguration {
pub mod_id: String,
pub mod_name: String,
pub mod_description: String,
pub mod_version: String,
pub include_audio: bool,
pub include_data: bool,
pub include_fonts: bool,
pub include_icon: bool,
pub include_img: bool,
pub include_plugins: bool,
pub include_languages: bool,
pub include_maps: bool,
pub include_movies: bool
}

View File

@ -144,7 +144,7 @@ pub fn get_omori_key() -> anyhow::Result<Vec<u8>> {
}
const EXPECTED_PATHS: [&str; 32] = [
const EXPECTED_PATHS_BASE: [&str; 32] = [
"index.html",
"editor.json",
"audio/bgm/AMB_forest.rpgmvo",
@ -181,7 +181,43 @@ const EXPECTED_PATHS: [&str; 32] = [
pub fn validate_omori_installation(base: &PathBuf) -> bool {
info!("Validating {:?}", base);
for path in EXPECTED_PATHS {
for path in EXPECTED_PATHS_BASE {
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
}
const EXPECTED_PATHS_PLAYTEST: [&str; 17] = [
"index.html",
"Game.rpgproject",
"img",
"img/pictures",
"audio",
"audio/bgm",
"js",
"languages",
"js/plugins",
"js/plugins/Omori BASE.js",
"package.json",
"data/Actors.json",
"data/System.json",
"data/Atlas.yaml",
"data/Troops.json",
"img/system/Window.png",
"img/atlases/battleATLAS.png",
];
pub fn validate_playtest(base: &PathBuf) -> bool {
info!("Validating {:?}", base);
for path in EXPECTED_PATHS_PLAYTEST {
trace!("Checking {path}");
let mut real = base.clone();
real.push(path);