Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add editor plugins, and registration of classes at the proper initialization level #393

Merged
merged 1 commit into from
Sep 23, 2023
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
2 changes: 2 additions & 0 deletions godot-codegen/src/class_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate

let constructor = make_constructor(class, ctx);
let api_level = util::get_api_level(class);
let init_level = api_level.to_init_level();

let FnDefinitions {
functions: methods,
Expand Down Expand Up @@ -618,6 +619,7 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate
type Base = #base_ty;
type Declarer = crate::obj::dom::EngineDomain;
type Mem = crate::obj::mem::#memory;
const INIT_LEVEL: Option<crate::init::InitLevel> = #init_level;

fn class_name() -> ClassName {
ClassName::from_ascii_cstr(#class_name_cstr)
Expand Down
1 change: 1 addition & 0 deletions godot-codegen/src/codegen_special_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const SELECTED_CLASSES: &[&str] = &[
"CollisionObject2D",
"CollisionShape2D",
"Control",
"EditorPlugin",
"Engine",
"FileAccess",
"HTTPRequest",
Expand Down
13 changes: 9 additions & 4 deletions godot-codegen/src/special_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,15 @@ pub(crate) fn is_class_deleted(class_name: &TyName) -> bool {

match class_name {
// Hardcoded cases that are not accessible.
| "JavaClassWrapper" // only on Android.
| "JavaScriptBridge" // only on WASM.
| "ThemeDB" // lazily loaded; TODO enable this.
// Only on Android.
| "JavaClassWrapper"
| "JNISingleton"
| "JavaClass"
// Only on WASM.
| "JavaScriptBridge"
| "JavaScriptObject"
Comment on lines +74 to +80
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice, how did you find those classes? It looks like their presence didn't trigger an error (even now with full-ci checking everything)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a script to write a file that made a class inheriting from every single class in the gdextension api. some classes crashed on startup because i assume they're not meant to be extended (like AudioServer), but these threw an error because they werent available.

// lazily loaded; TODO enable this.
| "ThemeDB"

// Thread APIs.
| "Thread"
Expand Down Expand Up @@ -124,7 +130,6 @@ fn is_class_experimental(class_name: &TyName) -> bool {
| "NavigationRegion3D"
| "NavigationServer2D"
| "NavigationServer3D"
| "ProjectSettings"
| "SkeletonModification2D"
| "SkeletonModification2DCCDIK"
| "SkeletonModification2DFABRIK"
Expand Down
9 changes: 9 additions & 0 deletions godot-codegen/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ impl ClassCodegenLevel {
Self::Lazy => unreachable!("lazy classes should be deleted at the moment"),
}
}

pub fn to_init_level(self) -> TokenStream {
match self {
Self::Servers => quote! { Some(crate::init::InitLevel::Servers) },
Self::Scene => quote! { Some(crate::init::InitLevel::Scene) },
Self::Editor => quote! { Some(crate::init::InitLevel::Editor) },
Self::Lazy => quote! { None },
}
}
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion godot-core/src/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,12 @@ fn gdext_on_level_init(level: InitLevel) {
}
InitLevel::Scene => {
sys::load_class_method_table(sys::ClassApiLevel::Scene);
crate::auto_register_classes();
}
InitLevel::Editor => {
sys::load_class_method_table(sys::ClassApiLevel::Editor);
}
}
crate::auto_register_classes(level);
}
}

Expand Down
3 changes: 3 additions & 0 deletions godot-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ pub mod private {
use std::io::Write;
std::io::stdout().flush().expect("flush stdout");
}

/// Ensure `T` is an editor plugin.
pub const fn is_editor_plugin<T: crate::obj::Inherits<crate::engine::EditorPlugin>>() {}
}

macro_rules! generate_gdextension_api_version {
Expand Down
8 changes: 8 additions & 0 deletions godot-core/src/obj/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use crate::builder::ClassBuilder;
use crate::builtin::GodotString;
use crate::init::InitLevel;
use crate::obj::Base;

use crate::builtin::meta::ClassName;
Expand Down Expand Up @@ -34,6 +35,12 @@ where
/// Defines the memory strategy.
type Mem: mem::Memory;

/// During which initialization level this class is available/should be initialized with Godot.
///
/// Is `None` if the class has complicated initialization requirements, and generally cannot be inherited
/// from.
const INIT_LEVEL: Option<InitLevel>;

/// The name of the class, under which it is registered in Godot.
///
/// This may deviate from the Rust struct name: `HttpRequest::class_name().as_str() == "HTTPRequest"`.
Expand All @@ -60,6 +67,7 @@ unsafe impl GodotClass for () {
type Base = ();
type Declarer = dom::EngineDomain;
type Mem = mem::ManualMemory;
const INIT_LEVEL: Option<InitLevel> = None;

fn class_name() -> ClassName {
ClassName::none()
Expand Down
48 changes: 42 additions & 6 deletions godot-core/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

#![allow(dead_code)] // FIXME

use crate::init::InitLevel;
use crate::log;
use crate::obj::*;
use crate::private::as_storage;
use crate::storage::InstanceStorage;
Expand All @@ -29,6 +31,7 @@ use std::{fmt, ptr};
pub struct ClassPlugin {
pub class_name: ClassName,
pub component: PluginComponent,
pub init_level: Option<InitLevel>,
}

/// Type-erased function object, holding a `register_class` function.
Expand Down Expand Up @@ -109,6 +112,9 @@ pub enum PluginComponent {
p_name: sys::GDExtensionConstStringNamePtr,
) -> sys::GDExtensionClassCallVirtual,
},

#[cfg(since_api = "4.1")]
EditorPlugin,
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
Expand All @@ -120,6 +126,8 @@ struct ClassRegistrationInfo {
generated_register_fn: Option<ErasedRegisterFn>,
user_register_fn: Option<ErasedRegisterFn>,
godot_params: sys::GDExtensionClassCreationInfo,
init_level: InitLevel,
is_editor_plugin: bool,
}

/// Registers a class with static type information.
Expand All @@ -128,7 +136,8 @@ pub fn register_class<
+ cap::ImplementsGodotVirtual
+ cap::GodotToString
+ cap::GodotNotification
+ cap::GodotRegisterClass,
+ cap::GodotRegisterClass
+ GodotClass,
>() {
// TODO: provide overloads with only some trait impls

Expand All @@ -155,22 +164,33 @@ pub fn register_class<
raw: callbacks::register_class_by_builder::<T>,
}),
godot_params,
init_level: T::INIT_LEVEL.unwrap_or_else(|| {
panic!("Unknown initialization level for class {}", T::class_name())
}),
is_editor_plugin: false,
});
}

