Skip to content

Commit

Permalink
Add editor_plugin as an option for godot classes
Browse files Browse the repository at this point in the history
Require that editor plugins inherit from `EditorPlugin`
Make classes get registered at the base class's init level
Unexclude `ProjectSettings`, since it isn't experimental
  • Loading branch information
lilizoey committed Sep 11, 2023
1 parent 9e6fe56 commit 5bd20d3
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 17 deletions.
5 changes: 4 additions & 1 deletion godot-codegen/src/class_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,9 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate
};

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

let FnDefinitions {
functions: methods,
Expand Down Expand Up @@ -615,6 +617,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
13 changes: 9 additions & 4 deletions godot-codegen/src/special_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ pub(crate) fn is_class_deleted(class_name: &TyName) -> bool {

match class_name.godot_ty.as_str() {
// 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"
// lazily loaded; TODO enable this.
| "ThemeDB"

// Thread APIs.
| "Thread"
Expand Down Expand Up @@ -100,7 +106,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 @@ -64,6 +64,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 },
}
}
}

/// Small utility that turns an optional vector (often encountered as JSON deserialization type) into a slice.
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
16 changes: 15 additions & 1 deletion 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 Expand Up @@ -118,7 +126,13 @@ pub trait Share {
/// print_node(Node3D::new_alloc().upcast());
/// ```
///
pub trait Inherits<Base>: GodotClass {}
pub trait Inherits<Base>: GodotClass {
/// Used in contexts where we want to ensure that a class inherits from some other class.
///
/// This cannot be used for safety, as the trait isn't unsafe.
#[doc(hidden)]
const INHERITS: () = ();
}

impl<T: GodotClass> Inherits<T> for T {}

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
3 changes: 3 additions & 0 deletions godot-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ proc-macro2 = "1.0.63"
quote = "1.0.29"

venial = "0.5"

[build-dependencies]
godot-bindings = { path = "../godot-bindings" }
35 changes: 35 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,23 @@ 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,
});

impl #class_name {
#[doc(hidden)]
const __INHERITS_EDITOR_PLUGIN: () = <#class_name as ::godot::obj::Inherits<::godot::engine::EditorPlugin>>::INHERITS;
}
}
} 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 +66,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 +84,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 +110,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 +126,25 @@ fn parse_struct_attributes(class: &Struct) -> ParseResult<ClassAttributes> {
is_tool = true;
}

if let Some(editor_plugin) = parser.handle_alone_ident("editor_plugin")? {
if cfg!(before_api = "4.1") {
return bail!(
editor_plugin,
"Editor plugins are not supported in Godot 4.0"
);
}

is_editor_plugin = true;
}

parser.finish()?;
}

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

Expand Down Expand Up @@ -190,6 +224,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
Loading

0 comments on commit 5bd20d3

Please sign in to comment.