Skip to content
Draft
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
54 changes: 28 additions & 26 deletions src/main/kotlin/computer/obscure/twine/nativex/FunctionRegistrar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, KFunction<*>> {
return obj::class.functions
.mapNotNull { func ->
func.findAnnotation<TwineNativeFunction>()?.let {annotation ->
val customName = annotation.name.takeIf { it != TwineNative.INHERIT_TAG } ?: func.name
customName to func
}
}.toMap()
}
}

fun register() {
registerFunctions()
Expand All @@ -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<TwineNativeFunction>() == 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<TwineNativeFunction>()
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
}
})
Expand All @@ -57,19 +62,16 @@ class FunctionRegistrar(private val owner: TwineNative) {
fun registerOverloads() {
val functionMap = mutableMapOf<String, MutableList<KFunction<*>>>()

val functions = getFunctions(owner)
functions.forEach { function ->
val nativeAnnotation = function.findAnnotation<TwineNativeFunction>()
val overloadAnnotation = function.findAnnotation<TwineOverload>()
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<TwineNativeFunction>()
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<TwineOverload>()
?: return@forEach

functionMap.computeIfAbsent(name) {
mutableListOf() }.add(rawFunction)
}

// Find a match depending on the arg count and the arg types
Expand Down
47 changes: 35 additions & 12 deletions src/main/kotlin/computer/obscure/twine/nativex/PropertyRegistrar.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<String, NativeProperty> {
return owner::class.memberProperties
.mapNotNull { prop ->
val annotation = prop.findAnnotation<TwineNativeProperty>() ?: 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<TwineNativeProperty>()?.let { annotation ->
val customName = annotation.name.takeIf { it != TwineNative.INHERIT_TAG } ?: prop.name
customName to prop
}
}.toMap()
fun registerProperties(props: Map<String, NativeProperty>?) {
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 {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
53 changes: 51 additions & 2 deletions src/main/kotlin/computer/obscure/twine/nativex/TwineNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String, NativeProperty> = mutableMapOf()

companion object {
val INHERIT_TAG = "INHERIT_FROM_DEFINITION"
const val INHERIT_TAG = "INHERIT_FROM_DEFINITION"
}

/**
Expand All @@ -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

}
Original file line number Diff line number Diff line change
@@ -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?
)
44 changes: 44 additions & 0 deletions src/test/kotlin/computer/obscure/twine/TwineCodeBlockTest.kt
Original file line number Diff line number Diff line change
@@ -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())
}
}
5 changes: 1 addition & 4 deletions src/test/kotlin/computer/obscure/twine/TwineErrorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
)
}
Expand Down
3 changes: 1 addition & 2 deletions src/test/kotlin/computer/obscure/twine/TwineNativeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class TwineNativeTestClass: TwineNative() {

@TwineNativeFunction
fun testNew() {
println("new")

}
}

Expand Down Expand Up @@ -84,7 +84,6 @@ class TwineNativeTest {
return test.testNew()
""")

println(result)
// assertEquals("50", result.toString())
}
}