#[macro_use] extern crate log; #[cfg_attr(feature = "scripting", macro_use)] #[cfg(feature = "scripting")] extern crate lua_ffi; extern crate strum; #[macro_use] extern crate strum_macros; use std::cell::UnsafeCell; use std::env; use std::path::PathBuf; use std::sync::Mutex; use std::time::Instant; use directories::ProjectDirs; use lazy_static::lazy_static; use pretty_env_logger::env_logger::Env; use crate::builtin_fs::BuiltinFS; use crate::framework::context::Context; use crate::framework::error::{GameError, GameResult}; use crate::framework::filesystem::{mount_user_vfs, mount_vfs}; use crate::framework::graphics; use crate::framework::ui::UI; use crate::framework::vfs::PhysicalFS; use crate::scene::loading_scene::LoadingScene; use crate::scene::Scene; use crate::shared_game_state::{SharedGameState, TimingMode}; use crate::texture_set::{G_MAG, I_MAG}; mod bmfont; mod bmfont_renderer; mod builtin_fs; mod caret; mod common; mod components; mod difficulty_modifier; mod encoding; mod engine_constants; mod entity; mod frame; mod framework; mod input; mod inventory; mod live_debugger; mod macros; mod map; mod menu; #[cfg(feature = "netplay")] mod netplay; mod npc; mod physics; mod player; mod profile; mod rng; mod scene; #[cfg(feature = "scripting")] mod scripting; mod settings; #[cfg(feature = "backend-gfx")] mod shaders; mod shared_game_state; mod sound; mod stage; mod text_script; mod texture_set; mod weapon; lazy_static! { pub static ref GAME_SUSPENDED: Mutex = Mutex::new(false); } pub struct Game { scene: Option>, state: UnsafeCell, ui: UI, start_time: Instant, last_tick: u128, next_tick: u128, loops: u64, } impl Game { fn new(ctx: &mut Context) -> GameResult { let s = Game { scene: None, ui: UI::new(ctx)?, state: UnsafeCell::new(SharedGameState::new(ctx)?), start_time: Instant::now(), last_tick: 0, next_tick: 0, loops: 0, }; Ok(s) } fn update(&mut self, ctx: &mut Context) -> GameResult { if let Some(scene) = self.scene.as_mut() { let state_ref = unsafe { &mut *self.state.get() }; match state_ref.timing_mode { TimingMode::_50Hz | TimingMode::_60Hz => { let last_tick = self.next_tick; while self.start_time.elapsed().as_nanos() >= self.next_tick && self.loops < 10 { if (state_ref.settings.speed - 1.0).abs() < 0.01 { self.next_tick += state_ref.timing_mode.get_delta() as u128; } else { self.next_tick += (state_ref.timing_mode.get_delta() as f64 / state_ref.settings.speed) as u128; } self.loops += 1; } if self.loops == 10 { log::warn!("Frame skip is way too high, a long system lag occurred?"); self.last_tick = self.start_time.elapsed().as_nanos(); self.next_tick = self.last_tick + (state_ref.timing_mode.get_delta() as f64 / state_ref.settings.speed) as u128; self.loops = 0; } if self.loops != 0 { scene.draw_tick(state_ref)?; self.last_tick = last_tick; } for _ in 0..self.loops { scene.tick(state_ref, ctx)?; } } TimingMode::FrameSynchronized => { scene.tick(state_ref, ctx)?; } } } Ok(()) } fn draw(&mut self, ctx: &mut Context) -> GameResult { let state_ref = unsafe { &mut *self.state.get() }; if state_ref.timing_mode != TimingMode::FrameSynchronized { let mut elapsed = self.start_time.elapsed().as_nanos(); // Even with the non-monotonic Instant mitigation at the start of the event loop, there's still a chance of it not working. // This check here should trigger if that happens and makes sure there's no panic from an underflow. if elapsed < self.last_tick { elapsed = self.last_tick; } let n1 = (elapsed - self.last_tick) as f64; let n2 = (self.next_tick - self.last_tick) as f64; state_ref.frame_time = if state_ref.settings.motion_interpolation { n1 / n2 } else { 1.0 }; } unsafe { G_MAG = if state_ref.settings.subpixel_coords { state_ref.scale } else { 1.0 }; I_MAG = state_ref.scale; } self.loops = 0; graphics::prepare_draw(ctx)?; graphics::clear(ctx, [0.0, 0.0, 0.0, 1.0].into()); if let Some(scene) = self.scene.as_mut() { scene.draw(state_ref, ctx)?; if state_ref.settings.touch_controls { state_ref.touch_controls.draw( state_ref.canvas_size, state_ref.scale, &state_ref.constants, &mut state_ref.texture_set, ctx, )?; } self.ui.draw(state_ref, ctx, scene)?; } graphics::present(ctx)?; Ok(()) } } pub fn init() -> GameResult { pretty_env_logger::env_logger::from_env(Env::default().default_filter_or("info")) //.filter(Some("ndk_glue"), LevelFilter::Trace) .init(); #[cfg(not(target_os = "android"))] let resource_dir = if let Ok(data_dir) = env::var("CAVESTORY_DATA_DIR") { PathBuf::from(data_dir) } else { let mut resource_dir = env::current_exe()?; if resource_dir.file_name().is_some() { let _ = resource_dir.pop(); } resource_dir.push("data"); resource_dir }; #[cfg(not(target_os = "android"))] log::info!("Resource directory: {:?}", resource_dir); log::info!("Initializing engine..."); let mut context = Context::new(); mount_vfs(&mut context, Box::new(BuiltinFS::new())); #[cfg(not(target_os = "android"))] mount_vfs(&mut context, Box::new(PhysicalFS::new(&resource_dir, true))); #[cfg(not(target_os = "android"))] let project_dirs = match ProjectDirs::from("", "", "doukutsu-rs") { Some(dirs) => dirs, None => { return Err(GameError::FilesystemError(String::from("No valid home directory path could be retrieved."))); } }; #[cfg(target_os = "android")] { let mut data_path = PathBuf::from(ndk_glue::native_activity().internal_data_path().to_string_lossy().to_string()); let mut user_path = data_path.clone(); data_path.push("data"); user_path.push("saves"); let _ = std::fs::create_dir_all(&data_path); let _ = std::fs::create_dir_all(&user_path); log::info!("Android data directories: data_path={:?} user_path={:?}", &data_path, &user_path); mount_vfs(&mut context, Box::new(PhysicalFS::new(&data_path, true))); mount_user_vfs(&mut context, Box::new(PhysicalFS::new(&user_path, false))); } #[cfg(not(target_os = "android"))] mount_user_vfs(&mut context, Box::new(PhysicalFS::new(project_dirs.data_local_dir(), false))); let game = UnsafeCell::new(Game::new(&mut context)?); let state_ref = unsafe { &mut *((&mut *game.get()).state.get()) }; #[cfg(feature = "scripting")] { state_ref.lua.update_refs(unsafe { (&*game.get()).state.get() }, &mut context as *mut Context); } state_ref.next_scene = Some(Box::new(LoadingScene::new())); context.run(unsafe { &mut *game.get() })?; Ok(()) }