1use crate::hierarchy;
19use crate::input::Input;
20use crate::physics::Physics;
21use crate::scene::Scene;
22use crate::script::{Phase, ScriptHost, ScriptRuntime, Time};
23
24const FIXED_DT: f32 = 1.0 / 60.0;
25const MAX_FRAME_DT: f32 = 0.1;
26
27#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
29#[serde(default)]
30pub struct RuntimeConfig {
31 pub fixed_dt: f32,
33 pub max_frame_dt: f32,
35 pub max_fixed_steps: u32,
38}
39
40impl Default for RuntimeConfig {
41 fn default() -> Self {
42 Self { fixed_dt: FIXED_DT, max_frame_dt: MAX_FRAME_DT, max_fixed_steps: 8 }
43 }
44}
45
46impl RuntimeConfig {
47 pub fn validate(&self) -> Result<(), String> {
49 if !self.fixed_dt.is_finite() || self.fixed_dt <= 0.0 {
50 return Err("fixed_dt must be finite and positive".into());
51 }
52 if !self.max_frame_dt.is_finite() || self.max_frame_dt <= 0.0 {
53 return Err("max_frame_dt must be finite and positive".into());
54 }
55 if self.max_fixed_steps == 0 {
56 return Err("max_fixed_steps must be positive".into());
57 }
58 Ok(())
59 }
60}
61
62#[derive(Clone, Copy, Debug, Default, PartialEq)]
64pub struct FrameStats {
65 pub fixed_steps: u32,
67 pub dropped_time: f32,
69 pub interpolation_alpha: f32,
72}
73
74pub struct Runtime {
77 pub scene: Scene,
79 pub physics: Physics,
81 pub scripts: ScriptRuntime,
83 pub input: Input,
85 pub time: Time,
87 pub log: Vec<String>,
89 accumulator: f32,
90 config: RuntimeConfig,
91 paused: bool,
92 time_scale: f32,
93 stats: FrameStats,
94}
95
96impl Runtime {
97 pub fn set_bindings<S: AsRef<str>>(&mut self, defs: &[(String, Vec<S>)]) {
99 self.input.set_bindings(defs);
100 }
101
102 pub fn new(scene: Scene, host: Option<ScriptHost>) -> Self {
104 Self::with_config(scene, host, RuntimeConfig::default()).expect("valid default timing")
105 }
106
107 pub fn with_config(mut scene: Scene, host: Option<ScriptHost>, config: RuntimeConfig) -> Result<Self, String> {
110 config.validate()?;
111 hierarchy::update_globals(&mut scene.world);
112 let mut scripts = ScriptRuntime::default();
113 scripts.set_host(host);
114 Ok(Self {
115 scene,
116 physics: Physics::new(),
117 scripts,
118 input: Input::default(),
119 time: Time::default(),
120 log: Vec::new(),
121 accumulator: 0.0,
122 config,
123 paused: false,
124 time_scale: 1.0,
125 stats: FrameStats::default(),
126 })
127 }
128
129 pub fn config(&self) -> &RuntimeConfig {
131 &self.config
132 }
133
134 pub fn is_paused(&self) -> bool {
136 self.paused
137 }
138
139 pub fn set_paused(&mut self, paused: bool) {
141 self.paused = paused;
142 }
143
144 pub fn time_scale(&self) -> f32 {
146 self.time_scale
147 }
148
149 pub fn set_time_scale(&mut self, scale: f32) -> Result<(), String> {
152 if !scale.is_finite() || !(0.0..=100.0).contains(&scale) {
153 return Err("time scale must be finite and between 0 and 100".into());
154 }
155 self.time_scale = scale;
156 Ok(())
157 }
158
159 pub fn frame_stats(&self) -> FrameStats {
161 self.stats
162 }
163
164 pub fn step_once(&mut self) {
167 self.advance(self.config.fixed_dt);
168 }
169
170 pub fn drain_log(&mut self) -> std::vec::Drain<'_, String> {
172 self.log.drain(..)
173 }
174
175 pub fn tick(&mut self, dt: f32) {
178 self.stats = FrameStats::default();
179 if self.paused || self.time_scale == 0.0 || !dt.is_finite() || dt <= 0.0 {
180 return;
181 }
182 self.advance(dt.min(self.config.max_frame_dt) * self.time_scale);
183 }
184
185 fn advance(&mut self, dt: f32) {
186 self.stats = FrameStats::default();
187 self.time.dt = dt;
188 self.time.elapsed += dt;
189 self.time.frame = self.time.frame.saturating_add(1);
190
191 self.input.begin_frame(dt);
192 self.input.canvas.begin();
193 self.scene.signals.begin_frame();
194 self.scripts.sync(&self.scene, &mut self.log);
195 self.scripts.run(Phase::Update, &mut self.scene, &self.physics, &self.input, self.time, &mut self.log);
196 crate::gameplay::update_lifetimes(&mut self.scene.world, dt);
197
198 let sky = &mut self.scene.settings.atmosphere;
199 if sky.speed != 0.0 {
200 sky.time = (sky.time + sky.speed * dt).rem_euclid(24.0);
201 }
202 self.update_autofocus(dt);
203 crate::interaction::update(&mut self.scene.world, dt);
204 crate::modifiers::update(&mut self.scene.world, self.time.elapsed, dt);
205 crate::tween::update(&mut self.scene.world, dt);
206 crate::animation::update(&mut self.scene.world, dt);
207 crate::neural::update(&mut self.scene.world, dt);
208 crate::procedural::update(&mut self.scene.world, self.time.elapsed, dt, true);
209
210 self.accumulator += dt;
211 while self.accumulator >= self.config.fixed_dt && self.stats.fixed_steps < self.config.max_fixed_steps {
212 self.accumulator -= self.config.fixed_dt;
213 self.scripts.run(
214 Phase::Fixed,
215 &mut self.scene,
216 &self.physics,
217 &self.input,
218 Time { dt: self.config.fixed_dt, ..self.time },
219 &mut self.log,
220 );
221 crate::water::apply_buoyancy(&mut self.scene.world, self.time.elapsed, self.config.fixed_dt);
222 self.physics.step(&mut self.scene, self.config.fixed_dt);
223 self.stats.fixed_steps += 1;
224 }
225 let physics = &self.physics;
227 let ground = |from: glam::Vec3, len: f32, me: kvas_ecs::Entity| {
228 let opts = crate::physics::QueryOptions { exclude: Some(me), filter: !crate::ragdoll::RAGDOLL_GROUP, ..Default::default() };
229 physics.raycast_with_options(from, glam::Vec3::NEG_Y, len, opts).map(|h| h.point.y)
230 };
231 crate::skeletal::update_with(&mut self.scene.world, self.time.elapsed, dt, true, Some(&ground));
232 if self.accumulator >= self.config.fixed_dt {
233 let remainder = self.accumulator % self.config.fixed_dt;
234 self.stats.dropped_time = self.accumulator - remainder;
235 self.accumulator = remainder;
236 }
237 self.stats.interpolation_alpha = self.accumulator / self.config.fixed_dt;
238 hierarchy::update_globals(&mut self.scene.world);
239 }
240}
241
242impl Runtime {
243 pub fn apply_scene_request(&mut self, reg: &crate::registry::Registry, read: impl Fn(&str) -> Result<String, String>) -> Option<Result<String, String>> {
247 let request = self.input.take_scene_request()?;
248 let current = self.input.scene_path();
249 Some(match request {
250 crate::input::SceneRequest::Load(path) => self.load_scene_text(reg, &path, read(&path)),
251 crate::input::SceneRequest::Reload if current.is_empty() => Err("reload_scene: the host did not say which scene is running".into()),
252 crate::input::SceneRequest::Reload => self.load_scene_text(reg, ¤t, read(¤t)),
253 crate::input::SceneRequest::Add(path) => read(&path).and_then(|text| {
254 let warnings = self.scene.add_json(reg, &text)?;
255 self.log.extend(warnings);
256 Ok(path)
257 }),
258 })
259 }
260
261 fn load_scene_text(&mut self, reg: &crate::registry::Registry, path: &str, text: Result<String, String>) -> Result<String, String> {
262 let (scene, warnings) = Scene::from_json(reg, &text.map_err(|e| format!("{path}: {e}"))?).map_err(|e| format!("{path}: {e}"))?;
263 self.replace_scene(scene);
264 self.log.extend(warnings);
265 self.input.set_scene_path(path);
266 Ok(path.to_string())
267 }
268
269 pub fn replace_scene(&mut self, mut scene: Scene) {
272 hierarchy::update_globals(&mut scene.world);
273 let volume = self.scene.audio.master_volume;
274 self.scripts.restart_instances();
275 self.physics = Physics::new();
276 self.scene = scene;
277 self.scene.audio.master_volume = volume;
278 self.accumulator = 0.0;
279 }
280
281 fn update_autofocus(&mut self, dt: f32) {
283 let world = &self.scene.world;
284 let Some((entity, camera)) = world.iter::<crate::components::Camera>().find(|(_, c)| c.primary && c.autofocus && c.dof).map(|(e, c)| (e, c.clone())) else { return };
285 let m = hierarchy::global_matrix(world, entity);
286 let (origin, forward) = (m.w_axis.truncate(), -m.z_axis.truncate().normalize_or_zero());
287 let target = self.physics.raycast(origin + forward * camera.near, forward, camera.far.min(300.0)).map_or(camera.far.min(60.0), |h| h.distance + camera.near);
288 if let Some(c) = self.scene.world.get_mut::<crate::components::Camera>(entity) {
289 let k = 1.0 - (-camera.focus_speed.max(0.1) * dt).exp();
290 c.focus_distance += (target - c.focus_distance) * k;
291 }
292 }
293}
294
295impl Drop for Runtime {
296 fn drop(&mut self) {
297 self.scripts.set_host(None);
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::input::SceneRequest;
306 use crate::registry::Registry;
307
308 #[test]
309 fn scripts_can_switch_add_and_reload_scenes() {
310 let reg = Registry::default();
311 let mut a = Scene::default();
312 a.spawn("Start");
313 let mut b = Scene::default();
314 b.spawn("Level 2");
315 let files = [("a.scene.json", a.to_json(®)), ("b.scene.json", b.to_json(®))];
316 let read = |p: &str| files.iter().find(|f| f.0 == p).map(|f| f.1.clone()).ok_or_else(|| format!("missing {p}"));
317 let mut rt = Runtime::new(Scene::from_json(®, &files[0].1).unwrap().0, None);
318 assert!(rt.apply_scene_request(®, read).is_none(), "nothing asked");
319 rt.input.request_scene(SceneRequest::Reload);
320 assert!(rt.apply_scene_request(®, read).unwrap().is_err(), "the host did not say which scene runs");
321 rt.input.set_scene_path("a.scene.json");
322 rt.input.request_scene(SceneRequest::Load("b.scene.json".into()));
323 assert_eq!(rt.apply_scene_request(®, read).unwrap().unwrap(), "b.scene.json");
324 assert!(rt.scene.find("Level 2").is_some() && rt.scene.find("Start").is_none());
325 assert_eq!(rt.input.scene_path(), "b.scene.json");
326 rt.input.request_scene(SceneRequest::Add("a.scene.json".into()));
327 rt.apply_scene_request(®, read).unwrap().unwrap();
328 assert!(rt.scene.find("Level 2").is_some() && rt.scene.find("Start").is_some());
329 rt.input.request_scene(SceneRequest::Reload);
330 rt.apply_scene_request(®, read).unwrap().unwrap();
331 assert!(rt.scene.find("Start").is_none(), "reload restores b only");
332 rt.input.request_scene(SceneRequest::Load("missing.scene.json".into()));
333 assert!(rt.apply_scene_request(®, read).unwrap().is_err());
334 assert!(rt.scene.find("Level 2").is_some(), "a failed load keeps the running scene");
335 }
336}