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

Support abstract classes #339

Merged
merged 6 commits into from
Jul 15, 2022
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
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ nav:
- Versioning: user-guide/versioning.md
- Supported platforms: user-guide/supported-platforms.md
- Advanced:
- Abstract Classes: user-guide/advanced/abstract_classes.md
- Custom src dirs: user-guide/advanced/custom-src-dirs.md
- Custom gradle wrapper path: user-guide/advanced/custom_gradle_wrapper_path.md
- Gradle plugin configuration: user-guide/advanced/gradle-plugin-configuration.md
Expand Down
51 changes: 51 additions & 0 deletions docs/src/doc/user-guide/advanced/abstract_classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
You can define and derive from any abstract class you define, as long as any of your superclasses is a godot class.

This allows you to define default functions for your inheriting classes and override them in some, but not all subclasses if you want.

You can define a abstract class and register it's members the same way as you do for normal classes.

Under the hood, we only register your normal classes, and let them register all members your abstract class defines.

!!! info
For this reason, the `@RegisterClass` annotation is optional for abstract classes.

!!! warning
As in kotlin, you cannot instantiate abstract classes directly from any other scripting language like GDScript! In fact, godot does not even know (or care) that your abstract class exists.

# Example

Abstract class definition:
```kotlin
// register class annotation is optional for abstract classes
abstract class AbstractClassInheritanceParent: Node() {

@Export
@RegisterProperty
var registeredExportedPropertyInAbstractClass = false

@RegisterSignal
val signalInAbstractClass by signal<String>("blubb")

@RegisterFunction
fun functionInAbstractClassWithDefaultImplementation() {
// some implementation
}

@RegisterFunction
abstract fun abstractFunction()
}
```

Child class definition:
```kotlin
@RegisterClass
class AbstractClassInheritanceChild: AbstractClassInheritanceParent() {
@RegisterFunction
override fun abstractFunction() {
// some implementation
}
}
```

