use std::sync::{Arc, Mutex};
use bevy::ecs::schedule::ScheduleLabel;
use pyo3::{
exceptions::{PyRuntimeError, PyTypeError, PyValueError},
prelude::*,
types::PyType,
};
use crate::ecs::{component::PyComponent, resource::PyResource};
#[pyfunction]
pub fn state(py: Python, cls: Bound<PyType>) -> PyResult<Py<PyType>> {
let enum_type = py.import("enum")?.getattr("Enum")?;
if !cls.is_subclass(&enum_type)? {
return Err(PyTypeError::new_err(
"@state decorator can only be applied to Enum subclasses",
));
}
let members_dict = cls.getattr("__members__")?;
let values = members_dict.call_method0("values")?;
let mut has_members = false;
for _ in values.try_iter()? {
has_members = true;
break;
}
if !has_members {
return Err(PyValueError::new_err(
"State enum must have at least one variant",
));
}
cls.setattr("__pybevy_state__", true)?;
Ok(cls.unbind())
}
#[pyclass(name = "State", frozen, extends = PyResource)]
pub struct PyState {
current: Arc<Mutex<Py<PyAny>>>,
state_type: Py<PyType>,
}
#[pymethods]
impl PyState {
#[new]
fn py_new(py: Python, initial_state: Py<PyAny>) -> PyResult<(Self, PyResource)> {
let state_type = initial_state.bind(py).get_type().unbind();
Self::validate_state_type(py, &state_type)?;
Ok((
PyState {
current: Arc::new(Mutex::new(initial_state)),
state_type,
},
PyResource,
))
}
fn get(&self, py: Python) -> Py<PyAny> {
self.current.lock().unwrap().clone_ref(py)
}
fn __eq__(&self, _py: Python, other: &Bound<PyAny>) -> PyResult<bool> {
let self_ptr = self as *const Self as *const ();
let other_ptr = other.as_ptr() as *const ();
if self_ptr == other_ptr {
return Ok(true);
}
Ok(false)
}
fn __repr__(&self, py: Python) -> PyResult<String> {
let state_str = self.current.lock().unwrap().bind(py).repr()?.to_string();
Ok(format!("State({})", state_str))
}
}
impl PyState {
pub fn new(py: Python, state_value: Py<PyAny>) -> PyResult<Py<Self>> {
let state_type = state_value.bind(py).get_type().unbind();
Self::validate_state_type(py, &state_type)?;
Py::new(
py,
(
PyState {
current: Arc::new(Mutex::new(state_value)),
state_type,
},
PyResource,
),
)
}
pub fn current_value(&self, py: Python) -> Py<PyAny> {
self.current.lock().unwrap().clone_ref(py)
}
pub fn set_value(&self, new_value: Py<PyAny>) {
*self.current.lock().unwrap() = new_value;
}
fn validate_state_type(py: Python, state_type: &Py<PyType>) -> PyResult<()> {
let type_bound = state_type.bind(py);
if !type_bound.hasattr("__pybevy_state__")? {
return Err(PyTypeError::new_err(format!(
"Type '{}' is not a valid state type. Did you forget the @state decorator?",
type_bound.name()?
)));
}
Ok(())
}
}
#[pyclass(name = "NextState", frozen, extends = PyResource)]
pub struct PyNextState {
inner: Arc<Mutex<NextStateInner>>,
state_type: Py<PyType>,
initial_enter_pending: Arc<Mutex<bool>>,
}
enum NextStateInner {
Unchanged,
Pending(Py<PyAny>),
}
#[pymethods]
impl PyNextState {
fn set(&self, py: Python, state: Py<PyAny>) -> PyResult<()> {
let state_bound = state.bind(py);
let provided_type = state_bound.get_type();
if !provided_type.is(&self.state_type.bind(py)) {
return Err(PyTypeError::new_err(format!(
"State type mismatch: expected {}, got {}",
self.state_type.bind(py).name()?,
provided_type.name()?
)));
}
*self.inner.lock().unwrap() = NextStateInner::Pending(state);
Ok(())
}
fn reset(&self) -> PyResult<()> {
*self.inner.lock().unwrap() = NextStateInner::Unchanged;
Ok(())
}
fn is_pending(&self) -> bool {
matches!(*self.inner.lock().unwrap(), NextStateInner::Pending(_))
}
fn peek_pending(&self, py: Python) -> Option<Py<PyAny>> {
match &*self.inner.lock().unwrap() {
NextStateInner::Pending(state) => Some(state.clone_ref(py)),
NextStateInner::Unchanged => None,
}
}
fn __repr__(&self, py: Python) -> PyResult<String> {
let inner = self.inner.lock().unwrap();
match &*inner {
NextStateInner::Unchanged => Ok("NextState(Unchanged)".to_string()),
NextStateInner::Pending(state) => {
let state_str = state.bind(py).repr()?.to_string();
Ok(format!("NextState(Pending({}))", state_str))
}
}
}
}
impl PyNextState {
pub fn new(py: Python, state_type: Py<PyType>) -> PyResult<Py<Self>> {
PyState::validate_state_type(py, &state_type)?;
Py::new(
py,
(
PyNextState {
inner: Arc::new(Mutex::new(NextStateInner::Unchanged)),
state_type,
initial_enter_pending: Arc::new(Mutex::new(true)),
},
PyResource,
),
)
}
pub fn take_pending(&self) -> Option<Py<PyAny>> {
let mut inner = self.inner.lock().unwrap();
match std::mem::replace(&mut *inner, NextStateInner::Unchanged) {
NextStateInner::Pending(state) => Some(state),
NextStateInner::Unchanged => None,
}
}
pub fn take_initial_enter_pending(&self) -> bool {
let mut pending = self.initial_enter_pending.lock().unwrap();
if *pending {
*pending = false;
true
} else {
false
}
}
}
#[pyclass(name = "OnEnterSchedule", frozen)]
pub struct PyOnEnterSchedule {
state_value: Py<PyAny>,
}
#[pymethods]
impl PyOnEnterSchedule {
fn __repr__(&self, py: Python) -> PyResult<String> {
let state_str = self.state_value.bind(py).repr()?.to_string();
Ok(format!("OnEnter({})", state_str))
}
fn __hash__(&self, py: Python) -> PyResult<u64> {
Ok(self.state_value.bind(py).hash()? as u64)
}
fn __eq__(&self, py: Python, other: &Bound<PyAny>) -> PyResult<bool> {
if let Ok(other_schedule) = other.extract::<PyRef<Self>>() {
Ok(self
.state_value
.bind(py)
.is(&other_schedule.state_value.bind(py)))
} else {
Ok(false)
}
}
}
#[pyclass(name = "OnExitSchedule", frozen)]
pub struct PyOnExitSchedule {
state_value: Py<PyAny>,
}
#[pymethods]
impl PyOnExitSchedule {
fn __repr__(&self, py: Python) -> PyResult<String> {
let state_str = self.state_value.bind(py).repr()?.to_string();
Ok(format!("OnExit({})", state_str))
}
fn __hash__(&self, py: Python) -> PyResult<u64> {
Ok(self.state_value.bind(py).hash()? as u64)
}
fn __eq__(&self, py: Python, other: &Bound<PyAny>) -> PyResult<bool> {
if let Ok(other_schedule) = other.extract::<PyRef<Self>>() {
Ok(self
.state_value
.bind(py)
.is(&other_schedule.state_value.bind(py)))
} else {
Ok(false)
}
}
}
#[pyclass(name = "OnTransitionSchedule", frozen)]
pub struct PyOnTransitionSchedule {
exited: Py<PyAny>,
entered: Py<PyAny>,
}
#[pymethods]
impl PyOnTransitionSchedule {
fn __repr__(&self, py: Python) -> PyResult<String> {
let exited_str = self.exited.bind(py).repr()?.to_string();
let entered_str = self.entered.bind(py).repr()?.to_string();
Ok(format!("OnTransition({} -> {})", exited_str, entered_str))
}
fn __hash__(&self, py: Python) -> PyResult<u64> {
let hash1 = self.exited.bind(py).hash()? as u64;
let hash2 = self.entered.bind(py).hash()? as u64;
Ok(hash1.wrapping_mul(31).wrapping_add(hash2))
}
fn __eq__(&self, py: Python, other: &Bound<PyAny>) -> PyResult<bool> {
if let Ok(other_schedule) = other.extract::<PyRef<Self>>() {
let exited_eq = self.exited.bind(py).is(&other_schedule.exited.bind(py));
let entered_eq = self.entered.bind(py).is(&other_schedule.entered.bind(py));
Ok(exited_eq && entered_eq)
} else {
Ok(false)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum ScheduleKind {
Enter,
Exit,
}
#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]
pub struct StateScheduleLabel {
kind: ScheduleKind,
state_hash: u64,
}
impl StateScheduleLabel {
pub fn on_enter(state_hash: u64) -> Self {
Self {
kind: ScheduleKind::Enter,
state_hash,
}
}
pub fn on_exit(state_hash: u64) -> Self {
Self {
kind: ScheduleKind::Exit,
state_hash,
}
}
}
#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash)]
pub struct TransitionScheduleLabel {
exit_hash: u64,
enter_hash: u64,
}
impl TransitionScheduleLabel {
pub fn new(exit_hash: u64, enter_hash: u64) -> Self {
Self {
exit_hash,
enter_hash,
}
}
}
impl PyOnEnterSchedule {
pub fn to_bevy_label(&self, py: Python) -> PyResult<StateScheduleLabel> {
let hash = self.state_value.bind(py).hash()? as u64;
Ok(StateScheduleLabel::on_enter(hash))
}
}
impl PyOnExitSchedule {
pub fn to_bevy_label(&self, py: Python) -> PyResult<StateScheduleLabel> {
let hash = self.state_value.bind(py).hash()? as u64;
Ok(StateScheduleLabel::on_exit(hash))
}
}
impl PyOnTransitionSchedule {
pub fn to_bevy_label(&self, py: Python) -> PyResult<TransitionScheduleLabel> {
let exit_hash = self.exited.bind(py).hash()? as u64;
let enter_hash = self.entered.bind(py).hash()? as u64;
Ok(TransitionScheduleLabel::new(exit_hash, enter_hash))
}
}
#[pyfunction]
#[pyo3(name = "OnEnter")]
pub fn on_enter(py: Python, state: Py<PyAny>) -> PyResult<Py<PyOnEnterSchedule>> {
let state_type = state.bind(py).get_type().unbind();
PyState::validate_state_type(py, &state_type)?;
Py::new(py, PyOnEnterSchedule { state_value: state })
}
#[pyfunction]
#[pyo3(name = "OnExit")]
pub fn on_exit(py: Python, state: Py<PyAny>) -> PyResult<Py<PyOnExitSchedule>> {
let state_type = state.bind(py).get_type().unbind();
PyState::validate_state_type(py, &state_type)?;
Py::new(py, PyOnExitSchedule { state_value: state })
}
#[pyfunction]
#[pyo3(name = "OnTransition")]
pub fn on_transition(
py: Python,
exited: Py<PyAny>,
entered: Py<PyAny>,
) -> PyResult<Py<PyOnTransitionSchedule>> {
let exited_type = exited.bind(py).get_type();
let entered_type = entered.bind(py).get_type();
if !exited_type.is(&entered_type) {
return Err(PyTypeError::new_err(
"OnTransition requires both states to be the same type",
));
}
PyState::validate_state_type(py, &exited_type.unbind())?;
Py::new(py, PyOnTransitionSchedule { exited, entered })
}
#[pyclass(name = "DespawnOnExit", frozen, extends = PyComponent)]
pub struct PyDespawnOnExit {
state_value: Py<PyAny>,
}
#[pymethods]
impl PyDespawnOnExit {
#[new]
fn new(py: Python, state: Py<PyAny>) -> PyResult<(Self, PyComponent)> {
let state_type = state.bind(py).get_type().unbind();
PyState::validate_state_type(py, &state_type)?;
Ok((PyDespawnOnExit { state_value: state }, PyComponent))
}
fn __repr__(&self, py: Python) -> PyResult<String> {
let state_str = self.state_value.bind(py).repr()?.to_string();
Ok(format!("DespawnOnExit({})", state_str))
}
fn state_value(&self, py: Python) -> Py<PyAny> {
self.state_value.clone_ref(py)
}
}
#[pyclass(name = "DespawnOnEnter", frozen, extends = PyComponent)]
pub struct PyDespawnOnEnter {
state_value: Py<PyAny>,
}
#[pymethods]
impl PyDespawnOnEnter {
#[new]
fn new(py: Python, state: Py<PyAny>) -> PyResult<(Self, PyComponent)> {
let state_type = state.bind(py).get_type().unbind();
PyState::validate_state_type(py, &state_type)?;
Ok((PyDespawnOnEnter { state_value: state }, PyComponent))
}
fn __repr__(&self, py: Python) -> PyResult<String> {
let state_str = self.state_value.bind(py).repr()?.to_string();
Ok(format!("DespawnOnEnter({})", state_str))
}
fn state_value(&self, py: Python) -> Py<PyAny> {
self.state_value.clone_ref(py)
}
}
#[pyfunction]
pub fn in_state(py: Python, state: Py<PyAny>) -> PyResult<Py<PyAny>> {
let state_type = state.bind(py).get_type().unbind();
PyState::validate_state_type(py, &state_type)?;
use std::ffi::CString;
use pyo3::types::PyDict;
let globals = PyDict::new(py);
let ecs_module = py.import("pybevy.ecs")?;
globals.set_item("Res", ecs_module.getattr("Res")?)?;
globals.set_item("State", ecs_module.getattr("State")?)?;
let locals = PyDict::new(py);
locals.set_item("target_state", state)?;
let code = CString::new(
r#"
def _make_in_state_condition(target):
"""Factory that creates a condition checking for a specific state."""
def in_state_condition(current: Res[State]) -> bool:
"""Check if current State matches the target state."""
return current.get() == target
# Set a meaningful name for debugging
in_state_condition.__name__ = f"in_state({target})"
return in_state_condition
result = _make_in_state_condition(target_state)
"#,
)?;
py.run(&code, Some(&globals), Some(&locals))?;
let condition = locals
.get_item("result")?
.ok_or_else(|| PyRuntimeError::new_err("Failed to create in_state condition"))?;
Ok(condition.unbind())
}
fn apply_transition_for_state(
py: Python,
world: &mut bevy::ecs::world::World,
state_type_name: &str,
) -> PyResult<bool> {
use bevy::ecs::schedule::Schedules;
use crate::ecs::resource_type::PyResourceStorage;
let mut next_state_opt: Option<Py<PyNextState>> = None;
let mut state_opt: Option<Py<PyState>> = None;
{
let resource_storage = world.resource::<PyResourceStorage>();
for (_, resource_py) in resource_storage.resources.iter() {
let resource = resource_py.bind(py);
if let Ok(next_state) = resource.cast::<PyNextState>() {
let type_name = next_state
.borrow()
.state_type
.bind(py)
.name()
.unwrap()
.to_string();
if type_name == state_type_name {
next_state_opt = Some(
resource_py
.clone_ref(py)
.extract::<Py<PyNextState>>(py)
.unwrap(),
);
}
}
if let Ok(state) = resource.cast::<PyState>() {
let type_name = state
.borrow()
.state_type
.bind(py)
.name()
.unwrap()
.to_string();
if type_name == state_type_name {
state_opt = Some(
resource_py
.clone_ref(py)
.extract::<Py<PyState>>(py)
.unwrap(),
);
}
}
}
}
let next_state_py = match next_state_opt {
Some(ns) => ns,
None => return Ok(false), };
let state_py = match state_opt {
Some(s) => s,
None => return Ok(false), };
let next_state_borrow = next_state_py.bind(py).borrow();
let pending_transition = next_state_borrow.take_pending();
let initial_enter =
pending_transition.is_none() && next_state_borrow.take_initial_enter_pending();
drop(next_state_borrow);
if initial_enter {
let current_state = {
let state = state_py.bind(py).borrow();
state.current_value(py)
};
let hash = current_state.bind(py).hash()? as u64;
let enter_label = StateScheduleLabel::on_enter(hash);
let has_enter_schedule = world.resource::<Schedules>().contains(enter_label.clone());
if has_enter_schedule {
world.try_run_schedule(enter_label).ok();
}
return Ok(true);
}
let pending_transition = match pending_transition {
Some(new_state) => new_state,
None => return Ok(false), };
let current_state = {
let state = state_py.bind(py).borrow();
state.current_value(py)
};
let old_hash = current_state.bind(py).hash()? as u64;
let new_hash = pending_transition.bind(py).hash()? as u64;
let exit_label = StateScheduleLabel::on_exit(old_hash);
let has_exit_schedule = world.resource::<Schedules>().contains(exit_label.clone());
if has_exit_schedule {
world.try_run_schedule(exit_label).ok();
}
{
let state = state_py.bind(py).borrow();
state.set_value(pending_transition.clone_ref(py));
}
let transition_label = TransitionScheduleLabel::new(old_hash, new_hash);
let has_transition_schedule = world
.resource::<Schedules>()
.contains(transition_label.clone());
if has_transition_schedule {
world.try_run_schedule(transition_label).ok();
}
let enter_label = StateScheduleLabel::on_enter(new_hash);
let has_enter_schedule = world.resource::<Schedules>().contains(enter_label.clone());
if has_enter_schedule {
world.try_run_schedule(enter_label).ok();
}
Ok(true)
}
pub fn apply_state_transitions(py: Python, world: &mut bevy::ecs::world::World) -> PyResult<()> {
use std::{
collections::HashSet,
sync::atomic::{AtomicBool, Ordering},
};
use crate::ecs::resource_type::PyResourceStorage;
static PROCESSING: AtomicBool = AtomicBool::new(false);
if PROCESSING.swap(true, Ordering::SeqCst) {
return Ok(()); }
struct ProcessingGuard;
impl Drop for ProcessingGuard {
fn drop(&mut self) {
PROCESSING.store(false, Ordering::SeqCst);
}
}
let _guard = ProcessingGuard;
let mut state_type_names = HashSet::new();
{
let resource_storage = world.resource::<PyResourceStorage>();
for (_, resource_py) in resource_storage.resources.iter() {
let resource = resource_py.bind(py);
if let Ok(next_state) = resource.cast::<PyNextState>() {
let type_name = next_state
.borrow()
.state_type
.bind(py)
.name()
.unwrap()
.to_string();
state_type_names.insert(type_name);
}
}
}
for type_name in state_type_names {
apply_transition_for_state(py, world, &type_name)?;
}
Ok(())
}