Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 95 additions & 28 deletions godot-core/src/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::out;
mod reexport_pub {
#[cfg(not(wasm_nothreads))]
pub use super::sys::main_thread_id;
pub use super::sys::{is_main_thread, GdextBuild};
pub use super::sys::{is_main_thread, GdextBuild, InitStage};
}
pub use reexport_pub::*;

Expand All @@ -29,7 +29,7 @@ struct InitUserData {

#[cfg(since_api = "4.5")]
unsafe extern "C" fn startup_func<E: ExtensionLibrary>() {
E::on_main_loop_startup();
E::on_stage_init(InitStage::MainLoop);
}

#[cfg(since_api = "4.5")]
Expand All @@ -39,7 +39,7 @@ unsafe extern "C" fn frame_func<E: ExtensionLibrary>() {

#[cfg(since_api = "4.5")]
unsafe extern "C" fn shutdown_func<E: ExtensionLibrary>() {
E::on_main_loop_shutdown();
E::on_stage_deinit(InitStage::MainLoop);
}

#[doc(hidden)]
Expand Down Expand Up @@ -146,7 +146,7 @@ unsafe extern "C" fn ffi_initialize_layer<E: ExtensionLibrary>(
// SAFETY: Godot will call this from the main thread, after `__gdext_load_library` where the library is initialized,
// and only once per level.
unsafe { gdext_on_level_init(level, userdata) };
E::on_level_init(level);
E::on_stage_init(level.to_stage());
}

// Swallow panics. TODO consider crashing if gdext init fails.
Expand All @@ -172,7 +172,7 @@ unsafe extern "C" fn ffi_deinitialize_layer<E: ExtensionLibrary>(
drop(Box::from_raw(userdata.cast::<InitUserData>()));
}

E::on_level_deinit(level);
E::on_stage_deinit(level.to_stage());
gdext_on_level_deinit(level);
});
}
Expand Down Expand Up @@ -327,43 +327,118 @@ pub unsafe trait ExtensionLibrary {
InitLevel::Scene
}

