Skip to main content

natnet/
modeldef.rs

1//! MODELDEF data descriptions (message id 5).
2//!
3//! - **Envelope:** NatNet 3.x and 4.0 carry a count, then type and body per
4//!   dataset; 4.1 adds a byte count after each type. The version picks the
5//!   envelope; payload bytes are never sniffed.
6//! - **Types:** 0–6 are typed; unknown 4.1 types become [`DataSet::Other`].
7//!   Layouts for types 2–6 are spec-derived, unconfirmed against Motive.
8//! - **Strict parsing** rejects residue, truncation and malformed counts.
9//! - **Compatible parsing** drops residue after type 0 fields and after the
10//!   payload, keeps type 1 tails verbatim, and (4.1 only) keeps a type 2–6
11//!   body that would not re-encode identically, including invalid UTF-8, as
12//!   [`DataSet::Other`]. Legacy layouts have no body size, so a malformed
13//!   body is an error there.
14//! - **Checked encoding** reproduces the parsed bytes, minus anything
15//!   compatible parsing discarded.
16
17use crate::bytes::{Reader, Writer, checked_i32, vec_for_count};
18use crate::error::{Error, Result};
19use crate::message::{self, NAT_MODELDEF};
20use crate::version::{Layout, ParsePolicy, Version};
21
22/// Dataset type id: marker set.
23pub const TYPE_MARKER_SET: i32 = 0;
24/// Dataset type id: rigid body.
25pub const TYPE_RIGID_BODY: i32 = 1;
26/// Dataset type id: skeleton.
27pub const TYPE_SKELETON: i32 = 2;
28/// Dataset type id: force plate.
29pub const TYPE_FORCE_PLATE: i32 = 3;
30/// Dataset type id: device.
31pub const TYPE_DEVICE: i32 = 4;
32/// Dataset type id: camera.
33pub const TYPE_CAMERA: i32 = 5;
34/// Dataset type id: asset.
35pub const TYPE_ASSET: i32 = 6;
36
37/// A marker-set data description: a name and its member marker names.
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct MarkerSetDef {
41    /// Marker-set name.
42    pub name: String,
43    /// Member marker names.
44    pub marker_names: Vec<String>,
45}
46
47/// A rigid-body data description.
48///
49/// `tail` contains the version-dependent marker description after the common
50/// name/id/parent/offset fields. Compatible parsing keeps it byte-for-byte,
51/// including unknown extensions. Strict parsing and checked encoding validate
52/// the mandatory prefix; 4.1 preserves suffixes, while legacy requires an
53/// exact tail because it has no body size. Both reject truncated or malformed
54/// prefixes. Use [`ModelDef::to_payload_lossy`] to retain an invalid tail.
55/// Skeleton bones and asset rigid bodies require an exact marker-description
56/// tail in every layout ([`SkeletonDef::rigid_bodies`], [`AssetDef::rigid_bodies`]).
57#[derive(Debug, Clone, PartialEq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub struct RigidBodyDef {
60    /// Rigid-body name.
61    pub name: String,
62    /// Streaming id for a top-level rigid body; in a skeleton, the bone index.
63    pub id: i32,
64    /// Parent id, or −1 for none.
65    pub parent_id: i32,
66    /// Pivot offset `[x, y, z]` in the server's native frame.
67    pub offset: [f32; 3],
68    /// Version-dependent marker description bytes.
69    pub tail: Vec<u8>,
70}
71
72/// A skeleton data description (dataset type 2).
73///
74/// Wire: `cstr name`, `int32 id`, `int32 nRigidBodies`, then that many
75/// rigid-body descriptions. Spec-derived (SDK `UnpackSkeletonDescription`),
76/// unconfirmed against real Motive bytes. Bone tails contain
77/// exactly the version-dependent marker description, with no opaque suffix.
78#[derive(Debug, Clone, PartialEq)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80pub struct SkeletonDef {
81    /// Skeleton name.
82    pub name: String,
83    /// Skeleton id, as streamed in FrameOfData skeletons.
84    pub id: i32,
85    /// Bone descriptions. `NatNetTypes.h` documents a bone's `id` as its
86    /// bone index and `parent_id` as the parent bone.
87    pub rigid_bodies: Vec<RigidBodyDef>,
88}
89
90/// A force-plate data description (dataset type 3).
91///
92/// Spec-derived (SDK `UnpackForcePlateDescription`, `sForcePlateDescription`),
93/// unconfirmed against real Motive bytes.
94#[derive(Debug, Clone, PartialEq)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub struct ForcePlateDef {
97    /// Force-plate id, as streamed in FrameOfData force plates.
98    pub id: i32,
99    /// Serial number.
100    pub serial_number: String,
101    /// Physical width (manufacturer supplied).
102    pub width: f32,
103    /// Physical length (manufacturer supplied).
104    pub length: f32,
105    /// Electrical-centre offset `[x, y, z]` (manufacturer supplied).
106    pub origin: [f32; 3],
107    /// 12x12 calibration matrix, row-major as on the wire.
108    pub calibration_matrix: [[f32; 12]; 12],
109    /// Four plate corners `[x, y, z]` in world coordinates.
110    pub corners: [[f32; 3]; 4],
111    /// Plate type (C3D convention).
112    pub plate_type: i32,
113    /// Channel data type (`NatNetTypes.h`: 0 calibrated force data, 1 raw
114    /// analog voltages).
115    pub channel_data_type: i32,
116    /// Channel names.
117    pub channel_names: Vec<String>,
118}
119
120/// A peripheral-device data description (dataset type 4).
121///
122/// Spec-derived (SDK `UnpackDeviceDescription`, `sDeviceDescription`),
123/// unconfirmed against real Motive bytes.
124#[derive(Debug, Clone, PartialEq, Eq)]
125#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
126pub struct DeviceDef {
127    /// Device id, as streamed in FrameOfData devices.
128    pub id: i32,
129    /// Device name as shown in Motive.
130    pub name: String,
131    /// Serial number.
132    pub serial_number: String,
133    /// Device type code.
134    pub device_type: i32,
135    /// Channel data type code.
136    pub channel_data_type: i32,
137    /// Channel names.
138    pub channel_names: Vec<String>,
139}
140
141/// A camera data description (dataset type 5).
142///
143/// Wire: `cstr name`, `[f32; 3]` position, `[f32; 4]` orientation.
144/// Spec-derived (SDK `UnpackCameraDescription`), unconfirmed against real
145/// Motive bytes.
146#[derive(Debug, Clone, PartialEq)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148pub struct CameraDef {
149    /// Camera name.
150    pub name: String,
151    /// Position `[x, y, z]` in the server's native frame.
152    pub position: [f32; 3],
153    /// Orientation quaternion `[x, y, z, w]` in the server's native frame.
154    pub orientation: [f32; 4],
155}
156
157/// A marker description inside an [`AssetDef`].
158///
159/// Spec-derived layout: name, id, initial position, size, and raw 16-bit
160/// params (bit 0 is documented as active).
161#[derive(Debug, Clone, PartialEq)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163pub struct MarkerDef {
164    /// Marker name.
165    pub name: String,
166    /// Marker id.
167    pub id: i32,
168    /// Initial position `[x, y, z]`.
169    pub position: [f32; 3],
170    /// Marker size.
171    pub size: f32,
172    /// Raw parameter bits.
173    pub params: u16,
174}
175
176/// An asset data description (dataset type 6), for example a trained marker
177/// set.
178///
179/// Spec-derived and unconfirmed against real Motive bytes. Rigid bodies
180/// follow the same exact-tail rule as [`SkeletonDef`] bones.
181#[derive(Debug, Clone, PartialEq)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
183pub struct AssetDef {
184    /// Asset name.
185    pub name: String,
186    /// Asset type (`NatNetTypes.h` `AssetTypes`: 1 is a trained marker set).
187    pub asset_type: i32,
188    /// Asset id, as streamed in FrameOfData assets.
189    pub id: i32,
190    /// Rigid-body descriptions.
191    pub rigid_bodies: Vec<RigidBodyDef>,
192    /// Marker descriptions.
193    pub markers: Vec<MarkerDef>,
194}
195
196/// One dataset in a MODELDEF reply.
197#[derive(Debug, Clone, PartialEq)]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199#[non_exhaustive]
200pub enum DataSet {
201    /// A marker-set description.
202    MarkerSet(MarkerSetDef),
203    /// A rigid-body description.
204    RigidBody(RigidBodyDef),
205    /// A skeleton description.
206    Skeleton(SkeletonDef),
207    /// A force-plate description.
208    ForcePlate(Box<ForcePlateDef>),
209    /// A peripheral-device description.
210    Device(DeviceDef),
211    /// A camera description.
212    Camera(CameraDef),
213    /// An asset description.
214    Asset(AssetDef),
215    /// A dataset kept verbatim: unknown 4.1 type or unrepresentable type 2–6
216    /// body (see the [module docs](self)).
217    Other {
218        /// The dataset type id.
219        type_id: i32,
220        /// The dataset body bytes.
221        body: Vec<u8>,
222    },
223}
224
225impl DataSet {
226    /// The dataset's wire type id.
227    #[must_use]
228    pub const fn type_id(&self) -> i32 {
229        match self {
230            Self::MarkerSet(_) => TYPE_MARKER_SET,
231            Self::RigidBody(_) => TYPE_RIGID_BODY,
232            Self::Skeleton(_) => TYPE_SKELETON,
233            Self::ForcePlate(_) => TYPE_FORCE_PLATE,
234            Self::Device(_) => TYPE_DEVICE,
235            Self::Camera(_) => TYPE_CAMERA,
236            Self::Asset(_) => TYPE_ASSET,
237            Self::Other { type_id, .. } => *type_id,
238        }
239    }
240}
241
242/// A decoded MODELDEF reply: an ordered list of data descriptions.
243#[derive(Debug, Clone, PartialEq)]
244#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
245pub struct ModelDef {
246    /// The datasets, in wire order.
247    pub datasets: Vec<DataSet>,
248}
249
250fn invalid_count(count: i32, offset: usize) -> Error {
251    Error::InvalidCount { count, offset }
252}
253
254/// Reads a count, checking `count * min_entry_bytes` fits in the remaining
255/// bytes before anything is allocated.
256fn count(r: &mut Reader<'_>, min_entry_bytes: usize) -> Result<usize> {
257    let offset = r.position();
258    let raw = r.i32()?;
259    if raw < 0 {
260        return Err(invalid_count(raw, offset));
261    }
262    r.guard_count(raw, min_entry_bytes)
263}
264
265fn size(r: &mut Reader<'_>) -> Result<usize> {
266    let offset = r.position();
267    let raw = r.i32()?;
268    if raw < 0 {
269        return Err(invalid_count(raw, offset));
270    }
271    let size = usize::try_from(raw).map_err(|_| invalid_count(raw, offset))?;
272    if size > r.remaining() {
273        return Err(Error::UnexpectedEof {
274            offset: r.position(),
275            needed: size,
276            available: r.remaining(),
277        });
278    }
279    Ok(size)
280}
281
282fn read_marker_set_def(r: &mut Reader<'_>) -> Result<MarkerSetDef> {
283    let name = r.cstr()?;
284    let n = count(r, 1)?;
285    let mut marker_names = vec_for_count(n);
286    for _ in 0..n {
287        marker_names.push(r.cstr()?);
288    }
289    Ok(MarkerSetDef { name, marker_names })
290}
291
292fn rigid_marker_min(layout: Layout) -> usize {
293    if layout.rigid_body_marker_names() {
294        17
295    } else {
296        16
297    }
298}
299
300fn walk_rigid_body_tail(r: &mut Reader<'_>, layout: Layout) -> Result<()> {
301    let n = count(r, rigid_marker_min(layout))?;
302    for _ in 0..n {
303        r.vec3()?;
304    }
305    for _ in 0..n {
306        r.i32()?;
307    }
308    if layout.rigid_body_marker_names() {
309        for _ in 0..n {
310            r.cstr()?;
311        }
312    }
313    Ok(())
314}
315
316fn read_rigid_body_def(r: &mut Reader<'_>, layout: Layout) -> Result<RigidBodyDef> {
317    let name = r.cstr()?;
318    let id = r.i32()?;
319    let parent_id = r.i32()?;
320    let offset = r.vec3()?;
321    let tail_start = r.position();
322    walk_rigid_body_tail(r, layout)?;
323    let tail = r.span(tail_start, r.position())?.to_vec();
324    Ok(RigidBodyDef {
325        name,
326        id,
327        parent_id,
328        offset,
329        tail,
330    })
331}
332
333const MIN_RIGID_BODY_DEF: usize = 25;
334const MIN_MARKER_DEF: usize = 23;
335
336fn read_rigid_body_defs(r: &mut Reader<'_>, layout: Layout) -> Result<Vec<RigidBodyDef>> {
337    let n = count(r, MIN_RIGID_BODY_DEF)?;
338    let mut out = vec_for_count(n);
339    for _ in 0..n {
340        out.push(read_rigid_body_def(r, layout)?);
341    }
342    Ok(out)
343}
344
345fn read_names(r: &mut Reader<'_>) -> Result<Vec<String>> {
346    let n = count(r, 1)?;
347    let mut out = vec_for_count(n);
348    for _ in 0..n {
349        out.push(r.cstr()?);
350    }
351    Ok(out)
352}
353
354fn read_skeleton_def(r: &mut Reader<'_>, layout: Layout) -> Result<SkeletonDef> {
355    Ok(SkeletonDef {
356        name: r.cstr()?,
357        id: r.i32()?,
358        rigid_bodies: read_rigid_body_defs(r, layout)?,
359    })
360}
361
362fn read_force_plate_def(r: &mut Reader<'_>) -> Result<ForcePlateDef> {
363    let id = r.i32()?;
364    let serial_number = r.cstr()?;
365    let width = r.f32()?;
366    let length = r.f32()?;
367    let origin = r.vec3()?;
368    let mut calibration_matrix = [[0.0; 12]; 12];
369    for row in &mut calibration_matrix {
370        for value in row {
371            *value = r.f32()?;
372        }
373    }
374    let mut corners = [[0.0; 3]; 4];
375    for corner in &mut corners {
376        *corner = r.vec3()?;
377    }
378    Ok(ForcePlateDef {
379        id,
380        serial_number,
381        width,
382        length,
383        origin,
384        calibration_matrix,
385        corners,
386        plate_type: r.i32()?,
387        channel_data_type: r.i32()?,
388        channel_names: read_names(r)?,
389    })
390}
391
392fn read_device_def(r: &mut Reader<'_>) -> Result<DeviceDef> {
393    Ok(DeviceDef {
394        id: r.i32()?,
395        name: r.cstr()?,
396        serial_number: r.cstr()?,
397        device_type: r.i32()?,
398        channel_data_type: r.i32()?,
399        channel_names: read_names(r)?,
400    })
401}
402
403fn read_camera_def(r: &mut Reader<'_>) -> Result<CameraDef> {
404    Ok(CameraDef {
405        name: r.cstr()?,
406        position: r.vec3()?,
407        orientation: r.quat()?,
408    })
409}
410
411fn read_marker_def(r: &mut Reader<'_>) -> Result<MarkerDef> {
412    Ok(MarkerDef {
413        name: r.cstr()?,
414        id: r.i32()?,
415        position: r.vec3()?,
416        size: r.f32()?,
417        params: r.u16()?,
418    })
419}
420
421fn read_asset_def(r: &mut Reader<'_>, layout: Layout) -> Result<AssetDef> {
422    let name = r.cstr()?;
423    let asset_type = r.i32()?;
424    let id = r.i32()?;
425    let rigid_bodies = read_rigid_body_defs(r, layout)?;
426    let n = count(r, MIN_MARKER_DEF)?;
427    let mut markers = vec_for_count(n);
428    for _ in 0..n {
429        markers.push(read_marker_def(r)?);
430    }
431    Ok(AssetDef {
432        name,
433        asset_type,
434        id,
435        rigid_bodies,
436        markers,
437    })
438}
439
440fn read_described(type_id: i32, r: &mut Reader<'_>, layout: Layout) -> Result<Option<DataSet>> {
441    Ok(Some(match type_id {
442        TYPE_SKELETON => DataSet::Skeleton(read_skeleton_def(r, layout)?),
443        TYPE_FORCE_PLATE => DataSet::ForcePlate(Box::new(read_force_plate_def(r)?)),
444        TYPE_DEVICE => DataSet::Device(read_device_def(r)?),
445        TYPE_CAMERA => DataSet::Camera(read_camera_def(r)?),
446        TYPE_ASSET => DataSet::Asset(read_asset_def(r, layout)?),
447        _ => return Ok(None),
448    }))
449}
450
451fn walk_dataset(r: &mut Reader<'_>, type_id: i32, layout: Layout, version: Version) -> Result<()> {
452    match type_id {
453        TYPE_MARKER_SET => {
454            read_marker_set_def(r)?;
455        }
456        TYPE_RIGID_BODY => {
457            read_rigid_body_def(r, layout)?;
458        }
459        _ => {
460            if read_described(type_id, r, layout)?.is_none() {
461                return Err(Error::UnsupportedDataSet {
462                    type_id,
463                    major: version.major,
464                    minor: version.minor,
465                });
466            }
467        }
468    }
469    Ok(())
470}
471
472fn validate_rigid_tail(tail: &[u8], layout: Layout) -> Result<()> {
473    let mut r = Reader::new(tail);
474    walk_rigid_body_tail(&mut r, layout)?;
475    if !layout.has_section_sizes() {
476        r.finish()?;
477    }
478    Ok(())
479}
480
481fn validate_nested_rigid_tail(tail: &[u8], layout: Layout) -> Result<()> {
482    let mut r = Reader::new(tail);
483    walk_rigid_body_tail(&mut r, layout)?;
484    r.finish()
485}
486
487fn parse_marker_set_reader(r: &mut Reader<'_>, policy: ParsePolicy) -> Result<DataSet> {
488    let marker_set = read_marker_set_def(r)?;
489    if policy == ParsePolicy::Strict {
490        r.finish()?;
491    } else {
492        r.take(r.remaining())?;
493    }
494    Ok(DataSet::MarkerSet(marker_set))
495}
496
497fn parse_rigid_body_reader(
498    r: &mut Reader<'_>,
499    layout: Layout,
500    policy: ParsePolicy,
501) -> Result<DataSet> {
502    let name = r.cstr()?;
503    let id = r.i32()?;
504    let parent_id = r.i32()?;
505    let offset = r.vec3()?;
506    let tail_start = r.position();
507    let tail_len = r.remaining();
508    let mut tail_reader = r.subreader(tail_len)?;
509    if policy == ParsePolicy::Strict {
510        walk_rigid_body_tail(&mut tail_reader, layout)?;
511    }
512    let tail = r.span(tail_start, tail_start + tail_len)?.to_vec();
513    Ok(DataSet::RigidBody(RigidBodyDef {
514        name,
515        id,
516        parent_id,
517        offset,
518        tail,
519    }))
520}
521
522fn parse_described_reader(
523    type_id: i32,
524    r: &mut Reader<'_>,
525    layout: Layout,
526    version: Version,
527    policy: ParsePolicy,
528) -> Result<DataSet> {
529    let start = r.position();
530    let len = r.remaining();
531    let mut body = r.subreader(len)?;
532    let parsed = read_described(type_id, &mut body, layout).and_then(|dataset| {
533        body.finish()?;
534        Ok(dataset)
535    });
536    let bytes = r.span(start, start + len)?;
537    match (policy, parsed) {
538        (ParsePolicy::Strict, Ok(Some(dataset))) => Ok(dataset),
539        (ParsePolicy::Strict, Err(error)) => Err(error),
540        (ParsePolicy::Compatible, Ok(Some(dataset)))
541            if encode_dataset(&dataset, layout, version).is_ok_and(|encoded| encoded == bytes) =>
542        {
543            Ok(dataset)
544        }
545        _ => Ok(DataSet::Other {
546            type_id,
547            body: bytes.to_vec(),
548        }),
549    }
550}
551
552fn parse_dataset_reader(
553    type_id: i32,
554    r: &mut Reader<'_>,
555    layout: Layout,
556    version: Version,
557    policy: ParsePolicy,
558) -> Result<DataSet> {
559    match type_id {
560        TYPE_MARKER_SET => parse_marker_set_reader(r, policy),
561        TYPE_RIGID_BODY => parse_rigid_body_reader(r, layout, policy),
562        TYPE_SKELETON..=TYPE_ASSET => parse_described_reader(type_id, r, layout, version, policy),
563        _ => Ok(DataSet::Other {
564            type_id,
565            body: r.take(r.remaining())?.to_vec(),
566        }),
567    }
568}
569
570fn parse_dataset(
571    type_id: i32,
572    body: &[u8],
573    layout: Layout,
574    version: Version,
575    policy: ParsePolicy,
576) -> Result<DataSet> {
577    let mut r = Reader::new(body);
578    let dataset = parse_dataset_reader(type_id, &mut r, layout, version, policy)?;
579    r.finish()?;
580    Ok(dataset)
581}
582
583impl ModelDef {
584    /// Parses a MODELDEF datagram with compatible validation; use
585    /// [`Self::parse_with_policy`] for strict decoding.
586    ///
587    /// # Errors
588    /// Returns a header, unsupported-version, count, or dataset decoding error.
589    pub fn parse(datagram: &[u8], version: Version) -> Result<Self> {
590        Self::parse_with_policy(datagram, version, ParsePolicy::Compatible)
591    }
592
593    /// Parses a MODELDEF payload (bytes after the 4-byte header) with
594    /// compatible validation.
595    ///
596    /// # Errors
597    /// Returns an unsupported-version, count, or dataset decoding error.
598    pub fn parse_payload(payload: &[u8], version: Version) -> Result<Self> {
599        Self::parse_payload_with_policy(payload, version, ParsePolicy::Compatible)
600    }
601
602    /// Parses a datagram with an explicit version and validation policy.
603    /// # Errors
604    /// Returns a header, unsupported-version, count, or policy validation error.
605    pub fn parse_with_policy(
606        datagram: &[u8],
607        version: Version,
608        policy: ParsePolicy,
609    ) -> Result<Self> {
610        Self::parse_payload_with_policy(message::expect(datagram, NAT_MODELDEF)?, version, policy)
611    }
612
613    /// Parses a payload with an explicit version and validation policy.
614    /// # Errors
615    /// Returns an unsupported-version, count, or policy validation error.
616    pub fn parse_payload_with_policy(
617        payload: &[u8],
618        version: Version,
619        policy: ParsePolicy,
620    ) -> Result<Self> {
621        let layout = version.layout()?;
622        let mut r = Reader::new(payload);
623        let dataset_count = count(&mut r, if layout.has_section_sizes() { 8 } else { 4 })?;
624        let mut datasets = vec_for_count(dataset_count);
625
626        for _ in 0..dataset_count {
627            let type_id = r.i32()?;
628            if layout.has_section_sizes() {
629                let body_size = size(&mut r)?;
630                let mut body_reader = r.subreader(body_size)?;
631                let dataset =
632                    parse_dataset_reader(type_id, &mut body_reader, layout, version, policy)?;
633                body_reader.finish()?;
634                datasets.push(dataset);
635            } else {
636                let start = r.position();
637                walk_dataset(&mut r, type_id, layout, version)?;
638                let end = r.position();
639                let body = r.span(start, end)?;
640                datasets.push(parse_dataset(type_id, body, layout, version, policy)?);
641            }
642        }
643
644        if policy == ParsePolicy::Strict {
645            r.finish()?;
646        }
647        Ok(Self { datasets })
648    }
649
650    /// Encodes a checked MODELDEF payload (without the four-byte header).
651    ///
652    /// # Errors
653    /// Returns an unsupported-version error, or a checked encoding error for an
654    /// invalid string, count, dataset, or rigid-body marker prefix.
655    pub fn to_payload(&self, version: Version) -> Result<Vec<u8>> {
656        self.encode_payload(version.layout()?, version)
657    }
658
659    /// Encodes a checked datagram (header plus payload).
660    ///
661    /// # Errors
662    /// Same as [`Self::to_payload`], plus [`Error::LengthOverflow`] for a
663    /// payload above the 65 535-byte `u16` limit.
664    pub fn to_datagram(&self, version: Version) -> Result<Vec<u8>> {
665        let payload = self.to_payload(version)?;
666        message::frame_checked(NAT_MODELDEF, &payload)
667    }
668
669    /// Encodes a payload without value validation: strings may truncate and
670    /// counts may saturate or use type aliases.
671    ///
672    /// # Errors
673    /// Returns [`Error::UnsupportedVersion`] for unsupported layouts.
674    pub fn to_payload_lossy(&self, version: Version) -> Result<Vec<u8>> {
675        Ok(self.encode_payload_lossy(version.layout()?))
676    }
677
678    /// Encodes without value validation. Payloads over 65 535 bytes are emitted
679    /// in full with a saturated declared size.
680    ///
681    /// # Errors
682    /// Returns [`Error::UnsupportedVersion`] for unsupported layouts.
683    pub fn to_datagram_lossy(&self, version: Version) -> Result<Vec<u8>> {
684        let payload = self.to_payload_lossy(version)?;
685        Ok(message::frame(NAT_MODELDEF, &payload))
686    }
687
688    fn encode_payload(&self, layout: Layout, version: Version) -> Result<Vec<u8>> {
689        let mut w = Writer::new();
690        w.i32(checked_i32(self.datasets.len(), "dataset count")?);
691        for dataset in &self.datasets {
692            let type_id = dataset.type_id();
693            let body = encode_dataset(dataset, layout, version)?;
694            w.i32(type_id);
695            if layout.has_section_sizes() {
696                w.i32(checked_i32(body.len(), "dataset body")?);
697            }
698            w.bytes(&body);
699        }
700        Ok(w.into_vec())
701    }
702
703    fn encode_payload_lossy(&self, layout: Layout) -> Vec<u8> {
704        let mut w = Writer::new();
705        w.i32(i32::try_from(self.datasets.len()).unwrap_or(i32::MAX));
706        for dataset in &self.datasets {
707            let type_id = dataset.type_id();
708            let body = encode_dataset_lossy(dataset);
709            w.i32(type_id);
710            if layout.has_section_sizes() {
711                w.i32(i32::try_from(body.len()).unwrap_or(i32::MAX));
712            }
713            w.bytes(&body);
714        }
715        w.into_vec()
716    }
717
718    /// Resolves a rigid-body name to its streaming id. Returns the first
719    /// match, or `None`.
720    #[must_use]
721    pub fn resolve_rigid_body(&self, name: &str) -> Option<i32> {
722        self.datasets.iter().find_map(|ds| match ds {
723            DataSet::RigidBody(rb) if rb.name == name => Some(rb.id),
724            _ => None,
725        })
726    }
727
728    /// All rigid-body (name, id) pairs in the definition.
729    pub fn rigid_bodies(&self) -> impl Iterator<Item = (&str, i32)> {
730        self.datasets.iter().filter_map(|ds| match ds {
731            DataSet::RigidBody(rb) => Some((rb.name.as_str(), rb.id)),
732            _ => None,
733        })
734    }
735
736    /// Counts datasets of a given wire `type_id`.
737    #[must_use]
738    pub fn count_of_type(&self, type_id: i32) -> usize {
739        self.datasets
740            .iter()
741            .filter(|d| d.type_id() == type_id)
742            .count()
743    }
744}
745
746fn write_rigid_body_head(w: &mut Writer, rigid_body: &RigidBodyDef) -> Result<()> {
747    w.try_cstr(&rigid_body.name, "rigid-body name")?;
748    w.i32(rigid_body.id);
749    w.i32(rigid_body.parent_id);
750    w.vec3(rigid_body.offset);
751    Ok(())
752}
753
754fn write_nested_rigid_bodies(
755    w: &mut Writer,
756    rigid_bodies: &[RigidBodyDef],
757    layout: Layout,
758    count_field: &'static str,
759) -> Result<()> {
760    w.i32(checked_i32(rigid_bodies.len(), count_field)?);
761    for rigid_body in rigid_bodies {
762        write_rigid_body_head(w, rigid_body)?;
763        validate_nested_rigid_tail(&rigid_body.tail, layout)?;
764        w.bytes(&rigid_body.tail);
765    }
766    Ok(())
767}
768
769fn write_names(
770    w: &mut Writer,
771    names: &[String],
772    count_field: &'static str,
773    name_field: &'static str,
774) -> Result<()> {
775    w.i32(checked_i32(names.len(), count_field)?);
776    for name in names {
777        w.try_cstr(name, name_field)?;
778    }
779    Ok(())
780}
781
782fn write_force_plate_fixed(w: &mut Writer, plate: &ForcePlateDef) {
783    w.f32(plate.width);
784    w.f32(plate.length);
785    w.vec3(plate.origin);
786    for row in &plate.calibration_matrix {
787        for value in row {
788            w.f32(*value);
789        }
790    }
791    for corner in &plate.corners {
792        w.vec3(*corner);
793    }
794    w.i32(plate.plate_type);
795    w.i32(plate.channel_data_type);
796}
797
798fn write_marker_def_fixed(w: &mut Writer, marker: &MarkerDef) {
799    w.i32(marker.id);
800    w.vec3(marker.position);
801    w.f32(marker.size);
802    w.u16(marker.params);
803}
804
805fn other_has_typed_roundtrip(type_id: i32, body: &[u8], layout: Layout, version: Version) -> bool {
806    let mut reader = Reader::new(body);
807    let Ok(Some(dataset)) = read_described(type_id, &mut reader, layout) else {
808        return false;
809    };
810    if reader.finish().is_err() {
811        return false;
812    }
813    encode_dataset(&dataset, layout, version).is_ok_and(|encoded| encoded == body)
814}
815
816fn encode_dataset(dataset: &DataSet, layout: Layout, version: Version) -> Result<Vec<u8>> {
817    let mut w = Writer::new();
818    match dataset {
819        DataSet::MarkerSet(marker_set) => {
820            w.try_cstr(&marker_set.name, "marker-set name")?;
821            write_names(
822                &mut w,
823                &marker_set.marker_names,
824                "marker count",
825                "marker name",
826            )?;
827        }
828        DataSet::RigidBody(rigid_body) => {
829            write_rigid_body_head(&mut w, rigid_body)?;
830            validate_rigid_tail(&rigid_body.tail, layout)?;
831            w.bytes(&rigid_body.tail);
832        }
833        DataSet::Skeleton(skeleton) => {
834            w.try_cstr(&skeleton.name, "skeleton name")?;
835            w.i32(skeleton.id);
836            write_nested_rigid_bodies(
837                &mut w,
838                &skeleton.rigid_bodies,
839                layout,
840                "skeleton rigid body count",
841            )?;
842        }
843        DataSet::ForcePlate(plate) => {
844            w.i32(plate.id);
845            w.try_cstr(&plate.serial_number, "force plate serial number")?;
846            write_force_plate_fixed(&mut w, plate);
847            write_names(
848                &mut w,
849                &plate.channel_names,
850                "force plate channel count",
851                "force plate channel name",
852            )?;
853        }
854        DataSet::Device(device) => {
855            w.i32(device.id);
856            w.try_cstr(&device.name, "device name")?;
857            w.try_cstr(&device.serial_number, "device serial number")?;
858            w.i32(device.device_type);
859            w.i32(device.channel_data_type);
860            write_names(
861                &mut w,
862                &device.channel_names,
863                "device channel count",
864                "device channel name",
865            )?;
866        }
867        DataSet::Camera(camera) => {
868            w.try_cstr(&camera.name, "camera name")?;
869            w.vec3(camera.position);
870            w.quat(camera.orientation);
871        }
872        DataSet::Asset(asset) => {
873            w.try_cstr(&asset.name, "asset name")?;
874            w.i32(asset.asset_type);
875            w.i32(asset.id);
876            write_nested_rigid_bodies(
877                &mut w,
878                &asset.rigid_bodies,
879                layout,
880                "asset rigid body count",
881            )?;
882            w.i32(checked_i32(asset.markers.len(), "asset marker count")?);
883            for marker in &asset.markers {
884                w.try_cstr(&marker.name, "asset marker name")?;
885                write_marker_def_fixed(&mut w, marker);
886            }
887        }
888        DataSet::Other { type_id, body } => {
889            if *type_id == TYPE_MARKER_SET || *type_id == TYPE_RIGID_BODY {
890                return Err(Error::InvalidValue {
891                    field: "dataset type",
892                    reason: "type 0 and type 1 require their typed representation",
893                });
894            }
895            if (TYPE_SKELETON..=TYPE_ASSET).contains(type_id)
896                && other_has_typed_roundtrip(*type_id, body, layout, version)
897            {
898                return Err(Error::InvalidValue {
899                    field: "dataset type",
900                    reason: "known dataset body has a typed representation",
901                });
902            }
903            if !layout.has_section_sizes() {
904                let mut r = Reader::new(body);
905                walk_dataset(&mut r, *type_id, layout, version)?;
906                r.finish()?;
907            }
908            w.bytes(body);
909        }
910    }
911    Ok(w.into_vec())
912}
913
914fn lossy_count(len: usize) -> i32 {
915    i32::try_from(len).unwrap_or(i32::MAX)
916}
917
918fn lossy_rigid_bodies(w: &mut Writer, rigid_bodies: &[RigidBodyDef]) {
919    w.i32(lossy_count(rigid_bodies.len()));
920    for rigid_body in rigid_bodies {
921        lossy_rigid_body(w, rigid_body);
922    }
923}
924
925fn lossy_rigid_body(w: &mut Writer, rigid_body: &RigidBodyDef) {
926    w.cstr(&rigid_body.name);
927    w.i32(rigid_body.id);
928    w.i32(rigid_body.parent_id);
929    w.vec3(rigid_body.offset);
930    w.bytes(&rigid_body.tail);
931}
932
933fn lossy_names(w: &mut Writer, names: &[String]) {
934    w.i32(lossy_count(names.len()));
935    for name in names {
936        w.cstr(name);
937    }
938}
939
940fn encode_dataset_lossy(dataset: &DataSet) -> Vec<u8> {
941    let mut w = Writer::new();
942    match dataset {
943        DataSet::MarkerSet(marker_set) => {
944            w.cstr(&marker_set.name);
945            lossy_names(&mut w, &marker_set.marker_names);
946        }
947        DataSet::RigidBody(rigid_body) => lossy_rigid_body(&mut w, rigid_body),
948        DataSet::Skeleton(skeleton) => {
949            w.cstr(&skeleton.name);
950            w.i32(skeleton.id);
951            lossy_rigid_bodies(&mut w, &skeleton.rigid_bodies);
952        }
953        DataSet::ForcePlate(plate) => {
954            w.i32(plate.id);
955            w.cstr(&plate.serial_number);
956            write_force_plate_fixed(&mut w, plate);
957            lossy_names(&mut w, &plate.channel_names);
958        }
959        DataSet::Device(device) => {
960            w.i32(device.id);
961            w.cstr(&device.name);
962            w.cstr(&device.serial_number);
963            w.i32(device.device_type);
964            w.i32(device.channel_data_type);
965            lossy_names(&mut w, &device.channel_names);
966        }
967        DataSet::Camera(camera) => {
968            w.cstr(&camera.name);
969            w.vec3(camera.position);
970            w.quat(camera.orientation);
971        }
972        DataSet::Asset(asset) => {
973            w.cstr(&asset.name);
974            w.i32(asset.asset_type);
975            w.i32(asset.id);
976            lossy_rigid_bodies(&mut w, &asset.rigid_bodies);
977            w.i32(lossy_count(asset.markers.len()));
978            for marker in &asset.markers {
979                w.cstr(&marker.name);
980                write_marker_def_fixed(&mut w, marker);
981            }
982        }
983        DataSet::Other { body, .. } => w.bytes(body),
984    }
985    w.into_vec()
986}