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 last_input_seq(&self, game_id: u32) -> u32 {
159 self.sim.last_input_seq(game_id)
160 }
161
162 pub fn take_events_json(&mut self) -> String {
165 let events: Vec<CoreEvent> = self.events.drain(..).collect();
166
167 serde_json::to_string(&events).unwrap_or_else(|_| "[]".to_string())
168 }
169
170 pub fn is_alive(&self, game_id: u32) -> bool {
173 self.sim.is_alive(game_id)
174 }
175
176 pub fn actor_position(&self, game_id: u32) -> Option<[f32; 2]> {
177 self.sim.actor_position(&self.world, game_id)
178 }
179
180 pub fn prediction_state(&self, game_id: u32) -> Option<([f32; PLAYER_STATE_LEN], bool)> {
181 self.sim.prediction_state(&self.world, game_id)
182 }
183
184 pub fn alive_players_flat(&self) -> Vec<f32> {
185 self.sim.alive_players_flat(&self.world)
186 }
187
188 pub fn players_json(&self) -> String {
191 self.sim.players_json()
192 }
193
194 pub fn step(&mut self, dt: f32) {
199 self.accumulator = (self.accumulator + dt).min(MAX_ACCUMULATED_TIME);
200
201 while self.accumulator >= self.time_step {
202 self.step_fixed();
203 self.accumulator -= self.time_step;
204 }
205
206 self.sim.refresh_cached(&self.world);
207
208 let mut ctx = SimCtx {
209 world: &mut self.world,
210 cfg: &self.cfg,
211 map: &self.map,
212 nav: &self.nav,
213 spatial: &mut self.spatial,
214 rng: &mut self.rng,
215 events: &mut self.events,
216 bodies_to_destroy: &mut self.bodies_to_destroy,
217 };
218
219 self.sim.on_ai_tick(&mut ctx, dt);
220 }
221
222 fn step_fixed(&mut self) {
224 let mut ctx = SimCtx {
225 world: &mut self.world,
226 cfg: &self.cfg,
227 map: &self.map,
228 nav: &self.nav,
229 spatial: &mut self.spatial,
230 rng: &mut self.rng,
231 events: &mut self.events,
232 bodies_to_destroy: &mut self.bodies_to_destroy,
233 };
234
235 self.sim.on_fixed_step(&mut ctx, self.time_step);
236
237 let (collision_send, collision_recv) = channel();
239 let (force_send, _force_recv) = channel();
240 let collector = ChannelEventCollector::new(collision_send, force_send);
241
242 self.world.step_with_events(&(), &collector);
243
244 let mut contacts = Vec::new();
245
246 while let Ok(event) = collision_recv.try_recv() {
247 if let CollisionEvent::Started(h1, h2, _) = event {
248 contacts.push((h1, h2));
249 }
250 }
251
252 let mut ctx = SimCtx {
253 world: &mut self.world,
254 cfg: &self.cfg,
255 map: &self.map,
256 nav: &self.nav,
257 spatial: &mut self.spatial,
258 rng: &mut self.rng,
259 events: &mut self.events,
260 bodies_to_destroy: &mut self.bodies_to_destroy,
261 };
262
263 self.sim.on_contacts(&mut ctx, &contacts);
264 self.destroy_queued_bodies();
265 }
266
267 fn destroy_queued_bodies(&mut self) {
269 if self.bodies_to_destroy.is_empty() {
270 return;
271 }
272
273 let handles: Vec<RigidBodyHandle> = self.bodies_to_destroy.drain(..).collect();
274
275 for handle in handles {
276 self.sim.on_before_destroy(&self.world, handle);
277 self.world.remove_body(handle);
278 }
279 }
280
281 pub fn build_snapshot_blocks(&mut self) -> Vec<(String, Block)> {
286 let (mut blocks, has_events) = self.sim.build_snapshot_blocks();
287
288 if let Some(map) = &self.map {
290 blocks.push((
291 map.set_id.clone(),
292 Block::IndexedNoNull8(map.dynamic_map_data(&self.world)),
293 ));
294 }
295
296 self.last_body_has_events = has_events;
297
298 blocks
299 }
300
301 pub fn body_has_events(&self) -> bool {
304 self.last_body_has_events
305 }
306
307 pub fn remove_players_and_shots(&mut self) -> Vec<String> {
312 self.sim.remove_players_and_shots(&mut self.world)
313 }
314
315 pub fn clear(&mut self) {
317 if let Some(mut map) = self.map.take() {
320 map.destroy(&mut self.world);
321 }
322
323 let handles: Vec<RigidBodyHandle> = self.world.rigid_bodies().map(|(handle, _)| handle).collect();
324
325 for handle in handles {
326 self.world.remove_body(handle);
327 }
328
329 self.sim.clear();
330 self.nav = None;
331 self.spatial.clear();
332 self.bodies_to_destroy.clear();
333
334 self.accumulator = 0.0;
335 }
336
337 pub fn serialize_state(&self) -> Result<Vec<u8>, String> {
343 let dump = EngineDump {
344 world: WorldDump {
345 gravity: [self.world.gravity.x, self.world.gravity.y],
346 integration_parameters: self.world.integration_parameters,
347 islands: self.world.islands.clone(),
348 broad_phase: self.world.broad_phase.clone(),
349 narrow_phase: self.world.narrow_phase.clone(),
350 bodies: self.world.bodies.clone(),
351 colliders: self.world.colliders.clone(),
352 impulse_joints: self.world.impulse_joints.clone(),
353 multibody_joints: self.world.multibody_joints.clone(),
354 },
355 map: &self.map,
356 rng: &self.rng,
357 accumulator: self.accumulator,
358 sim: self.sim.serialize(),
359 };
360
361 serde_json::to_vec(&dump).map_err(|e| e.to_string())
362 }
363
364 pub fn deserialize_state(&mut self, data: &[u8]) -> Result<(), String> {
367 let dump: EngineDumpOwned = serde_json::from_slice(data).map_err(|e| e.to_string())?;
368
369 let mut world = PhysicsWorld::new();
370
371 world.gravity = Vector::new(dump.world.gravity[0], dump.world.gravity[1]);
372 world.integration_parameters = dump.world.integration_parameters;
373 world.islands = dump.world.islands;
374 world.broad_phase = dump.world.broad_phase;
375 world.narrow_phase = dump.world.narrow_phase;
376 world.bodies = dump.world.bodies;
377 world.colliders = dump.world.colliders;
378 world.impulse_joints = dump.world.impulse_joints;
379 world.multibody_joints = dump.world.multibody_joints;
380
381 self.world = world;
382 self.map = dump.map;
383 self.rng = dump.rng;
384 self.accumulator = dump.accumulator;
385 self.sim.deserialize(dump.sim)?;
386
387 self.bodies_to_destroy.clear();
388 self.events.clear();
389
390 self.nav = self
393 .map
394 .as_ref()
395 .map(|map| NavigationSystem::generate(&map.grid, &map.physics_static, map.step));
396
397 self.sim.rebuild_spatial_grid(&self.world, &mut self.spatial);
398 self.sim.refresh_cached(&self.world);
399
400 Ok(())
401 }
402}
403
404#[derive(Serialize)]
405struct WorldDump {
406 gravity: [f32; 2],
407 integration_parameters: IntegrationParameters,
408 islands: IslandManager,
409 broad_phase: BroadPhaseBvh,
410 narrow_phase: NarrowPhase,
411 bodies: RigidBodySet,
412 colliders: ColliderSet,
413 impulse_joints: ImpulseJointSet,
414 multibody_joints: MultibodyJointSet,
415}
416
417#[derive(Deserialize)]
418struct WorldDumpOwned {
419 gravity: [f32; 2],
420 integration_parameters: IntegrationParameters,
421 islands: IslandManager,
422 broad_phase: BroadPhaseBvh,
423 narrow_phase: NarrowPhase,
424 bodies: RigidBodySet,
425 colliders: ColliderSet,
426 impulse_joints: ImpulseJointSet,
427 multibody_joints: MultibodyJointSet,
428}
429
430#[derive(Serialize)]
431struct EngineDump<'a> {
432 world: WorldDump,
433 map: &'a Option<GameMap>,
434 rng: &'a Rng,
435 accumulator: f32,
436 sim: serde_json::Value,
437}
438
439#[derive(Deserialize)]
440struct EngineDumpOwned {
441 world: WorldDumpOwned,
442 map: Option<GameMap>,
443 rng: Rng,
444 accumulator: f32,
445 sim: serde_json::Value,
446}
447
448#[cfg(test)]
455mod fixture {
456 use super::*;
457 use crate::config::FieldValue;
458 use serde::Deserialize;
459 use std::collections::{BTreeMap, BTreeSet};
460
461 #[derive(Deserialize)]
462 pub struct TestConfig {}
463
464 pub struct TestGame;
465
466 impl GameDef for TestGame {
467 type Config = TestConfig;
468 type Sim = TestSim;
469 }
470
471 #[derive(Clone, Copy)]
472 struct TestActor {
473 x: f32,
474 y: f32,
475 vx: f32,
476 vy: f32,
477 team: u8,
478 alive: bool,
479 }
480
481 pub struct TestSim {
482 actors: BTreeMap<u32, TestActor>,
483 scripted: BTreeSet<u32>,
484 }
485
486 impl GameSim<TestGame> for TestSim {
487 fn new(_cfg: &TestConfig, _engine_cfg: &EngineConfig) -> Self {
488 Self {
489 actors: BTreeMap::new(),
490 scripted: BTreeSet::new(),
491 }
492 }
493
494 fn spawn_actor(
495 &mut self,
496 _world: &mut PhysicsWorld,
497 _events: &mut Vec<CoreEvent>,
498 game_id: u32,
499 _model_name: &str,
500 team_id: u8,
501 x: f32,
502 y: f32,
503 _angle_deg: f32,
504 ) -> Result<(), String> {
505 self.actors.insert(
506 game_id,
507 TestActor { x, y, vx: 0.0, vy: 0.0, team: team_id, alive: true },
508 );
509
510 Ok(())
511 }
512
513 fn remove_actor(&mut self, _world: &mut PhysicsWorld, game_id: u32) {
514 self.actors.remove(&game_id);
515 self.scripted.remove(&game_id);
516 }
517
518 fn reset_actor(&mut self, _world: &mut PhysicsWorld, game_id: u32, team_id: u8, x: f32, y: f32, _angle_deg: f32) {
519 if let Some(actor) = self.actors.get_mut(&game_id) {
520 actor.x = x;
521 actor.y = y;
522 actor.team = team_id;
523 actor.alive = true;
524 }
525 }
526
527 fn reset_all_vitals(&mut self, _events: &mut Vec<CoreEvent>) {
528 for actor in self.actors.values_mut() {
529 actor.alive = true;
530 }
531 }
532
533 fn spawn_scripted_actor(
534 &mut self,
535 world: &mut PhysicsWorld,
536 _rng: &mut Rng,
537 events: &mut Vec<CoreEvent>,
538 game_id: u32,
539 model_name: &str,
540 team_id: u8,
541 x: f32,
542 y: f32,
543 angle_deg: f32,
544 ) -> Result<(), String> {
545 self.spawn_actor(world, events, game_id, model_name, team_id, x, y, angle_deg)?;
546 self.scripted.insert(game_id);
547
548 Ok(())
549 }
550
551 fn remove_scripted_actor(&mut self, world: &mut PhysicsWorld, game_id: u32) {
552 self.remove_actor(world, game_id);
553 }
554
555 fn apply_input(&mut self, game_id: u32, _seq: u32, action: &str, key_name: &str) {
556 let Some(actor) = self.actors.get_mut(&game_id) else {
557 return;
558 };
559
560 let magnitude = if action == "down" { 40.0 } else { 0.0 };
561
562 match key_name {
563 "forward" => actor.vy = -magnitude,
564 "back" => actor.vy = magnitude,
565 _ => {}
566 }
567 }
568
569 fn last_input_seq(&self, _game_id: u32) -> u32 {
570 0
571 }
572
573 fn is_alive(&self, game_id: u32) -> bool {
574 self.actors.get(&game_id).is_some_and(|a| a.alive)
575 }
576
577 fn actor_position(&self, _world: &PhysicsWorld, game_id: u32) -> Option<[f32; 2]> {
578 self.actors.get(&game_id).map(|a| [a.x, a.y])
579 }
580
581 fn prediction_state(&self, _world: &PhysicsWorld, game_id: u32) -> Option<([f32; PLAYER_STATE_LEN], bool)> {
582 self.actors
583 .get(&game_id)
584 .map(|a| ([a.x, a.y, 0.0, a.vx, a.vy, 0.0, 0.0, 0.0], false))
585 }
586
587 fn alive_players_flat(&self, _world: &PhysicsWorld) -> Vec<f32> {
588 self.actors
589 .iter()
590 .filter(|(_, a)| a.alive)
591 .flat_map(|(id, a)| [*id as f32, a.x, a.y])
592 .collect()
593 }
594
595 fn players_json(&self) -> String {
596 let rows: Vec<serde_json::Value> = self
597 .actors
598 .iter()
599 .map(|(id, a)| serde_json::json!({ "id": id, "x": a.x, "y": a.y, "team": a.team }))
600 .collect();
601
602 serde_json::to_string(&rows).unwrap()
603 }
604
605 fn on_fixed_step(&mut self, _ctx: &mut SimCtx, dt: f32) {
606 for actor in self.actors.values_mut() {
607 actor.x += actor.vx * dt;
608 actor.y += actor.vy * dt;
609 }
610 }
611
612 fn on_contacts(&mut self, _ctx: &mut SimCtx, _pairs: &[(ColliderHandle, ColliderHandle)]) {}
613
614 fn on_before_destroy(&mut self, _world: &PhysicsWorld, _handle: RigidBodyHandle) {}
615
616 fn on_ai_tick(&mut self, _ctx: &mut SimCtx, _dt: f32) {
617 for &id in &self.scripted {
619 if let Some(actor) = self.actors.get_mut(&id) {
620 actor.vx = 1.0;
621 }
622 }
623 }
624
625 fn refresh_cached(&mut self, _world: &PhysicsWorld) {}
626
627 fn build_snapshot_blocks(&mut self) -> (Vec<(String, Block)>, bool) {
628 let rows: Vec<(u8, Option<Vec<FieldValue>>)> = self
629 .actors
630 .iter()
631 .map(|(id, a)| (*id as u8, Some(vec![FieldValue::F32(a.x), FieldValue::F32(a.y)])))
632 .collect();
633
634 (vec![("actor".to_string(), Block::Indexed8(rows))], false)
635 }
636
637 fn remove_players_and_shots(&mut self, _world: &mut PhysicsWorld) -> Vec<String> {
638 let names: Vec<String> = self.actors.keys().map(|id| id.to_string()).collect();
639
640 self.actors.clear();
641 self.scripted.clear();
642
643 names
644 }
645
646 fn clear(&mut self) {
647 self.actors.clear();
648 self.scripted.clear();
649 }
650
651 fn serialize(&self) -> serde_json::Value {
652 let rows: Vec<serde_json::Value> = self
653 .actors
654 .iter()
655 .map(|(id, a)| serde_json::json!({ "id": id, "x": a.x, "y": a.y, "team": a.team }))
656 .collect();
657
658 serde_json::json!({ "actors": rows })
659 }
660
661 fn deserialize(&mut self, value: serde_json::Value) -> Result<(), String> {
662 let rows = value["actors"].as_array().ok_or("missing actors")?;
663
664 self.actors.clear();
665
666 for row in rows {
667 let id = row["id"].as_u64().ok_or("bad id")? as u32;
668 let x = row["x"].as_f64().ok_or("bad x")? as f32;
669 let y = row["y"].as_f64().ok_or("bad y")? as f32;
670 let team = row["team"].as_u64().ok_or("bad team")? as u8;
671
672 self.actors.insert(id, TestActor { x, y, vx: 0.0, vy: 0.0, team, alive: true });
673 }
674
675 Ok(())
676 }
677
678 fn rebuild_spatial_grid(&self, _world: &PhysicsWorld, _spatial: &mut SpatialGrid) {}
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::fixture::{TestConfig, TestGame};
685 use super::*;
686 use crate::snapshot::SnapshotPacker;
687
688 fn engine_config() -> EngineConfig {
689 serde_json::from_value(serde_json::json!({
690 "timeStep": 1.0 / 120.0,
691 "snapshot": {
692 "version": 3,
693 "port": 5,
694 "keys": {
695 "actor": { "id": 1, "kind": "indexed8", "class": "hot", "fields": [
696 { "name": "x", "ty": "f32", "interp": "lerp" },
697 { "name": "y", "ty": "f32", "interp": "lerp" }
698 ] }
699 }
700 },
701 "seed": 42
702 }))
703 .unwrap()
704 }
705
706 fn make_sim() -> EngineSim<TestGame> {
707 EngineSim::new(engine_config(), &TestConfig {})
708 }
709
710 #[test]
711 fn spawn_actor_and_fixed_step_moves_position() {
712 let mut sim = make_sim();
713
714 sim.spawn_actor(1, "m", 1, 0.0, 0.0, 0.0).unwrap();
715 sim.apply_input(1, 1, "down", "forward");
716
717 for _ in 0..60 {
718 sim.step(1.0 / 120.0);
719 }
720
721 let pos = sim.actor_position(1).unwrap();
722
723 assert!(pos[1] < 0.0); }
725
726 #[test]
727 fn remove_actor_clears_state() {
728 let mut sim = make_sim();
729
730 sim.spawn_actor(1, "m", 1, 5.0, 5.0, 0.0).unwrap();
731 assert!(sim.is_alive(1));
732
733 sim.remove_actor(1);
734 assert!(!sim.is_alive(1));
735 assert!(sim.actor_position(1).is_none());
736 }
737
738 #[test]
739 fn scripted_actor_runs_ai_tick() {
740 let mut sim = make_sim();
741
742 sim.spawn_scripted_actor(9, "m", 1, 0.0, 0.0, 0.0).unwrap();
743
744 sim.step(1.0 / 120.0);
747 sim.step(1.0 / 120.0);
748
749 let after = sim.actor_position(9).unwrap();
750
751 assert!(after[0] > 0.0); }
753
754 #[test]
755 fn build_snapshot_blocks_packs_through_generic_schema() {
756 let mut sim = make_sim();
757
758 sim.spawn_actor(1, "m", 1, 3.0, 4.0, 0.0).unwrap();
759 sim.spawn_actor(2, "m", 1, 7.0, 8.0, 0.0).unwrap();
760
761 let blocks = sim.build_snapshot_blocks();
762 let mut packer = SnapshotPacker::new(engine_config().snapshot);
763
764 assert!(packer.pack_body(&blocks).is_ok());
765 }
766
767 #[test]
768 fn clear_removes_all_actors() {
769 let mut sim = make_sim();
770
771 sim.spawn_actor(1, "m", 1, 0.0, 0.0, 0.0).unwrap();
772 sim.spawn_scripted_actor(2, "m", 1, 0.0, 0.0, 0.0).unwrap();
773
774 sim.clear();
775
776 assert!(!sim.is_alive(1));
777 assert!(!sim.is_alive(2));
778 assert_eq!(sim.alive_players_flat().len(), 0);
779 }
780
781 #[test]
782 fn serialize_deserialize_round_trips_actors() {
783 let mut sim = make_sim();
784
785 sim.spawn_actor(1, "m", 3, 11.0, 22.0, 0.0).unwrap();
786
787 let dump = sim.sim.serialize();
788 let mut restored = make_sim();
789
790 restored.sim.deserialize(dump).unwrap();
791
792 assert_eq!(restored.actor_position(1), Some([11.0, 22.0]));
793 }
794}