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