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

#[class(tool)] for per-class editor run behavior #374

Merged
merged 3 commits into from
Aug 6, 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: 1 addition & 1 deletion godot-core/src/builtin/real.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ macro_rules! real {

/// Array of reals.
///
/// ### Examples
/// # Example
/// ```
/// use godot_core::builtin::{real, reals};
///
Expand Down
2 changes: 1 addition & 1 deletion godot-core/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ where
///
/// The path must be absolute (typically starting with `res://`), a local path will fail.
///
/// # Example:
/// # Example
/// Loads a scene called `Main` located in the `path/to` subdirectory of the Godot project and caches it in a variable.
/// The resource is directly stored with type `PackedScene`.
///
Expand Down
25 changes: 14 additions & 11 deletions godot-core/src/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
init: *mut sys::GDExtensionInitialization,
) -> sys::GDExtensionBool {
let init_code = || {
let virtuals_active_in_editor = match E::editor_run_behavior() {
EditorRunBehavior::Full => true,
EditorRunBehavior::NoVirtuals => false,
let tool_only_in_editor = match E::editor_run_behavior() {
EditorRunBehavior::ToolClassesOnly => true,
EditorRunBehavior::AllClasses => false,
};

let config = sys::GdextConfig {
virtuals_active_in_editor,
tool_only_in_editor,
is_editor: cell::OnceCell::new(),
};

Expand Down Expand Up @@ -132,7 +132,7 @@ pub unsafe trait ExtensionLibrary {

/// Determines if and how an extension's code is run in the editor.
fn editor_run_behavior() -> EditorRunBehavior {
EditorRunBehavior::NoVirtuals
EditorRunBehavior::ToolClassesOnly
}
}

Expand All @@ -144,19 +144,22 @@ pub unsafe trait ExtensionLibrary {
///
/// In many cases, users write extension code with the intention to run in games, not inside the editor.
/// This is why the default behavior in gdext deviates from Godot: lifecycle callbacks are disabled inside the
/// editor. It is however possible to restore the original behavior with this enum.
/// editor (see [`ToolClassesOnly`][Self::ToolClassesOnly]). It is possible to configure this.
///
/// See also [`ExtensionLibrary::editor_run_behavior()`].
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub enum EditorRunBehavior {
/// Runs the extension with full functionality in editor.
Full,
/// Only runs `#[class(tool)]` classes in the editor.
///
/// All classes are registered, and calls from GDScript to Rust are possible. However, virtual lifecycle callbacks
/// (`_ready`, `_process`, `_physics_process`, ...) are not run unless the class is annotated with `#[class(tool)]`.
ToolClassesOnly,

/// Does not invoke any Godot virtual functions.
/// Runs the extension with full functionality in editor.
///
/// Classes are still registered, and calls from GDScript to Rust are still possible.
NoVirtuals,
/// Ignores any `#[class(tool)]` annotations.
AllClasses,
}

pub trait ExtensionLayer: 'static {
Expand Down
17 changes: 17 additions & 0 deletions godot-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ pub mod private {
sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor);
}

pub struct ClassConfig {
pub is_tool: bool,
}

pub fn is_class_inactive(is_tool: bool) -> bool {
if is_tool {
return false;
}

// SAFETY: only invoked after global library initialization.
let global_config = unsafe { sys::config() };
let is_editor = || crate::engine::Engine::singleton().is_editor_hint();

global_config.tool_only_in_editor //.
&& *global_config.is_editor.get_or_init(is_editor)
}

fn print_panic(err: Box<dyn std::any::Any + Send>) {
if let Some(s) = err.downcast_ref::<&'static str>() {
print_panic_message(s);
Expand Down
4 changes: 4 additions & 0 deletions godot-core/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ use std::any::Any;
use std::collections::HashMap;
use std::{fmt, ptr};

// TODO(bromeon): some information coming from the proc-macro API is deferred through PluginComponent, while others is directly
// translated to code. Consider moving more code to the PluginComponent, which allows for more dynamic registration and will
// be easier for a future builder API.

/// Piece of information that is gathered by the self-registration ("plugin") system.
#[derive(Debug)]
pub struct ClassPlugin {
Expand Down
2 changes: 1 addition & 1 deletion godot-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ struct GdextRuntimeMetadata {
}

pub struct GdextConfig {
pub virtuals_active_in_editor: bool,
pub tool_only_in_editor: bool,
pub is_editor: cell::OnceCell<bool>,
}

Expand Down
59 changes: 33 additions & 26 deletions godot-macros/src/derive_from_variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,26 @@ pub fn transform(decl: Declaration) -> ParseResult<TokenStream> {
name,
name_string,
} = decl_get_info(&decl);

let mut body = quote! {
let root = variant.try_to::<godot::builtin::Dictionary>()?;
let root = root.get(#name_string).ok_or(godot::builtin::VariantConversionError::BadType)?;
let root = variant.try_to::<::godot::builtin::Dictionary>()?;
let root = root.get(#name_string).ok_or(::godot::builtin::VariantConversionError::BadType)?;
};

match decl {
Declaration::Struct(s) => match s.fields {
venial::StructFields::Unit => make_unit_struct(&mut body),
venial::StructFields::Tuple(fields) if fields.fields.len() == 1 => {
StructFields::Unit => make_unit_struct(&mut body),
StructFields::Tuple(fields) if fields.fields.len() == 1 => {
make_new_type_struct(&mut body, fields)
}
venial::StructFields::Tuple(fields) => make_tuple_struct(fields, &mut body, &name),
venial::StructFields::Named(fields) => make_named_struct(fields, &mut body, &name),
StructFields::Tuple(fields) => make_tuple_struct(fields, &mut body, &name),
StructFields::Named(fields) => make_named_struct(fields, &mut body, &name),
},
Declaration::Enum(enum_) => {
if enum_.variants.is_empty() {
// Uninhabited enums have no values, so we cannot convert an actual Variant into them.
body = quote! {
panic!();
panic!("cannot convert Variant into uninhabited enum {}", #name_string);
}
} else {
let mut matches = quote! {};
Expand Down Expand Up @@ -89,19 +91,21 @@ pub fn transform(decl: Declaration) -> ParseResult<TokenStream> {
body = quote! {
#body
#matches
Err(godot::builtin::VariantConversionError::MissingValue)
Err(::godot::builtin::VariantConversionError::MissingValue)
};
}
}

// decl_get_info() above ensured that no other cases are possible.
_ => unreachable!(),
}

let gen = generic_params.as_ref().map(|x| x.as_inline_args());
Ok(quote! {
impl #generic_params godot::builtin::FromVariant for #name #gen #where_ {
impl #generic_params ::godot::builtin::FromVariant for #name #gen #where_ {
fn try_from_variant(
variant: &godot::builtin::Variant
) -> Result<Self, godot::builtin::VariantConversionError> {
variant: &::godot::builtin::Variant
) -> Result<Self, ::godot::builtin::VariantConversionError> {
#body
}
}
Expand All @@ -123,7 +127,7 @@ fn make_named_struct(
(
quote! {
let #ident = root.get(#string_ident)
.ok_or(godot::builtin::VariantConversionError::MissingValue)?;
.ok_or(::godot::builtin::VariantConversionError::MissingValue)?;
},
quote! { #ident: #ident.try_to()? },
)
Expand All @@ -132,7 +136,7 @@ fn make_named_struct(
let (set_idents, set_self): (Vec<_>, Vec<_>) = fields.unzip();
*body = quote! {
#body
let root = root.try_to::<godot::builtin::Dictionary>()?;
let root = root.try_to::<::godot::builtin::Dictionary>()?;
#(
#set_idents
)*
Expand All @@ -157,7 +161,7 @@ fn make_tuple_struct(
} else {
quote! {
let #ident = root.pop_front()
.ok_or(godot::builtin::VariantConversionError::MissingValue)?
.ok_or(::godot::builtin::VariantConversionError::MissingValue)?
.try_to::<#field_type>()?;
}
},
Expand All @@ -166,7 +170,7 @@ fn make_tuple_struct(
let (idents, ident_set): (Vec<_>, Vec<_>) = ident_and_set.unzip();
*body = quote! {
#body
let mut root = root.try_to::<godot::builtin::VariantArray>()?;
let mut root = root.try_to::<::godot::builtin::VariantArray>()?;
#(
#ident_set
)*
Expand Down Expand Up @@ -202,7 +206,7 @@ fn make_enum_new_type(
) -> TokenStream {
let field_type = &field.ty;
quote! {
if let Ok(child) = root.try_to::<godot::builtin::Dictionary>() {
if let Ok(child) = root.try_to::<::godot::builtin::Dictionary>() {
if let Some(variant) = child.get(#variant_name_string) {
return Ok(Self::#variant_name(variant.try_to::<#field_type>()?));
}
Expand All @@ -217,7 +221,7 @@ fn make_enum_new_type_skipped(
) -> TokenStream {
let field_type = &field.ty;
quote! {
if let Ok(child) = root.try_to::<godot::builtin::Dictionary>() {
if let Ok(child) = root.try_to::<::godot::builtin::Dictionary>() {
if let Some(v) = child.get(#variant_name_string) {
if v.is_nil() {
return Ok(Self::#variant_name(
Expand All @@ -244,7 +248,7 @@ fn make_enum_tuple(
} else {
quote! {
let #ident = variant.pop_front()
.ok_or(godot::builtin::VariantConversionError::MissingValue)?
.ok_or(::godot::builtin::VariantConversionError::MissingValue)?
.try_to::<#field_type>()?;
}
};
Expand All @@ -253,10 +257,10 @@ fn make_enum_tuple(
let (idents, set_idents): (Vec<_>, Vec<_>) = fields.unzip();

quote! {
let child = root.try_to::<godot::builtin::Dictionary>();
let child = root.try_to::<::godot::builtin::Dictionary>();
if let Ok(child) = child {
if let Some(variant) = child.get(#variant_name_string) {
let mut variant = variant.try_to::<godot::builtin::VariantArray>()?;
let mut variant = variant.try_to::<::godot::builtin::VariantArray>()?;
#(#set_idents)*
return Ok(Self::#variant_name(#(#idents ,)*));
}
Expand All @@ -272,28 +276,31 @@ fn make_enum_named(
let field_name = &field.name;
let field_name_string = &field.name.to_string();
let field_type = &field.ty;
let set_field = if has_attr(&field.attributes,"variant","skip") {
let set_field = if has_attr(&field.attributes, "variant", "skip") {
quote! {
let #field_name = <#field_type as Default>::default();
}
} else {
quote! {
let #field_name = variant.get(#field_name_string)
.ok_or(godot::builtin::VariantConversionError::MissingValue)?
.try_to::<#field_type>()?;
.ok_or(::godot::builtin::VariantConversionError::MissingValue)?
.try_to::<#field_type>()?;
}
};
(field_name.to_token_stream(), set_field)
});

let (fields, set_fields): (Vec<_>, Vec<_>) = fields.unzip();
quote! {
if let Ok(root) = root.try_to::<godot::builtin::Dictionary>() {
if let Ok(root) = root.try_to::<::godot::builtin::Dictionary>() {
if let Some(variant) = root.get(#variant_name_string) {
let variant = variant.try_to::<godot::builtin::Dictionary>()?;
let variant = variant.try_to::<::godot::builtin::Dictionary>()?;
#(
#set_fields
)*
return Ok(Self::#variant_name{ #(#fields,)* });
return Ok(Self::#variant_name {
#( #fields, )*
});
}
}
}
Expand Down
26 changes: 25 additions & 1 deletion godot-macros/src/derive_godot_class/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub fn transform(decl: Declaration) -> ParseResult<TokenStream> {
create_fn = quote! { None };
};

let config_impl = make_config_impl(class_name, struct_cfg.is_tool);

Ok(quote! {
unsafe impl ::godot::obj::GodotClass for #class_name {
type Base = #base_class;
Expand All @@ -59,6 +61,7 @@ pub fn transform(decl: Declaration) -> ParseResult<TokenStream> {

#godot_init_impl
#godot_exports_impl
#config_impl

::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin {
class_name: #class_name_obj,
Expand All @@ -77,6 +80,7 @@ pub fn transform(decl: Declaration) -> ParseResult<TokenStream> {
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;

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

if parser.handle_alone("tool")? {
is_tool = true;
}

parser.finish()?;
}

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

Expand Down Expand Up @@ -144,6 +153,7 @@ fn parse_fields(class: &Struct) -> ParseResult<Fields> {
field.export = Some(export);
parser.finish()?;
}

// #[var]
if let Some(mut parser) = KvParser::parse(&named_field.attributes, "var")? {
let var = FieldVar::new_from_kv(&mut parser)?;
Expand Down Expand Up @@ -171,6 +181,7 @@ fn parse_fields(class: &Struct) -> ParseResult<Fields> {
struct ClassAttributes {
base_ty: Ident,
has_generated_init: bool,
is_tool: bool,
}

struct Fields {
Expand Down Expand Up @@ -210,7 +221,7 @@ fn make_godot_init_impl(class_name: &Ident, fields: Fields) -> TokenStream {
let rest_init = fields.all_fields.into_iter().map(|field| {
let field_name = field.name;
let value_expr = match field.default {
None => quote!(::std::default::Default::default()),
None => quote! { ::std::default::Default::default() },
Some(default) => default,
};
quote! { #field_name: #value_expr, }
Expand All @@ -228,6 +239,19 @@ fn make_godot_init_impl(class_name: &Ident, fields: Fields) -> TokenStream {
}
}

fn make_config_impl(class_name: &Ident, is_tool: bool) -> TokenStream {
quote! {
impl #class_name {
#[doc(hidden)]
pub fn __config() -> ::godot::private::ClassConfig {
::godot::private::ClassConfig {
is_tool: #is_tool,
}
}
}
}
}

/// Checks at compile time that a function with the given name exists on `Self`.
#[must_use]
fn make_existence_check(ident: &Ident) -> TokenStream {
Expand Down
Loading