KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt

406 lines
13 KiB
Kotlin

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<ManualLazy<*>>() // 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 <T> lazyProperty(name: String, default: JsonElement, transform: JsonElement.() -> T): ManualLazy<T> {
return ManualLazy {
transform(lookupProperty(name, default))
}.also { parametersLazies.add(it) }
}
protected fun <T> lazyProperty(name: String, transform: JsonElement.() -> T): ManualLazy<T> {
return ManualLazy {
transform(lookupProperty(name))
}.also { parametersLazies.add(it) }
}
protected fun <T> lazyProperty(name: String, default: () -> JsonElement, transform: JsonElement.() -> T): ManualLazy<T> {
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<String> 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<ItemDescriptor> by lazyProperty("learnBlueprintsOnPickup", { JsonArray() }) { asJsonArray.stream().map { ItemDescriptor(it) }.collect(ImmutableSet.toImmutableSet()) }
// collection -> entry
val collectablesOnPickup: Map<String, String> by lazyProperty("collectablesOnPickup", { JsonObject() }) { asJsonObject.entrySet().stream().map { it.key to it.value.asString }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) }
var drawables: ImmutableList<Drawable> by Delegates.notNull()
private set
protected fun setIconDrawables(drawables: Collection<Drawable>) {
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<Drawable>()
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<ItemStack>() {
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<Vector2f>() {})
}
private val scales by lazy {
Starbound.gson.getAdapter(object : TypeToken<Either<Float, Vector2f>>() {})
}
private val colors by lazy {
Starbound.gson.getAdapter(object : TypeToken<RGBAColor>() {})
}
}
}