1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! The application: a world plus the loop that runs it.
use alloc::boxed::Box;
#[cfg(feature = "std")]
use std::path::Path;
use concinnity_core::Driver;
use crate::{Error, World, driver};
/// A runnable application.
///
/// Built either from a [`World`] assembled in process, or from a world already
/// compiled into a blob file.
///
/// ```no_run
/// # use concinnity::{App, World};
/// App::from_world(World::new()).run().expect("the app runs");
/// ```
pub struct App {
inner: Box<dyn Driver>,
}
impl core::fmt::Debug for App {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("App").finish_non_exhaustive()
}
}
impl App {
/// An app that runs `world`.
pub fn from_world(world: World) -> Self {
Self {
inner: driver::select(world.into_inner()),
}
}
/// An app that runs the world compiled into the blob file at `path`, as
/// written by the `cook` module. Overflow payload blobs are that
/// file's siblings named by index, so a world written to `data/0` reads
/// `data/1`, `data/2`, ... beside it.
///
/// What the app writes at runtime -- its settings, save files, crash
/// reports, and shader caches -- lands beside the world it read, stepping
/// out of a directory named `data`, so `from_blob("mygame/data/0")` keeps
/// them under `mygame/`. A world that declares an `AppConfig` with a `home`
/// chooses the location itself.
///
/// Loading and running report the same [`Error`], so one `?` carries both.
///
/// ```no_run
/// # use concinnity::{App, Error};
/// # fn main() -> Result<(), Error> {
/// App::from_blob("data/0")?.run()
/// # }
/// ```
#[cfg(feature = "std")]
pub fn from_blob(path: impl AsRef<Path>) -> Result<Self, Error> {
concinnity_engine::App::from_blob(path.as_ref())
.map(|app| Self {
inner: driver::adopt(app),
})
.map_err(crate::error::from_startup)
}
/// The same app on the headless loop: the simulation systems stepped on a
/// fixed virtual timestep, with no window and no renderer. A world that
/// declares a `GraphicsConfig` keeps it and draws nothing, which is what
/// lets a test or a simulation-only tool run a world authored to be seen.
///
/// The `no_std` build has no other loop to run, so there it changes
/// nothing.
///
/// ```no_run
/// # use concinnity::{App, World};
/// App::from_world(World::new())
/// .into_headless()
/// .run()
/// .expect("the app runs");
/// ```
pub fn into_headless(self) -> Self {
Self {
inner: driver::headless(self.inner.into_world()),
}
}
/// Run the app until its window closes, a system stops the world, its last
/// system finishes, or the process is interrupted.
///
/// A headless run has no window to close and no clock to follow: the world
/// steps on a fixed virtual timestep, as fast as the host can step it.
pub fn run(self) -> Result<(), Error> {
self.inner.run().map_err(Error::from)
}
#[cfg(test)]
pub(crate) fn inner_mut(&mut self) -> &mut dyn Driver {
&mut *self.inner
}
}
#[cfg(all(test, feature = "cook"))]
mod tests {
use super::App;
use crate::components::DirectionalLight;
use crate::cook;
// The ahead-of-time pair, end to end: what `write_blob` puts on disk is
// what `from_blob` reads back, with no state-tree anchor in between.
#[test]
fn a_written_blob_loads_back_into_a_runnable_app() {
let tree = concinnity_testing::TempTree::new();
let primary = tree.join("data/0");
cook::world()
.add(
"sun",
DirectionalLight {
intensity: 3.5,
..Default::default()
},
)
.write_blob(&primary)
.expect("the world is written");
let app = App::from_blob(&primary).expect("the written blob loads");
crate::test_support::assert_starts_headless(app);
}
// A path with no blob behind it is an error naming the file, not a panic
// and not an empty world that fails later.
#[test]
fn a_missing_blob_reports_the_path_it_could_not_read() {
let tree = concinnity_testing::TempTree::new();
let missing = tree.join("concinnity-no-such-blob/0");
let err = App::from_blob(&missing).expect_err("nothing to load");
assert!(matches!(err, crate::Error::MissingData { .. }), "{err:?}");
assert!(err.to_string().contains("concinnity-no-such-blob"), "{err}");
}
}