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 map_cfg.validate()?;
84
85 if let Some(mut old) = self.map.take() {
86 old.destroy(&mut self.world);
87 }
88
89 let map = GameMap::create(&mut self.world, &map_cfg, self.cfg.map_scale, &self.cfg.map_set_id);
90
91 self.nav = Some(if map.is_layered() {
94 NavigationSystem::generate_layered(map.levels(), map.step)
95 } else {
96 NavigationSystem::generate(&map.grid, &map.physics_static, map.step)
97 });
98
99 self.map = Some(map);
100
101 Ok(())
102 }
103
104 pub fn map_info_json(&self) -> String {
107 let Some(map) = &self.map else {
108 return "null".to_string();
109 };
110
111 let width = map.grid.first().map(|row| row.len()).unwrap_or(0) as f32 * map.step;
112 let height = map.grid.len() as f32 * map.step;
113
114 serde_json::json!({
115 "setId": map.set_id,
116 "step": map.step,
117 "width": width,
118 "height": height,
119 "respawns": map.respawns,
120 "levels": map.level_count(),
121 })
122 .to_string()
123 }
124
125 pub fn spawn_actor(&mut self, game_id: u32, model_name: &str, team_id: u8, x: f32, y: f32, angle_deg: f32) -> Result<(), String> {
128 self.sim
129 .spawn_actor(&mut self.world, &mut self.events, game_id, model_name, team_id, x, y, angle_deg)
130 }
131
132 pub fn remove_actor(&mut self, game_id: u32) {
133 self.sim.remove_actor(&mut self.world, game_id);
134 }
135
136 pub fn reset_actor(&mut self, game_id: u32, team_id: u8, x: f32, y: f32, angle_deg: f32) {
137 self.sim.reset_actor(&mut self.world, game_id, team_id, x, y, angle_deg);
138 }
139
140 pub fn reset_all_vitals(&mut self) {
141 self.sim.reset_all_vitals(&mut self.events);
142 }
143
144 pub fn set_actor_level(&mut self, game_id: u32, level: u8) {
146 self.sim.set_actor_level(&mut self.world, game_id, level);
147 }
148
149 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> {
150 self.sim.spawn_scripted_actor(
151 &mut self.world,
152 &mut self.rng,
153 &mut self.events,
154 game_id,
155 model_name,
156 team_id,
157 x,
158 y,
159 angle_deg,
160 )
161 }
162
163 pub fn remove_scripted_actor(&mut self, game_id: u32) {
164 self.sim.remove_scripted_actor(&mut self.world, game_id);
165 }
166
167 pub fn apply_input(&mut self, game_id: u32, seq: u32, action: &str, key_name: &str) {
170 self.sim.apply_input(game_id, seq, action, key_name);
171 }
172
173 pub fn apply_aim(&mut self, game_id: u32, seq: u32, x: f32, y: f32, flags: u32) {
175 self.sim.apply_aim(game_id, seq, x, y, flags);
176 }
177
178 pub fn last_input_seq(&self, game_id: u32) -> u32 {
179 self.sim.last_input_seq(game_id)
180 }
181
182 pub fn take_events_json(&mut self) -> String {
185 let events: Vec<CoreEvent> = self.events.drain(..).collect();
186
187 serde_json::to_string(&events).unwrap_or_else(|_| "[]".to_string())
188 }
189
190 pub fn is_alive(&self, game_id: u32) -> bool {
193 self.sim.is_alive(game_id)
194 }
195
196 pub fn actor_position(&self, game_id: u32) -> Option<[f32; 2]> {
197 self.sim.actor_position(&self.world, game_id)
198 }
199
200 pub fn prediction_state(&self, game_id: u32) -> Option<([f32; PLAYER_STATE_LEN], bool)> {
201 self.sim.prediction_state(&self.world, game_id)
202 }
203
204 pub fn alive_players_flat(&self) -> Vec<f32> {
205 self.sim.alive_players_flat(&self.world)
206 }
207
208 pub fn players_json(&self) -> String {
211 self.sim.players_json()
212 }
213
214 pub fn step(&mut self, dt: f32) {
219 self.accumulator = (self.accumulator + dt).min(MAX_ACCUMULATED_TIME);
220
221 while self.accumulator >= self.time_step {
222 self.step_fixed();
223 self.accumulator -= self.time_step;
224 }
225
226 self.sim.refresh_cached(&self.world);
227
228 let mut ctx = SimCtx {
229 world: &mut self.world,
230 cfg: &self.cfg,
231 map: &self.map,
232 nav: &self.nav,
233 spatial: &mut self.spatial,
234 rng: &mut self.rng,
235 events: &mut self.events,
236 bodies_to_destroy: &mut self.bodies_to_destroy,
237 };
238
239 self.sim.on_ai_tick(&mut ctx, dt);
240 }
241
242 fn step_fixed(&mut self) {
244 let mut ctx = SimCtx {
245 world: &mut self.world,
246 cfg: &self.cfg,
247 map: &self.map,
248 nav: &self.nav,
249 spatial: &mut self.spatial,
250 rng: &mut self.rng,
251 events: &mut self.events,
252 bodies_to_destroy: &mut self.bodies_to_destroy,
253 };
254
255 self.sim.on_fixed_step(&mut ctx, self.time_step);
256
257 let (collision_send, collision_recv) = channel();
259 let (force_send, _force_recv) = channel();
260 let collector = ChannelEventCollector::new(collision_send, force_send);
261
262 self.world.step_with_events(&(), &collector);
263
264 let mut contacts = Vec::new();
265
266 while let Ok(event) = collision_recv.try_recv() {
267 if let CollisionEvent::Started(h1, h2, _) = event {
268 contacts.push((h1, h2));
269 }
270 }
271
272 let mut ctx = SimCtx {
273 world: &mut self.world,
274 cfg: &self.cfg,
275 map: &self.map,
276 nav: &self.nav,
277 spatial: &mut self.spatial,
278 rng: &mut self.rng,
279 events: &mut self.events,
280 bodies_to_destroy: &mut self.bodies_to_destroy,
281 };
282
283 self.sim.on_contacts(&mut ctx, &contacts);
284 self.destroy_queued_bodies();
285 }
286
287 fn destroy_queued_bodies(&mut self) {
289 if self.bodies_to_destroy.is_empty() {
290 return;
291 }
292
293 let handles: Vec<RigidBodyHandle> = self.bodies_to_destroy.drain(..).collect();
294
295 for handle in handles {
296 self.sim.on_before_destroy(&self.world, handle);
297 self.world.remove_body(handle);
298 }
299 }
300
301 pub fn build_snapshot_blocks(&mut self) -> Vec<(String, Block)> {
306 let (mut blocks, has_events) = self.sim.build_snapshot_blocks();
307
308 if let Some(map) = &self.map {
312 let with_velocities = self
313 .cfg
314 .snapshot
315 .keys
316 .get(&map.set_id)
317 .is_some_and(|schema| schema.optional_from.is_some());
318
319 blocks.push((
320 map.set_id.clone(),
321 Block::IndexedNoNull8(map.dynamic_map_data(&self.world, with_velocities)),
322 ));
323 }
324
325 self.last_body_has_events = has_events;
326
327 blocks
328 }
329
330 pub fn body_has_events(&self) -> bool {
333 self.last_body_has_events
334 }
335
336 pub fn remove_players_and_shots(&mut self) -> Vec<String> {
341 self.sim.remove_players_and_shots(&mut self.world)
342 }
343
344 pub fn clear(&mut self) {
346 if let Some(mut map) = self.map.take() {
349 map.destroy(&mut self.world);
350 }
351
352 let handles: Vec<RigidBodyHandle> = self.world.rigid_bodies().map(|(handle, _)| handle).collect();
353
354 for handle in handles {
355 self.world.remove_body(handle);
356 }
357
358 self.sim.clear();
359 self.nav = None;
360 self.spatial.clear();
361 self.bodies_to_destroy.clear();
362
363 self.accumulator = 0.0;
364 }
365
366 pub fn debug_json(&self) -> String {
372 crate::debug::engine_json(
373 &self.world,
374 &self.map,
375 &self.nav,
376 &self.spatial,
377 &self.rng,
378 self.accumulator,
379 self.time_step,
380 )
381 .to_string()
382 }
383
384 pub fn abi_describe(&self) -> String {
387 crate::abi::describe_json(crate::abi::ENGINE_GAME_OPS, self.sim.dispatch_ops())
388 }
389
390 pub fn dispatch(&mut self, op: &str, payload: &[u8]) -> Vec<u8> {
395 let out = match op {
396 crate::abi::OP_DEBUG_JSON => Some(self.debug_json().into_bytes()),
397 _ => self.sim.dispatch_op(op, payload),
398 };
399
400 crate::abi::dispatch_result(out)
401 }
402
403 pub fn serialize_state(&self) -> Result<Vec<u8>, String> {
409 let dump = EngineDump {
410 world: WorldDump {
411 gravity: [self.world.gravity.x, self.world.gravity.y],
412 integration_parameters: self.world.integration_parameters,
413 islands: self.world.islands.clone(),
414 broad_phase: self.world.broad_phase.clone(),
415 narrow_phase: self.world.narrow_phase.clone(),
416 bodies: self.world.bodies.clone(),
417 colliders: self.world.colliders.clone(),
418 impulse_joints: self.world.impulse_joints.clone(),
419 multibody_joints: self.world.multibody_joints.clone(),
420 },
421 map: &self.map,
422 rng: &self.rng,
423 accumulator: self.accumulator,
424 sim: self.sim.serialize(),
425 };
426
427 serde_json::to_vec(&dump).map_err(|e| e.to_string())
428 }
429
430 pub fn deserialize_state(&mut self, data: &[u8]) -> Result<(), String> {
433 let dump: EngineDumpOwned = serde_json::from_slice(data).map_err(|e| e.to_string())?;
434
435 let mut world = PhysicsWorld::new();
436
437 world.gravity = Vector::new(dump.world.gravity[0], dump.world.gravity[1]);
438 world.integration_parameters = dump.world.integration_parameters;
439 world.islands = dump.world.islands;
440 world.broad_phase = dump.world.broad_phase;
441 world.narrow_phase = dump.world.narrow_phase;
442 world.bodies = dump.world.bodies;
443 world.colliders = dump.world.colliders;
444 world.impulse_joints = dump.world.impulse_joints;
445 world.multibody_joints = dump.world.multibody_joints;
446
447 self.world = world;
448 self.map = dump.map;
449 self.rng = dump.rng;
450 self.accumulator = dump.accumulator;
451 self.sim.deserialize(dump.sim)?;
452
453 self.bodies_to_destroy.clear();
454 self.events.clear();
455
456 self.nav = self.map.as_ref().map(|map| {
459 if map.is_layered() {
460 NavigationSystem::generate_layered(map.levels(), map.step)
461 } else {
462 NavigationSystem::generate(&map.grid, &map.physics_static, map.step)
463 }
464 });
465
466 self.sim.rebuild_spatial_grid(&self.world, &mut self.spatial);
467 self.sim.refresh_cached(&self.world);
468
469 Ok(())
470 }
471}
472
473#[derive(Serialize)]
474struct WorldDump {
475 gravity: [f32; 2],
476 integration_parameters: IntegrationParameters,
477 islands: IslandManager,
478 broad_phase: BroadPhaseBvh,
479 narrow_phase: NarrowPhase,
480 bodies: RigidBodySet,
481 colliders: ColliderSet,
482 impulse_joints: ImpulseJointSet,
483 multibody_joints: MultibodyJointSet,
484}
485
486#[derive(Deserialize)]
487struct WorldDumpOwned {
488 gravity: [f32; 2],
489 integration_parameters: IntegrationParameters,
490 islands: IslandManager,
491 broad_phase: BroadPhaseBvh,
492 narrow_phase: NarrowPhase,
493 bodies: RigidBodySet,
494 colliders: ColliderSet,
495 impulse_joints: ImpulseJointSet,
496 multibody_joints: MultibodyJointSet,
497}
498
499#[derive(Serialize)]
500struct EngineDump<'a> {
501 world: WorldDump,
502 map: &'a Option<GameMap>,
503 rng: &'a Rng,
504 accumulator: f32,
505 sim: serde_json::Value,
506}
507
508#[derive(Deserialize)]
509struct EngineDumpOwned {
510 world: WorldDumpOwned,
511 map: Option<GameMap>,
512 rng: Rng,
513 accumulator: f32,
514 sim: serde_json::Value,
515}
516
517#[cfg(test)]
524mod fixture {
525 use super::*;
526 use crate::config::FieldValue;
527 use serde::Deserialize;
528 use std::collections::{BTreeMap, BTreeSet};
529
530 #[derive(Deserialize)]
531 pub struct TestConfig {}
532
533 pub struct TestGame;
534
535 impl GameDef for TestGame {
536 type Config = TestConfig;
537 type Sim = TestSim;
538 }
539
540 #[derive(Clone, Copy)]
541 struct TestActor {
542 x: f32,
543 y: f32,
544 vx: f32,
545 vy: f32,
546 team: u8,
547 alive: bool,
548 }
549
550 pub struct TestSim {
551 actors: BTreeMap<u32, TestActor>,
552 scripted: BTreeSet<u32>,
553 }
554
555 impl GameSim<TestGame> for TestSim {
556 fn new(_cfg: &TestConfig, _engine_cfg: &EngineConfig) -> Self {
557 Self {
558 actors: BTreeMap::new(),
559 scripted: BTreeSet::new(),
560 }
561 }
562
563 fn spawn_actor(
564 &mut self,
565 _world: &mut PhysicsWorld,
566 _events: &mut Vec<CoreEvent>,
567 game_id: u32,
568 _model_name: &str,
569 team_id: u8,
570 x: f32,
571 y: f32,
572 _angle_deg: f32,
573 ) -> Result<(), String> {
574 self.actors.insert(
575 game_id,
576 TestActor { x, y, vx: 0.0, vy: 0.0, team: team_id, alive: true },
577 );
578
579 Ok(())
580 }
581
582 fn remove_actor(&mut self, _world: &mut PhysicsWorld, game_id: u32) {
583 self.actors.remove(&game_id);
584 self.scripted.remove(&game_id);
585 }
586
587 fn reset_actor(&mut self, _world: &mut PhysicsWorld, game_id: u32, team_id: u8, x: f32, y: f32, _angle_deg: f32) {
588 if let Some(actor) = self.actors.get_mut(&game_id) {
589 actor.x = x;
590 actor.y = y;
591 actor.team = team_id;
592 actor.alive = true;
593 }
594 }
595
596 fn reset_all_vitals(&mut self, _events: &mut Vec<CoreEvent>) {
597 for actor in self.actors.values_mut() {
598 actor.alive = true;
599 }
600 }
601
602 fn spawn_scripted_actor(
603 &mut self,
604 world: &mut PhysicsWorld,
605 _rng: &mut Rng,
606 events: &mut Vec<CoreEvent>,
607 game_id: u32,
608 model_name: &str,
609 team_id: u8,
610 x: f32,
611 y: f32,
612 angle_deg: f32,
613 ) -> Result<(), String> {
614 self.spawn_actor(world, events, game_id, model_name, team_id, x, y, angle_deg)?;
615 self.scripted.insert(game_id);
616
617 Ok(())
618 }
619
620 fn remove_scripted_actor(&mut self, world: &mut PhysicsWorld, game_id: u32) {
621 self.remove_actor(world, game_id);
622 }
623
624 fn apply_input(&mut self, game_id: u32, _seq: u32, action: &str, key_name: &str) {
625 let Some(actor) = self.actors.get_mut(&game_id) else {
626 return;
627 };
628
629 let magnitude = if action == "down" { 40.0 } else { 0.0 };
630
631 match key_name {
632 "forward" => actor.vy = -magnitude,
633 "back" => actor.vy = magnitude,
634 _ => {}
635 }
636 }
637
638 fn last_input_seq(&self, _game_id: u32) -> u32 {
639 0
640 }
641
642 fn is_alive(&self, game_id: u32) -> bool {
643 self.actors.get(&game_id).is_some_and(|a| a.alive)
644 }
645
646 fn actor_position(&self, _world: &PhysicsWorld, game_id: u32) -> Option<[f32; 2]> {
647 self.actors.get(&game_id).map(|a| [a.x, a.y])
648 }
649
650 fn prediction_state(&self, _world: &PhysicsWorld, game_id: u32) -> Option<([f32; PLAYER_STATE_LEN], bool)> {
651 self.actors
652 .get(&game_id)
653 .map(|a| ([a.x, a.y, 0.0, a.vx, a.vy, 0.0, 0.0, 0.0], false))
654 }
655
656 fn alive_players_flat(&self, _world: &PhysicsWorld) -> Vec<f32> {
657 self.actors
658 .iter()
659 .filter(|(_, a)| a.alive)
660 .flat_map(|(id, a)| [*id as f32, a.x, a.y])
661 .collect()
662 }
663
664 fn players_json(&self) -> String {
665 let rows: Vec<serde_json::Value> = self
666 .actors
667 .iter()
668 .map(|(id, a)| serde_json::json!({ "id": id, "x": a.x, "y": a.y, "team": a.team }))
669 .collect();
670
671 serde_json::to_string(&rows).unwrap()
672 }
673
674 fn on_fixed_step(&mut self, _ctx: &mut SimCtx, dt: f32) {
675 for actor in self.actors.values_mut() {
676 actor.x += actor.vx * dt;
677 actor.y += actor.vy * dt;
678 }
679 }
680
681 fn on_contacts(&mut self, _ctx: &mut SimCtx, _pairs: &[(ColliderHandle, ColliderHandle)]) {}
682
683 fn on_before_destroy(&mut self, _world: &PhysicsWorld, _handle: RigidBodyHandle) {}
684
685 fn on_ai_tick(&mut self, _ctx: &mut SimCtx, _dt: f32) {
686 for &id in &self.scripted {
688 if let Some(actor) = self.actors.get_mut(&id) {
689 actor.vx = 1.0;
690 }
691 }
692 }
693
694 fn refresh_cached(&mut self, _world: &PhysicsWorld) {}
695
696 fn build_snapshot_blocks(&mut self) -> (Vec<(String, Block)>, bool) {
697 let rows: Vec<(u8, Option<Vec<FieldValue>>)> = self
698 .actors
699 .iter()
700 .map(|(id, a)| (*id as u8, Some(vec![FieldValue::F32(a.x), FieldValue::F32(a.y)])))
701 .collect();
702
703 (vec![("actor".to_string(), Block::Indexed8(rows))], false)
704 }
705
706 fn remove_players_and_shots(&mut self, _world: &mut PhysicsWorld) -> Vec<String> {
707 let names: Vec<String> = self.actors.keys().map(|id| id.to_string()).collect();
708
709 self.actors.clear();
710 self.scripted.clear();
711
712 names
713 }
714
715 fn clear(&mut self) {
716 self.actors.clear();
717 self.scripted.clear();
718 }
719
720 fn serialize(&self) -> serde_json::Value {
721 let rows: Vec<serde_json::Value> = self
722 .actors
723 .iter()
724 .map(|(id, a)| serde_json::json!({ "id": id, "x": a.x, "y": a.y, "team": a.team }))
725 .collect();
726
727 serde_json::json!({ "actors": rows })
728 }
729
730 fn deserialize(&mut self, value: serde_json::Value) -> Result<(), String> {
731 let rows = value["actors"].as_array().ok_or("missing actors")?;
732
733 self.actors.clear();
734
735 for row in rows {
736 let id = row["id"].as_u64().ok_or("bad id")? as u32;
737 let x = row["x"].as_f64().ok_or("bad x")? as f32;
738 let y = row["y"].as_f64().ok_or("bad y")? as f32;
739 let team = row["team"].as_u64().ok_or("bad team")? as u8;
740
741 self.actors.insert(id, TestActor { x, y, vx: 0.0, vy: 0.0, team, alive: true });
742 }
743
744 Ok(())
745 }
746
747 fn rebuild_spatial_grid(&self, _world: &PhysicsWorld, _spatial: &mut SpatialGrid) {}
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::fixture::{TestConfig, TestGame};
754 use super::*;
755 use crate::snapshot::SnapshotPacker;
756
757 fn engine_config() -> EngineConfig {
758 serde_json::from_value(serde_json::json!({
759 "timeStep": 1.0 / 120.0,
760 "snapshot": {
761 "version": 3,
762 "port": 5,
763 "keys": {
764 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
765 { "name": "x", "ty": "f32", "interp": "lerp" },
766 { "name": "y", "ty": "f32", "interp": "lerp" }
767 ] }
768 }
769 },
770 "seed": 42
771 }))
772 .unwrap()
773 }
774
775 fn make_sim() -> EngineSim<TestGame> {
776 EngineSim::new(engine_config(), &TestConfig {})
777 }
778
779 #[test]
780 fn spawn_actor_and_fixed_step_moves_position() {
781 let mut sim = make_sim();
782
783 sim.spawn_actor(1, "m", 1, 0.0, 0.0, 0.0).unwrap();
784 sim.apply_input(1, 1, "down", "forward");
785
786 for _ in 0..60 {
787 sim.step(1.0 / 120.0);
788 }
789
790 let pos = sim.actor_position(1).unwrap();
791
792 assert!(pos[1] < 0.0); }
794
795 #[test]
796 fn remove_actor_clears_state() {
797 let mut sim = make_sim();
798
799 sim.spawn_actor(1, "m", 1, 5.0, 5.0, 0.0).unwrap();
800 assert!(sim.is_alive(1));
801
802 sim.remove_actor(1);
803 assert!(!sim.is_alive(1));
804 assert!(sim.actor_position(1).is_none());
805 }
806
807 #[test]
808 fn scripted_actor_runs_ai_tick() {
809 let mut sim = make_sim();
810
811 sim.spawn_scripted_actor(9, "m", 1, 0.0, 0.0, 0.0).unwrap();
812
813 sim.step(1.0 / 120.0);
816 sim.step(1.0 / 120.0);
817
818 let after = sim.actor_position(9).unwrap();
819
820 assert!(after[0] > 0.0); }
822
823 #[test]
824 fn build_snapshot_blocks_packs_through_generic_schema() {
825 let mut sim = make_sim();
826
827 sim.spawn_actor(1, "m", 1, 3.0, 4.0, 0.0).unwrap();
828 sim.spawn_actor(2, "m", 1, 7.0, 8.0, 0.0).unwrap();
829
830 let blocks = sim.build_snapshot_blocks();
831 let mut packer = SnapshotPacker::new(engine_config().snapshot);
832
833 assert!(packer.pack_body(&blocks).is_ok());
834 }
835
836 #[test]
837 fn clear_removes_all_actors() {
838 let mut sim = make_sim();
839
840 sim.spawn_actor(1, "m", 1, 0.0, 0.0, 0.0).unwrap();
841 sim.spawn_scripted_actor(2, "m", 1, 0.0, 0.0, 0.0).unwrap();
842
843 sim.clear();
844
845 assert!(!sim.is_alive(1));
846 assert!(!sim.is_alive(2));
847 assert_eq!(sim.alive_players_flat().len(), 0);
848 }
849
850 fn tiny_map_json() -> &'static str {
853 r#"{
854 "setId": "tiny",
855 "scale": 1,
856 "step": 10,
857 "map": [[1, 0], [0, 0]],
858 "physicsStatic": [1],
859 "respawns": { "team1": [[5, 5, 0]] }
860 }"#
861 }
862
863 fn layered_map_json() -> &'static str {
866 r#"{
867 "setId": "layered",
868 "scale": 1,
869 "step": 10,
870 "map": [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]],
871 "physicsStatic": [],
872 "levels": {
873 "1": {
874 "map": [[0, 0, 0, 0, 0, 0, 0, 0], [0, 8, 8, 8, 8, 8, 8, 0], [0, 8, 9, 9, 9, 9, 8, 0], [0, 8, 9, 9, 9, 9, 8, 0], [0, 8, 9, 9, 9, 9, 8, 0], [0, 8, 9, 9, 9, 9, 8, 0], [0, 8, 8, 8, 8, 8, 8, 0], [0, 0, 0, 0, 0, 0, 0, 0]],
875 "floor": [9, 8],
876 "walls": [8]
877 }
878 }
879 }"#
880 }
881
882 #[test]
883 fn load_map_rejects_invalid_layers() {
884 let mut sim = make_sim();
885
886 let broken = r#"{
888 "step": 10,
889 "map": [[0, 0], [0, 0]],
890 "levels": { "1": { "map": [[0, 0]], "floor": [] } }
891 }"#;
892
893 assert!(sim.load_map(broken).is_err());
894 assert!(sim.map.is_none());
895 assert!(sim.nav.is_none());
896 }
897
898 #[test]
899 fn load_map_picks_layered_nav() {
900 let mut sim = make_sim();
901
902 sim.load_map(layered_map_json()).unwrap();
903
904 let map = sim.map.as_ref().unwrap();
905
906 assert!(map.is_layered());
907 assert_eq!(map.level_count(), 2);
908
909 let counts = sim.nav.as_ref().unwrap().nodes_by_level();
910
911 assert!(counts[0] > 0 && counts[1] > 0, "{counts:?}");
912 }
913
914 #[test]
915 fn debug_json_dumps_world_map_and_rng() {
916 let mut sim = make_sim();
917
918 sim.load_map(tiny_map_json()).unwrap();
919
920 let dump: serde_json::Value = serde_json::from_str(&sim.debug_json()).unwrap();
921
922 let bodies = dump["bodies"].as_array().unwrap();
923 let colliders = dump["colliders"].as_array().unwrap();
924
925 assert_eq!(bodies.len(), 1);
926 assert_eq!(colliders.len(), 1);
927 assert_eq!(bodies[0]["bodyType"], "Fixed");
928 assert_eq!(bodies[0]["translation"], serde_json::json!([5.0, 5.0]));
929 assert_eq!(colliders[0]["shape"], "cuboid");
930 assert_eq!(colliders[0]["halfExtents"], serde_json::json!([5.0, 5.0]));
931 assert_eq!(colliders[0]["isSensor"], false);
932 assert_eq!(colliders[0]["parent"], bodies[0]["handle"]);
933
934 assert_eq!(dump["map"]["setId"], "tiny");
935 assert_eq!(dump["map"]["staticBodies"], 1);
936 assert_eq!(dump["map"]["grid"]["rows"], 2);
937 assert_eq!(dump["map"]["respawns"]["team1"], 1);
938
939 assert!(dump["nav"]["nodes"].as_u64().unwrap() > 0);
940 assert_eq!(dump["spatial"]["cells"], 0);
941 assert_eq!(dump["rng"]["state"], "42");
942 assert_eq!(dump["step"]["accumulator"], 0.0);
943 }
944
945 #[test]
946 fn debug_json_is_null_for_map_and_nav_without_map() {
947 let dump: serde_json::Value = serde_json::from_str(&make_sim().debug_json()).unwrap();
948
949 assert!(dump["map"].is_null());
950 assert!(dump["nav"].is_null());
951 assert_eq!(dump["bodies"].as_array().unwrap().len(), 0);
952 }
953
954 #[test]
955 fn debug_json_is_stable_between_identical_runs() {
956 let mut a = make_sim();
957 let mut b = make_sim();
958
959 for sim in [&mut a, &mut b] {
960 sim.load_map(tiny_map_json()).unwrap();
961 sim.spawn_actor(1, "m", 1, 3.0, 4.0, 0.0).unwrap();
962 sim.step(1.0 / 120.0);
963 }
964
965 assert_eq!(a.debug_json(), b.debug_json());
966 }
967
968 #[test]
969 fn serialize_deserialize_round_trips_actors() {
970 let mut sim = make_sim();
971
972 sim.spawn_actor(1, "m", 3, 11.0, 22.0, 0.0).unwrap();
973
974 let dump = sim.sim.serialize();
975 let mut restored = make_sim();
976
977 restored.sim.deserialize(dump).unwrap();
978
979 assert_eq!(restored.actor_position(1), Some([11.0, 22.0]));
980 }
981}