Skip to main content

lotus_shared/
message.rs

1//! Nachrichten zwischen Scripts oder von der Engine verarbeiten.
2//! Siehe [Message] und [MessageType] für weitere Informationen.
3//!
4//! Handle messages between scripts or from the engine.
5//! See [Message] and [MessageType] for more information.
6use std::borrow::Cow;
7
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9
10/// Nachricht, die zwischen Scripts oder von der Engine gesendet werden kann.
11///
12/// Represents a message that can be sent between scripts or from the engine.
13///
14/// # Example
15/// ```no_run
16/// # use serde::{Deserialize, Serialize};
17/// # use lotus_shared::message::{Message, MessageType};
18/// # use lotus_shared::message_type;
19///
20/// // Define a message type, has to implement Serialize and Deserialize
21/// #[derive(Serialize, Deserialize)]
22/// struct TestMessage {
23///     value: i32,
24/// }
25///
26/// // Register the message type
27/// message_type!(TestMessage, "test", "message");
28/// ```
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Message {
31    meta: MessageMeta,
32    #[cfg_attr(feature = "engine", serde(default))]
33    source: MessageSource,
34    value: serde_json::Value,
35}
36
37/// Metadaten für einen Nachrichtentyp.
38///
39/// Die Kombination aus `namespace` und `identifier` sollte für jeden Nachrichtentyp global eindeutig sein.
40///
41/// Represents the metadata for a message type.
42///
43/// The combination of `namespace` and `identifier` should be globally unique for each message type.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
45pub struct MessageMeta {
46    /// Namespace des Nachrichtentyps.
47    ///
48    /// The namespace of the message type.
49    pub namespace: Cow<'static, str>,
50    /// Identifikator des Nachrichtentyps.
51    ///
52    /// The identifier of the message type.
53    pub identifier: Cow<'static, str>,
54    /// Bus, über den die Nachricht gesendet werden soll.
55    ///
56    /// The bus the message should be sent on.
57    pub bus: Option<Cow<'static, str>>,
58}
59
60impl MessageMeta {
61    /// Erstellt neue Nachrichten-Metadaten.
62    ///
63    /// Creates a new message meta.
64    pub const fn new(
65        namespace: &'static str,
66        identifier: &'static str,
67        bus: Option<&'static str>,
68    ) -> Self {
69        Self {
70            namespace: Cow::Borrowed(namespace),
71            identifier: Cow::Borrowed(identifier),
72            bus: match bus {
73                Some(bus) => Some(Cow::Borrowed(bus)),
74                None => None,
75            },
76        }
77    }
78}
79
80/// Nachrichtentyp, der zwischen Scripts oder von der Engine gesendet werden kann.
81///
82/// Die Konstante [`MessageType::MESSAGE_META`] sollte global eindeutige Metadaten für den Nachrichtentyp liefern.
83///
84/// Represents a message type that can be sent between scripts or from the engine.
85///
86/// The [`MessageType::MESSAGE_META`] constant should return a globally unique message meta for the message type.
87pub trait MessageType: Serialize + DeserializeOwned {
88    /// Metadaten für den Nachrichtentyp.
89    ///
90    /// The metadata for the message type.
91    const MESSAGE_META: MessageMeta;
92}
93
94/// Quelle einer Nachricht.
95///
96/// Represents the source of a message.
97#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
98pub struct MessageSource {
99    /// Kupplung, über die die Nachricht von einem anderen Fahrzeug kommt, falls zutreffend.
100    ///
101    /// If the message is coming from another vehicle across couplings, this will be Some.
102    pub coupling: Option<Coupling>,
103    /// Modul-Slot-Index des sendenden Moduls, falls zutreffend.
104    ///
105    /// If the message is coming from a module, these will be Some.
106    pub module_slot_index: Option<u16>,
107    /// Cockpit-Index des sendenden Modul-Slots, falls vorhanden.
108    ///
109    /// Cockpit index of the sending module slot, if applicable.
110    pub module_slot_cockpit_index: Option<u8>,
111}
112
113impl MessageSource {
114    /// Gibt `true` zurück, wenn die Nachricht vom vorderen Fahrzeug kommt.
115    ///
116    /// Returns `true` if the message is coming from the vehicle in front.
117    pub fn is_front(&self) -> bool {
118        matches!(self.coupling, Some(Coupling::Front))
119    }
120
121    /// Gibt `true` zurück, wenn die Nachricht vom hinteren Fahrzeug kommt.
122    ///
123    /// Returns `true` if the message is coming from the vehicle in rear.
124    pub fn is_rear(&self) -> bool {
125        matches!(self.coupling, Some(Coupling::Rear))
126    }
127}
128
129#[doc(hidden)]
130#[macro_export]
131macro_rules! message_type {
132    ($type:ty, $namespace:expr, $identifier:expr, $bus:expr) => {
133        impl $crate::message::MessageType for $type {
134            const MESSAGE_META: $crate::message::MessageMeta =
135                $crate::message::MessageMeta::new($namespace, $identifier, Some($bus));
136        }
137    };
138    ($type:ty, $namespace:expr, $identifier:expr) => {
139        impl $crate::message::MessageType for $type {
140            const MESSAGE_META: $crate::message::MessageMeta =
141                $crate::message::MessageMeta::new($namespace, $identifier, None);
142        }
143    };
144}
145
146#[doc(inline)]
147pub use message_type;
148
149/// Fehler, wenn das Deserialisieren einer Nachrichtennutzlast fehlschlägt.
150///
151/// Error returned when deserializing a message payload fails.
152#[derive(Debug, thiserror::Error)]
153pub enum MessageValueError {
154    /// Der Nachrichtentyp entspricht nicht dem angeforderten Typ.
155    ///
156    /// The message type does not match the requested type.
157    #[error("invalid message type")]
158    InvalidType,
159    #[error("{0}")]
160    Serialization(SerializationError),
161}
162
163/// Serialisierungsfehler beim Lesen oder Schreiben von Nachrichtendaten.
164///
165/// Serialization failure while reading or writing message data.
166#[derive(Debug, thiserror::Error)]
167#[error("serialization error: {0}")]
168pub struct SerializationError(String);
169
170/// Fehler, der von [`Message::handle`] zurückgegeben wird.
171///
172/// Error returned by [`Message::handle`].
173#[derive(Debug, thiserror::Error)]
174pub enum MessageHandleError {
175    /// Fehler beim Deserialisieren der Nachrichtennutzlast.
176    ///
177    /// Failure while deserializing the message payload.
178    #[error("{0}")]
179    Serialization(SerializationError),
180    /// Die Handler-Funktion hat einen Fehler zurückgegeben.
181    ///
182    /// The handler function returned an error.
183    #[error("handler error: {0}")]
184    Handler(Box<dyn std::error::Error>),
185}
186
187impl Message {
188    /// Erstellt eine neue Nachricht mit dem angegebenen Wert.
189    ///
190    /// Creates a new message with the given value.
191    pub fn new<T: MessageType>(value: &T) -> Self {
192        Self {
193            meta: T::MESSAGE_META.clone(),
194            source: MessageSource::default(),
195            value: serde_json::to_value(value).unwrap(),
196        }
197    }
198
199    /// Gibt die Metadaten des Nachrichtentyps zurück.
200    ///
201    /// Returns the message type metadata.
202    pub fn meta(&self) -> &MessageMeta {
203        &self.meta
204    }
205
206    /// Gibt die Quelle der Nachricht zurück.
207    ///
208    /// Returns the source of the message.
209    pub fn source(&self) -> &MessageSource {
210        &self.source
211    }
212
213    /// Gibt den Nachrichtenwert als angegebenen Typ zurück.
214    /// Liefert einen [`MessageValueError`], wenn der Typ nicht passt.
215    ///
216    /// Returns the message value as the given type. Returns a [MessageValueError] if the message has a different type.
217    pub fn value<T: MessageType>(&self) -> Result<T, MessageValueError> {
218        if self.meta != T::MESSAGE_META {
219            return Err(MessageValueError::InvalidType);
220        }
221
222        serde_json::from_value(self.value.clone())
223            .map_err(|e| MessageValueError::Serialization(SerializationError(e.to_string())))
224    }
225
226    /// Gibt `true` zurück, wenn die Nachricht den angegebenen Typ hat.
227    ///
228    /// Returns `true` if the message has the given type.
229    pub fn has_type<T: MessageType>(&self) -> bool {
230        self.meta == T::MESSAGE_META
231    }
232
233    /// Verarbeitet die Nachricht mit der angegebenen Handler-Funktion.
234    /// Liefert `Ok(true)`, wenn verarbeitet, `Ok(false)` bei Typabweichung,
235    /// oder `Err` bei Deserialisierungs- bzw. Handlerfehler.
236    ///
237    /// Die Handler-Funktion sollte `Ok(())` zurückgeben, wenn die Nachricht erfolgreich verarbeitet wurde.
238    ///
239    /// Handle the message with the given handler function.
240    /// Returns `Ok(true)` if the message was handled, `Ok(false)` if the message has a different type,
241    /// or `Err` if the message could not be deserialized or the handler function returned an error.
242    ///
243    /// The handler function should return `Ok(())` if the message was handled successfully.
244    pub fn handle<T: MessageType>(
245        &self,
246        f: impl FnOnce(T) -> Result<(), Box<dyn std::error::Error>>,
247    ) -> Result<bool, MessageHandleError> {
248        match self.value::<T>() {
249            Ok(v) => f(v).map_err(MessageHandleError::Handler).map(|_| true),
250            Err(MessageValueError::InvalidType) => Ok(false),
251            Err(MessageValueError::Serialization(e)) => Err(MessageHandleError::Serialization(e)),
252        }
253    }
254
255    #[cfg(feature = "engine")]
256    /// Gibt eine Kopie dieser Nachricht mit aktualisierter Quelle zurück.
257    ///
258    /// Returns a copy of this message with an updated source.
259    pub fn with_source(&self, source: MessageSource) -> Self {
260        Self {
261            meta: self.meta.clone(),
262            source,
263            value: self.value.clone(),
264        }
265    }
266}
267
268/// Wandelt einen Wert in einen oder mehrere [`MessageTarget`]-Empfänger um.
269///
270/// Converts a value into one or more [`MessageTarget`] recipients.
271pub trait IntoMessageTargets {
272    fn into_message_targets(self) -> impl IntoIterator<Item = MessageTarget>;
273}
274
275impl IntoMessageTargets for MessageTarget {
276    fn into_message_targets(self) -> impl IntoIterator<Item = MessageTarget> {
277        [self]
278    }
279}
280
281impl<T> IntoMessageTargets for T
282where
283    T: IntoIterator<Item = MessageTarget>,
284{
285    fn into_message_targets(self) -> impl IntoIterator<Item = MessageTarget> {
286        self
287    }
288}
289
290/// Sendet die Nachricht an die angegebenen Ziele.
291///
292/// Sends the message to the given targets.
293///
294/// # Example
295/// ```no_run
296/// # #[cfg(target_arch = "wasm32")]
297/// # {
298/// # use lotus_shared::message::{Message, MessageTarget, send_message};
299/// # use serde::{Deserialize, Serialize};
300/// # use lotus_shared::message_type;
301/// # #[derive(Serialize, Deserialize)]
302/// # struct TestMessage { value: i32 };
303/// # message_type!(TestMessage, "test", "message");
304/// // Send a message with only a single target
305/// send_message(&TestMessage { value: 42 }, MessageTarget::Myself);
306/// // Send a message to multiple targets
307/// send_message(&TestMessage { value: 42 }, [MessageTarget::Myself, MessageTarget::ModuleSlot(0)]);
308/// # }
309/// ```
310#[cfg(feature = "ffi")]
311pub fn send_message<T: MessageType>(message: &T, targets: impl IntoMessageTargets) {
312    let message = Message::new(message);
313    let this = lotus_script_sys::FfiObject::new(&message);
314    let targets = targets
315        .into_message_targets()
316        .into_iter()
317        .collect::<Vec<_>>();
318    let targets = lotus_script_sys::FfiObject::new(&targets);
319
320    unsafe { lotus_script_sys::messages::send(targets.packed(), this.packed()) }
321}
322
323/// Ziel einer Nachricht.
324///
325/// Represents a message target.
326#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
327pub enum MessageTarget {
328    /// Das Script selbst.
329    ///
330    /// The script itself.
331    Myself,
332    /// Das Child-Script am angegebenen Index in der Child-Script-Liste.
333    ///
334    /// The child script at the given index in the child-script list.
335    #[deprecated(note = "use `MessageTarget::ModuleSlot` for vehicle module slots")]
336    #[doc(hidden)]
337    ChildByIndex(usize),
338    /// Das Modul im Fahrzeug-Modulslot mit dem angegebenen Index (`module_slot_index`).
339    ///
340    /// The module in the vehicle module slot with the given index (`module_slot_index`).
341    ModuleSlot(usize),
342    /// An alle Module im Cockpit mit dem angegebenen Index.
343    ///
344    /// To all modules in the cockpit with the given index.
345    Cockpit(u8),
346    /// Broadcast an Scripts gemäß dem angegebenen Umfang.
347    ///
348    /// Broadcast to scripts based on the specified scope.
349    Broadcast {
350        /// Ob gekuppelte Fahrzeuge einbezogen werden.
351        ///
352        /// Whether to include coupled vehicles.
353        across_couplings: bool,
354        /// Ob das sendende Script einbezogen wird.
355        ///
356        /// Whether to include the sending script.
357        include_self: bool,
358    },
359    /// Senden an eine bestimmte Kupplung.
360    ///
361    /// Send to a specific coupling.
362    AcrossCoupling {
363        /// Kupplung, an die gesendet wird.
364        ///
365        /// The coupling to send to.
366        coupling: Coupling,
367        /// Ob die Nachricht zur nächsten Kupplung weitergeleitet wird.
368        ///
369        /// Whether to cascade the message to the next coupling.
370        cascade: bool,
371    },
372    /// Das übergeordnete Script.
373    ///
374    /// The parent script.
375    Parent,
376}
377
378impl MessageTarget {
379    /// Hilfsfunktion für Broadcast-Ziele ohne das eigene Script.
380    ///
381    /// Helper to create a broadcast target that excludes self
382    pub fn broadcast_except_self(across_couplings: bool) -> Self {
383        Self::Broadcast {
384            across_couplings,
385            include_self: false,
386        }
387    }
388
389    /// Sendet an alle Scripts in der Zugbildung, einschließlich des eigenen.
390    ///
391    /// Broadcasts to all scripts in the train composition, including self.
392    pub fn broadcast_all() -> Self {
393        Self::Broadcast {
394            across_couplings: true,
395            include_self: true,
396        }
397    }
398}
399
400/// Kupplungsrichtung zwischen Fahrzeugen in einem Zug.
401///
402/// Coupling direction between vehicles in a train.
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub enum Coupling {
406    /// Kupplung zum vorderen Fahrzeug.
407    ///
408    /// The coupling to the front vehicle.
409    Front,
410    /// Kupplung zum hinteren Fahrzeug.
411    ///
412    /// The coupling to the rear vehicle.
413    Rear,
414}
415
416impl Coupling {
417    #[cfg(feature = "ffi")]
418    /// Öffnet den angegebenen Bus.
419    ///
420    /// Opens the given bus.
421    pub fn open_bus(&self, bus: &str) {
422        let bus = lotus_script_sys::FfiObject::new(&bus);
423        unsafe { lotus_script_sys::vehicle::open_bus(*self as u32, bus.packed()) };
424    }
425
426    #[cfg(feature = "ffi")]
427    /// Schließt den angegebenen Bus.
428    ///
429    /// Closes the given bus.
430    pub fn close_bus(&self, bus: &str) {
431        let bus = lotus_script_sys::FfiObject::new(&bus);
432        unsafe { lotus_script_sys::vehicle::close_bus(*self as u32, bus.packed()) };
433    }
434
435    #[cfg(feature = "ffi")]
436    /// Gibt `true` zurück, wenn der angegebene Bus geöffnet ist.
437    ///
438    /// Returns `true` if the given bus is open.
439    pub fn is_open(&self, bus: &str) -> bool {
440        let bus = lotus_script_sys::FfiObject::new(&bus);
441        unsafe { lotus_script_sys::vehicle::is_bus_open(*self as u32, bus.packed()) == 1 }
442    }
443
444    #[cfg(feature = "ffi")]
445    /// Gibt `true` zurück, wenn an dieser Kupplung ein Fahrzeug gekoppelt ist.
446    ///
447    /// Returns `true` if a vehicle is coupled at this coupling.
448    pub fn is_coupled(&self) -> bool {
449        unsafe { lotus_script_sys::vehicle::is_coupled(*self as u32) == 1 }
450    }
451}
452
453impl From<u32> for Coupling {
454    fn from(value: u32) -> Self {
455        match value {
456            0 => Self::Front,
457            1 => Self::Rear,
458            _ => panic!("invalid coupling value: {}", value),
459        }
460    }
461}
462
463impl From<usize> for Coupling {
464    fn from(value: usize) -> Self {
465        match value {
466            0 => Self::Front,
467            1 => Self::Rear,
468            _ => panic!("invalid coupling value: {}", value),
469        }
470    }
471}
472
473impl From<Coupling> for usize {
474    fn from(value: Coupling) -> Self {
475        match value {
476            Coupling::Front => 0,
477            Coupling::Rear => 1,
478        }
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use serde::Deserialize;
485
486    use super::*;
487
488    #[derive(Debug, Serialize, Deserialize, PartialEq)]
489    struct TestMessage {
490        value: i32,
491    }
492
493    message_type!(TestMessage, "test", "message", "ibis");
494
495    #[test]
496    fn test_message() {
497        let message = Message::new(&TestMessage { value: 42 });
498        assert_eq!(message.meta(), &TestMessage::MESSAGE_META);
499
500        let value = message.value::<TestMessage>().unwrap();
501
502        assert_eq!(value, TestMessage { value: 42 });
503
504        message
505            .handle::<TestMessage>(|m| {
506                assert_eq!(m.value, 42);
507                Ok(())
508            })
509            .expect("message handle failed");
510    }
511}