Skip to main content

audio_code/
engine.rs

1//! The game loop: [`Engine`], [`Scene`] and [`Context`].
2
3use 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::net::{Net, NetEvent};
13use crate::physics::{BodyId, World};
14use crate::shortcuts::Attached;
15use crate::speech::Speech;
16use crate::storage::Storage;
17use crate::time::Time;
18use crate::ui::{Ui, UiEvent};
19
20/// One screen or state of your game: main menu, a level, the game over screen...
21///
22/// Only [`Scene::update`] is required. Scenes are switched with [`Context::switch_scene`],
23/// stacked with [`Context::push_scene`] (e.g. a pause screen) and removed with
24/// [`Context::pop_scene`].
25pub trait Scene: 'static {
26    /// Called once when the scene becomes active for the first time.
27    fn start(&mut self, ctx: &mut Context) {
28        let _ = ctx;
29    }
30
31    /// Called every frame. Put your game logic here.
32    fn update(&mut self, ctx: &mut Context);
33
34    /// Called at a fixed rate (60 times per second by default) right after each physics step.
35    /// Use it for logic that must not depend on the frame rate.
36    fn fixed_update(&mut self, ctx: &mut Context) {
37        let _ = ctx;
38    }
39
40    /// Called for every UI event before [`Scene::update`]. An alternative to polling
41    /// `ctx.ui.clicked(...)` and friends in `update`.
42    fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
43        let _ = (event, ctx);
44    }
45
46    /// Called for every network event (a player joined, a packet arrived, a player left)
47    /// before [`Scene::update`]. An alternative to polling `ctx.net.received(..)` and
48    /// friends in `update`.
49    fn on_net(&mut self, event: &NetEvent, ctx: &mut Context) {
50        let _ = (event, ctx);
51    }
52
53    /// Called when another scene is pushed on top of this one.
54    fn pause(&mut self, ctx: &mut Context) {
55        let _ = ctx;
56    }
57
58    /// Called when this scene is on top again after the scene above was popped.
59    fn resume(&mut self, ctx: &mut Context) {
60        let _ = ctx;
61    }
62
63    /// Called when the scene is removed (popped, switched away, or the game quits).
64    fn end(&mut self, ctx: &mut Context) {
65        let _ = ctx;
66    }
67}
68
69enum SceneChange {
70    Push(Box<dyn Scene>),
71    Pop,
72    Switch(Box<dyn Scene>),
73}
74
75/// Everything your game can use, passed to every [`Scene`] method.
76pub struct Context {
77    /// Frame timing and scheduling.
78    pub time: Time,
79    /// Keyboard state.
80    pub input: Input,
81    /// Sound effects, positioned sounds and music.
82    pub audio: Audio,
83    /// Screen reader / text-to-speech output.
84    pub speech: Speech,
85    /// Virtual menus and dialogs.
86    pub ui: Ui,
87    /// Physics world.
88    pub physics: World,
89    /// Values that are saved when the game quits (high scores, settings).
90    pub storage: Storage,
91    /// Translations and the current language.
92    pub lang: Lang,
93    /// Online play: hosting, joining and sending packets.
94    pub net: Net,
95    pub(crate) follow: Option<BodyId>,
96    pub(crate) attached: Vec<Attached>,
97    pub(crate) engine_sounds: Vec<(BodyId, Sound)>,
98    pub(crate) once: HashSet<String>,
99    title: String,
100    quit: bool,
101    change: Option<SceneChange>,
102    globals: HashMap<TypeId, Box<dyn Any>>,
103}
104
105impl std::fmt::Debug for Context {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("Context").field("title", &self.title).field("time", &self.time).finish_non_exhaustive()
108    }
109}
110
111impl Context {
112    /// Requests the game to end after this frame.
113    pub fn quit(&mut self) {
114        self.quit = true;
115    }
116
117    /// Whether [`Context::quit`] was called.
118    pub fn is_quitting(&self) -> bool {
119        self.quit
120    }
121
122    /// The game title given to [`Engine::new`].
123    pub fn title(&self) -> &str {
124        &self.title
125    }
126
127    /// Replaces the current scene with another one after this frame.
128    pub fn switch_scene(&mut self, scene: impl Scene) {
129        self.change = Some(SceneChange::Switch(Box::new(scene)));
130    }
131
132    /// Puts a scene on top of the current one (which is paused) after this frame.
133    pub fn push_scene(&mut self, scene: impl Scene) {
134        self.change = Some(SceneChange::Push(Box::new(scene)));
135    }
136
137    /// Removes the current scene after this frame. Popping the last scene quits the game.
138    pub fn pop_scene(&mut self) {
139        self.change = Some(SceneChange::Pop);
140    }
141
142    /// Stores a value shared by all scenes (settings, score, save data...).
143    /// There is one slot per type; storing again replaces the old value.
144    pub fn insert<T: 'static>(&mut self, value: T) {
145        self.globals.insert(TypeId::of::<T>(), Box::new(value));
146    }
147
148    /// Reads a shared value stored with [`Context::insert`].
149    pub fn get<T: 'static>(&self) -> Option<&T> {
150        self.globals.get(&TypeId::of::<T>()).and_then(|b| b.downcast_ref())
151    }
152
153    /// Mutable access to a shared value.
154    pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
155        self.globals.get_mut(&TypeId::of::<T>()).and_then(|b| b.downcast_mut())
156    }
157
158    /// Mutable access to a shared value, inserting `T::default()` if it is missing.
159    pub fn get_or_default<T: Default + 'static>(&mut self) -> &mut T {
160        self.globals
161            .entry(TypeId::of::<T>())
162            .or_insert_with(|| Box::new(T::default()))
163            .downcast_mut()
164            .expect("type id matches")
165    }
166
167    /// Removes a shared value and returns it.
168    pub fn remove<T: 'static>(&mut self) -> Option<T> {
169        self.globals.remove(&TypeId::of::<T>()).and_then(|b| b.downcast().ok()).map(|b| *b)
170    }
171}
172
173/// Configures and starts the game.
174///
175/// ```no_run
176/// use audio_code::prelude::*;
177///
178/// struct Hello;
179///
180/// impl Scene for Hello {
181///     fn start(&mut self, ctx: &mut Context) {
182///         ctx.speech.speak("Hello! Press space to beep, escape to quit.");
183///     }
184///
185///     fn update(&mut self, ctx: &mut Context) {
186///         if ctx.input.pressed(Key::Space) {
187///             ctx.audio.play_tone(440.0, 0.2);
188///         }
189///         if ctx.input.pressed(Key::Escape) {
190///             ctx.quit();
191///         }
192///     }
193/// }
194///
195/// fn main() -> audio_code::Result<()> {
196///     Engine::new("Hello").run(Hello)
197/// }
198/// ```
199#[derive(Debug, Clone)]
200pub struct Engine {
201    pub(crate) title: String,
202    pub(crate) fps: u32,
203    pub(crate) physics_rate: u32,
204    pub(crate) sound_dir: PathBuf,
205    pub(crate) echo_speech: bool,
206    pub(crate) window_size: (u32, u32),
207    pub(crate) headless: bool,
208    pub(crate) server: bool,
209    pub(crate) gravity: glam::Vec3,
210    pub(crate) save_file: Option<PathBuf>,
211    pub(crate) save_secret: Option<String>,
212    pub(crate) lang_dir: PathBuf,
213    pub(crate) language: Option<String>,
214}
215
216impl Engine {
217    /// Creates a new engine configuration. The title is used for the window and is what the
218    /// screen reader announces when the window gets focus.
219    pub fn new(title: impl Into<String>) -> Self {
220        Self {
221            title: title.into(),
222            fps: 60,
223            physics_rate: 60,
224            sound_dir: PathBuf::from("sounds"),
225            echo_speech: false,
226            window_size: (640, 480),
227            headless: false,
228            server: false,
229            gravity: glam::Vec3::ZERO,
230            save_file: None,
231            save_secret: None,
232            lang_dir: PathBuf::from("lang"),
233            language: None,
234        }
235    }
236
237    /// Target frames per second (default 60).
238    pub fn fps(mut self, fps: u32) -> Self {
239        self.fps = fps.clamp(1, 1000);
240        self
241    }
242
243    /// Physics and [`Scene::fixed_update`] steps per second (default 60).
244    pub fn physics_rate(mut self, rate: u32) -> Self {
245        self.physics_rate = rate.clamp(1, 1000);
246        self
247    }
248
249    /// Gravity of the physics world, e.g. [`EARTH_GRAVITY`](crate::physics::EARTH_GRAVITY).
250    /// Default is none (top down games). Can be changed later with `ctx.physics.set_gravity`.
251    pub fn gravity(mut self, gravity: glam::Vec3) -> Self {
252        self.gravity = gravity;
253        self
254    }
255
256    /// File used by [`Context::storage`] (default `<title>.save` in the working directory).
257    pub fn save_file(mut self, path: impl Into<PathBuf>) -> Self {
258        self.save_file = Some(path.into());
259        self
260    }
261
262    /// Encrypts the save file of [`Context::storage`] with a key made from `secret`
263    /// (optional, default off). Players can no longer read or edit it, and a changed or
264    /// damaged file is detected and kept as `<file>.broken`. An existing plain text save is
265    /// converted on the next save. See [`crate::storage`] for what this protects against.
266    pub fn encrypt_save(mut self, secret: impl Into<String>) -> Self {
267        self.save_secret = Some(secret.into());
268        self
269    }
270
271    /// Folder searched for sounds (default `"sounds"`). See [`crate::audio`].
272    pub fn sound_dir(mut self, dir: impl Into<PathBuf>) -> Self {
273        self.sound_dir = dir.into();
274        self
275    }
276
277    /// Folder with the translation files `<code>.txt` (default `"lang"`). See
278    /// [`crate::lang`].
279    pub fn lang_dir(mut self, dir: impl Into<PathBuf>) -> Self {
280        self.lang_dir = dir.into();
281        self
282    }
283
284    /// The language the game starts in, e.g. `"de"` (default: the system language if the
285    /// game has texts for it, else English). A language the player chose with
286    /// [`Context::set_language`] still wins.
287    pub fn language(mut self, code: impl Into<String>) -> Self {
288        self.language = Some(code.into());
289        self
290    }
291
292    /// Print everything spoken to the console as well (default `false`).
293    pub fn echo_speech(mut self, echo: bool) -> Self {
294        self.echo_speech = echo;
295        self
296    }
297
298    /// Window size in pixels (default 640×480). The window shows nothing, but sighted
299    /// helpers can still find and focus it.
300    pub fn window_size(mut self, width: u32, height: u32) -> Self {
301        self.window_size = (width, height);
302        self
303    }
304
305    /// Use no audio device and print speech to the console instead of using prism.
306    /// Meant for automated tests and tools.
307    pub fn headless(mut self) -> Self {
308        self.headless = true;
309        self
310    }
311
312    /// Runs a dedicated server: no window, no sound, speech goes to the console (so
313    /// `ctx.speak` works as a log). The storage is saved as usual. Runs until
314    /// [`Context::quit`] is called. Host with `ctx.net.host(..)` in the first scene.
315    ///
316    /// ```no_run
317    /// use audio_code::prelude::*;
318    ///
319    /// fn main() -> audio_code::Result<()> {
320    ///     Engine::new("My game").run_server_fn(|ctx| {
321    ///         if ctx.once("host") {
322    ///             ctx.net.host(net::DEFAULT_PORT).expect("port is free");
323    ///             ctx.speak("Server started");
324    ///         }
325    ///         for (from, packet) in ctx.net.messages() {
326    ///             ctx.net.send_all_except(from, packet);
327    ///         }
328    ///     })
329    /// }
330    /// ```
331    pub fn run_server(mut self, scene: impl Scene) -> Result<()> {
332        self.server = true;
333        let mut runner = self.build(scene);
334        while runner.frame() {
335            std::thread::sleep(runner.next_frame_time().saturating_duration_since(Instant::now()));
336        }
337        Ok(())
338    }
339
340    /// Like [`Engine::run_server`] for a server written as one closure.
341    pub fn run_server_fn(self, update: impl FnMut(&mut Context) + 'static) -> Result<()> {
342        self.run_server(FnScene(update))
343    }
344
345    /// Opens the window and runs the game until it quits.
346    pub fn run(self, scene: impl Scene) -> Result<()> {
347        crate::window::run(self, Box::new(scene))
348    }
349
350    /// Like [`Engine::run`], but builds the first scene with access to the [`Context`].
351    /// Handy when the scene needs to create bodies or load sounds in its constructor:
352    ///
353    /// ```no_run
354    /// # use audio_code::prelude::*;
355    /// struct Level { player: BodyId }
356    ///
357    /// impl Level {
358    ///     fn new(ctx: &mut Context) -> Self {
359    ///         Self { player: ctx.physics.add(Body::kinematic(Vec3::ZERO)) }
360    ///     }
361    /// }
362    /// # impl Scene for Level { fn update(&mut self, _: &mut Context) {} }
363    ///
364    /// fn main() -> audio_code::Result<()> {
365    ///     Engine::new("Game").run_with(Level::new)
366    /// }
367    /// ```
368    pub fn run_with<S: Scene>(self, build: impl FnOnce(&mut Context) -> S + 'static) -> Result<()> {
369        self.run(Lazy::new(build))
370    }
371
372    /// Like [`Engine::run`], but uses a native wxWidgets frame (feature `wxdragon`).
373    /// See [`crate::wx`] for embedding a game into an existing wxDragon application.
374    #[cfg(feature = "wxdragon")]
375    pub fn run_wx(self, scene: impl Scene) -> Result<()> {
376        crate::wx::run(self, Box::new(scene))
377    }
378
379    /// Like [`Engine::run_with`] for a wxWidgets frame (feature `wxdragon`).
380    #[cfg(feature = "wxdragon")]
381    pub fn run_wx_with<S: Scene>(self, build: impl FnOnce(&mut Context) -> S + 'static) -> Result<()> {
382        self.run_wx(Lazy::new(build))
383    }
384
385    /// Runs a game written as a single closure that is called every frame.
386    /// The closure can keep its own state in captured variables. See [`crate::run`].
387    pub fn run_fn(self, update: impl FnMut(&mut Context) + 'static) -> Result<()> {
388        self.run(FnScene(update))
389    }
390
391    /// Like [`Engine::build`] for a closure game.
392    pub fn build_fn(self, update: impl FnMut(&mut Context) + 'static) -> Runner {
393        self.build(FnScene(update))
394    }
395
396    /// Creates the [`Context`] and runner without a window. Window backends use this; you
397    /// can use it for tests or to drive the engine from your own loop.
398    pub fn build(self, scene: impl Scene) -> Runner {
399        Runner::new(self, Box::new(scene))
400    }
401}
402
403/// Loads the translation files and picks the start language: the player's saved choice, then
404/// [`Engine::language`], then the system language, then the fallback.
405fn load_lang(config: &Engine, storage: &Storage) -> Lang {
406    let mut lang = Lang::new();
407    if lang.load_dir(&config.lang_dir) == 0
408        && config.lang_dir.is_relative()
409        && let Some(resources) = crate::bundle::resources()
410    {
411        lang.load_dir(resources.join(&config.lang_dir));
412    }
413    let saved = storage.get::<String>(crate::shortcuts::LANGUAGE_KEY).and_then(|c| lang.resolve(&c));
414    let configured = config.language.as_deref().and_then(|c| lang.resolve(c));
415    let system = if config.headless { None } else { Lang::system().and_then(|c| lang.resolve_loaded(&c)) };
416    if let Some(code) = saved.or(configured).or(system) {
417        lang.set(&code);
418    } else if let Some(code) = &config.language {
419        lang.set(code); // reports the unknown language
420    }
421    lang
422}
423
424/// A scene made from a closure.
425struct FnScene<F>(F);
426
427impl<F: FnMut(&mut Context) + 'static> Scene for FnScene<F> {
428    fn update(&mut self, ctx: &mut Context) {
429        (self.0)(ctx);
430    }
431}
432
433/// A scene that is built on its first `start` call.
434struct Lazy<S, F> {
435    build: Option<F>,
436    scene: Option<S>,
437}
438
439impl<S: Scene, F: FnOnce(&mut Context) -> S + 'static> Lazy<S, F> {
440    fn new(build: F) -> Self {
441        Self { build: Some(build), scene: None }
442    }
443}
444
445impl<S: Scene, F: FnOnce(&mut Context) -> S + 'static> Scene for Lazy<S, F> {
446    fn start(&mut self, ctx: &mut Context) {
447        if let Some(build) = self.build.take() {
448            let mut scene = build(ctx);
449            scene.start(ctx);
450            self.scene = Some(scene);
451        }
452    }
453    fn update(&mut self, ctx: &mut Context) {
454        if let Some(s) = &mut self.scene {
455            s.update(ctx);
456        }
457    }
458    fn fixed_update(&mut self, ctx: &mut Context) {
459        if let Some(s) = &mut self.scene {
460            s.fixed_update(ctx);
461        }
462    }
463    fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
464        if let Some(s) = &mut self.scene {
465            s.on_ui(event, ctx);
466        }
467    }
468    fn on_net(&mut self, event: &NetEvent, ctx: &mut Context) {
469        if let Some(s) = &mut self.scene {
470            s.on_net(event, ctx);
471        }
472    }
473    fn pause(&mut self, ctx: &mut Context) {
474        if let Some(s) = &mut self.scene {
475            s.pause(ctx);
476        }
477    }
478    fn resume(&mut self, ctx: &mut Context) {
479        if let Some(s) = &mut self.scene {
480            s.resume(ctx);
481        }
482    }
483    fn end(&mut self, ctx: &mut Context) {
484        if let Some(s) = &mut self.scene {
485            s.end(ctx);
486        }
487    }
488}
489
490/// Drives scenes frame by frame. Window backends feed it key events and call
491/// [`Runner::frame`]. You only need this when writing your own backend or tests.
492pub struct Runner {
493    /// The context passed to scenes.
494    pub ctx: Context,
495    scenes: Vec<Box<dyn Scene>>,
496    started: bool,
497    accumulator: f32,
498    fixed_dt: f32,
499    frame_time: Duration,
500    next_frame: Instant,
501}
502
503impl std::fmt::Debug for Runner {
504    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505        f.debug_struct("Runner").field("scenes", &self.scenes.len()).finish_non_exhaustive()
506    }
507}
508
509impl Runner {
510    pub(crate) fn new(config: Engine, scene: Box<dyn Scene>) -> Self {
511        let storage = if config.headless {
512            Storage::in_memory()
513        } else {
514            let file = config.save_file.clone().unwrap_or_else(|| crate::storage::default_file(&config.title));
515            match &config.save_secret {
516                Some(secret) => Storage::open_encrypted(file, secret),
517                None => Storage::open(file),
518            }
519        };
520        let lang = load_lang(&config, &storage);
521        let ctx = Context {
522            time: Time::new(),
523            input: Input::default(),
524            audio: if config.headless || config.server {
525                Audio::silent(config.sound_dir.clone())
526            } else {
527                Audio::new(config.sound_dir.clone())
528            },
529            speech: if config.headless || config.server { Speech::console() } else { Speech::new(config.echo_speech) },
530            ui: Ui::default(),
531            physics: {
532                let mut world = World::new();
533                world.set_gravity(config.gravity);
534                world
535            },
536            storage,
537            lang,
538            net: Net::new(config.title.clone()),
539            follow: None,
540            attached: Vec::new(),
541            engine_sounds: Vec::new(),
542            once: HashSet::new(),
543            title: config.title.clone(),
544            quit: false,
545            change: None,
546            globals: HashMap::new(),
547        };
548        Self {
549            ctx,
550            scenes: vec![scene],
551            started: false,
552            accumulator: 0.0,
553            fixed_dt: 1.0 / config.physics_rate as f32,
554            frame_time: Duration::from_secs_f64(1.0 / config.fps as f64),
555            next_frame: Instant::now(),
556        }
557    }
558
559    /// Reports a key going down. `repeat` is `true` for OS key repeats.
560    pub fn key_down(&mut self, key: Key, repeat: bool) {
561        self.ctx.input.push(KeyEvent::Down { key, repeat });
562    }
563
564    /// Reports a key going up.
565    pub fn key_up(&mut self, key: Key) {
566        self.ctx.input.push(KeyEvent::Up { key });
567    }
568
569    /// Reports typed text.
570    pub fn text(&mut self, text: &str) {
571        self.ctx.input.push_text(text);
572    }
573
574    /// Releases all keys (call when the window loses focus).
575    pub fn focus_lost(&mut self) {
576        self.ctx.input.release_all();
577    }
578
579    /// When the next frame is due.
580    pub fn next_frame_time(&self) -> Instant {
581        self.next_frame
582    }
583
584    /// Whether the game wants to quit.
585    pub fn should_quit(&self) -> bool {
586        self.ctx.quit
587    }
588
589    /// Runs one frame now. Returns `false` when the game has quit.
590    pub fn frame(&mut self) -> bool {
591        let now = Instant::now();
592        // Schedule the next frame, but never try to catch up on missed ones.
593        self.next_frame = (self.next_frame + self.frame_time).max(now);
594
595        if !self.started {
596            self.started = true;
597            self.ctx.time.advance(now);
598            if let Some(scene) = self.scenes.last_mut() {
599                scene.start(&mut self.ctx);
600            }
601            self.apply_change();
602            return !self.ctx.quit;
603        }
604
605        let ctx = &mut self.ctx;
606        ctx.time.advance(now);
607        ctx.input.begin_frame(now);
608        ctx.net.update();
609
610        let consumed = ctx.ui.update(&ctx.input, &mut ctx.speech, &mut ctx.audio, &ctx.lang);
611        ctx.input.set_blocked(consumed || ctx.ui.is_open());
612
613        if let Some(scene) = self.scenes.last_mut() {
614            for event in self.ctx.ui.take_events() {
615                scene.on_ui(&event, &mut self.ctx);
616            }
617            if !self.ctx.net.events.is_empty() {
618                // Handed out one by one, then put back so `ctx.net.received(..)` still works.
619                let events = std::mem::take(&mut self.ctx.net.events);
620                for event in &events {
621                    scene.on_net(event, &mut self.ctx);
622                }
623                let added = std::mem::replace(&mut self.ctx.net.events, events);
624                self.ctx.net.events.extend(added);
625            }
626
627            self.ctx.physics.clear_events();
628            self.accumulator += self.ctx.time.delta();
629            let mut steps = 0;
630            while self.accumulator >= self.fixed_dt && steps < 8 {
631                self.accumulator -= self.fixed_dt;
632                self.ctx.physics.step(self.fixed_dt);
633                scene.fixed_update(&mut self.ctx);
634                steps += 1;
635            }
636            if steps == 8 {
637                self.accumulator = 0.0;
638            }
639
640            scene.update(&mut self.ctx);
641        }
642        self.ctx.sync_world();
643        self.ctx.sync_vehicles();
644        self.apply_change();
645
646        if self.ctx.quit {
647            self.shutdown();
648            return false;
649        }
650        true
651    }
652
653    fn apply_change(&mut self) {
654        // A scene's start() may itself request another change, so loop.
655        while let Some(change) = self.ctx.change.take() {
656            match change {
657                SceneChange::Push(mut scene) => {
658                    if let Some(top) = self.scenes.last_mut() {
659                        top.pause(&mut self.ctx);
660                    }
661                    scene.start(&mut self.ctx);
662                    self.scenes.push(scene);
663                }
664                SceneChange::Pop => {
665                    if let Some(mut top) = self.scenes.pop() {
666                        top.end(&mut self.ctx);
667                    }
668                    match self.scenes.last_mut() {
669                        Some(top) => top.resume(&mut self.ctx),
670                        None => self.ctx.quit = true,
671                    }
672                }
673                SceneChange::Switch(mut scene) => {
674                    if let Some(mut top) = self.scenes.pop() {
675                        top.end(&mut self.ctx);
676                    }
677                    scene.start(&mut self.ctx);
678                    self.scenes.push(scene);
679                }
680            }
681        }
682    }
683
684    /// Ends all scenes. Called automatically when the game quits.
685    pub fn shutdown(&mut self) {
686        while let Some(mut scene) = self.scenes.pop() {
687            scene.end(&mut self.ctx);
688        }
689        self.ctx.net.close();
690        if let Err(e) = self.ctx.storage.save() {
691            eprintln!("audio-code: could not save storage: {e}");
692        }
693        self.ctx.quit = true;
694    }
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use std::cell::RefCell;
701    use std::rc::Rc;
702
703    struct Recorder(Rc<RefCell<Vec<String>>>, &'static str);
704
705    impl Scene for Recorder {
706        fn start(&mut self, _: &mut Context) {
707            self.0.borrow_mut().push(format!("{} start", self.1));
708        }
709        fn update(&mut self, ctx: &mut Context) {
710            self.0.borrow_mut().push(format!("{} update", self.1));
711            if self.1 == "a" && ctx.input.pressed(Key::Enter) {
712                ctx.push_scene(Recorder(self.0.clone(), "b"));
713            }
714            if self.1 == "b" && ctx.input.pressed(Key::Escape) {
715                ctx.pop_scene();
716            }
717        }
718        fn pause(&mut self, _: &mut Context) {
719            self.0.borrow_mut().push(format!("{} pause", self.1));
720        }
721        fn resume(&mut self, _: &mut Context) {
722            self.0.borrow_mut().push(format!("{} resume", self.1));
723        }
724        fn end(&mut self, _: &mut Context) {
725            self.0.borrow_mut().push(format!("{} end", self.1));
726        }
727    }
728
729    #[test]
730    fn scene_stack() {
731        let log = Rc::new(RefCell::new(Vec::new()));
732        let mut runner = Engine::new("test").headless().build(Recorder(log.clone(), "a"));
733        runner.frame();
734        runner.key_down(Key::Enter, false);
735        runner.frame();
736        runner.key_up(Key::Enter);
737        runner.key_down(Key::Escape, false);
738        runner.frame();
739        runner.key_up(Key::Escape);
740        runner.key_down(Key::Escape, false);
741        runner.frame();
742        assert_eq!(
743            *log.borrow(),
744            ["a start", "a update", "a pause", "b start", "b update", "b end", "a resume", "a update"]
745        );
746    }
747
748    use crate::net::Packet;
749
750    struct NetLog(Rc<RefCell<Vec<String>>>);
751
752    impl Scene for NetLog {
753        fn update(&mut self, ctx: &mut Context) {
754            for (from, packet) in ctx.net.received("hi") {
755                ctx.net.send(from, Packet::new("echo").with(packet.int(0)));
756            }
757        }
758        fn on_net(&mut self, event: &NetEvent, _: &mut Context) {
759            self.0.borrow_mut().push(format!("{event:?}"));
760        }
761    }
762
763    #[test]
764    fn network_events_reach_scenes() {
765        use crate::net::PeerId;
766        let log = Rc::new(RefCell::new(Vec::new()));
767        let mut server = Engine::new("net").headless().build(NetLog(log.clone()));
768        let mut client = Engine::new("net").headless().build_fn(|_| {});
769        server.frame();
770        client.frame();
771        server.ctx.net.host(0).unwrap();
772        client.ctx.net.join(&format!("127.0.0.1:{}", server.ctx.net.port().unwrap()));
773        let start = Instant::now();
774        let mut echoed = None;
775        while echoed.is_none() || client.ctx.net.ping(PeerId::SERVER) == 0.0 {
776            assert!(start.elapsed() < Duration::from_secs(8), "no answer");
777            server.frame();
778            client.frame();
779            if client.ctx.net.joined() == [PeerId::SERVER] {
780                client.ctx.net.send_all(Packet::new("hi").with(7));
781            }
782            if let Some((_, p)) = client.ctx.net.received("echo").first() {
783                echoed = Some(p.int(0));
784            }
785            std::thread::sleep(Duration::from_millis(5));
786        }
787        assert_eq!(echoed, Some(7));
788        let log = log.borrow();
789        assert_eq!(log[0], "Connected(PeerId(1))");
790        assert!(log[1].starts_with("Message(PeerId(1)"));
791    }
792
793    #[test]
794    fn globals() {
795        let mut runner = Engine::new("test").headless().build(Recorder(Rc::default(), "x"));
796        runner.ctx.insert(5u32);
797        *runner.ctx.get_mut::<u32>().unwrap() += 1;
798        assert_eq!(runner.ctx.get::<u32>(), Some(&6));
799        assert_eq!(*runner.ctx.get_or_default::<String>(), "");
800    }
801
802    #[test]
803    fn start_language() {
804        let runner = Engine::new("test").headless().build(Recorder(Rc::default(), "x"));
805        assert_eq!(runner.ctx.lang.current(), "en");
806        let mut runner = Engine::new("test").headless().language("de").build(Recorder(Rc::default(), "x"));
807        assert_eq!(runner.ctx.lang.current(), "de");
808        assert_eq!(runner.ctx.tr("ui.yes"), "Ja");
809        assert!(runner.ctx.set_language("en-US"));
810        assert_eq!(runner.ctx.storage.get::<String>("language").as_deref(), Some("en"));
811        assert!(!runner.ctx.set_language("xx"));
812    }
813}