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`)
* `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
* 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`)
* Used by object and plant anchoring code to determine valid placement
* 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)
#### .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
---------------
@ -102,6 +117,19 @@ val color: TileColor = TileColor.DEFAULT
* Added `animator.hasEffect(effect: string): boolean`
* 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
---------------

View File

@ -7,6 +7,7 @@ import io.netty.channel.ChannelInitializer
import io.netty.channel.local.LocalAddress
import io.netty.channel.local.LocalChannel
import io.netty.channel.socket.nio.NioSocketChannel
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound
@ -34,8 +35,40 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn
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) {
client.mailbox.execute { task.invoke(client) }
client.execute { task.invoke(client) }
}
override fun toString(): String {

View File

@ -71,9 +71,9 @@ enum class RenderLayer {
fun tileLayer(isBackground: Boolean, isModifier: Boolean, tile: AbstractTileState): Point {
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 {
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
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.Long2ObjectOpenHashMap
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.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateWorldPropertiesPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.TileModification
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
@ -66,6 +70,10 @@ class ClientWorld(
override val eventLoop: BlockableEventLoop
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 Layer(private val view: ITileAccess, private val isBackground: Boolean) {
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 {
val ring = listOf(
Vector2i(0, 0),

View File

@ -32,6 +32,7 @@ import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.EnumSet
// uint8_t
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)
}
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 {
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 PASSIVE = EntityDamageTeam(TeamType.PASSIVE)
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.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
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.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
@ -230,6 +232,10 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig
*/
abstract fun flop(): Drawable
open fun toJson(): JsonObject {
return JsonObject()
}
companion object {
val EMPTY = Empty()
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 transformations = gson.getAdapter(Transformations::class.java)
override fun write(out: JsonWriter?, value: Drawable?) {
TODO("Not yet implemented")
override fun write(out: JsonWriter, value: Drawable?) {
out.value(value?.toJson())
}
override fun read(`in`: JsonReader): Drawable {

View File

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

View File

@ -502,7 +502,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
obj.orientationIndex = orientation.toLong()
obj.joinWorld(parent)
} 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) {
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 {
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 count = indexNoYield(data, 2L) ?: indexNoYield(data, "count") ?: 1L
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 {
val name = data[1L] ?: data["name"] ?: data["item"]
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 ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -10,7 +11,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
@JsonFactory
data class LiquidDefinition(
val name: String,
val liquidId: Int,
val liquidId: Int?,
val description: String = "...",
val tickDelta: Int = 1,
val color: RGBAColor,
@ -24,7 +25,7 @@ data class LiquidDefinition(
val isMeta: Boolean = false,
) {
@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 {
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
data class TileDefinition(
val materialId: Int,
val materialId: Int?,
val materialName: String,
val particleColor: RGBAColor? = null,
val itemDrop: String? = null,
@ -54,7 +54,7 @@ data class TileDefinition(
val blocksLiquidFlow: Boolean = collisionKind.isSolidCollision,
) : IRenderableTile, IThingWithDescription by descriptionData {
init {
require(materialId > 0) { "Invalid tile ID $materialId" }
require(materialId == null || materialId > 0) { "Invalid tile ID $materialId" }
}
fun supportsModifier(modifier: Registry.Entry<TileModifierDefinition>): Boolean {

View File

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

View File

@ -536,6 +536,9 @@ class WorldLayout {
fun getWeighting(x: Int, y: Int): List<RegionWeighting> {
val weighting = ArrayList<RegionWeighting>()
if (layers.isEmpty())
return weighting
fun addLayerWeighting(layer: Layer, x: Int, weightFactor: Double) {
if (layer.cells.isEmpty())
return
@ -610,6 +613,28 @@ class WorldLayout {
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>() {
override fun write(out: JsonWriter, value: WorldLayout?) {
if (value == null)

View File

@ -513,6 +513,16 @@ class WorldTemplate(val geometry: WorldGeometry) {
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 {
private val LOGGER = LogManager.getLogger()

View File

@ -25,6 +25,30 @@ interface IContainer {
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
fun add(item: ItemStack, simulate: Boolean = false): ItemStack {
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)
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 {
@ -314,7 +334,7 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para
if (isEmpty)
return other.isEmpty
return other.size == size && other.config == config && other.parameters == parameters
return matches(other) && size == other.size
}
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>() {
override fun write(out: JsonWriter, value: ItemStack?) {
val json = value?.toJson()

View File

@ -8,6 +8,7 @@ import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.longs.Long2ObjectAVLTreeMap
import org.classdump.luna.ByteString
import org.classdump.luna.Conversions
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table
import org.classdump.luna.TableFactory
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.Vector2i
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.world.physics.Poly
fun ExecutionContext.toVector2i(table: Any): Vector2i {
@ -46,6 +48,12 @@ fun ExecutionContext.toVector2d(table: Any): Vector2d {
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 {
val vertices = ArrayList<Vector2d>()
@ -303,14 +311,18 @@ fun TableFactory.createJsonArray(): Table {
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 {
this[1L] = value.component1()
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 {
value.vertices.withIndex().forEach { (i, v) -> this[i + 1L] = from(v) }
}
@ -326,14 +338,18 @@ fun TableFactory.fromCollection(value: Collection<JsonElement?>): Table {
return table
}
fun TableFactory.from(value: IStruct2i): Table {
fun TableFactory.from(value: IStruct2i?): Table? {
value ?: return null
return newTable(2, 0).also {
it.rawset(1L, value.component1())
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 {
it.rawset(1L, value.component1())
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 {
it.rawset(1L, value.component1())
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 {
it.rawset(1L, value.redInt.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 {
it.rawset(1L, value.mins.x)
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.impl.NonsuspendableFunctionException
import org.classdump.luna.lib.ArgumentIterator
import org.classdump.luna.lib.TableLib
import org.classdump.luna.runtime.AbstractFunction0
import org.classdump.luna.runtime.AbstractFunction1
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.LuaFunction
import org.classdump.luna.runtime.UnresolvedControlThrowable
import kotlin.math.max
import kotlin.math.min
fun ExecutionContext.indexNoYield(table: Any, key: Any): Any? {
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: 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>> {
var key: Any? = initialKey() ?: return ObjectIterators.emptyIterator()
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 {
val table = newTable(values.size, 0)
@ -110,6 +156,10 @@ fun TableFactory.tableOf(vararg values: Any?): Table {
return table
}
fun TableFactory.tableOf(): Table {
return newTable()
}
@Deprecated("Function is a stub")
fun luaStub(message: String = "not yet implemented"): 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 result = get.value.evaluate(seed ?: System.nanoTime(), level)
.stream().filter { it.isNotEmpty }.map { it.toTable(context)!! }.toList()
context.returnBuffer.setTo(context.from(result))
context.returnBuffer.setTo(context.tableOf(*get.value.evaluate(seed ?: System.nanoTime(), level).filter { it.isNotEmpty }.map { context.from(it.toJson()) }.toTypedArray()))
}
private fun materialMiningSound(context: ExecutionContext, arguments: ArgumentIterator) {

View File

@ -1,24 +1,164 @@
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.LuaRuntimeException
import org.classdump.luna.Table
import org.classdump.luna.runtime.ExecutionContext
import org.classdump.luna.runtime.LuaFunction
import ru.dbotthepony.kommons.collect.map
import ru.dbotthepony.kommons.collect.toList
import ru.dbotthepony.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.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.lua.contains
import ru.dbotthepony.kstarbound.lua.from
import ru.dbotthepony.kstarbound.lua.get
import ru.dbotthepony.kstarbound.lua.indexNoYield
import ru.dbotthepony.kstarbound.lua.iterator
import ru.dbotthepony.kstarbound.lua.luaFunction
import ru.dbotthepony.kstarbound.lua.luaFunctionN
import ru.dbotthepony.kstarbound.lua.luaStub
import ru.dbotthepony.kstarbound.lua.nextOptionalInteger
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
import ru.dbotthepony.kstarbound.lua.toLine2d
import ru.dbotthepony.kstarbound.lua.toPoly
import ru.dbotthepony.kstarbound.lua.toVector2d
import ru.dbotthepony.kstarbound.lua.toVector2i
import ru.dbotthepony.kstarbound.lua.unpackAsArray
import ru.dbotthepony.kstarbound.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.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.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.Poly
import java.util.Collections
import java.util.EnumSet
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) {
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)
}
}
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.luaFunction
import ru.dbotthepony.kstarbound.lua.set
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toColor
import ru.dbotthepony.kstarbound.lua.toJson
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
@ -55,7 +56,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
table["boundBox"] = luaFunction { returnBuffer.setTo(from(self.metaBoundingBox)) }
// 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["setSoundEffectEnabled"] = luaFunction { state: Boolean -> self.soundEffectEnabled = state }

View File

@ -3,23 +3,40 @@
package ru.dbotthepony.kstarbound.math
import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kommons.math.intersectRectangles
import ru.dbotthepony.kommons.math.rectangleContainsRectangle
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
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,
* and [maxs] as uppermost corner of BB
*/
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 {
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.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}" }
}
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 minus(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 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 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 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 {
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)
}
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
*/
@ -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
*/
@ -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 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)
}
val center: Vector2d
get() = p0 + difference * 0.5
operator fun plus(other: IStruct2d): Line2d {
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)
}
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 {
val EMPTY = Intersection(false, KOptional(), KOptional(), false, false)
val EMPTY = Intersection(false, null, null, false, false)
}
}
fun difference(): Vector2d {
return p1 - p0
}
val difference: Vector2d
get() = p1 - p0
fun reverse(): Line2d {
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 {
val (c, d) = other
val ab = difference()
val cd = other.difference()
val ab = difference
val cd = other.difference
val abCross = p0.cross(p1)
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 {
return Intersection.EMPTY
}
@ -132,8 +134,8 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
return Intersection(
intersects = intersects,
t = KOptional(ta),
point = KOptional((p1 - p0) * ta + p0),
t = ta,
point = (p1 - p0) * ta + p0,
coincides = false,
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 {
val diff = difference()
val diff = difference
val (x, y) = axis
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)
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 {
@ -161,7 +163,7 @@ data class Line2d(val p0: Vector2d, val p1: Vector2d) {
if (!infinite)
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>() {

View File

@ -245,5 +245,15 @@ data class Vector2d(
@JvmField val POSITIVE_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) {
connection.resetOccupiedEntityIDs()
TODO("Not yet implemented")
}
}

View File

@ -209,7 +209,7 @@ class ServerWorld private constructor(
if (source != null && health?.isDead == true) {
source.receiveMessage("tileBroken", jsonArrayOf(
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,
health.isHarvested
))
@ -222,7 +222,7 @@ class ServerWorld private constructor(
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)
var size: Int
@ -438,7 +438,6 @@ class ServerWorld private constructor(
}
override fun setProperty0(key: String, value: JsonElement) {
super.setProperty0(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 (
intersection.intersects &&
intersection.point.get() != proposed.p0 &&
intersection.point.get() != proposed.p1
intersection.point != proposed.p0 &&
intersection.point != proposed.p1
) {
valid = false
break

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.util
import com.google.gson.JsonElement
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
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 {
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 {
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.objects.Object2IntAVLTreeMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kommons.util.KOptional
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> {
val entriesDirect = ArrayList<AbstractEntity>()
fun query(rect: AABB, filter: Predicate<in AbstractEntity> = Predicate { true }, withEdges: Boolean = true): MutableList<AbstractEntity> {
val entriesDirect = ObjectArrayList<AbstractEntity>()
iterate(rect, withEdges = withEdges, visitor = {
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?
}
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) {
walk<Unit>(rect, { visitor(it); KOptional() }, withEdges)
}
@ -301,7 +306,7 @@ class EntityIndex(val geometry: WorldGeometry) {
val sector = map[index(x, y)] ?: continue
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)
if (visit.isPresent)

View File

@ -23,13 +23,24 @@ data class RayCastResult(
}
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),
// 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),
// continue tracing, write hit tile into traversed tiles list
/**
* continue tracing, write hit tile into traversed tiles list
*/
CONTINUE(false, true);
companion object {
@ -120,7 +131,7 @@ fun ICellAccess.castRay(
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)
val c = if (result.write || result.hit) {

View File

@ -32,9 +32,8 @@ class Sky() {
var skyParameters by networkedGroup.upstream.add(networkedJson(SkyParameters()))
var skyType by networkedGroup.upstream.add(networkedEnumStupid(SkyType.ORBITAL))
var time by networkedGroup.upstream.add(networkedDouble())
private set
var flyingType by networkedGroup.upstream.add(networkedEnum(FlyingType.NONE))
private set
var enterHyperspace by networkedGroup.upstream.add(networkedBoolean())
@ -67,6 +66,15 @@ class Sky() {
var pathRotation: Double = 0.0
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
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.TileDefinition
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.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
@ -269,8 +270,9 @@ sealed class TileModification {
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.isInfinite)
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
// (unless we are removing existing liquid)
// while checks above makes vanilla client look stupid when it tries to pour liquids into other
// liquids, we must think better than vanilla client.
@ -290,8 +292,11 @@ sealed class TileModification {
} else {
cell.liquid.reset()
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)

View File

@ -1,10 +1,13 @@
package ru.dbotthepony.kstarbound.world
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArraySet
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 org.apache.logging.log4j.LogManager
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.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
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.WorldTemplate
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.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ParallelPerform
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.api.ICellAccess
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.entities.AbstractEntity
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)
}
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) {
}
@ -417,6 +435,39 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
.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 {
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
}
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 {
private val LOGGER = LogManager.getLogger()

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world
import com.google.common.collect.ImmutableList
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet
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))
}
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)
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))
}
if (lines.isEmpty) {
ImmutableList.copyOf(lines)
}
fun split(poly: Poly): List<Poly> {
if (splitLines.isEmpty()) {
return listOf(poly)
}
val split = ObjectArrayList<Poly>()
split.add(poly)
for ((line, corrections) in lines) {
for ((line, corrections) in splitLines) {
val (correctionIfLeft, correctionIfRight) = corrections
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)
// check if it is an actual split, if poly points just rest on that line consider they rest on left side
if (result.intersects) {
val point = line.intersect(vedge, true).point.orThrow { RuntimeException() }
if (result.intersects && result.point != null) {
val point = result.point
if (left0 < 0.0 && left1 >= 0.0) {
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)
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.JsonElement
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.koptional
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.InteractAction
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.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
@ -68,6 +72,9 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
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
* 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())
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
private set
/**
* Used for spatial index
* Used for spatial index, AABB in world coordinates
*/
abstract val metaBoundingBox: AABB
open val collisionArea: AABB
get() = NEVER
get() = AABB.NEVER
open fun onNetworkUpdate() {
@ -128,8 +141,13 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
if (innerWorld != null)
throw IllegalStateException("Already spawned (in world $innerWorld)")
if (entityID == 0)
entityID = world.nextEntityID.incrementAndGet()
if (entityID == 0) {
if (world is ClientWorld) {
entityID = world.client.activeConnection?.nextEntityID() ?: world.nextEntityID.incrementAndGet()
} else {
entityID = world.nextEntityID.incrementAndGet()
}
}
world.eventLoop.ensureSameThread()
@ -143,6 +161,12 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
world.entityList.add(this)
spatialEntry = world.entityIndex.Entry(this)
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) {
@ -167,6 +191,13 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
world.clients.forEach {
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 {
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
import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
/**
* Monsters, NPCs, Players
*/
abstract class ActorEntity() : DynamicEntity() {
final override val movement: ActorMovementController = ActorMovementController()
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.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.JumpProfile
@ -82,7 +83,7 @@ class ActorMovementController() : MovementController() {
var controlMove: Direction? = null
var actorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY
var actorMovementParameters: ActorMovementParameters = Globals.actorMovementParameters
var movementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY
var controlActorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY
@ -189,6 +190,11 @@ class ActorMovementController() : MovementController() {
return params
}
fun resetBaseParameters(base: ActorMovementParameters) {
actorMovementParameters = Globals.actorMovementParameters.merge(base)
movementParameters = calculateMovementParameters(actorMovementParameters)
}
fun clearControls() {
controlRotationRate = 0.0
controlAcceleration = Vector2d.ZERO

View File

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

View File

@ -48,7 +48,6 @@ class ItemDropEntity() : DynamicEntity() {
var shouldNotExpire = false
val age = RelativeClock()
var intangibleTimer = GameTimer(0.0)
private set
init {
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.Globals
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.HumanoidEmote
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerGamemode
@ -99,8 +100,23 @@ class PlayerEntity() : HumanoidActorEntity() {
networkGroup.upstream.add(effectAnimator.networkGroup)
networkGroup.upstream.add(statusController)
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
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.defs.actor.EquipmentSlot
import ru.dbotthepony.kstarbound.defs.actor.EssentialSlot
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
@ -84,7 +85,7 @@ class PlayerInventory {
.map { it to networkedItem() }
.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 {
// 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
var handSlot 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
}
private val sitPositions = ArrayList<Vector2d>()
private var sitFlipDirection = false
private var sitOrientation = LoungeOrientation.NONE
private var sitAngle = 0.0
private var sitCoverImage = ""
private var sitFlipImages = false
private val sitStatusEffects = ObjectArraySet<Either<StatModifier, String>>()
private val sitEffectEmitters = ObjectArraySet<String>()
private var sitEmote: String? = null
private var sitDance: String? = null
private var sitArmorCosmeticOverrides: JsonObject = JsonObject()
private var sitCursorOverride: String? = null
val sitPositions = ArrayList<Vector2d>()
var sitFlipDirection = false
private set
var sitOrientation = LoungeOrientation.NONE
private set
var sitAngle = 0.0
private set
var sitCoverImage = ""
private set
var sitFlipImages = false
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() {
orientation ?: return

View File

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

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.world.physics
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 {
// not loaded, block collisions by default
@ -21,4 +22,9 @@ enum class CollisionType(override val jsonName: String, val isEmpty: Boolean, va
else
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.stream.JsonReader
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.lwjgl.opengl.GL11.GL_LINES
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))
return null
val normals = ObjectOpenHashSet<Vector2d>()
val normals = HashSet<Vector2d>(edges.size + other.edges.size)
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 }
}
val intersections = ArrayList<Penetration>()
val intersections = ObjectArrayList<Penetration>()
for (normal in normals) {
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 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) {
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.DisplayName
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.Vector2fCodec
import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.EventCounterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup