use std::any::{Any, TypeId};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::time::{Duration, Instant};
use crate::audio::{Audio, Sound};
use crate::error::Result;
use crate::input::{Input, Key, KeyEvent};
use crate::lang::Lang;
use crate::physics::{BodyId, World};
use crate::shortcuts::Attached;
use crate::speech::Speech;
use crate::storage::Storage;
use crate::time::Time;
use crate::ui::{Ui, UiEvent};
pub trait Scene: 'static {
fn start(&mut self, ctx: &mut Context) {
let _ = ctx;
}
fn update(&mut self, ctx: &mut Context);
fn fixed_update(&mut self, ctx: &mut Context) {
let _ = ctx;
}
fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
let _ = (event, ctx);
}
fn pause(&mut self, ctx: &mut Context) {
let _ = ctx;
}
fn resume(&mut self, ctx: &mut Context) {
let _ = ctx;
}
fn end(&mut self, ctx: &mut Context) {
let _ = ctx;
}
}
enum SceneChange {
Push(Box<dyn Scene>),
Pop,
Switch(Box<dyn Scene>),
}
pub struct Context {
pub time: Time,
pub input: Input,
pub audio: Audio,
pub speech: Speech,
pub ui: Ui,
pub physics: World,
pub storage: Storage,
pub lang: Lang,
pub(crate) follow: Option<BodyId>,
pub(crate) attached: Vec<Attached>,
pub(crate) engine_sounds: Vec<(BodyId, Sound)>,
pub(crate) once: HashSet<String>,
title: String,
quit: bool,
change: Option<SceneChange>,
globals: HashMap<TypeId, Box<dyn Any>>,
}
impl std::fmt::Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context").field("title", &self.title).field("time", &self.time).finish_non_exhaustive()
}
}
impl Context {
pub fn quit(&mut self) {
self.quit = true;
}
pub fn is_quitting(&self) -> bool {
self.quit
}
pub fn title(&self) -> &str {
&self.title
}
pub fn switch_scene(&mut self, scene: impl Scene) {
self.change = Some(SceneChange::Switch(Box::new(scene)));
}
pub fn push_scene(&mut self, scene: impl Scene) {
self.change = Some(SceneChange::Push(Box::new(scene)));
}
pub fn pop_scene(&mut self) {
self.change = Some(SceneChange::Pop);
}
pub fn insert<T: 'static>(&mut self, value: T) {
self.globals.insert(TypeId::of::<T>(), Box::new(value));
}
pub fn get<T: 'static>(&self) -> Option<&T> {
self.globals.get(&TypeId::of::<T>()).and_then(|b| b.downcast_ref())
}
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.globals.get_mut(&TypeId::of::<T>()).and_then(|b| b.downcast_mut())
}
pub fn get_or_default<T: Default + 'static>(&mut self) -> &mut T {
self.globals
.entry(TypeId::of::<T>())
.or_insert_with(|| Box::new(T::default()))
.downcast_mut()
.expect("type id matches")
}
pub fn remove<T: 'static>(&mut self) -> Option<T> {
self.globals.remove(&TypeId::of::<T>()).and_then(|b| b.downcast().ok()).map(|b| *b)
}
}
#[derive(Debug, Clone)]
pub struct Engine {
pub(crate) title: String,
pub(crate) fps: u32,
pub(crate) physics_rate: u32,
pub(crate) sound_dir: PathBuf,
pub(crate) echo_speech: bool,
pub(crate) window_size: (u32, u32),
pub(crate) headless: bool,
pub(crate) gravity: glam::Vec3,
pub(crate) save_file: Option<PathBuf>,
pub(crate) save_secret: Option<String>,
pub(crate) lang_dir: PathBuf,
pub(crate) language: Option<String>,
}
impl Engine {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
fps: 60,
physics_rate: 60,
sound_dir: PathBuf::from("sounds"),
echo_speech: false,
window_size: (640, 480),
headless: false,
gravity: glam::Vec3::ZERO,
save_file: None,
save_secret: None,
lang_dir: PathBuf::from("lang"),
language: None,
}
}
pub fn fps(mut self, fps: u32) -> Self {
self.fps = fps.clamp(1, 1000);
self
}
pub fn physics_rate(mut self, rate: u32) -> Self {
self.physics_rate = rate.clamp(1, 1000);
self
}
pub fn gravity(mut self, gravity: glam::Vec3) -> Self {
self.gravity = gravity;
self
}
pub fn save_file(mut self, path: impl Into<PathBuf>) -> Self {
self.save_file = Some(path.into());
self
}
pub fn encrypt_save(mut self, secret: impl Into<String>) -> Self {
self.save_secret = Some(secret.into());
self
}
pub fn sound_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.sound_dir = dir.into();
self
}
pub fn lang_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.lang_dir = dir.into();
self
}
pub fn language(mut self, code: impl Into<String>) -> Self {
self.language = Some(code.into());
self
}
pub fn echo_speech(mut self, echo: bool) -> Self {
self.echo_speech = echo;
self
}
pub fn window_size(mut self, width: u32, height: u32) -> Self {
self.window_size = (width, height);
self
}
pub fn headless(mut self) -> Self {
self.headless = true;
self
}
pub fn run(self, scene: impl Scene) -> Result<()> {
crate::window::run(self, Box::new(scene))
}
pub fn run_with<S: Scene>(self, build: impl FnOnce(&mut Context) -> S + 'static) -> Result<()> {
self.run(Lazy::new(build))
}
#[cfg(feature = "wxdragon")]
pub fn run_wx(self, scene: impl Scene) -> Result<()> {
crate::wx::run(self, Box::new(scene))
}
#[cfg(feature = "wxdragon")]
pub fn run_wx_with<S: Scene>(self, build: impl FnOnce(&mut Context) -> S + 'static) -> Result<()> {
self.run_wx(Lazy::new(build))
}
pub fn run_fn(self, update: impl FnMut(&mut Context) + 'static) -> Result<()> {
self.run(FnScene(update))
}
pub fn build_fn(self, update: impl FnMut(&mut Context) + 'static) -> Runner {
self.build(FnScene(update))
}
pub fn build(self, scene: impl Scene) -> Runner {
Runner::new(self, Box::new(scene))
}
}
fn load_lang(config: &Engine, storage: &Storage) -> Lang {
let mut lang = Lang::new();
if lang.load_dir(&config.lang_dir) == 0
&& config.lang_dir.is_relative()
&& let Some(resources) = crate::bundle::resources()
{
lang.load_dir(resources.join(&config.lang_dir));
}
let saved = storage.get::<String>(crate::shortcuts::LANGUAGE_KEY).and_then(|c| lang.resolve(&c));
let configured = config.language.as_deref().and_then(|c| lang.resolve(c));
let system = if config.headless { None } else { Lang::system().and_then(|c| lang.resolve_loaded(&c)) };
if let Some(code) = saved.or(configured).or(system) {
lang.set(&code);
} else if let Some(code) = &config.language {
lang.set(code); }
lang
}
struct FnScene<F>(F);
impl<F: FnMut(&mut Context) + 'static> Scene for FnScene<F> {
fn update(&mut self, ctx: &mut Context) {
(self.0)(ctx);
}
}
struct Lazy<S, F> {
build: Option<F>,
scene: Option<S>,
}
impl<S: Scene, F: FnOnce(&mut Context) -> S + 'static> Lazy<S, F> {
fn new(build: F) -> Self {
Self { build: Some(build), scene: None }
}
}
impl<S: Scene, F: FnOnce(&mut Context) -> S + 'static> Scene for Lazy<S, F> {
fn start(&mut self, ctx: &mut Context) {
if let Some(build) = self.build.take() {
let mut scene = build(ctx);
scene.start(ctx);
self.scene = Some(scene);
}
}
fn update(&mut self, ctx: &mut Context) {
if let Some(s) = &mut self.scene {
s.update(ctx);
}
}
fn fixed_update(&mut self, ctx: &mut Context) {
if let Some(s) = &mut self.scene {
s.fixed_update(ctx);
}
}
fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
if let Some(s) = &mut self.scene {
s.on_ui(event, ctx);
}
}
fn pause(&mut self, ctx: &mut Context) {
if let Some(s) = &mut self.scene {
s.pause(ctx);
}
}
fn resume(&mut self, ctx: &mut Context) {
if let Some(s) = &mut self.scene {
s.resume(ctx);
}
}
fn end(&mut self, ctx: &mut Context) {
if let Some(s) = &mut self.scene {
s.end(ctx);
}
}
}
pub struct Runner {
pub ctx: Context,
scenes: Vec<Box<dyn Scene>>,
started: bool,
accumulator: f32,
fixed_dt: f32,
frame_time: Duration,
next_frame: Instant,
}
impl std::fmt::Debug for Runner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Runner").field("scenes", &self.scenes.len()).finish_non_exhaustive()
}
}
impl Runner {
pub(crate) fn new(config: Engine, scene: Box<dyn Scene>) -> Self {
let storage = if config.headless {
Storage::in_memory()
} else {
let file = config.save_file.clone().unwrap_or_else(|| crate::storage::default_file(&config.title));
match &config.save_secret {
Some(secret) => Storage::open_encrypted(file, secret),
None => Storage::open(file),
}
};
let lang = load_lang(&config, &storage);
let ctx = Context {
time: Time::new(),
input: Input::default(),
audio: if config.headless {
Audio::silent(config.sound_dir.clone())
} else {
Audio::new(config.sound_dir.clone())
},
speech: if config.headless { Speech::console() } else { Speech::new(config.echo_speech) },
ui: Ui::default(),
physics: {
let mut world = World::new();
world.set_gravity(config.gravity);
world
},
storage,
lang,
follow: None,
attached: Vec::new(),
engine_sounds: Vec::new(),
once: HashSet::new(),
title: config.title.clone(),
quit: false,
change: None,
globals: HashMap::new(),
};
Self {
ctx,
scenes: vec![scene],
started: false,
accumulator: 0.0,
fixed_dt: 1.0 / config.physics_rate as f32,
frame_time: Duration::from_secs_f64(1.0 / config.fps as f64),
next_frame: Instant::now(),
}
}
pub fn key_down(&mut self, key: Key, repeat: bool) {
self.ctx.input.push(KeyEvent::Down { key, repeat });
}
pub fn key_up(&mut self, key: Key) {
self.ctx.input.push(KeyEvent::Up { key });
}
pub fn text(&mut self, text: &str) {
self.ctx.input.push_text(text);
}
pub fn focus_lost(&mut self) {
self.ctx.input.release_all();
}
pub fn next_frame_time(&self) -> Instant {
self.next_frame
}
pub fn should_quit(&self) -> bool {
self.ctx.quit
}
pub fn frame(&mut self) -> bool {
let now = Instant::now();
self.next_frame = (self.next_frame + self.frame_time).max(now);
if !self.started {
self.started = true;
self.ctx.time.advance(now);
if let Some(scene) = self.scenes.last_mut() {
scene.start(&mut self.ctx);
}
self.apply_change();
return !self.ctx.quit;
}
let ctx = &mut self.ctx;
ctx.time.advance(now);
ctx.input.begin_frame(now);
let consumed = ctx.ui.update(&ctx.input, &mut ctx.speech, &mut ctx.audio, &ctx.lang);
ctx.input.set_blocked(consumed || ctx.ui.is_open());
if let Some(scene) = self.scenes.last_mut() {
for event in self.ctx.ui.take_events() {
scene.on_ui(&event, &mut self.ctx);
}
self.ctx.physics.clear_events();
self.accumulator += self.ctx.time.delta();
let mut steps = 0;
while self.accumulator >= self.fixed_dt && steps < 8 {
self.accumulator -= self.fixed_dt;
self.ctx.physics.step(self.fixed_dt);
scene.fixed_update(&mut self.ctx);
steps += 1;
}
if steps == 8 {
self.accumulator = 0.0;
}
scene.update(&mut self.ctx);
}
self.ctx.sync_world();
self.ctx.sync_vehicles();
self.apply_change();
if self.ctx.quit {
self.shutdown();
return false;
}
true
}
fn apply_change(&mut self) {
while let Some(change) = self.ctx.change.take() {
match change {
SceneChange::Push(mut scene) => {
if let Some(top) = self.scenes.last_mut() {
top.pause(&mut self.ctx);
}
scene.start(&mut self.ctx);
self.scenes.push(scene);
}
SceneChange::Pop => {
if let Some(mut top) = self.scenes.pop() {
top.end(&mut self.ctx);
}
match self.scenes.last_mut() {
Some(top) => top.resume(&mut self.ctx),
None => self.ctx.quit = true,
}
}
SceneChange::Switch(mut scene) => {
if let Some(mut top) = self.scenes.pop() {
top.end(&mut self.ctx);
}
scene.start(&mut self.ctx);
self.scenes.push(scene);
}
}
}
}
pub fn shutdown(&mut self) {
while let Some(mut scene) = self.scenes.pop() {
scene.end(&mut self.ctx);
}
if let Err(e) = self.ctx.storage.save() {
eprintln!("audio-code: could not save storage: {e}");
}
self.ctx.quit = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::rc::Rc;
struct Recorder(Rc<RefCell<Vec<String>>>, &'static str);
impl Scene for Recorder {
fn start(&mut self, _: &mut Context) {
self.0.borrow_mut().push(format!("{} start", self.1));
}
fn update(&mut self, ctx: &mut Context) {
self.0.borrow_mut().push(format!("{} update", self.1));
if self.1 == "a" && ctx.input.pressed(Key::Enter) {
ctx.push_scene(Recorder(self.0.clone(), "b"));
}
if self.1 == "b" && ctx.input.pressed(Key::Escape) {
ctx.pop_scene();
}
}
fn pause(&mut self, _: &mut Context) {
self.0.borrow_mut().push(format!("{} pause", self.1));
}
fn resume(&mut self, _: &mut Context) {
self.0.borrow_mut().push(format!("{} resume", self.1));
}
fn end(&mut self, _: &mut Context) {
self.0.borrow_mut().push(format!("{} end", self.1));
}
}
#[test]
fn scene_stack() {
let log = Rc::new(RefCell::new(Vec::new()));
let mut runner = Engine::new("test").headless().build(Recorder(log.clone(), "a"));
runner.frame();
runner.key_down(Key::Enter, false);
runner.frame();
runner.key_up(Key::Enter);
runner.key_down(Key::Escape, false);
runner.frame();
runner.key_up(Key::Escape);
runner.key_down(Key::Escape, false);
runner.frame();
assert_eq!(
*log.borrow(),
["a start", "a update", "a pause", "b start", "b update", "b end", "a resume", "a update"]
);
}
#[test]
fn globals() {
let mut runner = Engine::new("test").headless().build(Recorder(Rc::default(), "x"));
runner.ctx.insert(5u32);
*runner.ctx.get_mut::<u32>().unwrap() += 1;
assert_eq!(runner.ctx.get::<u32>(), Some(&6));
assert_eq!(*runner.ctx.get_or_default::<String>(), "");
}
#[test]
fn start_language() {
let runner = Engine::new("test").headless().build(Recorder(Rc::default(), "x"));
assert_eq!(runner.ctx.lang.current(), "en");
let mut runner = Engine::new("test").headless().language("de").build(Recorder(Rc::default(), "x"));
assert_eq!(runner.ctx.lang.current(), "de");
assert_eq!(runner.ctx.tr("ui.yes"), "Ja");
assert!(runner.ctx.set_language("en-US"));
assert_eq!(runner.ctx.storage.get::<String>("language").as_deref(), Some("en"));
assert!(!runner.ctx.set_language("xx"));
}
}