Registration process and expected behavior
Registration is the bridge between JVM source code and Godot's runtime type system. It answers two different questions:
- Which classes and members should Godot know about?
- How should each selected class or member behave in Godot?
Keeping those questions separate is the most useful mental model for understanding the system.
An annotation can participate in either question:
@Script,@Visible,@Emit, and@Registerselect declarations.@Tool,@Export, property hints, and@Rpcconfigure declarations.@Notificationboth selects a method and supplies its notification value.- Some configuration annotations are themselves meta-annotated with a selection annotation. Whether that implied selection counts depends on the registration mode.
The mode changes selection. It does not remove compatibility requirements or change what the annotations mean after a declaration has been selected.
The pipeline at a glance
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
The important consequence is that the registrar does not process source text directly. Kotlin, Java, and Scala all compile to JVM bytecode, but they encode properties, annotations, accessors, and synthetic helpers differently. The front end first reconstructs a small language-neutral view before applying registration policy.
Stage 1: compiling user code
The normal JVM compilation tasks run first. Depending on the configured languages, that can include:
compileKotlincompileJavacompileScala
Registration therefore sees compiled classes, not declarations that only exist in an unsaved editor buffer. Newly created or changed scripts become available to Godot after a successful build.
The registration task receives:
- the compiled user classpath
- the compiled output roots that identify project-owned classes
- the project name
- the selected annotation-processing mode
The default mode is Inferred.
Stage 2: discovering candidate classes
ClassGraph scans the compiled classpath. A candidate script class must:
- inherit
godot.core.KtObject, normally through a Godot API type such asNode,Resource, or one of their descendants - be user code rather than a generated Godot base binding
- have an identifiable Kotlin, Java, or Scala source file
Automatic mode does not make arbitrary JVM classes into Godot scripts. It
automatically selects from this candidate set of Godot-compatible classes.
Language identification uses the source-file extension stored in bytecode:
.ktselects Kotlin reconstruction.javaselects Java reconstruction.scalaselects Scala reconstruction
An unknown language is skipped with a warning because the processor cannot safely guess how its bytecode maps back to source-level properties and signals.
For a project class, the source file name without its extension must match the class's simple name. Godot combines that file name with the source package to associate the source resource with the compiled class by fully qualified name. The registrar does not reconstruct or store a source path.
Stage 3: reconstructing a logical class
The processor normalizes each candidate into a LogicalClassShape containing:
- the class and its annotations
- logical properties
- logical signals
- logical methods
Only declared members are placed in this local shape. Inherited Godot API members are not copied into every child.
Why language normalization is necessary
A source property does not have one universal JVM representation.
Kotlin may emit:
- a backing field
- getter and setter methods
- a synthetic annotation carrier
- delegated-property support fields
Java may expose:
- a public field
- JavaBean-style
getX,isX, andsetXmethods
Scala commonly exposes:
- an accessor named like the property
- a setter named
property_$eq
The language adapter reconstructs those representations into one logical property and merges relevant annotations from the field, getter, setter, and language-specific annotation carriers.
Accessor-shaped methods
A method can look like both a property accessor and a callable function. Registration intent resolves the ambiguity:
@Visibleindicates property intent.@Registerindicates function intent.- both can register both views.
- without an explicit function intent, an accessor remains a property rather than becoming a duplicate callable.
This decision happens after the language adapter has identified the shape.
Stage 4: selecting declarations
The selected AnnotationProcessingMode controls which logical declarations
enter the registration model.
Selection matrix
| Declaration | Explicit | Inferred | Automatic |
|---|---|---|---|
| Class | direct @Script |
effective @Script |
every Godot-compatible candidate |
| Property | direct @Visible |
effective @Visible |
every mappable logical property |
| Signal | direct @Emit |
every logical signal | every logical signal |
| Method | direct @Register or @Notification |
effective @Register, or a mappable Godot override |
every mappable logical method |
| Property exported | direct @Export |
effective @Export |
yes, by default |
"Effective" means the annotation may be present directly or reached recursively through meta-annotations.
Explicit mode: spelling out the public Godot surface
Explicit mode uses only directly declared selection annotations.
Examples of implications that do not select in this mode:
@Toolcontains@Script, but@Toolalone does not select the class.@Exportcontains@Visible, but@Exportalone does not select the property.- a property hint such as
@IntRangecontains@Export, but it does not select or export the property by itself. @Rpccontains@Register, but@Rpcalone does not select the function.- overriding
_readydoes not select_ready.
The direct annotations must be written alongside the configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Use Explicit mode when the source should visibly describe the complete Godot API boundary.
Inferred mode: following annotation meaning
Inferred mode recursively expands meta-annotations.
The built-in chains include:
1 2 3 4 5 | |
This means configuration can also provide selection:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Inferred mode also:
- selects every logical signal in a selected class
- selects compatible overrides of Godot base methods
Ordinary project methods still need an effective @Register. Inferred does
not mean every public method is exposed.
Custom project annotations can participate in the same mechanism by being meta-annotated with the built-in annotations.
Automatic mode: selecting compatible declarations
Automatic mode selects compatible declarations by shape and type:
- all candidate Godot classes
- all mappable properties
- all logical signals
- all mappable methods
Properties are exported by default.
1 2 3 4 5 6 7 8 9 10 11 | |
Annotations remain useful as metadata:
1 2 3 4 5 6 7 | |
Here the property and function would already be selected. The annotations configure the inspector and networking behavior.
Stage 5: mapping annotations to metadata
Once a declaration is selected, annotations configure the final model.
Class metadata
@Script(className = "...")supplies a custom registered name.@Toolmarks the script as runnable in the editor.
Without a custom name, registrationNameMode determines how the default name
is computed.
Property metadata
@Exportmarks a property for the inspector in Explicit or Inferred mode.- Automatic properties are exported by default.
- property-hint annotations configure controls such as numeric ranges, file pickers, multiline text, colors, enums, and flags.
Property registration and inspector export are related but distinct:
- a registered property is available to Godot
- an exported property is additionally presented in the inspector
Signal metadata
The signal type provides its parameter types. @Emit can provide friendly
parameter names. Without explicit names, generated names such as param1 are
used.
Function metadata
@Rpccreates an RPC configuration.@Notification(value)marks a selected method as a notification handler.
@Notification is special: automatic method selection does not turn an
ordinary method into a notification handler. The annotation is still
required because its numeric notification value is metadata that cannot be
inferred.
Stage 6: building and validating the registration model
Selected declarations are mapped into:
ScriptClassRegisteredPropertyRegisteredSignalRegisteredFunctionRegisteredConstructor
Creating this model resolves:
- registered names
- Godot base classes and script inheritance
- property binding through a property reference or accessor methods
- parameter and return types
- property mutability and nullability
- property hints
- RPC and notification configuration
The model is then checked as a whole. Registration fails when an invalid Godot-facing API would be generated.
Class requirements
- A registered class must inherit a Godot-compatible type.
- A public parameterless constructor is optional. When present, the generated registrar exposes it for Godot instantiation; constructors with parameters remain JVM-only.
- Registered names must be unique within the known JVM registration set.
- Generic classes cannot be exposed as concrete Godot script types.
Function requirements
- Registered methods must be public.
- Parameters and return values must be mappable to Godot.
- Unrelated JVM source classes cannot appear in the exposed signature.
- A function can have at most 16 parameters.
- Generic registered methods are not supported.
Property requirements
A registered property must be public through its field or accessors and use a supported Godot-facing type, including:
- supported primitives and strings
- Godot core types
- Godot node and ref-counted types
- supported Kotlin and Java collections
- enums
BitField<Enum>
Additional constraints include:
- Godot core types and primitive-like registered values cannot be nullable.
- Godot core types cannot use
lateinit; provide a default value. - a property can have only one effective editor hint
- a hint must match the property type
- a
BitFieldenum can have at most 32 entries
Registered Kotlin val properties are allowed and are exposed read-only.
Mutability is not a universal requirement.
Signal requirements
- The logical signal must resolve to a
Signaltype. - Typed signal arity is limited by the available
Signal0throughSignal16families. - Signals should be stable immutable members; replacing a signal instance would break the registered connection identity.
RPC requirements
An RPC method must also be a selected registered function. This happens:
- directly with
@Registerin Explicit mode - through
@Rpc's meta-annotation in Inferred mode - through automatic method selection in Automatic mode
A non-zero transfer channel only has an effect with
UNRELIABLE_ORDERED; other transfer modes produce a warning.
Stage 7: generating registrar source
The back end consumes only the validated model. It does not repeat language-specific bytecode interpretation.
For each registered class it generates a registrar that records:
- the registered class name
- source file name for language identification, without a reconstructed path
- registered supertypes
- Godot base type
- abstract/concrete state
- constructor, when a public parameterless constructor exists
- signals
- properties
- notification handlers
- functions and RPC configuration
Abstract classes get registrar metadata for inheritance and default-value support but are not instantiated and do not directly register concrete members.
Stage 8: packaging and runtime loading
The Gradle plugin compiles the generated registrar code and packages it with the user project artifacts. At runtime:
- the JVM bootstrap discovers the generated registrars
- each registrar describes its class to the Godot JVM bridge
- Godot creates script instances through the registered constructor
- generated bindings route property, signal, function, RPC, and notification operations between Godot and the JVM
The registration mode is a build-time selection policy. Runtime code consumes the resulting registrar and does not re-evaluate the annotations.
In the editor, scripts loaded from .kt, .java, .scala, or .gdj files are
tracked as physical resources without keeping them alive. Each source resource
caches its last parsed FQCN and file modification time. A JAR reload checks all
live physical resources but rereads and reparses only sources whose modification
time changed.
The reload first removes the previous KtClass from every physical script and
builds temporary FQCN and registered-name maps from their current contents. Each
new JAR class then claims the matching physical script, reuses a script with the
same registered name, or creates a virtual jvm:// script. The persistent FQCN
map therefore describes the current JAR assignment, while parsed source FQCNs
remain local cache values. Scripts absent from the new JAR are retained by the
registered-name map only while a node, placeholder, or another owner still
references them.
This allows an attached empty or invalid source file to become associated after valid source is written and the project is built, regardless of whether the source or JAR reload happens first.
Inheritance behavior
Registration preserves the script family rather than flattening every inherited member into every class.
Expected behavior:
- exposed parent members remain available to children
- a child override supplies the child behavior
- the closest declaration controls an overridden member
- interfaces and abstract script classes participate in the registered type family
- only locally declared members are reconstructed in a class's logical shape
This avoids duplicate registrations while retaining normal JVM dispatch.
Build mode and IDE mode must agree
The Gradle build mode is configured in build.gradle.kts:
1 2 3 4 5 | |
The IntelliJ plugin stores its inspection mode as project editor state. Set it under Settings | Godot Kotlin/JVM | Annotation processing mode.
These settings currently have separate owners. Keep them on the same value:
- Gradle decides what the build registers.
- IntelliJ decides what the editor reports.
Both default to Inferred.
Diagnosing unexpected registration
Use the pipeline in order instead of starting at code generation.
A class is missing
Check:
- Does it inherit a Godot API class?
- Was its source language identified?
- Does the current mode select it?
- In Explicit/Inferred mode, is the required direct/effective
@Scriptpresent? - Does its registered name collide with another class?
A property is missing
Check:
- Was it reconstructed as one logical property?
- Is its field or accessor public?
- Does the mode select it?
- Is the type mappable?
- Is an accessor intentionally registered as a function instead?
A function is missing
Check:
- Is it public and locally declared?
- Does the mode select it?
- Is it an accessor-shaped method without explicit function intent?
- Are all signature types mappable?
- Does it exceed 16 parameters?
A signal is missing
Check:
- Was the source declaration reconstructed as a logical signal?
- Does it have a
SignalNtype? - In Explicit mode, does it have direct
@Emit? - Is its containing class selected?
The editor and build disagree
Check that the IntelliJ project setting and Gradle
annotationProcessingMode match before changing analyzer logic.
Ownership boundaries
The registration implementation intentionally keeps a short chain of responsibility:
JvmLanguage: reconstruct source meaning from language-specific bytecodeRegistrationPolicy: decide what the selected mode includesRegistrationMapper: create the final model and attach metadata- registration-model checks: reject invalid Godot-facing APIs
- registrar generator: emit code from valid models
- Gradle plugin: order tasks and package outputs
- IntelliJ plugin: report the same expected policy while editing
When changing behavior, update the narrowest owner and add coverage across Kotlin, Java, Scala, and all three registration modes where applicable.