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}