diff --git a/ADDITIONS.md b/ADDITIONS.md index f3ca40ee..f9bfc0b1 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -119,6 +119,9 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a * Example: Status controller scripts now get `monster` bindings when running in context of Monster's status controller, etc * `behavior.behavior` third argument (specified commonly as `_ENV`) is ignored and can be omitted (set to nil) * It was used solely to get Lua engine (Lua execution context), and could have been deprecated long time ago even in original engine, because there is now a way in original engine to get Lua engine when binding is called + * Added `sb.logFatal`, similar to other log functions + * `print(...)` now prints to both console (stdout) and logs + * `sb.log` functions now accept everything `string.format` accepts, and not only `%s` and `%%` ## Random * Added `random:randn(deviation: double, mean: double): double`, returns normally distributed double, where `deviation` stands for [Standard deviation](https://en.wikipedia.org/wiki/Standard_deviation), and `mean` specifies middle point diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index bd1be672..45db946a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -374,6 +374,14 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para return createDescriptor().toJson() } + fun toTable(allocator: TableFactory): Table? { + if (isEmpty) { + return null + } + + return createDescriptor().toTable(allocator) + } + class Adapter(gson: Gson) : TypeAdapter() { override fun write(out: JsonWriter, value: ItemStack?) { val json = value?.toJson() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt index 56a41052..71f85ac6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt @@ -206,8 +206,8 @@ class LuaEnvironment : StateContext { // TODO: NYI, maybe polyfill? Utf8Lib.installInto(this, globals) - provideRootBindings(this) - provideUtilityBindings(this) + // provideRootBindings(this) + // provideUtilityBindings(this) } private val scripts = ObjectArraySet() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt index 7f659f85..418f3d64 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt @@ -22,6 +22,8 @@ import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter +import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings +import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings import ru.dbotthepony.kstarbound.util.random.random import java.io.Closeable import java.lang.ref.Cleaner @@ -71,47 +73,8 @@ class LuaThread private constructor( LuaJNR.INSTANCE.luaopen_utf8(this.pointer) this.storeGlobal("utf8") - push { - LOGGER.info(it.nextString()) - 0 - } - - storeGlobal("__print") - - push { - val path = it.nextString() - - try { - load(Starbound.readLuaScript(path).join(), "@$path") - 1 - } catch (err: Exception) { - 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") + provideUtilityBindings(this) + provideRootBindings(this) load(globalScript, "@starbound.jar!/scripts/global.lua") call() @@ -818,6 +781,10 @@ class LuaThread private constructor( return this@LuaThread.typeAt(position) } + fun hasNext(): Boolean { + return position <= top + } + fun nextString(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): String { if (position !in 1 ..this.top) throw IllegalArgumentException("bad argument #$position: string expected, got nil") @@ -851,6 +818,13 @@ class LuaThread private constructor( return value ?: throw IllegalArgumentException("bad argument #$position: anything expected, got ${this@LuaThread.typeAt(position)}") } + fun nextOptionalJson(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement? { + if (position !in 1 ..this.top) + return null + + return this@LuaThread.getJson(position, limit = limit) + } + fun nextTable(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonObject { if (position !in 1 ..this.top) throw IllegalArgumentException("bad argument #$position: table expected, got nil") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt index 44171986..8ab91771 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt @@ -1,25 +1,27 @@ package ru.dbotthepony.kstarbound.lua.bindings import com.google.gson.JsonElement -import org.apache.logging.log4j.LogManager +import com.google.gson.JsonNull import org.classdump.luna.ByteString import org.classdump.luna.Table import org.classdump.luna.runtime.ExecutionContext +import ru.dbotthepony.kommons.util.XXHash32 +import ru.dbotthepony.kommons.util.XXHash64 import ru.dbotthepony.kstarbound.math.vector.Vector2d 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.LuaThread.Companion +import ru.dbotthepony.kstarbound.lua.LuaType import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.luaFunctionArray import ru.dbotthepony.kstarbound.lua.luaFunctionN -import ru.dbotthepony.kstarbound.lua.nextOptionalFloat import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toJson -import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.userdata.LuaPerlinNoise import ru.dbotthepony.kstarbound.lua.userdata.LuaRandomGenerator @@ -28,26 +30,17 @@ 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.staticRandom32 +import ru.dbotthepony.kstarbound.util.random.staticRandom32FromList +import ru.dbotthepony.kstarbound.util.random.staticRandom64FromList import ru.dbotthepony.kstarbound.util.random.staticRandomDouble +import ru.dbotthepony.kstarbound.util.random.staticRandomDoubleFromList +import ru.dbotthepony.kstarbound.util.random.staticRandomIntFromList import ru.dbotthepony.kstarbound.util.random.staticRandomLong +import ru.dbotthepony.kstarbound.util.random.staticRandomLongFromList +import ru.dbotthepony.kstarbound.util.random.toBytes import ru.dbotthepony.kstarbound.util.toStarboundString import java.util.* -import java.util.random.RandomGenerator - -private val LOGGER = LogManager.getLogger() - -private val logInfo = luaFunctionN("logInfo") { args -> - LOGGER.info(args.nextString().toString().format(*args.copyRemaining())) -} - -private val logWarn = luaFunctionN("logWarn") { args -> - LOGGER.warn(args.nextString().toString().format(*args.copyRemaining())) -} - -private val logError = luaFunctionN("logError") { args -> - LOGGER.error(args.nextString().toString().format(*args.copyRemaining())) -} +import kotlin.collections.ArrayList private val interpolateSinEase = luaFunctionArray { args -> if (args.size < 3) @@ -71,75 +64,224 @@ private val interpolateSinEase = luaFunctionArray { args -> } } -private val replaceTags = luaFunction { string: ByteString, tags: Table -> - returnBuffer.setTo(SBPattern.of(string.toString()).resolveOrSkip({ tags[it]?.toString() }).toByteString()) +// 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 val makePerlinSource = luaFunction { settings: Table -> returnBuffer.setTo(LuaPerlinNoise(AbstractPerlinNoise.of(Starbound.gson.fromJson(settings.toJson(), PerlinNoiseParameters::class.java)))) } -private val staticRandomI32 = luaFunctionArray { - returnBuffer.setTo(staticRandom32(*it)) -} +private fun hash32(args: LuaThread.ArgStack): Int { + val digest = XXHash32(2938728349.toInt()) -private val staticRandomDouble = luaFunctionArray { - returnBuffer.setTo(staticRandomDouble(*it)) -} - -private val staticRandomDoubleRange = luaFunctionN("staticRandomDoubleRange") { - val min = it.nextFloat() - val max = it.nextFloat() - returnBuffer.setTo(staticRandomDouble(*it.copyRemaining()) * (max - min) + min) -} - -private val staticRandomI32Range = luaFunctionN("staticRandomI32Range") { - val min = it.nextInteger() - val max = it.nextInteger() - returnBuffer.setTo(staticRandomLong(min, max, *it.copyRemaining())) -} - -private val mergeJson = luaFunction { a: Any?, b: Any? -> - returnBuffer.setTo(from(ru.dbotthepony.kstarbound.json.mergeJson(toJsonFromLua(a), toJsonFromLua(b)))) -} - -fun provideUtilityBindings(lua: LuaEnvironment) { - val table = lua.newTable() - lua.globals["sb"] = table - - table["makeUuid"] = luaFunction { - returnBuffer.setTo(UUID(lua.random.nextLong(), lua.random.nextLong()).toStarboundString().toByteString()) + 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") + } } - table["logInfo"] = logInfo - table["logWarn"] = logWarn - table["logError"] = logError + return digest.digestAsInt() +} - table["nrand"] = luaFunctionN("nrand") { args -> - val stdev = args.nextOptionalFloat() ?: 1.0 - val mean = args.nextOptionalFloat() ?: 0.0 - lua.random.nextNormalDouble(stdev, mean) +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") + } } - table["print"] = lua.globals["tostring"] + 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 +} + +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") + } + + 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) + + /*table["print"] = lua.globals["tostring"] table["printJson"] = lua.globals["tostring"] table["interpolateSinEase"] = interpolateSinEase - table["replaceTags"] = replaceTags table["makeRandomSource"] = luaFunction { seed: Long? -> returnBuffer.setTo(LuaRandomGenerator(random(seed ?: lua.random.nextLong()))) } - table["makePerlinSource"] = makePerlinSource + table["makePerlinSource"] = makePerlinSource*/ - table["staticRandomI32"] = staticRandomI32 - table["staticRandomI64"] = staticRandomI32 - table["staticRandomDouble"] = staticRandomDouble + lua.pop() - table["staticRandomDoubleRange"] = staticRandomDoubleRange - table["staticRandomI32Range"] = staticRandomI32Range - table["staticRandomI64Range"] = staticRandomI32Range - - table["jsonMerge"] = mergeJson } fun provideConfigBindings(lua: LuaEnvironment, lookup: ExecutionContext.(path: JsonPath, ifMissing: Any?) -> Any?) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt index 75d4bdff..f6ca3d4e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -35,19 +35,19 @@ fun random(seed: Long = System.nanoTime()): RandomGenerator { */ val threadLocalRandom: RandomGenerator by ThreadLocal.withInitial { random() } -private fun toBytes(accept: ByteConsumer, value: Short) { +fun toBytes(accept: ByteConsumer, value: Short) { accept.accept(value.toByte()) accept.accept((value.toInt() ushr 8).toByte()) } -private fun toBytes(accept: ByteConsumer, value: Int) { +fun toBytes(accept: ByteConsumer, value: Int) { accept.accept(value.toByte()) accept.accept((value ushr 8).toByte()) accept.accept((value ushr 16).toByte()) accept.accept((value ushr 24).toByte()) } -private fun toBytes(accept: ByteConsumer, value: Long) { +fun toBytes(accept: ByteConsumer, value: Long) { accept.accept(value.toByte()) accept.accept((value ushr 8).toByte()) accept.accept((value ushr 16).toByte()) @@ -58,15 +58,19 @@ private fun toBytes(accept: ByteConsumer, value: Long) { accept.accept((value ushr 56).toByte()) } -private fun toBytes(accept: ByteConsumer, value: Double) { +fun toBytes(accept: ByteConsumer, value: Double) { toBytes(accept, value.toBits()) } -private fun toBytes(accept: ByteConsumer, value: Float) { +fun toBytes(accept: ByteConsumer, value: Float) { toBytes(accept, value.toBits()) } fun staticRandom32(vararg values: Any?): Int { + return staticRandom32FromList(values.asIterable()) +} + +fun staticRandom32FromList(values: Iterable): Int { val digest = XXHash32(2938728349.toInt()) for (value in values) { @@ -96,6 +100,10 @@ fun staticRandomDouble(vararg values: Any?): Double { return staticRandom64(*values).ushr(11) * 1.1102230246251565E-16 } +fun staticRandomDoubleFromList(values: Iterable): Double { + return staticRandom64FromList(values).ushr(11) * 1.1102230246251565E-16 +} + fun staticRandomInt(min: Int, max: Int, vararg values: Any?): Int { val hash = staticRandomDouble(*values) return (min + (max - min + 1) * hash).toInt() @@ -106,7 +114,21 @@ fun staticRandomLong(min: Long, max: Long, vararg values: Any?): Long { return (min + (max - min + 1L) * hash).toLong() } +fun staticRandomIntFromList(min: Int, max: Int, values: Iterable): Int { + val hash = staticRandomDoubleFromList(values) + return (min + (max - min + 1) * hash).toInt() +} + +fun staticRandomLongFromList(min: Long, max: Long, values: Iterable): Long { + val hash = staticRandomDoubleFromList(values) + return (min + (max - min + 1L) * hash).toLong() +} + fun staticRandom64(vararg values: Any?): Long { + return staticRandom64FromList(values.asIterable()) +} + +fun staticRandom64FromList(values: Iterable): Long { val digest = XXHash64(1997293021376312589L) for (value in values) { diff --git a/src/main/resources/scripts/global.lua b/src/main/resources/scripts/global.lua index d5378c38..ad2f8056 100644 --- a/src/main/resources/scripts/global.lua +++ b/src/main/resources/scripts/global.lua @@ -147,6 +147,10 @@ end do local __print = __print + local __print_warn = __print_warn + local __print_error = __print_error + local __print_fatal = __print_fatal + local format = string.format function print(...) local values = {} @@ -158,6 +162,22 @@ do __print(table.concat(values, '\t')) end + + function sb.logInfo(text, ...) + __print(format(text, ...)) + end + + function sb.logWarn(text, ...) + __print_warn(format(text, ...)) + end + + function sb.logError(text, ...) + __print_error(format(text, ...)) + end + + function sb.logFatal(text, ...) + __print_fatal(format(text, ...)) + end end do @@ -258,3 +278,5 @@ do __random_seed(floor(seed)) end end + +