Skip to main content

pybevy_core/
reload_request.rs

1//! Cross-crate reload request mailbox and shared resources.
2//!
3//! This module provides simple Bevy Resources that allow `pybevy_mcp`
4//! to communicate with the main `pybevy` crate without direct dependencies.
5//!
6//! Flow:
7//! 1. `pybevy_mcp` writes a `ReloadRequestMode` into `PendingReloadRequest`
8//! 2. The hot reload system in the main crate checks and drains it each frame
9
10use std::collections::HashMap;
11
12use bevy::{ecs::component::ComponentId, prelude::Resource};
13use pyo3::{Py, PyAny, ffi::PyTypeObject};
14
15/// The mode of reload to perform
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ReloadRequestMode {
18    Full,
19    Partial,
20}
21
22/// Mailbox resource: MCP writes, hot reload reads.
23#[derive(Resource, Default)]
24pub struct PendingReloadRequest {
25    pub mode: Option<ReloadRequestMode>,
26}
27
28/// Stores the last Python system error for MCP to read.
29/// Written by DynamicSystem on error, read by MCP's `get_last_error`.
30#[derive(Resource, Default, Clone)]
31pub struct LastSystemError {
32    pub error: Option<String>,
33    /// Full Python traceback with file paths and line numbers.
34    pub traceback: Option<String>,
35    pub timestamp_secs: f64,
36}
37
38/// Metadata for a registered custom Python component.
39#[derive(Debug, Clone)]
40pub struct CustomComponentEntry {
41    /// Python type pointer (stable for interpreter lifetime)
42    pub type_ptr: *const PyTypeObject,
43    /// Python class name (e.g., "Player", "Health")
44    pub name: String,
45    /// Whether this component uses PyObject storage (true) or Wrapper storage (false)
46    pub is_pyobject_storage: bool,
47}
48
49// SAFETY: PyTypeObject pointers are stable for the lifetime of the Python interpreter
50unsafe impl Send for CustomComponentEntry {}
51unsafe impl Sync for CustomComponentEntry {}
52
53/// Registry of custom Python components, accessible from MCP handlers.
54///
55/// Written by `register_custom_component()` in the main crate,
56/// read by MCP handlers to identify custom component names and extract fields.
57#[derive(Resource, Default)]
58pub struct CustomComponentInfo {
59    entries: HashMap<ComponentId, CustomComponentEntry>,
60}
61
62impl CustomComponentInfo {
63    /// Register a custom component entry
64    pub fn insert(&mut self, id: ComponentId, entry: CustomComponentEntry) {
65        self.entries.insert(id, entry);
66    }
67
68    /// Look up a custom component by ComponentId
69    pub fn get(&self, id: ComponentId) -> Option<&CustomComponentEntry> {
70        self.entries.get(&id)
71    }
72
73    /// Iterate over all registered custom components
74    pub fn iter(&self) -> impl Iterator<Item = (ComponentId, &CustomComponentEntry)> {
75        self.entries.iter().map(|(&id, entry)| (id, entry))
76    }
77
78    /// Clear all entries (used during full reload)
79    pub fn clear(&mut self) {
80        self.entries.clear();
81    }
82
83    /// Update the type_ptr for an existing entry (used during hot reload aliasing).
84    /// After reload, Python classes get new PyTypeObject pointers; this keeps the
85    /// entry pointing at the current (live) type object.
86    pub fn update_type_ptr(
87        &mut self,
88        component_id: ComponentId,
89        new_type_ptr: *const PyTypeObject,
90    ) {
91        if let Some(entry) = self.entries.get_mut(&component_id) {
92            entry.type_ptr = new_type_ptr;
93        }
94    }
95}
96
97/// Metadata for a registered custom Python resource.
98#[derive(Debug, Clone)]
99pub struct CustomResourceEntry {
100    /// Python type pointer (stable for interpreter lifetime)
101    pub type_ptr: *const PyTypeObject,
102    /// Python class name (e.g., "GameState", "Score")
103    pub name: String,
104}
105
106// SAFETY: PyTypeObject pointers are stable for the lifetime of the Python interpreter
107unsafe impl Send for CustomResourceEntry {}
108unsafe impl Sync for CustomResourceEntry {}
109
110/// Registry of custom Python resources, accessible from MCP handlers.
111///
112/// Written by `register_custom_resource()` in the main crate,
113/// read by MCP handlers to include custom resources in `list_resources`.
114#[derive(Resource, Default)]
115pub struct CustomResourceInfo {
116    entries: HashMap<ComponentId, CustomResourceEntry>,
117}
118
119impl CustomResourceInfo {
120    /// Register a custom resource entry
121    pub fn insert(&mut self, id: ComponentId, entry: CustomResourceEntry) {
122        self.entries.insert(id, entry);
123    }
124
125    /// Look up a custom resource by ComponentId
126    pub fn get(&self, id: ComponentId) -> Option<&CustomResourceEntry> {
127        self.entries.get(&id)
128    }
129
130    /// Iterate over all registered custom resources
131    pub fn iter(&self) -> impl Iterator<Item = (ComponentId, &CustomResourceEntry)> {
132        self.entries.iter().map(|(&id, entry)| (id, entry))
133    }
134
135    /// Clear all entries (used during full reload)
136    pub fn clear(&mut self) {
137        self.entries.clear();
138    }
139
140    /// Update the type_ptr for an existing entry (used during hot reload aliasing).
141    pub fn update_type_ptr(
142        &mut self,
143        component_id: ComponentId,
144        new_type_ptr: *const PyTypeObject,
145    ) {
146        if let Some(entry) = self.entries.get_mut(&component_id) {
147            entry.type_ptr = new_type_ptr;
148        }
149    }
150}
151
152/// Result of a reload operation, readable by MCP.
153///
154/// Written by `perform_reload()` in the main crate after each reload attempt.
155#[derive(Resource, Default, Clone)]
156pub struct ReloadResult {
157    /// Whether the reload was escalated from Partial to Full
158    pub escalated: bool,
159    /// Reason for escalation, if any
160    pub escalation_reason: Option<String>,
161    /// The mode that was actually used
162    pub actual_mode: Option<ReloadRequestMode>,
163    /// Whether the last reload attempt failed
164    pub failed: bool,
165    /// Reason for failure, if any
166    pub failure_reason: Option<String>,
167    /// Whether the app is running code from a previous generation after a failure
168    pub running_previous_generation: bool,
169    /// Plugin names added since last reload (restart may be required)
170    pub plugins_added: Option<Vec<String>>,
171    /// Plugin names removed since last reload (restart required to take effect)
172    pub plugins_removed: Option<Vec<String>>,
173    /// System names removed or renamed since last reload (load_scene required to clear stale schedule entries)
174    pub systems_removed: Option<Vec<String>>,
175}
176
177/// Storage for custom Python resources.
178/// Maps ComponentIds to Python objects. Lives in pybevy_core so that
179/// both the main crate and pybevy_control can access it.
180#[derive(Default, Resource)]
181pub struct PyResourceStorage {
182    pub resources: HashMap<ComponentId, Py<PyAny>>,
183}
184
185// SAFETY: We ensure all Python access happens within Python::attach
186unsafe impl Send for PyResourceStorage {}
187unsafe impl Sync for PyResourceStorage {}