pybevy 0.2.1

PyBevy: A Python Real-Time Engine Built on Bevy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
use pyo3::{PyTypeInfo, exceptions::PyTypeError, prelude::*, types::PyType};

use super::{
    PyEntity,
    component_type::{ComponentRegistry, PyComponentType},
};

/// Base class for all events in PyBevy.
///
/// Events can be triggered immediately via World.trigger() or Commands.trigger().
/// Observers watching for these events will run when they are triggered.
///
/// # Example
/// ```python
/// from dataclasses import dataclass
/// from pybevy.ecs import Event, Entity
///
/// @dataclass
/// class PlayerDied(Event):
///     player_id: int
///     cause: str
///
/// # Entity-targeted event (has 'entity' field)
/// @dataclass
/// class Explode(Event):
///     entity: Entity
///     radius: float
/// ```
#[pyclass(name = "Event", subclass)]
#[derive(Debug, Clone)]
pub struct PyEvent;

#[pymethods]
impl PyEvent {
    #[new]
    #[pyo3(signature = (*_args, **_kwargs))]
    pub fn new(
        _args: &Bound<'_, pyo3::types::PyTuple>,
        _kwargs: Option<&Bound<'_, pyo3::types::PyDict>>,
    ) -> Self {
        PyEvent
    }
}

/// Marker class for component addition lifecycle events.
/// Use with On[OnAdd, ComponentType] to observe when components are added.
#[pyclass(name = "OnAdd")]
#[derive(Debug, Clone)]
pub struct PyOnAdd;

/// Marker class for component insertion lifecycle events.
/// Use with On[OnInsert, ComponentType] to observe when components are inserted.
#[pyclass(name = "OnInsert")]
#[derive(Debug, Clone)]
pub struct PyOnInsert;

/// Marker class for component removal lifecycle events.
/// Use with On[OnRemove, ComponentType] to observe when components are removed.
#[pyclass(name = "OnRemove")]
#[derive(Debug, Clone)]
pub struct PyOnRemove;

/// Marker class for component replacement lifecycle events.
/// Use with On[OnReplace, ComponentType] to observe when components are replaced.
#[pyclass(name = "OnReplace")]
#[derive(Debug, Clone)]
pub struct PyOnReplace;

/// Marker class for entity despawn lifecycle events.
/// Use with On[OnDespawn, ComponentType] to observe when entities with the component are despawned.
#[pyclass(name = "OnDespawn")]
#[derive(Debug, Clone)]
pub struct PyOnDespawn;

/// System parameter for observers that provides access to the triggered event.
///
/// # Type Parameters
/// - `E`: The event type (required)
/// - `B`: Optional bundle filter - observer only triggers if entity has these components
///
/// # Example
/// ```python
/// from pybevy.ecs import On, Event
///
/// # Simple observer (no bundle filter)
/// def on_player_died(trigger: On[PlayerDied]) -> None:
///     event = trigger.event()
///     print(f"Player died: {event.player_id}")
///
/// # With bundle filter - only triggers for entities with Mine component
/// def on_explode(trigger: On[Explode, Mine]) -> None:
///     entity = trigger.entity()
///     event = trigger.event()
///     print(f"Mine {entity} exploded")
/// ```
#[pyclass(name = "On", frozen)]
pub struct PyOn {
    pub(crate) event_data: Py<PyAny>,
    pub(crate) entity: Option<bevy::ecs::entity::Entity>,
}

