package ru.dbotthepony.kstarbound.lua.bindings import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.internal.bind.TypeAdapters import com.google.gson.stream.JsonWriter import org.classdump.luna.ByteString import org.classdump.luna.runtime.ExecutionContext import ru.dbotthepony.kommons.util.XXHash32 import ru.dbotthepony.kommons.util.XXHash64 import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.LuaThread import ru.dbotthepony.kstarbound.lua.LuaType import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise import ru.dbotthepony.kstarbound.lua.userdata.LuaRandom import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.nextNormalDouble import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.toBytes import ru.dbotthepony.kstarbound.util.toStarboundString import java.io.StringWriter import java.util.* // TODO: Lua-side implementation for better performance? private fun replaceTags(args: LuaThread.ArgStack): Int { val string = args.nextString() val tagsList = args.lua .readTable( args.position++, { getString(it) ?: throw IllegalStateException("Tags table contain non-string keys") }, { getString(it) ?: throw IllegalStateException("Tags table contain non-string values") } ) ?: throw IllegalArgumentException("bad argument #2: table expected, got ${args.peek(args.position - 1)}") val tags = tagsList.toMap() args.lua.push(SBPattern.of(string).resolveOrSkip({ tags[it] })) return 1 } private fun hash32(args: LuaThread.ArgStack): Int { val digest = XXHash32(2938728349.toInt()) while (args.hasNext()) { when (args.peek()) { LuaType.BOOLEAN -> digest.update(if (args.nextBoolean()) 1 else 0) LuaType.NUMBER -> toBytes(digest::update, args.nextDouble()) LuaType.STRING -> digest.update(args.nextString().toByteArray(Charsets.UTF_8)) else -> throw IllegalArgumentException("bad argument #${args.position} to staticRandomI32") } } return digest.digestAsInt() } private fun hash64(args: LuaThread.ArgStack): Long { val digest = XXHash64(1997293021376312589L) while (args.hasNext()) { when (args.peek()) { LuaType.BOOLEAN -> digest.update(if (args.nextBoolean()) 1 else 0) LuaType.NUMBER -> toBytes(digest::update, args.nextDouble()) LuaType.STRING -> digest.update(args.nextString().toByteArray(Charsets.UTF_8)) else -> throw IllegalArgumentException("bad argument #${args.position} to staticRandomI32") } } return digest.digestAsLong() } private fun staticRandomI32(args: LuaThread.ArgStack): Int { args.lua.push(hash32(args).toLong()) return 1 } private fun staticRandomI64(args: LuaThread.ArgStack): Int { args.lua.push(hash64(args)) return 1 } private fun staticRandomDouble(args: LuaThread.ArgStack): Int { args.lua.push(hash64(args).ushr(11) * 1.1102230246251565E-16) return 1 } private fun staticRandomDoubleRange(args: LuaThread.ArgStack): Int { val min = args.nextDouble() val max = args.nextDouble() val double = hash64(args).ushr(11) * 1.1102230246251565E-16 args.lua.push(double * (max - min) + min) return 1 } // FIXME: incorrect. private fun staticRandomI64Range(args: LuaThread.ArgStack): Int { val min = args.nextDouble() val max = args.nextDouble() val double = hash64(args).ushr(11) * 1.1102230246251565E-16 args.lua.push((min + (max - min + 1L) * double).toLong()) return 1 } // FIXME: incorrect. private fun staticRandomI32Range(args: LuaThread.ArgStack): Int { val min = args.nextDouble() val max = args.nextDouble() val double = hash64(args).ushr(11) * 1.1102230246251565E-16 args.lua.push((min + (max - min + 1L) * double).toInt().toLong()) return 1 } // TODO: Lua-side implementation for better performance? private fun makeUuid(args: LuaThread.ArgStack): Int { args.lua.push(UUID(args.lua.random.nextLong(), args.lua.random.nextLong()).toStarboundString()) return 1 } private fun nrand(args: LuaThread.ArgStack): Int { val stdev = args.nextOptionalDouble() ?: 1.0 val mean = args.nextOptionalDouble() ?: 0.0 args.lua.push(args.lua.random.nextNormalDouble(stdev, mean)) return 1 } // TODO: Lua-side implementation for better performance? private fun jsonMerge(args: LuaThread.ArgStack): Int { val a = args.nextOptionalJson() ?: JsonNull.INSTANCE val b = args.nextOptionalJson() ?: JsonNull.INSTANCE args.lua.push(ru.dbotthepony.kstarbound.json.mergeJson(a, b)) return 1 } // TODO: Lua-side implementation for better performance? private fun jsonQuery(args: LuaThread.ArgStack): Int { val json = args.nextOptionalJson() ?: JsonNull.INSTANCE val path = args.nextString() val default = args.nextOptionalJson() ?: JsonNull.INSTANCE args.lua.push(JsonPath.query(path).get(json, default)) return 1 } private fun printJson(args: LuaThread.ArgStack): Int { val json = args.nextJson() val pretty = args.nextOptionalBoolean() ?: false val strBuilder = StringWriter() val writer = JsonWriter(strBuilder) writer.isLenient = true if (pretty) { writer.setIndent(" ") } TypeAdapters.JSON_ELEMENT.write(writer, json) args.lua.push(strBuilder.toString()) return 1 } fun provideUtilityBindings(lua: LuaThread) { with(lua) { push { LuaThread.LOGGER.info(it.nextString()) 0 } storeGlobal("__print") push { LuaThread.LOGGER.warn(it.nextString()) 0 } storeGlobal("__print_warn") push { LuaThread.LOGGER.error(it.nextString()) 0 } storeGlobal("__print_error") push { LuaThread.LOGGER.fatal(it.nextString()) 0 } storeGlobal("__print_fatal") push { val path = it.nextString() try { load(Starbound.readLuaScript(path).join(), "@$path") 1 } catch (err: Exception) { LuaThread.LOGGER.error("Exception loading Lua script $path", err) throw err } } storeGlobal("__require") push { push(random.nextDouble()) 1 } storeGlobal("__random_double") push { push(random.nextLong(it.nextLong(), it.nextLong())) 1 } storeGlobal("__random_long") push { random = random(it.nextLong()) 0 } storeGlobal("__random_seed") push { push(it.lua.getNamedHandle(it.nextString())) 1 } storeGlobal("gethandle") push { val find = it.lua.findNamedHandle(it.nextString()) if (find == null) { 0 } else { push(find) 1 } } storeGlobal("findhandle") } lua.pushTable() lua.dup() lua.storeGlobal("sb") lua.setTableValue("makeUuid", ::makeUuid) lua.setTableValue("nrand", ::nrand) lua.setTableValue("jsonMerge", ::jsonMerge) lua.setTableValue("jsonQuery", ::jsonQuery) lua.setTableValue("replaceTags", ::replaceTags) lua.setTableValue("staticRandomI32", ::staticRandomI32) lua.setTableValue("staticRandomI32Range", ::staticRandomI32Range) lua.setTableValue("staticRandomI64", ::staticRandomI64) lua.setTableValue("staticRandomI64Range", ::staticRandomI64Range) lua.setTableValue("staticRandomDouble", ::staticRandomDouble) lua.setTableValue("staticRandomDoubleRange", ::staticRandomDoubleRange) lua.pushTable() val randomMeta = lua.createHandle("RandomGenerator") lua.pushBinding("init", LuaRandom::init) lua.pushBinding("addEntropy", LuaRandom::addEntropy) lua.pushBinding("randu32", LuaRandom::randu32) lua.pushBinding("randi32", LuaRandom::randi32) lua.pushBinding("randu64", LuaRandom::randu64) lua.pushBinding("randi64", LuaRandom::randi64) lua.pushBinding("randf", LuaRandom::randf) lua.pushBinding("randd", LuaRandom::randd) lua.pushBinding("randInt", LuaRandom::randLong) lua.pushBinding("randUInt", LuaRandom::randLong) lua.pushBinding("randLong", LuaRandom::randLong) lua.pushBinding("randULong", LuaRandom::randLong) lua.pushBinding("randn", LuaRandom::randn) lua.pop() lua.push("makeRandomSource") lua.push { args -> val seed = args.nextOptionalLong() ?: args.lua.random.nextLong() lua.pushTable() lua.push("__index") lua.push(randomMeta) lua.setTableValue() lua.pushObject(LuaRandom(random(seed))) 1 } lua.setTableValue() lua.pushTable() val noiseMeta = lua.createHandle("PerlinSource") lua.pushBinding("get", LuaPerlinNoise::get) lua.pushBinding("seed", LuaPerlinNoise::seed) lua.pushBinding("parameters", LuaPerlinNoise::parameters) lua.pushBinding("init", LuaPerlinNoise::init) lua.pop() lua.push("makePerlinSource") lua.push { args -> val params = AbstractPerlinNoise.of(Starbound.gson.fromJson(args.nextJson(), PerlinNoiseParameters::class.java)) lua.pushTable() lua.push("__index") lua.push(noiseMeta) // cyclic reference through GC root lua.setTableValue() lua.pushObject(LuaPerlinNoise(params)) 1 } lua.setTableValue() lua.setTableValue("printJson", ::printJson) lua.pop() } fun provideConfigBindings(lua: LuaEnvironment, lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) { val config = lua.newTable() lua.globals["config"] = config config["getParameter"] = createConfigBinding(lookup) } fun createConfigBinding(lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) = luaFunction { name: ByteString, default: Any? -> val get = lookup(this, JsonPath.query(name.decode()), default) if (get is JsonElement) returnBuffer.setTo(from(get)) else returnBuffer.setTo(get) }