!!! warning "Registration of overridden members"
As you can see in the example; you need to explicitly register any member in the child class which you override from the abstract parent class. Otherwise they will not be registered and thus are not known to godot.
6 changes: 6 additions & 0 deletions harness/tests/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ _global_script_classes=[ {
"path": "res://src/main/kotlin/godot/tests/coretypes/Vector3Test.kt"
}, {
"base": "Node",
"class": "godot_tests_inheritance_AbstractClassInheritanceChild",
"language": "Kotlin",
"path": "res://src/main/kotlin/godot/tests/inheritance/AbstractClassInheritanceChild.kt"
}, {
"base": "Node",
"class": "godot_tests_inheritance_ClassInheritanceChild",
"language": "Kotlin",
"path": "res://src/main/kotlin/godot/tests/inheritance/ClassInheritanceChild.kt"
Expand All @@ -85,6 +90,7 @@ _global_script_class_icons={
"godot_tests_coretypes_BasisTest": "",
"godot_tests_coretypes_StringTest": "",
"godot_tests_coretypes_Vector3Test": "",
"godot_tests_inheritance_AbstractClassInheritanceChild": "",
"godot_tests_inheritance_ClassInheritanceChild": "",
"godot_tests_inheritance_ClassInheritanceParent": "",
"godot_tests_subpackage_OtherScript": ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package godot.tests.inheritance

import godot.annotation.RegisterClass
import godot.annotation.RegisterFunction
import godot.annotation.RegisterProperty
import godot.annotation.RegisterSignal
import godot.signals.signal

@RegisterClass
class AbstractClassInheritanceChild: AbstractClassInheritanceParent() {

@RegisterSignal
override val signalTestOverridden by signal<String, Int>("blubb", "habbalubbb")

//---------------- Here to check ------------------

@RegisterProperty
var childOpenFunctionHasBeenCalled = false

//-------------------------------------------------

override var openVar: Int = 100

@RegisterFunction
override fun openFunction() {
childOpenFunctionHasBeenCalled = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package godot.tests.inheritance

import godot.Node
import godot.annotation.Export
import godot.annotation.RegisterFunction
import godot.annotation.RegisterProperty
import godot.annotation.RegisterSignal
import godot.signals.signal

// register class annotation is optional for abstract classes
abstract class AbstractClassInheritanceParent: Node() {

@Export
@RegisterProperty
var registeredExportedPropertyInParent = false

@RegisterSignal
val signalTestNotOverridden by signal<String>("blubb")

@RegisterSignal
open val signalTestOverridden by signal<String, Int>("blubb", "habbalubb")

//---------------- Here to check ------------------

@RegisterProperty
var closedFunctionHasBeenCalled = false

//-------------------------------------------------

@RegisterProperty
var closedVar = 0

@RegisterProperty
open var openVar = 0

@RegisterFunction
fun closedFunction() {
closedFunctionHasBeenCalled = true
}

@RegisterFunction
abstract fun openFunction()
}
28 changes: 28 additions & 0 deletions harness/tests/test/unit/test_inheritance_abstract.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
extends "res://addons/gut/test.gd"


func test_call_parent_closed_method_from_child() -> void:
var child_script = godot_tests_inheritance_AbstractClassInheritanceChild.new()
child_script.closed_function()
assert_true(child_script.closed_function_has_been_called)
child_script.free()

func test_call_parent_open_method_from_child() -> void:
var child_script = godot_tests_inheritance_AbstractClassInheritanceChild.new()
child_script.open_function()
assert_true(child_script.child_open_function_has_been_called)
child_script.free()

func test_call_parent_closed_var_from_child() -> void:
var child_script = godot_tests_inheritance_AbstractClassInheritanceChild.new()
assert_eq(child_script.closed_var, 0, "Parent's closed var should be 0")
child_script.closed_var = 1
assert_eq(child_script.closed_var, 1, "Parent's closed var should be set to 1")
child_script.free()

func test_call_parent_open_var_from_child() -> void:
var child_script = godot_tests_inheritance_AbstractClassInheritanceChild.new()
assert_eq(child_script.open_var, 100, "Open var inherited from parent should be to 100 by default.")
child_script.open_var = 101
assert_eq(child_script.open_var, 101, "Open var inherited from parent should now be 101")
child_script.free()
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ class ClassRegistrarFileBuilder(
private val classRegistrarBuilder = TypeSpec
.classBuilder("${registeredClass.name}Registrar")
.addModifiers(KModifier.OPEN)
.let { classBuilder ->
if (registeredClass.isAbstract) {
classBuilder.addKdoc("Registrar for abstract class. Does not register any members as it's only used for default value providing if any properties with default values are provided in the abstract class. Members of this abstract class are registered by the inheriting registrars")
} else classBuilder
}

private val className = ClassName(registeredClass.containingPackage, registeredClass.name)

Expand All @@ -27,15 +32,23 @@ class ClassRegistrarFileBuilder(
.addModifiers(KModifier.OVERRIDE)
.addParameter("registry", ClassName("godot.registration", "ClassRegistry"))
.beginControlFlow("with(registry)") //START: with registry
.beginControlFlow(
"registerClass<%T>(%S,·%S,·%T::class,·${registeredClass.isTool},·%S,·%S)·{",
className,
registeredClass.resPath,
registeredClass.supertypes.first().fqName,
className,
registeredClass.godotBaseClass,
registeredClass.registeredName
) //START: registerClass
.let { funSpecBuilder ->
if (!registeredClass.isAbstract) {
funSpecBuilder.beginControlFlow(
"registerClass<%T>(%S,·%S,·%T::class,·${registeredClass.isTool},·%S,·%S)·{",
className,
registeredClass.resPath,
registeredClass.supertypes.first().fqName,
className,
registeredClass.godotBaseClass,
registeredClass.registeredName
) //START: registerClass
} else {
funSpecBuilder
.addComment("Abstract classes don't need to have any members to be registered")
}
}


fun build(): Pair<String, Array<Any>> {
if (!registeredClass.directlyInheritsGodotBaseClass) {
Expand All @@ -52,14 +65,22 @@ class ClassRegistrarFileBuilder(
)
}

ConstructorRegistrationGenerator.generate(registeredClass, className, registerClassControlFlow)
FunctionRegistrationGenerator.generate(registeredClass, className, registerClassControlFlow)
SignalRegistrationGenerator.generate(registeredClass, className, registerClassControlFlow)
PropertyRegistrationGenerator.generate(registeredClass, className, registerClassControlFlow, classRegistrarBuilder)
if (!registeredClass.isAbstract) {
ConstructorRegistrationGenerator.generate(registeredClass, className, registerClassControlFlow)
FunctionRegistrationGenerator.generate(registeredClass, className, registerClassControlFlow)
SignalRegistrationGenerator.generate(registeredClass, className, registerClassControlFlow)
PropertyRegistrationGenerator.generate(registeredClass, className, registerClassControlFlow, classRegistrarBuilder)
} else {
PropertyRegistrationGenerator.generateForAbstractClass(registeredClass, classRegistrarBuilder)
}

classRegistrarBuilder.addFunction(
registerClassControlFlow
.endControlFlow() //END: registerClass
.let { funSpecBuilder ->
if (!registeredClass.isAbstract) {
funSpecBuilder.endControlFlow() //END: registerClass
} else funSpecBuilder
}
.endControlFlow() //END: with registry
.build()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ object PropertyRegistrationGenerator {
}
}

fun generateForAbstractClass(
registeredClass: RegisteredClass,
classRegistrarBuilder: TypeSpec.Builder
) {
registeredClass
.properties
.forEach { registeredProperty ->
generateAndProvideDefaultValueProvider(registeredProperty, classRegistrarBuilder)
}
}

private fun registerProperty(
registeredProperty: RegisteredProperty,
className: ClassName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import godot.entrygenerator.ext.hasAnnotation
open class Clazz(
open val fqName: String,
open val supertypes: List<Clazz> = emptyList(),
open val annotations: List<ClassAnnotation> = emptyList()
open val annotations: List<ClassAnnotation> = emptyList(),
open val isAbstract: Boolean = false
) : GodotJvmSourceElement {
val name: String
get() = fqName.substringAfterLast(".")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ data class RegisteredClass(
val constructors: List<RegisteredConstructor> = emptyList(),
val functions: List<RegisteredFunction> = emptyList(),
val signals: List<RegisteredSignal> = emptyList(),
val properties: List<RegisteredProperty> = emptyList()
) : Clazz(fqName, supertypes) {
val properties: List<RegisteredProperty> = emptyList(),
override val isAbstract: Boolean = false
) : Clazz(fqName, supertypes, isAbstract = isAbstract) {
internal val registeredName: String
get() {
val customName = annotations
Expand Down
Loading