#[pymethods]
impl PyOn {
    #[classmethod]
    #[pyo3(signature = (key, /))]
    pub fn __class_getitem__(
        _cls: &Bound<'_, PyType>,
        key: &Bound<'_, PyAny>,
    ) -> PyResult<Py<PyAny>> {
        let py = key.py();

        // Check if key is a tuple (On[E, B]) or single type (On[E])
        if let Ok(tuple) = key.cast_exact::<pyo3::types::PyTuple>() {
            // On[E, B] - could be event type with bundle filter OR lifecycle event with component
            if tuple.len() != 2 {
                return Err(PyTypeError::new_err(
                    "On expects 1 or 2 type parameters: On[EventType] or On[EventType, BundleType] or On[LifecycleEvent, ComponentType]",
                ));
            }

            let first_key = tuple.get_item(0)?;
            let second_key = tuple.get_item(1)?;

            // Check if first parameter is a lifecycle event marker
            let first_type_obj = first_key.cast_exact::<PyType>()?;

            // Check for lifecycle event markers
            if first_type_obj.is(&PyOnAdd::type_object(py)) {
                // On[OnAdd, Component] - component addition lifecycle event
                let comp_type = if let Ok(comp_type_obj) = second_key.cast_exact::<PyType>() {
                    PyComponentType::try_from((comp_type_obj, py))?
                } else {
                    return Err(PyTypeError::new_err(
                        "Second parameter to On[OnAdd, ...] must be a Component type",
                    ));
                };
                return Py::new(
                    py,
                    PyOnTypeParam {
                        event_type: EventType::Add(comp_type),
                        bundle_filter: None,
                    },
                )
                .map(|obj| obj.into_any());
            } else if first_type_obj.is(&PyOnInsert::type_object(py)) {
                let comp_type = if let Ok(comp_type_obj) = second_key.cast_exact::<PyType>() {
                    PyComponentType::try_from((comp_type_obj, py))?
                } else {
                    return Err(PyTypeError::new_err(
                        "Second parameter to On[OnInsert, ...] must be a Component type",
                    ));
                };
                return Py::new(
                    py,
                    PyOnTypeParam {
                        event_type: EventType::Insert(comp_type),
                        bundle_filter: None,
                    },
                )
                .map(|obj| obj.into_any());
            } else if first_type_obj.is(&PyOnRemove::type_object(py)) {
                let comp_type = if let Ok(comp_type_obj) = second_key.cast_exact::<PyType>() {
                    PyComponentType::try_from((comp_type_obj, py))?
                } else {
                    return Err(PyTypeError::new_err(
                        "Second parameter to On[OnRemove, ...] must be a Component type",
                    ));
                };
                return Py::new(
                    py,
                    PyOnTypeParam {
                        event_type: EventType::Remove(comp_type),
                        bundle_filter: None,
                    },
                )
                .map(|obj| obj.into_any());
            } else if first_type_obj.is(&PyOnReplace::type_object(py)) {
                let comp_type = if let Ok(comp_type_obj) = second_key.cast_exact::<PyType>() {
                    PyComponentType::try_from((comp_type_obj, py))?
                } else {
                    return Err(PyTypeError::new_err(
                        "Second parameter to On[OnReplace, ...] must be a Component type",
                    ));
                };
                return Py::new(
                    py,
                    PyOnTypeParam {
                        event_type: EventType::Replace(comp_type),
                        bundle_filter: None,
                    },
                )
                .map(|obj| obj.into_any());
            } else if first_type_obj.is(&PyOnDespawn::type_object(py)) {
                let comp_type = if let Ok(comp_type_obj) = second_key.cast_exact::<PyType>() {
                    PyComponentType::try_from((comp_type_obj, py))?
                } else {
                    return Err(PyTypeError::new_err(
                        "Second parameter to On[OnDespawn, ...] must be a Component type",
                    ));
                };
                return Py::new(
                    py,
                    PyOnTypeParam {
                        event_type: EventType::Despawn(comp_type),
                        bundle_filter: None,
                    },
                )
                .map(|obj| obj.into_any());
            }

            // Not a lifecycle event, try regular event with bundle filter
            // Extract event type
            let event_type = EventType::from_py_type(py, first_type_obj)?;

            // Extract bundle filter (component type or tuple of component types)
            let bundle_filter = if let Ok(bundle_type_obj) = second_key.cast_exact::<PyType>() {
                // Single component: On[Event, Component]
                let comp_type = PyComponentType::try_from((bundle_type_obj, py))?;
                Some(vec![comp_type])
            } else if let Ok(origin) = second_key.getattr("__origin__") {
                // Multiple components: On[Event, tuple[A, B, C]] (GenericAlias)
                // Check if origin is tuple type
                if origin.is_instance_of::<pyo3::types::PyType>()
                    && origin
                        .cast::<pyo3::types::PyType>()
                        .map(|t| t.name().map(|n| n == "tuple").unwrap_or(false))
                        .unwrap_or(false)
                {
                    let args = second_key.getattr("__args__")?;
                    let mut components = Vec::new();
                    for item in args.try_iter()? {
                        let item = item?;
                        let comp_type_obj = item.cast_exact::<PyType>().map_err(|_| {
                            PyTypeError::new_err(format!(
                                "All items in bundle filter tuple must be Component types, got {:?}",
                                item
                            ))
                        })?;
                        let comp_type = PyComponentType::try_from((comp_type_obj, py))?;
                        components.push(comp_type);
                    }
                    if components.is_empty() {
                        return Err(PyTypeError::new_err("Bundle filter tuple cannot be empty"));
                    }
                    Some(components)
                } else {
                    return Err(PyTypeError::new_err(format!(
                        "Second parameter to On must be a Component type or tuple[...], got {:?}",
                        second_key
                    )));
                }
            } else if let Ok(iter) = second_key.try_iter() {
                // Multiple components: On[Event, (A, B, C)] (actual tuple at runtime)
                let mut components = Vec::new();
                for item in iter {
                    let item = item?;
                    let comp_type_obj = item.cast_exact::<PyType>().map_err(|_| {
                        PyTypeError::new_err(format!(
                            "All items in bundle filter tuple must be Component types, got {:?}",
                            item
                        ))
                    })?;
                    let comp_type = PyComponentType::try_from((comp_type_obj, py))?;
                    components.push(comp_type);
                }
                if components.is_empty() {
                    return Err(PyTypeError::new_err("Bundle filter tuple cannot be empty"));
                }
                Some(components)
            } else {
                return Err(PyTypeError::new_err(format!(
                    "Second parameter to On must be a Component type or tuple of Component types, got {:?}",
                    second_key
                )));
            };

            Py::new(
                py,
                PyOnTypeParam {
                    event_type,
                    bundle_filter,
                },
            )
            .map(|obj| obj.into_any())
        } else {
            // On[E] - event type only, no bundle filter
            let event_type = if let Ok(event_type_obj) = key.cast_exact::<PyType>() {
                EventType::from_py_type(py, event_type_obj)?
            } else {
                return Err(PyTypeError::new_err(format!(
                    "Parameter to On must be an Event type, got {:?}",
                    key
                )));
            };

            Py::new(
                py,
                PyOnTypeParam {
                    event_type,
                    bundle_filter: None,
                },
            )
            .map(|obj| obj.into_any())
        }
    }

