Skip to main content

sectorsync_wire/
lib.rs

1//! Wire frame traits and default frame shapes for `SectorSync`.
2
3#![forbid(unsafe_code)]
4
5use sectorsync_core::prelude::{
6    BarrierId, BarrierState, ClientId, CommandEnvelope, CommandId, CommandPriority, ComponentId,
7    ComponentStore, EntityHandle, EntityId, EventId, EventKind, EventPriority, OwnerEpoch,
8    ReplicationPlan, Station, StationEvent, StationId, Tick,
9};
10
11/// Runtime frame kind.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum FrameKind {
14    /// Replication update frame.
15    Replication = 0,
16    /// Command acknowledgement frame.
17    CommandAck = 1,
18    /// Runtime barrier notification.
19    Barrier = 2,
20    /// Client command ingress frame.
21    Command = 3,
22    /// Cross-station event frame.
23    StationEvent = 4,
24    /// Gateway-to-station command dispatch frame.
25    CommandDispatch = 5,
26}
27
28impl FrameKind {
29    /// Converts a byte into a frame kind.
30    pub const fn from_byte(byte: u8) -> Option<Self> {
31        match byte {
32            0 => Some(Self::Replication),
33            1 => Some(Self::CommandAck),
34            2 => Some(Self::Barrier),
35            3 => Some(Self::Command),
36            4 => Some(Self::StationEvent),
37            5 => Some(Self::CommandDispatch),
38            _ => None,
39        }
40    }
41}
42
43/// Replication frame metadata produced per client.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct ReplicationFrame {
46    /// Target client.
47    pub client_id: ClientId,
48    /// Server tick represented by this frame.
49    pub server_tick: Tick,
50    /// Number of entity updates in this frame.
51    pub entity_count: u32,
52    /// Estimated payload bytes before transport overhead.
53    pub estimated_payload_bytes: u32,
54    /// Concrete entity/component deltas included in this frame.
55    pub entities: Vec<EntityDelta>,
56}
57
58/// Entity delta included in a replication frame.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct EntityDelta {
61    /// Entity being updated.
62    pub entity_id: EntityId,
63    /// Owner epoch observed by the sender.
64    pub owner_epoch: OwnerEpoch,
65    /// Component deltas for this entity.
66    pub components: Vec<ComponentDelta>,
67}
68
69/// Component delta included in an entity delta.
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct ComponentDelta {
72    /// Component id.
73    pub component_id: ComponentId,
74    /// Component version.
75    pub version: u64,
76    /// Runtime-defined flags.
77    pub flags: u8,
78    /// Encoded component bytes.
79    pub bytes: Vec<u8>,
80}
81
82/// Borrowed replication frame decoded directly from immutable wire bytes.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub struct ReplicationFrameRef<'a> {
85    /// Target client.
86    pub client_id: ClientId,
87    /// Server tick represented by this frame.
88    pub server_tick: Tick,
89    /// Number of entity updates reported by the sender.
90    pub entity_count: u32,
91    /// Estimated payload bytes before transport overhead.
92    pub estimated_payload_bytes: u32,
93    encoded_entities: &'a [u8],
94    encoded_entity_count: usize,
95}
96
97impl<'a> ReplicationFrameRef<'a> {
98    /// Number of concrete entity deltas encoded in this frame.
99    pub const fn encoded_entity_count(self) -> usize {
100        self.encoded_entity_count
101    }
102
103    /// Iterates concrete entity deltas without allocating.
104    pub fn entities(self) -> EntityDeltaRefIter<'a> {
105        EntityDeltaRefIter {
106            cursor: Cursor::new(self.encoded_entities),
107            remaining: self.encoded_entity_count,
108        }
109    }
110
111    /// Materializes the compatible owned frame shape.
112    pub fn to_owned(self) -> ReplicationFrame {
113        ReplicationFrame {
114            client_id: self.client_id,
115            server_tick: self.server_tick,
116            entity_count: self.entity_count,
117            estimated_payload_bytes: self.estimated_payload_bytes,
118            entities: self.entities().map(EntityDeltaRef::to_owned).collect(),
119        }
120    }
121}
122
123/// Borrowed entity delta inside a [`ReplicationFrameRef`].
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub struct EntityDeltaRef<'a> {
126    /// Entity being updated.
127    pub entity_id: EntityId,
128    /// Owner epoch observed by the sender.
129    pub owner_epoch: OwnerEpoch,
130    encoded_components: &'a [u8],
131    encoded_component_count: usize,
132}
133
134impl<'a> EntityDeltaRef<'a> {
135    /// Number of component deltas encoded for this entity.
136    pub const fn encoded_component_count(self) -> usize {
137        self.encoded_component_count
138    }
139
140    /// Iterates component deltas without allocating or copying payload bytes.
141    pub fn components(self) -> ComponentDeltaRefIter<'a> {
142        ComponentDeltaRefIter {
143            cursor: Cursor::new(self.encoded_components),
144            remaining: self.encoded_component_count,
145        }
146    }
147
148    /// Materializes the compatible owned entity delta shape.
149    pub fn to_owned(self) -> EntityDelta {
150        EntityDelta {
151            entity_id: self.entity_id,
152            owner_epoch: self.owner_epoch,
153            components: self.components().map(ComponentDeltaRef::to_owned).collect(),
154        }
155    }
156}
157
158/// Borrowed component delta inside an [`EntityDeltaRef`].
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub struct ComponentDeltaRef<'a> {
161    /// Component id.
162    pub component_id: ComponentId,
163    /// Component version.
164    pub version: u64,
165    /// Runtime-defined flags.
166    pub flags: u8,
167    /// Encoded component bytes borrowed from the input frame.
168    pub bytes: &'a [u8],
169}
170
171impl ComponentDeltaRef<'_> {
172    /// Materializes the compatible owned component delta shape.
173    pub fn to_owned(self) -> ComponentDelta {
174        ComponentDelta {
175            component_id: self.component_id,
176            version: self.version,
177            flags: self.flags,
178            bytes: self.bytes.to_vec(),
179        }
180    }
181}
182
183/// Exact-size iterator over validated borrowed entity deltas.
184#[derive(Clone, Debug)]
185pub struct EntityDeltaRefIter<'a> {
186    cursor: Cursor<'a>,
187    remaining: usize,
188}
189
190impl<'a> Iterator for EntityDeltaRefIter<'a> {
191    type Item = EntityDeltaRef<'a>;
192
193    fn next(&mut self) -> Option<Self::Item> {
194        if self.remaining == 0 {
195            return None;
196        }
197        let entity = decode_entity_ref(&mut self.cursor)
198            .expect("ReplicationFrameRef validates all entity bytes before iteration");
199        self.remaining -= 1;
200        Some(entity)
201    }
202
203    fn size_hint(&self) -> (usize, Option<usize>) {
204        (self.remaining, Some(self.remaining))
205    }
206}
207
208impl ExactSizeIterator for EntityDeltaRefIter<'_> {}
209
210/// Exact-size iterator over validated borrowed component deltas.
211#[derive(Clone, Debug)]
212pub struct ComponentDeltaRefIter<'a> {
213    cursor: Cursor<'a>,
214    remaining: usize,
215}
216
217impl<'a> Iterator for ComponentDeltaRefIter<'a> {
218    type Item = ComponentDeltaRef<'a>;
219
220    fn next(&mut self) -> Option<Self::Item> {
221        if self.remaining == 0 {
222            return None;
223        }
224        let component = decode_component_ref(&mut self.cursor)
225            .expect("ReplicationFrameRef validates all component bytes before iteration");
226        self.remaining -= 1;
227        Some(component)
228    }
229
230    fn size_hint(&self) -> (usize, Option<usize>) {
231        (self.remaining, Some(self.remaining))
232    }
233}
234
235impl ExactSizeIterator for ComponentDeltaRefIter<'_> {}
236
237/// Limits used by `ReplicationFrameBuilder`.
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
239pub struct ReplicationFrameLimits {
240    /// Maximum entity deltas to materialize in one frame.
241    pub max_entity_deltas: usize,
242    /// Maximum component deltas to include per entity.
243    pub max_components_per_entity: usize,
244    /// Maximum component payload bytes to include per component.
245    pub max_component_bytes: usize,
246}
247
248impl Default for ReplicationFrameLimits {
249    fn default() -> Self {
250        Self {
251            max_entity_deltas: 256,
252            max_components_per_entity: 16,
253            max_component_bytes: 1024,
254        }
255    }
256}
257
258/// Component selection for frame building.
259#[derive(Clone, Debug, Default, PartialEq, Eq)]
260pub struct ComponentSelection {
261    /// Component ids to include when present and dirty.
262    pub component_ids: Vec<ComponentId>,
263}
264
265/// Frame builder statistics.
266#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
267pub struct ReplicationFrameBuildStats {
268    /// Entity handles selected by the replication plan.
269    pub planned_entities: usize,
270    /// Entity deltas materialized into the frame.
271    pub encoded_entities: usize,
272    /// Component deltas materialized into the frame.
273    pub encoded_components: usize,
274    /// Entity deltas skipped by builder limits.
275    pub skipped_entities_by_limit: usize,
276    /// Entity deltas skipped because the concrete frame byte budget filled.
277    pub skipped_entities_by_frame_bytes: usize,
278    /// Component deltas skipped by builder limits.
279    pub skipped_components_by_limit: usize,
280    /// Component payloads skipped because they exceed byte limits.
281    pub skipped_components_by_size: usize,
282}
283
284/// Result of building a replication frame.
285#[derive(Clone, Debug, PartialEq, Eq)]
286pub struct ReplicationFrameBuild {
287    /// Built frame.
288    pub frame: ReplicationFrame,
289    /// Build statistics.
290    pub stats: ReplicationFrameBuildStats,
291}
292
293/// Builds concrete replication frames from a core replication plan.
294#[derive(Clone, Copy, Debug, Default)]
295pub struct ReplicationFrameBuilder {
296    /// Builder limits.
297    pub limits: ReplicationFrameLimits,
298}
299
300impl ReplicationFrameBuilder {
301    const BINARY_HEADER_BYTES: usize = 1 + 8 + 8 + 4 + 4 + 4;
302    const BINARY_ENTITY_METADATA_BYTES: usize = 8 + 8 + 2;
303    const BINARY_COMPONENT_METADATA_BYTES: usize = 2 + 8 + 1 + 4;
304
305    /// Creates a frame builder with explicit limits.
306    pub const fn new(limits: ReplicationFrameLimits) -> Self {
307        Self { limits }
308    }
309
310    /// Returns a bounded initial byte-capacity hint for direct binary encoding.
311    ///
312    /// The planner estimate supplements fixed wire metadata and is clamped by
313    /// the active entity, component, and component-byte limits. The result is a
314    /// hint rather than an encoded-size guarantee because dirty/missing
315    /// components are resolved during encoding.
316    pub fn binary_capacity_hint(
317        &self,
318        plan: &ReplicationPlan,
319        selection: &ComponentSelection,
320    ) -> usize {
321        let entities = plan.entities.len().min(self.limits.max_entity_deltas);
322        let components = selection
323            .component_ids
324            .len()
325            .min(self.limits.max_components_per_entity);
326        let fixed_bytes =
327            Self::BINARY_HEADER_BYTES.saturating_add(entities.saturating_mul(
328                Self::BINARY_ENTITY_METADATA_BYTES.saturating_add(
329                    components.saturating_mul(Self::BINARY_COMPONENT_METADATA_BYTES),
330                ),
331            ));
332        let maximum_payload_bytes = entities
333            .saturating_mul(components)
334            .saturating_mul(self.limits.max_component_bytes);
335        fixed_bytes.saturating_add(plan.stats.estimated_bytes.min(maximum_payload_bytes))
336    }
337
338    /// Returns a dense-dirty-data initial capacity hint from at most four
339    /// uniformly distributed entities in `plan`.
340    ///
341    /// Sampling avoids a full metadata prepass. If any sample has no encodable
342    /// dirty component, this conservatively returns zero and lets the output
343    /// buffer grow normally. Nonzero estimates remain bounded by active limits.
344    pub fn sampled_binary_capacity_hint(
345        &self,
346        station: &Station,
347        plan: &ReplicationPlan,
348        components: &ComponentStore,
349        selection: &ComponentSelection,
350    ) -> usize {
351        const MAX_SAMPLES: usize = 4;
352        let candidates = plan.entities.len();
353        let samples = candidates.min(MAX_SAMPLES);
354        if samples == 0 {
355            return 0;
356        }
357        let denominator = samples.saturating_mul(2);
358        let mut sampled_bytes = 0_usize;
359        for sample in 0..samples {
360            let numerator = sample.saturating_mul(2).saturating_add(1);
361            let index = numerator.saturating_mul(candidates) / denominator;
362            let entity_bytes = self.binary_entity_bytes(
363                station,
364                plan.entities[index.min(candidates - 1)],
365                components,
366                selection,
367            );
368            if entity_bytes == 0 {
369                return 0;
370            }
371            sampled_bytes = sampled_bytes.saturating_add(entity_bytes);
372        }
373        let estimated_entity_bytes = sampled_bytes.saturating_mul(candidates).div_ceil(samples);
374        let maximum_entities = candidates.min(self.limits.max_entity_deltas);
375        let maximum_components = selection
376            .component_ids
377            .len()
378            .min(self.limits.max_components_per_entity);
379        let maximum_entity_bytes = Self::BINARY_ENTITY_METADATA_BYTES.saturating_add(
380            maximum_components.saturating_mul(
381                Self::BINARY_COMPONENT_METADATA_BYTES
382                    .saturating_add(self.limits.max_component_bytes),
383            ),
384        );
385        Self::BINARY_HEADER_BYTES.saturating_add(
386            estimated_entity_bytes.min(maximum_entities.saturating_mul(maximum_entity_bytes)),
387        )
388    }
389
390    fn binary_entity_bytes(
391        &self,
392        station: &Station,
393        handle: EntityHandle,
394        components: &ComponentStore,
395        selection: &ComponentSelection,
396    ) -> usize {
397        if station.get(handle).is_none() {
398            return 0;
399        }
400        let mut bytes = Self::BINARY_ENTITY_METADATA_BYTES;
401        let mut encoded_components = 0_usize;
402        for component_id in &selection.component_ids {
403            if encoded_components >= self.limits.max_components_per_entity {
404                break;
405            }
406            let Some(blob) = components.get_blob(*component_id, handle) else {
407                continue;
408            };
409            if !blob.dirty || blob.bytes.len() > self.limits.max_component_bytes {
410                continue;
411            }
412            bytes = bytes
413                .saturating_add(Self::BINARY_COMPONENT_METADATA_BYTES)
414                .saturating_add(blob.bytes.len());
415            encoded_components += 1;
416        }
417        if encoded_components == 0 { 0 } else { bytes }
418    }
419
420    /// Builds a frame from a station plan and component store.
421    pub fn build(
422        &self,
423        client_id: ClientId,
424        server_tick: Tick,
425        station: &Station,
426        plan: &ReplicationPlan,
427        components: &ComponentStore,
428        selection: &ComponentSelection,
429    ) -> ReplicationFrameBuild {
430        let mut stats = ReplicationFrameBuildStats {
431            planned_entities: plan.entities.len(),
432            ..ReplicationFrameBuildStats::default()
433        };
434        let mut entity_deltas =
435            Vec::with_capacity(plan.entities.len().min(self.limits.max_entity_deltas));
436        let mut estimated_payload_bytes = 0_usize;
437
438        for handle in &plan.entities {
439            if entity_deltas.len() >= self.limits.max_entity_deltas {
440                stats.skipped_entities_by_limit += 1;
441                continue;
442            }
443            let Some(entity) = station.get(*handle) else {
444                continue;
445            };
446
447            let mut component_deltas = Vec::new();
448            for component_id in &selection.component_ids {
449                if component_deltas.len() >= self.limits.max_components_per_entity {
450                    stats.skipped_components_by_limit += 1;
451                    continue;
452                }
453                let Some(blob) = components.get_blob(*component_id, *handle) else {
454                    continue;
455                };
456                if !blob.dirty {
457                    continue;
458                }
459                if blob.bytes.len() > self.limits.max_component_bytes {
460                    stats.skipped_components_by_size += 1;
461                    continue;
462                }
463                estimated_payload_bytes = estimated_payload_bytes
464                    .saturating_add(2 + 8 + 1 + 4)
465                    .saturating_add(blob.bytes.len());
466                component_deltas.push(ComponentDelta {
467                    component_id: *component_id,
468                    version: blob.version,
469                    flags: 0,
470                    bytes: blob.bytes.clone(),
471                });
472            }
473
474            if component_deltas.is_empty() {
475                continue;
476            }
477
478            stats.encoded_components += component_deltas.len();
479            estimated_payload_bytes = estimated_payload_bytes.saturating_add(8 + 8 + 2);
480            entity_deltas.push(EntityDelta {
481                entity_id: entity.id,
482                owner_epoch: entity.role.owner_epoch(),
483                components: component_deltas,
484            });
485        }
486
487        stats.encoded_entities = entity_deltas.len();
488        ReplicationFrameBuild {
489            frame: ReplicationFrame {
490                client_id,
491                server_tick,
492                entity_count: u32::try_from(plan.entities.len()).unwrap_or(u32::MAX),
493                estimated_payload_bytes: u32::try_from(estimated_payload_bytes).unwrap_or(u32::MAX),
494                entities: entity_deltas,
495            },
496            stats,
497        }
498    }
499
500    /// Builds and appends one binary replication frame directly into `out`.
501    ///
502    /// This preserves the same limits, dirty-component filtering, statistics,
503    /// and wire shape as [`Self::build`] followed by [`BinaryFrameEncoder`],
504    /// without allocating an intermediate entity/component delta tree or
505    /// cloning component payloads. Existing bytes in `out` are retained.
506    #[allow(clippy::too_many_arguments)]
507    pub fn encode_binary_into(
508        &self,
509        client_id: ClientId,
510        server_tick: Tick,
511        station: &Station,
512        plan: &ReplicationPlan,
513        components: &ComponentStore,
514        selection: &ComponentSelection,
515        out: &mut Vec<u8>,
516    ) -> Result<ReplicationFrameBuildStats, BinaryEncodeError> {
517        self.encode_binary_bounded_into(
518            client_id,
519            server_tick,
520            station,
521            plan,
522            components,
523            selection,
524            usize::MAX,
525            out,
526        )
527    }
528
529    /// Builds and appends one replication frame under a concrete wire-byte budget.
530    ///
531    /// The budget applies only to bytes appended by this frame. If the next
532    /// entity would exceed it, that entity is rolled back and planning stops.
533    #[allow(clippy::too_many_arguments)]
534    pub fn encode_binary_bounded_into(
535        &self,
536        client_id: ClientId,
537        server_tick: Tick,
538        station: &Station,
539        plan: &ReplicationPlan,
540        components: &ComponentStore,
541        selection: &ComponentSelection,
542        max_frame_bytes: usize,
543        out: &mut Vec<u8>,
544    ) -> Result<ReplicationFrameBuildStats, BinaryEncodeError> {
545        if max_frame_bytes < Self::BINARY_HEADER_BYTES {
546            return Err(BinaryEncodeError::FrameBudgetTooSmall {
547                budget: max_frame_bytes,
548                required: Self::BINARY_HEADER_BYTES,
549            });
550        }
551        let frame_start = out.len();
552        let mut stats = ReplicationFrameBuildStats {
553            planned_entities: plan.entities.len(),
554            ..ReplicationFrameBuildStats::default()
555        };
556        let mut estimated_payload_bytes = 0_usize;
557
558        out.push(FrameKind::Replication as u8);
559        out.extend_from_slice(&client_id.get().to_le_bytes());
560        out.extend_from_slice(&server_tick.get().to_le_bytes());
561        out.extend_from_slice(
562            &u32::try_from(plan.entities.len())
563                .unwrap_or(u32::MAX)
564                .to_le_bytes(),
565        );
566        let estimated_payload_offset = out.len();
567        out.extend_from_slice(&0_u32.to_le_bytes());
568        let entity_count_offset = out.len();
569        out.extend_from_slice(&0_u32.to_le_bytes());
570
571        for handle in &plan.entities {
572            if stats.encoded_entities >= self.limits.max_entity_deltas {
573                stats.skipped_entities_by_limit += 1;
574                continue;
575            }
576            let Some(entity) = station.get(*handle) else {
577                continue;
578            };
579
580            let entity_start = out.len();
581            let estimated_before_entity = estimated_payload_bytes;
582            out.extend_from_slice(&entity.id.get().to_le_bytes());
583            out.extend_from_slice(&entity.role.owner_epoch().get().to_le_bytes());
584            let component_count_offset = out.len();
585            out.extend_from_slice(&0_u16.to_le_bytes());
586            let mut component_count = 0_usize;
587
588            for component_id in &selection.component_ids {
589                if component_count >= self.limits.max_components_per_entity {
590                    stats.skipped_components_by_limit += 1;
591                    continue;
592                }
593                let Some(blob) = components.get_blob(*component_id, *handle) else {
594                    continue;
595                };
596                if !blob.dirty {
597                    continue;
598                }
599                if blob.bytes.len() > self.limits.max_component_bytes {
600                    stats.skipped_components_by_size += 1;
601                    continue;
602                }
603
604                out.extend_from_slice(&component_id.get().to_le_bytes());
605                out.extend_from_slice(&blob.version.to_le_bytes());
606                out.push(0);
607                write_bytes("replication.component.bytes", &blob.bytes, out)?;
608                estimated_payload_bytes = estimated_payload_bytes
609                    .saturating_add(2 + 8 + 1 + 4)
610                    .saturating_add(blob.bytes.len());
611                component_count += 1;
612            }
613
614            if component_count == 0 {
615                out.truncate(entity_start);
616                continue;
617            }
618
619            if out.len().saturating_sub(frame_start) > max_frame_bytes {
620                out.truncate(entity_start);
621                estimated_payload_bytes = estimated_before_entity;
622                stats.skipped_entities_by_frame_bytes =
623                    stats.skipped_entities_by_frame_bytes.saturating_add(1);
624                break;
625            }
626
627            let component_count =
628                u16::try_from(component_count).map_err(|_| BinaryEncodeError::TooManyItems {
629                    field: "replication.entity.components",
630                    actual: component_count,
631                })?;
632            out[component_count_offset..component_count_offset + 2]
633                .copy_from_slice(&component_count.to_le_bytes());
634            stats.encoded_components += usize::from(component_count);
635            stats.encoded_entities += 1;
636            estimated_payload_bytes = estimated_payload_bytes.saturating_add(8 + 8 + 2);
637        }
638
639        let encoded_entities =
640            u32::try_from(stats.encoded_entities).map_err(|_| BinaryEncodeError::TooManyItems {
641                field: "replication.entities",
642                actual: stats.encoded_entities,
643            })?;
644        out[estimated_payload_offset..estimated_payload_offset + 4].copy_from_slice(
645            &u32::try_from(estimated_payload_bytes)
646                .unwrap_or(u32::MAX)
647                .to_le_bytes(),
648        );
649        out[entity_count_offset..entity_count_offset + 4]
650            .copy_from_slice(&encoded_entities.to_le_bytes());
651        Ok(stats)
652    }
653}
654
655/// Command acknowledgement frame.
656#[derive(Clone, Debug, PartialEq, Eq)]
657pub struct CommandAckFrame {
658    /// Target client.
659    pub client_id: ClientId,
660    /// Acknowledged command.
661    pub command_id: CommandId,
662    /// Server tick at acknowledgement.
663    pub server_tick: Tick,
664    /// Whether the command was accepted by the runtime pipeline.
665    pub accepted: bool,
666    /// Game/runtime reject reason code.
667    pub reason_code: u16,
668}
669
670/// Client command ingress frame.
671///
672/// The server stamps `received_at` when converting this into a
673/// `CommandEnvelope`; game validation and anti-cheat checks remain outside the
674/// wire codec.
675#[derive(Clone, Debug, PartialEq, Eq)]
676pub struct CommandFrame {
677    /// Client that submitted the command.
678    pub client_id: ClientId,
679    /// Command id used for replay and audit.
680    pub command_id: CommandId,
681    /// Entity the command intends to control.
682    pub entity_id: EntityId,
683    /// Client-side sequence number.
684    pub sequence: u64,
685    /// Game-defined command kind.
686    pub kind: u32,
687    /// Command priority.
688    pub priority: CommandPriority,
689    /// Opaque payload owned by the embedding game.
690    pub payload: Vec<u8>,
691}
692
693impl CommandFrame {
694    /// Converts an ingress frame into a runtime command envelope.
695    pub fn into_envelope(self, received_at: Tick) -> CommandEnvelope {
696        CommandEnvelope {
697            id: self.command_id,
698            client_id: self.client_id,
699            entity_id: self.entity_id,
700            sequence: self.sequence,
701            received_at,
702            kind: self.kind,
703            priority: self.priority,
704            payload: self.payload,
705        }
706    }
707
708    /// Converts a command envelope into a wire frame, dropping server-only tick
709    /// metadata.
710    pub fn from_envelope(envelope: &CommandEnvelope) -> Self {
711        Self {
712            client_id: envelope.client_id,
713            command_id: envelope.id,
714            entity_id: envelope.entity_id,
715            sequence: envelope.sequence,
716            kind: envelope.kind,
717            priority: envelope.priority,
718            payload: envelope.payload.clone(),
719        }
720    }
721}
722
723/// Internal gateway-to-station command dispatch frame.
724///
725/// Unlike `CommandFrame`, this preserves the server `received_at` tick stamped
726/// by the gateway pipeline before the command is forwarded to a station node.
727#[derive(Clone, Debug, PartialEq, Eq)]
728pub struct CommandDispatchFrame {
729    /// Target station selected by gateway/deployment routing.
730    pub station_id: StationId,
731    /// Client that submitted the command.
732    pub client_id: ClientId,
733    /// Command id used for replay and audit.
734    pub command_id: CommandId,
735    /// Entity the command intends to control.
736    pub entity_id: EntityId,
737    /// Client-side sequence number.
738    pub sequence: u64,
739    /// Server tick observed when the command entered `SectorSync`.
740    pub received_at: Tick,
741    /// Game-defined command kind.
742    pub kind: u32,
743    /// Command priority.
744    pub priority: CommandPriority,
745    /// Opaque payload owned by the embedding game.
746    pub payload: Vec<u8>,
747}
748
749impl CommandDispatchFrame {
750    /// Converts a stamped command envelope into an internal dispatch frame.
751    pub fn from_envelope(station_id: StationId, envelope: &CommandEnvelope) -> Self {
752        Self {
753            station_id,
754            client_id: envelope.client_id,
755            command_id: envelope.id,
756            entity_id: envelope.entity_id,
757            sequence: envelope.sequence,
758            received_at: envelope.received_at,
759            kind: envelope.kind,
760            priority: envelope.priority,
761            payload: envelope.payload.clone(),
762        }
763    }
764
765    /// Converts an internal dispatch frame back into a command envelope.
766    pub fn into_envelope(self) -> CommandEnvelope {
767        CommandEnvelope {
768            id: self.command_id,
769            client_id: self.client_id,
770            entity_id: self.entity_id,
771            sequence: self.sequence,
772            received_at: self.received_at,
773            kind: self.kind,
774            priority: self.priority,
775            payload: self.payload,
776        }
777    }
778}
779
780/// Cross-station event frame.
781#[derive(Clone, Debug, PartialEq, Eq)]
782pub struct StationEventFrame {
783    /// Idempotency key.
784    pub event_id: EventId,
785    /// Source station.
786    pub source_station: StationId,
787    /// Target station.
788    pub target_station: StationId,
789    /// Tick observed at source.
790    pub source_tick: Tick,
791    /// Tick at which target should apply the event.
792    pub target_tick: Tick,
793    /// Priority class.
794    pub priority: EventPriority,
795    /// Event payload kind.
796    pub kind: EventKind,
797}
798
799impl StationEventFrame {
800    /// Converts a runtime station event into a wire frame.
801    pub fn from_event(event: &StationEvent) -> Self {
802        Self {
803            event_id: event.id,
804            source_station: event.source,
805            target_station: event.target,
806            source_tick: event.source_tick,
807            target_tick: event.target_tick,
808            priority: event.priority,
809            kind: event.kind.clone(),
810        }
811    }
812
813    /// Converts a wire frame into a runtime station event.
814    pub fn into_event(self) -> StationEvent {
815        StationEvent {
816            id: self.event_id,
817            source: self.source_station,
818            target: self.target_station,
819            source_tick: self.source_tick,
820            target_tick: self.target_tick,
821            priority: self.priority,
822            kind: self.kind,
823        }
824    }
825}
826
827/// Runtime barrier notification frame.
828#[derive(Clone, Debug, PartialEq, Eq)]
829pub struct BarrierFrame {
830    /// Target client.
831    pub client_id: ClientId,
832    /// Barrier id.
833    pub barrier_id: BarrierId,
834    /// Server tick associated with this barrier state.
835    pub server_tick: Tick,
836    /// Current barrier state.
837    pub state: BarrierState,
838}
839
840/// Decoded runtime frame.
841#[derive(Clone, Debug, PartialEq, Eq)]
842pub enum RuntimeFrame {
843    /// Replication update.
844    Replication(ReplicationFrame),
845    /// Client command ingress.
846    Command(CommandFrame),
847    /// Gateway-to-station command dispatch.
848    CommandDispatch(CommandDispatchFrame),
849    /// Command acknowledgement.
850    CommandAck(CommandAckFrame),
851    /// Barrier notification.
852    Barrier(BarrierFrame),
853    /// Cross-station event.
854    StationEvent(StationEventFrame),
855}
856
857/// Encodes frames into bytes.
858pub trait FrameEncoder {
859    /// Encoder error type.
860    type Error;
861
862    /// Encodes a replication frame into `out`.
863    fn encode_replication(
864        &mut self,
865        frame: &ReplicationFrame,
866        out: &mut Vec<u8>,
867    ) -> Result<(), Self::Error>;
868
869    /// Encodes a command acknowledgement frame into `out`.
870    fn encode_command_ack(
871        &mut self,
872        frame: &CommandAckFrame,
873        out: &mut Vec<u8>,
874    ) -> Result<(), Self::Error>;
875
876    /// Encodes a client command frame into `out`.
877    fn encode_command(
878        &mut self,
879        frame: &CommandFrame,
880        out: &mut Vec<u8>,
881    ) -> Result<(), Self::Error>;
882
883    /// Encodes an internal command dispatch frame into `out`.
884    fn encode_command_dispatch(
885        &mut self,
886        frame: &CommandDispatchFrame,
887        out: &mut Vec<u8>,
888    ) -> Result<(), Self::Error>;
889
890    /// Encodes a cross-station event frame into `out`.
891    fn encode_station_event(
892        &mut self,
893        frame: &StationEventFrame,
894        out: &mut Vec<u8>,
895    ) -> Result<(), Self::Error>;
896
897    /// Encodes a barrier frame into `out`.
898    fn encode_barrier(
899        &mut self,
900        frame: &BarrierFrame,
901        out: &mut Vec<u8>,
902    ) -> Result<(), Self::Error>;
903}
904
905/// Decodes frames from bytes.
906pub trait FrameDecoder {
907    /// Decoder error type.
908    type Error;
909
910    /// Decodes one runtime frame.
911    fn decode(&mut self, input: &[u8]) -> Result<RuntimeFrame, Self::Error>;
912}
913
914/// Binary decode error.
915#[derive(Clone, Debug, PartialEq, Eq)]
916pub enum BinaryDecodeError {
917    /// Input is empty.
918    Empty,
919    /// Unknown frame kind byte.
920    UnknownFrameKind(u8),
921    /// Frame ended before all fields were available.
922    Truncated {
923        /// Required bytes.
924        needed: usize,
925        /// Available bytes.
926        available: usize,
927    },
928    /// Barrier state byte is invalid.
929    InvalidBarrierState(u8),
930    /// Command priority byte is invalid.
931    InvalidCommandPriority(u8),
932    /// Event priority byte is invalid.
933    InvalidEventPriority(u8),
934    /// Event kind byte is invalid.
935    InvalidEventKind(u8),
936    /// Trailing bytes were present after a complete frame.
937    TrailingBytes(usize),
938}
939
940impl core::fmt::Display for BinaryDecodeError {
941    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
942        match self {
943            Self::Empty => f.write_str("empty frame"),
944            Self::UnknownFrameKind(kind) => write!(f, "unknown frame kind {kind}"),
945            Self::Truncated { needed, available } => {
946                write!(f, "truncated frame: needed {needed}, available {available}")
947            }
948            Self::InvalidBarrierState(state) => write!(f, "invalid barrier state {state}"),
949            Self::InvalidCommandPriority(priority) => {
950                write!(f, "invalid command priority {priority}")
951            }
952            Self::InvalidEventPriority(priority) => write!(f, "invalid event priority {priority}"),
953            Self::InvalidEventKind(kind) => write!(f, "invalid event kind {kind}"),
954            Self::TrailingBytes(bytes) => write!(f, "frame has {bytes} trailing bytes"),
955        }
956    }
957}
958
959impl std::error::Error for BinaryDecodeError {}
960
961/// Error produced by borrowed replication-frame decoding.
962#[derive(Clone, Debug, PartialEq, Eq)]
963pub enum ReplicationFrameRefDecodeError {
964    /// Binary framing or field validation failed.
965    Binary(BinaryDecodeError),
966    /// The input was a valid known frame kind other than replication.
967    UnexpectedFrameKind(FrameKind),
968}
969
970impl core::fmt::Display for ReplicationFrameRefDecodeError {
971    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
972        match self {
973            Self::Binary(error) => error.fmt(f),
974            Self::UnexpectedFrameKind(actual) => {
975                write!(f, "expected Replication frame, found {actual:?}")
976            }
977        }
978    }
979}
980
981impl std::error::Error for ReplicationFrameRefDecodeError {
982    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
983        match self {
984            Self::Binary(error) => Some(error),
985            Self::UnexpectedFrameKind(_) => None,
986        }
987    }
988}
989
990impl From<BinaryDecodeError> for ReplicationFrameRefDecodeError {
991    fn from(value: BinaryDecodeError) -> Self {
992        Self::Binary(value)
993    }
994}
995
996/// Binary encode error.
997#[derive(Clone, Debug, PartialEq, Eq)]
998pub enum BinaryEncodeError {
999    /// A concrete frame budget could not fit the fixed wire header.
1000    FrameBudgetTooSmall {
1001        /// Configured frame budget.
1002        budget: usize,
1003        /// Minimum fixed header bytes required.
1004        required: usize,
1005    },
1006    /// A list length exceeded `u32::MAX`.
1007    TooManyItems {
1008        /// Field being encoded.
1009        field: &'static str,
1010        /// Actual item count.
1011        actual: usize,
1012    },
1013    /// A byte payload exceeded `u32::MAX`.
1014    PayloadTooLarge {
1015        /// Field being encoded.
1016        field: &'static str,
1017        /// Actual byte count.
1018        actual: usize,
1019    },
1020}
1021
1022impl core::fmt::Display for BinaryEncodeError {
1023    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1024        match self {
1025            Self::FrameBudgetTooSmall { budget, required } => write!(
1026                f,
1027                "replication frame budget {budget} bytes is smaller than {required}-byte header"
1028            ),
1029            Self::TooManyItems { field, actual } => {
1030                write!(f, "{field} has too many items: {actual}")
1031            }
1032            Self::PayloadTooLarge { field, actual } => {
1033                write!(f, "{field} payload is too large: {actual} bytes")
1034            }
1035        }
1036    }
1037}
1038
1039impl std::error::Error for BinaryEncodeError {}
1040
1041/// Simple little-endian binary frame decoder.
1042#[derive(Clone, Copy, Debug, Default)]
1043pub struct BinaryFrameDecoder;
1044
1045impl BinaryFrameDecoder {
1046    /// Decodes and fully validates one replication frame while borrowing all
1047    /// entity/component storage and component payload bytes from `input`.
1048    pub fn decode_replication<'a>(
1049        &mut self,
1050        input: &'a [u8],
1051    ) -> Result<ReplicationFrameRef<'a>, ReplicationFrameRefDecodeError> {
1052        let mut cursor = Cursor::new(input);
1053        let kind_byte = cursor.read_u8()?;
1054        let kind = FrameKind::from_byte(kind_byte)
1055            .ok_or(BinaryDecodeError::UnknownFrameKind(kind_byte))?;
1056        if kind != FrameKind::Replication {
1057            return Err(ReplicationFrameRefDecodeError::UnexpectedFrameKind(kind));
1058        }
1059        let frame = decode_replication_ref(&mut cursor)?;
1060        cursor.finish()?;
1061        Ok(frame)
1062    }
1063}
1064
1065impl FrameDecoder for BinaryFrameDecoder {
1066    type Error = BinaryDecodeError;
1067
1068    fn decode(&mut self, input: &[u8]) -> Result<RuntimeFrame, Self::Error> {
1069        let mut cursor = Cursor::new(input);
1070        let kind_byte = cursor.read_u8()?;
1071        let kind = FrameKind::from_byte(kind_byte)
1072            .ok_or(BinaryDecodeError::UnknownFrameKind(kind_byte))?;
1073        let frame = match kind {
1074            FrameKind::Replication => {
1075                RuntimeFrame::Replication(decode_replication_owned(&mut cursor)?)
1076            }
1077            FrameKind::CommandAck => RuntimeFrame::CommandAck(CommandAckFrame {
1078                client_id: ClientId::new(cursor.read_u64()?),
1079                command_id: CommandId::new(cursor.read_u64()?),
1080                server_tick: Tick::new(cursor.read_u64()?),
1081                accepted: cursor.read_u8()? != 0,
1082                reason_code: cursor.read_u16()?,
1083            }),
1084            FrameKind::Command => RuntimeFrame::Command(CommandFrame {
1085                client_id: ClientId::new(cursor.read_u64()?),
1086                command_id: CommandId::new(cursor.read_u64()?),
1087                entity_id: EntityId::new(cursor.read_u64()?),
1088                sequence: cursor.read_u64()?,
1089                kind: cursor.read_u32()?,
1090                priority: decode_command_priority(cursor.read_u8()?)?,
1091                payload: {
1092                    let byte_len = cursor.read_u32()? as usize;
1093                    cursor.read_bytes(byte_len)?
1094                },
1095            }),
1096            FrameKind::CommandDispatch => RuntimeFrame::CommandDispatch(CommandDispatchFrame {
1097                station_id: StationId::new(cursor.read_u32()?),
1098                client_id: ClientId::new(cursor.read_u64()?),
1099                command_id: CommandId::new(cursor.read_u64()?),
1100                entity_id: EntityId::new(cursor.read_u64()?),
1101                sequence: cursor.read_u64()?,
1102                received_at: Tick::new(cursor.read_u64()?),
1103                kind: cursor.read_u32()?,
1104                priority: decode_command_priority(cursor.read_u8()?)?,
1105                payload: {
1106                    let byte_len = cursor.read_u32()? as usize;
1107                    cursor.read_bytes(byte_len)?
1108                },
1109            }),
1110            FrameKind::Barrier => RuntimeFrame::Barrier(BarrierFrame {
1111                client_id: ClientId::new(cursor.read_u64()?),
1112                barrier_id: BarrierId::new(cursor.read_u64()?),
1113                server_tick: Tick::new(cursor.read_u64()?),
1114                state: decode_barrier_state(cursor.read_u8()?)?,
1115            }),
1116            FrameKind::StationEvent => RuntimeFrame::StationEvent(StationEventFrame {
1117                event_id: EventId::new(cursor.read_u64()?),
1118                source_station: StationId::new(cursor.read_u32()?),
1119                target_station: StationId::new(cursor.read_u32()?),
1120                source_tick: Tick::new(cursor.read_u64()?),
1121                target_tick: Tick::new(cursor.read_u64()?),
1122                priority: decode_event_priority(cursor.read_u8()?)?,
1123                kind: decode_event_kind(&mut cursor)?,
1124            }),
1125        };
1126        cursor.finish()?;
1127        Ok(frame)
1128    }
1129}
1130
1131/// Simple little-endian binary frame encoder.
1132#[derive(Clone, Copy, Debug, Default)]
1133pub struct BinaryFrameEncoder;
1134
1135impl BinaryFrameEncoder {
1136    /// Encodes a stamped command envelope directly as a dispatch frame.
1137    ///
1138    /// This avoids cloning the opaque payload into an intermediate owned
1139    /// [`CommandDispatchFrame`] when the frame is immediately transmitted.
1140    pub fn encode_command_dispatch_envelope(
1141        &mut self,
1142        station_id: StationId,
1143        envelope: &CommandEnvelope,
1144        out: &mut Vec<u8>,
1145    ) -> Result<(), BinaryEncodeError> {
1146        out.push(FrameKind::CommandDispatch as u8);
1147        out.extend_from_slice(&station_id.get().to_le_bytes());
1148        out.extend_from_slice(&envelope.client_id.get().to_le_bytes());
1149        out.extend_from_slice(&envelope.id.get().to_le_bytes());
1150        out.extend_from_slice(&envelope.entity_id.get().to_le_bytes());
1151        out.extend_from_slice(&envelope.sequence.to_le_bytes());
1152        out.extend_from_slice(&envelope.received_at.get().to_le_bytes());
1153        out.extend_from_slice(&envelope.kind.to_le_bytes());
1154        out.push(encode_command_priority(envelope.priority));
1155        write_bytes("command_dispatch.payload", &envelope.payload, out)
1156    }
1157}
1158
1159impl FrameEncoder for BinaryFrameEncoder {
1160    type Error = BinaryEncodeError;
1161
1162    fn encode_replication(
1163        &mut self,
1164        frame: &ReplicationFrame,
1165        out: &mut Vec<u8>,
1166    ) -> Result<(), Self::Error> {
1167        out.push(FrameKind::Replication as u8);
1168        out.extend_from_slice(&frame.client_id.get().to_le_bytes());
1169        out.extend_from_slice(&frame.server_tick.get().to_le_bytes());
1170        out.extend_from_slice(&frame.entity_count.to_le_bytes());
1171        out.extend_from_slice(&frame.estimated_payload_bytes.to_le_bytes());
1172        write_len_u32("replication.entities", frame.entities.len(), out)?;
1173        for entity in &frame.entities {
1174            out.extend_from_slice(&entity.entity_id.get().to_le_bytes());
1175            out.extend_from_slice(&entity.owner_epoch.get().to_le_bytes());
1176            write_len_u16(
1177                "replication.entity.components",
1178                entity.components.len(),
1179                out,
1180            )?;
1181            for component in &entity.components {
1182                out.extend_from_slice(&component.component_id.get().to_le_bytes());
1183                out.extend_from_slice(&component.version.to_le_bytes());
1184                out.push(component.flags);
1185                write_bytes("replication.component.bytes", &component.bytes, out)?;
1186            }
1187        }
1188        Ok(())
1189    }
1190
1191    fn encode_command_ack(
1192        &mut self,
1193        frame: &CommandAckFrame,
1194        out: &mut Vec<u8>,
1195    ) -> Result<(), Self::Error> {
1196        out.push(FrameKind::CommandAck as u8);
1197        out.extend_from_slice(&frame.client_id.get().to_le_bytes());
1198        out.extend_from_slice(&frame.command_id.get().to_le_bytes());
1199        out.extend_from_slice(&frame.server_tick.get().to_le_bytes());
1200        out.push(u8::from(frame.accepted));
1201        out.extend_from_slice(&frame.reason_code.to_le_bytes());
1202        Ok(())
1203    }
1204
1205    fn encode_command(
1206        &mut self,
1207        frame: &CommandFrame,
1208        out: &mut Vec<u8>,
1209    ) -> Result<(), Self::Error> {
1210        out.push(FrameKind::Command as u8);
1211        out.extend_from_slice(&frame.client_id.get().to_le_bytes());
1212        out.extend_from_slice(&frame.command_id.get().to_le_bytes());
1213        out.extend_from_slice(&frame.entity_id.get().to_le_bytes());
1214        out.extend_from_slice(&frame.sequence.to_le_bytes());
1215        out.extend_from_slice(&frame.kind.to_le_bytes());
1216        out.push(encode_command_priority(frame.priority));
1217        write_bytes("command.payload", &frame.payload, out)?;
1218        Ok(())
1219    }
1220
1221    fn encode_command_dispatch(
1222        &mut self,
1223        frame: &CommandDispatchFrame,
1224        out: &mut Vec<u8>,
1225    ) -> Result<(), Self::Error> {
1226        out.push(FrameKind::CommandDispatch as u8);
1227        out.extend_from_slice(&frame.station_id.get().to_le_bytes());
1228        out.extend_from_slice(&frame.client_id.get().to_le_bytes());
1229        out.extend_from_slice(&frame.command_id.get().to_le_bytes());
1230        out.extend_from_slice(&frame.entity_id.get().to_le_bytes());
1231        out.extend_from_slice(&frame.sequence.to_le_bytes());
1232        out.extend_from_slice(&frame.received_at.get().to_le_bytes());
1233        out.extend_from_slice(&frame.kind.to_le_bytes());
1234        out.push(encode_command_priority(frame.priority));
1235        write_bytes("command_dispatch.payload", &frame.payload, out)
1236    }
1237
1238    fn encode_station_event(
1239        &mut self,
1240        frame: &StationEventFrame,
1241        out: &mut Vec<u8>,
1242    ) -> Result<(), Self::Error> {
1243        out.push(FrameKind::StationEvent as u8);
1244        out.extend_from_slice(&frame.event_id.get().to_le_bytes());
1245        out.extend_from_slice(&frame.source_station.get().to_le_bytes());
1246        out.extend_from_slice(&frame.target_station.get().to_le_bytes());
1247        out.extend_from_slice(&frame.source_tick.get().to_le_bytes());
1248        out.extend_from_slice(&frame.target_tick.get().to_le_bytes());
1249        out.push(encode_event_priority(frame.priority));
1250        encode_event_kind(&frame.kind, out);
1251        Ok(())
1252    }
1253
1254    fn encode_barrier(
1255        &mut self,
1256        frame: &BarrierFrame,
1257        out: &mut Vec<u8>,
1258    ) -> Result<(), Self::Error> {
1259        out.push(FrameKind::Barrier as u8);
1260        out.extend_from_slice(&frame.client_id.get().to_le_bytes());
1261        out.extend_from_slice(&frame.barrier_id.get().to_le_bytes());
1262        out.extend_from_slice(&frame.server_tick.get().to_le_bytes());
1263        out.push(encode_barrier_state(frame.state));
1264        Ok(())
1265    }
1266}
1267
1268fn write_len_u16(
1269    field: &'static str,
1270    len: usize,
1271    out: &mut Vec<u8>,
1272) -> Result<(), BinaryEncodeError> {
1273    let len =
1274        u16::try_from(len).map_err(|_| BinaryEncodeError::TooManyItems { field, actual: len })?;
1275    out.extend_from_slice(&len.to_le_bytes());
1276    Ok(())
1277}
1278
1279fn write_len_u32(
1280    field: &'static str,
1281    len: usize,
1282    out: &mut Vec<u8>,
1283) -> Result<(), BinaryEncodeError> {
1284    let len =
1285        u32::try_from(len).map_err(|_| BinaryEncodeError::TooManyItems { field, actual: len })?;
1286    out.extend_from_slice(&len.to_le_bytes());
1287    Ok(())
1288}
1289
1290fn write_bytes(
1291    field: &'static str,
1292    bytes: &[u8],
1293    out: &mut Vec<u8>,
1294) -> Result<(), BinaryEncodeError> {
1295    let len = u32::try_from(bytes.len()).map_err(|_| BinaryEncodeError::PayloadTooLarge {
1296        field,
1297        actual: bytes.len(),
1298    })?;
1299    out.extend_from_slice(&len.to_le_bytes());
1300    out.extend_from_slice(bytes);
1301    Ok(())
1302}
1303
1304fn encode_barrier_state(state: BarrierState) -> u8 {
1305    match state {
1306        BarrierState::Running => 0,
1307        BarrierState::Requested => 1,
1308        BarrierState::WaitingTickBoundary => 2,
1309        BarrierState::Frozen => 3,
1310        BarrierState::Resuming => 4,
1311    }
1312}
1313
1314fn decode_barrier_state(state: u8) -> Result<BarrierState, BinaryDecodeError> {
1315    match state {
1316        0 => Ok(BarrierState::Running),
1317        1 => Ok(BarrierState::Requested),
1318        2 => Ok(BarrierState::WaitingTickBoundary),
1319        3 => Ok(BarrierState::Frozen),
1320        4 => Ok(BarrierState::Resuming),
1321        _ => Err(BinaryDecodeError::InvalidBarrierState(state)),
1322    }
1323}
1324
1325fn encode_command_priority(priority: CommandPriority) -> u8 {
1326    match priority {
1327        CommandPriority::Normal => 0,
1328        CommandPriority::High => 1,
1329        CommandPriority::Low => 2,
1330    }
1331}
1332
1333fn decode_command_priority(priority: u8) -> Result<CommandPriority, BinaryDecodeError> {
1334    match priority {
1335        0 => Ok(CommandPriority::Normal),
1336        1 => Ok(CommandPriority::High),
1337        2 => Ok(CommandPriority::Low),
1338        _ => Err(BinaryDecodeError::InvalidCommandPriority(priority)),
1339    }
1340}
1341
1342fn encode_event_priority(priority: EventPriority) -> u8 {
1343    match priority {
1344        EventPriority::Critical => 0,
1345        EventPriority::Important => 1,
1346        EventPriority::BestEffort => 2,
1347    }
1348}
1349
1350fn decode_event_priority(priority: u8) -> Result<EventPriority, BinaryDecodeError> {
1351    match priority {
1352        0 => Ok(EventPriority::Critical),
1353        1 => Ok(EventPriority::Important),
1354        2 => Ok(EventPriority::BestEffort),
1355        _ => Err(BinaryDecodeError::InvalidEventPriority(priority)),
1356    }
1357}
1358
1359fn encode_event_kind(kind: &EventKind, out: &mut Vec<u8>) {
1360    match kind {
1361        EventKind::Custom(kind) => {
1362            out.push(0);
1363            out.extend_from_slice(&kind.to_le_bytes());
1364        }
1365        EventKind::HandoffPrepare { entity_id } => {
1366            out.push(1);
1367            out.extend_from_slice(&entity_id.get().to_le_bytes());
1368        }
1369        EventKind::HandoffCommit {
1370            entity_id,
1371            owner_epoch,
1372        } => {
1373            out.push(2);
1374            out.extend_from_slice(&entity_id.get().to_le_bytes());
1375            out.extend_from_slice(&owner_epoch.get().to_le_bytes());
1376        }
1377    }
1378}
1379
1380fn decode_event_kind(cursor: &mut Cursor<'_>) -> Result<EventKind, BinaryDecodeError> {
1381    match cursor.read_u8()? {
1382        0 => Ok(EventKind::Custom(cursor.read_u32()?)),
1383        1 => Ok(EventKind::HandoffPrepare {
1384            entity_id: EntityId::new(cursor.read_u64()?),
1385        }),
1386        2 => Ok(EventKind::HandoffCommit {
1387            entity_id: EntityId::new(cursor.read_u64()?),
1388            owner_epoch: OwnerEpoch::new(cursor.read_u64()?),
1389        }),
1390        kind => Err(BinaryDecodeError::InvalidEventKind(kind)),
1391    }
1392}
1393
1394fn decode_replication_ref<'a>(
1395    cursor: &mut Cursor<'a>,
1396) -> Result<ReplicationFrameRef<'a>, BinaryDecodeError> {
1397    let client_id = ClientId::new(cursor.read_u64()?);
1398    let server_tick = Tick::new(cursor.read_u64()?);
1399    let entity_count = cursor.read_u32()?;
1400    let estimated_payload_bytes = cursor.read_u32()?;
1401    let encoded_entity_count = cursor.read_u32()? as usize;
1402    let entities_start = cursor.offset;
1403    for _ in 0..encoded_entity_count {
1404        decode_entity_ref(cursor)?;
1405    }
1406    let encoded_entities = &cursor.input[entities_start..cursor.offset];
1407    Ok(ReplicationFrameRef {
1408        client_id,
1409        server_tick,
1410        entity_count,
1411        estimated_payload_bytes,
1412        encoded_entities,
1413        encoded_entity_count,
1414    })
1415}
1416
1417fn decode_replication_owned(
1418    cursor: &mut Cursor<'_>,
1419) -> Result<ReplicationFrame, BinaryDecodeError> {
1420    let client_id = ClientId::new(cursor.read_u64()?);
1421    let server_tick = Tick::new(cursor.read_u64()?);
1422    let entity_count = cursor.read_u32()?;
1423    let estimated_payload_bytes = cursor.read_u32()?;
1424    let encoded_entity_count = cursor.read_u32()? as usize;
1425    let mut entities = Vec::with_capacity(encoded_entity_count);
1426    for _ in 0..encoded_entity_count {
1427        let entity_id = EntityId::new(cursor.read_u64()?);
1428        let owner_epoch = OwnerEpoch::new(cursor.read_u64()?);
1429        let encoded_component_count = cursor.read_u16()? as usize;
1430        let mut components = Vec::with_capacity(encoded_component_count);
1431        for _ in 0..encoded_component_count {
1432            components.push(decode_component_ref(cursor)?.to_owned());
1433        }
1434        entities.push(EntityDelta {
1435            entity_id,
1436            owner_epoch,
1437            components,
1438        });
1439    }
1440    Ok(ReplicationFrame {
1441        client_id,
1442        server_tick,
1443        entity_count,
1444        estimated_payload_bytes,
1445        entities,
1446    })
1447}
1448
1449fn decode_entity_ref<'a>(cursor: &mut Cursor<'a>) -> Result<EntityDeltaRef<'a>, BinaryDecodeError> {
1450    let entity_id = EntityId::new(cursor.read_u64()?);
1451    let owner_epoch = OwnerEpoch::new(cursor.read_u64()?);
1452    let encoded_component_count = cursor.read_u16()? as usize;
1453    let components_start = cursor.offset;
1454    for _ in 0..encoded_component_count {
1455        decode_component_ref(cursor)?;
1456    }
1457    let encoded_components = &cursor.input[components_start..cursor.offset];
1458    Ok(EntityDeltaRef {
1459        entity_id,
1460        owner_epoch,
1461        encoded_components,
1462        encoded_component_count,
1463    })
1464}
1465
1466fn decode_component_ref<'a>(
1467    cursor: &mut Cursor<'a>,
1468) -> Result<ComponentDeltaRef<'a>, BinaryDecodeError> {
1469    let component_id = ComponentId::new(cursor.read_u16()?);
1470    let version = cursor.read_u64()?;
1471    let flags = cursor.read_u8()?;
1472    let byte_len = cursor.read_u32()? as usize;
1473    let bytes = cursor.read_slice(byte_len)?;
1474    Ok(ComponentDeltaRef {
1475        component_id,
1476        version,
1477        flags,
1478        bytes,
1479    })
1480}
1481
1482#[derive(Clone, Debug)]
1483struct Cursor<'a> {
1484    input: &'a [u8],
1485    offset: usize,
1486}
1487
1488impl<'a> Cursor<'a> {
1489    fn new(input: &'a [u8]) -> Self {
1490        Self { input, offset: 0 }
1491    }
1492
1493    fn read_u8(&mut self) -> Result<u8, BinaryDecodeError> {
1494        self.require(1)?;
1495        let value = self.input[self.offset];
1496        self.offset += 1;
1497        Ok(value)
1498    }
1499
1500    fn read_u16(&mut self) -> Result<u16, BinaryDecodeError> {
1501        let bytes = self.read_array::<2>()?;
1502        Ok(u16::from_le_bytes(bytes))
1503    }
1504
1505    fn read_u32(&mut self) -> Result<u32, BinaryDecodeError> {
1506        let bytes = self.read_array::<4>()?;
1507        Ok(u32::from_le_bytes(bytes))
1508    }
1509
1510    fn read_u64(&mut self) -> Result<u64, BinaryDecodeError> {
1511        let bytes = self.read_array::<8>()?;
1512        Ok(u64::from_le_bytes(bytes))
1513    }
1514
1515    fn read_array<const N: usize>(&mut self) -> Result<[u8; N], BinaryDecodeError> {
1516        self.require(N)?;
1517        let mut out = [0_u8; N];
1518        out.copy_from_slice(&self.input[self.offset..self.offset + N]);
1519        self.offset += N;
1520        Ok(out)
1521    }
1522
1523    fn read_bytes(&mut self, len: usize) -> Result<Vec<u8>, BinaryDecodeError> {
1524        Ok(self.read_slice(len)?.to_vec())
1525    }
1526
1527    fn read_slice(&mut self, len: usize) -> Result<&'a [u8], BinaryDecodeError> {
1528        self.require(len)?;
1529        let bytes = &self.input[self.offset..self.offset + len];
1530        self.offset += len;
1531        Ok(bytes)
1532    }
1533
1534    fn require(&self, count: usize) -> Result<(), BinaryDecodeError> {
1535        let needed = self.offset.saturating_add(count);
1536        if needed > self.input.len() {
1537            Err(BinaryDecodeError::Truncated {
1538                needed,
1539                available: self.input.len(),
1540            })
1541        } else {
1542            Ok(())
1543        }
1544    }
1545
1546    fn finish(&self) -> Result<(), BinaryDecodeError> {
1547        if self.offset == self.input.len() {
1548            Ok(())
1549        } else {
1550            Err(BinaryDecodeError::TrailingBytes(
1551                self.input.len().saturating_sub(self.offset),
1552            ))
1553        }
1554    }
1555}
1556
1557#[cfg(test)]
1558mod tests {
1559    use super::*;
1560
1561    #[test]
1562    fn binary_codec_roundtrips_replication_frame() {
1563        let frame = ReplicationFrame {
1564            client_id: ClientId::new(9),
1565            server_tick: Tick::new(42),
1566            entity_count: 17,
1567            estimated_payload_bytes: 544,
1568            entities: vec![EntityDelta {
1569                entity_id: EntityId::new(100),
1570                owner_epoch: OwnerEpoch::new(2),
1571                components: vec![
1572                    ComponentDelta {
1573                        component_id: ComponentId::new(1),
1574                        version: 9,
1575                        flags: 0,
1576                        bytes: vec![1, 2, 3, 4],
1577                    },
1578                    ComponentDelta {
1579                        component_id: ComponentId::new(2),
1580                        version: 10,
1581                        flags: 1,
1582                        bytes: vec![5, 6],
1583                    },
1584                ],
1585            }],
1586        };
1587        let mut encoder = BinaryFrameEncoder;
1588        let mut bytes = Vec::new();
1589        encoder
1590            .encode_replication(&frame, &mut bytes)
1591            .expect("encoder is infallible");
1592
1593        let borrowed = BinaryFrameDecoder
1594            .decode_replication(&bytes)
1595            .expect("borrowed decode should work");
1596        assert_eq!(borrowed.client_id, frame.client_id);
1597        assert_eq!(borrowed.server_tick, frame.server_tick);
1598        assert_eq!(borrowed.encoded_entity_count(), 1);
1599        let mut entities = borrowed.entities();
1600        assert_eq!(entities.len(), 1);
1601        let entity = entities.next().expect("entity should be borrowed");
1602        assert_eq!(entity.entity_id, frame.entities[0].entity_id);
1603        assert_eq!(entity.encoded_component_count(), 2);
1604        let mut components = entity.components();
1605        assert_eq!(components.len(), 2);
1606        let first = components.next().expect("component should be borrowed");
1607        assert_eq!(first.bytes, frame.entities[0].components[0].bytes);
1608        let input_start = bytes.as_ptr() as usize;
1609        let input_end = input_start.saturating_add(bytes.len());
1610        let payload_start = first.bytes.as_ptr() as usize;
1611        assert!(payload_start >= input_start);
1612        assert!(payload_start.saturating_add(first.bytes.len()) <= input_end);
1613        assert_eq!(borrowed.to_owned(), frame);
1614
1615        let decoded = BinaryFrameDecoder
1616            .decode(&bytes)
1617            .expect("decode should work");
1618        assert_eq!(decoded, RuntimeFrame::Replication(frame));
1619    }
1620
1621    #[test]
1622    fn borrowed_replication_decode_rejects_wrong_kind_truncation_and_trailing_bytes() {
1623        assert_eq!(
1624            BinaryFrameDecoder.decode_replication(&[FrameKind::CommandAck as u8]),
1625            Err(ReplicationFrameRefDecodeError::UnexpectedFrameKind(
1626                FrameKind::CommandAck
1627            ))
1628        );
1629
1630        let frame = ReplicationFrame {
1631            client_id: ClientId::new(9),
1632            server_tick: Tick::new(42),
1633            entity_count: 1,
1634            estimated_payload_bytes: 1,
1635            entities: vec![EntityDelta {
1636                entity_id: EntityId::new(100),
1637                owner_epoch: OwnerEpoch::new(2),
1638                components: vec![ComponentDelta {
1639                    component_id: ComponentId::new(1),
1640                    version: 9,
1641                    flags: 0,
1642                    bytes: vec![1],
1643                }],
1644            }],
1645        };
1646        let mut bytes = Vec::new();
1647        BinaryFrameEncoder
1648            .encode_replication(&frame, &mut bytes)
1649            .expect("frame should encode");
1650        let truncated = &bytes[..bytes.len() - 1];
1651        assert!(matches!(
1652            BinaryFrameDecoder.decode_replication(truncated),
1653            Err(ReplicationFrameRefDecodeError::Binary(
1654                BinaryDecodeError::Truncated { .. }
1655            ))
1656        ));
1657        bytes.push(0xff);
1658        assert_eq!(
1659            BinaryFrameDecoder.decode_replication(&bytes),
1660            Err(ReplicationFrameRefDecodeError::Binary(
1661                BinaryDecodeError::TrailingBytes(1)
1662            ))
1663        );
1664    }
1665
1666    #[test]
1667    fn binary_codec_roundtrips_command_ack_frame() {
1668        let frame = CommandAckFrame {
1669            client_id: ClientId::new(1),
1670            command_id: CommandId::new(2),
1671            server_tick: Tick::new(3),
1672            accepted: false,
1673            reason_code: 7,
1674        };
1675        let mut encoder = BinaryFrameEncoder;
1676        let mut bytes = Vec::new();
1677        encoder
1678            .encode_command_ack(&frame, &mut bytes)
1679            .expect("encoder is infallible");
1680
1681        let decoded = BinaryFrameDecoder
1682            .decode(&bytes)
1683            .expect("decode should work");
1684        assert_eq!(decoded, RuntimeFrame::CommandAck(frame));
1685    }
1686
1687    #[test]
1688    fn binary_codec_roundtrips_command_frame() {
1689        let frame = CommandFrame {
1690            client_id: ClientId::new(1),
1691            command_id: CommandId::new(2),
1692            entity_id: EntityId::new(3),
1693            sequence: 4,
1694            kind: 5,
1695            priority: CommandPriority::High,
1696            payload: vec![9, 8, 7],
1697        };
1698        let mut encoder = BinaryFrameEncoder;
1699        let mut bytes = Vec::new();
1700        encoder
1701            .encode_command(&frame, &mut bytes)
1702            .expect("encoder is infallible");
1703
1704        let decoded = BinaryFrameDecoder
1705            .decode(&bytes)
1706            .expect("decode should work");
1707        assert_eq!(decoded, RuntimeFrame::Command(frame));
1708    }
1709
1710    #[test]
1711    fn binary_codec_roundtrips_command_dispatch_frame() {
1712        let frame = CommandDispatchFrame {
1713            station_id: StationId::new(10),
1714            client_id: ClientId::new(1),
1715            command_id: CommandId::new(2),
1716            entity_id: EntityId::new(3),
1717            sequence: 4,
1718            received_at: Tick::new(99),
1719            kind: 5,
1720            priority: CommandPriority::High,
1721            payload: vec![9, 8, 7],
1722        };
1723        let mut encoder = BinaryFrameEncoder;
1724        let mut bytes = Vec::new();
1725        encoder
1726            .encode_command_dispatch(&frame, &mut bytes)
1727            .expect("encoder is infallible");
1728        let envelope = CommandEnvelope {
1729            id: frame.command_id,
1730            client_id: frame.client_id,
1731            entity_id: frame.entity_id,
1732            sequence: frame.sequence,
1733            received_at: frame.received_at,
1734            kind: frame.kind,
1735            priority: frame.priority,
1736            payload: frame.payload.clone(),
1737        };
1738        let mut direct = Vec::new();
1739        encoder
1740            .encode_command_dispatch_envelope(frame.station_id, &envelope, &mut direct)
1741            .expect("direct encoder is infallible");
1742        assert_eq!(direct, bytes);
1743
1744        let decoded = BinaryFrameDecoder
1745            .decode(&bytes)
1746            .expect("decode should work");
1747        assert_eq!(decoded, RuntimeFrame::CommandDispatch(frame));
1748    }
1749
1750    #[test]
1751    fn command_frame_converts_to_runtime_envelope() {
1752        let frame = CommandFrame {
1753            client_id: ClientId::new(1),
1754            command_id: CommandId::new(2),
1755            entity_id: EntityId::new(3),
1756            sequence: 4,
1757            kind: 5,
1758            priority: CommandPriority::Low,
1759            payload: vec![1, 2, 3],
1760        };
1761
1762        let envelope = frame.clone().into_envelope(Tick::new(99));
1763        assert_eq!(envelope.id, frame.command_id);
1764        assert_eq!(envelope.received_at, Tick::new(99));
1765        assert_eq!(CommandFrame::from_envelope(&envelope), frame);
1766    }
1767
1768    #[test]
1769    fn command_dispatch_frame_preserves_stamped_envelope_tick() {
1770        let envelope = CommandEnvelope {
1771            id: CommandId::new(2),
1772            client_id: ClientId::new(1),
1773            entity_id: EntityId::new(3),
1774            sequence: 4,
1775            received_at: Tick::new(99),
1776            kind: 5,
1777            priority: CommandPriority::Low,
1778            payload: vec![1, 2, 3],
1779        };
1780
1781        let frame = CommandDispatchFrame::from_envelope(StationId::new(10), &envelope);
1782        assert_eq!(frame.station_id, StationId::new(10));
1783        assert_eq!(frame.received_at, Tick::new(99));
1784        assert_eq!(frame.into_envelope(), envelope);
1785    }
1786
1787    #[test]
1788    fn binary_codec_roundtrips_barrier_frame() {
1789        let frame = BarrierFrame {
1790            client_id: ClientId::new(1),
1791            barrier_id: BarrierId::new(99),
1792            server_tick: Tick::new(11),
1793            state: BarrierState::Frozen,
1794        };
1795        let mut encoder = BinaryFrameEncoder;
1796        let mut bytes = Vec::new();
1797        encoder
1798            .encode_barrier(&frame, &mut bytes)
1799            .expect("encoder is infallible");
1800
1801        let decoded = BinaryFrameDecoder
1802            .decode(&bytes)
1803            .expect("decode should work");
1804        assert_eq!(decoded, RuntimeFrame::Barrier(frame));
1805    }
1806
1807    #[test]
1808    fn binary_codec_roundtrips_station_event_frame() {
1809        let frames = [
1810            StationEventFrame {
1811                event_id: EventId::new(1),
1812                source_station: StationId::new(10),
1813                target_station: StationId::new(11),
1814                source_tick: Tick::new(2),
1815                target_tick: Tick::new(3),
1816                priority: EventPriority::Critical,
1817                kind: EventKind::Custom(7),
1818            },
1819            StationEventFrame {
1820                event_id: EventId::new(2),
1821                source_station: StationId::new(10),
1822                target_station: StationId::new(11),
1823                source_tick: Tick::new(2),
1824                target_tick: Tick::new(3),
1825                priority: EventPriority::Important,
1826                kind: EventKind::HandoffPrepare {
1827                    entity_id: EntityId::new(99),
1828                },
1829            },
1830            StationEventFrame {
1831                event_id: EventId::new(3),
1832                source_station: StationId::new(10),
1833                target_station: StationId::new(11),
1834                source_tick: Tick::new(2),
1835                target_tick: Tick::new(3),
1836                priority: EventPriority::BestEffort,
1837                kind: EventKind::HandoffCommit {
1838                    entity_id: EntityId::new(99),
1839                    owner_epoch: OwnerEpoch::new(5),
1840                },
1841            },
1842        ];
1843
1844        for frame in frames {
1845            let mut encoder = BinaryFrameEncoder;
1846            let mut bytes = Vec::new();
1847            encoder
1848                .encode_station_event(&frame, &mut bytes)
1849                .expect("encoder is infallible");
1850
1851            let decoded = BinaryFrameDecoder
1852                .decode(&bytes)
1853                .expect("decode should work");
1854            assert_eq!(decoded, RuntimeFrame::StationEvent(frame));
1855        }
1856    }
1857
1858    #[test]
1859    fn station_event_frame_converts_to_runtime_event() {
1860        let event = StationEvent {
1861            id: EventId::new(1),
1862            source: StationId::new(10),
1863            target: StationId::new(11),
1864            source_tick: Tick::new(2),
1865            target_tick: Tick::new(3),
1866            priority: EventPriority::Critical,
1867            kind: EventKind::HandoffPrepare {
1868                entity_id: EntityId::new(99),
1869            },
1870        };
1871
1872        let frame = StationEventFrame::from_event(&event);
1873        assert_eq!(frame.event_id, event.id);
1874        assert_eq!(frame.clone().into_event(), event);
1875    }
1876
1877    #[test]
1878    fn frame_builder_materializes_dirty_component_deltas() {
1879        use sectorsync_core::prelude::{
1880            Bounds, ComponentDescriptor, ComponentMigrationMode, ComponentSyncMode, InstanceId,
1881            NodeId, PolicyId, Position3, ReplicationPlan, StationConfig, Vec3, Vec3LeCodec,
1882        };
1883
1884        let mut station = Station::new(StationConfig {
1885            station_id: sectorsync_core::prelude::StationId::new(1),
1886            node_id: NodeId::new(1),
1887            instance_id: InstanceId::new(1),
1888            tick_rate_hz: 20,
1889        });
1890        let handle = station
1891            .spawn_owned(
1892                EntityId::new(10),
1893                Position3::new(0.0, 0.0, 0.0),
1894                Bounds::Point,
1895                PolicyId::new(0),
1896            )
1897            .expect("spawn should work");
1898
1899        let descriptor = ComponentDescriptor::sparse_blob(
1900            ComponentId::new(1),
1901            "velocity",
1902            ComponentSyncMode::Delta,
1903            ComponentMigrationMode::Copy,
1904            12,
1905        );
1906        let mut components = ComponentStore::default();
1907        components
1908            .set_typed(
1909                &descriptor,
1910                handle,
1911                3,
1912                &Vec3LeCodec,
1913                &Vec3::new(1.0, 2.0, 3.0),
1914            )
1915            .expect("typed component should encode");
1916
1917        let builder = ReplicationFrameBuilder::new(ReplicationFrameLimits {
1918            max_entity_deltas: 8,
1919            max_components_per_entity: 4,
1920            max_component_bytes: 64,
1921        });
1922        let plan = ReplicationPlan {
1923            entities: vec![handle],
1924            stats: sectorsync_core::prelude::ReplicationStats::default(),
1925        };
1926        let selection = ComponentSelection {
1927            component_ids: vec![ComponentId::new(1)],
1928        };
1929        let build = builder.build(
1930            ClientId::new(5),
1931            Tick::new(9),
1932            &station,
1933            &plan,
1934            &components,
1935            &selection,
1936        );
1937
1938        assert_eq!(build.stats.encoded_entities, 1);
1939        assert_eq!(build.stats.encoded_components, 1);
1940        assert_eq!(build.frame.entities[0].entity_id, EntityId::new(10));
1941        assert_eq!(build.frame.entities[0].components[0].version, 3);
1942
1943        let mut materialized_bytes = Vec::new();
1944        BinaryFrameEncoder
1945            .encode_replication(&build.frame, &mut materialized_bytes)
1946            .expect("materialized frame should encode");
1947        let mut direct_bytes = Vec::new();
1948        let direct_stats = builder
1949            .encode_binary_into(
1950                ClientId::new(5),
1951                Tick::new(9),
1952                &station,
1953                &plan,
1954                &components,
1955                &selection,
1956                &mut direct_bytes,
1957            )
1958            .expect("plan should encode directly");
1959
1960        assert_eq!(direct_stats, build.stats);
1961        assert_eq!(direct_bytes, materialized_bytes);
1962        assert_eq!(
1963            builder.sampled_binary_capacity_hint(&station, &plan, &components, &selection),
1964            direct_bytes.len()
1965        );
1966        assert_bounded_binary_encoding(
1967            builder,
1968            &station,
1969            &plan,
1970            &components,
1971            &selection,
1972            direct_bytes.len(),
1973        );
1974    }
1975
1976    fn assert_bounded_binary_encoding(
1977        builder: ReplicationFrameBuilder,
1978        station: &Station,
1979        plan: &ReplicationPlan,
1980        components: &ComponentStore,
1981        selection: &ComponentSelection,
1982        unbounded_len: usize,
1983    ) {
1984        let mut bounded_bytes = Vec::new();
1985        let bounded = builder
1986            .encode_binary_bounded_into(
1987                ClientId::new(5),
1988                Tick::new(9),
1989                station,
1990                plan,
1991                components,
1992                selection,
1993                unbounded_len - 1,
1994                &mut bounded_bytes,
1995            )
1996            .expect("bounded frame should roll back the oversized entity");
1997        assert_eq!(bounded.encoded_entities, 0);
1998        assert_eq!(bounded.encoded_components, 0);
1999        assert_eq!(bounded.skipped_entities_by_frame_bytes, 1);
2000        assert!(bounded_bytes.len() < unbounded_len);
2001        let RuntimeFrame::Replication(frame) = BinaryFrameDecoder
2002            .decode(&bounded_bytes)
2003            .expect("bounded empty frame should remain valid")
2004        else {
2005            panic!("expected replication frame");
2006        };
2007        assert!(frame.entities.is_empty());
2008
2009        let mut prefix = vec![9, 8, 7];
2010        assert_eq!(
2011            builder.encode_binary_bounded_into(
2012                ClientId::new(5),
2013                Tick::new(9),
2014                station,
2015                plan,
2016                components,
2017                selection,
2018                ReplicationFrameBuilder::BINARY_HEADER_BYTES - 1,
2019                &mut prefix,
2020            ),
2021            Err(BinaryEncodeError::FrameBudgetTooSmall {
2022                budget: ReplicationFrameBuilder::BINARY_HEADER_BYTES - 1,
2023                required: ReplicationFrameBuilder::BINARY_HEADER_BYTES,
2024            })
2025        );
2026        assert_eq!(prefix, [9, 8, 7]);
2027    }
2028
2029    #[test]
2030    fn binary_capacity_hint_is_bounded_by_active_builder_limits() {
2031        let builder = ReplicationFrameBuilder::new(ReplicationFrameLimits {
2032            max_entity_deltas: 2,
2033            max_components_per_entity: 2,
2034            max_component_bytes: 32,
2035        });
2036        let plan = ReplicationPlan {
2037            entities: vec![
2038                EntityHandle::new(1, 0),
2039                EntityHandle::new(2, 0),
2040                EntityHandle::new(3, 0),
2041            ],
2042            stats: sectorsync_core::prelude::ReplicationStats {
2043                estimated_bytes: usize::MAX,
2044                ..sectorsync_core::prelude::ReplicationStats::default()
2045            },
2046        };
2047        let selection = ComponentSelection {
2048            component_ids: vec![
2049                ComponentId::new(1),
2050                ComponentId::new(2),
2051                ComponentId::new(3),
2052            ],
2053        };
2054
2055        let fixed_bytes = 29 + 2 * (18 + 2 * 15);
2056        let maximum_payload_bytes = 2 * 2 * 32;
2057        assert_eq!(
2058            builder.binary_capacity_hint(&plan, &selection),
2059            fixed_bytes + maximum_payload_bytes
2060        );
2061    }
2062
2063    #[test]
2064    fn sampled_capacity_hint_falls_back_when_any_sample_has_no_dirty_data() {
2065        use sectorsync_core::prelude::{
2066            Bounds, ComponentDescriptor, ComponentMigrationMode, ComponentSyncMode, InstanceId,
2067            NodeId, PolicyId, Position3, ReplicationStats, StationConfig, U32LeCodec,
2068        };
2069
2070        let mut station = Station::new(StationConfig {
2071            station_id: StationId::new(1),
2072            node_id: NodeId::new(1),
2073            instance_id: InstanceId::new(1),
2074            tick_rate_hz: 20,
2075        });
2076        let descriptor = ComponentDescriptor::sparse_blob(
2077            ComponentId::new(1),
2078            "health",
2079            ComponentSyncMode::Delta,
2080            ComponentMigrationMode::Copy,
2081            4,
2082        );
2083        let mut components = ComponentStore::default();
2084        let mut handles = Vec::new();
2085        for entity in 0_u64..4 {
2086            let handle = station
2087                .spawn_owned(
2088                    EntityId::new(entity),
2089                    Position3::new(0.0, 0.0, 0.0),
2090                    Bounds::Point,
2091                    PolicyId::new(1),
2092                )
2093                .expect("entity ids are unique");
2094            if entity < 3 {
2095                components
2096                    .set_typed(&descriptor, handle, 1, &U32LeCodec, &100)
2097                    .expect("component should fit");
2098            }
2099            handles.push(handle);
2100        }
2101        let plan = ReplicationPlan {
2102            entities: handles,
2103            stats: ReplicationStats {
2104                selected: 4,
2105                estimated_bytes: 128,
2106                ..ReplicationStats::default()
2107            },
2108        };
2109        let selection = ComponentSelection {
2110            component_ids: vec![ComponentId::new(1)],
2111        };
2112
2113        assert_eq!(
2114            ReplicationFrameBuilder::default().sampled_binary_capacity_hint(
2115                &station,
2116                &plan,
2117                &components,
2118                &selection,
2119            ),
2120            0
2121        );
2122    }
2123}