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::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
19/// One screen or state of your game: main menu, a level, the game over screen...
20///
21/// Only [`Scene::update`] is required. Scenes are switched with [`Context::switch_scene`],
22/// stacked with [`Context::push_scene`] (e.g. a pause screen) and removed with
23/// [`Context::pop_scene`].
24pub trait Scene: 'static {
25    /// Called once when the scene becomes active for the first time.
26    fn start(&mut self, ctx: &mut Context) {
27        let _ = ctx;
28    }
29
30    /// Called every frame. Put your game logic here.
31    fn update(&mut self, ctx: &mut Context);
32
33    /// Called at a fixed rate (60 times per second by default) right after each physics step.
34    /// Use it for logic that must not depend on the frame rate.
35    fn fixed_update(&mut self, ctx: &mut Context) {
36        let _ = ctx;
37    }
38
39    /// Called for every UI event before [`Scene::update`]. An alternative to polling
40    /// `ctx.ui.clicked(...)` and friends in `update`.
41    fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
42        let _ = (event, ctx);
43    }
44
45    /// Called when another scene is pushed on top of this one.
46    fn pause(&mut self, ctx: &mut Context) {
47        let _ = ctx;
48    }
49
50    /// Called when this scene is on top again after the scene above was popped.
51    fn resume(&mut self, ctx: &mut Context) {
52        let _ = ctx;
53    }
54
55    /// Called when the scene is removed (popped, switched away, or the game quits).
56    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
67/// Everything your game can use, passed to every [`Scene`] method.
68pub struct Context {
69    /// Frame timing and scheduling.
70    pub time: Time,
71    /// Keyboard state.
72    pub input: Input,
73    /// Sound effects, positioned sounds and music.
74    pub audio: Audio,
75    /// Screen reader / text-to-speech output.
76    pub speech: Speech,
77    /// Virtual menus and dialogs.
78    pub ui: Ui,
79    /// Physics world.
80    pub physics: World,
81    /// Values that are saved when the game quits (high scores, settings).
82    pub storage: Storage,
83    /// Translations and the current language.
84    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    /// Requests the game to end after this frame.
103    pub fn quit(&mut self) {
104        self.quit = true;
105    }
106
107    /// Whether [`Context::quit`] was called.
108    pub fn is_quitting(&self) -> bool {
109        self.quit
110    }
111
112    /// The game title given to [`Engine::new`].
113    pub fn title(&self) -> &str {
114        &self.title
115    }
116
117    /// Replaces the current scene with another one after this frame.
118    pub fn switch_scene(&mut self, scene: impl Scene) {
119        self.change = Some(SceneChange::Switch(Box::new(scene)));
120    }
121
122    /// Puts a scene on top of the current one (which is paused) after this frame.
123    pub fn push_scene(&mut self, scene: impl Scene) {
124        self.change = Some(SceneChange::Push(Box::new(scene)));
125    }
126
127    /// Removes the current scene after this frame. Popping the last scene quits the game.
128    pub fn pop_scene(&mut self) {
129        self.change = Some(SceneChange::Pop);
130    }
131
132    /// Stores a value shared by all scenes (settings, score, save data...).
133    /// There is one slot per type; storing again replaces the old value.
134    pub fn insert<T: 'static>(&mut self, value: T) {
135        self.globals.insert(TypeId::of::<T>(), Box::new(value));
136    }
137
138    /// Reads a shared value stored with [`Context::insert`].
139    pub fn get<T: 'static>(&self) -> Option<&T> {
140        self.globals.get(&TypeId::of::<T>()).and_then(|b| b.downcast_ref())
141    }
142
143    /// Mutable access to a shared value.
144    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    /// Mutable access to a shared value, inserting `T::default()` if it is missing.
149    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    /// Removes a shared value and returns it.
158    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/// Configures and starts the game.
164///
165/// ```no_run
166/// use audio_code::prelude::*;
167///
168/// struct Hello;
169///
170/// impl Scene for Hello {
171///     fn start(&mut self, ctx: &mut Context) {
172///         ctx.speech.speak("Hello! Press space to beep, escape to quit.");
173///     }
174///
175///     fn update(&mut self, ctx: &mut Context) {
176///         if ctx.input.pressed(Key::Space) {
177///             ctx.audio.play_tone(440.0, 0.2);
178///         }
179///         if ctx.input.pressed(Key::Escape) {
180///             ctx.quit();
181///         }
182///     }
183/// }
184///
185/// fn main() -> audio_code::Result<()> {
186///     Engine::new("Hello").run(Hello)
187/// }
188/// ```
189#[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    /// Creates a new engine configuration. The title is used for the window and is what the
207    /// screen reader announces when the window gets focus.
208    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    /// Target frames per second (default 60).
226    pub fn fps(mut self, fps: u32) -> Self {
227        self.fps = fps.clamp(1, 1000);
228        self
229    }
230
231    /// Physics and [`Scene::fixed_update`] steps per second (default 60).
232    pub fn physics_rate(mut self, rate: u32) -> Self {
233        self.physics_rate = rate.clamp(1, 1000);
234        self
235    }
236
237    /// Gravity of the physics world, e.g. [`EARTH_GRAVITY`](crate::physics::EARTH_GRAVITY).
238    /// Default is none (top down games). Can be changed later with `ctx.physics.set_gravity`.
239    pub fn gravity(mut self, gravity: glam::Vec3) -> Self {
240        self.gravity = gravity;
241        self
242    }
243
244    /// File used by [`Context::storage`] (default `<title>.save` in the working directory).
245    pub fn save_file(mut self, path: impl Into<PathBuf>) -> Self {
246        self.save_file = Some(path.into());
247        self
248    }
249
250    /// Encrypts the save file of [`Context::storage`] with a key made from `secret`
251    /// (optional, default off). Players can no longer read or edit it, and a changed or
252    /// damaged file is detected and kept as `<file>.broken`. An existing plain text save is
253    /// converted on the next save. See [`crate::storage`] for what this protects against.
254    pub fn encrypt_save(mut self, secret: impl Into<String>) -> Self {
255        self.save_secret = Some(secret.into());
256        self
257    }
258
259    /// Folder searched for sounds (default `"sounds"`). See [`crate::audio`].
260    pub fn sound_dir(mut self, dir: impl Into<PathBuf>) -> Self {
261        self.sound_dir = dir.into();
262        self
263    }
264
265    /// Folder with the translation files `<code>.txt` (default `"lang"`). See
266    /// [`crate::lang`].
267    pub fn lang_dir(mut self, dir: impl Into<PathBuf>) -> Self {
268        self.lang_dir = dir.into();
269        self
270    }
271
272    /// The language the game starts in, e.g. `"de"` (default: the system language if the
273    /// game has texts for it, else English). A language the player chose with
274    /// [`Context::set_language`] still wins.
275    pub fn language(mut self, code: impl Into<String>) -> Self {
276        self.language = Some(code.into());
277        self
278    }
279
280    /// Print everything spoken to the console as well (default `false`).
281    pub fn echo_speech(mut self, echo: bool) -> Self {
282        self.echo_speech = echo;
283        self
284    }
285
286    /// Window size in pixels (default 640×480). The window shows nothing, but sighted
287    /// helpers can still find and focus it.
288    pub fn window_size(mut self, width: u32, height: u32) -> Self {
289        self.window_size = (width, height);
290        self
291    }
292
293    /// Use no audio device and print speech to the console instead of using prism.
294    /// Meant for automated tests and tools.
295    pub fn headless(mut self) -> Self {
296        self.headless = true;
297        self
298    }
299
300    /// Opens the window and runs the game until it quits.
301    pub fn run(self, scene: impl Scene) -> Result<()> {
302        crate::window::run(self, Box::new(scene))
303    }
304
305    /// Like [`Engine::run`], but builds the first scene with access to the [`Context`].
306    /// Handy when the scene needs to create bodies or load sounds in its constructor:
307    ///
308    /// ```no_run
309    /// # use audio_code::prelude::*;
310    /// struct Level { player: BodyId }
311    ///
312    /// impl Level {
313    ///     fn new(ctx: &mut Context) -> Self {
314    ///         Self { player: ctx.physics.add(Body::kinematic(Vec3::ZERO)) }
315    ///     }
316    /// }
317    /// # impl Scene for Level { fn update(&mut self, _: &mut Context) {} }
318    ///
319    /// fn main() -> audio_code::Result<()> {
320    ///     Engine::new("Game").run_with(Level::new)
321    /// }
322    /// ```
323    pub fn run_with<S: Scene>(self, build: impl FnOnce(&mut Context) -> S + 'static) -> Result<()> {
324        self.run(Lazy::new(build))
325    }
326
327    /// Like [`Engine::run`], but uses a native wxWidgets frame (feature `wxdragon`).
328    /// See [`crate::wx`] for embedding a game into an existing wxDragon application.
329    #[cfg(feature = "wxdragon")]
330    pub fn run_wx(self, scene: impl Scene) -> Result<()> {
331        crate::wx::run(self, Box::new(scene))
332    }
333
334    /// Like [`Engine::run_with`] for a wxWidgets frame (feature `wxdragon`).
335    #[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    /// Runs a game written as a single closure that is called every frame.
341    /// The closure can keep its own state in captured variables. See [`crate::run`].
342    pub fn run_fn(self, update: impl FnMut(&mut Context) + 'static) -> Result<()> {
343        self.run(FnScene(update))
344    }
345
346    /// Like [`Engine::build`] for a closure game.
347    pub fn build_fn(self, update: impl FnMut(&mut Context) + 'static) -> Runner {
348        self.build(FnScene(update))
349    }
350
351    /// Creates the [`Context`] and runner without a window. Window backends use this; you
352    /// can use it for tests or to drive the engine from your own loop.
353    pub fn build(self, scene: impl Scene) -> Runner {
354        Runner::new(self, Box::new(scene))
355    }
356}
357
358/// Loads the translation files and picks the start language: the player's saved choice, then
359/// [`Engine::language`], then the system language, then the fallback.
360fn 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); // reports the unknown language
375    }
376    lang
377}
378
379/// A scene made from a closure.
380struct 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
388/// A scene that is built on its first `start` call.
389struct 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
440/// Drives scenes frame by frame. Window backends feed it key events and call
441/// [`Runner::frame`]. You only need this when writing your own backend or tests.
442pub struct Runner {
443    /// The context passed to scenes.
444    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    /// Reports a key going down. `repeat` is `true` for OS key repeats.
509    pub fn key_down(&mut self, key: Key, repeat: bool) {
510        self.ctx.input.push(KeyEvent::Down { key, repeat });
511    }
512
513    /// Reports a key going up.
514    pub fn key_up(&mut self, key: Key) {
515        self.ctx.input.push(KeyEvent::Up { key });
516    }
517
518    /// Reports typed text.
519    pub fn text(&mut self, text: &str) {
520        self.ctx.input.push_text(text);
521    }
522
523    /// Releases all keys (call when the window loses focus).
524    pub fn focus_lost(&mut self) {
525        self.ctx.input.release_all();
526    }
527
528    /// When the next frame is due.
529    pub fn next_frame_time(&self) -> Instant {
530        self.next_frame
531    }
532
533    /// Whether the game wants to quit.
534    pub fn should_quit(&self) -> bool {
535        self.ctx.quit
536    }
537
538    /// Runs one frame now. Returns `false` when the game has quit.
539    pub fn frame(&mut self) -> bool {
540        let now = Instant::now();
541        // Schedule the next frame, but never try to catch up on missed ones.
542        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        // A scene's start() may itself request another change, so loop.
594        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    /// Ends all scenes. Called automatically when the game quits.
624    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}