Skip to main content

furmint_runtime/
app.rs

1use std::collections::HashMap;
2use std::fs;
3use std::fs::File;
4use std::path::PathBuf;
5use std::sync::mpsc::Sender;
6use std::time::{Duration, Instant};
7
8use log::{debug, error, info, warn};
9
10use crate::error::{RuntimeError, RuntimeResult};
11use crate::events::{AppEvent, AppEventReceivingSystem, Events};
12use crate::plugins::{Plugin, PluginBuildContext, PluginUpdateContext};
13use crate::scenes::{
14    ActiveScene, Scene, SceneCommand, SceneCommands, SceneLibrary, SceneLoader, SceneSystem,
15};
16use crate::time;
17use crate::time::Time;
18use furmint_registry::{ComponentFactory, ComponentRegistry};
19use furmint_resources::assets::{AssetServer, Handle};
20use serde::{Deserialize, Serialize};
21use specs::{Component, Dispatcher, DispatcherBuilder, World, WorldExt};
22
23/// Alias for system registration closure
24pub type SystemRegistration = Box<dyn FnOnce(&mut DispatcherBuilder<'static, 'static>) + Send>;
25
26/// The application struct which contains [`World`] and `specs`
27/// `Dispatcher`
28pub struct App {
29    /// ECS world
30    world: World,
31    /// ECS dispatchers, keyed by stage
32    dispatchers: HashMap<Stage, Dispatcher<'static, 'static>>,
33    /// App plugins
34    plugins: Vec<Box<dyn Plugin + 'static + Send + Sync>>,
35    /// Is the app running?
36    running: bool,
37}
38
39/// Builder for [`App`]
40#[derive(Default)]
41pub struct AppBuilder {
42    config: &'static str,
43    world: World,
44    component_registry: ComponentRegistry,
45    dispatcher_builders: HashMap<Stage, DispatcherBuilder<'static, 'static>>,
46    dispatcher_config: HashMap<Stage, Vec<SystemRegistration>>,
47    plugins: Vec<Box<dyn Plugin + Send + Sync>>,
48}
49
50/// Execution stages for systems in a frame
51///
52/// Systems are grouped by stage and run in this order:
53/// `PreUpdate` -> `Update` -> `PostUpdate` -> `Runtime`
54#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
55pub enum Stage {
56    /// Runs before game logic. Use for input handling and preparation
57    PreUpdate,
58    /// Main game logic. Most user systems should be here
59    Update,
60    /// Runs after game logic. Use for cleanup or derived updates
61    PostUpdate,
62    /// Internal engine systems (e.g. scene management)`
63    Runtime,
64}
65
66impl Stage {
67    /// Order of stages
68    pub const ORDER: [Stage; 4] = [
69        Stage::PreUpdate,
70        Stage::Update,
71        Stage::PostUpdate,
72        Stage::Runtime,
73    ];
74}
75
76/// Application config defined by user
77#[derive(Deserialize, Serialize, Debug)]
78struct Config {
79    /// Path to assets directory
80    assets_path: String,
81    /// First scene to display
82    initial_scene: String,
83    /// ECS system dispatch tick rate
84    tick_period: u64,
85}
86
87impl App {
88    /// Runs the event loop. Must be called only once.
89    pub fn run(mut self) -> RuntimeResult<()> {
90        self.running = true;
91        while self.running {
92            self.run_frame()?;
93        }
94        Ok(())
95    }
96
97    fn run_frame(&mut self) -> RuntimeResult<()> {
98        let frame_start = Instant::now();
99
100        self.handle_app_events();
101        self.dispatch_stage(Stage::PreUpdate);
102        self.dispatch_stage(Stage::Update);
103        self.dispatch_stage(Stage::PostUpdate);
104
105        self.handle_scene_commands();
106        self.dispatch_stage(Stage::Runtime);
107
108        self.world.maintain();
109
110        let dt = self.finish_tick(frame_start.elapsed());
111
112        self.update_plugins(dt);
113
114        Ok(())
115    }
116
117    fn handle_app_events(&mut self) {
118        let mut receiver = self.world.write_resource::<Events<AppEvent>>();
119        receiver.update();
120        for ev in receiver.iter() {
121            debug!("app event: {:?}", ev);
122            match ev {
123                AppEvent::ExitRequested => {
124                    info!("got ExitRequested, exiting app");
125                    self.running = false;
126                }
127            }
128        }
129    }
130
131    fn dispatch_stage(&mut self, stage: Stage) {
132        self.dispatchers
133            .get_mut(&stage)
134            .expect("stage dispatcher missing")
135            .dispatch(&self.world);
136    }
137
138    fn update_plugins(&mut self, dt_seconds: f64) {
139        let mut ctx = PluginUpdateContext::new(dt_seconds, &mut self.world);
140        for plugin in &mut self.plugins {
141            if let Err(e) = plugin.update(&mut ctx) {
142                error!("failed to update plugin: {e}");
143            }
144        }
145    }
146
147    fn finish_tick(&mut self, elapsed: Duration) -> f64 {
148        let elapsed_nanos = elapsed.as_nanos() as u64;
149        let mut time = self.world.write_resource::<Time>();
150
151        let frame_nanos = if elapsed_nanos < time.tick_period {
152            let remaining = time.tick_period - elapsed_nanos;
153            std::thread::sleep(Duration::from_nanos(remaining));
154            time.tick_period
155        } else {
156            warn!(
157                "cannot keep up, tick took {} > {} nanoseconds",
158                elapsed_nanos, time.tick_period
159            );
160            elapsed_nanos
161        };
162
163        let dt = frame_nanos as f64 / time::NANOS_PER_SECOND;
164        time.ticks += 1;
165        time.delta_seconds = dt;
166        time.total_elapsed_time += dt;
167
168        dt
169    }
170
171    fn handle_scene_commands(&mut self) {
172        let commands = {
173            let mut scene_commands = self.world.write_resource::<SceneCommands>();
174            scene_commands.drain().collect::<Vec<_>>()
175        };
176
177        for command in commands {
178            match command {
179                SceneCommand::SwitchTo(new_scene) => {
180                    let old_entities = {
181                        let mut active_scene = self.world.write_resource::<ActiveScene>();
182                        std::mem::take(&mut active_scene.entities)
183                    };
184                    for entity in old_entities {
185                        let _ = self.world.delete_entity(entity);
186                    }
187                    let mut active_scene = self.world.write_resource::<ActiveScene>();
188                    active_scene.set(new_scene);
189                }
190                _ => todo!(),
191            }
192        }
193    }
194}
195
196impl AppBuilder {
197    /// Create a new [`AppBuilder`]
198    pub fn new() -> AppBuilder {
199        let mut dispatcher_builders = HashMap::new();
200        let mut dispatcher_config = HashMap::new();
201
202        for stage in Stage::ORDER {
203            dispatcher_builders.insert(stage, DispatcherBuilder::new());
204            dispatcher_config.insert(stage, Vec::new());
205        }
206
207        Self {
208            config: "",
209            world: World::new(),
210            component_registry: ComponentRegistry::new(),
211            dispatcher_builders,
212            dispatcher_config,
213            plugins: Vec::new(),
214        }
215    }
216
217    /// Set the application config
218    pub fn with_config(mut self, path: &'static str) -> AppBuilder {
219        self.config = path;
220        self
221    }
222
223    /// Register a system in the default gameplay stage ([`Stage::Update`])
224    pub fn with_system<S>(self, sys: S) -> Self
225    where
226        for<'a> S: specs::System<'a> + Send + 'static,
227    {
228        self.with_system_in_stage(Stage::Update, sys)
229    }
230
231    /// Register a system in a specific stage
232    pub fn with_system_in_stage<S>(mut self, stage: Stage, sys: S) -> Self
233    where
234        for<'a> S: specs::System<'a> + Send + 'static,
235    {
236        push_system_registration(
237            self.dispatcher_config
238                .get_mut(&stage)
239                .expect("stage config missing"),
240            sys,
241            &[],
242        );
243
244        self
245    }
246
247    /// Register a system with dependencies in the default gameplay stage
248    pub fn with_system_deps<S>(self, sys: S, deps: &'static [&'static str]) -> Self
249    where
250        for<'a> S: specs::System<'a> + Send + 'static,
251    {
252        self.with_system_deps_in_stage(Stage::Update, sys, deps)
253    }
254
255    /// Register a system with dependencies in a specific stage
256    pub fn with_system_deps_in_stage<S>(
257        mut self,
258        stage: Stage,
259        sys: S,
260        deps: &'static [&'static str],
261    ) -> Self
262    where
263        for<'a> S: specs::System<'a> + Send + 'static,
264    {
265        push_system_registration(
266            self.dispatcher_config
267                .get_mut(&stage)
268                .expect("stage config missing"),
269            sys,
270            deps,
271        );
272
273        self
274    }
275
276    /// Register a component in the ECS
277    pub fn with_component<C: Component, F: ComponentFactory + Default + 'static>(
278        mut self,
279    ) -> AppBuilder
280    where
281        <C as Component>::Storage: Default,
282    {
283        self.world.register::<C>();
284        self.component_registry
285            .register(F::name(), Box::new(F::default()));
286        self
287    }
288
289    /// Register a plugin
290    pub fn with_plugin<C: Plugin + 'static + Send + Sync>(mut self, plugin: C) -> AppBuilder {
291        self.plugins.push(Box::new(plugin));
292        self
293    }
294
295    /// Builds an [`App`]
296    pub fn build(mut self) -> RuntimeResult<App> {
297        if self.config.is_empty() {
298            return Err(RuntimeError::EmptyConfigPath);
299        }
300
301        let config_reader =
302            fs::read(self.config).map_err(|source| RuntimeError::ConfigIo { source })?;
303
304        let config: Config = toml::from_slice(&config_reader)?;
305
306        let mut asset_server = AssetServer::with_root(config.assets_path.clone());
307        asset_server.register_loader::<Scene>(Box::new(SceneLoader));
308
309        self.world.register::<Handle<Scene>>();
310
311        self.world.insert(Time {
312            ticks: 0,
313            tick_period: config.tick_period,
314            total_elapsed_time: 0.0,
315            delta_seconds: 0.0,
316        });
317
318        self.world.insert(SceneCommands::default());
319
320        self.load_scenes(&config, &mut asset_server)?;
321
322        self.world.insert(ActiveScene {
323            name: Some(config.initial_scene),
324            loaded: false,
325            entities: Vec::new(),
326        });
327
328        self.world.insert(asset_server);
329        self.world.insert(Events::<AppEvent>::default());
330
331        self.register_internal_systems();
332        self.build_plugins()?;
333        self.build_dispatcher_configs();
334
335        let dispatchers = self.build_dispatchers();
336        let sender = self.world.write_resource::<Sender<AppEvent>>().clone();
337        ctrlc::set_handler(move || {
338            sender
339                .send(AppEvent::ExitRequested)
340                .expect("failed to send exit request message");
341        })
342        .expect("error setting Ctrl-C handler");
343
344        self.world.insert(self.component_registry);
345        // systems shouldn't have access to this, and it already got passed to plugins by this point
346        self.world.remove::<Sender<AppEvent>>();
347
348        Ok(App {
349            world: self.world,
350            dispatchers,
351            plugins: self.plugins,
352            running: false,
353        })
354    }
355
356    fn load_scenes(
357        &mut self,
358        config: &Config,
359        asset_server: &mut AssetServer,
360    ) -> RuntimeResult<()> {
361        let scenes_path = PathBuf::from(&config.assets_path).join("scenes");
362
363        debug!("config.assets_path={}", config.assets_path);
364        debug!("scenes_path={}", scenes_path.display());
365
366        let mut library = SceneLibrary::default();
367
368        for entry in fs::read_dir(scenes_path)? {
369            let entry = entry?;
370            let mut reader = File::open(entry.path())?;
371
372            let scene = asset_server.load_reader::<Scene>(
373                entry
374                    .file_name()
375                    .to_str()
376                    .expect("failed to convert scene path to str"),
377                &mut reader,
378            )?;
379            let name = scene.read().name.clone();
380            library.scenes.insert(name.clone(), scene);
381        }
382
383        self.world.insert(library);
384
385        Ok(())
386    }
387
388    fn build_plugins(&mut self) -> RuntimeResult<()> {
389        for plugin in self.plugins.iter_mut() {
390            let event_sender = self.world.write_resource::<Sender<AppEvent>>().clone();
391            let mut ctx = PluginBuildContext {
392                world: &mut self.world,
393                dispatcher_config: &mut self.dispatcher_config,
394                component_registry: &mut self.component_registry,
395                event_sender,
396            };
397
398            plugin.build(&mut ctx).map_err(|source| {
399                error!("failed to build plugin `{}`: {}", plugin.name(), source);
400
401                RuntimeError::PluginSetup {
402                    plugin: plugin.name().to_string(),
403                    source,
404                }
405            })?;
406
407            debug!("registered plugin {}", plugin.name());
408        }
409
410        Ok(())
411    }
412
413    fn register_internal_systems(&mut self) {
414        let (sender, receiver) = std::sync::mpsc::channel::<AppEvent>();
415        self.world.insert(sender);
416
417        let runtime = self
418            .dispatcher_builders
419            .get_mut(&Stage::Runtime)
420            .expect("runtime dispatcher builder missing");
421
422        runtime.add(SceneSystem, "furmint.scene_system", &[]);
423
424        let pre_update = self
425            .dispatcher_builders
426            .get_mut(&Stage::PreUpdate)
427            .expect("pre-update dispatcher builder missing");
428        pre_update.add(
429            AppEventReceivingSystem::new(receiver),
430            "furmint.app_event_receiving",
431            &[],
432        );
433    }
434
435    fn build_dispatcher_configs(&mut self) {
436        for stage in Stage::ORDER {
437            let builder = self
438                .dispatcher_builders
439                .get_mut(&stage)
440                .expect("dispatcher builder missing");
441
442            let regs = self
443                .dispatcher_config
444                .get_mut(&stage)
445                .expect("dispatcher config missing");
446
447            for reg in regs.drain(..) {
448                reg(builder);
449            }
450
451            #[cfg(debug_assertions)]
452            {
453                debug!("system graph for stage {:?}:", stage);
454                builder.print_par_seq();
455            }
456        }
457    }
458
459    fn build_dispatchers(&mut self) -> HashMap<Stage, Dispatcher<'static, 'static>> {
460        let mut dispatchers = HashMap::new();
461
462        for stage in Stage::ORDER {
463            let builder = self
464                .dispatcher_builders
465                .remove(&stage)
466                .expect("dispatcher builder missing");
467
468            dispatchers.insert(stage, builder.build());
469        }
470
471        dispatchers
472    }
473}
474
475pub(crate) fn push_system_registration<S>(
476    config: &mut Vec<SystemRegistration>,
477    sys: S,
478    deps: &'static [&'static str],
479) where
480    for<'a> S: specs::System<'a> + Send + 'static,
481{
482    config.push(Box::new(move |builder| {
483        let name = std::any::type_name::<S>();
484        builder.add(sys, name, deps);
485    }));
486}