More Lua bindings

This commit is contained in:
DBotThePony 2024-05-02 17:22:50 +07:00
parent 151571e8d0
commit 782fb29dbf
Signed by: DBot
GPG Key ID: DCC23B5715498507
19 changed files with 659 additions and 532 deletions

View File

@ -129,6 +129,15 @@ val color: TileColor = TileColor.DEFAULT
* Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target`
* `world.entityHandItem(id: EntityID, hand: String): String` now accepts `"secondary"` as `hand` argument (in addition to `"primary"`/`"alt"`)
* `world.containerConsume(id: EntityID, item: ItemDescriptor, exact: Boolean?): Boolean?` now accepts `exact` which forces exact match on item descriptor (default `false`)
* `world.containerStackItems(id: EntityID, items: ItemDescriptor): ItemDescriptor` now actually does what it says on tin, instead of being equal to `world.containerAddItems`
* **ONLY** for local entities, or when using native protocol (but why would you ever mutate containers over network in first place)
* Remote entities on legacy protocol will behave like `world.containerAddItems` has been called
* `world.containerItemApply(id: EntityID, items: ItemDescriptor, slot: Int): ItemDescriptor` is no longer equal to `world.containerSwapItemsNoCombine` and does what its docs say, but im not sure if it is ever makes sense
* Clarification - Original docs are not very clear, but what it does is it tries to put provided item into target slot _only_ if it contains item of same type (contains stackable). If slot is empty or item in slot can not be stacked with provided item, this function does nothing (and returns stack initially passed to the function)
* **ONLY** for local entities, or when using native protocol (but why would you ever mutate containers over network in first place)
* Remote entities on legacy protocol will try to simulate new behavior locally using item checks and remote `putItems` message
---------------

View File

@ -0,0 +1,13 @@
package ru.dbotthepony.kstarbound.item
class ContainerIterator(private val container: IContainer) : Iterator<ItemStack> {
private var index = 0
override fun hasNext(): Boolean {
return index < container.size
}
override fun next(): ItemStack {
return container[index++]
}
}

View File

@ -6,12 +6,13 @@ import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.ints.IntIterable
import it.unimi.dsi.fastutil.ints.IntIterator
import it.unimi.dsi.fastutil.ints.IntIterators
import it.unimi.dsi.fastutil.ints.IntList
import it.unimi.dsi.fastutil.ints.IntLists
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import java.util.random.RandomGenerator
interface IContainer {
interface IContainer : Iterable<ItemStack> {
var size: Int
operator fun get(index: Int): ItemStack
operator fun set(index: Int, value: ItemStack)
@ -114,20 +115,41 @@ interface IContainer {
}
}
// returns not inserted items
fun add(item: ItemStack, simulate: Boolean = false): ItemStack {
/**
* not inserted items -> slots touched
*/
fun add(item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
return put({ IntIterators.fromTo(0, size) }, item, simulate)
}
// returns not inserted items
fun put(slots: IntIterable, item: ItemStack, simulate: Boolean = false): ItemStack {
/**
* not inserted items -> slots touched
*/
fun stackWithExisting(item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
val validSlots = IntArrayList(size)
for (slot in 0 until size) {
if (this[slot].isNotEmpty) {
validSlots.add(slot)
}
}
return put(validSlots, item, simulate)
}
/**
* not inserted items -> slots touched
*/
fun put(slots: IntIterable, item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
return put(slots::iterator, item, simulate)
}
// returns not inserted items
fun put(slots: () -> IntIterator, item: ItemStack, simulate: Boolean = false): ItemStack {
/**
* not inserted items -> slots touched
*/
fun put(slots: () -> IntIterator, item: ItemStack, simulate: Boolean = false): Pair<ItemStack, IntList> {
val copy = item.copy()
val touched = IntArrayList()
var itr = slots.invoke()
while (itr.hasNext()) {
@ -139,6 +161,7 @@ interface IContainer {
val itemThere = this[slot]
if (itemThere.isStackable(copy)) {
touched.add(slot)
val newCount = (itemThere.size + copy.size).coerceAtMost(itemThere.maxStackSize)
val diff = newCount - itemThere.size
@ -148,7 +171,7 @@ interface IContainer {
itemThere.size += diff
if (copy.isEmpty)
return ItemStack.EMPTY
return ItemStack.EMPTY to touched
}
}
@ -163,23 +186,23 @@ interface IContainer {
val itemThere = this[slot]
if (itemThere.isEmpty) {
if (itemThere.isEmpty) {
if (copy.size > copy.maxStackSize) {
if (!simulate)
this[slot] = copy.copy(copy.maxStackSize)
touched.add(slot)
copy.size -= copy.maxStackSize
} else {
if (!simulate)
this[slot] = copy
if (copy.size > copy.maxStackSize) {
if (!simulate)
this[slot] = copy.copy(copy.maxStackSize)
return ItemStack.EMPTY
}
copy.size -= copy.maxStackSize
} else {
if (!simulate)
this[slot] = copy
return ItemStack.EMPTY to touched
}
}
}
return copy
return copy to touched
}
fun take(slot: Int, amount: Long): ItemStack {
@ -233,20 +256,20 @@ interface IContainer {
} else if (item.isNotEmpty && existingItem.isEmpty) {
// place into slot
// use put because item might be bigger than max stack size
return put(IntLists.singleton(slot), item)
return put(IntLists.singleton(slot), item).first
} else {
// If something is there, try to stack with it first. If we can't stack,
// then swap.
if (tryCombine && existingItem.isStackable(item)) {
return put(IntLists.singleton(slot), item)
return put(IntLists.singleton(slot), item).first
} else {
this[slot] = ItemStack.EMPTY
val slots = IntArrayList(IntIterators.fromTo(0, size))
slots.removeInt(slot)
slots.add(0, slot)
val remaining = put(slots, item)
val remaining = put(slots, item).first
if (remaining.isNotEmpty && this[slot].isStackable(remaining)) {
// damn
@ -258,6 +281,14 @@ interface IContainer {
}
}
fun combineAt(slot: Int, item: ItemStack): ItemStack {
if (this[slot].isEmpty || !this[slot].isStackable(item)) {
return item
} else {
return put(IntLists.singleton(slot), item).first
}
}
fun take(descriptor: ItemDescriptor, exactMatch: Boolean = false, simulate: Boolean = false): Boolean {
var toTake = descriptor.count
@ -387,7 +418,7 @@ interface IContainer {
val lost = ArrayList<ItemStack>()
for (i in size until read.size) {
val remaining = add(read[i])
val remaining = add(read[i]).first
if (remaining.isNotEmpty) {
lost.add(remaining)
@ -408,4 +439,8 @@ interface IContainer {
return emptyList()
}
}
override fun iterator(): Iterator<ItemStack> {
return ContainerIterator(this)
}
}

View File

@ -41,6 +41,10 @@ object ItemRegistry {
val isNotEmpty: Boolean
get() = !isEmpty
// for Lua scripts
val nameOrNull: String?
get() = if (isEmpty) null else name
val directory = file?.computeDirectory() ?: "/"
}

View File

@ -366,6 +366,14 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para
return createDescriptor().toJson()
}
fun toTable(allocator: TableFactory): Table? {
if (isEmpty) {
return null
}
return createDescriptor().toTable(allocator)
}
class Adapter(gson: Gson) : TypeAdapter<ItemStack>() {
override fun write(out: JsonWriter, value: ItemStack?) {
val json = value?.toJson()

View File

@ -156,6 +156,26 @@ fun TableFactory.tableOf(vararg values: Any?): Table {
return table
}
fun TableFactory.tableOf(vararg values: Int): Table {
val table = newTable(values.size, 0)
for ((i, v) in values.withIndex()) {
table[i + 1L] = v.toLong()
}
return table
}
fun TableFactory.tableOf(vararg values: Long): Table {
val table = newTable(values.size, 0)
for ((i, v) in values.withIndex()) {
table[i + 1L] = v
}
return table
}
fun TableFactory.tableMapOf(vararg values: Pair<Any, Any?>): Table {
val table = newTable(0, values.size)

View File

@ -4,6 +4,7 @@ import org.classdump.luna.ByteString
import org.classdump.luna.LuaRuntimeException
import org.classdump.luna.Table
import org.classdump.luna.runtime.ExecutionContext
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
@ -15,17 +16,23 @@ 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.set
import ru.dbotthepony.kstarbound.lua.tableMapOf
import ru.dbotthepony.kstarbound.lua.tableOf
import ru.dbotthepony.kstarbound.lua.toAABB
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.lua.userdata.LuaFuture
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.stream
import ru.dbotthepony.kstarbound.util.random.shuffle
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.World
@ -35,8 +42,11 @@ 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.InteractiveEntity
import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.tile.ContainerObject
import ru.dbotthepony.kstarbound.world.entities.tile.LoungeableObject
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.Poly
@ -356,4 +366,241 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn
returnBuffer.setTo(tableOf(*entity.portrait(ActorEntity.PortraitMode.entries.valueOf(mode.decode())).map { from(it.toJson()) }.toTypedArray()))
}
callbacks["entityHandItem"] = luaFunction { id: Number, hand: ByteString ->
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
when (val gethand = hand.decode().lowercase()) {
"primary" -> returnBuffer.setTo(entity.primaryHandItem.entry.nameOrNull)
"alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.entry.nameOrNull)
else -> throw LuaRuntimeException("Unknown tool hand $gethand")
}
}
callbacks["entityHandItemDescriptor"] = luaFunction { id: Number, hand: ByteString ->
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
when (val gethand = hand.decode().lowercase()) {
"primary" -> returnBuffer.setTo(entity.primaryHandItem.toTable(this))
"alt", "secondary" -> returnBuffer.setTo(entity.secondaryHandItem.toTable(this))
else -> throw LuaRuntimeException("Unknown tool hand $gethand")
}
}
callbacks["entityUniqueId"] = luaFunction { id: Number ->
returnBuffer.setTo(self.entities[id.toInt()]?.uniqueID?.get())
}
callbacks["getObjectParameter"] = luaFunction { id: Number, parameter: ByteString, defaultValue: Any? ->
// FIXME: this is stupid (defaultValue is ignored when we lookup parameter on non existing entity),
// but we must retain original behavior
val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo()
val result = entity.lookupProperty(parameter.decode())
if (result.isJsonNull) {
returnBuffer.setTo(defaultValue)
} else {
returnBuffer.setTo(from(result))
}
}
callbacks["getNpcScriptParameter"] = luaStub("getNpcScriptParameter")
callbacks["objectSpaces"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo(tableOf())
returnBuffer.setTo(tableOf(*entity.occupySpaces.map { from(it - entity.tilePosition) }.toTypedArray()))
}
callbacks["farmableStage"] = luaStub("farmableStage")
callbacks["containerSize"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.items.size)
}
callbacks["containerClose"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(false)
// FIXME: this doesn't get networked if called on client
// (AND this is the reason why in multiplayer player can't see chest/container open animations when
// other players open them)
entity.closeContainer()
returnBuffer.setTo(true)
}
callbacks["containerOpen"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(false)
// FIXME: this doesn't get networked if called on client
// (AND this is the reason why in multiplayer player can't see chest/container open animations when
// other players open them)
entity.openContainer()
returnBuffer.setTo(true)
}
callbacks["containerItems"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(tableOf(*entity.items.filter { it.isNotEmpty }.map { it.toTable(this) }.toTypedArray()))
}
callbacks["containerItemAt"] = luaFunction { id: Number, index: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
if (index.toInt() < entity.items.size)
returnBuffer.setTo(entity.items[index.toInt()].toTable(this))
}
callbacks["containerConsume"] = luaFunction { id: Number, desc: Any, exactMatch: Boolean? ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.takeItem(ItemDescriptor(desc), exact = exactMatch ?: false).getNow(null))
}
callbacks["containerConsumeAt"] = luaFunction { id: Number, slot: Number, amount: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.takeItemAt(slot.toInt(), amount.toLong()).getNow(null))
}
callbacks["containerAvailable"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(entity.items.take(ItemDescriptor(desc), simulate = true))
}
// why we have containerItems + containerTakeAll, when we could have containerItems + containerClear?????????
callbacks["containerTakeAll"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
returnBuffer.setTo(tableOf(*entity.clearContainer().getNow(listOf()).map { it.toTable(this) }.toTypedArray()))
}
callbacks["containerTakeAt"] = luaFunction { id: Number, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
if (slot.toInt() < entity.items.size)
returnBuffer.setTo(entity.takeItemAt(slot.toInt()).getNow(null))
}
callbacks["containerTakeNumItemsAt"] = luaFunction { id: Number, slot: Number, amount: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
if (slot.toInt() < entity.items.size)
returnBuffer.setTo(entity.takeItemAt(slot.toInt(), amount.toLong()).getNow(null))
}
callbacks["containerItemsCanFit"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
val item = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(item.size - entity.items.add(item, simulate = true).first.size)
}
callbacks["containerItemsFitWhere"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo()
val item = ItemDescriptor(desc).build(random = lua.random)
val (leftover, touched) = entity.items.add(item, simulate = true)
returnBuffer.setTo(tableMapOf(
"leftover" to leftover.size,
"slots" to tableOf(*touched.toIntArray())
))
}
callbacks["containerAddItems"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.addItems(build).getNow(build)?.toTable(this))
}
callbacks["containerStackItems"] = luaFunction { id: Number, desc: Any ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.stackWithExisting(build).getNow(build)?.toTable(this))
}
callbacks["containerPutItemsAt"] = luaFunction { id: Number, desc: Any, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.putItems(slot.toInt(), build).getNow(build)?.toTable(this))
}
callbacks["containerSwapItems"] = luaFunction { id: Number, desc: Any, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.swapItems(slot.toInt(), build, tryCombine = true).getNow(build)?.toTable(this))
}
callbacks["containerSwapItemsNoCombine"] = luaFunction { id: Number, desc: Any, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.swapItems(slot.toInt(), build, tryCombine = false).getNow(build)?.toTable(this))
}
callbacks["containerItemApply"] = luaFunction { id: Number, desc: Any, slot: Number ->
val entity = self.entities[id.toInt()] as? ContainerObject ?: return@luaFunction returnBuffer.setTo(desc)
val build = ItemDescriptor(desc).build(random = lua.random)
returnBuffer.setTo(entity.combineItems(slot.toInt(), build).getNow(build)?.toTable(this))
}
callbacks["callScriptedEntity"] = luaFunctionN("callScriptedEntity") {
val id = it.nextInteger()
val function = it.nextString().decode()
val entity = self.entities[id.toInt()] ?: throw LuaRuntimeException("Entity with ID $id does not exist")
if (entity !is ScriptedEntity)
throw LuaRuntimeException("$entity is not scripted entity")
if (entity.isRemote)
throw LuaRuntimeException("$entity is not owned by this side")
returnBuffer.setToContentsOf(entity.callScript(function, *it.copyRemaining()))
}
callbacks["findUniqueEntity"] = luaStub("findUniqueEntity")
callbacks["sendEntityMessage"] = luaFunctionN("sendEntityMessage") {
val id = it.nextAny()
val func = it.nextString().decode()
if (id is Number) {
val entityID = id.toInt()
returnBuffer.setTo(LuaFuture(
future = self.dispatchEntityMessage(self.connectionID, entityID, func, it.copyRemaining()
.stream().map { toJsonFromLua(it) }.collect(JsonArrayCollector)).thenApply { from(it) },
isLocal = Connection.connectionForEntityID(entityID) == self.connectionID
))
} else {
id as ByteString
val entityID = id.decode()
val findAlreadyLoaded = self.entities.values.find { it.uniqueID.get() == entityID }
val isLocal = if (findAlreadyLoaded == null)
self.isServer
else
!findAlreadyLoaded.isRemote
returnBuffer.setTo(LuaFuture(
future = self.dispatchEntityMessage(self.connectionID, entityID, func, it.copyRemaining()
.stream().map { toJsonFromLua(it) }.collect(JsonArrayCollector)).thenApply { from(it) },
isLocal = isLocal
))
}
}
callbacks["loungeableOccupied"] = luaStub("loungeableOccupied")
callbacks["isMonster"] = luaStub("isMonster")
callbacks["monsterType"] = luaStub("monsterType")
callbacks["npcType"] = luaStub("npcType")
callbacks["stagehandType"] = luaStub("stagehandType")
callbacks["isNpc"] = luaStub("isNpc")
callbacks["isEntityInteractive"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()]
if (entity is InteractiveEntity)
returnBuffer.setTo(entity.isInteractive)
}
callbacks["entityMouthPosition"] = luaFunction { id: Number ->
val entity = self.entities[id.toInt()] ?: return@luaFunction returnBuffer.setTo()
// original entine returns non nil only for "Chatty entity"
returnBuffer.setTo(from(entity.mouthPosition))
}
callbacks["entityTypeName"] = luaStub("entityTypeName")
}

View File

@ -0,0 +1,76 @@
package ru.dbotthepony.kstarbound.lua.userdata
import org.classdump.luna.Table
import org.classdump.luna.Userdata
import org.classdump.luna.impl.ImmutableTable
import ru.dbotthepony.kstarbound.lua.luaFunction
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
/**
* [isLocal] tells us whenever result() should block current thread if [future] is not yet fulfilled
* (to retain original behavior for "locally async" calls)
*
* god damn it.
*/
class LuaFuture(val future: CompletableFuture<out Any?>, val isLocal: Boolean) : Userdata<CompletableFuture<out Any?>>() {
override fun getMetatable(): Table {
return metadata
}
override fun setMetatable(mt: Table?): Table {
throw UnsupportedOperationException()
}
override fun getUserValue(): CompletableFuture<out Any?> {
return future
}
override fun setUserValue(value: CompletableFuture<out Any?>?): CompletableFuture<out Any?> {
throw UnsupportedOperationException()
}
companion object {
private val metadata = ImmutableTable.Builder()
.add("finished", luaFunction { self: LuaFuture ->
returnBuffer.setTo(self.future.isDone)
})
.add("succeeded", luaFunction { self: LuaFuture ->
returnBuffer.setTo(!self.future.isCompletedExceptionally)
})
.add("failed", luaFunction { self: LuaFuture ->
returnBuffer.setTo(self.future.isCompletedExceptionally)
})
.add("result", luaFunction { self: LuaFuture ->
try {
if (self.future.isCompletedExceptionally) {
returnBuffer.setTo()
} else if (self.isLocal) {
returnBuffer.setTo(self.future.join())
} else {
returnBuffer.setTo(self.future.getNow(null))
}
} catch (err: CompletionException) {
returnBuffer.setTo()
} catch (err: CancellationException) {
returnBuffer.setTo()
}
})
.add("error", luaFunction { self: LuaFuture ->
// this is slow, but we can't get Exception out of CompletableFuture without latter throwing former
try {
if (self.isLocal) {
returnBuffer.setTo(self.future.join())
} else {
returnBuffer.setTo(self.future.getNow(null))
}
} catch (err: CompletionException) {
returnBuffer.setTo(err.message ?: "internal error")
} catch (err: CancellationException) {
returnBuffer.setTo(err.message ?: "internal error")
}
})
.build()
}
}

View File

@ -45,24 +45,20 @@ class EntityMessagePacket(val entity: Either<Int, String>, val message: String,
}
private fun handle(connection: Connection, world: World<*, *>) {
val entity = if (entity.isLeft) {
world.entities[entity.left()]
val future = if (entity.isLeft) {
world.dispatchEntityMessage(connection.connectionID, entity.left(), message, arguments)
} else {
world.entities.values.firstOrNull { it.uniqueID.get() == entity.right() }
world.dispatchEntityMessage(connection.connectionID, entity.right(), message, arguments)
}
if (entity == null) {
connection.send(EntityMessageResponsePacket(Either.left("No such entity ${this@EntityMessagePacket.entity}"), id))
} else {
entity.dispatchMessage(connection.connectionID, message, arguments)
.thenAccept(Consumer {
connection.send(EntityMessageResponsePacket(Either.right(it), id))
})
.exceptionally(Function {
connection.send(EntityMessageResponsePacket(Either.left(it.message ?: "Internal server error"), id))
null
})
}
future
.thenAccept(Consumer {
connection.send(EntityMessageResponsePacket(Either.right(it), id))
})
.exceptionally(Function {
connection.send(EntityMessageResponsePacket(Either.left(it.message ?: "Internal server error"), id))
null
})
}
override fun play(connection: ServerConnection) {

View File

@ -14,6 +14,7 @@ import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.io.DataInputStream
import java.io.DataOutputStream
@ -36,7 +37,7 @@ class EntityMessageResponsePacket(val response: Either<String, JsonElement>, val
if (message != null) {
if (response.isLeft) {
message.completeExceptionally(AbstractEntity.MessageCallException(response.left()))
message.completeExceptionally(World.MessageCallException(response.left()))
} else {
message.complete(response.right())
}
@ -50,7 +51,7 @@ class EntityMessageResponsePacket(val response: Either<String, JsonElement>, val
if (message != null) {
if (response.isLeft) {
message.completeExceptionally(AbstractEntity.MessageCallException(response.left()))
message.completeExceptionally(World.MessageCallException(response.left()))
} else {
message.complete(response.right())
}

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.EntityInteractResultPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
@ -28,14 +29,13 @@ class EntityInteractPacket(val request: InteractRequest, val id: UUID) : IServer
override fun play(connection: ServerConnection) {
if (request.target >= 0) {
connection.enqueue {
connection.send(EntityInteractResultPacket(entities[request.target]?.interact(request) ?: InteractAction.NONE, id, request.source))
connection.send(EntityInteractResultPacket((entities[request.target] as? InteractiveEntity)?.interact(request) ?: InteractAction.NONE, id, request.source))
}
} else {
val other = connection.server.channels.connectionByID(Connection.connectionForEntityID(request.target)) ?: throw IllegalArgumentException("No such connection ID ${Connection.connectionForEntityID(request.target)} for EntityInteractPacket")
if (other == connection) {
throw IllegalStateException("Attempt to interact with own entity through server?")
}
if (other == connection)
throw IllegalArgumentException("Attempt to interact with own entity through server?")
other.send(this)
}

View File

@ -1,442 +0,0 @@
package ru.dbotthepony.kstarbound.util
import java.util.*
import java.util.concurrent.Callable
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Delayed
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.FutureTask
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.LockSupport
import java.util.function.Consumer
import kotlin.NoSuchElementException
import kotlin.collections.ArrayList
private fun <E : Comparable<E>> LinkedList<E>.enqueue(value: E) {
if (isEmpty()) {
add(value)
} else if (first >= value) {
addFirst(value)
} else if (last <= value) {
addLast(value)
} else {
val iterator = listIterator()
while (iterator.hasNext()) {
val i = iterator.next()
if (i >= value) {
iterator.previous()
iterator.add(value)
break
}
}
}
}
/**
* [ScheduledExecutorService] which act as a mailbox, [executeQueuedTasks] must be called from main thread.
*
* [submit], [execute], etc can be called on any thread. If any of enqueueing methods are called on the same thread
* as where [executeQueuedTasks] was called, executes provided lambda immediately and returns completed future.
*/
class MailboxExecutorService(@Volatile var thread: Thread = Thread.currentThread()) : ScheduledExecutorService {
private val futureQueue = ConcurrentLinkedQueue<FutureTask<*>>()
private val timers = LinkedList<Timer<*>>()
private val repeatableTimers = LinkedList<RepeatableTimer>()
@Volatile
private var isShutdown = false
@Volatile
private var isTerminated = false
private val timeOrigin = JVMClock()
var exceptionHandler: Consumer<Throwable>? = null
private inner class Timer<T>(task: Callable<T>, val executeAt: Long) : FutureTask<T>(task), ScheduledFuture<T> {
override fun compareTo(other: Delayed): Int {
return getDelay(TimeUnit.NANOSECONDS).compareTo(other.getDelay(TimeUnit.NANOSECONDS))
}
override fun getDelay(unit: TimeUnit): Long {
return unit.convert(executeAt, TimeUnit.NANOSECONDS) - timeOrigin.nanos
}
}
private data class CompletedFuture<T>(private val value: T) : Future<T> {
override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
return false
}
override fun isCancelled(): Boolean {
return false
}
override fun isDone(): Boolean {
return true
}
override fun get(): T {
return value
}
override fun get(timeout: Long, unit: TimeUnit): T {
return value
}
companion object {
val VOID = CompletedFuture(Unit)
}
}
private inner class RepeatableTimer(
task: Runnable,
initialDelay: Long,
val period: Long,
val fixedDelay: Boolean,
): FutureTask<Unit>({ task.run() }), ScheduledFuture<Unit> {
var next = initialDelay
private set
public override fun runAndReset(): Boolean {
if (fixedDelay) {
next += period
return super.runAndReset()
} else {
try {
return super.runAndReset()
} finally {
next += period
}
}
}
override fun compareTo(other: Delayed): Int {
return getDelay(TimeUnit.NANOSECONDS).compareTo(other.getDelay(TimeUnit.NANOSECONDS))
}
override fun getDelay(unit: TimeUnit): Long {
return unit.convert(next, TimeUnit.NANOSECONDS) - timeOrigin.nanos
}
}
fun isSameThread(): Boolean {
return Thread.currentThread() === thread
}
fun executeQueuedTasks() {
thread = Thread.currentThread()
if (isShutdown) {
if (!isTerminated) {
isTerminated = true
futureQueue.forEach {
it.cancel(false)
}
futureQueue.clear()
timers.clear()
repeatableTimers.clear()
return
}
}
var next = futureQueue.poll()
while (next != null) {
if (isTerminated) return
next.run()
Thread.interrupted()
try {
next.get()
} catch (err: ExecutionException) {
exceptionHandler?.accept(err)
}
next = futureQueue.poll()
}
while (!timers.isEmpty()) {
if (isTerminated) return
val first = timers.first
if (first.isCancelled) {
timers.removeFirst()
} else if (first.executeAt <= timeOrigin.nanos) {
first.run()
Thread.interrupted()
try {
first.get()
} catch (err: ExecutionException) {
exceptionHandler?.accept(err)
}
timers.removeFirst()
} else {
break
}
}
if (repeatableTimers.isNotEmpty()) {
val executed = LinkedList<RepeatableTimer>()
while (repeatableTimers.isNotEmpty()) {
if (isTerminated) return
val first = repeatableTimers.first
if (first.isDone) {
repeatableTimers.removeFirst()
} else if (first.next <= timeOrigin.nanos) {
if (first.runAndReset()) {
executed.add(first)
}
repeatableTimers.removeFirst()
} else {
break
}
}
executed.forEach { repeatableTimers.enqueue(it) }
}
}
override fun execute(command: Runnable) {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
command.run()
} else {
futureQueue.add(FutureTask(command, Unit))
LockSupport.unpark(thread)
}
}
override fun shutdown() {
isShutdown = true
}
override fun shutdownNow(): List<Runnable> {
if (isTerminated) return listOf()
isShutdown = true
isTerminated = true
val result = ArrayList<Runnable>()
futureQueue.forEach {
it.cancel(false)
result.add(it)
}
futureQueue.clear()
timers.forEach { it.cancel(false) }
repeatableTimers.forEach { it.cancel(false) }
timers.clear()
repeatableTimers.clear()
return result
}
override fun isShutdown(): Boolean {
return isShutdown
}
override fun isTerminated(): Boolean {
return isTerminated
}
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
throw UnsupportedOperationException()
}
override fun <T : Any?> submit(task: Callable<T>): Future<T> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) return CompletedFuture(task.call())
return FutureTask(task).also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun <T : Any?> submit(task: Runnable, result: T): Future<T> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { task.run(); return CompletedFuture(result) }
return FutureTask { task.run(); result }.also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun submit(task: Runnable): Future<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { task.run(); return CompletedFuture.VOID }
return FutureTask { task.run() }.also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun <T : Any?> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.map { CompletedFuture(it.call()) }
} else {
return tasks.map { submit(it) }.onEach { it.get() }
}
}
override fun <T : Any?> invokeAll(
tasks: Collection<Callable<T>>,
timeout: Long,
unit: TimeUnit
): List<Future<T>> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.map { CompletedFuture(it.call()) }
} else {
return tasks.map { submit(it) }.onEach { it.get(timeout, unit) }
}
}
override fun <T : Any?> invokeAny(tasks: Collection<Callable<T>>): T {
if (tasks.isEmpty())
throw NoSuchElementException("Provided task list is empty")
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.first().call()
} else {
return submit(tasks.first()).get()
}
}
override fun <T : Any?> invokeAny(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): T {
if (tasks.isEmpty())
throw NoSuchElementException("Provided task list is empty")
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.first().call()
} else {
return submit(tasks.first()).get(timeout, unit)
}
}
fun <V> join(future: Future<V>): V {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (!isSameThread())
return future.get()
while (!future.isDone) {
executeQueuedTasks()
LockSupport.parkNanos(1_000_000L)
}
return future.get()
}
override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
val timer = Timer({ command.run() }, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit))
if (isSameThread() && delay <= 0L) {
timer.run()
Thread.interrupted()
} else if (isSameThread()) {
timers.enqueue(timer)
} else {
execute {
if (timer.isCancelled) {
// do nothing
} else if (timer.executeAt <= timeOrigin.nanos) {
timer.run()
Thread.interrupted()
} else {
timers.enqueue(timer)
}
}
}
return timer
}
override fun <V : Any?> schedule(callable: Callable<V>, delay: Long, unit: TimeUnit): ScheduledFuture<V> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
val timer = Timer(callable, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit))
if (isSameThread() && delay <= 0L) {
timer.run()
Thread.interrupted()
} else if (isSameThread()) {
timers.enqueue(timer)
} else {
execute {
if (timer.isCancelled) {
// do nothing
} else if (timer.executeAt <= timeOrigin.nanos) {
timer.run()
Thread.interrupted()
} else {
timers.enqueue(timer)
}
}
}
return timer
}
override fun scheduleAtFixedRate(
command: Runnable,
initialDelay: Long,
period: Long,
unit: TimeUnit
): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
return RepeatableTimer(
command,
timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit),
TimeUnit.NANOSECONDS.convert(period, unit), true)
.also {
execute {
if (it.isCancelled) {
// do nothing
} else {
repeatableTimers.enqueue(it)
}
}
}
}
override fun scheduleWithFixedDelay(
command: Runnable,
initialDelay: Long,
delay: Long,
unit: TimeUnit
): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
return RepeatableTimer(
command,
timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit),
TimeUnit.NANOSECONDS.convert(delay, unit), false)
.also {
execute {
if (it.isCancelled) {
// do nothing
} else {
repeatableTimers.enqueue(it)
}
}
}
}
}

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
@ -14,6 +15,7 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kstarbound.Registry
@ -32,6 +34,7 @@ import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.DamageNotificationPacket
import ru.dbotthepony.kstarbound.network.packets.DamageRequestPacket
import ru.dbotthepony.kstarbound.network.packets.EntityMessagePacket
import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
@ -582,6 +585,29 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
abstract fun remote(connectionID: Int): Connection?
// doesn't write stacktrace
class MessageCallException(message: String) : RuntimeException(message, null, true, false)
fun dispatchEntityMessage(sourceConnection: Int, entityID: Int, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
val connectionID = Connection.connectionForEntityID(entityID)
if (connectionID == this.connectionID) {
val entity = entities[entityID] ?: return CompletableFuture.failedFuture(MessageCallException("No such entity $entityID"))
return entity.tryHandleMessage(sourceConnection, message, arguments)
} else {
val connection = remote(connectionID) ?: return CompletableFuture.failedFuture(NoSuchElementException("Can't dispatch entity message, no such connection $connectionID"))
val future = CompletableFuture<JsonElement>()
val uuid = UUID(random.nextLong(), random.nextLong())
pendingEntityMessages.put(uuid, future)
connection.send(EntityMessagePacket(Either.left(entityID), message, arguments, uuid, sourceConnection))
return future
}
}
fun dispatchEntityMessage(sourceConnection: Int, entityID: String, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
TODO()
}
// this *could* have been divided into per-entity map and beheaded world's map
// but we can't, because response packets contain only message UUID, and don't contain entity ID
val pendingEntityMessages: Cache<UUID, CompletableFuture<JsonElement>> = Caffeine.newBuilder()

View File

@ -9,6 +9,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.nullable
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient
@ -34,7 +35,6 @@ import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.EntityIndex
import ru.dbotthepony.kstarbound.world.TileRayFilter
@ -42,8 +42,11 @@ import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.castRay
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataOutputStream
import java.util.PriorityQueue
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import java.util.function.Predicate
@ -81,9 +84,6 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
LOGGER.error("Error while executing queued task on $this", it)
}
var mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger }
private set
private var innerWorld: World<*, *>? = null
val world: World<*, *>
@ -201,9 +201,6 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
check(!world.entities.containsKey(entityID)) { "Duplicate entity ID: $entityID" }
if (mailbox.isShutdown)
mailbox = MailboxExecutorService().also { it.exceptionHandler = exceptionLogger }
innerWorld = world
world.entities[entityID] = this
world.entityList.add(this)
@ -227,7 +224,9 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
removalReason = reason
mailbox.shutdownNow()
scheduledTasks.forEach { it.cancel(false) }
scheduledTasks.clear()
check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" }
world.entityList.remove(this)
@ -255,12 +254,24 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
}
}
open fun interact(request: InteractRequest): InteractAction {
return InteractAction.NONE
// for fast check on completed tasks
// This is necessary to cancel tasks when we are removed, so we don't reference ourselves
// in event loop after we have been removed
private val scheduledTasks = PriorityQueue<ScheduledFuture<*>>()
protected fun scheduleInTicks(ticks: Int, action: Runnable) {
scheduledTasks.add(world.eventLoop.schedule(action, ticks * Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS))
}
protected fun schedule(time: Long, unit: TimeUnit, action: Runnable) {
scheduledTasks.add(world.eventLoop.schedule(action, time, unit))
}
var isRemote: Boolean = false
open val mouthPosition: Vector2d
get() = position
private fun isDamageAuthoritative(target: AbstractEntity): Boolean {
// Damage manager is authoritative if either one of the entities is
// masterOnly, OR the manager is server-side and both entities are
@ -371,7 +382,9 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
open fun damagedOther(notification: DamageNotificationPacket) {}
open fun tick(delta: Double) {
mailbox.executeQueuedTasks()
while (scheduledTasks.isNotEmpty() && scheduledTasks.peek().isDone) {
scheduledTasks.poll()
}
if (networkGroup.upstream.isInterpolating) {
networkGroup.upstream.tickInterpolation(delta)
@ -467,30 +480,26 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
return null
}
// doesn't write stacktrace
class MessageCallException(message: String) : RuntimeException(message, null, true, false)
fun tryHandleMessage(sourceConnection: Int, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
val response = try {
handleMessage(sourceConnection, message, arguments)
} catch (err: Throwable) {
return CompletableFuture.failedFuture(err)
}
if (response == null) {
return CompletableFuture.failedFuture(World.MessageCallException("Message '$message' was not handled"))
} else {
return CompletableFuture.completedFuture(response)
}
}
fun dispatchMessage(sourceConnection: Int, message: String, arguments: JsonArray): CompletableFuture<JsonElement> {
if (isRemote) {
val connection = world.remote(connectionID) ?: return CompletableFuture.failedFuture(NoSuchElementException("Can't dispatch entity message, no such connection $connectionID"))
val future = CompletableFuture<JsonElement>()
val uuid = UUID(world.random.nextLong(), world.random.nextLong())
world.pendingEntityMessages.put(uuid, future)
connection.send(EntityMessagePacket(Either.left(entityID), message, arguments, uuid, sourceConnection))
return future
} else {
val response = try {
handleMessage(sourceConnection, message, arguments)
} catch (err: Throwable) {
return CompletableFuture.failedFuture(err)
}
if (response == null) {
return CompletableFuture.failedFuture(MessageCallException("Message '$message' was not handled"))
} else {
return CompletableFuture.completedFuture(response)
}
if (!isRemote) {
return tryHandleMessage(sourceConnection, message, arguments)
}
return world.dispatchEntityMessage(sourceConnection, entityID, message, arguments)
}
open fun render(client: StarboundClient, layers: LayeredRenderer) {

View File

@ -0,0 +1,14 @@
package ru.dbotthepony.kstarbound.world.entities.api
import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest
// sigh
interface InteractiveEntity {
val isInteractive: Boolean
get() = false
fun interact(request: InteractRequest): InteractAction {
return InteractAction.NONE
}
}

View File

@ -0,0 +1,4 @@
package ru.dbotthepony.kstarbound.world.entities.api
interface LoungeableEntity {
}

View File

@ -5,7 +5,6 @@ import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.ints.IntIterators
import it.unimi.dsi.fastutil.ints.IntLists
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
@ -26,6 +25,8 @@ import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.item.IContainer
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
@ -38,10 +39,11 @@ import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.concurrent.CompletableFuture
import java.util.random.RandomGenerator
class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) {
var opened by networkedSignedInt().also { networkGroup.upstream.add(it) }
var openFrameIndex by networkedSignedInt().also { networkGroup.upstream.add(it) }
var isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) }
var craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear }
val items = Container(lookupProperty("slotCount").asInt).also { networkGroup.upstream.add(it) }
@ -65,6 +67,25 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
lookupProperty("itemAgeMultiplier") { JsonPrimitive(1.0) }.asDouble
}.also { parametersLazies.add(it) }
private var openCount = 0
fun openContainer() {
if (openCount++ == 0) {
openFrameIndex = lookupProperty("openFrameIndex") { JsonPrimitive(2) }.asInt
scheduleInTicks(lookupProperty("autoCloseCooldown").asInt) {
closeContainer()
}
}
}
fun closeContainer() {
if (--openCount <= 0) {
openCount = 0
openFrameIndex = 0
}
}
override fun tick(delta: Double) {
super.tick(delta)
@ -106,7 +127,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
override fun deserialize(data: JsonObject) {
super.deserialize(data)
opened = data.get("opened", 0)
openFrameIndex = data.get("opened", 0)
isCrafting = data.get("crafting", false)
craftingProgress = data.get("craftingProgress", 0.0)
isInitialized = data.get("initialized", true)
@ -118,7 +139,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
// required by original engine
data["currentState"] = 0
data["opened"] = opened
data["opened"] = openFrameIndex
data["crafting"] = isCrafting
data["craftingProgress"] = craftingProgress
data["initialized"] = isInitialized
@ -150,7 +171,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
LOGGER.error("Unknown treasure pool $get! Can't generate container contents at $tilePosition.")
} else {
for (item in treasurePool.value.evaluate(random, level)) {
val leftover = items.add(item)
val leftover = items.add(item).first
if (leftover.isNotEmpty) {
LOGGER.warn("Tried to overfill container at $tilePosition")
@ -189,7 +210,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
}
override fun handleMessage(connection: Int, message: String, arguments: JsonArray): JsonElement? {
return when (message.lowercase()) {
return when (message.lowercase()) { // because legacy protocol allows it
"startcrafting" -> {
startCrafting()
JsonNull.INSTANCE
@ -206,12 +227,14 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
}
// returns not inserted items
"additems" -> items.add(ItemDescriptor(arguments[0]).build()).toJson()
"putitems" -> items.put(IntLists.singleton(arguments[0].asInt), ItemDescriptor(arguments[1]).build()).toJson()
"additems" -> items.add(ItemDescriptor(arguments[0]).build()).first.toJson()
"stackitems" -> items.stackWithExisting(ItemDescriptor(arguments[0]).build()).first.toJson()
"putitems" -> items.put(IntLists.singleton(arguments[0].asInt), ItemDescriptor(arguments[1]).build()).first.toJson()
"takeitems" -> items.take(arguments[0].asInt, arguments[1].asLong).toJson()
"swapitems" -> items.swap(arguments[0].asInt, ItemDescriptor(arguments[1]).build(), if (arguments.size() >= 3) arguments[2].asBoolean else true).toJson()
"combineitems" -> items.combineAt(arguments[0].asInt, ItemDescriptor(arguments[1]).build()).toJson()
"applyaugment" -> TODO("applyaugment")
"consumeitems" -> JsonPrimitive(items.take(ItemDescriptor(arguments[0])))
"consumeitems" -> JsonPrimitive(items.take(ItemDescriptor(arguments[0]), exactMatch = arguments.get(1, false)))
"consumeitemsat" -> JsonPrimitive(items.takeExact(arguments[0].asInt, arguments[1].asLong))
"clearcontainer" -> {
val result = JsonArray()
@ -230,6 +253,88 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
}
}
fun addItems(item: ItemStack): CompletableFuture<ItemStack> {
if (isRemote) {
return dispatchMessage(world.connectionID, "addItems", jsonArrayOf(item.toJson())).thenApply { ItemDescriptor(it).build() }
} else {
return CompletableFuture.completedFuture(items.add(item).first)
}
}
fun putItems(slot: Int, item: ItemStack): CompletableFuture<ItemStack> {
if (isRemote) {
return dispatchMessage(world.connectionID, "putItems", jsonArrayOf(item.toJson(), slot)).thenApply { ItemDescriptor(it).build() }
} else {
return CompletableFuture.completedFuture(items.put(IntLists.singleton(slot), item).first)
}
}
fun stackWithExisting(item: ItemStack): CompletableFuture<ItemStack> {
if (isRemote) {
if (world.remote(connectionID)!!.isLegacy) {
return dispatchMessage(world.connectionID, "addItems", jsonArrayOf(item.toJson())).thenApply { ItemDescriptor(it).build() }
} else {
return dispatchMessage(world.connectionID, "stackItems", jsonArrayOf(item.toJson())).thenApply { ItemDescriptor(it).build() }
}
} else {
return CompletableFuture.completedFuture(items.stackWithExisting(item).first)
}
}
fun swapItems(slot: Int, item: ItemStack, tryCombine: Boolean = true): CompletableFuture<ItemStack> {
if (isRemote) {
return dispatchMessage(world.connectionID, "swapItems", jsonArrayOf(slot, item.toJson(), tryCombine)).thenApply { ItemDescriptor(it).build() }
} else {
return CompletableFuture.completedFuture(items.swap(slot, item, tryCombine))
}
}
fun combineItems(slot: Int, item: ItemStack): CompletableFuture<ItemStack> {
if (isRemote) {
if (world.remote(connectionID)!!.isLegacy) {
if (items[slot].isEmpty || !items[slot].isStackable(item)) {
return CompletableFuture.completedFuture(item)
} else {
return dispatchMessage(world.connectionID, "putItems", jsonArrayOf(slot, item.toJson()))
.thenApply { ItemDescriptor(it).build() }
}
} else {
return dispatchMessage(world.connectionID, "combineItems", jsonArrayOf(slot, item.toJson()))
.thenApply { ItemDescriptor(it).build() }
}
} else {
return CompletableFuture.completedFuture(items.combineAt(slot, item))
}
}
fun takeItem(item: ItemDescriptor, exact: Boolean = false): CompletableFuture<Boolean> {
if (isRemote) {
return dispatchMessage(world.connectionID, "consumeItems", jsonArrayOf(item.toJson(), exact)).thenApply { it.asBoolean }
} else {
return CompletableFuture.completedFuture(items.take(item, exactMatch = exact))
}
}
fun takeItemAt(index: Int, amount: Long = Long.MAX_VALUE): CompletableFuture<Boolean> {
if (isRemote) {
return dispatchMessage(world.connectionID, "consumeItemsAt", jsonArrayOf(index, amount)).thenApply { it.asBoolean }
} else {
return CompletableFuture.completedFuture(items.takeExact(index, amount))
}
}
fun clearContainer(): CompletableFuture<List<ItemStack>> {
if (isRemote) {
return dispatchMessage(world.connectionID, "clearContainer", JsonArray()).thenApply {
(it as JsonArray).stream().map { ItemDescriptor(it).build() }.filter { it.isNotEmpty }.toList()
}
} else {
val future = CompletableFuture.completedFuture(items.filter { it.isNotEmpty })
items.clear()
return future
}
}
private fun startCrafting() {
}

View File

@ -20,9 +20,10 @@ import ru.dbotthepony.kstarbound.util.coalesceNull
import ru.dbotthepony.kstarbound.util.valueOf
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity
import java.lang.Math.toRadians
class LoungeableObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config) {
class LoungeableObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(config), LoungeableEntity {
init {
isInteractive = true
}

View File

@ -90,6 +90,7 @@ import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.api.InteractiveEntity
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataOutputStream
@ -98,7 +99,7 @@ import java.util.HashMap
import java.util.random.RandomGenerator
import kotlin.math.min
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity(), ScriptedEntity {
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity(), ScriptedEntity, InteractiveEntity {
override fun deserialize(data: JsonObject) {
super.deserialize(data)
direction = data.get("direction", directions) { Direction.LEFT }
@ -142,7 +143,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
data["parameters"] = JsonObject().also {
for ((k, v) in parameters) {
it[k] = v.deepCopy()
it[k] = v
}
}
}
@ -203,7 +204,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
networkGroup.upstream.add(uniqueID)
}
var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) }
final override var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) }
val declaredMaterialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) }
private val materialSpaces0 = ManualLazy {