1use std::any::{Any, TypeId};
4use std::collections::{HashMap, HashSet};
5use std::path::PathBuf;
6use std::time::{Duration, Instant};
7
8use crate::audio::{Audio, Sound};
9use crate::error::Result;
10use crate::input::{Input, Key, KeyEvent};
11use crate::lang::Lang;
12use crate::physics::{BodyId, World};
13use crate::shortcuts::Attached;
14use crate::speech::Speech;
15use crate::storage::Storage;
16use crate::time::Time;
17use crate::ui::{Ui, UiEvent};
18
19pub trait Scene: 'static {
25 fn start(&mut self, ctx: &mut Context) {
27 let _ = ctx;
28 }
29
30 fn update(&mut self, ctx: &mut Context);
32
33 fn fixed_update(&mut self, ctx: &mut Context) {
36 let _ = ctx;
37 }
38
39 fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
42 let _ = (event, ctx);
43 }
44
45 fn pause(&mut self, ctx: &mut Context) {
47 let _ = ctx;
48 }
49
50 fn resume(&mut self, ctx: &mut Context) {
52 let _ = ctx;
53 }
54
55 fn end(&mut self, ctx: &mut Context) {
57 let _ = ctx;
58 }
59}
60
61enum SceneChange {
62 Push(Box<dyn Scene>),
63 Pop,
64 Switch(Box<dyn Scene>),
65}
66
67pub struct Context {
69 pub time: Time,
71 pub input: Input,
73 pub audio: Audio,
75 pub speech: Speech,
77 pub ui: Ui,
79 pub physics: World,
81 pub storage: Storage,
83 pub lang: Lang,
85 pub(crate) follow: Option<BodyId>,
86 pub(crate) attached: Vec<Attached>,
87 pub(crate) engine_sounds: Vec<(BodyId, Sound)>,
88 pub(crate) once: HashSet<String>,
89 title: String,
90 quit: bool,
91 change: Option<SceneChange>,
92 globals: HashMap<TypeId, Box<dyn Any>>,
93}
94
95impl std::fmt::Debug for Context {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.debug_struct("Context").field("title", &self.title).field("time", &self.time).finish_non_exhaustive()
98 }
99}
100
101impl Context {
102 pub fn quit(&mut self) {
104 self.quit = true;
105 }
106
107 pub fn is_quitting(&self) -> bool {
109 self.quit
110 }
111
112 pub fn title(&self) -> &str {
114 &self.title
115 }
116
117 pub fn switch_scene(&mut self, scene: impl Scene) {
119 self.change = Some(SceneChange::Switch(Box::new(scene)));
120 }
121
122 pub fn push_scene(&mut self, scene: impl Scene) {
124 self.change = Some(SceneChange::Push(Box::new(scene)));
125 }
126
127 pub fn pop_scene(&mut self) {
129 self.change = Some(SceneChange::Pop);
130 }
131
132 pub fn insert<T: 'static>(&mut self, value: T) {
135 self.globals.insert(TypeId::of::<T>(), Box::new(value));
136 }
137
138 pub fn get<T: 'static>(&self) -> Option<&T> {
140 self.globals.get(&TypeId::of::<T>()).and_then(|b| b.downcast_ref())
141 }
142
143 pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
145 self.globals.get_mut(&TypeId::of::<T>()).and_then(|b| b.downcast_mut())
146 }
147
148 pub fn get_or_default<T: Default + 'static>(&mut self) -> &mut T {
150 self.globals
151 .entry(TypeId::of::<T>())
152 .or_insert_with(|| Box::new(T::default()))
153 .downcast_mut()
154 .expect("type id matches")
155 }
156
157 pub fn remove<T: 'static>(&mut self) -> Option<T> {
159 self.globals.remove(&TypeId::of::<T>()).and_then(|b| b.downcast().ok()).map(|b| *b)
160 }
161}
162
163#[derive(Debug, Clone)]
190pub struct Engine {
191 pub(crate) title: String,
192 pub(crate) fps: u32,
193 pub(crate) physics_rate: u32,
194 pub(crate) sound_dir: PathBuf,
195 pub(crate) echo_speech: bool,
196 pub(crate) window_size: (u32, u32),
197 pub(crate) headless: bool,
198 pub(crate) gravity: glam::Vec3,
199 pub(crate) save_file: Option<PathBuf>,
200 pub(crate) save_secret: Option<String>,
201 pub(crate) lang_dir: PathBuf,
202 pub(crate) language: Option<String>,
203}
204
205impl Engine {
206 pub fn new(title: impl Into<String>) -> Self {
209 Self {
210 title: title.into(),
211 fps: 60,
212 physics_rate: 60,
213 sound_dir: PathBuf::from("sounds"),
214 echo_speech: false,
215 window_size: (640, 480),
216 headless: false,
217 gravity: glam::Vec3::ZERO,
218 save_file: None,
219 save_secret: None,
220 lang_dir: PathBuf::from("lang"),
221 language: None,
222 }
223 }
224
225 pub fn fps(mut self, fps: u32) -> Self {
227 self.fps = fps.clamp(1, 1000);
228 self
229 }
230
231 pub fn physics_rate(mut self, rate: u32) -> Self {
233 self.physics_rate = rate.clamp(1, 1000);
234 self
235 }
236
237 pub fn gravity(mut self, gravity: glam::Vec3) -> Self {
240 self.gravity = gravity;
241 self
242 }
243
244 pub fn save_file(mut self, path: impl Into<PathBuf>) -> Self {
246 self.save_file = Some(path.into());
247 self
248 }
249
250 pub fn encrypt_save(mut self, secret: impl Into<String>) -> Self {
255 self.save_secret = Some(secret.into());
256 self
257 }
258
259 pub fn sound_dir(mut self, dir: impl Into<PathBuf>) -> Self {
261 self.sound_dir = dir.into();
262 self
263 }
264
265 pub fn lang_dir(mut self, dir: impl Into<PathBuf>) -> Self {
268 self.lang_dir = dir.into();
269 self
270 }
271
272 pub fn language(mut self, code: impl Into<String>) -> Self {
276 self.language = Some(code.into());
277 self
278 }
279
280 pub fn echo_speech(mut self, echo: bool) -> Self {
282 self.echo_speech = echo;
283 self
284 }
285
286 pub fn window_size(mut self, width: u32, height: u32) -> Self {
289 self.window_size = (width, height);
290 self
291 }
292
293 pub fn headless(mut self) -> Self {
296 self.headless = true;
297 self
298 }
299
300 pub fn run(self, scene: impl Scene) -> Result<()> {
302 crate::window::run(self, Box::new(scene))
303 }
304
305 pub fn run_with<S: Scene>(self, build: impl FnOnce(&mut Context) -> S + 'static) -> Result<()> {
324 self.run(Lazy::new(build))
325 }
326
327 #[cfg(feature = "wxdragon")]
330 pub fn run_wx(self, scene: impl Scene) -> Result<()> {
331 crate::wx::run(self, Box::new(scene))
332 }
333
334 #[cfg(feature = "wxdragon")]
336 pub fn run_wx_with<S: Scene>(self, build: impl FnOnce(&mut Context) -> S + 'static) -> Result<()> {
337 self.run_wx(Lazy::new(build))
338 }
339
340 pub fn run_fn(self, update: impl FnMut(&mut Context) + 'static) -> Result<()> {
343 self.run(FnScene(update))
344 }
345
346 pub fn build_fn(self, update: impl FnMut(&mut Context) + 'static) -> Runner {
348 self.build(FnScene(update))
349 }
350
351 pub fn build(self, scene: impl Scene) -> Runner {
354 Runner::new(self, Box::new(scene))
355 }
356}
357
358fn load_lang(config: &Engine, storage: &Storage) -> Lang {
361 let mut lang = Lang::new();
362 if lang.load_dir(&config.lang_dir) == 0
363 && config.lang_dir.is_relative()
364 && let Some(resources) = crate::bundle::resources()
365 {
366 lang.load_dir(resources.join(&config.lang_dir));
367 }
368 let saved = storage.get::<String>(crate::shortcuts::LANGUAGE_KEY).and_then(|c| lang.resolve(&c));
369 let configured = config.language.as_deref().and_then(|c| lang.resolve(c));
370 let system = if config.headless { None } else { Lang::system().and_then(|c| lang.resolve_loaded(&c)) };
371 if let Some(code) = saved.or(configured).or(system) {
372 lang.set(&code);
373 } else if let Some(code) = &config.language {
374 lang.set(code); }
376 lang
377}
378
379struct FnScene<F>(F);
381
382impl<F: FnMut(&mut Context) + 'static> Scene for FnScene<F> {
383 fn update(&mut self, ctx: &mut Context) {
384 (self.0)(ctx);
385 }
386}
387
388struct Lazy<S, F> {
390 build: Option<F>,
391 scene: Option<S>,
392}
393
394impl<S: Scene, F: FnOnce(&mut Context) -> S + 'static> Lazy<S, F> {
395 fn new(build: F) -> Self {
396 Self { build: Some(build), scene: None }
397 }
398}
399
400impl<S: Scene, F: FnOnce(&mut Context) -> S + 'static> Scene for Lazy<S, F> {
401 fn start(&mut self, ctx: &mut Context) {
402 if let Some(build) = self.build.take() {
403 let mut scene = build(ctx);
404 scene.start(ctx);
405 self.scene = Some(scene);
406 }
407 }
408 fn update(&mut self, ctx: &mut Context) {
409 if let Some(s) = &mut self.scene {
410 s.update(ctx);
411 }
412 }
413 fn fixed_update(&mut self, ctx: &mut Context) {
414 if let Some(s) = &mut self.scene {
415 s.fixed_update(ctx);
416 }
417 }
418 fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
419 if let Some(s) = &mut self.scene {
420 s.on_ui(event, ctx);
421 }
422 }
423 fn pause(&mut self, ctx: &mut Context) {
424 if let Some(s) = &mut self.scene {
425 s.pause(ctx);
426 }
427 }
428 fn resume(&mut self, ctx: &mut Context) {
429 if let Some(s) = &mut self.scene {
430 s.resume(ctx);
431 }
432 }
433 fn end(&mut self, ctx: &mut Context) {
434 if let Some(s) = &mut self.scene {
435 s.end(ctx);
436 }
437 }
438}
439
440pub struct Runner {
443 pub ctx: Context,
445 scenes: Vec<Box<dyn Scene>>,
446 started: bool,
447 accumulator: f32,
448 fixed_dt: f32,
449 frame_time: Duration,
450 next_frame: Instant,
451}
452
453impl std::fmt::Debug for Runner {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 f.debug_struct("Runner").field("scenes", &self.scenes.len()).finish_non_exhaustive()
456 }
457}
458
459impl Runner {
460 pub(crate) fn new(config: Engine, scene: Box<dyn Scene>) -> Self {
461 let storage = if config.headless {
462 Storage::in_memory()
463 } else {
464 let file = config.save_file.clone().unwrap_or_else(|| crate::storage::default_file(&config.title));
465 match &config.save_secret {
466 Some(secret) => Storage::open_encrypted(file, secret),
467 None => Storage::open(file),
468 }
469 };
470 let lang = load_lang(&config, &storage);
471 let ctx = Context {
472 time: Time::new(),
473 input: Input::default(),
474 audio: if config.headless {
475 Audio::silent(config.sound_dir.clone())
476 } else {
477 Audio::new(config.sound_dir.clone())
478 },
479 speech: if config.headless { Speech::console() } else { Speech::new(config.echo_speech) },
480 ui: Ui::default(),
481 physics: {
482 let mut world = World::new();
483 world.set_gravity(config.gravity);
484 world
485 },
486 storage,
487 lang,
488 follow: None,
489 attached: Vec::new(),
490 engine_sounds: Vec::new(),
491 once: HashSet::new(),
492 title: config.title.clone(),
493 quit: false,
494 change: None,
495 globals: HashMap::new(),
496 };
497 Self {
498 ctx,
499 scenes: vec![scene],
500 started: false,
501 accumulator: 0.0,
502 fixed_dt: 1.0 / config.physics_rate as f32,
503 frame_time: Duration::from_secs_f64(1.0 / config.fps as f64),
504 next_frame: Instant::now(),
505 }
506 }
507
508 pub fn key_down(&mut self, key: Key, repeat: bool) {
510 self.ctx.input.push(KeyEvent::Down { key, repeat });
511 }
512
513 pub fn key_up(&mut self, key: Key) {
515 self.ctx.input.push(KeyEvent::Up { key });
516 }
517
518 pub fn text(&mut self, text: &str) {
520 self.ctx.input.push_text(text);
521 }
522
523 pub fn focus_lost(&mut self) {
525 self.ctx.input.release_all();
526 }
527
528 pub fn next_frame_time(&self) -> Instant {
530 self.next_frame
531 }
532
533 pub fn should_quit(&self) -> bool {
535 self.ctx.quit
536 }
537
538 pub fn frame(&mut self) -> bool {
540 let now = Instant::now();
541 self.next_frame = (self.next_frame + self.frame_time).max(now);
543
544 if !self.started {
545 self.started = true;
546 self.ctx.time.advance(now);
547 if let Some(scene) = self.scenes.last_mut() {
548 scene.start(&mut self.ctx);
549 }
550 self.apply_change();
551 return !self.ctx.quit;
552 }
553
554 let ctx = &mut self.ctx;
555 ctx.time.advance(now);
556 ctx.input.begin_frame(now);
557
558 let consumed = ctx.ui.update(&ctx.input, &mut ctx.speech, &mut ctx.audio, &ctx.lang);
559 ctx.input.set_blocked(consumed || ctx.ui.is_open());
560
561 if let Some(scene) = self.scenes.last_mut() {
562 for event in self.ctx.ui.take_events() {
563 scene.on_ui(&event, &mut self.ctx);
564 }
565
566 self.ctx.physics.clear_events();
567 self.accumulator += self.ctx.time.delta();
568 let mut steps = 0;
569 while self.accumulator >= self.fixed_dt && steps < 8 {
570 self.accumulator -= self.fixed_dt;
571 self.ctx.physics.step(self.fixed_dt);
572 scene.fixed_update(&mut self.ctx);
573 steps += 1;
574 }
575 if steps == 8 {
576 self.accumulator = 0.0;
577 }
578
579 scene.update(&mut self.ctx);
580 }
581 self.ctx.sync_world();
582 self.ctx.sync_vehicles();
583 self.apply_change();
584
585 if self.ctx.quit {
586 self.shutdown();
587 return false;
588 }
589 true
590 }
591
592 fn apply_change(&mut self) {
593 while let Some(change) = self.ctx.change.take() {
595 match change {
596 SceneChange::Push(mut scene) => {
597 if let Some(top) = self.scenes.last_mut() {
598 top.pause(&mut self.ctx);
599 }
600 scene.start(&mut self.ctx);
601 self.scenes.push(scene);
602 }
603 SceneChange::Pop => {
604 if let Some(mut top) = self.scenes.pop() {
605 top.end(&mut self.ctx);
606 }
607 match self.scenes.last_mut() {
608 Some(top) => top.resume(&mut self.ctx),
609 None => self.ctx.quit = true,
610 }
611 }
612 SceneChange::Switch(mut scene) => {
613 if let Some(mut top) = self.scenes.pop() {
614 top.end(&mut self.ctx);
615 }
616 scene.start(&mut self.ctx);
617 self.scenes.push(scene);
618 }
619 }
620 }
621 }
622
623 pub fn shutdown(&mut self) {
625 while let Some(mut scene) = self.scenes.pop() {
626 scene.end(&mut self.ctx);
627 }
628 if let Err(e) = self.ctx.storage.save() {
629 eprintln!("audio-code: could not save storage: {e}");
630 }
631 self.ctx.quit = true;
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use std::cell::RefCell;
639 use std::rc::Rc;
640
641 struct Recorder(Rc<RefCell<Vec<String>>>, &'static str);
642
643 impl Scene for Recorder {
644 fn start(&mut self, _: &mut Context) {
645 self.0.borrow_mut().push(format!("{} start", self.1));
646 }
647 fn update(&mut self, ctx: &mut Context) {
648 self.0.borrow_mut().push(format!("{} update", self.1));
649 if self.1 == "a" && ctx.input.pressed(Key::Enter) {
650 ctx.push_scene(Recorder(self.0.clone(), "b"));
651 }
652 if self.1 == "b" && ctx.input.pressed(Key::Escape) {
653 ctx.pop_scene();
654 }
655 }
656 fn pause(&mut self, _: &mut Context) {
657 self.0.borrow_mut().push(format!("{} pause", self.1));
658 }
659 fn resume(&mut self, _: &mut Context) {
660 self.0.borrow_mut().push(format!("{} resume", self.1));
661 }
662 fn end(&mut self, _: &mut Context) {
663 self.0.borrow_mut().push(format!("{} end", self.1));
664 }
665 }
666
667 #[test]
668 fn scene_stack() {
669 let log = Rc::new(RefCell::new(Vec::new()));
670 let mut runner = Engine::new("test").headless().build(Recorder(log.clone(), "a"));
671 runner.frame();
672 runner.key_down(Key::Enter, false);
673 runner.frame();
674 runner.key_up(Key::Enter);
675 runner.key_down(Key::Escape, false);
676 runner.frame();
677 runner.key_up(Key::Escape);
678 runner.key_down(Key::Escape, false);
679 runner.frame();
680 assert_eq!(
681 *log.borrow(),
682 ["a start", "a update", "a pause", "b start", "b update", "b end", "a resume", "a update"]
683 );
684 }
685
686 #[test]
687 fn globals() {
688 let mut runner = Engine::new("test").headless().build(Recorder(Rc::default(), "x"));
689 runner.ctx.insert(5u32);
690 *runner.ctx.get_mut::<u32>().unwrap() += 1;
691 assert_eq!(runner.ctx.get::<u32>(), Some(&6));
692 assert_eq!(*runner.ctx.get_or_default::<String>(), "");
693 }
694
695 #[test]
696 fn start_language() {
697 let runner = Engine::new("test").headless().build(Recorder(Rc::default(), "x"));
698 assert_eq!(runner.ctx.lang.current(), "en");
699 let mut runner = Engine::new("test").headless().language("de").build(Recorder(Rc::default(), "x"));
700 assert_eq!(runner.ctx.lang.current(), "de");
701 assert_eq!(runner.ctx.tr("ui.yes"), "Ja");
702 assert!(runner.ctx.set_language("en-US"));
703 assert_eq!(runner.ctx.storage.get::<String>("language").as_deref(), Some("en"));
704 assert!(!runner.ctx.set_language("xx"));
705 }
706}