Add more world bindings

This commit is contained in:
DBotThePony 2024-04-20 14:44:20 +07:00
parent 5c840eaf77
commit 7e26f0d3b8
Signed by: DBot
GPG Key ID: DCC23B5715498507
50 changed files with 1686 additions and 115 deletions

View File

@ -73,12 +73,27 @@ val color: TileColor = TileColor.DEFAULT
* `centered` (defaults to `true`) * `centered` (defaults to `true`)
* `fullbright` (defaults to `false`) * `fullbright` (defaults to `false`)
#### .liquid
* `liquidId` is no longer essential and can be skipped; engine **will not** assign it to anything, but liquid will still be fully functional from engine's point of view
* However, this has serious implications:
* Liquid will become "invisible" to legacy clients (this is not guaranteed, and if it ever "bleeds" into structures sent to legacy clients due to missed workarounds in code, legacy client will blow up.)
* Lua scripts written solely for original engine won't see this liquid too (this includes base game assets!), unless they use new improved functions
* `liquidId` can be specified as any number in 1 -> 2^31 - 1 range (0 is reserved for "empty" meta-liquid)
* This will make liquid "invisible" to original clients only, Lua code should continue to function normally
* This is not guaranteed, and if it ever "bleeds" into structures sent to legacy clients due to missed workarounds in code, legacy client will blow up.
#### .matierial #### .matierial
* Meta-materials are no longer treated uniquely, and are defined as "real" materials, just like every other material, but still preserve unique interactions.
* `materialId` is no longer essential and can be skipped, with same notes as described in `liquidId`.
* `materialId` can be specified as any number in 1 -> 2^31 - 1 (softly excluding reserved "meta materials" ID range, since this range is not actually reserved, but is expected to be used solely by meta materials), with legacy client implications only.
* Implemented `isConnectable`, which was planned by original developers, but scrapped in process (defaults to `true`, by default only next meta-materials have it set to false: `empty`, `null` and `boundary`) * Implemented `isConnectable`, which was planned by original developers, but scrapped in process (defaults to `true`, by default only next meta-materials have it set to false: `empty`, `null` and `boundary`)
* Used by object and plant anchoring code to determine valid placement * Used by object and plant anchoring code to determine valid placement
* Used by world tile rendering code (render piece rule `Connects`) * Used by world tile rendering code (render piece rule `Connects`)
* And finally, used by `canPlaceMaterial` to determine whenever player can place blocks next to it (at least one such tile should be present for player to be able to place blocks next to it) * And finally, used by `canPlaceMaterial` to determine whenever player can place blocks next to it (at least one such tile should be present for player to be able to place blocks next to it)
#### .matmod
* `modId` is no longer essential and can be skipped, or specified as any number in 1 -> 2^31 range, with notes of `materialId` and `liquidId` apply.
## Scripting ## Scripting
--------------- ---------------
@ -102,6 +117,19 @@ val color: TileColor = TileColor.DEFAULT
* Added `animator.hasEffect(effect: string): boolean` * Added `animator.hasEffect(effect: string): boolean`
* Added `animator.parts(): List<string>` * Added `animator.parts(): List<string>`
#### world
* 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.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.gravityVector(at: Vector2d): Vector2d`. **Attention:** directional gravity is WIP.
* Added `world.itemDropLineQuery(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.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target`
## Behavior ## Behavior
--------------- ---------------

View File

@ -7,6 +7,7 @@ import io.netty.channel.ChannelInitializer
import io.netty.channel.local.LocalAddress import io.netty.channel.local.LocalAddress
import io.netty.channel.local.LocalChannel import io.netty.channel.local.LocalChannel
import io.netty.channel.socket.nio.NioSocketChannel import io.netty.channel.socket.nio.NioSocketChannel
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -34,8 +35,40 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn
channel.flush() channel.flush()
} }
private val occupiedEntityIDs = IntAVLTreeSet()
private var nextEntityID = 0
fun nextEntityID(): Int {
if (nextEntityID !in entityIDRange) {
nextEntityID = entityIDRange.first
}
var itrs = 0
while (occupiedEntityIDs.contains(nextEntityID)) {
if (++nextEntityID !in entityIDRange) {
nextEntityID = entityIDRange.first
}
if (itrs++ > 66000) {
throw RuntimeException("Ran out of entity IDs to allocate!")
}
}
occupiedEntityIDs.add(nextEntityID)
return nextEntityID
}
fun freeEntityID(id: Int) {
occupiedEntityIDs.remove(id)
}
fun resetOccupiedEntityIDs() {
occupiedEntityIDs.clear()
}
fun enqueue(task: StarboundClient.() -> Unit) { fun enqueue(task: StarboundClient.() -> Unit) {
client.mailbox.execute { task.invoke(client) } client.execute { task.invoke(client) }
} }
override fun toString(): String { override fun toString(): String {

View File

@ -71,9 +71,9 @@ enum class RenderLayer {
fun tileLayer(isBackground: Boolean, isModifier: Boolean, tile: AbstractTileState): Point { fun tileLayer(isBackground: Boolean, isModifier: Boolean, tile: AbstractTileState): Point {
if (isModifier) { if (isModifier) {
return tileLayer(isBackground, true, tile.modifier?.value?.renderParameters?.zLevel ?: 0L, tile.modifier?.value?.modId?.toLong() ?: 0L, tile.modifierHueShift) return tileLayer(isBackground, true, tile.modifier.value.renderParameters.zLevel, tile.modifier.value.modId?.toLong() ?: 0L, tile.modifierHueShift)
} else { } else {
return tileLayer(isBackground, false, tile.material.value.renderParameters.zLevel, tile.material.value.materialId.toLong(), tile.hueShift) return tileLayer(isBackground, false, tile.material.value.renderParameters.zLevel, tile.material.value.materialId?.toLong() ?: 0L, tile.hueShift)
} }
} }

View File

@ -1,6 +1,8 @@
package ru.dbotthepony.kstarbound.client.world package ru.dbotthepony.kstarbound.client.world
import com.google.common.base.Supplier import com.google.common.base.Supplier
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.LongArraySet import it.unimi.dsi.fastutil.longs.LongArraySet
@ -20,9 +22,11 @@ import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateWorldPropertiesPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.TileModification
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
@ -66,6 +70,10 @@ class ClientWorld(
override val eventLoop: BlockableEventLoop override val eventLoop: BlockableEventLoop
get() = client get() = client
override fun setProperty0(key: String, value: JsonElement) {
client.activeConnection?.send(UpdateWorldPropertiesPacket(JsonObject().apply { add(key, value) }))
}
inner class RenderRegion(val x: Int, val y: Int) { inner class RenderRegion(val x: Int, val y: Int) {
inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) {
val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, RenderLayer.Point>>() val bakedMeshes = ArrayList<Pair<ConfiguredMesh<*>, RenderLayer.Point>>()
@ -305,6 +313,17 @@ class ClientWorld(
} }
} }
override fun applyTileModifications(
modifications: Collection<Pair<Vector2i, TileModification>>,
allowEntityOverlap: Boolean,
ignoreTileProtection: Boolean
): List<Pair<Vector2i, TileModification>> {
// 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)
TODO("Not yet implemented")
}
companion object { companion object {
val ring = listOf( val ring = listOf(
Vector2i(0, 0), Vector2i(0, 0),

View File

@ -32,6 +32,7 @@ import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.EnumSet
// uint8_t // uint8_t
enum class TeamType(override val jsonName: String) : IStringSerializable { enum class TeamType(override val jsonName: String) : IStringSerializable {
@ -83,7 +84,44 @@ data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int =
stream.writeShort(team) stream.writeShort(team)
} }
fun canDamage(victim: EntityDamageTeam, victimIsSelf: Boolean): Boolean {
if (victimIsSelf) {
return type == TeamType.INDISCRIMINATE
}
return when (type) {
TeamType.NULL -> false
TeamType.FRIENDLY -> victim.type in damageableByFriendly
TeamType.ENEMY -> victim.type in damageableByEnemy || victim.type == TeamType.ENEMY && team != victim.team
TeamType.PVP -> victim.type in damageableByFriendly || victim.type == TeamType.PVP && (team == 0 || team != victim.team)
TeamType.PASSIVE -> false // never deal damage
TeamType.GHOSTLY -> false // never deal damage
TeamType.ENVIRONMENT -> victim.type in damageableByEnvironment
TeamType.INDISCRIMINATE -> victim.type != TeamType.GHOSTLY
TeamType.ASSISTANT -> victim.type in damageableByFriendly
}
}
companion object { companion object {
private val damageableByFriendly = EnumSet.noneOf(TeamType::class.java)
private val damageableByEnemy = EnumSet.noneOf(TeamType::class.java)
private val damageableByEnvironment = EnumSet.noneOf(TeamType::class.java)
init {
damageableByFriendly.add(TeamType.ENEMY)
damageableByFriendly.add(TeamType.PASSIVE)
damageableByFriendly.add(TeamType.ENVIRONMENT)
damageableByFriendly.add(TeamType.INDISCRIMINATE)
damageableByEnemy.add(TeamType.FRIENDLY)
damageableByEnemy.add(TeamType.PVP)
damageableByEnemy.add(TeamType.INDISCRIMINATE)
damageableByEnvironment.add(TeamType.FRIENDLY)
damageableByEnvironment.add(TeamType.PVP)
damageableByEnvironment.add(TeamType.INDISCRIMINATE)
}
val NULL = EntityDamageTeam() val NULL = EntityDamageTeam()
val PASSIVE = EntityDamageTeam(TeamType.PASSIVE) val PASSIVE = EntityDamageTeam(TeamType.PASSIVE)
val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write) val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write)

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@ -21,6 +22,7 @@ import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -230,6 +232,10 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
*/ */
abstract fun flop(): Drawable abstract fun flop(): Drawable
open fun toJson(): JsonObject {
return JsonObject()
}
companion object { companion object {
val EMPTY = Empty() val EMPTY = Empty()
val CENTERED = Transformations(true) val CENTERED = Transformations(true)
@ -245,8 +251,8 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
private val vertices = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, Vector2f::class.java)) as TypeAdapter<ImmutableList<Vector2f>> private val vertices = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, Vector2f::class.java)) as TypeAdapter<ImmutableList<Vector2f>>
private val transformations = gson.getAdapter(Transformations::class.java) private val transformations = gson.getAdapter(Transformations::class.java)
override fun write(out: JsonWriter?, value: Drawable?) { override fun write(out: JsonWriter, value: Drawable?) {
TODO("Not yet implemented") out.value(value?.toJson())
} }
override fun read(`in`: JsonReader): Drawable { override fun read(`in`: JsonReader): Drawable {

View File

@ -3,15 +3,15 @@ package ru.dbotthepony.kstarbound.defs
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class EntityType(override val jsonName: String) : IStringSerializable { enum class EntityType(override val jsonName: String, val storeName: String) : IStringSerializable {
PLANT("PlantEntity"), PLANT("plant", "PlantEntity"),
OBJECT("ObjectEntity"), OBJECT("object", "ObjectEntity"),
VEHICLE("VehicleEntity"), VEHICLE("vehicle", "VehicleEntity"),
ITEM_DROP("ItemDropEntity"), ITEM_DROP("itemDrop", "ItemDropEntity"),
PLANT_DROP("PlantDropEntity"), // wat PLANT_DROP("plantDrop", "PlantDropEntity"), // wat
PROJECTILE("ProjectileEntity"), PROJECTILE("projectile", "ProjectileEntity"),
STAGEHAND("StagehandEntity"), STAGEHAND("stagehand", "StagehandEntity"),
MONSTER("MonsterEntity"), MONSTER("monster", "MonsterEntity"),
NPC("NpcEntity"), NPC("npc", "NpcEntity"),
PLAYER("PlayerEntity"); PLAYER("player", "PlayerEntity");
} }

View File

@ -502,7 +502,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
obj.orientationIndex = orientation.toLong() obj.orientationIndex = orientation.toLong()
obj.joinWorld(parent) obj.joinWorld(parent)
} else { } else {
LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!") LOGGER.error("Dungeon generator tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!")
} }
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Exception while putting dungeon object $obj at ${obj!!.tilePosition}", err) LOGGER.error("Exception while putting dungeon object $obj at ${obj!!.tilePosition}", err)

View File

@ -98,6 +98,9 @@ fun ItemDescriptor(data: Table, stateMachine: StateMachine): Supplier<ItemDescri
} }
fun ExecutionContext.ItemDescriptor(data: Table): ItemDescriptor { fun ExecutionContext.ItemDescriptor(data: Table): ItemDescriptor {
if (data.metatable?.rawget("__nils") != null)
return ItemDescriptor(toJsonFromLua(data)) // assume it is json
val name = indexNoYield(data, 1L) ?: indexNoYield(data, "name") ?: indexNoYield(data, "item") val name = indexNoYield(data, 1L) ?: indexNoYield(data, "name") ?: indexNoYield(data, "item")
val count = indexNoYield(data, 2L) ?: indexNoYield(data, "count") ?: 1L val count = indexNoYield(data, 2L) ?: indexNoYield(data, "count") ?: 1L
val parameters = indexNoYield(data, 3L) ?: indexNoYield(data, "parameters") ?: indexNoYield(data, "data") val parameters = indexNoYield(data, 3L) ?: indexNoYield(data, "parameters") ?: indexNoYield(data, "data")
@ -114,7 +117,17 @@ fun ExecutionContext.ItemDescriptor(data: Table): ItemDescriptor {
} }
} }
@Deprecated("Does not obey meta methods, need to find replacement where possible") fun ExecutionContext.ItemDescriptor(data: Any?): ItemDescriptor {
if (data is ByteString) {
return ItemDescriptor(data.decode(), 1L, JsonObject())
} else if (data is Table) {
return ItemDescriptor(data)
} else {
return ItemDescriptor.EMPTY
}
}
@Deprecated("Does not obey meta methods, find replacement where possible")
fun ItemDescriptor(data: Table): ItemDescriptor { fun ItemDescriptor(data: Table): ItemDescriptor {
val name = data[1L] ?: data["name"] ?: data["item"] val name = data[1L] ?: data["name"] ?: data["item"]
val count = data[2L] ?: data["count"] ?: 1L val count = data[2L] ?: data["count"] ?: 1L

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -10,7 +11,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
@JsonFactory @JsonFactory
data class LiquidDefinition( data class LiquidDefinition(
val name: String, val name: String,
val liquidId: Int, val liquidId: Int?,
val description: String = "...", val description: String = "...",
val tickDelta: Int = 1, val tickDelta: Int = 1,
val color: RGBAColor, val color: RGBAColor,
@ -24,7 +25,7 @@ data class LiquidDefinition(
val isMeta: Boolean = false, val isMeta: Boolean = false,
) { ) {
@JsonFactory @JsonFactory
data class Interaction(val liquid: Int, val liquidResult: Int? = null, val materialResult: String? = null) { data class Interaction(val liquid: Either<Int, String>, val liquidResult: Either<Int, String>? = null, val materialResult: String? = null) {
init { init {
require(liquidResult != null || materialResult != null) { "Both liquidResult and materialResult are missing" } require(liquidResult != null || materialResult != null) { "Both liquidResult and materialResult are missing" }
} }

View File

@ -15,7 +15,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
@JsonFactory @JsonFactory
data class TileDefinition( data class TileDefinition(
val materialId: Int, val materialId: Int?,
val materialName: String, val materialName: String,
val particleColor: RGBAColor? = null, val particleColor: RGBAColor? = null,
val itemDrop: String? = null, val itemDrop: String? = null,
@ -54,7 +54,7 @@ data class TileDefinition(
val blocksLiquidFlow: Boolean = collisionKind.isSolidCollision, val blocksLiquidFlow: Boolean = collisionKind.isSolidCollision,
) : IRenderableTile, IThingWithDescription by descriptionData { ) : IRenderableTile, IThingWithDescription by descriptionData {
init { init {
require(materialId > 0) { "Invalid tile ID $materialId" } require(materialId == null || materialId > 0) { "Invalid tile ID $materialId" }
} }
fun supportsModifier(modifier: Registry.Entry<TileModifierDefinition>): Boolean { fun supportsModifier(modifier: Registry.Entry<TileModifierDefinition>): Boolean {

View File

@ -11,7 +11,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
@JsonFactory @JsonFactory
data class TileModifierDefinition( data class TileModifierDefinition(
val modId: Int, val modId: Int?,
val modName: String, val modName: String,
val itemDrop: String? = null, val itemDrop: String? = null,
val health: Double? = null, val health: Double? = null,
@ -39,7 +39,7 @@ data class TileModifierDefinition(
override val renderParameters: RenderParameters override val renderParameters: RenderParameters
) : IRenderableTile, IThingWithDescription by descriptionData { ) : IRenderableTile, IThingWithDescription by descriptionData {
init { init {
require(modId > 0) { "Invalid tile modifier ID $modId" } require(modId == null || modId > 0) { "Invalid tile modifier ID $modId" }
} }
val actualDamageTable: TileDamageConfig by lazy { val actualDamageTable: TileDamageConfig by lazy {

View File

@ -536,6 +536,9 @@ class WorldLayout {
fun getWeighting(x: Int, y: Int): List<RegionWeighting> { fun getWeighting(x: Int, y: Int): List<RegionWeighting> {
val weighting = ArrayList<RegionWeighting>() val weighting = ArrayList<RegionWeighting>()
if (layers.isEmpty())
return weighting
fun addLayerWeighting(layer: Layer, x: Int, weightFactor: Double) { fun addLayerWeighting(layer: Layer, x: Int, weightFactor: Double) {
if (layer.cells.isEmpty()) if (layer.cells.isEmpty())
return return
@ -610,6 +613,28 @@ class WorldLayout {
return weighting return weighting
} }
fun findLayer(y: Int): Layer? {
if (layers.isEmpty())
return null
if (y == layers.first().yStart) {
return layers.first()
} else if (y < layers.first().yStart) {
return null
} else if (y >= layers.last().yStart) {
return layers.last()
} else {
return layers[layers.indexOfFirst { it.yStart >= y } - 1]
}
}
fun findLayerAndCell(x: Int, y: Int): Pair<Layer, Region>? {
// find the target layer
val layer = findLayer(y) ?: return null
val cell = layer.findContainingCell(x)
return layer to layer.cells[cell.first]
}
companion object : TypeAdapter<WorldLayout>() { companion object : TypeAdapter<WorldLayout>() {
override fun write(out: JsonWriter, value: WorldLayout?) { override fun write(out: JsonWriter, value: WorldLayout?) {
if (value == null) if (value == null)

View File

@ -513,6 +513,16 @@ class WorldTemplate(val geometry: WorldGeometry) {
return info return info
} }
fun isSurfaceLayer(x: Int, y: Int): Boolean {
val parameters = worldParameters as? TerrestrialWorldParameters ?: return false
val layout = worldLayout ?: return false
return layout.findLayerAndCell(x, y)?.first == layout.findLayerAndCell(x, parameters.surfaceLayer.layerBaseHeight)?.first
}
fun isSurfaceLayer(pos: Vector2i): Boolean {
return isSurfaceLayer(pos.x, pos.y)
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()

View File

@ -25,6 +25,30 @@ interface IContainer {
return any return any
} }
fun hasCountOfItem(item: ItemDescriptor, exactMatch: Boolean = false): Long {
var count = 0L
for (i in 0 until size) {
if (this[i].matches(item, exactMatch)) {
count += this[i].size
}
}
return count
}
fun hasCountOfItem(item: ItemStack, exactMatch: Boolean = false): Long {
var count = 0L
for (i in 0 until size) {
if (this[i].matches(item, exactMatch)) {
count += this[i].size
}
}
return count
}
// puts item into container, returns remaining not put items // puts item into container, returns remaining not put items
fun add(item: ItemStack, simulate: Boolean = false): ItemStack { fun add(item: ItemStack, simulate: Boolean = false): ItemStack {
val copy = item.copy() val copy = item.copy()

View File

@ -304,7 +304,27 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para
if (isEmpty || other.isEmpty) if (isEmpty || other.isEmpty)
return false return false
return size != 0L && other.size != 0L && maxStackSize > size && other.config == config && other.parameters == parameters return size != 0L && other.size != 0L && maxStackSize > size && entry == other.entry && other.config == config && other.parameters == parameters
}
/**
* whenever items match, ignoring their sizes
*/
fun matches(other: ItemStack, exact: Boolean = false): Boolean {
if (isEmpty && other.isEmpty)
return true
return entry == other.entry && (!exact || parameters == other.parameters)
}
/**
* whenever items match, ignoring their sizes
*/
fun matches(other: ItemDescriptor, exact: Boolean = false): Boolean {
if (isEmpty && other.isEmpty)
return true
return entry.name == other.name && (!exact || parameters == other.parameters)
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -314,7 +334,7 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para
if (isEmpty) if (isEmpty)
return other.isEmpty return other.isEmpty
return other.size == size && other.config == config && other.parameters == parameters return matches(other) && size == other.size
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -346,18 +366,6 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para
} }
} }
fun toTable(allocator: TableFactory): Table? {
if (isEmpty) {
return null
}
return allocator.newTable(0, 3).also {
it.rawset("name", entry.name)
it.rawset("count", size)
it.rawset("parameters", allocator.from(parameters))
}
}
class Adapter(gson: Gson) : TypeAdapter<ItemStack>() { class Adapter(gson: Gson) : TypeAdapter<ItemStack>() {
override fun write(out: JsonWriter, value: ItemStack?) { override fun write(out: JsonWriter, value: ItemStack?) {
val json = value?.toJson() val json = value?.toJson()

View File

@ -8,6 +8,7 @@ import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.longs.Long2ObjectAVLTreeMap import it.unimi.dsi.fastutil.longs.Long2ObjectAVLTreeMap
import org.classdump.luna.ByteString import org.classdump.luna.ByteString
import org.classdump.luna.Conversions import org.classdump.luna.Conversions
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table import org.classdump.luna.Table
import org.classdump.luna.TableFactory import org.classdump.luna.TableFactory
import org.classdump.luna.impl.NonsuspendableFunctionException import org.classdump.luna.impl.NonsuspendableFunctionException
@ -24,6 +25,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2f import ru.dbotthepony.kstarbound.math.vector.Vector2f
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
fun ExecutionContext.toVector2i(table: Any): Vector2i { fun ExecutionContext.toVector2i(table: Any): Vector2i {
@ -46,6 +48,12 @@ fun ExecutionContext.toVector2d(table: Any): Vector2d {
return Vector2d(x.toDouble(), y.toDouble()) return Vector2d(x.toDouble(), y.toDouble())
} }
fun ExecutionContext.toLine2d(table: Any): Line2d {
val p0 = toVector2d(indexNoYield(table, 1L) ?: throw LuaRuntimeException("Invalid line: $table"))
val p1 = toVector2d(indexNoYield(table, 2L) ?: throw LuaRuntimeException("Invalid line: $table"))
return Line2d(p0, p1)
}
fun ExecutionContext.toPoly(table: Table): Poly { fun ExecutionContext.toPoly(table: Table): Poly {
val vertices = ArrayList<Vector2d>() val vertices = ArrayList<Vector2d>()
@ -303,14 +311,18 @@ fun TableFactory.createJsonArray(): Table {
return createJsonTable(LUA_HINT_ARRAY, 0, 0).data return createJsonTable(LUA_HINT_ARRAY, 0, 0).data
} }
fun TableFactory.from(value: IStruct2d): Table { fun TableFactory.from(value: IStruct2d?): Table? {
value ?: return null
return newTable(2, 0).apply { return newTable(2, 0).apply {
this[1L] = value.component1() this[1L] = value.component1()
this[2L] = value.component2() this[2L] = value.component2()
} }
} }
fun TableFactory.from(value: Poly): Table { fun TableFactory.from(value: Poly?): Table? {
value ?: return null
return newTable(value.vertices.size, 0).apply { return newTable(value.vertices.size, 0).apply {
value.vertices.withIndex().forEach { (i, v) -> this[i + 1L] = from(v) } value.vertices.withIndex().forEach { (i, v) -> this[i + 1L] = from(v) }
} }
@ -326,14 +338,18 @@ fun TableFactory.fromCollection(value: Collection<JsonElement?>): Table {
return table return table
} }
fun TableFactory.from(value: IStruct2i): Table { fun TableFactory.from(value: IStruct2i?): Table? {
value ?: return null
return newTable(2, 0).also { return newTable(2, 0).also {
it.rawset(1L, value.component1()) it.rawset(1L, value.component1())
it.rawset(2L, value.component2()) it.rawset(2L, value.component2())
} }
} }
fun TableFactory.from(value: IStruct3i): Table { fun TableFactory.from(value: IStruct3i?): Table? {
value ?: return null
return newTable(3, 0).also { return newTable(3, 0).also {
it.rawset(1L, value.component1()) it.rawset(1L, value.component1())
it.rawset(2L, value.component2()) it.rawset(2L, value.component2())
@ -341,7 +357,9 @@ fun TableFactory.from(value: IStruct3i): Table {
} }
} }
fun TableFactory.from(value: IStruct4i): Table { fun TableFactory.from(value: IStruct4i?): Table? {
value ?: return null
return newTable(3, 0).also { return newTable(3, 0).also {
it.rawset(1L, value.component1()) it.rawset(1L, value.component1())
it.rawset(2L, value.component2()) it.rawset(2L, value.component2())
@ -350,7 +368,9 @@ fun TableFactory.from(value: IStruct4i): Table {
} }
} }
fun TableFactory.from(value: RGBAColor): Table { fun TableFactory.from(value: RGBAColor?): Table? {
value ?: return null
return newTable(3, 0).also { return newTable(3, 0).also {
it.rawset(1L, value.redInt.toLong()) it.rawset(1L, value.redInt.toLong())
it.rawset(2L, value.greenInt.toLong()) it.rawset(2L, value.greenInt.toLong())
@ -367,7 +387,9 @@ fun TableFactory.from(value: Collection<Any>): Table {
} }
} }
fun TableFactory.from(value: AABB): Table { fun TableFactory.from(value: AABB?): Table? {
value ?: return null
return newTable(3, 0).also { return newTable(3, 0).also {
it.rawset(1L, value.mins.x) it.rawset(1L, value.mins.x)
it.rawset(2L, value.mins.y) it.rawset(2L, value.mins.y)

View File

@ -7,6 +7,7 @@ import org.classdump.luna.Table
import org.classdump.luna.TableFactory import org.classdump.luna.TableFactory
import org.classdump.luna.impl.NonsuspendableFunctionException import org.classdump.luna.impl.NonsuspendableFunctionException
import org.classdump.luna.lib.ArgumentIterator import org.classdump.luna.lib.ArgumentIterator
import org.classdump.luna.lib.TableLib
import org.classdump.luna.runtime.AbstractFunction0 import org.classdump.luna.runtime.AbstractFunction0
import org.classdump.luna.runtime.AbstractFunction1 import org.classdump.luna.runtime.AbstractFunction1
import org.classdump.luna.runtime.AbstractFunction2 import org.classdump.luna.runtime.AbstractFunction2
@ -17,6 +18,8 @@ import org.classdump.luna.runtime.Dispatch
import org.classdump.luna.runtime.ExecutionContext import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction import org.classdump.luna.runtime.LuaFunction
import org.classdump.luna.runtime.UnresolvedControlThrowable import org.classdump.luna.runtime.UnresolvedControlThrowable
import kotlin.math.max
import kotlin.math.min
fun ExecutionContext.indexNoYield(table: Any, key: Any): Any? { fun ExecutionContext.indexNoYield(table: Any, key: Any): Any? {
return try { return try {
@ -82,6 +85,14 @@ operator fun Table.get(index: Any): Any? = rawget(index)
operator fun Table.get(index: Long): Any? = rawget(index) operator fun Table.get(index: Long): Any? = rawget(index)
operator fun Table.get(index: Int): Any? = rawget(index.toLong()) operator fun Table.get(index: Int): Any? = rawget(index.toLong())
operator fun Table.contains(index: Any): Boolean {
return rawget(index) != null
}
operator fun Table.contains(index: Long): Boolean {
return rawget(index) != null
}
operator fun Table.iterator(): Iterator<Map.Entry<Any, Any>> { operator fun Table.iterator(): Iterator<Map.Entry<Any, Any>> {
var key: Any? = initialKey() ?: return ObjectIterators.emptyIterator() var key: Any? = initialKey() ?: return ObjectIterators.emptyIterator()
data class Pair(override val key: Any, override val value: Any) : Map.Entry<Any, Any> data class Pair(override val key: Any, override val value: Any) : Map.Entry<Any, Any>
@ -100,6 +111,41 @@ operator fun Table.iterator(): Iterator<Map.Entry<Any, Any>> {
} }
} }
/**
* to be used in places where we need to "unpack" table, like this:
*
* ```lua
* local array = unpack(tab)
* ```
*
* except this function unpacks using rawget
*/
fun Table.unpackAsArray(): Array<Any?> {
var min = Long.MAX_VALUE
var max = 0L
for ((k, v) in this) {
if (k is Long) {
max = max(k, max)
min = min(k, min)
}
}
val length = max - min
if (length <= 0L)
return arrayOf()
val array = arrayOfNulls<Any>(length.toInt())
var i2 = 0
for (i in min .. max) {
array[i2++] = this[i]
}
return array
}
fun TableFactory.tableOf(vararg values: Any?): Table { fun TableFactory.tableOf(vararg values: Any?): Table {
val table = newTable(values.size, 0) val table = newTable(values.size, 0)
@ -110,6 +156,10 @@ fun TableFactory.tableOf(vararg values: Any?): Table {
return table return table
} }
fun TableFactory.tableOf(): Table {
return newTable()
}
@Deprecated("Function is a stub") @Deprecated("Function is a stub")
fun luaStub(message: String = "not yet implemented"): LuaFunction<Any?, Any?, Any?, Any?, Any?> { fun luaStub(message: String = "not yet implemented"): LuaFunction<Any?, Any?, Any?, Any?, Any?> {
return object : LuaFunction<Any?, Any?, Any?, Any?, Any?>() { return object : LuaFunction<Any?, Any?, Any?, Any?, Any?>() {

View File

@ -191,10 +191,7 @@ private fun createTreasure(context: ExecutionContext, arguments: ArgumentIterato
val get = Registries.treasurePools[pool] ?: throw LuaRuntimeException("No such treasure pool $pool") val get = Registries.treasurePools[pool] ?: throw LuaRuntimeException("No such treasure pool $pool")
val result = get.value.evaluate(seed ?: System.nanoTime(), level) context.returnBuffer.setTo(context.tableOf(*get.value.evaluate(seed ?: System.nanoTime(), level).filter { it.isNotEmpty }.map { context.from(it.toJson()) }.toTypedArray()))
.stream().filter { it.isNotEmpty }.map { it.toTable(context)!! }.toList()
context.returnBuffer.setTo(context.from(result))
} }
private fun materialMiningSound(context: ExecutionContext, arguments: ArgumentIterator) { private fun materialMiningSound(context: ExecutionContext, arguments: ArgumentIterator) {

View File

@ -1,24 +1,164 @@
package ru.dbotthepony.kstarbound.lua.bindings package ru.dbotthepony.kstarbound.lua.bindings
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table import org.classdump.luna.Table
import org.classdump.luna.runtime.ExecutionContext import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kommons.collect.map import ru.dbotthepony.kommons.collect.map
import ru.dbotthepony.kommons.collect.toList import ru.dbotthepony.kommons.collect.toList
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.contains
import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.iterator import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction 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.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toLine2d
import ru.dbotthepony.kstarbound.lua.toPoly import ru.dbotthepony.kstarbound.lua.toPoly
import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.lua.unpackAsArray
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.GameTimer
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.shuffle
import ru.dbotthepony.kstarbound.util.valueOf import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.RayFilterResult
import ru.dbotthepony.kstarbound.world.TileModification
import ru.dbotthepony.kstarbound.world.TileRayFilter
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.castRay
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.Collections
import java.util.EnumSet import java.util.EnumSet
import java.util.function.Predicate import java.util.function.Predicate
import java.util.stream.Collectors
import kotlin.math.PI
private val directionalAngles = intArrayOf(4, 8, 12, 0, 2, 6, 10, 14, 1, 3, 7, 5, 15, 13, 9, 11).let { arr ->
Array(arr.size) { Vector2d.angle(arr[it] * PI / 8.0) }
}
private fun ExecutionContext.resolvePolyCollision(self: World<*, *>, originalPoly: Poly, position: Vector2d, maximumCorrection: Double, collisions: Set<CollisionType>) {
val poly = originalPoly + position
data class Entry(val poly: Poly, val center: Vector2d, var distance: Double = 0.0) : Comparable<Entry> {
override fun compareTo(other: Entry): Int {
return distance.compareTo(other.distance)
}
}
val tiles = ObjectArrayList<Entry>()
for (tile in self.queryTileCollisions(poly.aabb.enlarge(maximumCorrection + 1.0, maximumCorrection + 1.0))) {
if (tile.type in collisions) {
tiles.add(Entry(tile.poly, tile.poly.centre))
}
}
fun separate(loops: Int, axis: Vector2d?): Vector2d? {
var body = poly
var correction = Vector2d.ZERO
for (i in 0 until loops) {
val center = body.centre
for (tile in tiles)
tile.distance = tile.center.distanceSquared(center)
tiles.sort()
var anyIntersects = false
for (tile in tiles) {
val intersection = tile.poly.intersect(body, axis)
if (intersection != null) {
anyIntersects = true
body += intersection.vector
correction += intersection.vector
if (correction.length >= maximumCorrection)
return null
}
}
if (!anyIntersects)
return correction
}
for (tile in tiles) {
if (tile.poly.intersect(body) != null) {
return null
}
}
return correction
}
var trySeparate = separate(10, null)
// First try any-directional SAT separation for two loops
if (trySeparate != null) {
returnBuffer.setTo(from(position + trySeparate))
return
}
// Then, try direction-limiting SAT in cardinals, then 45 degs, then in
// between, for 16 total angles in a circle.
// TODO: if "any direction" separation fails, then this will surely fail too?
for (axis in directionalAngles) {
trySeparate = separate(4, axis)
if (trySeparate != null) {
returnBuffer.setTo(from(position + trySeparate))
return
}
}
returnBuffer.setTo()
}
private fun ExecutionContext.returnLiquid(liquid: AbstractLiquidState, returnNames: Boolean?) {
if (returnNames == true) {
returnBuffer.setTo(tableOf(liquid.state.key, liquid.level))
} else if (liquid.state.id != null) {
returnBuffer.setTo(tableOf(liquid.state.id, liquid.level))
}
}
private val LOGGER = LogManager.getLogger()
fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
val callbacks = lua.newTable() val callbacks = lua.newTable()
@ -74,4 +214,379 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
returnBuffer.setTo(self.collide(toPoly(rect), Predicate { it.type in actualCollisions }).findAny().isPresent) returnBuffer.setTo(self.collide(toPoly(rect), Predicate { it.type in actualCollisions }).findAny().isPresent)
} }
} }
callbacks["pointCollision"] = luaFunction { rect: Table, collisions: Table? ->
if (collisions == null) {
returnBuffer.setTo(self.collide(toVector2d(rect), Predicate { it.type.isSolidCollision }))
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
returnBuffer.setTo(self.collide(toVector2d(rect), Predicate { it.type in actualCollisions }))
}
}
callbacks["pointTileCollision"] = luaFunction { rect: Table, collisions: Table? ->
val cell = self.getCell(toVector2i(rect))
if (collisions == null) {
returnBuffer.setTo(cell.foreground.material.value.collisionKind.isSolidCollision)
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
returnBuffer.setTo(cell.foreground.material.value.collisionKind in actualCollisions)
}
}
callbacks["lineTileCollision"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? ->
if (collisions == null) {
returnBuffer.setTo(self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind.isSolidCollision) RayFilterResult.HIT else RayFilterResult.SKIP }).traversedTiles.isNotEmpty())
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
returnBuffer.setTo(self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind in actualCollisions) RayFilterResult.HIT else RayFilterResult.SKIP }).traversedTiles.isNotEmpty())
}
}
callbacks["lineTileCollisionPoint"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? ->
val result = if (collisions == null) {
self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind.isSolidCollision) RayFilterResult.HIT else RayFilterResult.SKIP })
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
self.castRay(toVector2d(pos0), toVector2d(pos1), TileRayFilter { cell, fraction, x, y, normal, borderX, borderY -> if (cell.foreground.material.value.collisionKind in actualCollisions) RayFilterResult.HIT else RayFilterResult.SKIP })
}
if (result.hitTile == null) {
returnBuffer.setTo()
} else {
returnBuffer.setTo(tableOf(from(result.hitTile.borderCross), from(result.hitTile.normal.normal)))
}
}
callbacks["rectTileCollision"] = luaFunction { rect: Table, collisions: Table? ->
if (collisions == null) {
returnBuffer.setTo(self.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind.isSolidCollision }))
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
returnBuffer.setTo(self.anyCellSatisfies(toAABB(rect), World.CellPredicate { x, y, cell -> cell.foreground.material.value.collisionKind in actualCollisions }))
}
}
callbacks["lineCollision"] = luaFunction { pos0: Table, pos1: Table, collisions: Table? ->
val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
val result = self.collide(Line2d(toVector2d(pos0), toVector2d(pos1))) { it.type in actualCollisions }
if (result == null) {
returnBuffer.setTo()
} else {
returnBuffer.setTo(from(result.border), from(result.normal))
}
}
callbacks["polyCollision"] = luaFunction { rect: Table, translate: Table?, collisions: Table? ->
if (collisions == null) {
returnBuffer.setTo(self.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type.isSolidCollision }).findAny().isPresent)
} else {
val actualCollisions = EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
returnBuffer.setTo(self.collide(toPoly(rect).let { if (translate != null) it + toVector2d(translate) else it }, Predicate { it.type in actualCollisions }).findAny().isPresent)
}
}
callbacks["collisionBlocksAlongLine"] = luaFunction { pos0: Table, pos1: Table, collisions: Table?, limit: Number? ->
val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
var actualLimit = limit?.toInt() ?: Int.MAX_VALUE
if (actualLimit < 0)
actualLimit = Int.MAX_VALUE
val result = self.castRay(toVector2d(pos0), toVector2d(pos1)) { cell, fraction, x, y, normal, borderX, borderY ->
if (cell.foreground.material.value.collisionKind in actualCollisions) {
val tlimit = --actualLimit
if (tlimit > 0) {
RayFilterResult.CONTINUE
} else if (tlimit == 0) {
RayFilterResult.HIT
} else {
RayFilterResult.BREAK
}
} else {
RayFilterResult.SKIP
}
}
returnBuffer.setTo(tableOf(*result.traversedTiles.map { from(it.pos) }.toTypedArray()))
}
callbacks["liquidAlongLine"] = luaFunction { pos0: Table, pos1: Table ->
val liquid = newTable()
var i = 1L
self.castRay(toVector2d(pos0), toVector2d(pos1)) { cell, fraction, x, y, normal, borderX, borderY ->
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f && cell.liquid.state.id != null) {
liquid[i++] = tableOf(tableOf(x, y), tableOf(cell.liquid.state.id, cell.liquid.level.toDouble()))
}
RayFilterResult.SKIP
}
returnBuffer.setTo(liquid)
}
callbacks["liquidNamesAlongLine"] = luaFunction { pos0: Table, pos1: Table ->
val liquid = newTable()
var i = 1L
self.castRay(toVector2d(pos0), toVector2d(pos1)) { cell, fraction, x, y, normal, borderX, borderY ->
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) {
liquid[i++] = tableOf(tableOf(x, y), tableOf(cell.liquid.state.key, cell.liquid.level.toDouble()))
}
RayFilterResult.SKIP
}
returnBuffer.setTo(liquid)
}
callbacks["resolvePolyCollision"] = luaFunction { poly: Table, position: Table, maximumCorrection: Number, collisions: Table? ->
val actualCollisions = if (collisions == null) CollisionType.SOLID else EnumSet.copyOf(collisions.iterator().map { CollisionType.entries.valueOf((it.value as ByteString).decode()) }.toList())
resolvePolyCollision(self, toPoly(poly), toVector2d(position), maximumCorrection.toDouble(), actualCollisions)
}
callbacks["tileIsOccupied"] = luaFunction { pos: Table, isForeground: Boolean?, includeEmphemeral: Boolean? ->
val cell = self.getCell(toVector2i(pos))
if (cell.tile(isForeground == false).material.isNotEmptyTile) {
returnBuffer.setTo(true)
} else {
returnBuffer.setTo(self.entityIndex.tileEntitiesAt(toVector2i(pos)).any { !it.isEphemeral || includeEmphemeral == true })
}
}
callbacks["placeObject"] = luaFunction { type: ByteString, pos0: Table, objectDirection: Number?, parameters: Table? ->
val pos = toVector2i(pos0)
try {
val prototype = Registries.worldObjects[type.decode()] ?: throw LuaRuntimeException("No such object $type")
var direction = Direction.RIGHT
if (objectDirection != null && objectDirection.toLong() < 0L)
direction = Direction.LEFT
val json = if (parameters == null) JsonObject() else parameters.toJson(true) as JsonObject
val orientation = prototype.value.findValidOrientation(self, pos, direction)
if (orientation == -1) {
LOGGER.debug("Lua script tried to place object {} at {}, but it can't be placed there!", prototype.key, pos)
returnBuffer.setTo(false)
} else {
val create = WorldObject.create(prototype, pos, json)
create?.orientationIndex = orientation.toLong()
create?.joinWorld(self)
returnBuffer.setTo(create != null)
}
} catch (err: Throwable) {
LOGGER.error("Exception while placing world object $type at $pos", err)
returnBuffer.setTo(false)
}
}
callbacks["spawnItem"] = luaFunctionN("spawnItem") {
val itemType = toJsonFromLua(it.nextAny())
val pos = toVector2d(it.nextTable())
val inputCount = it.nextOptionalInteger() ?: 1L
val inputParameters = toJsonFromLua(it.nextOptionalAny(null))
val initialVelocity = toVector2d(it.nextOptionalAny(tableOf(0L, 0L)))
val intangibleTime = it.nextOptionalAny(null)
try {
val descriptor: ItemDescriptor
if (itemType is JsonObject) {
descriptor = ItemDescriptor(itemType)
} else {
descriptor = ItemDescriptor(itemType.asString, inputCount, if (inputParameters.isJsonNull) JsonObject() else inputParameters.asJsonObject)
}
if (descriptor.isEmpty) {
LOGGER.debug("Lua script tried to create non existing item {} at {}", itemType, pos)
returnBuffer.setTo()
} else {
val create = ItemDropEntity(descriptor)
create.movement.velocity = initialVelocity
if (intangibleTime is Number) {
create.intangibleTimer = GameTimer(intangibleTime.toDouble())
}
create.joinWorld(self)
returnBuffer.setTo(create.entityID)
}
} catch (err: Throwable) {
LOGGER.error("Exception while creating item $itemType at $pos", err)
returnBuffer.setTo()
}
}
callbacks["spawnTreasure"] = luaFunction { position: Table, pool: ByteString, level: Number, seed: Number? ->
val entities = IntArrayList()
try {
val items = Registries.treasurePools
.getOrThrow(pool.decode())
.value
// not using lua.random because we are, well, world's bindings
.evaluate(if (seed != null) random(seed.toLong()) else self.random, level.toDouble())
val pos = toVector2d(position)
for (item in items) {
val entity = ItemDropEntity(item)
entity.position = pos
entity.joinWorld(self)
entities.add(entity.entityID)
}
} catch (err: Throwable) {
LOGGER.error("Exception while spawning treasure from $pool at $position", err)
}
returnBuffer.setTo(tableOf(*entities.toTypedArray()))
}
callbacks["spawnMonster"] = luaStub("spawnMonster")
callbacks["spawnNpc"] = luaStub("spawnNpc")
callbacks["spawnStagehand"] = luaStub("spawnStagehand")
callbacks["spawnProjectile"] = luaStub("spawnProjectile")
callbacks["spawnVehicle"] = luaStub("spawnVehicle")
callbacks["threatLevel"] = luaFunction { returnBuffer.setTo(self.template.threatLevel) }
callbacks["time"] = luaFunction { returnBuffer.setTo(self.sky.time) }
callbacks["day"] = luaFunction { returnBuffer.setTo(self.sky.day) }
callbacks["timeOfDay"] = luaFunction { returnBuffer.setTo(self.sky.timeOfDay) }
callbacks["dayLength"] = luaFunction { returnBuffer.setTo(self.sky.dayLength) }
callbacks["getProperty"] = luaFunction { name: ByteString, orElse: Any? ->
returnBuffer.setTo(from(self.getProperty(name.decode()) { toJsonFromLua(orElse) }))
}
callbacks["setProperty"] = luaFunction { name: ByteString, value: Any? ->
self.setProperty(name.decode(), toJsonFromLua(value))
}
callbacks["liquidAt"] = luaFunction { posOrRect: Table ->
if (posOrRect[1L] is Number) {
val cell = self.getCell(toVector2i(posOrRect))
if (cell.liquid.state.isNotEmptyLiquid) {
returnLiquid(cell.liquid, false)
}
} else {
val level = self.averageLiquidLevel(toAABB(posOrRect))
if (level != null && level.type.id != null) {
returnBuffer.setTo(tableOf(level.type.id, level.average))
}
}
}
callbacks["liquidNameAt"] = luaFunction { posOrRect: Table ->
if (posOrRect[1L] is Number) {
val cell = self.getCell(toVector2i(posOrRect))
if (cell.liquid.state.isNotEmptyLiquid) {
returnLiquid(cell.liquid, true)
}
} else {
val level = self.averageLiquidLevel(toAABB(posOrRect))
if (level != null) {
returnBuffer.setTo(tableOf(level.type.key, level.average))
}
}
}
callbacks["gravity"] = luaFunction { pos: Table ->
returnBuffer.setTo(self.gravityAt(toVector2d(pos)).y)
}
callbacks["gravityVector"] = luaFunction { pos: Table ->
returnBuffer.setTo(from(self.gravityAt(toVector2d(pos))))
}
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())
}
callbacks["destroyLiquid"] = luaFunction { pos: Table ->
val action = TileModification.Pour(BuiltinMetaMaterials.NO_LIQUID.ref, 0f)
val cell = self.getCell(toVector2i(pos))
self.applyTileModifications(listOf(toVector2i(pos) to action), false)
if (cell.liquid.state.isNotEmptyLiquid)
returnLiquid(cell.liquid, false)
}
callbacks["destroyNamedLiquid"] = luaFunction { pos: Table ->
val action = TileModification.Pour(BuiltinMetaMaterials.NO_LIQUID.ref, 0f)
val cell = self.getCell(toVector2i(pos))
self.applyTileModifications(listOf(toVector2i(pos) to action), false)
if (cell.liquid.state.isNotEmptyLiquid)
returnLiquid(cell.liquid, true)
}
callbacks["isTileProtected"] = luaFunction { pos: Table -> returnBuffer.setTo(self.getCell(toVector2i(pos)).dungeonId in self.protectedDungeonIDs) }
callbacks["findPlatformerPath"] = luaStub("findPlatformerPath")
callbacks["platformerPathStart"] = luaStub("platformerPathStart")
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["terrestrial"] = luaFunction { returnBuffer.setTo(self.template.worldParameters is TerrestrialWorldParameters) }
callbacks["itemDropItem"] = luaFunction { id: Number ->
returnBuffer.setTo(from((self.entities[id.toInt()] as? ItemDropEntity)?.item?.toJson()))
}
callbacks["biomeBlocksAt"] = luaFunction { pos: Table, returnNames: Boolean? ->
val info = self.template.cellInfo(toVector2i(pos))
val blocks = tableOf()
var i = 1L
val biome = info.blockBiome
if (biome != null) {
biome.mainBlock.native.entry?.id?.let { blocks[i++] = it }
biome.subBlocks.forEach { it.native.entry?.id?.let { blocks[i++] = it } }
}
returnBuffer.setTo(blocks)
}
callbacks["biomeBlockNamesAt"] = luaFunction { pos: Table, returnNames: Boolean? ->
val info = self.template.cellInfo(toVector2i(pos))
val blocks = tableOf()
var i = 1L
val biome = info.blockBiome
if (biome != null) {
biome.mainBlock.native.entry?.key?.let { blocks[i++] = it }
biome.subBlocks.forEach { it.native.entry?.key?.let { blocks[i++] = it } }
}
returnBuffer.setTo(blocks)
}
callbacks["dungeonId"] = luaFunction { pos: Table -> returnBuffer.setTo(self.getCell(toVector2i(pos)).dungeonId) }
provideWorldEntitiesBindings(self, callbacks, lua)
if (self is ServerWorld) {
provideServerWorldBindings(self, callbacks, lua)
}
}
private fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvironment) {
} }

View File

@ -0,0 +1,359 @@
package ru.dbotthepony.kstarbound.lua.bindings
import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table
import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.LoungeOrientation
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.luaStub
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
import ru.dbotthepony.kstarbound.lua.toLine2d
import ru.dbotthepony.kstarbound.lua.toPoly
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.lua.unpackAsArray
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.util.random.shuffle
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ActorEntity
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.api.InspectableEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.tile.LoungeableObject
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.*
import java.util.function.Predicate
private enum class EntityBoundMode(override val jsonName: String) : IStringSerializable {
META_BOUNDING_BOX("MetaBoundBox"),
COLLISION_AREA("CollisionArea"),
POSITION("Position")
}
private fun ExecutionContext.entityQueryImpl(self: World<*, *>, options: Table, predicate: Predicate<AbstractEntity> = Predicate { true }): Table {
val withoutEntityId = (indexNoYield(options, "withoutEntityId") as Number?)?.toInt()
val includedTypes = EnumSet.allOf(EntityType::class.java)
val getIncludedTypes = indexNoYield(options, "includedTypes") as Table?
if (getIncludedTypes != null) {
includedTypes.clear()
for ((_, v) in getIncludedTypes) {
when (val t = (v as ByteString).decode()) {
"mobile" -> {
includedTypes.add(EntityType.PLAYER)
includedTypes.add(EntityType.MONSTER)
includedTypes.add(EntityType.NPC)
includedTypes.add(EntityType.PROJECTILE)
includedTypes.add(EntityType.ITEM_DROP)
includedTypes.add(EntityType.VEHICLE)
}
"creature" -> {
includedTypes.add(EntityType.PLAYER)
includedTypes.add(EntityType.MONSTER)
includedTypes.add(EntityType.NPC)
}
else -> {
includedTypes.add(EntityType.entries.valueOf(t))
}
}
}
}
val callScript = (indexNoYield(options, "callScript") as ByteString?)?.decode()
val callScriptArgs = (indexNoYield(options, "callScriptArgs") as Table?)?.unpackAsArray() ?: arrayOf()
val callScriptResult = indexNoYield(options, "callScriptResult") ?: true
val lineQuery = (indexNoYield(options, "line") as Table?)?.let { toLine2d(it) }
val polyQuery = (indexNoYield(options, "poly") as Table?)?.let { toPoly(it) }
val rectQuery = (indexNoYield(options, "rect") as Table?)?.let { toAABB(it) }
val radius = indexNoYield(options, "radius")
val radiusQuery = if (radius is Number) {
val center = toVector2d(indexNoYield(options, "center") ?: throw LuaRuntimeException("Specified 'radius', but not 'center'"))
center to radius.toDouble()
} else {
null
}
val boundMode = EntityBoundMode.entries.valueOf((indexNoYield(options, "boundMode") as ByteString?)?.decode() ?: "CollisionArea")
val innerPredicate = Predicate<AbstractEntity> {
if (!predicate.test(it)) return@Predicate false
if (it.type !in includedTypes) return@Predicate false
if (it.entityID == withoutEntityId) return@Predicate false
if (callScript != null) {
if (it !is ScriptedEntity || it.isRemote) return@Predicate false
val call = it.callScript(callScript, *callScriptArgs)
if (call.isEmpty() || call[0] != callScriptResult) return@Predicate false
}
when (boundMode) {
EntityBoundMode.META_BOUNDING_BOX -> {
// If using MetaBoundBox, the regular line / box query methods already
// enforce collision with MetaBoundBox
if (radiusQuery != null) {
return@Predicate self.geometry.rectIntersectsCircle(it.metaBoundingBox, radiusQuery.first, radiusQuery.second)
}
}
EntityBoundMode.COLLISION_AREA -> {
// Collision area queries either query based on the collision area if
// that's given, or as a fallback the regular bound box.
var collisionArea = it.collisionArea
if (collisionArea.isEmpty)
collisionArea = it.metaBoundingBox
if (lineQuery != null)
return@Predicate self.geometry.lineIntersectsRect(lineQuery, collisionArea)
if (polyQuery != null)
return@Predicate self.geometry.polyIntersectsPoly(polyQuery, Poly(collisionArea))
if (rectQuery != null)
return@Predicate self.geometry.rectIntersectsRect(rectQuery, collisionArea)
if (radiusQuery != null)
return@Predicate self.geometry.rectIntersectsCircle(collisionArea, radiusQuery.first, radiusQuery.second)
}
EntityBoundMode.POSITION -> {
if (lineQuery != null)
return@Predicate self.geometry.lineIntersectsRect(lineQuery, AABB.rectangle(it.position, 0.0))
if (polyQuery != null)
return@Predicate self.geometry.polyContains(polyQuery, it.position)
if (rectQuery != null)
return@Predicate self.geometry.rectContains(rectQuery, it.position)
if (radiusQuery != null)
return@Predicate self.geometry.diff(radiusQuery.first, it.position).length <= radiusQuery.second
}
}
true
}
val entitites = if (lineQuery != null) {
// TODO: this is wildly inefficient
self.entityIndex.query(AABB(lineQuery), innerPredicate)
} else if (polyQuery != null) {
self.entityIndex.query(polyQuery.aabb, innerPredicate)
} else if (rectQuery != null) {
self.entityIndex.query(rectQuery, innerPredicate)
} else if (radiusQuery != null) {
self.entityIndex.query(AABB.withSide(radiusQuery.first, radiusQuery.second), innerPredicate)
} else {
mutableListOf()
}
when (val order = (indexNoYield(options, "order") as ByteString?)?.decode()?.lowercase()) {
null -> {} // do nothing
"random" -> entitites.shuffle(self.random)
"nearest" -> {
val nearestPosition = lineQuery?.p0 ?: polyQuery?.centre ?: rectQuery?.centre ?: radiusQuery?.first ?: Vector2d.ZERO
entitites.sortWith { o1, o2 ->
self.geometry.diff(o1.position, nearestPosition).lengthSquared.compareTo(self.geometry.diff(o2.position, nearestPosition).lengthSquared) }
}
else -> throw LuaRuntimeException("Unknown entity sort order $order!")
}
return tableOf(*entitites.map { it.entityID.toLong() }.toTypedArray())
}
private fun ExecutionContext.intermediateQueryFunction(self: World<*, *>, pos1: Table, pos2OrRadius: Any, options: Table?, predicate: Predicate<AbstractEntity>) {
val actualOptions = options ?: tableOf()
if (pos2OrRadius is Number) {
actualOptions["center"] = pos1
actualOptions["radius"] = pos2OrRadius
} else {
pos2OrRadius as Table
actualOptions["rect"] = tableOf(pos1[1L], pos1[2L], pos2OrRadius[1L], pos2OrRadius[2L])
}
returnBuffer.setTo(entityQueryImpl(self, actualOptions, predicate))
}
private fun ExecutionContext.intermediateLineQueryFunction(self: World<*, *>, pos1: Table, pos2: Table, options: Table?, predicate: Predicate<AbstractEntity>) {
val actualOptions = options ?: tableOf()
actualOptions["line"] = tableOf(pos1, pos2)
returnBuffer.setTo(entityQueryImpl(self, actualOptions, predicate))
}
private inline fun <reified T : AbstractEntity> createQueryFunction(self: World<*, *>) = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? ->
intermediateQueryFunction(self, pos1, pos2OrRadius, options, Predicate { it is T })
}
private inline fun <reified T : AbstractEntity> createLineQueryFunction(self: World<*, *>) = luaFunction { pos1: Table, pos2: Table, options: Table? ->
intermediateLineQueryFunction(self, pos1, pos2, options, Predicate { it is T })
}
fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEnvironment) {
callbacks["entityQuery"] = createQueryFunction<AbstractEntity>(self)
callbacks["monsterQuery"] = createQueryFunction<AbstractEntity>(self) // TODO
callbacks["npcQuery"] = createQueryFunction<AbstractEntity>(self) // TODO
callbacks["itemDropQuery"] = createQueryFunction<ItemDropEntity>(self)
callbacks["playerQuery"] = createQueryFunction<PlayerEntity>(self)
callbacks["entityLineQuery"] = createLineQueryFunction<AbstractEntity>(self)
callbacks["monsterLineQuery"] = createLineQueryFunction<AbstractEntity>(self) // TODO
callbacks["npcLineQuery"] = createLineQueryFunction<AbstractEntity>(self) // TODO
callbacks["itemDropLineQuery"] = createLineQueryFunction<ItemDropEntity>(self)
callbacks["playerLineQuery"] = createLineQueryFunction<PlayerEntity>(self)
callbacks["objectQuery"] = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? ->
var objectName: String? = null
if (options != null)
objectName = (indexNoYield(options, "name") as ByteString?)?.decode()
intermediateQueryFunction(self, pos1, pos2OrRadius, options, Predicate {
it is WorldObject && (objectName == null || it.config.key == objectName)
})
}
callbacks["objectLineQuery"] = luaFunction { pos1: Table, pos2: Table, options: Table? ->
var objectName: String? = null
if (options != null)
objectName = (indexNoYield(options, "name") as ByteString?)?.decode()
intermediateLineQueryFunction(self, pos1, pos2, options, Predicate {
it is WorldObject && (objectName == null || it.config.key == objectName)
})
}
callbacks["loungeableQuery"] = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? ->
var orientationName: String? = null
if (options != null)
orientationName = (indexNoYield(options, "orientation") as ByteString?)?.decode()
val orientation = when (orientationName) {
null -> LoungeOrientation.NONE
else -> LoungeOrientation.entries.valueOf(orientationName)
}
intermediateQueryFunction(self, pos1, pos2OrRadius, options, Predicate {
it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation)
})
}
callbacks["loungeableLineQuery"] = luaFunction { pos1: Table, pos2: Table, options: Table? ->
var orientationName: String? = null
if (options != null)
orientationName = (indexNoYield(options, "orientation") as ByteString?)?.decode()
val orientation = when (orientationName) {
null -> LoungeOrientation.NONE
else -> LoungeOrientation.entries.valueOf(orientationName!!)
}
intermediateLineQueryFunction(self, pos1, pos2, options, Predicate {
it is LoungeableObject && (orientation == LoungeOrientation.NONE || it.sitOrientation == orientation)
})
}
callbacks["objectAt"] = luaFunction { pos: Table ->
returnBuffer.setTo(self.entityIndex.tileEntityAt(toVector2i(pos))?.entityID)
}
callbacks["entityExists"] = luaFunction { id: Number ->
returnBuffer.setTo(id.toInt() in self.entities)
}
callbacks["entityCanDamage"] = luaFunction { source: Number, target: Number ->
val a = self.entities[source.toInt()]
val b = self.entities[target.toInt()]
returnBuffer.setTo(a != null && b != null && a.team.get().canDamage(b.team.get(), a == b))
}
callbacks["entityDamageTeam"] = luaFunction { id: Number ->
returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.entities[id]?.team?.get())))
}
callbacks["entityAggressive"] = luaStub("entityAggressive")
callbacks["entityType"] = luaFunction { id: Number -> returnBuffer.setTo(self.entities[id.toInt()]?.type?.jsonName) }
callbacks["entityPosition"] = luaFunction { id: Number -> returnBuffer.setTo(from(self.entities[id.toInt()]?.position)) }
callbacks["entityVelocity"] = luaFunction { id: Number -> returnBuffer.setTo(from((self.entities[id.toInt()] as? DynamicEntity)?.movement?.velocity)) }
callbacks["entityMetaBoundBox"] = luaFunction { id: Number -> returnBuffer.setTo(from(self.entities[id.toInt()]?.metaBoundingBox)) }
callbacks["entityCurrency"] = luaFunction { id: Number, currency: ByteString -> returnBuffer.setTo((self.entities[id.toInt()] as? PlayerEntity)?.inventory?.currencies?.get(currency.decode())) }
callbacks["entityHasCountOfItem"] = luaFunction { id: Number, descriptor: Any, exact: Boolean? ->
val player = self.entities[id.toInt()] as? PlayerEntity ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(player.inventory.hasCountOfItem(ItemDescriptor(descriptor), exact ?: false))
}
callbacks["entityHealth"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ActorEntity ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(tableOf(entity.health, entity.maxHealth))
}
callbacks["entitySpecies"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.species)
}
callbacks["entityGender"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.gender.jsonName)
}
callbacks["entityName"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo()
// TODO
when (entity) {
is ActorEntity -> returnBuffer.setTo(entity.name)
is WorldObject -> returnBuffer.setTo(entity.config.key)
}
}
callbacks["entityDescription"] = luaFunction { id: Number, species: ByteString? ->
val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo()
if (entity is InspectableEntity) {
returnBuffer.setTo(entity.inspectionDescription(species?.decode()))
} else {
returnBuffer.setTo(entity.description)
}
}
callbacks["entityPortrait"] = luaFunction { id: Number, mode: ByteString ->
val entity = self.entities[id.toInt()] as? ActorEntity ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(tableOf(*entity.portrait(ActorEntity.PortraitMode.entries.valueOf(mode.decode())).map { from(it.toJson()) }.toTypedArray()))
}
}

View File

@ -20,6 +20,7 @@ import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.iterator import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.set import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toColor import ru.dbotthepony.kstarbound.lua.toColor
import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua import ru.dbotthepony.kstarbound.lua.toJsonFromLua
@ -55,7 +56,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
table["boundBox"] = luaFunction { returnBuffer.setTo(from(self.metaBoundingBox)) } table["boundBox"] = luaFunction { returnBuffer.setTo(from(self.metaBoundingBox)) }
// original engine parity, it returns occupied spaces in local coordinates // original engine parity, it returns occupied spaces in local coordinates
table["spaces"] = luaFunction { returnBuffer.setTo(from(self.occupySpaces.map { from(it - self.tilePosition) })) } table["spaces"] = luaFunction { returnBuffer.setTo(tableOf(*self.occupySpaces.map { from(it - self.tilePosition) }.toTypedArray())) }
table["setProcessingDirectives"] = luaFunction { directives: ByteString -> self.animator.processingDirectives = directives.decode() } table["setProcessingDirectives"] = luaFunction { directives: ByteString -> self.animator.processingDirectives = directives.decode() }
table["setSoundEffectEnabled"] = luaFunction { state: Boolean -> self.soundEffectEnabled = state } table["setSoundEffectEnabled"] = luaFunction { state: Boolean -> self.soundEffectEnabled = state }

View File

@ -3,23 +3,40 @@
package ru.dbotthepony.kstarbound.math package ru.dbotthepony.kstarbound.math
import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kommons.math.intersectRectangles import ru.dbotthepony.kommons.math.intersectRectangles
import ru.dbotthepony.kommons.math.rectangleContainsRectangle import ru.dbotthepony.kommons.math.rectangleContainsRectangle
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
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 kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
/** /**
* Axis Aligned Bounding Box, represented by two points, [mins] as lowermost corner of BB, * Axis Aligned Bounding Box, represented by two points, [mins] as lowermost corner of BB,
* and [maxs] as uppermost corner of BB * and [maxs] as uppermost corner of BB
*/ */
data class AABB(val mins: Vector2d, val maxs: Vector2d) { data class AABB(val mins: Vector2d, val maxs: Vector2d) {
constructor(line: Line2d) : this(
Vector2d(min(line.p0.x, line.p1.x), min(line.p0.y, line.p1.y)),
Vector2d(max(line.p0.x, line.p1.x), max(line.p0.y, line.p1.y)),
)
init { init {
require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" } // require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" }
require(mins.y <= maxs.y) { "mins.y ${mins.y} is more than maxs.y ${maxs.y}" } // require(mins.y <= maxs.y) { "mins.y ${mins.y} is more than maxs.y ${maxs.y}" }
} }
val isEmpty: Boolean
get() = mins.x > maxs.x || mins.y > maxs.y
val isZero: Boolean
get() = mins == maxs
val isEmptyOrZero: Boolean
get() = isEmpty || isZero
operator fun plus(other: AABB) = AABB(mins + other.mins, maxs + other.maxs) operator fun plus(other: AABB) = AABB(mins + other.mins, maxs + other.maxs)
operator fun minus(other: AABB) = AABB(mins - other.mins, maxs - other.maxs) operator fun minus(other: AABB) = AABB(mins - other.mins, maxs - other.maxs)
operator fun times(other: AABB) = AABB(mins * other.mins, maxs * other.maxs) operator fun times(other: AABB) = AABB(mins * other.mins, maxs * other.maxs)
@ -50,6 +67,8 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
val width get() = maxs.x - mins.x val width get() = maxs.x - mins.x
val height get() = maxs.y - mins.y val height get() = maxs.y - mins.y
val volume get() = max(width * height, 0.0)
val extents get() = Vector2d(width * 0.5, height * 0.5) val extents get() = Vector2d(width * 0.5, height * 0.5)
val diameter get() = mins.distance(maxs) val diameter get() = mins.distance(maxs)
@ -57,6 +76,15 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
val perimeter get() = (xSpan + ySpan) * 2.0 val perimeter get() = (xSpan + ySpan) * 2.0
val edges: List<Line2d> by lazy {
immutableList {
accept(Line2d(Vector2d(mins.x, mins.y), Vector2d(maxs.x, mins.y)))
accept(Line2d(Vector2d(maxs.x, mins.y), Vector2d(maxs.x, maxs.y)))
accept(Line2d(Vector2d(maxs.x, maxs.y), Vector2d(mins.x, maxs.y)))
accept(Line2d(Vector2d(mins.x, maxs.y), Vector2d(mins.x, mins.y)))
}
}
fun isInside(point: IStruct2d): Boolean { fun isInside(point: IStruct2d): Boolean {
return point.component1() in mins.x .. maxs.x && point.component2() in mins.y .. maxs.y return point.component1() in mins.x .. maxs.x && point.component2() in mins.y .. maxs.y
} }
@ -76,6 +104,10 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
return intersectRectangles(mins, maxs, other.mins, other.maxs) return intersectRectangles(mins, maxs, other.mins, other.maxs)
} }
fun intersect(line: Line2d): Boolean {
return line.p0 in this || line.p1 in this || edges.any { it.intersect(line).intersects }
}
/** /**
* Returns whenever [other] is contained (encased) inside this AABB * Returns whenever [other] is contained (encased) inside this AABB
*/ */
@ -159,6 +191,13 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
} }
} }
fun overlap(other: AABB): AABB {
return AABB(
Vector2d(max(mins.x, other.mins.x), max(mins.y, other.mins.y)),
Vector2d(min(maxs.x, other.maxs.x), min(maxs.y, other.maxs.y)),
)
}
/** /**
* Returns AABB which contains both AABBs * Returns AABB which contains both AABBs
*/ */
@ -234,6 +273,11 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
) )
} }
fun leftCorner(pos: Vector2d, width: Double, height: Double): AABB {
return AABB(pos, pos + Vector2d(width, height))
}
@JvmField val ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO) @JvmField val ZERO = AABB(Vector2d.ZERO, Vector2d.ZERO)
@JvmField val NEVER = AABB(Vector2d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
} }
} }

View File

@ -32,6 +32,9 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
normal = Vector2d(-diff.y, diff.x) normal = Vector2d(-diff.y, diff.x)
} }
val center: Vector2d
get() = p0 + difference * 0.5
operator fun plus(other: IStruct2d): Line2d { operator fun plus(other: IStruct2d): Line2d {
return Line2d(p0 + other, p1 + other) return Line2d(p0 + other, p1 + other)
} }
@ -48,15 +51,14 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
return Line2d(p0 * other, p1 * other) return Line2d(p0 * other, p1 * other)
} }
data class Intersection(val intersects: Boolean, val point: KOptional<Vector2d>, val t: KOptional<Double>, val coincides: Boolean, val glances: Boolean) { data class Intersection(val intersects: Boolean, val point: Vector2d?, val t: Double?, val coincides: Boolean, val glances: Boolean) {
companion object { companion object {
val EMPTY = Intersection(false, KOptional(), KOptional(), false, false) val EMPTY = Intersection(false, null, null, false, false)
} }
} }
fun difference(): Vector2d { val difference: Vector2d
return p1 - p0 get() = p1 - p0
}
fun reverse(): Line2d { fun reverse(): Line2d {
return Line2d(p1, p0) return Line2d(p1, p0)
@ -82,8 +84,8 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
fun intersect(other: Line2d, infinite: Boolean = false): Intersection { fun intersect(other: Line2d, infinite: Boolean = false): Intersection {
val (c, d) = other val (c, d) = other
val ab = difference() val ab = difference
val cd = other.difference() val cd = other.difference
val abCross = p0.cross(p1) val abCross = p0.cross(p1)
val cdCross = c.cross(d) val cdCross = c.cross(d)
@ -120,7 +122,7 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
} }
} }
return Intersection(intersects, KOptional.ofNullable(point), KOptional(t), true, intersects) return Intersection(intersects, point, t, true, intersects)
} else { } else {
return Intersection.EMPTY return Intersection.EMPTY
} }
@ -132,8 +134,8 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
return Intersection( return Intersection(
intersects = intersects, intersects = intersects,
t = KOptional(ta), t = ta,
point = KOptional((p1 - p0) * ta + p0), point = (p1 - p0) * ta + p0,
coincides = false, coincides = false,
glances = !infinite && intersects && (ta <= NEAR_ZERO || ta >= NEAR_ONE || tb <= NEAR_ZERO || tb >= NEAR_ONE) glances = !infinite && intersects && (ta <= NEAR_ZERO || ta >= NEAR_ONE || tb <= NEAR_ZERO || tb >= NEAR_ONE)
) )
@ -141,7 +143,7 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
} }
fun project(axis: IStruct2d): Double { fun project(axis: IStruct2d): Double {
val diff = difference() val diff = difference
val (x, y) = axis val (x, y) = axis
return ((x - p0.x) * diff.x + (y - p0.y) * diff.y) / diff.lengthSquared return ((x - p0.x) * diff.x + (y - p0.y) * diff.y) / diff.lengthSquared
} }
@ -152,7 +154,7 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
if (!infinite) if (!infinite)
proj = proj.coerceIn(0.0, 1.0) proj = proj.coerceIn(0.0, 1.0)
return (Vector2d(other) - p0 + difference() * proj).length return (Vector2d(other) - p0 + difference * proj).length
} }
fun distanceTo(other: Vector2d, infinite: Boolean = false): Double { fun distanceTo(other: Vector2d, infinite: Boolean = false): Double {
@ -161,7 +163,7 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
if (!infinite) if (!infinite)
proj = proj.coerceIn(0.0, 1.0) proj = proj.coerceIn(0.0, 1.0)
return (other - p0 + difference() * proj).length return (other - p0 + difference * proj).length
} }
class Adapter(gson: Gson) : TypeAdapter<Line2d>() { class Adapter(gson: Gson) : TypeAdapter<Line2d>() {

View File

@ -245,5 +245,15 @@ data class Vector2d(
@JvmField val POSITIVE_XY = Vector2d(1.0, 1.0) @JvmField val POSITIVE_XY = Vector2d(1.0, 1.0)
@JvmField val NEGATIVE_XY = Vector2d(-1.0, -1.0) @JvmField val NEGATIVE_XY = Vector2d(-1.0, -1.0)
fun angle(angle: Double, amplitude: Double = 1.0): Vector2d {
if (amplitude == 0.0)
return ZERO
val sin = sin(angle)
val cos = cos(angle)
return Vector2d(cos * amplitude, sin * amplitude)
}
} }
} }

View File

@ -15,6 +15,7 @@ class WorldStopPacket(val reason: String = "") : IClientPacket {
} }
override fun play(connection: ClientConnection) { override fun play(connection: ClientConnection) {
connection.resetOccupiedEntityIDs()
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View File

@ -209,7 +209,7 @@ class ServerWorld private constructor(
if (source != null && health?.isDead == true) { if (source != null && health?.isDead == true) {
source.receiveMessage("tileBroken", jsonArrayOf( source.receiveMessage("tileBroken", jsonArrayOf(
pos, if (isBackground) "background" else "foreground", pos, if (isBackground) "background" else "foreground",
tile!!.tile(isBackground).material.id ?: 0, // TODO: string identifiers support tile!!.tile(isBackground).material.id ?: tile.tile(isBackground).material.key, // TODO: explicit string identifiers support
tile.dungeonId, tile.dungeonId,
health.isHarvested health.isHarvested
)) ))
@ -222,7 +222,7 @@ class ServerWorld private constructor(
return topMost return topMost
} }
fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false): List<Pair<Vector2i, TileModification>> { override fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean): List<Pair<Vector2i, TileModification>> {
val unapplied = ArrayList(modifications) val unapplied = ArrayList(modifications)
var size: Int var size: Int
@ -438,7 +438,6 @@ class ServerWorld private constructor(
} }
override fun setProperty0(key: String, value: JsonElement) { override fun setProperty0(key: String, value: JsonElement) {
super.setProperty0(key, value)
broadcast(UpdateWorldPropertiesPacket(JsonObject().apply { add(key, value) })) broadcast(UpdateWorldPropertiesPacket(JsonObject().apply { add(key, value) }))
} }

View File

@ -312,8 +312,8 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
if ( if (
intersection.intersects && intersection.intersects &&
intersection.point.get() != proposed.p0 && intersection.point != proposed.p0 &&
intersection.point.get() != proposed.p1 intersection.point != proposed.p1
) { ) {
valid = false valid = false
break break

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.util package ru.dbotthepony.kstarbound.util
import com.google.gson.JsonElement import com.google.gson.JsonElement
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import java.util.* import java.util.*
@ -70,5 +71,5 @@ fun <C : Comparable<C>, T : Any> Stream<Pair<C, T>>.binnedChoice(value: C): Opti
} }
fun <E : IStringSerializable> Collection<E>.valueOf(value: String): E { fun <E : IStringSerializable> Collection<E>.valueOf(value: String): E {
return firstOrNull { it.match(value) } ?: throw NoSuchElementException("$value is not a valid element") return firstOrNull { it.match(value) } ?: throw NoSuchElementException("'$value' is not a valid ${first()::class.qualifiedName}")
} }

View File

@ -238,3 +238,14 @@ fun RandomGenerator.nextRange(range: IStruct2i): Int {
fun RandomGenerator.nextRange(range: IStruct2d): Double { fun RandomGenerator.nextRange(range: IStruct2d): Double {
return if (range.component1() == range.component2()) return range.component1() else nextDouble(range.component1(), range.component2()) return if (range.component1() == range.component2()) return range.component1() else nextDouble(range.component1(), range.component2())
} }
fun <T> MutableList<T>.shuffle(random: RandomGenerator) {
for (i in 0 until size) {
val rand = random.nextInt(size)
val a = this[i]
val b = this[rand]
this[i] = b
this[rand] = a
}
}

View File

@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.LongArrayList import it.unimi.dsi.fastutil.longs.LongArrayList
import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
@ -250,8 +251,8 @@ class EntityIndex(val geometry: WorldGeometry) {
} }
} }
fun query(rect: AABB, filter: Predicate<in AbstractEntity> = Predicate { true }, withEdges: Boolean = true): List<AbstractEntity> { fun query(rect: AABB, filter: Predicate<in AbstractEntity> = Predicate { true }, withEdges: Boolean = true): MutableList<AbstractEntity> {
val entriesDirect = ArrayList<AbstractEntity>() val entriesDirect = ObjectArrayList<AbstractEntity>()
iterate(rect, withEdges = withEdges, visitor = { iterate(rect, withEdges = withEdges, visitor = {
if (filter.test(it)) entriesDirect.add(it) if (filter.test(it)) entriesDirect.add(it)
@ -282,6 +283,10 @@ class EntityIndex(val geometry: WorldGeometry) {
return first(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces }) as TileEntity? return first(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces }) as TileEntity?
} }
fun tileEntitiesAt(pos: Vector2i): MutableList<TileEntity> {
return query(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces }) as MutableList<TileEntity>
}
fun iterate(rect: AABB, visitor: (AbstractEntity) -> Unit, withEdges: Boolean = true) { fun iterate(rect: AABB, visitor: (AbstractEntity) -> Unit, withEdges: Boolean = true) {
walk<Unit>(rect, { visitor(it); KOptional() }, withEdges) walk<Unit>(rect, { visitor(it); KOptional() }, withEdges)
} }
@ -301,7 +306,7 @@ class EntityIndex(val geometry: WorldGeometry) {
val sector = map[index(x, y)] ?: continue val sector = map[index(x, y)] ?: continue
for (entry in sector.entries) { for (entry in sector.entries) {
if (entry.intersects(actualRegion, withEdges) && seen.add(entry.id)) { if (seen.add(entry.id) && entry.intersects(actualRegion, withEdges)) {
val visit = visitor(entry.value) val visit = visitor(entry.value)
if (visit.isPresent) if (visit.isPresent)

View File

@ -23,13 +23,24 @@ data class RayCastResult(
} }
enum class RayFilterResult(val hit: Boolean, val write: Boolean) { enum class RayFilterResult(val hit: Boolean, val write: Boolean) {
// stop tracing, write hit tile into traversed tiles list /**
* stop tracing, write hit tile into traversed tiles list
*/
HIT(true, true), HIT(true, true),
// stop tracing, don't write hit tile into traversed tiles list
HIT_SKIP(true, false), /**
// continue tracing, don't write hit tile into traversed tiles list * stop tracing, don't write hit tile into traversed tiles list
*/
BREAK(true, false),
/**
* continue tracing, don't write hit tile into traversed tiles list
*/
SKIP(false, false), SKIP(false, false),
// continue tracing, write hit tile into traversed tiles list
/**
* continue tracing, write hit tile into traversed tiles list
*/
CONTINUE(false, true); CONTINUE(false, true);
companion object { companion object {
@ -120,7 +131,7 @@ fun ICellAccess.castRay(
normal = yNormal normal = yNormal
} }
cell = getCell(cellPosX, cellPosY) ?: return RayCastResult(hitTiles, null, travelled / distance, start, start + direction * travelled, direction) cell = getCell(cellPosX, cellPosY)
result = filter.test(cell, 0.0, cellPosX, cellPosY, normal, start.x + direction.x * travelled, start.y + direction.y * travelled) result = filter.test(cell, 0.0, cellPosX, cellPosY, normal, start.x + direction.x * travelled, start.y + direction.y * travelled)
val c = if (result.write || result.hit) { val c = if (result.write || result.hit) {

View File

@ -32,9 +32,8 @@ class Sky() {
var skyParameters by networkedGroup.upstream.add(networkedJson(SkyParameters())) var skyParameters by networkedGroup.upstream.add(networkedJson(SkyParameters()))
var skyType by networkedGroup.upstream.add(networkedEnumStupid(SkyType.ORBITAL)) var skyType by networkedGroup.upstream.add(networkedEnumStupid(SkyType.ORBITAL))
var time by networkedGroup.upstream.add(networkedDouble()) var time by networkedGroup.upstream.add(networkedDouble())
private set
var flyingType by networkedGroup.upstream.add(networkedEnum(FlyingType.NONE)) var flyingType by networkedGroup.upstream.add(networkedEnum(FlyingType.NONE))
private set private set
var enterHyperspace by networkedGroup.upstream.add(networkedBoolean()) var enterHyperspace by networkedGroup.upstream.add(networkedBoolean())
@ -67,6 +66,15 @@ class Sky() {
var pathRotation: Double = 0.0 var pathRotation: Double = 0.0
private set private set
val dayLength: Double
get() = skyParameters.dayLength ?: 1000.0
val day: Int
get() = if (dayLength <= 1.0) 0 else (time / dayLength).toInt()
val timeOfDay: Double
get() = if (dayLength <= 1.0) 0.0 else time % dayLength
var destination: SkyParameters? = null var destination: SkyParameters? = null
private set private set

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isEmptyModifier 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.isNotEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
@ -269,8 +270,9 @@ sealed class TileModification {
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.isInfinite) if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.isInfinite)
return false // it makes no sense to try to pour liquid into infinite source return false // it makes no sense to try to pour liquid into infinite source
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.state != state.entry) if (state.isNotEmptyLiquid && cell.liquid.state.isNotEmptyLiquid && cell.liquid.state != state.entry)
return false // it makes also makes no sense to magically replace liquid what is already there return false // it makes also makes no sense to magically replace liquid what is already there
// (unless we are removing existing liquid)
// while checks above makes vanilla client look stupid when it tries to pour liquids into other // while checks above makes vanilla client look stupid when it tries to pour liquids into other
// liquids, we must think better than vanilla client. // liquids, we must think better than vanilla client.
@ -290,8 +292,11 @@ sealed class TileModification {
} else { } else {
cell.liquid.reset() cell.liquid.reset()
cell.liquid.state = state cell.liquid.state = state
cell.liquid.level = level
cell.liquid.pressure = 1f if (state.isNotEmptyLiquid) {
cell.liquid.level = level
cell.liquid.pressure = 1f
}
} }
world.setCell(position, cell) world.setCell(position, cell)

View File

@ -1,10 +1,13 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
@ -12,11 +15,14 @@ import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.AABBi
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.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.mergeJson
@ -24,10 +30,10 @@ import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
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.ParallelPerform
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
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
@ -265,6 +271,18 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
mergeJson(worldProperties, properties) mergeJson(worldProperties, properties)
} }
fun getProperty(name: String): JsonElement {
return worldProperties[name] ?: JsonNull.INSTANCE
}
fun getProperty(name: String, orElse: JsonElement): JsonElement {
return worldProperties[name] ?: orElse
}
fun getProperty(name: String, orElse: () -> JsonElement): JsonElement {
return worldProperties[name] ?: orElse()
}
protected open fun setProperty0(key: String, value: JsonElement) { protected open fun setProperty0(key: String, value: JsonElement) {
} }
@ -417,6 +435,39 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
.filterNotNull() .filterNotNull()
} }
fun collide(point: Vector2d, filter: Predicate<CollisionPoly>): Boolean {
return queryTileCollisions(AABB.withSide(point, 2.0)).any { filter.test(it) && point in it.poly }
}
data class LineCollisionResult(val poly: CollisionPoly, val border: Vector2d, val normal: Vector2d)
// ugly.
// but original code is way worse than this, because it considers A FUCKING RECTANGLE
// holy shiet
// we, on other hand, consider only tiles along line
fun collide(line: Line2d, filter: Predicate<CollisionPoly>): LineCollisionResult? {
var found: LineCollisionResult? = null
castRay(line.p0, line.p1) { cell, fraction, x, y, normal, borderX, borderY ->
val query = queryTileCollisions(AABB.withSide(Vector2d(borderX, borderY), 2.0))
for (poly in query) {
if (filter.test(poly)) {
val intersect = poly.poly.intersect(line)
if (intersect != null) {
found = LineCollisionResult(poly, intersect.second.point!!, intersect.first)
return@castRay RayFilterResult.BREAK
}
}
}
RayFilterResult.SKIP
}
return found
}
fun polyIntersects(with: Poly, filter: Predicate<CollisionPoly> = Predicate { true }, tolerance: Double = 0.0): Boolean { fun polyIntersects(with: Poly, filter: Predicate<CollisionPoly> = Predicate { true }, tolerance: Double = 0.0): Boolean {
return collide(with, filter).anyMatch { it.penetration >= tolerance } return collide(with, filter).anyMatch { it.penetration >= tolerance }
} }
@ -433,6 +484,36 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return template.worldParameters?.gravity ?: Vector2d.ZERO return template.worldParameters?.gravity ?: Vector2d.ZERO
} }
data class LiquidLevel(val type: Registry.Entry<LiquidDefinition>, val average: Double)
fun averageLiquidLevel(rect: AABB): LiquidLevel? {
val liquidLevels = Object2DoubleOpenHashMap<Registry.Entry<LiquidDefinition>>()
var area = 0.0
anyCellSatisfies(rect) { x, y, cell ->
val blockIncidence = AABB.leftCorner(Vector2d(x.toDouble(), y.toDouble()), 1.0, 1.0).overlap(rect).volume
area += blockIncidence
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) {
liquidLevels.put(cell.liquid.state, liquidLevels.getDouble(cell.liquid.state) + cell.liquid.level.coerceAtMost(1f) * blockIncidence)
}
false
}
if (liquidLevels.isEmpty()) {
return null
}
val max = liquidLevels.object2DoubleEntrySet().maxBy { it.doubleValue }!!
return LiquidLevel(max.key, max.doubleValue / area)
}
/**
* returns unapplied tile modifications
*/
abstract fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false): List<Pair<Vector2i, TileModification>>
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.common.collect.ImmutableList
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kstarbound.io.readVector2i import ru.dbotthepony.kstarbound.io.readVector2i
@ -242,7 +243,7 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean = true, val loop
return poly.distance(nearestTo(poly.centre, point)) return poly.distance(nearestTo(poly.centre, point))
} }
fun split(poly: Poly): List<Poly> { val splitLines: ImmutableList<Pair<Line2d, Pair<Vector2d, Vector2d>>> by lazy {
val lines = ObjectArrayList<Pair<Line2d, Pair<Vector2d, Vector2d>>>(4) val lines = ObjectArrayList<Pair<Line2d, Pair<Vector2d, Vector2d>>>(4)
if (x.isSplitting) { if (x.isSplitting) {
@ -255,14 +256,18 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean = true, val loop
lines.add(Line2d(Vector2d(0.0, y.cellsD), Vector2d(1.0, y.cellsD)) to (Vector2d(0.0, -y.cellsD) to Vector2d.ZERO)) lines.add(Line2d(Vector2d(0.0, y.cellsD), Vector2d(1.0, y.cellsD)) to (Vector2d(0.0, -y.cellsD) to Vector2d.ZERO))
} }
if (lines.isEmpty) { ImmutableList.copyOf(lines)
}
fun split(poly: Poly): List<Poly> {
if (splitLines.isEmpty()) {
return listOf(poly) return listOf(poly)
} }
val split = ObjectArrayList<Poly>() val split = ObjectArrayList<Poly>()
split.add(poly) split.add(poly)
for ((line, corrections) in lines) { for ((line, corrections) in splitLines) {
val (correctionIfLeft, correctionIfRight) = corrections val (correctionIfLeft, correctionIfRight) = corrections
val itr = split.listIterator() val itr = split.listIterator()
@ -285,8 +290,8 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean = true, val loop
val result = line.intersect(vedge, true) val result = line.intersect(vedge, true)
// check if it is an actual split, if poly points just rest on that line consider they rest on left side // check if it is an actual split, if poly points just rest on that line consider they rest on left side
if (result.intersects) { if (result.intersects && result.point != null) {
val point = line.intersect(vedge, true).point.orThrow { RuntimeException() } val point = result.point
if (left0 < 0.0 && left1 >= 0.0) { if (left0 < 0.0 && left1 >= 0.0) {
leftPoints.add(vedge.p1 + correctionIfLeft) leftPoints.add(vedge.p1 + correctionIfLeft)
@ -326,4 +331,67 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean = true, val loop
val wrap = wrap(point) val wrap = wrap(point)
return split(poly).any { it.contains(wrap) } return split(poly).any { it.contains(wrap) }
} }
fun split(lineToSplit: Line2d): List<Line2d> {
val result = ObjectArrayList<Line2d>()
result.add(lineToSplit)
val itr = result.listIterator()
for ((splitLine, corrections) in splitLines) {
val (correctionIfLeft, correctionIfRight) = corrections
for (line in itr) {
val intersect = splitLine.intersect(line, true)
if (intersect.intersects && intersect.point != null) {
val left0 = splitLine.isLeft(line.p0)
val left1 = splitLine.isLeft(line.p1)
itr.remove()
if (left0 < 0.0 && left1 >= 0.0) {
// crossing from right to left
itr.add(Line2d(line.p1 + correctionIfLeft, intersect.point + correctionIfLeft))
itr.add(Line2d(intersect.point + correctionIfRight, line.p0 + correctionIfRight))
} else {
// crossing from left to right OR starting right at left border?
itr.add(Line2d(line.p0 + correctionIfLeft, intersect.point + correctionIfLeft))
itr.add(Line2d(intersect.point + correctionIfRight, line.p1 + correctionIfRight))
}
}
}
}
return result
}
fun rectIntersectsCircle(rect: AABB, center: Vector2d, radius: Double): Boolean {
if (center in rect)
return true
return rect.edges.any { lineIntersectsCircle(it, center, radius) }
}
fun lineIntersectsCircle(line: Line2d, center: Vector2d, radius: Double): Boolean {
return split(line).any { it.distanceTo(nearestTo(it.center, center)) <= radius }
}
fun lineIntersectsRect(line: Line2d, rect: AABB): Boolean {
val lines = split(line)
val rects = split(rect).first
return lines.any { l -> rects.any { r -> r.intersect(l) } }
}
fun polyIntersectsPoly(a: Poly, b: Poly): Boolean {
val sa = split(a)
val sb = split(b)
return sa.any { p -> sb.any { p.intersect(it) != null } }
}
fun rectIntersectsRect(a: AABB, b: AABB): Boolean {
val sa = split(a).first
val sb = split(b).first
return sa.any { p -> sb.any { p.intersect(it) } }
}
} }

View File

@ -2,6 +2,8 @@ package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
@ -14,6 +16,8 @@ import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
@ -68,6 +72,9 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
abstract val type: EntityType abstract val type: EntityType
open val isEphemeral: Boolean
get() = false
/** /**
* If set, then the entity will be discoverable by its unique id and will be * If set, then the entity will be discoverable by its unique id and will be
* indexed in the stored world. Unique ids must be different across all * indexed in the stored world. Unique ids must be different across all
@ -105,16 +112,22 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
val networkGroup = MasterElement(NetworkedGroup()) val networkGroup = MasterElement(NetworkedGroup())
abstract fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) abstract fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean)
fun writeNetwork(isLegacy: Boolean): ByteArrayList {
val stream = FastByteArrayOutputStream()
writeNetwork(DataOutputStream(stream), isLegacy)
return ByteArrayList.wrap(stream.array, stream.length)
}
protected var spatialEntry: EntityIndex.Entry? = null protected var spatialEntry: EntityIndex.Entry? = null
private set private set
/** /**
* Used for spatial index * Used for spatial index, AABB in world coordinates
*/ */
abstract val metaBoundingBox: AABB abstract val metaBoundingBox: AABB
open val collisionArea: AABB open val collisionArea: AABB
get() = NEVER get() = AABB.NEVER
open fun onNetworkUpdate() { open fun onNetworkUpdate() {
@ -128,8 +141,13 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
if (innerWorld != null) if (innerWorld != null)
throw IllegalStateException("Already spawned (in world $innerWorld)") throw IllegalStateException("Already spawned (in world $innerWorld)")
if (entityID == 0) if (entityID == 0) {
entityID = world.nextEntityID.incrementAndGet() if (world is ClientWorld) {
entityID = world.client.activeConnection?.nextEntityID() ?: world.nextEntityID.incrementAndGet()
} else {
entityID = world.nextEntityID.incrementAndGet()
}
}
world.eventLoop.ensureSameThread() world.eventLoop.ensureSameThread()
@ -143,6 +161,12 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
world.entityList.add(this) world.entityList.add(this)
spatialEntry = world.entityIndex.Entry(this) spatialEntry = world.entityIndex.Entry(this)
onJoinWorld(world) onJoinWorld(world)
if (world is ClientWorld && !isRemote) {
val connection = world.client.activeConnection
// TODO: incomplete
connection?.send(EntityCreatePacket(type, writeNetwork(connection.isLegacy), networkGroup.write(0L, connection.isLegacy).first, entityID))
}
} }
fun remove(reason: RemovalReason) { fun remove(reason: RemovalReason) {
@ -167,6 +191,13 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
world.clients.forEach { world.clients.forEach {
it.forget(this, reason) it.forget(this, reason)
} }
} else if (world is ClientWorld && !isRemote) {
val connection = world.client.activeConnection
if (connection != null) {
connection.send(EntityDestroyPacket(entityID, writeNetwork(connection.isLegacy), reason.dying))
connection.freeEntityID(entityID)
}
} }
} }
@ -194,6 +225,5 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val NEVER = AABB(Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY), Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY))
} }
} }

View File

@ -1,9 +1,35 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
/** /**
* Monsters, NPCs, Players * Monsters, NPCs, Players
*/ */
abstract class ActorEntity() : DynamicEntity() { abstract class ActorEntity() : DynamicEntity() {
final override val movement: ActorMovementController = ActorMovementController() final override val movement: ActorMovementController = ActorMovementController()
abstract val statusController: StatusController abstract val statusController: StatusController
enum class DamageBarType {
DEFAULT, NONE, SPECIAL
}
abstract val health: Double
abstract val maxHealth: Double
abstract val damageBarType: DamageBarType
abstract val name: String
enum class PortraitMode(override val jsonName: String) : IStringSerializable {
HEAD("head"),
BUST("bust"),
FULL("full"),
FULL_NEUTRAL("fullneutral"),
FULL_NUDE("fullnude"),
FULL_NEUTRAL_NUDE("fullneutralnude");
}
open fun portrait(mode: PortraitMode): List<Drawable> {
return emptyList()
}
} }

View File

@ -4,6 +4,7 @@ import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.JumpProfile import ru.dbotthepony.kstarbound.defs.JumpProfile
@ -82,7 +83,7 @@ class ActorMovementController() : MovementController() {
var controlMove: Direction? = null var controlMove: Direction? = null
var actorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY var actorMovementParameters: ActorMovementParameters = Globals.actorMovementParameters
var movementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY var movementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY
var controlActorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY var controlActorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY
@ -189,6 +190,11 @@ class ActorMovementController() : MovementController() {
return params return params
} }
fun resetBaseParameters(base: ActorMovementParameters) {
actorMovementParameters = Globals.actorMovementParameters.merge(base)
movementParameters = calculateMovementParameters(actorMovementParameters)
}
fun clearControls() { fun clearControls() {
controlRotationRate = 0.0 controlRotationRate = 0.0
controlAcceleration = Vector2d.ZERO controlAcceleration = Vector2d.ZERO

View File

@ -4,6 +4,7 @@ import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.actor.Gender
import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
@ -17,6 +18,9 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedStatefulItem
abstract class HumanoidActorEntity() : ActorEntity() { abstract class HumanoidActorEntity() : ActorEntity() {
abstract val aimPosition: Vector2d abstract val aimPosition: Vector2d
abstract val species: String
abstract val gender: Gender
val effects = EffectEmitter(this) val effects = EffectEmitter(this)
// it makes no sense to split ToolUser' logic into separate class // it makes no sense to split ToolUser' logic into separate class

View File

@ -48,7 +48,6 @@ class ItemDropEntity() : DynamicEntity() {
var shouldNotExpire = false var shouldNotExpire = false
val age = RelativeClock() val age = RelativeClock()
var intangibleTimer = GameTimer(0.0) var intangibleTimer = GameTimer(0.0)
private set
init { init {
movement.applyParameters(Globals.itemDrop.movementSettings) movement.applyParameters(Globals.itemDrop.movementSettings)

View File

@ -0,0 +1,22 @@
package ru.dbotthepony.kstarbound.world.entities.api
interface InspectableEntity {
/**
* Default implementation returns true
*/
val isInspectable: Boolean
get() = true
/**
* If this entity can be entered into the player log, will return the log identifier.
*/
val inspectionLogName: String?
get() = null
/**
* Long description to display when inspected, if any
*/
fun inspectionDescription(species: String?): String? {
return null
}
}

View File

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

View File

@ -8,6 +8,7 @@ import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.actor.Gender
import ru.dbotthepony.kstarbound.defs.actor.HumanoidData import ru.dbotthepony.kstarbound.defs.actor.HumanoidData
import ru.dbotthepony.kstarbound.defs.actor.HumanoidEmote import ru.dbotthepony.kstarbound.defs.actor.HumanoidEmote
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerGamemode import ru.dbotthepony.kstarbound.defs.actor.player.PlayerGamemode
@ -99,8 +100,23 @@ class PlayerEntity() : HumanoidActorEntity() {
networkGroup.upstream.add(effectAnimator.networkGroup) networkGroup.upstream.add(effectAnimator.networkGroup)
networkGroup.upstream.add(statusController) networkGroup.upstream.add(statusController)
networkGroup.upstream.add(techController.networkGroup) networkGroup.upstream.add(techController.networkGroup)
movement.resetBaseParameters(Globals.player.movementParameters)
} }
override val health: Double
get() = statusController.resources["health"]!!.value
override val maxHealth: Double
get() = statusController.resources["health"]!!.maxValue!!
override val damageBarType: DamageBarType
get() = DamageBarType.DEFAULT
override val name: String
get() = humanoidData.name
override val species: String
get() = humanoidData.species
override val gender: Gender
get() = humanoidData.gender
override val metaBoundingBox: AABB override val metaBoundingBox: AABB
get() = Globals.player.metaBoundBox + position get() = Globals.player.metaBoundBox + position

View File

@ -13,6 +13,7 @@ import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.defs.actor.EquipmentSlot import ru.dbotthepony.kstarbound.defs.actor.EquipmentSlot
import ru.dbotthepony.kstarbound.defs.actor.EssentialSlot import ru.dbotthepony.kstarbound.defs.actor.EssentialSlot
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
@ -84,7 +85,7 @@ class PlayerInventory {
.map { it to networkedItem() } .map { it to networkedItem() }
.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) .collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))
private val currencies = NetworkedMap(keyCodec = InternedStringCodec, valueCodec = UnsignedVarLongCodec to LongValueCodec, isDumb = true) val currencies = NetworkedMap(keyCodec = InternedStringCodec, valueCodec = UnsignedVarLongCodec to LongValueCodec, isDumb = true)
init { init {
// this is required for original engine // this is required for original engine
@ -124,6 +125,27 @@ class PlayerInventory {
} }
} }
fun hasCountOfItem(item: ItemDescriptor, exactMatch: Boolean = false): Long {
var count = 0L
if (handSlot.matches(item, exactMatch)) {
count += handSlot.size
}
if (trashSlot.matches(item, exactMatch)) {
count += trashSlot.size
}
for (pitem in equipment.values) {
if (pitem.get().matches(item, exactMatch)) {
count += trashSlot.size
}
}
bags.values.forEach { count += it.hasCountOfItem(item, exactMatch) }
return count
}
// "swap slot" in original sources // "swap slot" in original sources
var handSlot by networkGroup.add(networkedItem()) var handSlot by networkGroup.add(networkedItem())
var trashSlot by networkGroup.add(networkedItem()) var trashSlot by networkGroup.add(networkedItem())

View File

@ -27,18 +27,30 @@ class LoungeableObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(c
isInteractive = true isInteractive = true
} }
private val sitPositions = ArrayList<Vector2d>() val sitPositions = ArrayList<Vector2d>()
private var sitFlipDirection = false
private var sitOrientation = LoungeOrientation.NONE var sitFlipDirection = false
private var sitAngle = 0.0 private set
private var sitCoverImage = "" var sitOrientation = LoungeOrientation.NONE
private var sitFlipImages = false private set
private val sitStatusEffects = ObjectArraySet<Either<StatModifier, String>>() var sitAngle = 0.0
private val sitEffectEmitters = ObjectArraySet<String>() private set
private var sitEmote: String? = null var sitCoverImage = ""
private var sitDance: String? = null private set
private var sitArmorCosmeticOverrides: JsonObject = JsonObject() var sitFlipImages = false
private var sitCursorOverride: String? = null private set
val sitStatusEffects = ObjectArraySet<Either<StatModifier, String>>()
val sitEffectEmitters = ObjectArraySet<String>()
var sitEmote: String? = null
private set
var sitDance: String? = null
private set
var sitArmorCosmeticOverrides: JsonObject = JsonObject()
private set
var sitCursorOverride: String? = null
private set
private fun updateSitParams() { private fun updateSitParams() {
orientation ?: return orientation ?: return

View File

@ -13,6 +13,7 @@ import com.google.gson.reflect.TypeToken
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString import org.classdump.luna.ByteString
import org.classdump.luna.Table import org.classdump.luna.Table
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
@ -103,6 +104,10 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
open fun loadParameters(parameters: JsonObject) { open fun loadParameters(parameters: JsonObject) {
if ("uniqueId" in parameters) {
uniqueID.accept(KOptional(parameters["uniqueId"]!!.asString))
}
for ((k, v) in parameters.entrySet()) { for ((k, v) in parameters.entrySet()) {
this.parameters[k] = v this.parameters[k] = v
} }
@ -397,6 +402,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
provideEntityBindings(this, lua) provideEntityBindings(this, lua)
provideAnimatorBindings(animator, lua) provideAnimatorBindings(animator, lua)
lua.attach(config.value.scripts) lua.attach(config.value.scripts)
lua.random = world.random
luaUpdate.stepCount = lookupProperty(JsonPath("scriptDelta")) { JsonPrimitive(5) }.asDouble luaUpdate.stepCount = lookupProperty(JsonPath("scriptDelta")) { JsonPrimitive(5) }.asDouble
lua.init() lua.init()
} }

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.world.physics package ru.dbotthepony.kstarbound.world.physics
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import java.util.*
enum class CollisionType(override val jsonName: String, val isEmpty: Boolean, val isSolidCollision: Boolean, val isTileCollision: Boolean) : IStringSerializable { enum class CollisionType(override val jsonName: String, val isEmpty: Boolean, val isSolidCollision: Boolean, val isTileCollision: Boolean) : IStringSerializable {
// not loaded, block collisions by default // not loaded, block collisions by default
@ -21,4 +22,9 @@ enum class CollisionType(override val jsonName: String, val isEmpty: Boolean, va
else else
return other return other
} }
companion object {
val SOLID: Set<CollisionType> = Collections.unmodifiableSet(EnumSet.copyOf(entries.filter { it.isSolidCollision }.toSet()))
val TILE: Set<CollisionType> = Collections.unmodifiableSet(EnumSet.copyOf(entries.filter { it.isTileCollision }.toSet()))
}
} }

View File

@ -7,6 +7,7 @@ import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.lwjgl.opengl.GL11.GL_LINES import org.lwjgl.opengl.GL11.GL_LINES
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
@ -273,7 +274,7 @@ class Poly private constructor(val edges: ImmutableList<Line2d>, val vertices: I
if (isEmpty || !aabb.intersectWeak(other.aabb)) if (isEmpty || !aabb.intersectWeak(other.aabb))
return null return null
val normals = ObjectOpenHashSet<Vector2d>() val normals = HashSet<Vector2d>(edges.size + other.edges.size)
edges.forEach { normals.add(it.normal) } edges.forEach { normals.add(it.normal) }
other.edges.forEach { normals.add(it.normal) } other.edges.forEach { normals.add(it.normal) }
@ -281,7 +282,7 @@ class Poly private constructor(val edges: ImmutableList<Line2d>, val vertices: I
normals.removeIf { it.dot(axis) == 0.0 } normals.removeIf { it.dot(axis) == 0.0 }
} }
val intersections = ArrayList<Penetration>() val intersections = ObjectArrayList<Penetration>()
for (normal in normals) { for (normal in normals) {
val projectThis = project(normal) val projectThis = project(normal)
@ -349,12 +350,27 @@ class Poly private constructor(val edges: ImmutableList<Line2d>, val vertices: I
} }
} }
if (intersections.isEmpty()) if (intersections.isEmpty)
return null return null
return intersections.min() return intersections.min()
} }
fun intersect(line: Line2d): Pair<Vector2d, Line2d.Intersection>? {
val intersections = ObjectArrayList<Pair<Vector2d, Line2d.Intersection>>()
for (edge in edges) {
val intersect = line.intersect(edge)
if (intersect.intersects && !intersect.coincides) {
intersections.add(edge.normal to intersect)
}
}
intersections.sortWith { o1, o2 -> o1.second.t!!.compareTo(o2.second.t!!) }
return intersections.firstOrNull()
}
fun render(client: StarboundClient = StarboundClient.current(), color: RGBAColor = RGBAColor.LIGHT_GREEN) { fun render(client: StarboundClient = StarboundClient.current(), color: RGBAColor = RGBAColor.LIGHT_GREEN) {
if (isEmpty) return if (isEmpty) return

View File

@ -5,8 +5,8 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import ru.dbotthepony.kstarbound.io.Vector2fCodec
import ru.dbotthepony.kstarbound.math.vector.Vector2f import ru.dbotthepony.kstarbound.math.vector.Vector2f
import ru.dbotthepony.kstarbound.math.vector.Vector2fCodec
import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.EventCounterElement import ru.dbotthepony.kstarbound.network.syncher.EventCounterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup