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}