diff --git a/src/main/kotlin/computer/obscure/twine/nativex/FunctionRegistrar.kt b/src/main/kotlin/computer/obscure/twine/nativex/FunctionRegistrar.kt index 43fc243..923016e 100644 --- a/src/main/kotlin/computer/obscure/twine/nativex/FunctionRegistrar.kt +++ b/src/main/kotlin/computer/obscure/twine/nativex/FunctionRegistrar.kt @@ -15,7 +15,18 @@ import kotlin.reflect.full.functions import kotlin.reflect.full.isSupertypeOf class FunctionRegistrar(private val owner: TwineNative) { - val functions = owner::class.functions + + companion object { + fun getFunctions(obj: Any): Map> { + return obj::class.functions + .mapNotNull { func -> + func.findAnnotation()?.let {annotation -> + val customName = annotation.name.takeIf { it != TwineNative.INHERIT_TAG } ?: func.name + customName to func + } + }.toMap() + } + } fun register() { registerFunctions() @@ -26,25 +37,19 @@ class FunctionRegistrar(private val owner: TwineNative) { * Registers functions annotated with {@code TwineNativeFunction} into the Lua table. */ fun registerFunctions() { + val functions = getFunctions(owner) functions.forEach { function -> - if (function.findAnnotation() == null) - return@forEach + val name = function.key + val rawFunction = function.value - // Set the name of the method based on the string given to the annotation - val annotation = function.findAnnotation() - var annotatedFunctionName = annotation?.name ?: function.name - - if (annotatedFunctionName == TwineNative.INHERIT_TAG) - annotatedFunctionName = function.name - - owner.table.set(annotatedFunctionName, object : VarArgFunction() { + owner.table.set(name, object : VarArgFunction() { override fun invoke(args: Varargs): Varargs { return try { - val kotlinArgs = args.toKotlinArgs(function) - val result = function.call(owner, *kotlinArgs) + val kotlinArgs = args.toKotlinArgs(rawFunction) + val result = rawFunction.call(owner, *kotlinArgs) result.toLuaValue() } catch (e: InvocationTargetException) { - ErrorHandler.throwError(e, function) + ErrorHandler.throwError(e, rawFunction) } as Varargs } }) @@ -57,19 +62,16 @@ class FunctionRegistrar(private val owner: TwineNative) { fun registerOverloads() { val functionMap = mutableMapOf>>() + val functions = getFunctions(owner) functions.forEach { function -> - val nativeAnnotation = function.findAnnotation() - val overloadAnnotation = function.findAnnotation() - if (nativeAnnotation == null || overloadAnnotation == null) - return@forEach - - // Set the name of the method based on the string given to the annotation - val annotation = function.findAnnotation() - var annotatedFunctionName = annotation?.name ?: function.name - - if (annotatedFunctionName == TwineNative.INHERIT_TAG) - annotatedFunctionName = function.name - functionMap.computeIfAbsent(annotatedFunctionName) { mutableListOf() }.add(function) + val name = function.key + val rawFunction = function.value + + rawFunction.findAnnotation() + ?: return@forEach + + functionMap.computeIfAbsent(name) { + mutableListOf() }.add(rawFunction) } // Find a match depending on the arg count and the arg types diff --git a/src/main/kotlin/computer/obscure/twine/nativex/PropertyRegistrar.kt b/src/main/kotlin/computer/obscure/twine/nativex/PropertyRegistrar.kt index 2670965..1104432 100644 --- a/src/main/kotlin/computer/obscure/twine/nativex/PropertyRegistrar.kt +++ b/src/main/kotlin/computer/obscure/twine/nativex/PropertyRegistrar.kt @@ -1,6 +1,7 @@ package computer.obscure.twine.nativex import computer.obscure.twine.annotations.TwineNativeProperty +import computer.obscure.twine.nativex.classes.NativeProperty import computer.obscure.twine.nativex.conversion.ClassMapper.toClass import computer.obscure.twine.nativex.conversion.Converter.toKotlinValue import computer.obscure.twine.nativex.conversion.Converter.toLuaValue @@ -10,29 +11,48 @@ import org.luaj.vm2.lib.OneArgFunction import org.luaj.vm2.lib.ThreeArgFunction import org.luaj.vm2.lib.TwoArgFunction import kotlin.reflect.KMutableProperty -import kotlin.reflect.KProperty import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.memberProperties class PropertyRegistrar(private val owner: TwineNative) { + companion object { + fun getProperties(owner: TwineNative): Map { + return owner::class.memberProperties + .mapNotNull { prop -> + val annotation = prop.findAnnotation() ?: return@mapNotNull null + + val name = + annotation.name.takeIf { it != TwineNative.INHERIT_TAG } + ?: prop.name + + val getter = prop.getter + val setter = (prop as? KMutableProperty<*>)?.setter + + val defaultValue = getter.call(owner) + + name to NativeProperty( + name = name, + getter = getter, + setter = setter, + defaultValue = defaultValue + ) + } + .toMap() + } + } + /** * Registers properties annotated with {@code TwineNativeProperty} into the Lua table. */ - fun registerProperties() { - val properties = owner::class.memberProperties - .mapNotNull { prop -> - prop.findAnnotation()?.let { annotation -> - val customName = annotation.name.takeIf { it != TwineNative.INHERIT_TAG } ?: prop.name - customName to prop - } - }.toMap() + fun registerProperties(props: Map?) { + val properties = props ?: getProperties(owner) val metatable = LuaTable() // Handle property getting metatable.set("__index", object : TwoArgFunction() { override fun call(self: LuaValue, key: LuaValue): LuaValue { - val prop = properties[key.tojstring()] as? KProperty<*> + val prop = properties[key.tojstring()] ?: return error("No property '${key.tojstring()}'") return try { @@ -48,11 +68,14 @@ class PropertyRegistrar(private val owner: TwineNative) { // Handle property setting metatable.set("__newindex", object : ThreeArgFunction() { override fun call(self: LuaValue, key: LuaValue, value: LuaValue): LuaValue { - val prop = properties[key.tojstring()] as? KMutableProperty<*> + val prop = properties[key.tojstring()] ?: return error("No property '${key.tojstring()}'") + val setter = prop.setter + ?: return error("No setter found on property '${key.tojstring()}'") + return try { - val setterParamType = prop.setter.parameters[1].type + val setterParamType = setter.parameters[1].type val convertedValue = if (value.istable()) value.checktable().toClass(prop.setter) diff --git a/src/main/kotlin/computer/obscure/twine/nativex/TwineEngine.kt b/src/main/kotlin/computer/obscure/twine/nativex/TwineEngine.kt index 6d18e6d..12b0778 100644 --- a/src/main/kotlin/computer/obscure/twine/nativex/TwineEngine.kt +++ b/src/main/kotlin/computer/obscure/twine/nativex/TwineEngine.kt @@ -73,6 +73,7 @@ class TwineEngine { * Adds a [TwineNative] into the globals using its [TwineNative.valueName]. */ fun set(native: TwineNative) { + native.__finalizeNative() globals.set(native.valueName, native.table) } diff --git a/src/main/kotlin/computer/obscure/twine/nativex/TwineNative.kt b/src/main/kotlin/computer/obscure/twine/nativex/TwineNative.kt index 2a6e2d5..ed9c5aa 100644 --- a/src/main/kotlin/computer/obscure/twine/nativex/TwineNative.kt +++ b/src/main/kotlin/computer/obscure/twine/nativex/TwineNative.kt @@ -16,6 +16,8 @@ package computer.obscure.twine.nativex import computer.obscure.twine.TwineTable +import computer.obscure.twine.nativex.classes.NativeProperty +import org.luaj.vm2.Globals /** * Abstract class TwineNative serves as a bridge between Kotlin and Lua, allowing functions and properties @@ -30,8 +32,11 @@ abstract class TwineNative( /** The name of the Lua table/property for this object. */ override var valueName: String = "" ) : TwineTable(valueName) { + private var finalized = false + private var properties: Map = mutableMapOf() + companion object { - val INHERIT_TAG = "INHERIT_FROM_DEFINITION" + const val INHERIT_TAG = "INHERIT_FROM_DEFINITION" } /** @@ -40,8 +45,52 @@ abstract class TwineNative( init { val functionRegistrar = FunctionRegistrar(this) functionRegistrar.register() + } + + /** + * Runs immediately after the native is registered in a [TwineEngine]. + */ + internal fun __finalizeNative() { + if (finalized) return + finalized = true val propertyRegistrar = PropertyRegistrar(this) - propertyRegistrar.registerProperties() + properties = PropertyRegistrar.getProperties(this) + propertyRegistrar.registerProperties(properties) + } + + protected fun requireFinalized() { + check(finalized) { + "TwineNative '$valueName' used before being registered with TwineEngine!" + } + } + + fun toCodeBlock(globals: Globals): String { + requireFinalized() + + val props = properties + val sb = StringBuilder() + + for ((name, prop) in props) { + val currentValue = prop.getter.call(this) + val defaultValue = prop.defaultValue + + println("${prop.name}: $defaultValue -> $currentValue") + + if (currentValue != defaultValue) { + sb.append("$valueName.$name = ${serializeLua(currentValue, globals)}\n") + } + } + + return sb.toString().trimEnd() } + + fun serializeLua(value: Any?, globals: Globals): String = when (value) { + null -> "nil" + is Boolean, is Int, is Long, is Float, is Double -> value.toString() + is String -> "\"${value.replace("\"", "\\\"")}\"" + is TwineNative -> value.toCodeBlock(globals) + else -> error("Unsupported Lua value: ${value::class}") + } as String + } diff --git a/src/main/kotlin/computer/obscure/twine/nativex/classes/NativeProperty.kt b/src/main/kotlin/computer/obscure/twine/nativex/classes/NativeProperty.kt new file mode 100644 index 0000000..a5fbc8b --- /dev/null +++ b/src/main/kotlin/computer/obscure/twine/nativex/classes/NativeProperty.kt @@ -0,0 +1,10 @@ +package computer.obscure.twine.nativex.classes + +import kotlin.reflect.KFunction + +data class NativeProperty( + val name: String, + val getter: KFunction<*>, + val setter: KFunction<*>?, + val defaultValue: Any? +) \ No newline at end of file diff --git a/src/test/kotlin/computer/obscure/twine/TwineCodeBlockTest.kt b/src/test/kotlin/computer/obscure/twine/TwineCodeBlockTest.kt new file mode 100644 index 0000000..3c0a249 --- /dev/null +++ b/src/test/kotlin/computer/obscure/twine/TwineCodeBlockTest.kt @@ -0,0 +1,44 @@ +package computer.obscure.twine + +import computer.obscure.twine.annotations.TwineNativeFunction +import computer.obscure.twine.annotations.TwineNativeProperty +import computer.obscure.twine.nativex.TwineEngine +import computer.obscure.twine.nativex.TwineNative +import org.junit.jupiter.api.Test + +class TwineCodeBlockTestInstance : TwineNative("code") { + lateinit var engine: TwineEngine + + @TwineNativeProperty + var testProperty: String = "hello" + + @TwineNativeFunction + fun toCodeBlock() { + println(super.toCodeBlock(engine.globals)) + } +} + +class TwineCodeBlockTest { + fun run(script: String): Any { + val engine = TwineEngine() + + engine.set( + TwineCodeBlockTestInstance().apply { + this.engine = engine + } + ) + + + return engine + .run("test.lua", script) + .getOrThrow() + } + + @Test + fun `testBaseMethod should throw`() { + val result = run(""" + code.testProperty = "hello 10298309123" + code.toCodeBlock() + """.trimIndent()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/computer/obscure/twine/TwineErrorTest.kt b/src/test/kotlin/computer/obscure/twine/TwineErrorTest.kt index e401fc6..9a83602 100644 --- a/src/test/kotlin/computer/obscure/twine/TwineErrorTest.kt +++ b/src/test/kotlin/computer/obscure/twine/TwineErrorTest.kt @@ -9,9 +9,6 @@ class TwineErrorTest { fun run(script: String): Any { val engine = TwineEngine() - val twineNative = TwineBaseTestClass() - engine.setBase(twineNative) - return engine .run("test.lua", script) .getOrThrow() @@ -24,7 +21,7 @@ class TwineErrorTest { } assertEquals( - "Lua error in test.lua:1 – attempted to index a nil value", + "Lua error in test.lua:1 — attempted to index a nil value", exception.message ) } diff --git a/src/test/kotlin/computer/obscure/twine/TwineNativeTest.kt b/src/test/kotlin/computer/obscure/twine/TwineNativeTest.kt index f046bed..38c240c 100644 --- a/src/test/kotlin/computer/obscure/twine/TwineNativeTest.kt +++ b/src/test/kotlin/computer/obscure/twine/TwineNativeTest.kt @@ -30,7 +30,7 @@ class TwineNativeTestClass: TwineNative() { @TwineNativeFunction fun testNew() { - println("new") + } } @@ -84,7 +84,6 @@ class TwineNativeTest { return test.testNew() """) - println(result) // assertEquals("50", result.toString()) } } \ No newline at end of file