From 7bf458c3cc775c2a3d3a9b639a8ee6f5c02ccb3d Mon Sep 17 00:00:00 2001 From: Mitchell Date: Fri, 21 Aug 2026 01:20:22 -0500 Subject: [PATCH] move menu work, start on preliminary save system --- Cargo.lock | 106 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 ++- src/main.rs | 5 +- src/main_menu.rs | 129 ++++++++++++++++++++++++++++++++++++++++------- src/save.rs | 125 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 350 insertions(+), 21 deletions(-) create mode 100644 src/save.rs diff --git a/Cargo.lock b/Cargo.lock index 83cb843..28e57e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,18 @@ dependencies = [ "libc", ] +[[package]] +name = "app_dirs2" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e7b35733e3a8c1ccb90385088dd5b6eaa61325cb4d1ad56e683b5224ff352e" +dependencies = [ + "jni 0.21.1", + "ndk-context", + "winapi", + "xdg", +] + [[package]] name = "approx" version = "0.5.1" @@ -2269,6 +2281,37 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -3141,6 +3184,59 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -5090,8 +5186,12 @@ dependencies = [ name = "tt_net_test_vibeless" version = "0.1.0" dependencies = [ + "app_dirs2", "bevy", "derive_more", + "jiff", + "ron", + "serde", ] [[package]] @@ -6078,6 +6178,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + [[package]] name = "xkbcommon-dl" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 8a29f5c..7e45eb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,12 @@ version = "0.1.0" edition = "2024" [dependencies] -bevy = "0.19" +app_dirs2 = "2.5.5" +bevy = { version = "0.19", features = ["system_clipboard"] } derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] } +jiff = { version = "0.2.35", features = ["serde"] } +ron = "0.12.2" +serde = { version = "1.0.229", features = ["derive"] } [profile.dev] opt-level = 1 diff --git a/src/main.rs b/src/main.rs index 0853936..00bdaca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,11 @@ use bevy::{camera::ScalingMode, color::palettes::css::GREEN, prelude::*}; use main_menu::MainMenuPlugin; use networking::NetworkingPlugin; +use crate::save::SavePlugin; + mod main_menu; mod networking; +mod save; /// We target this 16:9 resolution to make things easier. pub const WINDOW_TARGET_WIDTH_U32: u32 = 3840; @@ -22,7 +25,7 @@ pub enum AppState { fn main() { App::new() - .add_plugins((DefaultPlugins, MainMenuPlugin, NetworkingPlugin)) + .add_plugins((DefaultPlugins, MainMenuPlugin, NetworkingPlugin, SavePlugin)) .add_systems(Startup, setup) .add_systems(Update, debug_border) .insert_resource(ClearColor(Color::srgb(0.07, 0.33, 0.45))) diff --git a/src/main_menu.rs b/src/main_menu.rs index e291b8f..917055c 100644 --- a/src/main_menu.rs +++ b/src/main_menu.rs @@ -1,4 +1,8 @@ +use crate::save::get_saves; use crate::{AppState, WINDOW_TARGET_HEIGHT, WINDOW_TARGET_WIDTH}; +use bevy::color::palettes::tailwind::SLATE_900; +use bevy::input_focus::tab_navigation::TabIndex; +use bevy::text::{EditableText, TextCursorStyle}; use bevy::ui_widgets::Button; use bevy::{prelude::*, ui_widgets::Activate}; use derive_more::{Deref, DerefMut}; @@ -9,6 +13,7 @@ impl Plugin for MainMenuPlugin { fn build(&self, app: &mut App) { app.add_systems(OnEnter(AppState::Menu), setup_menu) .add_systems(OnExit(AppState::Menu), cleanup_menu) + // .add_systems(Startup, enable_ui_debug) .add_systems(Update, (menu_redraw).run_if(in_state(AppState::Menu))) .add_observer(button_activate_observer) .add_systems(Update, (scale_ui, style_ui_elements)); @@ -78,6 +83,7 @@ pub enum MenuLayer { Root, Play, LoadSave, + NewGame, Browse, DirectConnect, Settings, @@ -110,16 +116,31 @@ impl MenuLayer { menu_button("Direct Connect", Some(MenuLayer::DirectConnect)), ] }), - MenuLayer::LoadSave => Box::new(bsn! { - menu_layer(layer) + MenuLayer::LoadSave => { + // TODO: this needs to be on an IO thread + let saves = get_saves().unwrap_or_default(); + let entries: Vec<_> = saves + .into_iter() + .map(|m| menu_button(m.save_name, None)) + .collect(); + + Box::new(bsn! { + menu_layer(layer) + ChildOf(menu_root) + Children [ + {entries} + ( + menu_button("New Game", Some(MenuLayer::NewGame)) + InteractionColors::GOOD + ), + ] + }) + } + MenuLayer::NewGame => Box::new(bsn! { + full_menu_layer(layer) ChildOf(menu_root) Children [ - menu_button("Save 1", None), - menu_button("Save 2", None), - ( - menu_button("New Game", None) - InteractionColors::GOOD - ), + {new_game_menu()} ] }), MenuLayer::Browse => Box::new(bsn! { @@ -170,6 +191,13 @@ struct LastMenu(Vec); #[derive(Resource, Debug, Clone, PartialEq, Eq, Hash, Deref, DerefMut)] struct CurrentMenu(Vec); +// fn enable_ui_debug(mut debug_options: ResMut) { +// debug_options.enabled = true; +// debug_options.outline_border_box = true; +// debug_options.outline_padding_box = true; +// debug_options.outline_content_box = true; +// } + fn setup_menu(mut commands: Commands) { commands.insert_resource(CurrentMenu(vec![MenuLayer::Root])); commands.insert_resource(LastMenu(vec![])); @@ -233,6 +261,70 @@ fn menu_redraw( } } +fn new_game_menu() -> impl SceneList { + bsn_list![ + ( + Text("New Game") + Node { + align_self: AlignSelf::Center, + } + TextFont { + // font: FontSourceTemplate::Handle("fonts/FiraSans-Bold.ttf"), + font_size: px(100.0), + } + TextColor(Color::srgb(0.9, 0.9, 0.9)) + TextShadow + ), + ( + Text("Save Name") + Node { + margin: UiRect::axes(px(12), px(0)), + } + TextFont { + // font: FontSourceTemplate::Handle("fonts/FiraSans-Bold.ttf"), + font_size: px(66.0), + } + TextColor(Color::srgb(0.9, 0.9, 0.9)) + TextShadow + ), + ( + #NewGameTextField + Node { + border: Val::all(px(6)), + padding: Val::all(px(12)), + border_radius: BorderRadius::all(px(12)), + } + // BorderColor::from(SLATE_800) + BorderColor::from(SLATE_900) + EditableText { + cursor_width: 0.05, + } + TextLayout::no_wrap() + TextFont { + font_size: FontSize::Px(66.), + } + TextCursorStyle { + color: Color::BLACK + } + TabIndex(0) + BackgroundColor(Color::srgb(0.48, 0.84, 0.87)) + ), + ( + Node { + margin: UiRect::top(px(48)), + } + Children [ + menu_button("Create", None) + Node { + margin: UiRect::left(Val::Auto), + } + InteractionColors::GOOD + on(|event: On, q_text:| { info!("create!") }) + ] + ), + ] +} + fn menu_layer(layer: usize) -> impl Scene { let name = format!("MenuLayer{}", layer); bsn! { @@ -242,8 +334,6 @@ fn menu_layer(layer: usize) -> impl Scene { width: percent(100.0 / N_LAYERS as f32), height: percent(100), flex_direction: FlexDirection::Column, - // align_items: AlignItems::Start, - align_items: AlignItems::Baseline, justify_content: JustifyContent::FlexEnd, row_gap: px(100), } @@ -251,21 +341,22 @@ fn menu_layer(layer: usize) -> impl Scene { } fn full_menu_layer(layer: usize) -> impl Scene { + let name = format!("FullMenuLayer{}", layer); bsn! { - menu_layer(layer) + Name(name) + Layer(layer) Node { - width: Val::Auto, + flex_direction: FlexDirection::Column, + align_self: AlignSelf::FlexEnd, flex_grow: 1., + border_radius: BorderRadius::all(px(24)), + padding: Val::all(px(24)), } - BackgroundColor(Color::srgba(0.8, 0.1, 0.1, 0.7)) + BackgroundColor(Color::srgb(0.4, 0.4, 0.4)) } } -fn menu_button(label: &str, next_layer: Option) -> impl Scene { - let next_layer_component = next_layer - .map(|n| Box::new(bsn! { NextLayer(n) }) as Box) - .unwrap_or(Box::new(bsn! {})); - +fn menu_button(label: impl Into, next_layer: Option) -> impl Scene { bsn! { Button Node { @@ -277,7 +368,7 @@ fn menu_button(label: &str, next_layer: Option) -> impl Scene { align_items: AlignItems::Center, margin: UiRect::all(px(4)), } - next_layer_component + {next_layer.map(|l| template_value(NextLayer(l)))} BorderColor::from(Color::BLACK) InteractionColors::DEFAULT Interaction diff --git a/src/save.rs b/src/save.rs new file mode 100644 index 0000000..9a9841e --- /dev/null +++ b/src/save.rs @@ -0,0 +1,125 @@ +use app_dirs2::{AppInfo, app_dir}; +use bevy::prelude::*; +use jiff::Zoned; +use serde::{Deserialize, Serialize}; + +pub const APP_INFO: AppInfo = AppInfo { + name: "OfficeWarper", + author: "MarinoDev", +}; + +#[derive(Resource, Debug, Clone, PartialEq, Eq, Hash)] +pub struct ActiveGameSave { + name: String, +} + +#[derive(Message, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct SaveMessage; + +pub struct SavePlugin; + +/// The metadata stored with each game save +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct GameSaveMetadata { + pub save_name: String, + pub version: String, + pub timestamp: Zoned, +} + +impl Plugin for SavePlugin { + fn build(&self, app: &mut App) { + app.add_message::() + .add_systems(PostUpdate, handle_saves); + } +} + +fn handle_saves( + mut save_messages: MessageReader, + active_game_save: Option>, +) { + for _save in save_messages.read() { + let Some(ref active_game_save) = active_game_save else { + warn!("Save requested, but no active game save exists!"); + return; + }; + + info!("Saving!"); + let save_subpath = format!("saves/{}", active_game_save.name); + let save_path = match app_dir(app_dirs2::AppDataType::UserData, &APP_INFO, &*save_subpath) { + Ok(p) => p, + Err(e) => { + error!( + "Failed to get the save path for \"{}\": {e}", + active_game_save.name + ); + return; + } + }; + + if let Err(e) = std::fs::write(save_path.join("version.txt"), "v0.0.1") { + error!("Error when writing the game version: {e}"); + return; + } + + info!("Game saved!"); + } +} + +pub fn get_saves() -> Result, std::io::Error> { + let saves_path = match app_dir(app_dirs2::AppDataType::UserData, &APP_INFO, "saves") { + Ok(p) => p, + Err(app_dirs2::AppDirsError::Io(e)) => { + error!("Failed to get path to the saves: {e}",); + return Err(e); + } + Err(app_dirs2::AppDirsError::NotSupported) => { + error!("The app data directory is not known on this platform!"); + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "the app data directory is not known on this platform", + )); + } + Err(app_dirs2::AppDirsError::InvalidAppInfo) => { + // :/ + panic!("APP_INFO invalid!"); + } + }; + + let dir = match std::fs::read_dir(saves_path) { + Ok(dir) => dir, + Err(e) => { + error!("Failed to read the saves path: {e}"); + return Err(e); + } + }; + // TODO: do a meta.ron file instead of name.txt + let save_folders = dir + .filter_map(|entry| { + entry + .ok() + .filter(|e| e.file_type().is_ok_and(|t| t.is_dir())) + }) + .map(|dir| dir.path()); + + let mut saves = vec![]; + for save_folder in save_folders { + let meta_raw = match std::fs::read_to_string(save_folder.join("meta.ron")) { + Ok(s) => s, + Err(e) => { + error!("Failed to read meta for save \"{save_folder:?}\""); + return Err(e); + } + }; + + let meta: GameSaveMetadata = match ron::from_str(&meta_raw) { + Err(e) => { + error!("Error when deserializing metadata for save \"{save_folder:?}\": {e}"); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)); + } + Ok(m) => m, + }; + saves.push(meta); + } + + Ok(saves) +}