1use std::collections::{HashMap, HashSet};
3use std::sync::Mutex;
4
5use pite_core::NodeId;
6use rapier2d::control::{CharacterLength, KinematicCharacterController};
7use rapier2d::dynamics::CCDSolver;
8use rapier2d::prelude::*;
9
10use crate::character::CharacterMoveResult;
11use crate::query::RaycastHit;
12use crate::shape::ShapeConfig;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum PhysicsEvent {
16 BodyEntered {
17 area_id: NodeId,
18 body_id: NodeId,
19 },
20 BodyExited {
21 area_id: NodeId,
22 body_id: NodeId,
23 },
24 AreaEntered {
25 area_id: NodeId,
26 other_area_id: NodeId,
27 },
28 AreaExited {
29 area_id: NodeId,
30 other_area_id: NodeId,
31 },
32}
33
34#[derive(Default)]
35struct PhysicsEventCollector {
36 events: Mutex<Vec<CollisionEvent>>,
37}
38
39impl EventHandler for PhysicsEventCollector {
40 fn handle_collision_event(
41 &self,
42 _bodies: &RigidBodySet,
43 _colliders: &ColliderSet,
44 event: CollisionEvent,
45 _: Option<&ContactPair>,
46 ) {
47 if let Ok(mut lock) = self.events.lock() {
48 lock.push(event);
49 }
50 }
51
52 fn handle_contact_force_event(
53 &self,
54 _dt: Real,
55 _bodies: &RigidBodySet,
56 _colliders: &ColliderSet,
57 _contact_pair: &ContactPair,
58 _total_force_magnitude: Real,
59 ) {
60 }
61}
62
63pub struct PhysicsWorld {
64 pub gravity: Vector<Real>,
65 integration_parameters: IntegrationParameters,
66 physics_pipeline: PhysicsPipeline,
67 island_manager: IslandManager,
68 broad_phase: DefaultBroadPhase,
69 narrow_phase: NarrowPhase,
70 rigid_body_set: RigidBodySet,
71 collider_set: ColliderSet,
72 impulse_joint_set: ImpulseJointSet,
73 multibody_joint_set: MultibodyJointSet,
74 ccd_solver: CCDSolver,
75 query_pipeline: QueryPipeline,
76 character_controller: KinematicCharacterController,
77 event_collector: PhysicsEventCollector,
78
79 node_to_body: HashMap<NodeId, RigidBodyHandle>,
81 body_to_node: HashMap<RigidBodyHandle, NodeId>,
82 node_to_collider: HashMap<NodeId, ColliderHandle>,
83 collider_to_node: HashMap<ColliderHandle, NodeId>,
84 collider_to_body: HashMap<ColliderHandle, NodeId>,
85
86 shape_configs: HashMap<NodeId, (ShapeConfig, Vector<Real>)>,
88
89 area_nodes: HashSet<NodeId>,
91 character_nodes: HashSet<NodeId>,
92 rigid_nodes: HashSet<NodeId>,
93 static_nodes: HashSet<NodeId>,
94}
95
96impl Default for PhysicsWorld {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl PhysicsWorld {
103 pub fn new() -> Self {
104 let mut character_controller = KinematicCharacterController::default();
105 character_controller.up = -Vector::y_axis(); character_controller.offset = CharacterLength::Absolute(0.01);
107 character_controller.slide = true;
108
109 let mut integration_parameters = IntegrationParameters::default();
110 integration_parameters.dt = 1.0 / 60.0;
111
112 Self {
113 gravity: Vector::new(0.0, 980.0), integration_parameters,
115 physics_pipeline: PhysicsPipeline::new(),
116 island_manager: IslandManager::new(),
117 broad_phase: DefaultBroadPhase::new(),
118 narrow_phase: NarrowPhase::new(),
119 rigid_body_set: RigidBodySet::new(),
120 collider_set: ColliderSet::new(),
121 impulse_joint_set: ImpulseJointSet::new(),
122 multibody_joint_set: MultibodyJointSet::new(),
123 ccd_solver: CCDSolver::new(),
124 query_pipeline: QueryPipeline::new(),
125 character_controller,
126 event_collector: PhysicsEventCollector::default(),
127
128 node_to_body: HashMap::new(),
129 body_to_node: HashMap::new(),
130 node_to_collider: HashMap::new(),
131 collider_to_node: HashMap::new(),
132 collider_to_body: HashMap::new(),
133
134 shape_configs: HashMap::new(),
135
136 area_nodes: HashSet::new(),
137 character_nodes: HashSet::new(),
138 rigid_nodes: HashSet::new(),
139 static_nodes: HashSet::new(),
140 }
141 }
142
143 pub fn add_static_body(&mut self, id: NodeId, position: (f32, f32), rotation: f32) {
145 self.remove_node(&id);
146 let rb = RigidBodyBuilder::fixed()
147 .translation(Vector::new(position.0, position.1))
148 .rotation(rotation)
149 .build();
150 let handle = self.rigid_body_set.insert(rb);
151 self.node_to_body.insert(id.clone(), handle);
152 self.body_to_node.insert(handle, id.clone());
153 self.static_nodes.insert(id);
154 }
155
156 pub fn add_character_body(&mut self, id: NodeId, position: (f32, f32), rotation: f32) {
158 self.remove_node(&id);
159 let rb = RigidBodyBuilder::kinematic_position_based()
160 .translation(Vector::new(position.0, position.1))
161 .rotation(rotation)
162 .build();
163 let handle = self.rigid_body_set.insert(rb);
164 self.node_to_body.insert(id.clone(), handle);
165 self.body_to_node.insert(handle, id.clone());
166 self.character_nodes.insert(id);
167 }
168
169 pub fn add_rigid_body(
171 &mut self,
172 id: NodeId,
173 position: (f32, f32),
174 rotation: f32,
175 mass: f32,
176 gravity_scale: f32,
177 lock_rotation: bool,
178 ) {
179 self.remove_node(&id);
180 let mut builder = RigidBodyBuilder::dynamic()
181 .translation(Vector::new(position.0, position.1))
182 .rotation(rotation)
183 .gravity_scale(gravity_scale)
184 .additional_mass(mass.max(0.001));
185 if lock_rotation {
186 builder = builder.lock_rotations();
187 }
188 let handle = self.rigid_body_set.insert(builder.build());
189 self.node_to_body.insert(id.clone(), handle);
190 self.body_to_node.insert(handle, id.clone());
191 self.rigid_nodes.insert(id);
192 }
193
194 pub fn add_area(&mut self, id: NodeId, position: (f32, f32), rotation: f32) {
196 self.remove_node(&id);
197 let rb = RigidBodyBuilder::fixed()
198 .translation(Vector::new(position.0, position.1))
199 .rotation(rotation)
200 .build();
201 let handle = self.rigid_body_set.insert(rb);
202 self.node_to_body.insert(id.clone(), handle);
203 self.body_to_node.insert(handle, id.clone());
204 self.area_nodes.insert(id);
205 }
206
207 pub fn attach_shape(
209 &mut self,
210 body_id: &NodeId,
211 shape_node_id: NodeId,
212 config: ShapeConfig,
213 offset: (f32, f32),
214 rotation: f32,
215 is_sensor: bool,
216 collision_layer: u32,
217 collision_mask: u32,
218 friction: f32,
219 restitution: f32,
220 ) {
221 if let Some(&body_handle) = self.node_to_body.get(body_id) {
222 let shared_shape = config.to_shared_shape();
223 let mut collider_builder = ColliderBuilder::new(shared_shape)
224 .translation(Vector::new(offset.0, offset.1))
225 .rotation(rotation)
226 .friction(friction)
227 .restitution(restitution)
228 .active_events(ActiveEvents::COLLISION_EVENTS)
229 .active_collision_types(ActiveCollisionTypes::all());
230
231 if is_sensor || self.area_nodes.contains(body_id) {
232 collider_builder = collider_builder.sensor(true);
233 }
234
235 let memberships = Group::from_bits_truncate(collision_layer);
237 let filter = Group::from_bits_truncate(collision_mask);
238 collider_builder =
239 collider_builder.collision_groups(InteractionGroups::new(memberships, filter));
240
241 let collider = collider_builder.build();
242 let collider_handle = self.collider_set.insert_with_parent(
243 collider,
244 body_handle,
245 &mut self.rigid_body_set,
246 );
247
248 self.node_to_collider
249 .insert(shape_node_id.clone(), collider_handle);
250 self.collider_to_node
251 .insert(collider_handle, shape_node_id.clone());
252 self.collider_to_body
253 .insert(collider_handle, body_id.clone());
254 self.shape_configs
255 .insert(shape_node_id, (config, Vector::new(offset.0, offset.1)));
256 }
257 }
258
259 pub fn remove_node(&mut self, id: &NodeId) {
261 if let Some(body_handle) = self.node_to_body.remove(id) {
262 self.body_to_node.remove(&body_handle);
263 self.area_nodes.remove(id);
264 self.character_nodes.remove(id);
265 self.rigid_nodes.remove(id);
266 self.static_nodes.remove(id);
267 self.rigid_body_set.remove(
268 body_handle,
269 &mut self.island_manager,
270 &mut self.collider_set,
271 &mut self.impulse_joint_set,
272 &mut self.multibody_joint_set,
273 true,
274 );
275 }
276 if let Some(collider_handle) = self.node_to_collider.remove(id) {
277 self.collider_to_node.remove(&collider_handle);
278 self.collider_to_body.remove(&collider_handle);
279 self.shape_configs.remove(id);
280 self.collider_set.remove(
281 collider_handle,
282 &mut self.island_manager,
283 &mut self.rigid_body_set,
284 true,
285 );
286 }
287 }
288
289 pub fn step(&mut self) {
291 let physics_hooks = ();
292 self.physics_pipeline.step(
293 &self.gravity,
294 &self.integration_parameters,
295 &mut self.island_manager,
296 &mut self.broad_phase,
297 &mut self.narrow_phase,
298 &mut self.rigid_body_set,
299 &mut self.collider_set,
300 &mut self.impulse_joint_set,
301 &mut self.multibody_joint_set,
302 &mut self.ccd_solver,
303 Some(&mut self.query_pipeline),
304 &physics_hooks,
305 &self.event_collector,
306 );
307 }
308
309 pub fn move_and_slide(
311 &mut self,
312 character_id: &NodeId,
313 desired_velocity: (f32, f32),
314 dt: f32,
315 up_direction: (f32, f32),
316 ) -> CharacterMoveResult {
317 let Some(&body_handle) = self.node_to_body.get(character_id) else {
318 return CharacterMoveResult::default();
319 };
320
321 let (rb_pos, first_collider) = match self.rigid_body_set.get(body_handle) {
322 Some(b) => {
323 let t = *b.translation();
324 let c = b.colliders().first().copied();
325 (t, c)
326 }
327 None => return CharacterMoveResult::default(),
328 };
329
330 let Some(collider_handle) = first_collider else {
331 let movement = (desired_velocity.0 * dt, desired_velocity.1 * dt);
333 if let Some(b) = self.rigid_body_set.get_mut(body_handle) {
334 b.set_translation(
335 Vector::new(rb_pos.x + movement.0, rb_pos.y + movement.1),
336 true,
337 );
338 }
339 return CharacterMoveResult {
340 movement,
341 velocity: desired_velocity,
342 is_on_floor: false,
343 is_on_wall: false,
344 is_on_ceiling: false,
345 };
346 };
347
348 let collider = match self.collider_set.get(collider_handle) {
349 Some(c) => c,
350 None => return CharacterMoveResult::default(),
351 };
352
353 let shape = collider.shape();
354 let current_pos = collider.position();
355 let desired_translation = Vector::new(desired_velocity.0 * dt, desired_velocity.1 * dt);
356
357 let up_vec = Vector::new(up_direction.0, up_direction.1);
358 let up = UnitVector::try_new(up_vec, 0.0001).unwrap_or_else(|| -Vector::y_axis());
359 self.character_controller.up = up;
360
361 self.query_pipeline.update(&self.collider_set);
362
363 let filter = QueryFilter::default()
364 .exclude_rigid_body(body_handle)
365 .exclude_sensors();
366
367 let mut is_on_floor = false;
368 let mut is_on_wall = false;
369 let mut is_on_ceiling = false;
370
371 let movement = self.character_controller.move_shape(
372 dt,
373 &self.rigid_body_set,
374 &self.collider_set,
375 &self.query_pipeline,
376 shape,
377 current_pos,
378 desired_translation,
379 filter,
380 |collision| {
381 let normal = collision.hit.normal2;
382 let dot = normal.dot(up.as_ref());
383 if dot > 0.7 {
384 is_on_floor = true;
385 } else if dot < -0.7 {
386 is_on_ceiling = true;
387 } else {
388 is_on_wall = true;
389 }
390 },
391 );
392
393 if movement.grounded {
394 is_on_floor = true;
395 }
396
397 let actual_translation = movement.translation;
399 let new_pos = Vector::new(
400 rb_pos.x + actual_translation.x,
401 rb_pos.y + actual_translation.y,
402 );
403 if let Some(b) = self.rigid_body_set.get_mut(body_handle) {
404 b.set_translation(new_pos, true);
405 }
406
407 let effective_vel = if dt > 0.0 {
408 (actual_translation.x / dt, actual_translation.y / dt)
409 } else {
410 desired_velocity
411 };
412
413 CharacterMoveResult {
414 movement: (actual_translation.x, actual_translation.y),
415 velocity: effective_vel,
416 is_on_floor,
417 is_on_wall,
418 is_on_ceiling,
419 }
420 }
421
422 pub fn apply_impulse(&mut self, id: &NodeId, impulse: (f32, f32)) {
424 if let Some(&body_handle) = self.node_to_body.get(id) {
425 if let Some(body) = self.rigid_body_set.get_mut(body_handle) {
426 body.apply_impulse(Vector::new(impulse.0, impulse.1), true);
427 }
428 }
429 }
430
431 pub fn apply_force(&mut self, id: &NodeId, force: (f32, f32)) {
433 if let Some(&body_handle) = self.node_to_body.get(id) {
434 if let Some(body) = self.rigid_body_set.get_mut(body_handle) {
435 body.add_force(Vector::new(force.0, force.1), true);
436 }
437 }
438 }
439
440 pub fn is_rigid_body(&self, id: &NodeId) -> bool {
441 self.rigid_nodes.contains(id)
442 }
443
444 pub fn get_body_position(&self, id: &NodeId) -> Option<(f64, f64)> {
446 let handle = self.node_to_body.get(id)?;
447 let body = self.rigid_body_set.get(*handle)?;
448 let t = body.translation();
449 Some((t.x as f64, t.y as f64))
450 }
451
452 pub fn set_body_position(&mut self, id: &NodeId, pos: (f64, f64)) {
454 if let Some(&handle) = self.node_to_body.get(id) {
455 if let Some(body) = self.rigid_body_set.get_mut(handle) {
456 body.set_translation(Vector::new(pos.0 as f32, pos.1 as f32), true);
457 }
458 }
459 }
460
461 pub fn get_body_rotation(&self, id: &NodeId) -> Option<f64> {
463 let handle = self.node_to_body.get(id)?;
464 let body = self.rigid_body_set.get(*handle)?;
465 Some(body.rotation().angle() as f64)
466 }
467
468 pub fn raycast(&self, from: (f32, f32), to: (f32, f32), mask: u32) -> Option<RaycastHit> {
470 let ray_origin = Point::new(from.0, from.1);
471 let dir = Vector::new(to.0 - from.0, to.1 - from.1);
472 let max_toi = dir.norm();
473 if max_toi <= 0.0001 {
474 return None;
475 }
476 let ray_dir = dir / max_toi;
477 let ray = Ray::new(ray_origin, ray_dir);
478
479 let groups = InteractionGroups::new(Group::all(), Group::from_bits_truncate(mask));
480 let filter = QueryFilter::default().groups(groups);
481
482 let (handle, intersection) = self.query_pipeline.cast_ray_and_get_normal(
483 &self.rigid_body_set,
484 &self.collider_set,
485 &ray,
486 max_toi,
487 true,
488 filter,
489 )?;
490
491 let collider_node = self.collider_to_node.get(&handle)?.clone();
492 let body_node = self.collider_to_body.get(&handle).cloned();
493 let hit_point = ray.point_at(intersection.time_of_impact);
494 let normal = intersection.normal;
495
496 Some(RaycastHit {
497 point: (hit_point.x, hit_point.y),
498 normal: (normal.x, normal.y),
499 distance: intersection.time_of_impact,
500 collider_node,
501 body_node,
502 })
503 }
504
505 pub fn drain_events(&mut self) -> Vec<PhysicsEvent> {
507 let mut raw_events = Vec::new();
508 if let Ok(mut lock) = self.event_collector.events.lock() {
509 raw_events.append(&mut *lock);
510 }
511
512 let mut events = Vec::new();
513 for ev in raw_events {
514 match ev {
515 CollisionEvent::Started(h1, h2, _flags) => {
516 self.resolve_collision_pair(h1, h2, true, &mut events);
517 }
518 CollisionEvent::Stopped(h1, h2, _flags) => {
519 self.resolve_collision_pair(h1, h2, false, &mut events);
520 }
521 }
522 }
523 events
524 }
525
526 fn resolve_collision_pair(
527 &self,
528 h1: ColliderHandle,
529 h2: ColliderHandle,
530 started: bool,
531 out: &mut Vec<PhysicsEvent>,
532 ) {
533 let b1 = self.collider_to_body.get(&h1);
534 let b2 = self.collider_to_body.get(&h2);
535 let Some(node1) = b1 else { return };
536 let Some(node2) = b2 else { return };
537
538 let is_area1 = self.area_nodes.contains(node1);
539 let is_area2 = self.area_nodes.contains(node2);
540
541 if is_area1 && is_area2 {
542 if started {
543 out.push(PhysicsEvent::AreaEntered {
544 area_id: node1.clone(),
545 other_area_id: node2.clone(),
546 });
547 out.push(PhysicsEvent::AreaEntered {
548 area_id: node2.clone(),
549 other_area_id: node1.clone(),
550 });
551 } else {
552 out.push(PhysicsEvent::AreaExited {
553 area_id: node1.clone(),
554 other_area_id: node2.clone(),
555 });
556 out.push(PhysicsEvent::AreaExited {
557 area_id: node2.clone(),
558 other_area_id: node1.clone(),
559 });
560 }
561 } else if is_area1 && !is_area2 {
562 if started {
563 out.push(PhysicsEvent::BodyEntered {
564 area_id: node1.clone(),
565 body_id: node2.clone(),
566 });
567 } else {
568 out.push(PhysicsEvent::BodyExited {
569 area_id: node1.clone(),
570 body_id: node2.clone(),
571 });
572 }
573 } else if !is_area1 && is_area2 {
574 if started {
575 out.push(PhysicsEvent::BodyEntered {
576 area_id: node2.clone(),
577 body_id: node1.clone(),
578 });
579 } else {
580 out.push(PhysicsEvent::BodyExited {
581 area_id: node2.clone(),
582 body_id: node1.clone(),
583 });
584 }
585 }
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592
593 #[test]
594 fn test_character_moves_and_hits_floor() {
595 let mut world = PhysicsWorld::new();
596
597 let floor_id = NodeId::from("Floor");
599 world.add_static_body(floor_id.clone(), (0.0, 100.0), 0.0);
600 world.attach_shape(
601 &floor_id,
602 NodeId::from("FloorShape"),
603 ShapeConfig::Box {
604 width: 200.0,
605 height: 20.0,
606 },
607 (0.0, 0.0),
608 0.0,
609 false,
610 1,
611 1,
612 0.5,
613 0.0,
614 );
615
616 let player_id = NodeId::from("Player");
618 world.add_character_body(player_id.clone(), (0.0, 70.0), 0.0);
619 world.attach_shape(
620 &player_id,
621 NodeId::from("PlayerShape"),
622 ShapeConfig::Box {
623 width: 20.0,
624 height: 20.0,
625 },
626 (0.0, 0.0),
627 0.0,
628 false,
629 1,
630 1,
631 0.0,
632 0.0,
633 );
634
635 let res = world.move_and_slide(&player_id, (0.0, 50.0), 1.0, (0.0, -1.0));
637 assert!(res.is_on_floor);
638 let pos = world.get_body_position(&player_id).unwrap();
639 assert!((pos.1 - 80.0).abs() < 1.0, "Expected y ~ 80, got {}", pos.1);
641 }
642
643 #[test]
644 fn test_rigidbody_falls_under_gravity() {
645 let mut world = PhysicsWorld::new();
646 let crate_id = NodeId::from("Crate");
647 world.add_rigid_body(crate_id.clone(), (0.0, 0.0), 0.0, 1.0, 1.0, false);
648 world.attach_shape(
649 &crate_id,
650 NodeId::from("CrateShape"),
651 ShapeConfig::Box {
652 width: 10.0,
653 height: 10.0,
654 },
655 (0.0, 0.0),
656 0.0,
657 false,
658 1,
659 1,
660 0.5,
661 0.0,
662 );
663
664 for _ in 0..10 {
666 world.step();
667 }
668
669 let pos = world.get_body_position(&crate_id).unwrap();
670 assert!(pos.1 > 0.0, "RigidBody should have fallen under gravity");
671 }
672
673 #[test]
674 fn test_area2d_signals_on_overlap() {
675 let mut world = PhysicsWorld::new();
676
677 let area_id = NodeId::from("Coin");
678 world.add_area(area_id.clone(), (0.0, 0.0), 0.0);
679 world.attach_shape(
680 &area_id,
681 NodeId::from("CoinShape"),
682 ShapeConfig::Circle { radius: 20.0 },
683 (0.0, 0.0),
684 0.0,
685 true,
686 1,
687 1,
688 0.0,
689 0.0,
690 );
691
692 let body_id = NodeId::from("Ball");
693 world.add_rigid_body(body_id.clone(), (0.0, 0.0), 0.0, 1.0, 0.0, false);
694 world.attach_shape(
695 &body_id,
696 NodeId::from("BallShape"),
697 ShapeConfig::Circle { radius: 10.0 },
698 (0.0, 0.0),
699 0.0,
700 false,
701 1,
702 1,
703 0.0,
704 0.0,
705 );
706
707 world.step();
708 let events = world.drain_events();
709 assert!(
710 events.contains(&PhysicsEvent::BodyEntered {
711 area_id: area_id.clone(),
712 body_id: body_id.clone()
713 }),
714 "Expected BodyEntered event for area and body"
715 );
716 }
717
718 #[test]
719 fn test_character_triggers_area() {
720 let mut world = PhysicsWorld::new();
721 let area_id = NodeId::from("Area");
722 world.add_area(area_id.clone(), (0.0, 20.0), 0.0);
723 world.attach_shape(
724 &area_id,
725 NodeId::from("AreaShape"),
726 ShapeConfig::Box {
727 width: 50.0,
728 height: 50.0,
729 },
730 (0.0, 0.0),
731 0.0,
732 true,
733 1,
734 1,
735 0.0,
736 0.0,
737 );
738
739 let char_id = NodeId::from("Char");
740 world.add_character_body(char_id.clone(), (0.0, 0.0), 0.0);
741 world.attach_shape(
742 &char_id,
743 NodeId::from("CharShape"),
744 ShapeConfig::Box {
745 width: 20.0,
746 height: 20.0,
747 },
748 (0.0, 0.0),
749 0.0,
750 false,
751 1,
752 1,
753 0.0,
754 0.0,
755 );
756
757 world.move_and_slide(&char_id, (0.0, 200.0), 1.0 / 60.0, (0.0, -1.0));
758 world.step();
759 let events = world.drain_events();
760 println!("Events: {events:?}");
761 assert!(!events.is_empty());
762 }
763
764 #[test]
765 fn test_raycast_detects_solid() {
766 let mut world = PhysicsWorld::new();
767
768 let wall_id = NodeId::from("Wall");
769 world.add_static_body(wall_id.clone(), (100.0, 0.0), 0.0);
770 world.attach_shape(
771 &wall_id,
772 NodeId::from("WallShape"),
773 ShapeConfig::Box {
774 width: 20.0,
775 height: 100.0,
776 },
777 (0.0, 0.0),
778 0.0,
779 false,
780 1,
781 1,
782 0.0,
783 0.0,
784 );
785
786 world.step(); let hit = world
790 .raycast((0.0, 0.0), (200.0, 0.0), 1)
791 .expect("ray should hit wall");
792 assert_eq!(hit.collider_node, NodeId::from("WallShape"));
793 assert_eq!(hit.body_node, Some(wall_id));
794 assert!((hit.point.0 - 90.0).abs() < 1.0);
796 assert_eq!(hit.normal, (-1.0, 0.0));
797
798 let miss = world.raycast((0.0, 0.0), (200.0, 0.0), 2);
800 assert!(miss.is_none());
801 }
802}