container objects "functional"

This commit is contained in:
DBotThePony 2024-04-15 21:28:43 +07:00
parent 77aa05d9f3
commit 9f52e2314d
Signed by: DBot
GPG Key ID: DCC23B5715498507
9 changed files with 465 additions and 32 deletions
src/main/kotlin/ru/dbotthepony/kstarbound

View File

@ -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 {

View File

@ -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<IItemDefinition>, 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 {

View File

@ -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("<liquid>", surfaceLiquid), imageScale))
if (visitable.surfaceLiquid.isNotEmptyLiquid && liquidImages.isNotBlank()) {
layers.add(Layer(liquidImages.replace("<liquid>", visitable.surfaceLiquid.entry!!.key), imageScale))
} else {
if (baseCount > 0) {
layers.add(Layer("${baseImages.replace("<biome>", visitable.primaryBiome).replace("<num>", 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("<liquid>", surfaceLiquid)))
images.add(getLR(liquidTextures.replace("<liquid>", visitable.surfaceLiquid.entry!!.key)))
val masksL = ArrayList<String>()
val masksR = ArrayList<String>()

View File

@ -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
}
}

View File

@ -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<ItemStack> {
clear()
val read = ArrayList<ItemStack>()
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<ItemStack>()
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()
}
}
}

View File

@ -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<IItemDefinition>
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)
}
}

View File

@ -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 {

View File

@ -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<ObjectDefinition>) : 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<ItemStack>()
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<ItemStack>
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<ItemStack>
}
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) {}
}
}

View File

@ -504,6 +504,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
}
if (shouldBreak) {
callBreak(false)
}
}
}