From 7f16e643f33ded1ade8d0fbc7afee181eab29404 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 6 May 2024 22:45:50 +0700 Subject: [PATCH] Expose almost all World Lua bindings --- ADDITIONS.md | 29 +++ .../kotlin/ru/dbotthepony/kstarbound/Ext.kt | 37 --- .../kstarbound/client/world/ClientWorld.kt | 15 +- .../kstarbound/defs/dungeon/DungeonBrush.kt | 6 +- .../kstarbound/defs/tile/TileDamageResult.kt | 6 +- .../kstarbound/defs/world/WorldTemplate.kt | 14 +- .../dbotthepony/kstarbound/lua/Conversions.kt | 19 +- .../kstarbound/lua/bindings/WorldBindings.kt | 18 +- .../bindings/WorldEnvironmentalBindings.kt | 223 ++++++++++++++++++ .../kstarbound/server/world/ServerWorld.kt | 22 +- .../kstarbound/world/Raycasting.kt | 2 +- .../ru/dbotthepony/kstarbound/world/Utils.kt | 28 +++ .../ru/dbotthepony/kstarbound/world/World.kt | 30 ++- .../kstarbound/world/api/TileColor.kt | 5 +- 14 files changed, 388 insertions(+), 66 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/Utils.kt diff --git a/ADDITIONS.md b/ADDITIONS.md index 60ff9ef0..555b550c 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -137,6 +137,27 @@ val color: TileColor = TileColor.DEFAULT * Added `world.unsetUniverseFlag(flag: String): Boolean` * Added `world.placeDungeonAsync(name: String, position: Vector2d, dungeonID: Int?, seed: Long?): RpcPromise` * Added `world.tryPlaceDungeonAsync(name: String, position: Vector2d, dungeonID: Int?, seed: Long?): RpcPromise` + * Added `world.materialColorName(pos: Vector2i, layer: String): String`. Returns one of next strings: + * `"default"` + * `"red"` + * `"blue"` + * `"green"` + * `"yellow"` + * `"brown"` + * `"purple"` + * `"black"` + * `"white"` + * Added `world.spawnLiquidPromise(pos: Vector2i, liquid: Any, quantity: Float): RpcPromise` + * However, correct clientside result will be returned _only_ when using native protocol + * Added `world.damageTilesPromise(positions: List, layer: String, damageSource: Vector2d, damageType: String, damageAmount: Double, harvestLevel: Int = 999, sourceEntity: EntityID = 0): RpcPromise`, which return one of next values on promise completion: + * `"none"` + * `"protected"` + * `"normal"` + * Keep in mind that it returns top-most damage status, which means: + * If some tiles were protected, and others were not, it will return `"normal"`. + * If all tiles were protected, it will return `"protected"`. + * If none tiles were damaged, it will return `"none"`. + * Added `world.damageTileAreaPromise(radius: Double, position: Vector2i, layer: String, damageSource: Vector2d, damageType: String, damageAmount: Double, harvestLevel: Int = 999, sourceEntity: EntityID = 0): RpcPromise`, with same notes as `world.damageTilesPromise()` apply #### Changes @@ -155,6 +176,14 @@ val color: TileColor = TileColor.DEFAULT * `world.tryPlaceDungeon(name: String, position: Vector2d, dungeonID: Int?, seed: Long?): Boolean` now accepts optional `seed`. If not specified, engine will determine one (like original engine does). * Please update code to use `world.tryPlaceDungeonAsync`, because there are absolutely no guarantees dungeon will be generated the moment `world.tryPlaceDungeon` call returns * `world.setDungeonGravity(id: Int, gravity: Either)` now accept directional vector. **Attention:** Directional gravity is WIP. + * `world.setMaterialColor(pos: Vector2i, layer: String, color: Any): Boolean` + * Now returns boolean whenever cell was set (returns `false` when called with position outside of loaded chunks) + * Now accepts string name along integer index + * `world.placeMaterial(pos: Vector2i, layer: String, material: String, hueShift: Number?, allowOverlap: Boolean): RpcPromise` now returns `RpcPromise` of unapplied tile modifications instead of `Boolean`, which wasn't representative anyway, and outright wrong if this function was called on client + * However, correct clientside results will be returned _only_ when using native protocol + * `world.placeMod(pos: Vector2i, layer: String, mod: String, hueShift: Number?, allowOverlap: Boolean): RpcPromise` now returns `RpcPromise` of unapplied tile modifications instead of `Boolean`, which wasn't representative anyway, and outright wrong if this function was called on client + * However, correct clientside results will be returned _only_ when using native protocol + * `world.spawnLiquid(pos: Vector2i, liquid: Any, quantity: Float): Boolean` now accepts both liquid ID and liquid name. However, you should be using `world.spawnLiquidPromise(pos: Vector2i, liquid: Any, quantity: Float): RpcPromise` instead #### Fixes diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt index 160fbc05..541b2d6e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt @@ -47,44 +47,7 @@ operator fun ThreadLocal.setValue(thisRef: Any, property: KProperty<*>, v operator fun ImmutableMap.Builder.set(key: K, value: V): ImmutableMap.Builder = put(key, value) -fun String.sintern(): String = Starbound.STRINGS.intern(this) - inline fun Gson.fromJson(reader: JsonReader): T? = fromJson(reader, T::class.java) inline fun Gson.fromJson(reader: JsonElement): T? = getAdapter(T::class.java).read(FastJsonTreeReader(reader)) fun Gson.fromJsonFast(reader: JsonElement, type: Class): T = getAdapter(type).read(FastJsonTreeReader(reader)) - -/** - * guarantees even distribution of tasks while also preserving encountered order of elements - */ -fun Collection.batch(executor: ForkJoinPool, batchSize: Int = 16, mapper: (IStarboundFile) -> KOptional): Stream { - require(batchSize >= 1) { "Invalid batch size: $batchSize" } - - if (batchSize == 1 || size <= batchSize) { - val tasks = ArrayList>>() - - for (listedFile in this) { - tasks.add(executor.submit(Callable { mapper.invoke(listedFile) })) - } - - return tasks.stream().map { it.join() }.filter { it.isPresent }.map { it.value } - } - - val batches = ArrayList>>>() - var batch = ArrayList(batchSize) - - for (listedFile in this) { - batch.add(listedFile) - - if (batch.size >= batchSize) { - val mbatch = batch - batches.add(executor.submit(Callable { mbatch.map { mapper.invoke(it) } })) - batch = ArrayList(batchSize) - } - } - - if (batch.isNotEmpty()) - batches.add(executor.submit(Callable { batch.map { mapper.invoke(it) } })) - - return batches.stream().flatMap { it.join().stream() }.filter { it.isPresent }.map { it.value } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index 7604db3d..de0fd13a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -24,6 +24,8 @@ import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.Mesh import ru.dbotthepony.kstarbound.client.render.RenderLayer import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition +import ru.dbotthepony.kstarbound.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity @@ -43,6 +45,7 @@ import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess import ru.dbotthepony.kstarbound.world.api.TileView +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.positiveModulo import java.time.Duration import java.util.* @@ -332,7 +335,7 @@ class ClientWorld( modifications: Collection>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean - ): List> { + ): CompletableFuture>> { // send packets to server here // this is required because Lua scripts call this method // and Lua scripts want these changes to be applied serverside (good game security, i approve) @@ -400,6 +403,16 @@ class ClientWorld( return future } + override fun damageTiles( + positions: Collection, + isBackground: Boolean, + sourcePosition: Vector2d, + damage: TileDamage, + source: AbstractEntity? + ): CompletableFuture { + TODO("Not yet implemented") + } + companion object { val ring = listOf( Vector2i(0, 0), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt index 7f0239b6..48d6d53e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt @@ -2,7 +2,6 @@ package ru.dbotthepony.kstarbound.defs.dungeon import com.google.common.collect.ImmutableList import com.google.gson.Gson -import com.google.gson.JsonArray import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter @@ -29,7 +28,6 @@ import ru.dbotthepony.kstarbound.json.builder.JsonIgnore import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState -import ru.dbotthepony.kstarbound.world.api.AbstractTileState import ru.dbotthepony.kstarbound.world.api.ImmutableTileState import ru.dbotthepony.kstarbound.world.api.MutableLiquidState import ru.dbotthepony.kstarbound.world.api.TileColor @@ -114,14 +112,14 @@ abstract class DungeonBrush { isBackground, data.material, data.modifier, data.modHueShift, data.hueShift, - TileColor.entries.firstOrNull { it.lowercase == data.colorVariant.lowercase() } ?: TileColor.entries[data.colorVariant.toIntOrNull() ?: throw JsonSyntaxException("Invalid color variant: ${data.colorVariant}")] + TileColor.entries.firstOrNull { it.jsonName == data.colorVariant.lowercase() } ?: TileColor.entries[data.colorVariant.toIntOrNull() ?: throw JsonSyntaxException("Invalid color variant: ${data.colorVariant}")] ) constructor(isBackground: Boolean, material: Registry.Ref, data: TiledData) : this( isBackground, material, data.modifier, data.hueshift, data.modhueshift, - TileColor.entries.firstOrNull { it.lowercase == data.colorVariant.lowercase() } ?: TileColor.entries[data.colorVariant.toIntOrNull() ?: throw JsonSyntaxException("Invalid color variant: ${data.colorVariant}")] + TileColor.entries.firstOrNull { it.jsonName == data.colorVariant.lowercase() } ?: TileColor.entries[data.colorVariant.toIntOrNull() ?: throw JsonSyntaxException("Invalid color variant: ${data.colorVariant}")] ) @JsonFactory diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageResult.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageResult.kt index a87aba05..ca0b0a36 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageResult.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageResult.kt @@ -1,7 +1,11 @@ package ru.dbotthepony.kstarbound.defs.tile -enum class TileDamageResult { +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable + +enum class TileDamageResult : IStringSerializable { NONE, PROTECTED, NORMAL; + + override val jsonName: String = name.lowercase() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index 816c5cf2..70d6d6fd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -159,7 +159,7 @@ class WorldTemplate(val geometry: WorldGeometry) { return true } - fun surfaceLevel(): Int { + val surfaceLevel: Int get() { val parameters = worldParameters if (parameters is TerrestrialWorldParameters) { @@ -169,6 +169,18 @@ class WorldTemplate(val geometry: WorldGeometry) { return geometry.size.y / 2 } + val undergroundLevel: Int get() { + val parameters = worldParameters + + if (parameters is TerrestrialWorldParameters) { + return parameters.surfaceLayer.layerMinHeight + } else if (parameters is FloatingDungeonWorldParameters) { + return parameters.dungeonUndergroundLevel + } + + return 0 + } + class PotentialBiomeItems( // Potential items that would spawn at the given block assuming it is at val surfaceBiomeItems: List = listOf(), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt index 3002e451..aabf0dde 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt @@ -359,16 +359,6 @@ fun TableFactory.from(value: Poly?): Table? { } } -fun TableFactory.fromCollection(value: Collection): Table { - val table = newTable(value.size, 0) - - for ((k, v) in value.withIndex()) { - table.rawset(Conversions.normaliseKey(k + 1), from(v)) - } - - return table -} - fun TableFactory.from(value: IStruct2i?): Table? { value ?: return null @@ -428,3 +418,12 @@ fun TableFactory.from(value: AABB?): Table? { it.rawset(4L, value.maxs.y) } } + +fun TableFactory.tableFrom(collection: Collection): Table { + val alloc = newTable(collection.size, 0) + + for ((i, v) in collection.withIndex()) + alloc[i + 1L] = v + + return alloc +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt index 3b3005f6..1c9836ba 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -40,6 +40,7 @@ import ru.dbotthepony.kstarbound.lua.toPoly import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2i import ru.dbotthepony.kstarbound.lua.unpackAsArray +import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.server.world.ServerWorld @@ -518,9 +519,21 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { returnBuffer.setTo(from(self.gravityAt(toVector2d(pos)))) } + callbacks["spawnLiquidPromise"] = luaFunction { pos: Table, liquid: Any, quantity: Number -> + val action = TileModification.Pour(if (liquid is ByteString) Registries.liquid.ref(liquid.decode()) else Registries.liquid.ref((liquid as Number).toInt()), quantity.toFloat()) + + returnBuffer.setTo( + LuaFuture( + future = self.applyTileModifications(listOf(toVector2i(pos) to action), false) + .thenApply { it.isEmpty() }, + isLocal = false + ) + ) + } + callbacks["spawnLiquid"] = luaFunction { pos: Table, liquid: Any, quantity: Number -> val action = TileModification.Pour(if (liquid is ByteString) Registries.liquid.ref(liquid.decode()) else Registries.liquid.ref((liquid as Number).toInt()), quantity.toFloat()) - returnBuffer.setTo(self.applyTileModifications(listOf(toVector2i(pos) to action), false).isEmpty()) + returnBuffer.setTo(self.applyTileModifications(listOf(toVector2i(pos) to action), false).thenApply { it.isEmpty() }.getNow(true)) } callbacks["destroyLiquid"] = luaFunction { pos: Table -> @@ -549,7 +562,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { callbacks["type"] = luaFunction { returnBuffer.setTo(self.template.worldParameters?.typeName ?: "unknown") } callbacks["size"] = luaFunction { returnBuffer.setTo(from(self.geometry.size)) } callbacks["inSurfaceLayer"] = luaFunction { pos: Table -> returnBuffer.setTo(self.template.isSurfaceLayer(toVector2i(pos))) } - callbacks["surfaceLevel"] = luaFunction { returnBuffer.setTo(self.template.surfaceLevel()) } + callbacks["surfaceLevel"] = luaFunction { returnBuffer.setTo(self.template.surfaceLevel) } callbacks["terrestrial"] = luaFunction { returnBuffer.setTo(self.template.worldParameters is TerrestrialWorldParameters) } callbacks["itemDropItem"] = luaFunction { id: Number -> @@ -589,6 +602,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { callbacks["dungeonId"] = luaFunction { pos: Table -> returnBuffer.setTo(self.getCell(toVector2i(pos)).dungeonId) } provideWorldEntitiesBindings(self, callbacks, lua) + provideWorldEnvironmentalBindings(self, callbacks, lua) if (self is ServerWorld) { provideServerWorldBindings(self, callbacks, lua) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt new file mode 100644 index 00000000..062076b3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEnvironmentalBindings.kt @@ -0,0 +1,223 @@ +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.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult +import ru.dbotthepony.kstarbound.defs.tile.TileDamageType +import ru.dbotthepony.kstarbound.defs.tile.isEmptyModifier +import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile +import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyModifier +import ru.dbotthepony.kstarbound.defs.tile.isNullTile +import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.from +import ru.dbotthepony.kstarbound.lua.get +import ru.dbotthepony.kstarbound.lua.iterator +import ru.dbotthepony.kstarbound.lua.luaFunction +import ru.dbotthepony.kstarbound.lua.luaFunctionN +import ru.dbotthepony.kstarbound.lua.luaStub +import ru.dbotthepony.kstarbound.lua.nextOptionalInteger +import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.tableFrom +import ru.dbotthepony.kstarbound.lua.tableOf +import ru.dbotthepony.kstarbound.lua.toVector2d +import ru.dbotthepony.kstarbound.lua.toVector2i +import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture +import ru.dbotthepony.kstarbound.util.valueOf +import ru.dbotthepony.kstarbound.world.TileModification +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.api.TileColor +import ru.dbotthepony.kstarbound.world.tileAreaBrush +import java.util.concurrent.CompletableFuture + +private val foregroundStr = ByteString.of("foreground") +private val backgroundStr = ByteString.of("background") + +private fun isBackground(layer: ByteString): Boolean { + return if (layer == backgroundStr) + true + else if (layer == foregroundStr) + false + else + throw LuaRuntimeException("Invalid tile layer $layer") +} + +private fun ExecutionContext.damageTilesImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture { + val positions = it.nextTable().iterator().map { toVector2i(it.value) }.toList() + val isBackground = isBackground(it.nextString()) + + val sourcePosition = toVector2d(it.nextTable()) + val damageType = TileDamageType.entries.valueOf(it.nextString().decode()) + val damage = it.nextFloat() + val harvestLevel = it.nextOptionalInteger()?.toInt() ?: 999 + val sourceEntity = self.entities[it.nextOptionalInteger()?.toInt() ?: 0] + + return self.damageTiles(positions, isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity) +} + +private fun ExecutionContext.damageTileAreaImpl(self: World<*, *>, it: ArgumentIterator): CompletableFuture { + val center = toVector2i(it.nextTable()) + val radius = it.nextFloat() + val isBackground = isBackground(it.nextString()) + + 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(tileAreaBrush(center, radius), isBackground, sourcePosition, TileDamage(damageType, damage, harvestLevel), sourceEntity) +} + +fun provideWorldEnvironmentalBindings(self: World<*, *>, callbacks: Table, lua: LuaEnvironment) { + callbacks["lightLevel"] = luaStub("lightLevel") + callbacks["windLevel"] = luaStub("windLevel") + + callbacks["breathable"] = luaFunction { pos: Table -> + returnBuffer.setTo(self.isBreathable(toVector2i(pos))) + } + + callbacks["underground"] = luaFunction { pos: Table -> + returnBuffer.setTo(self.template.undergroundLevel >= toVector2d(pos).y) + } + + callbacks["material"] = luaFunction { pos: Table, layer: ByteString -> + val isBackground = isBackground(layer) + val tile = self.getCell(toVector2i(pos)).tile(isBackground) + + if (tile.material.isNullTile) { + returnBuffer.setTo() + } else if (tile.material.isEmptyTile) { + returnBuffer.setTo(false) + } else { + returnBuffer.setTo(tile.material.key) + } + + } + callbacks["mod"] = luaFunction { pos: Table, layer: ByteString -> + val isBackground = isBackground(layer) + val tile = self.getCell(toVector2i(pos)).tile(isBackground) + + if (tile.modifier.isNotEmptyModifier) { + returnBuffer.setTo(tile.modifier.key) + } + } + + callbacks["materialHueShift"] = luaFunction { pos: Table, layer: ByteString -> + val isBackground = isBackground(layer) + val tile = self.getCell(toVector2i(pos)).tile(isBackground) + returnBuffer.setTo(tile.hueShift) + } + + callbacks["modHueShift"] = luaFunction { pos: Table, layer: ByteString -> + val isBackground = isBackground(layer) + val tile = self.getCell(toVector2i(pos)).tile(isBackground) + returnBuffer.setTo(tile.modifierHueShift) + } + + callbacks["materialColor"] = luaFunction { pos: Table, layer: ByteString -> + val isBackground = isBackground(layer) + val tile = self.getCell(toVector2i(pos)).tile(isBackground) + returnBuffer.setTo(tile.color.ordinal) + } + + callbacks["materialColorName"] = luaFunction { pos: Table, layer: ByteString -> + val isBackground = isBackground(layer) + val tile = self.getCell(toVector2i(pos)).tile(isBackground) + returnBuffer.setTo(tile.color.jsonName) + } + + callbacks["setMaterialColor"] = luaFunction { pos: Table, layer: ByteString, color: Any -> + val isBackground = isBackground(layer) + + val actualColor = if (color is Number) + TileColor.entries[color.toInt()] + else if (color is ByteString) + TileColor.entries.valueOf(color.decode()) + else + throw LuaRuntimeException("Unknown tile color $color") + + val actualPos = toVector2i(pos) + val cell = self.getCell(actualPos).mutable() + cell.tile(isBackground).color = actualColor + returnBuffer.setTo(self.setCell(actualPos, cell)) + } + + callbacks["oceanLevel"] = luaFunction { pos: Table -> + returnBuffer.setTo(self.template.cellInfo(toVector2i(pos)).oceanLiquidLevel) + } + + callbacks["environmentStatusEffects"] = luaFunction { pos: Table -> + returnBuffer.setTo(tableFrom(self.environmentStatusEffects(toVector2i(pos)))) + } + + callbacks["damageTiles"] = luaFunctionN("damageTiles") { + returnBuffer.setTo(damageTilesImpl(self, it).getNow(TileDamageResult.NONE) != TileDamageResult.NONE) + } + + callbacks["damageTilesPromise"] = luaFunctionN("damageTilesPromise") { + returnBuffer.setTo( + LuaFuture( + future = damageTilesImpl(self, it).thenApply { it.jsonName }, + isLocal = false + ) + ) + } + + callbacks["damageTileArea"] = luaFunctionN("damageTileArea") { + returnBuffer.setTo(damageTileAreaImpl(self, it).getNow(TileDamageResult.NONE) != TileDamageResult.NONE) + } + + callbacks["damageTileAreaPromise"] = luaFunctionN("damageTileAreaPromise") { + returnBuffer.setTo( + LuaFuture( + future = damageTileAreaImpl(self, it).thenApply { it.jsonName }, + isLocal = false + ) + ) + } + + callbacks["placeMaterial"] = luaFunctionN("placeMaterial") { + val pos = toVector2i(it.nextTable()) + val isBackground = isBackground(it.nextString()) + val material = Registries.tiles.getOrThrow(it.nextString().decode()) + val hueShift: Float? = (it.nextAny() as? Number)?.let { it.toFloat() / 255f * 360f } + val allowOverlap = it.nextBoolean() + + val action = TileModification.PlaceMaterial(isBackground, material.ref, hueShift) + + // original engine return here doesn't make any sense whatsoever + returnBuffer.setTo( + LuaFuture( + future = self.applyTileModifications(listOf(pos to action), allowOverlap, false) + .thenApply { tableFrom(it.map { from(it.first) }) }, + isLocal = false + ) + ) + } + + callbacks["placeMod"] = luaFunctionN("placeMod") { + val pos = toVector2i(it.nextTable()) + val isBackground = isBackground(it.nextString()) + val modifier = Registries.tileModifiers.getOrThrow(it.nextString().decode()) + val hueShift: Float? = (it.nextAny() as? Number)?.let { it.toFloat() / 255f * 360f } + val allowOverlap = it.nextBoolean() + + val action = TileModification.PlaceModifier(isBackground, modifier.ref, hueShift) + + // original engine return here doesn't make any sense whatsoever + returnBuffer.setTo( + LuaFuture( + future = self.applyTileModifications(listOf(pos to action), allowOverlap, false) + .thenApply { tableFrom(it.map { from(it.first) }) }, + isLocal = false + ) + ) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index e3a48d1d..6d204eab 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -337,6 +337,18 @@ class ServerWorld private constructor( return updated } + override fun damageTiles( + positions: Collection, + isBackground: Boolean, + sourcePosition: Vector2d, + damage: TileDamage, + source: AbstractEntity? + ): CompletableFuture { + return runBlocking { + CompletableFuture.completedFuture(damageTiles(positions, isBackground, sourcePosition, damage, source, null)) + } + } + /** * this method does not block if pacer is null (safe to use with runBlocking {}) */ @@ -460,8 +472,10 @@ class ServerWorld private constructor( return topMost } - override fun applyTileModifications(modifications: Collection>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean): List> { - return runBlocking { applyTileModifications(modifications, allowEntityOverlap, ignoreTileProtection, null) } + override fun applyTileModifications(modifications: Collection>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean): CompletableFuture>> { + return runBlocking { + CompletableFuture.completedFuture(applyTileModifications(modifications, allowEntityOverlap, ignoreTileProtection, null)) + } } suspend fun applyTileModifications(modifications: Collection>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false, pacer: ActionPacer?): List> { @@ -629,7 +643,7 @@ class ServerWorld private constructor( try { LOGGER.debug("Trying to find player spawn position...") - var pos = hint ?: CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart(random) }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble()) + var pos = hint ?: CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart(random) }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel.toDouble()) var previous = pos LOGGER.debug("Trying to find player spawn position near {}...", pos) @@ -686,7 +700,7 @@ class ServerWorld private constructor( } } - pos = CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart(random) }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble()) + pos = CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart(random) }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel.toDouble()) if (previous != pos) { LOGGER.debug("Still trying to find player spawn position near {}...", pos) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt index 2b1699a5..8c2b4af0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt @@ -99,7 +99,7 @@ fun ICellAccess.castRay( val hitTiles = ArrayList() var cellPosX = roundTowardsNegativeInfinity(start.x) var cellPosY = roundTowardsNegativeInfinity(start.y) - var cell = getCell(cellPosX, cellPosY) ?: return RayCastResult(start, Vector2d.ZERO) + var cell = getCell(cellPosX, cellPosY) val direction = (end - start).unitVector diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Utils.kt new file mode 100644 index 00000000..cca9d149 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Utils.kt @@ -0,0 +1,28 @@ +package ru.dbotthepony.kstarbound.world + +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import ru.dbotthepony.kstarbound.math.vector.Vector2i +import kotlin.math.pow +import kotlin.math.roundToInt + +fun tileAreaBrush(pos: Vector2i, radius: Double, square: Boolean = false): List { + if (radius <= 0.0) + return emptyList() + + val result = ObjectArrayList() + val squareRadius = (if (square) Double.POSITIVE_INFINITY else radius).pow(2.0) + val intRadius = radius.roundToInt() + + for (x in -intRadius .. intRadius) { + for (y in -intRadius .. intRadius) { + val length = x * x + y * y + + if (length <= squareRadius) { + result.add(Vector2i(x + pos.x, y + pos.y)) + } + } + } + + result.sortWith { o1, o2 -> o1.distanceSquared(pos).compareTo(o2.distanceSquared(pos)) } + return result +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 56f4bb8d..e7153643 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -30,6 +30,8 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.ARTIFICIAL_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.DESTROYED_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition +import ru.dbotthepony.kstarbound.defs.tile.TileDamage +import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate @@ -42,6 +44,7 @@ import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket +import ru.dbotthepony.kstarbound.util.ActionPacer import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.api.ICellAccess @@ -587,11 +590,23 @@ abstract class World, ChunkType : Chunk, val average: Double) @@ -622,7 +637,7 @@ abstract class World, ChunkType : Chunk>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false): List> + abstract fun applyTileModifications(modifications: Collection>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false): CompletableFuture>> fun addDamageNotification(notification: DamageNotificationPacket) { pushRemoteDamageNotification(notification) @@ -719,6 +734,15 @@ abstract class World, ChunkType : Chunk { + // TODO: judging by original code, they wanted to allow definition + // of custom environmental effects per dungeon id + // But it never happened + return template.worldParameters?.environmentStatusEffects ?: setOf() + } + + abstract fun damageTiles(positions: Collection, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): CompletableFuture + companion object { private val LOGGER = LogManager.getLogger() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/TileColor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/TileColor.kt index f4e7bad5..3fe249c3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/TileColor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/TileColor.kt @@ -1,9 +1,10 @@ package ru.dbotthepony.kstarbound.world.api import com.google.common.collect.ImmutableMap +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable // uint8_t -enum class TileColor { +enum class TileColor : IStringSerializable { DEFAULT, RED, BLUE, @@ -14,7 +15,7 @@ enum class TileColor { BLACK, WHITE; - val lowercase = name.lowercase() + override val jsonName = name.lowercase() companion object { private val map: ImmutableMap