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 {}