/// Custom logic when a certain init-level of Godot is loaded.
/// Custom logic when a certain initialization stage is loaded.
///
/// This will only be invoked for levels >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific levels.
/// This will be invoked for stages >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific stages.
///
/// The stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop` (4.5+). \
/// The `MainLoop` stage represents the fully initialized state of Godot, after all initialization levels and classes have been loaded.
///
/// See also [`on_main_loop_frame()`][Self::on_main_loop_frame] for per-frame processing.
///
/// # Panics
/// If the overridden method panics, an error will be printed, but GDExtension loading is **not** aborted.
#[allow(unused_variables)]
fn on_level_init(level: InitLevel) {
// Nothing by default.
#[expect(deprecated)] // Fall back to older API.
fn on_stage_init(stage: InitStage) {
stage
.try_to_level()
.inspect(|&level| Self::on_level_init(level));

#[cfg(since_api = "4.5")] // Compat layer.
if stage == InitStage::MainLoop {
Self::on_main_loop_startup();
}
}

/// Custom logic when a certain init-level of Godot is unloaded.
/// Custom logic when a certain initialization stage is unloaded.
///
/// This will only be invoked for levels >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific levels.
/// This will be invoked for stages >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific stages.
///
/// The stages are unloaded in reverse order: `MainLoop` (4.5+) → `Editor` (if in editor) → `Scene` → `Servers` → `Core`. \
/// At the time `MainLoop` is deinitialized, all classes are still available.
///
/// # Panics
/// If the overridden method panics, an error will be printed, but GDExtension unloading is **not** aborted.
#[allow(unused_variables)]
#[expect(deprecated)] // Fall back to older API.
fn on_stage_deinit(stage: InitStage) {
#[cfg(since_api = "4.5")] // Compat layer.
if stage == InitStage::MainLoop {
Self::on_main_loop_shutdown();
}

stage
.try_to_level()
.inspect(|&level| Self::on_level_deinit(level));
}

/// Old callback before [`on_stage_init()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage.
#[deprecated = "Use `on_stage_init()` instead, which also includes the MainLoop stage."]
#[allow(unused_variables)]
fn on_level_init(level: InitLevel) {
// Nothing by default.
}

/// Old callback before [`on_stage_deinit()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage.
#[deprecated = "Use `on_stage_deinit()` instead, which also includes the MainLoop stage."]
#[allow(unused_variables)]
fn on_level_deinit(level: InitLevel) {
// Nothing by default.
}

/// Callback that is called after all initialization levels when Godot is fully initialized.
#[cfg(since_api = "4.5")]
#[deprecated = "Use `on_stage_init(InitStage::MainLoop)` instead."]
#[doc(hidden)] // Added by mistake -- works but don't advertise.
fn on_main_loop_startup() {
// Nothing by default.
}

/// Callback that is called for every process frame.
///
/// This will run after all `_process()` methods on Node, and before `ScriptServer::frame()`.
#[cfg(since_api = "4.5")]
fn on_main_loop_frame() {
#[deprecated = "Use `on_stage_deinit(InitStage::MainLoop)` instead."]
#[doc(hidden)] // Added by mistake -- works but don't advertise.
fn on_main_loop_shutdown() {
// Nothing by default.
}

/// Callback that is called before Godot is shutdown when it is still fully initialized.
/// Callback invoked for every process frame.
///
/// This is called during the main loop, after Godot is fully initialized. It runs after all
/// [`process()`][crate::classes::INode::process] methods on Node, and before the Godot-internal `ScriptServer::frame()`.
/// This is intended to be the equivalent of [`IScriptLanguageExtension::frame()`][`crate::classes::IScriptLanguageExtension::frame()`]
/// for GDExtension language bindings that don't use the script API.
///
/// # Example
/// To hook into startup/shutdown of the main loop, use [`on_stage_init()`][Self::on_stage_init] and
/// [`on_stage_deinit()`][Self::on_stage_deinit] and watch for [`InitStage::MainLoop`].
///
/// ```no_run
/// # use godot::init::*;
/// # struct MyExtension;
/// #[gdextension]
/// unsafe impl ExtensionLibrary for MyExtension {
/// fn on_stage_init(stage: InitStage) {
/// if stage == InitStage::MainLoop {
/// // Startup code after fully initialized.
/// }
/// }
///
/// fn on_main_loop_frame() {
/// // Per-frame logic.
/// }
///
/// fn on_stage_deinit(stage: InitStage) {
/// if stage == InitStage::MainLoop {
/// // Cleanup code before shutdown.
/// }
/// }
/// }
/// ```
///
/// # Panics
/// If the overridden method panics, an error will be printed, but execution continues.
#[cfg(since_api = "4.5")]
fn on_main_loop_shutdown() {
fn on_main_loop_frame() {
// Nothing by default.
}

Expand Down Expand Up @@ -394,7 +469,7 @@ pub unsafe trait ExtensionLibrary {
/// #[cfg(feature = "nothreads")]
/// return None;
///
/// // Tell gdext we add a custom suffix to the binary with thread support.
/// // Tell godot-rust we add a custom suffix to the binary with thread support.
/// // Please note that this is not needed if "mycrate.threads.wasm" is used.
/// // (You could return `None` as well in that particular case.)
/// #[cfg(not(feature = "nothreads"))]
Expand Down Expand Up @@ -438,15 +513,7 @@ pub enum EditorRunBehavior {

// ----------------------------------------------------------------------------------------------------------------------------------------------

/// Stage of the Godot initialization process.
///
/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level,
/// a different amount of engine functionality is available. Deinitialization happens in reverse order.
///
/// See also:
/// - [`ExtensionLibrary::on_level_init()`]
/// - [`ExtensionLibrary::on_level_deinit()`]
pub type InitLevel = sys::InitLevel;
pub use sys::InitLevel;

// ----------------------------------------------------------------------------------------------------------------------------------------------

Expand Down
120 changes: 120 additions & 0 deletions godot-ffi/src/init_level.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

/// Stage of the Godot initialization process.
///
/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level,
/// a different amount of engine functionality is available. Deinitialization happens in reverse order.
///
/// See also:
// Explicit HTML links because this is re-exported in godot::init, and we can't document a `use` statement.
/// - [`InitStage`](enum.InitStage.html): all levels + main loop.
/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init)
/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit)
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum InitLevel {
/// First level loaded by Godot. Builtin types are available, classes are not.
Core,

/// Second level loaded by Godot. Only server classes and builtins are available.
Servers,

/// Third level loaded by Godot. Most classes are available.
Scene,

/// Fourth level loaded by Godot, only in the editor. All classes are available.
Editor,
}

impl InitLevel {
#[doc(hidden)]
pub fn from_sys(level: crate::GDExtensionInitializationLevel) -> Self {
match level {
crate::GDEXTENSION_INITIALIZATION_CORE => Self::Core,
crate::GDEXTENSION_INITIALIZATION_SERVERS => Self::Servers,
crate::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene,
crate::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor,
_ => {
eprintln!("WARNING: unknown initialization level {level}");
Self::Scene
}
}
}

#[doc(hidden)]
pub fn to_sys(self) -> crate::GDExtensionInitializationLevel {
match self {
Self::Core => crate::GDEXTENSION_INITIALIZATION_CORE,
Self::Servers => crate::GDEXTENSION_INITIALIZATION_SERVERS,
Self::Scene => crate::GDEXTENSION_INITIALIZATION_SCENE,
Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR,
}
}

/// Convert this initialization level to an initialization stage.
pub fn to_stage(self) -> InitStage {
match self {
Self::Core => InitStage::Core,
Self::Servers => InitStage::Servers,
Self::Scene => InitStage::Scene,
Self::Editor => InitStage::Editor,
}
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------

/// Extended initialization stage that includes both initialization levels and the main loop.
///
/// This enum extends [`InitLevel`] with a `MainLoop` variant, representing the fully initialized state of Godot
/// after all initialization levels have been loaded and before any deinitialization begins.
///
/// During initialization, stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop`. \
/// During deinitialization, stages are unloaded in reverse order.
///
/// See also:
/// - [`InitLevel`](enum.InitLevel.html): only levels, without `MainLoop`.
/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init)
/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit)
/// - [`ExtensionLibrary::on_main_loop_frame()`](trait.ExtensionLibrary.html#method.on_main_loop_frame)
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[non_exhaustive]
pub enum InitStage {
/// First level loaded by Godot. Builtin types are available, classes are not.
Core,

/// Second level loaded by Godot. Only server classes and builtins are available.
Servers,

/// Third level loaded by Godot. Most classes are available.
Scene,

/// Fourth level loaded by Godot, only in the editor. All classes are available.
Editor,

/// The main loop stage, representing the fully initialized state of Godot.
///
/// This variant is only available in Godot 4.5+. In earlier versions, it will never be passed to callbacks.
#[cfg(since_api = "4.5")]
MainLoop,
}

impl InitStage {
/// Try to convert this initialization stage to an initialization level.
///
/// Returns `None` for [`InitStage::MainLoop`], as it doesn't correspond to a Godot initialization level.
pub fn try_to_level(self) -> Option<InitLevel> {
match self {
Self::Core => Some(InitLevel::Core),
Self::Servers => Some(InitLevel::Servers),
Self::Scene => Some(InitLevel::Scene),
Self::Editor => Some(InitLevel::Editor),
#[cfg(since_api = "4.5")]
Self::MainLoop => None,
}
}
}
46 changes: 3 additions & 43 deletions godot-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub use gen::table_scene_classes::*;
pub use gen::table_servers_classes::*;
pub use gen::table_utilities::*;
pub use global::*;
pub use init_level::*;
pub use string_cache::StringCache;
pub use toolbox::*;

Expand All @@ -98,6 +99,7 @@ pub use crate::godot_ffi::{
// API to access Godot via FFI

mod binding;
mod init_level;

pub use binding::*;
use binding::{
Expand All @@ -109,49 +111,7 @@ use binding::{
#[cfg(not(wasm_nothreads))]
static MAIN_THREAD_ID: ManualInitCell<std::thread::ThreadId> = ManualInitCell::new();

/// Stage of the Godot initialization process.
///
/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level,
/// a different amount of engine functionality is available. Deinitialization happens in reverse order.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum InitLevel {
/// First level loaded by Godot. Builtin types are available, classes are not.
Core,

/// Second level loaded by Godot. Only server classes and builtins are available.
Servers,

/// Third level loaded by Godot. Most classes are available.
Scene,

/// Fourth level loaded by Godot, only in the editor. All classes are available.
Editor,
}

impl InitLevel {
#[doc(hidden)]
pub fn from_sys(level: crate::GDExtensionInitializationLevel) -> Self {
match level {
crate::GDEXTENSION_INITIALIZATION_CORE => Self::Core,
crate::GDEXTENSION_INITIALIZATION_SERVERS => Self::Servers,
crate::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene,
crate::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor,
_ => {
eprintln!("WARNING: unknown initialization level {level}");
Self::Scene
}
}
}
#[doc(hidden)]
pub fn to_sys(self) -> crate::GDExtensionInitializationLevel {
match self {
Self::Core => crate::GDEXTENSION_INITIALIZATION_CORE,
Self::Servers => crate::GDEXTENSION_INITIALIZATION_SERVERS,
Self::Scene => crate::GDEXTENSION_INITIALIZATION_SCENE,
Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR,
}
}
}
// ----------------------------------------------------------------------------------------------------------------------------------------------

pub struct GdextRuntimeMetadata {
godot_version: GDExtensionGodotVersion,
Expand Down
2 changes: 1 addition & 1 deletion godot/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub use super::classes::{
pub use super::global::{
godot_error, godot_print, godot_print_rich, godot_script_error, godot_warn,
};
pub use super::init::{gdextension, ExtensionLibrary, InitLevel};
pub use super::init::{gdextension, ExtensionLibrary, InitLevel, InitStage};
pub use super::meta::error::{ConvertError, IoError};
pub use super::meta::{FromGodot, GodotConvert, ToGodot};
pub use super::obj::{
Expand Down
Loading
Loading