From 311c6a4e472a8841fb2e25617c847054bf642af7 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 22 Dec 2024 21:54:12 +0700 Subject: [PATCH] Migrate world bindings to PUC Lua --- ADDITIONS.md | 52 +- .../ru/dbotthepony/kstarbound/lua/LuaJNR.java | 2 +- .../ru/dbotthepony/kstarbound/Registry.kt | 4 + .../dbotthepony/kstarbound/defs/EntityType.kt | 6 + .../kstarbound/defs/world/WorldTemplate.kt | 21 +- .../kstarbound/lua/CommonHandleRegistry.kt | 6 + .../dbotthepony/kstarbound/lua/Conversions.kt | 169 +- .../dbotthepony/kstarbound/lua/LuaHandle.kt | 27 +- .../kstarbound/lua/LuaHandleThread.kt | 52 - .../kstarbound/lua/LuaSharedState.kt | 89 + .../dbotthepony/kstarbound/lua/LuaThread.kt | 417 +++-- .../kstarbound/lua/bindings/EntityBindings.kt | 2 +- .../kstarbound/lua/bindings/RootBindings.kt | 7 +- .../lua/bindings/ServerWorldBindings.kt | 558 +++--- .../lua/bindings/UtilityBindings.kt | 26 +- .../kstarbound/lua/bindings/WorldBindings.kt | 1322 +++++++------ .../lua/bindings/WorldEntityBindings.kt | 1637 +++++++++++------ .../bindings/WorldEnvironmentalBindings.kt | 478 +++-- .../kstarbound/lua/userdata/LuaFuture.kt | 128 +- .../kstarbound/lua/userdata/LuaPathFinder.kt | 191 +- .../kstarbound/server/world/ServerWorld.kt | 2 +- .../ru/dbotthepony/kstarbound/util/Utils.kt | 6 + .../kstarbound/world/Raycasting.kt | 8 + .../ru/dbotthepony/kstarbound/world/World.kt | 10 +- .../kstarbound/world/entities/PathFinder.kt | 8 + .../world/entities/StagehandEntity.kt | 3 + .../world/entities/api/ScriptedEntity.kt | 5 +- src/main/resources/scripts/global.lua | 4 +- src/main/resources/scripts/server_world.lua | 20 + src/main/resources/scripts/world.lua | 502 +++++ .../dbotthepony/kstarbound/test/LuaTests.kt | 18 +- 31 files changed, 3870 insertions(+), 1910 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/CommonHandleRegistry.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaHandleThread.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaSharedState.kt create mode 100644 src/main/resources/scripts/server_world.lua create mode 100644 src/main/resources/scripts/world.lua diff --git a/ADDITIONS.md b/ADDITIONS.md index 44b31a76..07179fc9 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -127,6 +127,15 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a * Added `noise:init(seed: long)`, re-initializes noise generator with new seed, but same parameters (object returned by `sb.makePerlinSource`) * Added `math.clamp(value, min, max)` * Added `math.lerp(t, a, b)` + * Added `findhandle(name: String): Any?` and `gethandle(name: String): Any`, for getting engine-"private" handles, usually tables, which have a string-defined name attached to them + * `findhandle` will return nothing if handle does not exist, while `gethandle` will throw an exception if handle does not exist. Those who come from Garry's Mod you should notice this functionality is very similar to [FindMetaTable](https://wiki.facepunch.com/gmod/Global.FindMetaTable), and you won't be wrong + * Handles (Lua values stored on Lua stack in separate technical thread) are used mostly for storing metamethods for Java objects exposed to Lua state, such as `RandomGenerator`, or `CompletableFuture`. + To those tech-savvy Lua wizards who wonder why handles are used and not `LUA_REGISTRYINDEX` - using thread stack is considerably faster than using `LUA_REGISTRYINDEX`, the only downside is that stack size is limited by compile-time constant, so it can't grow indefinitely + * Currently, next handles are available for Lua code to get: + * RandomGenerator + * LuaFuture + * PerlinNoise + * PathFinder ## 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 @@ -176,6 +185,17 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a * Added `status.minimumLiquidStatusEffectPercentage(): Double` * Added `status.setMinimumLiquidStatusEffectPercentage(value: Double)` +## Path Finder + +In new engine, pathfinder returned by `world.platformerPathStart()`, if unable find path to goal inside `finder:explore()` due to budget constraints, launches +off-thread explorer which continues to search path where `explore()` left off, allowing to make world responsive while multiple NPCs are path finding. + + * `finder:explore(maxIterations: Int? = 800, useOffThread: Boolean = true): List?` now accepts second (optional) argument telling whenever it should continue to work off-thread automatically + * Defaults to true, since it shouldn't cause any major issues. However, if you want to continue manually exploring sometime in future, you can specify second argument as `false` + * Added `finder:runAsync()`, which launches off-thread exploration task immediately. Does nothing if already exploring off-thread + * Added `finder:isExploringOffThread(): Boolean`, used to determine if pathfinder is actively working off-thread (always returns false if work is finished regardless of actual result) + * Added `finder:usedOffThread(): Boolean`, used to determine if pathfinder ever worked off-thread + ## world #### Additions @@ -183,12 +203,20 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a * Added `world.liquidNamesAlongLine(start: Vector2d, end: Vector2d): List`, will return Liquid' name instead of its ID * Added `world.liquidNameAt(at: Vector2i): LiquidState?`, will return Liquid' name instead of its ID * Added `world.biomeBlockNamesAt(at: Vector2i): List?`, will return Block names instead of their IDs - * Added `world.destroyNamedLiquid(at: Vector2i): LiquidState?`, will return Liquid' name instead of its ID + * Added `world.destroyLiquidPromise(at: Vector2i): RpcPromise`, returns promise with two values (vararg, not table): whenever operation was successful, and previously stored liquid state, with liquid name, and not liquid id * Added `world.gravityVector(at: Vector2d): Vector2d`. **Attention:** directional gravity is WIP. * Added `world.itemDropLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` * Added `world.playerLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` * Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` * Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` + * Implemented `Poly` entity queries, which were mentioned in engine's code but never implemented as actual Lua bindings (you could still use these in original engine IF you used non-line bindings AND specified poly as `poly = { ... }` in `options` table): + * Added `world.entityPolyQuery(poly: Poly, options: Table?): List` + * Added `world.itemDropPolyQuery(poly: Poly, options: Table?): List` + * Added `world.npcPolyQuery(poly: Poly, options: Table?): List` + * Added `world.monsterPolyQuery(poly: Poly, options: Table?): List` + * Added `world.playerPolyQuery(poly: Poly, options: Table?): List` + * Added `world.objectPolyQuery(poly: Poly, options: Table?): List` + * Added `world.loungeablePolyQuery(poly: Poly, options: Table?): List` * Added `world.loadUniqueEntityAsync(id: String): RpcPromise` * Added `world.findUniqueEntityAsync(id: String): RpcPromise` * `world.findUniqueEntity` is legacy function, and to retain legacy behavior it will **block** world thread upon `result()` call if entity has not been found yet @@ -217,13 +245,30 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a * If all tiles were protected, it will return `"protected"`. * If none tiles were damaged, it will return `"none"`. * Added `world.damageTileAreaPromise(radius: Double, position: Vector2i, layer: String, damageSource: Vector2d, damageType: String, damageAmount: Double, harvestLevel: Int = 999, sourceEntity: EntityID = 0): RpcPromise`, with same notes as `world.damageTilesPromise()` apply - * Added `world.placeMaterialPromise(pos: Vector2i, layer: String, material: String, hueShift: Number?, allowOverlap: Boolean): RpcPromise`, returning promise of unapplied tile modifications + * Added `world.placeMaterialPromise(pos: Vector2i, layer: String, material: String, hueShift: Number?, allowOverlap: Boolean): RpcPromise>`, returning promise of unapplied tile modifications * However, correct clientside result will be returned _only_ when using native protocol - * Added `world.placeModPromise(pos: Vector2i, layer: String, modifier: String, hueShift: Number?, allowOverlap: Boolean): RpcPromise`, returning promise of unapplied tile modifications + * Added `world.placeModPromise(pos: Vector2i, layer: String, modifier: String, hueShift: Number?, allowOverlap: Boolean): RpcPromise>`, returning promise of unapplied tile modifications * However, correct clientside result will be returned _only_ when using native protocol + * Added `world.weatherStatusEffects(pos: Vector2i): List` + * Added async (`RpcPromise<>`) variant for next functions (to call new variant, add `Async` at end of function's name, `containerTakeNumItemsAt` -> `containerTakeNumItemsAtAsync`), which allows to somewhat properly interact with remote containers (e.g. modify serverside container on client): + * Where possible, this new functionality makes use of existing functions (available on both new and old engines), but if remote is original engine, some functions might misbehave due to emulation of desired behavior. + Generally, you should avoid interacting with remote containers through Lua scripts, but since protocol allows it (and scripts previously could interact with them, albeit interaction was crippled), these improved functions were provided for your convenience. + * `world.containerConsume` + * `world.containerConsumeAt` + * `world.containerTakeAll` + * `world.containerTakeAt` + * `world.containerTakeNumItemsAt` + * `world.containerAddItems` + * `world.containerStackItems` + * `world.containerPutItemsAt` + * `world.containerSwapItems` + * `world.containerSwapItemsNoCombine` + * `world.containerItemApply` #### Changes + * `world.getObjectParameter(id: EntityID, path: String, default: Json): Json` now returns third argument as-is (which is insanely faster), without copying or any other transformations of any kind + * This MIGHT break code which expect this function to always copy third argument, or transform it into json * `world.entityHandItem(id: EntityID, hand: String): String` now accepts `"secondary"` as `hand` argument (in addition to `"primary"`/`"alt"`) * `world.containerConsume(id: EntityID, item: ItemDescriptor, exact: Boolean?): Boolean?` now accepts `exact` which forces exact match on item descriptor (default `false`) * `world.flyingType(): String` has been made shared (previously was server world only) @@ -258,6 +303,7 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a * `world.loadUniqueEntity()` **is busted in new engine** and will cause major issues if used (because engine is incapable for synchronous loading of world chunks, everything is async) * If your mod is using it **PLEASE** switch to `world.loadUniqueEntityAsync(id: String): RpcPromise` * `world.spawnLiquid()` is deprecated, use `world.spawnLiquidPromise()` instead + * `world.destroyLiquid()` is deprecated for two reasons - it doesn't report whenever operation successful, and returns liquid id instead of liquid name, use `world.destroyLiquidPromise()` instead * `world.damageTiles()` is deprecated, use `world.damageTilesPromise()` instead * `world.damageTileArea()` is deprecated, use `world.damageTileAreaPromise()` instead * `world.placeMaterial()` is deprecated, use `world.placeMaterialPromise()` instead diff --git a/src/main/java/ru/dbotthepony/kstarbound/lua/LuaJNR.java b/src/main/java/ru/dbotthepony/kstarbound/lua/LuaJNR.java index 3e5fd412..98438119 100644 --- a/src/main/java/ru/dbotthepony/kstarbound/lua/LuaJNR.java +++ b/src/main/java/ru/dbotthepony/kstarbound/lua/LuaJNR.java @@ -159,7 +159,7 @@ public interface LuaJNR { // проверка стека @IgnoreError - public int lua_checkstack(@NotNull Pointer luaState, int value); + public boolean lua_checkstack(@NotNull Pointer luaState, int value); @IgnoreError public int lua_absindex(@NotNull Pointer luaState, int value); @IgnoreError diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index 07772a2b..285eecc4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -276,6 +276,10 @@ class Registry(val name: String, val storeJson: Boolean = true) { } } + fun ref(index: Either): Ref { + return index.map({ ref(it) }, { ref(it) }) + } + operator fun contains(index: String) = lock.read { index in keysInternal } operator fun contains(index: Int) = lock.read { index in idsInternal } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt index 2cb0d049..6233f878 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt @@ -24,6 +24,8 @@ import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity import ru.dbotthepony.kstarbound.world.entities.tile.PlantPieceEntity import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import java.io.DataInputStream +import java.util.Collections +import java.util.EnumSet enum class EntityType(override val jsonName: String, val storeName: String, val canBeCreatedByClient: Boolean, val canBeSpawnedByClient: Boolean, val ephemeralIfSpawnedByClient: Boolean = true) : IStringSerializable { PLANT("plant", "PlantEntity", false, false) { @@ -158,4 +160,8 @@ enum class EntityType(override val jsonName: String, val storeName: String, val abstract suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity abstract fun fromStorage(data: JsonObject): AbstractEntity + + companion object { + val ALL: Set = Collections.unmodifiableSet(EnumSet.allOf(EntityType::class.java)) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index 5916cc3a..2ace2633 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.math.quintic2 +import ru.dbotthepony.kstarbound.util.floorToInt import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.staticRandomInt import ru.dbotthepony.kstarbound.world.Universe @@ -32,6 +33,7 @@ import ru.dbotthepony.kstarbound.world.physics.Poly import java.time.Duration import java.util.concurrent.CopyOnWriteArrayList import java.util.random.RandomGenerator +import kotlin.math.floor class WorldTemplate(val geometry: WorldGeometry) { var seed: Long = 0L @@ -326,16 +328,23 @@ class WorldTemplate(val geometry: WorldGeometry) { } fun cellInfo(x: Int, y: Int): CellInfo { - worldLayout ?: return CellInfo(x, y) - val vec = Vector2i(x, y) - return cellCache[vec.hashCode() and 255].get(vec) - } - - fun cellInfo(pos: Vector2i): CellInfo { + val pos = geometry.wrap(x, y) worldLayout ?: return CellInfo(pos.x, pos.y) return cellCache[pos.hashCode() and 255].get(pos) } + @Suppress("NAME_SHADOWING") + fun cellInfo(pos: Vector2i): CellInfo { + val pos = geometry.wrap(pos) + worldLayout ?: return CellInfo(pos.x, pos.y) + return cellCache[pos.hashCode() and 255].get(pos) + } + + fun cellInfo(pos: Vector2d): CellInfo { + val (x, y) = geometry.wrap(pos) + return cellInfo(x.floorToInt(), y.floorToInt()) + } + private fun cellInfo0(x: Int, y: Int): CellInfo { val info = CellInfo(x, y) val layout = worldLayout ?: return info diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/CommonHandleRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/CommonHandleRegistry.kt new file mode 100644 index 00000000..a6a6ec50 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/CommonHandleRegistry.kt @@ -0,0 +1,6 @@ +package ru.dbotthepony.kstarbound.lua + +data class CommonHandleRegistry( + val future: LuaHandle, + val pathFinder: LuaHandle, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt index f1f0728b..8d5b440a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt @@ -15,6 +15,7 @@ import org.classdump.luna.runtime.AbstractFunction3 import org.classdump.luna.runtime.ExecutionContext import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2f @@ -556,6 +557,63 @@ fun LuaThread.ArgStack.nextOptionalVector2d(position: Int = this.position++): Ve return lua.getVector2d(position) } +fun LuaThread.getVector2iOrAABB(stackIndex: Int = -1): Either? { + val abs = this.absStackIndex(stackIndex) + + if (!this.isTable(abs)) + return null + + push(3) + val type = loadTableValue(abs) + pop() + + if (type == LuaType.NUMBER) { + return Either.right(getAABB(stackIndex) ?: return null) + } else { + return Either.left(getVector2i(stackIndex) ?: return null) + } +} + +fun LuaThread.ArgStack.nextVector2iOrAABB(position: Int = this.position++): Either { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: Vector2d expected, got nil") + + return lua.getVector2iOrAABB(position) + ?: throw IllegalArgumentException("bad argument #$position: Vector2d or AABB expected, got ${lua.typeAt(position)}") +} + +fun LuaThread.ArgStack.nextOptionalVector2iOrAABB(position: Int = this.position++): Either? { + if (position !in 1 ..this.top) + return null + + return lua.getVector2iOrAABB(position) +} + +fun LuaThread.getRegistryID(stackIndex: Int = -1): Either? { + val abs = absStackIndex(stackIndex) + + when (typeAt(abs)) { + LuaType.NUMBER -> return Either.right(getLong(stackIndex)!!.toInt()) + LuaType.STRING -> return Either.left(getString(stackIndex)!!) + else -> return null + } +} + +fun LuaThread.ArgStack.nextRegistryID(position: Int = this.position++): Either { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: Vector2d expected, got nil") + + return lua.getRegistryID(position) + ?: throw IllegalArgumentException("bad argument #$position: Vector2d or AABB expected, got ${lua.typeAt(position)}") +} + +fun LuaThread.ArgStack.nextOptionalRegistryID(position: Int = this.position++): Either? { + if (position !in 1 ..this.top) + return null + + return lua.getRegistryID(position) +} + fun LuaThread.getVector2i(stackIndex: Int = -1): Vector2i? { val abs = this.absStackIndex(stackIndex) @@ -750,7 +808,8 @@ fun LuaThread.ArgStack.nextOptionalAABBi(position: Int = this.position++): AABBi return lua.getAABBi(position) } -fun LuaThread.push(value: IStruct4i) { +fun LuaThread.push(value: IStruct4i?) { + value ?: return push() pushTable(arraySize = 4) val table = stackTop val (x, y, z, w) = value @@ -772,7 +831,8 @@ fun LuaThread.push(value: IStruct4i) { setTableValue(table) } -fun LuaThread.push(value: IStruct3i) { +fun LuaThread.push(value: IStruct3i?) { + value ?: return push() pushTable(arraySize = 3) val table = stackTop val (x, y, z) = value @@ -790,7 +850,8 @@ fun LuaThread.push(value: IStruct3i) { setTableValue(table) } -fun LuaThread.push(value: IStruct2i) { +fun LuaThread.push(value: IStruct2i?) { + value ?: return push() pushTable(arraySize = 2) val table = stackTop val (x, y) = value @@ -804,7 +865,8 @@ fun LuaThread.push(value: IStruct2i) { setTableValue(table) } -fun LuaThread.push(value: IStruct4f) { +fun LuaThread.push(value: IStruct4f?) { + value ?: return push() pushTable(arraySize = 4) val table = stackTop val (x, y, z, w) = value @@ -826,7 +888,8 @@ fun LuaThread.push(value: IStruct4f) { setTableValue(table) } -fun LuaThread.push(value: IStruct3f) { +fun LuaThread.push(value: IStruct3f?) { + value ?: return push() pushTable(arraySize = 3) val table = stackTop val (x, y, z) = value @@ -844,7 +907,8 @@ fun LuaThread.push(value: IStruct3f) { setTableValue(table) } -fun LuaThread.push(value: IStruct2f) { +fun LuaThread.push(value: IStruct2f?) { + value ?: return push() pushTable(arraySize = 2) val table = stackTop val (x, y) = value @@ -858,7 +922,8 @@ fun LuaThread.push(value: IStruct2f) { setTableValue(table) } -fun LuaThread.push(value: IStruct4d) { +fun LuaThread.push(value: IStruct4d?) { + value ?: return push() pushTable(arraySize = 4) val table = stackTop val (x, y, z, w) = value @@ -880,7 +945,8 @@ fun LuaThread.push(value: IStruct4d) { setTableValue(table) } -fun LuaThread.push(value: IStruct3d) { +fun LuaThread.push(value: IStruct3d?) { + value ?: return push() pushTable(arraySize = 3) val table = stackTop val (x, y, z) = value @@ -898,7 +964,8 @@ fun LuaThread.push(value: IStruct3d) { setTableValue(table) } -fun LuaThread.push(value: IStruct2d) { +fun LuaThread.push(value: IStruct2d?) { + value ?: return push() pushTable(arraySize = 2) val table = stackTop val (x, y) = value @@ -911,3 +978,87 @@ fun LuaThread.push(value: IStruct2d) { push(y) setTableValue(table) } + +fun LuaThread.setTableValue(key: String, value: IStruct2i?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: String, value: IStruct3i?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: String, value: IStruct4i?) { push(key); push(value); setTableValue() } + +fun LuaThread.setTableValue(key: String, value: IStruct2d?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: String, value: IStruct3d?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: String, value: IStruct4d?) { push(key); push(value); setTableValue() } + +fun LuaThread.setTableValue(key: String, value: IStruct2f?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: String, value: IStruct3f?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: String, value: IStruct4f?) { push(key); push(value); setTableValue() } + +fun LuaThread.setTableValue(key: Long, value: IStruct2i?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Long, value: IStruct3i?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Long, value: IStruct4i?) { push(key); push(value); setTableValue() } + +fun LuaThread.setTableValue(key: Long, value: IStruct2d?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Long, value: IStruct3d?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Long, value: IStruct4d?) { push(key); push(value); setTableValue() } + +fun LuaThread.setTableValue(key: Long, value: IStruct2f?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Long, value: IStruct3f?) { push(key); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Long, value: IStruct4f?) { push(key); push(value); setTableValue() } + +fun LuaThread.setTableValue(key: Int, value: IStruct2i?) { push(key.toLong()); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Int, value: IStruct3i?) { push(key.toLong()); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Int, value: IStruct4i?) { push(key.toLong()); push(value); setTableValue() } + +fun LuaThread.setTableValue(key: Int, value: IStruct2d?) { push(key.toLong()); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Int, value: IStruct3d?) { push(key.toLong()); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Int, value: IStruct4d?) { push(key.toLong()); push(value); setTableValue() } + +fun LuaThread.setTableValue(key: Int, value: IStruct2f?) { push(key.toLong()); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Int, value: IStruct3f?) { push(key.toLong()); push(value); setTableValue() } +fun LuaThread.setTableValue(key: Int, value: IStruct4f?) { push(key.toLong()); push(value); setTableValue() } + +fun LuaThread.push(value: AABB?) { + value ?: return push() + pushTable(arraySize = 4) + val table = stackTop + val (x, y) = value.mins + val (z, w) = value.maxs + + push(1) + push(x) + setTableValue(table) + + push(2) + push(y) + setTableValue(table) + + push(3) + push(z) + setTableValue(table) + + push(4) + push(w) + setTableValue(table) +} + +fun LuaThread.push(value: AABBi?) { + value ?: return push() + pushTable(arraySize = 4) + val table = stackTop + val (x, y) = value.mins + val (z, w) = value.maxs + + push(1) + push(x.toLong()) + setTableValue(table) + + push(2) + push(y.toLong()) + setTableValue(table) + + push(3) + push(z.toLong()) + setTableValue(table) + + push(4) + push(w.toLong()) + setTableValue(table) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaHandle.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaHandle.kt index 9d2e2e44..ead39f40 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaHandle.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaHandle.kt @@ -2,9 +2,10 @@ package ru.dbotthepony.kstarbound.lua import ru.dbotthepony.kstarbound.Starbound import java.io.Closeable +import java.lang.ref.Cleaner.Cleanable -class LuaHandle(private val parent: LuaHandleThread, val handle: Int) : Closeable { - private val cleanables = ArrayList() +class LuaHandle(private val parent: LuaSharedState, val handle: Int, val key: Any?) : Closeable { + private val cleanable: Cleanable var isValid = true private set @@ -12,28 +13,24 @@ class LuaHandle(private val parent: LuaHandleThread, val handle: Int) : Closeabl init { val parent = parent val handle = handle + val key = key - cleanables.add(Starbound.CLEANER.register(this) { - parent.freeHandle(handle) - }::clean) + cleanable = Starbound.CLEANER.register(this) { + parent.freeHandle(handle, key) + } } fun push(into: LuaThread) { check(isValid) { "Tried to use NULL handle!" } - parent.thread.push() - parent.thread.copy(handle, -1) - parent.thread.moveStackValuesOnto(into) - } - - fun onClose(cleanable: Runnable) { - check(isValid) { "No longer valid" } - cleanables.add(cleanable) + parent.handlesThread.push() + parent.handlesThread.copy(handle, -1) + parent.handlesThread.moveStackValuesOnto(into) } override fun close() { if (!isValid) return - cleanables.forEach { it.run() } + cleanable.clean() isValid = false } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaHandleThread.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaHandleThread.kt deleted file mode 100644 index de407e9a..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaHandleThread.kt +++ /dev/null @@ -1,52 +0,0 @@ -package ru.dbotthepony.kstarbound.lua - -import it.unimi.dsi.fastutil.ints.IntAVLTreeSet -import java.util.concurrent.ConcurrentLinkedQueue - -class LuaHandleThread(mainThread: LuaThread) { - private val pendingFree = ConcurrentLinkedQueue() - private val freeHandles = IntAVLTreeSet() - private var nextHandle = 0 - // faster code path - private var handlesInUse = 0 - - val thread = mainThread.newThread(true) - - init { - mainThread.storeRef(LuaThread.LUA_REGISTRYINDEX) - } - - fun freeHandle(handle: Int) { - pendingFree.add(handle) - } - - fun cleanup() { - if (handlesInUse == 0) return - var handle = pendingFree.poll() - - while (handle != null) { - handlesInUse-- - freeHandles.add(handle) - thread.push() - thread.copy(-1, handle) - thread.pop() - - handle = pendingFree.poll() - } - } - - fun allocateHandle(): LuaHandle { - handlesInUse++ - - if (freeHandles.isEmpty()) { - return LuaHandle(this, ++nextHandle) - } else { - val handle = freeHandles.firstInt() - freeHandles.remove(handle) - - thread.copy(-1, handle) - thread.pop() - return LuaHandle(this, handle) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaSharedState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaSharedState.kt new file mode 100644 index 00000000..c5e332d1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaSharedState.kt @@ -0,0 +1,89 @@ +package ru.dbotthepony.kstarbound.lua + +import it.unimi.dsi.fastutil.ints.IntAVLTreeSet +import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture +import ru.dbotthepony.kstarbound.lua.userdata.LuaPathFinder +import ru.dbotthepony.kstarbound.util.random.random +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.random.RandomGenerator +import kotlin.properties.Delegates + +class LuaSharedState(val handlesThread: LuaThread) { + private val pendingFree = ConcurrentLinkedQueue() + private val freeHandles = IntAVLTreeSet() + private var nextHandle = 0 + // faster code path + private var handlesInUse = 0 + + private val namedHandles = HashMap() + var random: RandomGenerator = random() + + var commonHandles by Delegates.notNull() + private set + + fun initializeHandles(mainThread: LuaThread) { + val future = LuaFuture.initializeHandle(mainThread) + val pathFinder = LuaPathFinder.initializeHandle(mainThread) + + commonHandles = CommonHandleRegistry( + future = future, + pathFinder = pathFinder, + ) + } + + fun freeHandle(handle: Int, key: Any?) { + pendingFree.add(handle) + + if (key != null) { + namedHandles.remove(key) + } + } + + fun cleanup() { + if (handlesInUse == 0) return + var handle = pendingFree.poll() + + while (handle != null) { + handlesInUse-- + freeHandles.add(handle) + handlesThread.push() + handlesThread.copy(-1, handle) + handlesThread.pop() + + handle = pendingFree.poll() + } + } + + fun allocateHandle(name: Any?): LuaHandle { + require(name == null || name !in namedHandles) { "Named handle '$name' already exists" } + handlesInUse++ + + if (freeHandles.isEmpty()) { + if (nextHandle % 10 == 0) { + handlesThread.ensureExtraCapacity(10) + } + + return LuaHandle(this, ++nextHandle, name).also { + if (name != null) namedHandles[name] = it + } + } else { + val handle = freeHandles.firstInt() + freeHandles.remove(handle) + + handlesThread.copy(-1, handle) + handlesThread.pop() + + return LuaHandle(this, handle, name).also { + if (name != null) namedHandles[name] = it + } + } + } + + fun getNamedHandle(key: Any): LuaHandle { + return namedHandles[key] ?: throw NoSuchElementException("No such handle: $key") + } + + fun findNamedHandle(key: Any): LuaHandle? { + return namedHandles[key] + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt index 67e9305f..1e0440ca 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaThread.kt @@ -17,24 +17,28 @@ import org.apache.logging.log4j.LogManager import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryUtil import ru.dbotthepony.kommons.gson.set -import ru.dbotthepony.kommons.util.Delegate 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 import java.nio.ByteBuffer import java.nio.ByteOrder -import java.util.concurrent.ConcurrentLinkedQueue import java.util.random.RandomGenerator import kotlin.math.floor import kotlin.properties.Delegates import kotlin.system.exitProcess +/** + * Class representing a native Lua thread + stack, and transiently, a native Lua state + * + * These are not garbage collected, and *must* be [close]d manually when no longer in use, more specifically, + * right after [LuaThread] creation, it creates several cyclic references through GC root, + * so it never gets reclaimed by GC unless [close] is called + */ @Suppress("unused") class LuaThread private constructor( private val pointer: Pointer, @@ -52,7 +56,7 @@ class LuaThread private constructor( CallContext.getCallContext(Type.SINT, arrayOf(Type.POINTER), CallingConvention.DEFAULT, false) ) - this.cleanable = Starbound.CLEANER.register(this) { + this.cleanable = Cleaner.Cleanable { LuaJNR.INSTANCE.lua_close(pointer) panic.dispose() } @@ -60,8 +64,19 @@ class LuaThread private constructor( panic.setAutoRelease(false) LuaJNR.INSTANCE.lua_atpanic(pointer, panic.address) - randomHolder = Delegate.Box(random()) - handleThread = LuaHandleThread(this) + val handles = LuaJNR.INSTANCE.lua_newthread(pointer) + storeRef(LUA_REGISTRYINDEX) + val handlesThread = LuaThread(handles, stringInterner) + sharedState = LuaSharedState(handlesThread) + sharedState.handlesThread.sharedState = sharedState + + push("__nils") + createHandle("__nils") + pop() + + push("__typehint") + createHandle("__typehint") + pop() LuaJNR.INSTANCE.luaopen_base(this.pointer) this.storeGlobal("_G") @@ -76,10 +91,12 @@ class LuaThread private constructor( LuaJNR.INSTANCE.luaopen_utf8(this.pointer) this.storeGlobal("utf8") + sharedState.initializeHandles(this) + provideUtilityBindings(this) provideRootBindings(this) - load(globalScript, "@starbound.jar!/scripts/global.lua") + load(globalScript, "@/internal/global.lua") call() } @@ -87,9 +104,20 @@ class LuaThread private constructor( fun invoke(args: ArgStack): Int } + fun interface Binding { + fun invoke(self: T, arguments: ArgStack): Int + } + + fun interface Binding1 { + fun invoke(self: T, extra: A0, arguments: ArgStack): Int + } + + fun interface Binding2 { + fun invoke(self: T, extra: A0, extra1: A1, arguments: ArgStack): Int + } + private var cleanable: Cleaner.Cleanable? = null - private var randomHolder: Delegate by Delegates.notNull() - private var handleThread by Delegates.notNull() + private var sharedState by Delegates.notNull() /** * Responsible for generating random numbers using math.random @@ -98,25 +126,16 @@ class LuaThread private constructor( * math.randomseed sets this property to brand-new generator with required seed */ var random: RandomGenerator - get() = randomHolder.get() - set(value) = randomHolder.accept(value) + get() = sharedState.random + set(value) { sharedState.random = value } - private fun initializeFrom(other: LuaThread, skipHandle: Boolean) { - randomHolder = other.randomHolder + val commonHandles get() = sharedState.commonHandles - if (!skipHandle) - handleThread = other.handleThread - } - - private fun cleanup() { - handleThread.cleanup() - } - - fun newThread(skipHandle: Boolean = false): LuaThread { + fun newThread(): LuaThread { val pointer = LuaJNR.INSTANCE.lua_newthread(pointer) return LuaThread(pointer, stringInterner).also { - it.initializeFrom(this, skipHandle) + it.sharedState = sharedState } } @@ -183,7 +202,7 @@ class LuaThread private constructor( } fun call(numArgs: Int = 0, numResults: Int = 0): Int { - cleanup() + sharedState.cleanup() val status = LuaJNR.INSTANCE.lua_pcallk(this.pointer, numArgs, numResults, 0, 0L, 0L) if (status == LUA_ERRRUN) { @@ -407,6 +426,20 @@ class LuaThread private constructor( return value } + fun getInt(stackIndex: Int = -1): Int? { + if (!this.isNumber(stackIndex)) + return null + + val stack = MemoryStack.stackPush() + val status = stack.mallocInt(1) + val value = LuaJNR.INSTANCE.lua_tointegerx(this.pointer, stackIndex, MemoryUtil.memAddress(status)) + val b = status[0] > 0 + stack.close() + + if (!b) return null + return value.toInt() + } + private fun getLongRaw(stackIndex: Int = -1): Long { val stack = MemoryStack.stackPush() val status = stack.mallocInt(1) @@ -623,11 +656,11 @@ class LuaThread private constructor( return LuaJNI.lua_tojobject(pointer.address(), stackIndex) } - fun iterateTable(stackIndex: Int = -1, keyVisitor: LuaThread.(stackIndex: Int) -> Unit, valueVisitor: LuaThread.(stackIndex: Int) -> Unit) { + fun iterateTable(stackIndex: Int = -1, keyVisitor: LuaThread.(stackIndex: Int) -> Unit, valueVisitor: LuaThread.(stackIndex: Int) -> Unit): Boolean { val abs = this.absStackIndex(stackIndex) if (!this.isTable(abs)) - return + return false this.push() val top = this.stackTop @@ -641,6 +674,8 @@ class LuaThread private constructor( } finally { LuaJNR.INSTANCE.lua_settop(this.pointer, top - 1) } + + return true } fun readTableKeys(stackIndex: Int = -1, keyVisitor: LuaThread.(stackIndex: Int) -> T): MutableList? { @@ -795,6 +830,12 @@ class LuaThread private constructor( val lua get() = this@LuaThread var position = 1 + init { + if (top >= 10) { + this@LuaThread.ensureExtraCapacity(10) + } + } + fun peek(position: Int = this.position): LuaType { if (position !in 1 .. top) return LuaType.NONE @@ -802,10 +843,63 @@ class LuaThread private constructor( return this@LuaThread.typeAt(position) } + fun peekAndSkipNothing(): LuaType { + val peek = peek() + if (peek.isNothing) position++ + return peek + } + fun hasNext(): Boolean { return position <= top } + fun skip(amount: Int = 1) { + position += amount + } + + fun copyRemaining(): JsonArray { + val result = JsonArray() + + while (position <= top) { + result.add(nextJson()) + } + + return result + } + + fun iterateTable(position: Int = this.position++, keyVisitor: LuaThread.(stackIndex: Int) -> Unit, valueVisitor: LuaThread.(stackIndex: Int) -> Unit) { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: table expected, got nil") + + require(this@LuaThread.iterateTable(position, keyVisitor, valueVisitor)) { + "bad argument #$position: table expected, got ${this@LuaThread.typeAt(position)}" + } + } + + fun readTableKeys(position: Int = this.position++, keyVisitor: LuaThread.(stackIndex: Int) -> T): MutableList { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: table expected, got nil") + + return this@LuaThread.readTableKeys(position, keyVisitor) + ?: throw IllegalArgumentException("bad argument #$position: table expected, got ${this@LuaThread.typeAt(position)}") + } + + fun readTableValues(position: Int = this.position++, valueVisitor: LuaThread.(stackIndex: Int) -> T): MutableList { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: table expected, got nil") + + return this@LuaThread.readTableValues(position, valueVisitor) + ?: throw IllegalArgumentException("bad argument #$position: table expected, got ${this@LuaThread.typeAt(position)}") + } + + fun readTable(position: Int = this.position++, keyVisitor: LuaThread.(stackIndex: Int) -> K, valueVisitor: LuaThread.(stackIndex: Int) -> V): MutableList> { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: table expected, got nil") + + return this@LuaThread.readTable(position, keyVisitor, valueVisitor) + ?: throw IllegalArgumentException("bad argument #$position: table expected, got ${this@LuaThread.typeAt(position)}") + } + inline fun nextObject(position: Int = this.position++): T { if (position !in 1 ..this.top) throw IllegalArgumentException("bad argument #$position: Java object expected, got nil") @@ -845,6 +939,16 @@ class LuaThread private constructor( ?: throw IllegalArgumentException("bad argument #$position: long expected, got ${this@LuaThread.typeAt(position)}") } + fun nextInt(position: Int = this.position++): Int { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: number expected, got nil") + + return this@LuaThread.getInt(position) + ?: throw IllegalArgumentException("bad argument #$position: long expected, got ${this@LuaThread.typeAt(position)}") + } + + fun nextOptionalInt(position: Int = this.position++) = nextOptionalLong(position)?.toInt() + fun nextJson(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement { if (position !in 1 ..this.top) throw IllegalArgumentException("bad argument #$position: json expected, got nil") @@ -853,6 +957,14 @@ class LuaThread private constructor( return value ?: throw IllegalArgumentException("bad argument #$position: anything expected, got ${this@LuaThread.typeAt(position)}") } + fun nextJsonArray(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonArray { + if (position !in 1 ..this.top) + throw IllegalArgumentException("bad argument #$position: json expected, got nil") + + val value = this@LuaThread.getJson(position, limit = limit) + return value as? JsonArray ?: throw IllegalArgumentException("bad argument #$position: JsonArray 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 @@ -868,13 +980,6 @@ class LuaThread private constructor( return value ?: throw IllegalArgumentException("Lua code error: bad argument #$position: table expected, got ${this@LuaThread.typeAt(position)}") } - fun nextAny(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement? { - if (position !in 1 ..this.top) - throw IllegalArgumentException("bad argument #$position: json expected, got nil") - - return this@LuaThread.getJson(position, limit = limit) - } - fun nextDouble(position: Int = this.position++): Double { if (position !in 1 ..this.top) throw IllegalArgumentException("bad argument #$position: number expected, got nil") @@ -909,7 +1014,7 @@ class LuaThread private constructor( else if (type == LuaType.BOOLEAN) return this@LuaThread.getBoolean(position) else - throw IllegalArgumentException("Lua code error: bad argument #$position: boolean expected, got $type") + throw IllegalArgumentException("bad argument #$position: boolean expected, got $type") } fun nextBoolean(position: Int = this.position++): Boolean { @@ -921,78 +1026,118 @@ class LuaThread private constructor( } } - fun push(function: Fn, performanceCritical: Boolean) { - LuaJNI.lua_pushcclosure(pointer.address()) lazy@{ - cleanup() - val realLuaState: LuaThread + private fun closure(p: Long, function: Fn, performanceCritical: Boolean): Int { + sharedState.cleanup() + val realLuaState: LuaThread - if (pointer.address() != it) { - realLuaState = LuaThread(LuaJNR.RUNTIME.memoryManager.newPointer(it), stringInterner = stringInterner) - realLuaState.initializeFrom(this, false) - } else { - realLuaState = this - } + if (pointer.address() != p) { + realLuaState = LuaThread(LuaJNR.RUNTIME.memoryManager.newPointer(p), stringInterner = stringInterner) + realLuaState.sharedState = sharedState + } else { + realLuaState = this + } - val args = realLuaState.ArgStack(realLuaState.stackTop) - val rememberStack: ArrayList? + val args = realLuaState.ArgStack(realLuaState.stackTop) + val rememberStack: ArrayList? - if (performanceCritical) { - rememberStack = null - } else { - rememberStack = ArrayList(Exception().stackTraceToString().split('\n')) + if (performanceCritical) { + rememberStack = null + } else { + rememberStack = ArrayList(Exception().stackTraceToString().split('\n')) - rememberStack.removeAt(0) // java.lang. ... - // rememberStack.removeAt(0) // at ... push( ... ) - } + rememberStack.removeAt(0) // java.lang. ... + // rememberStack.removeAt(0) // at ... push( ... ) + } + try { + val value = function.invoke(args) + check(value >= 0) { "Internal JVM error: ${function::class.qualifiedName} returned incorrect number of arguments to be popped from stack by Lua" } + return value + } catch (err: Throwable) { try { - val value = function.invoke(args) - check(value >= 0) { "Internal JVM error: ${function::class.qualifiedName} returned incorrect number of arguments to be popped from stack by Lua" } - return@lazy value - } catch (err: Throwable) { - try { - if (performanceCritical) { - realLuaState.push(err.stackTraceToString()) - return@lazy -1 - } else { - rememberStack!! - val newStack = err.stackTraceToString().split('\n').toMutableList() + if (performanceCritical) { + realLuaState.push(err.stackTraceToString()) + return -1 + } else { + rememberStack!! + val newStack = err.stackTraceToString().split('\n').toMutableList() - val rememberIterator = rememberStack.listIterator(rememberStack.size) - val iterator = newStack.listIterator(newStack.size) - var hit = false + val rememberIterator = rememberStack.listIterator(rememberStack.size) + val iterator = newStack.listIterator(newStack.size) + var hit = false - while (rememberIterator.hasPrevious() && iterator.hasPrevious()) { - val a = rememberIterator.previous() - val b = iterator.previous() + while (rememberIterator.hasPrevious() && iterator.hasPrevious()) { + val a = rememberIterator.previous() + val b = iterator.previous() - if (a == b) { - hit = true - iterator.remove() - } else { - break - } + if (a == b) { + hit = true + iterator.remove() + } else { + break } - - if (hit) { - newStack[newStack.size - 1] = "\t<...>" - } - - realLuaState.push(newStack.joinToString("\n")) - return@lazy -1 } - } catch(err2: Throwable) { - realLuaState.push("JVM suffered an exception while handling earlier exception: ${err2.stackTraceToString()}; earlier: ${err.stackTraceToString()}") - return@lazy -1 + + if (hit) { + newStack[newStack.size - 1] = "\t<...>" + } + + realLuaState.push(newStack.joinToString("\n")) + return -1 } + } catch(err2: Throwable) { + realLuaState.push("JVM suffered an exception while handling earlier exception: ${err2.stackTraceToString()}; earlier: ${err.stackTraceToString()}") + return -1 } } } + fun push(function: Fn, performanceCritical: Boolean) { + LuaJNI.lua_pushcclosure(pointer.address()) { + closure(it, function, performanceCritical) + } + } + fun push(function: Fn) = this.push(function, !RECORD_STACK_TRACES) - fun interface Binding { - fun invoke(self: T, arguments: ArgStack): Int + fun push(self: T, function: Binding) { + push { + function.invoke(self, it) + } + } + + fun push(self: T, extra: A0, function: Binding1) { + push { + function.invoke(self, extra, it) + } + } + + fun push(self: T, extra: A0, extra1: A1, function: Binding2) { + push { + function.invoke(self, extra, extra1, it) + } + } + + fun pushBinding(self: T, name: String, function: Binding) { + push(name) + push { function.invoke(self, it) } + setTableValue() + } + + fun pushBinding(self: T, extra: A0, name: String, function: Binding1) { + push(name) + push { function.invoke(self, extra, it) } + setTableValue() + } + + fun pushBinding(self: T, extra: A0, extra1: A1, name: String, function: Binding2) { + push(name) + push { function.invoke(self, extra, extra1, it) } + setTableValue() + } + + fun ensureExtraCapacity(maxSize: Int): Boolean { + return LuaJNR.INSTANCE.lua_checkstack(pointer, maxSize) } inline fun pushBinding(fn: Binding) { @@ -1061,8 +1206,12 @@ class LuaThread private constructor( } } - fun push(value: String) { - pushStringIntoThread(this, value) + fun push(value: String?) { + if (value == null) { + push() + } else { + pushStringIntoThread(this, value) + } } fun push(value: LuaHandle) { @@ -1074,32 +1223,16 @@ class LuaThread private constructor( } /** - * Allocates a handle for top value in stack, allowing it to be referenced anywhere in engine's code + * Allocates a handle for top value in stack (without popping it), allowing it to be referenced anywhere in engine's code * without directly storing it anywhere in Lua's code - */ - fun createHandle(): LuaHandle { - push() - copy(-2, -1) - moveStackValuesOnto(handleThread.thread) - return handleThread.allocateHandle() - } - - private val namedHandles = HashMap() - - /** - * Same as [createHandle], but makes it permanent (unless manually closed through [LuaHandle.close]) * - * Makes handle available though [pushHandle] method - * - * This is useful for not creating cyclic references going through GC root + * Optionally specified [name] will make handle available permanently (unless manually closed), + * and allows handle to be retrieved in future using [pushNamedHandle] and [getNamedHandle] methods */ - fun createHandle(key: Any): LuaHandle { - require(key !in namedHandles) { "Named handle '$key' already exists" } - val handle = createHandle() - namedHandles[key] = handle - // onClose should be called only on same thread as Lua's, because it is invoked only on LuaHandle#close - handle.onClose { namedHandles.remove(key) } - return handle + fun createHandle(name: Any? = null): LuaHandle { + dup() + moveStackValuesOnto(sharedState.handlesThread) + return sharedState.allocateHandle(name) } /** @@ -1107,12 +1240,23 @@ class LuaThread private constructor( * * @throws NoSuchElementException if no such handle exists */ - fun pushHandle(key: Any): LuaHandle { - val handle = namedHandles[key] ?: throw NoSuchElementException("No such handle: $key") + fun pushNamedHandle(key: Any): LuaHandle { + val handle = getNamedHandle(key) push(handle) return handle } + /** + * @throws NoSuchElementException if no such handle exists + */ + fun getNamedHandle(key: Any): LuaHandle { + return sharedState.getNamedHandle(key) + } + + fun findNamedHandle(key: Any): LuaHandle? { + return sharedState.findNamedHandle(key) + } + fun copy(fromIndex: Int, toIndex: Int) { LuaJNR.INSTANCE.lua_copy(pointer, fromIndex, toIndex) } @@ -1164,6 +1308,11 @@ class LuaThread private constructor( setTableValue(key) { throw NotImplementedError("NYI: $key") } } + @Deprecated("Lua function is a stub") + fun setTableValueToEmpty(key: String) { + setTableValue(key) { 0 } + } + fun setTableValue(key: String, value: Int) { this.push(key) this.push(value.toLong()) @@ -1207,6 +1356,14 @@ class LuaThread private constructor( return setTableValue(key.toLong(), value) } + fun setTableValue(key: Int, value: Int?) { + return setTableValue(key.toLong(), value) + } + + fun setTableValue(key: Int, value: Long?) { + return setTableValue(key.toLong(), value) + } + fun setTableValue(key: Int, value: String) { return setTableValue(key.toLong(), value) } @@ -1229,12 +1386,24 @@ class LuaThread private constructor( return setTableValue(key, value.toLong()) } + fun setTableValue(key: Long, value: Int?) { + return setTableValue(key, value?.toLong()) + } + fun setTableValue(key: Long, value: Long) { this.push(key) this.push(value) this.setTableValue() } + fun setTableValue(key: Long, value: Long?) { + if (value != null) { + this.push(key) + this.push(value) + this.setTableValue() + } + } + fun setTableValue(key: Long, value: String) { this.push(key) this.push(value) @@ -1251,7 +1420,13 @@ class LuaThread private constructor( this.setTableValue() } - fun push(value: JsonElement?) { + fun push(value: JsonElement?, recCounter: Int = 0) { + // TODO: more accurate metric? + // this need to strike balance between safety and speed + if (recCounter != 0 && recCounter and 8 == 0) { + ensureExtraCapacity(9) + } + when (value) { null, JsonNull.INSTANCE -> { this.push() @@ -1281,7 +1456,7 @@ class LuaThread private constructor( for ((i, v) in value.withIndex()) { this.push(i + 1L) - this.push(v) + this.push(v, recCounter + 1) this.setTableValue(index) } @@ -1295,7 +1470,7 @@ class LuaThread private constructor( for ((k, v) in value.entrySet()) { this.push(k) - this.push(v) + this.push(v, recCounter + 1) this.setTableValue(index) } @@ -1317,6 +1492,9 @@ class LuaThread private constructor( private val globalScript by lazy { loadInternalScript("global") } private val sharedBuffers = ThreadLocal() + private val __nils = makeNativeString("__nils") + private val __typehint = makeNativeString("__typehint") + private fun loadStringIntoBuffer(value: String): Long { val bytes = value.toByteArray(Charsets.UTF_8) @@ -1336,9 +1514,6 @@ class LuaThread private constructor( return p } - private val __nils = makeNativeString("__nils") - private val __typehint = makeNativeString("__typehint") - private val sharedStringBufferPtr: Long get() { var p: Long? = sharedBuffers.get() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt index c4641e33..0784f860 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt @@ -28,7 +28,7 @@ fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) { if (self is NPCEntity) provideNPCBindings(self, lua) - provideWorldBindings(self.world, lua) + //provideWorldBindings(self.world, lua) val table = lua.newTable() lua.globals["entity"] = table diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt index e5db29d6..db74f5b7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt @@ -24,6 +24,7 @@ import ru.dbotthepony.kstarbound.lua.nextVector2i import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.luaFunctionN import ru.dbotthepony.kstarbound.lua.luaStub +import ru.dbotthepony.kstarbound.lua.nextInt import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.tableOf @@ -36,7 +37,7 @@ private val LOGGER = LogManager.getLogger() private fun lookup(registry: Registry, args: LuaThread.ArgStack): Registry.Entry? { return when (val type = args.peek()) { - LuaType.NUMBER -> registry[args.nextLong().toInt()] + LuaType.NUMBER -> registry[args.nextInt()] LuaType.STRING -> registry[args.nextString()] LuaType.NONE, LuaType.NIL -> null else -> throw IllegalArgumentException("Invalid registry key type: $type") @@ -46,7 +47,7 @@ private fun lookup(registry: Registry, args: LuaThread.ArgStack): R private fun lookupStrict(registry: Registry, args: LuaThread.ArgStack): Registry.Entry { return when (val type = args.peek()) { LuaType.NUMBER -> { - val key = args.nextLong().toInt() + val key = args.nextInt() registry[key] ?: throw LuaRuntimeException("No such ${registry.name}: $key") } @@ -207,7 +208,7 @@ private fun techConfig(args: LuaThread.ArgStack): Int { private fun createBiome(args: LuaThread.ArgStack): Int { val name = args.nextString() val seed = args.nextLong() - val verticalMidPoint = args.nextLong().toInt() + val verticalMidPoint = args.nextInt() val threatLevel = args.nextDouble() try { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt index 03115f01..4b1680a9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt @@ -1,12 +1,8 @@ package ru.dbotthepony.kstarbound.lua.bindings import org.apache.logging.log4j.LogManager -import org.classdump.luna.ByteString import org.classdump.luna.LuaRuntimeException -import org.classdump.luna.Table -import org.classdump.luna.runtime.AbstractFunction1 -import org.classdump.luna.runtime.ExecutionContext -import org.classdump.luna.runtime.UnresolvedControlThrowable +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.EntityType @@ -15,19 +11,16 @@ import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables import ru.dbotthepony.kstarbound.defs.world.BiomePlaceablesDefinition -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.luaStub -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableOf -import ru.dbotthepony.kstarbound.lua.toAABB -import ru.dbotthepony.kstarbound.lua.toAABBi -import ru.dbotthepony.kstarbound.lua.toByteString +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.LuaType +import ru.dbotthepony.kstarbound.lua.nextAABB +import ru.dbotthepony.kstarbound.lua.nextAABBi +import ru.dbotthepony.kstarbound.lua.nextVector2d +import ru.dbotthepony.kstarbound.lua.nextVector2i +import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.lua.toJsonFromLua -import ru.dbotthepony.kstarbound.lua.toVector2d -import ru.dbotthepony.kstarbound.lua.toVector2i import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture +import ru.dbotthepony.kstarbound.lua.userdata.push import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.staticRandom64 @@ -41,59 +34,25 @@ import java.util.concurrent.TimeUnit private val LOGGER = LogManager.getLogger() -private class LoadUniqueEntityFunction(val self: ServerWorld) : AbstractFunction1() { - override fun invoke(context: ExecutionContext, arg1: ByteString) { - // no, I give up, this shit is beyond bonkers - // I don't care that mods could break - // mods WILL break - // just please, I beg you, for the love of God and Jesus, - // stop fucking designing async functions as sync ones - LOGGER.warn("Lua function world.loadUniqueEntity() has been called. If after this line world stop responding, this is the reason. Called in ${LuaRuntimeException("Calling world.loadUniqueEntity()").errorLocation}") - LOGGER.warn("To modders: Please refer to Lua docs for information on non-busted version of this function, world.loadUniqueEntityAsync()") - - val promise = self.loadUniqueEntity(arg1.decode()) - - if (promise.isDone) { - context.returnBuffer.setTo(promise.get()?.entityID) - } else { - try { - context.pause() - } catch (err: UnresolvedControlThrowable) { - err.resolve(this, promise) - } - } - } - - override fun resume(context: ExecutionContext, suspendedState: Any) { - val promise = suspendedState as CompletableFuture - - if (promise.isDone) { - context.returnBuffer.setTo(promise.get()?.entityID) - } else { - try { - context.pause() - } catch (err: UnresolvedControlThrowable) { - err.resolve(this, promise) - } - } - } -} - -private fun ExecutionContext.placeDungeonImpl(self: ServerWorld, force: Boolean, async: Boolean, name: ByteString, position: Table, dungeonID: Number?, seed: Number?) { - val pos = toVector2i(position) - val actualSeed = seed?.toLong() ?: staticRandom64(pos.x, pos.y, "DungeonPlacement") - val dungeonDef = Registries.dungeons[name.decode()] +private fun placeDungeonImpl(self: ServerWorld, force: Boolean, async: Boolean, args: LuaThread.ArgStack): Int { + val name = args.nextString() + val pos = args.nextVector2i() + val dungeonID = args.nextOptionalLong()?.toInt() + val seed = args.nextOptionalLong() + val actualSeed = seed ?: staticRandom64(pos.x, pos.y, "DungeonPlacement") + val dungeonDef = Registries.dungeons[name] if (dungeonDef == null) { LOGGER.error("Lua script tried to spawn dungeon $name, however, no such dungeon exist") if (async) { - returnBuffer.setTo(LuaFuture( + args.lua.push(LuaFuture( future = CompletableFuture.failedFuture(NoSuchElementException("No such dungeon: $name")), - isLocal = false + isLocal = false, + handler = { 0 } )) } else { - returnBuffer.setTo(false) + args.lua.push(false) } } else { val future = dungeonDef.value.generate( @@ -109,232 +68,329 @@ private fun ExecutionContext.placeDungeonImpl(self: ServerWorld, force: Boolean, scope = if (async) Starbound.GLOBAL_SCOPE else self.eventLoop.scope).thenApply { it.hasGenerated } if (async) { - returnBuffer.setTo(LuaFuture( + args.lua.push(LuaFuture( future = future, - isLocal = false + isLocal = false, + handler = { push(it); 1 } )) } else { - returnBuffer.setTo(future.getNow(true)) + args.lua.push(future.getNow(true)) } } + + return 1 } -fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvironment) { - callbacks["breakObject"] = luaFunction { id: Number, smash: Boolean? -> - val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo(false) +private fun breakObject(self: ServerWorld, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? WorldObject + val smash = args.nextOptionalBoolean() - if (entity.isRemote) { - // we can't break objects now owned by us - returnBuffer.setTo(false) - } else { - if (smash == true) - entity.health = 0.0 + if (entity == null) { + args.lua.push(false) + } else if (entity.isRemote) { + // we can't break objects now owned by us + args.lua.push(false) + } else { + if (smash == true) + entity.health = 0.0 - entity.remove(AbstractEntity.RemovalReason.DYING) - returnBuffer.setTo(true) - } + entity.remove(AbstractEntity.RemovalReason.DYING) + args.lua.push(true) } - callbacks["isVisibleToPlayer"] = luaFunction { region: Table -> - val parse = toAABB(region) - returnBuffer.setTo(self.clients.any { it.isTracking(parse) }) + return 1 +} + +private fun isVisibleToPlayer(self: ServerWorld, args: LuaThread.ArgStack): Int { + val region = args.nextAABB() + args.lua.push(self.clients.any { it.isTracking(region) }) + return 1 +} + +private fun loadRegion(self: ServerWorld, args: LuaThread.ArgStack): Int { + // FIXME: in original engine this supposedly blocks everything else from happening until region is loaded + // And we can't block event loop, because this will make region be unable to be loaded in first place + val region = args.nextAABB() + + // keep in ram for at most 2400 ticks + val tickets = self.temporaryChunkTicket(region, 2400).get() + val future = CompletableFuture.allOf(*tickets.map { it.chunk }.toTypedArray()) + + future.thenApply { + self.eventLoop.schedule(Runnable { + tickets.forEach { it.cancel() } + }, 4L, TimeUnit.SECONDS) + }.exceptionally { + self.eventLoop.schedule(Runnable { + tickets.forEach { it.cancel() } + }, 4L, TimeUnit.SECONDS) } - callbacks["loadRegion"] = luaFunction { region: Table -> - // FIXME: in original engine this supposedly blocks everything else from happening until region is loaded - // And we can't block event loop, because this will make region be unable to be loaded in first place - // god damn it - val parse = toAABB(region) - // keep in ram for at most 2400 ticks - val tickets = self.temporaryChunkTicket(parse, 2400).get() - val future = CompletableFuture.allOf(*tickets.map { it.chunk }.toTypedArray()) + args.lua.push(LuaFuture( + future = future, + isLocal = false, + handler = { 0 } + )) - future.thenApply { - self.eventLoop.schedule(Runnable { - tickets.forEach { it.cancel() } - }, 4L, TimeUnit.SECONDS) - }.exceptionally { - self.eventLoop.schedule(Runnable { - tickets.forEach { it.cancel() } - }, 4L, TimeUnit.SECONDS) - } + return 1 +} - returnBuffer.setTo(LuaFuture( - future = future, - isLocal = false - )) +private fun regionActive(self: ServerWorld, args: LuaThread.ArgStack): Int { + val chunks = self.geometry.region2Chunks(args.nextAABB()) + // TODO: i have no idea what mods expect this to return, so i'll have to guess + args.lua.push(chunks.all { self.chunkMap[it]?.state == ChunkState.FULL }) + return 1 +} + +private fun setTileProtection(self: ServerWorld, args: LuaThread.ArgStack): Int { + self.switchDungeonIDProtection(args.nextInt(), args.nextBoolean()) + return 0 +} + +private fun isPlayerModified(self: ServerWorld, args: LuaThread.ArgStack): Int { + args.lua.push(self.isPlayerModified(args.nextAABBi())) + return 1 +} + +private fun forceDestroyLiquid(self: ServerWorld, args: LuaThread.ArgStack): Int { + val position = args.nextVector2i() + val cell = self.getCell(position).mutable() + + if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) { + args.lua.push(cell.liquid.level.toDouble()) + + cell.liquid.reset() + self.setCell(position, cell.immutable()) + return 1 } - callbacks["regionActive"] = luaFunction { region: Table -> - val chunks = self.geometry.region2Chunks(toAABB(region)) - // TODO: i have no idea what mods expect this to return, so i'll have to guess - returnBuffer.setTo(chunks.all { self.chunkMap[it]?.state == ChunkState.FULL }) + return 0 +} + +private fun loadUniqueEntityAsync(self: ServerWorld, args: LuaThread.ArgStack): Int { + args.lua.push( + LuaFuture( + future = self.loadUniqueEntity(args.nextString()).thenApply { KOptional.ofNullable(it) }, + isLocal = false, + handler = { + push(it.orNull()?.entityID?.toLong()) + 1 + } + ) + ) + + return 1 +} + +private fun setUniqueId(self: ServerWorld, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val name = args.nextString() + val entity = self.entities[id] ?: throw IllegalStateException("No such entity with ID $id (tried to set unique id to $name)") + + if (entity.isRemote) + throw IllegalStateException("Entity $entity is not owned by this side") + + if ( + entity.type == EntityType.NPC || + entity.type == EntityType.STAGEHAND || + entity.type == EntityType.MONSTER || + entity.type == EntityType.OBJECT + ) { + entity.uniqueID.accept(name.sbIntern()) + } else { + throw IllegalStateException("Entity type is restricted from having unique ID: $entity (tried to set unique id to $name)") } - callbacks["setTileProtection"] = luaFunction { id: Number, enable: Boolean -> - self.switchDungeonIDProtection(id.toInt(), enable) + return 0 +} + +private fun takeItemDrop(self: ServerWorld, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? ItemDropEntity ?: return 0 + val takenBy = args.nextOptionalLong()?.toInt() + + if (entity.isLocal && entity.canTake) { + entity.take(takenBy ?: 0).createDescriptor().store(args.lua) + return 1 } - callbacks["isPlayerModified"] = luaFunction { region: Table -> - returnBuffer.setTo(self.isPlayerModified(toAABBi(region))) + return 0 +} + +private fun setPlayerStart(self: ServerWorld, args: LuaThread.ArgStack): Int { + self.setPlayerSpawn(args.nextVector2d(), args.nextOptionalBoolean() == true) + return 0 +} + +private fun players(self: ServerWorld, args: LuaThread.ArgStack): Int { + args.lua.pushTable(self.clients.size) + var i = 1L + + for (p in self.clients) { + val entityID = p.client.playerEntity?.entityID + + if (entityID != null) + args.lua.setTableValue(i++, entityID) } - callbacks["forceDestroyLiquid"] = luaFunction { position: Table -> - val parse = toVector2i(position) - val cell = self.getCell(parse).mutable() + return 1 +} - if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) { - returnBuffer.setTo(cell.liquid.level.toDouble()) +private fun setSkyTime(self: ServerWorld, args: LuaThread.ArgStack): Int { + val time = args.nextDouble() - cell.liquid.reset() - self.setCell(parse, cell.immutable()) - } + if (time < 0.0) + throw LuaRuntimeException("Negative time? $time") + + self.sky.time = time + return 0 +} + +private fun setUniverseFlag(self: ServerWorld, args: LuaThread.ArgStack): Int { + args.lua.push(self.server.addUniverseFlag(args.nextString().sbIntern())) + return 1 +} + +private fun unsetUniverseFlag(self: ServerWorld, args: LuaThread.ArgStack): Int { + args.lua.push(self.server.removeUniverseFlag(args.nextString())) + return 1 +} + +private fun universeFlags(self: ServerWorld, args: LuaThread.ArgStack): Int { + val flags = self.server.getUniverseFlags() + + args.lua.pushTable(flags.size) + + for ((i, flag) in flags.withIndex()) { + args.lua.setTableValue(i + 1L, flag) } - callbacks["loadUniqueEntity"] = LoadUniqueEntityFunction(self) + return 1 +} - callbacks["loadUniqueEntityAsync"] = luaFunction { name: ByteString -> - returnBuffer.setTo(LuaFuture( - future = self.loadUniqueEntity(name.decode()), - isLocal = false - )) +private fun universeFlagSet(self: ServerWorld, args: LuaThread.ArgStack): Int { + args.lua.push(self.server.hasUniverseFlag(args.nextString())) + return 1 +} + +private fun placeDungeon(self: ServerWorld, args: LuaThread.ArgStack): Int { + return placeDungeonImpl(self, force = true, async = false, args) +} + +private fun tryPlaceDungeon(self: ServerWorld, args: LuaThread.ArgStack): Int { + return placeDungeonImpl(self, force = false, async = false, args) +} + +private fun placeDungeonAsync(self: ServerWorld, args: LuaThread.ArgStack): Int { + return placeDungeonImpl(self, force = true, async = true, args) +} + +private fun tryPlaceDungeonAsync(self: ServerWorld, args: LuaThread.ArgStack): Int { + return placeDungeonImpl(self, force = false, async = true, args) +} + +private fun setDungeonGravity(self: ServerWorld, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val peek = args.peek() + + if (peek.isNothing) { + self.setDungeonGravity(id, null) + } else if (peek == LuaType.NUMBER) { + self.setDungeonGravity(id, args.nextDouble()) + } else if (peek == LuaType.TABLE) { + self.setDungeonGravity(id, args.nextVector2d()) + } else { + throw LuaRuntimeException("Illegal gravity argument to setDungeonGravity: $peek") } - callbacks["setUniqueId"] = luaFunction { id: Number, name: ByteString -> - val entity = self.entities[id.toInt()] ?: throw LuaRuntimeException("No such entity with ID $id (tried to set unique id to $name)") + return 0 +} - if (entity.isRemote) - throw LuaRuntimeException("Entity $entity is not owned by this side") +private fun setDungeonBreathable(self: ServerWorld, args: LuaThread.ArgStack): Int { + self.setDungeonBreathable(args.nextInt(), args.nextOptionalBoolean()) + return 0 +} - if ( - entity.type == EntityType.NPC || - entity.type == EntityType.STAGEHAND || - entity.type == EntityType.MONSTER || - entity.type == EntityType.OBJECT - ) { - entity.uniqueID.accept(name.decode().sbIntern()) - } else { - throw LuaRuntimeException("Entity type is restricted from having unique ID: $entity (tried to set unique id to $name)") - } - } +private fun setDungeonId(self: ServerWorld, args: LuaThread.ArgStack): Int { + val region = args.nextAABBi() + val id = args.nextInt() - callbacks["takeItemDrop"] = luaFunction { id: Number, takenBy: Number? -> - val entity = self.entities[id.toInt()] as? ItemDropEntity ?: return@luaFunction returnBuffer.setTo() + for (x in region.mins.x .. region.maxs.x) { + for (y in region.mins.y .. region.maxs.y) { + val cell = self.getCell(x, y) - if (!entity.isRemote && entity.canTake) { - returnBuffer.setTo(entity.take(takenBy?.toInt() ?: 0).toTable(this)) - } - } - - callbacks["setPlayerStart"] = luaFunction { position: Table, respawnInWorld: Boolean? -> - self.setPlayerSpawn(toVector2d(position), respawnInWorld == true) - } - - callbacks["players"] = luaFunction { - returnBuffer.setTo(tableOf(*self.clients.map { it.client.playerID }.toTypedArray())) - } - - callbacks["fidelity"] = luaFunction { - returnBuffer.setTo("high".toByteString()) - } - - callbacks["setSkyTime"] = luaFunction { newTime: Number -> - val cast = newTime.toDouble() - - if (cast < 0.0) - throw LuaRuntimeException("Negative time? $cast") - - self.sky.time = cast - } - - callbacks["setUniverseFlag"] = luaFunction { flag: ByteString -> - returnBuffer.setTo(self.server.addUniverseFlag(flag.decode().sbIntern())) - } - - callbacks["unsetUniverseFlag"] = luaFunction { flag: ByteString -> - returnBuffer.setTo(self.server.removeUniverseFlag(flag.decode())) - } - - callbacks["universeFlags"] = luaFunction { - returnBuffer.setTo(tableOf(*self.server.getUniverseFlags().toTypedArray())) - } - - callbacks["universeFlagSet"] = luaFunction { flag: ByteString -> - returnBuffer.setTo(self.server.hasUniverseFlag(flag.decode())) - } - - callbacks["placeDungeon"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> - placeDungeonImpl(self, true, false, name, position, dungeonID, seed) - } - - callbacks["tryPlaceDungeon"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> - placeDungeonImpl(self, false, false, name, position, dungeonID, seed) - } - - callbacks["placeDungeonAsync"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> - placeDungeonImpl(self, true, true, name, position, dungeonID, seed) - } - - callbacks["tryPlaceDungeonAsync"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> - placeDungeonImpl(self, false, true, name, position, dungeonID, seed) - } - - // terraforming - callbacks["addBiomeRegion"] = luaStub("addBiomeRegion") - callbacks["expandBiomeRegion"] = luaStub("expandBiomeRegion") - callbacks["pregenerateAddBiome"] = luaStub("pregenerateAddBiome") - callbacks["pregenerateExpandBiome"] = luaStub("pregenerateExpandBiome") - callbacks["setLayerEnvironmentBiome"] = luaStub("setLayerEnvironmentBiome") - callbacks["setPlanetType"] = luaStub("setPlanetType") - - callbacks["setDungeonGravity"] = luaFunction { id: Number, gravity: Any? -> - if (gravity == null) { - self.setDungeonGravity(id.toInt(), null) - } else if (gravity is Table) { - self.setDungeonGravity(id.toInt(), toVector2d(gravity)) - } else if (gravity is Number) { - self.setDungeonGravity(id.toInt(), gravity.toDouble()) - } else { - throw LuaRuntimeException("Illegal argument to setDungeonGravity: $gravity") - } - } - - callbacks["setDungeonBreathable"] = luaFunction { id: Number, breathable: Boolean? -> - self.setDungeonBreathable(id.toInt(), breathable) - } - - callbacks["setDungeonId"] = luaFunction { region: Table, id: Number -> - val parse = toAABBi(region) - val actualId = id.toInt() - - for (x in parse.mins.x .. parse.maxs.x) { - for (y in parse.mins.y .. parse.maxs.y) { - val cell = self.getCell(x, y) - - if (cell.dungeonId != actualId) { - self.setCell(x, y, cell.mutable().also { it.dungeonId = actualId }) - } + if (cell.dungeonId != id) { + self.setCell(x, y, cell.mutable().also { it.dungeonId = id }) } } } - callbacks["enqueuePlacement"] = luaFunction { distributions: Table, dungeonId: Number? -> - val items = ArrayList() - - for ((_, v) in distributions) { - // original engine treats distributions table as if it was originating from biome json files - val unprepared = Starbound.gson.fromJson(toJsonFromLua(v), BiomePlaceablesDefinition.DistributionItem::class.java) - val prepared = unprepared.create(BiomeDefinition.CreationParams(hueShift = 0.0, random = lua.random)) - items.add(prepared) - } - - // TODO: Enqueued placements are lost on world shutdown. - // Shouldn't we serialize them to persistent storage? - returnBuffer.setTo( - LuaFuture( - future = self.enqueuePlacement(items, dungeonId?.toInt()), - isLocal = false - ) - ) - } + return 0 +} + +private fun enqueuePlacement(self: ServerWorld, args: LuaThread.ArgStack): Int { + val distributions = args.nextJsonArray() + val dungeonId = args.nextOptionalInt() + val items = ArrayList() + + for (v in distributions) { + // original engine treats distributions table as if it was originating from biome json files + val unprepared = Starbound.gson.fromJson(toJsonFromLua(v), BiomePlaceablesDefinition.DistributionItem::class.java) + val prepared = unprepared.create(BiomeDefinition.CreationParams(hueShift = 0.0, random = args.lua.random)) + items.add(prepared) + } + + // TODO: Enqueued placements are lost on world shutdown. + // Shouldn't we serialize them to persistent storage? + args.lua.push( + LuaFuture( + future = self.enqueuePlacement(items, dungeonId), + isLocal = false, + handler = { push(it); 1 } + ) + ) + + return 1 +} + +private val script by lazy { LuaThread.loadInternalScript("server_world") } + +fun provideServerWorldBindings(self: ServerWorld, lua: LuaThread) { + lua.pushBinding(self, "breakObject", ::breakObject) + lua.pushBinding(self, "isVisibleToPlayer", ::isVisibleToPlayer) + lua.pushBinding(self, "loadRegion", ::loadRegion) + lua.pushBinding(self, "regionActive", ::regionActive) + lua.pushBinding(self, "setTileProtection", ::setTileProtection) + lua.pushBinding(self, "isPlayerModified", ::isPlayerModified) + lua.pushBinding(self, "forceDestroyLiquid", ::forceDestroyLiquid) + lua.pushBinding(self, "loadUniqueEntityAsync", ::loadUniqueEntityAsync) + lua.pushBinding(self, "setUniqueId", ::setUniqueId) + lua.pushBinding(self, "takeItemDrop", ::takeItemDrop) + lua.pushBinding(self, "setPlayerStart", ::setPlayerStart) + lua.pushBinding(self, "players", ::players) + lua.pushBinding(self, "setSkyTime", ::setSkyTime) + lua.pushBinding(self, "setUniverseFlag", ::setUniverseFlag) + lua.pushBinding(self, "unsetUniverseFlag", ::unsetUniverseFlag) + lua.pushBinding(self, "universeFlags", ::universeFlags) + lua.pushBinding(self, "universeFlagSet", ::universeFlagSet) + lua.pushBinding(self, "placeDungeon", ::placeDungeon) + lua.pushBinding(self, "tryPlaceDungeon", ::tryPlaceDungeon) + lua.pushBinding(self, "placeDungeonAsync", ::placeDungeonAsync) + lua.pushBinding(self, "tryPlaceDungeonAsync", ::tryPlaceDungeonAsync) + lua.pushBinding(self, "setDungeonGravity", ::setDungeonGravity) + lua.pushBinding(self, "setDungeonBreathable", ::setDungeonBreathable) + lua.pushBinding(self, "setDungeonId", ::setDungeonId) + lua.pushBinding(self, "enqueuePlacement", ::enqueuePlacement) + + // terraforming + lua.setTableValueToStub("addBiomeRegion") + lua.setTableValueToStub("expandBiomeRegion") + lua.setTableValueToStub("pregenerateAddBiome") + lua.setTableValueToStub("pregenerateExpandBiome") + lua.setTableValueToStub("setLayerEnvironmentBiome") + lua.setTableValueToStub("setPlanetType") + + lua.load(script, "@/internal/server_world.lua") + lua.call() } 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 9e245249..36bfbffb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/UtilityBindings.kt @@ -229,6 +229,26 @@ fun provideUtilityBindings(lua: LuaThread) { } 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() @@ -250,7 +270,7 @@ fun provideUtilityBindings(lua: LuaThread) { lua.setTableValue("staticRandomDoubleRange", ::staticRandomDoubleRange) lua.pushTable() - val randomMeta = lua.createHandle() + val randomMeta = lua.createHandle("RandomGenerator") lua.pushBinding("init", LuaRandom::init) lua.pushBinding("addEntropy", LuaRandom::addEntropy) @@ -273,7 +293,7 @@ fun provideUtilityBindings(lua: LuaThread) { val seed = args.nextOptionalLong() ?: args.lua.random.nextLong() lua.pushTable() lua.push("__index") - lua.push(randomMeta) // cyclic reference through GC root + lua.push(randomMeta) lua.setTableValue() lua.pushObject(LuaRandom(random(seed))) 1 @@ -282,7 +302,7 @@ fun provideUtilityBindings(lua: LuaThread) { lua.setTableValue() lua.pushTable() - val noiseMeta = lua.createHandle() + val noiseMeta = lua.createHandle("PerlinSource") lua.pushBinding("get", LuaPerlinNoise::get) lua.pushBinding("seed", LuaPerlinNoise::seed) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt index aca02d62..34dc2353 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -1,93 +1,334 @@ package ru.dbotthepony.kstarbound.lua.bindings +import com.google.gson.JsonNull import com.google.gson.JsonObject -import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList import kotlinx.coroutines.runBlocking import org.apache.logging.log4j.LogManager -import org.classdump.luna.ByteString -import org.classdump.luna.LuaRuntimeException -import org.classdump.luna.Table -import org.classdump.luna.runtime.ExecutionContext -import org.classdump.luna.runtime.LuaFunction -import ru.dbotthepony.kommons.collect.map -import ru.dbotthepony.kommons.collect.toList import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.ActorMovementParameters -import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.actor.NPCVariant import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor -import ru.dbotthepony.kstarbound.defs.monster.MonsterVariant import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters import ru.dbotthepony.kstarbound.fromJsonFast -import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.mergeJson -import ru.dbotthepony.kstarbound.math.vector.Vector2d -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.contains -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.indexNoYield -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.luaFunctionN -import ru.dbotthepony.kstarbound.lua.luaStub -import ru.dbotthepony.kstarbound.lua.nextOptionalInteger -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableOf -import ru.dbotthepony.kstarbound.lua.toAABB -import ru.dbotthepony.kstarbound.lua.toByteString -import ru.dbotthepony.kstarbound.lua.toJson -import ru.dbotthepony.kstarbound.lua.toJsonFromLua -import ru.dbotthepony.kstarbound.lua.toLine2d -import ru.dbotthepony.kstarbound.lua.toPoly -import ru.dbotthepony.kstarbound.lua.toVector2d -import ru.dbotthepony.kstarbound.lua.toVector2i -import ru.dbotthepony.kstarbound.lua.unpackAsArray +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.LuaType +import ru.dbotthepony.kstarbound.lua.nextAABB +import ru.dbotthepony.kstarbound.lua.nextOptionalVector2d +import ru.dbotthepony.kstarbound.lua.nextPoly +import ru.dbotthepony.kstarbound.lua.nextRegistryID +import ru.dbotthepony.kstarbound.lua.nextVector2d +import ru.dbotthepony.kstarbound.lua.nextVector2i +import ru.dbotthepony.kstarbound.lua.nextVector2iOrAABB +import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture import ru.dbotthepony.kstarbound.lua.userdata.LuaPathFinder -import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.lua.userdata.push import ru.dbotthepony.kstarbound.math.Line2d +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.util.CarriedExecutor import ru.dbotthepony.kstarbound.util.GameTimer +import ru.dbotthepony.kstarbound.util.coalesceNull import ru.dbotthepony.kstarbound.util.random.random -import ru.dbotthepony.kstarbound.util.random.shuffle import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.RayCastResult import ru.dbotthepony.kstarbound.world.RayFilterResult import ru.dbotthepony.kstarbound.world.TileModification import ru.dbotthepony.kstarbound.world.TileRayFilter import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState import ru.dbotthepony.kstarbound.world.castRay -import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity import ru.dbotthepony.kstarbound.world.entities.MonsterEntity import ru.dbotthepony.kstarbound.world.entities.NPCEntity import ru.dbotthepony.kstarbound.world.entities.PathFinder import ru.dbotthepony.kstarbound.world.entities.StagehandEntity -import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.Poly -import java.util.Collections -import java.util.EnumSet -import java.util.function.Predicate -import java.util.stream.Collectors +import java.util.* import kotlin.math.PI private val directionalAngles = intArrayOf(4, 8, 12, 0, 2, 6, 10, 14, 1, 3, 7, 5, 15, 13, 9, 11).let { arr -> Array(arr.size) { Vector2d.angle(arr[it] * PI / 8.0) } } -private fun ExecutionContext.resolvePolyCollision(self: World<*, *>, originalPoly: Poly, position: Vector2d, maximumCorrection: Double, collisions: Set) { +private fun LuaThread.returnLiquid(liquid: AbstractLiquidState, returnNames: Boolean): Int { + if (returnNames) { + pushTable(2) + setTableValue(1, liquid.state.key) + setTableValue(2, liquid.level) + return 1 + } else if (liquid.state.id != null) { + pushTable(2) + setTableValue(1, liquid.state.id) + setTableValue(2, liquid.level) + return 1 + } else { + return 0 + } +} + +private val LOGGER = LogManager.getLogger() + +private fun flyingType(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.sky.flyingType.jsonName) + return 1 +} + +private fun warpPhase(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.sky.warpPhase.jsonName) + return 1 +} + +private fun skyTime(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.sky.time) + return 1 +} + +private fun magnitude(self: World<*, *>, args: LuaThread.ArgStack): Int { + val arg1 = args.nextVector2d() + val arg2 = args.nextOptionalVector2d() + + if (arg2 == null) { + args.lua.push(arg1.length) + } else { + args.lua.push(self.geometry.diff(arg1, arg2).length) + } + + return 1 +} + +private fun distance(self: World<*, *>, args: LuaThread.ArgStack): Int { + val arg1 = args.nextVector2d() + val arg2 = args.nextVector2d() + + args.lua.push(self.geometry.diff(arg1, arg2)) + + return 1 +} + +private fun polyContains(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.geometry.polyContains(args.nextPoly(), args.nextVector2d())) + return 1 +} + +private fun xwrap(self: World<*, *>, args: LuaThread.ArgStack): Int { + val peek = args.peek() + + if (peek == LuaType.NUMBER) + args.lua.push(self.geometry.x.cell(args.nextDouble())) + else + args.lua.push(self.geometry.wrap(args.nextVector2d())) + + return 1 +} + +private fun ywrap(self: World<*, *>, args: LuaThread.ArgStack): Int { + val peek = args.peek() + + if (peek == LuaType.NUMBER) + args.lua.push(self.geometry.y.cell(args.nextDouble())) + else + args.lua.push(self.geometry.wrap(args.nextVector2d())) + + return 1 +} + +private fun nearestTo(self: World<*, *>, args: LuaThread.ArgStack): Int { + val p0 = args.peek() + val p1 = args.peek(args.position + 1) + + require(p0 == p1) { "both arguments to world.nearestTo must be of same type (given: $p0 and $p1)" } + + if (p0 == LuaType.NUMBER) { + args.lua.push(self.geometry.x.nearestTo(args.nextDouble(), args.nextDouble())) + } else { + args.lua.push(self.geometry.nearestTo(args.nextVector2d(), args.nextVector2d())) + } + + return 1 +} + +private fun readCollisionTypes(args: LuaThread.ArgStack): Set { + return when (val type = args.peekAndSkipNothing()) { + LuaType.TABLE -> EnumSet.copyOf(args.readTableValues { CollisionType.entries.valueOf(args.lua.getString(it) ?: throw IllegalArgumentException("collision list contains non-string values")) }) + LuaType.NIL, LuaType.NONE -> CollisionType.SOLID + else -> throw IllegalArgumentException("bad argument #${args.position}: table expected, got $type") + } +} + +private fun rectCollision(self: World<*, *>, args: LuaThread.ArgStack): Int { + val rect = args.nextAABB() + val collisions = readCollisionTypes(args) + args.lua.push(self.chunkMap.collide(rect) { it.type in collisions }.findAny().isPresent) + + return 1 +} + +private fun pointCollision(self: World<*, *>, args: LuaThread.ArgStack): Int { + val point = args.nextVector2d() + val collisions = readCollisionTypes(args) + args.lua.push(self.chunkMap.collide(point) { it.type in collisions }) + + return 1 +} + +private fun pointTileCollision(self: World<*, *>, args: LuaThread.ArgStack): Int { + val cell = self.getCell(args.nextVector2i()) + val collisions = readCollisionTypes(args) + args.lua.push(cell.foreground.material.value.collisionKind in collisions) + + return 1 +} + +private fun castRay(self: World<*, *>, line: Line2d, collisions: Set): RayCastResult { + return self.castRay(line, TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind in collisions) RayFilterResult.HIT else RayFilterResult.SKIP }) +} + +private fun lineTileCollision(self: World<*, *>, args: LuaThread.ArgStack): Int { + val line = Line2d(args.nextVector2d(), args.nextVector2d()) + val collisions = readCollisionTypes(args) + + args.lua.push(castRay(self, line, collisions).traversedTiles.isNotEmpty()) + return 1 +} + +private fun lineTileCollisionPoint(self: World<*, *>, args: LuaThread.ArgStack): Int { + val line = Line2d(args.nextVector2d(), args.nextVector2d()) + val collisions = readCollisionTypes(args) + val result = castRay(self, line, collisions) + + if (result.hitTile == null) { + return 0 + } else { + args.lua.pushTable(2) + args.lua.push(1) + args.lua.push(result.hitTile.borderCross) + args.lua.setTableValue() + args.lua.push(2) + args.lua.push(result.hitTile.normal.normal) + args.lua.setTableValue() + return 1 + } +} + +private fun rectTileCollision(self: World<*, *>, args: LuaThread.ArgStack): Int { + val rect = args.nextAABB() + val collisions = readCollisionTypes(args) + args.lua.push(self.chunkMap.anyCellSatisfies(rect, World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind in collisions })) + + return 1 +} + +private fun lineCollision(self: World<*, *>, args: LuaThread.ArgStack): Int { + val line = Line2d(args.nextVector2d(), args.nextVector2d()) + val collisions = readCollisionTypes(args) + val result = self.chunkMap.collide(line) { it.type in collisions } + + if (result == null) { + return 0 + } else { + args.lua.push(result.border) + args.lua.push(result.normal) + return 2 + } +} + +private fun polyCollision(self: World<*, *>, args: LuaThread.ArgStack): Int { + val poly = args.nextPoly() + val translation = args.nextOptionalVector2d() + val collisions = readCollisionTypes(args) + val result = self.chunkMap.collide(if (translation == null) poly else poly + translation) { it.type in collisions } + + args.lua.push(result.findAny().isPresent) + return 1 +} + +private fun collisionBlocksAlongLine(self: World<*, *>, args: LuaThread.ArgStack): Int { + val line = Line2d(args.nextVector2d(), args.nextVector2d()) + val collisions = readCollisionTypes(args) + var limit = args.nextOptionalLong() ?: Long.MAX_VALUE + + if (limit < 0L) + limit = Long.MAX_VALUE + + val result = self.castRay(line) { cell, fraction, x, y, normal, borderX, borderY -> + if (cell.foreground.material.value.collisionKind in collisions) { + val tlimit = --limit + + if (tlimit > 0L) { + RayFilterResult.CONTINUE + } else if (tlimit == 0L) { + RayFilterResult.HIT + } else { + RayFilterResult.BREAK + } + } else { + RayFilterResult.SKIP + } + } + + args.lua.pushTable(result.traversedTiles.size) + + for ((i, data) in result.traversedTiles.withIndex()) { + args.lua.push(i + 1L) + args.lua.push(data.pos) + args.lua.setTableValue() + } + + return 1 +} + +private fun liquidAlongLine(self: World<*, *>, names: Boolean, args: LuaThread.ArgStack): Int { + var i = 1L + + args.lua.pushTable() + + self.castRay(args.nextVector2d(), args.nextVector2d()) { cell, fraction, x, y, normal, borderX, borderY -> + if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f && (names || cell.liquid.state.id != null)) { + args.lua.push(i++) + args.lua.pushTable(2) + + args.lua.push(1) + args.lua.push(Vector2i(x, y)) + args.lua.setTableValue() + + args.lua.push(2) + args.lua.pushTable(2) + + if (names) + args.lua.setTableValue(1, cell.liquid.state.key) + else + args.lua.setTableValue(1, cell.liquid.state.id!!) + + args.lua.setTableValue(2, cell.liquid.level.toDouble()) + args.lua.setTableValue() + + args.lua.setTableValue() + } + + RayFilterResult.SKIP + } + + return 1 +} + +private fun resolvePolyCollision(self: World<*, *>, args: LuaThread.ArgStack): Int { + val originalPoly = args.nextPoly() + val position = args.nextVector2d() + val maximumCorrection = args.nextDouble() + val collisions = readCollisionTypes(args) val poly = originalPoly + position data class Entry(val poly: Poly, val center: Vector2d, var distance: Double = 0.0) : Comparable { @@ -148,8 +389,8 @@ private fun ExecutionContext.resolvePolyCollision(self: World<*, *>, originalPol // First try any-directional SAT separation for two loops if (trySeparate != null) { - returnBuffer.setTo(from(position + trySeparate)) - return + args.lua.push(position + trySeparate) + return 1 } // Then, try direction-limiting SAT in cardinals, then 45 degs, then in @@ -159,564 +400,549 @@ private fun ExecutionContext.resolvePolyCollision(self: World<*, *>, originalPol trySeparate = separate(4, axis) if (trySeparate != null) { - returnBuffer.setTo(from(position + trySeparate)) - return + args.lua.push(position + trySeparate) + return 1 } } - returnBuffer.setTo() + return 0 } -private fun ExecutionContext.returnLiquid(liquid: AbstractLiquidState, returnNames: Boolean?) { - if (returnNames == true) { - returnBuffer.setTo(tableOf(liquid.state.key, liquid.level)) - } else if (liquid.state.id != null) { - returnBuffer.setTo(tableOf(liquid.state.id, liquid.level)) +private fun tileIsOccupied(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val cell = self.getCell(pos) + val isForeground = args.nextOptionalBoolean() + val includeEphemeral = args.nextOptionalBoolean() == true + + if (cell.tile(isForeground == false).material.isNotEmptyTile) { + args.lua.push(true) + } else { + args.lua.push(self.entityIndex.tileEntitiesAt(pos).any { !it.isEphemeral || includeEphemeral }) + } + + return 1 +} + +private fun placeObject(self: World<*, *>, args: LuaThread.ArgStack): Int { + val type = args.nextString() + val pos = args.nextVector2i() + val objectDirection = args.nextOptionalLong()?.toInt() + var parameters = args.nextOptionalJson() + + if (parameters !is JsonObject) + parameters = JsonObject() + + try { + val prototype = Registries.worldObjects[type] ?: throw IllegalStateException("No such object $type") + var direction = Direction.RIGHT + + if (objectDirection != null && objectDirection.toLong() < 0L) + direction = Direction.LEFT + + val orientation = prototype.value.findValidOrientation(self, pos, direction) + + if (orientation == -1) { + LOGGER.debug("Lua script tried to place object {} at {}, but it can't be placed there!", prototype.key, pos) + args.lua.push(false) + } else { + val create = WorldObject.create(prototype, pos, parameters) + create?.orientationIndex = orientation.toLong() + create?.joinWorld(self) + args.lua.push(create != null) + } + } catch (err: Throwable) { + LOGGER.error("Exception while placing world object $type at $pos", err) + args.lua.push(false) + } + + return 1 +} + +private fun spawnItem(self: World<*, *>, args: LuaThread.ArgStack): Int { + val descriptor: ItemDescriptor + val pos: Vector2d + val peek = args.peek() + + if (peek == LuaType.TABLE) { + descriptor = ItemDescriptor(args) + pos = args.nextVector2d() + args.skip(2) + } else if (peek == LuaType.STRING) { + val name = args.nextString() + pos = args.nextVector2d() + val count = args.nextOptionalLong() ?: 1L + val parameters = args.nextOptionalJson()?.coalesceNull?.asJsonObject ?: JsonObject() + descriptor = ItemDescriptor(name, count, parameters) + } else { + throw IllegalArgumentException("bad argument #1 to spawnItem: ItemDescriptor expected, got $peek") + } + + val initialVelocity = args.nextOptionalVector2d() ?: Vector2d.ZERO + val intangibleTime = args.nextOptionalDouble() ?: 0.0 + + try { + if (descriptor.isEmpty) { + if (LOGGER.isDebugEnabled) + LOGGER.debug("Lua script tried to create non existing item $descriptor at $pos") + + return 0 + } else { + val create = ItemDropEntity(descriptor, args.lua.random) + create.movement.velocity = initialVelocity + create.intangibleTimer = GameTimer(intangibleTime) + + create.joinWorld(self) + args.lua.push(create.entityID.toLong()) + return 1 + } + } catch (err: Throwable) { + LOGGER.error("Exception while creating item $descriptor at $pos", err) + return 0 } } -private val LOGGER = LogManager.getLogger() +private fun spawnTreasure(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2d() + val pool = args.nextString() + val level = args.nextDouble() + val seed = args.nextOptionalLong() -fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { - val callbacks = lua.newTable() - lua.globals["world"] = callbacks + args.lua.pushTable() + var i = 1L - callbacks["flyingType"] = luaFunction { - returnBuffer.setTo(self.sky.flyingType.jsonName) + try { + val items = Registries.treasurePools + .getOrThrow(pool) + .value + .evaluate(if (seed != null) random(seed.toLong()) else args.lua.random, level) + + for (item in items) { + val entity = ItemDropEntity(item) + entity.position = pos + entity.joinWorld(self) + + args.lua.push(i++) + args.lua.push(entity.entityID.toLong()) + args.lua.setTableValue() + } + } catch (err: Throwable) { + LOGGER.error("Exception while spawning treasure from pool '$pool' at $pos", err) } - callbacks["warpPhase"] = luaFunction { - returnBuffer.setTo(self.sky.warpPhase.jsonName) - } + return 1 +} - callbacks["skyTime"] = luaFunction { - returnBuffer.setTo(self.sky.time) - } +private fun spawnMonster(self: World<*, *>, args: LuaThread.ArgStack): Int { + val type = args.nextString() + val position = args.nextVector2d() + val overrides = args.nextOptionalJson() - callbacks["magnitude"] = luaFunction { arg1: Table, arg2: Table? -> - if (arg2 == null) { - returnBuffer.setTo(toVector2d(arg1).length) + try { + val parameters = JsonObject() + parameters["aggressive"] = args.lua.random.nextBoolean() + + if (overrides != null) { + mergeJson(parameters, overrides) + } + + val level = parameters["level"]?.asDouble ?: 1.0 + val seed: Long + + if ("seed" !in parameters || !parameters["seed"].asJsonPrimitive.isNumber) { + seed = args.lua.random.nextLong() } else { - returnBuffer.setTo(self.geometry.diff(toVector2d(arg1), toVector2d(arg2)).length) + seed = parameters["seed"].asLong + } + + val variant = Registries.monsterTypes.getOrThrow(type).value.create(seed, parameters) + val monster = MonsterEntity(variant, level) + monster.position = position + monster.joinWorld(self) + args.lua.push(monster.entityID.toLong()) + return 1 + } catch (err: Throwable) { + LOGGER.error("Exception caught while spawning Monster type $type", err) + } + + return 0 +} + +private fun spawnNpc(self: World<*, *>, args: LuaThread.ArgStack): Int { + val position = args.nextVector2d() + val species = args.nextString() + val type = args.nextString() + val level = args.nextDouble() + val seed = args.nextOptionalLong() ?: args.lua.random.nextLong() + val overrides = args.nextOptionalJson() ?: JsonNull.INSTANCE + + try { + // TODO: this blocks world thread + val npc = runBlocking { + NPCEntity(NPCVariant.create( + Registries.species.getOrThrow(species), + type, + level, + seed, + overrides + )) + } + + npc.position = position + npc.joinWorld(self) + args.lua.push(npc.entityID.toLong()) + return 1 + } catch (err: Throwable) { + LOGGER.error("Exception caught while spawning NPC $type with species $species", err) + } + + return 0 +} + +private fun spawnStagehand(self: World<*, *>, args: LuaThread.ArgStack): Int { + val position = args.nextVector2d() + val type = args.nextString() + val overrides = args.nextOptionalJson() ?: JsonNull.INSTANCE + + try { + val stagehand = StagehandEntity(type, overrides) + stagehand.position = position + stagehand.joinWorld(self) + args.lua.push(stagehand.entityID.toLong()) + return 1 + } catch (err: Throwable) { + LOGGER.error("Exception caught while spawning stagehand of type '$type'", err) + } + + return 0 +} + +private fun threatLevel(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.template.threatLevel) + return 1 +} + +private fun day(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.sky.day.toLong()) + return 1 +} + +private fun timeOfDay(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.sky.timeOfDay) + return 1 +} + +private fun dayLength(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.sky.dayLength) + return 1 +} + +private fun getProperty(self: World<*, *>, args: LuaThread.ArgStack): Int { + val name = args.nextString() + args.lua.push(self.getProperty(name) { args.nextOptionalJson() ?: JsonNull.INSTANCE }) + return 1 +} + +private fun setProperty(self: World<*, *>, args: LuaThread.ArgStack): Int { + val name = args.nextString() + self.setProperty(name, args.nextOptionalJson() ?: JsonNull.INSTANCE) + return 1 +} + +private fun liquidAt(self: World<*, *>, names: Boolean, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2iOrAABB() + + if (pos.isLeft) { + val cell = self.getCell(pos.left()) + + if (cell.liquid.state.isNotEmptyLiquid) { + return args.lua.returnLiquid(cell.liquid, names) + } + } else { + val level = self.averageLiquidLevel(pos.right()) + + if (level != null && (names || level.type.id != null)) { + args.lua.pushTable(2) + + if (names) + args.lua.setTableValue(1, level.type.key) + else + args.lua.setTableValue(1, level.type.id) + + args.lua.setTableValue(2, level.average) + return 1 } } - callbacks["distance"] = luaFunction { arg1: Table, arg2: Table -> - returnBuffer.setTo(from(self.geometry.diff(toVector2d(arg1), toVector2d(arg2)))) - } + return 0 +} - callbacks["polyContains"] = luaFunction { poly: Table, position: Table -> - returnBuffer.setTo(self.geometry.polyContains(toPoly(poly), toVector2d(position))) - } +private fun gravity(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.chunkMap.gravityAt(args.nextVector2d()).y) + return 1 +} - callbacks["xwrap"] = luaFunction { position: Any -> - if (position is Number) { - returnBuffer.setTo(self.geometry.x.cell(position.toDouble())) - } else { - returnBuffer.setTo(from(self.geometry.wrap(toVector2d(position)))) - } - } +private fun gravityVector(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.chunkMap.gravityAt(args.nextVector2d())) + return 1 +} - callbacks["ywrap"] = luaFunction { position: Any -> - if (position is Number) { - returnBuffer.setTo(self.geometry.y.cell(position.toDouble())) - } else { - returnBuffer.setTo(from(self.geometry.wrap(toVector2d(position)))) - } - } +private fun spawnLiquid(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val id = args.nextRegistryID() + val quantity = args.nextDouble() - callbacks["nearestTo"] = luaFunction { pos0: Any, pos1: Any -> - if (pos1 is Number) { - val source = if (pos0 is Number) pos0.toDouble() else toVector2d(pos0).x - returnBuffer.setTo(self.geometry.x.nearestTo(source, pos1.toDouble())) - } else { - val source = if (pos0 is Number) Vector2d(pos0.toDouble()) else toVector2d(pos0) - returnBuffer.setTo(self.geometry.nearestTo(source, toVector2d(pos1))) - } - } + val action = TileModification.Pour(Registries.liquid.ref(id), quantity.toFloat()) + args.lua.push(self.applyTileModifications(listOf(pos to action), false).thenApply { it.isEmpty() }.getNow(true)) + return 1 +} - callbacks["rectCollision"] = luaFunction { rect: Table, collisions: Table? -> - if (collisions == null) { - returnBuffer.setTo(self.chunkMap.collide(toPoly(rect), Predicate { it.type.isSolidCollision }).findAny().isPresent) - } else { - val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - returnBuffer.setTo(self.chunkMap.collide(toPoly(rect), Predicate { it.type in actualCollisions }).findAny().isPresent) - } - } +private fun spawnLiquidPromise(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val id = args.nextRegistryID() + val quantity = args.nextDouble() + val action = TileModification.Pour(Registries.liquid.ref(id), quantity.toFloat()) - callbacks["pointCollision"] = luaFunction { rect: Table, collisions: Table? -> - if (collisions == null) { - returnBuffer.setTo(self.chunkMap.collide(toVector2d(rect), Predicate { it.type.isSolidCollision })) - } else { - val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - returnBuffer.setTo(self.chunkMap.collide(toVector2d(rect), Predicate { it.type in actualCollisions })) - } - } - - callbacks["pointTileCollision"] = luaFunction { rect: Table, collisions: Table? -> - val cell = self.getCell(toVector2i(rect)) - - if (collisions == null) { - returnBuffer.setTo(cell.foreground.material.value.collisionKind.isSolidCollision) - } else { - val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - returnBuffer.setTo(cell.foreground.material.value.collisionKind in actualCollisions) - } - } - - callbacks["lineTileCollision"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? -> - if (collisions == null) { - returnBuffer.setTo(self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind.isSolidCollision) RayFilterResult.HIT else RayFilterResult.SKIP }).traversedTiles.isNotEmpty()) - } else { - val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - returnBuffer.setTo(self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind in actualCollisions) RayFilterResult.HIT else RayFilterResult.SKIP }).traversedTiles.isNotEmpty()) - } - } - - callbacks["lineTileCollisionPoint"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? -> - val result = if (collisions == null) { - self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind.isSolidCollision) RayFilterResult.HIT else RayFilterResult.SKIP }) - } else { - val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind in actualCollisions) RayFilterResult.HIT else RayFilterResult.SKIP }) - } - - if (result.hitTile == null) { - returnBuffer.setTo() - } else { - returnBuffer.setTo(tableOf(from(result.hitTile.borderCross), from(result.hitTile.normal.normal))) - } - } - - callbacks["rectTileCollision"] = luaFunction { rect: Table, collisions: Table? -> - if (collisions == null) { - returnBuffer.setTo(self.chunkMap.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision })) - } else { - val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - val a = self.chunkMap.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind in actualCollisions }) - returnBuffer.setTo(a) - } - } - - callbacks["lineCollision"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? -> - val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - - val result = self.chunkMap.collide(Line2d(toVector2d(pos0), toVector2d(pos1))) { it.type in actualCollisions } - - if (result == null) { - returnBuffer.setTo() - } else { - returnBuffer.setTo(from(result.border), from(result.normal)) - } - } - - callbacks["polyCollision"] = luaFunction { rect: Table, translate: Table?, collisions: Table? -> - if (collisions == null) { - returnBuffer.setTo(self.chunkMap.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type.isSolidCollision }).findAny().isPresent) - } else { - val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - returnBuffer.setTo(self.chunkMap.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type in actualCollisions }).findAny().isPresent) - } - } - - callbacks["collisionBlocksAlongLine"] = luaFunction { pos0: Table, pos1: Table, collisions: Table?, limit: Number? -> - val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - var actualLimit = limit?.toInt() ?: Int.MAX_VALUE - - if (actualLimit < 0) - actualLimit = Int.MAX_VALUE - - val result = self.castRay(toVector2d(pos0), toVector2d(pos1)) { cell, fraction, x, y, normal, borderX, borderY -> - if (cell.foreground.material.value.collisionKind in actualCollisions) { - val tlimit = --actualLimit - - if (tlimit > 0) { - RayFilterResult.CONTINUE - } else if (tlimit == 0) { - RayFilterResult.HIT - } else { - RayFilterResult.BREAK - } - } else { - RayFilterResult.SKIP + args.lua.push( + LuaFuture( + future = self.applyTileModifications(listOf(pos to action), false).thenApply { it.isEmpty() }, + isLocal = false, + handler = { + push(it) + 1 } - } - - returnBuffer.setTo(tableOf(*result.traversedTiles.map { from(it.pos) }.toTypedArray())) - } - - callbacks["liquidAlongLine"] = luaFunction { pos0: Table, pos1: Table -> - val liquid = newTable() - var i = 1L - - self.castRay(toVector2d(pos0), toVector2d(pos1)) { cell, fraction, x, y, normal, borderX, borderY -> - if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f && cell.liquid.state.id != null) { - liquid[i++] = tableOf(tableOf(x, y), tableOf(cell.liquid.state.id, cell.liquid.level.toDouble())) - } - - RayFilterResult.SKIP - } - - returnBuffer.setTo(liquid) - } - - callbacks["liquidNamesAlongLine"] = luaFunction { pos0: Table, pos1: Table -> - val liquid = newTable() - var i = 1L - - self.castRay(toVector2d(pos0), toVector2d(pos1)) { cell, fraction, x, y, normal, borderX, borderY -> - if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) { - liquid[i++] = tableOf(tableOf(x, y), tableOf(cell.liquid.state.key, cell.liquid.level.toDouble())) - } - - RayFilterResult.SKIP - } - - returnBuffer.setTo(liquid) - } - - callbacks["resolvePolyCollision"] = luaFunction { poly: Table, position: Table, maximumCorrection: Number, collisions: Table? -> - val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList()) - resolvePolyCollision(self, toPoly(poly), toVector2d(position), maximumCorrection.toDouble(), actualCollisions) - } - - callbacks["tileIsOccupied"] = luaFunction { pos: Table, isForeground: Boolean?, includeEmphemeral: Boolean? -> - val cell = self.getCell(toVector2i(pos)) - - if (cell.tile(isForeground == false).material.isNotEmptyTile) { - returnBuffer.setTo(true) - } else { - returnBuffer.setTo(self.entityIndex.tileEntitiesAt(toVector2i(pos)).any { !it.isEphemeral || includeEmphemeral == true }) - } - } - - callbacks["placeObject"] = luaFunction { type: ByteString, pos0: Table, objectDirection: Number?, parameters: Table? -> - val pos = toVector2i(pos0) - - try { - val prototype = Registries.worldObjects[type.decode()] ?: throw LuaRuntimeException("No such object $type") - var direction = Direction.RIGHT - - if (objectDirection != null && objectDirection.toLong() < 0L) - direction = Direction.LEFT - - val json = if (parameters == null) JsonObject() else parameters.toJson(true) as JsonObject - val orientation = prototype.value.findValidOrientation(self, pos, direction) - - if (orientation == -1) { - LOGGER.debug("Lua script tried to place object {} at {}, but it can't be placed there!", prototype.key, pos) - returnBuffer.setTo(false) - } else { - val create = WorldObject.create(prototype, pos, json) - create?.orientationIndex = orientation.toLong() - create?.joinWorld(self) - returnBuffer.setTo(create != null) - } - } catch (err: Throwable) { - LOGGER.error("Exception while placing world object $type at $pos", err) - returnBuffer.setTo(false) - } - } - - callbacks["spawnItem"] = luaFunctionN("spawnItem") { - val itemType = toJsonFromLua(it.nextAny()) - val pos = toVector2d(it.nextTable()) - val inputCount = it.nextOptionalInteger() ?: 1L - val inputParameters = toJsonFromLua(it.nextOptionalAny(null)) - val initialVelocity = toVector2d(it.nextOptionalAny(tableOf(0L, 0L))) - val intangibleTime = it.nextOptionalAny(null) - - try { - val descriptor: ItemDescriptor - - if (itemType is JsonObject) { - descriptor = ItemDescriptor(itemType) - } else { - descriptor = ItemDescriptor(itemType.asString, inputCount, if (inputParameters.isJsonNull) JsonObject() else inputParameters.asJsonObject) - } - - if (descriptor.isEmpty) { - LOGGER.debug("Lua script tried to create non existing item {} at {}", itemType, pos) - returnBuffer.setTo() - } else { - val create = ItemDropEntity(descriptor, lua.random) - create.movement.velocity = initialVelocity - - if (intangibleTime is Number) { - create.intangibleTimer = GameTimer(intangibleTime.toDouble()) - } - - create.joinWorld(self) - returnBuffer.setTo(create.entityID) - } - } catch (err: Throwable) { - LOGGER.error("Exception while creating item $itemType at $pos", err) - returnBuffer.setTo() - } - } - - callbacks["spawnTreasure"] = luaFunction { position: Table, pool: ByteString, level: Number, seed: Number? -> - val entities = IntArrayList() - - try { - val items = Registries.treasurePools - .getOrThrow(pool.decode()) - .value - .evaluate(if (seed != null) random(seed.toLong()) else lua.random, level.toDouble()) - - val pos = toVector2d(position) - - for (item in items) { - val entity = ItemDropEntity(item) - entity.position = pos - entity.joinWorld(self) - entities.add(entity.entityID) - } - } catch (err: Throwable) { - LOGGER.error("Exception while spawning treasure from pool '$pool' at $position", err) - } - - returnBuffer.setTo(tableOf(*entities.toTypedArray())) - } - - callbacks["spawnMonster"] = luaFunction { type: ByteString, position: Table, overrides: Any? -> - try { - val parameters = JsonObject() - parameters["aggressive"] = lua.random.nextBoolean() - - if (overrides != null) { - mergeJson(parameters, toJsonFromLua(overrides)) - } - - val level = parameters["level"]?.asDouble ?: 1.0 - val seed: Long - - if ("seed" !in parameters || !parameters["seed"].asJsonPrimitive.isNumber) { - seed = lua.random.nextLong() - } else { - seed = parameters["seed"].asLong - } - - val variant = Registries.monsterTypes.getOrThrow(type.decode()).value.create(seed, parameters) - val monster = MonsterEntity(variant, level) - monster.position = toVector2d(position) - monster.joinWorld(self) - returnBuffer.setTo(monster.entityID) - } catch (err: Throwable) { - LOGGER.error("Exception caught while spawning Monster type $type", err) - } - } - - callbacks["spawnNpc"] = luaFunctionN("spawnNpc") { - val position = it.nextTable() - val species = it.nextString().decode() - val type = it.nextString().decode() - val level = it.nextFloat() - val seed = it.nextOptionalInteger() ?: lua.random.nextLong() - val overrides = toJsonFromLua(it.nextOptionalAny(null)) - - try { - // TODO: this blocks world thread - val npc = runBlocking { - NPCEntity(NPCVariant.create( - Registries.species.getOrThrow(species), - type, - level, - seed, - overrides - )) - } - - npc.position = toVector2d(position) - npc.joinWorld(self) - returnBuffer.setTo(npc.entityID) - } catch (err: Throwable) { - LOGGER.error("Exception caught while spawning NPC $type with species $species", err) - } - } - - callbacks["spawnStagehand"] = luaFunction { position: Table, type: ByteString, overrides: Any? -> - try { - val stagehand = StagehandEntity(type.decode(), toJsonFromLua(overrides)) - stagehand.position = toVector2d(position) - stagehand.joinWorld(self) - returnBuffer.setTo(stagehand.entityID) - } catch (err: Throwable) { - LOGGER.error("Exception caught while spawning stagehand of type '$type'", err) - } - } - - callbacks["spawnProjectile"] = luaStub("spawnProjectile") - callbacks["spawnVehicle"] = luaStub("spawnVehicle") - - callbacks["threatLevel"] = luaFunction { returnBuffer.setTo(self.template.threatLevel) } - callbacks["time"] = luaFunction { returnBuffer.setTo(self.sky.time) } - callbacks["day"] = luaFunction { returnBuffer.setTo(self.sky.day) } - callbacks["timeOfDay"] = luaFunction { returnBuffer.setTo(self.sky.timeOfDay) } - callbacks["dayLength"] = luaFunction { returnBuffer.setTo(self.sky.dayLength) } - - callbacks["getProperty"] = luaFunction { name: ByteString, orElse: Any? -> - returnBuffer.setTo(from(self.getProperty(name.decode()) { toJsonFromLua(orElse) })) - } - - callbacks["setProperty"] = luaFunction { name: ByteString, value: Any? -> - self.setProperty(name.decode(), toJsonFromLua(value)) - } - - callbacks["liquidAt"] = luaFunction { posOrRect: Table -> - if (posOrRect[3L] !is Number) { - val cell = self.getCell(toVector2i(posOrRect)) - - if (cell.liquid.state.isNotEmptyLiquid) { - returnLiquid(cell.liquid, false) - } - } else { - val level = self.averageLiquidLevel(toAABB(posOrRect)) - - if (level != null && level.type.id != null) { - returnBuffer.setTo(tableOf(level.type.id, level.average)) - } - } - } - - callbacks["liquidNameAt"] = luaFunction { posOrRect: Table -> - if (posOrRect[3L] !is Number) { - val cell = self.getCell(toVector2i(posOrRect)) - - if (cell.liquid.state.isNotEmptyLiquid) { - returnLiquid(cell.liquid, true) - } - } else { - val level = self.averageLiquidLevel(toAABB(posOrRect)) - - if (level != null) { - returnBuffer.setTo(tableOf(level.type.key, level.average)) - } - } - } - - callbacks["gravity"] = luaFunction { pos: Table -> - returnBuffer.setTo(self.chunkMap.gravityAt(toVector2d(pos)).y) - } - - callbacks["gravityVector"] = luaFunction { pos: Table -> - returnBuffer.setTo(from(self.chunkMap.gravityAt(toVector2d(pos)))) - } - - callbacks["spawnLiquidPromise"] = luaFunction { pos: Table, liquid: Any, quantity: Number -> - val action = TileModification.Pour(if (liquid is ByteString) Registries.liquid.ref(liquid.decode()) else Registries.liquid.ref((liquid as Number).toInt()), quantity.toFloat()) - - returnBuffer.setTo( - LuaFuture( - future = self.applyTileModifications(listOf(toVector2i(pos) to action), false) - .thenApply { it.isEmpty() }, - isLocal = false - ) ) + ) + + return 1 +} + +private fun destroyLiquid(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val action = TileModification.Pour(BuiltinMetaMaterials.NO_LIQUID.ref, 0f) + val cell = self.getCell(pos) + self.applyTileModifications(listOf(pos to action), false) + + if (cell.liquid.state.isNotEmptyLiquid) { + return args.lua.returnLiquid(cell.liquid, false) } - callbacks["spawnLiquid"] = luaFunction { pos: Table, liquid: Any, quantity: Number -> - val action = TileModification.Pour(if (liquid is ByteString) Registries.liquid.ref(liquid.decode()) else Registries.liquid.ref((liquid as Number).toInt()), quantity.toFloat()) - returnBuffer.setTo(self.applyTileModifications(listOf(toVector2i(pos) to action), false).thenApply { it.isEmpty() }.getNow(true)) - } + return 0 +} - callbacks["destroyLiquid"] = luaFunction { pos: Table -> - val action = TileModification.Pour(BuiltinMetaMaterials.NO_LIQUID.ref, 0f) - val cell = self.getCell(toVector2i(pos)) - self.applyTileModifications(listOf(toVector2i(pos) to action), false) +private fun destroyLiquidPromise(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val action = TileModification.Pour(BuiltinMetaMaterials.NO_LIQUID.ref, 0f) + val cell = self.getCell(pos).immutable() + self.applyTileModifications(listOf(pos to action), false) - if (cell.liquid.state.isNotEmptyLiquid) - returnLiquid(cell.liquid, false) - } + args.lua.push( + LuaFuture( + self.applyTileModifications(listOf(pos to action), false).thenApply { it.isEmpty() }, + false, + { + push(it) + returnLiquid(cell.liquid, true) + 1 + } + ) + ) - callbacks["destroyNamedLiquid"] = luaFunction { pos: Table -> - val action = TileModification.Pour(BuiltinMetaMaterials.NO_LIQUID.ref, 0f) - val cell = self.getCell(toVector2i(pos)) - self.applyTileModifications(listOf(toVector2i(pos) to action), false) + return 1 +} - if (cell.liquid.state.isNotEmptyLiquid) - returnLiquid(cell.liquid, true) - } +private fun isTileProtected(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.isDungeonIDProtected(self.getCell(args.nextVector2i()).dungeonId)) + return 1 +} - callbacks["isTileProtected"] = luaFunction { pos: Table -> returnBuffer.setTo(self.isDungeonIDProtected(self.getCell(toVector2i(pos)).dungeonId)) } +private fun findPlatformerPath(self: World<*, *>, args: LuaThread.ArgStack): Int { + LOGGER.warn("world.findPlatformerPath() was called, this will cause world lag. Consider switching to world.platformerPathStart(), since it is multithreaded in new engine.") - callbacks["findPlatformerPath"] = luaFunction { start: Table, end: Table, actorParams: Table, searchParams: Table -> - LOGGER.warn("world.findPlatformerPath() was called, this will cause world lag. Consider switching to world.platformerPathStart(), since it is multithreaded in new engine.") + val start = args.nextVector2d() + val end = args.nextVector2d() + val actorParams = args.nextJson() + val searchParams = args.nextJson() - val finder = PathFinder( - self, - toVector2d(start), - toVector2d(end), - Starbound.gson.fromJsonFast(toJsonFromLua(actorParams), ActorMovementParameters::class.java), - Starbound.gson.fromJsonFast(toJsonFromLua(searchParams), PathFinder.Parameters::class.java)) + val finder = PathFinder( + self, + start, + end, + Starbound.gson.fromJsonFast(actorParams, ActorMovementParameters::class.java), + Starbound.gson.fromJsonFast(searchParams, PathFinder.Parameters::class.java)) - finder.run(Int.MAX_VALUE) - returnBuffer.setTo(LuaPathFinder.convertPath(this, finder.result.orNull())) - } + finder.run(Int.MAX_VALUE) + return LuaPathFinder.convertPath(args.lua, finder.result.orNull()) +} - val pacer = CarriedExecutor(Starbound.EXECUTOR) +private fun platformerPathStart(self: World<*, *>, pacer: CarriedExecutor, args: LuaThread.ArgStack): Int { + val start = args.nextVector2d() + val end = args.nextVector2d() + val actorParams = args.nextJson() + val searchParams = args.nextJson() - callbacks["platformerPathStart"] = luaFunction { start: Table, end: Table, actorParams: Table, searchParams: Table -> - returnBuffer.setTo(LuaPathFinder(pacer, PathFinder( - self, - toVector2d(start), - toVector2d(end), - Starbound.gson.fromJsonFast(toJsonFromLua(actorParams), ActorMovementParameters::class.java), - Starbound.gson.fromJsonFast(toJsonFromLua(searchParams), PathFinder.Parameters::class.java)))) - } + args.lua.push(LuaPathFinder(pacer, PathFinder( + self, + start, + end, + Starbound.gson.fromJsonFast(actorParams, ActorMovementParameters::class.java), + Starbound.gson.fromJsonFast(searchParams, PathFinder.Parameters::class.java)))) - callbacks["type"] = luaFunction { returnBuffer.setTo(self.template.worldParameters?.typeName.toByteString() ?: "unknown".toByteString()) } - callbacks["size"] = luaFunction { returnBuffer.setTo(from(self.geometry.size)) } - callbacks["inSurfaceLayer"] = luaFunction { pos: Table -> returnBuffer.setTo(self.template.isSurfaceLayer(toVector2i(pos))) } - callbacks["surfaceLevel"] = luaFunction { returnBuffer.setTo(self.template.surfaceLevel) } - callbacks["terrestrial"] = luaFunction { returnBuffer.setTo(self.template.worldParameters is TerrestrialWorldParameters) } + return 1 +} - callbacks["itemDropItem"] = luaFunction { id: Number -> - returnBuffer.setTo(from((self.entities[id.toInt()] as? ItemDropEntity)?.item?.toJson())) - } +private fun type(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.template.worldParameters?.typeName ?: "unknown") + return 1 +} - callbacks["biomeBlocksAt"] = luaFunction { pos: Table, returnNames: Boolean? -> - val info = self.template.cellInfo(toVector2i(pos)) - val blocks = tableOf() - var i = 1L +private fun size(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.geometry.size) + return 1 +} - val biome = info.blockBiome +private fun inSurfaceLayer(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.template.isSurfaceLayer(args.nextVector2i())) + return 1 +} - if (biome != null) { - biome.mainBlock.native.entry?.id?.let { blocks[i++] = it } - biome.subBlocks.forEach { it.native.entry?.id?.let { blocks[i++] = it } } +private fun surfaceLevel(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.template.surfaceLevel.toLong()) + return 1 +} + +private fun terrestrial(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.template.worldParameters is TerrestrialWorldParameters) + return 1 +} + +private fun itemDropItem(self: World<*, *>, args: LuaThread.ArgStack): Int { + val entity = self.entities[args.nextInt()] as? ItemDropEntity ?: return 0 + entity.item.createDescriptor().store(args.lua) + return 1 +} + +private fun biomeBlocksAt(self: World<*, *>, names: Boolean, args: LuaThread.ArgStack): Int { + val info = self.template.cellInfo(args.nextVector2i()) + args.lua.pushTable() + var i = 1L + + val biome = info.blockBiome + + if (biome != null) { + if (names) { + biome.mainBlock.native.entry?.key?.let { args.lua.setTableValue(i++, it) } + biome.subBlocks.forEach { it.native.entry?.key?.let { args.lua.setTableValue(i++, it) } } + } else { + biome.mainBlock.native.entry?.id?.let { args.lua.setTableValue(i++, it) } + biome.subBlocks.forEach { it.native.entry?.id?.let { args.lua.setTableValue(i++, it) } } } - returnBuffer.setTo(blocks) } - callbacks["biomeBlockNamesAt"] = luaFunction { pos: Table, returnNames: Boolean? -> - val info = self.template.cellInfo(toVector2i(pos)) - val blocks = tableOf() - var i = 1L + return 1 +} - val biome = info.blockBiome +private fun dungeonId(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.getCell(args.nextVector2i()).dungeonId.toLong()) + return 1 +} - if (biome != null) { - biome.mainBlock.native.entry?.key?.let { blocks[i++] = it } - biome.subBlocks.forEach { it.native.entry?.key?.let { blocks[i++] = it } } - } +private val worldScript by lazy { LuaThread.loadInternalScript("world") } - returnBuffer.setTo(blocks) - } +fun provideWorldBindings(self: World<*, *>, lua: LuaThread) { + lua.pushTable() + lua.dup() + lua.storeGlobal("world") - callbacks["dungeonId"] = luaFunction { pos: Table -> returnBuffer.setTo(self.getCell(toVector2i(pos)).dungeonId) } + lua.pushBinding(self, "flyingType", ::flyingType) + lua.pushBinding(self, "warpPhase", ::warpPhase) + lua.pushBinding(self, "skyTime", ::skyTime) + lua.pushBinding(self, "time", ::skyTime) - provideWorldEntitiesBindings(self, callbacks, lua) - provideWorldEnvironmentalBindings(self, callbacks, lua) + lua.pushBinding(self, "magnitude", ::magnitude) + lua.pushBinding(self, "distance", ::distance) + + lua.pushBinding(self, "polyContains", ::polyContains) + + lua.pushBinding(self, "xwrap", ::xwrap) + lua.pushBinding(self, "ywrap", ::ywrap) + lua.pushBinding(self, "nearestTo", ::nearestTo) + + lua.pushBinding(self, "rectCollision", ::rectCollision) + lua.pushBinding(self, "polyCollision", ::polyCollision) + lua.pushBinding(self, "pointCollision", ::pointCollision) + lua.pushBinding(self, "pointTileCollision", ::pointTileCollision) + lua.pushBinding(self, "lineTileCollision", ::lineTileCollision) + lua.pushBinding(self, "lineTileCollisionPoint", ::lineTileCollisionPoint) + lua.pushBinding(self, "rectTileCollision", ::rectTileCollision) + lua.pushBinding(self, "lineCollision", ::lineCollision) + + lua.pushBinding(self, "collisionBlocksAlongLine", ::collisionBlocksAlongLine) + lua.pushBinding(self, false, "liquidAlongLine", ::liquidAlongLine) + lua.pushBinding(self, true, "liquidNamesAlongLine", ::liquidAlongLine) + + lua.pushBinding(self, "resolvePolyCollision", ::resolvePolyCollision) + lua.pushBinding(self, "tileIsOccupied", ::tileIsOccupied) + lua.pushBinding(self, "placeObject", ::placeObject) + lua.pushBinding(self, "spawnItem", ::spawnItem) + lua.pushBinding(self, "spawnTreasure", ::spawnTreasure) + lua.pushBinding(self, "spawnMonster", ::spawnMonster) + lua.pushBinding(self, "spawnNpc", ::spawnNpc) + lua.pushBinding(self, "spawnStagehand", ::spawnStagehand) + + lua.setTableValueToStub("spawnProjectile") + lua.setTableValueToStub("spawnVehicle") + + lua.pushBinding(self, "threatLevel", ::threatLevel) + lua.pushBinding(self, "day", ::day) + lua.pushBinding(self, "timeOfDay", ::timeOfDay) + lua.pushBinding(self, "dayLength", ::dayLength) + + lua.pushBinding(self, "getProperty", ::getProperty) + lua.pushBinding(self, "setProperty", ::setProperty) + + lua.pushBinding(self, false, "liquidAt", ::liquidAt) + lua.pushBinding(self, true, "liquidNameAt", ::liquidAt) + + lua.pushBinding(self, "gravity", ::gravity) + lua.pushBinding(self, "gravityVector", ::gravityVector) + + lua.pushBinding(self, "spawnLiquid", ::spawnLiquid) + lua.pushBinding(self, "spawnLiquidPromise", ::spawnLiquidPromise) + + lua.pushBinding(self, "destroyLiquid", ::destroyLiquid) + lua.pushBinding(self, "destroyLiquidPromise", ::destroyLiquidPromise) + + lua.pushBinding(self, "isTileProtected", ::isTileProtected) + + lua.pushBinding(self, "findPlatformerPath", ::findPlatformerPath) + lua.pushBinding(self, CarriedExecutor(Starbound.EXECUTOR), "platformerPathStart", ::platformerPathStart) + + lua.pushBinding(self, "type", ::type) + lua.pushBinding(self, "size", ::size) + lua.pushBinding(self, "inSurfaceLayer", ::inSurfaceLayer) + lua.pushBinding(self, "surfaceLevel", ::surfaceLevel) + lua.pushBinding(self, "terrestrial", ::terrestrial) + lua.pushBinding(self, "itemDropItem", ::itemDropItem) + + lua.pushBinding(self, false, "biomeBlocksAt", ::biomeBlocksAt) + lua.pushBinding(self, true, "biomeBlockNamesAt", ::biomeBlocksAt) + + lua.pushBinding(self, "dungeonId", ::dungeonId) + + lua.setTableValueToEmpty("debugPoint") + lua.setTableValueToEmpty("debugLine") + lua.setTableValueToEmpty("debugPoly") + lua.setTableValueToEmpty("debugText") + + provideWorldEnvironmentalBindings(self, lua) + provideWorldEntitiesBindings(self, lua) if (self is ServerWorld) { - provideServerWorldBindings(self, callbacks, lua) + provideServerWorldBindings(self, lua) } - // TODO - callbacks["debugPoint"] = luaFunction { } - callbacks["debugLine"] = luaFunction { } - callbacks["debugPoly"] = luaFunction { } - callbacks["debugText"] = luaFunction { } + lua.pop() + + lua.load(worldScript, "@/internal/world.lua") + lua.call() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt index fd1e1c42..e3bd91a1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt @@ -1,39 +1,28 @@ package ru.dbotthepony.kstarbound.lua.bindings -import org.classdump.luna.ByteString -import org.classdump.luna.LuaRuntimeException -import org.classdump.luna.Table -import org.classdump.luna.runtime.ExecutionContext -import ru.dbotthepony.kommons.gson.JsonArrayCollector +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.`object`.LoungeOrientation +import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.json.builder.IStringSerializable -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.indexNoYield -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.luaFunctionN -import ru.dbotthepony.kstarbound.lua.luaStub -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableMapOf -import ru.dbotthepony.kstarbound.lua.tableOf -import ru.dbotthepony.kstarbound.lua.toAABB -import ru.dbotthepony.kstarbound.lua.toByteString -import ru.dbotthepony.kstarbound.lua.toJsonFromLua -import ru.dbotthepony.kstarbound.lua.toLine2d -import ru.dbotthepony.kstarbound.lua.toPoly -import ru.dbotthepony.kstarbound.lua.toVector2d -import ru.dbotthepony.kstarbound.lua.toVector2i -import ru.dbotthepony.kstarbound.lua.unpackAsArray +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.LuaType +import ru.dbotthepony.kstarbound.lua.nextAABB +import ru.dbotthepony.kstarbound.lua.nextInt +import ru.dbotthepony.kstarbound.lua.nextPoly +import ru.dbotthepony.kstarbound.lua.nextVector2d +import ru.dbotthepony.kstarbound.lua.nextVector2i +import ru.dbotthepony.kstarbound.lua.push +import ru.dbotthepony.kstarbound.lua.setTableValue import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture +import ru.dbotthepony.kstarbound.lua.userdata.push import ru.dbotthepony.kstarbound.math.AABB -import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.network.Connection -import ru.dbotthepony.kstarbound.stream import ru.dbotthepony.kstarbound.util.random.shuffle import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.world.World @@ -42,6 +31,9 @@ import ru.dbotthepony.kstarbound.world.entities.ActorEntity import ru.dbotthepony.kstarbound.world.entities.DynamicEntity import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity +import ru.dbotthepony.kstarbound.world.entities.MonsterEntity +import ru.dbotthepony.kstarbound.world.entities.NPCEntity +import ru.dbotthepony.kstarbound.world.entities.StagehandEntity import ru.dbotthepony.kstarbound.world.entities.api.InspectableEntity import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity @@ -51,581 +43,1114 @@ import ru.dbotthepony.kstarbound.world.entities.tile.LoungeableObject import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.physics.Poly import java.util.* +import java.util.concurrent.CompletableFuture import java.util.function.Predicate -private enum class EntityBoundMode(override val jsonName: String) : IStringSerializable { - META_BOUNDING_BOX("MetaBoundBox"), - COLLISION_AREA("CollisionArea"), - POSITION("Position") +private enum class EntityBoundMode { + META_BOUNDING_BOX, + COLLISION_AREA, + POSITION; } -private val callScriptStr = ByteString.of("callScript") -private val callScriptArgsStr = ByteString.of("callScriptArgs") -private val callScriptResultStr = ByteString.of("callScriptResult") -private val lineStr = ByteString.of("line") -private val polyStr = ByteString.of("poly") -private val rectStr = ByteString.of("rect") -private val withoutEntityIdStr = ByteString.of("withoutEntityId") -private val includedTypesStr = ByteString.of("includedTypes") -private val radiusStr = ByteString.of("radius") -private val centerStr = ByteString.of("center") -private val boundModeStr = ByteString.of("boundMode") -private val orderStr = ByteString.of("order") +private enum class Order { + WHATEVER, RANDOM, NEAREST +} -private fun ExecutionContext.entityQueryImpl(self: World<*, *>, lua: LuaEnvironment, options: Table, predicate: Predicate = Predicate { true }): Table { - val withoutEntityId = (indexNoYield(options, withoutEntityIdStr) as Number?)?.toInt() - - val includedTypes = EnumSet.allOf(EntityType::class.java) - val getIncludedTypes = indexNoYield(options, includedTypesStr) as Table? - - if (getIncludedTypes != null) { - includedTypes.clear() - - for ((_, v) in getIncludedTypes) { - when (val t = (v as ByteString).decode()) { - "mobile" -> { - includedTypes.add(EntityType.PLAYER) - includedTypes.add(EntityType.MONSTER) - includedTypes.add(EntityType.NPC) - includedTypes.add(EntityType.PROJECTILE) - includedTypes.add(EntityType.ITEM_DROP) - includedTypes.add(EntityType.VEHICLE) - } - - "creature" -> { - includedTypes.add(EntityType.PLAYER) - includedTypes.add(EntityType.MONSTER) - includedTypes.add(EntityType.NPC) - } - - else -> { - includedTypes.add(EntityType.entries.valueOf(t)) - } - } - } - } - - val callScript = (indexNoYield(options, callScriptStr) as ByteString?)?.decode() - val callScriptArgs = (indexNoYield(options, callScriptArgsStr) as Table?)?.unpackAsArray() ?: arrayOf() - val callScriptResult = indexNoYield(options, callScriptResultStr) ?: true - - val lineQuery = (indexNoYield(options, lineStr) as Table?)?.let { toLine2d(it) } - val polyQuery = (indexNoYield(options, polyStr) as Table?)?.let { toPoly(it) } - val rectQuery = (indexNoYield(options, rectStr) as Table?)?.let { toAABB(it) } - - val radius = indexNoYield(options, radiusStr) - - val radiusQuery = if (radius is Number) { - val center = toVector2d(indexNoYield(options, centerStr) ?: throw LuaRuntimeException("Specified 'radius', but not 'center'")) - center to radius.toDouble() +private fun LuaThread.ArgStack.getTypes(): Set { + if (peek().isNothing) { + return EntityType.ALL } else { - null - } + val types = EnumSet.noneOf(EntityType::class.java) - val boundMode = EntityBoundMode.entries.valueOf((indexNoYield(options, boundModeStr) as ByteString?)?.decode() ?: "CollisionArea") - - val innerPredicate = Predicate { - if (!predicate.test(it)) return@Predicate false - if (it.type !in includedTypes) return@Predicate false - if (it.entityID == withoutEntityId) return@Predicate false - - if (callScript != null) { - if (it !is ScriptedEntity || it.isRemote) return@Predicate false - val call = it.callScript(callScript, *callScriptArgs) - if (call.isEmpty() || call[0] != callScriptResult) return@Predicate false - } - - when (boundMode) { - EntityBoundMode.META_BOUNDING_BOX -> { - // If using MetaBoundBox, the regular line / box query methods already - // enforce collision with MetaBoundBox - - if (radiusQuery != null) { - return@Predicate self.geometry.rectIntersectsCircle(it.metaBoundingBox, radiusQuery.first, radiusQuery.second) + iterateTable(keyVisitor = {}, valueVisitor = { + when (val type = getLong(it)!!.toInt()) { + // mobile + 10 -> { + types.add(EntityType.PLAYER) + types.add(EntityType.MONSTER) + types.add(EntityType.NPC) + types.add(EntityType.PROJECTILE) + types.add(EntityType.ITEM_DROP) + types.add(EntityType.VEHICLE) } + + // creature + 11 -> { + types.add(EntityType.PLAYER) + types.add(EntityType.MONSTER) + types.add(EntityType.NPC) + } + + // specific + else -> types.add(EntityType.entries[type]) } + }) - EntityBoundMode.COLLISION_AREA -> { - // Collision area queries either query based on the collision area if - // that's given, or as a fallback the regular bound box. - - var collisionArea = it.collisionArea - - if (collisionArea.isEmpty) - collisionArea = it.metaBoundingBox - - if (lineQuery != null) - return@Predicate self.geometry.lineIntersectsRect(lineQuery, collisionArea) - - if (polyQuery != null) - return@Predicate self.geometry.polyIntersectsPoly(polyQuery, Poly(collisionArea)) - - if (rectQuery != null) - return@Predicate self.geometry.rectIntersectsRect(rectQuery, collisionArea) - - if (radiusQuery != null) - return@Predicate self.geometry.rectIntersectsCircle(collisionArea, radiusQuery.first, radiusQuery.second) - } - - EntityBoundMode.POSITION -> { - if (lineQuery != null) - return@Predicate self.geometry.lineIntersectsRect(lineQuery, AABB.rectangle(it.position, 0.0)) - - if (polyQuery != null) - return@Predicate self.geometry.polyContains(polyQuery, it.position) - - if (rectQuery != null) - return@Predicate self.geometry.rectContains(rectQuery, it.position) - - if (radiusQuery != null) - return@Predicate self.geometry.diff(radiusQuery.first, it.position).length <= radiusQuery.second - } - } - - true + return types } +} - val entitites = if (lineQuery != null) { - // TODO: this is wildly inefficient - self.entityIndex.query(AABB(lineQuery), innerPredicate) - } else if (polyQuery != null) { - self.entityIndex.query(polyQuery.aabb, innerPredicate) - } else if (rectQuery != null) { - self.entityIndex.query(rectQuery, innerPredicate) - } else if (radiusQuery != null) { - self.entityIndex.query(AABB.withSide(radiusQuery.first, radiusQuery.second), innerPredicate) +private data class CallScriptData( + val name: String, + val arguments: JsonArray, + val expectedResult: JsonElement +) + +private fun LuaThread.ArgStack.getScriptData(): CallScriptData? { + if (peek() == LuaType.STRING) { + return CallScriptData(nextString(), nextJson().asJsonArray, nextJson()) } else { - mutableListOf() + skip(3) + return null } - - when (val order = (indexNoYield(options, orderStr) as ByteString?)?.decode()?.lowercase()) { - null -> {} // do nothing - "random" -> entitites.shuffle(lua.random) - "nearest" -> { - val nearestPosition = lineQuery?.p0 ?: polyQuery?.centre ?: rectQuery?.centre ?: radiusQuery?.first ?: Vector2d.ZERO - - entitites.sortWith { o1, o2 -> - self.geometry.diff(o1.position, nearestPosition).lengthSquared.compareTo(self.geometry.diff(o2.position, nearestPosition).lengthSquared) } - } - else -> throw LuaRuntimeException("Unknown entity sort order $order!") - } - - return tableOf(*entitites.map { it.entityID.toLong() }.toTypedArray()) } -private fun ExecutionContext.intermediateQueryFunction(self: World<*, *>, lua: LuaEnvironment, pos1: Table, pos2OrRadius: Any, options: Table?, predicate: Predicate) { - val actualOptions = options ?: tableOf() +private fun AbstractEntity.genericCheck( + predicate: Predicate, + scriptData: CallScriptData?, + withoutEntityId: Int?, + includeTypes: Set +): Boolean { + if (!predicate.test(this)) return false + if (entityID == withoutEntityId) return false + if (type !in includeTypes) return false - if (pos2OrRadius is Number) { - actualOptions[centerStr] = pos1 - actualOptions[radiusStr] = pos2OrRadius + if (scriptData != null) { + if (isRemote || this !is ScriptedEntity) return false + val results = callScript(scriptData.name, scriptData.arguments) + if (results != scriptData.expectedResult) return false + } + + return true +} + +private data class GenericQueryData( + val includeTypes: Set, + val withoutEntityId: Int?, + val boundMode: EntityBoundMode, + val scriptData: CallScriptData?, + val order: Order +) + +private fun genericQueryData(args: LuaThread.ArgStack): GenericQueryData { + val includeTypes = args.getTypes() + val withoutEntityId = args.nextOptionalLong()?.toInt() + val boundMode = EntityBoundMode.entries[args.nextInt()] + val scriptData = args.getScriptData() + val order = Order.entries[args.nextInt()] + + return GenericQueryData(includeTypes, withoutEntityId, boundMode, scriptData, order) +} + +private fun LuaThread.returnQueryResult(results: List): Int { + pushTable(results.size) + + for ((i, entity) in results.withIndex()) { + setTableValue(i + 1L, entity.entityID.toLong()) + } + + return 1 +} + +private fun queryAABBImpl(self: World<*, *>, predicate: Predicate, args: LuaThread.ArgStack): Int { + val aabb = args.nextAABB() + val (includeTypes, withoutEntityId, boundMode, scriptData, order) = genericQueryData(args) + + val innerPredicate: Predicate = when (boundMode) { + EntityBoundMode.META_BOUNDING_BOX -> Predicate p@{ + it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes) + } + + EntityBoundMode.COLLISION_AREA -> Predicate p@{ + if (!it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes)) return@p false + val collisionArea = it.collisionArea + return@p collisionArea.isEmpty || self.geometry.rectIntersectsRect(aabb, collisionArea) + } + + EntityBoundMode.POSITION -> Predicate p@{ + if (!it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes)) return@p false + return@p self.geometry.rectContains(aabb, it.position) + } + } + + val entities = self.entityIndex.query(aabb, innerPredicate) + + when (order) { + Order.WHATEVER -> {} // as-is + Order.RANDOM -> entities.shuffle(args.lua.random) + Order.NEAREST -> { + val nearest = aabb.centre + entities.sortWith { o1, o2 -> self.geometry.diff(o1.position, nearest).lengthSquared.compareTo(self.geometry.diff(o2.position, nearest).lengthSquared) } + } + } + + return args.lua.returnQueryResult(entities) +} + +private fun queryRadiusImpl(self: World<*, *>, predicate: Predicate, args: LuaThread.ArgStack): Int { + val point = args.nextVector2d() + val radius = args.nextDouble() + val (includeTypes, withoutEntityId, boundMode, scriptData, order) = genericQueryData(args) + + val innerPredicate: Predicate = when (boundMode) { + EntityBoundMode.META_BOUNDING_BOX -> Predicate p@{ + it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes) && self.geometry.rectIntersectsCircle(it.metaBoundingBox, point, radius) + } + + EntityBoundMode.COLLISION_AREA -> Predicate p@{ + if (!it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes)) return@p false + var collisionArea = it.collisionArea + + if (collisionArea.isEmpty) + collisionArea = it.metaBoundingBox + + return@p self.geometry.rectIntersectsCircle(collisionArea, point, radius) + } + + EntityBoundMode.POSITION -> Predicate p@{ + if (!it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes)) return@p false + return@p self.geometry.diff(point, it.position).length <= radius + } + } + + val entities = self.entityIndex.query(AABB.withSide(point, radius), innerPredicate) + + when (order) { + Order.WHATEVER -> {} // as-is + Order.RANDOM -> entities.shuffle(args.lua.random) + Order.NEAREST -> { + entities.sortWith { o1, o2 -> self.geometry.diff(o1.position, point).lengthSquared.compareTo(self.geometry.diff(o2.position, point).lengthSquared) } + } + } + + return args.lua.returnQueryResult(entities) +} + +private fun queryLineImpl(self: World<*, *>, predicate: Predicate, args: LuaThread.ArgStack): Int { + val line = Line2d(args.nextVector2d(), args.nextVector2d()) + val (includeTypes, withoutEntityId, boundMode, scriptData, order) = genericQueryData(args) + + val innerPredicate: Predicate = when (boundMode) { + EntityBoundMode.META_BOUNDING_BOX -> Predicate p@{ + it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes) + } + + EntityBoundMode.COLLISION_AREA -> Predicate p@{ + if (!it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes)) return@p false + var collisionArea = it.collisionArea + + if (collisionArea.isEmpty) + collisionArea = it.metaBoundingBox + + return@p self.geometry.lineIntersectsRect(line, collisionArea) + } + + EntityBoundMode.POSITION -> Predicate p@{ + if (!it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes)) return@p false + return@p self.geometry.lineIntersectsRect(line, AABB.withSide(it.position, 0.0)) + } + } + + val entities = self.entityIndex.query(line, innerPredicate) + + when (order) { + Order.WHATEVER -> {} // as-is + Order.RANDOM -> entities.shuffle(args.lua.random) + Order.NEAREST -> { + // TODO + entities.sortWith { o1, o2 -> self.geometry.diff(o1.position, line.p0).lengthSquared.compareTo(self.geometry.diff(o2.position, line.p0).lengthSquared) } + } + } + + return args.lua.returnQueryResult(entities) +} + +private fun queryPolyImpl(self: World<*, *>, predicate: Predicate, args: LuaThread.ArgStack): Int { + val poly = args.nextPoly() + val (includeTypes, withoutEntityId, boundMode, scriptData, order) = genericQueryData(args) + + val innerPredicate: Predicate = when (boundMode) { + EntityBoundMode.META_BOUNDING_BOX -> Predicate p@{ + it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes) + } + + EntityBoundMode.COLLISION_AREA -> Predicate p@{ + if (!it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes)) return@p false + var collisionArea = it.collisionArea + + if (collisionArea.isEmpty) + collisionArea = it.metaBoundingBox + + return@p self.geometry.polyIntersectsPoly(poly, Poly(collisionArea)) + } + + EntityBoundMode.POSITION -> Predicate p@{ + if (!it.genericCheck(predicate, scriptData, withoutEntityId, includeTypes)) return@p false + return@p self.geometry.polyContains(poly, it.position) + } + } + + val entities = self.entityIndex.query(poly.aabb, innerPredicate) + + when (order) { + Order.WHATEVER -> {} // as-is + Order.RANDOM -> entities.shuffle(args.lua.random) + Order.NEAREST -> { + val nearest = poly.centre + entities.sortWith { o1, o2 -> self.geometry.diff(o1.position, nearest).lengthSquared.compareTo(self.geometry.diff(o2.position, nearest).lengthSquared) } + } + } + + return args.lua.returnQueryResult(entities) +} + +private fun queryObjectAABBImpl(self: World<*, *>, args: LuaThread.ArgStack): Int { + val name = args.nextOptionalString() + return queryAABBImpl(self, Predicate { it is WorldObject && (name == null || it.config.key == name) }, args) +} + +private fun queryObjectLineImpl(self: World<*, *>, args: LuaThread.ArgStack): Int { + val name = args.nextOptionalString() + return queryLineImpl(self, Predicate { it is WorldObject && (name == null || it.config.key == name) }, args) +} + +private fun queryObjectRadiusImpl(self: World<*, *>, args: LuaThread.ArgStack): Int { + val name = args.nextOptionalString() + return queryRadiusImpl(self, Predicate { it is WorldObject && (name == null || it.config.key == name) }, args) +} + +private fun queryObjectPolyImpl(self: World<*, *>, args: LuaThread.ArgStack): Int { + val name = args.nextOptionalString() + return queryRadiusImpl(self, Predicate { it is WorldObject && (name == null || it.config.key == name) }, args) +} + +private fun queryLoungeableAABBImpl(self: World<*, *>, args: LuaThread.ArgStack): Int { + val orientation = LoungeOrientation.entries[args.nextInt()] + return queryAABBImpl(self, Predicate { it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation) }, args) +} + +private fun queryLoungeableLineImpl(self: World<*, *>, args: LuaThread.ArgStack): Int { + val orientation = LoungeOrientation.entries[args.nextInt()] + return queryLineImpl(self, Predicate { it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation) }, args) +} + +private fun queryLoungeableRadiusImpl(self: World<*, *>, args: LuaThread.ArgStack): Int { + val orientation = LoungeOrientation.entries[args.nextInt()] + return queryRadiusImpl(self, Predicate { it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation) }, args) +} + +private fun queryLoungeablePolyImpl(self: World<*, *>, args: LuaThread.ArgStack): Int { + val orientation = LoungeOrientation.entries[args.nextInt()] + return queryRadiusImpl(self, Predicate { it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation) }, args) +} + +private fun objectAt(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.entityIndex.tileEntityAt(args.nextVector2i())?.entityID?.toLong()) + return 1 +} + +private fun entityExists(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.entities.containsKey(args.nextInt())) + return 1 +} + +private fun entityCanDamage(self: World<*, *>, args: LuaThread.ArgStack): Int { + val a = self.entities[args.nextInt()] + val b = self.entities[args.nextInt()] + + args.lua.push(a != null && b != null && a.team.get().canDamage(b.team.get(), a == b)) + return 1 +} + +private fun entityDamageTeam(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] ?: return 0 + args.lua.push(Starbound.gson.toJsonTree(entity.team.get())) + return 1 +} + +private fun entityType(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] ?: return 0 + args.lua.push(entity.type.jsonName) + return 1 +} + +private fun entityPosition(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] ?: return 0 + args.lua.push(entity.position) + return 1 +} + +private fun entityVelocity(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? DynamicEntity ?: return 0 + args.lua.push(entity.movement.velocity) + return 1 +} + +private fun entityMetaBoundBox(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] ?: return 0 + args.lua.push(entity.metaBoundingBox) + return 1 +} + +private fun entityCurrency(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val currency = args.nextString() + val entity = self.entities[id] as? PlayerEntity ?: return 0 + args.lua.push(entity.inventory.currencies[currency]) + return 1 +} + +private fun entityHasCountOfItem(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val desc = ItemDescriptor(args) + val exactMatch = args.nextOptionalBoolean() ?: false + val entity = self.entities[id] as? PlayerEntity ?: return 0 + args.lua.push(entity.inventory.hasCountOfItem(desc, exactMatch)) + return 1 +} + +private fun entityHealth(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? ActorEntity ?: return 0 + args.lua.pushTable(2) + args.lua.setTableValue(1L, entity.health) + args.lua.setTableValue(2L, entity.maxHealth) + return 1 +} + +private fun entitySpecies(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? HumanoidActorEntity ?: return 0 + args.lua.push(entity.humanoidIdentity.species.key.left()) + return 1 +} + +private fun entityGender(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? HumanoidActorEntity ?: return 0 + args.lua.push(entity.humanoidIdentity.gender.jsonName) + return 1 +} + +private fun entityName(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] ?: return 0 + + // TODO + when (entity) { + is ActorEntity -> args.lua.push(entity.name) + is WorldObject -> args.lua.push(entity.config.key) + else -> return 0 + } + + return 1 +} + +private fun entityDescription(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] ?: return 0 + + if (entity is InspectableEntity) { + val species = args.nextOptionalString() + args.lua.push(entity.inspectionDescription(species)) } else { - pos2OrRadius as Table - actualOptions[rectStr] = tableOf(pos1[1L], pos1[2L], pos2OrRadius[1L], pos2OrRadius[2L]) + args.lua.push(entity.description) } - returnBuffer.setTo(entityQueryImpl(self, lua, actualOptions, predicate)) + return 1 } -private fun ExecutionContext.intermediateLineQueryFunction(self: World<*, *>, lua: LuaEnvironment, pos1: Table, pos2: Table, options: Table?, predicate: Predicate) { - val actualOptions = options ?: tableOf() - actualOptions[lineStr] = tableOf(pos1, pos2) - returnBuffer.setTo(entityQueryImpl(self, lua, actualOptions, predicate)) -} +private fun entityPortrait(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val mode = args.nextString() + val entity = self.entities[id] as? ActorEntity ?: return 0 -private inline fun createQueryFunction(self: World<*, *>, lua: LuaEnvironment) = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? -> - intermediateQueryFunction(self, lua, pos1, pos2OrRadius, options, Predicate { it is T }) -} + val parts = entity.portrait(ActorEntity.PortraitMode.entries.valueOf(mode)) -private inline fun createLineQueryFunction(self: World<*, *>, lua: LuaEnvironment) = luaFunction { pos1: Table, pos2: Table, options: Table? -> - intermediateLineQueryFunction(self, lua, pos1, pos2, options, Predicate { it is T }) -} + args.lua.pushTable(parts.size) -fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEnvironment) { - callbacks["entityQuery"] = createQueryFunction(self, lua) - callbacks["monsterQuery"] = createQueryFunction(self, lua) // TODO - callbacks["npcQuery"] = createQueryFunction(self, lua) // TODO - callbacks["itemDropQuery"] = createQueryFunction(self, lua) - callbacks["playerQuery"] = createQueryFunction(self, lua) - - callbacks["entityLineQuery"] = createLineQueryFunction(self, lua) - callbacks["monsterLineQuery"] = createLineQueryFunction(self, lua) // TODO - callbacks["npcLineQuery"] = createLineQueryFunction(self, lua) // TODO - callbacks["itemDropLineQuery"] = createLineQueryFunction(self, lua) - callbacks["playerLineQuery"] = createLineQueryFunction(self, lua) - - callbacks["objectQuery"] = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? -> - var objectName: String? = null - - if (options != null) - objectName = (indexNoYield(options, "name") as ByteString?)?.decode() - - intermediateQueryFunction(self, lua, pos1, pos2OrRadius, options, Predicate { - it is WorldObject && (objectName == null || it.config.key == objectName) - }) + for ((i, part) in parts.withIndex()) { + args.lua.setTableValue(i + 1L, part.toJson()) } - callbacks["objectLineQuery"] = luaFunction { pos1: Table, pos2: Table, options: Table? -> - var objectName: String? = null + return 1 +} - if (options != null) - objectName = (indexNoYield(options, "name") as ByteString?)?.decode() +private fun entityHandItem(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val primary = args.nextBoolean() - intermediateLineQueryFunction(self, lua, pos1, pos2, options, Predicate { - it is WorldObject && (objectName == null || it.config.key == objectName) - }) + val entity = self.entities[id] as? HumanoidActorEntity ?: return 0 + + if (primary) { + args.lua.push(entity.primaryHandItem.entry.nameOrNull) + } else { + args.lua.push(entity.secondaryHandItem.entry.nameOrNull) } - callbacks["loungeableQuery"] = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? -> - var orientationName: String? = null + return 1 +} - if (options != null) - orientationName = (indexNoYield(options, "orientation") as ByteString?)?.decode() +private fun entityHandItemDescriptor(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val primary = args.nextBoolean() - val orientation = when (orientationName) { - null -> LoungeOrientation.NONE - else -> LoungeOrientation.entries.valueOf(orientationName) + val entity = self.entities[id] as? HumanoidActorEntity ?: return 0 + + if (primary) { + entity.primaryHandItem.createDescriptor().store(args.lua) + } else { + entity.secondaryHandItem.createDescriptor().store(args.lua) + } + + return 1 +} + +private fun entityUniqueId(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + args.lua.push(self.entities[id]?.uniqueID?.get()) + return 1 +} + +private fun getObjectParameter(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val parameter = args.nextString() + + // FIXME: this is stupid (defaultValue is ignored when we lookup parameter on non existing entity), + // but we must retain original behavior + val entity = self.entities[id] as? WorldObject ?: return 0 + val result = entity.lookupProperty(parameter) + + if (result.isJsonNull) { + // TODO: while this is faster, it does not correspond to original engine behavior where "default value" is always copied (lua -> json -> lua) + if (args.top == 2) { + args.lua.push() + } else if (args.top != 3) { + args.lua.dup(3) } - - intermediateQueryFunction(self, lua, pos1, pos2OrRadius, options, Predicate { - it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation) - }) + } else { + args.lua.push(result) } - callbacks["loungeableLineQuery"] = luaFunction { pos1: Table, pos2: Table, options: Table? -> - var orientationName: String? = null + return 1 +} - if (options != null) - orientationName = (indexNoYield(options, "orientation") as ByteString?)?.decode() +private fun objectSpaces(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? WorldObject - val orientation = when (orientationName) { - null -> LoungeOrientation.NONE - else -> LoungeOrientation.entries.valueOf(orientationName!!) - } + if (entity == null) { + args.lua.pushTable() + } else { + args.lua.pushTable(entity.occupySpaces.size) - intermediateLineQueryFunction(self, lua, pos1, pos2, options, Predicate { - it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation) - }) - } - - callbacks["objectAt"] = luaFunction { pos: Table -> - returnBuffer.setTo(self.entityIndex.tileEntityAt(toVector2i(pos))?.entityID) - } - - callbacks["entityExists"] = luaFunction { id: Number -> - returnBuffer.setTo(id.toInt() in self.entities) - } - - callbacks["entityCanDamage"] = luaFunction { source: Number, target: Number -> - val a = self.entities[source.toInt()] - val b = self.entities[target.toInt()] - - returnBuffer.setTo(a != null && b != null && a.team.get().canDamage(b.team.get(), a == b)) - } - - callbacks["entityDamageTeam"] = luaFunction { id: Number -> - returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.entities[id]?.team?.get()))) - } - - callbacks["entityAggressive"] = luaStub("entityAggressive") - - callbacks["entityType"] = luaFunction { id: Number -> returnBuffer.setTo(self.entities[id.toInt()]?.type?.jsonName) } - callbacks["entityPosition"] = luaFunction { id: Number -> returnBuffer.setTo(from(self.entities[id.toInt()]?.position)) } - callbacks["entityVelocity"] = luaFunction { id: Number -> returnBuffer.setTo(from((self.entities[id.toInt()] as? DynamicEntity)?.movement?.velocity)) } - callbacks["entityMetaBoundBox"] = luaFunction { id: Number -> returnBuffer.setTo(from(self.entities[id.toInt()]?.metaBoundingBox)) } - callbacks["entityCurrency"] = luaFunction { id: Number, currency: ByteString -> returnBuffer.setTo((self.entities[id.toInt()] as? PlayerEntity)?.inventory?.currencies?.get(currency.decode())) } - callbacks["entityHasCountOfItem"] = luaFunction { id: Number, descriptor: Any, exact: Boolean? -> - val player = self.entities[id.toInt()] as? PlayerEntity ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(player.inventory.hasCountOfItem(ItemDescriptor(descriptor), exact ?: false)) - } - - callbacks["entityHealth"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] as? ActorEntity ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(tableOf(entity.health, entity.maxHealth)) - } - - callbacks["entitySpecies"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(entity.humanoidIdentity.species.key.left().toByteString()) - } - - callbacks["entityGender"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(entity.humanoidIdentity.gender.jsonName.toByteString()) - } - - callbacks["entityName"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo() - - // TODO - when (entity) { - is ActorEntity -> returnBuffer.setTo(entity.name.toByteString()) - is WorldObject -> returnBuffer.setTo(entity.config.key.toByteString()) + for ((i, space) in entity.occupySpaces.withIndex()) { + args.lua.setTableValue(i + 1L, space - entity.tilePosition) } } - callbacks["entityDescription"] = luaFunction { id: Number, species: ByteString? -> - val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo() + return 1 +} - if (entity is InspectableEntity) { - returnBuffer.setTo(entity.inspectionDescription(species?.decode())) +private fun containerSize(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? ContainerObject ?: return 0 + args.lua.push(entity.items.size.toLong()) + return 1 +} + +private fun containerClose(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? ContainerObject + + if (entity == null) { + args.lua.push(false) + return 1 + } + + // FIXME: this doesn't get networked if called on client + // (AND this is the reason why in multiplayer player can't see chest/container open animations when + // other players open them) + entity.closeContainer() + args.lua.push(true) + return 1 +} + +private fun containerOpen(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? ContainerObject + + if (entity == null) { + args.lua.push(false) + return 1 + } + + // FIXME: this doesn't get networked if called on client + // (AND this is the reason why in multiplayer player can't see chest/container open animations when + // other players open them) + entity.openContainer() + args.lua.push(true) + return 1 +} + +private fun containerItems(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? ContainerObject ?: return 0 + + args.lua.pushTable() + var i = 1L + + for (item in entity.items) { + if (item.isNotEmpty) { + args.lua.push(i++) + item.createDescriptor().store(args.lua) + args.lua.setTableValue() + } + } + + return 1 +} + +private fun containerItemAt(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val slot = args.nextInt() + val entity = self.entities[id] as? ContainerObject ?: return 0 + + if (slot in 0 until entity.items.size) { + entity.items[slot].createDescriptor().store(args.lua) + return 1 + } + + return 0 +} + +private fun containerConsume(self: World<*, *>, async: Boolean, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val desc = ItemDescriptor(args) + val exactMatch = args.nextOptionalBoolean() ?: false + val entity = self.entities[id] as? ContainerObject ?: return 0 + + if (async) { + args.lua.push( + LuaFuture( + future = entity.takeItem(desc, exact = exactMatch), + isLocal = false, + handler = { + push(it) + 1 + } + ) + ) + } else { + args.lua.push(entity.takeItem(desc, exact = exactMatch).getNow(null) as Boolean?) + } + + return 1 +} + +private fun containerConsumeAt(self: World<*, *>, async: Boolean, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val slot = args.nextInt() + val amount = args.nextLong() + val entity = self.entities[id] as? ContainerObject ?: return 0 + + if (async) { + args.lua.push( + LuaFuture( + future = entity.takeItemAt(slot, amount), + isLocal = false, + handler = { + args.lua.push(it) + 1 + } + ) + ) + } else { + args.lua.push(entity.takeItemAt(slot, amount).getNow(null) as Boolean?) + } + + return 1 +} + +private fun containerAvailable(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val desc = ItemDescriptor(args) + val entity = self.entities[id] as? ContainerObject ?: return 0 + args.lua.push(entity.items.take(desc, simulate = true)) + return 1 +} + +// why we have containerItems + containerTakeAll, when we could have containerItems + containerClear????????? +private fun containerTakeAll(self: World<*, *>, async: Boolean, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val entity = self.entities[id] as? ContainerObject ?: return 0 + + if (async) { + args.lua.push( + LuaFuture( + future = entity.clearContainer(), + isLocal = false, + handler = { + args.lua.pushTable(it.size) + + for ((i, item) in it.withIndex()) { + args.lua.push(i + 1L) + item.createDescriptor().store(args.lua) + args.lua.setTableValue() + } + + 1 + } + ) + ) + } else { + val items = entity.clearContainer().getNow(listOf()) + args.lua.pushTable(items.size) + + for ((i, item) in items.withIndex()) { + args.lua.push(i + 1L) + item.createDescriptor().store(args.lua) + args.lua.setTableValue() + } + } + + return 1 +} + +private fun containerTakeAt(self: World<*, *>, async: Boolean, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val slot = args.nextInt() + val entity = self.entities[id] as? ContainerObject ?: return 0 + + if (slot in 0 until entity.items.size) { + if (async) { + args.lua.push( + LuaFuture( + future = entity.takeItemAt(slot), + isLocal = false, + handler = { + push(it) + 1 + } + ) + ) } else { - returnBuffer.setTo(entity.description) + args.lua.push(entity.takeItemAt(slot).getNow(null)) } + + return 1 } - callbacks["entityPortrait"] = luaFunction { id: Number, mode: ByteString -> - val entity = self.entities[id.toInt()] as? ActorEntity ?: return@luaFunction returnBuffer.setTo() - - returnBuffer.setTo(tableOf(*entity.portrait(ActorEntity.PortraitMode.entries.valueOf(mode.decode())).map { from(it.toJson()) }.toTypedArray())) - } - - callbacks["entityHandItem"] = luaFunction { id: Number, hand: ByteString -> - val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo() - - when (val gethand = hand.decode().lowercase()) { - "primary" -> returnBuffer.setTo(entity.primaryHandItem.entry.nameOrNull.toByteString()) - "alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.entry.nameOrNull.toByteString()) - else -> throw LuaRuntimeException("Unknown tool hand $gethand") - } - } - - callbacks["entityHandItemDescriptor"] = luaFunction { id: Number, hand: ByteString -> - val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo() - - when (val gethand = hand.decode().lowercase()) { - "primary" -> returnBuffer.setTo(entity.primaryHandItem.toTable(this)) - "alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.toTable(this)) - else -> throw LuaRuntimeException("Unknown tool hand $gethand") - } - } - - callbacks["entityUniqueId"] = luaFunction { id: Number -> - returnBuffer.setTo(self.entities[id.toInt()]?.uniqueID?.get()) - } - - callbacks["getObjectParameter"] = luaFunction { id: Number, parameter: ByteString, defaultValue: Any? -> - // FIXME: this is stupid (defaultValue is ignored when we lookup parameter on non existing entity), - // but we must retain original behavior - val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo() - val result = entity.lookupProperty(parameter.decode()) - - if (result.isJsonNull) { - returnBuffer.setTo(defaultValue) - } else { - returnBuffer.setTo(from(result)) - } - } - - callbacks["getNpcScriptParameter"] = luaStub("getNpcScriptParameter") - - callbacks["objectSpaces"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo(tableOf()) - returnBuffer.setTo(tableOf(*entity.occupySpaces.map { from(it - entity.tilePosition) }.toTypedArray())) - } - - callbacks["farmableStage"] = luaStub("farmableStage") - - callbacks["containerSize"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(entity.items.size) - } - - callbacks["containerClose"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(false) - // FIXME: this doesn't get networked if called on client - // (AND this is the reason why in multiplayer player can't see chest/container open animations when - // other players open them) - entity.closeContainer() - returnBuffer.setTo(true) - } - - callbacks["containerOpen"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(false) - // FIXME: this doesn't get networked if called on client - // (AND this is the reason why in multiplayer player can't see chest/container open animations when - // other players open them) - entity.openContainer() - returnBuffer.setTo(true) - } - - callbacks["containerItems"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(tableOf(*entity.items.filter { it.isNotEmpty }.map { it.toTable(this) }.toTypedArray())) - } - - callbacks["containerItemAt"] = luaFunction { id: Number, index: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - - if (index.toInt() < entity.items.size) - returnBuffer.setTo(entity.items[index.toInt()].toTable(this)) - } - - callbacks["containerConsume"] = luaFunction { id: Number, desc: Any, exactMatch: Boolean? -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(entity.takeItem(ItemDescriptor(desc), exact = exactMatch ?: false).getNow(null)) - } - - callbacks["containerConsumeAt"] = luaFunction { id: Number, slot: Number, amount: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(entity.takeItemAt(slot.toInt(), amount.toLong()).getNow(null)) - } - - callbacks["containerAvailable"] = luaFunction { id: Number, desc: Any -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(entity.items.take(ItemDescriptor(desc), simulate = true)) - } - - // why we have containerItems + containerTakeAll, when we could have containerItems + containerClear????????? - callbacks["containerTakeAll"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(tableOf(*entity.clearContainer().getNow(listOf()).map { it.toTable(this) }.toTypedArray())) - } - - callbacks["containerTakeAt"] = luaFunction { id: Number, slot: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - - if (slot.toInt() < entity.items.size) - returnBuffer.setTo(entity.takeItemAt(slot.toInt()).getNow(null)) - } - - callbacks["containerTakeNumItemsAt"] = luaFunction { id: Number, slot: Number, amount: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - - if (slot.toInt() < entity.items.size) - returnBuffer.setTo(entity.takeItemAt(slot.toInt(), amount.toLong()).getNow(null)) - } - - callbacks["containerItemsCanFit"] = luaFunction { id: Number, desc: Any -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - val item = ItemDescriptor(desc).build(random = lua.random) - returnBuffer.setTo(item.size - entity.items.add(item, simulate = true).first.size) - } - - callbacks["containerItemsFitWhere"] = luaFunction { id: Number, desc: Any -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo() - val item = ItemDescriptor(desc).build(random = lua.random) - val (leftover, touched) = entity.items.add(item, simulate = true) - - returnBuffer.setTo(tableMapOf( - "leftover" to leftover.size, - "slots" to tableOf(*touched.toIntArray()) - )) - } - - callbacks["containerAddItems"] = luaFunction { id: Number, desc: Any -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc) - val build = ItemDescriptor(desc).build(random = lua.random) - returnBuffer.setTo(entity.addItems(build).getNow(build)?.toTable(this)) - } - - callbacks["containerStackItems"] = luaFunction { id: Number, desc: Any -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc) - val build = ItemDescriptor(desc).build(random = lua.random) - returnBuffer.setTo(entity.stackWithExisting(build).getNow(build)?.toTable(this)) - } - - callbacks["containerPutItemsAt"] = luaFunction { id: Number, desc: Any, slot: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc) - val build = ItemDescriptor(desc).build(random = lua.random) - returnBuffer.setTo(entity.putItems(slot.toInt(), build).getNow(build)?.toTable(this)) - } - - callbacks["containerSwapItems"] = luaFunction { id: Number, desc: Any, slot: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc) - val build = ItemDescriptor(desc).build(random = lua.random) - returnBuffer.setTo(entity.swapItems(slot.toInt(), build, tryCombine = true).getNow(build)?.toTable(this)) - } - - callbacks["containerSwapItemsNoCombine"] = luaFunction { id: Number, desc: Any, slot: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc) - val build = ItemDescriptor(desc).build(random = lua.random) - returnBuffer.setTo(entity.swapItems(slot.toInt(), build, tryCombine = false).getNow(build)?.toTable(this)) - } - - callbacks["containerItemApply"] = luaFunction { id: Number, desc: Any, slot: Number -> - val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc) - val build = ItemDescriptor(desc).build(random = lua.random) - returnBuffer.setTo(entity.combineItems(slot.toInt(), build).getNow(build)?.toTable(this)) - } - - callbacks["callScriptedEntity"] = luaFunctionN("callScriptedEntity") { - val id = it.nextInteger() - val function = it.nextString().decode() - val entity = self.entities[id.toInt()] ?: return@luaFunctionN //?: throw LuaRuntimeException("Entity with ID $id does not exist") - - if (entity !is ScriptedEntity) - throw LuaRuntimeException("$entity is not scripted entity") - - if (entity.isRemote) - throw LuaRuntimeException("$entity is not owned by this side") - - returnBuffer.setToContentsOf(entity.callScript(function, *it.copyRemaining())) - } - - callbacks["findUniqueEntity"] = luaFunction { id: ByteString -> - returnBuffer.setTo(LuaFuture( - future = self.findUniqueEntity(id.decode()).thenApply { from(it) }, - isLocal = self.isServer - )) - } - - callbacks["findUniqueEntityAsync"] = luaFunction { id: ByteString -> - returnBuffer.setTo(LuaFuture( - future = self.findUniqueEntity(id.decode()).thenApply { from(it) }, - isLocal = false - )) - } - - callbacks["sendEntityMessage"] = luaFunctionN("sendEntityMessage") { - val id = it.nextAny() - val func = it.nextString().decode() - - if (id is Number) { - val entityID = id.toInt() - - returnBuffer.setTo(LuaFuture( - future = self.dispatchEntityMessage(self.connectionID, entityID, func, it.copyRemaining() - .stream().map { toJsonFromLua(it) }.collect(JsonArrayCollector)).thenApply { from(it) }, - isLocal = Connection.connectionForEntityID(entityID) == self.connectionID - )) - } else { - id as ByteString - val entityID = id.decode() - val findAlreadyLoaded = self.entities.values.find { it.uniqueID.get() == entityID } - - val isLocal = if (findAlreadyLoaded == null) - self.isServer - else - !findAlreadyLoaded.isRemote - - returnBuffer.setTo(LuaFuture( - future = self.dispatchEntityMessage(self.connectionID, entityID, func, it.copyRemaining() - .stream().map { toJsonFromLua(it) }.collect(JsonArrayCollector)).thenApply { from(it) }, - isLocal = isLocal - )) - } - } - - callbacks["loungeableOccupied"] = luaStub("loungeableOccupied") - callbacks["isMonster"] = luaStub("isMonster") - callbacks["monsterType"] = luaStub("monsterType") - callbacks["npcType"] = luaStub("npcType") - callbacks["stagehandType"] = luaStub("stagehandType") - callbacks["isNpc"] = luaStub("isNpc") - - callbacks["isEntityInteractive"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] - - if (entity is InteractiveEntity) - returnBuffer.setTo(entity.isInteractive) - } - - callbacks["entityMouthPosition"] = luaFunction { id: Number -> - val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo() - // original entine returns non nil only for "Chatty entity" - returnBuffer.setTo(from(entity.mouthPosition)) - } - - callbacks["entityTypeName"] = luaStub("entityTypeName") + return 0 +} + +private fun containerTakeNumItemsAt(self: World<*, *>, async: Boolean, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val slot = args.nextInt() + val amount = args.nextInt() + + require(amount >= 0L) { "Invalid amount to take: $amount" } + val entity = self.entities[id] as? ContainerObject ?: return 0 + + if (amount > 0L && slot in 0 until entity.items.size) { + if (async) { + args.lua.push( + LuaFuture( + future = entity.takeItemAt(slot, amount.toLong()), + isLocal = false, + handler = { + push(it) + 1 + } + ) + ) + } else { + args.lua.push(entity.takeItemAt(slot, amount.toLong()).getNow(null)) + } + + return 1 + } + + return 0 +} + +private fun containerItemsCanFit(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val desc = ItemDescriptor(args) + val entity = self.entities[id] as? ContainerObject ?: return run { desc.store(args.lua); 1 } + val build = desc.build(random = args.lua.random) + val (leftover, _) = entity.items.add(build, simulate = true) + + args.lua.push(build.size - leftover.size) + return 1 +} + +private fun containerItemsFitWhere(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val desc = ItemDescriptor(args) + val entity = self.entities[id] as? ContainerObject ?: return run { desc.store(args.lua); 1 } + val build = desc.build(random = args.lua.random) + val (leftover, touched) = entity.items.add(build, simulate = true) + + args.lua.pushTable(hashSize = 2) + + args.lua.setTableValue("leftover", leftover.size) + args.lua.push("slots") + args.lua.pushTable(touched.size) + + for ((i, v) in touched.withIndex()) { + args.lua.setTableValue(i + 1L, v.toLong()) + } + + args.lua.setTableValue() + + return 1 +} + +private fun slotlessContainerBinding( + self: World<*, *>, + async: Boolean, + dispatch: (ContainerObject, ItemStack) -> CompletableFuture, + args: LuaThread.ArgStack +): Int { + val id = args.nextInt() + val desc = ItemDescriptor(args) + val entity = self.entities[id] as? ContainerObject ?: return run { desc.store(args.lua); 1 } + val build = desc.build(random = args.lua.random) + + if (async) { + args.lua.push( + LuaFuture( + future = dispatch(entity, build).thenApply { it.createDescriptor() }, + isLocal = false, + handler = { + it.store(args.lua) + 1 + } + ) + ) + + return 1 + } else { + dispatch(entity, build).getNow(build).createDescriptor().store(args.lua) + return 1 + } +} + +private fun slottedContainerBinding( + self: World<*, *>, + async: Boolean, + dispatch: (ContainerObject, Int, ItemStack) -> CompletableFuture, + args: LuaThread.ArgStack +): Int { + val id = args.nextInt() + val desc = ItemDescriptor(args) + val slot = args.nextInt() + val entity = self.entities[id] as? ContainerObject ?: return run { desc.store(args.lua); 1 } + val build = desc.build(random = args.lua.random) + + if (async) { + args.lua.push( + LuaFuture( + future = dispatch(entity, slot, build).thenApply { it.createDescriptor() }, + isLocal = false, + handler = { + it.store(args.lua) + 1 + } + ) + ) + + return 1 + } else { + dispatch(entity, slot, build).getNow(build).createDescriptor().store(args.lua) + return 1 + } +} + +private fun containerSwapItems(self: World<*, *>, async: Boolean, combine: Boolean, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val desc = ItemDescriptor(args) + val slot = args.nextInt() + // retain original behavior where engine always decodes item descriptor and guarantees to return a copy + val entity = self.entities[id] as? ContainerObject ?: return run { desc.store(args.lua); 1 } + val build = desc.build(random = args.lua.random) + + if (async) { + args.lua.push( + LuaFuture( + future = entity.swapItems(slot, build, tryCombine = combine).thenApply { it.createDescriptor() }, + isLocal = false, + handler = { + it.store(args.lua) + 1 + } + ) + ) + + return 1 + } else { + entity.swapItems(slot, build, tryCombine = combine).getNow(build).createDescriptor().store(args.lua) + return 1 + } +} + +private fun callScriptedEntity(self: World<*, *>, args: LuaThread.ArgStack): Int { + val id = args.nextInt() + val function = args.nextString() + // TODO: uncomment to retain original behavior + val entity = self.entities[id] ?: return 0 //?: throw IllegalStateException("Entity with ID $id does not exist") + + if (entity !is ScriptedEntity) + throw IllegalStateException("$entity is not scripted entity") + + if (entity.isRemote) + throw IllegalStateException("$entity is not owned by this side") + + args.lua.push(entity.callScript(function, args.copyRemaining())) + return 1 +} + +private fun findUniqueEntity(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push( + LuaFuture( + future = self.findUniqueEntity(args.nextString()).thenApply { KOptional.ofNullable(it) }, + isLocal = self.isServer, + handler = { + it.ifPresent { + push(it) + return@LuaFuture 1 + } + + 0 + } + ) + ) + + return 1 +} + +private fun findUniqueEntityAsync(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push( + LuaFuture( + future = self.findUniqueEntity(args.nextString()).thenApply { KOptional.ofNullable(it) }, + isLocal = false, + handler = { + it.ifPresent { + push(it) + return@LuaFuture 1 + } + + 0 + } + ) + ) + + return 1 +} + +private fun sendEntityMessage(self: World<*, *>, args: LuaThread.ArgStack): Int { + val peek = args.peek() + + if (peek == LuaType.NUMBER) { + val entityID = args.nextInt() + val func = args.nextString() + + args.lua.push( + LuaFuture( + future = self.dispatchEntityMessage(self.connectionID, entityID, func, args.copyRemaining()), + isLocal = Connection.connectionForEntityID(entityID) == self.connectionID, + handler = { + push(it) + 1 + } + ) + ) + } else { + val entityID = args.nextString() + val func = args.nextString() + val findAlreadyLoaded = self.entities.values.find { it.uniqueID.get() == entityID } + + val isLocal = if (findAlreadyLoaded == null) + self.isServer + else + !findAlreadyLoaded.isRemote + + args.lua.push( + LuaFuture( + future = self.dispatchEntityMessage(self.connectionID, entityID, func, args.copyRemaining()), + isLocal = isLocal, + handler = { + push(it) + 1 + } + ) + ) + } + + return 1 +} + +private fun isMonster(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.entities[args.nextInt()] is MonsterEntity) + return 1 +} + +private fun monsterType(self: World<*, *>, args: LuaThread.ArgStack): Int { + val entity = self.entities[args.nextInt()] as? MonsterEntity ?: return 0 + args.lua.push(entity.variant.type) + return 1 +} + +private fun npcType(self: World<*, *>, args: LuaThread.ArgStack): Int { + val entity = self.entities[args.nextInt()] as? NPCEntity ?: return 0 + args.lua.push(entity.variant.typeName) + return 1 +} + +private fun stagehandType(self: World<*, *>, args: LuaThread.ArgStack): Int { + val entity = self.entities[args.nextInt()] as? StagehandEntity ?: return 0 + args.lua.push(entity.typeName) + return 1 +} + +private fun isNpc(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.entities[args.nextInt()] is NPCEntity) + return 1 +} + +private fun isEntityInteractive(self: World<*, *>, args: LuaThread.ArgStack): Int { + val entity = self.entities[args.nextInt()] + + if (entity is InteractiveEntity) { + args.lua.push(entity.isInteractive) + return 1 + } + + return 0 +} + +private fun entityMouthPosition(self: World<*, *>, args: LuaThread.ArgStack): Int { + val entity = self.entities[args.nextInt()] ?: return 0 + // original engine returns non nil only for "Chatty entity" + args.lua.push(entity.mouthPosition) + return 1 +} + +fun provideWorldEntitiesBindings(self: World<*, *>, lua: LuaThread) { + lua.pushBinding(self, "queryObjectAABBImpl", ::queryObjectAABBImpl) + lua.pushBinding(self, "queryObjectLineImpl", ::queryObjectLineImpl) + lua.pushBinding(self, "queryObjectRadiusImpl", ::queryObjectRadiusImpl) + lua.pushBinding(self, "queryObjectPolyImpl", ::queryObjectPolyImpl) + + lua.pushBinding(self, "queryLoungeableAABBImpl", ::queryLoungeableAABBImpl) + lua.pushBinding(self, "queryLoungeableLineImpl", ::queryLoungeableLineImpl) + lua.pushBinding(self, "queryLoungeableRadiusImpl", ::queryLoungeableRadiusImpl) + lua.pushBinding(self, "queryLoungeablePolyImpl", ::queryLoungeablePolyImpl) + + lua.pushBinding(self, Predicate { true }, "queryAABBImpl", ::queryAABBImpl) + lua.pushBinding(self, Predicate { true }, "queryRadiusImpl", ::queryRadiusImpl) + lua.pushBinding(self, Predicate { true }, "queryLineImpl", ::queryLineImpl) + lua.pushBinding(self, Predicate { true }, "queryPolyImpl", ::queryPolyImpl) + + lua.pushBinding(self, Predicate { it is NPCEntity }, "npcQueryAABBImpl", ::queryAABBImpl) + lua.pushBinding(self, Predicate { it is NPCEntity }, "npcQueryRadiusImpl", ::queryRadiusImpl) + lua.pushBinding(self, Predicate { it is NPCEntity }, "npcQueryLineImpl", ::queryLineImpl) + lua.pushBinding(self, Predicate { it is NPCEntity }, "npcQueryPolyImpl", ::queryPolyImpl) + + lua.pushBinding(self, Predicate { it is MonsterEntity }, "monsterQueryAABBImpl", ::queryAABBImpl) + lua.pushBinding(self, Predicate { it is MonsterEntity }, "monsterQueryRadiusImpl", ::queryRadiusImpl) + lua.pushBinding(self, Predicate { it is MonsterEntity }, "monsterQueryLineImpl", ::queryLineImpl) + lua.pushBinding(self, Predicate { it is MonsterEntity }, "monsterQueryPolyImpl", ::queryPolyImpl) + + lua.pushBinding(self, Predicate { it is PlayerEntity }, "playerQueryAABBImpl", ::queryAABBImpl) + lua.pushBinding(self, Predicate { it is PlayerEntity }, "playerQueryRadiusImpl", ::queryRadiusImpl) + lua.pushBinding(self, Predicate { it is PlayerEntity }, "playerQueryLineImpl", ::queryLineImpl) + lua.pushBinding(self, Predicate { it is PlayerEntity }, "playerQueryPolyImpl", ::queryPolyImpl) + + lua.pushBinding(self, Predicate { it is ItemDropEntity }, "itemDropQueryAABBImpl", ::queryAABBImpl) + lua.pushBinding(self, Predicate { it is ItemDropEntity }, "itemDropQueryRadiusImpl", ::queryRadiusImpl) + lua.pushBinding(self, Predicate { it is ItemDropEntity }, "itemDropQueryLineImpl", ::queryLineImpl) + lua.pushBinding(self, Predicate { it is ItemDropEntity }, "itemDropQueryPolyImpl", ::queryPolyImpl) + + lua.pushBinding(self, "objectAt", ::objectAt) + lua.pushBinding(self, "entityExists", ::entityExists) + lua.pushBinding(self, "entityCanDamage", ::entityCanDamage) + lua.pushBinding(self, "entityDamageTeam", ::entityDamageTeam) + lua.pushBinding(self, "entityType", ::entityType) + lua.pushBinding(self, "entityPosition", ::entityPosition) + lua.pushBinding(self, "entityVelocity", ::entityVelocity) + lua.pushBinding(self, "entityMetaBoundBox", ::entityMetaBoundBox) + lua.pushBinding(self, "entityCurrency", ::entityCurrency) + lua.pushBinding(self, "entityHasCountOfItem", ::entityHasCountOfItem) + lua.pushBinding(self, "entityHealth", ::entityHealth) + lua.pushBinding(self, "entitySpecies", ::entitySpecies) + lua.pushBinding(self, "entityGender", ::entityGender) + lua.pushBinding(self, "entityName", ::entityName) + lua.pushBinding(self, "entityDescription", ::entityDescription) + lua.pushBinding(self, "entityPortrait", ::entityPortrait) + lua.pushBinding(self, "entityHandItem", ::entityHandItem) + lua.pushBinding(self, "entityHandItemDescriptor", ::entityHandItemDescriptor) + lua.pushBinding(self, "entityUniqueId", ::entityUniqueId) + lua.pushBinding(self, "getObjectParameter", ::getObjectParameter) + lua.pushBinding(self, "objectSpaces", ::objectSpaces) + + lua.pushBinding(self, "containerSize", ::containerSize) + lua.pushBinding(self, "containerClose", ::containerClose) + lua.pushBinding(self, "containerOpen", ::containerOpen) + lua.pushBinding(self, "containerItems", ::containerItems) + lua.pushBinding(self, "containerItemAt", ::containerItemAt) + lua.pushBinding(self, false, "containerConsume", ::containerConsume) + lua.pushBinding(self, true, "containerConsumeAsync", ::containerConsume) + lua.pushBinding(self, false, "containerConsumeAt", ::containerConsumeAt) + lua.pushBinding(self, true, "containerConsumeAtAsync", ::containerConsumeAt) + lua.pushBinding(self, "containerAvailable", ::containerAvailable) + lua.pushBinding(self, false, "containerTakeAll", ::containerTakeAll) + lua.pushBinding(self, true, "containerTakeAllAsync", ::containerTakeAll) + lua.pushBinding(self, false, "containerTakeAt", ::containerTakeAt) + lua.pushBinding(self, true, "containerTakeAtAsync", ::containerTakeAt) + lua.pushBinding(self, false, "containerTakeNumItemsAt", ::containerTakeNumItemsAt) + lua.pushBinding(self, true, "containerTakeNumItemsAtAsync", ::containerTakeNumItemsAt) + + lua.pushBinding(self, "containerItemsCanFit", ::containerItemsCanFit) + lua.pushBinding(self, "containerItemsFitWhere", ::containerItemsFitWhere) + + lua.pushBinding(self, false, ContainerObject::addItems, "containerAddItems", ::slotlessContainerBinding) + lua.pushBinding(self, true, ContainerObject::addItems, "containerAddItemsAsync", ::slotlessContainerBinding) + lua.pushBinding(self, false, ContainerObject::stackWithExisting, "containerStackItems", ::slotlessContainerBinding) + lua.pushBinding(self, true, ContainerObject::stackWithExisting, "containerStackItemsAsync", ::slotlessContainerBinding) + lua.pushBinding(self, false, ContainerObject::putItems, "containerPutItemsAt", ::slottedContainerBinding) + lua.pushBinding(self, true, ContainerObject::putItems, "containerPutItemsAtAsync", ::slottedContainerBinding) + lua.pushBinding(self, false, true, "containerSwapItems", ::containerSwapItems) + lua.pushBinding(self, true, true, "containerSwapItemsAsync", ::containerSwapItems) + lua.pushBinding(self, false, false, "containerSwapItemsNoCombine", ::containerSwapItems) + lua.pushBinding(self, true, false, "containerSwapItemsNoCombineAsync", ::containerSwapItems) + lua.pushBinding(self, false, ContainerObject::combineItems, "containerItemApply", ::slottedContainerBinding) + lua.pushBinding(self, true, ContainerObject::combineItems, "containerItemApplyAsync", ::slottedContainerBinding) + + lua.pushBinding(self, "callScriptedEntity", ::callScriptedEntity) + + lua.pushBinding(self, "findUniqueEntity", ::findUniqueEntity) + lua.pushBinding(self, "findUniqueEntityAsync", ::findUniqueEntityAsync) + + lua.pushBinding(self, "sendEntityMessage", ::sendEntityMessage) + + lua.pushBinding(self, "stagehandType", ::stagehandType) + lua.pushBinding(self, "monsterType", ::monsterType) + lua.pushBinding(self, "npcType", ::npcType) + lua.pushBinding(self, "isMonster", ::isMonster) + lua.pushBinding(self, "isNpc", ::isNpc) + lua.pushBinding(self, "isEntityInteractive", ::isEntityInteractive) + lua.pushBinding(self, "entityMouthPosition", ::entityMouthPosition) + + lua.setTableValueToStub("entityAggressive") + lua.setTableValueToStub("getNpcScriptParameter") + lua.setTableValueToStub("entityTypeName") + lua.setTableValueToStub("loungeableOccupied") + lua.setTableValueToStub("farmableStage") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt index 36e92688..56e75fd8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt @@ -1,36 +1,20 @@ package ru.dbotthepony.kstarbound.lua.bindings -import org.classdump.luna.ByteString -import org.classdump.luna.LuaRuntimeException -import org.classdump.luna.Table -import org.classdump.luna.lib.ArgumentIterator -import org.classdump.luna.runtime.ExecutionContext -import ru.dbotthepony.kommons.collect.map -import ru.dbotthepony.kommons.collect.toList import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageType -import ru.dbotthepony.kstarbound.defs.tile.isEmptyModifier import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyModifier import ru.dbotthepony.kstarbound.defs.tile.isNullTile -import ru.dbotthepony.kstarbound.lua.LuaEnvironment -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.iterator -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.luaFunctionN -import ru.dbotthepony.kstarbound.lua.luaStub -import ru.dbotthepony.kstarbound.lua.nextOptionalInteger -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableFrom -import ru.dbotthepony.kstarbound.lua.tableOf -import ru.dbotthepony.kstarbound.lua.toByteString -import ru.dbotthepony.kstarbound.lua.toVector2d -import ru.dbotthepony.kstarbound.lua.toVector2i +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.getVector2i +import ru.dbotthepony.kstarbound.lua.nextVector2d +import ru.dbotthepony.kstarbound.lua.nextVector2i +import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture +import ru.dbotthepony.kstarbound.lua.userdata.push import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.world.TileModification @@ -39,51 +23,39 @@ import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.tileAreaBrush import java.util.concurrent.CompletableFuture -private val foregroundStr = ByteString.of("foreground") -private val backgroundStr = ByteString.of("background") +private fun damageTilesImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture { + val positions = args.readTableValues { getVector2i() ?: throw IllegalArgumentException("Positions table contains invalid positions") } + val isBackground = args.nextBoolean() -private fun isBackground(layer: ByteString): Boolean { - return if (layer == backgroundStr) - true - else if (layer == foregroundStr) - false - else - throw LuaRuntimeException("Invalid tile layer $layer") -} - -private fun ExecutionContext.damageTilesImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture { - val positions = it.nextTable().iterator().map { toVector2i(it.value) }.toList() - val isBackground = isBackground(it.nextString()) - - val sourcePosition = toVector2d(it.nextTable()) - val damageType = TileDamageType.entries.valueOf(it.nextString().decode()) - val damage = it.nextFloat() - val harvestLevel = it.nextOptionalInteger()?.toInt() ?: 999 - val sourceEntity = self.entities[it.nextOptionalInteger()?.toInt() ?: 0] + val sourcePosition = args.nextVector2d() + val damageType = TileDamageType.entries.valueOf(args.nextString()) + val damage = args.nextDouble() + val harvestLevel = args.nextOptionalLong()?.toInt() ?: 999 + val sourceEntity = self.entities[args.nextOptionalLong()?.toInt() ?: 0] return self.damageTiles(positions, isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity) } -private fun ExecutionContext.damageTileAreaImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture { - val center = toVector2i(it.nextTable()) - val radius = it.nextFloat() - val isBackground = isBackground(it.nextString()) +private fun damageTileAreaImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture { + val center = args.nextVector2i() + val radius = args.nextDouble() + val isBackground = args.nextBoolean() - val sourcePosition = toVector2d(it.nextTable()) - val damageType = TileDamageType.entries.valueOf(it.nextString().decode()) - val damage = it.nextFloat() - val harvestLevel = it.nextOptionalInteger()?.toInt() ?: 999 - val sourceEntity = self.entities[it.nextOptionalInteger()?.toInt() ?: 0] + val sourcePosition = args.nextVector2d() + val damageType = TileDamageType.entries.valueOf(args.nextString()) + val damage = args.nextDouble() + val harvestLevel = args.nextOptionalLong()?.toInt() ?: 999 + val sourceEntity = self.entities[args.nextOptionalLong()?.toInt() ?: 0] return self.damageTiles(tileAreaBrush(center, radius), isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity) } -private fun ExecutionContext.placeMaterialImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture> { - val pos = toVector2i(it.nextTable()) - val isBackground = isBackground(it.nextString()) - val material = Registries.tiles.getOrThrow(it.nextString().decode()) - val hueShift: Float? = (it.nextAny() as? Number)?.let { it.toFloat() / 255f * 360f } - val allowOverlap = it.nextBoolean() +private fun placeMaterialImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture> { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() + val material = Registries.tiles.getOrThrow(args.nextString()) + val hueShift: Float? = args.nextOptionalDouble()?.let { it.toFloat() / 255f * 360f } + val allowOverlap = args.nextBoolean() val action = TileModification.PlaceMaterial(isBackground, material.ref, hueShift) @@ -91,12 +63,12 @@ private fun ExecutionContext.placeMaterialImpl(self: World<*, *>, it: ArgumentIt return self.applyTileModifications(listOf(pos to action), allowOverlap, false).thenApply { it.map { it.first } } } -private fun ExecutionContext.placeModImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture> { - val pos = toVector2i(it.nextTable()) - val isBackground = isBackground(it.nextString()) - val material = Registries.tileModifiers.getOrThrow(it.nextString().decode()) - val hueShift: Float? = (it.nextAny() as? Number)?.let { it.toFloat() / 255f * 360f } - val allowOverlap = it.nextBoolean() +private fun placeModImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture> { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() + val material = Registries.tileModifiers.getOrThrow(args.nextString()) + val hueShift: Float? = args.nextOptionalDouble()?.let { it.toFloat() / 255f * 360f } + val allowOverlap = args.nextBoolean() val action = TileModification.PlaceModifier(isBackground, material.ref, hueShift) @@ -104,137 +76,261 @@ private fun ExecutionContext.placeModImpl(self: World<*, *>, it: ArgumentIterato return self.applyTileModifications(listOf(pos to action), allowOverlap, false).thenApply { it.map { it.first } } } -fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua: LuaEnvironment) { - callbacks["lightLevel"] = luaStub("lightLevel") - callbacks["windLevel"] = luaStub("windLevel") +private fun breathable(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.chunkMap.isBreathable(args.nextVector2d())) + return 1 +} - callbacks["breathable"] = luaFunction { pos: Table -> - returnBuffer.setTo(self.chunkMap.isBreathable(toVector2i(pos))) - } +private fun underground(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.template.undergroundLevel >= args.nextVector2d().y) + return 1 +} - callbacks["underground"] = luaFunction { pos: Table -> - returnBuffer.setTo(self.template.undergroundLevel >= toVector2d(pos).y) - } +private fun material(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() - callbacks["material"] = luaFunction { pos: Table, layer: ByteString -> - val isBackground = isBackground(layer) - val tile = self.getCell(toVector2i(pos)).tile(isBackground) + val tile = self.getCell(pos).tile(isBackground) - if (tile.material.isNullTile) { - returnBuffer.setTo() - } else if (tile.material.isEmptyTile) { - returnBuffer.setTo(false) - } else { - returnBuffer.setTo(tile.material.key.toByteString()) - } - - } - callbacks["mod"] = luaFunction { pos: Table, layer: ByteString -> - val isBackground = isBackground(layer) - val tile = self.getCell(toVector2i(pos)).tile(isBackground) - - if (tile.modifier.isNotEmptyModifier) { - returnBuffer.setTo(tile.modifier.key.toByteString()) - } - } - - callbacks["materialHueShift"] = luaFunction { pos: Table, layer: ByteString -> - val isBackground = isBackground(layer) - val tile = self.getCell(toVector2i(pos)).tile(isBackground) - returnBuffer.setTo(tile.hueShift.toDouble()) - } - - callbacks["modHueShift"] = luaFunction { pos: Table, layer: ByteString -> - val isBackground = isBackground(layer) - val tile = self.getCell(toVector2i(pos)).tile(isBackground) - returnBuffer.setTo(tile.modifierHueShift.toDouble()) - } - - callbacks["materialColor"] = luaFunction { pos: Table, layer: ByteString -> - val isBackground = isBackground(layer) - val tile = self.getCell(toVector2i(pos)).tile(isBackground) - returnBuffer.setTo(tile.color.ordinal.toLong()) - } - - callbacks["materialColorName"] = luaFunction { pos: Table, layer: ByteString -> - val isBackground = isBackground(layer) - val tile = self.getCell(toVector2i(pos)).tile(isBackground) - returnBuffer.setTo(tile.color.jsonName.toByteString()) - } - - callbacks["setMaterialColor"] = luaFunction { pos: Table, layer: ByteString, color: Any -> - val isBackground = isBackground(layer) - - val actualColor = if (color is Number) - TileColor.entries[color.toInt()] - else if (color is ByteString) - TileColor.entries.valueOf(color.decode()) - else - throw LuaRuntimeException("Unknown tile color $color") - - val actualPos = toVector2i(pos) - val cell = self.getCell(actualPos).mutable() - cell.tile(isBackground).color = actualColor - returnBuffer.setTo(self.setCell(actualPos, cell)) - } - - callbacks["oceanLevel"] = luaFunction { pos: Table -> - returnBuffer.setTo(self.template.cellInfo(toVector2i(pos)).oceanLiquidLevel.toLong()) - } - - callbacks["environmentStatusEffects"] = luaFunction { pos: Table -> - returnBuffer.setTo(tableFrom(self.environmentStatusEffects(toVector2i(pos)).map { from(Starbound.gson.toJsonTree(it)) })) - } - - callbacks["damageTiles"] = luaFunctionN("damageTiles") { - returnBuffer.setTo(damageTilesImpl(self, it).getNow(TileDamageResult.NONE) != TileDamageResult.NONE) - } - - callbacks["damageTilesPromise"] = luaFunctionN("damageTilesPromise") { - returnBuffer.setTo( - LuaFuture( - future = damageTilesImpl(self, it).thenApply { it.jsonName }, - isLocal = false - ) - ) - } - - callbacks["damageTileArea"] = luaFunctionN("damageTileArea") { - returnBuffer.setTo(damageTileAreaImpl(self, it).getNow(TileDamageResult.NONE) != TileDamageResult.NONE) - } - - callbacks["damageTileAreaPromise"] = luaFunctionN("damageTileAreaPromise") { - returnBuffer.setTo( - LuaFuture( - future = damageTileAreaImpl(self, it).thenApply { it.jsonName }, - isLocal = false - ) - ) - } - - callbacks["placeMaterial"] = luaFunctionN("placeMaterial") { - returnBuffer.setTo(placeMaterialImpl(self, it).getNow(listOf()).isEmpty()) - } - - callbacks["placeMaterialPromise"] = luaFunctionN("placeMaterialPromise") { - returnBuffer.setTo( - LuaFuture( - future = placeMaterialImpl(self, it).thenApply { tableFrom(it.map { from(it) }) }, - isLocal = false - ) - ) - } - - callbacks["placeMod"] = luaFunctionN("placeMod") { - returnBuffer.setTo(placeModImpl(self, it).getNow(listOf()).isEmpty()) - } - - callbacks["placeModPromise"] = luaFunctionN("placeModPromise") { - returnBuffer.setTo( - LuaFuture( - future = placeModImpl(self, it).thenApply { tableFrom(it.map { from(it) }) }, - isLocal = false - ) - ) + if (tile.material.isNullTile) { + return 0 + } else if (tile.material.isEmptyTile) { + args.lua.push(false) + return 1 + } else { + args.lua.push(tile.material.key) + return 1 } } + +private fun mod(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() + val tile = self.getCell(pos).tile(isBackground) + + if (tile.modifier.isNotEmptyModifier) { + args.lua.push(tile.modifier.key) + return 1 + } + + return 0 +} + +private fun materialHueShift(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() + val tile = self.getCell(pos).tile(isBackground) + + args.lua.push(tile.hueShift) + return 1 +} + +private fun modHueShift(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() + val tile = self.getCell(pos).tile(isBackground) + + args.lua.push(tile.modifierHueShift) + return 1 +} + +private fun materialColor(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() + val tile = self.getCell(pos).tile(isBackground) + + args.lua.push(tile.color.ordinal.toLong()) + return 1 +} + +private fun materialColorName(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() + val tile = self.getCell(pos).tile(isBackground) + + args.lua.push(tile.color.jsonName) + return 1 +} + +private fun setMaterialColorNumber(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() + val getColor = args.nextInt() + val color = TileColor.entries.getOrNull(getColor) ?: throw IllegalArgumentException("invalid color: $getColor") + + val cell = self.getCell(pos).mutable() + cell.tile(isBackground).color = color + args.lua.push(self.setCell(pos, cell)) + return 1 +} + +private fun setMaterialColorName(self: World<*, *>, args: LuaThread.ArgStack): Int { + val pos = args.nextVector2i() + val isBackground = args.nextBoolean() + val getColor = args.nextString() + val color = TileColor.entries.valueOf(getColor) + + val cell = self.getCell(pos).mutable() + cell.tile(isBackground).color = color + args.lua.push(self.setCell(pos, cell)) + return 1 +} + +private fun oceanLevel(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(self.template.cellInfo(args.nextVector2d()).oceanLiquidLevel.toLong()) + return 1 +} + +private fun environmentStatusEffects(self: World<*, *>, args: LuaThread.ArgStack): Int { + val effects = self.environmentStatusEffects(args.nextVector2d()) + + args.lua.pushTable(effects.size) + + for ((i, effect) in effects.withIndex()) { + args.lua.push(i + 1L) + args.lua.push(Starbound.gson.toJsonTree(effect)) + args.lua.setTableValue() + } + + return 1 +} + +private fun weatherStatusEffects(self: World<*, *>, args: LuaThread.ArgStack): Int { + val effects = self.weatherStatusEffects(args.nextVector2d()) + + args.lua.pushTable(effects.size) + + for ((i, effect) in effects.withIndex()) { + args.lua.push(i + 1L) + args.lua.push(Starbound.gson.toJsonTree(effect)) + args.lua.setTableValue() + } + + return 1 +} + +private fun damageTiles(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(damageTilesImpl(self, args).getNow(TileDamageResult.NONE) != TileDamageResult.NONE) + return 1 +} + +private fun damageTilesPromise(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push( + LuaFuture( + future = damageTilesImpl(self, args).thenApply { it.jsonName }, + isLocal = false, + handler = { + push(it) + 1 + } + ) + ) + + return 1 +} + +private fun damageTileArea(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(damageTileAreaImpl(self, args).getNow(TileDamageResult.NONE) != TileDamageResult.NONE) + return 1 +} + +private fun damageTileAreaPromise(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push( + LuaFuture( + future = damageTileAreaImpl(self, args).thenApply { it.jsonName }, + isLocal = false, + handler = { + push(it) + 1 + } + ) + ) + + return 1 +} + +private fun placeMaterial(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(placeMaterialImpl(self, args).getNow(listOf()).isEmpty()) + return 1 +} + +private fun placeMaterialPromise(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push( + LuaFuture( + future = placeMaterialImpl(self, args), + isLocal = false, + handler = { + pushTable(it.size) + + for ((i, pos) in it.withIndex()) { + push(i + 1L) + push(pos) + setTableValue() + } + + 1 + } + ) + ) + + return 1 +} + +private fun placeMod(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push(placeModImpl(self, args).getNow(listOf()).isEmpty()) + return 1 +} + +private fun placeModPromise(self: World<*, *>, args: LuaThread.ArgStack): Int { + args.lua.push( + LuaFuture( + future = placeModImpl(self, args), + isLocal = false, + handler = { + pushTable(it.size) + + for ((i, pos) in it.withIndex()) { + push(i + 1L) + push(pos) + setTableValue() + } + + 1 + } + ) + ) + return 1 +} + +fun provideWorldEnvironmentalBindings(self: World<*, *>, lua: LuaThread) { + lua.setTableValueToStub("lightLevel") + lua.setTableValueToStub("windLevel") + + lua.pushBinding(self, "breathable", ::breathable) + lua.pushBinding(self, "underground", ::underground) + + lua.pushBinding(self, "material", ::material) + lua.pushBinding(self, "mod", ::mod) + lua.pushBinding(self, "materialHueShift", ::materialHueShift) + lua.pushBinding(self, "modHueShift", ::modHueShift) + lua.pushBinding(self, "materialColor", ::materialColor) + lua.pushBinding(self, "materialColorName", ::materialColorName) + lua.pushBinding(self, "setMaterialColorNumber", ::setMaterialColorNumber) + lua.pushBinding(self, "setMaterialColorName", ::setMaterialColorName) + + lua.pushBinding(self, "oceanLevel", ::oceanLevel) + lua.pushBinding(self, "environmentStatusEffects", ::environmentStatusEffects) + lua.pushBinding(self, "weatherStatusEffects", ::weatherStatusEffects) + + lua.pushBinding(self, "damageTiles", ::damageTiles) + lua.pushBinding(self, "damageTilesPromise", ::damageTilesPromise) + + lua.pushBinding(self, "damageTileArea", ::damageTileArea) + lua.pushBinding(self, "damageTileAreaPromise", ::damageTileAreaPromise) + + lua.pushBinding(self, "placeMaterial", ::placeMaterial) + lua.pushBinding(self, "placeMaterialPromise", ::placeMaterialPromise) + + lua.pushBinding(self, "placeMod", ::placeMod) + lua.pushBinding(self, "placeModPromise", ::placeModPromise) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt index 382696fb..1010161b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaFuture.kt @@ -1,10 +1,7 @@ package ru.dbotthepony.kstarbound.lua.userdata -import org.classdump.luna.Table -import org.classdump.luna.Userdata -import org.classdump.luna.impl.ImmutableTable -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.luaFunction +import ru.dbotthepony.kstarbound.lua.LuaHandle +import ru.dbotthepony.kstarbound.lua.LuaThread import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionException @@ -15,68 +12,83 @@ import java.util.concurrent.CompletionException * * god damn it. */ -class LuaFuture(val future: CompletableFuture, val isLocal: Boolean) : Userdata>() { - override fun getMetatable(): Table { - return metadata +class LuaFuture(val future: CompletableFuture, val isLocal: Boolean, val handler: LuaThread.(T) -> Int) { + fun finished(args: LuaThread.ArgStack): Int { + args.lua.push(future.isDone) + return 1 } - override fun setMetatable(mt: Table?): Table { - throw UnsupportedOperationException() + fun succeeded(args: LuaThread.ArgStack): Int { + args.lua.push(future.isDone && !future.isCompletedExceptionally) + return 1 } - override fun getUserValue(): CompletableFuture { - return future + fun failed(args: LuaThread.ArgStack): Int { + args.lua.push(future.isCompletedExceptionally) + return 1 } - override fun setUserValue(value: CompletableFuture?): CompletableFuture { - throw UnsupportedOperationException() + fun result(args: LuaThread.ArgStack): Int { + try { + if (future.isCompletedExceptionally) { + return 0 + } else if (isLocal) { + handler(args.lua, future.join()) + return 1 + } else { + val result = future.getNow(null) ?: return 0 + return handler(args.lua, result) + } + } catch (err: CompletionException) { + return 0 + } catch (err: CancellationException) { + return 0 + } + } + + fun error(args: LuaThread.ArgStack): Int { + // this is slow, but we can't get Exception out of CompletableFuture without latter throwing former + try { + if (isLocal) { + future.join() + } else { + future.getNow(null) + } + + return 0 + } catch (err: CompletionException) { + args.lua.push(err.message ?: "internal error") + return 1 + } catch (err: CancellationException) { + args.lua.push(err.message ?: "internal error") + return 1 + } } companion object { - private fun __index(): Table { - return metadata - } + fun initializeHandle(lua: LuaThread): LuaHandle { + lua.pushTable() + val handle = lua.createHandle("LuaFuture") - private val metadata = ImmutableTable.Builder() - .add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) - .add("finished", luaFunction { self: LuaFuture -> - returnBuffer.setTo(self.future.isDone) - }) - .add("succeeded", luaFunction { self: LuaFuture -> - returnBuffer.setTo(!self.future.isCompletedExceptionally) - }) - .add("failed", luaFunction { self: LuaFuture -> - returnBuffer.setTo(self.future.isCompletedExceptionally) - }) - .add("result", luaFunction { self: LuaFuture -> - try { - if (self.future.isCompletedExceptionally) { - returnBuffer.setTo() - } else if (self.isLocal) { - returnBuffer.setTo(self.future.join()) - } else { - returnBuffer.setTo(self.future.getNow(null)) - } - } catch (err: CompletionException) { - returnBuffer.setTo() - } catch (err: CancellationException) { - returnBuffer.setTo() - } - }) - .add("error", luaFunction { self: LuaFuture -> - // this is slow, but we can't get Exception out of CompletableFuture without latter throwing former - try { - if (self.isLocal) { - returnBuffer.setTo(self.future.join()) - } else { - returnBuffer.setTo(self.future.getNow(null)) - } - } catch (err: CompletionException) { - returnBuffer.setTo(err.message ?: "internal error") - } catch (err: CancellationException) { - returnBuffer.setTo(err.message ?: "internal error") - } - }) - .build() + lua.pushBinding("finished", LuaFuture<*>::finished) + lua.pushBinding("succeeded", LuaFuture<*>::succeeded) + lua.pushBinding("failed", LuaFuture<*>::failed) + lua.pushBinding("result", LuaFuture<*>::result) + lua.pushBinding("error", LuaFuture<*>::error) + + lua.pop() + + return handle + } } } + +fun LuaThread.push(value: LuaFuture<*>) { + pushTable() + + push("__index") + push(commonHandles.future) + setTableValue() + + pushObject(value) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPathFinder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPathFinder.kt index 074d9377..06799e18 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPathFinder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaPathFinder.kt @@ -1,101 +1,126 @@ package ru.dbotthepony.kstarbound.lua.userdata -import org.classdump.luna.Table -import org.classdump.luna.TableFactory -import org.classdump.luna.Userdata -import org.classdump.luna.impl.ImmutableTable -import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.lua.from -import ru.dbotthepony.kstarbound.lua.get -import ru.dbotthepony.kstarbound.lua.luaFunction -import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.lua.tableOf -import ru.dbotthepony.kstarbound.lua.toByteString -import ru.dbotthepony.kstarbound.lua.userdata.BehaviorState.Companion +import ru.dbotthepony.kstarbound.lua.LuaHandle +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.lua.setTableValue import ru.dbotthepony.kstarbound.util.CarriedExecutor import ru.dbotthepony.kstarbound.util.supplyAsync import ru.dbotthepony.kstarbound.world.entities.PathFinder -import java.util.concurrent.CompletableFuture - -class LuaPathFinder(val pacer: CarriedExecutor, val self: PathFinder) : Userdata() { - override fun getMetatable(): Table { - return Companion.metatable - } - - override fun setMetatable(mt: Table?): Table { - throw UnsupportedOperationException() - } - - override fun getUserValue(): PathFinder { - return self - } - - override fun setUserValue(value: PathFinder?): PathFinder { - throw UnsupportedOperationException() - } +class LuaPathFinder(private val pacer: CarriedExecutor, private val self: PathFinder) { private var isRunning = false - companion object { - private val cost = "const".toByteString()!! - private val action = "action".toByteString()!! - private val jumpVelocity = "jumpVelocity".toByteString()!! - private val source = "source".toByteString()!! - private val target = "target".toByteString()!! + fun result(args: LuaThread.ArgStack): Int { + if (self.result.isEmpty) + return 0 - fun convertPath(tables: TableFactory, list: List?): Table? { - list ?: return null - val table = tables.newTable(list.size, 0) + val list = self.result.value ?: return 0 + return convertPath(args.lua, list) + } + + fun explore(args: LuaThread.ArgStack): Int { + val maxExplore = args.nextOptionalLong()?.toInt() + val useOffThread = args.nextOptionalBoolean() ?: true + + if (isRunning) { + if (self.result.isEmpty) + return 0 + else { + args.lua.push(self.result.value != null) + return 1 + } + } else if (self.result.isPresent) { + args.lua.push(self.result.value != null) + return 1 + } else { + val immediateMaxExplore = maxExplore?.times(4)?.coerceAtMost(800) ?: 800 + val result = self.run(immediateMaxExplore) + + if (result != null) { + args.lua.push(result) + return 1 + } else if (self.result.isEmpty && useOffThread) { + // didn't explore enough, run in separate thread to not block main thread + pacer.supplyAsync(self) + isRunning = true + return 0 + } + } + + return 0 + } + + fun isExploringOffThread(args: LuaThread.ArgStack): Int { + args.lua.push(isRunning && self.result.isEmpty) + return 1 + } + + fun usedOffThread(args: LuaThread.ArgStack): Int { + args.lua.push(isRunning) + return 1 + } + + fun runAsync(args: LuaThread.ArgStack): Int { + if (!isRunning) { + pacer.supplyAsync(self) + isRunning = true + } + + return 0 + } + + companion object { + fun convertPath(lua: LuaThread, list: List?): Int { + list ?: return 0 + lua.pushTable(list.size) var i = 1L for (edge in list) { - val edgeTable = tables.tableOf() - edgeTable[cost] = edge.cost - edgeTable[action] = edge.action.luaName - edgeTable[jumpVelocity] = tables.from(edge.velocity) - edgeTable[source] = edge.source.toTable(tables) - edgeTable[target] = edge.target.toTable(tables) - table[i++] = edgeTable + lua.push(i++) + + lua.pushTable(hashSize = 5) + + lua.setTableValue("cost", edge.cost) + lua.setTableValue("action", edge.action.jsonName) + lua.setTableValue("jumpVelocity", edge.velocity) + + lua.push("source") + edge.source.store(lua) + lua.setTableValue() + + lua.push("target") + edge.target.store(lua) + lua.setTableValue() + + lua.setTableValue() } - return table + return 1 } - private fun __index(): Table { - return metatable + fun initializeHandle(lua: LuaThread): LuaHandle { + lua.pushTable() + val handle = lua.createHandle("PathFinder") + + lua.pushBinding("explore", LuaPathFinder::explore) + lua.pushBinding("result", LuaPathFinder::result) + lua.pushBinding("runAsync", LuaPathFinder::runAsync) + lua.pushBinding("isExploringOffThread", LuaPathFinder::isExploringOffThread) + lua.pushBinding("usedOffThread", LuaPathFinder::usedOffThread) + + lua.pop() + + return handle } - - private val metatable = ImmutableTable.Builder() - .add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) - .add("explore", luaFunction { self: LuaPathFinder, maxExplore: Number? -> - if (self.isRunning) { - if (self.self.result.isEmpty) - returnBuffer.setTo(null) - else - returnBuffer.setTo(self.self.result.value != null) - } else if (self.self.result.isPresent) { - returnBuffer.setTo(self.self.result.value != null) - } else { - val immediateMaxExplore = maxExplore?.toInt()?.times(4)?.coerceAtMost(800) ?: 800 - val result = self.self.run(immediateMaxExplore) - - if (result != null) { - returnBuffer.setTo(result) - } else if (self.self.result.isEmpty) { - // didn't explore enough, run in separate thread to not block main thread - self.pacer.supplyAsync(self.self) - self.isRunning = true - returnBuffer.setTo(null) - } - } - }) - .add("result", luaFunction { self: LuaPathFinder -> - if (self.self.result.isEmpty) - return@luaFunction returnBuffer.setTo() - - val list = self.self.result.value ?: return@luaFunction returnBuffer.setTo() - returnBuffer.setTo(convertPath(this, list)) - }) - .build() } -} \ No newline at end of file +} + +fun LuaThread.push(value: LuaPathFinder) { + pushTable() + + push("__index") + push(commonHandles.pathFinder) + setTableValue() + + pushObject(value) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 03c0395b..a52e4979 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -308,7 +308,7 @@ class ServerWorld private constructor( queuedPlacementsInternal.remove(entry) }.exceptionally { queuedPlacementsInternal.remove(entry) - null + throw it } return future diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index ef1d56e9..c5d072c8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -13,6 +13,7 @@ import java.util.stream.Stream import kotlin.NoSuchElementException import kotlin.collections.Collection import kotlin.collections.List +import kotlin.math.floor fun String.sbIntern(): String { return Starbound.STRINGS.intern(this) @@ -103,3 +104,8 @@ fun Collection.getWrapAround(index: Int): T { return this.elementAt(positiveModulo(index, size)) } + +/** + * Rounds value down towards negative infinity (1.4 -> 1, 1.6 -> 1, -0.1 -> -1) + */ +fun Double.floorToInt() = floor(this).toInt() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt index 8c2b4af0..bdb14541 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.world +import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity @@ -171,3 +172,10 @@ fun ICellAccess.castRay( return RayCastResult(hitTiles, null, 1.0, start, start + direction * distance, direction) } + +fun ICellAccess.castRay( + line: Line2d, + filter: TileRayFilter +): RayCastResult { + return castRay(line.p0, line.p1, filter) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 95368db3..2c24385a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -48,6 +48,7 @@ import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.util.BlockableEventLoop +import ru.dbotthepony.kstarbound.util.floorToInt import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.AbstractCell @@ -72,6 +73,7 @@ import java.util.stream.Stream import kotlin.collections.ArrayList import kotlin.collections.HashMap import kotlin.math.PI +import kotlin.math.floor import kotlin.math.roundToInt abstract class World, ChunkType : Chunk>(val template: WorldTemplate) : ICellAccess { @@ -252,6 +254,10 @@ abstract class World, ChunkType : Chunk): Stream { + return collide(Poly(with), filter) + } + fun collide(point: Vector2d, filter: Predicate): Boolean { return queryTileCollisions(AABB.withSide(point, 2.0)).any { filter.test(it) && point in it.poly } } @@ -838,7 +844,7 @@ abstract class World, ChunkType : Chunk { - return environmentStatusEffects(x.toInt(), y.toInt()) + return environmentStatusEffects(x.floorToInt(), y.floorToInt()) } fun environmentStatusEffects(pos: IStruct2i): Collection { @@ -857,7 +863,7 @@ abstract class World, ChunkType : Chunk { - return weatherStatusEffects(x.toInt(), y.toInt()) + return weatherStatusEffects(x.floorToInt(), y.floorToInt()) } fun weatherStatusEffects(pos: IStruct2i): Collection { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathFinder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathFinder.kt index fc0cd89c..7c00f892 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathFinder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathFinder.kt @@ -11,8 +11,10 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.lua.LuaThread import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.setTableValue import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.math.AABB @@ -159,6 +161,12 @@ class PathFinder(val world: World<*, *>, val start: Vector2d, val goal: Vector2d return table } + fun store(lua: LuaThread) { + lua.pushTable(hashSize = 2) + lua.setTableValue("position", position) + lua.setTableValue("velocity", velocity) + } + fun reconstructPath(): MutableList { val result = ArrayList() var parent = parent diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StagehandEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StagehandEntity.kt index af88a7dc..a1dc6897 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StagehandEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StagehandEntity.kt @@ -63,6 +63,9 @@ class StagehandEntity(isRemote: Boolean = false) : AbstractEntity(), ScriptedEnt private var config: JsonObject = JsonObject() + val typeName: String + get() = config["type"]?.asString ?: "" + var isScripted = false private set diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/ScriptedEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/ScriptedEntity.kt index 15d2f6b3..f237c4b1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/ScriptedEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/ScriptedEntity.kt @@ -1,9 +1,12 @@ package ru.dbotthepony.kstarbound.world.entities.api +import com.google.gson.JsonArray +import com.google.gson.JsonElement + interface ScriptedEntity { // Call a script function directly with the given arguments, should return // nothing only on failure. - fun callScript(fnName: String, vararg arguments: Any?): Array + fun callScript(fnName: String, arguments: JsonArray): JsonElement // Execute the given code directly in the underlying context, return nothing // on failure. diff --git a/src/main/resources/scripts/global.lua b/src/main/resources/scripts/global.lua index ad5b96e2..c43aa0c9 100644 --- a/src/main/resources/scripts/global.lua +++ b/src/main/resources/scripts/global.lua @@ -16,12 +16,14 @@ local math = math local string = string local format = string.format -local function checkarg(value, index, expected, fnName, overrideExpected) +function checkarg(value, index, expected, fnName, overrideExpected) if type(value) ~= expected then error(string.format('bad argument #%d to %s: %s expected, got %s', index, fnName, overrideExpected or expected, type(value)), 3) end end +local checkarg = checkarg + -- this replicates original engine code, but it shouldn't work in first place local function __newindex(self, key, value) local nils = getmetatable(self).__nils diff --git a/src/main/resources/scripts/server_world.lua b/src/main/resources/scripts/server_world.lua new file mode 100644 index 00000000..8476a8c4 --- /dev/null +++ b/src/main/resources/scripts/server_world.lua @@ -0,0 +1,20 @@ + +function world.loadUniqueEntity(name) + -- no, I give up, this shit is beyond bonkers + -- I don't care that mods could break + -- mods WILL break + -- just please, I beg you, for the love of God and Jesus, + -- stop fucking designing async functions as sync ones + + local future = world.loadUniqueEntityAsync(name) + + while not future:finished() do + coroutine.yield() + end + + return future:result() +end + +function world.fidelity() + return 'high' +end diff --git a/src/main/resources/scripts/world.lua b/src/main/resources/scripts/world.lua new file mode 100644 index 00000000..679330a0 --- /dev/null +++ b/src/main/resources/scripts/world.lua @@ -0,0 +1,502 @@ + +local string_lower = string.lower +local checkarg = checkarg + +local function determineLayer(layer) + if layer == 'foreground' then + return false + elseif layer == 'background' then + return true + else + error('Unknown tile layer ' .. tostring(layer), 3) + end +end + +local functionsWithLayers = { + 'material', + 'mod', + 'materialHueShift', + 'modHueShift', + 'materialColor', + 'materialColorName', + 'damageTiles', + 'damageTilesPromise', + 'placeMaterial', + 'placeMaterialPromise', + 'placeMod', + 'placeModPromise', +} + +for _, fnName in ipairs(functionsWithLayers) do + local impl = assert(world[fnName]) + + world[fnName] = function(pos, layer, ...) + return impl(pos, determineLayer(layer), ...) + end +end + +local __setMaterialColorNumber = world.setMaterialColorNumber +local __setMaterialColorName = world.setMaterialColorName + +function world.setMaterialColor(pos, layer, value) + if type(value) == 'number' then + return __setMaterialColorNumber(pos, determineLayer(layer), value) + else + return __setMaterialColorName(pos, determineLayer(layer), value) + end +end + +for _, fnName in ipairs({'damageTileArea', 'damageTileAreaPromise'}) do + local impl = assert(world[fnName]) + + world[fnName] = function(a, b, layer, ...) + return impl(a, b, determineLayer(layer), ...) + end +end + +for _, name in ipairs({'entityHandItem', 'entityHandItemDescriptor'}) do + local impl = assert(world[name]) + local fullName = 'world.' .. name + + world[name] = function(id, hand, ...) + checkarg(hand, 2, 'string', fullName) + + local lower = string_lower(hand) + + if hand == 'primary' then + return impl(id, true, ...) + elseif hand == 'alt' or hand == 'secondary' then + return impl(id, false, ...) + else + error('unknown tool hand ' .. hand, 2) + end + end +end + +local entityTypes = { + plant = 0, + object = 1, + vehicle = 2, + itemDrop = 3, + plantDrop = 4, + projectile = 5, + stagehand = 6, + monster = 7, + npc = 8, + player = 9, + mobile = 10, + creature = 11, +} + +local function entityTypeNamesToIntegers(input, fullName) + if input then + for i, v in ipairs(input) do + -- could have used string.lower, but original engine does case-sensitive comparison here + -- so we can save quite a lot cpu cycles for ourselves by not doing string.lower + local lookup = entityTypes[v] + + if not lookup then + error('invalid entity type ' .. tostring(v) .. ' for ' .. fullName .. ' in types table at index ' .. i, 3) + end + + entityTypes[i] = lookup + end + + return input + end +end + +local boundModes = { + metaboundbox = 0, + collisionarea = 1, + position = 2 +} + +local function boundMode(input, fullName) + -- collision area by default + if not input then return 1 end + + if type(input) ~= 'string' then + error('bad "boundMode" for ' .. fullName .. ', not a string: ' .. type(input), 3) + end + + local lookup = boundModes[string_lower(input)] + + if not lookup then + error('bad "boundMode": ' .. tostring(input), 3) + end + + return lookup +end + +local function order(input, fullName) + if not input then + return 0 + elseif input == 'random' then + return 1 + elseif input == 'nearest' then + return 2 + else + error('bad "order" for ' .. fullName .. ': ' .. tostring(input), 3) + end +end + +local regularQueryFunctions = { + ['entity%sQuery'] = 'query%sImpl', + ['monster%sQuery'] = 'monsterQuery%sImpl', + ['npc%sQuery'] = 'npcQuery%sImpl', + ['itemDrop%sQuery'] = 'itemDropQuery%sImpl', + ['player%sQuery'] = 'playerQuery%sImpl', +} + +for fnName, implName in pairs(regularQueryFunctions) do + do + local aabbImpl = assert(world[string.format(implName, 'AABB')]) + local radiusImpl = assert(world[string.format(implName, 'Radius')]) + local fnName = string.format(fnName, '') + local fullName = 'world.' .. fnName + + world[fnName] = function(point, pointOrRadius, options) + options = options or {} + checkarg(point, 1, 'table', fullName, 'Vector2d') + checkarg(options, 3, 'table', fullName) + + if options.callScriptArgs and type(options.callScriptArgs) ~= 'table' then + error('bad "callScriptArgs" in "options" for ' .. fullName .. ', not a table: ' .. type(options.callScriptArgs), 2) + end + + if type(pointOrRadius) == 'table' then + -- AABB + return aabbImpl( + {point[1], point[2], pointOrRadius[1], pointOrRadius[2]}, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + else + -- point + radius + checkarg(pointOrRadius, 2, 'number', fullName, 'number or Vector2d') + + return radiusImpl( + point, + pointOrRadius, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + end + end + end + + do + local lineImpl = assert(world[string.format(implName, 'Line')]) + local fnName = string.format(fnName, 'Line') + local fullName = 'world.' .. fnName + + world[fnName] = function(p0, p1, options) + options = options or {} + checkarg(p0, 1, 'table', fullName, 'Vector2d') + checkarg(p1, 2, 'table', fullName, 'Vector2d') + + if options.callScriptArgs and type(options.callScriptArgs) ~= 'table' then + error('bad "callScriptArgs" in "options" for ' .. fullName .. ', not a table: ' .. type(options.callScriptArgs), 2) + end + + return lineImpl( + p0, p1, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + end + end + + do + local polyImpl = assert(world[string.format(implName, 'Poly')]) + local fnName = string.format(fnName, 'Poly') + local fullName = 'world.' .. fnName + + world[fnName] = function(poly, options) + options = options or {} + checkarg(poly, 1, 'table', fullName, 'Poly') + + if options.callScriptArgs and type(options.callScriptArgs) ~= 'table' then + error('bad "callScriptArgs" in "options" for ' .. fullName .. ', not a table: ' .. type(options.callScriptArgs), 2) + end + + return polyImpl( + poly, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + end + end +end + +do + local fnName = 'object%sQuery' + local implName = 'queryObject%sImpl' + + do + local aabbImpl = assert(world[string.format(implName, 'AABB')]) + local radiusImpl = assert(world[string.format(implName, 'Radius')]) + local fnName = string.format(fnName, '') + local fullName = 'world.' .. fnName + + world[fnName] = function(point, pointOrRadius, options) + options = options or {} + checkarg(point, 1, 'table', fullName, 'Vector2d') + checkarg(options, 3, 'table', fullName) + + if options.callScriptArgs and type(options.callScriptArgs) ~= 'table' then + error('bad "callScriptArgs" in "options" for ' .. fullName .. ', not a table: ' .. type(options.callScriptArgs), 2) + end + + if options.name and type(options.name) ~= 'string' then + error('bad "name" in "options" for ' .. fullName .. ', not a table: ' .. type(options.name), 2) + end + + if type(pointOrRadius) == 'table' then + -- AABB + return aabbImpl( + options.name, + {point[1], point[2], pointOrRadius[1], pointOrRadius[2]}, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + else + -- point + radius + checkarg(pointOrRadius, 2, 'number', fullName, 'number or Vector2d') + + return radiusImpl( + options.name, + point, + pointOrRadius, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + end + end + end + + do + local lineImpl = assert(world[string.format(implName, 'Line')]) + local fnName = string.format(fnName, 'Line') + local fullName = 'world.' .. fnName + + world[fnName] = function(p0, p1, options) + options = options or {} + checkarg(p0, 1, 'table', fullName, 'Vector2d') + checkarg(p1, 2, 'table', fullName, 'Vector2d') + + if options.callScriptArgs and type(options.callScriptArgs) ~= 'table' then + error('bad "callScriptArgs" in "options" for ' .. fullName .. ', not a table: ' .. type(options.callScriptArgs), 2) + end + + if options.name and type(options.name) ~= 'string' then + error('bad "name" in "options" for ' .. fullName .. ', not a table: ' .. type(options.name), 2) + end + + return lineImpl( + options.name, + p0, p1, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + end + end + + do + local polyImpl = assert(world[string.format(implName, 'Poly')]) + local fnName = string.format(fnName, 'Poly') + local fullName = 'world.' .. fnName + + world[fnName] = function(poly, options) + options = options or {} + checkarg(poly, 1, 'table', fullName, 'Poly') + + if options.callScriptArgs and type(options.callScriptArgs) ~= 'table' then + error('bad "callScriptArgs" in "options" for ' .. fullName .. ', not a table: ' .. type(options.callScriptArgs), 2) + end + + if options.name and type(options.name) ~= 'string' then + error('bad "name" in "options" for ' .. fullName .. ', not a table: ' .. type(options.name), 2) + end + + return polyImpl( + options.name, + poly, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + end + end +end + +do + local fnName = 'loungeable%sQuery' + local implName = 'queryLoungeable%sImpl' + + local orientations = { + none = 0, + sit = 1, + lay = 2, + stand = 3 + } + + local function orientation(input, fullName) + if not input then return 0 end + + local lookup = orientations[input] + + if not lookup then + if type(input) ~= 'string' then + error('bad "orientation" in "options" for ' .. fullName .. ', not a string: ' .. type(input), 3) + else + error('bad "orientation" in "options" for ' .. fullName .. ', not a valid orientation: ' .. input, 3) + end + end + + return lookup + end + + do + local aabbImpl = assert(world[string.format(implName, 'AABB')]) + local radiusImpl = assert(world[string.format(implName, 'Radius')]) + local fnName = string.format(fnName, '') + local fullName = 'world.' .. fnName + + world[fnName] = function(point, pointOrRadius, options) + options = options or {} + checkarg(point, 1, 'table', fullName, 'Vector2d') + checkarg(options, 3, 'table', fullName) + + if options.callScriptArgs and type(options.callScriptArgs) ~= 'table' then + error('bad "callScriptArgs" in "options" for ' .. fullName .. ', not a table: ' .. type(options.callScriptArgs), 2) + end + + if type(pointOrRadius) == 'table' then + -- AABB + return aabbImpl( + orientation(options.orientation), + {point[1], point[2], pointOrRadius[1], pointOrRadius[2]}, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + else + -- point + radius + checkarg(pointOrRadius, 2, 'number', fullName, 'number or Vector2d') + + return radiusImpl( + orientation(options.orientation), + point, + pointOrRadius, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + end + end + end + + do + local lineImpl = assert(world[string.format(implName, 'Line')]) + local fnName = string.format(fnName, 'Line') + local fullName = 'world.' .. fnName + + world[fnName] = function(p0, p1, options) + options = options or {} + checkarg(p0, 1, 'table', fullName, 'Vector2d') + checkarg(p1, 2, 'table', fullName, 'Vector2d') + + if options.callScriptArgs and type(options.callScriptArgs) ~= 'table' then + error('bad "callScriptArgs" in "options" for ' .. fullName .. ', not a table: ' .. type(options.callScriptArgs), 2) + end + + return lineImpl( + orientation(options.orientation), + p0, p1, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + end + end + + do + local polyImpl = assert(world[string.format(implName, 'Poly')]) + local fnName = string.format(fnName, 'Poly') + local fullName = 'world.' .. fnName + + world[fnName] = function(poly, options) + options = options or {} + checkarg(poly, 1, 'table', fullName, 'Poly') + + if options.callScriptArgs and type(options.callScriptArgs) ~= 'table' then + error('bad "callScriptArgs" in "options" for ' .. fullName .. ', not a table: ' .. type(options.callScriptArgs), 2) + end + + return polyImpl( + orientation(options.orientation), + poly, + entityTypeNamesToIntegers(options.includedTypes, fullName), + options.withoutEntityId, + boundMode(options.boundMode, fullName), + options.callScript, + options.callScriptArgs or {}, + options.callScriptResult, + order(options.order, fullName), + ) + end + end +end diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/LuaTests.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/LuaTests.kt index 99a3aa8f..81750ae1 100644 --- a/src/test/kotlin/ru/dbotthepony/kstarbound/test/LuaTests.kt +++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/LuaTests.kt @@ -1,14 +1,24 @@ package ru.dbotthepony.kstarbound.test +import com.kenai.jffi.MemoryIO import org.junit.jupiter.api.Test -import ru.dbotthepony.kstarbound.lua.LuaState +import ru.dbotthepony.kstarbound.lua.LuaHandle +import ru.dbotthepony.kstarbound.lua.LuaThread +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import java.io.File object LuaTests { @Test fun test() { - val lua = LuaState() - lua.load("print('Hello, world!')") - lua.call() + val lua = LuaThread() + + lua.ensureExtraCapacity(1000) + + lua.loadGlobal("collectgarbage") + lua.push("count") + lua.call(1, 1) + println(lua.popDouble()!! * 1024) + lua.close() } }