/// Lets Godot know about all classes that have self-registered through the plugin system.
pub fn auto_register_classes() {
out!("Auto-register classes...");
pub fn auto_register_classes(init_level: InitLevel) {
out!("Auto-register classes at level `{init_level:?}`...");

// Note: many errors are already caught by the compiler, before this runtime validation even takes place:
// * missing #[derive(GodotClass)] or impl GodotClass for T
// * duplicate impl GodotInit for T
//

let mut map = HashMap::<ClassName, ClassRegistrationInfo>::new();

crate::private::iterate_plugins(|elem: &ClassPlugin| {
//out!("* Plugin: {elem:#?}");
match elem.init_level {
None => {
log::godot_error!("Unknown initialization level for class {}", elem.class_name);
return;
}
Some(elem_init_level) if elem_init_level != init_level => return,
_ => (),
}

let name = elem.class_name;
let class_info = map
Expand All @@ -183,11 +203,14 @@ pub fn auto_register_classes() {
//out!("Class-map: {map:#?}");

for info in map.into_values() {
out!("Register class: {}", info.class_name);
out!(
"Register class: {} at level `{init_level:?}`",
info.class_name
);
register_class_raw(info);
}

out!("All classes auto-registered.");
out!("All classes for level `{init_level:?}` auto-registered.");
}

/// Populate `c` with all the relevant data from `component` (depending on component type).
Expand Down Expand Up @@ -227,6 +250,10 @@ fn fill_class_info(component: PluginComponent, c: &mut ClassRegistrationInfo) {
c.godot_params.notification_func = user_on_notification_fn;
c.godot_params.get_virtual_func = Some(get_virtual_fn);
}
#[cfg(since_api = "4.1")]
PluginComponent::EditorPlugin => {
c.is_editor_plugin = true;
}
}
// out!("| reg (after): {c:?}");
// out!();
Expand Down Expand Up @@ -282,6 +309,13 @@ fn register_class_raw(info: ClassRegistrationInfo) {
if let Some(register_fn) = info.user_register_fn {
(register_fn.raw)(&mut class_builder);
}

#[cfg(since_api = "4.1")]
if info.is_editor_plugin {
unsafe { interface_fn!(editor_add_plugin)(class_name.string_sys()) };
}
#[cfg(before_api = "4.1")]
assert!(!info.is_editor_plugin);
}

/// Callbacks that are passed as function pointers to Godot upon class registration.
Expand Down Expand Up @@ -444,6 +478,8 @@ fn default_registration_info(class_name: ClassName) -> ClassRegistrationInfo {
generated_register_fn: None,
user_register_fn: None,
godot_params: default_creation_info(),
init_level: InitLevel::Scene,
is_editor_plugin: false,
}
}

Expand Down
26 changes: 26 additions & 0 deletions godot-macros/src/class/derive_godot_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult<TokenStream> {
let prv = quote! { ::godot::private };
let godot_exports_impl = make_property_impl(class_name, &fields);

let editor_plugin = if struct_cfg.is_editor_plugin {
quote! {
::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin {
class_name: #class_name_obj,
component: #prv::PluginComponent::EditorPlugin,
init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL,
});

const _: () = #prv::is_editor_plugin::<#class_name>();
}
} else {
quote! {}
};

let (godot_init_impl, create_fn);
if struct_cfg.has_generated_init {
godot_init_impl = make_godot_init_impl(class_name, fields);
Expand All @@ -49,6 +63,7 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult<TokenStream> {
type Base = #base_class;
type Declarer = ::godot::obj::dom::UserDomain;
type Mem = <Self::Base as ::godot::obj::GodotClass>::Mem;
const INIT_LEVEL: Option<::godot::init::InitLevel> = <#base_class as ::godot::obj::GodotClass>::INIT_LEVEL;

fn class_name() -> ::godot::builtin::meta::ClassName {
::godot::builtin::meta::ClassName::from_ascii_cstr(#class_name_cstr)
Expand All @@ -66,8 +81,11 @@ pub fn derive_godot_class(decl: Declaration) -> ParseResult<TokenStream> {
generated_create_fn: #create_fn,
free_fn: #prv::callbacks::free::<#class_name>,
},
init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL,
});

#editor_plugin

#prv::class_macros::#inherits_macro!(#class_name);
})
}
Expand All @@ -89,6 +107,7 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult<ClassAttributes> {
let mut base_ty = ident("RefCounted");
let mut has_generated_init = false;
let mut is_tool = false;
let mut is_editor_plugin = false;

// #[class] attribute on struct
if let Some(mut parser) = KvParser::parse(&class.attributes, "class")? {
Expand All @@ -104,13 +123,19 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult<ClassAttributes> {
is_tool = true;
}

// TODO: better error message when using in Godot 4.0
if parser.handle_alone_ident("editor_plugin")?.is_some() {
is_editor_plugin = true;
}

parser.finish()?;
}

Ok(ClassAttributes {
base_ty,
has_generated_init,
is_tool,
is_editor_plugin,
})
}

Expand Down Expand Up @@ -190,6 +215,7 @@ struct ClassAttributes {
base_ty: Ident,
has_generated_init: bool,
is_tool: bool,
is_editor_plugin: bool,
}

fn make_godot_init_impl(class_name: &Ident, fields: Fields) -> TokenStream {
Expand Down
2 changes: 2 additions & 0 deletions godot-macros/src/class/godot_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ fn transform_inherent_impl(mut decl: Impl) -> Result<TokenStream, Error> {
raw: #prv::callbacks::register_user_binds::<#class_name>,
},
},
init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL,
});
};

