diff --git a/kubi/src/init.rs b/kubi/src/init.rs index e6fa838..56fcbf0 100644 --- a/kubi/src/init.rs +++ b/kubi/src/init.rs @@ -12,18 +12,25 @@ pub fn initialize_from_args( // If an address is provided, we're in multiplayer mode (the first argument is the address) // Otherwise, we're in singleplayer mode and working with local stuff let args: Vec = env::args().collect(); - if args.len() > 1 { - // Parse the address and switch the state to connecting - let address = args[1].parse::().expect("invalid address"); - all_storages.add_unique(GameType::Muliplayer); - all_storages.add_unique(ServerAddress(address)); - all_storages.borrow::>().unwrap().0 = Some(GameState::Connecting); - } else { + if cfg!(target_os = "android") || (args.get(1) == Some(&"android".into())) { + // TODO REMOVE: temporarily bypass menu on Android as hUI (0.1.0-alpha.5) doesnt play well with touchscreens (yet? :3) + // TODO REMOVE: disable save files on Android as they're stored in relative path rn + all_storages.add_unique(GameType::Singleplayer); + all_storages.borrow::>().unwrap().0 = Some(GameState::LoadingWorld); + } else if args.get(1) == Some(&"play".into()) { // Open the local save file let save_file = open_local_save_file(Path::new("./world.kubi")).expect("failed to open save file"); all_storages.add_unique(IOThreadManager::new(save_file)); // Switch the state and kick off the world loading all_storages.add_unique(GameType::Singleplayer); all_storages.borrow::>().unwrap().0 = Some(GameState::LoadingWorld); + } else if args.len() > 1 { + // Parse the address and switch the state to connecting + let address = args[1].parse::().expect("invalid address"); + all_storages.add_unique(GameType::Muliplayer); + all_storages.add_unique(ServerAddress(address)); + all_storages.borrow::>().unwrap().0 = Some(GameState::Connecting); + } else { + all_storages.borrow::>().unwrap().0 = Some(GameState::MainMenu); } } diff --git a/kubi/src/lib.rs b/kubi/src/lib.rs index b94e8ef..d2e1712 100644 --- a/kubi/src/lib.rs +++ b/kubi/src/lib.rs @@ -16,6 +16,7 @@ use shipyard::{ WorkloadModificator, SystemModificator }; +use ui::{main_menu::update_main_menu, settings_ui::f1_held_settings_condition}; use winit::{ event_loop::{EventLoop, ControlFlow}, event::{Event, WindowEvent} @@ -35,6 +36,7 @@ pub(crate) use ui::{ crosshair_ui, settings_ui, shutdown_screen, + main_menu, }; pub(crate) mod rendering; pub(crate) mod world; @@ -77,10 +79,11 @@ use block_placement::update_block_placement; use delta_time::{DeltaTime, init_delta_time}; use cursor_lock::{debug_toggle_lock, insert_lock_state, lock_cursor_now, update_cursor_lock_state}; use control_flow::{exit_on_esc, insert_control_flow_unique, RequestExit}; -use state::{init_state, is_connecting, is_ingame, is_ingame_or_loading, is_loading, is_shutting_down, update_state}; +use state::{init_state, is_connecting, is_ingame, is_ingame_or_loading, is_ingame_or_loading_or_connecting_or_shutting_down, is_loading, is_main_menu, is_shutting_down, update_state}; use networking::{update_networking, update_networking_late, is_multiplayer, disconnect_on_exit, is_singleplayer}; use init::initialize_from_args; use hui_integration::{kubi_ui_begin, /*kubi_ui_draw,*/ kubi_ui_end, kubi_ui_init}; +use main_menu::render_main_menu_ui; use loading_screen::update_loading_screen; use shutdown_screen::update_shutdown_screen; use connecting_screen::update_connecting_screen; @@ -109,7 +112,7 @@ fn startup() -> Workload { insert_lock_state, init_state, initialize_from_args, - lock_cursor_now, + // lock_cursor_now, init_input, insert_control_flow_unique, init_delta_time, @@ -126,13 +129,18 @@ fn update() -> Workload { update_cursor_lock_state, process_inputs, kubi_ui_begin, + ( + update_main_menu + ).into_sequential_workload().run_if(is_main_menu), ( init_game_world.run_if_missing_unique::(), ( spawn_player.run_if_storage_empty::(), ).into_sequential_workload().run_if(is_singleplayer), ).into_sequential_workload().run_if(is_ingame_or_loading), - update_networking().run_if(is_multiplayer), + ( + update_networking + ).into_sequential_workload().run_if(is_multiplayer).run_if(is_ingame_or_loading_or_connecting_or_shutting_down), ( update_connecting_screen, ).into_sequential_workload().run_if(is_connecting), @@ -153,12 +161,14 @@ fn update() -> Workload { //UI: render_chat, draw_crosshair, - render_settings_ui, + render_settings_ui.run_if(f1_held_settings_condition), ).into_sequential_workload().run_if(is_ingame), ( update_shutdown_screen, ).into_sequential_workload().run_if(is_shutting_down), - update_networking_late.run_if(is_multiplayer), + ( + update_networking_late + ).into_sequential_workload().run_if(is_multiplayer).run_if(is_ingame_or_loading_or_connecting_or_shutting_down), compute_cameras, kubi_ui_end, exit_on_esc, diff --git a/kubi/src/networking.rs b/kubi/src/networking.rs index ab9d18b..812bd65 100644 --- a/kubi/src/networking.rs +++ b/kubi/src/networking.rs @@ -11,9 +11,9 @@ use kubi_shared::networking::{ }; use crate::{ events::EventComponent, + fixed_timestamp::FixedTimestamp, + state::{is_ingame_or_loading, is_ingame_or_loading_or_connecting_or_shutting_down}, world::tasks::ChunkTaskManager, - state::is_ingame_or_loading, - fixed_timestamp::FixedTimestamp }; mod handshake; @@ -185,13 +185,15 @@ fn is_join_state( } pub fn is_multiplayer( - game_type: UniqueView + game_type: Option> ) -> bool { + let Some(game_type) = game_type else { return false }; *game_type == GameType::Muliplayer } pub fn is_singleplayer( - game_type: UniqueView + game_type: Option> ) -> bool { + let Some(game_type) = game_type else { return false }; *game_type == GameType::Singleplayer } diff --git a/kubi/src/state.rs b/kubi/src/state.rs index 0c2a294..fd9c0aa 100644 --- a/kubi/src/state.rs +++ b/kubi/src/state.rs @@ -5,6 +5,7 @@ use std::mem::take; pub enum GameState { #[default] Initial, + MainMenu, Connecting, LoadingWorld, InGame, @@ -34,6 +35,12 @@ pub fn is_changing_state( state.0.is_some() } +pub fn is_main_menu( + state: UniqueView +) -> bool { + *state == GameState::MainMenu +} + pub fn is_connecting( state: UniqueView ) -> bool { @@ -64,6 +71,12 @@ pub fn is_ingame_or_loading( matches!(*state, GameState::InGame | GameState::LoadingWorld) } +pub fn is_ingame_or_loading_or_connecting_or_shutting_down( + state: UniqueView +) -> bool { + matches!(*state, GameState::InGame | GameState::LoadingWorld | GameState::Connecting) +} + pub fn is_ingame_or_shutting_down( state: UniqueView ) -> bool { diff --git a/kubi/src/ui.rs b/kubi/src/ui.rs index 7a2c58b..7472f80 100644 --- a/kubi/src/ui.rs +++ b/kubi/src/ui.rs @@ -1,3 +1,4 @@ +pub(crate) mod main_menu; pub(crate) mod loading_screen; pub(crate) mod connecting_screen; pub(crate) mod shutdown_screen; diff --git a/kubi/src/ui/main_menu.rs b/kubi/src/ui/main_menu.rs new file mode 100644 index 0000000..e0e4211 --- /dev/null +++ b/kubi/src/ui/main_menu.rs @@ -0,0 +1,165 @@ +use std::path::Path; + +use glam::vec4; +use hui::{ + element::{ + container::Container, + interactable::ElementInteractableExt, + text::Text, + UiElementExt + }, + layout::Alignment, + rect::Corners, + signal::Signal, + rect_frame, + size, +}; +use kubi_shared::data::{io_thread::IOThreadManager, open_local_save_file}; +use settings_overlay::{not_settings_ui_shown, settings_overlay_logic}; +use shipyard::{AllStoragesView, AllStoragesViewMut, IntoWorkload, NonSendSync, SystemModificator, Unique, UniqueView, UniqueViewMut, Workload, WorkloadModificator}; +use crate::{ + control_flow::RequestExit, + hui_integration::UiState, networking::GameType, rendering::Renderer, state::{GameState, NextState}}; + + +mod settings_overlay; + +#[derive(Clone, Copy)] +enum MainMenuPage { + TopMenu, + Settings, + // ListWorlds { + // list: Vec, + // }, +} + +#[derive(Unique)] +struct MainMenuState { + page: MainMenuPage, +} + +#[derive(Signal, Clone, Copy)] +enum MainMenuSignal { + GotoPage(MainMenuPage), + PlayOffline, + PlayOnline, + Quit, + // CreatePlayWorld { + // name: String, + // }, + // PlayWorld { + // path: PathBuf, + // }, +} + +pub fn main_menu_leave( + storages: AllStoragesView, +) { + if storages.remove_unique::().is_err() { + log::warn!("what the fuck? shouldn't matter tho") + } +} + +pub fn render_main_menu_ui( + mut hui: NonSendSync>, + ren: UniqueView, +) { + Container::default() + .with_size(size!(100%, 100%)) + .with_padding(30.) + .with_gap(20.) + .with_background(Corners::top_bottom( + vec4(0.0, 0.0, 0.0, 0.85), + vec4(0.0, 0.0, 0.0, 0.0)) + ) + .with_children(|ui| { + Container::default() + .with_size(size!(100%, auto)) + .with_align(Alignment::Center) + .with_children(|ui| { + Text::new("Kubi") + .with_text_size(120) + .add_child(ui); + }) + .add_child(ui); + Container::default() + .with_size(size!(100%, 100%=)) + .with_align(Alignment::Center) + .with_children(|ui| { + Container::default() + .with_align((Alignment::Center, Alignment::Begin)) + .with_padding(20.) + .with_gap(10.) + .with_background(rect_frame! { + color: (0.1, 0.1, 0.1), + corner_radius: 10., + }) + .with_children(|ui| { + for (button_text, button_signal) in [ + ("Singleplayer", MainMenuSignal::PlayOffline), + ("Multiplayer", MainMenuSignal::PlayOnline), + ("Settings", MainMenuSignal::GotoPage(MainMenuPage::Settings)), + ("Quit", MainMenuSignal::Quit), + ] { + Container::default() + .with_size(size!(300, 50)) + .with_align(Alignment::Center) + .with_background(rect_frame! { + color: (0.2, 0.2, 0.2), + corner_radius: 3., + }) + .with_children(|ui| { + Text::new(button_text) + .with_text_size(24) + .add_child(ui); + }) + .on_click(move || button_signal) + .add_child(ui) + } + }) + .add_child(ui); + }) + .add_child(ui); + }) + .add_root(&mut hui.hui, ren.size_vec2()); +} + +fn main_menu_process_signals( + storages: AllStoragesViewMut, +) { + let mut hui = storages.borrow::>>().unwrap(); + let mut quit = storages.borrow::>().unwrap(); + hui.hui.process_signals(|signal| { + match signal { + MainMenuSignal::PlayOffline => { + log::info!("play button pressed"); + // Open the local save file + let save_file = open_local_save_file(Path::new("./world.kubi")).expect("failed to open save file"); + storages.add_unique(IOThreadManager::new(save_file)); + // Switch the state and kick off the world loading + storages.add_unique(GameType::Singleplayer); + storages.borrow::>().unwrap().0 = Some(GameState::LoadingWorld); + } + MainMenuSignal::PlayOnline => { + + }, + MainMenuSignal::GotoPage(page) => { + log::info!("goto page button pressed"); + storages.add_unique(MainMenuState { page }); + } + MainMenuSignal::Quit => { + log::info!("quit button pressed"); + quit.0 = true; + } + _ => {} + } + }); +} + +pub fn update_main_menu() -> Workload { + ( + render_main_menu_ui.run_if(not_settings_ui_shown), + settings_overlay_logic, + main_menu_process_signals, + ).into_sequential_workload() +} \ No newline at end of file diff --git a/kubi/src/ui/main_menu/settings_overlay.rs b/kubi/src/ui/main_menu/settings_overlay.rs new file mode 100644 index 0000000..3424b3f --- /dev/null +++ b/kubi/src/ui/main_menu/settings_overlay.rs @@ -0,0 +1,68 @@ +use std::path::Path; + +use glam::vec4; +use hui::{ + element::{ + container::Container, + interactable::ElementInteractableExt, + text::Text, + UiElementExt + }, + layout::Alignment, + rect::Corners, + signal::Signal, + rect_frame, + size, +}; +use kubi_shared::data::{io_thread::IOThreadManager, open_local_save_file}; +use shipyard::{AllStoragesView, AllStoragesViewMut, IntoWorkload, NonSendSync, SystemModificator, Unique, UniqueView, UniqueViewMut, Workload, WorkloadModificator}; +use crate::{ + control_flow::RequestExit, hui_integration::UiState, main_menu::MainMenuPage, networking::GameType, rendering::Renderer, state::{GameState, NextState}}; + +use super::{super::settings_ui::render_settings_ui2, MainMenuSignal, MainMenuState}; + +pub fn settings_ui_shown(mms: Option>) -> bool { + let Some(mms) = mms else { return false }; + matches!(mms.page, MainMenuPage::Settings) +} + +#[allow(clippy::just_underscores_and_digits)] +pub fn not_settings_ui_shown(_0: Option>) -> bool { + !settings_ui_shown(_0) +} + +// HACK: shows the back button over the settings UI +fn show_settings_back_button( + mut hui: NonSendSync>, + ren: UniqueView, +) { + Container::default() + .with_size(size!(100%, 100%)) + .with_align((Alignment::Center, Alignment::End)) + .with_padding(30.) + .with_children(|ui| { + Container::default() + .with_background(rect_frame! { + color: (0.1, 0.1, 0.1), + corner_radius: 10., + }) + .with_padding((30., 5.)) + .with_size(size!(auto, 50)) + .with_align(Alignment::Center) + .with_children(|ui| { + Text::new("Back to the Main Menu") + .with_text_size(24) + .add_child(ui); + }) + .on_click(|| MainMenuSignal::GotoPage(MainMenuPage::TopMenu)) + .add_child(ui); + }) + .add_root(&mut hui.hui, ren.size_vec2()); +} + +pub fn settings_overlay_logic() -> Workload { + ( + render_settings_ui2, + show_settings_back_button, + ).into_sequential_workload().run_if(settings_ui_shown) +} \ No newline at end of file diff --git a/kubi/src/ui/settings_ui.rs b/kubi/src/ui/settings_ui.rs index 7c3777f..7da9b95 100644 --- a/kubi/src/ui/settings_ui.rs +++ b/kubi/src/ui/settings_ui.rs @@ -19,7 +19,7 @@ enum SettingsSignal { SetRenderDistance(u8), SetEnableDynamicCrosshair(bool), SetEnableVsync(bool), - SetEnableDebugChunkBorder(bool), + // SetEnableDebugChunkBorder(bool), SetMouseSensitivity(f32), } @@ -68,18 +68,19 @@ fn checkbox( .add_child(ui); } +pub fn f1_held_settings_condition( + kbd: UniqueView, +) -> bool { + //f1 must be held down to open settings + //TODO implement ModalManager instead of this + kbd.keyboard_state.contains(KeyCode::F1 as u32) +} + pub fn render_settings_ui( mut ui: NonSendSync>, mut ren: UniqueViewMut, mut settings: UniqueViewMut, - kbd: UniqueView, ) { - //f1 must be held down to open settings - //TODO implement ModalManager instead of this - if !kbd.keyboard_state.contains(KeyCode::F1 as u32) { - return - } - Container::default() .with_size(size!(100%)) .with_background((0., 0., 0., 0.5)) @@ -132,13 +133,13 @@ pub fn render_settings_ui( ); Break.add_child(ui); - checkbox( - ui, - "Debug Chunk Border", - settings.debug_draw_current_chunk_border, - SettingsSignal::SetEnableDebugChunkBorder - ); - Break.add_child(ui); + // checkbox( + // ui, + // "Debug Chunk Border", + // settings.debug_draw_current_chunk_border, + // SettingsSignal::SetEnableDebugChunkBorder + // ); + // Break.add_child(ui); Text::new("Mouse Sensitivity") .add_child(ui); @@ -160,7 +161,16 @@ pub fn render_settings_ui( settings.vsync = value; ren.reload_settings(&settings); }, - SettingsSignal::SetEnableDebugChunkBorder(value) => settings.debug_draw_current_chunk_border = value && cfg!(not(target_os = "android")), + // SettingsSignal::SetEnableDebugChunkBorder(value) => settings.debug_draw_current_chunk_border = value && cfg!(not(target_os = "android")), SettingsSignal::SetMouseSensitivity(value) => settings.mouse_sensitivity = value, }); } + +#[allow(clippy::just_underscores_and_digits)] +pub fn render_settings_ui2( + _0: NonSendSync>, + _1: UniqueViewMut, + _2: UniqueViewMut, +) { + render_settings_ui(_0, _1, _2); +} diff --git a/kubi/src/ui/shutdown_screen.rs b/kubi/src/ui/shutdown_screen.rs index 5fb6f0c..5e07030 100644 --- a/kubi/src/ui/shutdown_screen.rs +++ b/kubi/src/ui/shutdown_screen.rs @@ -18,7 +18,12 @@ fn intercept_exit( mut state: UniqueViewMut, cur_state: UniqueView, termination_state: Option>, + iota: Option>, ) { + // If iota is missing, don't bother with shutdown state (likely running without a save file) + if iota.is_none() { + return; + } if exit.0 { if *cur_state == GameState::ShuttingDown { // If we're already shutting down, check if we're done diff --git a/kubi/src/world/loading.rs b/kubi/src/world/loading.rs index 5963be1..88b2d33 100644 --- a/kubi/src/world/loading.rs +++ b/kubi/src/world/loading.rs @@ -507,9 +507,13 @@ fn process_completed_tasks( /// Save all modified chunks to the disk pub fn save_on_exit( - io: UniqueView, + io: Option>, world: UniqueView, ) { + let Some(io) = io else { + log::warn!("no IO thread manager, skipping save on exit"); + return + }; for (&position, chunk) in &world.chunks { if let Some(block_data) = &chunk.block_data { if chunk.data_modified {