KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt

313 lines
12 KiB
Kotlin

package ru.dbotthepony.kstarbound.defs.`object`
import com.google.common.collect.ImmutableList
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.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.clear
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2f
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.render.RenderLayer
import ru.dbotthepony.kstarbound.defs.Drawable
import ru.dbotthepony.kstarbound.defs.JsonReference
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.json.setAdapter
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World
import java.util.concurrent.CompletableFuture
import kotlin.math.PI
data class ObjectOrientation(
val json: JsonObject,
val flipImages: Boolean = false,
val drawables: ImmutableList<Drawable>,
val renderLayer: RenderLayer.Point,
val imagePosition: Vector2f,
val frames: Int,
val animationCycle: Double,
// world tiles this object occupy while in this orientation
val occupySpaces: ImmutableSet<Vector2i>,
val boundingBox: AABBi,
val metaBoundBox: AABB?,
val anchors: ImmutableSet<Anchor>,
val anchorAny: Boolean,
val directionAffinity: Direction?,
val materialSpaces: ImmutableList<Pair<Vector2i, Registry.Ref<TileDefinition>>>,
val interactiveSpaces: ImmutableSet<Vector2i>,
val lightPosition: Vector2i,
val beamAngle: Double,
val statusEffectArea: Vector2d?,
val touchDamage: JsonElement,
val particleEmitters: ArrayList<ParticleEmissionEntry>,
) {
fun placementValid(world: World<*, *>, position: Vector2i, ignoreProtectedDungeons: Boolean = false): Boolean {
if (occupySpaces.isEmpty())
return true
return occupySpaces.all {
val cell = world.chunkMap.getCell(it + position)
//if (!cell.foreground.material.isEmptyTile) println("not empty tile: ${it + position}, space $it, pos $position")
//if (cell.dungeonId in world.protectedDungeonIDs) println("position is protected: ${it + position}")
cell.foreground.material.isEmptyTile && (ignoreProtectedDungeons || cell.dungeonId !in world.protectedDungeonIDs)
}
}
fun anchorsValid(world: World<*, *>, position: Vector2i, considerNullAsValid: Boolean = false): Boolean {
if (anchors.isEmpty())
return true
var anyValid = false
for (anchor in anchors) {
val isValid = anchor.isValidPlacement(world, position, considerNullAsValid)
if (isValid)
anyValid = true
else if (!anchorAny) {
// println("anchor $anchor reported false for $position ${world.chunkMap.getCell(position + anchor.position)}")
return false
}
}
return anyValid
}
companion object {
private val LOGGER = LogManager.getLogger()
fun preprocess(json: JsonArray): JsonArray {
val actual = ArrayList<JsonObject>()
for (elem in json) {
val obj = elem.asJsonObject
if ("dualImage" in obj) {
var copy = obj.deepCopy()
copy["image"] = obj["dualImage"]!!
copy["flipImages"] = true
copy["direction"] = "left"
actual.add(copy)
copy = obj.deepCopy()
copy["image"] = obj["dualImage"]!!
copy["flipImages"] = false
copy["direction"] = "right"
actual.add(copy)
} else if ("leftImage" in obj) {
require("rightImage" in obj) { "Provided leftImage, but there is no rightImage!" }
var copy = obj.deepCopy()
copy["image"] = obj["leftImage"]!!
copy["direction"] = "left"
actual.add(copy)
copy = obj.deepCopy()
copy["image"] = obj["rightImage"]!!
copy["direction"] = "right"
actual.add(copy)
} else {
actual.add(obj)
}
}
json.clear()
actual.forEach { json.add(it) }
return json
}
}
class Adapter(gson: Gson) {
private val vectors = gson.getAdapter(Vector2f::class.java)
private val vectorsi = gson.getAdapter(Vector2i::class.java)
private val vectorsd = gson.getAdapter(Vector2d::class.java)
private val drawables = gson.getAdapter(Drawable::class.java)
private val aabbs = gson.getAdapter(AABB::class.java)
private val emitter = gson.getAdapter(ParticleEmissionEntry::class.java)
private val emitters = gson.listAdapter<ParticleEmissionEntry>()
private val spaces = gson.setAdapter<Vector2i>()
private val materialSpaces = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, TypeToken.getParameterized(Pair::class.java, Vector2i::class.java, String::class.java).type)) as TypeAdapter<ImmutableList<Pair<Vector2i, String>>>
suspend fun read(obj: JsonObject, folder: String): ObjectOrientation {
val drawables = ArrayList<Drawable>()
val flipImages = obj.get("flipImages", false)
val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object"))
AssetPathStack(folder) {
if ("imageLayers" in obj) {
for (value in obj["imageLayers"].asJsonArray) {
var result = this.drawables.fromJsonTree(value)
if (flipImages) {
result = result.flop()
}
drawables.add(result)
}
} else {
var result = this.drawables.fromJsonTree(obj)
if (flipImages) {
result = result.flop()
}
drawables.add(result)
}
}
val imagePosition = (obj["imagePosition"]?.let { vectors.fromJsonTree(it) } ?: Vector2f.ZERO) / PIXELS_IN_STARBOUND_UNITf
val imagePositionI = (obj["imagePosition"]?.let { vectorsi.fromJsonTree(it) } ?: Vector2i.ZERO)
val frames = obj.get("frames", 1)
val animationCycle = obj.get("animationCycle", 1.0)
var occupySpaces = obj["spaces"]?.let { spaces.fromJsonTree(it) } ?: ImmutableSet.of(Vector2i.ZERO)
if ("spaceScan" in obj) {
occupySpaces = ImmutableSet.of()
for (drawable in drawables) {
if (drawable is Drawable.Image) {
val bound = drawable.path.with { "default" }
val sprite = bound.sprite
if (sprite != null) {
val new = ImmutableSet.Builder<Vector2i>()
new.addAll(occupySpaces)
new.addAll(sprite.worldSpacesAsync(imagePositionI, obj["spaceScan"].asDouble, flipImages).await())
occupySpaces = new.build()
} else {
LOGGER.error("Unable to space scan image, not a valid sprite reference: $bound")
}
}
}
}
val minX: Int
val minY: Int
val maxX: Int
val maxY: Int
if (occupySpaces.isNotEmpty()) {
minX = occupySpaces.minOf { it.x }
minY = occupySpaces.minOf { it.y }
maxX = occupySpaces.maxOf { it.x }
maxY = occupySpaces.maxOf { it.y }
} else {
minX = 0
minY = 0
maxX = 0
maxY = 0
}
val boundingBox = AABBi(Vector2i(minX, minY), Vector2i(maxX, maxY))
val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) }
val requireTilledAnchors = obj.get("requireTilledAnchors", false)
val requireSoilAnchors = obj.get("requireSoilAnchors", false)
val anchorMaterial = obj["anchorMaterial"]?.asString?.let { Registries.tiles.ref(it) }
val anchors = ImmutableSet.Builder<Anchor>()
for (v in obj.get("anchors", JsonArray())) {
when (v.asString.lowercase()) {
"left" -> occupySpaces.stream().filter { it.x == minX }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"right" -> occupySpaces.stream().filter { it.x == maxX }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"top" -> occupySpaces.stream().filter { it.y == maxY }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"bottom" -> occupySpaces.stream().filter { it.y == minY }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"background" -> occupySpaces.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
else -> throw JsonSyntaxException("Unknown anchor type $v")
}
}
for (v in obj.get("bgAnchors", JsonArray()))
anchors.add(Anchor(true, vectorsi.fromJsonTree(v), requireTilledAnchors, requireSoilAnchors, anchorMaterial))
for (v in obj.get("fgAnchors", JsonArray()))
anchors.add(Anchor(false, vectorsi.fromJsonTree(v), requireTilledAnchors, requireSoilAnchors, anchorMaterial))
val anchorAny = obj["anchorAny"]?.asBoolean ?: false
val directionAffinity = obj["direction"]?.asString?.uppercase()?.let { Direction.valueOf(it) }
val materialSpaces: ImmutableList<Pair<Vector2i, String>>
if ("materialSpaces" in obj) {
materialSpaces = this.materialSpaces.fromJsonTree(obj["materialSpaces"])
} else {
val collisionSpaces = obj["collisionSpaces"]?.let { this.spaces.fromJsonTree(it) } ?: occupySpaces
val builder = ImmutableList.Builder<Pair<Vector2i, String>>()
when (val collisionType = obj.get("collision", "none").lowercase()) {
"solid" -> collisionSpaces.forEach { builder.add(it to BuiltinMetaMaterials.OBJECT_SOLID.key) }
"platform" -> collisionSpaces.forEach { if (it.y == boundingBox.maxs.y) builder.add(it to BuiltinMetaMaterials.OBJECT_PLATFORM.key) }
"none" -> {}
else -> throw JsonSyntaxException("Unknown collision type $collisionType")
}
materialSpaces = builder.build()
}
val interactiveSpaces = obj["interactiveSpaces"]?.let { this.spaces.fromJsonTree(it) } ?: occupySpaces
val lightPosition = obj["lightPosition"]?.let { vectorsi.fromJsonTree(it) } ?: Vector2i.ZERO
val beamAngle = obj.get("beamAngle", 0.0) / 180.0 * PI
val statusEffectArea = obj["statusEffectArea"]?.let { vectorsd.fromJsonTree(it) }
val touchDamage = Starbound.loadJsonAsset(obj["touchDamage"] ?: JsonNull.INSTANCE, AssetPathStack.last()).await()
val emitters = ArrayList<ParticleEmissionEntry>()
if ("particleEmitter" in obj) {
emitters.add(this.emitter.fromJsonTree(obj["particleEmitter"]))
}
if ("particleEmitters" in obj) {
emitters.addAll(this.emitters.fromJsonTree(obj["particleEmitters"]))
}
return ObjectOrientation(
obj,
flipImages = flipImages,
drawables = ImmutableList.copyOf(drawables),
renderLayer = renderLayer,
imagePosition = imagePosition,
frames = frames,
animationCycle = animationCycle,
occupySpaces = occupySpaces,
boundingBox = boundingBox,
metaBoundBox = metaBoundBox,
anchors = anchors.build(),
anchorAny = anchorAny,
directionAffinity = directionAffinity,
materialSpaces = materialSpaces.stream().map { it.first to Registries.tiles.ref(it.second) }.collect(ImmutableList.toImmutableList()),
interactiveSpaces = interactiveSpaces,
lightPosition = lightPosition,
beamAngle = beamAngle,
statusEffectArea = statusEffectArea,
touchDamage = touchDamage,
particleEmitters = emitters,
)
}
}
}