Migrate world bindings to PUC Lua

This commit is contained in:
DBotThePony 2024-12-22 21:54:12 +07:00
parent 9687c25bb0
commit 311c6a4e47
Signed by: DBot
GPG Key ID: DCC23B5715498507
31 changed files with 3870 additions and 1910 deletions

View File

@ -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 `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.clamp(value, min, max)`
* Added `math.lerp(t, a, b)` * 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 ## 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 * 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.minimumLiquidStatusEffectPercentage(): Double`
* Added `status.setMinimumLiquidStatusEffectPercentage(value: 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<PathNode>?` 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 ## world
#### Additions #### 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<LiquidState>`, will return Liquid' name instead of its ID * Added `world.liquidNamesAlongLine(start: Vector2d, end: Vector2d): List<LiquidState>`, will return Liquid' name instead of its ID
* Added `world.liquidNameAt(at: Vector2i): LiquidState?`, 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<String>?`, will return Block names instead of their IDs * Added `world.biomeBlockNamesAt(at: Vector2i): List<String>?`, 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<Boolean, LiquidState?>`, 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.gravityVector(at: Vector2d): Vector2d`. **Attention:** directional gravity is WIP.
* Added `world.itemDropLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>` * Added `world.itemDropLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* Added `world.playerLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>` * Added `world.playerLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>` * Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>` * Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* 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<EntityID>`
* Added `world.itemDropPolyQuery(poly: Poly, options: Table?): List<EntityID>`
* Added `world.npcPolyQuery(poly: Poly, options: Table?): List<EntityID>`
* Added `world.monsterPolyQuery(poly: Poly, options: Table?): List<EntityID>`
* Added `world.playerPolyQuery(poly: Poly, options: Table?): List<EntityID>`
* Added `world.objectPolyQuery(poly: Poly, options: Table?): List<EntityID>`
* Added `world.loungeablePolyQuery(poly: Poly, options: Table?): List<EntityID>`
* Added `world.loadUniqueEntityAsync(id: String): RpcPromise<EntityID>` * Added `world.loadUniqueEntityAsync(id: String): RpcPromise<EntityID>`
* Added `world.findUniqueEntityAsync(id: String): RpcPromise<Vector2d?>` * Added `world.findUniqueEntityAsync(id: String): RpcPromise<Vector2d?>`
* `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 * `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 all tiles were protected, it will return `"protected"`.
* If none tiles were damaged, it will return `"none"`. * 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<String>`, with same notes as `world.damageTilesPromise()` apply * Added `world.damageTileAreaPromise(radius: Double, position: Vector2i, layer: String, damageSource: Vector2d, damageType: String, damageAmount: Double, harvestLevel: Int = 999, sourceEntity: EntityID = 0): RpcPromise<String>`, with same notes as `world.damageTilesPromise()` apply
* Added `world.placeMaterialPromise(pos: Vector2i, layer: String, material: String, hueShift: Number?, allowOverlap: Boolean): RpcPromise<Vector2i>`, returning promise of unapplied tile modifications * Added `world.placeMaterialPromise(pos: Vector2i, layer: String, material: String, hueShift: Number?, allowOverlap: Boolean): RpcPromise<List<Vector2i>>`, returning promise of unapplied tile modifications
* However, correct clientside result will be returned _only_ when using native protocol * 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<Vector2i>`, returning promise of unapplied tile modifications * Added `world.placeModPromise(pos: Vector2i, layer: String, modifier: String, hueShift: Number?, allowOverlap: Boolean): RpcPromise<List<Vector2i>>`, returning promise of unapplied tile modifications
* However, correct clientside result will be returned _only_ when using native protocol * However, correct clientside result will be returned _only_ when using native protocol
* Added `world.weatherStatusEffects(pos: Vector2i): List<EphemeralStatusEffect>`
* 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 #### 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.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.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) * `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) * `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<EntityID>` * If your mod is using it **PLEASE** switch to `world.loadUniqueEntityAsync(id: String): RpcPromise<EntityID>`
* `world.spawnLiquid()` is deprecated, use `world.spawnLiquidPromise()` instead * `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.damageTiles()` is deprecated, use `world.damageTilesPromise()` instead
* `world.damageTileArea()` is deprecated, use `world.damageTileAreaPromise()` instead * `world.damageTileArea()` is deprecated, use `world.damageTileAreaPromise()` instead
* `world.placeMaterial()` is deprecated, use `world.placeMaterialPromise()` instead * `world.placeMaterial()` is deprecated, use `world.placeMaterialPromise()` instead

View File

@ -159,7 +159,7 @@ public interface LuaJNR {
// проверка стека // проверка стека
@IgnoreError @IgnoreError
public int lua_checkstack(@NotNull Pointer luaState, int value); public boolean lua_checkstack(@NotNull Pointer luaState, int value);
@IgnoreError @IgnoreError
public int lua_absindex(@NotNull Pointer luaState, int value); public int lua_absindex(@NotNull Pointer luaState, int value);
@IgnoreError @IgnoreError

View File

@ -276,6 +276,10 @@ class Registry<T : Any>(val name: String, val storeJson: Boolean = true) {
} }
} }
fun ref(index: Either<String, Int>): Ref<T> {
return index.map({ ref(it) }, { ref(it) })
}
operator fun contains(index: String) = lock.read { index in keysInternal } operator fun contains(index: String) = lock.read { index in keysInternal }
operator fun contains(index: Int) = lock.read { index in idsInternal } operator fun contains(index: Int) = lock.read { index in idsInternal }

View File

@ -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.PlantPieceEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.io.DataInputStream 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 { 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) { 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 suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity
abstract fun fromStorage(data: JsonObject): AbstractEntity abstract fun fromStorage(data: JsonObject): AbstractEntity
companion object {
val ALL: Set<EntityType> = Collections.unmodifiableSet(EnumSet.allOf(EntityType::class.java))
}
} }

View File

