Skip to main content

sectorsync_runtime/
lib.rs

1//! Multi-station orchestration helpers for `SectorSync`.
2
3#![forbid(unsafe_code)]
4
5pub mod deployment;
6
7use std::collections::{BTreeMap, BTreeSet};
8
9use sectorsync_core::prelude::{
10    BarrierId, BarrierScope, BarrierState, CellCoord3, CellIndex, CellLoadSample, ClientId,
11    CommandEnvelope, CommandId, CommandIngress, CommandQueueError, CommandQueueMode, CommandQueues,
12    ComponentStore, EntityHandle, EntityId, EventQueueError, EventQueueLimits, EventQueues,
13    GatewayError, GatewaySessionTable, HandoffTransfer, HotspotDecision, HotspotPlanner,
14    HotspotSeverity, HotspotThresholds, NodeId, OwnerEpoch, PolicyTable, PushOutcome,
15    ReplicationBudget, ReplicationPlan, ReplicationPlanner, RuntimeBarrier, RuntimeUpgradeHook,
16    SnapshotVersion, SplitProposal, Station, StationError, StationEvent, StationId,
17    StationLoadSample, StationSnapshot, Tick, ViewerQuery, VisibilityFilter,
18};
19use sectorsync_transport::{
20    OutboundPacket, StationOutboundPacket, StationTransportReceiver, StationTransportSink,
21    TransportReceiver, TransportSink,
22};
23use sectorsync_wire::{
24    BarrierFrame, BinaryDecodeError, BinaryEncodeError, BinaryFrameDecoder, BinaryFrameEncoder,
25    CommandAckFrame, CommandDispatchFrame, CommandFrame, ComponentSelection, FrameDecoder,
26    FrameEncoder, ReplicationFrame, ReplicationFrameBuildStats, ReplicationFrameBuilder,
27    RuntimeFrame, StationEventFrame,
28};
29
30pub use deployment::{
31    DeploymentConfig, DeploymentError, DeploymentNodeRoute, DeploymentNodeState,
32    DeploymentRouteTable, DeploymentStationMove, DeploymentStationRoute, DeploymentStats,
33    GatewayDeliveryError, GatewayDeliveryRoute,
34};
35
36/// Client replication transport bridge configuration.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct ReplicationTransportConfig {
39    /// Planner budget used for every viewer unless the caller builds frames manually.
40    pub budget: ReplicationBudget,
41    /// Whether to send replication frames with no encoded entity deltas.
42    pub send_empty_frames: bool,
43}
44
45/// Client replication transport bridge statistics.
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
47pub struct ReplicationTransportStats {
48    /// Viewer queries planned.
49    pub viewers_planned: usize,
50    /// Frames skipped because they had no encoded entity deltas.
51    pub frames_skipped_empty: usize,
52    /// Frames encoded and sent to client transport.
53    pub frames_sent: usize,
54    /// Bytes sent through client transport.
55    pub bytes_sent: usize,
56    /// Entities selected by AOI planning.
57    pub entities_selected: usize,
58    /// Entities skipped by replication planner budget.
59    pub entities_skipped_by_budget: usize,
60    /// Entities skipped by replication planner cadence.
61    pub entities_skipped_by_cadence: usize,
62    /// Entity deltas encoded into replication frames.
63    pub entities_encoded: usize,
64    /// Component deltas encoded into replication frames.
65    pub components_encoded: usize,
66}
67
68/// Result of one viewer replication send attempt.
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub struct ReplicationTransportReport {
71    /// Target client.
72    pub client_id: ClientId,
73    /// Candidate entities selected by the replication planner.
74    pub selected_entities: usize,
75    /// Entity deltas encoded into the frame.
76    pub encoded_entities: usize,
77    /// Component deltas encoded into the frame.
78    pub encoded_components: usize,
79    /// Estimated bytes from the replication planner.
80    pub estimated_plan_bytes: usize,
81    /// Candidate entities skipped because the planner budget was exhausted.
82    pub skipped_by_budget: usize,
83    /// Candidate entities skipped because cadence had not elapsed.
84    pub skipped_by_cadence: usize,
85    /// Encoded wire bytes submitted to transport.
86    pub bytes_sent: usize,
87    /// Whether a frame was sent.
88    pub sent: bool,
89}
90
91/// Error produced while planning, building, encoding, or sending replication.
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub enum ReplicationTransportError<E> {
94    /// Wire encoding failed.
95    Encode(BinaryEncodeError),
96    /// Underlying client transport failed.
97    Transport(E),
98}
99
100impl<E: core::fmt::Display> core::fmt::Display for ReplicationTransportError<E> {
101    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102        match self {
103            Self::Encode(error) => write!(f, "{error}"),
104            Self::Transport(error) => write!(f, "{error}"),
105        }
106    }
107}
108
109impl<E> std::error::Error for ReplicationTransportError<E>
110where
111    E: std::error::Error + 'static,
112{
113    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
114        match self {
115            Self::Encode(error) => Some(error),
116            Self::Transport(error) => Some(error),
117        }
118    }
119}
120
121impl<E> From<BinaryEncodeError> for ReplicationTransportError<E> {
122    fn from(value: BinaryEncodeError) -> Self {
123        Self::Encode(value)
124    }
125}
126
127/// Bridge between replication planning/frame building and client packet transport.
128#[derive(Clone, Debug)]
129pub struct ReplicationTransportBridge {
130    config: ReplicationTransportConfig,
131    builder: ReplicationFrameBuilder,
132    stats: ReplicationTransportStats,
133}
134
135impl ReplicationTransportBridge {
136    /// Creates a replication transport bridge.
137    pub const fn new(config: ReplicationTransportConfig, builder: ReplicationFrameBuilder) -> Self {
138        Self {
139            config,
140            builder,
141            stats: ReplicationTransportStats {
142                viewers_planned: 0,
143                frames_skipped_empty: 0,
144                frames_sent: 0,
145                bytes_sent: 0,
146                entities_selected: 0,
147                entities_skipped_by_budget: 0,
148                entities_skipped_by_cadence: 0,
149                entities_encoded: 0,
150                components_encoded: 0,
151            },
152        }
153    }
154
155    /// Returns configuration.
156    pub const fn config(&self) -> ReplicationTransportConfig {
157        self.config
158    }
159
160    /// Returns frame builder configuration.
161    pub const fn builder(&self) -> ReplicationFrameBuilder {
162        self.builder
163    }
164
165    /// Returns accumulated statistics.
166    pub const fn stats(&self) -> ReplicationTransportStats {
167        self.stats
168    }
169
170    /// Plans, builds, encodes, and sends one viewer replication frame.
171    #[allow(clippy::too_many_arguments)]
172    pub fn send_viewer<T, F>(
173        &mut self,
174        transport: &mut T,
175        station: &Station,
176        index: &CellIndex,
177        policies: &PolicyTable,
178        components: &ComponentStore,
179        selection: &ComponentSelection,
180        viewer: &ViewerQuery,
181        filter: &F,
182    ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
183    where
184        T: TransportSink,
185        F: VisibilityFilter,
186    {
187        let plan = ReplicationPlanner::plan_for_viewer(
188            station,
189            index,
190            policies,
191            viewer,
192            filter,
193            self.config.budget,
194        );
195        self.send_plan(
196            transport,
197            viewer.client_id,
198            station.tick(),
199            station,
200            components,
201            selection,
202            &plan,
203        )
204    }
205
206    /// Plans with cadence, builds, encodes, and sends one viewer replication frame.
207    #[allow(clippy::too_many_arguments)]
208    pub fn send_viewer_with_cadence<T, F, L>(
209        &mut self,
210        transport: &mut T,
211        station: &Station,
212        index: &CellIndex,
213        policies: &PolicyTable,
214        components: &ComponentStore,
215        selection: &ComponentSelection,
216        viewer: &ViewerQuery,
217        filter: &F,
218        last_sent: L,
219    ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
220    where
221        T: TransportSink,
222        F: VisibilityFilter,
223        L: Fn(EntityHandle) -> Option<Tick>,
224    {
225        let plan = ReplicationPlanner::plan_for_viewer_with_cadence(
226            station,
227            index,
228            policies,
229            viewer,
230            filter,
231            self.config.budget,
232            last_sent,
233        );
234        self.send_plan(
235            transport,
236            viewer.client_id,
237            station.tick(),
238            station,
239            components,
240            selection,
241            &plan,
242        )
243    }
244
245    /// Plans by priority, builds, encodes, and sends one viewer replication frame.
246    #[allow(clippy::too_many_arguments)]
247    pub fn send_viewer_prioritized<T, F>(
248        &mut self,
249        transport: &mut T,
250        station: &Station,
251        index: &CellIndex,
252        policies: &PolicyTable,
253        components: &ComponentStore,
254        selection: &ComponentSelection,
255        viewer: &ViewerQuery,
256        filter: &F,
257    ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
258    where
259        T: TransportSink,
260        F: VisibilityFilter,
261    {
262        let plan = ReplicationPlanner::plan_for_viewer_prioritized(
263            station,
264            index,
265            policies,
266            viewer,
267            filter,
268            self.config.budget,
269        );
270        self.send_plan(
271            transport,
272            viewer.client_id,
273            station.tick(),
274            station,
275            components,
276            selection,
277            &plan,
278        )
279    }
280
281    /// Plans by priority and cadence, builds, encodes, and sends one viewer replication frame.
282    #[allow(clippy::too_many_arguments)]
283    pub fn send_viewer_prioritized_with_cadence<T, F, L>(
284        &mut self,
285        transport: &mut T,
286        station: &Station,
287        index: &CellIndex,
288        policies: &PolicyTable,
289        components: &ComponentStore,
290        selection: &ComponentSelection,
291        viewer: &ViewerQuery,
292        filter: &F,
293        last_sent: L,
294    ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
295    where
296        T: TransportSink,
297        F: VisibilityFilter,
298        L: Fn(EntityHandle) -> Option<Tick>,
299    {
300        let plan = ReplicationPlanner::plan_for_viewer_prioritized_with_cadence(
301            station,
302            index,
303            policies,
304            viewer,
305            filter,
306            self.config.budget,
307            last_sent,
308        );
309        self.send_plan(
310            transport,
311            viewer.client_id,
312            station.tick(),
313            station,
314            components,
315            selection,
316            &plan,
317        )
318    }
319
320    /// Builds, encodes, and sends a caller-provided replication plan.
321    #[allow(clippy::too_many_arguments)]
322    pub fn send_plan<T>(
323        &mut self,
324        transport: &mut T,
325        client_id: ClientId,
326        server_tick: Tick,
327        station: &Station,
328        components: &ComponentStore,
329        selection: &ComponentSelection,
330        plan: &ReplicationPlan,
331    ) -> Result<ReplicationTransportReport, ReplicationTransportError<T::Error>>
332    where
333        T: TransportSink,
334    {
335        self.stats.viewers_planned = self.stats.viewers_planned.saturating_add(1);
336        self.stats.entities_selected = self
337            .stats
338            .entities_selected
339            .saturating_add(plan.stats.selected);
340        self.stats.entities_skipped_by_budget = self
341            .stats
342            .entities_skipped_by_budget
343            .saturating_add(plan.stats.skipped_by_budget);
344        self.stats.entities_skipped_by_cadence = self
345            .stats
346            .entities_skipped_by_cadence
347            .saturating_add(plan.stats.skipped_by_cadence);
348
349        let build =
350            self.builder
351                .build(client_id, server_tick, station, plan, components, selection);
352        let build_stats = build.stats;
353        self.stats.entities_encoded = self
354            .stats
355            .entities_encoded
356            .saturating_add(build_stats.encoded_entities);
357        self.stats.components_encoded = self
358            .stats
359            .components_encoded
360            .saturating_add(build_stats.encoded_components);
361
362        if build.frame.entities.is_empty() && !self.config.send_empty_frames {
363            self.stats.frames_skipped_empty = self.stats.frames_skipped_empty.saturating_add(1);
364            return Ok(replication_report(client_id, plan, build_stats, 0, false));
365        }
366
367        let mut bytes = Vec::new();
368        BinaryFrameEncoder.encode_replication(&build.frame, &mut bytes)?;
369        let byte_len = bytes.len();
370        transport
371            .send(OutboundPacket { client_id, bytes })
372            .map_err(ReplicationTransportError::Transport)?;
373        self.stats.frames_sent = self.stats.frames_sent.saturating_add(1);
374        self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(byte_len);
375
376        Ok(replication_report(
377            client_id,
378            plan,
379            build_stats,
380            byte_len,
381            true,
382        ))
383    }
384}
385
386impl Default for ReplicationTransportBridge {
387    fn default() -> Self {
388        Self::new(
389            ReplicationTransportConfig::default(),
390            ReplicationFrameBuilder::default(),
391        )
392    }
393}
394
395fn replication_report(
396    client_id: ClientId,
397    plan: &ReplicationPlan,
398    build_stats: ReplicationFrameBuildStats,
399    bytes_sent: usize,
400    sent: bool,
401) -> ReplicationTransportReport {
402    ReplicationTransportReport {
403        client_id,
404        selected_entities: plan.stats.selected,
405        encoded_entities: build_stats.encoded_entities,
406        encoded_components: build_stats.encoded_components,
407        estimated_plan_bytes: plan.stats.estimated_bytes,
408        skipped_by_budget: plan.stats.skipped_by_budget,
409        skipped_by_cadence: plan.stats.skipped_by_cadence,
410        bytes_sent,
411        sent,
412    }
413}
414
415/// Replication receive bridge configuration.
416#[derive(Clone, Copy, Debug, PartialEq, Eq)]
417pub struct ReplicationReceiveConfig {
418    /// Local client id expected inside replication frames.
419    pub client_id: ClientId,
420    /// Expected remote sender identity when the transport can identify it.
421    pub expected_source: Option<ClientId>,
422}
423
424impl ReplicationReceiveConfig {
425    /// Creates receive configuration for a local client.
426    pub const fn new(client_id: ClientId) -> Self {
427        Self {
428            client_id,
429            expected_source: None,
430        }
431    }
432
433    /// Returns a copy that expects packets from `source`.
434    #[must_use]
435    pub const fn with_expected_source(mut self, source: ClientId) -> Self {
436        self.expected_source = Some(source);
437        self
438    }
439}
440
441/// Replication receive bridge statistics.
442#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
443pub struct ReplicationReceiveStats {
444    /// Packets consumed from client transport.
445    pub packets_received: usize,
446    /// Bytes consumed from client transport.
447    pub bytes_received: usize,
448    /// Replication frames decoded and accepted.
449    pub frames_received: usize,
450    /// Packets rejected by wire decoding.
451    pub frames_rejected_decode: usize,
452    /// Packets rejected because they were not replication frames.
453    pub frames_rejected_unexpected: usize,
454    /// Packets rejected because the transport source did not match.
455    pub frames_rejected_source: usize,
456    /// Packets rejected because the frame target did not match this client.
457    pub frames_rejected_target: usize,
458    /// Entity deltas received.
459    pub entities_received: usize,
460    /// Component deltas received.
461    pub components_received: usize,
462}
463
464/// Result of pumping replication packets.
465#[derive(Clone, Debug, Default, PartialEq, Eq)]
466pub struct ReplicationReceivePump {
467    /// Packets consumed from client transport.
468    pub packets_received: usize,
469    /// Bytes consumed from client transport.
470    pub bytes_received: usize,
471    /// Decoded replication frames.
472    pub frames: Vec<ReplicationFrame>,
473}
474
475impl ReplicationReceivePump {
476    /// Returns accepted frame count.
477    pub fn frames_received(&self) -> usize {
478        self.frames.len()
479    }
480
481    /// Returns received entity delta count.
482    pub fn entities_received(&self) -> usize {
483        self.frames.iter().map(|frame| frame.entities.len()).sum()
484    }
485
486    /// Returns received component delta count.
487    pub fn components_received(&self) -> usize {
488        self.frames
489            .iter()
490            .flat_map(|frame| &frame.entities)
491            .map(|entity| entity.components.len())
492            .sum()
493    }
494}
495
496/// Error produced while receiving replication frames.
497#[derive(Clone, Debug, PartialEq, Eq)]
498pub enum ReplicationReceiveError<E> {
499    /// Underlying client transport failed.
500    Transport(E),
501    /// Wire decoding failed.
502    Decode(BinaryDecodeError),
503    /// Packet decoded as a non-replication frame.
504    UnexpectedFrame,
505    /// Packet source did not match expected remote.
506    SourceMismatch {
507        /// Expected source.
508        expected: ClientId,
509        /// Actual source if transport identified one.
510        actual: Option<ClientId>,
511    },
512    /// Replication frame targeted another client.
513    TargetMismatch {
514        /// Expected local client id.
515        expected: ClientId,
516        /// Actual frame target.
517        actual: ClientId,
518    },
519}
520
521impl<E: core::fmt::Display> core::fmt::Display for ReplicationReceiveError<E> {
522    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
523        match self {
524            Self::Transport(error) => write!(f, "{error}"),
525            Self::Decode(error) => write!(f, "{error}"),
526            Self::UnexpectedFrame => f.write_str("client packet was not a replication frame"),
527            Self::SourceMismatch { expected, actual } => write!(
528                f,
529                "replication source mismatch: expected {}, actual {:?}",
530                expected.get(),
531                actual.map(ClientId::get)
532            ),
533            Self::TargetMismatch { expected, actual } => write!(
534                f,
535                "replication target mismatch: expected {}, actual {}",
536                expected.get(),
537                actual.get()
538            ),
539        }
540    }
541}
542
543impl<E> std::error::Error for ReplicationReceiveError<E>
544where
545    E: std::error::Error + 'static,
546{
547    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
548        match self {
549            Self::Transport(error) => Some(error),
550            Self::Decode(error) => Some(error),
551            Self::UnexpectedFrame | Self::SourceMismatch { .. } | Self::TargetMismatch { .. } => {
552                None
553            }
554        }
555    }
556}
557
558impl<E> From<BinaryDecodeError> for ReplicationReceiveError<E> {
559    fn from(value: BinaryDecodeError) -> Self {
560        Self::Decode(value)
561    }
562}
563
564/// Bridge between client packet transport and decoded replication frames.
565#[derive(Clone, Debug)]
566pub struct ReplicationReceiveBridge {
567    config: ReplicationReceiveConfig,
568    stats: ReplicationReceiveStats,
569}
570
571impl ReplicationReceiveBridge {
572    /// Creates a receive bridge.
573    pub const fn new(config: ReplicationReceiveConfig) -> Self {
574        Self {
575            config,
576            stats: ReplicationReceiveStats {
577                packets_received: 0,
578                bytes_received: 0,
579                frames_received: 0,
580                frames_rejected_decode: 0,
581                frames_rejected_unexpected: 0,
582                frames_rejected_source: 0,
583                frames_rejected_target: 0,
584                entities_received: 0,
585                components_received: 0,
586            },
587        }
588    }
589
590    /// Returns configuration.
591    pub const fn config(&self) -> ReplicationReceiveConfig {
592        self.config
593    }
594
595    /// Returns accumulated statistics.
596    pub const fn stats(&self) -> ReplicationReceiveStats {
597        self.stats
598    }
599
600    /// Receives and decodes up to `max_packets` replication frames.
601    pub fn pump<T>(
602        &mut self,
603        transport: &mut T,
604        max_packets: usize,
605    ) -> Result<ReplicationReceivePump, ReplicationReceiveError<T::Error>>
606    where
607        T: TransportReceiver,
608    {
609        let mut pump = ReplicationReceivePump::default();
610        for _ in 0..max_packets {
611            let Some(packet) = transport
612                .try_recv()
613                .map_err(ReplicationReceiveError::Transport)?
614            else {
615                break;
616            };
617            self.stats.packets_received = self.stats.packets_received.saturating_add(1);
618            self.stats.bytes_received =
619                self.stats.bytes_received.saturating_add(packet.bytes.len());
620            pump.packets_received = pump.packets_received.saturating_add(1);
621            pump.bytes_received = pump.bytes_received.saturating_add(packet.bytes.len());
622
623            if let Some(expected) = self.config.expected_source
624                && packet.client_id != Some(expected)
625            {
626                self.stats.frames_rejected_source =
627                    self.stats.frames_rejected_source.saturating_add(1);
628                return Err(ReplicationReceiveError::SourceMismatch {
629                    expected,
630                    actual: packet.client_id,
631                });
632            }
633
634            let decoded = match BinaryFrameDecoder.decode(&packet.bytes) {
635                Ok(decoded) => decoded,
636                Err(error) => {
637                    self.stats.frames_rejected_decode =
638                        self.stats.frames_rejected_decode.saturating_add(1);
639                    return Err(ReplicationReceiveError::Decode(error));
640                }
641            };
642            let RuntimeFrame::Replication(frame) = decoded else {
643                self.stats.frames_rejected_unexpected =
644                    self.stats.frames_rejected_unexpected.saturating_add(1);
645                return Err(ReplicationReceiveError::UnexpectedFrame);
646            };
647            if frame.client_id != self.config.client_id {
648                self.stats.frames_rejected_target =
649                    self.stats.frames_rejected_target.saturating_add(1);
650                return Err(ReplicationReceiveError::TargetMismatch {
651                    expected: self.config.client_id,
652                    actual: frame.client_id,
653                });
654            }
655
656            self.stats.frames_received = self.stats.frames_received.saturating_add(1);
657            self.stats.entities_received = self
658                .stats
659                .entities_received
660                .saturating_add(frame.entities.len());
661            let components = frame
662                .entities
663                .iter()
664                .map(|entity| entity.components.len())
665                .sum::<usize>();
666            self.stats.components_received =
667                self.stats.components_received.saturating_add(components);
668            pump.frames.push(frame);
669        }
670        Ok(pump)
671    }
672}
673
674/// Low-level client transport bridge configuration.
675#[derive(Clone, Copy, Debug, PartialEq, Eq)]
676pub struct ClientTransportConfig {
677    /// Local client id expected inside client-bound frames.
678    pub client_id: ClientId,
679    /// Remote server/gateway id used as the transport packet target for commands.
680    pub server_id: ClientId,
681    /// Expected remote sender identity when the transport can identify it.
682    pub expected_source: Option<ClientId>,
683}
684
685impl ClientTransportConfig {
686    /// Creates client transport configuration for a server/gateway target.
687    pub const fn new(client_id: ClientId, server_id: ClientId) -> Self {
688        Self {
689            client_id,
690            server_id,
691            expected_source: None,
692        }
693    }
694
695    /// Returns a copy that expects inbound packets from `source`.
696    #[must_use]
697    pub const fn with_expected_source(mut self, source: ClientId) -> Self {
698        self.expected_source = Some(source);
699        self
700    }
701}
702
703/// Low-level client transport bridge statistics.
704#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
705pub struct ClientTransportStats {
706    /// Command frames encoded and submitted to transport.
707    pub commands_sent: usize,
708    /// Command bytes submitted to transport.
709    pub command_bytes_sent: usize,
710    /// Packets consumed from transport.
711    pub packets_received: usize,
712    /// Bytes consumed from transport.
713    pub bytes_received: usize,
714    /// Command ACK frames decoded and accepted.
715    pub command_acks_received: usize,
716    /// Replication frames decoded and accepted.
717    pub replication_frames_received: usize,
718    /// Barrier frames decoded and accepted.
719    pub barrier_frames_received: usize,
720    /// Packets rejected by wire decoding.
721    pub frames_rejected_decode: usize,
722    /// Packets rejected because they were not client-bound frames.
723    pub frames_rejected_unexpected: usize,
724    /// Packets rejected because the transport source did not match.
725    pub frames_rejected_source: usize,
726    /// Packets rejected because the frame target did not match this client.
727    pub frames_rejected_target: usize,
728    /// Entity deltas received in replication frames.
729    pub entities_received: usize,
730    /// Component deltas received in replication frames.
731    pub components_received: usize,
732}
733
734/// Result of sending one command frame.
735#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
736pub struct ClientCommandSendReport {
737    /// Submitted command id.
738    pub command_id: CommandId,
739    /// Encoded command bytes submitted to transport.
740    pub bytes_sent: usize,
741}
742
743/// Client-bound frame categories accepted by `ClientTransportBridge`.
744#[derive(Clone, Copy, Debug, PartialEq, Eq)]
745pub enum ClientInboundFrameKind {
746    /// Command acknowledgement.
747    CommandAck,
748    /// Replication update.
749    Replication,
750    /// Runtime barrier notification.
751    Barrier,
752}
753
754/// Result of pumping client-bound frames.
755#[derive(Clone, Debug, Default, PartialEq, Eq)]
756pub struct ClientTransportPump {
757    /// Packets consumed from transport.
758    pub packets_received: usize,
759    /// Bytes consumed from transport.
760    pub bytes_received: usize,
761    /// Command acknowledgements decoded and accepted.
762    pub command_acks: Vec<CommandAckFrame>,
763    /// Replication frames decoded and accepted.
764    pub replication_frames: Vec<ReplicationFrame>,
765    /// Barrier frames decoded and accepted.
766    pub barriers: Vec<BarrierFrame>,
767}
768
769impl ClientTransportPump {
770    /// Returns received command ACK count.
771    pub fn command_acks_received(&self) -> usize {
772        self.command_acks.len()
773    }
774
775    /// Returns accepted replication frame count.
776    pub fn replication_frames_received(&self) -> usize {
777        self.replication_frames.len()
778    }
779
780    /// Returns accepted barrier frame count.
781    pub fn barrier_frames_received(&self) -> usize {
782        self.barriers.len()
783    }
784
785    /// Returns received entity delta count.
786    pub fn entities_received(&self) -> usize {
787        self.replication_frames
788            .iter()
789            .map(|frame| frame.entities.len())
790            .sum()
791    }
792
793    /// Returns received component delta count.
794    pub fn components_received(&self) -> usize {
795        self.replication_frames
796            .iter()
797            .flat_map(|frame| &frame.entities)
798            .map(|entity| entity.components.len())
799            .sum()
800    }
801}
802
803/// Error produced by the low-level client transport bridge.
804#[derive(Clone, Debug, PartialEq, Eq)]
805pub enum ClientTransportBridgeError<E> {
806    /// Outbound command used a different client id than the bridge config.
807    CommandClientMismatch {
808        /// Expected local client id.
809        expected: ClientId,
810        /// Actual command client id.
811        actual: ClientId,
812    },
813    /// Wire encoding failed.
814    Encode(BinaryEncodeError),
815    /// Underlying client transport failed.
816    Transport(E),
817    /// Wire decoding failed.
818    Decode(BinaryDecodeError),
819    /// Packet decoded as a frame that is not client-bound.
820    UnexpectedFrame,
821    /// Packet source did not match expected remote.
822    SourceMismatch {
823        /// Expected source.
824        expected: ClientId,
825        /// Actual source if transport identified one.
826        actual: Option<ClientId>,
827    },
828    /// Client-bound frame targeted another client.
829    TargetMismatch {
830        /// Frame category.
831        kind: ClientInboundFrameKind,
832        /// Expected local client id.
833        expected: ClientId,
834        /// Actual frame target.
835        actual: ClientId,
836    },
837}
838
839impl<E: core::fmt::Display> core::fmt::Display for ClientTransportBridgeError<E> {
840    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
841        match self {
842            Self::CommandClientMismatch { expected, actual } => write!(
843                f,
844                "command client mismatch: expected {}, actual {}",
845                expected.get(),
846                actual.get()
847            ),
848            Self::Encode(error) => write!(f, "{error}"),
849            Self::Transport(error) => write!(f, "{error}"),
850            Self::Decode(error) => write!(f, "{error}"),
851            Self::UnexpectedFrame => f.write_str("packet was not a client-bound frame"),
852            Self::SourceMismatch { expected, actual } => write!(
853                f,
854                "client packet source mismatch: expected {}, actual {:?}",
855                expected.get(),
856                actual.map(ClientId::get)
857            ),
858            Self::TargetMismatch {
859                kind,
860                expected,
861                actual,
862            } => write!(
863                f,
864                "client {:?} frame target mismatch: expected {}, actual {}",
865                kind,
866                expected.get(),
867                actual.get()
868            ),
869        }
870    }
871}
872
873impl<E> std::error::Error for ClientTransportBridgeError<E>
874where
875    E: std::error::Error + 'static,
876{
877    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
878        match self {
879            Self::Encode(error) => Some(error),
880            Self::Transport(error) => Some(error),
881            Self::Decode(error) => Some(error),
882            Self::CommandClientMismatch { .. }
883            | Self::UnexpectedFrame
884            | Self::SourceMismatch { .. }
885            | Self::TargetMismatch { .. } => None,
886        }
887    }
888}
889
890impl<E> From<BinaryEncodeError> for ClientTransportBridgeError<E> {
891    fn from(value: BinaryEncodeError) -> Self {
892        Self::Encode(value)
893    }
894}
895
896impl<E> From<BinaryDecodeError> for ClientTransportBridgeError<E> {
897    fn from(value: BinaryDecodeError) -> Self {
898        Self::Decode(value)
899    }
900}
901
902/// Low-level bridge for client command send and client-bound frame receive.
903#[derive(Clone, Debug)]
904pub struct ClientTransportBridge {
905    config: ClientTransportConfig,
906    stats: ClientTransportStats,
907}
908
909impl ClientTransportBridge {
910    /// Creates a client transport bridge.
911    pub const fn new(config: ClientTransportConfig) -> Self {
912        Self {
913            config,
914            stats: ClientTransportStats {
915                commands_sent: 0,
916                command_bytes_sent: 0,
917                packets_received: 0,
918                bytes_received: 0,
919                command_acks_received: 0,
920                replication_frames_received: 0,
921                barrier_frames_received: 0,
922                frames_rejected_decode: 0,
923                frames_rejected_unexpected: 0,
924                frames_rejected_source: 0,
925                frames_rejected_target: 0,
926                entities_received: 0,
927                components_received: 0,
928            },
929        }
930    }
931
932    /// Returns configuration.
933    pub const fn config(&self) -> ClientTransportConfig {
934        self.config
935    }
936
937    /// Returns accumulated statistics.
938    pub const fn stats(&self) -> ClientTransportStats {
939        self.stats
940    }
941
942    /// Encodes and sends a client command frame to the configured server id.
943    pub fn send_command_frame<T>(
944        &mut self,
945        transport: &mut T,
946        frame: &CommandFrame,
947    ) -> Result<ClientCommandSendReport, ClientTransportBridgeError<T::Error>>
948    where
949        T: TransportSink,
950    {
951        if frame.client_id != self.config.client_id {
952            return Err(ClientTransportBridgeError::CommandClientMismatch {
953                expected: self.config.client_id,
954                actual: frame.client_id,
955            });
956        }
957
958        let mut bytes = Vec::new();
959        BinaryFrameEncoder.encode_command(frame, &mut bytes)?;
960        let bytes_sent = bytes.len();
961        transport
962            .send(OutboundPacket {
963                client_id: self.config.server_id,
964                bytes,
965            })
966            .map_err(ClientTransportBridgeError::Transport)?;
967        self.stats.commands_sent = self.stats.commands_sent.saturating_add(1);
968        self.stats.command_bytes_sent = self.stats.command_bytes_sent.saturating_add(bytes_sent);
969
970        Ok(ClientCommandSendReport {
971            command_id: frame.command_id,
972            bytes_sent,
973        })
974    }
975
976    /// Receives and decodes up to `max_packets` client-bound frames.
977    pub fn pump<T>(
978        &mut self,
979        transport: &mut T,
980        max_packets: usize,
981    ) -> Result<ClientTransportPump, ClientTransportBridgeError<T::Error>>
982    where
983        T: TransportReceiver,
984    {
985        let mut pump = ClientTransportPump::default();
986        for _ in 0..max_packets {
987            let Some(packet) = transport
988                .try_recv()
989                .map_err(ClientTransportBridgeError::Transport)?
990            else {
991                break;
992            };
993            self.stats.packets_received = self.stats.packets_received.saturating_add(1);
994            self.stats.bytes_received =
995                self.stats.bytes_received.saturating_add(packet.bytes.len());
996            pump.packets_received = pump.packets_received.saturating_add(1);
997            pump.bytes_received = pump.bytes_received.saturating_add(packet.bytes.len());
998
999            if let Some(expected) = self.config.expected_source
1000                && packet.client_id != Some(expected)
1001            {
1002                self.stats.frames_rejected_source =
1003                    self.stats.frames_rejected_source.saturating_add(1);
1004                return Err(ClientTransportBridgeError::SourceMismatch {
1005                    expected,
1006                    actual: packet.client_id,
1007                });
1008            }
1009
1010            let decoded = match BinaryFrameDecoder.decode(&packet.bytes) {
1011                Ok(decoded) => decoded,
1012                Err(error) => {
1013                    self.stats.frames_rejected_decode =
1014                        self.stats.frames_rejected_decode.saturating_add(1);
1015                    return Err(ClientTransportBridgeError::Decode(error));
1016                }
1017            };
1018            match decoded {
1019                RuntimeFrame::CommandAck(frame) => {
1020                    self.validate_client_target(
1021                        ClientInboundFrameKind::CommandAck,
1022                        frame.client_id,
1023                    )?;
1024                    self.stats.command_acks_received =
1025                        self.stats.command_acks_received.saturating_add(1);
1026                    pump.command_acks.push(frame);
1027                }
1028                RuntimeFrame::Replication(frame) => {
1029                    self.validate_client_target(
1030                        ClientInboundFrameKind::Replication,
1031                        frame.client_id,
1032                    )?;
1033                    self.stats.replication_frames_received =
1034                        self.stats.replication_frames_received.saturating_add(1);
1035                    self.stats.entities_received = self
1036                        .stats
1037                        .entities_received
1038                        .saturating_add(frame.entities.len());
1039                    let components = frame
1040                        .entities
1041                        .iter()
1042                        .map(|entity| entity.components.len())
1043                        .sum::<usize>();
1044                    self.stats.components_received =
1045                        self.stats.components_received.saturating_add(components);
1046                    pump.replication_frames.push(frame);
1047                }
1048                RuntimeFrame::Barrier(frame) => {
1049                    self.validate_client_target(ClientInboundFrameKind::Barrier, frame.client_id)?;
1050                    self.stats.barrier_frames_received =
1051                        self.stats.barrier_frames_received.saturating_add(1);
1052                    pump.barriers.push(frame);
1053                }
1054                RuntimeFrame::Command(_)
1055                | RuntimeFrame::CommandDispatch(_)
1056                | RuntimeFrame::StationEvent(_) => {
1057                    self.stats.frames_rejected_unexpected =
1058                        self.stats.frames_rejected_unexpected.saturating_add(1);
1059                    return Err(ClientTransportBridgeError::UnexpectedFrame);
1060                }
1061            }
1062        }
1063        Ok(pump)
1064    }
1065
1066    fn validate_client_target<E>(
1067        &mut self,
1068        kind: ClientInboundFrameKind,
1069        actual: ClientId,
1070    ) -> Result<(), ClientTransportBridgeError<E>> {
1071        if actual == self.config.client_id {
1072            return Ok(());
1073        }
1074        self.stats.frames_rejected_target = self.stats.frames_rejected_target.saturating_add(1);
1075        Err(ClientTransportBridgeError::TargetMismatch {
1076            kind,
1077            expected: self.config.client_id,
1078            actual,
1079        })
1080    }
1081}
1082
1083/// Runtime barrier notification transport statistics.
1084#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1085pub struct BarrierTransportStats {
1086    /// Barrier frames encoded and submitted to client transport.
1087    pub notifications_sent: usize,
1088    /// Client targets submitted to transport.
1089    pub clients_notified: usize,
1090    /// Encoded bytes submitted to transport.
1091    pub bytes_sent: usize,
1092}
1093
1094/// Result of one barrier notification broadcast.
1095#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1096pub struct BarrierTransportReport {
1097    /// Barrier id.
1098    pub barrier_id: BarrierId,
1099    /// Barrier state sent to clients.
1100    pub state: BarrierState,
1101    /// Server tick associated with the notification.
1102    pub server_tick: Tick,
1103    /// Client targets requested by the caller.
1104    pub clients_requested: usize,
1105    /// Client targets successfully submitted to transport.
1106    pub clients_sent: usize,
1107    /// Encoded bytes submitted to transport.
1108    pub bytes_sent: usize,
1109}
1110
1111/// Error produced while encoding or sending barrier notifications.
1112#[derive(Clone, Debug, PartialEq, Eq)]
1113pub enum BarrierTransportError<E> {
1114    /// Wire encoding failed.
1115    Encode(BinaryEncodeError),
1116    /// Underlying client transport failed.
1117    Transport(E),
1118}
1119
1120impl<E: core::fmt::Display> core::fmt::Display for BarrierTransportError<E> {
1121    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1122        match self {
1123            Self::Encode(error) => write!(f, "{error}"),
1124            Self::Transport(error) => write!(f, "{error}"),
1125        }
1126    }
1127}
1128
1129impl<E> std::error::Error for BarrierTransportError<E>
1130where
1131    E: std::error::Error + 'static,
1132{
1133    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1134        match self {
1135            Self::Encode(error) => Some(error),
1136            Self::Transport(error) => Some(error),
1137        }
1138    }
1139}
1140
1141impl<E> From<BinaryEncodeError> for BarrierTransportError<E> {
1142    fn from(value: BinaryEncodeError) -> Self {
1143        Self::Encode(value)
1144    }
1145}
1146
1147/// Low-level bridge for sending runtime barrier notifications to clients.
1148#[derive(Clone, Debug, Default)]
1149pub struct BarrierTransportBridge {
1150    stats: BarrierTransportStats,
1151}
1152
1153impl BarrierTransportBridge {
1154    /// Creates a barrier notification transport bridge.
1155    pub const fn new() -> Self {
1156        Self {
1157            stats: BarrierTransportStats {
1158                notifications_sent: 0,
1159                clients_notified: 0,
1160                bytes_sent: 0,
1161            },
1162        }
1163    }
1164
1165    /// Returns accumulated statistics.
1166    pub const fn stats(&self) -> BarrierTransportStats {
1167        self.stats
1168    }
1169
1170    /// Sends one barrier notification to one client.
1171    pub fn send_state<T>(
1172        &mut self,
1173        transport: &mut T,
1174        client_id: ClientId,
1175        barrier_id: BarrierId,
1176        server_tick: Tick,
1177        state: BarrierState,
1178    ) -> Result<usize, BarrierTransportError<T::Error>>
1179    where
1180        T: TransportSink,
1181    {
1182        let frame = BarrierFrame {
1183            client_id,
1184            barrier_id,
1185            server_tick,
1186            state,
1187        };
1188        let mut bytes = Vec::new();
1189        BinaryFrameEncoder.encode_barrier(&frame, &mut bytes)?;
1190        let bytes_sent = bytes.len();
1191        transport
1192            .send(OutboundPacket { client_id, bytes })
1193            .map_err(BarrierTransportError::Transport)?;
1194        self.stats.notifications_sent = self.stats.notifications_sent.saturating_add(1);
1195        self.stats.clients_notified = self.stats.clients_notified.saturating_add(1);
1196        self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(bytes_sent);
1197        Ok(bytes_sent)
1198    }
1199
1200    /// Sends one runtime barrier notification to one client.
1201    pub fn send_barrier<T>(
1202        &mut self,
1203        transport: &mut T,
1204        client_id: ClientId,
1205        barrier: RuntimeBarrier,
1206    ) -> Result<usize, BarrierTransportError<T::Error>>
1207    where
1208        T: TransportSink,
1209    {
1210        self.send_state(
1211            transport,
1212            client_id,
1213            barrier.id,
1214            barrier.target_tick,
1215            barrier.state,
1216        )
1217    }
1218
1219    /// Broadcasts one barrier state to a bounded caller-provided client list.
1220    pub fn broadcast_state<T, I>(
1221        &mut self,
1222        transport: &mut T,
1223        clients: I,
1224        barrier_id: BarrierId,
1225        server_tick: Tick,
1226        state: BarrierState,
1227    ) -> Result<BarrierTransportReport, BarrierTransportError<T::Error>>
1228    where
1229        T: TransportSink,
1230        I: IntoIterator<Item = ClientId>,
1231    {
1232        let mut report = BarrierTransportReport {
1233            barrier_id,
1234            state,
1235            server_tick,
1236            clients_requested: 0,
1237            clients_sent: 0,
1238            bytes_sent: 0,
1239        };
1240        for client_id in clients {
1241            report.clients_requested = report.clients_requested.saturating_add(1);
1242            let bytes_sent =
1243                self.send_state(transport, client_id, barrier_id, server_tick, state)?;
1244            report.clients_sent = report.clients_sent.saturating_add(1);
1245            report.bytes_sent = report.bytes_sent.saturating_add(bytes_sent);
1246        }
1247        Ok(report)
1248    }
1249
1250    /// Broadcasts one runtime barrier to a bounded caller-provided client list.
1251    pub fn broadcast_barrier<T, I>(
1252        &mut self,
1253        transport: &mut T,
1254        clients: I,
1255        barrier: RuntimeBarrier,
1256    ) -> Result<BarrierTransportReport, BarrierTransportError<T::Error>>
1257    where
1258        T: TransportSink,
1259        I: IntoIterator<Item = ClientId>,
1260    {
1261        self.broadcast_state(
1262            transport,
1263            clients,
1264            barrier.id,
1265            barrier.target_tick,
1266            barrier.state,
1267        )
1268    }
1269}
1270
1271/// Accepted command ACK reason code.
1272pub const GATEWAY_COMMAND_ACK_ACCEPTED: u16 = 0;
1273/// Command was rejected by generic gateway/session state.
1274pub const GATEWAY_COMMAND_ACK_GATEWAY_REJECTED: u16 = 1;
1275/// Command was rejected by gateway rate limiting.
1276pub const GATEWAY_COMMAND_ACK_RATE_LIMITED: u16 = 2;
1277/// Command was rejected as stale or replayed.
1278pub const GATEWAY_COMMAND_ACK_REPLAY_OR_STALE: u16 = 3;
1279/// Command could not be queued because a target station queue was full.
1280pub const GATEWAY_COMMAND_ACK_QUEUE_FULL: u16 = 4;
1281/// Command was rejected by the station barrier ingress policy.
1282pub const GATEWAY_COMMAND_ACK_BARRIER_REJECTED: u16 = 5;
1283/// Command route pointed at a station queue that was not registered.
1284pub const GATEWAY_COMMAND_ACK_MISSING_QUEUE: u16 = 6;
1285/// Command could not be resolved through deployment metadata.
1286pub const GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED: u16 = 7;
1287
1288/// Gateway command pipeline configuration.
1289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1290pub struct GatewayCommandPipelineConfig {
1291    /// Encode negative ACKs for gateway/queue rejections.
1292    pub ack_rejections: bool,
1293}
1294
1295impl Default for GatewayCommandPipelineConfig {
1296    fn default() -> Self {
1297        Self {
1298            ack_rejections: true,
1299        }
1300    }
1301}
1302
1303/// Gateway command pipeline statistics.
1304#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1305pub struct GatewayCommandPipelineStats {
1306    /// Command frames decoded.
1307    pub command_frames_decoded: usize,
1308    /// Frames rejected by the binary decoder.
1309    pub frames_rejected_decode: usize,
1310    /// Non-command frames rejected by this pipeline.
1311    pub frames_rejected_non_command: usize,
1312    /// Commands admitted by gateway/session metadata.
1313    pub commands_admitted: usize,
1314    /// Commands enqueued into target station queues.
1315    pub commands_enqueued: usize,
1316    /// Commands rejected by gateway/session metadata.
1317    pub commands_rejected_gateway: usize,
1318    /// Commands rejected by station queue or station queue lookup.
1319    pub commands_rejected_queue: usize,
1320    /// Commands resolved to deployment node delivery routes.
1321    pub commands_routed_deployment: usize,
1322    /// Commands rejected by deployment node/station route metadata.
1323    pub commands_rejected_deployment: usize,
1324    /// ACK frames encoded.
1325    pub acks_encoded: usize,
1326}
1327
1328/// Gateway command pipeline error.
1329#[derive(Clone, Debug, PartialEq, Eq)]
1330pub enum GatewayCommandPipelineError {
1331    /// Wire decode failed.
1332    Decode(BinaryDecodeError),
1333    /// Frame decoded correctly but was not a command frame.
1334    NonCommandFrame,
1335    /// Gateway/session metadata rejected the command.
1336    Gateway(GatewayError),
1337    /// Gateway route pointed at a missing station queue.
1338    MissingQueue(StationId),
1339    /// Target station queue rejected the command.
1340    Queue(CommandQueueError),
1341    /// Deployment route metadata rejected command delivery.
1342    Deployment(DeploymentError),
1343    /// ACK encode failed.
1344    Encode(BinaryEncodeError),
1345}
1346
1347impl core::fmt::Display for GatewayCommandPipelineError {
1348    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1349        match self {
1350            Self::Decode(error) => write!(f, "{error}"),
1351            Self::NonCommandFrame => f.write_str("gateway command pipeline expected command frame"),
1352            Self::Gateway(error) => write!(f, "{error}"),
1353            Self::MissingQueue(station_id) => write!(
1354                f,
1355                "gateway command route target station {} has no queue",
1356                station_id.get()
1357            ),
1358            Self::Queue(error) => write!(f, "{error}"),
1359            Self::Deployment(error) => write!(f, "{error}"),
1360            Self::Encode(error) => write!(f, "{error}"),
1361        }
1362    }
1363}
1364
1365impl std::error::Error for GatewayCommandPipelineError {
1366    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1367        match self {
1368            Self::Decode(error) => Some(error),
1369            Self::Gateway(error) => Some(error),
1370            Self::Queue(error) => Some(error),
1371            Self::Deployment(error) => Some(error),
1372            Self::Encode(error) => Some(error),
1373            Self::NonCommandFrame | Self::MissingQueue(_) => None,
1374        }
1375    }
1376}
1377
1378/// Gateway command pipeline result.
1379#[derive(Clone, Debug, Default, PartialEq, Eq)]
1380pub struct GatewayCommandPipelineReport {
1381    /// Client id when a command frame was decoded.
1382    pub client_id: Option<ClientId>,
1383    /// Command id when a command frame was decoded.
1384    pub command_id: Option<CommandId>,
1385    /// Target station when gateway routing succeeded.
1386    pub station_id: Option<StationId>,
1387    /// Target node when deployment routing succeeded.
1388    pub node_id: Option<NodeId>,
1389    /// Resolved deployment delivery route.
1390    pub delivery: Option<GatewayDeliveryRoute>,
1391    /// Stamped command envelope for external dispatch, when not enqueued locally.
1392    pub command: Option<CommandEnvelope>,
1393    /// Whether the command was queued for station application.
1394    pub accepted: bool,
1395    /// ACK reason code. Zero means accepted.
1396    pub reason_code: u16,
1397    /// Encoded ACK bytes, when an ACK was produced.
1398    pub ack_bytes: Option<Vec<u8>>,
1399    /// Decode, gateway, queue, or encode error detail.
1400    pub error: Option<GatewayCommandPipelineError>,
1401}
1402
1403/// Business-agnostic gateway command frame pipeline.
1404#[derive(Clone, Debug)]
1405pub struct GatewayCommandPipeline {
1406    config: GatewayCommandPipelineConfig,
1407    decoder: BinaryFrameDecoder,
1408    encoder: BinaryFrameEncoder,
1409    stats: GatewayCommandPipelineStats,
1410}
1411
1412impl GatewayCommandPipeline {
1413    /// Creates a pipeline.
1414    pub fn new(config: GatewayCommandPipelineConfig) -> Self {
1415        Self {
1416            config,
1417            decoder: BinaryFrameDecoder,
1418            encoder: BinaryFrameEncoder,
1419            stats: GatewayCommandPipelineStats::default(),
1420        }
1421    }
1422
1423    /// Returns configuration.
1424    pub const fn config(&self) -> GatewayCommandPipelineConfig {
1425        self.config
1426    }
1427
1428    /// Returns statistics.
1429    pub const fn stats(&self) -> GatewayCommandPipelineStats {
1430        self.stats
1431    }
1432
1433    /// Processes one decoded-transport command packet and optionally produces
1434    /// an encoded command ACK.
1435    pub fn process(
1436        &mut self,
1437        gateway: &mut GatewaySessionTable,
1438        station_queues: &mut BTreeMap<StationId, CommandQueues>,
1439        input: &[u8],
1440        now: Tick,
1441        ingress: CommandIngress,
1442    ) -> GatewayCommandPipelineReport {
1443        let command_frame = match self.decode_command_frame(input) {
1444            Ok(command_frame) => command_frame,
1445            Err(error) => {
1446                return GatewayCommandPipelineReport {
1447                    error: Some(error),
1448                    ..GatewayCommandPipelineReport::default()
1449                };
1450            }
1451        };
1452
1453        self.process_command_frame(gateway, station_queues, command_frame, now, ingress)
1454    }
1455
1456    /// Decodes and admits one command packet, then resolves a deployment route
1457    /// for external node dispatch without touching local station queues.
1458    pub fn dispatch(
1459        &mut self,
1460        gateway: &mut GatewaySessionTable,
1461        deployment: &DeploymentRouteTable,
1462        input: &[u8],
1463        now: Tick,
1464    ) -> GatewayCommandPipelineReport {
1465        let command_frame = match self.decode_command_frame(input) {
1466            Ok(command_frame) => command_frame,
1467            Err(error) => {
1468                return GatewayCommandPipelineReport {
1469                    error: Some(error),
1470                    ..GatewayCommandPipelineReport::default()
1471                };
1472            }
1473        };
1474
1475        self.dispatch_command_frame(gateway, deployment, command_frame, now)
1476    }
1477
1478    fn decode_command_frame(
1479        &mut self,
1480        input: &[u8],
1481    ) -> Result<CommandFrame, GatewayCommandPipelineError> {
1482        let frame = match self.decoder.decode(input) {
1483            Ok(frame) => frame,
1484            Err(error) => {
1485                self.stats.frames_rejected_decode =
1486                    self.stats.frames_rejected_decode.saturating_add(1);
1487                return Err(GatewayCommandPipelineError::Decode(error));
1488            }
1489        };
1490
1491        let RuntimeFrame::Command(command_frame) = frame else {
1492            self.stats.frames_rejected_non_command =
1493                self.stats.frames_rejected_non_command.saturating_add(1);
1494            return Err(GatewayCommandPipelineError::NonCommandFrame);
1495        };
1496        self.stats.command_frames_decoded = self.stats.command_frames_decoded.saturating_add(1);
1497
1498        Ok(command_frame)
1499    }
1500
1501    fn process_command_frame(
1502        &mut self,
1503        gateway: &mut GatewaySessionTable,
1504        station_queues: &mut BTreeMap<StationId, CommandQueues>,
1505        command_frame: CommandFrame,
1506        now: Tick,
1507        ingress: CommandIngress,
1508    ) -> GatewayCommandPipelineReport {
1509        let client_id = command_frame.client_id;
1510        let command_id = command_frame.command_id;
1511        let command = command_frame.into_envelope(now);
1512        let admission = match gateway.admit_command(&command) {
1513            Ok(admission) => {
1514                self.stats.commands_admitted = self.stats.commands_admitted.saturating_add(1);
1515                admission
1516            }
1517            Err(error) => {
1518                self.stats.commands_rejected_gateway =
1519                    self.stats.commands_rejected_gateway.saturating_add(1);
1520                return self.rejected_report(
1521                    client_id,
1522                    command_id,
1523                    None,
1524                    now,
1525                    gateway_reject_reason_code(error),
1526                    GatewayCommandPipelineError::Gateway(error),
1527                );
1528            }
1529        };
1530
1531        let station_id = admission.route.station_id;
1532        let Some(queue) = station_queues.get_mut(&station_id) else {
1533            self.stats.commands_rejected_queue =
1534                self.stats.commands_rejected_queue.saturating_add(1);
1535            return self.rejected_report(
1536                client_id,
1537                command_id,
1538                Some(station_id),
1539                now,
1540                GATEWAY_COMMAND_ACK_MISSING_QUEUE,
1541                GatewayCommandPipelineError::MissingQueue(station_id),
1542            );
1543        };
1544
1545        if let Err(error) = queue.push(command, ingress) {
1546            self.stats.commands_rejected_queue =
1547                self.stats.commands_rejected_queue.saturating_add(1);
1548            return self.rejected_report(
1549                client_id,
1550                command_id,
1551                Some(station_id),
1552                now,
1553                queue_reject_reason_code(error),
1554                GatewayCommandPipelineError::Queue(error),
1555            );
1556        }
1557
1558        self.stats.commands_enqueued = self.stats.commands_enqueued.saturating_add(1);
1559        self.accepted_report(client_id, command_id, station_id, now)
1560    }
1561
1562    fn dispatch_command_frame(
1563        &mut self,
1564        gateway: &mut GatewaySessionTable,
1565        deployment: &DeploymentRouteTable,
1566        command_frame: CommandFrame,
1567        now: Tick,
1568    ) -> GatewayCommandPipelineReport {
1569        let client_id = command_frame.client_id;
1570        let command_id = command_frame.command_id;
1571        let command = command_frame.into_envelope(now);
1572        let admission = match gateway.admit_command(&command) {
1573            Ok(admission) => {
1574                self.stats.commands_admitted = self.stats.commands_admitted.saturating_add(1);
1575                admission
1576            }
1577            Err(error) => {
1578                self.stats.commands_rejected_gateway =
1579                    self.stats.commands_rejected_gateway.saturating_add(1);
1580                return self.rejected_report(
1581                    client_id,
1582                    command_id,
1583                    None,
1584                    now,
1585                    gateway_reject_reason_code(error),
1586                    GatewayCommandPipelineError::Gateway(error),
1587                );
1588            }
1589        };
1590
1591        let delivery = match deployment.resolve_gateway_route(admission.route) {
1592            Ok(delivery) => {
1593                self.stats.commands_routed_deployment =
1594                    self.stats.commands_routed_deployment.saturating_add(1);
1595                delivery
1596            }
1597            Err(error) => {
1598                self.stats.commands_rejected_deployment =
1599                    self.stats.commands_rejected_deployment.saturating_add(1);
1600                return self.rejected_report(
1601                    client_id,
1602                    command_id,
1603                    Some(admission.route.station_id),
1604                    now,
1605                    GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED,
1606                    GatewayCommandPipelineError::Deployment(error),
1607                );
1608            }
1609        };
1610
1611        self.dispatch_report(command, delivery, now)
1612    }
1613
1614    fn accepted_report(
1615        &mut self,
1616        client_id: ClientId,
1617        command_id: CommandId,
1618        station_id: StationId,
1619        now: Tick,
1620    ) -> GatewayCommandPipelineReport {
1621        let ack = CommandAckFrame {
1622            client_id,
1623            command_id,
1624            server_tick: now,
1625            accepted: true,
1626            reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1627        };
1628        match self.encode_ack(&ack) {
1629            Ok(ack_bytes) => GatewayCommandPipelineReport {
1630                client_id: Some(client_id),
1631                command_id: Some(command_id),
1632                station_id: Some(station_id),
1633                node_id: None,
1634                delivery: None,
1635                command: None,
1636                accepted: true,
1637                reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1638                ack_bytes: Some(ack_bytes),
1639                error: None,
1640            },
1641            Err(error) => GatewayCommandPipelineReport {
1642                client_id: Some(client_id),
1643                command_id: Some(command_id),
1644                station_id: Some(station_id),
1645                node_id: None,
1646                delivery: None,
1647                command: None,
1648                accepted: false,
1649                reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1650                ack_bytes: None,
1651                error: Some(GatewayCommandPipelineError::Encode(error)),
1652            },
1653        }
1654    }
1655
1656    fn dispatch_report(
1657        &mut self,
1658        command: CommandEnvelope,
1659        delivery: GatewayDeliveryRoute,
1660        now: Tick,
1661    ) -> GatewayCommandPipelineReport {
1662        let ack = CommandAckFrame {
1663            client_id: command.client_id,
1664            command_id: command.id,
1665            server_tick: now,
1666            accepted: true,
1667            reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1668        };
1669        match self.encode_ack(&ack) {
1670            Ok(ack_bytes) => GatewayCommandPipelineReport {
1671                client_id: Some(command.client_id),
1672                command_id: Some(command.id),
1673                station_id: Some(delivery.station_id),
1674                node_id: Some(delivery.node_id),
1675                delivery: Some(delivery),
1676                command: Some(command),
1677                accepted: true,
1678                reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1679                ack_bytes: Some(ack_bytes),
1680                error: None,
1681            },
1682            Err(error) => GatewayCommandPipelineReport {
1683                client_id: Some(command.client_id),
1684                command_id: Some(command.id),
1685                station_id: Some(delivery.station_id),
1686                node_id: Some(delivery.node_id),
1687                delivery: Some(delivery),
1688                command: None,
1689                accepted: false,
1690                reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
1691                ack_bytes: None,
1692                error: Some(GatewayCommandPipelineError::Encode(error)),
1693            },
1694        }
1695    }
1696
1697    fn rejected_report(
1698        &mut self,
1699        client_id: ClientId,
1700        command_id: CommandId,
1701        station_id: Option<StationId>,
1702        now: Tick,
1703        reason_code: u16,
1704        error: GatewayCommandPipelineError,
1705    ) -> GatewayCommandPipelineReport {
1706        let ack_bytes = if self.config.ack_rejections {
1707            let ack = CommandAckFrame {
1708                client_id,
1709                command_id,
1710                server_tick: now,
1711                accepted: false,
1712                reason_code,
1713            };
1714            match self.encode_ack(&ack) {
1715                Ok(bytes) => Some(bytes),
1716                Err(encode_error) => {
1717                    return GatewayCommandPipelineReport {
1718                        client_id: Some(client_id),
1719                        command_id: Some(command_id),
1720                        station_id,
1721                        node_id: None,
1722                        delivery: None,
1723                        command: None,
1724                        accepted: false,
1725                        reason_code,
1726                        ack_bytes: None,
1727                        error: Some(GatewayCommandPipelineError::Encode(encode_error)),
1728                    };
1729                }
1730            }
1731        } else {
1732            None
1733        };
1734
1735        GatewayCommandPipelineReport {
1736            client_id: Some(client_id),
1737            command_id: Some(command_id),
1738            station_id,
1739            node_id: None,
1740            delivery: None,
1741            command: None,
1742            accepted: false,
1743            reason_code,
1744            ack_bytes,
1745            error: Some(error),
1746        }
1747    }
1748
1749    fn encode_ack(&mut self, ack: &CommandAckFrame) -> Result<Vec<u8>, BinaryEncodeError> {
1750        let mut out = Vec::new();
1751        self.encoder.encode_command_ack(ack, &mut out)?;
1752        self.stats.acks_encoded = self.stats.acks_encoded.saturating_add(1);
1753        Ok(out)
1754    }
1755}
1756
1757impl Default for GatewayCommandPipeline {
1758    fn default() -> Self {
1759        Self::new(GatewayCommandPipelineConfig::default())
1760    }
1761}
1762
1763const fn gateway_reject_reason_code(error: GatewayError) -> u16 {
1764    match error {
1765        GatewayError::ReplayOrStale { .. } => GATEWAY_COMMAND_ACK_REPLAY_OR_STALE,
1766        GatewayError::RateLimited { .. } => GATEWAY_COMMAND_ACK_RATE_LIMITED,
1767        GatewayError::MissingSession(_)
1768        | GatewayError::SessionDisconnected { .. }
1769        | GatewayError::BadGeneration { .. }
1770        | GatewayError::CapacityFull { .. } => GATEWAY_COMMAND_ACK_GATEWAY_REJECTED,
1771    }
1772}
1773
1774const fn queue_reject_reason_code(error: CommandQueueError) -> u16 {
1775    match error {
1776        CommandQueueError::QueueFull(_) => GATEWAY_COMMAND_ACK_QUEUE_FULL,
1777        CommandQueueError::RejectedByBarrier(_) => GATEWAY_COMMAND_ACK_BARRIER_REJECTED,
1778    }
1779}
1780
1781/// Gateway-side client command transport bridge statistics.
1782#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1783pub struct GatewayClientTransportStats {
1784    /// Client packets consumed from transport.
1785    pub packets_received: usize,
1786    /// Client packet bytes consumed from transport.
1787    pub bytes_received: usize,
1788    /// Command frames decoded from client packets.
1789    pub command_frames_received: usize,
1790    /// Packets rejected because transport source and command client differed.
1791    pub source_mismatches: usize,
1792    /// Commands accepted by the gateway command pipeline.
1793    pub commands_accepted: usize,
1794    /// Commands rejected by the gateway command pipeline.
1795    pub commands_rejected: usize,
1796    /// Command ACK frames submitted to client transport.
1797    pub acks_sent: usize,
1798    /// Command ACK bytes submitted to client transport.
1799    pub ack_bytes_sent: usize,
1800}
1801
1802/// Result of pumping gateway-side client command packets.
1803#[derive(Clone, Debug, Default, PartialEq, Eq)]
1804pub struct GatewayClientTransportPump {
1805    /// Client packets consumed from transport.
1806    pub packets_received: usize,
1807    /// Client packet bytes consumed from transport.
1808    pub bytes_received: usize,
1809    /// Gateway pipeline reports produced from accepted or rejected commands.
1810    pub reports: Vec<GatewayCommandPipelineReport>,
1811    /// Command ACK frames submitted to client transport.
1812    pub acks_sent: usize,
1813    /// Command ACK bytes submitted to client transport.
1814    pub ack_bytes_sent: usize,
1815}
1816
1817impl GatewayClientTransportPump {
1818    /// Returns processed command count.
1819    pub fn commands_processed(&self) -> usize {
1820        self.reports.len()
1821    }
1822
1823    /// Returns accepted command count.
1824    pub fn commands_accepted(&self) -> usize {
1825        self.reports.iter().filter(|report| report.accepted).count()
1826    }
1827
1828    /// Returns rejected command count.
1829    pub fn commands_rejected(&self) -> usize {
1830        self.reports
1831            .iter()
1832            .filter(|report| !report.accepted)
1833            .count()
1834    }
1835}
1836
1837/// Error produced while pumping gateway-side client command packets.
1838#[derive(Clone, Debug, PartialEq, Eq)]
1839pub enum GatewayClientTransportError<E> {
1840    /// Underlying client transport failed while receiving or sending acknowledgements.
1841    Transport(E),
1842    /// Wire decoding failed.
1843    Decode(BinaryDecodeError),
1844    /// Packet decoded as a non-command frame.
1845    NonCommandFrame,
1846    /// Transport source client and command frame client disagreed.
1847    SourceMismatch {
1848        /// Client identified by the transport.
1849        packet_client_id: ClientId,
1850        /// Client encoded inside the command frame.
1851        frame_client_id: ClientId,
1852    },
1853}
1854
1855impl<E: core::fmt::Display> core::fmt::Display for GatewayClientTransportError<E> {
1856    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1857        match self {
1858            Self::Transport(error) => write!(f, "{error}"),
1859            Self::Decode(error) => write!(f, "{error}"),
1860            Self::NonCommandFrame => f.write_str("gateway client transport expected command frame"),
1861            Self::SourceMismatch {
1862                packet_client_id,
1863                frame_client_id,
1864            } => write!(
1865                f,
1866                "gateway client source mismatch: packet {}, frame {}",
1867                packet_client_id.get(),
1868                frame_client_id.get()
1869            ),
1870        }
1871    }
1872}
1873
1874impl<E> std::error::Error for GatewayClientTransportError<E>
1875where
1876    E: std::error::Error + 'static,
1877{
1878    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1879        match self {
1880            Self::Transport(error) => Some(error),
1881            Self::Decode(error) => Some(error),
1882            Self::NonCommandFrame | Self::SourceMismatch { .. } => None,
1883        }
1884    }
1885}
1886
1887/// Low-level bridge from client packet transport into the gateway command pipeline.
1888#[derive(Clone, Debug, Default)]
1889pub struct GatewayClientTransportBridge {
1890    stats: GatewayClientTransportStats,
1891}
1892
1893impl GatewayClientTransportBridge {
1894    /// Creates a gateway client transport bridge.
1895    pub const fn new() -> Self {
1896        Self {
1897            stats: GatewayClientTransportStats {
1898                packets_received: 0,
1899                bytes_received: 0,
1900                command_frames_received: 0,
1901                source_mismatches: 0,
1902                commands_accepted: 0,
1903                commands_rejected: 0,
1904                acks_sent: 0,
1905                ack_bytes_sent: 0,
1906            },
1907        }
1908    }
1909
1910    /// Returns accumulated statistics.
1911    pub const fn stats(&self) -> GatewayClientTransportStats {
1912        self.stats
1913    }
1914
1915    /// Pumps up to `max_packets` client command packets into station queues and
1916    /// sends produced ACKs back through the same bounded client transport.
1917    #[allow(clippy::too_many_arguments)]
1918    pub fn pump_ingress<T, E>(
1919        &mut self,
1920        transport: &mut T,
1921        pipeline: &mut GatewayCommandPipeline,
1922        gateway: &mut GatewaySessionTable,
1923        station_queues: &mut BTreeMap<StationId, CommandQueues>,
1924        now: Tick,
1925        ingress: CommandIngress,
1926        max_packets: usize,
1927    ) -> Result<GatewayClientTransportPump, GatewayClientTransportError<E>>
1928    where
1929        T: TransportReceiver<Error = E> + TransportSink<Error = E>,
1930    {
1931        let mut pump = GatewayClientTransportPump::default();
1932        for _ in 0..max_packets {
1933            let Some(packet) = transport
1934                .try_recv()
1935                .map_err(GatewayClientTransportError::Transport)?
1936            else {
1937                break;
1938            };
1939            self.stats.packets_received = self.stats.packets_received.saturating_add(1);
1940            self.stats.bytes_received =
1941                self.stats.bytes_received.saturating_add(packet.bytes.len());
1942            pump.packets_received = pump.packets_received.saturating_add(1);
1943            pump.bytes_received = pump.bytes_received.saturating_add(packet.bytes.len());
1944
1945            let command_frame = match pipeline.decode_command_frame(&packet.bytes) {
1946                Ok(command_frame) => command_frame,
1947                Err(GatewayCommandPipelineError::Decode(error)) => {
1948                    return Err(GatewayClientTransportError::Decode(error));
1949                }
1950                Err(GatewayCommandPipelineError::NonCommandFrame) => {
1951                    return Err(GatewayClientTransportError::NonCommandFrame);
1952                }
1953                Err(error) => {
1954                    unreachable!(
1955                        "decode_command_frame only returns decode/non-command errors: {error}"
1956                    );
1957                }
1958            };
1959            self.stats.command_frames_received =
1960                self.stats.command_frames_received.saturating_add(1);
1961
1962            if let Some(packet_client_id) = packet.client_id
1963                && packet_client_id != command_frame.client_id
1964            {
1965                self.stats.source_mismatches = self.stats.source_mismatches.saturating_add(1);
1966                return Err(GatewayClientTransportError::SourceMismatch {
1967                    packet_client_id,
1968                    frame_client_id: command_frame.client_id,
1969                });
1970            }
1971
1972            let ack_client_id = command_frame.client_id;
1973            let report = pipeline.process_command_frame(
1974                gateway,
1975                station_queues,
1976                command_frame,
1977                now,
1978                ingress,
1979            );
1980            if report.accepted {
1981                self.stats.commands_accepted = self.stats.commands_accepted.saturating_add(1);
1982            } else {
1983                self.stats.commands_rejected = self.stats.commands_rejected.saturating_add(1);
1984            }
1985
1986            if let Some(bytes) = &report.ack_bytes {
1987                let ack_len = bytes.len();
1988                transport
1989                    .send(OutboundPacket {
1990                        client_id: ack_client_id,
1991                        bytes: bytes.clone(),
1992                    })
1993                    .map_err(GatewayClientTransportError::Transport)?;
1994                self.stats.acks_sent = self.stats.acks_sent.saturating_add(1);
1995                self.stats.ack_bytes_sent = self.stats.ack_bytes_sent.saturating_add(ack_len);
1996                pump.acks_sent = pump.acks_sent.saturating_add(1);
1997                pump.ack_bytes_sent = pump.ack_bytes_sent.saturating_add(ack_len);
1998            }
1999            pump.reports.push(report);
2000        }
2001        Ok(pump)
2002    }
2003}
2004
2005/// Small in-process station collection for simulations and embedders.
2006#[derive(Clone, Debug, Default)]
2007pub struct StationSet {
2008    stations: Vec<Station>,
2009}
2010
2011impl StationSet {
2012    /// Adds a station to the collection.
2013    pub fn push(&mut self, station: Station) {
2014        self.stations.push(station);
2015    }
2016
2017    /// Gets a station by id.
2018    pub fn get(&self, station_id: StationId) -> Option<&Station> {
2019        self.stations
2020            .iter()
2021            .find(|station| station.config().station_id == station_id)
2022    }
2023
2024    /// Gets a mutable station by id.
2025    pub fn get_mut(&mut self, station_id: StationId) -> Option<&mut Station> {
2026        self.stations
2027            .iter_mut()
2028            .find(|station| station.config().station_id == station_id)
2029    }
2030
2031    /// Gets two distinct mutable stations by id.
2032    pub fn get_pair_mut(
2033        &mut self,
2034        left_id: StationId,
2035        right_id: StationId,
2036    ) -> Option<(&mut Station, &mut Station)> {
2037        if left_id == right_id {
2038            return None;
2039        }
2040
2041        let left_index = self
2042            .stations
2043            .iter()
2044            .position(|station| station.config().station_id == left_id)?;
2045        let right_index = self
2046            .stations
2047            .iter()
2048            .position(|station| station.config().station_id == right_id)?;
2049
2050        if left_index < right_index {
2051            let (left, right) = self.stations.split_at_mut(right_index);
2052            Some((&mut left[left_index], &mut right[0]))
2053        } else {
2054            let (left, right) = self.stations.split_at_mut(left_index);
2055            Some((&mut right[0], &mut left[right_index]))
2056        }
2057    }
2058
2059    /// Iterates over stations.
2060    pub fn iter(&self) -> impl Iterator<Item = &Station> {
2061        self.stations.iter()
2062    }
2063
2064    /// Iterates mutably over stations.
2065    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Station> {
2066        self.stations.iter_mut()
2067    }
2068
2069    /// Returns station ids matching a barrier scope.
2070    pub fn station_ids_in_scope(&self, scope: BarrierScope) -> Vec<StationId> {
2071        self.stations
2072            .iter()
2073            .filter(|station| match scope {
2074                BarrierScope::Instance(instance_id) => station.config().instance_id == instance_id,
2075                BarrierScope::Station(station_id) => station.config().station_id == station_id,
2076            })
2077            .map(|station| station.config().station_id)
2078            .collect()
2079    }
2080
2081    /// Number of stations.
2082    pub fn len(&self) -> usize {
2083        self.stations.len()
2084    }
2085
2086    /// Returns whether no stations are registered.
2087    pub fn is_empty(&self) -> bool {
2088        self.stations.is_empty()
2089    }
2090}
2091
2092/// Station-local spatial indexes keyed by station id.
2093#[derive(Clone, Debug, Default)]
2094pub struct StationIndexSet {
2095    indexes: Vec<(StationId, CellIndex)>,
2096}
2097
2098impl StationIndexSet {
2099    /// Adds or replaces one station index.
2100    pub fn insert(&mut self, station_id: StationId, index: CellIndex) {
2101        if let Some((_, existing)) = self.indexes.iter_mut().find(|(id, _)| *id == station_id) {
2102            *existing = index;
2103        } else {
2104            self.indexes.push((station_id, index));
2105        }
2106    }
2107
2108    /// Gets one station index.
2109    pub fn get(&self, station_id: StationId) -> Option<&CellIndex> {
2110        self.indexes
2111            .iter()
2112            .find(|(id, _)| *id == station_id)
2113            .map(|(_, index)| index)
2114    }
2115
2116    /// Gets one mutable station index.
2117    pub fn get_mut(&mut self, station_id: StationId) -> Option<&mut CellIndex> {
2118        self.indexes
2119            .iter_mut()
2120            .find(|(id, _)| *id == station_id)
2121            .map(|(_, index)| index)
2122    }
2123
2124    /// Gets two distinct mutable station indexes.
2125    pub fn get_pair_mut(
2126        &mut self,
2127        left_id: StationId,
2128        right_id: StationId,
2129    ) -> Option<(&mut CellIndex, &mut CellIndex)> {
2130        if left_id == right_id {
2131            return None;
2132        }
2133
2134        let left_index = self.indexes.iter().position(|(id, _)| *id == left_id)?;
2135        let right_index = self.indexes.iter().position(|(id, _)| *id == right_id)?;
2136
2137        if left_index < right_index {
2138            let (left, right) = self.indexes.split_at_mut(right_index);
2139            Some((&mut left[left_index].1, &mut right[0].1))
2140        } else {
2141            let (left, right) = self.indexes.split_at_mut(left_index);
2142            Some((&mut right[0].1, &mut left[right_index].1))
2143        }
2144    }
2145
2146    /// Number of indexes.
2147    pub fn len(&self) -> usize {
2148        self.indexes.len()
2149    }
2150
2151    /// Iterates over registered indexes.
2152    pub fn iter(&self) -> impl Iterator<Item = (StationId, &CellIndex)> {
2153        self.indexes
2154            .iter()
2155            .map(|(station_id, index)| (*station_id, index))
2156    }
2157
2158    /// Returns whether no indexes are registered.
2159    pub fn is_empty(&self) -> bool {
2160        self.indexes.is_empty()
2161    }
2162}
2163
2164/// Lightweight coefficients used to derive hotspot/scheduler load samples.
2165#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2166pub struct StationLoadSamplerConfig {
2167    /// Estimated bytes contributed by one stored entity in the measurement window.
2168    pub estimated_bytes_per_entity: usize,
2169    /// Estimated bytes contributed by one subscriber routed to a station.
2170    pub estimated_bytes_per_subscriber: usize,
2171    /// Estimated bytes contributed by one queued station event.
2172    pub estimated_bytes_per_event: usize,
2173    /// Runtime cost units assigned to one authoritative entity.
2174    pub tick_cost_per_owned_entity: u64,
2175    /// Runtime cost units assigned to one read-only ghost entity.
2176    pub tick_cost_per_ghost_entity: u64,
2177    /// Runtime cost units assigned to one occupied spatial cell.
2178    pub tick_cost_per_occupied_cell: u64,
2179    /// Runtime cost units assigned to one queued station event.
2180    pub tick_cost_per_queued_event: u64,
2181}
2182
2183impl Default for StationLoadSamplerConfig {
2184    fn default() -> Self {
2185        Self {
2186            estimated_bytes_per_entity: 48,
2187            estimated_bytes_per_subscriber: 16,
2188            estimated_bytes_per_event: 32,
2189            tick_cost_per_owned_entity: 2,
2190            tick_cost_per_ghost_entity: 1,
2191            tick_cost_per_occupied_cell: 1,
2192            tick_cost_per_queued_event: 1,
2193        }
2194    }
2195}
2196
2197/// Runtime helper that derives `StationLoadSample` from existing low-level state.
2198#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2199pub struct StationLoadSampler {
2200    config: StationLoadSamplerConfig,
2201}
2202
2203impl StationLoadSampler {
2204    /// Creates a station load sampler.
2205    pub const fn new(config: StationLoadSamplerConfig) -> Self {
2206        Self { config }
2207    }
2208
2209    /// Returns the sampler configuration.
2210    pub const fn config(&self) -> StationLoadSamplerConfig {
2211        self.config
2212    }
2213
2214    /// Samples one station from its storage, optional index, queued event count,
2215    /// and caller-provided subscriber estimate.
2216    pub fn sample_station(
2217        &self,
2218        station: &Station,
2219        index: Option<&CellIndex>,
2220        queued_events: usize,
2221        subscribers: usize,
2222    ) -> StationLoadSample {
2223        let (owned_entities, ghost_entities) = count_station_roles(station);
2224        let cells = index
2225            .map(|index| self.sample_cells(station, index))
2226            .unwrap_or_default();
2227        StationLoadSample {
2228            station_id: station.config().station_id,
2229            owned_entities,
2230            ghost_entities,
2231            subscribers,
2232            queued_events,
2233            estimated_bytes: self.estimate_station_bytes(
2234                owned_entities,
2235                ghost_entities,
2236                subscribers,
2237                queued_events,
2238            ),
2239            tick_cost_units: self.estimate_tick_cost(
2240                owned_entities,
2241                ghost_entities,
2242                cells.len(),
2243                queued_events,
2244            ),
2245            cells,
2246        }
2247    }
2248
2249    /// Samples every station in deterministic station-set order.
2250    ///
2251    /// `subscriber_counts` is explicit integration input: `SectorSync` can use it
2252    /// for load decisions, but does not own gateway/client/session business state.
2253    /// Counts with the same station id are aggregated with saturating arithmetic.
2254    /// Because the event router and subscriber input are station-scoped, their
2255    /// pressure stays on the station sample instead of being invented per cell.
2256    pub fn sample_all(
2257        &self,
2258        stations: &StationSet,
2259        indexes: &StationIndexSet,
2260        router: &EventRouter,
2261        subscriber_counts: &[(StationId, usize)],
2262    ) -> Vec<StationLoadSample> {
2263        let subscribers_by_station = station_count_map(subscriber_counts);
2264        stations
2265            .iter()
2266            .map(|station| {
2267                let station_id = station.config().station_id;
2268                self.sample_station(
2269                    station,
2270                    indexes.get(station_id),
2271                    router.queued_len(station_id).unwrap_or(0),
2272                    subscribers_by_station
2273                        .get(&station_id)
2274                        .copied()
2275                        .unwrap_or(0),
2276                )
2277            })
2278            .collect()
2279    }
2280
2281    fn sample_cells(&self, station: &Station, index: &CellIndex) -> Vec<CellLoadSample> {
2282        index
2283            .cell_occupancy()
2284            .into_iter()
2285            .map(|occupancy| {
2286                let mut owned_entities = 0usize;
2287                let mut ghost_entities = 0usize;
2288                for handle in index.handles_in_cell_slice(occupancy.cell) {
2289                    if let Some(record) = station.get(*handle) {
2290                        if record.is_owned() {
2291                            owned_entities = owned_entities.saturating_add(1);
2292                        } else {
2293                            ghost_entities = ghost_entities.saturating_add(1);
2294                        }
2295                    }
2296                }
2297                let entities = owned_entities.saturating_add(ghost_entities);
2298                CellLoadSample {
2299                    cell: occupancy.cell,
2300                    owned_entities,
2301                    ghost_entities,
2302                    subscribers: 0,
2303                    estimated_updates: entities,
2304                    estimated_bytes: entities
2305                        .saturating_mul(self.config.estimated_bytes_per_entity),
2306                    tick_cost_units: self.estimate_tick_cost(owned_entities, ghost_entities, 1, 0),
2307                    event_pressure: 0,
2308                }
2309            })
2310            .collect()
2311    }
2312
2313    fn estimate_station_bytes(
2314        &self,
2315        owned_entities: usize,
2316        ghost_entities: usize,
2317        subscribers: usize,
2318        queued_events: usize,
2319    ) -> usize {
2320        owned_entities
2321            .saturating_add(ghost_entities)
2322            .saturating_mul(self.config.estimated_bytes_per_entity)
2323            .saturating_add(subscribers.saturating_mul(self.config.estimated_bytes_per_subscriber))
2324            .saturating_add(queued_events.saturating_mul(self.config.estimated_bytes_per_event))
2325    }
2326
2327    fn estimate_tick_cost(
2328        &self,
2329        owned_entities: usize,
2330        ghost_entities: usize,
2331        occupied_cells: usize,
2332        queued_events: usize,
2333    ) -> u64 {
2334        (owned_entities as u64)
2335            .saturating_mul(self.config.tick_cost_per_owned_entity)
2336            .saturating_add(
2337                (ghost_entities as u64).saturating_mul(self.config.tick_cost_per_ghost_entity),
2338            )
2339            .saturating_add(
2340                (occupied_cells as u64).saturating_mul(self.config.tick_cost_per_occupied_cell),
2341            )
2342            .saturating_add(
2343                (queued_events as u64).saturating_mul(self.config.tick_cost_per_queued_event),
2344            )
2345    }
2346}
2347
2348impl Default for StationLoadSampler {
2349    fn default() -> Self {
2350        Self::new(StationLoadSamplerConfig::default())
2351    }
2352}
2353
2354fn count_station_roles(station: &Station) -> (usize, usize) {
2355    let mut owned_entities = 0usize;
2356    let mut ghost_entities = 0usize;
2357    for record in station.iter() {
2358        if record.is_owned() {
2359            owned_entities = owned_entities.saturating_add(1);
2360        } else {
2361            ghost_entities = ghost_entities.saturating_add(1);
2362        }
2363    }
2364    (owned_entities, ghost_entities)
2365}
2366
2367fn station_count_map(counts: &[(StationId, usize)]) -> BTreeMap<StationId, usize> {
2368    let mut map = BTreeMap::new();
2369    for (station_id, count) in counts {
2370        let entry = map.entry(*station_id).or_insert(0usize);
2371        *entry = entry.saturating_add(*count);
2372    }
2373    map
2374}
2375
2376/// Result of an in-process entity owner migration.
2377#[derive(Clone, Debug, PartialEq)]
2378pub struct EntityMigrationReport {
2379    /// Transfer payload used for the migration.
2380    pub transfer: HandoffTransfer,
2381    /// Source-side ghost handle after commit.
2382    pub source_ghost: EntityHandle,
2383    /// Target-side authoritative handle after commit.
2384    pub target_owner: EntityHandle,
2385}
2386
2387/// Entity migration error.
2388#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2389pub enum EntityMigrationError {
2390    /// Source and target station ids must differ.
2391    SameSourceAndTarget(StationId),
2392    /// Source station was not found.
2393    MissingSource(StationId),
2394    /// Target station was not found.
2395    MissingTarget(StationId),
2396    /// Station-level operation failed.
2397    Station(StationError),
2398}
2399
2400impl core::fmt::Display for EntityMigrationError {
2401    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2402        match self {
2403            Self::SameSourceAndTarget(id) => {
2404                write!(f, "source and target station are both {}", id.get())
2405            }
2406            Self::MissingSource(id) => write!(f, "source station {} is missing", id.get()),
2407            Self::MissingTarget(id) => write!(f, "target station {} is missing", id.get()),
2408            Self::Station(error) => write!(f, "{error}"),
2409        }
2410    }
2411}
2412
2413impl std::error::Error for EntityMigrationError {}
2414
2415impl From<StationError> for EntityMigrationError {
2416    fn from(value: StationError) -> Self {
2417        Self::Station(value)
2418    }
2419}
2420
2421/// Runtime helper for in-process station-to-station owner migration.
2422#[derive(Clone, Copy, Debug, Default)]
2423pub struct EntityMigrationExecutor;
2424
2425impl EntityMigrationExecutor {
2426    /// Migrates one authoritative entity from source station to target station.
2427    pub fn migrate_entity(
2428        stations: &mut StationSet,
2429        entity_id: EntityId,
2430        source_station: StationId,
2431        target_station: StationId,
2432        ghost_ttl_ticks: u64,
2433    ) -> Result<EntityMigrationReport, EntityMigrationError> {
2434        if source_station == target_station {
2435            return Err(EntityMigrationError::SameSourceAndTarget(source_station));
2436        }
2437
2438        if stations.get(source_station).is_none() {
2439            return Err(EntityMigrationError::MissingSource(source_station));
2440        }
2441        if stations.get(target_station).is_none() {
2442            return Err(EntityMigrationError::MissingTarget(target_station));
2443        }
2444
2445        let (source, target) = stations
2446            .get_pair_mut(source_station, target_station)
2447            .expect("stations were checked above");
2448        let target_epoch = next_target_epoch(target);
2449        let source_ghost_expires_at =
2450            Tick::new(source.tick().get().saturating_add(ghost_ttl_ticks));
2451        let transfer = source.prepare_outgoing_handoff(
2452            entity_id,
2453            target_station,
2454            target_epoch,
2455            source_ghost_expires_at,
2456        )?;
2457        target.prewarm_handoff_ghost(&transfer)?;
2458        let target_owner = target.commit_incoming_handoff(transfer.clone())?;
2459        let source_ghost = source.commit_outgoing_handoff(&transfer)?;
2460
2461        Ok(EntityMigrationReport {
2462            transfer,
2463            source_ghost,
2464            target_owner,
2465        })
2466    }
2467}
2468
2469fn next_target_epoch(station: &mut Station) -> OwnerEpoch {
2470    station.next_owner_epoch()
2471}
2472
2473/// Dynamic ownership table for fixed 3D cells.
2474#[derive(Clone, Debug, Default, PartialEq, Eq)]
2475pub struct CellOwnershipTable {
2476    owners: BTreeMap<CellCoord3, StationId>,
2477}
2478
2479impl CellOwnershipTable {
2480    /// Assigns one cell to a station and returns the previous owner.
2481    pub fn assign(&mut self, cell: CellCoord3, station_id: StationId) -> Option<StationId> {
2482        self.owners.insert(cell, station_id)
2483    }
2484
2485    /// Returns the current owner for one cell.
2486    pub fn owner_of(&self, cell: CellCoord3) -> Option<StationId> {
2487        self.owners.get(&cell).copied()
2488    }
2489
2490    /// Applies a split proposal by assigning all proposed cells to `target_station`.
2491    pub fn apply_split(
2492        &mut self,
2493        proposal: &SplitProposal,
2494        target_station: StationId,
2495    ) -> CellOwnershipUpdate {
2496        let mut moved_cells = Vec::new();
2497        for cell in &proposal.cells_to_move {
2498            let previous = self.assign(*cell, target_station);
2499            if previous != Some(target_station) {
2500                moved_cells.push(*cell);
2501            }
2502        }
2503        CellOwnershipUpdate {
2504            source_station: proposal.source_station,
2505            target_station,
2506            moved_cells,
2507        }
2508    }
2509
2510    /// Number of explicitly assigned cells.
2511    pub fn len(&self) -> usize {
2512        self.owners.len()
2513    }
2514
2515    /// Returns whether no cells are explicitly assigned.
2516    pub fn is_empty(&self) -> bool {
2517        self.owners.is_empty()
2518    }
2519}
2520
2521/// Result of applying cell ownership changes.
2522#[derive(Clone, Debug, Default, PartialEq, Eq)]
2523pub struct CellOwnershipUpdate {
2524    /// Previous/source station.
2525    pub source_station: StationId,
2526    /// New/target station.
2527    pub target_station: StationId,
2528    /// Cells whose owner changed.
2529    pub moved_cells: Vec<CellCoord3>,
2530}
2531
2532/// Result of migrating entities indexed by moved cells.
2533#[derive(Clone, Debug, Default, PartialEq)]
2534pub struct CellMigrationReport {
2535    /// Source station.
2536    pub source_station: StationId,
2537    /// Target station.
2538    pub target_station: StationId,
2539    /// Cells scanned for owner entities.
2540    pub scanned_cells: Vec<CellCoord3>,
2541    /// Entity migrations that were committed.
2542    pub entity_migrations: Vec<EntityMigrationReport>,
2543    /// Candidate handles that no longer resolved to an entity.
2544    pub skipped_missing_handles: usize,
2545    /// Candidate entities skipped because they were ghosts or non-authoritative.
2546    pub skipped_non_owned: usize,
2547    /// Duplicate candidate entities skipped after first occurrence.
2548    pub skipped_duplicate_entities: usize,
2549}
2550
2551/// Cell-level migration error.
2552#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2553pub enum CellMigrationError {
2554    /// Entity migration failed.
2555    Entity(EntityMigrationError),
2556    /// Target owner record was not found after a successful migration.
2557    MissingTargetRecord(EntityId),
2558    /// Source ghost record was not found after a successful migration.
2559    MissingSourceRecord(EntityId),
2560}
2561
2562impl core::fmt::Display for CellMigrationError {
2563    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2564        match self {
2565            Self::Entity(error) => write!(f, "{error}"),
2566            Self::MissingTargetRecord(id) => {
2567                write!(f, "target owner record for entity {} is missing", id.get())
2568            }
2569            Self::MissingSourceRecord(id) => {
2570                write!(f, "source ghost record for entity {} is missing", id.get())
2571            }
2572        }
2573    }
2574}
2575
2576impl std::error::Error for CellMigrationError {}
2577
2578impl From<EntityMigrationError> for CellMigrationError {
2579    fn from(value: EntityMigrationError) -> Self {
2580        Self::Entity(value)
2581    }
2582}
2583
2584/// Executes cell-level ownership migration using station-local indexes.
2585#[derive(Clone, Copy, Debug, Default)]
2586pub struct CellMigrationExecutor;
2587
2588impl CellMigrationExecutor {
2589    /// Migrates owned entities found in `cells` from source station to target station.
2590    pub fn migrate_cells(
2591        stations: &mut StationSet,
2592        source_index: &mut CellIndex,
2593        target_index: &mut CellIndex,
2594        source_station: StationId,
2595        target_station: StationId,
2596        cells: &[CellCoord3],
2597        ghost_ttl_ticks: u64,
2598    ) -> Result<CellMigrationReport, CellMigrationError> {
2599        let mut report = CellMigrationReport {
2600            source_station,
2601            target_station,
2602            scanned_cells: cells.to_vec(),
2603            ..CellMigrationReport::default()
2604        };
2605        let mut seen_handles = BTreeSet::new();
2606        let mut entity_ids = Vec::new();
2607
2608        {
2609            let source = stations
2610                .get(source_station)
2611                .ok_or(EntityMigrationError::MissingSource(source_station))?;
2612            for cell in cells {
2613                for handle in source_index.handles_in_cell(*cell) {
2614                    if !seen_handles.insert(handle) {
2615                        report.skipped_duplicate_entities += 1;
2616                        continue;
2617                    }
2618                    let Some(record) = source.get(handle) else {
2619                        report.skipped_missing_handles += 1;
2620                        continue;
2621                    };
2622                    if record.is_owned() {
2623                        entity_ids.push(record.id);
2624                    } else {
2625                        report.skipped_non_owned += 1;
2626                    }
2627                }
2628            }
2629        }
2630
2631        let mut seen_entities = BTreeSet::new();
2632        for entity_id in entity_ids {
2633            if !seen_entities.insert(entity_id) {
2634                report.skipped_duplicate_entities += 1;
2635                continue;
2636            }
2637            let migration = EntityMigrationExecutor::migrate_entity(
2638                stations,
2639                entity_id,
2640                source_station,
2641                target_station,
2642                ghost_ttl_ticks,
2643            )?;
2644
2645            {
2646                let target = stations
2647                    .get(target_station)
2648                    .ok_or(EntityMigrationError::MissingTarget(target_station))?;
2649                let target_record = target
2650                    .get(migration.target_owner)
2651                    .ok_or(CellMigrationError::MissingTargetRecord(entity_id))?;
2652                target_index.upsert(
2653                    migration.target_owner,
2654                    target_record.position,
2655                    target_record.bounds,
2656                );
2657            }
2658
2659            {
2660                let source = stations
2661                    .get(source_station)
2662                    .ok_or(EntityMigrationError::MissingSource(source_station))?;
2663                let source_record = source
2664                    .get(migration.source_ghost)
2665                    .ok_or(CellMigrationError::MissingSourceRecord(entity_id))?;
2666                source_index.upsert(
2667                    migration.source_ghost,
2668                    source_record.position,
2669                    source_record.bounds,
2670                );
2671            }
2672
2673            report.entity_migrations.push(migration);
2674        }
2675
2676        Ok(report)
2677    }
2678}
2679
2680/// Automatic split scheduler configuration.
2681#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2682pub struct SplitSchedulerConfig {
2683    /// Hotspot thresholds.
2684    pub thresholds: HotspotThresholds,
2685    /// Maximum split actions to create per scheduling pass.
2686    pub max_actions_per_pass: usize,
2687    /// Maximum cells to move in each split action.
2688    pub max_cells_per_action: usize,
2689    /// Source ghost TTL used during migration execution.
2690    pub ghost_ttl_ticks: u64,
2691    /// Minimum load-score gap required between source and target.
2692    pub min_score_improvement: u64,
2693    /// Maximum permitted target load score after moved cell pressure is added.
2694    pub max_target_score_after_move: u64,
2695    /// Ticks a source station must wait before another split can be planned.
2696    pub split_cooldown_ticks: u64,
2697    /// Whether warm target stations may receive split cells.
2698    pub allow_warm_targets: bool,
2699}
2700
2701impl Default for SplitSchedulerConfig {
2702    fn default() -> Self {
2703        Self {
2704            thresholds: HotspotThresholds::default(),
2705            max_actions_per_pass: 4,
2706            max_cells_per_action: 4,
2707            ghost_ttl_ticks: 4,
2708            min_score_improvement: 1,
2709            max_target_score_after_move: u64::MAX,
2710            split_cooldown_ticks: 0,
2711            allow_warm_targets: true,
2712        }
2713    }
2714}
2715
2716/// One scheduled split action.
2717#[derive(Clone, Debug, PartialEq, Eq)]
2718pub struct SplitAction {
2719    /// Source station selected for split.
2720    pub source_station: StationId,
2721    /// Target station selected to receive cells.
2722    pub target_station: StationId,
2723    /// Cell split proposal.
2724    pub proposal: SplitProposal,
2725    /// Source load score observed when planning.
2726    pub source_score: u64,
2727    /// Target load score observed when planning.
2728    pub target_score: u64,
2729    /// Estimated target score after moving proposed cell pressure.
2730    pub estimated_target_score_after_move: u64,
2731}
2732
2733/// Split schedule produced from a load snapshot.
2734#[derive(Clone, Debug, Default, PartialEq, Eq)]
2735pub struct SplitSchedule {
2736    /// Hotspot decisions produced for every input station.
2737    pub decisions: Vec<HotspotDecision>,
2738    /// Actions selected for execution.
2739    pub actions: Vec<SplitAction>,
2740    /// Hot stations skipped because no distinct target existed.
2741    pub skipped_no_target: usize,
2742    /// Hot stations skipped because no cells were proposed.
2743    pub skipped_no_cells: usize,
2744    /// Hot stations skipped because source station is inside split cooldown.
2745    pub skipped_cooldown: usize,
2746    /// Hot stations skipped because all targets were too warm or hot.
2747    pub skipped_target_severity: usize,
2748    /// Hot stations skipped because target capacity would be exceeded.
2749    pub skipped_target_capacity: usize,
2750    /// Hot stations skipped because target score improvement was too small.
2751    pub skipped_insufficient_improvement: usize,
2752}
2753
2754/// Mutable planning state for conservative split scheduling.
2755#[derive(Clone, Debug, Default, PartialEq, Eq)]
2756pub struct SplitSchedulerState {
2757    last_split_at: BTreeMap<StationId, Tick>,
2758}
2759
2760impl SplitSchedulerState {
2761    /// Returns the last split tick for a source station.
2762    pub fn last_split_at(&self, station_id: StationId) -> Option<Tick> {
2763        self.last_split_at.get(&station_id).copied()
2764    }
2765
2766    /// Records one executed or externally accepted split action.
2767    pub fn record_action(&mut self, action: &SplitAction, tick: Tick) {
2768        self.last_split_at.insert(action.source_station, tick);
2769    }
2770
2771    /// Records all actions in a schedule at the same tick.
2772    pub fn record_schedule(&mut self, schedule: &SplitSchedule, tick: Tick) {
2773        for action in &schedule.actions {
2774            self.record_action(action, tick);
2775        }
2776    }
2777
2778    /// Returns whether a station is inside split cooldown.
2779    pub fn is_in_cooldown(
2780        &self,
2781        station_id: StationId,
2782        current_tick: Tick,
2783        cooldown_ticks: u64,
2784    ) -> bool {
2785        if cooldown_ticks == 0 {
2786            return false;
2787        }
2788        let Some(last_split) = self.last_split_at(station_id) else {
2789            return false;
2790        };
2791        current_tick.get().saturating_sub(last_split.get()) < cooldown_ticks
2792    }
2793}
2794
2795/// Result of executing a split schedule.
2796#[derive(Clone, Debug, Default, PartialEq)]
2797pub struct SplitScheduleExecutionReport {
2798    /// Ownership changes applied.
2799    pub ownership_updates: Vec<CellOwnershipUpdate>,
2800    /// Cell migration reports.
2801    pub cell_migrations: Vec<CellMigrationReport>,
2802}
2803
2804/// Split schedule execution error.
2805#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2806pub enum SplitScheduleExecutionError {
2807    /// Source index is missing.
2808    MissingSourceIndex(StationId),
2809    /// Target index is missing.
2810    MissingTargetIndex(StationId),
2811    /// Cell migration failed.
2812    CellMigration(CellMigrationError),
2813}
2814
2815impl core::fmt::Display for SplitScheduleExecutionError {
2816    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2817        match self {
2818            Self::MissingSourceIndex(id) => write!(f, "source index {} is missing", id.get()),
2819            Self::MissingTargetIndex(id) => write!(f, "target index {} is missing", id.get()),
2820            Self::CellMigration(error) => write!(f, "{error}"),
2821        }
2822    }
2823}
2824
2825impl std::error::Error for SplitScheduleExecutionError {}
2826
2827impl From<CellMigrationError> for SplitScheduleExecutionError {
2828    fn from(value: CellMigrationError) -> Self {
2829        Self::CellMigration(value)
2830    }
2831}
2832
2833/// Conservative automatic split scheduler.
2834#[derive(Clone, Copy, Debug)]
2835pub struct SplitScheduler {
2836    /// Scheduler configuration.
2837    pub config: SplitSchedulerConfig,
2838}
2839
2840impl SplitScheduler {
2841    /// Creates a split scheduler.
2842    pub const fn new(config: SplitSchedulerConfig) -> Self {
2843        Self { config }
2844    }
2845
2846    /// Plans split actions from station load samples.
2847    pub fn plan(&self, samples: &[StationLoadSample]) -> SplitSchedule {
2848        self.plan_with_state(samples, None, Tick::new(0))
2849    }
2850
2851    /// Plans split actions using optional cooldown state.
2852    pub fn plan_with_state(
2853        &self,
2854        samples: &[StationLoadSample],
2855        state: Option<&SplitSchedulerState>,
2856        current_tick: Tick,
2857    ) -> SplitSchedule {
2858        let decisions = samples
2859            .iter()
2860            .map(|sample| HotspotPlanner::evaluate(sample, self.config.thresholds))
2861            .collect::<Vec<_>>();
2862        let mut schedule = SplitSchedule {
2863            decisions,
2864            ..SplitSchedule::default()
2865        };
2866
2867        for source in samples {
2868            if schedule.actions.len() >= self.config.max_actions_per_pass {
2869                break;
2870            }
2871            let Some(source_decision) = schedule
2872                .decisions
2873                .iter()
2874                .find(|decision| decision.station_id == source.station_id)
2875            else {
2876                continue;
2877            };
2878            if source_decision.severity != HotspotSeverity::Hot {
2879                continue;
2880            }
2881            if state.is_some_and(|state| {
2882                state.is_in_cooldown(
2883                    source.station_id,
2884                    current_tick,
2885                    self.config.split_cooldown_ticks,
2886                )
2887            }) {
2888                schedule.skipped_cooldown += 1;
2889                continue;
2890            }
2891
2892            let proposal =
2893                HotspotPlanner::propose_cell_split(source, self.config.max_cells_per_action);
2894            if proposal.cells_to_move.is_empty() {
2895                schedule.skipped_no_cells += 1;
2896                continue;
2897            }
2898            let target_selection =
2899                select_split_target(source, &proposal, samples, &schedule.decisions, self.config);
2900            let Some(target) = target_selection.target else {
2901                if target_selection.considered_targets == 0 {
2902                    schedule.skipped_no_target += 1;
2903                } else {
2904                    schedule.skipped_target_severity +=
2905                        usize::from(target_selection.rejected_by_severity > 0);
2906                    schedule.skipped_target_capacity +=
2907                        usize::from(target_selection.rejected_by_capacity > 0);
2908                    schedule.skipped_insufficient_improvement +=
2909                        usize::from(target_selection.rejected_by_improvement > 0);
2910                }
2911                continue;
2912            };
2913            let target_score = station_load_score(target);
2914            let estimated_target_score_after_move =
2915                target_score.saturating_add(proposal.moved_pressure_score);
2916            schedule.actions.push(SplitAction {
2917                source_station: source.station_id,
2918                target_station: target.station_id,
2919                proposal,
2920                source_score: station_load_score(source),
2921                target_score,
2922                estimated_target_score_after_move,
2923            });
2924        }
2925
2926        schedule
2927    }
2928
2929    /// Executes a split schedule by applying ownership updates and migrating entities.
2930    pub fn execute(
2931        &self,
2932        schedule: &SplitSchedule,
2933        stations: &mut StationSet,
2934        indexes: &mut StationIndexSet,
2935        ownership: &mut CellOwnershipTable,
2936    ) -> Result<SplitScheduleExecutionReport, SplitScheduleExecutionError> {
2937        let mut report = SplitScheduleExecutionReport::default();
2938
2939        for action in &schedule.actions {
2940            if indexes.get(action.source_station).is_none() {
2941                return Err(SplitScheduleExecutionError::MissingSourceIndex(
2942                    action.source_station,
2943                ));
2944            }
2945            if indexes.get(action.target_station).is_none() {
2946                return Err(SplitScheduleExecutionError::MissingTargetIndex(
2947                    action.target_station,
2948                ));
2949            }
2950
2951            let update = ownership.apply_split(&action.proposal, action.target_station);
2952            let (source_index, target_index) = indexes
2953                .get_pair_mut(action.source_station, action.target_station)
2954                .expect("indexes were checked above");
2955            let migration = CellMigrationExecutor::migrate_cells(
2956                stations,
2957                source_index,
2958                target_index,
2959                action.source_station,
2960                action.target_station,
2961                &update.moved_cells,
2962                self.config.ghost_ttl_ticks,
2963            )?;
2964            report.ownership_updates.push(update);
2965            report.cell_migrations.push(migration);
2966        }
2967
2968        Ok(report)
2969    }
2970}
2971
2972impl Default for SplitScheduler {
2973    fn default() -> Self {
2974        Self::new(SplitSchedulerConfig::default())
2975    }
2976}
2977
2978#[derive(Clone, Copy, Debug, Default)]
2979struct SplitTargetSelection<'a> {
2980    target: Option<&'a StationLoadSample>,
2981    considered_targets: usize,
2982    rejected_by_severity: usize,
2983    rejected_by_capacity: usize,
2984    rejected_by_improvement: usize,
2985}
2986
2987fn select_split_target<'a>(
2988    source: &StationLoadSample,
2989    proposal: &SplitProposal,
2990    samples: &'a [StationLoadSample],
2991    decisions: &[HotspotDecision],
2992    config: SplitSchedulerConfig,
2993) -> SplitTargetSelection<'a> {
2994    let mut selection = SplitTargetSelection::default();
2995    let source_score = station_load_score(source);
2996
2997    for target in samples {
2998        if target.station_id == source.station_id {
2999            continue;
3000        }
3001        selection.considered_targets += 1;
3002
3003        let severity = decisions
3004            .iter()
3005            .find(|decision| decision.station_id == target.station_id)
3006            .map_or(HotspotSeverity::Normal, |decision| decision.severity);
3007        if severity == HotspotSeverity::Hot
3008            || (severity == HotspotSeverity::Warm && !config.allow_warm_targets)
3009        {
3010            selection.rejected_by_severity += 1;
3011            continue;
3012        }
3013
3014        let target_score = station_load_score(target);
3015        if source_score.saturating_sub(target_score) < config.min_score_improvement {
3016            selection.rejected_by_improvement += 1;
3017            continue;
3018        }
3019        if target_score.saturating_add(proposal.moved_pressure_score)
3020            > config.max_target_score_after_move
3021        {
3022            selection.rejected_by_capacity += 1;
3023            continue;
3024        }
3025
3026        let target_key = (
3027            severity_rank(severity),
3028            target_score,
3029            target.station_id.get(),
3030        );
3031        let current_key = selection.target.map(|current| {
3032            let current_severity = decisions
3033                .iter()
3034                .find(|decision| decision.station_id == current.station_id)
3035                .map_or(HotspotSeverity::Normal, |decision| decision.severity);
3036            (
3037                severity_rank(current_severity),
3038                station_load_score(current),
3039                current.station_id.get(),
3040            )
3041        });
3042        if current_key.is_none_or(|current_key| target_key < current_key) {
3043            selection.target = Some(target);
3044        }
3045    }
3046
3047    selection
3048}
3049
3050fn severity_rank(severity: HotspotSeverity) -> u8 {
3051    match severity {
3052        HotspotSeverity::Normal => 0,
3053        HotspotSeverity::Warm => 1,
3054        HotspotSeverity::Hot => 2,
3055    }
3056}
3057
3058fn station_load_score(sample: &StationLoadSample) -> u64 {
3059    (sample.total_entities() as u64)
3060        .saturating_mul(8)
3061        .saturating_add((sample.subscribers as u64).saturating_mul(4))
3062        .saturating_add(sample.queued_events as u64)
3063        .saturating_add((sample.estimated_bytes / 256) as u64)
3064        .saturating_add(sample.tick_cost_units)
3065}
3066
3067/// Event router statistics.
3068#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3069pub struct EventRouterStats {
3070    /// Events accepted by target queues.
3071    pub routed_events: usize,
3072    /// Ready events drained for station application.
3073    pub drained_events: usize,
3074    /// Best-effort events dropped by bounded target queues.
3075    pub dropped_best_effort_events: usize,
3076}
3077
3078/// Event router error.
3079#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3080pub enum EventRouterError {
3081    /// Target station was not registered with the router.
3082    MissingTarget(StationId),
3083    /// Underlying target queue rejected the event.
3084    Queue(EventQueueError),
3085}
3086
3087impl core::fmt::Display for EventRouterError {
3088    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3089        match self {
3090            Self::MissingTarget(id) => write!(f, "event target station {} is missing", id.get()),
3091            Self::Queue(error) => write!(f, "{error}"),
3092        }
3093    }
3094}
3095
3096impl std::error::Error for EventRouterError {}
3097
3098impl From<EventQueueError> for EventRouterError {
3099    fn from(value: EventQueueError) -> Self {
3100        Self::Queue(value)
3101    }
3102}
3103
3104/// In-process station event router.
3105#[derive(Clone, Debug)]
3106pub struct EventRouter {
3107    limits: EventQueueLimits,
3108    queues: BTreeMap<StationId, EventQueues>,
3109    stats: EventRouterStats,
3110}
3111
3112impl EventRouter {
3113    /// Creates an empty event router.
3114    pub fn new(limits: EventQueueLimits) -> Self {
3115        Self {
3116            limits,
3117            queues: BTreeMap::new(),
3118            stats: EventRouterStats::default(),
3119        }
3120    }
3121
3122    /// Registers a station target queue.
3123    pub fn register_station(&mut self, station_id: StationId) {
3124        self.queues
3125            .entry(station_id)
3126            .or_insert_with(|| EventQueues::new(self.limits));
3127    }
3128
3129    /// Registers all stations in a set.
3130    pub fn register_stations(&mut self, stations: &StationSet) {
3131        for station in stations.iter() {
3132            self.register_station(station.config().station_id);
3133        }
3134    }
3135
3136    /// Routes an event to its target station queue.
3137    pub fn route(&mut self, event: StationEvent) -> Result<PushOutcome, EventRouterError> {
3138        let queue = self
3139            .queues
3140            .get_mut(&event.target)
3141            .ok_or(EventRouterError::MissingTarget(event.target))?;
3142        let outcome = queue.push(event)?;
3143        self.stats.routed_events += 1;
3144        if outcome == PushOutcome::DroppedOldestBestEffort {
3145            self.stats.dropped_best_effort_events += 1;
3146        }
3147        Ok(outcome)
3148    }
3149
3150    /// Drains events whose `target_tick` is ready for application.
3151    pub fn drain_ready(
3152        &mut self,
3153        station_id: StationId,
3154        current_tick: Tick,
3155    ) -> Result<Vec<StationEvent>, EventRouterError> {
3156        let queue = self
3157            .queues
3158            .get_mut(&station_id)
3159            .ok_or(EventRouterError::MissingTarget(station_id))?;
3160        let mut ready = Vec::new();
3161        let mut delayed = Vec::new();
3162
3163        while let Some(event) = queue.pop_next() {
3164            if event.target_tick <= current_tick {
3165                ready.push(event);
3166            } else {
3167                delayed.push(event);
3168            }
3169        }
3170
3171        for event in delayed {
3172            queue.push(event)?;
3173        }
3174        self.stats.drained_events += ready.len();
3175        Ok(ready)
3176    }
3177
3178    /// Returns queued event count for one station.
3179    pub fn queued_len(&self, station_id: StationId) -> Option<usize> {
3180        self.queues.get(&station_id).map(EventQueues::len)
3181    }
3182
3183    /// Returns router statistics.
3184    pub const fn stats(&self) -> EventRouterStats {
3185        self.stats
3186    }
3187}
3188
3189impl Default for EventRouter {
3190    fn default() -> Self {
3191        Self::new(EventQueueLimits::default())
3192    }
3193}
3194
3195/// Statistics for station event transport bridging.
3196#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3197pub struct StationEventTransportStats {
3198    /// Events encoded and submitted to station transport.
3199    pub events_sent: usize,
3200    /// Bytes submitted to station transport.
3201    pub bytes_sent: usize,
3202    /// Packets received from station transport.
3203    pub packets_received: usize,
3204    /// Bytes received from station transport.
3205    pub bytes_received: usize,
3206    /// Events decoded and accepted by the target router.
3207    pub events_routed: usize,
3208}
3209
3210/// Result of pumping station event packets for one target.
3211#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3212pub struct StationEventPumpReport {
3213    /// Target station pumped.
3214    pub target_station: StationId,
3215    /// Packets consumed from the station transport.
3216    pub packets_received: usize,
3217    /// Bytes consumed from the station transport.
3218    pub bytes_received: usize,
3219    /// Events accepted by the target router.
3220    pub events_routed: usize,
3221}
3222
3223/// Error produced while bridging station events through packet transport.
3224#[derive(Clone, Debug, PartialEq, Eq)]
3225pub enum StationEventTransportError<E> {
3226    /// Underlying station transport failed.
3227    Transport(E),
3228    /// Wire encoding failed.
3229    Encode(BinaryEncodeError),
3230    /// Wire decoding failed.
3231    Decode(BinaryDecodeError),
3232    /// Packet decoded as a non-event frame.
3233    UnexpectedFrame,
3234    /// Packet envelope and decoded event disagreed about endpoints.
3235    EndpointMismatch {
3236        /// Packet source station.
3237        packet_source: StationId,
3238        /// Packet target station.
3239        packet_target: StationId,
3240        /// Decoded event source station.
3241        event_source: StationId,
3242        /// Decoded event target station.
3243        event_target: StationId,
3244    },
3245    /// Event router rejected the decoded event.
3246    Router(EventRouterError),
3247}
3248
3249impl<E: core::fmt::Display> core::fmt::Display for StationEventTransportError<E> {
3250    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3251        match self {
3252            Self::Transport(error) => write!(f, "{error}"),
3253            Self::Encode(error) => write!(f, "{error}"),
3254            Self::Decode(error) => write!(f, "{error}"),
3255            Self::UnexpectedFrame => f.write_str("station transport packet was not an event frame"),
3256            Self::EndpointMismatch {
3257                packet_source,
3258                packet_target,
3259                event_source,
3260                event_target,
3261            } => write!(
3262                f,
3263                "station event endpoint mismatch: packet {}->{}, event {}->{}",
3264                packet_source.get(),
3265                packet_target.get(),
3266                event_source.get(),
3267                event_target.get()
3268            ),
3269            Self::Router(error) => write!(f, "{error}"),
3270        }
3271    }
3272}
3273
3274impl<E> std::error::Error for StationEventTransportError<E>
3275where
3276    E: std::error::Error + 'static,
3277{
3278    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
3279        match self {
3280            Self::Transport(error) => Some(error),
3281            Self::Encode(error) => Some(error),
3282            Self::Decode(error) => Some(error),
3283            Self::UnexpectedFrame | Self::EndpointMismatch { .. } => None,
3284            Self::Router(error) => Some(error),
3285        }
3286    }
3287}
3288
3289impl<E> From<BinaryEncodeError> for StationEventTransportError<E> {
3290    fn from(value: BinaryEncodeError) -> Self {
3291        Self::Encode(value)
3292    }
3293}
3294
3295impl<E> From<BinaryDecodeError> for StationEventTransportError<E> {
3296    fn from(value: BinaryDecodeError) -> Self {
3297        Self::Decode(value)
3298    }
3299}
3300
3301impl<E> From<EventRouterError> for StationEventTransportError<E> {
3302    fn from(value: EventRouterError) -> Self {
3303        Self::Router(value)
3304    }
3305}
3306
3307/// Bridge between typed station events and bounded station packet transport.
3308#[derive(Clone, Debug, Default)]
3309pub struct StationEventTransportBridge {
3310    stats: StationEventTransportStats,
3311}
3312
3313impl StationEventTransportBridge {
3314    /// Returns bridge statistics.
3315    pub const fn stats(&self) -> StationEventTransportStats {
3316        self.stats
3317    }
3318
3319    /// Encodes and sends one station event through the station transport.
3320    pub fn send_event<T>(
3321        &mut self,
3322        transport: &mut T,
3323        event: &StationEvent,
3324    ) -> Result<(), StationEventTransportError<T::Error>>
3325    where
3326        T: StationTransportSink,
3327    {
3328        let frame = StationEventFrame::from_event(event);
3329        let mut bytes = Vec::with_capacity(64);
3330        BinaryFrameEncoder.encode_station_event(&frame, &mut bytes)?;
3331        let byte_len = bytes.len();
3332        transport
3333            .send_station(StationOutboundPacket {
3334                source_station: event.source,
3335                target_station: event.target,
3336                bytes,
3337            })
3338            .map_err(StationEventTransportError::Transport)?;
3339        self.stats.events_sent = self.stats.events_sent.saturating_add(1);
3340        self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(byte_len);
3341        Ok(())
3342    }
3343
3344    /// Receives up to `max_packets` for `target_station`, decodes station
3345    /// events, and routes them into `router`.
3346    pub fn pump_target<T>(
3347        &mut self,
3348        transport: &mut T,
3349        router: &mut EventRouter,
3350        target_station: StationId,
3351        max_packets: usize,
3352    ) -> Result<StationEventPumpReport, StationEventTransportError<T::Error>>
3353    where
3354        T: StationTransportReceiver,
3355    {
3356        let mut report = StationEventPumpReport {
3357            target_station,
3358            ..StationEventPumpReport::default()
3359        };
3360        for _ in 0..max_packets {
3361            let Some(packet) = transport
3362                .try_recv_station(target_station)
3363                .map_err(StationEventTransportError::Transport)?
3364            else {
3365                break;
3366            };
3367            report.packets_received = report.packets_received.saturating_add(1);
3368            report.bytes_received = report.bytes_received.saturating_add(packet.bytes.len());
3369
3370            let decoded = BinaryFrameDecoder.decode(&packet.bytes)?;
3371            let RuntimeFrame::StationEvent(frame) = decoded else {
3372                return Err(StationEventTransportError::UnexpectedFrame);
3373            };
3374            if frame.source_station != packet.source_station
3375                || frame.target_station != packet.target_station
3376            {
3377                return Err(StationEventTransportError::EndpointMismatch {
3378                    packet_source: packet.source_station,
3379                    packet_target: packet.target_station,
3380                    event_source: frame.source_station,
3381                    event_target: frame.target_station,
3382                });
3383            }
3384
3385            router.route(frame.into_event())?;
3386            report.events_routed = report.events_routed.saturating_add(1);
3387        }
3388
3389        self.stats.packets_received = self
3390            .stats
3391            .packets_received
3392            .saturating_add(report.packets_received);
3393        self.stats.bytes_received = self
3394            .stats
3395            .bytes_received
3396            .saturating_add(report.bytes_received);
3397        self.stats.events_routed = self
3398            .stats
3399            .events_routed
3400            .saturating_add(report.events_routed);
3401        Ok(report)
3402    }
3403}
3404
3405/// Statistics for command dispatch transport bridging.
3406#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3407pub struct CommandDispatchTransportStats {
3408    /// Commands encoded and submitted to station transport.
3409    pub commands_sent: usize,
3410    /// Bytes submitted to station transport.
3411    pub bytes_sent: usize,
3412    /// Packets received from station transport.
3413    pub packets_received: usize,
3414    /// Bytes received from station transport.
3415    pub bytes_received: usize,
3416    /// Commands decoded and enqueued at the target station.
3417    pub commands_enqueued: usize,
3418    /// Commands rejected by target station queues.
3419    pub commands_rejected_queue: usize,
3420}
3421
3422/// Result of pumping command dispatch packets for one target.
3423#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3424pub struct CommandDispatchPumpReport {
3425    /// Target station pumped.
3426    pub target_station: StationId,
3427    /// Packets consumed from station transport.
3428    pub packets_received: usize,
3429    /// Bytes consumed from station transport.
3430    pub bytes_received: usize,
3431    /// Commands enqueued into the target queue.
3432    pub commands_enqueued: usize,
3433}
3434
3435/// Error produced while bridging command dispatch frames through station packet transport.
3436#[derive(Clone, Debug, PartialEq, Eq)]
3437pub enum CommandDispatchTransportError<E> {
3438    /// Underlying station transport failed.
3439    Transport(E),
3440    /// Wire encoding failed.
3441    Encode(BinaryEncodeError),
3442    /// Wire decoding failed.
3443    Decode(BinaryDecodeError),
3444    /// Packet decoded as a non-command-dispatch frame.
3445    UnexpectedFrame,
3446    /// Packet envelope and decoded dispatch frame disagreed about the target.
3447    EndpointMismatch {
3448        /// Packet source station.
3449        packet_source: StationId,
3450        /// Packet target station.
3451        packet_target: StationId,
3452        /// Decoded command dispatch target station.
3453        dispatch_target: StationId,
3454    },
3455    /// Target station queue was not registered.
3456    MissingQueue(StationId),
3457    /// Target station queue rejected the command.
3458    Queue(CommandQueueError),
3459}
3460
3461impl<E: core::fmt::Display> core::fmt::Display for CommandDispatchTransportError<E> {
3462    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3463        match self {
3464            Self::Transport(error) => write!(f, "{error}"),
3465            Self::Encode(error) => write!(f, "{error}"),
3466            Self::Decode(error) => write!(f, "{error}"),
3467            Self::UnexpectedFrame => {
3468                f.write_str("station transport packet was not a command dispatch frame")
3469            }
3470            Self::EndpointMismatch {
3471                packet_source,
3472                packet_target,
3473                dispatch_target,
3474            } => write!(
3475                f,
3476                "command dispatch endpoint mismatch: packet {}->{}, dispatch target {}",
3477                packet_source.get(),
3478                packet_target.get(),
3479                dispatch_target.get()
3480            ),
3481            Self::MissingQueue(station_id) => {
3482                write!(
3483                    f,
3484                    "command dispatch target station {} has no queue",
3485                    station_id.get()
3486                )
3487            }
3488            Self::Queue(error) => write!(f, "{error}"),
3489        }
3490    }
3491}
3492
3493impl<E> std::error::Error for CommandDispatchTransportError<E>
3494where
3495    E: std::error::Error + 'static,
3496{
3497    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
3498        match self {
3499            Self::Transport(error) => Some(error),
3500            Self::Encode(error) => Some(error),
3501            Self::Decode(error) => Some(error),
3502            Self::UnexpectedFrame | Self::EndpointMismatch { .. } | Self::MissingQueue(_) => None,
3503            Self::Queue(error) => Some(error),
3504        }
3505    }
3506}
3507
3508impl<E> From<BinaryEncodeError> for CommandDispatchTransportError<E> {
3509    fn from(value: BinaryEncodeError) -> Self {
3510        Self::Encode(value)
3511    }
3512}
3513
3514impl<E> From<BinaryDecodeError> for CommandDispatchTransportError<E> {
3515    fn from(value: BinaryDecodeError) -> Self {
3516        Self::Decode(value)
3517    }
3518}
3519
3520impl<E> From<CommandQueueError> for CommandDispatchTransportError<E> {
3521    fn from(value: CommandQueueError) -> Self {
3522        Self::Queue(value)
3523    }
3524}
3525
3526/// Bridge between stamped command envelopes and bounded station packet transport.
3527#[derive(Clone, Debug, Default)]
3528pub struct CommandDispatchTransportBridge {
3529    stats: CommandDispatchTransportStats,
3530}
3531
3532impl CommandDispatchTransportBridge {
3533    /// Returns bridge statistics.
3534    pub const fn stats(&self) -> CommandDispatchTransportStats {
3535        self.stats
3536    }
3537
3538    /// Encodes and sends a stamped command envelope to a station node.
3539    pub fn send_envelope<T>(
3540        &mut self,
3541        transport: &mut T,
3542        source_station: StationId,
3543        target_station: StationId,
3544        command: &CommandEnvelope,
3545    ) -> Result<(), CommandDispatchTransportError<T::Error>>
3546    where
3547        T: StationTransportSink,
3548    {
3549        let frame = CommandDispatchFrame::from_envelope(target_station, command);
3550        self.send_frame(transport, source_station, &frame)
3551    }
3552
3553    /// Encodes and sends a command dispatch frame to its target station.
3554    pub fn send_frame<T>(
3555        &mut self,
3556        transport: &mut T,
3557        source_station: StationId,
3558        frame: &CommandDispatchFrame,
3559    ) -> Result<(), CommandDispatchTransportError<T::Error>>
3560    where
3561        T: StationTransportSink,
3562    {
3563        let mut bytes = Vec::with_capacity(64);
3564        BinaryFrameEncoder.encode_command_dispatch(frame, &mut bytes)?;
3565        let byte_len = bytes.len();
3566        transport
3567            .send_station(StationOutboundPacket {
3568                source_station,
3569                target_station: frame.station_id,
3570                bytes,
3571            })
3572            .map_err(CommandDispatchTransportError::Transport)?;
3573        self.stats.commands_sent = self.stats.commands_sent.saturating_add(1);
3574        self.stats.bytes_sent = self.stats.bytes_sent.saturating_add(byte_len);
3575        Ok(())
3576    }
3577
3578    /// Receives up to `max_packets` for `target_station`, decodes command
3579    /// dispatch frames, and enqueues stamped commands into `station_queues`.
3580    pub fn pump_target<T>(
3581        &mut self,
3582        transport: &mut T,
3583        station_queues: &mut BTreeMap<StationId, CommandQueues>,
3584        target_station: StationId,
3585        max_packets: usize,
3586        ingress: CommandIngress,
3587    ) -> Result<CommandDispatchPumpReport, CommandDispatchTransportError<T::Error>>
3588    where
3589        T: StationTransportReceiver,
3590    {
3591        let mut report = CommandDispatchPumpReport {
3592            target_station,
3593            ..CommandDispatchPumpReport::default()
3594        };
3595        for _ in 0..max_packets {
3596            let Some(packet) = transport
3597                .try_recv_station(target_station)
3598                .map_err(CommandDispatchTransportError::Transport)?
3599            else {
3600                break;
3601            };
3602            report.packets_received = report.packets_received.saturating_add(1);
3603            report.bytes_received = report.bytes_received.saturating_add(packet.bytes.len());
3604
3605            let decoded = BinaryFrameDecoder.decode(&packet.bytes)?;
3606            let RuntimeFrame::CommandDispatch(frame) = decoded else {
3607                return Err(CommandDispatchTransportError::UnexpectedFrame);
3608            };
3609            if frame.station_id != packet.target_station {
3610                return Err(CommandDispatchTransportError::EndpointMismatch {
3611                    packet_source: packet.source_station,
3612                    packet_target: packet.target_station,
3613                    dispatch_target: frame.station_id,
3614                });
3615            }
3616
3617            let queue = station_queues.get_mut(&frame.station_id).ok_or(
3618                CommandDispatchTransportError::MissingQueue(frame.station_id),
3619            )?;
3620            match queue.push(frame.into_envelope(), ingress) {
3621                Ok(_) => {
3622                    report.commands_enqueued = report.commands_enqueued.saturating_add(1);
3623                }
3624                Err(error) => {
3625                    self.stats.commands_rejected_queue =
3626                        self.stats.commands_rejected_queue.saturating_add(1);
3627                    return Err(CommandDispatchTransportError::Queue(error));
3628                }
3629            }
3630        }
3631
3632        self.stats.packets_received = self
3633            .stats
3634            .packets_received
3635            .saturating_add(report.packets_received);
3636        self.stats.bytes_received = self
3637            .stats
3638            .bytes_received
3639            .saturating_add(report.bytes_received);
3640        self.stats.commands_enqueued = self
3641            .stats
3642            .commands_enqueued
3643            .saturating_add(report.commands_enqueued);
3644        Ok(report)
3645    }
3646}
3647
3648/// Budget for a load-aware scheduler step.
3649#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3650pub struct StationScheduleConfig {
3651    /// Maximum stations that may advance during one scheduler step.
3652    pub max_station_advances_per_step: usize,
3653}
3654
3655impl Default for StationScheduleConfig {
3656    fn default() -> Self {
3657        Self {
3658            max_station_advances_per_step: usize::MAX,
3659        }
3660    }
3661}
3662
3663/// Candidate selected by the load-aware station scheduler.
3664#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3665pub struct StationScheduleCandidate {
3666    /// Station selected for advancement.
3667    pub station_id: StationId,
3668    /// Deterministic pressure score derived from the latest load sample.
3669    pub load_score: u64,
3670    /// How far this station is behind the most advanced station in the set.
3671    pub tick_lag: u64,
3672}
3673
3674/// Result of one load-aware station scheduling pass.
3675#[derive(Clone, Debug, Default, PartialEq, Eq)]
3676pub struct StationSchedulePlan {
3677    /// Stations considered by this pass.
3678    pub candidates_considered: usize,
3679    /// Stations selected by this pass.
3680    pub stations_selected: usize,
3681    /// Station tick advances requested by this pass.
3682    pub total_advances: usize,
3683    /// Selected stations in deterministic execution order.
3684    pub selected: Vec<StationScheduleCandidate>,
3685}
3686
3687/// Basic in-process station scheduler.
3688#[derive(Clone, Debug, Default)]
3689pub struct StationScheduler {
3690    /// Total station ticks advanced by this scheduler.
3691    pub advanced_ticks: u64,
3692}
3693
3694impl StationScheduler {
3695    /// Advances every station by one tick.
3696    pub fn advance_all(&mut self, stations: &mut StationSet) {
3697        for station in stations.iter_mut() {
3698            station.advance_tick();
3699            self.advanced_ticks = self.advanced_ticks.saturating_add(1);
3700        }
3701    }
3702
3703    /// Plans a bounded station advancement pass from load samples.
3704    pub fn plan_loaded(
3705        &self,
3706        stations: &StationSet,
3707        samples: &[StationLoadSample],
3708        config: StationScheduleConfig,
3709    ) -> StationSchedulePlan {
3710        let candidates_considered = stations.len();
3711        let limit = config
3712            .max_station_advances_per_step
3713            .min(candidates_considered);
3714        let max_tick = stations
3715            .iter()
3716            .map(|station| station.tick().get())
3717            .max()
3718            .unwrap_or(0);
3719        let samples_by_station = samples
3720            .iter()
3721            .map(|sample| (sample.station_id, sample))
3722            .collect::<BTreeMap<_, _>>();
3723        let mut selected = stations
3724            .iter()
3725            .map(|station| {
3726                let station_id = station.config().station_id;
3727                let load_score = samples_by_station
3728                    .get(&station_id)
3729                    .map_or(0, |sample| station_schedule_score(sample));
3730                StationScheduleCandidate {
3731                    station_id,
3732                    load_score,
3733                    tick_lag: max_tick.saturating_sub(station.tick().get()),
3734                }
3735            })
3736            .collect::<Vec<_>>();
3737
3738        selected.sort_by(|left, right| {
3739            right
3740                .load_score
3741                .cmp(&left.load_score)
3742                .then_with(|| right.tick_lag.cmp(&left.tick_lag))
3743                .then_with(|| left.station_id.cmp(&right.station_id))
3744        });
3745        selected.truncate(limit);
3746
3747        StationSchedulePlan {
3748            candidates_considered,
3749            stations_selected: selected.len(),
3750            total_advances: selected.len(),
3751            selected,
3752        }
3753    }
3754
3755    /// Advances a bounded set of high-load stations by one tick each.
3756    pub fn advance_loaded(
3757        &mut self,
3758        stations: &mut StationSet,
3759        samples: &[StationLoadSample],
3760        config: StationScheduleConfig,
3761    ) -> StationSchedulePlan {
3762        let plan = self.plan_loaded(stations, samples, config);
3763        for candidate in &plan.selected {
3764            if let Some(station) = stations.get_mut(candidate.station_id) {
3765                station.advance_tick();
3766                self.advanced_ticks = self.advanced_ticks.saturating_add(1);
3767            }
3768        }
3769        plan
3770    }
3771
3772    /// Drains router events ready for each station's current tick.
3773    pub fn drain_ready_events(
3774        &mut self,
3775        stations: &StationSet,
3776        router: &mut EventRouter,
3777    ) -> Result<Vec<StationEvent>, EventRouterError> {
3778        let mut events = Vec::new();
3779        for station in stations.iter() {
3780            events.extend(router.drain_ready(station.config().station_id, station.tick())?);
3781        }
3782        Ok(events)
3783    }
3784}
3785
3786fn station_schedule_score(sample: &StationLoadSample) -> u64 {
3787    station_load_score(sample).saturating_add(sample.max_cell_pressure())
3788}
3789
3790/// Per-station progress inside a full runtime barrier.
3791#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3792pub enum StationBarrierPhase {
3793    /// Station is part of the barrier but has not reached the target tick.
3794    WaitingTick,
3795    /// Station reached the target tick and is frozen.
3796    Frozen,
3797    /// Station has resumed.
3798    Resumed,
3799}
3800
3801/// Barrier progress summary.
3802#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3803pub struct BarrierProgress {
3804    /// Barrier state.
3805    pub state: BarrierState,
3806    /// Number of stations covered by the barrier.
3807    pub station_count: usize,
3808    /// Number of stations frozen.
3809    pub frozen_count: usize,
3810    /// Target tick selected for the barrier.
3811    pub target_tick: Tick,
3812}
3813
3814/// Runtime barrier metrics.
3815#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3816pub struct BarrierMetrics {
3817    /// Number of stations covered by this barrier.
3818    pub station_count: usize,
3819    /// Number of snapshots exported while frozen.
3820    pub snapshots_exported: usize,
3821    /// Number of times polling observed at least one station still waiting.
3822    pub waiting_polls: u64,
3823    /// Number of times polling observed a fully frozen barrier.
3824    pub frozen_polls: u64,
3825}
3826
3827/// Runtime barrier execution error.
3828#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3829pub enum BarrierRuntimeError {
3830    /// A barrier is already active.
3831    AlreadyActive(BarrierId),
3832    /// No barrier is active.
3833    NoActiveBarrier,
3834    /// Barrier scope matched no stations.
3835    EmptyScope(BarrierScope),
3836    /// Requested operation requires frozen state.
3837    NotFrozen(BarrierState),
3838    /// A station covered by the barrier is missing.
3839    MissingStation(StationId),
3840}
3841
3842impl core::fmt::Display for BarrierRuntimeError {
3843    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3844        match self {
3845            Self::AlreadyActive(id) => write!(f, "barrier {} is already active", id.get()),
3846            Self::NoActiveBarrier => f.write_str("no active barrier"),
3847            Self::EmptyScope(scope) => write!(f, "barrier scope {scope:?} contains no stations"),
3848            Self::NotFrozen(state) => {
3849                write!(f, "barrier operation requires Frozen state, got {state:?}")
3850            }
3851            Self::MissingStation(id) => write!(f, "barrier station {} is missing", id.get()),
3852        }
3853    }
3854}
3855
3856impl std::error::Error for BarrierRuntimeError {}
3857
3858/// Full runtime barrier executor for in-process station sets.
3859#[derive(Clone, Debug, Default)]
3860pub struct BarrierController {
3861    active: Option<RuntimeBarrier>,
3862    phases: BTreeMap<StationId, StationBarrierPhase>,
3863    metrics: BarrierMetrics,
3864}
3865
3866impl BarrierController {
3867    /// Returns the active barrier, if any.
3868    pub const fn active(&self) -> Option<RuntimeBarrier> {
3869        self.active
3870    }
3871
3872    /// Requests a barrier over stations matching `scope`.
3873    pub fn request(
3874        &mut self,
3875        stations: &StationSet,
3876        id: BarrierId,
3877        scope: BarrierScope,
3878        target_tick: Tick,
3879        command_mode: CommandQueueMode,
3880    ) -> Result<BarrierProgress, BarrierRuntimeError> {
3881        if let Some(active) = self.active {
3882            return Err(BarrierRuntimeError::AlreadyActive(active.id));
3883        }
3884
3885        let station_ids = stations.station_ids_in_scope(scope);
3886        if station_ids.is_empty() {
3887            return Err(BarrierRuntimeError::EmptyScope(scope));
3888        }
3889
3890        let requested_at = station_ids
3891            .iter()
3892            .filter_map(|station_id| stations.get(*station_id).map(Station::tick))
3893            .map(Tick::get)
3894            .max()
3895            .map_or(Tick::new(0), Tick::new);
3896
3897        let mut barrier =
3898            RuntimeBarrier::requested(id, scope, requested_at, target_tick, command_mode);
3899        barrier.wait_for_tick_boundary();
3900
3901        self.metrics = BarrierMetrics {
3902            station_count: station_ids.len(),
3903            ..BarrierMetrics::default()
3904        };
3905        self.phases.clear();
3906        for station_id in station_ids {
3907            self.phases
3908                .insert(station_id, StationBarrierPhase::WaitingTick);
3909        }
3910        self.active = Some(barrier);
3911
3912        Ok(self.progress())
3913    }
3914
3915    /// Polls station ticks and freezes the barrier once all covered stations are aligned.
3916    pub fn poll(&mut self, stations: &StationSet) -> Result<BarrierProgress, BarrierRuntimeError> {
3917        let Some(mut barrier) = self.active else {
3918            return Err(BarrierRuntimeError::NoActiveBarrier);
3919        };
3920
3921        if matches!(barrier.state, BarrierState::Frozen) {
3922            self.metrics.frozen_polls = self.metrics.frozen_polls.saturating_add(1);
3923            return Ok(self.progress());
3924        }
3925
3926        let mut all_ready = true;
3927        for (station_id, phase) in &mut self.phases {
3928            let station = stations
3929                .get(*station_id)
3930                .ok_or(BarrierRuntimeError::MissingStation(*station_id))?;
3931            if station.tick() >= barrier.target_tick {
3932                *phase = StationBarrierPhase::Frozen;
3933            } else {
3934                all_ready = false;
3935            }
3936        }
3937
3938        if all_ready {
3939            barrier.freeze();
3940            self.active = Some(barrier);
3941            self.metrics.frozen_polls = self.metrics.frozen_polls.saturating_add(1);
3942        } else {
3943            self.metrics.waiting_polls = self.metrics.waiting_polls.saturating_add(1);
3944        }
3945
3946        Ok(self.progress())
3947    }
3948
3949    /// Exports station snapshots while the barrier is frozen.
3950    pub fn export_snapshots(
3951        &mut self,
3952        stations: &StationSet,
3953        version: SnapshotVersion,
3954    ) -> Result<Vec<StationSnapshot>, BarrierRuntimeError> {
3955        let barrier = self.active.ok_or(BarrierRuntimeError::NoActiveBarrier)?;
3956        if barrier.state != BarrierState::Frozen {
3957            return Err(BarrierRuntimeError::NotFrozen(barrier.state));
3958        }
3959
3960        let mut snapshots = Vec::with_capacity(self.phases.len());
3961        for station_id in self.phases.keys().copied() {
3962            let station = stations
3963                .get(station_id)
3964                .ok_or(BarrierRuntimeError::MissingStation(station_id))?;
3965            snapshots.push(station.snapshot(version));
3966        }
3967        self.metrics.snapshots_exported = self
3968            .metrics
3969            .snapshots_exported
3970            .saturating_add(snapshots.len());
3971        Ok(snapshots)
3972    }
3973
3974    /// Resumes all stations covered by the barrier and returns final metrics.
3975    pub fn resume(&mut self) -> Result<BarrierMetrics, BarrierRuntimeError> {
3976        let Some(mut barrier) = self.active else {
3977            return Err(BarrierRuntimeError::NoActiveBarrier);
3978        };
3979        if barrier.state != BarrierState::Frozen {
3980            return Err(BarrierRuntimeError::NotFrozen(barrier.state));
3981        }
3982
3983        barrier.resume();
3984        for phase in self.phases.values_mut() {
3985            *phase = StationBarrierPhase::Resumed;
3986        }
3987        barrier.finish();
3988        let metrics = self.metrics;
3989        self.active = None;
3990        self.phases.clear();
3991        self.metrics = BarrierMetrics::default();
3992        Ok(metrics)
3993    }
3994
3995    /// Returns current barrier progress.
3996    pub fn progress(&self) -> BarrierProgress {
3997        let state = self
3998            .active
3999            .map_or(BarrierState::Running, |barrier| barrier.state);
4000        let target_tick = self
4001            .active
4002            .map_or(Tick::new(0), |barrier| barrier.target_tick);
4003        let frozen_count = self
4004            .phases
4005            .values()
4006            .filter(|phase| matches!(phase, StationBarrierPhase::Frozen))
4007            .count();
4008
4009        BarrierProgress {
4010            state,
4011            station_count: self.phases.len(),
4012            frozen_count,
4013            target_tick,
4014        }
4015    }
4016}
4017
4018/// Report produced after applying an external upgrade hook to frozen snapshots.
4019#[derive(Clone, Debug, PartialEq, Eq)]
4020pub struct BarrierUpgradeReport {
4021    /// Snapshot version requested for export.
4022    pub version: SnapshotVersion,
4023    /// Snapshots passed through the upgrade hook.
4024    pub snapshots_migrated: usize,
4025    /// Stations restored from migrated snapshots.
4026    pub stations_restored: usize,
4027    /// Entity records restored across all stations.
4028    pub entities_restored: usize,
4029}
4030
4031/// Error produced while applying an upgrade hook around a frozen barrier.
4032#[derive(Clone, Debug, PartialEq, Eq)]
4033pub enum BarrierUpgradeError {
4034    /// Barrier was missing or not frozen.
4035    Barrier(BarrierRuntimeError),
4036    /// Station disappeared between snapshot export and restore.
4037    MissingStation(StationId),
4038    /// Restoring a migrated snapshot failed.
4039    Restore {
4040        /// Station being restored.
4041        station_id: StationId,
4042        /// Restore error.
4043        error: StationError,
4044    },
4045}
4046
4047impl core::fmt::Display for BarrierUpgradeError {
4048    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
4049        match self {
4050            Self::Barrier(error) => write!(f, "{error}"),
4051            Self::MissingStation(station_id) => {
4052                write!(f, "upgrade station {} is missing", station_id.get())
4053            }
4054            Self::Restore { station_id, error } => {
4055                write!(
4056                    f,
4057                    "upgrade restore for station {} failed: {error}",
4058                    station_id.get()
4059                )
4060            }
4061        }
4062    }
4063}
4064
4065impl std::error::Error for BarrierUpgradeError {
4066    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
4067        match self {
4068            Self::Barrier(error) => Some(error),
4069            Self::Restore { error, .. } => Some(error),
4070            Self::MissingStation(_) => None,
4071        }
4072    }
4073}
4074
4075impl From<BarrierRuntimeError> for BarrierUpgradeError {
4076    fn from(value: BarrierRuntimeError) -> Self {
4077        Self::Barrier(value)
4078    }
4079}
4080
4081/// Applies an external in-memory upgrade hook while a runtime barrier is frozen.
4082#[derive(Clone, Copy, Debug, Default)]
4083pub struct BarrierUpgradeExecutor;
4084
4085impl BarrierUpgradeExecutor {
4086    /// Exports frozen station snapshots, lets `hook` migrate them, and restores
4087    /// every station only after all migrated snapshots are valid.
4088    pub fn migrate_frozen<H>(
4089        controller: &mut BarrierController,
4090        stations: &mut StationSet,
4091        version: SnapshotVersion,
4092        hook: &mut H,
4093    ) -> Result<BarrierUpgradeReport, BarrierUpgradeError>
4094    where
4095        H: RuntimeUpgradeHook,
4096    {
4097        let report_version = version;
4098        let snapshots = controller.export_snapshots(stations, version)?;
4099        let mut restored = Vec::with_capacity(snapshots.len());
4100        let mut entities_restored = 0usize;
4101
4102        for snapshot in snapshots {
4103            let station_id = snapshot.meta.station_id;
4104            let config = stations
4105                .get(station_id)
4106                .ok_or(BarrierUpgradeError::MissingStation(station_id))?
4107                .config();
4108            hook.pre_upgrade(&snapshot.meta);
4109            let migrated = hook.migrate_state(snapshot);
4110            let migrated_meta = migrated.meta.clone();
4111            let restored_station = Station::restore(config, migrated)
4112                .map_err(|error| BarrierUpgradeError::Restore { station_id, error })?;
4113            entities_restored = entities_restored.saturating_add(restored_station.len());
4114            hook.post_upgrade(&migrated_meta);
4115            restored.push((station_id, restored_station));
4116        }
4117
4118        let stations_restored = restored.len();
4119        for (station_id, restored_station) in restored {
4120            let station = stations
4121                .get_mut(station_id)
4122                .ok_or(BarrierUpgradeError::MissingStation(station_id))?;
4123            *station = restored_station;
4124        }
4125
4126        Ok(BarrierUpgradeReport {
4127            version: report_version,
4128            snapshots_migrated: stations_restored,
4129            stations_restored,
4130            entities_restored,
4131        })
4132    }
4133}
4134
4135#[cfg(test)]
4136mod tests {
4137    use super::*;
4138    use sectorsync_core::prelude::{
4139        Bounds, CellCoord3, CellLoadSample, CommandEnvelope, CommandPriority, CommandQueueLimits,
4140        CompiledSyncPolicy, ComponentDescriptor, ComponentId, ComponentMigrationMode,
4141        ComponentSyncMode, EventId, EventKind, EventPriority, GatewayConfig, GridSpec,
4142        HotspotThresholds, InstanceId, NodeId, PolicyId, Position3, RangeOnlyVisibility,
4143        SnapshotMeta, StationConfig, StationLoadSample, U32LeCodec,
4144    };
4145    use sectorsync_transport::{
4146        ClientTransportLimits, FakeTransport, InMemoryStationTransport, InMemoryTransportHub,
4147        OutboundPacket, StationOutboundPacket, StationTransportSink, TransportReceiver,
4148        TransportSink,
4149    };
4150    use sectorsync_wire::{
4151        BarrierFrame, BinaryFrameDecoder, BinaryFrameEncoder, CommandAckFrame,
4152        CommandDispatchFrame, CommandFrame, ComponentDelta, EntityDelta, FrameDecoder,
4153        FrameEncoder, ReplicationFrame,
4154    };
4155
4156    fn station(station_id: u32, instance_id: u64) -> Station {
4157        Station::new(StationConfig {
4158            station_id: StationId::new(station_id),
4159            node_id: NodeId::new(0),
4160            instance_id: InstanceId::new(instance_id),
4161            tick_rate_hz: 20,
4162        })
4163    }
4164
4165    fn encode_command_frame(sequence: u64) -> Vec<u8> {
4166        let frame = CommandFrame {
4167            client_id: ClientId::new(7),
4168            command_id: CommandId::new(sequence),
4169            entity_id: EntityId::new(100),
4170            sequence,
4171            kind: 1,
4172            priority: CommandPriority::High,
4173            payload: b"move:north".to_vec(),
4174        };
4175        let mut bytes = Vec::new();
4176        BinaryFrameEncoder
4177            .encode_command(&frame, &mut bytes)
4178            .expect("command should encode");
4179        bytes
4180    }
4181
4182    fn command_queues() -> CommandQueues {
4183        CommandQueues::new(CommandQueueLimits {
4184            high: 4,
4185            normal: 4,
4186            low: 4,
4187        })
4188    }
4189
4190    fn gateway(max_commands_per_tick: usize) -> GatewaySessionTable {
4191        GatewaySessionTable::new(GatewayConfig {
4192            max_sessions: 8,
4193            reconnect_grace_ticks: 10,
4194            max_commands_per_tick,
4195        })
4196    }
4197
4198    #[test]
4199    fn barrier_freezes_snapshots_and_resumes_instance_scope() {
4200        let mut stations = StationSet::default();
4201        stations.push(station(1, 10));
4202        stations.push(station(2, 10));
4203
4204        for station in stations.iter_mut() {
4205            station.advance_tick();
4206            station.advance_tick();
4207        }
4208
4209        let mut controller = BarrierController::default();
4210        let requested = controller
4211            .request(
4212                &stations,
4213                BarrierId::new(7),
4214                BarrierScope::Instance(InstanceId::new(10)),
4215                Tick::new(2),
4216                CommandQueueMode::Buffer,
4217            )
4218            .expect("request should work");
4219        assert_eq!(requested.state, BarrierState::WaitingTickBoundary);
4220
4221        let frozen = controller.poll(&stations).expect("poll should work");
4222        assert_eq!(frozen.state, BarrierState::Frozen);
4223        assert_eq!(frozen.frozen_count, 2);
4224
4225        let snapshots = controller
4226            .export_snapshots(&stations, SnapshotVersion::default())
4227            .expect("snapshot should work while frozen");
4228        assert_eq!(snapshots.len(), 2);
4229
4230        let metrics = controller.resume().expect("resume should work");
4231        assert_eq!(metrics.station_count, 2);
4232        assert_eq!(metrics.snapshots_exported, 2);
4233        assert_eq!(controller.progress().state, BarrierState::Running);
4234    }
4235
4236    #[derive(Default)]
4237    struct MoveSnapshotUpgrade {
4238        pre: usize,
4239        migrations: usize,
4240        post: usize,
4241    }
4242
4243    impl RuntimeUpgradeHook for MoveSnapshotUpgrade {
4244        fn pre_upgrade(&mut self, meta: &SnapshotMeta) {
4245            self.pre = self.pre.saturating_add(1);
4246            assert_eq!(meta.version.runtime_version, 2);
4247        }
4248
4249        fn migrate_state(&mut self, mut snapshot: StationSnapshot) -> StationSnapshot {
4250            self.migrations = self.migrations.saturating_add(1);
4251            for entity in &mut snapshot.entities {
4252                entity.position.x += 10.0;
4253            }
4254            snapshot
4255        }
4256
4257        fn post_upgrade(&mut self, meta: &SnapshotMeta) {
4258            self.post = self.post.saturating_add(1);
4259            assert_eq!(meta.version.runtime_version, 2);
4260        }
4261    }
4262
4263    #[test]
4264    fn barrier_upgrade_executor_migrates_and_restores_frozen_snapshots() {
4265        let mut first = station(1, 10);
4266        first
4267            .spawn_owned(
4268                EntityId::new(100),
4269                Position3::new(1.0, 2.0, 3.0),
4270                Bounds::Point,
4271                PolicyId::new(0),
4272            )
4273            .expect("spawn should work");
4274        let mut stations = StationSet::default();
4275        stations.push(first);
4276        stations.push(station(2, 10));
4277
4278        for station in stations.iter_mut() {
4279            station.advance_tick();
4280            station.advance_tick();
4281        }
4282
4283        let mut controller = BarrierController::default();
4284        controller
4285            .request(
4286                &stations,
4287                BarrierId::new(8),
4288                BarrierScope::Instance(InstanceId::new(10)),
4289                Tick::new(2),
4290                CommandQueueMode::Buffer,
4291            )
4292            .expect("request should work");
4293        assert_eq!(
4294            controller.poll(&stations).expect("poll should work").state,
4295            BarrierState::Frozen
4296        );
4297
4298        let mut hook = MoveSnapshotUpgrade::default();
4299        let version = SnapshotVersion {
4300            runtime_version: 2,
4301            ..SnapshotVersion::default()
4302        };
4303        let report = BarrierUpgradeExecutor::migrate_frozen(
4304            &mut controller,
4305            &mut stations,
4306            version,
4307            &mut hook,
4308        )
4309        .expect("upgrade should migrate frozen snapshots");
4310
4311        assert_eq!(report.version, version);
4312        assert_eq!(report.snapshots_migrated, 2);
4313        assert_eq!(report.stations_restored, 2);
4314        assert_eq!(report.entities_restored, 1);
4315        assert_eq!(hook.pre, 2);
4316        assert_eq!(hook.migrations, 2);
4317        assert_eq!(hook.post, 2);
4318        let moved = stations
4319            .get(StationId::new(1))
4320            .expect("station should exist")
4321            .get_by_id(EntityId::new(100))
4322            .expect("entity should restore");
4323        assert_eq!(moved.position, Position3::new(11.0, 2.0, 3.0));
4324        assert_eq!(controller.progress().state, BarrierState::Frozen);
4325
4326        let metrics = controller.resume().expect("resume should work");
4327        assert_eq!(metrics.snapshots_exported, 2);
4328        assert_eq!(controller.progress().state, BarrierState::Running);
4329    }
4330
4331    #[test]
4332    fn barrier_transport_bridge_broadcasts_client_notifications() {
4333        let server_id = ClientId::new(0);
4334        let clients = [ClientId::new(7), ClientId::new(8)];
4335        let hub = InMemoryTransportHub::new(ClientTransportLimits {
4336            max_queued_packets_per_client: 4,
4337            max_packet_bytes: 512,
4338        });
4339        let mut server_transport = hub
4340            .endpoint(server_id, "127.0.0.1:23400".parse().expect("server addr"))
4341            .expect("server endpoint should register");
4342        let mut client_transports = clients
4343            .into_iter()
4344            .enumerate()
4345            .map(|(index, client_id)| {
4346                hub.endpoint(
4347                    client_id,
4348                    format!("127.0.0.1:{}", 23407 + index)
4349                        .parse()
4350                        .expect("client addr"),
4351                )
4352                .expect("client endpoint should register")
4353            })
4354            .collect::<Vec<_>>();
4355        let mut barrier = RuntimeBarrier::requested(
4356            BarrierId::new(5),
4357            BarrierScope::Instance(InstanceId::new(10)),
4358            Tick::new(10),
4359            Tick::new(12),
4360            CommandQueueMode::Buffer,
4361        );
4362        barrier.wait_for_tick_boundary();
4363        barrier.freeze();
4364
4365        let mut bridge = BarrierTransportBridge::default();
4366        let report = bridge
4367            .broadcast_barrier(&mut server_transport, clients, barrier)
4368            .expect("barrier should broadcast");
4369
4370        assert_eq!(report.barrier_id, barrier.id);
4371        assert_eq!(report.state, BarrierState::Frozen);
4372        assert_eq!(report.server_tick, Tick::new(12));
4373        assert_eq!(report.clients_requested, 2);
4374        assert_eq!(report.clients_sent, 2);
4375        assert!(report.bytes_sent > 0);
4376        assert_eq!(bridge.stats().notifications_sent, 2);
4377        assert_eq!(bridge.stats().clients_notified, 2);
4378        assert_eq!(bridge.stats().bytes_sent, report.bytes_sent);
4379
4380        for (index, client_id) in clients.into_iter().enumerate() {
4381            let mut client_bridge = ClientTransportBridge::new(
4382                ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
4383            );
4384            let pump = client_bridge
4385                .pump(&mut client_transports[index], 2)
4386                .expect("client should receive barrier");
4387            assert_eq!(pump.barrier_frames_received(), 1);
4388            assert_eq!(
4389                pump.barriers[0],
4390                BarrierFrame {
4391                    client_id,
4392                    barrier_id: barrier.id,
4393                    server_tick: barrier.target_tick,
4394                    state: BarrierState::Frozen,
4395                }
4396            );
4397        }
4398    }
4399
4400    #[test]
4401    fn replication_transport_bridge_sends_planned_frame() {
4402        let mut station = station(1, 10);
4403        let mut index = CellIndex::new(GridSpec::new(64.0).expect("grid is valid"));
4404        let mut policies = PolicyTable::default();
4405        policies.set(CompiledSyncPolicy::new(PolicyId::new(0), 1, 20, 256.0));
4406        let handle = station
4407            .spawn_owned(
4408                EntityId::new(100),
4409                Position3::new(0.0, 0.0, 0.0),
4410                Bounds::Point,
4411                PolicyId::new(0),
4412            )
4413            .expect("spawn should work");
4414        index.upsert(handle, Position3::new(0.0, 0.0, 0.0), Bounds::Point);
4415        let descriptor = ComponentDescriptor::sparse_blob(
4416            ComponentId::new(1),
4417            "health",
4418            ComponentSyncMode::Delta,
4419            ComponentMigrationMode::Copy,
4420            4,
4421        );
4422        let mut components = ComponentStore::default();
4423        components
4424            .set_typed(&descriptor, handle, 1, &U32LeCodec, &100)
4425            .expect("component should write");
4426        let selection = ComponentSelection {
4427            component_ids: vec![ComponentId::new(1)],
4428        };
4429        let viewer = ViewerQuery {
4430            client_id: ClientId::new(7),
4431            position: Position3::new(0.0, 0.0, 0.0),
4432            radius: 256.0,
4433            max_entities: 32,
4434        };
4435        let mut bridge = ReplicationTransportBridge::default();
4436        let mut transport = FakeTransport::default();
4437
4438        let report = bridge
4439            .send_viewer(
4440                &mut transport,
4441                &station,
4442                &index,
4443                &policies,
4444                &components,
4445                &selection,
4446                &viewer,
4447                &RangeOnlyVisibility,
4448            )
4449            .expect("replication should send");
4450
4451        assert!(report.sent);
4452        assert_eq!(report.client_id, viewer.client_id);
4453        assert_eq!(report.selected_entities, 1);
4454        assert_eq!(report.encoded_entities, 1);
4455        assert_eq!(report.encoded_components, 1);
4456        assert_eq!(transport.packets_sent(), 1);
4457        assert_eq!(transport.bytes_sent(), report.bytes_sent);
4458        assert_eq!(bridge.stats().frames_sent, 1);
4459        assert_eq!(bridge.stats().entities_selected, 1);
4460        assert_eq!(bridge.stats().components_encoded, 1);
4461    }
4462
4463    #[test]
4464    fn replication_transport_bridge_skips_empty_frames_by_default() {
4465        let mut station = station(1, 10);
4466        let mut index = CellIndex::new(GridSpec::new(64.0).expect("grid is valid"));
4467        let mut policies = PolicyTable::default();
4468        policies.set(CompiledSyncPolicy::new(PolicyId::new(0), 1, 20, 256.0));
4469        let handle = station
4470            .spawn_owned(
4471                EntityId::new(100),
4472                Position3::new(0.0, 0.0, 0.0),
4473                Bounds::Point,
4474                PolicyId::new(0),
4475            )
4476            .expect("spawn should work");
4477        index.upsert(handle, Position3::new(0.0, 0.0, 0.0), Bounds::Point);
4478        let components = ComponentStore::default();
4479        let selection = ComponentSelection {
4480            component_ids: vec![ComponentId::new(1)],
4481        };
4482        let viewer = ViewerQuery {
4483            client_id: ClientId::new(7),
4484            position: Position3::new(0.0, 0.0, 0.0),
4485            radius: 256.0,
4486            max_entities: 32,
4487        };
4488        let mut bridge = ReplicationTransportBridge::default();
4489        let mut transport = FakeTransport::default();
4490
4491        let report = bridge
4492            .send_viewer(
4493                &mut transport,
4494                &station,
4495                &index,
4496                &policies,
4497                &components,
4498                &selection,
4499                &viewer,
4500                &RangeOnlyVisibility,
4501            )
4502            .expect("empty replication should skip");
4503
4504        assert!(!report.sent);
4505        assert_eq!(report.selected_entities, 1);
4506        assert_eq!(report.encoded_entities, 0);
4507        assert_eq!(transport.packets_sent(), 0);
4508        assert_eq!(bridge.stats().frames_skipped_empty, 1);
4509        assert_eq!(bridge.stats().entities_selected, 1);
4510    }
4511
4512    #[test]
4513    #[allow(clippy::too_many_lines)]
4514    fn replication_transport_bridge_prioritized_reports_budget_skips() {
4515        let client_id = ClientId::new(7);
4516        let server_id = ClientId::new(0);
4517        let mut station = station(1, 10);
4518        let mut index = CellIndex::new(GridSpec::new(64.0).expect("grid is valid"));
4519        let mut policies = PolicyTable::default();
4520        let mut low = CompiledSyncPolicy::new(PolicyId::new(1), 1, 20, 256.0);
4521        low.priority_weight = 1;
4522        let mut high = CompiledSyncPolicy::new(PolicyId::new(2), 1, 20, 256.0);
4523        high.priority_weight = 10;
4524        policies.set(low);
4525        policies.set(high);
4526
4527        let low_handle = station
4528            .spawn_owned(
4529                EntityId::new(100),
4530                Position3::new(0.0, 0.0, 0.0),
4531                Bounds::Point,
4532                PolicyId::new(1),
4533            )
4534            .expect("spawn low should work");
4535        let high_handle = station
4536            .spawn_owned(
4537                EntityId::new(200),
4538                Position3::new(128.0, 0.0, 0.0),
4539                Bounds::Point,
4540                PolicyId::new(2),
4541            )
4542            .expect("spawn high should work");
4543        index.upsert(low_handle, Position3::new(0.0, 0.0, 0.0), Bounds::Point);
4544        index.upsert(high_handle, Position3::new(128.0, 0.0, 0.0), Bounds::Point);
4545
4546        let descriptor = ComponentDescriptor::sparse_blob(
4547            ComponentId::new(1),
4548            "health",
4549            ComponentSyncMode::Delta,
4550            ComponentMigrationMode::Copy,
4551            4,
4552        );
4553        let mut components = ComponentStore::default();
4554        components
4555            .set_typed(&descriptor, low_handle, 1, &U32LeCodec, &100)
4556            .expect("low component should write");
4557        components
4558            .set_typed(&descriptor, high_handle, 1, &U32LeCodec, &200)
4559            .expect("high component should write");
4560        let selection = ComponentSelection {
4561            component_ids: vec![ComponentId::new(1)],
4562        };
4563        let viewer = ViewerQuery {
4564            client_id,
4565            position: Position3::new(0.0, 0.0, 0.0),
4566            radius: 256.0,
4567            max_entities: 1,
4568        };
4569        let hub = InMemoryTransportHub::new(ClientTransportLimits {
4570            max_queued_packets_per_client: 4,
4571            max_packet_bytes: 512,
4572        });
4573        let mut client_transport = hub
4574            .endpoint(client_id, "127.0.0.1:23107".parse().expect("client addr"))
4575            .expect("client endpoint should register");
4576        let mut server_transport = hub
4577            .endpoint(server_id, "127.0.0.1:23100".parse().expect("server addr"))
4578            .expect("server endpoint should register");
4579        let mut bridge = ReplicationTransportBridge::new(
4580            ReplicationTransportConfig {
4581                budget: ReplicationBudget {
4582                    max_entities: 1,
4583                    max_bytes: 32,
4584                    estimated_entity_bytes: 32,
4585                },
4586                send_empty_frames: false,
4587            },
4588            ReplicationFrameBuilder::default(),
4589        );
4590
4591        let report = bridge
4592            .send_viewer_prioritized(
4593                &mut server_transport,
4594                &station,
4595                &index,
4596                &policies,
4597                &components,
4598                &selection,
4599                &viewer,
4600                &RangeOnlyVisibility,
4601            )
4602            .expect("prioritized replication should send");
4603
4604        assert!(report.sent);
4605        assert_eq!(report.selected_entities, 1);
4606        assert_eq!(report.skipped_by_budget, 1);
4607        assert_eq!(bridge.stats().entities_skipped_by_budget, 1);
4608
4609        let packet = client_transport
4610            .try_recv()
4611            .expect("receive should work")
4612            .expect("packet should exist");
4613        let RuntimeFrame::Replication(frame) = BinaryFrameDecoder
4614            .decode(&packet.bytes)
4615            .expect("frame decodes")
4616        else {
4617            panic!("expected replication frame");
4618        };
4619        assert_eq!(frame.entities.len(), 1);
4620        assert_eq!(frame.entities[0].entity_id, EntityId::new(200));
4621    }
4622
4623    #[test]
4624    fn replication_receive_bridge_decodes_target_frames() {
4625        let client_id = ClientId::new(7);
4626        let server_id = ClientId::new(0);
4627        let hub = InMemoryTransportHub::new(ClientTransportLimits {
4628            max_queued_packets_per_client: 4,
4629            max_packet_bytes: 512,
4630        });
4631        let mut client_transport = hub
4632            .endpoint(client_id, "127.0.0.1:23007".parse().expect("client addr"))
4633            .expect("client endpoint should register");
4634        let mut server_transport = hub
4635            .endpoint(server_id, "127.0.0.1:23000".parse().expect("server addr"))
4636            .expect("server endpoint should register");
4637        let frame = ReplicationFrame {
4638            client_id,
4639            server_tick: Tick::new(12),
4640            entity_count: 1,
4641            estimated_payload_bytes: 4,
4642            entities: vec![EntityDelta {
4643                entity_id: EntityId::new(100),
4644                owner_epoch: OwnerEpoch::new(1),
4645                components: vec![ComponentDelta {
4646                    component_id: ComponentId::new(1),
4647                    version: 1,
4648                    flags: 0,
4649                    bytes: 100_u32.to_le_bytes().to_vec(),
4650                }],
4651            }],
4652        };
4653        let mut bytes = Vec::new();
4654        BinaryFrameEncoder
4655            .encode_replication(&frame, &mut bytes)
4656            .expect("replication should encode");
4657        server_transport
4658            .send(OutboundPacket { client_id, bytes })
4659            .expect("replication packet should send");
4660
4661        let mut receive = ReplicationReceiveBridge::new(
4662            ReplicationReceiveConfig::new(client_id).with_expected_source(server_id),
4663        );
4664        let pump = receive
4665            .pump(&mut client_transport, 4)
4666            .expect("replication packet should receive");
4667
4668        assert_eq!(pump.frames_received(), 1);
4669        assert_eq!(pump.entities_received(), 1);
4670        assert_eq!(pump.components_received(), 1);
4671        assert_eq!(pump.frames[0].client_id, client_id);
4672        assert_eq!(receive.stats().packets_received, 1);
4673        assert_eq!(receive.stats().frames_received, 1);
4674        assert_eq!(receive.stats().entities_received, 1);
4675        assert_eq!(receive.stats().components_received, 1);
4676        assert!(receive.stats().bytes_received > 0);
4677    }
4678
4679    #[test]
4680    fn replication_receive_bridge_rejects_wrong_target() {
4681        let client_id = ClientId::new(7);
4682        let server_id = ClientId::new(0);
4683        let wrong_client_id = ClientId::new(99);
4684        let hub = InMemoryTransportHub::new(ClientTransportLimits {
4685            max_queued_packets_per_client: 4,
4686            max_packet_bytes: 512,
4687        });
4688        let mut client_transport = hub
4689            .endpoint(client_id, "127.0.0.1:23107".parse().expect("client addr"))
4690            .expect("client endpoint should register");
4691        let mut server_transport = hub
4692            .endpoint(server_id, "127.0.0.1:23100".parse().expect("server addr"))
4693            .expect("server endpoint should register");
4694        let frame = ReplicationFrame {
4695            client_id: wrong_client_id,
4696            server_tick: Tick::new(12),
4697            entity_count: 1,
4698            estimated_payload_bytes: 4,
4699            entities: vec![EntityDelta {
4700                entity_id: EntityId::new(100),
4701                owner_epoch: OwnerEpoch::new(1),
4702                components: vec![ComponentDelta {
4703                    component_id: ComponentId::new(1),
4704                    version: 1,
4705                    flags: 0,
4706                    bytes: 100_u32.to_le_bytes().to_vec(),
4707                }],
4708            }],
4709        };
4710        let mut bytes = Vec::new();
4711        BinaryFrameEncoder
4712            .encode_replication(&frame, &mut bytes)
4713            .expect("replication should encode");
4714        server_transport
4715            .send(OutboundPacket { client_id, bytes })
4716            .expect("replication packet should send");
4717
4718        let mut receive = ReplicationReceiveBridge::new(
4719            ReplicationReceiveConfig::new(client_id).with_expected_source(server_id),
4720        );
4721        let error = receive
4722            .pump(&mut client_transport, 4)
4723            .expect_err("wrong target should be rejected");
4724
4725        assert!(matches!(
4726            error,
4727            ReplicationReceiveError::TargetMismatch {
4728                expected,
4729                actual,
4730            } if expected == client_id && actual == wrong_client_id
4731        ));
4732        assert_eq!(receive.stats().packets_received, 1);
4733        assert_eq!(receive.stats().frames_received, 0);
4734        assert_eq!(receive.stats().frames_rejected_target, 1);
4735    }
4736
4737    #[test]
4738    #[allow(clippy::too_many_lines)]
4739    fn client_transport_bridge_sends_command_and_receives_client_frames() {
4740        let client_id = ClientId::new(7);
4741        let server_id = ClientId::new(0);
4742        let hub = InMemoryTransportHub::new(ClientTransportLimits {
4743            max_queued_packets_per_client: 8,
4744            max_packet_bytes: 512,
4745        });
4746        let mut client_transport = hub
4747            .endpoint(client_id, "127.0.0.1:23207".parse().expect("client addr"))
4748            .expect("client endpoint should register");
4749        let mut server_transport = hub
4750            .endpoint(server_id, "127.0.0.1:23200".parse().expect("server addr"))
4751            .expect("server endpoint should register");
4752        let mut bridge = ClientTransportBridge::new(
4753            ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
4754        );
4755        let command = CommandFrame {
4756            client_id,
4757            command_id: CommandId::new(42),
4758            entity_id: EntityId::new(100),
4759            sequence: 9,
4760            kind: 1,
4761            priority: CommandPriority::High,
4762            payload: b"move:north".to_vec(),
4763        };
4764
4765        let send = bridge
4766            .send_command_frame(&mut client_transport, &command)
4767            .expect("command should send");
4768        assert_eq!(send.command_id, command.command_id);
4769        assert!(send.bytes_sent > 0);
4770        assert_eq!(bridge.stats().commands_sent, 1);
4771        assert_eq!(bridge.stats().command_bytes_sent, send.bytes_sent);
4772        let inbound = server_transport
4773            .try_recv()
4774            .expect("server receive should work")
4775            .expect("command packet should arrive");
4776        assert_eq!(inbound.client_id, Some(client_id));
4777        let RuntimeFrame::Command(decoded) = BinaryFrameDecoder
4778            .decode(&inbound.bytes)
4779            .expect("command should decode")
4780        else {
4781            panic!("expected command frame");
4782        };
4783        assert_eq!(decoded, command);
4784
4785        let ack = CommandAckFrame {
4786            client_id,
4787            command_id: command.command_id,
4788            server_tick: Tick::new(12),
4789            accepted: true,
4790            reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
4791        };
4792        let mut ack_bytes = Vec::new();
4793        BinaryFrameEncoder
4794            .encode_command_ack(&ack, &mut ack_bytes)
4795            .expect("ACK should encode");
4796        server_transport
4797            .send(OutboundPacket {
4798                client_id,
4799                bytes: ack_bytes,
4800            })
4801            .expect("ACK should send");
4802
4803        let replication = ReplicationFrame {
4804            client_id,
4805            server_tick: Tick::new(12),
4806            entity_count: 1,
4807            estimated_payload_bytes: 4,
4808            entities: vec![EntityDelta {
4809                entity_id: EntityId::new(100),
4810                owner_epoch: OwnerEpoch::new(1),
4811                components: vec![ComponentDelta {
4812                    component_id: ComponentId::new(1),
4813                    version: 1,
4814                    flags: 0,
4815                    bytes: 100_u32.to_le_bytes().to_vec(),
4816                }],
4817            }],
4818        };
4819        let mut replication_bytes = Vec::new();
4820        BinaryFrameEncoder
4821            .encode_replication(&replication, &mut replication_bytes)
4822            .expect("replication should encode");
4823        server_transport
4824            .send(OutboundPacket {
4825                client_id,
4826                bytes: replication_bytes,
4827            })
4828            .expect("replication should send");
4829
4830        let barrier = BarrierFrame {
4831            client_id,
4832            barrier_id: BarrierId::new(5),
4833            server_tick: Tick::new(12),
4834            state: BarrierState::Frozen,
4835        };
4836        let mut barrier_bytes = Vec::new();
4837        BinaryFrameEncoder
4838            .encode_barrier(&barrier, &mut barrier_bytes)
4839            .expect("barrier should encode");
4840        server_transport
4841            .send(OutboundPacket {
4842                client_id,
4843                bytes: barrier_bytes,
4844            })
4845            .expect("barrier should send");
4846
4847        let pump = bridge
4848            .pump(&mut client_transport, 8)
4849            .expect("client frames should receive");
4850
4851        assert_eq!(pump.packets_received, 3);
4852        assert_eq!(pump.command_acks_received(), 1);
4853        assert_eq!(pump.replication_frames_received(), 1);
4854        assert_eq!(pump.barrier_frames_received(), 1);
4855        assert_eq!(pump.entities_received(), 1);
4856        assert_eq!(pump.components_received(), 1);
4857        assert_eq!(pump.command_acks[0], ack);
4858        assert_eq!(pump.replication_frames[0], replication);
4859        assert_eq!(pump.barriers[0], barrier);
4860        assert_eq!(bridge.stats().packets_received, 3);
4861        assert_eq!(bridge.stats().command_acks_received, 1);
4862        assert_eq!(bridge.stats().replication_frames_received, 1);
4863        assert_eq!(bridge.stats().barrier_frames_received, 1);
4864        assert_eq!(bridge.stats().entities_received, 1);
4865        assert_eq!(bridge.stats().components_received, 1);
4866    }
4867
4868    #[test]
4869    fn client_transport_bridge_rejects_wrong_ack_target() {
4870        let client_id = ClientId::new(7);
4871        let server_id = ClientId::new(0);
4872        let wrong_client_id = ClientId::new(99);
4873        let hub = InMemoryTransportHub::new(ClientTransportLimits {
4874            max_queued_packets_per_client: 4,
4875            max_packet_bytes: 512,
4876        });
4877        let mut client_transport = hub
4878            .endpoint(client_id, "127.0.0.1:23307".parse().expect("client addr"))
4879            .expect("client endpoint should register");
4880        let mut server_transport = hub
4881            .endpoint(server_id, "127.0.0.1:23300".parse().expect("server addr"))
4882            .expect("server endpoint should register");
4883        let mut bridge = ClientTransportBridge::new(
4884            ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
4885        );
4886        let ack = CommandAckFrame {
4887            client_id: wrong_client_id,
4888            command_id: CommandId::new(42),
4889            server_tick: Tick::new(12),
4890            accepted: true,
4891            reason_code: GATEWAY_COMMAND_ACK_ACCEPTED,
4892        };
4893        let mut ack_bytes = Vec::new();
4894        BinaryFrameEncoder
4895            .encode_command_ack(&ack, &mut ack_bytes)
4896            .expect("ACK should encode");
4897        server_transport
4898            .send(OutboundPacket {
4899                client_id,
4900                bytes: ack_bytes,
4901            })
4902            .expect("ACK should send");
4903
4904        let error = bridge
4905            .pump(&mut client_transport, 4)
4906            .expect_err("wrong target should be rejected");
4907
4908        assert!(matches!(
4909            error,
4910            ClientTransportBridgeError::TargetMismatch {
4911                kind: ClientInboundFrameKind::CommandAck,
4912                expected,
4913                actual,
4914            } if expected == client_id && actual == wrong_client_id
4915        ));
4916        assert_eq!(bridge.stats().packets_received, 1);
4917        assert_eq!(bridge.stats().command_acks_received, 0);
4918        assert_eq!(bridge.stats().frames_rejected_target, 1);
4919    }
4920
4921    #[test]
4922    fn gateway_client_transport_bridge_queues_command_and_sends_ack() {
4923        let client_id = ClientId::new(7);
4924        let server_id = ClientId::new(0);
4925        let station_id = StationId::new(1);
4926        let hub = InMemoryTransportHub::new(ClientTransportLimits {
4927            max_queued_packets_per_client: 8,
4928            max_packet_bytes: 512,
4929        });
4930        let mut client_transport = hub
4931            .endpoint(client_id, "127.0.0.1:23507".parse().expect("client addr"))
4932            .expect("client endpoint should register");
4933        let mut server_transport = hub
4934            .endpoint(server_id, "127.0.0.1:23500".parse().expect("server addr"))
4935            .expect("server endpoint should register");
4936        let mut client_bridge = ClientTransportBridge::new(
4937            ClientTransportConfig::new(client_id, server_id).with_expected_source(server_id),
4938        );
4939        let command = CommandFrame {
4940            client_id,
4941            command_id: CommandId::new(42),
4942            entity_id: EntityId::new(100),
4943            sequence: 9,
4944            kind: 1,
4945            priority: CommandPriority::High,
4946            payload: b"move:north".to_vec(),
4947        };
4948        client_bridge
4949            .send_command_frame(&mut client_transport, &command)
4950            .expect("client command should send");
4951
4952        let mut gateway = gateway(4);
4953        gateway
4954            .connect(client_id, station_id, Tick::new(10))
4955            .expect("client should connect");
4956        let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
4957        let mut pipeline = GatewayCommandPipeline::default();
4958        let mut gateway_bridge = GatewayClientTransportBridge::default();
4959
4960        let pump = gateway_bridge
4961            .pump_ingress(
4962                &mut server_transport,
4963                &mut pipeline,
4964                &mut gateway,
4965                &mut station_queues,
4966                Tick::new(10),
4967                CommandIngress::RUNNING,
4968                4,
4969            )
4970            .expect("gateway client transport should pump");
4971
4972        assert_eq!(pump.packets_received, 1);
4973        assert_eq!(pump.commands_processed(), 1);
4974        assert_eq!(pump.commands_accepted(), 1);
4975        assert_eq!(pump.acks_sent, 1);
4976        assert_eq!(gateway_bridge.stats().packets_received, 1);
4977        assert_eq!(gateway_bridge.stats().command_frames_received, 1);
4978        assert_eq!(gateway_bridge.stats().commands_accepted, 1);
4979        assert_eq!(gateway_bridge.stats().acks_sent, 1);
4980        let queued = station_queues
4981            .get_mut(&station_id)
4982            .expect("station queue should exist")
4983            .pop_next()
4984            .expect("command should queue");
4985        assert_eq!(queued.id, command.command_id);
4986
4987        let ack_pump = client_bridge
4988            .pump(&mut client_transport, 4)
4989            .expect("client should receive ACK");
4990        assert_eq!(ack_pump.command_acks_received(), 1);
4991        assert!(ack_pump.command_acks[0].accepted);
4992        assert_eq!(ack_pump.command_acks[0].command_id, command.command_id);
4993    }
4994
4995    #[test]
4996    fn gateway_client_transport_bridge_rejects_source_mismatch_before_admission() {
4997        let packet_client_id = ClientId::new(7);
4998        let frame_client_id = ClientId::new(8);
4999        let server_id = ClientId::new(0);
5000        let station_id = StationId::new(1);
5001        let hub = InMemoryTransportHub::new(ClientTransportLimits {
5002            max_queued_packets_per_client: 4,
5003            max_packet_bytes: 512,
5004        });
5005        let mut packet_client_transport = hub
5006            .endpoint(
5007                packet_client_id,
5008                "127.0.0.1:23607".parse().expect("client addr"),
5009            )
5010            .expect("client endpoint should register");
5011        let mut server_transport = hub
5012            .endpoint(server_id, "127.0.0.1:23600".parse().expect("server addr"))
5013            .expect("server endpoint should register");
5014        let command = CommandFrame {
5015            client_id: frame_client_id,
5016            command_id: CommandId::new(42),
5017            entity_id: EntityId::new(100),
5018            sequence: 9,
5019            kind: 1,
5020            priority: CommandPriority::High,
5021            payload: b"move:north".to_vec(),
5022        };
5023        let mut bytes = Vec::new();
5024        BinaryFrameEncoder
5025            .encode_command(&command, &mut bytes)
5026            .expect("command should encode");
5027        packet_client_transport
5028            .send(OutboundPacket {
5029                client_id: server_id,
5030                bytes,
5031            })
5032            .expect("packet should send");
5033
5034        let mut gateway = gateway(4);
5035        gateway
5036            .connect(frame_client_id, station_id, Tick::new(10))
5037            .expect("frame client should connect");
5038        let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
5039        let mut pipeline = GatewayCommandPipeline::default();
5040        let mut gateway_bridge = GatewayClientTransportBridge::default();
5041
5042        let error = gateway_bridge
5043            .pump_ingress(
5044                &mut server_transport,
5045                &mut pipeline,
5046                &mut gateway,
5047                &mut station_queues,
5048                Tick::new(10),
5049                CommandIngress::RUNNING,
5050                4,
5051            )
5052            .expect_err("source mismatch should reject before admission");
5053
5054        assert!(matches!(
5055            error,
5056            GatewayClientTransportError::SourceMismatch {
5057                packet_client_id: actual_packet,
5058                frame_client_id: actual_frame,
5059            } if actual_packet == packet_client_id && actual_frame == frame_client_id
5060        ));
5061        assert_eq!(gateway_bridge.stats().source_mismatches, 1);
5062        assert_eq!(gateway_bridge.stats().commands_accepted, 0);
5063        assert_eq!(pipeline.stats().commands_admitted, 0);
5064        assert_eq!(
5065            station_queues
5066                .get(&station_id)
5067                .expect("station queue should exist")
5068                .total_len(),
5069            0
5070        );
5071    }
5072
5073    #[test]
5074    fn gateway_command_pipeline_queues_command_and_encodes_ack() {
5075        let client_id = ClientId::new(7);
5076        let station_id = StationId::new(1);
5077        let mut gateway = gateway(4);
5078        gateway
5079            .connect(client_id, station_id, Tick::new(10))
5080            .expect("client should connect");
5081        let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
5082        let mut pipeline = GatewayCommandPipeline::default();
5083
5084        let report = pipeline.process(
5085            &mut gateway,
5086            &mut station_queues,
5087            &encode_command_frame(1),
5088            Tick::new(10),
5089            CommandIngress::RUNNING,
5090        );
5091
5092        assert!(report.accepted);
5093        assert_eq!(report.reason_code, GATEWAY_COMMAND_ACK_ACCEPTED);
5094        assert_eq!(report.station_id, Some(station_id));
5095        assert!(report.error.is_none());
5096        let ack_bytes = report.ack_bytes.expect("ACK should encode");
5097        let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
5098            .decode(&ack_bytes)
5099            .expect("ACK should decode")
5100        else {
5101            panic!("expected command ACK");
5102        };
5103        assert!(ack.accepted);
5104        assert_eq!(ack.command_id, CommandId::new(1));
5105        let queued = station_queues
5106            .get_mut(&station_id)
5107            .expect("queue should exist")
5108            .pop_next()
5109            .expect("command should queue");
5110        assert_eq!(queued.id, CommandId::new(1));
5111        assert_eq!(pipeline.stats().commands_admitted, 1);
5112        assert_eq!(pipeline.stats().commands_enqueued, 1);
5113        assert_eq!(pipeline.stats().acks_encoded, 1);
5114    }
5115
5116    #[test]
5117    fn gateway_command_pipeline_negative_acks_rate_limit() {
5118        let client_id = ClientId::new(7);
5119        let station_id = StationId::new(1);
5120        let mut gateway = gateway(1);
5121        gateway
5122            .connect(client_id, station_id, Tick::new(10))
5123            .expect("client should connect");
5124        let mut station_queues = BTreeMap::from([(station_id, command_queues())]);
5125        let mut pipeline = GatewayCommandPipeline::default();
5126
5127        assert!(
5128            pipeline
5129                .process(
5130                    &mut gateway,
5131                    &mut station_queues,
5132                    &encode_command_frame(1),
5133                    Tick::new(10),
5134                    CommandIngress::RUNNING,
5135                )
5136                .accepted
5137        );
5138        let rejected = pipeline.process(
5139            &mut gateway,
5140            &mut station_queues,
5141            &encode_command_frame(2),
5142            Tick::new(10),
5143            CommandIngress::RUNNING,
5144        );
5145
5146        assert!(!rejected.accepted);
5147        assert_eq!(rejected.reason_code, GATEWAY_COMMAND_ACK_RATE_LIMITED);
5148        assert!(matches!(
5149            rejected.error,
5150            Some(GatewayCommandPipelineError::Gateway(
5151                GatewayError::RateLimited { .. }
5152            ))
5153        ));
5154        let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
5155            .decode(&rejected.ack_bytes.expect("rejection ACK should encode"))
5156            .expect("ACK should decode")
5157        else {
5158            panic!("expected command ACK");
5159        };
5160        assert!(!ack.accepted);
5161        assert_eq!(ack.reason_code, GATEWAY_COMMAND_ACK_RATE_LIMITED);
5162        assert_eq!(pipeline.stats().commands_rejected_gateway, 1);
5163    }
5164
5165    #[test]
5166    fn gateway_command_pipeline_rejects_missing_station_queue() {
5167        let client_id = ClientId::new(7);
5168        let station_id = StationId::new(1);
5169        let mut gateway = gateway(4);
5170        gateway
5171            .connect(client_id, station_id, Tick::new(10))
5172            .expect("client should connect");
5173        let mut station_queues = BTreeMap::new();
5174        let mut pipeline = GatewayCommandPipeline::default();
5175
5176        let report = pipeline.process(
5177            &mut gateway,
5178            &mut station_queues,
5179            &encode_command_frame(1),
5180            Tick::new(10),
5181            CommandIngress::RUNNING,
5182        );
5183
5184        assert!(!report.accepted);
5185        assert_eq!(report.station_id, Some(station_id));
5186        assert_eq!(report.reason_code, GATEWAY_COMMAND_ACK_MISSING_QUEUE);
5187        assert!(matches!(
5188            report.error,
5189            Some(GatewayCommandPipelineError::MissingQueue(id)) if id == station_id
5190        ));
5191        assert_eq!(pipeline.stats().commands_admitted, 1);
5192        assert_eq!(pipeline.stats().commands_rejected_queue, 1);
5193    }
5194
5195    #[test]
5196    fn gateway_command_pipeline_dispatches_to_deployment_route() {
5197        let client_id = ClientId::new(7);
5198        let station_id = StationId::new(1);
5199        let node_id = NodeId::new(9);
5200        let mut gateway = gateway(4);
5201        gateway
5202            .connect(client_id, station_id, Tick::new(10))
5203            .expect("client should connect");
5204        let mut deployment = DeploymentRouteTable::new(DeploymentConfig {
5205            max_nodes: 4,
5206            max_stations_per_node: 4,
5207            stale_after_ticks: 10,
5208        });
5209        deployment
5210            .register_node(node_id, 4, Tick::new(10))
5211            .expect("node should register");
5212        deployment
5213            .assign_station(station_id, node_id, Tick::new(10))
5214            .expect("station should assign");
5215        let mut pipeline = GatewayCommandPipeline::default();
5216
5217        let report = pipeline.dispatch(
5218            &mut gateway,
5219            &deployment,
5220            &encode_command_frame(1),
5221            Tick::new(12),
5222        );
5223
5224        assert!(report.accepted);
5225        assert_eq!(report.station_id, Some(station_id));
5226        assert_eq!(report.node_id, Some(node_id));
5227        let delivery = report.delivery.expect("delivery should resolve");
5228        assert_eq!(delivery.client_id, client_id);
5229        assert_eq!(delivery.station_id, station_id);
5230        assert_eq!(delivery.node_id, node_id);
5231        assert_eq!(delivery.station_route_epoch, 1);
5232        assert_eq!(
5233            report
5234                .command
5235                .expect("command should be returned")
5236                .received_at,
5237            Tick::new(12)
5238        );
5239        let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
5240            .decode(&report.ack_bytes.expect("ACK should encode"))
5241            .expect("ACK should decode")
5242        else {
5243            panic!("expected command ACK");
5244        };
5245        assert!(ack.accepted);
5246        assert_eq!(pipeline.stats().commands_routed_deployment, 1);
5247    }
5248
5249    #[test]
5250    fn gateway_command_pipeline_negative_acks_missing_deployment_route() {
5251        let client_id = ClientId::new(7);
5252        let station_id = StationId::new(1);
5253        let mut gateway = gateway(4);
5254        gateway
5255            .connect(client_id, station_id, Tick::new(10))
5256            .expect("client should connect");
5257        let deployment = DeploymentRouteTable::default();
5258        let mut pipeline = GatewayCommandPipeline::default();
5259
5260        let report = pipeline.dispatch(
5261            &mut gateway,
5262            &deployment,
5263            &encode_command_frame(1),
5264            Tick::new(12),
5265        );
5266
5267        assert!(!report.accepted);
5268        assert_eq!(report.station_id, Some(station_id));
5269        assert_eq!(report.reason_code, GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED);
5270        assert!(matches!(
5271            report.error,
5272            Some(GatewayCommandPipelineError::Deployment(
5273                DeploymentError::MissingStation(id)
5274            )) if id == station_id
5275        ));
5276        let RuntimeFrame::CommandAck(ack) = BinaryFrameDecoder
5277            .decode(&report.ack_bytes.expect("rejection ACK should encode"))
5278            .expect("ACK should decode")
5279        else {
5280            panic!("expected command ACK");
5281        };
5282        assert!(!ack.accepted);
5283        assert_eq!(ack.reason_code, GATEWAY_COMMAND_ACK_DEPLOYMENT_REJECTED);
5284        assert_eq!(pipeline.stats().commands_rejected_deployment, 1);
5285    }
5286
5287    #[test]
5288    fn gateway_command_pipeline_rejects_non_command_frame() {
5289        let ack = CommandAckFrame {
5290            client_id: ClientId::new(7),
5291            command_id: CommandId::new(1),
5292            server_tick: Tick::new(10),
5293            accepted: true,
5294            reason_code: 0,
5295        };
5296        let mut bytes = Vec::new();
5297        BinaryFrameEncoder
5298            .encode_command_ack(&ack, &mut bytes)
5299            .expect("ACK should encode");
5300        let mut gateway = gateway(4);
5301        let mut station_queues = BTreeMap::new();
5302        let mut pipeline = GatewayCommandPipeline::default();
5303
5304        let report = pipeline.process(
5305            &mut gateway,
5306            &mut station_queues,
5307            &bytes,
5308            Tick::new(10),
5309            CommandIngress::RUNNING,
5310        );
5311
5312        assert!(!report.accepted);
5313        assert!(report.ack_bytes.is_none());
5314        assert_eq!(
5315            report.error,
5316            Some(GatewayCommandPipelineError::NonCommandFrame)
5317        );
5318        assert_eq!(pipeline.stats().frames_rejected_non_command, 1);
5319    }
5320
5321    #[test]
5322    fn migration_executor_moves_owner_and_leaves_source_ghost() {
5323        let mut stations = StationSet::default();
5324        let mut source = station(1, 10);
5325        source
5326            .spawn_owned(
5327                EntityId::new(99),
5328                Position3::new(1.0, 2.0, 3.0),
5329                Bounds::Point,
5330                PolicyId::new(0),
5331            )
5332            .expect("spawn should work");
5333        stations.push(source);
5334        stations.push(station(2, 10));
5335
5336        let report = EntityMigrationExecutor::migrate_entity(
5337            &mut stations,
5338            EntityId::new(99),
5339            StationId::new(1),
5340            StationId::new(2),
5341            4,
5342        )
5343        .expect("migration should work");
5344
5345        assert_eq!(report.transfer.target_station, StationId::new(2));
5346        assert!(
5347            !stations
5348                .get(StationId::new(1))
5349                .expect("source")
5350                .get_by_id(EntityId::new(99))
5351                .expect("source ghost")
5352                .is_owned()
5353        );
5354        assert!(
5355            stations
5356                .get(StationId::new(2))
5357                .expect("target")
5358                .get_by_id(EntityId::new(99))
5359                .expect("target owner")
5360                .is_owned()
5361        );
5362    }
5363
5364    #[test]
5365    fn event_router_delays_until_target_tick_and_scheduler_drains() {
5366        let mut stations = StationSet::default();
5367        stations.push(station(1, 10));
5368        stations.push(station(2, 10));
5369
5370        let mut router = EventRouter::default();
5371        router.register_stations(&stations);
5372        router
5373            .route(StationEvent {
5374                id: EventId::new(1),
5375                source: StationId::new(1),
5376                target: StationId::new(2),
5377                source_tick: Tick::new(0),
5378                target_tick: Tick::new(2),
5379                priority: EventPriority::Critical,
5380                kind: EventKind::Custom(7),
5381            })
5382            .expect("route should work");
5383
5384        let mut scheduler = StationScheduler::default();
5385        scheduler.advance_all(&mut stations);
5386        let drained = scheduler
5387            .drain_ready_events(&stations, &mut router)
5388            .expect("drain should work");
5389        assert!(drained.is_empty());
5390
5391        scheduler.advance_all(&mut stations);
5392        let drained = scheduler
5393            .drain_ready_events(&stations, &mut router)
5394            .expect("drain should work");
5395        assert_eq!(drained.len(), 1);
5396        assert_eq!(router.stats().routed_events, 1);
5397        assert_eq!(router.stats().drained_events, 1);
5398    }
5399
5400    #[test]
5401    fn station_scheduler_prioritizes_loaded_stations_with_budget() {
5402        let mut stations = StationSet::default();
5403        stations.push(station(1, 10));
5404        stations.push(station(2, 10));
5405        stations.push(station(3, 10));
5406
5407        let samples = vec![
5408            StationLoadSample {
5409                station_id: StationId::new(1),
5410                owned_entities: 1,
5411                ..StationLoadSample::default()
5412            },
5413            StationLoadSample {
5414                station_id: StationId::new(2),
5415                owned_entities: 100,
5416                subscribers: 40,
5417                queued_events: 20,
5418                tick_cost_units: 500,
5419                cells: vec![CellLoadSample {
5420                    cell: CellCoord3::new(0, 0, 0),
5421                    owned_entities: 90,
5422                    subscribers: 40,
5423                    event_pressure: 10,
5424                    ..CellLoadSample::default()
5425                }],
5426                ..StationLoadSample::default()
5427            },
5428            StationLoadSample {
5429                station_id: StationId::new(3),
5430                owned_entities: 25,
5431                subscribers: 10,
5432                queued_events: 5,
5433                tick_cost_units: 50,
5434                ..StationLoadSample::default()
5435            },
5436        ];
5437
5438        let mut scheduler = StationScheduler::default();
5439        let plan = scheduler.advance_loaded(
5440            &mut stations,
5441            &samples,
5442            StationScheduleConfig {
5443                max_station_advances_per_step: 2,
5444            },
5445        );
5446
5447        assert_eq!(plan.candidates_considered, 3);
5448        assert_eq!(plan.stations_selected, 2);
5449        assert_eq!(plan.total_advances, 2);
5450        assert_eq!(
5451            plan.selected
5452                .iter()
5453                .map(|candidate| candidate.station_id)
5454                .collect::<Vec<_>>(),
5455            vec![StationId::new(2), StationId::new(3)]
5456        );
5457        assert_eq!(scheduler.advanced_ticks, 2);
5458        assert_eq!(
5459            stations.get(StationId::new(1)).expect("station").tick(),
5460            Tick::new(0)
5461        );
5462        assert_eq!(
5463            stations.get(StationId::new(2)).expect("station").tick(),
5464            Tick::new(1)
5465        );
5466        assert_eq!(
5467            stations.get(StationId::new(3)).expect("station").tick(),
5468            Tick::new(1)
5469        );
5470    }
5471
5472    #[test]
5473    fn station_load_sampler_derives_cells_router_and_subscribers() {
5474        let station_id = StationId::new(1);
5475        let owner_position = Position3::new(1.0, 0.0, 0.0);
5476        let ghost_position = Position3::new(12.0, 0.0, 0.0);
5477        let policy_id = PolicyId::new(1);
5478        let mut station = station(1, 10);
5479        let owner = station
5480            .spawn_owned(EntityId::new(10), owner_position, Bounds::Point, policy_id)
5481            .expect("owner should spawn");
5482        let ghost = station.upsert_ghost(
5483            EntityId::new(20),
5484            ghost_position,
5485            Bounds::Point,
5486            policy_id,
5487            StationId::new(9),
5488            OwnerEpoch::new(3),
5489            Tick::new(30),
5490        );
5491
5492        let grid = GridSpec::new(10.0).expect("grid should build");
5493        let mut index = CellIndex::new(grid);
5494        index.upsert(owner, owner_position, Bounds::Point);
5495        index.upsert(ghost, ghost_position, Bounds::Point);
5496        let mut indexes = StationIndexSet::default();
5497        indexes.insert(station_id, index);
5498
5499        let mut stations = StationSet::default();
5500        stations.push(station);
5501        let mut router = EventRouter::default();
5502        router.register_station(station_id);
5503        for (event_id, kind) in [(1_u64, 1_u32), (2, 2)] {
5504            router
5505                .route(StationEvent {
5506                    id: EventId::new(event_id),
5507                    source: StationId::new(9),
5508                    target: station_id,
5509                    source_tick: Tick::new(0),
5510                    target_tick: Tick::new(4),
5511                    priority: EventPriority::Important,
5512                    kind: EventKind::Custom(kind),
5513                })
5514                .expect("event should queue");
5515        }
5516
5517        assert_eq!(indexes.iter().count(), 1);
5518        let load_sampler = StationLoadSampler::default();
5519        let samples = load_sampler.sample_all(
5520            &stations,
5521            &indexes,
5522            &router,
5523            &[(station_id, 2), (station_id, 3)],
5524        );
5525
5526        assert_eq!(samples.len(), 1);
5527        let sample = &samples[0];
5528        assert_eq!(sample.station_id, station_id);
5529        assert_eq!(sample.owned_entities, 1);
5530        assert_eq!(sample.ghost_entities, 1);
5531        assert_eq!(sample.subscribers, 5);
5532        assert_eq!(sample.queued_events, 2);
5533        assert_eq!(sample.estimated_bytes, 240);
5534        assert_eq!(sample.tick_cost_units, 7);
5535        assert_eq!(
5536            sample.cells,
5537            vec![
5538                CellLoadSample {
5539                    cell: grid.cell_at(owner_position),
5540                    owned_entities: 1,
5541                    ghost_entities: 0,
5542                    subscribers: 0,
5543                    estimated_updates: 1,
5544                    estimated_bytes: 48,
5545                    tick_cost_units: 3,
5546                    event_pressure: 0,
5547                },
5548                CellLoadSample {
5549                    cell: grid.cell_at(ghost_position),
5550                    owned_entities: 0,
5551                    ghost_entities: 1,
5552                    subscribers: 0,
5553                    estimated_updates: 1,
5554                    estimated_bytes: 48,
5555                    tick_cost_units: 2,
5556                    event_pressure: 0,
5557                },
5558            ]
5559        );
5560    }
5561
5562    #[test]
5563    fn station_event_transport_bridge_routes_events_through_bounded_packets() {
5564        let mut stations = StationSet::default();
5565        stations.push(station(1, 10));
5566        stations.push(station(2, 10));
5567
5568        let mut router = EventRouter::default();
5569        router.register_stations(&stations);
5570        let mut transport = InMemoryStationTransport::default();
5571        transport.register_station(StationId::new(2));
5572        let mut bridge = StationEventTransportBridge::default();
5573        let event = StationEvent {
5574            id: EventId::new(7),
5575            source: StationId::new(1),
5576            target: StationId::new(2),
5577            source_tick: Tick::new(0),
5578            target_tick: Tick::new(1),
5579            priority: EventPriority::Important,
5580            kind: EventKind::Custom(99),
5581        };
5582
5583        bridge
5584            .send_event(&mut transport, &event)
5585            .expect("event should encode and send");
5586        assert_eq!(transport.queued_len(StationId::new(2)), Some(1));
5587
5588        let report = bridge
5589            .pump_target(&mut transport, &mut router, StationId::new(2), 4)
5590            .expect("event should pump into router");
5591        assert_eq!(report.packets_received, 1);
5592        assert_eq!(report.events_routed, 1);
5593        assert_eq!(router.queued_len(StationId::new(2)), Some(1));
5594
5595        let mut scheduler = StationScheduler::default();
5596        scheduler.advance_all(&mut stations);
5597        let drained = scheduler
5598            .drain_ready_events(&stations, &mut router)
5599            .expect("drain should work");
5600        assert_eq!(drained, vec![event]);
5601        assert_eq!(bridge.stats().events_sent, 1);
5602        assert_eq!(bridge.stats().events_routed, 1);
5603        assert_eq!(transport.stats().packets_sent, 1);
5604        assert_eq!(transport.stats().packets_received, 1);
5605    }
5606
5607    #[test]
5608    fn command_dispatch_transport_bridge_enqueues_stamped_command() {
5609        let gateway_station = StationId::new(0);
5610        let target_station = StationId::new(2);
5611        let command = CommandEnvelope {
5612            id: CommandId::new(42),
5613            client_id: ClientId::new(7),
5614            entity_id: EntityId::new(100),
5615            sequence: 42,
5616            received_at: Tick::new(12),
5617            kind: 1,
5618            priority: CommandPriority::High,
5619            payload: b"move:north".to_vec(),
5620        };
5621        let mut transport = InMemoryStationTransport::default();
5622        transport.register_station(target_station);
5623        let mut queues = BTreeMap::from([(target_station, command_queues())]);
5624        let mut bridge = CommandDispatchTransportBridge::default();
5625
5626        bridge
5627            .send_envelope(&mut transport, gateway_station, target_station, &command)
5628            .expect("command dispatch should send");
5629        assert_eq!(transport.queued_len(target_station), Some(1));
5630        let report = bridge
5631            .pump_target(
5632                &mut transport,
5633                &mut queues,
5634                target_station,
5635                4,
5636                CommandIngress::RUNNING,
5637            )
5638            .expect("command dispatch should pump");
5639
5640        assert_eq!(report.packets_received, 1);
5641        assert_eq!(report.commands_enqueued, 1);
5642        let queued_command = queues
5643            .get_mut(&target_station)
5644            .expect("queue should exist")
5645            .pop_next()
5646            .expect("command should queue");
5647        assert_eq!(queued_command, command);
5648        assert_eq!(bridge.stats().commands_sent, 1);
5649        assert_eq!(bridge.stats().commands_enqueued, 1);
5650        assert_eq!(transport.stats().packets_sent, 1);
5651        assert_eq!(transport.stats().packets_received, 1);
5652    }
5653
5654    #[test]
5655    fn command_dispatch_transport_bridge_rejects_endpoint_mismatch() {
5656        let packet_target = StationId::new(2);
5657        let frame_target = StationId::new(3);
5658        let mut transport = InMemoryStationTransport::default();
5659        transport.register_station(packet_target);
5660        let frame = CommandDispatchFrame {
5661            station_id: frame_target,
5662            client_id: ClientId::new(7),
5663            command_id: CommandId::new(42),
5664            entity_id: EntityId::new(100),
5665            sequence: 42,
5666            received_at: Tick::new(12),
5667            kind: 1,
5668            priority: CommandPriority::High,
5669            payload: Vec::new(),
5670        };
5671        let mut bytes = Vec::new();
5672        BinaryFrameEncoder
5673            .encode_command_dispatch(&frame, &mut bytes)
5674            .expect("frame should encode");
5675        transport
5676            .send_station(StationOutboundPacket {
5677                source_station: StationId::new(0),
5678                target_station: packet_target,
5679                bytes,
5680            })
5681            .expect("bad packet should enter transport");
5682        let mut queues = BTreeMap::from([(packet_target, command_queues())]);
5683        let mut bridge = CommandDispatchTransportBridge::default();
5684
5685        let error = bridge
5686            .pump_target(
5687                &mut transport,
5688                &mut queues,
5689                packet_target,
5690                4,
5691                CommandIngress::RUNNING,
5692            )
5693            .expect_err("endpoint mismatch should reject");
5694
5695        assert!(matches!(
5696            error,
5697            CommandDispatchTransportError::EndpointMismatch {
5698                packet_source,
5699                packet_target: observed_packet_target,
5700                dispatch_target,
5701            } if packet_source == StationId::new(0)
5702                && observed_packet_target == packet_target
5703                && dispatch_target == frame_target
5704        ));
5705        assert!(
5706            queues
5707                .get_mut(&packet_target)
5708                .expect("queue should exist")
5709                .pop_next()
5710                .is_none()
5711        );
5712    }
5713
5714    #[test]
5715    fn cell_migration_moves_owned_entities_and_updates_indexes() {
5716        let grid = GridSpec::new(16.0).expect("valid grid");
5717        let cell = CellCoord3::new(0, 0, 0);
5718        let mut stations = StationSet::default();
5719        let mut source = station(1, 10);
5720        let first = source
5721            .spawn_owned(
5722                EntityId::new(1),
5723                Position3::new(1.0, 1.0, 1.0),
5724                Bounds::Point,
5725                PolicyId::new(0),
5726            )
5727            .expect("first spawn should work");
5728        let second = source
5729            .spawn_owned(
5730                EntityId::new(2),
5731                Position3::new(2.0, 1.0, 1.0),
5732                Bounds::Point,
5733                PolicyId::new(0),
5734            )
5735            .expect("second spawn should work");
5736        stations.push(source);
5737        stations.push(station(2, 10));
5738
5739        let mut source_index = CellIndex::new(grid);
5740        source_index.upsert(first, Position3::new(1.0, 1.0, 1.0), Bounds::Point);
5741        source_index.upsert(second, Position3::new(2.0, 1.0, 1.0), Bounds::Point);
5742        let mut target_index = CellIndex::new(grid);
5743
5744        let mut ownership = CellOwnershipTable::default();
5745        ownership.assign(cell, StationId::new(1));
5746        let update = ownership.apply_split(
5747            &SplitProposal {
5748                source_station: StationId::new(1),
5749                cells_to_move: vec![cell],
5750                moved_pressure_score: 10,
5751            },
5752            StationId::new(2),
5753        );
5754        assert_eq!(ownership.owner_of(cell), Some(StationId::new(2)));
5755        assert_eq!(update.moved_cells, vec![cell]);
5756
5757        let report = CellMigrationExecutor::migrate_cells(
5758            &mut stations,
5759            &mut source_index,
5760            &mut target_index,
5761            StationId::new(1),
5762            StationId::new(2),
5763            &update.moved_cells,
5764            4,
5765        )
5766        .expect("cell migration should work");
5767
5768        assert_eq!(report.entity_migrations.len(), 2);
5769        assert_eq!(target_index.entity_count(), 2);
5770        assert!(
5771            !stations
5772                .get(StationId::new(1))
5773                .expect("source")
5774                .get_by_id(EntityId::new(1))
5775                .expect("source ghost")
5776                .is_owned()
5777        );
5778        assert!(
5779            stations
5780                .get(StationId::new(2))
5781                .expect("target")
5782                .get_by_id(EntityId::new(1))
5783                .expect("target owner")
5784                .is_owned()
5785        );
5786    }
5787
5788    #[test]
5789    fn split_scheduler_plans_and_executes_hot_cell_move() {
5790        let grid = GridSpec::new(16.0).expect("valid grid");
5791        let hot_cell = CellCoord3::new(0, 0, 0);
5792        let mut stations = StationSet::default();
5793        let mut source = station(1, 10);
5794        let handle = source
5795            .spawn_owned(
5796                EntityId::new(1),
5797                Position3::new(1.0, 1.0, 1.0),
5798                Bounds::Point,
5799                PolicyId::new(0),
5800            )
5801            .expect("spawn should work");
5802        stations.push(source);
5803        stations.push(station(2, 10));
5804
5805        let mut source_index = CellIndex::new(grid);
5806        source_index.upsert(handle, Position3::new(1.0, 1.0, 1.0), Bounds::Point);
5807        let mut indexes = StationIndexSet::default();
5808        indexes.insert(StationId::new(1), source_index);
5809        indexes.insert(StationId::new(2), CellIndex::new(grid));
5810
5811        let samples = vec![
5812            StationLoadSample {
5813                station_id: StationId::new(1),
5814                owned_entities: 100,
5815                subscribers: 100,
5816                tick_cost_units: 1000,
5817                cells: vec![CellLoadSample {
5818                    cell: hot_cell,
5819                    owned_entities: 100,
5820                    subscribers: 100,
5821                    event_pressure: 10,
5822                    ..CellLoadSample::default()
5823                }],
5824                ..StationLoadSample::default()
5825            },
5826            StationLoadSample {
5827                station_id: StationId::new(2),
5828                owned_entities: 1,
5829                cells: vec![CellLoadSample {
5830                    cell: CellCoord3::new(10, 0, 0),
5831                    owned_entities: 1,
5832                    ..CellLoadSample::default()
5833                }],
5834                ..StationLoadSample::default()
5835            },
5836        ];
5837        let scheduler = SplitScheduler::new(SplitSchedulerConfig {
5838            thresholds: HotspotThresholds {
5839                max_station_entities: 10,
5840                max_station_subscribers: 10,
5841                max_cell_pressure: 10,
5842                ..HotspotThresholds::default()
5843            },
5844            max_actions_per_pass: 1,
5845            max_cells_per_action: 1,
5846            ghost_ttl_ticks: 4,
5847            ..SplitSchedulerConfig::default()
5848        });
5849        let schedule = scheduler.plan(&samples);
5850        assert_eq!(schedule.actions.len(), 1);
5851        assert_eq!(schedule.actions[0].target_station, StationId::new(2));
5852
5853        let mut ownership = CellOwnershipTable::default();
5854        ownership.assign(hot_cell, StationId::new(1));
5855        let report = scheduler
5856            .execute(&schedule, &mut stations, &mut indexes, &mut ownership)
5857            .expect("execute should work");
5858
5859        assert_eq!(ownership.owner_of(hot_cell), Some(StationId::new(2)));
5860        assert_eq!(report.cell_migrations.len(), 1);
5861        assert_eq!(report.cell_migrations[0].entity_migrations.len(), 1);
5862        assert_eq!(
5863            indexes
5864                .get(StationId::new(2))
5865                .expect("target index")
5866                .entity_count(),
5867            1
5868        );
5869    }
5870
5871    #[test]
5872    fn split_scheduler_respects_source_cooldown() {
5873        let hot_cell = CellCoord3::new(0, 0, 0);
5874        let samples = split_test_samples(hot_cell);
5875        let scheduler = SplitScheduler::new(SplitSchedulerConfig {
5876            thresholds: split_test_thresholds(),
5877            max_actions_per_pass: 1,
5878            max_cells_per_action: 1,
5879            split_cooldown_ticks: 10,
5880            ..SplitSchedulerConfig::default()
5881        });
5882        let mut state = SplitSchedulerState::default();
5883
5884        let initial = scheduler.plan_with_state(&samples, Some(&state), Tick::new(5));
5885        assert_eq!(initial.actions.len(), 1);
5886        state.record_schedule(&initial, Tick::new(5));
5887
5888        let cooled_down = scheduler.plan_with_state(&samples, Some(&state), Tick::new(8));
5889        assert!(cooled_down.actions.is_empty());
5890        assert_eq!(cooled_down.skipped_cooldown, 1);
5891
5892        let after_cooldown = scheduler.plan_with_state(&samples, Some(&state), Tick::new(16));
5893        assert_eq!(after_cooldown.actions.len(), 1);
5894    }
5895
5896    #[test]
5897    fn split_scheduler_reports_capacity_and_improvement_skips() {
5898        let hot_cell = CellCoord3::new(0, 0, 0);
5899        let samples = split_test_samples(hot_cell);
5900
5901        let capacity_guard = SplitScheduler::new(SplitSchedulerConfig {
5902            thresholds: split_test_thresholds(),
5903            max_actions_per_pass: 1,
5904            max_cells_per_action: 1,
5905            max_target_score_after_move: 1,
5906            ..SplitSchedulerConfig::default()
5907        });
5908        let capacity_schedule = capacity_guard.plan(&samples);
5909        assert!(capacity_schedule.actions.is_empty());
5910        assert_eq!(capacity_schedule.skipped_target_capacity, 1);
5911
5912        let improvement_guard = SplitScheduler::new(SplitSchedulerConfig {
5913            thresholds: split_test_thresholds(),
5914            max_actions_per_pass: 1,
5915            max_cells_per_action: 1,
5916            min_score_improvement: u64::MAX,
5917            ..SplitSchedulerConfig::default()
5918        });
5919        let improvement_schedule = improvement_guard.plan(&samples);
5920        assert!(improvement_schedule.actions.is_empty());
5921        assert_eq!(improvement_schedule.skipped_insufficient_improvement, 1);
5922    }
5923
5924    fn split_test_thresholds() -> HotspotThresholds {
5925        HotspotThresholds {
5926            max_station_entities: 10,
5927            max_station_subscribers: 10,
5928            max_cell_pressure: 10,
5929            ..HotspotThresholds::default()
5930        }
5931    }
5932
5933    fn split_test_samples(hot_cell: CellCoord3) -> Vec<StationLoadSample> {
5934        vec![
5935            StationLoadSample {
5936                station_id: StationId::new(1),
5937                owned_entities: 100,
5938                subscribers: 100,
5939                tick_cost_units: 1000,
5940                cells: vec![CellLoadSample {
5941                    cell: hot_cell,
5942                    owned_entities: 100,
5943                    subscribers: 100,
5944                    event_pressure: 10,
5945                    ..CellLoadSample::default()
5946                }],
5947                ..StationLoadSample::default()
5948            },
5949            StationLoadSample {
5950                station_id: StationId::new(2),
5951                owned_entities: 1,
5952                cells: vec![CellLoadSample {
5953                    cell: CellCoord3::new(10, 0, 0),
5954                    owned_entities: 1,
5955                    ..CellLoadSample::default()
5956                }],
5957                ..StationLoadSample::default()
5958            },
5959        ]
5960    }
5961}