Skip to content

Commit 04d5cf0

Browse files
authored
Add some coroutines utility functions (#704)
* Add awaitDeferred * Fix signal param name * Implement async load * Add an early return in case the resource is already loaded * Improve early return
1 parent 7303fe7 commit 04d5cf0

File tree

7 files changed

+244
-16
lines changed

7 files changed

+244
-16
lines changed

harness/tests/scripts/godot/tests/Invocation.gdj

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ supertypes = [
1313
kotlin.Any
1414
]
1515
signals = [
16-
no_param,
17-
one_param,
18-
two_param,
19-
signal_with_multiple_targets
16+
2017
]
2118
properties = [
2219
button,
@@ -71,13 +68,10 @@ properties = [
7168
stringtemplation,
7269
test_string,
7370
ascii_string,
74-
utf8_string,
75-
array
71+
utf8_string
7672
]
7773
functions = [
78-
target_function_one,
79-
target_function_two,
80-
int_value,
74+
int_value,
8175
long_value,
8276
float_value,
8377
double_value,

harness/tests/scripts/godot/tests/coroutine/CoroutineTest.gdj

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,19 @@ supertypes = [
1212
]
1313
signals = [
1414
signal_without_parameter,
15-
signal_with_parameters,
16-
signal_with_many_parameters
15+
signal_with_one_parameter,
16+
signal_with_many_parameters,
17+
run_on_main_thread_from_background_thread_finished,
18+
async_load_resource_finished
1719
]
1820
properties = [
1921
step
2022
]
2123
functions = [
2224
start_coroutine_without_parameter,
23-
start_coroutine_with_parameters,
24-
start_coroutine_with_many_parameters
25+
start_coroutine_with_one_parameter,
26+
start_coroutine_with_many_parameters,
27+
start_coroutine_undispatched,
28+
run_on_main_thread_from_background_thread,
29+
async_load_resource
2530
]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// THIS FILE IS GENERATED! DO NOT EDIT OR DELETE IT. EDIT OR DELETE THE ASSOCIATED SOURCE CODE FILE INSTEAD
2+
// Note: You can however freely move this file inside your godot project if you want. Keep in mind however, that if you rename the originating source code file, this file will be deleted and regenerated as a new file instead of being updated! Other modifications to the source file however, will result in this file being updated.
3+
4+
registeredName = SignalTest
5+
fqName = godot.tests.signal.SignalTest
6+
relativeSourcePath = src/main/kotlin/godot/tests/signal/SignalTest.kt
7+
baseType = Node
8+
supertypes = [
9+
godot.Node,
10+
godot.Object,
11+
godot.core.KtObject,
12+
kotlin.Any
13+
]
14+
signals = [
15+
no_param_signal_delegate,
16+
one_param_signal_delegate,
17+
two_param_signal_delegate,
18+
no_param_signal_field,
19+
one_param_signal_field,
20+
two_param_signal_field,
21+
signal_with_multiple_targets
22+
]
23+
properties = [
24+
other_script,
25+
array
26+
]
27+
functions = [
28+
_ready,
29+
target_function_one,
30+
target_function_two
31+
]

harness/tests/src/main/kotlin/godot/tests/coroutine/CoroutineTest.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package godot.tests.coroutine
22

33

44
import godot.Object
5+
import godot.PackedScene
6+
import godot.ResourceLoader
57
import godot.annotation.RegisterClass
68
import godot.annotation.RegisterFunction
79
import godot.annotation.RegisterProperty
@@ -12,6 +14,9 @@ import godot.core.signal1
1214
import godot.core.signal4
1315
import godot.coroutines.GodotCoroutine
1416
import godot.coroutines.await
17+
import godot.coroutines.awaitDeferred
18+
import godot.coroutines.awaitLoadAs
19+
import godot.global.GD
1520
import kotlinx.coroutines.CoroutineStart
1621

1722
@RegisterClass
@@ -55,4 +60,52 @@ class CoroutineTest : Object() {
5560
signalWithoutParameter.await()
5661
step = 8
5762
}
63+
64+
@RegisterSignal
65+
val runOnMainThreadFromBackgroundThreadFinished by signal1<Boolean>("is_test_successful")
66+
67+
@RegisterFunction
68+
fun runOnMainThreadFromBackgroundThread() {
69+
val thread = Thread.currentThread().name
70+
71+
GodotCoroutine {
72+
val bgThread = Thread.currentThread().name
73+
74+
var fgThread = ""
75+
awaitDeferred {
76+
fgThread = Thread.currentThread().name
77+
}
78+
79+
val bg2Thread = Thread.currentThread().name
80+
81+
GD.print("Thread names: ${listOf(thread, bgThread, fgThread, bg2Thread).joinToString()}")
82+
83+
awaitDeferred {
84+
runOnMainThreadFromBackgroundThreadFinished.emit(
85+
thread != bgThread
86+
&& thread != bg2Thread
87+
&& bgThread != fgThread
88+
&& thread == fgThread // check that the code is run on the main thread
89+
)
90+
}
91+
}
92+
}
93+
94+
@RegisterSignal
95+
val asyncLoadResourceFinished by signal1<Boolean>("is_test_successful")
96+
97+
@RegisterFunction
98+
fun asyncLoadResource() {
99+
GodotCoroutine {
100+
val resource = ResourceLoader.awaitLoadAs<PackedScene>("res://Spatial.tscn") { progress ->
101+
GD.print("Resource load progress: $progress")
102+
}
103+
104+
GD.print("Resource: $resource")
105+
106+
awaitDeferred {
107+
asyncLoadResourceFinished.emit(resource != null)
108+
}
109+
}
110+
}
58111
}

harness/tests/test/unit/test_coroutines.gd

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ func test_coroutine_await():
1515
assert_eq(test_script.step, 2, "Property should be 2 after coroutine ran and signal triggered.")
1616

1717
test_script.start_coroutine_with_one_parameter()
18-
await get_tree().create_timer(1).timeout
18+
await get_tree().create_timer(1).timeout
1919
assert_eq(test_script.step, 3, "Property should be 3 after coroutine started but waiting.")
2020

2121
test_script.signal_with_one_parameter.emit(4)
2222
await get_tree().create_timer(1).timeout
2323
assert_eq(test_script.step, 4, "Property should be 4 after coroutine ran.")
2424

2525
test_script.start_coroutine_with_many_parameters()
26-
await get_tree().create_timer(1).timeout
26+
await get_tree().create_timer(1).timeout
2727
assert_eq(test_script.step, 5, "Property should be 5 after coroutine started but waiting.")
2828

2929
test_script.signal_with_many_parameters.emit(6, 0.1, Vector2(0,0), "test")
@@ -34,4 +34,15 @@ func test_coroutine_await():
3434
assert_eq(test_script.step, 7, "Property should be immediately 7 when coroutine is undispatched.")
3535

3636
test_script.signal_without_parameter.emit()
37-
assert_eq(test_script.step, 8, "Property should be immediately 7 when coroutine is undispatched.")
37+
await get_tree().create_timer(1).timeout
38+
assert_eq(test_script.step, 8, "Property should be 8 when coroutine is resumed.")
39+
40+
test_script.run_on_main_thread_from_background_thread()
41+
var run_on_main_thread_from_background_thread_success = await test_script.run_on_main_thread_from_background_thread_finished
42+
assert_true(run_on_main_thread_from_background_thread_success, "Code should be executed on the correct threads")
43+
44+
test_script.async_load_resource()
45+
var async_load_resource_success = await test_script.async_load_resource_finished
46+
assert_true(async_load_resource_success, "Resource should be loaded")
47+
48+
test_script.free()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package godot.coroutines
2+
3+
import godot.core.Callable
4+
import godot.core.asCallable
5+
import kotlinx.coroutines.suspendCancellableCoroutine
6+
import kotlin.coroutines.resume
7+
8+
9+
/**
10+
* Suspends the current coroutine until the given block is executed.
11+
*
12+
* The block will be executed at the end of the frame on the main thread.
13+
*
14+
* Use it to call not thread safe code from godot and wait for the execution of it.
15+
*
16+
* @param block the code block to execute at the end of the frame
17+
*/
18+
public suspend inline fun <R> awaitDeferred(
19+
crossinline block: () -> R
20+
): R = suspendCancellableCoroutine { continuation ->
21+
Callable(
22+
{
23+
if (continuation.isActive) {
24+
continuation.resume(block())
25+
}
26+
}.asCallable()
27+
).callDeferred()
28+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package godot.coroutines
2+
3+
import godot.Error
4+
import godot.Resource
5+
import godot.ResourceLoader
6+
import godot.ResourceLoader.CacheMode
7+
import godot.core.variantArrayOf
8+
import godot.global.GD
9+
import godot.util.RealT
10+
import kotlinx.coroutines.CoroutineScope
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.launch
13+
import kotlinx.coroutines.suspendCancellableCoroutine
14+
import kotlin.coroutines.resume
15+
import kotlin.coroutines.resumeWithException
16+
17+
18+
/**
19+
* Suspends the coroutine until a resource is loaded from the given path.
20+
*
21+
* @param path The path of the resource to be loaded.
22+
* @param typeHint A hint about the type of resource being loaded.
23+
* @param useSubThreads Specifies whether to use sub-threads for loading the resource.
24+
* @param cacheMode The cache mode to be used while loading the resource.
25+
* @param onProgress A callback function to track the progress of resource loading.
26+
* @return The loaded resource, or null if there was an error.
27+
*/
28+
public suspend inline fun ResourceLoader.awaitLoad(
29+
path: String,
30+
typeHint: String = "",
31+
useSubThreads: Boolean = false,
32+
cacheMode: CacheMode = ResourceLoader.CacheMode.CACHE_MODE_REUSE,
33+
crossinline onProgress: (RealT) -> Unit = {},
34+
): Resource? {
35+
// early return in case the resource is already loaded
36+
if (this.hasCached(path)) {
37+
return this.load(path)
38+
}
39+
40+
val error = this.loadThreadedRequest(
41+
path = path,
42+
typeHint = typeHint,
43+
useSubThreads = useSubThreads,
44+
cacheMode = cacheMode,
45+
)
46+
47+
if (error != Error.OK) {
48+
GD.printErr("Could not trigger resource load. Got error: $error")
49+
return null
50+
}
51+
52+
return suspendCancellableCoroutine { continuation ->
53+
CoroutineScope(Dispatchers.Default).launch {
54+
var success = false
55+
do {
56+
val progress = variantArrayOf<Any?>()
57+
58+
val status = awaitDeferred {
59+
this@awaitLoad.loadThreadedGetStatus(
60+
path = path,
61+
progress = progress,
62+
).also {
63+
(progress.firstOrNull() as? RealT)?.let { onProgress(it) }
64+
}
65+
}
66+
67+
when (status) {
68+
ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED -> success = true
69+
ResourceLoader.ThreadLoadStatus.THREAD_LOAD_IN_PROGRESS -> {
70+
// no op
71+
}
72+
73+
ResourceLoader.ThreadLoadStatus.THREAD_LOAD_INVALID_RESOURCE,
74+
ResourceLoader.ThreadLoadStatus.THREAD_LOAD_FAILED -> continuation.resumeWithException(
75+
IllegalStateException("Failed to load resource: $status")
76+
)
77+
}
78+
} while (status == ResourceLoader.ThreadLoadStatus.THREAD_LOAD_IN_PROGRESS)
79+
80+
if (success) {
81+
continuation.resume(this@awaitLoad.loadThreadedGet(path))
82+
}
83+
}
84+
}
85+
}
86+
87+
/**
88+
* Suspends the coroutine until a resource is loaded from the given path.
89+
*
90+
* @param path The path of the resource to be loaded.
91+
* @param typeHint A hint about the type of resource being loaded.
92+
* @param useSubThreads Specifies whether to use sub-threads for loading the resource.
93+
* @param cacheMode The cache mode to be used while loading the resource.
94+
* @param onProgress A callback function to track the progress of resource loading.
95+
* @return The loaded resource, or null if there was an error.
96+
*/
97+
public suspend inline fun <R> ResourceLoader.awaitLoadAs(
98+
path: String,
99+
typeHint: String = "",
100+
useSubThreads: Boolean = false,
101+
cacheMode: CacheMode = ResourceLoader.CacheMode.CACHE_MODE_REUSE,
102+
crossinline onProgress: (RealT) -> Unit = {},
103+
): R? {
104+
@Suppress("UNCHECKED_CAST")
105+
return this.awaitLoad(path, typeHint, useSubThreads, cacheMode, onProgress) as? R
106+
}

0 commit comments

Comments
 (0)