Skip to main content

alice_physics/solver/
world_snapshot.rs

1//! Whole-world snapshot: every piece of [`PhysicsWorld`] state that
2//! [`PhysicsWorld::step`] reads, in one versioned blob with one checksum.
3//!
4// LIMITATION(COV-ENGINE-011): keeps only the per-body motion and sleep state and expects the caller to rebuild joints, colliders, force fields, materials and so on by replay.
5//! [`PhysicsWorld::serialize_state`] (rollback netcode) keeps only the
6//! per-body motion and sleep state and expects the caller to rebuild
7//! joints, colliders, force fields, materials and so on by replay. This
8//! module is the other end: [`PhysicsWorld::snapshot_world`] writes the whole
9//! world, and [`PhysicsWorld::from_world_snapshot`] /
10//! [`PhysicsWorld::restore_world`] bring it back so that every later `step`
11//! is bit-identical to the original's (branching search, rollback without a
12//! replay log, persistence).
13//!
14//! Since version 4 the header also carries two content hashes: the
15//! stepping-semantics identifier of the binary that wrote the blob
16//! ([`crate::PHYSICS_SEMANTICS_ID`]) and the world's rule identifier
17//! ([`PhysicsWorld::law_id`] of that semantics). A reader refuses a version 4
18//! blob written under other stepping semantics
19//! ([`WorldSnapshotError::SemanticsMismatch`]), and
20//! [`PhysicsWorld::restore_world_checked`] also compares the rule identifier
21//! with one the caller expects and reports what it could verify
22//! ([`LawCheck`]). Version 1 to 3 blobs carry neither hash; they are still
23//! read, and reported as [`LawCheck::Unpinned`].
24//!
25//! The field-by-field coverage, the format and the errors are documented on
26//! [`PhysicsWorld::snapshot_world`] and [`WorldSnapshotError`].
27
28use super::{fnv1a_fold, ConstraintBatch, ContactConstraint, DistanceConstraint, PhysicsWorld};
29use super::{BodyType, Broadphase, RigidBody, SolverBackend, SolverConfig};
30use crate::body_collider::BodyCollider;
31use crate::collider::{Capsule, Contact, ConvexHull, Sphere, AABB};
32use crate::compound::{CompoundChild, CompoundShape, ShapeRef};
33use crate::contact_cache::{BodyPairKey, CachedContactPoint, ContactCache, ContactManifold};
34use crate::event::{ContactEvent, ContactEventType, EventCollector, TriggerEvent};
35use crate::filter::CollisionFilter;
36use crate::force::{ForceField, ForceFieldInstance};
37use crate::joint::{
38    BallJoint, ConeTwistJoint, D6Joint, D6Motion, FixedJoint, HingeJoint, Joint, SliderJoint,
39    SpringJoint,
40};
41use crate::material::{
42    CombineRule, MaterialTable, PairOverride, PhysicsMaterial, MATERIAL_ID_CAPACITY,
43};
44use crate::math::{Fix128, QuatFix, Vec3Fix};
45use crate::semantics::PHYSICS_SEMANTICS_ID;
46use crate::shape::Shape;
47use crate::sleeping::{IslandManager, SleepConfig, SleepData, SleepState};
48use crate::static_collider::StaticCollider;
49use crate::world_participant::WorldFault;
50
51#[cfg(not(feature = "std"))]
52use alloc::collections::{BTreeMap, BTreeSet};
53#[cfg(not(feature = "std"))]
54use alloc::vec::Vec;
55#[cfg(feature = "std")]
56use std::collections::{BTreeMap, BTreeSet};
57
58/// Why [`PhysicsWorld::restore_world`] / [`PhysicsWorld::from_world_snapshot`]
59/// rejected a blob. The target world is unchanged in every case.
60///
61/// # Which input gives which error
62///
63/// | input | error |
64/// |---|---|
65/// | fewer than 24 bytes, a version 4 blob of fewer than 88 bytes, or shorter than the declared payload | [`Self::Truncated`] |
66/// | wrong magic | [`Self::BadMagic`] |
67/// | version 0 or above [`PhysicsWorld::WORLD_SNAPSHOT_VERSION`] | [`Self::UnsupportedVersion`] |
68/// | reserved bytes not 0 | [`Self::ReservedNotZero`] |
69/// | longer than the declared payload | [`Self::TrailingBytes`] |
70/// | checksum differs | [`Self::ChecksumMismatch`] |
71/// | version 4 header `semantics_id` differs from this binary's [`crate::PHYSICS_SEMANTICS_ID`] | [`Self::SemanticsMismatch`] |
72/// | version 4 header `law_id` differs from the one passed to [`PhysicsWorld::restore_world_checked`] | [`Self::LawIdMismatch`] |
73/// | unknown enum tag / non-boolean byte / unrepresentable value inside the payload | [`Self::InvalidValue`] |
74/// | material table with 0 or more than 65,536 materials, or a material whose id is not its index | [`Self::InvalidValue`] (`section: "material_table"`) |
75/// | a joint / constraint / contact / SDF collider / batch / proxy index past its target | [`Self::DanglingIndex`] |
76/// | target world holds a different number of SDF fields | [`Self::SdfFieldCountMismatch`] |
77/// | target world holds a different number of hooks / modifiers / bridges | [`Self::CallbackCountMismatch`] |
78/// | target world holds other participants (count, kinds, order) | [`Self::ParticipantMismatch`] |
79/// | a participant of the target world refuses its payload | [`Self::ParticipantState`] |
80/// | the target world declares other shared fields | [`Self::FieldState`] |
81/// | unknown fault code / participant fault tag, field mode or layout tag | [`Self::InvalidValue`] |
82///
83/// On every error the target world is left untouched.
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85#[non_exhaustive]
86pub enum WorldSnapshotError {
87    /// The blob ends before the header or the declared payload does.
88    Truncated,
89    /// The first 4 bytes are not [`PhysicsWorld::WORLD_SNAPSHOT_MAGIC`].
90    BadMagic,
91    /// The version is not one this build reads (1 to
92    /// [`PhysicsWorld::WORLD_SNAPSHOT_VERSION`]).
93    UnsupportedVersion {
94        /// Version stored in the blob.
95        found: u16,
96        /// Version this build reads.
97        supported: u16,
98    },
99    /// The reserved header bytes are not 0.
100    ReservedNotZero,
101    /// Bytes remain after the checksum, or after the last payload section.
102    TrailingBytes {
103        /// Number of unread bytes.
104        extra: usize,
105    },
106    /// The stored checksum differs from the one computed over the blob.
107    ChecksumMismatch {
108        /// Checksum stored in the blob.
109        stored: u64,
110        /// Checksum of the bytes as received.
111        computed: u64,
112    },
113    /// A value inside the payload is not valid for its field (unknown enum
114    /// tag, a boolean byte other than 0 / 1, a count that does not fit
115    /// `usize`, invalid metric weights, a material table holding 0 or more
116    /// than 65,536 materials or a material whose id is not its index).
117    InvalidValue {
118        /// Section the value was read in.
119        section: &'static str,
120    },
121    /// An index refers past the end of what it indexes (for example a joint
122    /// naming a body the snapshot does not hold).
123    DanglingIndex {
124        /// Section holding the index.
125        section: &'static str,
126        /// The index.
127        index: usize,
128        /// Length of what it indexes.
129        len: usize,
130    },
131    /// The snapshot holds a different number of SDF colliders than the target
132    /// world holds SDF fields (fields are code and are not in the blob).
133    SdfFieldCountMismatch {
134        /// SDF colliders in the snapshot.
135        snapshot: usize,
136        /// SDF colliders in the target world.
137        world: usize,
138    },
139    /// The snapshot was taken with a different number of pre-solve hooks,
140    /// contact modifiers or installed GPU bridges than the target world has.
141    CallbackCountMismatch {
142        /// `"pre_solve_hooks"`, `"contact_modifiers"` or `"gpu_solver_bridge"`.
143        section: &'static str,
144        /// Count in the snapshot.
145        snapshot: usize,
146        /// Count in the target world.
147        world: usize,
148    },
149    /// The target world's participants differ from the snapshot's in number,
150    /// kinds or order (participants are code and are not in the blob; the
151    /// target world must already hold the same ones).
152    ParticipantMismatch(crate::world_participant::ParticipantMismatch),
153    /// Participant `index` of the target world refused its payload
154    /// ([`crate::world_participant::Participant::check_state`]). Every
155    /// payload is checked before any is read.
156    ParticipantState {
157        /// Registration index of the participant.
158        index: usize,
159        /// Why it refused.
160        error: crate::world_participant::StateError,
161    },
162    /// The snapshot's shared fields do not match the target world's
163    /// declared fields (ids, modes, layouts;
164    /// [`crate::world_participant::FieldBoard::check_values`]).
165    FieldState(crate::world_participant::StateError),
166    /// A version 4 blob was written under other stepping semantics: its
167    /// header `semantics_id` is not this binary's
168    /// [`crate::PHYSICS_SEMANTICS_ID`], so a restored world would not step
169    /// the way the writer's did. Checked after the checksum.
170    SemanticsMismatch {
171        /// `semantics_id` stored in the blob's header.
172        stored: [u8; 32],
173        /// [`crate::PHYSICS_SEMANTICS_ID`] of this binary.
174        expected: [u8; 32],
175    },
176    /// The version 4 blob's header `law_id` differs from the identifier
177    /// passed to [`PhysicsWorld::restore_world_checked`].
178    LawIdMismatch {
179        /// `law_id` stored in the blob's header.
180        stored: [u8; 32],
181        /// The identifier the caller expected.
182        expected: [u8; 32],
183    },
184}
185
186/// What [`PhysicsWorld::restore_world_checked`] could verify about the rule
187/// identifiers of a blob it accepted.
188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189#[non_exhaustive]
190pub enum LawCheck {
191    /// Version 4: the header `semantics_id` equals this binary's
192    /// [`crate::PHYSICS_SEMANTICS_ID`], and the header `law_id` equals the
193    /// identifier the caller passed.
194    Verified,
195    /// Version 4: the header `semantics_id` equals this binary's
196    /// [`crate::PHYSICS_SEMANTICS_ID`]; the caller passed no identifier, so
197    /// the header `law_id` was not compared with anything.
198    SemanticsVerified,
199    /// Version 1 to 3: the blob carries no identifiers, so neither could be
200    /// verified. The blob is accepted; a caller that requires verification
201    /// can refuse it on this value.
202    Unpinned,
203}
204
205/// Lower-case hex of a 32-byte identifier, for error messages.
206struct Hex<'a>(&'a [u8; 32]);
207
208impl core::fmt::Display for Hex<'_> {
209    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
210        for b in self.0 {
211            write!(f, "{b:02x}")?;
212        }
213        Ok(())
214    }
215}
216
217impl core::fmt::Display for WorldSnapshotError {
218    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
219        match self {
220            Self::Truncated => write!(f, "world snapshot is truncated"),
221            Self::BadMagic => write!(f, "not a world snapshot (bad magic)"),
222            Self::UnsupportedVersion { found, supported } => write!(
223                f,
224                "world snapshot version {found} is not supported (expected {supported})"
225            ),
226            Self::ReservedNotZero => write!(f, "world snapshot reserved header bytes are not 0"),
227            Self::TrailingBytes { extra } => {
228                write!(f, "world snapshot has {extra} trailing bytes")
229            }
230            Self::ChecksumMismatch { stored, computed } => write!(
231                f,
232                "world snapshot checksum mismatch (stored {stored:#018x}, computed {computed:#018x})"
233            ),
234            Self::InvalidValue { section } => {
235                write!(f, "world snapshot has an invalid value in {section}")
236            }
237            Self::DanglingIndex { section, index, len } => write!(
238                f,
239                "world snapshot {section} refers to index {index} of {len}"
240            ),
241            Self::SdfFieldCountMismatch { snapshot, world } => write!(
242                f,
243                "world snapshot has {snapshot} SDF colliders, target world holds {world} SDF fields"
244            ),
245            Self::CallbackCountMismatch {
246                section,
247                snapshot,
248                world,
249            } => write!(
250                f,
251                "world snapshot was taken with {snapshot} {section}, target world has {world}"
252            ),
253            Self::ParticipantMismatch(m) => write!(
254                f,
255                "world snapshot participants differ from the target world's: {m:?}"
256            ),
257            Self::ParticipantState { index, error } => write!(
258                f,
259                "participant {index} of the target world refused its snapshot payload: {error:?}"
260            ),
261            Self::FieldState(error) => write!(
262                f,
263                "world snapshot fields differ from the target world's: {error:?}"
264            ),
265            Self::SemanticsMismatch { stored, expected } => write!(
266                f,
267                "world snapshot was written under semantics {}, this binary steps under {}",
268                Hex(stored),
269                Hex(expected)
270            ),
271            Self::LawIdMismatch { stored, expected } => write!(
272                f,
273                "world snapshot law id {} differs from the expected {}",
274                Hex(stored),
275                Hex(expected)
276            ),
277        }
278    }
279}
280
281#[cfg(feature = "std")]
282impl std::error::Error for WorldSnapshotError {}
283
284type Res<T> = Result<T, WorldSnapshotError>;
285
286/// Header length of version 1 to 3 blobs, and the part every version shares.
287const HEADER_LEN: usize = 16;
288/// Header length of version 4 blobs: the shared 16 bytes, then
289/// `semantics_id` and `law_id` (32 bytes each).
290const HEADER_LEN_V4: usize = HEADER_LEN + 64;
291/// Offset of `semantics_id` in a version 4 header.
292const SEMANTICS_ID_AT: usize = HEADER_LEN;
293/// Offset of `law_id` in a version 4 header.
294const LAW_ID_AT: usize = HEADER_LEN + 32;
295
296/// Header length of a blob of `version` (already checked to be supported).
297fn header_len(version: u16) -> usize {
298    if version >= 4 {
299        HEADER_LEN_V4
300    } else {
301        HEADER_LEN
302    }
303}
304const CHECKSUM_LEN: usize = 8;
305
306// ── Writer ────────────────────────────────────────────────────────────────
307
308struct W(Vec<u8>);
309
310impl W {
311    fn u8(&mut self, v: u8) {
312        self.0.push(v);
313    }
314    fn bool(&mut self, v: bool) {
315        self.0.push(u8::from(v));
316    }
317    fn u16(&mut self, v: u16) {
318        self.0.extend_from_slice(&v.to_le_bytes());
319    }
320    fn u32(&mut self, v: u32) {
321        self.0.extend_from_slice(&v.to_le_bytes());
322    }
323    fn i32(&mut self, v: i32) {
324        self.0.extend_from_slice(&v.to_le_bytes());
325    }
326    fn u64(&mut self, v: u64) {
327        self.0.extend_from_slice(&v.to_le_bytes());
328    }
329    fn usize(&mut self, v: usize) {
330        self.u64(v as u64);
331    }
332    fn f32(&mut self, v: f32) {
333        self.u32(v.to_bits());
334    }
335    fn fix(&mut self, v: Fix128) {
336        self.0.extend_from_slice(&v.hi.to_le_bytes());
337        self.0.extend_from_slice(&v.lo.to_le_bytes());
338    }
339    fn vec3(&mut self, v: Vec3Fix) {
340        self.fix(v.x);
341        self.fix(v.y);
342        self.fix(v.z);
343    }
344    fn quat(&mut self, q: QuatFix) {
345        self.fix(q.x);
346        self.fix(q.y);
347        self.fix(q.z);
348        self.fix(q.w);
349    }
350    fn opt_fix(&mut self, v: Option<Fix128>) {
351        match v {
352            Some(x) => {
353                self.u8(1);
354                self.fix(x);
355            }
356            None => self.u8(0),
357        }
358    }
359    fn aabb(&mut self, b: AABB) {
360        self.vec3(b.min);
361        self.vec3(b.max);
362    }
363}
364
365// ── Reader ────────────────────────────────────────────────────────────────
366
367struct R<'a> {
368    data: &'a [u8],
369    pos: usize,
370}
371
372impl<'a> R<'a> {
373    fn take(&mut self, n: usize) -> Res<&'a [u8]> {
374        let end = self
375            .pos
376            .checked_add(n)
377            .ok_or(WorldSnapshotError::Truncated)?;
378        if end > self.data.len() {
379            return Err(WorldSnapshotError::Truncated);
380        }
381        let s = &self.data[self.pos..end];
382        self.pos = end;
383        Ok(s)
384    }
385    fn arr<const N: usize>(&mut self) -> Res<[u8; N]> {
386        let mut a = [0u8; N];
387        a.copy_from_slice(self.take(N)?);
388        Ok(a)
389    }
390    fn u8(&mut self) -> Res<u8> {
391        Ok(self.take(1)?[0])
392    }
393    fn bool(&mut self, section: &'static str) -> Res<bool> {
394        match self.u8()? {
395            0 => Ok(false),
396            1 => Ok(true),
397            _ => Err(WorldSnapshotError::InvalidValue { section }),
398        }
399    }
400    fn u16(&mut self) -> Res<u16> {
401        Ok(u16::from_le_bytes(self.arr()?))
402    }
403    fn u32(&mut self) -> Res<u32> {
404        Ok(u32::from_le_bytes(self.arr()?))
405    }
406    fn i32(&mut self) -> Res<i32> {
407        Ok(i32::from_le_bytes(self.arr()?))
408    }
409    fn u64(&mut self) -> Res<u64> {
410        Ok(u64::from_le_bytes(self.arr()?))
411    }
412    fn usize(&mut self, section: &'static str) -> Res<usize> {
413        usize::try_from(self.u64()?).map_err(|_| WorldSnapshotError::InvalidValue { section })
414    }
415    /// A element count: each element takes at least `min_size` bytes, so a
416    /// count larger than the rest of the blob allows is truncation (and is
417    /// rejected before anything is allocated for it).
418    fn len(&mut self, section: &'static str, min_size: usize) -> Res<usize> {
419        let n = self.usize(section)?;
420        let remaining = self.data.len() - self.pos;
421        if n.checked_mul(min_size.max(1))
422            .is_none_or(|need| need > remaining)
423        {
424            return Err(WorldSnapshotError::Truncated);
425        }
426        Ok(n)
427    }
428    fn f32(&mut self) -> Res<f32> {
429        Ok(f32::from_bits(self.u32()?))
430    }
431    fn fix(&mut self) -> Res<Fix128> {
432        let hi = i64::from_le_bytes(self.arr()?);
433        let lo = u64::from_le_bytes(self.arr()?);
434        Ok(Fix128 { hi, lo })
435    }
436    fn vec3(&mut self) -> Res<Vec3Fix> {
437        Ok(Vec3Fix::new(self.fix()?, self.fix()?, self.fix()?))
438    }
439    fn quat(&mut self) -> Res<QuatFix> {
440        Ok(QuatFix {
441            x: self.fix()?,
442            y: self.fix()?,
443            z: self.fix()?,
444            w: self.fix()?,
445        })
446    }
447    fn opt_fix(&mut self, section: &'static str) -> Res<Option<Fix128>> {
448        Ok(if self.bool(section)? {
449            Some(self.fix()?)
450        } else {
451            None
452        })
453    }
454    fn aabb(&mut self) -> Res<AABB> {
455        Ok(AABB {
456            min: self.vec3()?,
457            max: self.vec3()?,
458        })
459    }
460}
461
462const fn invalid(section: &'static str) -> WorldSnapshotError {
463    WorldSnapshotError::InvalidValue { section }
464}
465
466// ── Leaf types ────────────────────────────────────────────────────────────
467
468fn w_config(w: &mut W, c: &SolverConfig) {
469    w.usize(c.substeps);
470    w.usize(c.iterations);
471    w.vec3(c.gravity);
472    w.fix(c.damping);
473    w.fix(c.warm_start_factor);
474    w.u8(match c.solver_backend {
475        SolverBackend::Xpbd => 0,
476        SolverBackend::Tgs => 1,
477    });
478}
479
480fn r_config(r: &mut R<'_>) -> Res<SolverConfig> {
481    const S: &str = "config";
482    Ok(SolverConfig {
483        substeps: r.usize(S)?,
484        iterations: r.usize(S)?,
485        gravity: r.vec3()?,
486        damping: r.fix()?,
487        warm_start_factor: r.fix()?,
488        solver_backend: match r.u8()? {
489            0 => SolverBackend::Xpbd,
490            1 => SolverBackend::Tgs,
491            _ => return Err(invalid(S)),
492        },
493    })
494}
495
496fn w_body(w: &mut W, b: &RigidBody) {
497    w.vec3(b.position);
498    w.vec3(b.velocity);
499    w.fix(b.inv_mass);
500    w.vec3(b.inv_inertia);
501    w.vec3(b.prev_position);
502    w.quat(b.rotation);
503    w.vec3(b.angular_velocity);
504    w.quat(b.prev_rotation);
505    w.fix(b.restitution);
506    w.fix(b.friction);
507    w.fix(b.gravity_scale);
508    w.fix(b.linear_damping);
509    w.fix(b.angular_damping);
510    w.bool(b.is_sensor);
511    w.u8(b.body_type as u8);
512    match b.kinematic_target {
513        Some((p, q)) => {
514            w.u8(1);
515            w.vec3(p);
516            w.quat(q);
517        }
518        None => w.u8(0),
519    }
520}
521
522fn r_body(r: &mut R<'_>) -> Res<RigidBody> {
523    const S: &str = "bodies";
524    Ok(RigidBody {
525        position: r.vec3()?,
526        velocity: r.vec3()?,
527        inv_mass: r.fix()?,
528        inv_inertia: r.vec3()?,
529        prev_position: r.vec3()?,
530        rotation: r.quat()?,
531        angular_velocity: r.vec3()?,
532        prev_rotation: r.quat()?,
533        restitution: r.fix()?,
534        friction: r.fix()?,
535        gravity_scale: r.fix()?,
536        linear_damping: r.fix()?,
537        angular_damping: r.fix()?,
538        is_sensor: r.bool(S)?,
539        body_type: match r.u8()? {
540            0 => BodyType::Dynamic,
541            1 => BodyType::Static,
542            2 => BodyType::Kinematic,
543            _ => return Err(invalid(S)),
544        },
545        kinematic_target: if r.bool(S)? {
546            Some((r.vec3()?, r.quat()?))
547        } else {
548            None
549        },
550    })
551}
552
553fn w_contact(w: &mut W, c: &Contact) {
554    w.fix(c.depth);
555    w.vec3(c.normal);
556    w.vec3(c.point_a);
557    w.vec3(c.point_b);
558}
559
560fn r_contact(r: &mut R<'_>) -> Res<Contact> {
561    Ok(Contact {
562        depth: r.fix()?,
563        normal: r.vec3()?,
564        point_a: r.vec3()?,
565        point_b: r.vec3()?,
566    })
567}
568
569fn w_joint(w: &mut W, j: &Joint) {
570    match j {
571        Joint::Ball(j) => {
572            w.u8(0);
573            w.usize(j.body_a);
574            w.usize(j.body_b);
575            w.vec3(j.local_anchor_a);
576            w.vec3(j.local_anchor_b);
577            w.fix(j.compliance);
578            w.opt_fix(j.break_force);
579        }
580        Joint::Hinge(j) => {
581            w.u8(1);
582            w.usize(j.body_a);
583            w.usize(j.body_b);
584            w.vec3(j.local_anchor_a);
585            w.vec3(j.local_anchor_b);
586            w.vec3(j.local_axis_a);
587            w.vec3(j.local_axis_b);
588            w.opt_fix(j.angle_min);
589            w.opt_fix(j.angle_max);
590            w.fix(j.compliance);
591            w.fix(j.angular_compliance);
592            w.opt_fix(j.break_force);
593        }
594        Joint::Fixed(j) => {
595            w.u8(2);
596            w.usize(j.body_a);
597            w.usize(j.body_b);
598            w.vec3(j.local_anchor_a);
599            w.vec3(j.local_anchor_b);
600            w.quat(j.relative_rotation);
601            w.fix(j.compliance);
602            w.fix(j.angular_compliance);
603            w.opt_fix(j.break_force);
604        }
605        Joint::Slider(j) => {
606            w.u8(3);
607            w.usize(j.body_a);
608            w.usize(j.body_b);
609            w.vec3(j.local_axis);
610            w.vec3(j.local_anchor_a);
611            w.vec3(j.local_anchor_b);
612            w.opt_fix(j.limit_min);
613            w.opt_fix(j.limit_max);
614            w.fix(j.compliance);
615            w.opt_fix(j.break_force);
616        }
617        Joint::Spring(j) => {
618            w.u8(4);
619            w.usize(j.body_a);
620            w.usize(j.body_b);
621            w.vec3(j.local_anchor_a);
622            w.vec3(j.local_anchor_b);
623            w.fix(j.rest_length);
624            w.fix(j.stiffness);
625            w.fix(j.damping);
626            w.opt_fix(j.break_force);
627        }
628        Joint::D6(j) => {
629            w.u8(5);
630            w.usize(j.body_a);
631            w.usize(j.body_b);
632            w.vec3(j.local_anchor_a);
633            w.vec3(j.local_anchor_b);
634            w.quat(j.local_frame_a);
635            w.quat(j.local_frame_b);
636            for m in [
637                j.linear_x,
638                j.linear_y,
639                j.linear_z,
640                j.angular_x,
641                j.angular_y,
642                j.angular_z,
643            ] {
644                w.u8(match m {
645                    D6Motion::Locked => 0,
646                    D6Motion::Free => 1,
647                    D6Motion::Limited => 2,
648                });
649            }
650            w.vec3(j.linear_limit_min);
651            w.vec3(j.linear_limit_max);
652            w.vec3(j.angular_limit_min);
653            w.vec3(j.angular_limit_max);
654            w.fix(j.compliance);
655            w.fix(j.angular_compliance);
656            w.opt_fix(j.break_force);
657        }
658        Joint::ConeTwist(j) => {
659            w.u8(6);
660            w.usize(j.body_a);
661            w.usize(j.body_b);
662            w.vec3(j.local_anchor_a);
663            w.vec3(j.local_anchor_b);
664            w.vec3(j.twist_axis_a);
665            w.vec3(j.twist_axis_b);
666            w.fix(j.cone_limit);
667            w.fix(j.twist_limit);
668            w.fix(j.compliance);
669            w.fix(j.angular_compliance);
670            w.opt_fix(j.break_force);
671        }
672    }
673}
674
675fn r_d6_motion(r: &mut R<'_>) -> Res<D6Motion> {
676    match r.u8()? {
677        0 => Ok(D6Motion::Locked),
678        1 => Ok(D6Motion::Free),
679        2 => Ok(D6Motion::Limited),
680        _ => Err(invalid("joints")),
681    }
682}
683
684fn r_joint(r: &mut R<'_>) -> Res<Joint> {
685    const S: &str = "joints";
686    Ok(match r.u8()? {
687        0 => Joint::Ball(BallJoint {
688            body_a: r.usize(S)?,
689            body_b: r.usize(S)?,
690            local_anchor_a: r.vec3()?,
691            local_anchor_b: r.vec3()?,
692            compliance: r.fix()?,
693            break_force: r.opt_fix(S)?,
694        }),
695        1 => Joint::Hinge(HingeJoint {
696            body_a: r.usize(S)?,
697            body_b: r.usize(S)?,
698            local_anchor_a: r.vec3()?,
699            local_anchor_b: r.vec3()?,
700            local_axis_a: r.vec3()?,
701            local_axis_b: r.vec3()?,
702            angle_min: r.opt_fix(S)?,
703            angle_max: r.opt_fix(S)?,
704            compliance: r.fix()?,
705            angular_compliance: r.fix()?,
706            break_force: r.opt_fix(S)?,
707        }),
708        2 => Joint::Fixed(FixedJoint {
709            body_a: r.usize(S)?,
710            body_b: r.usize(S)?,
711            local_anchor_a: r.vec3()?,
712            local_anchor_b: r.vec3()?,
713            relative_rotation: r.quat()?,
714            compliance: r.fix()?,
715            angular_compliance: r.fix()?,
716            break_force: r.opt_fix(S)?,
717        }),
718        3 => Joint::Slider(SliderJoint {
719            body_a: r.usize(S)?,
720            body_b: r.usize(S)?,
721            local_axis: r.vec3()?,
722            local_anchor_a: r.vec3()?,
723            local_anchor_b: r.vec3()?,
724            limit_min: r.opt_fix(S)?,
725            limit_max: r.opt_fix(S)?,
726            compliance: r.fix()?,
727            break_force: r.opt_fix(S)?,
728        }),
729        4 => Joint::Spring(SpringJoint {
730            body_a: r.usize(S)?,
731            body_b: r.usize(S)?,
732            local_anchor_a: r.vec3()?,
733            local_anchor_b: r.vec3()?,
734            rest_length: r.fix()?,
735            stiffness: r.fix()?,
736            damping: r.fix()?,
737            break_force: r.opt_fix(S)?,
738        }),
739        5 => Joint::D6(D6Joint {
740            body_a: r.usize(S)?,
741            body_b: r.usize(S)?,
742            local_anchor_a: r.vec3()?,
743            local_anchor_b: r.vec3()?,
744            local_frame_a: r.quat()?,
745            local_frame_b: r.quat()?,
746            linear_x: r_d6_motion(r)?,
747            linear_y: r_d6_motion(r)?,
748            linear_z: r_d6_motion(r)?,
749            angular_x: r_d6_motion(r)?,
750            angular_y: r_d6_motion(r)?,
751            angular_z: r_d6_motion(r)?,
752            linear_limit_min: r.vec3()?,
753            linear_limit_max: r.vec3()?,
754            angular_limit_min: r.vec3()?,
755            angular_limit_max: r.vec3()?,
756            compliance: r.fix()?,
757            angular_compliance: r.fix()?,
758            break_force: r.opt_fix(S)?,
759        }),
760        6 => Joint::ConeTwist(ConeTwistJoint {
761            body_a: r.usize(S)?,
762            body_b: r.usize(S)?,
763            local_anchor_a: r.vec3()?,
764            local_anchor_b: r.vec3()?,
765            twist_axis_a: r.vec3()?,
766            twist_axis_b: r.vec3()?,
767            cone_limit: r.fix()?,
768            twist_limit: r.fix()?,
769            compliance: r.fix()?,
770            angular_compliance: r.fix()?,
771            break_force: r.opt_fix(S)?,
772        }),
773        _ => return Err(invalid(S)),
774    })
775}
776
777fn w_force_field(w: &mut W, f: &ForceFieldInstance) {
778    match f.field {
779        ForceField::Directional {
780            direction,
781            strength,
782        } => {
783            w.u8(0);
784            w.vec3(direction);
785            w.fix(strength);
786        }
787        ForceField::Point {
788            center,
789            strength,
790            repulsive,
791            max_force,
792        } => {
793            w.u8(1);
794            w.vec3(center);
795            w.fix(strength);
796            w.bool(repulsive);
797            w.fix(max_force);
798        }
799        ForceField::Drag { coefficient } => {
800            w.u8(2);
801            w.fix(coefficient);
802        }
803        ForceField::Buoyancy {
804            surface_y,
805            density,
806            drag,
807        } => {
808            w.u8(3);
809            w.fix(surface_y);
810            w.fix(density);
811            w.fix(drag);
812        }
813        ForceField::Vortex {
814            center,
815            axis,
816            strength,
817            falloff_radius,
818        } => {
819            w.u8(4);
820            w.vec3(center);
821            w.vec3(axis);
822            w.fix(strength);
823            w.fix(falloff_radius);
824        }
825        ForceField::Explosion {
826            center,
827            strength,
828            radius,
829            falloff_power,
830        } => {
831            w.u8(5);
832            w.vec3(center);
833            w.fix(strength);
834            w.fix(radius);
835            w.fix(falloff_power);
836        }
837        ForceField::Magnetic {
838            position,
839            moment,
840            strength,
841        } => {
842            w.u8(6);
843            w.vec3(position);
844            w.vec3(moment);
845            w.fix(strength);
846        }
847    }
848    match &f.affected_bodies {
849        Some(list) => {
850            w.u8(1);
851            w.usize(list.len());
852            for &i in list {
853                w.usize(i);
854            }
855        }
856        None => w.u8(0),
857    }
858    w.bool(f.enabled);
859}
860
861fn r_force_field(r: &mut R<'_>) -> Res<ForceFieldInstance> {
862    const S: &str = "force_fields";
863    let field = match r.u8()? {
864        0 => ForceField::Directional {
865            direction: r.vec3()?,
866            strength: r.fix()?,
867        },
868        1 => ForceField::Point {
869            center: r.vec3()?,
870            strength: r.fix()?,
871            repulsive: r.bool(S)?,
872            max_force: r.fix()?,
873        },
874        2 => ForceField::Drag {
875            coefficient: r.fix()?,
876        },
877        3 => ForceField::Buoyancy {
878            surface_y: r.fix()?,
879            density: r.fix()?,
880            drag: r.fix()?,
881        },
882        4 => ForceField::Vortex {
883            center: r.vec3()?,
884            axis: r.vec3()?,
885            strength: r.fix()?,
886            falloff_radius: r.fix()?,
887        },
888        5 => ForceField::Explosion {
889            center: r.vec3()?,
890            strength: r.fix()?,
891            radius: r.fix()?,
892            falloff_power: r.fix()?,
893        },
894        6 => ForceField::Magnetic {
895            position: r.vec3()?,
896            moment: r.vec3()?,
897            strength: r.fix()?,
898        },
899        _ => return Err(invalid(S)),
900    };
901    let affected_bodies = if r.bool(S)? {
902        let n = r.len(S, 8)?;
903        let mut v = Vec::with_capacity(n);
904        for _ in 0..n {
905            v.push(r.usize(S)?);
906        }
907        Some(v)
908    } else {
909        None
910    };
911    Ok(ForceFieldInstance {
912        field,
913        affected_bodies,
914        enabled: r.bool(S)?,
915    })
916}
917
918fn w_static_collider(w: &mut W, c: &StaticCollider) {
919    match c {
920        StaticCollider::Plane(p) => {
921            w.u8(0);
922            w.vec3(p.normal);
923            w.fix(p.offset);
924        }
925        StaticCollider::HeightField(h) => {
926            w.u8(1);
927            w.usize(h.heights.len());
928            for &v in &h.heights {
929                w.fix(v);
930            }
931            w.u32(h.width);
932            w.u32(h.depth);
933            w.fix(h.spacing);
934            w.vec3(h.origin);
935        }
936        StaticCollider::TriMesh(m) => {
937            w.u8(2);
938            w.usize(m.triangles.len());
939            for t in &m.triangles {
940                w.vec3(t.v0);
941                w.vec3(t.v1);
942                w.vec3(t.v2);
943            }
944            w.usize(m.bvh.nodes.len());
945            for n in &m.bvh.nodes {
946                for v in n.aabb_min {
947                    w.i32(v);
948                }
949                w.u32(n.first_child_or_prim);
950                for v in n.aabb_max {
951                    w.i32(v);
952                }
953                w.u32(n.prim_count_escape);
954            }
955            w.usize(m.bvh.primitives.len());
956            for &p in &m.bvh.primitives {
957                w.u32(p);
958            }
959            w.aabb(m.bvh.bounds);
960            w.aabb(m.bounds);
961        }
962    }
963}
964
965fn r_static_collider(r: &mut R<'_>) -> Res<StaticCollider> {
966    const S: &str = "static_colliders";
967    Ok(match r.u8()? {
968        0 => StaticCollider::Plane(crate::plane_collider::PlaneCollider {
969            normal: r.vec3()?,
970            offset: r.fix()?,
971        }),
972        1 => {
973            let n = r.len(S, 16)?;
974            let mut heights = Vec::with_capacity(n);
975            for _ in 0..n {
976                heights.push(r.fix()?);
977            }
978            StaticCollider::HeightField(crate::heightfield::HeightField {
979                heights,
980                width: r.u32()?,
981                depth: r.u32()?,
982                spacing: r.fix()?,
983                origin: r.vec3()?,
984            })
985        }
986        2 => {
987            let n = r.len(S, 144)?;
988            let mut triangles = Vec::with_capacity(n);
989            for _ in 0..n {
990                triangles.push(crate::trimesh::Triangle {
991                    v0: r.vec3()?,
992                    v1: r.vec3()?,
993                    v2: r.vec3()?,
994                });
995            }
996            let n = r.len(S, 32)?;
997            let mut nodes = Vec::with_capacity(n);
998            for _ in 0..n {
999                let aabb_min = [r.i32()?, r.i32()?, r.i32()?];
1000                let first_child_or_prim = r.u32()?;
1001                let aabb_max = [r.i32()?, r.i32()?, r.i32()?];
1002                let prim_count_escape = r.u32()?;
1003                nodes.push(crate::bvh::BvhNode {
1004                    aabb_min,
1005                    first_child_or_prim,
1006                    aabb_max,
1007                    prim_count_escape,
1008                });
1009            }
1010            let n = r.len(S, 4)?;
1011            let mut primitives = Vec::with_capacity(n);
1012            for _ in 0..n {
1013                primitives.push(r.u32()?);
1014            }
1015            let bvh = crate::bvh::LinearBvh {
1016                nodes,
1017                primitives,
1018                bounds: r.aabb()?,
1019            };
1020            StaticCollider::TriMesh(crate::trimesh::TriMesh {
1021                triangles,
1022                bvh,
1023                bounds: r.aabb()?,
1024            })
1025        }
1026        _ => return Err(invalid(S)),
1027    })
1028}
1029
1030fn w_shape(w: &mut W, s: &Shape) {
1031    match *s {
1032        Shape::Box { half_extents } => {
1033            w.u8(0);
1034            w.vec3(half_extents);
1035        }
1036        Shape::Cylinder {
1037            radius,
1038            half_height,
1039        } => {
1040            w.u8(1);
1041            w.fix(radius);
1042            w.fix(half_height);
1043        }
1044        Shape::Cone {
1045            radius,
1046            half_height,
1047        } => {
1048            w.u8(2);
1049            w.fix(radius);
1050            w.fix(half_height);
1051        }
1052        Shape::Ellipsoid { radii } => {
1053            w.u8(3);
1054            w.vec3(radii);
1055        }
1056        Shape::Wedge {
1057            width,
1058            height,
1059            depth,
1060        } => {
1061            w.u8(4);
1062            w.fix(width);
1063            w.fix(height);
1064            w.fix(depth);
1065        }
1066        Shape::Torus {
1067            major_radius,
1068            minor_radius,
1069        } => {
1070            w.u8(5);
1071            w.fix(major_radius);
1072            w.fix(minor_radius);
1073        }
1074    }
1075}
1076
1077fn r_shape(r: &mut R<'_>) -> Res<Shape> {
1078    Ok(match r.u8()? {
1079        0 => Shape::Box {
1080            half_extents: r.vec3()?,
1081        },
1082        1 => Shape::Cylinder {
1083            radius: r.fix()?,
1084            half_height: r.fix()?,
1085        },
1086        2 => Shape::Cone {
1087            radius: r.fix()?,
1088            half_height: r.fix()?,
1089        },
1090        3 => Shape::Ellipsoid { radii: r.vec3()? },
1091        4 => Shape::Wedge {
1092            width: r.fix()?,
1093            height: r.fix()?,
1094            depth: r.fix()?,
1095        },
1096        5 => Shape::Torus {
1097            major_radius: r.fix()?,
1098            minor_radius: r.fix()?,
1099        },
1100        _ => return Err(invalid("body_colliders")),
1101    })
1102}
1103
1104fn w_body_collider(w: &mut W, c: Option<&BodyCollider>) {
1105    match c {
1106        None => w.u8(0),
1107        Some(BodyCollider::Shape(s)) => {
1108            w.u8(1);
1109            w_shape(w, s);
1110        }
1111        Some(BodyCollider::Compound(c)) => {
1112            w.u8(2);
1113            w.usize(c.children.len());
1114            for ch in &c.children {
1115                match &ch.shape {
1116                    ShapeRef::Sphere(s) => {
1117                        w.u8(0);
1118                        w.vec3(s.center);
1119                        w.fix(s.radius);
1120                    }
1121                    ShapeRef::Capsule(s) => {
1122                        w.u8(1);
1123                        w.vec3(s.a);
1124                        w.vec3(s.b);
1125                        w.fix(s.radius);
1126                    }
1127                    ShapeRef::Box(b) => {
1128                        w.u8(2);
1129                        w.vec3(b.center);
1130                        w.vec3(b.half_extents);
1131                        w.quat(b.rotation);
1132                    }
1133                    ShapeRef::ConvexHull(h) => {
1134                        w.u8(3);
1135                        w.usize(h.vertices.len());
1136                        for &v in &h.vertices {
1137                            w.vec3(v);
1138                        }
1139                    }
1140                }
1141                w.vec3(ch.local_position);
1142                w.quat(ch.local_rotation);
1143            }
1144            w.aabb(c.cached_aabb);
1145            w.bool(c.dirty);
1146        }
1147    }
1148}
1149
1150fn r_body_collider(r: &mut R<'_>) -> Res<Option<BodyCollider>> {
1151    const S: &str = "body_colliders";
1152    Ok(match r.u8()? {
1153        0 => None,
1154        1 => Some(BodyCollider::Shape(r_shape(r)?)),
1155        2 => {
1156            let n = r.len(S, 1)?;
1157            let mut children = Vec::with_capacity(n);
1158            for _ in 0..n {
1159                let shape = match r.u8()? {
1160                    0 => ShapeRef::Sphere(Sphere {
1161                        center: r.vec3()?,
1162                        radius: r.fix()?,
1163                    }),
1164                    1 => ShapeRef::Capsule(Capsule {
1165                        a: r.vec3()?,
1166                        b: r.vec3()?,
1167                        radius: r.fix()?,
1168                    }),
1169                    2 => ShapeRef::Box(crate::box_collider::OrientedBox {
1170                        center: r.vec3()?,
1171                        half_extents: r.vec3()?,
1172                        rotation: r.quat()?,
1173                    }),
1174                    3 => {
1175                        let m = r.len(S, 48)?;
1176                        let mut vertices = Vec::with_capacity(m);
1177                        for _ in 0..m {
1178                            vertices.push(r.vec3()?);
1179                        }
1180                        ShapeRef::ConvexHull(ConvexHull { vertices })
1181                    }
1182                    _ => return Err(invalid(S)),
1183                };
1184                children.push(CompoundChild {
1185                    shape,
1186                    local_position: r.vec3()?,
1187                    local_rotation: r.quat()?,
1188                });
1189            }
1190            let mut c = CompoundShape::new();
1191            c.children = children;
1192            c.cached_aabb = r.aabb()?;
1193            c.dirty = r.bool(S)?;
1194            Some(BodyCollider::Compound(c))
1195        }
1196        _ => return Err(invalid(S)),
1197    })
1198}
1199
1200fn w_contact_cache(w: &mut W, c: &ContactCache) {
1201    w.usize(c.manifolds.len());
1202    for m in &c.manifolds {
1203        w.u32(m.pair.body_a);
1204        w.u32(m.pair.body_b);
1205        w.usize(m.points.len());
1206        for p in &m.points {
1207            w.vec3(p.local_point_a);
1208            w.vec3(p.local_point_b);
1209            w.vec3(p.normal);
1210            w.fix(p.depth);
1211            w.fix(p.lambda_n);
1212            w.fix(p.lambda_t1);
1213            w.fix(p.lambda_t2);
1214            w.u32(p.age);
1215        }
1216        w.vec3(m.normal);
1217        w.fix(m.friction);
1218        w.fix(m.restitution);
1219        w.u32(m.stale_frames);
1220    }
1221    w.u32(c.max_stale_frames);
1222    w.fix(c.warm_start_factor);
1223}
1224
1225fn r_contact_cache(r: &mut R<'_>) -> Res<ContactCache> {
1226    const S: &str = "contact_cache";
1227    let n = r.len(S, 8)?;
1228    let mut manifolds = Vec::with_capacity(n);
1229    for _ in 0..n {
1230        let pair = BodyPairKey {
1231            body_a: r.u32()?,
1232            body_b: r.u32()?,
1233        };
1234        let k = r.len(S, 212)?;
1235        let mut points = Vec::with_capacity(k);
1236        for _ in 0..k {
1237            points.push(CachedContactPoint {
1238                local_point_a: r.vec3()?,
1239                local_point_b: r.vec3()?,
1240                normal: r.vec3()?,
1241                depth: r.fix()?,
1242                lambda_n: r.fix()?,
1243                lambda_t1: r.fix()?,
1244                lambda_t2: r.fix()?,
1245                age: r.u32()?,
1246            });
1247        }
1248        manifolds.push(ContactManifold {
1249            pair,
1250            points,
1251            normal: r.vec3()?,
1252            friction: r.fix()?,
1253            restitution: r.fix()?,
1254            stale_frames: r.u32()?,
1255        });
1256    }
1257    let mut cache = ContactCache::new();
1258    cache.manifolds = manifolds;
1259    cache.max_stale_frames = r.u32()?;
1260    cache.warm_start_factor = r.fix()?;
1261    // Rebuilt (not saved): the same index `ContactCache::end_frame` builds.
1262    #[cfg(feature = "std")]
1263    {
1264        cache.pair_index.clear();
1265        for (idx, m) in cache.manifolds.iter().enumerate() {
1266            cache.pair_index.insert(m.pair, idx);
1267        }
1268    }
1269    Ok(cache)
1270}
1271
1272const fn combine_tag(c: CombineRule) -> u8 {
1273    match c {
1274        CombineRule::Average => 0,
1275        CombineRule::Min => 1,
1276        CombineRule::Max => 2,
1277        CombineRule::Multiply => 3,
1278    }
1279}
1280
1281fn r_combine(r: &mut R<'_>) -> Res<CombineRule> {
1282    match r.u8()? {
1283        0 => Ok(CombineRule::Average),
1284        1 => Ok(CombineRule::Min),
1285        2 => Ok(CombineRule::Max),
1286        3 => Ok(CombineRule::Multiply),
1287        _ => Err(invalid("material_table")),
1288    }
1289}
1290
1291fn w_material_table(w: &mut W, t: &MaterialTable) {
1292    w.usize(t.materials.len());
1293    for m in &t.materials {
1294        w.u16(m.id);
1295        w.fix(m.static_friction);
1296        w.fix(m.dynamic_friction);
1297        w.fix(m.restitution);
1298        w.u8(combine_tag(m.friction_combine));
1299        w.u8(combine_tag(m.restitution_combine));
1300    }
1301    w.usize(t.pair_overrides.len());
1302    for p in &t.pair_overrides {
1303        w.u16(p.mat_a);
1304        w.u16(p.mat_b);
1305        w.fix(p.friction);
1306        w.fix(p.restitution);
1307    }
1308    w.u8(combine_tag(t.default_friction_combine));
1309    w.u8(combine_tag(t.default_restitution_combine));
1310}
1311
1312/// Reads the material table and rejects any table the `MaterialTable` API
1313/// cannot build: `MaterialTable::new` puts the default material at id 0 and
1314/// `try_register` assigns `id = len` up to 65,536 ids, so a valid table holds
1315/// `1..=65_536` materials with `materials[i].id == i` (`get` indexes by
1316/// position and falls back to entry 0, so an empty table panics there).
1317fn r_material_table(r: &mut R<'_>) -> Res<MaterialTable> {
1318    const S: &str = "material_table";
1319    let n = r.len(S, 52)?;
1320    // `n > 65_536` is also caught by the id check below (an id is a `u16`);
1321    // checking it here rejects before reading the entries.
1322    if n == 0 || n > MATERIAL_ID_CAPACITY {
1323        return Err(invalid(S));
1324    }
1325    let mut materials = Vec::with_capacity(n);
1326    for i in 0..n {
1327        let id = r.u16()?;
1328        if usize::from(id) != i {
1329            return Err(invalid(S));
1330        }
1331        materials.push(PhysicsMaterial {
1332            id,
1333            static_friction: r.fix()?,
1334            dynamic_friction: r.fix()?,
1335            restitution: r.fix()?,
1336            friction_combine: r_combine(r)?,
1337            restitution_combine: r_combine(r)?,
1338        });
1339    }
1340    let n = r.len(S, 36)?;
1341    let mut pair_overrides = Vec::with_capacity(n);
1342    for _ in 0..n {
1343        pair_overrides.push(PairOverride {
1344            mat_a: r.u16()?,
1345            mat_b: r.u16()?,
1346            friction: r.fix()?,
1347            restitution: r.fix()?,
1348        });
1349    }
1350    let mut t = MaterialTable::new();
1351    t.materials = materials;
1352    t.pair_overrides = pair_overrides;
1353    t.default_friction_combine = r_combine(r)?;
1354    t.default_restitution_combine = r_combine(r)?;
1355    Ok(t)
1356}
1357
1358fn w_pair(w: &mut W, p: (usize, usize)) {
1359    w.usize(p.0);
1360    w.usize(p.1);
1361}
1362
1363fn r_pair(r: &mut R<'_>) -> Res<(usize, usize)> {
1364    Ok((r.usize("events")?, r.usize("events")?))
1365}
1366
1367fn w_events(w: &mut W, e: &EventCollector) {
1368    w.usize(e.contact_events.len());
1369    for c in &e.contact_events {
1370        w.usize(c.body_a);
1371        w.usize(c.body_b);
1372        w.u8(match c.event_type {
1373            ContactEventType::Begin => 0,
1374            ContactEventType::Persist => 1,
1375            ContactEventType::End => 2,
1376        });
1377        w.vec3(c.normal);
1378        w.vec3(c.point);
1379        w.fix(c.depth);
1380        w.fix(c.relative_velocity);
1381    }
1382    w.usize(e.trigger_events.len());
1383    for t in &e.trigger_events {
1384        w.usize(t.trigger_body);
1385        w.usize(t.other_body);
1386        w.bool(t.entered);
1387    }
1388    w.usize(e.prev_pairs.len());
1389    for &p in &e.prev_pairs {
1390        w_pair(w, p);
1391    }
1392    w.usize(e.curr_pairs.len());
1393    for &p in &e.curr_pairs {
1394        w_pair(w, p);
1395    }
1396    w.usize(e.prev_triggers.len());
1397    for &(k, v) in &e.prev_triggers {
1398        w_pair(w, k);
1399        w_pair(w, v);
1400    }
1401    w.usize(e.curr_triggers.len());
1402    for (&k, &v) in &e.curr_triggers {
1403        w_pair(w, k);
1404        w_pair(w, v);
1405    }
1406}
1407
1408fn r_events(r: &mut R<'_>) -> Res<EventCollector> {
1409    const S: &str = "events";
1410    let mut e = EventCollector::new();
1411    let n = r.len(S, 145)?;
1412    for _ in 0..n {
1413        e.contact_events.push(ContactEvent {
1414            body_a: r.usize(S)?,
1415            body_b: r.usize(S)?,
1416            event_type: match r.u8()? {
1417                0 => ContactEventType::Begin,
1418                1 => ContactEventType::Persist,
1419                2 => ContactEventType::End,
1420                _ => return Err(invalid(S)),
1421            },
1422            normal: r.vec3()?,
1423            point: r.vec3()?,
1424            depth: r.fix()?,
1425            relative_velocity: r.fix()?,
1426        });
1427    }
1428    let n = r.len(S, 17)?;
1429    for _ in 0..n {
1430        e.trigger_events.push(TriggerEvent {
1431            trigger_body: r.usize(S)?,
1432            other_body: r.usize(S)?,
1433            entered: r.bool(S)?,
1434        });
1435    }
1436    let n = r.len(S, 16)?;
1437    for _ in 0..n {
1438        e.prev_pairs.push(r_pair(r)?);
1439    }
1440    let n = r.len(S, 16)?;
1441    let mut curr_pairs = BTreeSet::new();
1442    for _ in 0..n {
1443        curr_pairs.insert(r_pair(r)?);
1444    }
1445    e.curr_pairs = curr_pairs;
1446    let n = r.len(S, 32)?;
1447    for _ in 0..n {
1448        let k = r_pair(r)?;
1449        let v = r_pair(r)?;
1450        e.prev_triggers.push((k, v));
1451    }
1452    let n = r.len(S, 32)?;
1453    let mut curr_triggers = BTreeMap::new();
1454    for _ in 0..n {
1455        let k = r_pair(r)?;
1456        let v = r_pair(r)?;
1457        curr_triggers.insert(k, v);
1458    }
1459    e.curr_triggers = curr_triggers;
1460    Ok(e)
1461}
1462
1463fn w_tree(w: &mut W, t: &crate::dynamic_bvh::DynamicAabbTree) {
1464    w.usize(t.nodes.len());
1465    for n in &t.nodes {
1466        w.aabb(n.aabb);
1467        w.u32(n.parent);
1468        w.u32(n.left);
1469        w.u32(n.right);
1470        w.i32(n.height);
1471        w.u32(n.user_data);
1472        w.bool(n.is_leaf);
1473    }
1474    w.usize(t.free_list.len());
1475    for &f in &t.free_list {
1476        w.u32(f);
1477    }
1478    w.u32(t.root);
1479    w.fix(t.margin);
1480    let (l1, l2, linf) = t.metric.weights();
1481    w.fix(l1);
1482    w.fix(l2);
1483    w.fix(linf);
1484}
1485
1486fn r_tree(r: &mut R<'_>) -> Res<crate::dynamic_bvh::DynamicAabbTree> {
1487    const S: &str = "broadphase_tree";
1488    let mut t = crate::dynamic_bvh::DynamicAabbTree::new();
1489    let n = r.len(S, 117)?;
1490    for _ in 0..n {
1491        t.nodes.push(crate::dynamic_bvh::DynamicNode {
1492            aabb: r.aabb()?,
1493            parent: r.u32()?,
1494            left: r.u32()?,
1495            right: r.u32()?,
1496            height: r.i32()?,
1497            user_data: r.u32()?,
1498            is_leaf: r.bool(S)?,
1499        });
1500    }
1501    let n = r.len(S, 4)?;
1502    for _ in 0..n {
1503        t.free_list.push(r.u32()?);
1504    }
1505    t.root = r.u32()?;
1506    t.margin = r.fix()?;
1507    let (l1, l2, linf) = (r.fix()?, r.fix()?, r.fix()?);
1508    t.metric = crate::metric::MetricWeights::new(l1, l2, linf).map_err(|_| invalid(S))?;
1509    Ok(t)
1510}
1511
1512// ── Whole world ───────────────────────────────────────────────────────────
1513
1514/// Everything decoded from a blob, before it is moved into a world.
1515struct Decoded {
1516    config: SolverConfig,
1517    bodies: Vec<RigidBody>,
1518    distance_constraints: Vec<DistanceConstraint>,
1519    contact_constraints: Vec<ContactConstraint>,
1520    /// position, rotation, scale, body index, inv_rotation, scale_f32, inv_scale_f32
1521    sdf_poses: Vec<(Vec3Fix, QuatFix, Fix128, usize, QuatFix, f32, f32)>,
1522    sdf_collision_radius: Fix128,
1523    static_colliders: Vec<StaticCollider>,
1524    constraint_batches: Vec<ConstraintBatch>,
1525    batches_dirty: bool,
1526    batch_static_bodies: Vec<bool>,
1527    contact_cache: ContactCache,
1528    material_table: MaterialTable,
1529    body_materials: Vec<u16>,
1530    pre_solve_hooks: usize,
1531    contact_modifiers: usize,
1532    gpu_solver_bridge: usize,
1533    joints: Vec<Joint>,
1534    force_fields: Vec<ForceFieldInstance>,
1535    events: EventCollector,
1536    sleep_config: SleepConfig,
1537    sleep_data: Vec<SleepData>,
1538    body_collision_radii: Vec<Option<Fix128>>,
1539    broadphase: Broadphase,
1540    broadphase_tree: crate::dynamic_bvh::DynamicAabbTree,
1541    broadphase_proxies: Vec<Option<u32>>,
1542    body_colliders: Vec<Option<BodyCollider>>,
1543    body_filters: Vec<CollisionFilter>,
1544    overflow_detected: bool,
1545    /// (entries sorted by id, hits, misses)
1546    tgs_cache: (Vec<(u64, [Fix128; 3])>, u64, u64),
1547    /// (kind, payload) per participant, registration order (version 2)
1548    participants: Vec<(u32, Vec<u8>)>,
1549    /// The recorded fault (version 2)
1550    fault: Option<WorldFault>,
1551    /// The bytes of `FieldBoard::write_values` (version 2; an empty board for
1552    /// version 1)
1553    fields: Vec<u8>,
1554    /// The continuous collision setting (version 3; off for versions 1 and 2)
1555    ccd: super::WorldCcdConfig,
1556    /// The header's `(semantics_id, law_id)` (version 4; `None` before)
1557    ids: Option<([u8; 32], [u8; 32])>,
1558}
1559
1560/// Encoded tag of a static SDF collider's body index ([`crate::sdf_collider::SDF_STATIC`]),
1561/// kept apart from body indices so the blob is the same on 32- and 64-bit targets.
1562const SDF_STATIC_TAG: u8 = 0;
1563
1564impl PhysicsWorld {
1565    /// Magic of a [`Self::snapshot_world`] blob (`b"APWS"`, distinct from
1566    /// [`Self::STATE_MAGIC`] of the rollback blob).
1567    pub const WORLD_SNAPSHOT_MAGIC: [u8; 4] = *b"APWS";
1568
1569    /// Format version of a [`Self::snapshot_world`] blob. Version 1 blobs
1570    /// (no `participants`, `fault` or `fields` section) are still read, as a
1571    /// world without participants, fault or fields, version 1 and 2 blobs
1572    /// (no `continuous_collision` section) as a world with continuous
1573    /// collision off, and version 1 to 3 blobs (no `semantics_id` / `law_id`
1574    /// in the header) without the semantics check, reported as
1575    /// [`LawCheck::Unpinned`] by [`Self::restore_world_checked`]; a blob of
1576    /// any other version is rejected with
1577    /// [`WorldSnapshotError::UnsupportedVersion`].
1578    pub const WORLD_SNAPSHOT_VERSION: u16 = 4;
1579
1580    /// Write every piece of state [`Self::step`] reads into one versioned blob
1581    /// with a checksum.
1582    ///
1583    /// Restore it with [`Self::from_world_snapshot`] (into a new world) or
1584    /// [`Self::restore_world`] (into an existing one, keeping its SDF fields
1585    /// and callbacks). Every later `step` of the restored world is
1586    /// bit-identical to the original's.
1587    ///
1588    /// # Examples
1589    ///
1590    /// ```
1591    /// use alice_physics::{Fix128, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix};
1592    ///
1593    /// let mut world = PhysicsWorld::new(PhysicsConfig::default());
1594    /// world.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
1595    /// let dt = Fix128::from_ratio(1, 60);
1596    /// world.step_n(10, dt);
1597    ///
1598    /// let blob = world.snapshot_world();
1599    /// let mut branch = PhysicsWorld::from_world_snapshot(&blob).unwrap();
1600    /// world.step_n(30, dt);
1601    /// branch.step_n(30, dt);
1602    /// assert_eq!(world.snapshot_world(), branch.snapshot_world());
1603    /// ```
1604    ///
1605    /// # Field coverage
1606    ///
1607    /// Every field of [`PhysicsWorld`], classified. "Saved" fields are written
1608    /// byte-exact; "rebuilt" fields are derived on restore from saved ones;
1609    /// "not covered" fields cannot be written as data, and restore instead
1610    /// requires the target world to already hold the same number of them.
1611    ///
1612    /// | field | class | how / why |
1613    /// |---|---|---|
1614    /// | `config` | saved | all of [`SolverConfig`] |
1615    /// | `bodies` | saved | every [`RigidBody`] field, including `prev_*`, damping, `kinematic_target` |
1616    /// | `distance_constraints` | saved | including `cached_lambda` |
1617    /// | `contact_constraints` | saved | including `cached_lambda` |
1618    /// | `sdf_colliders` (pose) | saved | `position` / `rotation` / `scale` / `body_index` and the cached `inv_rotation` / `scale_f32` / `inv_scale_f32` as stored (not recomputed) |
1619    /// | `sdf_colliders` (`field`) | not covered | `Box<dyn SdfField>` is code; the target world keeps its own fields, and the count must match ([`WorldSnapshotError::SdfFieldCountMismatch`]) |
1620    /// | `sdf_collision_radius` | saved | |
1621    /// | `static_colliders` | saved | plane / height field / triangle mesh including its BVH nodes as stored |
1622    /// | `constraint_batches` / `batches_dirty` / `batch_static_bodies` | saved | as stored, so a stale-but-clean batch set is reproduced rather than recomputed |
1623    /// | `contact_cache` (manifolds, `max_stale_frames`, `warm_start_factor`) | saved | |
1624    /// | `contact_cache` (`pair_index`, std) | rebuilt | from the manifolds in order, the same way `ContactCache::end_frame` rebuilds it |
1625    /// | `material_table` / `body_materials` | saved | materials, pair overrides, default combine rules |
1626    /// | `pre_solve_hooks` / `contact_modifiers` (std) | not covered | closures / trait objects; the count is recorded and must match ([`WorldSnapshotError::CallbackCountMismatch`]) |
1627    /// | `gpu_solver_bridge` (feature `gpu-solver-bridge`) | not covered | trait object; "installed or not" is recorded and must match |
1628    /// | `joints` | saved | all seven kinds, every field |
1629    /// | `joint_motors` / `joint_motors_3d` | not covered | the restore target keeps its own motors (add them with [`PhysicsWorld::add_joint_motor`] / [`PhysicsWorld::add_joint_motor_3d`] before restoring), as the format predates them; a motor's gains and targets that change between frames are saved next to the snapshot |
1630    /// | `force_fields` | saved | field, `affected_bodies`, `enabled` |
1631    /// | `events` | saved | this frame's events and the previous / current pair and trigger sets that decide begin / persist / end |
1632    /// | `islands` (`sleep_data`, `config`) | saved | |
1633    /// | `islands` (union-find `parent` / `rank`) | rebuilt | `IslandManager::new` plus a union of every joint's bodies, the same as the start of every `step` |
1634    /// | `body_collision_radii` / `body_filters` | saved | as stored, including their own lengths |
1635    /// | `broadphase` / `broadphase_tree` / `broadphase_proxies` | saved | the persistent tree node by node (pair order follows the tree layout) |
1636    /// | `broadphase_hybrid` | rebuilt | empty; its pairs are a pure function of the bodies staged each substep, so a fresh one gives the same pairs |
1637    /// | `body_colliders` | saved | shape or compound, including the compound's cached AABB and dirty flag |
1638    /// | `overflow_detected` | saved | sticky flag |
1639    /// | `kinematic_substeps_left` | rebuilt | always 0 outside `step` (reset at the end of every step), and a snapshot is only taken between steps |
1640    /// | `tgs_impulse_cache` (std) | saved | entries sorted by id, hit / miss counters; the live set is rebuilt empty (the `sweep` at the end of every step empties it) |
1641    /// | `participants` (std) | saved / not covered | each participant's kind and its [`crate::world_participant::Participant::write_state`] payload are saved; the participant itself is code, so the target world must hold the same kinds in the same order ([`WorldSnapshotError::ParticipantMismatch`]), and every payload is checked before any is read ([`WorldSnapshotError::ParticipantState`]) |
1642    /// | `participant_plan` (std) | not covered | derived from the target world's own participants and fields |
1643    /// | `fault` | saved | the recorded fault, so a restored branch is still faulted |
1644    /// | `fields` | saved | the committed values once ([`crate::world_participant::FieldBoard::write_values`]); the target world must declare the same fields ([`WorldSnapshotError::FieldState`]) |
1645    /// | `ccd` | saved | [`PhysicsWorld::continuous_collision`] (version 3) |
1646    ///
1647    /// Not in the table because they are not [`PhysicsWorld`] fields:
1648    /// motors the caller applies itself with [`crate::motor::apply_motors`]
1649    /// and [`crate::character::CharacterController`] are owned by the caller and
1650    /// are applied from outside `step`, and cloth / fluid / FEM / vehicle state
1651    /// lives in its own types. Save them next to the world snapshot.
1652    ///
1653    // LIMITATION(COV-ENGINE-012): Known difference: the union-find of `islands` is rebuilt from the joints.
1654    /// Known difference: the union-find of `islands` is rebuilt from the joints. After a
1655    /// `remove_joint` between two steps, the original still carries that union
1656    /// until the next `step` resets it, while the restored world does not; the
1657    /// two differ only for [`PhysicsWorld::wake_body`] called before that next
1658    /// step. The structure is private to [`crate::sleeping`] and has no
1659    /// non-mutating accessor, so it cannot be copied from `&self`.
1660    ///
1661    /// # Format (version 4)
1662    ///
1663    /// | range | content |
1664    /// |---|---|
1665    /// | `[0..4)` | magic [`PhysicsWorld::WORLD_SNAPSHOT_MAGIC`] (`b"APWS"`) |
1666    /// | `[4..6)` | version u16 = [`PhysicsWorld::WORLD_SNAPSHOT_VERSION`] |
1667    /// | `[6..8)` | reserved u16 = 0 |
1668    /// | `[8..16)` | payload length u64 |
1669    /// | `[16..48)` | `semantics_id`: [`crate::PHYSICS_SEMANTICS_ID`] of the writing binary |
1670    /// | `[48..80)` | `law_id`: [`PhysicsWorld::law_id`] of this world under that `semantics_id` |
1671    /// | `[80..80+len)` | payload (the sections in the table above, little endian) |
1672    /// | last 8 | FNV-1a 64 of every preceding byte (header included) |
1673    ///
1674    /// Version 1 to 3 blobs have the 16-byte header only (no `semantics_id`
1675    /// or `law_id`) and their payload starts at byte 16; the payload itself
1676    /// is the same in version 3 and 4. Version 4 grows the header rather than
1677    /// using the reserved bytes, which stay 0.
1678    ///
1679    /// The header `law_id` is the rule of the world when it was written. It
1680    /// is not guaranteed to equal [`PhysicsWorld::law_id`] of a world after
1681    /// restore: the restored world keeps what the blob does not hold (for
1682    /// example its own participants), so whatever [`PhysicsWorld::law_id`]
1683    /// reads from those comes from the target world. The reader does not
1684    /// recompute it; [`Self::restore_world_checked`] compares the header
1685    /// value with the one the caller expects.
1686    ///
1687    /// Version 2 appends three sections to the version 1 payload, after the
1688    /// TGS cache, in this order:
1689    ///
1690    /// | section | content |
1691    /// |---|---|
1692    /// | `participants` | `count: u64`, then per participant `kind: u32`, `payload_len: u64`, payload |
1693    /// | `fault` | `u8` code: `0` none, `1` participant (`index: u64`, `kind: u32`, fault tag `u8`), `2` rigid overflow, `3` force out of range (`body: u64`), `4` field out of range (`field: u32`, `index: u64`) |
1694    /// | `fields` | [`crate::world_participant::FieldBoard::write_values`] |
1695    ///
1696    /// Version 3 appends one section after `fields`:
1697    ///
1698    /// | section | content |
1699    /// |---|---|
1700    /// | `continuous_collision` | `enabled: u8` (`0` / `1`), `motion_threshold` ([`PhysicsWorld::continuous_collision`]) |
1701    ///
1702    /// The version is raised for it because the reader rejects trailing
1703    /// bytes: a section appended to a version 2 blob could not be told apart
1704    /// from a corrupt one.
1705    #[must_use]
1706    pub fn snapshot_world(&self) -> Vec<u8> {
1707        let mut w = W(Vec::new());
1708        w.0.extend_from_slice(&Self::WORLD_SNAPSHOT_MAGIC);
1709        w.u16(Self::WORLD_SNAPSHOT_VERSION);
1710        w.u16(0);
1711        w.u64(0); // payload length, patched below
1712        w.0.extend_from_slice(&PHYSICS_SEMANTICS_ID);
1713        w.0.extend_from_slice(&self.law_id(&PHYSICS_SEMANTICS_ID));
1714
1715        w_config(&mut w, &self.config);
1716
1717        w.usize(self.bodies.len());
1718        for b in &self.bodies {
1719            w_body(&mut w, b);
1720        }
1721
1722        w.usize(self.distance_constraints.len());
1723        for c in &self.distance_constraints {
1724            w.usize(c.body_a);
1725            w.usize(c.body_b);
1726            w.vec3(c.local_anchor_a);
1727            w.vec3(c.local_anchor_b);
1728            w.fix(c.target_distance);
1729            w.fix(c.compliance);
1730            w.fix(c.cached_lambda);
1731        }
1732
1733        w.usize(self.contact_constraints.len());
1734        for c in &self.contact_constraints {
1735            w.usize(c.body_a);
1736            w.usize(c.body_b);
1737            w_contact(&mut w, &c.contact);
1738            w.fix(c.friction);
1739            w.fix(c.restitution);
1740            w.fix(c.cached_lambda);
1741        }
1742
1743        w.usize(self.sdf_colliders.len());
1744        for s in &self.sdf_colliders {
1745            w.vec3(s.position);
1746            w.quat(s.rotation);
1747            w.fix(s.scale);
1748            if s.body_index == crate::sdf_collider::SDF_STATIC {
1749                w.u8(SDF_STATIC_TAG);
1750            } else {
1751                w.u8(1);
1752                w.usize(s.body_index);
1753            }
1754            w.quat(s.inv_rotation);
1755            w.f32(s.scale_f32);
1756            w.f32(s.inv_scale_f32);
1757        }
1758        w.fix(self.sdf_collision_radius);
1759
1760        w.usize(self.static_colliders.len());
1761        for c in &self.static_colliders {
1762            w_static_collider(&mut w, c);
1763        }
1764
1765        w.usize(self.constraint_batches.len());
1766        for b in &self.constraint_batches {
1767            w.usize(b.distance_indices.len());
1768            for &i in &b.distance_indices {
1769                w.usize(i);
1770            }
1771            w.usize(b.contact_indices.len());
1772            for &i in &b.contact_indices {
1773                w.usize(i);
1774            }
1775        }
1776        w.bool(self.batches_dirty);
1777        w.usize(self.batch_static_bodies.len());
1778        for &s in &self.batch_static_bodies {
1779            w.bool(s);
1780        }
1781
1782        w_contact_cache(&mut w, &self.contact_cache);
1783        w_material_table(&mut w, &self.material_table);
1784        w.usize(self.body_materials.len());
1785        for &m in &self.body_materials {
1786            w.u16(m);
1787        }
1788
1789        // Callbacks are code: only their count is recorded.
1790        #[cfg(feature = "std")]
1791        {
1792            w.usize(self.pre_solve_hooks.len());
1793            w.usize(self.contact_modifiers.len());
1794        }
1795        #[cfg(not(feature = "std"))]
1796        {
1797            w.usize(0);
1798            w.usize(0);
1799        }
1800        #[cfg(feature = "gpu-solver-bridge")]
1801        w.usize(usize::from(self.gpu_solver_bridge.is_some()));
1802        #[cfg(not(feature = "gpu-solver-bridge"))]
1803        w.usize(0);
1804
1805        w.usize(self.joints.len());
1806        for j in &self.joints {
1807            w_joint(&mut w, j);
1808        }
1809
1810        w.usize(self.force_fields.len());
1811        for f in &self.force_fields {
1812            w_force_field(&mut w, f);
1813        }
1814
1815        w_events(&mut w, &self.events);
1816
1817        w.fix(self.islands.config.linear_threshold);
1818        w.fix(self.islands.config.angular_threshold);
1819        w.u32(self.islands.config.frames_to_sleep);
1820        w.usize(self.islands.sleep_data.len());
1821        for sd in &self.islands.sleep_data {
1822            w.u8(match sd.state {
1823                SleepState::Awake => 0,
1824                SleepState::Sleeping => 1,
1825            });
1826            w.u32(sd.idle_frames);
1827        }
1828
1829        w.usize(self.body_collision_radii.len());
1830        for &r in &self.body_collision_radii {
1831            w.opt_fix(r);
1832        }
1833
1834        w.u8(match self.broadphase {
1835            Broadphase::Bvh => 0,
1836            Broadphase::DynamicTree => 1,
1837            Broadphase::Hybrid => 2,
1838        });
1839        w_tree(&mut w, &self.broadphase_tree);
1840        w.usize(self.broadphase_proxies.len());
1841        for &p in &self.broadphase_proxies {
1842            match p {
1843                Some(id) => {
1844                    w.u8(1);
1845                    w.u32(id);
1846                }
1847                None => w.u8(0),
1848            }
1849        }
1850
1851        w.usize(self.body_colliders.len());
1852        for c in &self.body_colliders {
1853            w_body_collider(&mut w, c.as_ref());
1854        }
1855
1856        w.usize(self.body_filters.len());
1857        for f in &self.body_filters {
1858            w.u32(f.layer);
1859            w.u32(f.mask);
1860            w.u32(f.group);
1861        }
1862
1863        w.bool(self.overflow_detected);
1864
1865        #[cfg(feature = "std")]
1866        {
1867            let cache = &self.tgs_impulse_cache;
1868            let mut entries: Vec<_> = cache.entries.iter().map(|(&k, &v)| (k, v)).collect();
1869            entries.sort_unstable_by_key(|e| e.0);
1870            w.usize(entries.len());
1871            for (id, imp) in entries {
1872                w.u64(id);
1873                w.fix(imp.normal);
1874                w.fix(imp.tangent1);
1875                w.fix(imp.tangent2);
1876            }
1877            w.u64(cache.hits);
1878            w.u64(cache.misses);
1879        }
1880        #[cfg(not(feature = "std"))]
1881        {
1882            w.usize(0);
1883            w.u64(0);
1884            w.u64(0);
1885        }
1886
1887        #[cfg(feature = "std")]
1888        let participants = {
1889            let mut out = Vec::new();
1890            self.write_participants(&mut out);
1891            out
1892        };
1893        #[cfg(not(feature = "std"))]
1894        let participants: Vec<(u32, Vec<u8>)> = Vec::new();
1895        w.usize(participants.len());
1896        for (kind, payload) in &participants {
1897            w.u32(*kind);
1898            w.usize(payload.len());
1899            w.0.extend_from_slice(payload);
1900        }
1901        w_fault(&mut w, self.fault());
1902        self.fields().write_values(&mut w.0);
1903        w.bool(self.ccd.is_enabled());
1904        w.fix(self.ccd.motion_threshold());
1905
1906        let mut data = w.0;
1907        let payload_len = (data.len() - HEADER_LEN_V4) as u64;
1908        data[8..16].copy_from_slice(&payload_len.to_le_bytes());
1909        let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
1910        fnv1a_fold(&mut hash, &data);
1911        data.extend_from_slice(&hash.to_le_bytes());
1912        data
1913    }
1914
1915    /// Build a new world from a [`Self::snapshot_world`] blob.
1916    ///
1917    /// The new world holds no SDF fields, hooks, contact modifiers or GPU
1918    /// bridge, so a snapshot taken with any of those is rejected
1919    /// ([`WorldSnapshotError::SdfFieldCountMismatch`] /
1920    /// [`WorldSnapshotError::CallbackCountMismatch`]); use
1921    /// [`Self::restore_world`] on a world that holds them instead.
1922    ///
1923    /// Rotations are restored as stored, as in [`Self::restore_world`].
1924    ///
1925    /// # Errors
1926    ///
1927    /// See [`WorldSnapshotError`] for which input gives which error.
1928    pub fn from_world_snapshot(data: &[u8]) -> Result<Self, WorldSnapshotError> {
1929        let mut world = Self::new(SolverConfig::default());
1930        world.restore_world(data)?;
1931        Ok(world)
1932    }
1933
1934    /// Replace this world's state with a [`Self::snapshot_world`] blob.
1935    ///
1936    /// Everything in the blob replaces the current state, including the
1937    /// [`SolverConfig`] and the body population. What is not data stays:
1938    /// the SDF fields of `self.sdf_colliders` (paired with the blob's poses in
1939    /// order), the pre-solve hooks, contact modifiers and the installed GPU
1940    /// bridge; their counts must equal the snapshot's.
1941    ///
1942    /// Body and SDF collider rotations are restored as stored and brought to
1943    /// unit length by the next step, so a non-unit rotation in `data` scales
1944    /// what it rotates by `|q|^2` in the queries made before that step (see
1945    /// [`RigidBody::rotation`]). The next step also rebuilds each collider's
1946    /// cached inverse rotation and scale from its restored fields
1947    /// ([`crate::sdf_collider::SdfCollider::update_cache`]).
1948    ///
1949    /// # Errors
1950    ///
1951    /// See [`WorldSnapshotError`] for which input gives which error. On error `self` is unchanged.
1952    pub fn restore_world(&mut self, data: &[u8]) -> Result<(), WorldSnapshotError> {
1953        self.restore_world_checked(data, None).map(|_| ())
1954    }
1955
1956    /// [`Self::restore_world`], also comparing the blob's rule identifier
1957    /// with `expected_law_id` and reporting what could be verified.
1958    ///
1959    /// | blob | `expected_law_id` | result |
1960    /// |---|---|---|
1961    /// | version 4, header `semantics_id` is not [`crate::PHYSICS_SEMANTICS_ID`] | any | [`WorldSnapshotError::SemanticsMismatch`] |
1962    /// | version 4 | `Some(id)`, header `law_id == id` | [`LawCheck::Verified`] |
1963    /// | version 4 | `Some(id)`, header `law_id != id` | [`WorldSnapshotError::LawIdMismatch`] |
1964    /// | version 4 | `None` | [`LawCheck::SemanticsVerified`] |
1965    /// | version 1 to 3 | any (not compared) | [`LawCheck::Unpinned`] |
1966    ///
1967    /// `expected_law_id` is compared with the header as written, not with
1968    /// [`Self::law_id`] of the restored world (see the format notes on
1969    /// [`Self::snapshot_world`]). To accept only verified blobs, refuse
1970    /// anything but [`LawCheck::Verified`].
1971    ///
1972    /// # Intended use
1973    ///
1974    /// Pass the target world's own `self.law_id(&PHYSICS_SEMANTICS_ID)`,
1975    /// computed **before** the restore, as `expected_law_id`: the blob is then
1976    /// accepted only if it was written under the rule this world already
1977    /// runs. Passing the blob header's own `law_id` compares the blob with
1978    /// itself and always gives [`LawCheck::Verified`], which checks nothing.
1979    /// The target's participants are kept by the restore (they are code, not
1980    /// data), so a target whose participant has the same kind but another
1981    /// step rule is refused this way, while a plain [`Self::restore_world`]
1982    /// accepts it.
1983    ///
1984    /// [`LawCheck::Verified`] then means that the target was already built
1985    /// with the same rules as the blob before the restore. The restore
1986    /// overwrites the configuration, but the target's own `law_id` is taken
1987    /// before that and covers the whole configuration, so a target that
1988    /// differs only in configuration (gravity, materials, force fields, and
1989    /// so on) gets [`WorldSnapshotError::LawIdMismatch`]: the check is
1990    /// strict and never accepts a blob written under other rules.
1991    ///
1992    /// # Examples
1993    ///
1994    /// ```
1995    /// use alice_physics::{
1996    ///     Fix128, LawCheck, PhysicsConfig, PhysicsWorld, RigidBody, Vec3Fix,
1997    ///     WorldSnapshotError, PHYSICS_SEMANTICS_ID,
1998    /// };
1999    ///
2000    /// let mut world = PhysicsWorld::new(PhysicsConfig::default());
2001    /// world.add_body(RigidBody::new_dynamic(Vec3Fix::from_int(0, 5, 0), Fix128::ONE));
2002    /// let blob = world.snapshot_world();
2003    ///
2004    /// // A target running the same rule: its own law id, taken before the restore
2005    /// let mut same = PhysicsWorld::new(PhysicsConfig::default());
2006    /// let expected = same.law_id(&PHYSICS_SEMANTICS_ID);
2007    /// assert_eq!(same.restore_world_checked(&blob, Some(&expected)), Ok(LawCheck::Verified));
2008    ///
2009    /// // A target running another rule refuses the blob and keeps its state
2010    /// let mut other = PhysicsWorld::new(PhysicsConfig {
2011    ///     gravity: Vec3Fix::from_int(0, -3, 0),
2012    ///     ..PhysicsConfig::default()
2013    /// });
2014    /// let expected = other.law_id(&PHYSICS_SEMANTICS_ID);
2015    /// let before = other.snapshot_world();
2016    /// assert!(matches!(
2017    ///     other.restore_world_checked(&blob, Some(&expected)),
2018    ///     Err(WorldSnapshotError::LawIdMismatch { .. })
2019    /// ));
2020    /// assert_eq!(other.snapshot_world(), before);
2021    ///
2022    /// // Without an expected id only the stepping semantics is checked
2023    /// assert_eq!(other.restore_world_checked(&blob, None), Ok(LawCheck::SemanticsVerified));
2024    /// ```
2025    ///
2026    /// # Errors
2027    ///
2028    /// Every error of [`Self::restore_world`], and
2029    /// [`WorldSnapshotError::LawIdMismatch`]. On error `self` is unchanged.
2030    pub fn restore_world_checked(
2031        &mut self,
2032        data: &[u8],
2033        expected_law_id: Option<&[u8; 32]>,
2034    ) -> Result<LawCheck, WorldSnapshotError> {
2035        let d = decode(data)?;
2036        let check = law_check(d.ids, expected_law_id)?;
2037        self.check_attachable(&d)?;
2038        self.check_participants(&d.participants)?;
2039        self.fields()
2040            .check_values(&d.fields)
2041            .map_err(WorldSnapshotError::FieldState)?;
2042        self.install(d);
2043        Ok(check)
2044    }
2045
2046    fn check_attachable(&self, d: &Decoded) -> Res<()> {
2047        if d.sdf_poses.len() != self.sdf_colliders.len() {
2048            return Err(WorldSnapshotError::SdfFieldCountMismatch {
2049                snapshot: d.sdf_poses.len(),
2050                world: self.sdf_colliders.len(),
2051            });
2052        }
2053        #[cfg(feature = "std")]
2054        let (hooks, modifiers) = (self.pre_solve_hooks.len(), self.contact_modifiers.len());
2055        #[cfg(not(feature = "std"))]
2056        let (hooks, modifiers) = (0usize, 0usize);
2057        #[cfg(feature = "gpu-solver-bridge")]
2058        let bridge = usize::from(self.gpu_solver_bridge.is_some());
2059        #[cfg(not(feature = "gpu-solver-bridge"))]
2060        let bridge = 0usize;
2061        for (section, snapshot, world) in [
2062            ("pre_solve_hooks", d.pre_solve_hooks, hooks),
2063            ("contact_modifiers", d.contact_modifiers, modifiers),
2064            ("gpu_solver_bridge", d.gpu_solver_bridge, bridge),
2065        ] {
2066            if snapshot != world {
2067                return Err(WorldSnapshotError::CallbackCountMismatch {
2068                    section,
2069                    snapshot,
2070                    world,
2071                });
2072            }
2073        }
2074        Ok(())
2075    }
2076
2077    fn install(&mut self, d: Decoded) {
2078        self.config = d.config;
2079        self.bodies = d.bodies;
2080        self.distance_constraints = d.distance_constraints;
2081        self.contact_constraints = d.contact_constraints;
2082        for (s, p) in self.sdf_colliders.iter_mut().zip(d.sdf_poses) {
2083            s.position = p.0;
2084            s.rotation = p.1;
2085            s.scale = p.2;
2086            s.body_index = p.3;
2087            s.inv_rotation = p.4;
2088            s.scale_f32 = p.5;
2089            s.inv_scale_f32 = p.6;
2090        }
2091        self.sdf_collision_radius = d.sdf_collision_radius;
2092        self.static_colliders = d.static_colliders;
2093        self.constraint_batches = d.constraint_batches;
2094        self.batches_dirty = d.batches_dirty;
2095        self.batch_static_bodies = d.batch_static_bodies;
2096        self.contact_cache = d.contact_cache;
2097        self.material_table = d.material_table;
2098        self.body_materials = d.body_materials;
2099        self.joints = d.joints;
2100        self.force_fields = d.force_fields;
2101        self.events = d.events;
2102
2103        // Rebuilt: the union-find, as at the start of every `step`.
2104        let mut islands = IslandManager::new(d.sleep_data.len(), d.sleep_config);
2105        for j in &self.joints {
2106            let (a, b) = j.bodies();
2107            if a < d.sleep_data.len() && b < d.sleep_data.len() {
2108                islands.union(a, b);
2109            }
2110        }
2111        islands.sleep_data = d.sleep_data;
2112        self.islands = islands;
2113
2114        self.body_collision_radii = d.body_collision_radii;
2115        self.broadphase = d.broadphase;
2116        self.broadphase_tree = d.broadphase_tree;
2117        self.broadphase_proxies = d.broadphase_proxies;
2118        self.broadphase_hybrid = crate::bvh::BroadphaseHybrid::new();
2119        self.body_colliders = d.body_colliders;
2120        self.body_filters = d.body_filters;
2121        self.overflow_detected = d.overflow_detected;
2122        self.kinematic_substeps_left = 0;
2123
2124        #[cfg(feature = "std")]
2125        {
2126            let (entries, hits, misses) = d.tgs_cache;
2127            let mut cache = crate::solver_tgs::ImpulseCache::new();
2128            for (id, [n, t1, t2]) in entries {
2129                cache.entries.insert(
2130                    id,
2131                    crate::solver_tgs::CachedImpulse {
2132                        normal: n,
2133                        tangent1: t1,
2134                        tangent2: t2,
2135                    },
2136                );
2137            }
2138            // Rebuilt: the live set is empty between steps (`sweep` clears it).
2139            cache.hits = hits;
2140            cache.misses = misses;
2141            self.tgs_impulse_cache = cache;
2142        }
2143        #[cfg(not(feature = "std"))]
2144        let _ = d.tgs_cache; // the TGS backend falls back to XPBD without std
2145
2146        // Checked by `restore_world` before anything was installed.
2147        self.read_participants(&d.participants);
2148        self.set_fault(d.fault);
2149        self.fields_mut().read_values(&d.fields);
2150        self.ccd = d.ccd;
2151    }
2152}
2153
2154/// What a decoded blob's header identifiers verify against `expected`.
2155fn law_check(ids: Option<([u8; 32], [u8; 32])>, expected: Option<&[u8; 32]>) -> Res<LawCheck> {
2156    let Some((_, stored)) = ids else {
2157        return Ok(LawCheck::Unpinned);
2158    };
2159    match expected {
2160        None => Ok(LawCheck::SemanticsVerified),
2161        Some(e) if *e == stored => Ok(LawCheck::Verified),
2162        Some(e) => Err(WorldSnapshotError::LawIdMismatch {
2163            stored,
2164            expected: *e,
2165        }),
2166    }
2167}
2168
2169fn decode(data: &[u8]) -> Res<Decoded> {
2170    if data.len() < HEADER_LEN + CHECKSUM_LEN {
2171        return Err(WorldSnapshotError::Truncated);
2172    }
2173    if data[0..4] != PhysicsWorld::WORLD_SNAPSHOT_MAGIC {
2174        return Err(WorldSnapshotError::BadMagic);
2175    }
2176    let version = u16::from_le_bytes([data[4], data[5]]);
2177    if !(1..=PhysicsWorld::WORLD_SNAPSHOT_VERSION).contains(&version) {
2178        return Err(WorldSnapshotError::UnsupportedVersion {
2179            found: version,
2180            supported: PhysicsWorld::WORLD_SNAPSHOT_VERSION,
2181        });
2182    }
2183    if data[6] != 0 || data[7] != 0 {
2184        return Err(WorldSnapshotError::ReservedNotZero);
2185    }
2186    let header = header_len(version);
2187    if data.len() < header + CHECKSUM_LEN {
2188        return Err(WorldSnapshotError::Truncated);
2189    }
2190    let mut len_bytes = [0u8; 8];
2191    len_bytes.copy_from_slice(&data[8..16]);
2192    let payload_len = u64::from_le_bytes(len_bytes);
2193    let available = (data.len() - header - CHECKSUM_LEN) as u64;
2194    if payload_len > available {
2195        return Err(WorldSnapshotError::Truncated);
2196    }
2197    if payload_len < available {
2198        return Err(WorldSnapshotError::TrailingBytes {
2199            extra: (available - payload_len) as usize,
2200        });
2201    }
2202    let body_end = data.len() - CHECKSUM_LEN;
2203    let mut stored = [0u8; 8];
2204    stored.copy_from_slice(&data[body_end..]);
2205    let stored = u64::from_le_bytes(stored);
2206    let mut computed: u64 = 0xcbf2_9ce4_8422_2325;
2207    fnv1a_fold(&mut computed, &data[..body_end]);
2208    if stored != computed {
2209        return Err(WorldSnapshotError::ChecksumMismatch { stored, computed });
2210    }
2211    let ids = if version >= 4 {
2212        let mut semantics_id = [0u8; 32];
2213        semantics_id.copy_from_slice(&data[SEMANTICS_ID_AT..SEMANTICS_ID_AT + 32]);
2214        let mut law_id = [0u8; 32];
2215        law_id.copy_from_slice(&data[LAW_ID_AT..LAW_ID_AT + 32]);
2216        if semantics_id != PHYSICS_SEMANTICS_ID {
2217            return Err(WorldSnapshotError::SemanticsMismatch {
2218                stored: semantics_id,
2219                expected: PHYSICS_SEMANTICS_ID,
2220            });
2221        }
2222        Some((semantics_id, law_id))
2223    } else {
2224        None
2225    };
2226
2227    let mut r = R {
2228        data: &data[..body_end],
2229        pos: header,
2230    };
2231    let mut d = decode_payload(&mut r, version)?;
2232    d.ids = ids;
2233    if r.pos != body_end {
2234        return Err(WorldSnapshotError::TrailingBytes {
2235            extra: body_end - r.pos,
2236        });
2237    }
2238    validate(&d)?;
2239    Ok(d)
2240}
2241
2242fn decode_payload(r: &mut R<'_>, version: u16) -> Res<Decoded> {
2243    let config = r_config(r)?;
2244
2245    let n = r.len("bodies", 1)?;
2246    let mut bodies = Vec::with_capacity(n);
2247    for _ in 0..n {
2248        bodies.push(r_body(r)?);
2249    }
2250
2251    let n = r.len("distance_constraints", 160)?;
2252    let mut distance_constraints = Vec::with_capacity(n);
2253    for _ in 0..n {
2254        const S: &str = "distance_constraints";
2255        distance_constraints.push(DistanceConstraint {
2256            body_a: r.usize(S)?,
2257            body_b: r.usize(S)?,
2258            local_anchor_a: r.vec3()?,
2259            local_anchor_b: r.vec3()?,
2260            target_distance: r.fix()?,
2261            compliance: r.fix()?,
2262            cached_lambda: r.fix()?,
2263        });
2264    }
2265
2266    let n = r.len("contact_constraints", 208)?;
2267    let mut contact_constraints = Vec::with_capacity(n);
2268    for _ in 0..n {
2269        const S: &str = "contact_constraints";
2270        contact_constraints.push(ContactConstraint {
2271            body_a: r.usize(S)?,
2272            body_b: r.usize(S)?,
2273            contact: r_contact(r)?,
2274            friction: r.fix()?,
2275            restitution: r.fix()?,
2276            cached_lambda: r.fix()?,
2277        });
2278    }
2279
2280    let n = r.len("sdf_colliders", 193)?;
2281    let mut sdf_poses = Vec::with_capacity(n);
2282    for _ in 0..n {
2283        const S: &str = "sdf_colliders";
2284        let position = r.vec3()?;
2285        let rotation = r.quat()?;
2286        let scale = r.fix()?;
2287        let body_index = match r.u8()? {
2288            SDF_STATIC_TAG => crate::sdf_collider::SDF_STATIC,
2289            1 => r.usize(S)?,
2290            _ => return Err(invalid(S)),
2291        };
2292        let inv_rotation = r.quat()?;
2293        let scale_f32 = r.f32()?;
2294        let inv_scale_f32 = r.f32()?;
2295        sdf_poses.push((
2296            position,
2297            rotation,
2298            scale,
2299            body_index,
2300            inv_rotation,
2301            scale_f32,
2302            inv_scale_f32,
2303        ));
2304    }
2305    let sdf_collision_radius = r.fix()?;
2306
2307    let n = r.len("static_colliders", 1)?;
2308    let mut static_colliders = Vec::with_capacity(n);
2309    for _ in 0..n {
2310        static_colliders.push(r_static_collider(r)?);
2311    }
2312
2313    let n = r.len("constraint_batches", 16)?;
2314    let mut constraint_batches = Vec::with_capacity(n);
2315    for _ in 0..n {
2316        const S: &str = "constraint_batches";
2317        let k = r.len(S, 8)?;
2318        let mut distance_indices = Vec::with_capacity(k);
2319        for _ in 0..k {
2320            distance_indices.push(r.usize(S)?);
2321        }
2322        let k = r.len(S, 8)?;
2323        let mut contact_indices = Vec::with_capacity(k);
2324        for _ in 0..k {
2325            contact_indices.push(r.usize(S)?);
2326        }
2327        constraint_batches.push(ConstraintBatch {
2328            distance_indices,
2329            contact_indices,
2330        });
2331    }
2332    let batches_dirty = r.bool("constraint_batches")?;
2333    let n = r.len("batch_static_bodies", 1)?;
2334    let mut batch_static_bodies = Vec::with_capacity(n);
2335    for _ in 0..n {
2336        batch_static_bodies.push(r.bool("batch_static_bodies")?);
2337    }
2338
2339    let contact_cache = r_contact_cache(r)?;
2340    let material_table = r_material_table(r)?;
2341    let n = r.len("body_materials", 2)?;
2342    let mut body_materials = Vec::with_capacity(n);
2343    for _ in 0..n {
2344        body_materials.push(r.u16()?);
2345    }
2346
2347    let pre_solve_hooks = r.usize("pre_solve_hooks")?;
2348    let contact_modifiers = r.usize("contact_modifiers")?;
2349    let gpu_solver_bridge = r.usize("gpu_solver_bridge")?;
2350
2351    let n = r.len("joints", 1)?;
2352    let mut joints = Vec::with_capacity(n);
2353    for _ in 0..n {
2354        joints.push(r_joint(r)?);
2355    }
2356
2357    let n = r.len("force_fields", 1)?;
2358    let mut force_fields = Vec::with_capacity(n);
2359    for _ in 0..n {
2360        force_fields.push(r_force_field(r)?);
2361    }
2362
2363    let events = r_events(r)?;
2364
2365    let sleep_config = SleepConfig {
2366        linear_threshold: r.fix()?,
2367        angular_threshold: r.fix()?,
2368        frames_to_sleep: r.u32()?,
2369    };
2370    let n = r.len("islands", 5)?;
2371    let mut sleep_data = Vec::with_capacity(n);
2372    for _ in 0..n {
2373        let state = match r.u8()? {
2374            0 => SleepState::Awake,
2375            1 => SleepState::Sleeping,
2376            _ => return Err(invalid("islands")),
2377        };
2378        sleep_data.push(SleepData {
2379            state,
2380            idle_frames: r.u32()?,
2381        });
2382    }
2383
2384    let n = r.len("body_collision_radii", 1)?;
2385    let mut body_collision_radii = Vec::with_capacity(n);
2386    for _ in 0..n {
2387        body_collision_radii.push(r.opt_fix("body_collision_radii")?);
2388    }
2389
2390    let broadphase = match r.u8()? {
2391        0 => Broadphase::Bvh,
2392        1 => Broadphase::DynamicTree,
2393        2 => Broadphase::Hybrid,
2394        _ => return Err(invalid("broadphase")),
2395    };
2396    let broadphase_tree = r_tree(r)?;
2397    let n = r.len("broadphase_proxies", 1)?;
2398    let mut broadphase_proxies = Vec::with_capacity(n);
2399    for _ in 0..n {
2400        broadphase_proxies.push(if r.bool("broadphase_proxies")? {
2401            Some(r.u32()?)
2402        } else {
2403            None
2404        });
2405    }
2406
2407    let n = r.len("body_colliders", 1)?;
2408    let mut body_colliders = Vec::with_capacity(n);
2409    for _ in 0..n {
2410        body_colliders.push(r_body_collider(r)?);
2411    }
2412
2413    let n = r.len("body_filters", 12)?;
2414    let mut body_filters = Vec::with_capacity(n);
2415    for _ in 0..n {
2416        body_filters.push(CollisionFilter {
2417            layer: r.u32()?,
2418            mask: r.u32()?,
2419            group: r.u32()?,
2420        });
2421    }
2422
2423    let overflow_detected = r.bool("overflow_detected")?;
2424
2425    let n = r.len("tgs_impulse_cache", 56)?;
2426    let mut entries = Vec::with_capacity(n);
2427    for _ in 0..n {
2428        entries.push((r.u64()?, [r.fix()?, r.fix()?, r.fix()?]));
2429    }
2430    let tgs_cache = (entries, r.u64()?, r.u64()?);
2431
2432    let (participants, fault, fields) = if version >= 2 {
2433        let n = r.len("participants", 12)?;
2434        let mut participants = Vec::with_capacity(n);
2435        for _ in 0..n {
2436            let kind = r.u32()?;
2437            let len = r.len("participants", 1)?;
2438            participants.push((kind, r.take(len)?.to_vec()));
2439        }
2440        (participants, r_fault(r)?, r_fields(r)?)
2441    } else {
2442        let mut empty = Vec::new();
2443        crate::world_participant::FieldBoard::new().write_values(&mut empty);
2444        (Vec::new(), None, empty)
2445    };
2446    let ccd = if version >= 3 {
2447        super::WorldCcdConfig::new()
2448            .with_enabled(r.bool("continuous_collision")?)
2449            .with_motion_threshold(r.fix()?)
2450    } else {
2451        super::WorldCcdConfig::new()
2452    };
2453
2454    Ok(Decoded {
2455        config,
2456        bodies,
2457        distance_constraints,
2458        contact_constraints,
2459        sdf_poses,
2460        sdf_collision_radius,
2461        static_colliders,
2462        constraint_batches,
2463        batches_dirty,
2464        batch_static_bodies,
2465        contact_cache,
2466        material_table,
2467        body_materials,
2468        pre_solve_hooks,
2469        contact_modifiers,
2470        gpu_solver_bridge,
2471        joints,
2472        force_fields,
2473        events,
2474        sleep_config,
2475        sleep_data,
2476        body_collision_radii,
2477        broadphase,
2478        broadphase_tree,
2479        broadphase_proxies,
2480        body_colliders,
2481        body_filters,
2482        overflow_detected,
2483        tgs_cache,
2484        participants,
2485        fault,
2486        fields,
2487        ccd,
2488        ids: None,
2489    })
2490}
2491
2492fn w_fault(w: &mut W, fault: Option<WorldFault>) {
2493    match fault {
2494        None => w.u8(0),
2495        Some(WorldFault::Participant { index, kind, fault }) => {
2496            w.u8(1);
2497            w.usize(index);
2498            w.u32(kind.get());
2499            w.u8(fault.tag());
2500        }
2501        Some(WorldFault::RigidOverflow) => w.u8(2),
2502        Some(WorldFault::ForceOutOfRange { body }) => {
2503            w.u8(3);
2504            w.usize(body);
2505        }
2506        Some(WorldFault::FieldOutOfRange { field, index }) => {
2507            w.u8(4);
2508            w.u32(field.get());
2509            w.usize(index);
2510        }
2511    }
2512}
2513
2514fn r_fault(r: &mut R<'_>) -> Res<Option<WorldFault>> {
2515    const SECTION: &str = "fault";
2516    Ok(match r.u8()? {
2517        0 => None,
2518        1 => {
2519            let index = r.usize(SECTION)?;
2520            let kind = crate::world_participant::ParticipantKind::new(r.u32()?);
2521            let fault = crate::world_participant::ParticipantFault::from_tag(r.u8()?)
2522                .ok_or(WorldSnapshotError::InvalidValue { section: SECTION })?;
2523            Some(WorldFault::Participant { index, kind, fault })
2524        }
2525        2 => Some(WorldFault::RigidOverflow),
2526        3 => Some(WorldFault::ForceOutOfRange {
2527            body: r.usize(SECTION)?,
2528        }),
2529        4 => Some(WorldFault::FieldOutOfRange {
2530            field: crate::world_participant::PortId::new(r.u32()?),
2531            index: r.usize(SECTION)?,
2532        }),
2533        _ => return Err(WorldSnapshotError::InvalidValue { section: SECTION }),
2534    })
2535}
2536
2537/// The bytes of the `fields` section (`FieldBoard::write_values`), walked to
2538/// find where it ends; whether they fit the target world is checked on
2539/// restore (`FieldBoard::check_values`).
2540fn r_fields(r: &mut R<'_>) -> Res<Vec<u8>> {
2541    const SECTION: &str = "fields";
2542    let start = r.pos;
2543    let n = r.len(SECTION, 4 + 1 + 1 + 8)?;
2544    for _ in 0..n {
2545        r.u32()?;
2546        if r.u8()? > 1 {
2547            return Err(WorldSnapshotError::InvalidValue { section: SECTION });
2548        }
2549        let samples = match r.u8()? {
2550            0 => r.usize(SECTION)?,
2551            1 => {
2552                let origin = r.vec3()?;
2553                let cell = r.fix()?;
2554                let dims = [r.usize(SECTION)?, r.usize(SECTION)?, r.usize(SECTION)?];
2555                crate::world_participant::FieldLayout::Grid { origin, cell, dims }
2556                    .samples()
2557                    .ok_or(WorldSnapshotError::InvalidValue { section: SECTION })?
2558            }
2559            _ => return Err(WorldSnapshotError::InvalidValue { section: SECTION }),
2560        };
2561        let bytes = samples
2562            .checked_mul(16)
2563            .ok_or(WorldSnapshotError::Truncated)?;
2564        r.take(bytes)?;
2565    }
2566    Ok(r.data[start..r.pos].to_vec())
2567}
2568
2569/// Reject indices that `step` would follow past the end of a `Vec`.
2570fn validate(d: &Decoded) -> Res<()> {
2571    let n = d.bodies.len();
2572    let check = |section: &'static str, index: usize, len: usize| -> Res<()> {
2573        if index < len {
2574            Ok(())
2575        } else {
2576            Err(WorldSnapshotError::DanglingIndex {
2577                section,
2578                index,
2579                len,
2580            })
2581        }
2582    };
2583    for j in &d.joints {
2584        let (a, b) = j.bodies();
2585        check("joints", a, n)?;
2586        check("joints", b, n)?;
2587    }
2588    for c in &d.distance_constraints {
2589        check("distance_constraints", c.body_a, n)?;
2590        check("distance_constraints", c.body_b, n)?;
2591    }
2592    for c in &d.contact_constraints {
2593        check("contact_constraints", c.body_a, n)?;
2594        check("contact_constraints", c.body_b, n)?;
2595    }
2596    for p in &d.sdf_poses {
2597        if p.3 != crate::sdf_collider::SDF_STATIC {
2598            check("sdf_colliders", p.3, n)?;
2599        }
2600    }
2601    for b in &d.constraint_batches {
2602        for &i in &b.distance_indices {
2603            check("constraint_batches", i, d.distance_constraints.len())?;
2604        }
2605        for &i in &b.contact_indices {
2606            check("constraint_batches", i, d.contact_constraints.len())?;
2607        }
2608    }
2609    for &p in d.broadphase_proxies.iter().flatten() {
2610        check(
2611            "broadphase_proxies",
2612            p as usize,
2613            d.broadphase_tree.nodes.len(),
2614        )?;
2615    }
2616    Ok(())
2617}
2618
2619#[cfg(all(test, feature = "std"))]
2620mod tests {
2621    // Oracles for the whole-world snapshot.
2622    //
2623    // The comparator ([`digest`]) reads every [`PhysicsWorld`] field directly
2624    // (through `Debug` or field by field) and never goes through the snapshot
2625    // encoder, so a field the encoder drops on both sides still shows up as a
2626    // difference between the original and the restored world.
2627
2628    use super::super::*;
2629    use super::WorldSnapshotError;
2630    use crate::character::CharacterController;
2631    use crate::collider::Contact;
2632    use crate::compound::CompoundShape;
2633    use crate::force::{ForceField, ForceFieldInstance};
2634    use crate::heightfield::HeightField;
2635    use crate::joint::{
2636        BallJoint, ConeTwistJoint, D6Joint, FixedJoint, HingeJoint, Joint, SliderJoint, SpringJoint,
2637    };
2638    use crate::motor::{apply_motors, JointMotor, PdController};
2639    use crate::plane_collider::PlaneCollider;
2640    use crate::sdf_collider::{ClosureSdf, SdfCollider};
2641    use crate::shape::Shape;
2642    use crate::static_collider::StaticCollider;
2643    use crate::trimesh::TriMesh;
2644
2645    fn fx(n: i64, d: i64) -> Fix128 {
2646        Fix128::from_ratio(n, d)
2647    }
2648
2649    fn v(x: i64, y: i64, z: i64) -> Vec3Fix {
2650        Vec3Fix::from_int(x, y, z)
2651    }
2652
2653    /// Every field of `w`, rendered without the snapshot encoder.
2654    fn digest(w: &mut PhysicsWorld) -> Vec<(&'static str, String)> {
2655        let mut d = Vec::new();
2656        d.push(("config", format!("{:?}", w.config)));
2657        d.push(("bodies", format!("{:?}", w.bodies)));
2658        d.push((
2659            "distance_constraints",
2660            format!("{:?}", w.distance_constraints),
2661        ));
2662        d.push((
2663            "contact_constraints",
2664            format!("{:?}", w.contact_constraints),
2665        ));
2666        let sdf: Vec<String> = w
2667            .sdf_colliders
2668            .iter()
2669            .map(|s| {
2670                format!(
2671                    "{:?} {:?} {:?} {} {:?} {} {} field@probe={}",
2672                    s.position,
2673                    s.rotation,
2674                    s.scale,
2675                    s.body_index,
2676                    s.inv_rotation,
2677                    s.scale_f32.to_bits(),
2678                    s.inv_scale_f32.to_bits(),
2679                    s.field.distance(0.3, 0.2, 0.1).to_bits()
2680                )
2681            })
2682            .collect();
2683        d.push(("sdf_colliders", format!("{sdf:?}")));
2684        d.push((
2685            "sdf_collision_radius",
2686            format!("{:?}", w.sdf_collision_radius),
2687        ));
2688        let statics: Vec<String> = w
2689            .static_colliders
2690            .iter()
2691            .map(|c| match c {
2692                StaticCollider::Plane(p) => format!("plane {p:?}"),
2693                StaticCollider::HeightField(h) => format!("hf {h:?}"),
2694                StaticCollider::TriMesh(m) => format!(
2695                    "mesh {:?} {:?} {:?} {:?} {:?}",
2696                    m.triangles, m.bvh.nodes, m.bvh.primitives, m.bvh.bounds, m.bounds
2697                ),
2698            })
2699            .collect();
2700        d.push(("static_colliders", format!("{statics:?}")));
2701        let batches: Vec<_> = w
2702            .constraint_batches
2703            .iter()
2704            .map(|b| (b.distance_indices.clone(), b.contact_indices.clone()))
2705            .collect();
2706        d.push(("constraint_batches", format!("{batches:?}")));
2707        d.push(("batches_dirty", format!("{}", w.batches_dirty)));
2708        d.push((
2709            "batch_static_bodies",
2710            format!("{:?}", w.batch_static_bodies),
2711        ));
2712        let index_probe: Vec<_> = w
2713            .contact_cache
2714            .manifolds
2715            .iter()
2716            .map(|m| w.contact_cache.find(&m.pair).map(|f| f.stale_frames))
2717            .collect();
2718        d.push((
2719            "contact_cache",
2720            format!(
2721                "{:?} {} {:?}",
2722                w.contact_cache.manifolds,
2723                w.contact_cache.max_stale_frames,
2724                w.contact_cache.warm_start_factor
2725            ),
2726        ));
2727        d.push(("contact_cache.pair_index", format!("{index_probe:?}")));
2728        d.push((
2729            "material_table",
2730            format!(
2731                "{:?} {:?} {:?} {:?}",
2732                w.material_table.materials,
2733                w.material_table.pair_overrides,
2734                w.material_table.default_friction_combine,
2735                w.material_table.default_restitution_combine
2736            ),
2737        ));
2738        d.push(("body_materials", format!("{:?}", w.body_materials)));
2739        d.push((
2740            "callbacks",
2741            format!("{} {}", w.pre_solve_hooks.len(), w.contact_modifiers.len()),
2742        ));
2743        d.push(("joints", format!("{:?}", w.joints)));
2744        d.push(("force_fields", format!("{:?}", w.force_fields)));
2745        let e = &w.events;
2746        d.push((
2747            "events",
2748            format!(
2749                "{:?} {:?} {:?} {:?} {:?} {:?}",
2750                e.contact_events,
2751                e.trigger_events,
2752                e.prev_pairs,
2753                e.curr_pairs,
2754                e.prev_triggers,
2755                e.curr_triggers
2756            ),
2757        ));
2758        d.push(("islands.config", format!("{:?}", w.islands.config)));
2759        d.push(("islands.sleep_data", format!("{:?}", w.islands.sleep_data)));
2760        let n = w.islands.sleep_data.len();
2761        let mut partition = Vec::new();
2762        for i in 0..n {
2763            for j in 0..n {
2764                partition.push(w.islands.find(i) == w.islands.find(j));
2765            }
2766        }
2767        d.push(("islands.partition", format!("{partition:?}")));
2768        d.push((
2769            "body_collision_radii",
2770            format!("{:?}", w.body_collision_radii),
2771        ));
2772        d.push(("broadphase", format!("{:?}", w.broadphase)));
2773        let t = &w.broadphase_tree;
2774        d.push((
2775            "broadphase_tree",
2776            format!(
2777                "{:?} {:?} {} {:?} {:?}",
2778                t.nodes, t.free_list, t.root, t.margin, t.metric
2779            ),
2780        ));
2781        d.push(("broadphase_proxies", format!("{:?}", w.broadphase_proxies)));
2782        d.push(("body_colliders", format!("{:?}", w.body_colliders)));
2783        d.push(("body_filters", format!("{:?}", w.body_filters)));
2784        d.push(("overflow_detected", format!("{}", w.overflow_detected)));
2785        d.push((
2786            "kinematic_substeps_left",
2787            format!("{}", w.kinematic_substeps_left),
2788        ));
2789        let c = &w.tgs_impulse_cache;
2790        let mut entries: Vec<_> = c.entries.iter().map(|(k, v)| (*k, *v)).collect();
2791        entries.sort_unstable_by_key(|e| e.0);
2792        let mut live: Vec<_> = c.live.keys().copied().collect();
2793        live.sort_unstable();
2794        d.push((
2795            "tgs_impulse_cache",
2796            format!("{entries:?} {live:?} {} {}", c.hits, c.misses),
2797        ));
2798        d
2799    }
2800
2801    /// Fields whose digest differs between `a` and `b`.
2802    fn differing(a: &mut PhysicsWorld, b: &mut PhysicsWorld) -> Vec<&'static str> {
2803        digest(a)
2804            .into_iter()
2805            .zip(digest(b))
2806            .filter(|(x, y)| x.1 != y.1)
2807            .map(|(x, _)| x.0)
2808            .collect()
2809    }
2810
2811    fn ball_sdf() -> Box<dyn crate::sdf_collider::SdfField> {
2812        Box::new(ClosureSdf::new(
2813            |x, y, z| (x * x + y * y + z * z).sqrt() - 1.0,
2814            |x, y, z| {
2815                let l = (x * x + y * y + z * z).sqrt().max(1e-6);
2816                (x / l, y / l, z / l)
2817            },
2818        ))
2819    }
2820
2821    /// The SDF fields a restore target must already hold (code, not data).
2822    fn target_world() -> PhysicsWorld {
2823        let mut t = PhysicsWorld::new(SolverConfig::default());
2824        // attached to body 0 at the default pose: every pose field differs from
2825        // the scene's static, rotated, scaled collider
2826        t.add_sdf_collider(SdfCollider::new_dynamic(ball_sdf(), 0));
2827        t
2828    }
2829
2830    /// Caller-owned state stepped next to the world (not part of the snapshot).
2831    #[derive(Clone)]
2832    struct Driver {
2833        motors: Vec<JointMotor>,
2834        character: (Vec3Fix, Vec3Fix, bool, Option<usize>, Vec3Fix),
2835    }
2836
2837    impl Driver {
2838        fn new() -> Self {
2839            let mut pd = PdController::new(fx(20, 1), fx(2, 1), fx(50, 1));
2840            pd.set_position_target(fx(3, 2));
2841            Self {
2842                motors: vec![JointMotor::new(1, pd)],
2843                character: (v(1, 1, 3), Vec3Fix::ZERO, false, None, Vec3Fix::ZERO),
2844            }
2845        }
2846
2847        /// One frame: motors, a character that pushes bodies, then `step`.
2848        fn frame(&mut self, w: &mut PhysicsWorld, dt: Fix128) {
2849            apply_motors(&self.motors, &w.joints, &mut w.bodies, dt);
2850            let (p, vel, g, gb, pv) = self.character;
2851            let mut ch = CharacterController::new_default(p);
2852            ch.velocity = vel;
2853            ch.grounded = g;
2854            ch.ground_body_index = gb;
2855            ch.platform_velocity = pv;
2856            ch.move_and_slide(
2857                Vec3Fix::new(fx(1, 20), Fix128::ZERO, Fix128::ZERO),
2858                &w.bodies,
2859                &w.sdf_colliders,
2860            );
2861            for push in ch.compute_push_impulses(&w.bodies, fx(1, 2)) {
2862                let b = &mut w.bodies[push.body_index];
2863                if !b.is_static() {
2864                    b.velocity = b.velocity + push.impulse * b.inv_mass;
2865                }
2866            }
2867            self.character = (
2868                ch.position,
2869                ch.velocity,
2870                ch.grounded,
2871                ch.ground_body_index,
2872                ch.platform_velocity,
2873            );
2874            w.step(dt);
2875        }
2876    }
2877
2878    /// A scene that exercises every saved field: joints of all seven kinds,
2879    /// distance constraint, force fields with an affected-body list, static
2880    /// plane / height field / triangle mesh, SDF collider, shaped and compound
2881    /// bodies, materials with a pair override, filters, a sensor (trigger
2882    /// events), a kinematic body with a target, and a resting body that falls
2883    /// asleep.
2884    fn scene(backend: SolverBackend, broadphase: Broadphase) -> PhysicsWorld {
2885        let config = SolverConfig {
2886            solver_backend: backend,
2887            ..SolverConfig::default()
2888        };
2889        let mut w = PhysicsWorld::new(config);
2890        w.set_broadphase(broadphase);
2891        w.set_sdf_collision_radius(fx(3, 5));
2892        // a non-default fat margin, so the restored tree must carry it
2893        w.broadphase_tree.margin = fx(3, 8);
2894        w.set_sleep_config(SleepConfig {
2895            linear_threshold: fx(1, 2),
2896            angular_threshold: fx(1, 2),
2897            frames_to_sleep: 3,
2898        });
2899
2900        w.add_static_collider(StaticCollider::Plane(PlaneCollider::new(
2901            v(0, 1, 0),
2902            Fix128::ZERO,
2903        )));
2904        let heights: Vec<Fix128> = (0..16).map(|i| fx(i % 4, 8)).collect();
2905        w.add_static_collider(StaticCollider::HeightField(HeightField::new(
2906            heights,
2907            4,
2908            4,
2909            Fix128::ONE,
2910            v(6, 0, -2),
2911        )));
2912        w.add_static_collider(StaticCollider::TriMesh(TriMesh::from_indexed(
2913            &[v(-10, 0, -10), v(-6, 1, -10), v(-10, 0, -6), v(-6, 1, -6)],
2914            &[0, 1, 2, 1, 3, 2],
2915        )));
2916        w.add_sdf_collider(
2917            SdfCollider::new_static(
2918                ball_sdf(),
2919                v(-4, 0, 0),
2920                QuatFix::from_axis_angle(v(0, 1, 0), fx(1, 3)),
2921            )
2922            .with_scale(fx(3, 2)),
2923        );
2924
2925        // 0: rests on the plane and falls asleep
2926        w.add_body_with_radius(
2927            RigidBody::new_dynamic(
2928                Vec3Fix::new(Fix128::ZERO, fx(1, 2), Fix128::ZERO),
2929                Fix128::ONE,
2930            ),
2931            fx(1, 2),
2932        );
2933        // 1: anchor, 2..=8: one joint of each kind to the anchor
2934        let anchor = w.add_body(RigidBody::new_static(v(3, 6, 0)));
2935        let mut ids = Vec::new();
2936        for k in 0..7 {
2937            let id = w.add_body_with_radius(
2938                RigidBody::new_dynamic(v(3 + k, 5, 2 * k), Fix128::ONE),
2939                fx(1, 4),
2940            );
2941            ids.push(id);
2942        }
2943        let up = v(0, 1, 0);
2944        let z = Vec3Fix::ZERO;
2945        let mut d6 = D6Joint::new(anchor, ids[5], z, z);
2946        d6.linear_x = crate::joint::D6Motion::Limited;
2947        w.add_joint(Joint::Ball(BallJoint::new(anchor, ids[0], z, v(-1, 1, 0))));
2948        let mut hinge = HingeJoint::new(anchor, ids[1], z, v(-1, 0, 0), up, up);
2949        hinge.angle_min = Some(fx(-1, 2));
2950        hinge.angle_max = Some(fx(1, 2));
2951        w.add_joint(Joint::Hinge(hinge));
2952        w.add_joint(Joint::Fixed(FixedJoint::new(
2953            anchor,
2954            ids[2],
2955            z,
2956            v(0, 1, 0),
2957            QuatFix::IDENTITY,
2958        )));
2959        let mut slider = SliderJoint::new(anchor, ids[3], v(1, 0, 0), z, z);
2960        slider.limit_min = Some(fx(-1, 1));
2961        slider.break_force = Some(fx(1000, 1));
2962        w.add_joint(Joint::Slider(slider));
2963        w.add_joint(Joint::Spring(SpringJoint::new(
2964            anchor,
2965            ids[4],
2966            z,
2967            z,
2968            fx(3, 2),
2969            fx(40, 1),
2970            fx(1, 2),
2971        )));
2972        w.add_joint(Joint::D6(d6));
2973        w.add_joint(Joint::ConeTwist(ConeTwistJoint::new(
2974            anchor, ids[6], z, z, up, up,
2975        )));
2976        w.add_distance_constraint(DistanceConstraint {
2977            body_a: ids[0],
2978            body_b: ids[1],
2979            local_anchor_a: z,
2980            local_anchor_b: z,
2981            target_distance: fx(3, 2),
2982            compliance: fx(1, 1000),
2983            cached_lambda: Fix128::ZERO,
2984        });
2985
2986        // shaped + compound bodies above the height field, a body above the SDF
2987        let boxed = w
2988            .add_shaped_body(
2989                &Shape::Box {
2990                    half_extents: Vec3Fix::new(fx(1, 2), fx(1, 4), fx(1, 2)),
2991                },
2992                Fix128::ONE,
2993                v(7, 2, 0),
2994            )
2995            .unwrap();
2996        let mut compound = CompoundShape::new();
2997        compound.add_sphere(
2998            crate::collider::Sphere::new(Vec3Fix::ZERO, fx(1, 2)),
2999            v(-1, 0, 0) * fx(1, 2),
3000            QuatFix::IDENTITY,
3001        );
3002        compound.add_sphere(
3003            crate::collider::Sphere::new(Vec3Fix::ZERO, fx(1, 2)),
3004            v(1, 0, 0) * fx(1, 2),
3005            QuatFix::IDENTITY,
3006        );
3007        let comp = w
3008            .add_compound_body(&compound, Fix128::ONE, v(8, 3, 1))
3009            .unwrap();
3010        // compute the cached AABB, so `dirty` is false and the cache is non-zero
3011        if let Some(crate::body_collider::BodyCollider::Compound(c)) = &mut w.body_colliders[comp] {
3012            let _ = c.compute_aabb();
3013            assert!(!c.dirty);
3014        }
3015        let over_sdf =
3016            w.add_body_with_radius(RigidBody::new_dynamic(v(-4, 3, 0), Fix128::ONE), fx(1, 2));
3017        // sensor around the swinging bodies, kinematic body with a target
3018        let sensor = w.add_body_with_radius(RigidBody::new_sensor(v(3, 5, 0)), fx(2, 1));
3019        let mut kin = RigidBody::new_kinematic(v(0, 2, -3));
3020        kin.kinematic_target = Some((v(2, 2, -3), QuatFix::IDENTITY));
3021        let kin = w.add_body_with_radius(kin, fx(1, 2));
3022
3023        let rubber = w.material_table.register_rubber();
3024        let ice = w.material_table.register_ice();
3025        w.material_table
3026            .set_pair_override(rubber, ice, fx(1, 7), fx(2, 7));
3027        w.set_body_material(boxed, rubber);
3028        w.set_body_material(comp, ice);
3029        w.set_body_filter(over_sdf, CollisionFilter::new(2, 0xFFFF_FFFF));
3030
3031        w.add_force_field(
3032            ForceFieldInstance::new(ForceField::Directional {
3033                direction: v(1, 0, 0),
3034                strength: fx(3, 1),
3035            })
3036            .with_affected_bodies(vec![boxed, comp]),
3037        );
3038        w.add_force_field(ForceFieldInstance::new(ForceField::Drag {
3039            coefficient: fx(1, 10),
3040        }));
3041        w.add_force_field(ForceFieldInstance::new(ForceField::Vortex {
3042            center: v(3, 5, 0),
3043            axis: up,
3044            strength: fx(1, 2),
3045            falloff_radius: fx(20, 1),
3046        }));
3047        let mut blast = ForceFieldInstance::new(ForceField::Explosion {
3048            center: v(0, 0, 0),
3049            strength: fx(500, 1),
3050            radius: fx(50, 1),
3051            falloff_power: fx(2, 1),
3052        });
3053        blast.enabled = false;
3054        w.add_force_field(blast);
3055        let _ = (sensor, kin);
3056        w
3057    }
3058
3059    fn dt() -> Fix128 {
3060        fx(1, 60)
3061    }
3062
3063    /// Run `k` frames, add a manual contact (so `contact_constraints` and
3064    /// `contact_cache` are non-empty in the snapshot), snapshot, restore into a
3065    /// target holding the same SDF field, then run `m` more frames on both.
3066    /// Returns the field names that differed at any checkpoint.
3067    fn round_trip(
3068        backend: SolverBackend,
3069        broadphase: Broadphase,
3070        k: usize,
3071        m: usize,
3072        overflow: bool,
3073    ) -> Vec<&'static str> {
3074        let mut a = scene(backend, broadphase);
3075        let mut drv = Driver::new();
3076        for _ in 0..k {
3077            drv.frame(&mut a, dt());
3078        }
3079        a.add_contact(ContactConstraint::new(
3080            2,
3081            3,
3082            Contact {
3083                depth: fx(1, 100),
3084                normal: v(0, 1, 0),
3085                point_a: v(3, 5, 0),
3086                point_b: v(4, 5, 2),
3087            },
3088        ));
3089        a.overflow_detected = overflow;
3090        // populate the colour batches (dirty = false) so they differ from a
3091        // fresh world's (empty, dirty = true)
3092        a.rebuild_batches();
3093        assert!(!a.constraint_batches.is_empty() && !a.batches_dirty);
3094        assert!(!a.contact_constraints.is_empty() && !a.contact_cache.manifolds.is_empty());
3095        let blob = a.snapshot_world();
3096        let mut b = target_world();
3097        b.restore_world(&blob).expect("restore");
3098        let mut drv_b = drv.clone();
3099
3100        let mut diff = differing(&mut a, &mut b);
3101        for _ in 0..m {
3102            drv.frame(&mut a, dt());
3103            drv_b.frame(&mut b, dt());
3104            for f in differing(&mut a, &mut b) {
3105                if !diff.contains(&f) {
3106                    diff.push(f);
3107                }
3108            }
3109        }
3110        if a.snapshot_world() != b.snapshot_world() && !diff.contains(&"blob") {
3111            diff.push("blob");
3112        }
3113        diff
3114    }
3115
3116    #[test]
3117    fn round_trip_is_bit_identical_for_every_field_and_every_later_step() {
3118        for backend in [SolverBackend::Xpbd, SolverBackend::Tgs] {
3119            for bp in [Broadphase::Bvh, Broadphase::DynamicTree] {
3120                for (k, m) in [(0, 0), (0, 5), (1, 1), (7, 3), (20, 15)] {
3121                    let diff = round_trip(backend, bp, k, m, k % 2 == 1);
3122                    assert!(diff.is_empty(), "{backend:?} {bp:?} k={k} m={m}: {diff:?}");
3123                }
3124            }
3125        }
3126    }
3127
3128    /// Controls: the round trip above is only meaningful if the scene really
3129    /// holds a sleeping body, events, contact-cache entries, tree nodes, TGS
3130    /// warm-start entries and a non-trivial island partition at snapshot time.
3131    #[test]
3132    fn round_trip_scene_exercises_the_covered_state() {
3133        let mut a = scene(SolverBackend::Xpbd, Broadphase::DynamicTree);
3134        let mut drv = Driver::new();
3135        for _ in 0..20 {
3136            drv.frame(&mut a, dt());
3137        }
3138        assert!(a
3139            .islands
3140            .sleep_data
3141            .iter()
3142            .any(|s| s.state == SleepState::Sleeping && a.bodies.len() > 1));
3143        assert!(a.is_sleeping(0), "body 0 rests on the plane and sleeps");
3144        assert!(!a.events.curr_pairs.is_empty() || !a.events.prev_pairs.is_empty());
3145        assert!(
3146            !a.events.curr_triggers.is_empty(),
3147            "sensor overlaps the swinging bodies"
3148        );
3149        assert!(!a.broadphase_tree.nodes.is_empty());
3150        assert!(a.broadphase_proxies.iter().any(Option::is_some));
3151        assert_ne!(a.islands.find(1), a.islands.find(0));
3152        assert_eq!(a.islands.find(1), a.islands.find(2));
3153        assert!(a
3154            .body_colliders
3155            .iter()
3156            .any(|c| matches!(c, Some(crate::body_collider::BodyCollider::Compound(_)))));
3157
3158        let mut t = scene(SolverBackend::Tgs, Broadphase::Bvh);
3159        let mut drv = Driver::new();
3160        for _ in 0..7 {
3161            drv.frame(&mut t, dt());
3162        }
3163        assert!(!t.tgs_impulse_cache.entries.is_empty());
3164        assert!(t.tgs_impulse_cache.hits + t.tgs_impulse_cache.misses > 0);
3165        // the live set is empty between steps, which is why it is rebuilt empty
3166        assert!(t.tgs_impulse_cache.live.is_empty());
3167    }
3168
3169    #[test]
3170    fn step_n_equals_step_called_n_times() {
3171        for n in [0usize, 1, 2, 9] {
3172            let mut a = scene(SolverBackend::Xpbd, Broadphase::Bvh);
3173            let mut b = scene(SolverBackend::Xpbd, Broadphase::Bvh);
3174            a.step_n(n, dt());
3175            for _ in 0..n {
3176                b.step(dt());
3177            }
3178            assert!(differing(&mut a, &mut b).is_empty(), "n={n}");
3179            assert_eq!(a.snapshot_world(), b.snapshot_world());
3180        }
3181        // n = 0 leaves the world unchanged
3182        let mut a = scene(SolverBackend::Xpbd, Broadphase::Bvh);
3183        let before = a.snapshot_world();
3184        a.step_n(0, dt());
3185        assert_eq!(a.snapshot_world(), before);
3186        // and so does a non-positive dt for any n
3187        a.step_n(5, Fix128::ZERO);
3188        assert_eq!(a.snapshot_world(), before);
3189    }
3190
3191    /// The target keeps its SDF fields and callbacks; the counts must match.
3192    #[test]
3193    fn restore_requires_the_same_number_of_non_data_parts() {
3194        let mut a = scene(SolverBackend::Xpbd, Broadphase::Bvh);
3195        // differs from the target's config, so a partial install would show
3196        a.config.substeps = 3;
3197        let blob = a.snapshot_world();
3198
3199        // a fresh world has no SDF field
3200        assert!(matches!(
3201            PhysicsWorld::from_world_snapshot(&blob),
3202            Err(WorldSnapshotError::SdfFieldCountMismatch {
3203                snapshot: 1,
3204                world: 0
3205            })
3206        ));
3207
3208        // a pre-solve hook on the target that the source did not have
3209        let mut t = target_world();
3210        t.add_pre_solve_hook(Box::new(|_, _, _| true));
3211        let before = digest(&mut t);
3212        assert_eq!(
3213            t.restore_world(&blob),
3214            Err(WorldSnapshotError::CallbackCountMismatch {
3215                section: "pre_solve_hooks",
3216                snapshot: 0,
3217                world: 1,
3218            })
3219        );
3220        assert_eq!(
3221            digest(&mut t),
3222            before,
3223            "rejected restore leaves the target untouched"
3224        );
3225    }
3226
3227    // ---- coverage: codec variants and reader boundaries (2026-10-07) ----
3228
3229    /// A world holding every `Shape` variant, a compound with every child
3230    /// kind, every force field kind, a `Multiply` material and the hybrid
3231    /// broadphase (none of which the round-trip scene uses).
3232    fn variant_world() -> PhysicsWorld {
3233        use crate::body_collider::BodyCollider;
3234        use crate::collider::{Capsule, ConvexHull, Sphere};
3235        use crate::material::{CombineRule, PhysicsMaterial};
3236        let mut w = PhysicsWorld::new(SolverConfig::default());
3237        w.set_broadphase(Broadphase::Hybrid);
3238        let shapes = [
3239            Shape::Box {
3240                half_extents: Vec3Fix::new(fx(1, 2), fx(1, 4), fx(1, 2)),
3241            },
3242            Shape::Cylinder {
3243                radius: fx(1, 2),
3244                half_height: fx(3, 4),
3245            },
3246            Shape::Cone {
3247                radius: fx(1, 2),
3248                half_height: fx(1, 1),
3249            },
3250            Shape::Ellipsoid {
3251                radii: Vec3Fix::new(fx(1, 2), fx(3, 4), fx(1, 4)),
3252            },
3253            Shape::Wedge {
3254                width: fx(1, 1),
3255                height: fx(1, 2),
3256                depth: fx(3, 2),
3257            },
3258            Shape::Torus {
3259                major_radius: fx(1, 1),
3260                minor_radius: fx(1, 4),
3261            },
3262        ];
3263        for (k, shape) in shapes.iter().enumerate() {
3264            let i = w.add_body_with_radius(
3265                RigidBody::new_dynamic(v(3 * k as i64, 4, 0), Fix128::ONE),
3266                fx(3, 2),
3267            );
3268            w.body_colliders[i] = Some(BodyCollider::Shape(*shape));
3269        }
3270        let mut c = CompoundShape::new();
3271        c.add_sphere(
3272            Sphere::new(Vec3Fix::ZERO, fx(1, 4)),
3273            v(1, 0, 0),
3274            QuatFix::IDENTITY,
3275        );
3276        c.add_capsule(
3277            Capsule::new(v(0, -1, 0), v(0, 1, 0), fx(1, 4)),
3278            v(-1, 0, 0),
3279            QuatFix::IDENTITY,
3280        );
3281        c.add_box(
3282            crate::box_collider::OrientedBox {
3283                center: Vec3Fix::ZERO,
3284                half_extents: Vec3Fix::new(fx(1, 4), fx(1, 4), fx(1, 4)),
3285                rotation: QuatFix::IDENTITY,
3286            },
3287            v(0, 0, 1),
3288            QuatFix::from_axis_angle(v(0, 1, 0), fx(1, 3)),
3289        );
3290        c.add_convex_hull(
3291            ConvexHull::new(vec![v(0, 0, 0), v(1, 0, 0), v(0, 1, 0), v(0, 0, 1)]),
3292            v(0, 0, -1),
3293            QuatFix::IDENTITY,
3294        );
3295        let comp =
3296            w.add_body_with_radius(RigidBody::new_dynamic(v(0, 8, 4), Fix128::ONE), fx(2, 1));
3297        w.body_colliders[comp] = Some(BodyCollider::Compound(c));
3298
3299        let mut mat = PhysicsMaterial::new(0, fx(1, 3), fx(1, 5));
3300        mat.friction_combine = CombineRule::Multiply;
3301        mat.restitution_combine = CombineRule::Multiply;
3302        let id = w.material_table.register(mat);
3303        w.material_table.default_friction_combine = CombineRule::Multiply;
3304        w.set_body_material(comp, id);
3305
3306        w.add_force_field(
3307            ForceFieldInstance::new(ForceField::Point {
3308                center: v(0, 10, 0),
3309                strength: fx(5, 1),
3310                repulsive: true,
3311                max_force: fx(50, 1),
3312            })
3313            .with_affected_bodies(vec![0, comp]),
3314        );
3315        w.add_force_field(ForceFieldInstance::new(ForceField::Buoyancy {
3316            surface_y: fx(2, 1),
3317            density: fx(1000, 1),
3318            drag: fx(1, 2),
3319        }));
3320        w.add_force_field(ForceFieldInstance::new(ForceField::Magnetic {
3321            position: v(0, 0, 0),
3322            moment: v(0, 1, 0),
3323            strength: fx(1, 10),
3324        }));
3325        w
3326    }
3327
3328    /// Every codec variant survives a round trip: the restored world equals
3329    /// the original field by field, now and after the same later steps.
3330    #[test]
3331    fn round_trip_covers_every_shape_child_and_force_field_kind() {
3332        let mut a = variant_world();
3333        let blob = a.snapshot_world();
3334        let mut b = PhysicsWorld::from_world_snapshot(&blob).expect("restore");
3335        assert!(differing(&mut a, &mut b).is_empty());
3336        for _ in 0..5 {
3337            a.step(dt());
3338            b.step(dt());
3339        }
3340        assert_eq!(differing(&mut a, &mut b), Vec::<&str>::new());
3341        assert_eq!(a.snapshot_world(), b.snapshot_world());
3342    }
3343
3344    /// A dynamic SDF collider (attached to a body, not static) round-trips
3345    /// into a target holding the same field.
3346    #[test]
3347    fn round_trip_keeps_a_body_attached_sdf_collider() {
3348        let mut a = target_world();
3349        a.add_body_with_radius(RigidBody::new_dynamic(v(0, 3, 0), Fix128::ONE), fx(1, 2));
3350        let blob = a.snapshot_world();
3351        let mut b = target_world();
3352        b.restore_world(&blob).expect("restore");
3353        assert!(differing(&mut a, &mut b).is_empty());
3354    }
3355
3356    /// Every error renders a message naming its values.
3357    #[test]
3358    fn error_display_names_the_values() {
3359        use crate::world_participant::{ParticipantMismatch, StateError};
3360        let cases: [(WorldSnapshotError, &[&str]); 15] = [
3361            (WorldSnapshotError::Truncated, &["truncated"]),
3362            (WorldSnapshotError::BadMagic, &["magic"]),
3363            (
3364                WorldSnapshotError::UnsupportedVersion {
3365                    found: 77,
3366                    supported: 88,
3367                },
3368                &["77", "88"],
3369            ),
3370            (WorldSnapshotError::ReservedNotZero, &["reserved"]),
3371            (WorldSnapshotError::TrailingBytes { extra: 5 }, &["5"]),
3372            (
3373                WorldSnapshotError::ChecksumMismatch {
3374                    stored: 0x10,
3375                    computed: 0x20,
3376                },
3377                &["0x0000000000000010", "0x0000000000000020"],
3378            ),
3379            (
3380                WorldSnapshotError::InvalidValue { section: "joints" },
3381                &["joints"],
3382            ),
3383            (
3384                WorldSnapshotError::DanglingIndex {
3385                    section: "contacts",
3386                    index: 9,
3387                    len: 4,
3388                },
3389                &["contacts", "9", "4"],
3390            ),
3391            (
3392                WorldSnapshotError::SdfFieldCountMismatch {
3393                    snapshot: 2,
3394                    world: 3,
3395                },
3396                &["2", "3"],
3397            ),
3398            (
3399                WorldSnapshotError::CallbackCountMismatch {
3400                    section: "contact_modifiers",
3401                    snapshot: 6,
3402                    world: 7,
3403                },
3404                &["contact_modifiers", "6", "7"],
3405            ),
3406            (
3407                WorldSnapshotError::ParticipantMismatch(ParticipantMismatch::Count {
3408                    snapshot: 11,
3409                    world: 12,
3410                }),
3411                &["11", "12"],
3412            ),
3413            (
3414                WorldSnapshotError::ParticipantState {
3415                    index: 13,
3416                    error: StateError::InvalidValue,
3417                },
3418                &["13", "InvalidValue"],
3419            ),
3420            (
3421                WorldSnapshotError::FieldState(StateError::Length {
3422                    expected: 14,
3423                    found: 15,
3424                }),
3425                &["14", "15"],
3426            ),
3427            (
3428                WorldSnapshotError::SemanticsMismatch {
3429                    stored: [0xAB; 32],
3430                    expected: [0x01; 32],
3431                },
3432                &["semantics", &"ab".repeat(32), &"01".repeat(32)],
3433            ),
3434            (
3435                WorldSnapshotError::LawIdMismatch {
3436                    stored: [0x0F; 32],
3437                    expected: [0xF0; 32],
3438                },
3439                &["law id", &"0f".repeat(32), &"f0".repeat(32)],
3440            ),
3441        ];
3442        for (e, parts) in cases {
3443            let text = e.to_string();
3444            for p in parts {
3445                assert!(text.contains(p), "{e:?} -> {text:?} lacks {p:?}");
3446            }
3447        }
3448    }
3449
3450    /// Header checks that do not depend on the format version: a blob
3451    /// shorter than a header, a wrong magic, non-zero reserved bytes, a
3452    /// declared payload longer than the blob, extra bytes after the
3453    /// checksum and a corrupted payload byte are all rejected, and the
3454    /// target world is left untouched each time.
3455    #[test]
3456    fn restore_rejects_malformed_headers_and_corruption() {
3457        let src = variant_world();
3458        let blob = src.snapshot_world();
3459        let mut t = PhysicsWorld::new(SolverConfig::default());
3460        let before = digest(&mut t);
3461
3462        assert_eq!(
3463            t.restore_world(&blob[..10]),
3464            Err(WorldSnapshotError::Truncated)
3465        );
3466
3467        let mut bad = blob.clone();
3468        bad[0] ^= 0xFF;
3469        assert_eq!(t.restore_world(&bad), Err(WorldSnapshotError::BadMagic));
3470
3471        let mut bad = blob.clone();
3472        bad[6] = 1;
3473        assert_eq!(
3474            t.restore_world(&bad),
3475            Err(WorldSnapshotError::ReservedNotZero)
3476        );
3477
3478        let mut bad = blob.clone();
3479        bad[8..16].copy_from_slice(&u64::MAX.to_le_bytes());
3480        assert_eq!(t.restore_world(&bad), Err(WorldSnapshotError::Truncated));
3481
3482        let mut bad = blob.clone();
3483        bad.extend_from_slice(&[0, 0, 0]);
3484        assert_eq!(
3485            t.restore_world(&bad),
3486            Err(WorldSnapshotError::TrailingBytes { extra: 3 })
3487        );
3488
3489        let mut bad = blob.clone();
3490        bad[20] ^= 0x01;
3491        assert!(matches!(
3492            t.restore_world(&bad),
3493            Err(WorldSnapshotError::ChecksumMismatch { stored, computed }) if stored != computed
3494        ));
3495
3496        assert_eq!(digest(&mut t), before);
3497        // control: the untouched blob restores
3498        assert!(t.restore_world(&blob).is_ok());
3499    }
3500
3501    fn reader(data: &[u8]) -> super::R<'_> {
3502        super::R { data, pos: 0 }
3503    }
3504
3505    /// The leaf readers reject an unknown tag in their own section.
3506    #[test]
3507    fn leaf_readers_reject_unknown_tags() {
3508        let inv = |section| Some(WorldSnapshotError::InvalidValue { section });
3509        assert_eq!(
3510            super::r_shape(&mut reader(&[6])).err(),
3511            inv("body_colliders")
3512        );
3513        assert_eq!(
3514            super::r_body_collider(&mut reader(&[3])).err(),
3515            inv("body_colliders")
3516        );
3517        // a compound with one child of unknown kind 4
3518        let mut w = super::W(Vec::new());
3519        w.u8(2);
3520        w.usize(1);
3521        w.u8(4);
3522        assert_eq!(
3523            super::r_body_collider(&mut reader(&w.0)).err(),
3524            inv("body_colliders")
3525        );
3526        assert_eq!(
3527            super::r_force_field(&mut reader(&[7])).err(),
3528            inv("force_fields")
3529        );
3530        assert_eq!(super::r_d6_motion(&mut reader(&[3])).err(), inv("joints"));
3531        assert_eq!(
3532            super::r_combine(&mut reader(&[4])).err(),
3533            inv("material_table")
3534        );
3535        assert_eq!(super::r_fault(&mut reader(&[5])).err(), inv("fault"));
3536        // config: a valid encoding with its backend tag replaced
3537        let mut w = super::W(Vec::new());
3538        super::w_config(&mut w, &SolverConfig::default());
3539        *w.0.last_mut().expect("backend tag") = 2;
3540        assert_eq!(super::r_config(&mut reader(&w.0)).err(), inv("config"));
3541        // empty input is truncation, not a value error
3542        assert_eq!(
3543            super::r_shape(&mut reader(&[])).err(),
3544            Some(WorldSnapshotError::Truncated)
3545        );
3546    }
3547
3548    /// The fault codec round-trips every variant.
3549    #[test]
3550    fn fault_codec_round_trips_every_variant() {
3551        use crate::world_participant::{ParticipantFault, ParticipantKind, PortId, WorldFault};
3552        let faults = [
3553            None,
3554            Some(WorldFault::Participant {
3555                index: 3,
3556                kind: ParticipantKind::new(0xABCD),
3557                fault: ParticipantFault::InvalidState,
3558            }),
3559            Some(WorldFault::RigidOverflow),
3560            Some(WorldFault::ForceOutOfRange { body: 17 }),
3561            Some(WorldFault::FieldOutOfRange {
3562                field: PortId::new(9),
3563                index: 21,
3564            }),
3565        ];
3566        for f in faults {
3567            let mut w = super::W(Vec::new());
3568            super::w_fault(&mut w, f);
3569            let mut r = reader(&w.0);
3570            assert_eq!(super::r_fault(&mut r), Ok(f));
3571            assert_eq!(r.pos, w.0.len(), "{f:?} reads exactly what it wrote");
3572        }
3573        // a participant fault with an unknown fault tag
3574        let mut w = super::W(Vec::new());
3575        w.u8(1);
3576        w.usize(0);
3577        w.u32(1);
3578        w.u8(0xEE);
3579        assert_eq!(
3580            super::r_fault(&mut reader(&w.0)),
3581            Err(WorldSnapshotError::InvalidValue { section: "fault" })
3582        );
3583    }
3584
3585    // Version 4 header identifiers. oracle: the identifiers computed apart
3586    // from the blob (`PHYSICS_SEMANTICS_ID`, `law_id` of the written world)
3587    // and the documented offsets.
3588
3589    /// Recompute the trailing checksum so only the targeted check can fire.
3590    fn reseal(mut b: Vec<u8>) -> Vec<u8> {
3591        let n = b.len() - super::CHECKSUM_LEN;
3592        let mut h: u64 = 0xcbf2_9ce4_8422_2325;
3593        super::super::fnv1a_fold(&mut h, &b[..n]);
3594        b[n..].copy_from_slice(&h.to_le_bytes());
3595        b
3596    }
3597
3598    fn header_id(b: &[u8], at: usize) -> [u8; 32] {
3599        b[at..at + 32].try_into().expect("32 bytes")
3600    }
3601
3602    #[test]
3603    fn version_4_header_layout() {
3604        use crate::semantics::PHYSICS_SEMANTICS_ID;
3605        assert_eq!(super::HEADER_LEN_V4, 80);
3606        assert_eq!((super::SEMANTICS_ID_AT, super::LAW_ID_AT), (16, 48));
3607        assert_eq!(super::header_len(1), 16);
3608        assert_eq!(super::header_len(3), 16);
3609        assert_eq!(super::header_len(4), 80);
3610        let w = variant_world();
3611        let b = w.snapshot_world();
3612        assert_eq!(&b[4..6], &4u16.to_le_bytes());
3613        assert_eq!(header_id(&b, 16), PHYSICS_SEMANTICS_ID);
3614        assert_eq!(header_id(&b, 48), w.law_id(&PHYSICS_SEMANTICS_ID));
3615        let len = u64::from_le_bytes(b[8..16].try_into().expect("8 bytes")) as usize;
3616        assert_eq!(len, b.len() - 80 - 8);
3617    }
3618
3619    #[test]
3620    fn law_check_covers_every_case() {
3621        use super::{law_check, LawCheck};
3622        let (s, l) = ([1u8; 32], [2u8; 32]);
3623        let other = [3u8; 32];
3624        assert_eq!(law_check(None, None), Ok(LawCheck::Unpinned));
3625        assert_eq!(law_check(None, Some(&other)), Ok(LawCheck::Unpinned));
3626        assert_eq!(
3627            law_check(Some((s, l)), None),
3628            Ok(LawCheck::SemanticsVerified)
3629        );
3630        assert_eq!(law_check(Some((s, l)), Some(&l)), Ok(LawCheck::Verified));
3631        assert_eq!(
3632            law_check(Some((s, l)), Some(&other)),
3633            Err(WorldSnapshotError::LawIdMismatch {
3634                stored: l,
3635                expected: other
3636            })
3637        );
3638    }
3639
3640    /// A foreign `semantics_id` (checksum recomputed) is refused after the
3641    /// checksum, before anything is read; a changed `law_id` is refused only
3642    /// against an expected one; the next version is unsupported; the target
3643    /// is untouched in every case.
3644    #[test]
3645    fn version_4_identifier_checks() {
3646        use super::LawCheck;
3647        use crate::semantics::PHYSICS_SEMANTICS_ID;
3648        let src = variant_world();
3649        let blob = src.snapshot_world();
3650        let law = src.law_id(&PHYSICS_SEMANTICS_ID);
3651        let mut t = PhysicsWorld::new(SolverConfig::default());
3652        let before = digest(&mut t);
3653
3654        let mut x = blob.clone();
3655        x[16 + 7] ^= 0x40;
3656        let x = reseal(x);
3657        let foreign = WorldSnapshotError::SemanticsMismatch {
3658            stored: header_id(&x, 16),
3659            expected: PHYSICS_SEMANTICS_ID,
3660        };
3661        assert_eq!(t.restore_world(&x), Err(foreign));
3662        assert_eq!(t.restore_world_checked(&x, Some(&law)), Err(foreign));
3663
3664        let mut x = blob.clone();
3665        x[48 + 3] ^= 0x02;
3666        let x = reseal(x);
3667        assert_eq!(
3668            t.restore_world_checked(&x, Some(&law)),
3669            Err(WorldSnapshotError::LawIdMismatch {
3670                stored: header_id(&x, 48),
3671                expected: law
3672            })
3673        );
3674
3675        let mut x = blob.clone();
3676        x[4..6].copy_from_slice(&5u16.to_le_bytes());
3677        assert_eq!(
3678            t.restore_world(&reseal(x)),
3679            Err(WorldSnapshotError::UnsupportedVersion {
3680                found: 5,
3681                supported: 4
3682            })
3683        );
3684        // a version 4 header cut inside its identifiers
3685        assert_eq!(
3686            t.restore_world(&blob[..60]),
3687            Err(WorldSnapshotError::Truncated)
3688        );
3689        assert_eq!(digest(&mut t), before);
3690
3691        assert_eq!(
3692            t.restore_world_checked(&blob, None),
3693            Ok(LawCheck::SemanticsVerified)
3694        );
3695        let mut t = PhysicsWorld::new(SolverConfig::default());
3696        assert_eq!(
3697            t.restore_world_checked(&blob, Some(&law)),
3698            Ok(LawCheck::Verified)
3699        );
3700    }
3701
3702    /// The version 3 form of a version 4 blob (same payload, 16-byte header)
3703    /// restores to the same world and reports `Unpinned`.
3704    #[test]
3705    fn version_3_form_is_unpinned() {
3706        use super::LawCheck;
3707        let src = variant_world();
3708        let blob = src.snapshot_world();
3709        let mut v3 = blob[..16].to_vec();
3710        v3[4..6].copy_from_slice(&3u16.to_le_bytes());
3711        v3.extend_from_slice(&blob[80..]);
3712        let v3 = reseal(v3);
3713        let mut t = PhysicsWorld::new(SolverConfig::default());
3714        assert_eq!(
3715            t.restore_world_checked(&v3, Some(&[9; 32])),
3716            Ok(LawCheck::Unpinned)
3717        );
3718        assert_eq!(t.snapshot_world(), blob);
3719    }
3720}