Skip to main content

lumen_script/
wire.rs

1//! The encoded form of the script surface.
2//!
3//! A script host does not have to live in the engine's address space. When it
4//! does not, the values it is called with, the commands it emits, and the
5//! signatures it was described with all travel as bytes, so their encoding is
6//! part of the contract rather than an implementation detail.
7//!
8//! Most types derive their encoding. The two that cannot are
9//! [`PropertyKey`](lumen_core::property_store::PropertyKey) and
10//! [`PropertyValue`](lumen_core::property_store::PropertyValue), which
11//! [`ScriptCommand::SetProperty`](crate::ScriptCommand::SetProperty) carries:
12//! they are storage types, and the adapters live here so the storage layer
13//! stays free of an encoding it has no other use for.
14
15/// Encoding version of the script surface.
16///
17/// Covers [`ScriptValue`](crate::ScriptValue),
18/// [`ScriptCommand`](crate::ScriptCommand),
19/// [`FileDialogKind`](crate::FileDialogKind),
20/// [`ScriptTy`](crate::script_fn::ScriptTy),
21/// [`ScriptParam`](crate::script_fn::ScriptParam),
22/// [`ScriptSig`](crate::script_fn::ScriptSig),
23/// [`ScriptNs`](crate::script_fn::ScriptNs),
24/// [`ScriptPrelude`](crate::script_fn::ScriptPrelude), and [`PluginEvent`].
25///
26/// Enum variants are append-only. The encoding writes a variant by its index,
27/// so inserting or reordering one silently reinterprets every variant after it
28/// as a different command. Add new variants at the end of their enum. Any other
29/// change to a shape listed above (a renamed or retyped field, a removed
30/// variant) bumps this constant.
31pub const SCRIPT_WIRE_VERSION: u16 = 4;
32
33/// [`PropertyKey`](lumen_core::property_store::PropertyKey) on the wire.
34///
35/// An entity travels as its packed bits; a value naming an entity that cannot
36/// exist is rejected rather than reconstructed.
37pub mod property_key {
38    use bevy_ecs::prelude::Entity;
39    use lumen_core::property_store::PropertyKey;
40    use serde::de::Error as _;
41    use serde::{Deserialize, Deserializer, Serialize, Serializer};
42
43    #[derive(Serialize, Deserialize)]
44    enum KeyWire {
45        Global(String),
46        Entity(u64, String),
47    }
48
49    /// Encode a key.
50    pub fn serialize<S: Serializer>(key: &PropertyKey, s: S) -> Result<S::Ok, S::Error> {
51        let wire = match key {
52            PropertyKey::Global(name) => KeyWire::Global(name.to_string()),
53            PropertyKey::Entity(entity, name) => {
54                KeyWire::Entity(entity.to_bits(), name.to_string())
55            }
56        };
57        wire.serialize(s)
58    }
59
60    /// Decode a key.
61    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<PropertyKey, D::Error> {
62        match KeyWire::deserialize(d)? {
63            KeyWire::Global(name) => Ok(PropertyKey::Global(name.into())),
64            KeyWire::Entity(bits, name) => {
65                let entity = Entity::try_from_bits(bits)
66                    .ok_or_else(|| D::Error::custom(format!("not an entity id: {bits}")))?;
67                Ok(PropertyKey::Entity(entity, name.into()))
68            }
69        }
70    }
71}
72
73/// [`PropertyValue`](lumen_core::property_store::PropertyValue) on the wire.
74///
75/// The enumerated variants carry plain data.
76/// [`Custom`](lumen_core::property_store::PropertyValue::Custom) holds an
77/// `Arc<dyn Any>`, which has no encoding at all. A command carrying one fails
78/// to encode and says why; dropping the value instead would hand the far side a
79/// write that looks like it happened and did not.
80pub mod property_value {
81    use glam::Vec2;
82    use lumen_core::components::Color;
83    use lumen_core::property_store::PropertyValue;
84    use serde::ser::Error as _;
85    use serde::{Deserialize, Deserializer, Serialize, Serializer};
86
87    #[derive(Serialize, Deserialize)]
88    enum ValueWire {
89        Bool(bool),
90        I64(i64),
91        F64(f64),
92        Str(String),
93        Color([f32; 4]),
94        Vec2([f32; 2]),
95    }
96
97    /// Encode a value. Errors on
98    /// [`Custom`](PropertyValue::Custom).
99    pub fn serialize<S: Serializer>(value: &PropertyValue, s: S) -> Result<S::Ok, S::Error> {
100        let wire = match value {
101            PropertyValue::Bool(v) => ValueWire::Bool(*v),
102            PropertyValue::I64(v) => ValueWire::I64(*v),
103            PropertyValue::F64(v) => ValueWire::F64(*v),
104            PropertyValue::Str(v) => ValueWire::Str(v.to_string()),
105            PropertyValue::Color(c) => ValueWire::Color([c.r, c.g, c.b, c.a]),
106            PropertyValue::Vec2(v) => ValueWire::Vec2([v.x, v.y]),
107            PropertyValue::Custom(_) => {
108                return Err(S::Error::custom(
109                    "PropertyValue::Custom cannot cross the plugin boundary",
110                ));
111            }
112        };
113        wire.serialize(s)
114    }
115
116    /// Decode a value. Never yields [`Custom`](PropertyValue::Custom).
117    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<PropertyValue, D::Error> {
118        Ok(match ValueWire::deserialize(d)? {
119            ValueWire::Bool(v) => PropertyValue::Bool(v),
120            ValueWire::I64(v) => PropertyValue::I64(v),
121            ValueWire::F64(v) => PropertyValue::F64(v),
122            ValueWire::Str(v) => PropertyValue::Str(v.into()),
123            ValueWire::Color([r, g, b, a]) => PropertyValue::Color(Color { r, g, b, a }),
124            ValueWire::Vec2([x, y]) => PropertyValue::Vec2(Vec2::new(x, y)),
125        })
126    }
127}
128
129/// Something a plugin or a capability pushes at the engine outside a call.
130///
131/// This is the direction a worker thread uses: a plugin that watches a file,
132/// polls a device, or waits on a socket delivers what it found without being
133/// asked. The bytes cross the plugin boundary onto
134/// [`lumen_core::plugin_events`], and the script layer's per-tick drain
135/// decodes them here and routes each one
136/// ([`crate::runtime::collect_plugin_events`]).
137///
138/// A system on the main thread writes one as a message instead
139/// (`MessageWriter<PluginEvent>`, registered by
140/// [`crate::runtime::register_plugin_event_message`]); ordered before
141/// [`crate::runtime::collect_plugin_events`], it is delivered on the tick it
142/// was written. That is how an OS capability (a tray click, a global hotkey,
143/// a resolved file dialog) reaches the script without the core naming it.
144#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, bevy_ecs::message::Message)]
145pub enum PluginEvent {
146    /// Call a handler in the app's script.
147    Call {
148        /// The handler name, as an app's script spells it.
149        event: String,
150        /// Identifier the handler receives as its first argument, so one
151        /// handler can serve several sources; a per-key
152        /// `on(event, key, fn)` registration wins over the fallback.
153        key: String,
154        /// Handler to call when the app defines no `event`; empty for none.
155        fallback: String,
156        /// Arguments for the handler, after the key.
157        args: Vec<crate::ScriptValue>,
158    },
159    /// Apply commands, as if a call had emitted them.
160    Commands(Vec<crate::ScriptCommand>),
161}
162
163/// Push one event onto the bus, from inside the engine's own address space.
164///
165/// This is the delivery path for an engine-locked runtime module and for a
166/// browser add-on: the event goes onto [`lumen_core::plugin_events`] as the
167/// value itself, nothing encoded, and the script layer's per-tick drain routes
168/// it exactly like one a dlopened plugin pushed. The bus wakes a parked event
169/// loop, so an event pushed while the app idles in `Wait` runs on the tick it
170/// triggers, the same as the dlopen path. Returns `false` when the bus is
171/// gone.
172pub fn push_plugin_event(event: PluginEvent) -> bool {
173    lumen_core::plugin_events::push_plugin_value(Box::new(event))
174}