    /// Get the event data.
    pub fn event(&self, py: Python) -> Py<PyAny> {
        self.event_data.clone_ref(py)
    }

    /// Get the entity this event targets (for EntityEvents with 'entity' field).
    pub fn entity(&self) -> Option<PyEntity> {
        self.entity.map(PyEntity)
    }
}

/// Represents an event type in the observer system.
#[derive(Debug)]
pub enum EventType {
    /// Built-in component lifecycle events
    Add(PyComponentType),
    Insert(PyComponentType),
    Remove(PyComponentType),
    Replace(PyComponentType),
    Despawn(PyComponentType),

    /// Custom user-defined events (up to 20 supported)
    Custom(Py<PyType>),
}

impl Clone for EventType {
    fn clone(&self) -> Self {
        match self {
            EventType::Add(c) => EventType::Add(c.clone()),
            EventType::Insert(c) => EventType::Insert(c.clone()),
            EventType::Remove(c) => EventType::Remove(c.clone()),
            EventType::Replace(c) => EventType::Replace(c.clone()),
            EventType::Despawn(c) => EventType::Despawn(c.clone()),
            EventType::Custom(ty) => Python::attach(|py| EventType::Custom(ty.clone_ref(py))),
        }
    }
}

impl PartialEq for EventType {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (EventType::Add(a), EventType::Add(b)) => a == b,
            (EventType::Insert(a), EventType::Insert(b)) => a == b,
            (EventType::Remove(a), EventType::Remove(b)) => a == b,
            (EventType::Replace(a), EventType::Replace(b)) => a == b,
            (EventType::Despawn(a), EventType::Despawn(b)) => a == b,
            (EventType::Custom(a), EventType::Custom(b)) => a.is(b),
            _ => false,
        }
    }
}

