406 lines
13 KiB
Kotlin
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>() {})
|
|
}
|
|
}
|
|
}
|