From 9f52e2314d28ac99addee328dcbbf4429cfdfe45 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 15 Apr 2024 21:28:43 +0700 Subject: [PATCH] container objects "functional" --- .../ru/dbotthepony/kstarbound/Starbound.kt | 5 +- .../kstarbound/defs/item/ItemDescriptor.kt | 20 ++ .../dbotthepony/kstarbound/defs/world/Sky.kt | 13 +- .../dbotthepony/kstarbound/item/Container.kt | 13 - .../dbotthepony/kstarbound/item/IContainer.kt | 147 ++++++++++- .../dbotthepony/kstarbound/item/ItemStack.kt | 41 ++- .../world/entities/player/PlayerInventory.kt | 9 +- .../world/entities/tile/ContainerObject.kt | 248 +++++++++++++++++- .../world/entities/tile/WorldObject.kt | 1 + 9 files changed, 465 insertions(+), 32 deletions(-) delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/item/Container.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index ab9e45be..7426602a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -234,7 +234,10 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca throw NoSuchElementException("Script $path does not exist") } - return loader.compileTextChunk(path, find.readToString()) + val time = System.nanoTime() + val result = loader.compileTextChunk(path, find.readToString()) + LOGGER.debug("Compiled {} in {} ms", path, (System.nanoTime() - time) / 1_000_000L) + return result } fun loadScript(path: String): ChunkFactory { 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 8f560961..4b7ceb9d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -35,6 +35,7 @@ import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.indexNoYield import ru.dbotthepony.kstarbound.lua.toJson import java.io.DataInputStream @@ -109,6 +110,24 @@ fun ExecutionContext.ItemDescriptor(data: Table): ItemDescriptor { } } +@Deprecated("Does not obey meta methods, need to find replacement where possible") +fun ItemDescriptor(data: Table): ItemDescriptor { + val name = data[1L] ?: data["name"] ?: data["item"] + val count = data[2L] ?: data["count"] ?: 1L + val parameters = data[3L] ?: data["parameters"] ?: data["data"] + + if (name !is ByteString) throw LuaRuntimeException("Invalid item descriptor name (${name})") + if (count !is Number) throw LuaRuntimeException("Invalid item descriptor count (${count})") + + if (parameters == null) { + return ItemDescriptor(name.decode(), count.toLong()) + } else if (parameters is Table) { + return ItemDescriptor(name.decode(), count.toLong(), parameters.toJson(true) as JsonObject) + } else { + throw LuaRuntimeException("Invalid item descriptor parameters ($parameters)") + } +} + fun ItemDescriptor(stream: DataInputStream): ItemDescriptor { val name = stream.readInternedString() val count = stream.readVarLong() @@ -135,6 +154,7 @@ data class ItemDescriptor( constructor(ref: Registry.Ref, count: Long, parameters: JsonObject) : this(ref.key.left(), count, parameters) val isEmpty get() = count <= 0L || name == "" || ref.isEmpty + val isNotEmpty get() = !isEmpty val ref by lazy { if (name == "") Registries.items.emptyRef else Registries.items.ref(name) } override fun toString(): String { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt index 5bc27df8..1d7de4c0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt @@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.io.readColor import ru.dbotthepony.kstarbound.io.writeColor import ru.dbotthepony.kstarbound.json.builder.IStringSerializable @@ -178,10 +179,8 @@ data class SkyParameters( // If the planet has water, then draw the corresponding water image as the // base layer, otherwise use the bottom most mask image. - val surfaceLiquid = visitable.surfaceLiquid.entry?.key - - if (surfaceLiquid != null && liquidImages.isNotBlank()) { - layers.add(Layer(liquidImages.replace("", surfaceLiquid), imageScale)) + if (visitable.surfaceLiquid.isNotEmptyLiquid && liquidImages.isNotBlank()) { + layers.add(Layer(liquidImages.replace("", visitable.surfaceLiquid.entry!!.key), imageScale)) } else { if (baseCount > 0) { layers.add(Layer("${baseImages.replace("", visitable.primaryBiome).replace("", baseCount.toString())}?hueshift=${visitable.hueShift}", imageScale)) @@ -293,15 +292,13 @@ data class SkyParameters( val biomeHueShift = "?hueshift=${visitable.hueShift.toInt()}" - val surfaceLiquid = visitable.surfaceLiquid.entry?.key - - if (surfaceLiquid != null) { + if (visitable.surfaceLiquid.isNotEmptyLiquid) { val random = random(parameters.seed) for (i in 0 until 23) random.nextInt() - images.add(getLR(liquidTextures.replace("", surfaceLiquid))) + images.add(getLR(liquidTextures.replace("", visitable.surfaceLiquid.entry!!.key))) val masksL = ArrayList() val masksR = ArrayList() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/Container.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/Container.kt deleted file mode 100644 index 4b5108d8..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/Container.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ru.dbotthepony.kstarbound.item - -open class Container(final override val size: Int) : IContainer { - private val slots = Array(size) { ItemStack.EMPTY } - - override fun get(index: Int): ItemStack { - return slots[index] - } - - override fun set(index: Int, value: ItemStack) { - slots[index] = value - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt index b6e698bd..5fde2221 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt @@ -1,7 +1,152 @@ package ru.dbotthepony.kstarbound.item +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor + interface IContainer { - val size: Int + var size: Int operator fun get(index: Int): ItemStack operator fun set(index: Int, value: ItemStack) + + fun ageItems(by: Double): Boolean { + var any = false + + for (i in 0 until size) { + val (newItem, hasAdvanced) = this[i].advanceAge(by) + any = hasAdvanced || any + + if (newItem != null) { + this[i] = newItem + } + } + + return any + } + + // puts item into container, returns remaining not put items + fun add(item: ItemStack, simulate: Boolean = false): ItemStack { + val copy = item.copy() + + // first, try to put into not empty slots first + for (i in 0 until size) { + val itemThere = this[i] + + if (itemThere.isStackable(copy)) { + val newCount = (itemThere.size + copy.size).coerceAtMost(itemThere.maxStackSize) + val diff = newCount - itemThere.size + + copy.size -= diff + + if (!simulate) + itemThere.size += diff + + if (copy.isEmpty) + return ItemStack.EMPTY + } + } + + // then try to move into empty slots + for (i in 0 until size) { + val itemThere = this[i] + + if (itemThere.isEmpty) { + if (copy.size > copy.maxStackSize) { + if (!simulate) + this[i] = copy.copy(copy.maxStackSize) + + copy.size -= copy.maxStackSize + } else { + if (!simulate) + this[i] = copy + + return ItemStack.EMPTY + } + } + } + + return copy + } + + fun clear() + + fun toJson(store: Boolean = false): JsonArray { + val result = JsonArray(size) + + if (store) { + Starbound.storeJson { + for (i in 0 until size) { + result.add(this[i].toJson()) + } + } + } else { + for (i in 0 until size) { + result.add(this[i].toJson()) + } + } + + return result + } + + fun fromJson(json: JsonElement, resize: Boolean = false): List { + clear() + + val read = ArrayList() + val array = json.asJsonArray + var nonEmpty = 0 + + for (i in 0 until array.size()) { + val item = ItemDescriptor(array[i]) + + if (item.isNotEmpty) + nonEmpty++ + + read.add(ItemStack.create(item)) + } + + if (read.size > size) { + // we have a problem + + if (resize) { + // no problem + size = read.size + + for ((i, item) in read.withIndex()) { + this[i] = item + } + + return emptyList() + } else { + // first, put items to their place if they are within container size + + for (i in 0 until size) { + this[i] = read[i] + } + + // second, try to move remaining item into empty or stackable slots + val lost = ArrayList() + + for (i in size until read.size) { + val remaining = add(read[i]) + + if (remaining.isNotEmpty) { + lost.add(remaining) + } + } + + // if we weren't capable of putting excess items into container, then what a shame. + return lost + } + } else { + if (read.size < size && resize) + size = read.size + + for ((i, item) in read.withIndex()) { + this[i] = item + } + + return emptyList() + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index abcc4c5b..747dab9d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -18,6 +18,7 @@ import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeVarLong import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.from import java.io.DataOutputStream import java.util.concurrent.atomic.AtomicLong @@ -62,7 +63,8 @@ open class ItemStack { } val config: Registry.Ref - val parameters: JsonObject + var parameters: JsonObject + protected set val isEmpty: Boolean get() = size <= 0 || config.isEmpty @@ -81,6 +83,38 @@ open class ItemStack { size -= amount } + data class AgingResult(val new: ItemStack?, val ageUpdated: Boolean) + + private fun config() = config + + private val agingScripts: LuaEnvironment? by lazy { + val config = config().value ?: return@lazy null + //if (config.itemTags) + null + } + + open fun advanceAge(by: Double): AgingResult { + val agingScripts = agingScripts ?: return AgingResult(null, false) + + val descriptor = createDescriptor() + val updated = ItemDescriptor(agingScripts.invokeGlobal("ageItem", descriptor.toTable(agingScripts), by)[0] as Table) + + if (descriptor != updated) { + if (descriptor.name == updated.name) { + // only parameters got changed + this.parameters = descriptor.parameters + this.size = descriptor.count + changeset = CHANGESET.incrementAndGet() + return AgingResult(null, true) + } else { + // item got replaced by something else + return AgingResult(create(updated), true) + } + } + + return AgingResult(null, false) + } + fun createDescriptor(): ItemDescriptor { if (isEmpty) return ItemDescriptor.EMPTY @@ -151,7 +185,7 @@ open class ItemStack { return "ItemDescriptor[${config.value?.itemName}, count = $size, params = $parameters]" } - fun copy(): ItemStack { + fun copy(size: Long = this.size): ItemStack { if (isEmpty) return this @@ -206,6 +240,9 @@ open class ItemStack { val EMPTY = ItemStack() fun create(descriptor: ItemDescriptor): ItemStack { + if (descriptor.isEmpty) + return EMPTY + return ItemStack(descriptor) } } 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 9bf5c029..1927abf6 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 @@ -28,7 +28,14 @@ import java.io.DataInputStream import java.io.DataOutputStream class PlayerInventory { - inner class Bag(override val size: Int) : IContainer { + inner class Bag(size: Int) : IContainer { + override var size: Int = size + set(value) { throw UnsupportedOperationException() } + + override fun clear() { + slots.forEach { it.accept(ItemStack.EMPTY) } + } + val slots = immutableList(size) { networkedItem() } override fun get(index: Int): ItemStack { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt index 35bcdd36..0811efce 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt @@ -1,23 +1,259 @@ package ru.dbotthepony.kstarbound.world.entities.tile +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.InteractAction +import ru.dbotthepony.kstarbound.defs.InteractRequest +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.math.Interpolator +import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedBytes import ru.dbotthepony.kstarbound.network.syncher.networkedFloat import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt +import ru.dbotthepony.kstarbound.util.ManualLazy +import ru.dbotthepony.kstarbound.util.RelativeClock +import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity +import java.io.DataInputStream +import java.io.DataOutputStream class ContainerObject(config: Registry.Entry) : WorldObject(config) { var opened 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 } - // i have no words. - // this field embeds ENTIRE net state of 'ItemBag', - // and each time container is updated, its contents are networked fully - // each. damn. time. - // placeholder data, container size 40, read 0 items - var itemsNetState by networkedBytes(byteArrayOf(40, 0)).also { networkGroup.upstream.add(it) } + val items = Container(0).also { networkGroup.upstream.add(it) } + + // whenever set loot seed, put initial items, etc. + private var isInitialized = false + + private val lostItems = ArrayList() + private val ageItemsTimer = RelativeClock() + + private val ageItemsEvery = ManualLazy { + lookupProperty("ageItemsEvery") { JsonPrimitive(10.0) }.asDouble + }.also { parametersLazies.add(it) } + + private val itemAgeMultiplier = ManualLazy { + lookupProperty("itemAgeMultiplier") { JsonPrimitive(1.0) }.asDouble + }.also { parametersLazies.add(it) } + + override fun tick(delta: Double) { + super.tick(delta) + + if (world.isServer) { + for (item in lostItems) { + val entity = ItemDropEntity(item) + entity.position = position + entity.joinWorld(world) + } + + lostItems.clear() + ageItemsTimer.update(world.sky.time) + + if (ageItemsTimer.time >= ageItemsEvery.value) { + items.ageItems(ageItemsTimer.time * itemAgeMultiplier.value) + ageItemsTimer.set(0.0) + } + } + } + + override fun interact(request: InteractRequest): InteractAction { + return InteractAction(InteractAction.Type.OPEN_CONTAINER, entityID) + } + + override fun deserialize(data: JsonObject) { + super.deserialize(data) + + opened = data.get("opened", 0) + isCrafting = data.get("crafting", false) + craftingProgress = data.get("craftingProgress", 0.0) + isInitialized = data.get("initialized", true) + items.fromJson(data.get("items", JsonArray()), resize = true) + } + + override fun serialize(): JsonObject { + val data = super.serialize() + + // required by original engine + data["currentState"] = 0 + data["opened"] = opened + data["crafting"] = isCrafting + data["craftingProgress"] = craftingProgress + data["initialized"] = isInitialized + data["items"] = items.toJson(true) + + return data + } + + // Networking of this container to legacy clients is incredibly stupid, + // and networks entire state each time something has changed. + inner class Container(size: Int) : NetworkedElement(), IContainer { + private var items = Array(size) { ItemStack.EMPTY } + private var itemNetVersions = LongArray(size) + private var itemObservedVersion = LongArray(size) + private var lastSizeChange = 0L + + private fun observeItems() { + for (i in 0 until size) { + if (itemObservedVersion[i] != items[i].changeset) { + itemObservedVersion[i] = items[i].changeset + itemNetVersions[i] = currentVersion() + } + } + } + + override var size: Int = size + set(value) { + if (field == value) return + lastSizeChange = currentVersion() + + if (value > field) { + items = items.copyOf(value) as Array + + for (i in field until value) + items[i] = ItemStack.EMPTY + } else { + for (i in value until field) { + lostItems.add(items[i]) + } + + items = items.copyOf(value) as Array + } + + field = value + itemObservedVersion = itemObservedVersion.copyOf(value) + itemNetVersions = itemNetVersions.copyOf(value) + } + + override fun get(index: Int): ItemStack { + return items[index] + } + + override fun set(index: Int, value: ItemStack) { + items[index] = value.copy() + } + + override fun clear() { + items.fill(ItemStack.EMPTY) + } + + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + if (isLegacy) { + val stream = DataInputStream(FastByteArrayInputStream(data.readByteArray())) + + size = stream.readVarInt() + + val setItemsSize = stream.readVarInt() + items.fill(ItemStack.EMPTY) + + for (i in 0 until setItemsSize) { + items[i] = ItemStack(ItemDescriptor(stream)) + } + } else { + size = data.readVarInt() + items.fill(ItemStack.EMPTY) + + while (true) { + val index = data.readVarInt() - 1 + if (index == -1) break + items[index] = ItemStack(ItemDescriptor(data)) + } + } + } + + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) { + val wrapper = FastByteArrayOutputStream() + val stream = DataOutputStream(wrapper) + stream.writeVarInt(size) + var setItemsSize = items.indexOfLast { it.isNotEmpty } + + if (setItemsSize == -1) + setItemsSize = 0 + + stream.writeVarInt(setItemsSize) + + for (i in 0 until setItemsSize) { + items[i].write(stream) + } + + data.writeByteArray(wrapper.array, 0, wrapper.length) + } else { + data.writeVarInt(size) + + for (i in 0 until size) { + if (items[i].isNotEmpty) { + data.writeVarInt(i + 1) + items[i].write(data) + } + } + + data.writeVarInt(0) + } + } + + override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) { + if (isLegacy) { + readInitial(data, true) + } else { + if (data.readBoolean()) { + readInitial(data, false) + } else { + while (true) { + val index = data.readVarInt() - 1 + if (index == -1) break + items[index] = ItemStack(ItemDescriptor(data)) + } + } + } + } + + override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) { + if (isLegacy) { + writeInitial(data, true) + } else { + if (lastSizeChange >= remoteVersion) { + data.writeBoolean(true) + writeInitial(data, false) + } else { + observeItems() + data.writeBoolean(false) + + for (i in 0 until size) { + if (itemNetVersions[i] >= remoteVersion) { + data.writeVarInt(i + 1) + items[i].write(data) + } + } + + data.writeVarInt(0) + } + } + } + + override fun hasChangedSince(version: Long): Boolean { + observeItems() + return lastSizeChange >= version || itemNetVersions.any { it >= version } + } + + override fun readBlankDelta(interpolationDelay: Double) {} + override fun enableInterpolation(extrapolation: Double) {} + override fun disableInterpolation() {} + override fun tickInterpolation(delta: Double) {} + } } 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 3b5b6ded..ca2e7d8e 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 @@ -504,6 +504,7 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } if (shouldBreak) { + callBreak(false) } } }