impl EventType {
    /// Extract event type from a Python type annotation.
    pub(crate) fn from_py_type(_py: Python, ty: &Bound<'_, PyType>) -> PyResult<Self> {
        // Check if it's a subclass of Event
        if ty.is_subclass_of::<PyEvent>()? {
            Ok(EventType::Custom(ty.clone().unbind()))
        } else {
            Err(PyTypeError::new_err(format!(
                "Expected Event subclass, got {}",
                ty.name()?
            )))
        }
    }

    /// Check if this event type matches a Python event instance.
    #[allow(dead_code)] // used by pybevy_control
    pub(crate) fn matches(&self, py: Python, event: &Bound<'_, PyAny>) -> bool {
        match self {
            EventType::Custom(expected_type) => {
                let event_type = event.get_type();
                event_type.is(&expected_type.bind(py))
            }
            // TODO: Implement lifecycle event matching
            _ => false,
        }
    }
}

/// Bundle filter for observers.
///
/// Observers with bundle filters only trigger if the target entity has
/// at least one of the components in the bundle (OR logic).
#[derive(Debug, Clone)]
pub struct BundleFilter {
    pub(crate) components: Vec<PyComponentType>,
}

impl BundleFilter {
    /// Check if an entity matches this bundle filter.
    /// Returns true if the entity has at least one of the components (OR logic).
    pub(crate) fn matches(
        &self,
        world: &bevy::ecs::world::World,
        entity: bevy::ecs::entity::Entity,
    ) -> bool {
        // Get entity reference
        let entity_ref = match world.get_entity(entity) {
            Ok(e) => e,
            Err(_) => return false, // Entity doesn't exist
        };

        // Check if entity has any of the required components (OR logic)
        self.components
            .iter()
            .any(|comp_type| entity_has_component(world, &entity_ref, comp_type))
    }
}

/// Helper function to check if an entity has a specific component type
fn entity_has_component(
    world: &bevy::ecs::world::World,
    entity: &bevy::ecs::world::EntityRef,
    comp_type: &PyComponentType,
) -> bool {
    match comp_type {
        PyComponentType::Custom(type_ptr) => {
            // For custom components, look up the ComponentId from the registry
            if let Some(registry) = world.get_resource::<ComponentRegistry>() {
                if let Some(component_id) = registry.get(*type_ptr) {
                    // Check if entity has this component using the ComponentId
                    return entity.contains_id(component_id);
                }
            }
            // Component not registered or registry doesn't exist
            false
        }
        // For built-in components, use the generated entity_contains method
        _ => comp_type.entity_contains(entity),
    }
}

/// Public helper to check if an entity has a specific component type.
/// This is used by commands.rs to check for component existence before insertion.
pub(crate) fn entity_has_component_type(
    world: &bevy::ecs::world::World,
    entity: bevy::ecs::entity::Entity,
    comp_type: &PyComponentType,
) -> bool {
    match world.get_entity(entity) {
        Ok(entity_ref) => entity_has_component(world, &entity_ref, comp_type),
        Err(_) => false,
    }
}

/// Type parameter for On<E> or On<E, B> system parameters.
///
/// This is returned by On.__class_getitem__ when using On[EventType] or On[EventType, BundleType]
/// syntax in Python type annotations.
#[pyclass(name = "OnTypeParam", frozen)]
#[derive(Debug, Clone)]
pub struct PyOnTypeParam {
    pub(crate) event_type: EventType,
    pub(crate) bundle_filter: Option<Vec<PyComponentType>>,
}