Skip to main content

kvas_engine/
signals.rs

1//! Bounded next-frame messages shared by native gameplay scripts.
2//!
3//! Sending writes to a pending buffer. [`SignalBus::begin_frame`] publishes that
4//! buffer before script callbacks, so delivery is independent of script ordering.
5//! Every receiver may read the same event; reads do not consume messages. Process
6//! one-shot reactions in `Script::update`, since fixed steps share the same inbox.
7//! Signals are runtime-only and are omitted from scene JSON/snapshots.
8//!
9//! ```
10//! use kvas_engine::signals::SignalBus;
11//! let mut bus = SignalBus::with_capacity(16);
12//! bus.send("door.open", serde_json::json!({"key": 3}), None, None).unwrap();
13//! assert!(bus.incoming().is_empty());
14//! bus.begin_frame();
15//! assert_eq!(bus.channel("door.open").count(), 1);
16//! bus.begin_frame();
17//! assert!(bus.incoming().is_empty());
18//! ```
19
20use kvas_ecs::Entity;
21use serde_json::Value;
22
23/// One immutable delivered message, with a sequence unique within its bus.
24#[derive(Clone, Debug, PartialEq)]
25pub struct Signal {
26    /// Monotonic sequence starting at one; not a persistent/network identifier.
27    pub sequence: u64,
28    /// Case-sensitive routing channel, such as `door.open` or `damage`.
29    pub channel: String,
30    /// Optional sender identity. It can be stale by delivery time.
31    pub sender: Option<Entity>,
32    /// Optional recipient; `None` broadcasts to every interested script.
33    pub target: Option<Entity>,
34    /// JSON data crossing the host/library boundary without user-defined vtables.
35    pub payload: Value,
36}
37
38/// A message was rejected without changing either buffer or consuming a sequence.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum SignalError {
41    /// Routing channels must contain at least one non-whitespace character.
42    EmptyChannel,
43    /// The pending-buffer limit was reached; retry or deliberately drop the message.
44    Full,
45    /// All u64 sequence values have been used within this bus.
46    SequenceExhausted,
47    /// A script attempted to address an entity that is no longer alive.
48    TargetNotAlive,
49}
50
51impl std::fmt::Display for SignalError {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.write_str(match self {
54            Self::EmptyChannel => "signal channel is empty",
55            Self::Full => "signal pending buffer is full",
56            Self::SequenceExhausted => "signal sequence exhausted",
57            Self::TargetNotAlive => "signal target is not alive",
58        })
59    }
60}
61impl std::error::Error for SignalError {}
62
63/// Double-buffered message service owned by a [`crate::scene::Scene`].
64/// Each buffer is limited independently, so at most twice `capacity()` events
65/// are retained. Payload sizes are caller-owned; no background/global state exists.
66#[derive(Clone, Debug)]
67pub struct SignalBus {
68    incoming: Vec<Signal>,
69    pending: Vec<Signal>,
70    capacity: usize,
71    sequence: u64,
72}
73
74impl Default for SignalBus {
75    fn default() -> Self {
76        Self::with_capacity(256)
77    }
78}
79
80impl SignalBus {
81    /// Creates an empty bus. A zero capacity deliberately rejects all sends.
82    pub fn with_capacity(capacity: usize) -> Self {
83        Self { incoming: Vec::new(), pending: Vec::new(), capacity, sequence: 0 }
84    }
85    /// Enqueues a broadcast or directed signal for the next simulation frame.
86    /// Returns its sequence, or a typed rejection without modifying the bus.
87    /// The host may address any handle; script helpers validate recipient liveness.
88    pub fn send(&mut self, channel: &str, payload: Value, sender: Option<Entity>, target: Option<Entity>) -> Result<u64, SignalError> {
89        if channel.trim().is_empty() {
90            return Err(SignalError::EmptyChannel);
91        }
92        if self.pending.len() >= self.capacity {
93            return Err(SignalError::Full);
94        }
95        let sequence = self.sequence.checked_add(1).ok_or(SignalError::SequenceExhausted)?;
96        self.pending.push(Signal { sequence, channel: channel.to_owned(), sender, target, payload });
97        self.sequence = sequence;
98        Ok(sequence)
99    }
100    /// Expires the previous inbox and publishes all pending messages, preserving order.
101    /// Runtime calls this once before script sync/update, including during single-step.
102    pub fn begin_frame(&mut self) {
103        self.incoming.clear();
104        std::mem::swap(&mut self.incoming, &mut self.pending);
105    }
106    /// Current delivered snapshot, valid until the next mutable bus operation.
107    pub fn incoming(&self) -> &[Signal] {
108        &self.incoming
109    }
110    /// Messages on one exact channel, in send order, without consuming them.
111    pub fn channel<'a>(&'a self, channel: &'a str) -> impl Iterator<Item = &'a Signal> {
112        self.incoming.iter().filter(move |signal| signal.channel == channel)
113    }
114    /// Delivered messages visible to one recipient (broadcast plus exact directed target).
115    pub fn for_entity(&self, entity: Entity) -> impl Iterator<Item = &Signal> {
116        self.incoming.iter().filter(move |signal| signal.target.is_none_or(|target| target == entity))
117    }
118    /// Pending messages to be delivered at the next frame boundary.
119    pub fn pending_len(&self) -> usize {
120        self.pending.len()
121    }
122    /// Maximum event count in each buffer.
123    pub fn capacity(&self) -> usize {
124        self.capacity
125    }
126    /// Discards both inbox and pending messages, preserving the sequence and allocations.
127    pub fn clear(&mut self) {
128        self.incoming.clear();
129        self.pending.clear();
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    #[test]
137    fn buffering_limits_and_entity_generations_are_respected() {
138        let mut world = kvas_ecs::World::new();
139        let old = world.spawn();
140        let mut bus = SignalBus::with_capacity(2);
141        assert_eq!(bus.send("", Value::Null, None, None), Err(SignalError::EmptyChannel));
142        assert_eq!(bus.send("a", Value::Null, None, Some(old)), Ok(1));
143        assert_eq!(bus.send("a", Value::Null, None, None), Ok(2));
144        assert_eq!(bus.send("a", Value::Null, None, None), Err(SignalError::Full));
145        assert!(bus.incoming().is_empty());
146        bus.begin_frame();
147        assert_eq!(bus.for_entity(old).count(), 2);
148        world.despawn(old);
149        let new = world.spawn();
150        assert_eq!(old.index(), new.index());
151        assert_eq!(bus.for_entity(new).count(), 1);
152        assert_eq!(bus.send("a", Value::Null, None, None), Ok(3));
153        bus.begin_frame();
154        assert_eq!(bus.incoming().len(), 1);
155        assert_eq!(bus.incoming()[0].sequence, 3);
156        bus.clear();
157        assert_eq!(bus.pending_len(), 0);
158        assert!(bus.incoming().is_empty());
159    }
160}