@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.quintic2 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.random
import ru.dbotthepony.kstarbound.util.random.staticRandomInt import ru.dbotthepony.kstarbound.util.random.staticRandomInt
import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.Universe
@ -32,6 +33,7 @@ import ru.dbotthepony.kstarbound.world.physics.Poly
import java.time.Duration import java.time.Duration
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import kotlin.math.floor
class WorldTemplate(val geometry: WorldGeometry) { class WorldTemplate(val geometry: WorldGeometry) {
var seed: Long = 0L var seed: Long = 0L
@ -326,16 +328,23 @@ class WorldTemplate(val geometry: WorldGeometry) {
} }
fun cellInfo(x: Int, y: Int): CellInfo { fun cellInfo(x: Int, y: Int): CellInfo {
worldLayout ?: return CellInfo(x, y) val pos = geometry.wrap(x, y)
val vec = Vector2i(x, y)
return cellCache[vec.hashCode() and 255].get(vec)
}
fun cellInfo(pos: Vector2i): CellInfo {
worldLayout ?: return CellInfo(pos.x, pos.y) worldLayout ?: return CellInfo(pos.x, pos.y)
return cellCache[pos.hashCode() and 255].get(pos) 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 { private fun cellInfo0(x: Int, y: Int): CellInfo {
val info = CellInfo(x, y) val info = CellInfo(x, y)
val layout = worldLayout ?: return info val layout = worldLayout ?: return info

View File

@ -0,0 +1,6 @@
package ru.dbotthepony.kstarbound.lua
data class CommonHandleRegistry(
val future: LuaHandle,
val pathFinder: LuaHandle,
)

View File

@ -15,6 +15,7 @@ import org.classdump.luna.runtime.AbstractFunction3
import org.classdump.luna.runtime.ExecutionContext import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2f import ru.dbotthepony.kommons.util.IStruct2f
@ -556,6 +557,63 @@ fun LuaThread.ArgStack.nextOptionalVector2d(position: Int = this.position++): Ve
return lua.getVector2d(position) return lua.getVector2d(position)
} }
fun LuaThread.getVector2iOrAABB(stackIndex: Int = -1): Either<Vector2i, AABB>? {
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<Vector2i, AABB> {
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<Vector2i, AABB>? {
if (position !in 1 ..this.top)
return null
return lua.getVector2iOrAABB(position)
}
fun LuaThread.getRegistryID(stackIndex: Int = -1): Either<String, Int>? {
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<String, Int> {
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<String, Int>? {
if (position !in 1 ..this.top)
return null
return lua.getRegistryID(position)
}
fun LuaThread.getVector2i(stackIndex: Int = -1): Vector2i? { fun LuaThread.getVector2i(stackIndex: Int = -1): Vector2i? {
val abs = this.absStackIndex(stackIndex) val abs = this.absStackIndex(stackIndex)
@ -750,7 +808,8 @@ fun LuaThread.ArgStack.nextOptionalAABBi(position: Int = this.position++): AABBi
return lua.getAABBi(position) return lua.getAABBi(position)
} }
fun LuaThread.push(value: IStruct4i) { fun LuaThread.push(value: IStruct4i?) {
value ?: return push()
pushTable(arraySize = 4) pushTable(arraySize = 4)
val table = stackTop val table = stackTop
val (x, y, z, w) = value val (x, y, z, w) = value
@ -772,7 +831,8 @@ fun LuaThread.push(value: IStruct4i) {
setTableValue(table) setTableValue(table)
} }
fun LuaThread.push(value: IStruct3i) { fun LuaThread.push(value: IStruct3i?) {
value ?: return push()
pushTable(arraySize = 3) pushTable(arraySize = 3)
val table = stackTop val table = stackTop
val (x, y, z) = value val (x, y, z) = value
@ -790,7 +850,8 @@ fun LuaThread.push(value: IStruct3i) {
setTableValue(table) setTableValue(table)
} }
fun LuaThread.push(value: IStruct2i) { fun LuaThread.push(value: IStruct2i?) {
value ?: return push()
pushTable(arraySize = 2) pushTable(arraySize = 2)
val table = stackTop val table = stackTop
val (x, y) = value val (x, y) = value
@ -804,7 +865,8 @@ fun LuaThread.push(value: IStruct2i) {
setTableValue(table) setTableValue(table)
} }
fun LuaThread.push(value: IStruct4f) { fun LuaThread.push(value: IStruct4f?) {
value ?: return push()
pushTable(arraySize = 4) pushTable(arraySize = 4)
val table = stackTop val table = stackTop
val (x, y, z, w) = value val (x, y, z, w) = value
@ -826,7 +888,8 @@ fun LuaThread.push(value: IStruct4f) {
setTableValue(table) setTableValue(table)
} }
fun LuaThread.push(value: IStruct3f) { fun LuaThread.push(value: IStruct3f?) {
value ?: return push()
pushTable(arraySize = 3) pushTable(arraySize = 3)
val table = stackTop val table = stackTop
val (x, y, z) = value val (x, y, z) = value
@ -844,7 +907,8 @@ fun LuaThread.push(value: IStruct3f) {
setTableValue(table) setTableValue(table)
} }
fun LuaThread.push(value: IStruct2f) { fun LuaThread.push(value: IStruct2f?) {
value ?: return push()
pushTable(arraySize = 2) pushTable(arraySize = 2)
val table = stackTop val table = stackTop
val (x, y) = value val (x, y) = value
@ -858,7 +922,8 @@ fun LuaThread.push(value: IStruct2f) {
setTableValue(table) setTableValue(table)
} }
fun LuaThread.push(value: IStruct4d) { fun LuaThread.push(value: IStruct4d?) {
value ?: return push()
pushTable(arraySize = 4) pushTable(arraySize = 4)
val table = stackTop val table = stackTop
val (x, y, z, w) = value val (x, y, z, w) = value
@ -880,7 +945,8 @@ fun LuaThread.push(value: IStruct4d) {
setTableValue(table) setTableValue(table)
} }
fun LuaThread.push(value: IStruct3d) { fun LuaThread.push(value: IStruct3d?) {
value ?: return push()
pushTable(arraySize = 3) pushTable(arraySize = 3)
val table = stackTop val table = stackTop
val (x, y, z) = value val (x, y, z) = value
@ -898,7 +964,8 @@ fun LuaThread.push(value: IStruct3d) {
setTableValue(table) setTableValue(table)
} }
fun LuaThread.push(value: IStruct2d) { fun LuaThread.push(value: IStruct2d?) {
value ?: return push()
pushTable(arraySize = 2) pushTable(arraySize = 2)
val table = stackTop val table = stackTop
val (x, y) = value val (x, y) = value
@ -911,3 +978,87 @@ fun LuaThread.push(value: IStruct2d) {
push(y) push(y)
setTableValue(table) 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)
}

View File

@ -2,9 +2,10 @@ package ru.dbotthepony.kstarbound.lua
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import java.io.Closeable import java.io.Closeable
import java.lang.ref.Cleaner.Cleanable
class LuaHandle(private val parent: LuaHandleThread, val handle: Int) : Closeable { class LuaHandle(private val parent: LuaSharedState, val handle: Int, val key: Any?) : Closeable {
private val cleanables = ArrayList<Runnable>() private val cleanable: Cleanable
var isValid = true var isValid = true
private set private set
@ -12,28 +13,24 @@ class LuaHandle(private val parent: LuaHandleThread, val handle: Int) : Closeabl
init { init {
val parent = parent val parent = parent
val handle = handle val handle = handle
val key = key
cleanables.add(Starbound.CLEANER.register(this) { cleanable = Starbound.CLEANER.register(this) {
parent.freeHandle(handle) parent.freeHandle(handle, key)
}::clean) }
} }
fun push(into: LuaThread) { fun push(into: LuaThread) {
check(isValid) { "Tried to use NULL handle!" } check(isValid) { "Tried to use NULL handle!" }
parent.thread.push() parent.handlesThread.push()
parent.thread.copy(handle, -1) parent.handlesThread.copy(handle, -1)
parent.thread.moveStackValuesOnto(into) parent.handlesThread.moveStackValuesOnto(into)
}
fun onClose(cleanable: Runnable) {
check(isValid) { "No longer valid" }
cleanables.add(cleanable)
} }
override fun close() { override fun close() {
if (!isValid) return if (!isValid) return
cleanables.forEach { it.run() } cleanable.clean()
isValid = false isValid = false
} }
} }

View File

@ -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<Int>()
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)
}
}
}

View File

@ -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<Int>()
private val freeHandles = IntAVLTreeSet()
private var nextHandle = 0
// faster code path
private var handlesInUse = 0
private val namedHandles = HashMap<Any, LuaHandle>()
var random: RandomGenerator = random()
var commonHandles by Delegates.notNull<CommonHandleRegistry>()
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]
}
}

View File

@ -17,24 +17,28 @@ import org.apache.logging.log4j.LogManager
import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.Delegate
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings
import ru.dbotthepony.kstarbound.util.random.random
import java.io.Closeable import java.io.Closeable
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import kotlin.math.floor import kotlin.math.floor
import kotlin.properties.Delegates import kotlin.properties.Delegates
import kotlin.system.exitProcess 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") @Suppress("unused")
class LuaThread private constructor( class LuaThread private constructor(
private val pointer: Pointer, private val pointer: Pointer,
@ -52,7 +56,7 @@ class LuaThread private constructor(
CallContext.getCallContext(Type.SINT, arrayOf(Type.POINTER), CallingConvention.DEFAULT, false) 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) LuaJNR.INSTANCE.lua_close(pointer)
panic.dispose() panic.dispose()
} }
@ -60,8 +64,19 @@ class LuaThread private constructor(
panic.setAutoRelease(false) panic.setAutoRelease(false)
LuaJNR.INSTANCE.lua_atpanic(pointer, panic.address) LuaJNR.INSTANCE.lua_atpanic(pointer, panic.address)
randomHolder = Delegate.Box(random()) val handles = LuaJNR.INSTANCE.lua_newthread(pointer)
handleThread = LuaHandleThread(this) 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) LuaJNR.INSTANCE.luaopen_base(this.pointer)
this.storeGlobal("_G") this.storeGlobal("_G")
@ -76,10 +91,12 @@ class LuaThread private constructor(
LuaJNR.INSTANCE.luaopen_utf8(this.pointer) LuaJNR.INSTANCE.luaopen_utf8(this.pointer)
this.storeGlobal("utf8") this.storeGlobal("utf8")
sharedState.initializeHandles(this)
provideUtilityBindings(this) provideUtilityBindings(this)
provideRootBindings(this) provideRootBindings(this)
load(globalScript, "@starbound.jar!/scripts/global.lua") load(globalScript, "@/internal/global.lua")
call() call()
} }
@ -87,9 +104,20 @@ class LuaThread private constructor(
fun invoke(args: ArgStack): Int fun invoke(args: ArgStack): Int
} }
fun interface Binding<T> {
fun invoke(self: T, arguments: ArgStack): Int
}
fun interface Binding1<T, A0> {
fun invoke(self: T, extra: A0, arguments: ArgStack): Int
}
fun interface Binding2<T, A0, A1> {
fun invoke(self: T, extra: A0, extra1: A1, arguments: ArgStack): Int
}
private var cleanable: Cleaner.Cleanable? = null private var cleanable: Cleaner.Cleanable? = null
private var randomHolder: Delegate<RandomGenerator> by Delegates.notNull() private var sharedState by Delegates.notNull<LuaSharedState>()
private var handleThread by Delegates.notNull<LuaHandleThread>()
/** /**
* Responsible for generating random numbers using math.random * 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 * math.randomseed sets this property to brand-new generator with required seed
*/ */
var random: RandomGenerator var random: RandomGenerator
get() = randomHolder.get() get() = sharedState.random
set(value) = randomHolder.accept(value) set(value) { sharedState.random = value }
private fun initializeFrom(other: LuaThread, skipHandle: Boolean) { val commonHandles get() = sharedState.commonHandles
randomHolder = other.randomHolder
if (!skipHandle) fun newThread(): LuaThread {
handleThread = other.handleThread
}
private fun cleanup() {
handleThread.cleanup()
}
fun newThread(skipHandle: Boolean = false): LuaThread {
val pointer = LuaJNR.INSTANCE.lua_newthread(pointer) val pointer = LuaJNR.INSTANCE.lua_newthread(pointer)
return LuaThread(pointer, stringInterner).also { 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 { 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) val status = LuaJNR.INSTANCE.lua_pcallk(this.pointer, numArgs, numResults, 0, 0L, 0L)
if (status == LUA_ERRRUN) { if (status == LUA_ERRRUN) {
@ -407,6 +426,20 @@ class LuaThread private constructor(
return value 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 { private fun getLongRaw(stackIndex: Int = -1): Long {
val stack = MemoryStack.stackPush() val stack = MemoryStack.stackPush()
val status = stack.mallocInt(1) val status = stack.mallocInt(1)
@ -623,11 +656,11 @@ class LuaThread private constructor(
return LuaJNI.lua_tojobject(pointer.address(), stackIndex) 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) val abs = this.absStackIndex(stackIndex)
if (!this.isTable(abs)) if (!this.isTable(abs))
return return false
this.push() this.push()
val top = this.stackTop val top = this.stackTop
@ -641,6 +674,8 @@ class LuaThread private constructor(
} finally { } finally {
LuaJNR.INSTANCE.lua_settop(this.pointer, top - 1) LuaJNR.INSTANCE.lua_settop(this.pointer, top - 1)
} }
return true
} }
fun <T> readTableKeys(stackIndex: Int = -1, keyVisitor: LuaThread.(stackIndex: Int) -> T): MutableList<T>? { fun <T> readTableKeys(stackIndex: Int = -1, keyVisitor: LuaThread.(stackIndex: Int) -> T): MutableList<T>? {
@ -795,6 +830,12 @@ class LuaThread private constructor(
val lua get() = this@LuaThread val lua get() = this@LuaThread
var position = 1 var position = 1
init {
if (top >= 10) {
this@LuaThread.ensureExtraCapacity(10)
}
}
fun peek(position: Int = this.position): LuaType { fun peek(position: Int = this.position): LuaType {
if (position !in 1 .. top) if (position !in 1 .. top)
return LuaType.NONE return LuaType.NONE
@ -802,10 +843,63 @@ class LuaThread private constructor(
return this@LuaThread.typeAt(position) return this@LuaThread.typeAt(position)
} }
fun peekAndSkipNothing(): LuaType {
val peek = peek()
if (peek.isNothing) position++
return peek
}
fun hasNext(): Boolean { fun hasNext(): Boolean {
return position <= top 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 <T> readTableKeys(position: Int = this.position++, keyVisitor: LuaThread.(stackIndex: Int) -> T): MutableList<T> {
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 <T> readTableValues(position: Int = this.position++, valueVisitor: LuaThread.(stackIndex: Int) -> T): MutableList<T> {
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 <K, V> readTable(position: Int = this.position++, keyVisitor: LuaThread.(stackIndex: Int) -> K, valueVisitor: LuaThread.(stackIndex: Int) -> V): MutableList<Pair<K, V>> {
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 <reified T> nextObject(position: Int = this.position++): T { inline fun <reified T> nextObject(position: Int = this.position++): T {
if (position !in 1 ..this.top) if (position !in 1 ..this.top)
throw IllegalArgumentException("bad argument #$position: Java object expected, got nil") 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)}") ?: 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 { fun nextJson(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement {
if (position !in 1 ..this.top) if (position !in 1 ..this.top)
throw IllegalArgumentException("bad argument #$position: json expected, got nil") 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)}") 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? { fun nextOptionalJson(position: Int = this.position++, limit: Long = DEFAULT_STRING_LIMIT): JsonElement? {
if (position !in 1 ..this.top) if (position !in 1 ..this.top)
return null 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)}") 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 { fun nextDouble(position: Int = this.position++): Double {
if (position !in 1 ..this.top) if (position !in 1 ..this.top)
throw IllegalArgumentException("bad argument #$position: number expected, got nil") throw IllegalArgumentException("bad argument #$position: number expected, got nil")
@ -909,7 +1014,7 @@ class LuaThread private constructor(
else if (type == LuaType.BOOLEAN) else if (type == LuaType.BOOLEAN)
return this@LuaThread.getBoolean(position) return this@LuaThread.getBoolean(position)
else 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 { fun nextBoolean(position: Int = this.position++): Boolean {
@ -921,14 +1026,13 @@ class LuaThread private constructor(
} }
} }
fun push(function: Fn, performanceCritical: Boolean) { private fun closure(p: Long, function: Fn, performanceCritical: Boolean): Int {
LuaJNI.lua_pushcclosure(pointer.address()) lazy@{ sharedState.cleanup()
cleanup()
val realLuaState: LuaThread val realLuaState: LuaThread
if (pointer.address() != it) { if (pointer.address() != p) {
realLuaState = LuaThread(LuaJNR.RUNTIME.memoryManager.newPointer(it), stringInterner = stringInterner) realLuaState = LuaThread(LuaJNR.RUNTIME.memoryManager.newPointer(p), stringInterner = stringInterner)
realLuaState.initializeFrom(this, false) realLuaState.sharedState = sharedState
} else { } else {
realLuaState = this realLuaState = this
} }
@ -948,12 +1052,12 @@ class LuaThread private constructor(
try { try {
val value = function.invoke(args) 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" } check(value >= 0) { "Internal JVM error: ${function::class.qualifiedName} returned incorrect number of arguments to be popped from stack by Lua" }
return@lazy value return value
} catch (err: Throwable) { } catch (err: Throwable) {
try { try {
if (performanceCritical) { if (performanceCritical) {
realLuaState.push(err.stackTraceToString()) realLuaState.push(err.stackTraceToString())
return@lazy -1 return -1
} else { } else {
rememberStack!! rememberStack!!
val newStack = err.stackTraceToString().split('\n').toMutableList() val newStack = err.stackTraceToString().split('\n').toMutableList()
@ -979,20 +1083,61 @@ class LuaThread private constructor(
} }
realLuaState.push(newStack.joinToString("\n")) realLuaState.push(newStack.joinToString("\n"))
return@lazy -1 return -1
} }
} catch(err2: Throwable) { } catch(err2: Throwable) {
realLuaState.push("JVM suffered an exception while handling earlier exception: ${err2.stackTraceToString()}; earlier: ${err.stackTraceToString()}") realLuaState.push("JVM suffered an exception while handling earlier exception: ${err2.stackTraceToString()}; earlier: ${err.stackTraceToString()}")
return@lazy -1 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 push(function: Fn) = this.push(function, !RECORD_STACK_TRACES)
fun interface Binding<T> { fun <T> push(self: T, function: Binding<T>) {
fun invoke(self: T, arguments: ArgStack): Int push {
function.invoke(self, it)
}
}
fun <T, A0> push(self: T, extra: A0, function: Binding1<T, A0>) {
push {
function.invoke(self, extra, it)
}
}
fun <T, A0, A1> push(self: T, extra: A0, extra1: A1, function: Binding2<T, A0, A1>) {
push {
function.invoke(self, extra, extra1, it)
}
}
fun <T> pushBinding(self: T, name: String, function: Binding<T>) {
push(name)
push { function.invoke(self, it) }
setTableValue()
}
fun <T, A0> pushBinding(self: T, extra: A0, name: String, function: Binding1<T, A0>) {
push(name)
push { function.invoke(self, extra, it) }
setTableValue()
}
fun <T, A0, A1> pushBinding(self: T, extra: A0, extra1: A1, name: String, function: Binding2<T, A0, A1>) {
push(name)
push { function.invoke(self, extra, extra1, it) }
setTableValue()
}
fun ensureExtraCapacity(maxSize: Int): Boolean {
return LuaJNR.INSTANCE.lua_checkstack(pointer, maxSize)
} }
inline fun <reified T : Any> pushBinding(fn: Binding<T>) { inline fun <reified T : Any> pushBinding(fn: Binding<T>) {
@ -1061,9 +1206,13 @@ class LuaThread private constructor(
} }
} }
fun push(value: String) { fun push(value: String?) {
if (value == null) {
push()
} else {
pushStringIntoThread(this, value) pushStringIntoThread(this, value)
} }
}
fun push(value: LuaHandle) { fun push(value: LuaHandle) {
value.push(this) value.push(this)
@ -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 * 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<Any, LuaHandle>()
/**
* Same as [createHandle], but makes it permanent (unless manually closed through [LuaHandle.close])
* *
* Makes handle available though [pushHandle] method * Optionally specified [name] will make handle available permanently (unless manually closed),
* * and allows handle to be retrieved in future using [pushNamedHandle] and [getNamedHandle] methods
* This is useful for not creating cyclic references going through GC root
*/ */
fun createHandle(key: Any): LuaHandle { fun createHandle(name: Any? = null): LuaHandle {
require(key !in namedHandles) { "Named handle '$key' already exists" } dup()
val handle = createHandle() moveStackValuesOnto(sharedState.handlesThread)
namedHandles[key] = handle return sharedState.allocateHandle(name)
// 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
} }
/** /**
@ -1107,12 +1240,23 @@ class LuaThread private constructor(
* *
* @throws NoSuchElementException if no such handle exists * @throws NoSuchElementException if no such handle exists
*/ */
fun pushHandle(key: Any): LuaHandle { fun pushNamedHandle(key: Any): LuaHandle {
val handle = namedHandles[key] ?: throw NoSuchElementException("No such handle: $key") val handle = getNamedHandle(key)
push(handle) push(handle)
return 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) { fun copy(fromIndex: Int, toIndex: Int) {
LuaJNR.INSTANCE.lua_copy(pointer, fromIndex, toIndex) LuaJNR.INSTANCE.lua_copy(pointer, fromIndex, toIndex)
} }
@ -1164,6 +1308,11 @@ class LuaThread private constructor(
setTableValue(key) { throw NotImplementedError("NYI: $key") } 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) { fun setTableValue(key: String, value: Int) {
this.push(key) this.push(key)
this.push(value.toLong()) this.push(value.toLong())
@ -1207,6 +1356,14 @@ class LuaThread private constructor(
return setTableValue(key.toLong(), value) 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) { fun setTableValue(key: Int, value: String) {
return setTableValue(key.toLong(), value) return setTableValue(key.toLong(), value)
} }
@ -1229,12 +1386,24 @@ class LuaThread private constructor(
return setTableValue(key, value.toLong()) return setTableValue(key, value.toLong())
} }
fun setTableValue(key: Long, value: Int?) {
return setTableValue(key, value?.toLong())
}
fun setTableValue(key: Long, value: Long) { fun setTableValue(key: Long, value: Long) {
this.push(key) this.push(key)
this.push(value) this.push(value)
this.setTableValue() 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) { fun setTableValue(key: Long, value: String) {
this.push(key) this.push(key)
this.push(value) this.push(value)
@ -1251,7 +1420,13 @@ class LuaThread private constructor(
this.setTableValue() 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) { when (value) {
null, JsonNull.INSTANCE -> { null, JsonNull.INSTANCE -> {
this.push() this.push()
@ -1281,7 +1456,7 @@ class LuaThread private constructor(
for ((i, v) in value.withIndex()) { for ((i, v) in value.withIndex()) {
this.push(i + 1L) this.push(i + 1L)
this.push(v) this.push(v, recCounter + 1)
this.setTableValue(index) this.setTableValue(index)
} }
@ -1295,7 +1470,7 @@ class LuaThread private constructor(
for ((k, v) in value.entrySet()) { for ((k, v) in value.entrySet()) {
this.push(k) this.push(k)
this.push(v) this.push(v, recCounter + 1)
this.setTableValue(index) this.setTableValue(index)
} }
@ -1317,6 +1492,9 @@ class LuaThread private constructor(
private val globalScript by lazy { loadInternalScript("global") } private val globalScript by lazy { loadInternalScript("global") }
private val sharedBuffers = ThreadLocal<Long>() private val sharedBuffers = ThreadLocal<Long>()
private val __nils = makeNativeString("__nils")
private val __typehint = makeNativeString("__typehint")
private fun loadStringIntoBuffer(value: String): Long { private fun loadStringIntoBuffer(value: String): Long {
val bytes = value.toByteArray(Charsets.UTF_8) val bytes = value.toByteArray(Charsets.UTF_8)
@ -1336,9 +1514,6 @@ class LuaThread private constructor(
return p return p
} }
private val __nils = makeNativeString("__nils")
private val __typehint = makeNativeString("__typehint")
private val sharedStringBufferPtr: Long get() { private val sharedStringBufferPtr: Long get() {
var p: Long? = sharedBuffers.get() var p: Long? = sharedBuffers.get()

View File

@ -28,7 +28,7 @@ fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) {
if (self is NPCEntity) if (self is NPCEntity)
provideNPCBindings(self, lua) provideNPCBindings(self, lua)
provideWorldBindings(self.world, lua) //provideWorldBindings(self.world, lua)
val table = lua.newTable() val table = lua.newTable()
lua.globals["entity"] = table lua.globals["entity"] = table

View File

@ -24,6 +24,7 @@ import ru.dbotthepony.kstarbound.lua.nextVector2i
import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.luaFunctionN import ru.dbotthepony.kstarbound.lua.luaFunctionN
import ru.dbotthepony.kstarbound.lua.luaStub import ru.dbotthepony.kstarbound.lua.luaStub
import ru.dbotthepony.kstarbound.lua.nextInt
import ru.dbotthepony.kstarbound.lua.push import ru.dbotthepony.kstarbound.lua.push
import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.tableOf
@ -36,7 +37,7 @@ private val LOGGER = LogManager.getLogger()
private fun <T : Any> lookup(registry: Registry<T>, args: LuaThread.ArgStack): Registry.Entry<T>? { private fun <T : Any> lookup(registry: Registry<T>, args: LuaThread.ArgStack): Registry.Entry<T>? {
return when (val type = args.peek()) { return when (val type = args.peek()) {
LuaType.NUMBER -> registry[args.nextLong().toInt()] LuaType.NUMBER -> registry[args.nextInt()]
LuaType.STRING -> registry[args.nextString()] LuaType.STRING -> registry[args.nextString()]
LuaType.NONE, LuaType.NIL -> null LuaType.NONE, LuaType.NIL -> null
else -> throw IllegalArgumentException("Invalid registry key type: $type") else -> throw IllegalArgumentException("Invalid registry key type: $type")
@ -46,7 +47,7 @@ private fun <T : Any> lookup(registry: Registry<T>, args: LuaThread.ArgStack): R
private fun <T : Any> lookupStrict(registry: Registry<T>, args: LuaThread.ArgStack): Registry.Entry<T> { private fun <T : Any> lookupStrict(registry: Registry<T>, args: LuaThread.ArgStack): Registry.Entry<T> {
return when (val type = args.peek()) { return when (val type = args.peek()) {
LuaType.NUMBER -> { LuaType.NUMBER -> {
val key = args.nextLong().toInt() val key = args.nextInt()
registry[key] ?: throw LuaRuntimeException("No such ${registry.name}: $key") 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 { private fun createBiome(args: LuaThread.ArgStack): Int {
val name = args.nextString() val name = args.nextString()
val seed = args.nextLong() val seed = args.nextLong()
val verticalMidPoint = args.nextLong().toInt() val verticalMidPoint = args.nextInt()
val threatLevel = args.nextDouble() val threatLevel = args.nextDouble()
try { try {

View File

@ -1,12 +1,8 @@
package ru.dbotthepony.kstarbound.lua.bindings package ru.dbotthepony.kstarbound.lua.bindings
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table import ru.dbotthepony.kommons.util.KOptional
import org.classdump.luna.runtime.AbstractFunction1
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.UnresolvedControlThrowable
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType 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.BiomeDefinition
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceablesDefinition import ru.dbotthepony.kstarbound.defs.world.BiomePlaceablesDefinition
import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.LuaThread
import ru.dbotthepony.kstarbound.lua.iterator import ru.dbotthepony.kstarbound.lua.LuaType
import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.nextAABB
import ru.dbotthepony.kstarbound.lua.luaStub import ru.dbotthepony.kstarbound.lua.nextAABBi
import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.nextVector2d
import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.nextVector2i
import ru.dbotthepony.kstarbound.lua.toAABB import ru.dbotthepony.kstarbound.lua.push
import ru.dbotthepony.kstarbound.lua.toAABBi
import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.lua.toJsonFromLua 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.LuaFuture
import ru.dbotthepony.kstarbound.lua.userdata.push
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64 import ru.dbotthepony.kstarbound.util.random.staticRandom64
@ -41,59 +34,25 @@ import java.util.concurrent.TimeUnit
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private class LoadUniqueEntityFunction(val self: ServerWorld) : AbstractFunction1<ByteString>() { private fun placeDungeonImpl(self: ServerWorld, force: Boolean, async: Boolean, args: LuaThread.ArgStack): Int {
override fun invoke(context: ExecutionContext, arg1: ByteString) { val name = args.nextString()
// no, I give up, this shit is beyond bonkers val pos = args.nextVector2i()
// I don't care that mods could break val dungeonID = args.nextOptionalLong()?.toInt()
// mods WILL break val seed = args.nextOptionalLong()
// just please, I beg you, for the love of God and Jesus, val actualSeed = seed ?: staticRandom64(pos.x, pos.y, "DungeonPlacement")
// stop fucking designing async functions as sync ones val dungeonDef = Registries.dungeons[name]
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<AbstractEntity?>
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()]
if (dungeonDef == null) { if (dungeonDef == null) {
LOGGER.error("Lua script tried to spawn dungeon $name, however, no such dungeon exist") LOGGER.error("Lua script tried to spawn dungeon $name, however, no such dungeon exist")
if (async) { if (async) {
returnBuffer.setTo(LuaFuture( args.lua.push(LuaFuture(
future = CompletableFuture.failedFuture(NoSuchElementException("No such dungeon: $name")), future = CompletableFuture.failedFuture(NoSuchElementException("No such dungeon: $name")),
isLocal = false isLocal = false,
handler = { 0 }
)) ))
} else { } else {
returnBuffer.setTo(false) args.lua.push(false)
} }
} else { } else {
val future = dungeonDef.value.generate( val future = dungeonDef.value.generate(
@ -109,44 +68,53 @@ private fun ExecutionContext.placeDungeonImpl(self: ServerWorld, force: Boolean,
scope = if (async) Starbound.GLOBAL_SCOPE else self.eventLoop.scope).thenApply { it.hasGenerated } scope = if (async) Starbound.GLOBAL_SCOPE else self.eventLoop.scope).thenApply { it.hasGenerated }
if (async) { if (async) {
returnBuffer.setTo(LuaFuture( args.lua.push(LuaFuture(
future = future, future = future,
isLocal = false isLocal = false,
handler = { push(it); 1 }
)) ))
} else { } else {
returnBuffer.setTo(future.getNow(true)) args.lua.push(future.getNow(true))
}
} }
} }
fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvironment) { return 1
callbacks["breakObject"] = luaFunction { id: Number, smash: Boolean? -> }
val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo(false)
if (entity.isRemote) { 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 == null) {
args.lua.push(false)
} else if (entity.isRemote) {
// we can't break objects now owned by us // we can't break objects now owned by us
returnBuffer.setTo(false) args.lua.push(false)
} else { } else {
if (smash == true) if (smash == true)
entity.health = 0.0 entity.health = 0.0
entity.remove(AbstractEntity.RemovalReason.DYING) entity.remove(AbstractEntity.RemovalReason.DYING)
returnBuffer.setTo(true) args.lua.push(true)
}
} }
callbacks["isVisibleToPlayer"] = luaFunction { region: Table -> return 1
val parse = toAABB(region)
returnBuffer.setTo(self.clients.any { it.isTracking(parse) })
} }
callbacks["loadRegion"] = luaFunction { region: Table -> 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 // 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 // And we can't block event loop, because this will make region be unable to be loaded in first place
// god damn it val region = args.nextAABB()
val parse = toAABB(region)
// keep in ram for at most 2400 ticks // keep in ram for at most 2400 ticks
val tickets = self.temporaryChunkTicket(parse, 2400).get() val tickets = self.temporaryChunkTicket(region, 2400).get()
val future = CompletableFuture.allOf(*tickets.map { it.chunk }.toTypedArray()) val future = CompletableFuture.allOf(*tickets.map { it.chunk }.toTypedArray())
future.thenApply { future.thenApply {
@ -159,52 +127,69 @@ fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvi
}, 4L, TimeUnit.SECONDS) }, 4L, TimeUnit.SECONDS)
} }
returnBuffer.setTo(LuaFuture( args.lua.push(LuaFuture(
future = future, future = future,
isLocal = false isLocal = false,
handler = { 0 }
)) ))
return 1
} }
callbacks["regionActive"] = luaFunction { region: Table -> private fun regionActive(self: ServerWorld, args: LuaThread.ArgStack): Int {
val chunks = self.geometry.region2Chunks(toAABB(region)) val chunks = self.geometry.region2Chunks(args.nextAABB())
// TODO: i have no idea what mods expect this to return, so i'll have to guess // 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 }) args.lua.push(chunks.all { self.chunkMap[it]?.state == ChunkState.FULL })
return 1
} }
callbacks["setTileProtection"] = luaFunction { id: Number, enable: Boolean -> private fun setTileProtection(self: ServerWorld, args: LuaThread.ArgStack): Int {
self.switchDungeonIDProtection(id.toInt(), enable) self.switchDungeonIDProtection(args.nextInt(), args.nextBoolean())
return 0
} }
callbacks["isPlayerModified"] = luaFunction { region: Table -> private fun isPlayerModified(self: ServerWorld, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(self.isPlayerModified(toAABBi(region))) args.lua.push(self.isPlayerModified(args.nextAABBi()))
return 1
} }
callbacks["forceDestroyLiquid"] = luaFunction { position: Table -> private fun forceDestroyLiquid(self: ServerWorld, args: LuaThread.ArgStack): Int {
val parse = toVector2i(position) val position = args.nextVector2i()
val cell = self.getCell(parse).mutable() val cell = self.getCell(position).mutable()
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) { if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) {
returnBuffer.setTo(cell.liquid.level.toDouble()) args.lua.push(cell.liquid.level.toDouble())
cell.liquid.reset() cell.liquid.reset()
self.setCell(parse, cell.immutable()) self.setCell(position, cell.immutable())
} return 1
} }
callbacks["loadUniqueEntity"] = LoadUniqueEntityFunction(self) return 0
callbacks["loadUniqueEntityAsync"] = luaFunction { name: ByteString ->
returnBuffer.setTo(LuaFuture(
future = self.loadUniqueEntity(name.decode()),
isLocal = false
))
} }
callbacks["setUniqueId"] = luaFunction { id: Number, name: ByteString -> private fun loadUniqueEntityAsync(self: ServerWorld, args: LuaThread.ArgStack): Int {
val entity = self.entities[id.toInt()] ?: throw LuaRuntimeException("No such entity with ID $id (tried to set unique id to $name)") 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) if (entity.isRemote)
throw LuaRuntimeException("Entity $entity is not owned by this side") throw IllegalStateException("Entity $entity is not owned by this side")
if ( if (
entity.type == EntityType.NPC || entity.type == EntityType.NPC ||
@ -212,129 +197,200 @@ fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvi
entity.type == EntityType.MONSTER || entity.type == EntityType.MONSTER ||
entity.type == EntityType.OBJECT entity.type == EntityType.OBJECT
) { ) {
entity.uniqueID.accept(name.decode().sbIntern()) entity.uniqueID.accept(name.sbIntern())
} else { } else {
throw LuaRuntimeException("Entity type is restricted from having unique ID: $entity (tried to set unique id to $name)") throw IllegalStateException("Entity type is restricted from having unique ID: $entity (tried to set unique id to $name)")
}
} }
callbacks["takeItemDrop"] = luaFunction { id: Number, takenBy: Number? -> return 0
val entity = self.entities[id.toInt()] as? ItemDropEntity ?: return@luaFunction returnBuffer.setTo()
if (!entity.isRemote && entity.canTake) {
returnBuffer.setTo(entity.take(takenBy?.toInt() ?: 0).toTable(this))
}
} }
callbacks["setPlayerStart"] = luaFunction { position: Table, respawnInWorld: Boolean? -> private fun takeItemDrop(self: ServerWorld, args: LuaThread.ArgStack): Int {
self.setPlayerSpawn(toVector2d(position), respawnInWorld == true) 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["players"] = luaFunction { return 0
returnBuffer.setTo(tableOf(*self.clients.map { it.client.playerID }.toTypedArray()))
} }
callbacks["fidelity"] = luaFunction { private fun setPlayerStart(self: ServerWorld, args: LuaThread.ArgStack): Int {
returnBuffer.setTo("high".toByteString()) self.setPlayerSpawn(args.nextVector2d(), args.nextOptionalBoolean() == true)
return 0
} }
callbacks["setSkyTime"] = luaFunction { newTime: Number -> private fun players(self: ServerWorld, args: LuaThread.ArgStack): Int {
val cast = newTime.toDouble() args.lua.pushTable(self.clients.size)
var i = 1L
if (cast < 0.0) for (p in self.clients) {
throw LuaRuntimeException("Negative time? $cast") val entityID = p.client.playerEntity?.entityID
self.sky.time = cast if (entityID != null)
args.lua.setTableValue(i++, entityID)
} }
callbacks["setUniverseFlag"] = luaFunction { flag: ByteString -> return 1
returnBuffer.setTo(self.server.addUniverseFlag(flag.decode().sbIntern()))
} }
callbacks["unsetUniverseFlag"] = luaFunction { flag: ByteString -> private fun setSkyTime(self: ServerWorld, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(self.server.removeUniverseFlag(flag.decode())) val time = args.nextDouble()
if (time < 0.0)
throw LuaRuntimeException("Negative time? $time")
self.sky.time = time
return 0
} }
callbacks["universeFlags"] = luaFunction { private fun setUniverseFlag(self: ServerWorld, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(tableOf(*self.server.getUniverseFlags().toTypedArray())) args.lua.push(self.server.addUniverseFlag(args.nextString().sbIntern()))
return 1
} }
callbacks["universeFlagSet"] = luaFunction { flag: ByteString -> private fun unsetUniverseFlag(self: ServerWorld, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(self.server.hasUniverseFlag(flag.decode())) args.lua.push(self.server.removeUniverseFlag(args.nextString()))
return 1
} }
callbacks["placeDungeon"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> private fun universeFlags(self: ServerWorld, args: LuaThread.ArgStack): Int {
placeDungeonImpl(self, true, false, name, position, dungeonID, seed) val flags = self.server.getUniverseFlags()
args.lua.pushTable(flags.size)
for ((i, flag) in flags.withIndex()) {
args.lua.setTableValue(i + 1L, flag)
} }
callbacks["tryPlaceDungeon"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> return 1
placeDungeonImpl(self, false, false, name, position, dungeonID, seed)
} }
callbacks["placeDungeonAsync"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> private fun universeFlagSet(self: ServerWorld, args: LuaThread.ArgStack): Int {
placeDungeonImpl(self, true, true, name, position, dungeonID, seed) args.lua.push(self.server.hasUniverseFlag(args.nextString()))
return 1
} }
callbacks["tryPlaceDungeonAsync"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> private fun placeDungeon(self: ServerWorld, args: LuaThread.ArgStack): Int {
placeDungeonImpl(self, false, true, name, position, dungeonID, seed) return placeDungeonImpl(self, force = true, async = false, args)
} }
// terraforming private fun tryPlaceDungeon(self: ServerWorld, args: LuaThread.ArgStack): Int {
callbacks["addBiomeRegion"] = luaStub("addBiomeRegion") return placeDungeonImpl(self, force = false, async = false, args)
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? -> private fun placeDungeonAsync(self: ServerWorld, args: LuaThread.ArgStack): Int {
if (gravity == null) { return placeDungeonImpl(self, force = true, async = true, args)
self.setDungeonGravity(id.toInt(), null) }
} else if (gravity is Table) {
self.setDungeonGravity(id.toInt(), toVector2d(gravity)) private fun tryPlaceDungeonAsync(self: ServerWorld, args: LuaThread.ArgStack): Int {
} else if (gravity is Number) { return placeDungeonImpl(self, force = false, async = true, args)
self.setDungeonGravity(id.toInt(), gravity.toDouble()) }
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 { } else {
throw LuaRuntimeException("Illegal argument to setDungeonGravity: $gravity") throw LuaRuntimeException("Illegal gravity argument to setDungeonGravity: $peek")
}
} }
callbacks["setDungeonBreathable"] = luaFunction { id: Number, breathable: Boolean? -> return 0
self.setDungeonBreathable(id.toInt(), breathable)
} }
callbacks["setDungeonId"] = luaFunction { region: Table, id: Number -> private fun setDungeonBreathable(self: ServerWorld, args: LuaThread.ArgStack): Int {
val parse = toAABBi(region) self.setDungeonBreathable(args.nextInt(), args.nextOptionalBoolean())
val actualId = id.toInt() return 0
}
for (x in parse.mins.x .. parse.maxs.x) { private fun setDungeonId(self: ServerWorld, args: LuaThread.ArgStack): Int {
for (y in parse.mins.y .. parse.maxs.y) { val region = args.nextAABBi()
val id = args.nextInt()
for (x in region.mins.x .. region.maxs.x) {
for (y in region.mins.y .. region.maxs.y) {
val cell = self.getCell(x, y) val cell = self.getCell(x, y)
if (cell.dungeonId != actualId) { if (cell.dungeonId != id) {
self.setCell(x, y, cell.mutable().also { it.dungeonId = actualId }) self.setCell(x, y, cell.mutable().also { it.dungeonId = id })
}
} }
} }
} }
callbacks["enqueuePlacement"] = luaFunction { distributions: Table, dungeonId: Number? -> return 0
}
private fun enqueuePlacement(self: ServerWorld, args: LuaThread.ArgStack): Int {
val distributions = args.nextJsonArray()
val dungeonId = args.nextOptionalInt()
val items = ArrayList<BiomePlaceables.DistributionItem>() val items = ArrayList<BiomePlaceables.DistributionItem>()
for ((_, v) in distributions) { for (v in distributions) {
// original engine treats distributions table as if it was originating from biome json files // 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 unprepared = Starbound.gson.fromJson(toJsonFromLua(v), BiomePlaceablesDefinition.DistributionItem::class.java)
val prepared = unprepared.create(BiomeDefinition.CreationParams(hueShift = 0.0, random = lua.random)) val prepared = unprepared.create(BiomeDefinition.CreationParams(hueShift = 0.0, random = args.lua.random))
items.add(prepared) items.add(prepared)
} }
// TODO: Enqueued placements are lost on world shutdown. // TODO: Enqueued placements are lost on world shutdown.
// Shouldn't we serialize them to persistent storage? // Shouldn't we serialize them to persistent storage?
returnBuffer.setTo( args.lua.push(
LuaFuture( LuaFuture(
future = self.enqueuePlacement(items, dungeonId?.toInt()), future = self.enqueuePlacement(items, dungeonId),
isLocal = false 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()
} }

View File

@ -229,6 +229,26 @@ fun provideUtilityBindings(lua: LuaThread) {
} }
storeGlobal("__random_seed") storeGlobal("__random_seed")
push {
push(it.lua.getNamedHandle(it.nextString()))
1
}
storeGlobal("gethandle")
push {
val find = it.lua.findNamedHandle(it.nextString())
if (find == null) {
0
} else {
push(find)
1
}
}
storeGlobal("findhandle")
} }
lua.pushTable() lua.pushTable()
@ -250,7 +270,7 @@ fun provideUtilityBindings(lua: LuaThread) {
lua.setTableValue("staticRandomDoubleRange", ::staticRandomDoubleRange) lua.setTableValue("staticRandomDoubleRange", ::staticRandomDoubleRange)
lua.pushTable() lua.pushTable()
val randomMeta = lua.createHandle() val randomMeta = lua.createHandle("RandomGenerator")
lua.pushBinding("init", LuaRandom::init) lua.pushBinding("init", LuaRandom::init)
lua.pushBinding("addEntropy", LuaRandom::addEntropy) lua.pushBinding("addEntropy", LuaRandom::addEntropy)
@ -273,7 +293,7 @@ fun provideUtilityBindings(lua: LuaThread) {
val seed = args.nextOptionalLong() ?: args.lua.random.nextLong() val seed = args.nextOptionalLong() ?: args.lua.random.nextLong()
lua.pushTable() lua.pushTable()
lua.push("__index") lua.push("__index")
lua.push(randomMeta) // cyclic reference through GC root lua.push(randomMeta)
lua.setTableValue() lua.setTableValue()
lua.pushObject(LuaRandom(random(seed))) lua.pushObject(LuaRandom(random(seed)))
1 1
@ -282,7 +302,7 @@ fun provideUtilityBindings(lua: LuaThread) {
lua.setTableValue() lua.setTableValue()
lua.pushTable() lua.pushTable()
val noiseMeta = lua.createHandle() val noiseMeta = lua.createHandle("PerlinSource")
lua.pushBinding("get", LuaPerlinNoise::get) lua.pushBinding("get", LuaPerlinNoise::get)
lua.pushBinding("seed", LuaPerlinNoise::seed) lua.pushBinding("seed", LuaPerlinNoise::seed)

View File

@ -1,36 +1,20 @@
package ru.dbotthepony.kstarbound.lua.bindings 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.Registries
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType 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.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyModifier import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.isNullTile import ru.dbotthepony.kstarbound.defs.tile.isNullTile
import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.LuaThread
import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.getVector2i
import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.nextVector2d
import ru.dbotthepony.kstarbound.lua.iterator import ru.dbotthepony.kstarbound.lua.nextVector2i
import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.push
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.userdata.LuaFuture 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.math.vector.Vector2i
import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.TileModification import ru.dbotthepony.kstarbound.world.TileModification
@ -39,51 +23,39 @@ import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.tileAreaBrush import ru.dbotthepony.kstarbound.world.tileAreaBrush
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
private val foregroundStr = ByteString.of("foreground") private fun damageTilesImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture<TileDamageResult> {
private val backgroundStr = ByteString.of("background") val positions = args.readTableValues { getVector2i() ?: throw IllegalArgumentException("Positions table contains invalid positions") }
val isBackground = args.nextBoolean()
private fun isBackground(layer: ByteString): Boolean { val sourcePosition = args.nextVector2d()
return if (layer == backgroundStr) val damageType = TileDamageType.entries.valueOf(args.nextString())
true val damage = args.nextDouble()
else if (layer == foregroundStr) val harvestLevel = args.nextOptionalLong()?.toInt() ?: 999
false val sourceEntity = self.entities[args.nextOptionalLong()?.toInt() ?: 0]
else
throw LuaRuntimeException("Invalid tile layer $layer")
}
private fun ExecutionContext.damageTilesImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture<TileDamageResult> {
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]
return self.damageTiles(positions, isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity) return self.damageTiles(positions, isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity)
} }
private fun ExecutionContext.damageTileAreaImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture<TileDamageResult> { private fun damageTileAreaImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture<TileDamageResult> {
val center = toVector2i(it.nextTable()) val center = args.nextVector2i()
val radius = it.nextFloat() val radius = args.nextDouble()
val isBackground = isBackground(it.nextString()) val isBackground = args.nextBoolean()
val sourcePosition = toVector2d(it.nextTable()) val sourcePosition = args.nextVector2d()
val damageType = TileDamageType.entries.valueOf(it.nextString().decode()) val damageType = TileDamageType.entries.valueOf(args.nextString())
val damage = it.nextFloat() val damage = args.nextDouble()
val harvestLevel = it.nextOptionalInteger()?.toInt() ?: 999 val harvestLevel = args.nextOptionalLong()?.toInt() ?: 999
val sourceEntity = self.entities[it.nextOptionalInteger()?.toInt() ?: 0] val sourceEntity = self.entities[args.nextOptionalLong()?.toInt() ?: 0]
return self.damageTiles(tileAreaBrush(center, radius), isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity) return self.damageTiles(tileAreaBrush(center, radius), isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity)
} }
private fun ExecutionContext.placeMaterialImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture<List<Vector2i>> { private fun placeMaterialImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture<List<Vector2i>> {
val pos = toVector2i(it.nextTable()) val pos = args.nextVector2i()
val isBackground = isBackground(it.nextString()) val isBackground = args.nextBoolean()
val material = Registries.tiles.getOrThrow(it.nextString().decode()) val material = Registries.tiles.getOrThrow(args.nextString())
val hueShift: Float? = (it.nextAny() as? Number)?.let { it.toFloat() / 255f * 360f } val hueShift: Float? = args.nextOptionalDouble()?.let { it.toFloat() / 255f * 360f }
val allowOverlap = it.nextBoolean() val allowOverlap = args.nextBoolean()
val action = TileModification.PlaceMaterial(isBackground, material.ref, hueShift) 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 } } return self.applyTileModifications(listOf(pos to action), allowOverlap, false).thenApply { it.map { it.first } }
} }
private fun ExecutionContext.placeModImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture<List<Vector2i>> { private fun placeModImpl(self: World<*, *>, args: LuaThread.ArgStack): CompletableFuture<List<Vector2i>> {
val pos = toVector2i(it.nextTable()) val pos = args.nextVector2i()
val isBackground = isBackground(it.nextString()) val isBackground = args.nextBoolean()
val material = Registries.tileModifiers.getOrThrow(it.nextString().decode()) val material = Registries.tileModifiers.getOrThrow(args.nextString())
val hueShift: Float? = (it.nextAny() as? Number)?.let { it.toFloat() / 255f * 360f } val hueShift: Float? = args.nextOptionalDouble()?.let { it.toFloat() / 255f * 360f }
val allowOverlap = it.nextBoolean() val allowOverlap = args.nextBoolean()
val action = TileModification.PlaceModifier(isBackground, material.ref, hueShift) 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 } } return self.applyTileModifications(listOf(pos to action), allowOverlap, false).thenApply { it.map { it.first } }
} }
fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua: LuaEnvironment) { private fun breathable(self: World<*, *>, args: LuaThread.ArgStack): Int {
callbacks["lightLevel"] = luaStub("lightLevel") args.lua.push(self.chunkMap.isBreathable(args.nextVector2d()))
callbacks["windLevel"] = luaStub("windLevel") return 1
callbacks["breathable"] = luaFunction { pos: Table ->
returnBuffer.setTo(self.chunkMap.isBreathable(toVector2i(pos)))
} }
callbacks["underground"] = luaFunction { pos: Table -> private fun underground(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(self.template.undergroundLevel >= toVector2d(pos).y) args.lua.push(self.template.undergroundLevel >= args.nextVector2d().y)
return 1
} }
callbacks["material"] = luaFunction { pos: Table, layer: ByteString -> private fun material(self: World<*, *>, args: LuaThread.ArgStack): Int {
val isBackground = isBackground(layer) val pos = args.nextVector2i()
val tile = self.getCell(toVector2i(pos)).tile(isBackground) val isBackground = args.nextBoolean()
val tile = self.getCell(pos).tile(isBackground)
if (tile.material.isNullTile) { if (tile.material.isNullTile) {
returnBuffer.setTo() return 0
} else if (tile.material.isEmptyTile) { } else if (tile.material.isEmptyTile) {
returnBuffer.setTo(false) args.lua.push(false)
return 1
} else { } else {
returnBuffer.setTo(tile.material.key.toByteString()) args.lua.push(tile.material.key)
return 1
}
} }
} private fun mod(self: World<*, *>, args: LuaThread.ArgStack): Int {
callbacks["mod"] = luaFunction { pos: Table, layer: ByteString -> val pos = args.nextVector2i()
val isBackground = isBackground(layer) val isBackground = args.nextBoolean()
val tile = self.getCell(toVector2i(pos)).tile(isBackground) val tile = self.getCell(pos).tile(isBackground)
if (tile.modifier.isNotEmptyModifier) { if (tile.modifier.isNotEmptyModifier) {
returnBuffer.setTo(tile.modifier.key.toByteString()) args.lua.push(tile.modifier.key)
} return 1
} }
callbacks["materialHueShift"] = luaFunction { pos: Table, layer: ByteString -> return 0
val isBackground = isBackground(layer)
val tile = self.getCell(toVector2i(pos)).tile(isBackground)
returnBuffer.setTo(tile.hueShift.toDouble())
} }
callbacks["modHueShift"] = luaFunction { pos: Table, layer: ByteString -> private fun materialHueShift(self: World<*, *>, args: LuaThread.ArgStack): Int {
val isBackground = isBackground(layer) val pos = args.nextVector2i()
val tile = self.getCell(toVector2i(pos)).tile(isBackground) val isBackground = args.nextBoolean()
returnBuffer.setTo(tile.modifierHueShift.toDouble()) val tile = self.getCell(pos).tile(isBackground)
args.lua.push(tile.hueShift)
return 1
} }
callbacks["materialColor"] = luaFunction { pos: Table, layer: ByteString -> private fun modHueShift(self: World<*, *>, args: LuaThread.ArgStack): Int {
val isBackground = isBackground(layer) val pos = args.nextVector2i()
val tile = self.getCell(toVector2i(pos)).tile(isBackground) val isBackground = args.nextBoolean()
returnBuffer.setTo(tile.color.ordinal.toLong()) val tile = self.getCell(pos).tile(isBackground)
args.lua.push(tile.modifierHueShift)
return 1
} }
callbacks["materialColorName"] = luaFunction { pos: Table, layer: ByteString -> private fun materialColor(self: World<*, *>, args: LuaThread.ArgStack): Int {
val isBackground = isBackground(layer) val pos = args.nextVector2i()
val tile = self.getCell(toVector2i(pos)).tile(isBackground) val isBackground = args.nextBoolean()
returnBuffer.setTo(tile.color.jsonName.toByteString()) val tile = self.getCell(pos).tile(isBackground)
args.lua.push(tile.color.ordinal.toLong())
return 1
} }
callbacks["setMaterialColor"] = luaFunction { pos: Table, layer: ByteString, color: Any -> private fun materialColorName(self: World<*, *>, args: LuaThread.ArgStack): Int {
val isBackground = isBackground(layer) val pos = args.nextVector2i()
val isBackground = args.nextBoolean()
val tile = self.getCell(pos).tile(isBackground)
val actualColor = if (color is Number) args.lua.push(tile.color.jsonName)
TileColor.entries[color.toInt()] return 1
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 -> private fun setMaterialColorNumber(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(self.template.cellInfo(toVector2i(pos)).oceanLiquidLevel.toLong()) 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
} }
callbacks["environmentStatusEffects"] = luaFunction { pos: Table -> private fun setMaterialColorName(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(tableFrom(self.environmentStatusEffects(toVector2i(pos)).map { from(Starbound.gson.toJsonTree(it)) })) 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
} }
callbacks["damageTiles"] = luaFunctionN("damageTiles") { private fun oceanLevel(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(damageTilesImpl(self, it).getNow(TileDamageResult.NONE) != TileDamageResult.NONE) args.lua.push(self.template.cellInfo(args.nextVector2d()).oceanLiquidLevel.toLong())
return 1
} }
callbacks["damageTilesPromise"] = luaFunctionN("damageTilesPromise") { private fun environmentStatusEffects(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo( 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( LuaFuture(
future = damageTilesImpl(self, it).thenApply { it.jsonName }, future = damageTilesImpl(self, args).thenApply { it.jsonName },
isLocal = false isLocal = false,
handler = {
push(it)
1
}
) )
) )
return 1
} }
callbacks["damageTileArea"] = luaFunctionN("damageTileArea") { private fun damageTileArea(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(damageTileAreaImpl(self, it).getNow(TileDamageResult.NONE) != TileDamageResult.NONE) args.lua.push(damageTileAreaImpl(self, args).getNow(TileDamageResult.NONE) != TileDamageResult.NONE)
return 1
} }
callbacks["damageTileAreaPromise"] = luaFunctionN("damageTileAreaPromise") { private fun damageTileAreaPromise(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo( args.lua.push(
LuaFuture( LuaFuture(
future = damageTileAreaImpl(self, it).thenApply { it.jsonName }, future = damageTileAreaImpl(self, args).thenApply { it.jsonName },
isLocal = false isLocal = false,
handler = {
push(it)
1
}
) )
) )
return 1
} }
callbacks["placeMaterial"] = luaFunctionN("placeMaterial") { private fun placeMaterial(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo(placeMaterialImpl(self, it).getNow(listOf()).isEmpty()) args.lua.push(placeMaterialImpl(self, args).getNow(listOf()).isEmpty())
return 1
} }
callbacks["placeMaterialPromise"] = luaFunctionN("placeMaterialPromise") { private fun placeMaterialPromise(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo( args.lua.push(
LuaFuture( LuaFuture(
future = placeMaterialImpl(self, it).thenApply { tableFrom(it.map { from(it) }) }, future = placeMaterialImpl(self, args),
isLocal = false isLocal = false,
) handler = {
) pushTable(it.size)
for ((i, pos) in it.withIndex()) {
push(i + 1L)
push(pos)
setTableValue()
} }
callbacks["placeMod"] = luaFunctionN("placeMod") { 1
returnBuffer.setTo(placeModImpl(self, it).getNow(listOf()).isEmpty()) }
)
)
return 1
} }
callbacks["placeModPromise"] = luaFunctionN("placeModPromise") { private fun placeMod(self: World<*, *>, args: LuaThread.ArgStack): Int {
returnBuffer.setTo( args.lua.push(placeModImpl(self, args).getNow(listOf()).isEmpty())
return 1
}
private fun placeModPromise(self: World<*, *>, args: LuaThread.ArgStack): Int {
args.lua.push(
LuaFuture( LuaFuture(
future = placeModImpl(self, it).thenApply { tableFrom(it.map { from(it) }) }, future = placeModImpl(self, args),
isLocal = false 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)
} }

View File

@ -1,10 +1,7 @@
package ru.dbotthepony.kstarbound.lua.userdata package ru.dbotthepony.kstarbound.lua.userdata
import org.classdump.luna.Table import ru.dbotthepony.kstarbound.lua.LuaHandle
import org.classdump.luna.Userdata import ru.dbotthepony.kstarbound.lua.LuaThread
import org.classdump.luna.impl.ImmutableTable
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.luaFunction
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException import java.util.concurrent.CompletionException
@ -15,68 +12,83 @@ import java.util.concurrent.CompletionException
* *
* god damn it. * god damn it.
*/ */
class LuaFuture(val future: CompletableFuture<out Any?>, val isLocal: Boolean) : Userdata<CompletableFuture<out Any?>>() { class LuaFuture<T : Any>(val future: CompletableFuture<T>, val isLocal: Boolean, val handler: LuaThread.(T) -> Int) {
override fun getMetatable(): Table { fun finished(args: LuaThread.ArgStack): Int {
return metadata args.lua.push(future.isDone)
return 1
} }
override fun setMetatable(mt: Table?): Table { fun succeeded(args: LuaThread.ArgStack): Int {
throw UnsupportedOperationException() args.lua.push(future.isDone && !future.isCompletedExceptionally)
return 1
} }
override fun getUserValue(): CompletableFuture<out Any?> { fun failed(args: LuaThread.ArgStack): Int {
return future args.lua.push(future.isCompletedExceptionally)
return 1
} }
override fun setUserValue(value: CompletableFuture<out Any?>?): CompletableFuture<out Any?> { fun result(args: LuaThread.ArgStack): Int {
throw UnsupportedOperationException() 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 { companion object {
private fun __index(): Table { fun initializeHandle(lua: LuaThread): LuaHandle {
return metadata lua.pushTable()
val handle = lua.createHandle("LuaFuture")
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
}
}
} }
private val metadata = ImmutableTable.Builder() fun LuaThread.push(value: LuaFuture<*>) {
.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) pushTable()
.add("finished", luaFunction { self: LuaFuture ->
returnBuffer.setTo(self.future.isDone) push("__index")
}) push(commonHandles.future)
.add("succeeded", luaFunction { self: LuaFuture -> setTableValue()
returnBuffer.setTo(!self.future.isCompletedExceptionally)
}) pushObject(value)
.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()
}
} }

View File

@ -1,101 +1,126 @@
package ru.dbotthepony.kstarbound.lua.userdata package ru.dbotthepony.kstarbound.lua.userdata
import org.classdump.luna.Table import ru.dbotthepony.kstarbound.lua.LuaHandle
import org.classdump.luna.TableFactory import ru.dbotthepony.kstarbound.lua.LuaThread
import org.classdump.luna.Userdata import ru.dbotthepony.kstarbound.lua.setTableValue
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.util.CarriedExecutor import ru.dbotthepony.kstarbound.util.CarriedExecutor
import ru.dbotthepony.kstarbound.util.supplyAsync import ru.dbotthepony.kstarbound.util.supplyAsync
import ru.dbotthepony.kstarbound.world.entities.PathFinder import ru.dbotthepony.kstarbound.world.entities.PathFinder
import java.util.concurrent.CompletableFuture
class LuaPathFinder(val pacer: CarriedExecutor, val self: PathFinder) : Userdata<PathFinder>() {
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 private var isRunning = false
companion object { fun result(args: LuaThread.ArgStack): Int {
private val cost = "const".toByteString()!! if (self.result.isEmpty)
private val action = "action".toByteString()!! return 0
private val jumpVelocity = "jumpVelocity".toByteString()!!
private val source = "source".toByteString()!!
private val target = "target".toByteString()!!
fun convertPath(tables: TableFactory, list: List<PathFinder.Edge>?): Table? { val list = self.result.value ?: return 0
list ?: return null return convertPath(args.lua, list)
val table = tables.newTable(list.size, 0) }
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<PathFinder.Edge>?): Int {
list ?: return 0
lua.pushTable(list.size)
var i = 1L var i = 1L
for (edge in list) { for (edge in list) {
val edgeTable = tables.tableOf() lua.push(i++)
edgeTable[cost] = edge.cost
edgeTable[action] = edge.action.luaName lua.pushTable(hashSize = 5)
edgeTable[jumpVelocity] = tables.from(edge.velocity)
edgeTable[source] = edge.source.toTable(tables) lua.setTableValue("cost", edge.cost)
edgeTable[target] = edge.target.toTable(tables) lua.setTableValue("action", edge.action.jsonName)
table[i++] = edgeTable 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 { fun initializeHandle(lua: LuaThread): LuaHandle {
return metatable 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() fun LuaThread.push(value: LuaPathFinder) {
.add("__index", luaFunction { _: Any?, index: Any -> returnBuffer.setTo(__index()[index]) }) pushTable()
.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) { push("__index")
returnBuffer.setTo(result) push(commonHandles.pathFinder)
} else if (self.self.result.isEmpty) { setTableValue()
// 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() pushObject(value)
returnBuffer.setTo(convertPath(this, list))
})
.build()
}
} }

View File

@ -308,7 +308,7 @@ class ServerWorld private constructor(
queuedPlacementsInternal.remove(entry) queuedPlacementsInternal.remove(entry)
}.exceptionally { }.exceptionally {
queuedPlacementsInternal.remove(entry) queuedPlacementsInternal.remove(entry)
null throw it
} }
return future return future

View File

@ -13,6 +13,7 @@ import java.util.stream.Stream
import kotlin.NoSuchElementException import kotlin.NoSuchElementException
import kotlin.collections.Collection import kotlin.collections.Collection
import kotlin.collections.List import kotlin.collections.List
import kotlin.math.floor
fun String.sbIntern(): String { fun String.sbIntern(): String {
return Starbound.STRINGS.intern(this) return Starbound.STRINGS.intern(this)
@ -103,3 +104,8 @@ fun <T> Collection<T>.getWrapAround(index: Int): T {
return this.elementAt(positiveModulo(index, size)) 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()

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
@ -171,3 +172,10 @@ fun ICellAccess.castRay(
return RayCastResult(hitTiles, null, 1.0, start, start + direction * distance, direction) 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)
}

View File

@ -48,6 +48,7 @@ import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.floorToInt
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
@ -72,6 +73,7 @@ import java.util.stream.Stream
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.floor
import kotlin.math.roundToInt import kotlin.math.roundToInt
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess { abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess {
@ -252,6 +254,10 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
.filterNotNull() .filterNotNull()
} }
fun collide(with: AABB, filter: Predicate<CollisionPoly>): Stream<Poly.Penetration> {
return collide(Poly(with), filter)
}
fun collide(point: Vector2d, filter: Predicate<CollisionPoly>): Boolean { fun collide(point: Vector2d, filter: Predicate<CollisionPoly>): Boolean {
return queryTileCollisions(AABB.withSide(point, 2.0)).any { filter.test(it) && point in it.poly } return queryTileCollisions(AABB.withSide(point, 2.0)).any { filter.test(it) && point in it.poly }
} }
@ -838,7 +844,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
fun environmentStatusEffects(x: Double, y: Double): Collection<PersistentStatusEffect> { fun environmentStatusEffects(x: Double, y: Double): Collection<PersistentStatusEffect> {
return environmentStatusEffects(x.toInt(), y.toInt()) return environmentStatusEffects(x.floorToInt(), y.floorToInt())
} }
fun environmentStatusEffects(pos: IStruct2i): Collection<PersistentStatusEffect> { fun environmentStatusEffects(pos: IStruct2i): Collection<PersistentStatusEffect> {
@ -857,7 +863,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
fun weatherStatusEffects(x: Double, y: Double): Collection<EphemeralStatusEffect> { fun weatherStatusEffects(x: Double, y: Double): Collection<EphemeralStatusEffect> {
return weatherStatusEffects(x.toInt(), y.toInt()) return weatherStatusEffects(x.floorToInt(), y.floorToInt())
} }
fun weatherStatusEffects(pos: IStruct2i): Collection<EphemeralStatusEffect> { fun weatherStatusEffects(pos: IStruct2i): Collection<EphemeralStatusEffect> {

View File

@ -11,8 +11,10 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.lua.LuaThread
import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.setTableValue
import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toByteString import ru.dbotthepony.kstarbound.lua.toByteString
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
@ -159,6 +161,12 @@ class PathFinder(val world: World<*, *>, val start: Vector2d, val goal: Vector2d
return table return table
} }
fun store(lua: LuaThread) {
lua.pushTable(hashSize = 2)
lua.setTableValue("position", position)
lua.setTableValue("velocity", velocity)
}
fun reconstructPath(): MutableList<Edge> { fun reconstructPath(): MutableList<Edge> {
val result = ArrayList<Edge>() val result = ArrayList<Edge>()
var parent = parent var parent = parent

View File

@ -63,6 +63,9 @@ class StagehandEntity(isRemote: Boolean = false) : AbstractEntity(), ScriptedEnt
private var config: JsonObject = JsonObject() private var config: JsonObject = JsonObject()
val typeName: String
get() = config["type"]?.asString ?: ""
var isScripted = false var isScripted = false
private set private set

View File

@ -1,9 +1,12 @@
package ru.dbotthepony.kstarbound.world.entities.api package ru.dbotthepony.kstarbound.world.entities.api
import com.google.gson.JsonArray
import com.google.gson.JsonElement
interface ScriptedEntity { interface ScriptedEntity {
// Call a script function directly with the given arguments, should return // Call a script function directly with the given arguments, should return
// nothing only on failure. // nothing only on failure.
fun callScript(fnName: String, vararg arguments: Any?): Array<Any?> fun callScript(fnName: String, arguments: JsonArray): JsonElement
// Execute the given code directly in the underlying context, return nothing // Execute the given code directly in the underlying context, return nothing
// on failure. // on failure.

View File

@ -16,12 +16,14 @@ local math = math
local string = string local string = string
local format = string.format local format = string.format
local function checkarg(value, index, expected, fnName, overrideExpected) function checkarg(value, index, expected, fnName, overrideExpected)
if type(value) ~= expected then 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) error(string.format('bad argument #%d to %s: %s expected, got %s', index, fnName, overrideExpected or expected, type(value)), 3)
end end
end end
local checkarg = checkarg
-- this replicates original engine code, but it shouldn't work in first place -- this replicates original engine code, but it shouldn't work in first place
local function __newindex(self, key, value) local function __newindex(self, key, value)
local nils = getmetatable(self).__nils local nils = getmetatable(self).__nils

View File

@ -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

View File

@ -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

View File

@ -1,14 +1,24 @@
package ru.dbotthepony.kstarbound.test package ru.dbotthepony.kstarbound.test
import com.kenai.jffi.MemoryIO
import org.junit.jupiter.api.Test 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 { object LuaTests {
@Test @Test
fun test() { fun test() {
val lua = LuaState() val lua = LuaThread()
lua.load("print('Hello, world!')")
lua.call() lua.ensureExtraCapacity(1000)
lua.loadGlobal("collectgarbage")
lua.push("count")
lua.call(1, 1)
println(lua.popDouble()!! * 1024)
lua.close() lua.close()
} }
} }