diff --git a/ADDITIONS.md b/ADDITIONS.md index f3af2c87..b3ea88ee 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -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` +#### world + + * Added `world.liquidNamesAlongLine(start: Vector2d, end: Vector2d): List`, will return Liquid' name instead of its ID + * Added `world.liquidNameAt(at: Vector2i): LiquidState?`, will return Liquid' name instead of its ID + * Added `world.biomeBlockNamesAt(at: Vector2i): List?`, will return Block names instead of their IDs + * Added `world.destroyNamedLiquid(at: Vector2i): LiquidState?`, will return Liquid' name instead of its ID + * Added `world.gravityVector(at: Vector2d): Vector2d`. **Attention:** directional gravity is WIP. + * Added `world.itemDropLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` + * Added `world.playerLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` + * Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` + * Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` + * `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target` + ## Behavior --------------- diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt index 9506f07f..dbfa98d5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt index 537cb8d2..5bf5f2c9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/RenderLayer.kt @@ -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) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index 23332476..efcd0599 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -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, RenderLayer.Point>>() @@ -305,6 +313,17 @@ class ClientWorld( } } + override fun applyTileModifications( + modifications: Collection>, + allowEntityOverlap: Boolean, + ignoreTileProtection: Boolean + ): List> { + // 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), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt index c8880da6..009cf87e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt index 5ddd964f..2630c55a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt @@ -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> 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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt index 5119d8e6..18d6e064 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt @@ -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"); } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt index 966ada59..12558727 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt index bb3b2474..86e06b8c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -98,6 +98,9 @@ fun ItemDescriptor(data: Table, stateMachine: StateMachine): Supplier, val liquidResult: Either? = null, val materialResult: String? = null) { init { require(liquidResult != null || materialResult != null) { "Both liquidResult and materialResult are missing" } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt index 5c4a55be..e9db9e1a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt @@ -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): Boolean { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt index dcee8a97..cd15fc5e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt index e8a0a913..0c6e6c33 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt @@ -536,6 +536,9 @@ class WorldLayout { fun getWeighting(x: Int, y: Int): List { val weighting = ArrayList() + 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? { + // 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() { override fun write(out: JsonWriter, value: WorldLayout?) { if (value == null) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index bbf221c1..68aca882 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt index ea59140e..37875886 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt @@ -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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index 75ed7071..8690d36e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -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() { override fun write(out: JsonWriter, value: ItemStack?) { val json = value?.toJson() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt index 54a3d7e6..4b48ae11 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt @@ -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() @@ -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): 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): 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt index 5111d088..dc6c0193 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt @@ -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> { var key: Any? = initialKey() ?: return ObjectIterators.emptyIterator() data class Pair(override val key: Any, override val value: Any) : Map.Entry @@ -100,6 +111,41 @@ operator fun Table.iterator(): Iterator> { } } +/** + * 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 { + 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(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 { return object : LuaFunction() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt index 635bc922..3e6fe998 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt @@ -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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt index eb48f7d4..0f3a7ebc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -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) { + val poly = originalPoly + position + + data class Entry(val poly: Poly, val center: Vector2d, var distance: Double = 0.0) : Comparable { + override fun compareTo(other: Entry): Int { + return distance.compareTo(other.distance) + } + } + + val tiles = ObjectArrayList() + + 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) { + } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt new file mode 100644 index 00000000..c8e4fc92 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt @@ -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 = 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 { + 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) { + 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) { + val actualOptions = options ?: tableOf() + actualOptions["line"] = tableOf(pos1, pos2) + returnBuffer.setTo(entityQueryImpl(self, actualOptions, predicate)) +} + +private inline fun createQueryFunction(self: World<*, *>) = luaFunction { pos1: Table, pos2OrRadius: Any, options: Table? -> + intermediateQueryFunction(self, pos1, pos2OrRadius, options, Predicate { it is T }) +} + +private inline fun 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(self) + callbacks["monsterQuery"] = createQueryFunction(self) // TODO + callbacks["npcQuery"] = createQueryFunction(self) // TODO + callbacks["itemDropQuery"] = createQueryFunction(self) + callbacks["playerQuery"] = createQueryFunction(self) + + callbacks["entityLineQuery"] = createLineQueryFunction(self) + callbacks["monsterLineQuery"] = createLineQueryFunction(self) // TODO + callbacks["npcLineQuery"] = createLineQueryFunction(self) // TODO + callbacks["itemDropLineQuery"] = createLineQueryFunction(self) + callbacks["playerLineQuery"] = createLineQueryFunction(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())) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt index 635dfef2..cfed6f3a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt @@ -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 } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt index b05f4d95..93b6654c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt @@ -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 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)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt index cd4e6f61..8e1ebe52 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt @@ -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, val t: KOptional, 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() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/vector/Vector2d.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/vector/Vector2d.kt index f6d4ca5b..eb129369 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/vector/Vector2d.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/vector/Vector2d.kt @@ -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) + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStopPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStopPacket.kt index 27ef0aef..203e5d9b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStopPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStopPacket.kt @@ -15,6 +15,7 @@ class WorldStopPacket(val reason: String = "") : IClientPacket { } override fun play(connection: ClientConnection) { + connection.resetOccupiedEntityIDs() TODO("Not yet implemented") } } \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index c85a7c9e..6229e987 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -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>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false): List> { + override fun applyTileModifications(modifications: Collection>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean): List> { 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) })) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt index 9fe75dc6..2c34f528 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index ec0f7e11..d8594f4f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -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 , T : Any> Stream>.binnedChoice(value: C): Opti } fun Collection.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}") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt index 5b041bd4..eb52eaa9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -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 MutableList.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 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt index 14c7c59e..7c811e8a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/EntityIndex.kt @@ -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 = Predicate { true }, withEdges: Boolean = true): List { - val entriesDirect = ArrayList() + fun query(rect: AABB, filter: Predicate = Predicate { true }, withEdges: Boolean = true): MutableList { + val entriesDirect = ObjectArrayList() 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 { + return query(AABB(pos.toDoubleVector(), pos.toDoubleVector() + Vector2d.POSITIVE_XY), Predicate { it is TileEntity && pos in it.occupySpaces }) as MutableList + } + fun iterate(rect: AABB, visitor: (AbstractEntity) -> Unit, withEdges: Boolean = true) { walk(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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt index f27b5b31..b735da12 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Raycasting.kt @@ -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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt index da4ec784..abd8dc22 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt index d2ecc1a5..08e443e8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 5bf173d6..bd78dc68 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -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, ChunkType : Chunk JsonElement): JsonElement { + return worldProperties[name] ?: orElse() + } + protected open fun setProperty0(key: String, value: JsonElement) { } @@ -417,6 +435,39 @@ abstract class World, ChunkType : Chunk): 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): 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 = Predicate { true }, tolerance: Double = 0.0): Boolean { return collide(with, filter).anyMatch { it.penetration >= tolerance } } @@ -433,6 +484,36 @@ abstract class World, ChunkType : Chunk, val average: Double) + + fun averageLiquidLevel(rect: AABB): LiquidLevel? { + val liquidLevels = Object2DoubleOpenHashMap>() + 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>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false): List> + companion object { private val LOGGER = LogManager.getLogger() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt index d4128f93..3b395b4b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt @@ -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 { + val splitLines: ImmutableList>> by lazy { val lines = ObjectArrayList>>(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 { + if (splitLines.isEmpty()) { return listOf(poly) } val split = ObjectArrayList() 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 { + val result = ObjectArrayList() + 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) } } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index 034962c7..6f0b469c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -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 { 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 { 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 { 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 { 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 { 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 { 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)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt index 973469b7..a8cd1157 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt @@ -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 { + return emptyList() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt index 39390dc4..55a90d2b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt index 262f3c3a..855916f3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt index cd13063c..8cf9a62e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemDropEntity.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/InspectableEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/InspectableEntity.kt new file mode 100644 index 00000000..538ed73a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/InspectableEntity.kt @@ -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 + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/ScriptedEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/ScriptedEntity.kt new file mode 100644 index 00000000..15d2f6b3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/api/ScriptedEntity.kt @@ -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 + + // Execute the given code directly in the underlying context, return nothing + // on failure. + fun evalScript(code: String): Array +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt index 99df6799..e56cd06f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerInventory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerInventory.kt index 1927abf6..522de3b1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerInventory.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerInventory.kt @@ -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()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt index 14f968db..fba096dd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/LoungeableObject.kt @@ -27,18 +27,30 @@ class LoungeableObject(config: Registry.Entry) : WorldObject(c isInteractive = true } - private val sitPositions = ArrayList() - 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>() - private val sitEffectEmitters = ObjectArraySet() - private var sitEmote: String? = null - private var sitDance: String? = null - private var sitArmorCosmeticOverrides: JsonObject = JsonObject() - private var sitCursorOverride: String? = null + val sitPositions = ArrayList() + + 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>() + val sitEffectEmitters = ObjectArraySet() + + 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 51e6bba5..8cc51729 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -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) : 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) : 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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt index 66312e0c..67ad8c2a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt @@ -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 = Collections.unmodifiableSet(EnumSet.copyOf(entries.filter { it.isSolidCollision }.toSet())) + val TILE: Set = Collections.unmodifiableSet(EnumSet.copyOf(entries.filter { it.isTileCollision }.toSet())) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt index 2bef7f2f..619a70f1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -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, val vertices: I if (isEmpty || !aabb.intersectWeak(other.aabb)) return null - val normals = ObjectOpenHashSet() + val normals = HashSet(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, val vertices: I normals.removeIf { it.dot(axis) == 0.0 } } - val intersections = ArrayList() + val intersections = ObjectArrayList() for (normal in normals) { val projectThis = project(normal) @@ -349,12 +350,27 @@ class Poly private constructor(val edges: ImmutableList, val vertices: I } } - if (intersections.isEmpty()) + if (intersections.isEmpty) return null return intersections.min() } + fun intersect(line: Line2d): Pair? { + val intersections = ObjectArrayList>() + + 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 diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/NetworkedElementTests.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/NetworkedElementTests.kt index 79ad53c9..06bc7567 100644 --- a/src/test/kotlin/ru/dbotthepony/kstarbound/test/NetworkedElementTests.kt +++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/NetworkedElementTests.kt @@ -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