Skip to content

Commit 9004e71

Browse files
committed
Add XyNotification enums that accumulate all notification constants in scope
1 parent d60e3e1 commit 9004e71

File tree

3 files changed

+199
-2
lines changed

3 files changed

+199
-2
lines changed

godot-codegen/src/class_generator.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate
177177
let inherits_macro = format_ident!("inherits_transitive_{}", class_name.rust_ty);
178178
let all_bases = ctx.inheritance_tree().collect_all_bases(class_name);
179179
let virtual_trait = make_virtual_methods_trait(class, &all_bases, &virtual_trait_str, ctx);
180+
let notification_enum = make_notification_enum(class_name, &all_bases, ctx);
180181

181182
let memory = if class_name.rust_ty == "Object" {
182183
ident("DynamicRefCount")
@@ -203,6 +204,7 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate
203204
object_ptr: sys::GDExtensionObjectPtr,
204205
}
205206
#virtual_trait
207+
#notification_enum
206208
impl #class_name {
207209
#constructor
208210
#methods
@@ -264,6 +266,91 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate
264266
}
265267
}
266268

269+
fn make_notification_enum(
270+
class_name: &TyName,
271+
all_bases: &Vec<TyName>,
272+
ctx: &mut Context,
273+
) -> TokenStream {
274+
let Some(all_constants) = ctx.notification_constants(class_name) else {
275+
// Class has no notification constants: reuse (direct/indirect) base enum
276+
return TokenStream::new();
277+
};
278+
279+
// Collect all notification constants from current and base classes
280+
let mut all_constants = all_constants.clone();
281+
for base_name in all_bases {
282+
if let Some(constants) = ctx.notification_constants(base_name) {
283+
all_constants.extend(constants.iter().cloned());
284+
}
285+
}
286+
287+
workaround_constant_collision(&mut all_constants);
288+
289+
let enum_name = ctx.notification_enum_name(class_name);
290+
291+
let mut notification_enumerators_pascal = Vec::new();
292+
let mut notification_enumerators_ord = Vec::new();
293+
for (constant_ident, constant_value) in all_constants {
294+
notification_enumerators_pascal.push(constant_ident);
295+
notification_enumerators_ord.push(constant_value);
296+
}
297+
298+
quote! {
299+
/// Notification type for class `#class_name`.
300+
///
301+
/// Makes it easier to keep an overview all possible notification variants for a given class, including
302+
/// notifications defined in base classes.
303+
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
304+
#[repr(i32)]
305+
pub enum #enum_name {
306+
#(
307+
#notification_enumerators_pascal = #notification_enumerators_ord,
308+
)*
309+
310+
/// Since Godot represents notifications as integers, it's always possible that a notification outside the known types
311+
/// is received. For example, the user can manually issue notifications through `Object.notification()`.
312+
Unknown(i32),
313+
}
314+
315+
impl #enum_name {
316+
/// Always succeeds, mapping unknown integers to the `Unknown` variant.
317+
pub fn from_i32(enumerator: i32) -> Self {
318+
match enumerator {
319+
#(
320+
#notification_enumerators_ord => Self::#notification_enumerators_pascal,
321+
)*
322+
other_int => Self::Unknown(other_int),
323+
}
324+
}
325+
326+
pub fn to_i32(self) -> i32 {
327+
match self {
328+
#(
329+
Self::#notification_enumerators_pascal => #notification_enumerators_ord,
330+
)*
331+
Self::Unknown(int) => int,
332+
}
333+
}
334+
}
335+
}
336+
}
337+
338+
/// Workaround for Godot bug https://github.com/godotengine/godot/issues/75839
339+
///
340+
/// Godot has a collision for two notification constants (DRAW, NODE_CACHE_REQUESTED) in the same inheritance branch (as of 4.0.2).
341+
/// This cannot be represented in a Rust enum, so we merge the two constants into a single enumerator.
342+
fn workaround_constant_collision(all_constants: &mut Vec<(Ident, i32)>) {
343+
for first in ["Draw", "VisibilityChanged"] {
344+
if let Some(index_of_draw) = all_constants
345+
.iter()
346+
.position(|(constant_name, _)| constant_name == first)
347+
{
348+
all_constants[index_of_draw].0 = format_ident!("{first}OrNodeRecacheRequested");
349+
all_constants.retain(|(constant_name, _)| constant_name != "NodeRecacheRequested");
350+
}
351+
}
352+
}
353+
267354
fn make_builtin_class(
268355
class: &BuiltinClass,
269356
class_name: &TyName,

godot-codegen/src/context.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
*/
66

77
use crate::api_parser::Class;
8-
use crate::{ExtensionApi, RustTy, TyName};
8+
use crate::{util, ExtensionApi, RustTy, TyName};
9+
use proc_macro2::Ident;
10+
use quote::format_ident;
911
use std::collections::{HashMap, HashSet};
1012

1113
#[derive(Default)]
@@ -15,6 +17,8 @@ pub(crate) struct Context<'a> {
1517
singletons: HashSet<&'a str>,
1618
inheritance_tree: InheritanceTree,
1719
cached_rust_types: HashMap<String, RustTy>,
20+
notifications_by_class: HashMap<TyName, Vec<(Ident, i32)>>,
21+
notification_enum_names_by_class: HashMap<TyName, Ident>,
1822
}
1923

2024
impl<'a> Context<'a> {
@@ -39,15 +43,76 @@ impl<'a> Context<'a> {
3943
continue;
4044
}
4145

46+
// Populate class lookup by name
4247
println!("-- add engine class {}", class_name.description());
4348
ctx.engine_classes.insert(class_name.clone(), class);
4449

50+
// Populate derived-to-base relations
4551
if let Some(base) = class.inherits.as_ref() {
4652
let base_name = TyName::from_godot(base);
4753
println!(" -- inherits {}", base_name.description());
48-
ctx.inheritance_tree.insert(class_name, base_name);
54+
ctx.inheritance_tree.insert(class_name.clone(), base_name);
55+
}
56+
57+
// Populate notification constants
58+
if let Some(constants) = class.constants.as_ref() {
59+
let mut has_notifications = false;
60+
61+
for constant in constants.iter() {
62+
if let Some(rust_constant) = util::try_to_notification(&constant) {
63+
// First time
64+
if !has_notifications {
65+
ctx.notifications_by_class
66+
.insert(class_name.clone(), Vec::new());
67+
ctx.notification_enum_names_by_class.insert(
68+
class_name.clone(),
69+
make_notification_enum_name(&class_name),
70+
);
71+
has_notifications = true;
72+
}
73+
74+
ctx.notifications_by_class
75+
.get_mut(&class_name)
76+
.expect("just inserted constants; must be present")
77+
.push((rust_constant, constant.value));
78+
}
79+
}
4980
}
5081
}
82+
83+
// Populate remaining notification enum names, by copying the one to nearest base class that has at least 1 notification.
84+
// At this point all classes with notifications are registered.
85+
// (Used to avoid re-generating the same notification enum for multiple base classes).
86+
for class_name in ctx.engine_classes.keys() {
87+
if ctx
88+
.notification_enum_names_by_class
89+
.contains_key(class_name)
90+
{
91+
continue;
92+
}
93+
94+
let all_bases = ctx.inheritance_tree.collect_all_bases(class_name);
95+
96+
let mut nearest = None;
97+
for i in 0..all_bases.len() {
98+
if let Some(nearest_enum_name) =
99+
ctx.notification_enum_names_by_class.get(&all_bases[i])
100+
{
101+
nearest = Some((i, nearest_enum_name.clone()));
102+
break;
103+
}
104+
}
105+
let (nearest_index, nearest_enum_name) =
106+
nearest.expect("at least one base must have notifications");
107+
108+
// For all bases inheriting most-derived base that has notification constants, reuse the type name.
109+
for i in (0..nearest_index).rev() {
110+
let base_name = &all_bases[i];
111+
ctx.notification_enum_names_by_class
112+
.insert(base_name.clone(), nearest_enum_name.clone());
113+
}
114+
}
115+
51116
ctx
52117
}
53118

@@ -78,6 +143,17 @@ impl<'a> Context<'a> {
78143
self.cached_rust_types.get(ty)
79144
}
80145

146+
pub fn notification_constants(&'a self, class_name: &TyName) -> Option<&Vec<(Ident, i32)>> {
147+
self.notifications_by_class.get(class_name)
148+
}
149+
150+
pub fn notification_enum_name(&self, class_name: &TyName) -> Ident {
151+
self.notification_enum_names_by_class
152+
.get(class_name)
153+
.expect("every class must have a notification enum name")
154+
.clone()
155+
}
156+
81157
pub fn insert_rust_type(&mut self, ty: &str, resolved: RustTy) {
82158
let prev = self.cached_rust_types.insert(ty.to_string(), resolved);
83159
assert!(prev.is_none(), "no overwrites of RustTy");
@@ -96,6 +172,7 @@ impl InheritanceTree {
96172
assert!(existing.is_none(), "Duplicate inheritance insert");
97173
}
98174

175+
/// Returns all base classes, without the class itself, in order from nearest to furthest (object).
99176
pub fn collect_all_bases(&self, derived_name: &TyName) -> Vec<TyName> {
100177
let mut maybe_base = derived_name;
101178
let mut result = vec![];
@@ -107,3 +184,7 @@ impl InheritanceTree {
107184
result
108185
}
109186
}
187+
188+
fn make_notification_enum_name(class_name: &TyName) -> Ident {
189+
format_ident!("{}Notification", class_name.rust_ty)
190+
}

godot-codegen/src/util.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ pub fn make_constant_definition(constant: &ClassConstant) -> TokenStream {
115115
}
116116
}
117117

118+
/// Tries to interpret the constant as a notification one, and transforms it to a Rust identifier on success.
119+
pub fn try_to_notification(constant: &ClassConstant) -> Option<Ident> {
120+
constant
121+
.name
122+
.strip_prefix("NOTIFICATION_")
123+
.map(|s| ident(&shout_to_pascal(s)))
124+
}
125+
118126
fn make_enum_name(enum_name: &str) -> Ident {
119127
// TODO clean up enum name
120128

@@ -162,6 +170,27 @@ pub fn to_pascal_case(class_name: &str) -> String {
162170
.replace("GdNative", "GDNative")
163171
}
164172

173+
pub fn shout_to_pascal(shout_case: &str) -> String {
174+
// TODO use heck?
175+
176+
let mut result = String::with_capacity(shout_case.len());
177+
let mut next_upper = true;
178+
179+
for ch in shout_case.chars() {
180+
if next_upper {
181+
assert_ne!(ch, '_'); // no double underscore
182+
result.push(ch); // unchanged
183+
next_upper = false;
184+
} else if ch == '_' {
185+
next_upper = true;
186+
} else {
187+
result.push(ch.to_ascii_lowercase());
188+
}
189+
}
190+
191+
result
192+
}
193+
165194
pub fn ident(s: &str) -> Ident {
166195
format_ident!("{}", s)
167196
}

0 commit comments

Comments
 (0)