Expand Down Expand Up @@ -505,6 +506,7 @@ fn transform_trait_impl(original_impl: Impl) -> Result<TokenStream, Error> {
user_on_notification_fn: #on_notification_fn,
get_virtual_fn: #prv::callbacks::get_virtual::<#class_name>,
},
init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL,
});
};

Expand Down
14 changes: 14 additions & 0 deletions godot-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,20 @@ use crate::util::ident;
/// for more information and further customization.
///
/// This is very similar to [GDScript's `@tool` feature](https://docs.godotengine.org/en/stable/tutorials/plugins/running_code_in_the_editor.html).
///
/// # Editor Plugins
///
/// If you annotate a class with `#[class(editor_plugin)]`, it will be turned into an editor plugin. The
/// class must then inherit from `EditorPlugin`, and an instance of that class will be automatically added
/// to the editor when launched.
///
/// See [Godot's documentation of editor plugins](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/index.html)
/// for more information about editor plugins. But note that you do not need to create and enable the plugin
/// through Godot's `Create New Plugin` menu for it to work, simply annotating the class with `editor_plugin`
/// automatically enables it when the library is loaded.
///
/// This should usually be combined with `#[class(tool)]` so that the code you write will actually run in the
/// editor.
#[proc_macro_derive(GodotClass, attributes(class, base, var, export, init, signal))]
pub fn derive_godot_class(input: TokenStream) -> TokenStream {
translate(input, class::derive_godot_class)
Expand Down
14 changes: 10 additions & 4 deletions godot-macros/src/util/kv_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,16 @@ impl KvParser {
/// Handles a key that can only occur without a value, e.g. `#[attr(toggle)]`. Returns whether
/// the key is present.
pub fn handle_alone(&mut self, key: &str) -> ParseResult<bool> {
match self.handle_any(key) {
None => Ok(false),
Some(value) => match value {
None => Ok(true),
self.handle_alone_ident(key).map(|id| id.is_some())
}

/// Handles a key that can only occur without a value, e.g. `#[attr(toggle)]`. Returns the key if it is
/// present.
pub fn handle_alone_ident(&mut self, key: &str) -> ParseResult<Option<Ident>> {
match self.handle_any_entry(key) {
None => Ok(None),
Some((id, value)) => match value {
None => Ok(Some(id)),
Some(value) => bail!(&value.tokens[0], "key `{key}` should not have a value"),
},
}
Expand Down
Loading