1use std::sync::mpsc::channel;
2
3use rapier2d::prelude::*;
4use serde::{Deserialize, Serialize};
5
6use crate::nav::navigation::NavigationSystem;
7use crate::nav::spatial::SpatialGrid;
8use crate::config::{EngineConfig, PLAYER_STATE_LEN};
9use crate::events::CoreEvent;
10use crate::map::{GameMap, MapConfig};
11use crate::rng::Rng;
12use crate::sim::{GameDef, GameSim, SimCtx};
13use crate::snapshot::Block;
14
15const MAX_ACCUMULATED_TIME: f32 = 0.1;
17
18pub struct EngineSim<G: GameDef> {
26 pub cfg: EngineConfig,
27 pub world: PhysicsWorld,
28 pub map: Option<GameMap>,
29 pub nav: Option<NavigationSystem>,
30 pub spatial: SpatialGrid,
31 pub rng: Rng,
32
33 time_step: f32,
34 accumulator: f32,
35
36 pub events: Vec<CoreEvent>,
38
39 bodies_to_destroy: Vec<RigidBodyHandle>,
41
42 last_body_has_events: bool,
45
46 pub sim: G::Sim,
47}
48
49impl<G: GameDef> EngineSim<G> {
50 pub fn new(cfg: EngineConfig, game_cfg: &G::Config) -> Self {
51 let time_step = cfg.time_step;
52
53 let mut world = PhysicsWorld::new();
54
55 world.gravity = Vector::ZERO;
56 world.integration_parameters.dt = time_step;
57
58 let seed = cfg.seed;
59 let sim = G::Sim::new(game_cfg, &cfg);
60
61 Self {
62 world,
63 map: None,
64 nav: None,
65 spatial: SpatialGrid::new(600.0),
66 rng: Rng::new(seed),
67 time_step,
68 accumulator: 0.0,
69 events: Vec::new(),
70 bodies_to_destroy: Vec::new(),
71 last_body_has_events: false,
72 sim,
73 cfg,
74 }
75 }
76
77 pub fn load_map(&mut self, json: &str) -> Result<(), String> {
81 let map_cfg: MapConfig = serde_json::from_str(json).map_err(|e| format!("bad map json: {e}"))?;
82
83 if let Some(mut old) = self.map.take() {
84 old.destroy(&mut self.world);
85 }
86
87 let map = GameMap::create(&mut self.world, &map_cfg, self.cfg.map_scale, &self.cfg.map_set_id);
88
89 self.nav = Some(NavigationSystem::generate(&map.grid, &map.physics_static, map.step));
90 self.map = Some(map);
91
92 Ok(())
93 }
94
95 pub fn map_info_json(&self) -> String {
98 let Some(map) = &self.map else {
99 return "null".to_string();
100 };
101
102 let width = map.grid.first().map(|row| row.len()).unwrap_or(0) as f32 * map.step;
103 let height = map.grid.len() as f32 * map.step;
104
105 serde_json::json!({
106 "setId": map.set_id,
107 "step": map.step,
108 "width": width,
109 "height": height,
110 "respawns": map.respawns,
111 })
112 .to_string()
113 }
114
115 pub fn spawn_actor(&mut self, game_id: u32, model_name: &str, team_id: u8, x: f32, y: f32, angle_deg: f32) -> Result<(), String> {
118 self.sim
119 .spawn_actor(&mut self.world, &mut self.events, game_id, model_name, team_id, x, y, angle_deg)
120 }
121
122 pub fn remove_actor(&mut self, game_id: u32) {
123 self.sim.remove_actor(&mut self.world, game_id);
124 }
125
126 pub fn reset_actor(&mut self, game_id: u32, team_id: u8, x: f32, y: f32, angle_deg: f32) {
127 self.sim.reset_actor(&mut self.world, game_id, team_id, x, y, angle_deg);
128 }
129
130 pub fn reset_all_vitals(&mut self) {
131 self.sim.reset_all_vitals(&mut self.events);
132 }
133
134 pub fn spawn_scripted_actor(&mut self, game_id: u32, model_name: &str, team_id: u8, x: f32, y: f32, angle_deg: f32) -> Result<(), String> {
135 self.sim.spawn_scripted_actor(
136 &mut self.world,
137 &mut self.rng,
138 &mut self.events,
139 game_id,
140 model_name,
141 team_id,
142 x,
143 y,
144 angle_deg,
145 )
146 }
147
148 pub fn remove_scripted_actor(&mut self, game_id: u32) {
149 self.sim.remove_scripted_actor(&mut self.world, game_id);
150 }
151
152 pub fn apply_input(&mut self, game_id: u32, seq: u32, action: &str, key_name: &str) {
155 self.sim.apply_input(game_id, seq, action, key_name);
156 }
157
158 pub fn apply_aim(&mut self, game_id: u32, seq: u32, x: f32, y: f32, flags: u32) {
160 self.sim.apply_aim(game_id, seq, x, y, flags);
161 }
162
163 pub fn last_input_seq(&self, game_id: u32) -> u32 {
164 self.sim.last_input_seq(game_id)
165 }
166
167 pub fn take_events_json(&mut self) -> String {
170 let events: Vec<CoreEvent> = self.events.drain(..).collect();
171
172 serde_json::to_string(&events).unwrap_or_else(|_| "[]".to_string())
173 }
174
175 pub fn is_alive(&self, game_id: u32) -> bool {
178 self.sim.is_alive(game_id)
179 }
180
181 pub fn actor_position(&self, game_id: u32) -> Option<[f32; 2]> {
182 self.sim.actor_position(&self.world, game_id)
183 }
184
185 pub fn prediction_state(&self, game_id: u32) -> Option<([f32; PLAYER_STATE_LEN], bool)> {
186 self.sim.prediction_state(&self.world, game_id)
187 }
188
189 pub fn alive_players_flat(&self) -> Vec<f32> {
190 self.sim.alive_players_flat(&self.world)
191 }
192
193 pub fn players_json(&self) -> String {
196 self.sim.players_json()
197 }
198
199 pub fn step(&mut self, dt: f32) {
204 self.accumulator = (self.accumulator + dt).min(MAX_ACCUMULATED_TIME);
205
206 while self.accumulator >= self.time_step {
207 self.step_fixed();
208 self.accumulator -= self.time_step;
209 }
210
211 self.sim.refresh_cached(&self.world);
212
213 let mut ctx = SimCtx {
214 world: &mut self.world,
215 cfg: &self.cfg,
216 map: &self.map,
217 nav: &self.nav,
218 spatial: &mut self.spatial,
219 rng: &mut self.rng,
220 events: &mut self.events,
221 bodies_to_destroy: &mut self.bodies_to_destroy,
222 };
223
224 self.sim.on_ai_tick(&mut ctx, dt);
225 }
226
227 fn step_fixed(&mut self) {
229 let mut ctx = SimCtx {
230 world: &mut self.world,
231 cfg: &self.cfg,
232 map: &self.map,
233 nav: &self.nav,
234 spatial: &mut self.spatial,
235 rng: &mut self.rng,
236 events: &mut self.events,
237 bodies_to_destroy: &mut self.bodies_to_destroy,
238 };
239
240 self.sim.on_fixed_step(&mut ctx, self.time_step);
241
242 let (collision_send, collision_recv) = channel();
244 let (force_send, _force_recv) = channel();
245 let collector = ChannelEventCollector::new(collision_send, force_send);
246
247 self.world.step_with_events(&(), &collector);
248
249 let mut contacts = Vec::new();
250
251 while let Ok(event) = collision_recv.try_recv() {
252 if let CollisionEvent::Started(h1, h2, _) = event {
253 contacts.push((h1, h2));
254 }
255 }
256
257 let mut ctx = SimCtx {
258 world: &mut self.world,
259 cfg: &self.cfg,
260 map: &self.map,
261 nav: &self.nav,
262 spatial: &mut self.spatial,
263 rng: &mut self.rng,
264 events: &mut self.events,
265 bodies_to_destroy: &mut self.bodies_to_destroy,
266 };
267
268 self.sim.on_contacts(&mut ctx, &contacts);
269 self.destroy_queued_bodies();
270 }
271
272 fn destroy_queued_bodies(&mut self) {
274 if self.bodies_to_destroy.is_empty() {
275 return;
276 }
277
278 let handles: Vec<RigidBodyHandle> = self.bodies_to_destroy.drain(..).collect();
279
280 for handle in handles {
281 self.sim.on_before_destroy(&self.world, handle);
282 self.world.remove_body(handle);
283 }
284 }
285
286 pub fn build_snapshot_blocks(&mut self) -> Vec<(String, Block)> {
291 let (mut blocks, has_events) = self.sim.build_snapshot_blocks();
292
293 if let Some(map) = &self.map {
297 let with_velocities = self
298 .cfg
299 .snapshot
300 .keys
301 .get(&map.set_id)
302 .is_some_and(|schema| schema.optional_from.is_some());
303
304 blocks.push((
305 map.set_id.clone(),
306 Block::IndexedNoNull8(map.dynamic_map_data(&self.world, with_velocities)),
307 ));
308 }
309
310 self.last_body_has_events = has_events;
311
312 blocks
313 }
314
315 pub fn body_has_events(&self) -> bool {
318 self.last_body_has_events
319 }
320
321 pub fn remove_players_and_shots(&mut self) -> Vec<String> {
326 self.sim.remove_players_and_shots(&mut self.world)
327 }
328
329 pub fn clear(&mut self) {
331 if let Some(mut map) = self.map.take() {
334 map.destroy(&mut self.world);
335 }
336
337 let handles: Vec<RigidBodyHandle> = self.world.rigid_bodies().map(|(handle, _)| handle).collect();
338
339 for handle in handles {
340 self.world.remove_body(handle);
341 }
342
343 self.sim.clear();
344 self.nav = None;
345 self.spatial.clear();
346 self.bodies_to_destroy.clear();
347
348 self.accumulator = 0.0;
349 }
350
351 pub fn debug_json(&self) -> String {
357 crate::debug::engine_json(
358 &self.world,
359 &self.map,
360 &self.nav,
361 &self.spatial,
362 &self.rng,
363 self.accumulator,
364 self.time_step,
365 )
366 .to_string()
367 }
368
369 pub fn abi_describe(&self) -> String {
372 crate::abi::describe_json(crate::abi::ENGINE_GAME_OPS, self.sim.dispatch_ops())
373 }
374
375 pub fn dispatch(&mut self, op: &str, payload: &[u8]) -> Vec<u8> {
380 let out = match op {
381 crate::abi::OP_DEBUG_JSON => Some(self.debug_json().into_bytes()),
382 _ => self.sim.dispatch_op(op, payload),
383 };
384
385 crate::abi::dispatch_result(out)
386 }
387
388 pub fn serialize_state(&self) -> Result<Vec<u8>, String> {
394 let dump = EngineDump {
395 world: WorldDump {
396 gravity: [self.world.gravity.x, self.world.gravity.y],
397 integration_parameters: self.world.integration_parameters,
398 islands: self.world.islands.clone(),
399 broad_phase: self.world.broad_phase.clone(),
400 narrow_phase: self.world.narrow_phase.clone(),
401 bodies: self.world.bodies.clone(),
402 colliders: self.world.colliders.clone(),
403 impulse_joints: self.world.impulse_joints.clone(),
404 multibody_joints: self.world.multibody_joints.clone(),
405 },
406 map: &self.map,
407 rng: &self.rng,
408 accumulator: self.accumulator,
409 sim: self.sim.serialize(),
410 };
411
412 serde_json::to_vec(&dump).map_err(|e| e.to_string())
413 }
414
415 pub fn deserialize_state(&mut self, data: &[u8]) -> Result<(), String> {
418 let dump: EngineDumpOwned = serde_json::from_slice(data).map_err(|e| e.to_string())?;
419
420 let mut world = PhysicsWorld::new();
421
422 world.gravity = Vector::new(dump.world.gravity[0], dump.world.gravity[1]);
423 world.integration_parameters = dump.world.integration_parameters;
424 world.islands = dump.world.islands;
425 world.broad_phase = dump.world.broad_phase;
426 world.narrow_phase = dump.world.narrow_phase;
427 world.bodies = dump.world.bodies;
428 world.colliders = dump.world.colliders;
429 world.impulse_joints = dump.world.impulse_joints;
430 world.multibody_joints = dump.world.multibody_joints;
431
432 self.world = world;
433 self.map = dump.map;
434 self.rng = dump.rng;
435 self.accumulator = dump.accumulator;
436 self.sim.deserialize(dump.sim)?;
437
438 self.bodies_to_destroy.clear();
439 self.events.clear();
440
441 self.nav = self
444 .map
445 .as_ref()
446 .map(|map| NavigationSystem::generate(&map.grid, &map.physics_static, map.step));
447
448 self.sim.rebuild_spatial_grid(&self.world, &mut self.spatial);
449 self.sim.refresh_cached(&self.world);
450
451 Ok(())
452 }
453}
454
455#[derive(Serialize)]
456struct WorldDump {
457 gravity: [f32; 2],
458 integration_parameters: IntegrationParameters,
459 islands: IslandManager,
460 broad_phase: BroadPhaseBvh,
461 narrow_phase: NarrowPhase,
462 bodies: RigidBodySet,
463 colliders: ColliderSet,
464 impulse_joints: ImpulseJointSet,
465 multibody_joints: MultibodyJointSet,
466}
467
468#[derive(Deserialize)]
469struct WorldDumpOwned {
470 gravity: [f32; 2],
471 integration_parameters: IntegrationParameters,
472 islands: IslandManager,
473 broad_phase: BroadPhaseBvh,
474 narrow_phase: NarrowPhase,
475 bodies: RigidBodySet,
476 colliders: ColliderSet,
477 impulse_joints: ImpulseJointSet,
478 multibody_joints: MultibodyJointSet,
479}
480
481#[derive(Serialize)]
482struct EngineDump<'a> {
483 world: WorldDump,
484 map: &'a Option<GameMap>,
485 rng: &'a Rng,
486 accumulator: f32,
487 sim: serde_json::Value,
488}
489
490#[derive(Deserialize)]
491struct EngineDumpOwned {
492 world: WorldDumpOwned,
493 map: Option<GameMap>,
494 rng: Rng,
495 accumulator: f32,
496 sim: serde_json::Value,
497}
498
499#[cfg(test)]
506mod fixture {
507 use super::*;
508 use crate::config::FieldValue;
509 use serde::Deserialize;
510 use std::collections::{BTreeMap, BTreeSet};
511
512 #[derive(Deserialize)]
513 pub struct TestConfig {}
514
515 pub struct TestGame;
516
517 impl GameDef for TestGame {
518 type Config = TestConfig;
519 type Sim = TestSim;
520 }
521
522 #[derive(Clone, Copy)]
523 struct TestActor {
524 x: f32,
525 y: f32,
526 vx: f32,
527 vy: f32,
528 team: u8,
529 alive: bool,
530 }
531
532 pub struct TestSim {
533 actors: BTreeMap<u32, TestActor>,
534 scripted: BTreeSet<u32>,
535 }
536
537 impl GameSim<TestGame> for TestSim {
538 fn new(_cfg: &TestConfig, _engine_cfg: &EngineConfig) -> Self {
539 Self {
540 actors: BTreeMap::new(),
541 scripted: BTreeSet::new(),
542 }
543 }
544
545 fn spawn_actor(
546 &mut self,
547 _world: &mut PhysicsWorld,
548 _events: &mut Vec<CoreEvent>,
549 game_id: u32,
550 _model_name: &str,
551 team_id: u8,
552 x: f32,
553 y: f32,
554 _angle_deg: f32,
555 ) -> Result<(), String> {
556 self.actors.insert(
557 game_id,
558 TestActor { x, y, vx: 0.0, vy: 0.0, team: team_id, alive: true },
559 );
560
561 Ok(())
562 }
563
564 fn remove_actor(&mut self, _world: &mut PhysicsWorld, game_id: u32) {
565 self.actors.remove(&game_id);
566 self.scripted.remove(&game_id);
567 }
568
569 fn reset_actor(&mut self, _world: &mut PhysicsWorld, game_id: u32, team_id: u8, x: f32, y: f32, _angle_deg: f32) {
570 if let Some(actor) = self.actors.get_mut(&game_id) {
571 actor.x = x;
572 actor.y = y;
573 actor.team = team_id;
574 actor.alive = true;
575 }
576 }
577
578 fn reset_all_vitals(&mut self, _events: &mut Vec<CoreEvent>) {
579 for actor in self.actors.values_mut() {
580 actor.alive = true;
581 }
582 }
583
584 fn spawn_scripted_actor(
585 &mut self,
586 world: &mut PhysicsWorld,
587 _rng: &mut Rng,
588 events: &mut Vec<CoreEvent>,
589 game_id: u32,
590 model_name: &str,
591 team_id: u8,
592 x: f32,
593 y: f32,
594 angle_deg: f32,
595 ) -> Result<(), String> {
596 self.spawn_actor(world, events, game_id, model_name, team_id, x, y, angle_deg)?;
597 self.scripted.insert(game_id);
598
599 Ok(())
600 }
601
602 fn remove_scripted_actor(&mut self, world: &mut PhysicsWorld, game_id: u32) {
603 self.remove_actor(world, game_id);
604 }
605
606 fn apply_input(&mut self, game_id: u32, _seq: u32, action: &str, key_name: &str) {
607 let Some(actor) = self.actors.get_mut(&game_id) else {
608 return;
609 };
610
611 let magnitude = if action == "down" { 40.0 } else { 0.0 };
612
613 match key_name {
614 "forward" => actor.vy = -magnitude,
615 "back" => actor.vy = magnitude,
616 _ => {}
617 }
618 }
619
620 fn last_input_seq(&self, _game_id: u32) -> u32 {
621 0
622 }
623
624 fn is_alive(&self, game_id: u32) -> bool {
625 self.actors.get(&game_id).is_some_and(|a| a.alive)
626 }
627
628 fn actor_position(&self, _world: &PhysicsWorld, game_id: u32) -> Option<[f32; 2]> {
629 self.actors.get(&game_id).map(|a| [a.x, a.y])
630 }
631
632 fn prediction_state(&self, _world: &PhysicsWorld, game_id: u32) -> Option<([f32; PLAYER_STATE_LEN], bool)> {
633 self.actors
634 .get(&game_id)
635 .map(|a| ([a.x, a.y, 0.0, a.vx, a.vy, 0.0, 0.0, 0.0], false))
636 }
637
638 fn alive_players_flat(&self, _world: &PhysicsWorld) -> Vec<f32> {
639 self.actors
640 .iter()
641 .filter(|(_, a)| a.alive)
642 .flat_map(|(id, a)| [*id as f32, a.x, a.y])
643 .collect()
644 }
645
646 fn players_json(&self) -> String {
647 let rows: Vec<serde_json::Value> = self
648 .actors
649 .iter()
650 .map(|(id, a)| serde_json::json!({ "id": id, "x": a.x, "y": a.y, "team": a.team }))
651 .collect();
652
653 serde_json::to_string(&rows).unwrap()
654 }
655
656 fn on_fixed_step(&mut self, _ctx: &mut SimCtx, dt: f32) {
657 for actor in self.actors.values_mut() {
658 actor.x += actor.vx * dt;
659 actor.y += actor.vy * dt;
660 }
661 }
662
663 fn on_contacts(&mut self, _ctx: &mut SimCtx, _pairs: &[(ColliderHandle, ColliderHandle)]) {}
664
665 fn on_before_destroy(&mut self, _world: &PhysicsWorld, _handle: RigidBodyHandle) {}
666
667 fn on_ai_tick(&mut self, _ctx: &mut SimCtx, _dt: f32) {
668 for &id in &self.scripted {
670 if let Some(actor) = self.actors.get_mut(&id) {
671 actor.vx = 1.0;
672 }
673 }
674 }
675
676 fn refresh_cached(&mut self, _world: &PhysicsWorld) {}
677
678 fn build_snapshot_blocks(&mut self) -> (Vec<(String, Block)>, bool) {
679 let rows: Vec<(u8, Option<Vec<FieldValue>>)> = self
680 .actors
681 .iter()
682 .map(|(id, a)| (*id as u8, Some(vec![FieldValue::F32(a.x), FieldValue::F32(a.y)])))
683 .collect();
684
685 (vec![("actor".to_string(), Block::Indexed8(rows))], false)
686 }
687
688 fn remove_players_and_shots(&mut self, _world: &mut PhysicsWorld) -> Vec<String> {
689 let names: Vec<String> = self.actors.keys().map(|id| id.to_string()).collect();
690
691 self.actors.clear();
692 self.scripted.clear();
693
694 names
695 }
696
697 fn clear(&mut self) {
698 self.actors.clear();
699 self.scripted.clear();
700 }
701
702 fn serialize(&self) -> serde_json::Value {
703 let rows: Vec<serde_json::Value> = self
704 .actors
705 .iter()
706 .map(|(id, a)| serde_json::json!({ "id": id, "x": a.x, "y": a.y, "team": a.team }))
707 .collect();
708
709 serde_json::json!({ "actors": rows })
710 }
711
712 fn deserialize(&mut self, value: serde_json::Value) -> Result<(), String> {
713 let rows = value["actors"].as_array().ok_or("missing actors")?;
714
715 self.actors.clear();
716
717 for row in rows {
718 let id = row["id"].as_u64().ok_or("bad id")? as u32;
719 let x = row["x"].as_f64().ok_or("bad x")? as f32;
720 let y = row["y"].as_f64().ok_or("bad y")? as f32;
721 let team = row["team"].as_u64().ok_or("bad team")? as u8;
722
723 self.actors.insert(id, TestActor { x, y, vx: 0.0, vy: 0.0, team, alive: true });
724 }
725
726 Ok(())
727 }
728
729 fn rebuild_spatial_grid(&self, _world: &PhysicsWorld, _spatial: &mut SpatialGrid) {}
730 }
731}
732
733#[cfg(test)]
734mod tests {
735 use super::fixture::{TestConfig, TestGame};
736 use super::*;
737 use crate::snapshot::SnapshotPacker;
738
739 fn engine_config() -> EngineConfig {
740 serde_json::from_value(serde_json::json!({
741 "timeStep": 1.0 / 120.0,
742 "snapshot": {
743 "version": 3,
744 "port": 5,
745 "keys": {
746 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
747 { "name": "x", "ty": "f32", "interp": "lerp" },
748 { "name": "y", "ty": "f32", "interp": "lerp" }
749 ] }
750 }
751 },
752 "seed": 42
753 }))
754 .unwrap()
755 }
756
757 fn make_sim() -> EngineSim<TestGame> {
758 EngineSim::new(engine_config(), &TestConfig {})
759 }
760
761 #[test]
762 fn spawn_actor_and_fixed_step_moves_position() {
763 let mut sim = make_sim();
764
765 sim.spawn_actor(1, "m", 1, 0.0, 0.0, 0.0).unwrap();
766 sim.apply_input(1, 1, "down", "forward");
767
768 for _ in 0..60 {
769 sim.step(1.0 / 120.0);
770 }
771
772 let pos = sim.actor_position(1).unwrap();
773
774 assert!(pos[1] < 0.0); }
776
777 #[test]
778 fn remove_actor_clears_state() {
779 let mut sim = make_sim();
780
781 sim.spawn_actor(1, "m", 1, 5.0, 5.0, 0.0).unwrap();
782 assert!(sim.is_alive(1));
783
784 sim.remove_actor(1);
785 assert!(!sim.is_alive(1));
786 assert!(sim.actor_position(1).is_none());
787 }
788
789 #[test]
790 fn scripted_actor_runs_ai_tick() {
791 let mut sim = make_sim();
792
793 sim.spawn_scripted_actor(9, "m", 1, 0.0, 0.0, 0.0).unwrap();
794
795 sim.step(1.0 / 120.0);
798 sim.step(1.0 / 120.0);
799
800 let after = sim.actor_position(9).unwrap();
801
802 assert!(after[0] > 0.0); }
804
805 #[test]
806 fn build_snapshot_blocks_packs_through_generic_schema() {
807 let mut sim = make_sim();
808
809 sim.spawn_actor(1, "m", 1, 3.0, 4.0, 0.0).unwrap();
810 sim.spawn_actor(2, "m", 1, 7.0, 8.0, 0.0).unwrap();
811
812 let blocks = sim.build_snapshot_blocks();
813 let mut packer = SnapshotPacker::new(engine_config().snapshot);
814
815 assert!(packer.pack_body(&blocks).is_ok());
816 }
817
818 #[test]
819 fn clear_removes_all_actors() {
820 let mut sim = make_sim();
821
822 sim.spawn_actor(1, "m", 1, 0.0, 0.0, 0.0).unwrap();
823 sim.spawn_scripted_actor(2, "m", 1, 0.0, 0.0, 0.0).unwrap();
824
825 sim.clear();
826
827 assert!(!sim.is_alive(1));
828 assert!(!sim.is_alive(2));
829 assert_eq!(sim.alive_players_flat().len(), 0);
830 }
831
832 fn tiny_map_json() -> &'static str {
835 r#"{
836 "setId": "tiny",
837 "scale": 1,
838 "step": 10,
839 "map": [[1, 0], [0, 0]],
840 "physicsStatic": [1],
841 "respawns": { "team1": [[5, 5, 0]] }
842 }"#
843 }
844
845 #[test]
846 fn debug_json_dumps_world_map_and_rng() {
847 let mut sim = make_sim();
848
849 sim.load_map(tiny_map_json()).unwrap();
850
851 let dump: serde_json::Value = serde_json::from_str(&sim.debug_json()).unwrap();
852
853 let bodies = dump["bodies"].as_array().unwrap();
854 let colliders = dump["colliders"].as_array().unwrap();
855
856 assert_eq!(bodies.len(), 1);
857 assert_eq!(colliders.len(), 1);
858 assert_eq!(bodies[0]["bodyType"], "Fixed");
859 assert_eq!(bodies[0]["translation"], serde_json::json!([5.0, 5.0]));
860 assert_eq!(colliders[0]["shape"], "cuboid");
861 assert_eq!(colliders[0]["halfExtents"], serde_json::json!([5.0, 5.0]));
862 assert_eq!(colliders[0]["isSensor"], false);
863 assert_eq!(colliders[0]["parent"], bodies[0]["handle"]);
864
865 assert_eq!(dump["map"]["setId"], "tiny");
866 assert_eq!(dump["map"]["staticBodies"], 1);
867 assert_eq!(dump["map"]["grid"]["rows"], 2);
868 assert_eq!(dump["map"]["respawns"]["team1"], 1);
869
870 assert!(dump["nav"]["nodes"].as_u64().unwrap() > 0);
871 assert_eq!(dump["spatial"]["cells"], 0);
872 assert_eq!(dump["rng"]["state"], "42");
873 assert_eq!(dump["step"]["accumulator"], 0.0);
874 }
875
876 #[test]
877 fn debug_json_is_null_for_map_and_nav_without_map() {
878 let dump: serde_json::Value = serde_json::from_str(&make_sim().debug_json()).unwrap();
879
880 assert!(dump["map"].is_null());
881 assert!(dump["nav"].is_null());
882 assert_eq!(dump["bodies"].as_array().unwrap().len(), 0);
883 }
884
885 #[test]
886 fn debug_json_is_stable_between_identical_runs() {
887 let mut a = make_sim();
888 let mut b = make_sim();
889
890 for sim in [&mut a, &mut b] {
891 sim.load_map(tiny_map_json()).unwrap();
892 sim.spawn_actor(1, "m", 1, 3.0, 4.0, 0.0).unwrap();
893 sim.step(1.0 / 120.0);
894 }
895
896 assert_eq!(a.debug_json(), b.debug_json());
897 }
898
899 #[test]
900 fn serialize_deserialize_round_trips_actors() {
901 let mut sim = make_sim();
902
903 sim.spawn_actor(1, "m", 3, 11.0, 22.0, 0.0).unwrap();
904
905 let dump = sim.sim.serialize();
906 let mut restored = make_sim();
907
908 restored.sim.deserialize(dump).unwrap();
909
910 assert_eq!(restored.actor_position(1), Some([11.0, 22.0]));
911 }
912}