package ru.dbotthepony.kstarbound.item import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet import com.google.gson.Gson import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import com.google.gson.TypeAdapter import com.google.gson.annotations.JsonAdapter import com.google.gson.internal.bind.TypeAdapters import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.ObjectArrayList import org.classdump.luna.Table import org.classdump.luna.TableFactory import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeVarLong import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.math.vector.Vector2f import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.Drawable import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemRarity import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.ManualLazy import ru.dbotthepony.kstarbound.util.valueOf import java.io.DataOutputStream import java.lang.Math.toRadians import java.util.concurrent.atomic.AtomicLong import kotlin.math.max import kotlin.properties.Delegates /** * Base class for instanced items in game * * [config] is JsonObject returned by "builder" Lua script, or [entry]'s json if no such script exists * or it has failed */ @JsonAdapter(ItemStack.Adapter::class) open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, parameters: JsonObject, size: Long) { /** * unique number utilized to determine whenever stack has changed */ var changeset: Long = CHANGESET.incrementAndGet() private set /** * it uses global atomic long to guarantee stacks having different * changesets throughout entire lifetime of game */ protected fun bumpVersion() { changeset = CHANGESET.incrementAndGet() } var size: Long = size set(value) { val newValue = value.coerceAtLeast(0L) if (field != newValue) { field = newValue changeset = CHANGESET.incrementAndGet() } } open val networkElement: NetworkedElement? get() = null var parameters: JsonObject = parameters protected set protected val parametersLazies = ObjectArrayList>() // no CME checks protected val mergedJson = ManualLazy { mergeJson(config.deepCopy(), parameters) }.also { parametersLazies.add(it) } val isEmpty: Boolean get() = size <= 0 || entry.isEmpty val isNotEmpty: Boolean get() = size > 0 && !entry.isEmpty fun grow(amount: Long) { size += amount } fun shrink(amount: Long) { size -= amount } protected fun lookupProperty(name: String): JsonElement { return mergedJson.value[name] ?: JsonNull.INSTANCE } protected fun lookupProperty(name: String, orElse: JsonElement): JsonElement { return mergedJson.value[name] ?: orElse } protected fun lookupProperty(name: String, orElse: () -> JsonElement): JsonElement { return mergedJson.value[name] ?: orElse() } protected fun lazyProperty(name: String, default: JsonElement, transform: JsonElement.() -> T): ManualLazy { return ManualLazy { transform(lookupProperty(name, default)) }.also { parametersLazies.add(it) } } protected fun lazyProperty(name: String, transform: JsonElement.() -> T): ManualLazy { return ManualLazy { transform(lookupProperty(name)) }.also { parametersLazies.add(it) } } protected fun lazyProperty(name: String, default: () -> JsonElement, transform: JsonElement.() -> T): ManualLazy { return ManualLazy { transform(lookupProperty(name, default)) }.also { parametersLazies.add(it) } } val maxStackSize: Long by lazyProperty("maxStack", JsonPrimitive(Globals.itemParameters.defaultMaxStack)) { asLong } val shortDescription: String by lazyProperty("shortdescription", JsonPrimitive("")) { asString } val description: String by lazyProperty("description", JsonPrimitive("")) { asString } val rarity: ItemRarity by lazyProperty("rarity") { ItemRarity.entries.valueOf(asString) } val twoHanded: Boolean by lazyProperty("twoHanded", JsonPrimitive(false)) { asBoolean } val price: Long by lazyProperty("price", JsonPrimitive(Globals.itemParameters.defaultPrice)) { asLong } val tooltipKind: String by lazyProperty("tooltipKind", JsonPrimitive("")) { asString } val category: String by lazyProperty("category", JsonPrimitive("")) { asString } val pickupSounds: Set by lazyProperty("pickupSounds", { JsonArray() }) { if (asJsonArray.isEmpty) Globals.itemParameters.pickupSoundsAbsolute else asJsonArray.stream().map { it.asString }.collect(ImmutableSet.toImmutableSet()) } val timeToLive: Double by lazyProperty("timeToLive", JsonPrimitive(Globals.itemParameters.defaultTimeToLive)) { asDouble } val learnBlueprintsOnPickup: Set by lazyProperty("learnBlueprintsOnPickup", { JsonArray() }) { asJsonArray.stream().map { ItemDescriptor(it) }.collect(ImmutableSet.toImmutableSet()) } // collection -> entry val collectablesOnPickup: Map by lazyProperty("collectablesOnPickup", { JsonObject() }) { asJsonObject.entrySet().stream().map { it.key to it.value.asString }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) } var drawables: ImmutableList by Delegates.notNull() private set protected fun setIconDrawables(drawables: Collection) { val drawablesList = ArrayList(drawables) if (drawablesList.isNotEmpty()) { var boundingBox = drawablesList.first().boundingBox(true) for (i in 1 until drawablesList.size) { boundingBox = boundingBox.combine(drawablesList[i].boundingBox(true)) } for (drawable in drawablesList.indices) { drawablesList[drawable] = drawablesList[drawable].translate(boundingBox.centre.toFloatVector()) } // as original engine puts it: // TODO: Why 16? Is this the size of the icon container? Shouldn't this be configurable? // in other news, // TODO: scaling is always centered on 0.0, 0.0; this also should be configurable. val zoom = 16.0 / max(boundingBox.width, boundingBox.height) if (zoom < 1.0) { for (drawable in drawablesList.indices) { drawablesList[drawable] = drawablesList[drawable].scale(zoom.toFloat()) } } } this.drawables = ImmutableList.copyOf(drawablesList) } init { val inventoryIcon = lookupProperty("inventoryIcon", JsonPrimitive(Globals.itemParameters.missingIcon.fullPath)) if (inventoryIcon.isJsonArray) { val drawables = ArrayList() for (drawable in inventoryIcon.asJsonArray) { drawable as JsonObject val image = SpriteReference.create(AssetPathStack.relativeTo(entry.directory, drawable["image"].asString)) val position: Vector2f if ("position" in drawable) { position = Starbound.gson.fromJson(drawable["position"], Vector2f::class.java) } else { position = Vector2f.ZERO } val scale = if ("scale" in drawable) { scales.fromJsonTree(drawable["scale"]) } else { Either.left(1f) } val color = if ("color" in drawable) { colors.fromJsonTree(drawable["color"]) } else { RGBAColor.WHITE } val rotation = toRadians(drawable.get("rotation", 0.0)).toFloat() val transforms = Drawable.Transformations(drawable.get("centered", true), rotation, drawable.get("mirrored", false), scale) drawables.add(Drawable.Image(image, Either.right(transforms), position, color, drawable.get("fullbright", false))) } setIconDrawables(drawables) } else { val image = SpriteReference.create(AssetPathStack.relativeTo(entry.directory, inventoryIcon.asString)) val transforms = Drawable.CENTERED val drawable = Drawable.Image(image, Either.right(transforms)) setIconDrawables(listOf(drawable)) } } data class AgingResult(val new: ItemStack?, val ageUpdated: Boolean) 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(updated.build(), true) } } return AgingResult(null, false) } fun createDescriptor(): ItemDescriptor { if (isEmpty) return ItemDescriptor.EMPTY return ItemDescriptor(entry.name, size, parameters.deepCopy()) } // faster than creating an item descriptor and writing it (because it avoids copying and allocation) fun write(stream: DataOutputStream) { if (isEmpty) { stream.writeBinaryString("") stream.writeVarLong(0L) stream.writeJsonElement(JsonNull.INSTANCE) } else { stream.writeBinaryString(entry.name) stream.writeVarLong(size) stream.writeJsonElement(parameters) } } /** * Возвращает null если этот предмет пуст */ fun conciseToNull(): ItemStack? { if (isEmpty) { return null } else { return this } } fun mergeFrom(other: ItemStack, simulate: Boolean) { if (isStackable(other)) { val newCount = (size + other.size).coerceAtMost(maxStackSize) val diff = newCount - size other.size -= diff if (!simulate) size = newCount } } fun isStackable(other: ItemStack): Boolean { if (isEmpty || other.isEmpty) return false return size != 0L && other.size != 0L && maxStackSize > size && entry == other.entry && other.config == config && other.parameters == parameters } /** * whenever items match, ignoring their sizes */ fun matches(other: ItemStack, exact: Boolean = false): Boolean { if (isEmpty && other.isEmpty) return true return entry == other.entry && (!exact || parameters == other.parameters) } /** * whenever items match, ignoring their sizes */ fun matches(other: ItemDescriptor, exact: Boolean = false): Boolean { if (isEmpty && other.isEmpty) return true return entry.name == other.name && (!exact || parameters == other.parameters) } override fun equals(other: Any?): Boolean { if (other !is ItemStack) return false if (isEmpty) return other.isEmpty return matches(other) && size == other.size } override fun hashCode(): Int { return config.hashCode() } override fun toString(): String { if (isEmpty) return "ItemStack.EMPTY" return "ItemStack[${entry.name}, count = $size, params = $parameters]" } open fun copy(size: Long = this.size): ItemStack { if (isEmpty) return EMPTY return ItemStack(entry, config, parameters.deepCopy(), size) } fun toJson(): JsonElement { if (isEmpty) return JsonNull.INSTANCE return createDescriptor().toJson() } class Adapter(gson: Gson) : TypeAdapter() { override fun write(out: JsonWriter, value: ItemStack?) { val json = value?.toJson() if (json == null) out.nullValue() else out.value(json) } override fun read(`in`: JsonReader): ItemStack { if (`in`.consumeNull()) return EMPTY return ItemDescriptor(Starbound.ELEMENTS_ADAPTER.read(`in`)).build() } } companion object { private val CHANGESET = AtomicLong() @JvmField val EMPTY = ItemStack(ItemRegistry.AIR, ItemRegistry.AIR.json, JsonObject(), 0L) private val vectors by lazy { Starbound.gson.getAdapter(object : TypeToken() {}) } private val scales by lazy { Starbound.gson.getAdapter(object : TypeToken>() {}) } private val colors by lazy { Starbound.gson.getAdapter(object : TypeToken() {}) } } }