Skip to content

Commit 17236fb

Browse files
committed
refactor: extracted reflection based function calling to be reusable
1 parent 1bd2edc commit 17236fb

2 files changed

Lines changed: 126 additions & 51 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* FXGL - JavaFX Game Library. The MIT License (MIT).
3+
* Copyright (c) AlmasB (almaslvl@gmail.com).
4+
* See LICENSE for details.
5+
*/
6+
7+
package com.almasb.fxgl.core.reflect
8+
9+
import java.lang.RuntimeException
10+
import java.lang.reflect.Method
11+
import java.util.function.BiFunction
12+
13+
/**
14+
* Allows calling methods of any object using its String name
15+
* and String arguments.
16+
*
17+
* @author Almas Baim (https://github.com/AlmasB)
18+
*/
19+
class ReflectionFunctionCaller {
20+
21+
private val functions = hashMapOf<FunctionSignature, ReflectionFunction>()
22+
23+
/**
24+
* Provides conversions from [String] to other types.
25+
*/
26+
private val stringToObject = hashMapOf<Class<*>, (String) -> Any>()
27+
28+
var defaultFunctionHandler: BiFunction<String, List<String>, Any> = BiFunction { name, args ->
29+
throw RuntimeException("No function handler for $name with $args")
30+
}
31+
32+
val methods: List<Method>
33+
get() = functions.values.map { it.method }
34+
35+
init {
36+
// register common types
37+
stringToObject[String::class.java] = { it }
38+
stringToObject[Int::class.java] = { it.toInt() }
39+
stringToObject[Double::class.java] = { it.toDouble() }
40+
stringToObject[Boolean::class.java] = {
41+
when (it) {
42+
"true" -> true
43+
"false" -> false
44+
else -> throw RuntimeException("Cannot convert $it to Boolean")
45+
}
46+
}
47+
stringToObject[List::class.java] = { it.split(",") }
48+
}
49+
50+
fun <T : Any> addStringToObjectConverter(type: Class<T>, converter: (String) -> T) {
51+
stringToObject[type] = converter
52+
}
53+
54+
fun removeStringToObjectConverter(type: Class<*>) {
55+
stringToObject.remove(type)
56+
}
57+
58+
/**
59+
* Add all methods (incl. private) of [targetObject] to be invokable
60+
* by this reflection function caller.
61+
*/
62+
fun addFunctionCallTarget(targetObject: Any) {
63+
targetObject.javaClass.declaredMethods.forEach {
64+
val signature = FunctionSignature(it.name, it.parameterCount)
65+
val method = ReflectionFunction(targetObject, it)
66+
it.isAccessible = true
67+
68+
functions[signature] = method
69+
}
70+
}
71+
72+
/**
73+
* Remove all methods of a previously added [targetObject].
74+
*/
75+
fun removeFunctionCallTarget(targetObject: Any) {
76+
functions.filterValues { it.functionCallTarget === targetObject }
77+
.forEach { functions.remove(it.key) }
78+
}
79+
80+
/**
81+
* @return true if [functionName] with [paramCount] number of parameters exists
82+
*/
83+
fun exists(functionName: String, paramCount: Int): Boolean {
84+
return functions.containsKey(FunctionSignature(functionName, paramCount))
85+
}
86+
87+
fun call(functionName: String, args: List<String>): Any {
88+
return call(functionName, args.toTypedArray())
89+
}
90+
91+
fun call(functionName: String, args: Array<String>): Any {
92+
val function = functions[FunctionSignature(functionName, args.size)]
93+
94+
if (function != null) {
95+
val argsAsObjects = function.method.parameterTypes.mapIndexed { index, type ->
96+
val converter = stringToObject[type] ?: throw java.lang.RuntimeException("No converter found from String to $type")
97+
converter.invoke(args[index])
98+
}
99+
100+
// void returns null, but Any is expected, so we return 0 in such cases
101+
return function.method.invoke(function.functionCallTarget, *argsAsObjects.toTypedArray()) ?: 0
102+
}
103+
104+
return defaultFunctionHandler.apply(functionName, args.toList())
105+
}
106+
107+
private data class FunctionSignature(val name: String, val paramCount: Int)
108+
109+
/**
110+
* Stores the object [functionCallTarget] and the function [method] that can be invoked on the object.
111+
*/
112+
private data class ReflectionFunction(val functionCallTarget: Any, val method: Method)
113+
}

