1#[cfg(feature = "rerun")]
2use alloc::vec::Vec;
3use core::fmt;
4use core::ops;
5
6use bevy_ecs::prelude as ecs;
7use euclid::{Point3D, Vector3D};
8use ordered_float::NotNan;
9
10#[cfg(not(any(feature = "std", test)))]
12#[allow(
13 unused_imports,
14 reason = "unclear why this warns even though it is needed"
15)]
16use num_traits::float::Float as _;
17
18use super::collision::{
19 Contact, aab_raycast, collide_along_ray, escape_along_ray, find_colliding_cubes, nudge_on_ray,
20};
21use crate::block::{BlockCollision, Resolution};
22use crate::camera::Eye;
23use crate::fluff::Fluff;
24#[cfg(not(any(feature = "std", test)))]
25#[allow(
26 unused_imports,
27 reason = "unclear why this warns even though it is needed"
28)]
29use crate::math::Euclid as _;
30use crate::math::{
31 Aab, Cube, Face6, Face7, FreeCoordinate, FreePoint, FreeVector, PositiveSign, notnan,
32};
33use crate::physics::step::PhysicsOutputs;
34use crate::physics::{POSITION_EPSILON, StopAt, Velocity};
35use crate::raycast::Ray;
36use crate::rerun_glue as rg;
37use crate::space;
38use crate::time::Tick;
39use crate::transaction::{self, Equal, Transaction};
40use crate::util::{ConciseDebug, Fmt, Refmt as _, StatusText};
41
42const VELOCITY_EPSILON_SQUARED: NotNan<FreeCoordinate> = notnan!(1e-12);
46
47pub(crate) const VELOCITY_MAGNITUDE_LIMIT: FreeCoordinate = 1e4_f64;
52pub(crate) const VELOCITY_MAGNITUDE_LIMIT_SQUARED: FreeCoordinate =
53 VELOCITY_MAGNITUDE_LIMIT * VELOCITY_MAGNITUDE_LIMIT;
54
55#[derive(Clone, PartialEq, ecs::Component)]
58#[require(PhysicsOutputs, rg::Destination)]
59#[non_exhaustive]
60pub struct Body {
61 position: Point3D<NotNan<FreeCoordinate>, Cube>,
71
72 velocity: Vector3D<NotNan<FreeCoordinate>, Velocity>,
76
77 collision_box: Aab,
85
86 occupying: Aab,
94
95 pub flying: bool,
97 pub noclip: bool,
99
100 pub yaw: FreeCoordinate,
107
108 pub pitch: FreeCoordinate,
115}
116
117impl fmt::Debug for Body {
118 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
119 let Self {
120 position,
121 velocity,
122 collision_box,
123 occupying,
124 flying,
125 noclip,
126 yaw,
127 pitch,
128 } = self;
129 fmt.debug_struct("Body")
130 .field("position", &position.refmt(&ConciseDebug))
131 .field("velocity", &velocity.refmt(&ConciseDebug))
132 .field("collision_box", &collision_box)
133 .field("occupying", &occupying)
134 .field("flying", &flying)
135 .field("noclip", &noclip)
136 .field("yaw", &yaw)
137 .field("pitch", &pitch)
138 .finish()
139 }
140}
141
142impl Fmt<StatusText> for Body {
144 fn fmt(&self, fmt: &mut fmt::Formatter<'_>, _: &StatusText) -> fmt::Result {
145 let &Self {
146 position,
147 velocity,
148 collision_box: _,
149 occupying: _,
150 flying,
151 noclip,
152 yaw,
153 pitch,
154 } = self;
155 let dir_face = Face6::from_snapped_vector(self.look_direction()).unwrap();
156 write!(
157 fmt,
158 "Position: {} Yaw: {yaw:5.1}° Pitch: {pitch:5.1}°\n\
159 Velocity: {} Nearest axis to eye: {dir_face:?}",
160 position.refmt(&ConciseDebug),
161 velocity.refmt(&ConciseDebug),
162 )?;
163 if flying {
164 write!(fmt, " Flying")?;
165 }
166 if noclip {
167 write!(fmt, " Noclip")?;
168 }
169 Ok(())
170 }
171}
172
173impl Body {
174 #[track_caller]
180 pub fn new_minimal(position: impl Into<FreePoint>, collision_box: impl Into<Aab>) -> Self {
181 let position = position.into();
182 assert!(position.is_finite(), "body’s position must be finite");
183 let position = position.map(|c| NotNan::new(c).unwrap()); let collision_box = collision_box.into();
186 Self {
187 position,
188 velocity: Vector3D::zero(),
189 collision_box,
190 occupying: collision_box.translate(position.map(|c| c.into_inner()).to_vector()),
192 flying: false,
193 noclip: false,
194 yaw: 0.0,
195 pitch: 0.0,
196 }
197 }
198
199 #[cfg(test)]
201 pub(crate) fn step<CC>(
202 &mut self,
203 tick: Tick,
204 colliding_space: Option<&space::Read<'_>>,
205 collision_callback: CC,
206 ) -> BodyStepDetails
207 where
208 CC: FnMut(Contact),
209 {
210 self.step_with_rerun(
211 tick,
212 Vector3D::zero(),
213 colliding_space,
214 collision_callback,
215 &Default::default(),
216 )
217 }
218
219 pub(crate) fn step_with_rerun<CC>(
230 &mut self,
231 tick: Tick,
232 external_delta_v: Vector3D<NotNan<FreeCoordinate>, Velocity>,
233 mut colliding_space: Option<&space::Read<'_>>,
234 mut collision_callback: CC,
235 rerun_destination: &crate::rerun_glue::Destination,
236 ) -> BodyStepDetails
237 where
238 CC: FnMut(Contact),
239 {
240 #[cfg(not(feature = "rerun"))]
241 let _ = rerun_destination;
242
243 let velocity_before_gravity_and_collision = self.velocity;
244 let dt = NotNan::new(tick.delta_t().as_secs_f64()).unwrap();
245 let mut move_segments = [MoveSegment::default(); 3];
246 let mut move_segment_index = 0;
247 let mut already_colliding = None;
248 #[cfg(feature = "rerun")]
249 let mut contact_accum: Vec<Contact> = Vec::new();
250
251 self.velocity += external_delta_v;
252
253 if self.noclip {
254 colliding_space = None;
255 }
256
257 let mut collision_callback = |contact: Contact| {
258 if contact.normal() == Face7::Within {
259 already_colliding = Some(contact);
260 }
261 collision_callback(contact);
262
263 #[cfg(feature = "rerun")]
264 contact_accum.push(contact);
265 };
266
267 if !self.position.to_vector().square_length().is_finite() {
268 return BodyStepDetails {
270 quiescent: false,
271 already_colliding,
272 push_out: None,
273 move_segments,
274 delta_v: Vector3D::zero(),
275 };
276 }
277
278 if !self.flying
279 && !tick.paused()
280 && let Some(space) = colliding_space
281 {
282 self.velocity += space.physics().gravity.cast_unit() * dt;
283 }
284
285 #[cfg(feature = "rerun")]
288 let position_before_push_out = self.position;
289 let push_out_info = if let Some(space) = colliding_space {
290 self.push_out(space)
291 } else {
292 None
293 };
294
295 let velocity_magnitude_squared = self.velocity.square_length();
296 if !velocity_magnitude_squared.is_finite() {
297 self.velocity = Vector3D::zero();
298 } else if velocity_magnitude_squared <= VELOCITY_EPSILON_SQUARED || tick.paused() {
299 return BodyStepDetails {
300 quiescent: true,
301 already_colliding,
302 push_out: push_out_info,
303 move_segments,
304 delta_v: self.velocity - velocity_before_gravity_and_collision,
305 };
306 } else if velocity_magnitude_squared.into_inner() > VELOCITY_MAGNITUDE_LIMIT_SQUARED {
307 self.velocity *=
308 NotNan::new(VELOCITY_MAGNITUDE_LIMIT / velocity_magnitude_squared.sqrt()).unwrap();
309 }
310
311 let unobstructed_delta_position: Vector3D<_, _> =
313 self.velocity.map(NotNan::into_inner).cast_unit() * dt.into_inner();
314
315 #[cfg(feature = "rerun")]
317 let position_before_move_segments = self.position;
318 if let Some(space) = colliding_space {
319 let mut delta_position = unobstructed_delta_position;
320 while delta_position != Vector3D::zero() {
321 assert!(
322 move_segment_index < 3,
323 "sliding collision loop did not finish"
324 );
325 let (new_delta_position, segment) =
328 self.collide_and_advance(space, &mut collision_callback, delta_position);
329 delta_position = new_delta_position;
330
331 move_segments[move_segment_index] = segment;
333
334 move_segment_index += 1;
335 }
336 } else {
337 self.set_position(self.position.map(NotNan::into_inner) + unobstructed_delta_position);
338 move_segments[0] = MoveSegment {
339 delta_position: unobstructed_delta_position,
340 stopped_by: None,
341 };
342 }
343
344 let info = BodyStepDetails {
347 quiescent: false,
348 already_colliding,
349 push_out: push_out_info,
350 move_segments,
351 delta_v: self.velocity - velocity_before_gravity_and_collision,
352 };
353
354 #[cfg(feature = "rerun")]
355 {
356 use crate::content::palette;
357
358 rerun_destination.log(
360 &rg::entity_path!["step_info"],
361 &rg::archetypes::TextDocument::new(format!("{:#?}", info.refmt(&ConciseDebug))),
362 );
363
364 if let Some(space) = colliding_space {
366 let cubes = self
369 .collision_box_abs()
370 .expand(0.875)
371 .round_up_to_grid()
372 .interior_iter()
373 .filter(|cube| {
374 space.get_evaluated(*cube).uniform_collision() != Some(BlockCollision::None)
375 });
376 let (class_ids, colors): (Vec<rg::ClassId>, Vec<_>) = cubes
377 .clone()
378 .map(|cube| {
379 let this_contact =
381 contact_accum.iter().find(|contact| contact.cube() == cube);
382 if let Some(this_contact) = this_contact {
383 if this_contact.normal() == Face7::Within {
384 (
385 rg::ClassId::CollisionContactWithin,
386 palette::DEBUG_COLLISION_CUBE_WITHIN,
387 )
388 } else {
389 (
390 rg::ClassId::CollisionContactAgainst,
391 palette::DEBUG_COLLISION_CUBE_AGAINST,
392 )
393 }
394 } else {
395 (rg::ClassId::SpaceBlock, space.get_evaluated(cube).color())
396 }
397 })
398 .unzip();
399 rerun_destination.log(
400 &rg::entity_path!["blocks"],
401 &rg::convert_aabs(
402 cubes.map(|cube| {
403 let ev = space.get_evaluated(cube);
404 ev.voxels_bounds()
406 .to_free()
407 .scale(ev.voxels().resolution().recip_f64())
408 .translate(cube.lower_bounds().to_f64().to_vector())
409 }),
410 FreeVector::zero(),
411 )
412 .with_radii(class_ids.iter().map(|class| match class {
413 rg::ClassId::SpaceBlock => 0.001,
414 rg::ClassId::CollisionContactAgainst => 0.002,
415 rg::ClassId::CollisionContactWithin => 0.005,
416 _ => unreachable!(),
417 }))
418 .with_class_ids(class_ids)
419 .with_colors(colors),
420 );
421 }
422
423 rerun_destination.log(
425 &rg::EntityPath::new(vec![]),
426 &rg::archetypes::Points3D::new([rg::convert_point(self.position)]),
427 );
428
429 rerun_destination.log(
431 &rg::entity_path!["collision_box"],
432 &rg::convert_aabs(
433 [self.collision_box],
434 self.position.map(NotNan::into_inner).to_vector(),
435 )
436 .with_class_ids([rg::ClassId::BodyCollisionBox]),
437 );
438 rerun_destination.log(
439 &rg::entity_path!["occupying"],
440 &rg::convert_aabs([self.occupying], FreeVector::zero())
441 .with_class_ids([rg::ClassId::BodyCollisionBox]),
442 );
443
444 let arrow_offsets = || self.collision_box.corner_points().map(|p| p.to_vector());
447
448 match push_out_info {
451 Some(push_out_vector) => rerun_destination.log(
452 &rg::entity_path!["push_out"],
453 &rg::archetypes::Arrows3D::from_vectors(
454 arrow_offsets().map(|_| rg::convert_vec(push_out_vector)),
455 )
456 .with_origins(arrow_offsets().map(|offset| {
457 rg::convert_point(position_before_push_out.map(NotNan::into_inner) + offset)
458 })),
459 ),
460 None => rerun_destination.clear_recursive(&rg::entity_path!["push_out"]),
461 }
462
463 {
465 let move_segments = &move_segments[..move_segment_index]; rerun_destination.log(
467 &rg::entity_path!["move_segment"],
468 &rg::archetypes::Arrows3D::from_vectors(arrow_offsets().flat_map(|_| {
469 move_segments.iter().map(|seg| rg::convert_vec(seg.delta_position))
470 }))
471 .with_origins(arrow_offsets().flat_map(|offset| {
472 move_segments.iter().scan(
473 position_before_move_segments.map(NotNan::into_inner) + offset,
474 |pos, seg| {
475 let arrow_origin = rg::convert_point(*pos);
476 *pos += seg.delta_position;
477 Some(arrow_origin)
478 },
479 )
480 })),
481 );
482 }
483 }
484
485 info
486 }
487
488 fn collide_and_advance<CC>(
491 &mut self,
492 space: &space::Read<'_>,
493 collision_callback: &mut CC,
494 mut delta_position: FreeVector,
495 ) -> (FreeVector, MoveSegment)
496 where
497 CC: FnMut(Contact),
498 {
499 let movement_ignoring_collision =
500 Ray::new(self.position.map(NotNan::into_inner), delta_position);
501 let collision = collide_along_ray(
502 space,
503 movement_ignoring_collision,
504 self.collision_box, collision_callback,
506 StopAt::NotAlreadyColliding,
507 );
508
509 if let Some(collision) = collision {
510 let axis = collision
511 .contact
512 .normal()
513 .axis()
514 .expect("Face7::Within collisions should not reach here");
515 let motion_segment = nudge_on_ray(
519 self.collision_box, movement_ignoring_collision.scale_direction(collision.t_distance),
521 collision.contact.normal().opposite(),
522 collision.contact.resolution(),
523 true,
524 );
525 let unobstructed_delta_position = motion_segment.direction;
526 self.set_position(self.position.map(NotNan::into_inner) + unobstructed_delta_position);
527 delta_position -= unobstructed_delta_position;
529 delta_position[axis] = 0.0;
531
532 self.velocity[axis] = notnan!(0.0);
536
537 (
538 delta_position,
539 MoveSegment {
540 delta_position: unobstructed_delta_position,
541 stopped_by: Some(collision.contact),
542 },
543 )
544 } else {
545 self.set_position(self.position.map(NotNan::into_inner) + delta_position);
547 (
548 Vector3D::zero(),
549 MoveSegment {
550 delta_position,
551 stopped_by: None,
552 },
553 )
554 }
555 }
556
557 fn push_out(&mut self, space: &space::Read<'_>) -> Option<FreeVector> {
559 let colliding = find_colliding_cubes(space, self.collision_box_abs()).next().is_some();
562 if colliding {
563 let exit_backwards: FreeVector = -self.velocity.map(NotNan::into_inner).cast_unit(); let shortest_push_out = (-1..=1)
565 .flat_map(move |dx| {
566 (-1..=1).flat_map(move |dy| {
567 (-1..=1).map(move |dz| {
568 let direction = Vector3D::new(dx, dy, dz).map(FreeCoordinate::from);
569 if direction == Vector3D::zero() {
570 exit_backwards
573 } else {
574 direction
575 }
576 })
577 })
578 })
579 .filter_map(|direction| self.attempt_push_out(space, direction))
580 .min_by_key(|(_, distance)| *distance);
581
582 if let Some((new_position, _)) = shortest_push_out {
583 let old_position: FreePoint = self.position.map(NotNan::into_inner);
584 self.set_position(new_position);
585 return Some(new_position - old_position);
586 }
587 }
588 None
589 }
590
591 fn attempt_push_out(
594 &self,
595 space: &space::Read<'_>,
596 direction: FreeVector,
597 ) -> Option<(FreePoint, NotNan<FreeCoordinate>)> {
598 if false {
599 let direction = direction.normalize(); if direction.x.is_nan() {
605 return None;
608 }
609
610 let ray = Ray::new(self.position.map(NotNan::into_inner), direction);
611
612 let end = escape_along_ray(
613 space,
614 ray,
615 self.collision_box, )?;
617
618 let nudged_distance = end.t_distance + POSITION_EPSILON;
619 Some((
620 ray.scale_direction(nudged_distance).unit_endpoint(),
621 NotNan::new(nudged_distance).ok()?,
622 ))
623 } else {
624 let ray = Ray::new(self.position.map(NotNan::into_inner), direction);
625 'raycast: for ray_step in
627 aab_raycast(self.collision_box , ray, true)
628 {
629 let adjusted_segment = nudge_on_ray(
630 self.collision_box, ray.scale_direction(ray_step.t_distance()),
632 ray_step.face(),
633 Resolution::R1,
634 true,
635 );
636 let step_aab = self
637 .collision_box .translate(adjusted_segment.unit_endpoint().to_vector());
639 for cube in step_aab.round_up_to_grid().interior_iter() {
640 match space.get_evaluated(cube).uniform_collision() {
642 Some(BlockCollision::Hard) => {
643 continue 'raycast;
645 }
646 Some(BlockCollision::None) => {}
647 None => {
648 }
651 }
652 }
653 return Some((
655 adjusted_segment.unit_endpoint(),
656 NotNan::new(ray_step.t_distance() * direction.length()).ok()?,
657 ));
658 }
659
660 None
661 }
662 }
663
664 pub fn position(&self) -> FreePoint {
668 self.position.map(NotNan::into_inner)
669 }
670
671 pub fn set_position(&mut self, position: FreePoint) {
679 if !position.is_finite() {
680 return;
681 }
682
683 self.position = position.map(|c| NotNan::new(c).unwrap());
684
685 self.occupying =
688 self.collision_box.translate(self.position.map(NotNan::into_inner).to_vector());
689 }
690
691 pub fn velocity(&self) -> Vector3D<f64, Velocity> {
693 self.velocity.map(NotNan::into_inner)
694 }
695
696 #[allow(non_snake_case)]
701 pub fn add_velocity(&mut self, Δv: Vector3D<f64, Velocity>) {
702 if !Δv.is_finite() {
703 return;
704 }
705
706 self.velocity += Δv.map(|c| NotNan::new(c).unwrap());
707 }
708
709 pub fn set_velocity(&mut self, v: Vector3D<f64, Velocity>) {
714 if !v.is_finite() {
715 return;
716 }
717
718 self.velocity = v.map(|c| NotNan::new(c).unwrap());
719 }
720
721 pub fn collision_box_rel(&self) -> Aab {
734 self.collision_box
735 }
736
737 pub fn collision_box_abs(&self) -> Aab {
754 self.occupying
755 }
756
757 pub(crate) fn look_rotation(&self) -> euclid::Rotation3D<f64, Eye, Cube> {
758 euclid::Rotation3D::<_, Eye, Cube>::around_x(euclid::Angle {
759 radians: -self.pitch.to_radians(),
760 })
761 .then(&euclid::Rotation3D::around_y(euclid::Angle {
762 radians: -self.yaw.to_radians(),
763 }))
764 }
765
766 pub fn look_direction(&self) -> FreeVector {
768 self.look_rotation().transform_vector3d(Vector3D::new(0., 0., -1.))
769 }
770
771 pub fn set_look_direction(&mut self, direction: FreeVector) {
776 let horizontal_distance = direction.x.hypot(direction.z);
777
778 self.yaw = (180.0 - (direction.x).atan2(direction.z).to_degrees()).rem_euclid(360.0);
779 self.pitch = -(direction.y).atan2(horizontal_distance).to_degrees();
780 }
781
782 pub(crate) fn is_on_ground(&self, po: &PhysicsOutputs) -> bool {
784 self.velocity().y <= 0.0
785 && po.colliding_cubes.iter().any(|contact| contact.normal() == Face7::PY)
786 }
787}
788
789#[cfg(feature = "save")]
790impl serde::Serialize for Body {
791 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
792 where
793 S: serde::Serializer,
794 {
795 let &Body {
796 position,
797 velocity,
798 collision_box,
799 occupying,
800 flying,
801 noclip,
802 yaw,
803 pitch,
804 } = self;
805 crate::save::schema::BodySer::BodyV1 {
806 position: position.into(),
807 velocity: velocity.into(),
808 collision_box,
809 occupying,
810 flying,
811 noclip,
812 yaw,
813 pitch,
814 }
815 .serialize(serializer)
816 }
817}
818
819#[cfg(feature = "save")]
820impl<'de> serde::Deserialize<'de> for Body {
821 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
822 where
823 D: serde::Deserializer<'de>,
824 {
825 match crate::save::schema::BodySer::deserialize(deserializer)? {
826 crate::save::schema::BodySer::BodyV1 {
827 position,
828 velocity,
829 collision_box,
830 occupying,
831 flying,
832 noclip,
833 yaw,
834 pitch,
835 } => Ok(Body {
836 position: position.into(),
837 velocity: velocity.into(),
838 collision_box,
839 occupying,
840 flying,
841 noclip,
842 yaw,
843 pitch,
844 }),
845 }
846 }
847}
848
849#[derive(Clone, Copy, Debug, Default, PartialEq)]
853pub(crate) struct BodyStepInfo {
854 pub(crate) count: usize,
856}
857
858impl Fmt<StatusText> for BodyStepInfo {
859 fn fmt(&self, f: &mut fmt::Formatter<'_>, _: &StatusText) -> fmt::Result {
860 let Self { count } = self;
861 write!(f, "{count} bodies' steps")
862 }
863}
864
865impl ops::AddAssign for BodyStepInfo {
866 fn add_assign(&mut self, other: Self) {
867 let Self { count } = self;
868 *count += other.count;
869 }
870}
871
872#[derive(Clone, Copy, Debug)]
874#[non_exhaustive]
875#[doc(hidden)] pub struct BodyStepDetails {
877 quiescent: bool,
879
880 #[doc(hidden)] pub push_out: Option<FreeVector>,
884
885 already_colliding: Option<Contact>,
886
887 move_segments: [MoveSegment; 3],
890
891 delta_v: Vector3D<NotNan<f64>, Velocity>,
893}
894
895impl Fmt<ConciseDebug> for BodyStepDetails {
896 fn fmt(&self, fmt: &mut fmt::Formatter<'_>, fopt: &ConciseDebug) -> fmt::Result {
897 fmt.debug_struct("BodyStepDetails")
898 .field("quiescent", &self.quiescent)
899 .field("already_colliding", &self.already_colliding)
900 .field("push_out", &self.push_out.as_ref().map(|v| v.refmt(fopt)))
901 .field("move_segments", &self.move_segments.refmt(fopt))
902 .field("delta_v", &self.delta_v.refmt(fopt))
903 .finish()
904 }
905}
906
907impl BodyStepDetails {
908 pub(crate) fn impact_fluff(&self) -> Option<Fluff> {
909 let velocity = self.delta_v.map(NotNan::into_inner).length();
910 if velocity >= 0.25 && self.move_segments.iter().any(|s| s.stopped_by.is_some()) {
912 Some(Fluff::BlockImpact {
913 velocity: PositiveSign::try_from(velocity as f32).ok()?,
914 })
915 } else {
916 None
917 }
918 }
919}
920
921#[derive(Clone, Copy, Debug)]
923#[non_exhaustive]
924pub(crate) struct MoveSegment {
925 pub delta_position: FreeVector,
927 pub stopped_by: Option<Contact>,
931}
932
933impl Fmt<ConciseDebug> for MoveSegment {
934 fn fmt(&self, fmt: &mut fmt::Formatter<'_>, fopt: &ConciseDebug) -> fmt::Result {
935 let mut nonempty = false;
936 if self.delta_position != FreeVector::zero() {
937 nonempty = true;
938 write!(fmt, "move {:?}", self.delta_position.refmt(fopt))?;
939 }
940 if let Some(stopped_by) = &self.stopped_by {
941 if nonempty {
942 write!(fmt, " ")?;
943 }
944 nonempty = true;
945 write!(fmt, "stopped by {stopped_by:?}")?;
946 }
947 if !nonempty {
948 write!(fmt, "0")?;
949 }
950 Ok(())
951 }
952}
953
954impl Default for MoveSegment {
955 fn default() -> Self {
956 Self {
957 delta_position: Vector3D::zero(),
958 stopped_by: None,
959 }
960 }
961}
962
963#[derive(Clone, Debug, Default, PartialEq)]
967#[must_use]
968#[non_exhaustive]
969pub struct BodyTransaction {
970 set_position: Equal<FreePoint>,
971 set_look_direction: Equal<FreeVector>,
972}
973
974#[allow(missing_docs)] impl BodyTransaction {
976 #[inline]
977 pub fn with_position(mut self, position: FreePoint) -> Self {
978 self.set_position = Equal(Some(position));
979 self
980 }
981
982 #[inline]
983 pub fn with_look_direction(mut self, direction: FreeVector) -> Self {
984 self.set_look_direction = Equal(Some(direction));
985 self
986 }
987}
988
989impl transaction::Transactional for Body {
990 type Transaction = BodyTransaction;
991}
992
993impl Transaction for BodyTransaction {
994 type Target = Body;
995 type Context<'a> = ();
996 type CommitCheck = ();
997 type Output = transaction::NoOutput;
998 type Mismatch = BodyMismatch;
999
1000 fn check(
1001 &self,
1002 _body: &Body,
1003 (): Self::Context<'_>,
1004 ) -> Result<Self::CommitCheck, Self::Mismatch> {
1005 Ok(())
1007 }
1008
1009 fn commit(
1010 self,
1011 body: &mut Body,
1012 (): Self::CommitCheck,
1013 _outputs: &mut dyn FnMut(Self::Output),
1014 ) -> Result<(), transaction::CommitError> {
1015 let Self {
1016 set_position,
1017 set_look_direction,
1018 } = self;
1019 if let Equal(Some(position)) = set_position {
1020 body.set_position(position);
1021 }
1022 if let Equal(Some(direction)) = set_look_direction {
1023 body.set_look_direction(direction);
1024 }
1025 Ok(())
1026 }
1027}
1028
1029impl transaction::Merge for BodyTransaction {
1030 type MergeCheck = ();
1031 type Conflict = BodyConflict;
1032
1033 fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
1034 let Self {
1035 set_position,
1036 set_look_direction,
1037 } = self;
1038 let conflict = BodyConflict {
1039 position: set_position.check_merge(&other.set_position).is_err(),
1040 look_direction: set_look_direction.check_merge(&other.set_look_direction).is_err(),
1041 };
1042 if conflict
1043 != (BodyConflict {
1044 position: false,
1045 look_direction: false,
1046 })
1047 {
1048 return Err(conflict);
1049 }
1050
1051 Ok(())
1052 }
1053
1054 fn commit_merge(&mut self, other: Self, (): Self::MergeCheck) {
1055 let Self {
1056 set_position,
1057 set_look_direction,
1058 } = self;
1059 set_position.commit_merge(other.set_position, ());
1060 set_look_direction.commit_merge(other.set_look_direction, ());
1061 }
1062}
1063
1064#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
1066#[non_exhaustive]
1067pub enum BodyMismatch {}
1068
1069impl core::error::Error for BodyMismatch {
1070 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
1071 match *self {}
1072 }
1073}
1074
1075#[derive(Clone, Debug, Eq, PartialEq)]
1079pub struct BodyConflict {
1080 position: bool,
1081 look_direction: bool,
1082}
1083
1084impl core::error::Error for BodyConflict {}
1085
1086impl fmt::Display for BodyConflict {
1087 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1088 match *self {
1089 BodyConflict {
1090 position: true,
1091 look_direction: true,
1092 } => {
1093 write!(f, "conflicting changes to position and look direction")
1094 }
1095 BodyConflict {
1096 position: true,
1097 look_direction: false,
1098 } => {
1099 write!(f, "conflicting changes to position")
1100 }
1101 BodyConflict {
1102 position: false,
1103 look_direction: true,
1104 } => {
1105 write!(f, "conflicting changes to look direction")
1106 }
1107 BodyConflict {
1108 position: false,
1109 look_direction: false,
1110 } => {
1111 unreachable!()
1112 }
1113 }
1114 }
1115}
1116
1117#[cfg(test)]
1119mod tests {
1120 use super::*;
1121 use crate::transaction::{PredicateRes, TransactionTester};
1122 use euclid::{point3, vec3};
1123
1124 fn test_body() -> Body {
1125 Body {
1126 flying: false,
1127 noclip: false,
1128 ..Body::new_minimal([0., 2., 0.], Aab::new(-0.5, 0.5, -0.5, 0.5, -0.5, 0.5))
1129 }
1130 }
1131
1132 #[test]
1133 fn look_direction() {
1134 let do_test = |direction: [f64; 3], yaw, pitch| {
1135 let mut body = Body::new_minimal([10., 0., 0.], Aab::ZERO);
1136 body.set_look_direction(direction.into());
1137 println!("{direction:?} {yaw} {pitch}");
1138 assert_eq!(body.yaw, yaw);
1139 assert_eq!(body.pitch, pitch);
1140 };
1141
1142 do_test([0., 0., -1.], 0., 0.);
1143 do_test([1., 0., -1.], 45., 0.);
1144 do_test([1., 0., 0.], 90., 0.);
1145 do_test([0., 0., 1.], 180., 0.);
1146 do_test([-1., 0., 0.], 270., 0.);
1147
1148 let exactly_vertical_yaw = 180.;
1150 do_test([0., 1., 0.], exactly_vertical_yaw, -90.);
1151 do_test([0., 1., -1.], 0., -45.);
1152 do_test([0., 0., -1.], 0., 0.);
1153 do_test([0., -1., -1.], 0., 45.);
1154 do_test([0., -1., 0.], exactly_vertical_yaw, 90.);
1155 }
1156
1157 #[test]
1158 fn body_transaction_systematic() {
1159 fn check_position(expected: FreePoint) -> impl Fn(&Body, &Body) -> PredicateRes {
1160 move |_, after| {
1161 let actual = after.position.map(NotNan::into_inner);
1162 if actual != expected {
1163 return Err(format!("expected position {expected:#?}, got {actual:#?}").into());
1164 }
1165 if !after.occupying.contains(actual) {
1166 return Err("bad occupying".into());
1167 }
1168 Ok(())
1169 }
1170 }
1171 fn check_look_direction(expected: FreeVector) -> impl Fn(&Body, &Body) -> PredicateRes {
1172 move |_, after| {
1173 let actual = after.look_direction();
1174 if actual.angle_to(expected) > euclid::Angle::degrees(0.001) {
1176 return Err(
1177 format!("expected look direction {expected:#?}, got {actual:#?}").into(),
1178 );
1179 }
1180 Ok(())
1181 }
1182 }
1183
1184 TransactionTester::new()
1185 .transaction(BodyTransaction::default(), |_, _| Ok(()))
1186 .transaction(
1187 BodyTransaction::default().with_position(point3(0., 0., 0.)),
1188 check_position(point3(0., 0., 0.)),
1189 )
1190 .transaction(
1191 BodyTransaction::default().with_position(point3(1., 0., 0.)),
1192 check_position(point3(1., 0., 0.)),
1193 )
1194 .transaction(
1195 BodyTransaction::default().with_look_direction(vec3(1., 0., 0.)),
1196 check_look_direction(vec3(1., 0., 0.)),
1197 )
1198 .transaction(
1199 BodyTransaction::default().with_look_direction(vec3(0., 1., 0.)),
1200 check_look_direction(vec3(0., 1., 0.)),
1201 )
1202 .target(test_body)
1203 .test(());
1204 }
1205}