fxgl-gameplay/src/main/kotlin/com/almasb/fxgl/cutscene/dialogue/DialogueScriptRunner.kt

Lines changed: 13 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
package com.almasb.fxgl.cutscene.dialogue
88

99
import com.almasb.fxgl.core.collection.PropertyMap
10+
import com.almasb.fxgl.core.reflect.ReflectionFunctionCaller
1011
import com.almasb.fxgl.logging.Logger
11-
import java.lang.reflect.Method
12+
import java.util.function.BiFunction
1213

1314
/**
1415
*
@@ -275,67 +276,35 @@ abstract class FunctionCallHandler : FunctionCallDelegate {
275276

276277
private val log = Logger.get(javaClass)
277278

278-
private val methods = hashMapOf<MethodSignature, InvokableMethod>()
279-
280-
/**
281-
* Provides conversions from [String] to other types.
282-
*/
283-
val stringToObject = hashMapOf<Class<*>, (String) -> Any>()
279+
val rfc = ReflectionFunctionCaller()
284280

285281
init {
286-
// register common types
287-
stringToObject[String::class.java] = { it }
288-
stringToObject[Int::class.java] = { it.toInt() }
289-
stringToObject[Double::class.java] = { it.toDouble() }
290-
stringToObject[Boolean::class.java] = {
291-
when (it) {
292-
"true" -> true
293-
"false" -> false
294-
else -> throw java.lang.RuntimeException("Cannot convert $it to Boolean")
295-
}
296-
}
297-
298282
// add self as a delegate so that all methods of the implementing class
299283
// are automatically added into [methods]
300-
addFunctionCallDelegate(this)
284+
rfc.addFunctionCallTarget(this)
285+
rfc.defaultFunctionHandler = BiFunction { name, args ->
286+
handle(name, args.toTypedArray())
287+
}
301288
}
302289

303290
fun addFunctionCallDelegate(obj: FunctionCallDelegate) {
304-
obj.javaClass.declaredMethods.forEach {
305-
val signature = MethodSignature(it.name, it.parameterCount)
306-
val method = InvokableMethod(obj, it)
291+
rfc.addFunctionCallTarget(obj)
307292

308-
methods[signature] = method
309-
310-
log.debug("Added cmd: $method ($signature)")
293+
rfc.methods.forEach {
294+
log.debug("Added cmd: $it")
311295
}
312296
}
313297

314298
fun removeFunctionCallDelegate(obj: FunctionCallDelegate) {
315-
methods.filterValues { it.delegate === obj }
316-
.forEach { methods.remove(it.key) }
299+
rfc.removeFunctionCallTarget(obj)
317300
}
318301

319302
fun exists(functionName: String, paramCount: Int): Boolean {
320-
return methods.containsKey(MethodSignature(functionName, paramCount))
303+
return rfc.exists(functionName, paramCount)
321304
}
322305

323306
fun call(functionName: String, args: Array<String>): Any {
324-
val method = methods[MethodSignature(functionName, args.size)]
325-
326-
if (method != null) {
327-
val argsAsObjects = method.function.parameterTypes.mapIndexed { index, type ->
328-
val converter = stringToObject[type] ?: throw java.lang.RuntimeException("No converter found from String to $type")
329-
converter.invoke(args[index])
330-
}
331-
332-
// void returns null, but Any is expected, so we return 0 in such cases
333-
return method.function.invoke(method.delegate, *argsAsObjects.toTypedArray()) ?: 0
334-
}
335-
336-
log.warning("Unrecognized function: $functionName with ${args.size} arguments. Calling default implementation")
337-
338-
return handle(functionName, args)
307+
return rfc.call(functionName, args)
339308
}
340309

341310
/**
@@ -346,11 +315,4 @@ abstract class FunctionCallHandler : FunctionCallDelegate {
346315
log.warning("$functionName ${args.toList()}")
347316
return 0
348317
}
349-
350-
private data class MethodSignature(val name: String, val paramCount: Int)
351-
352-
/**
353-
* Stores the object [delegate] and the function [function] that can be invoked on the object.
354-
*/
355-
private data class InvokableMethod(val delegate: FunctionCallDelegate, val function: Method)
356318
}

0 commit comments

Comments
 (0)