KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/defs/TileDefinition.kt

519 lines
15 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.math.Vector2i
import ru.dbotthepony.kstarbound.util.Color
import ru.dbotthepony.kstarbound.world.IChunk
import java.io.File
data class TileDefinition(
val materialId: Int,
val materialName: String,
val particleColor: Color,
val itemDrop: String?,
val description: String,
val shortdescription: String,
val blocksLiquidFlow: Boolean,
val soil: Boolean,
val tillableMod: Int,
val racialDescription: ImmutableMap<String, String>,
val footstepSound: String?,
val health: Int,
val category: String,
val render: TileRenderDefinition
) {
init {
require(materialId >= 0) { "Material ID must be positive ($materialId given) ($materialName)" }
require(materialId != 0) { "Material ID 0 is reserved ($materialName)" }
}
}
class TileDefinitionBuilder {
var materialId = 0
var materialName = "unknown_tile"
var particleColor = Color.WHITE
var itemDrop: String? = "unknown"
var description = "..."
var shortdescription = "..."
var blocksLiquidFlow = true
var soil = false
var tillableMod = 0
val racialDescription = ArrayList<Pair<String, String>>()
var footstepSound: String? = null
var health = 0
var category = "generic"
val render = TileRenderDefinitionBuilder()
fun build(directory: String? = null): TileDefinition {
return TileDefinition(
racialDescription = ImmutableMap.builder<String, String>().also {
for ((k, v) in this.racialDescription) {
it.put(k, v)
}
}.build(),
materialId = materialId,
materialName = materialName,
particleColor = particleColor,
itemDrop = itemDrop,
description = description,
shortdescription = shortdescription,
blocksLiquidFlow = blocksLiquidFlow,
soil = soil,
tillableMod = tillableMod,
footstepSound = footstepSound,
health = health,
category = category,
render = render.build(directory)
)
}
companion object {
private val LOGGER = LogManager.getLogger()
fun fromJson(input: JsonObject): TileDefinitionBuilder {
val builder = TileDefinitionBuilder()
try {
builder.materialName = input["materialName"].asString
builder.materialId = input["materialId"].asInt
require(builder.materialId >= 0) { "Invalid materialId ${builder.materialId}" }
builder.particleColor = Color(input["particleColor"].asJsonArray)
builder.itemDrop = input["itemDrop"]?.asString
builder.description = input["description"]?.asString ?: builder.description
builder.shortdescription = input["shortdescription"]?.asString ?: builder.shortdescription
builder.footstepSound = input["footstepSound"]?.asString
builder.blocksLiquidFlow = input["footstepSound"]?.asBoolean ?: builder.blocksLiquidFlow
builder.soil = input["footstepSound"]?.asBoolean ?: builder.soil
builder.health = input["health"].asInt
builder.tillableMod = input["health"]?.asInt ?: builder.tillableMod
builder.category = input["category"].asString
if (input["variants"] != null) {
LOGGER.warn("Tile {} has `variants` ({}) defined as top level property (expected to be under `renderParameters`)", builder.materialName, input["variants"].asString)
}
for (key in input.keySet()) {
if (key.endsWith("Description") && key.length != "Description".length) {
builder.racialDescription.add(key.substring(0, key.length - "Description".length) to input[key].asString)
}
}
input["renderParameters"]?.asJsonObject?.let {
builder.render.texture = it["texture"].asString
builder.render.variants = it["variants"].asInt
builder.render.lightTransparent = it["lightTransparent"]?.asBoolean ?: builder.render.lightTransparent
builder.render.occludesBelow = it["occludesBelow"]?.asBoolean ?: builder.render.occludesBelow
builder.render.multiColored = it["multiColored"]?.asBoolean ?: builder.render.multiColored
builder.render.zLevel = it["zLevel"].asInt
}
builder.render.renderTemplate = input["renderTemplate"]?.asString?.let renderTemplate@{
return@renderTemplate TileRenderTemplate.load(it)
}
} catch(err: Throwable) {
throw IllegalArgumentException("Failed reading tile definition ${builder.materialName}", err)
}
return builder
}
}
}
/**
* Кусочек рендера тайла
*
* root.pieces[]
*/
data class TileRenderPiece(
val name: String,
val texture: File?,
val textureSize: Vector2i,
val texturePosition: Vector2i,
val colorStride: Vector2i?,
val variantStride: Vector2i?,
) {
companion object {
fun fromJson(name: String, input: JsonObject): TileRenderPiece {
val texture = input["texture"]?.asString?.let {
if (it[0] != '/') {
throw UnsupportedOperationException("Render piece has not absolute texture path: $it")
}
return@let File(it.substring(1))
}
val textureSize = Vector2i.fromJson(input["textureSize"].asJsonArray)
val texturePosition = Vector2i.fromJson(input["texturePosition"].asJsonArray)
val colorStride = input["colorStride"]?.asJsonArray?.let { Vector2i.fromJson(it) }
val variantStride = input["variantStride"]?.asJsonArray?.let { Vector2i.fromJson(it) }
return TileRenderPiece(name, texture, textureSize, texturePosition, colorStride, variantStride)
}
}
}
/**
* Кусочек правила рендера тайла
*
* root.rules.`name`.entries[]
*/
sealed class RenderRule(params: Map<String, Any>) {
val matchHue = params["matchHue"] as? Boolean ?: false
val inverse = params["inverse"] as? Boolean ?: false
abstract fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean
companion object {
fun factory(name: String, params: Map<String, Any>): RenderRule {
return when (name) {
"EqualsSelf" -> RenderRuleEqualsSelf(params)
"Shadows" -> RenderRuleShadows(params)
// неизвестно что оно делает
"Connects" -> AlwaysFailingRenderRule(params)
else -> throw IllegalArgumentException("Unknown render rule '$name'")
}
}
fun fromJson(input: JsonObject): RenderRule {
val params = ImmutableMap.builder<String, Any>()
for (key in input.keySet()) {
if (key != "type") {
val value = input[key] as? JsonPrimitive
if (value != null) {
if (value.isBoolean) {
params.put(key, value.asBoolean)
} else if (value.isNumber) {
params.put(key, value.asDouble)
} else {
params.put(key, value.asString)
}
}
}
}
return factory(input["type"].asString, params.build())
}
}
}
class RenderRuleEqualsSelf(params: Map<String, Any>) : RenderRule(params) {
override fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
val otherTile = getter[thisPos + offsetPos] ?: return inverse
if (inverse)
return otherTile.def != thisRef
return otherTile.def == thisRef
}
}
class RenderRuleShadows(params: Map<String, Any>) : RenderRule(params) {
override fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
return false // TODO
}
}
class AlwaysPassingRenderRule(params: Map<String, Any>) : RenderRule(params) {
override fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
return inverse
}
}
class AlwaysFailingRenderRule(params: Map<String, Any>) : RenderRule(params) {
override fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
return !inverse
}
}
enum class RenderRuleCombination {
ALL,
ANY
}
/**
* Правило рендера тайла
*
* root.rules[]
*/
data class TileRenderRule(
val name: String,
val join: RenderRuleCombination,
val pieces: List<RenderRule>
) {
fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
if (join == RenderRuleCombination.ANY) {
for (piece in pieces) {
if (piece.test(getter, thisRef, thisPos, offsetPos)) {
return true
}
}
return false
} else {
for (piece in pieces) {
if (!piece.test(getter, thisRef, thisPos, offsetPos)) {
return false
}
}
return true
}
}
companion object {
fun fromJson(name: String, input: JsonObject): TileRenderRule {
val join = input["join"]?.asString?.let {
when (it) {
"any" -> RenderRuleCombination.ANY
else -> RenderRuleCombination.ALL
}
} ?: RenderRuleCombination.ALL
val jEntries = input["entries"] as JsonArray
val pieces = ArrayList<RenderRule>(jEntries.size())
for (elem in jEntries) {
pieces.add(RenderRule.fromJson(elem.asJsonObject))
}
return TileRenderRule(name, join, ImmutableList.copyOf(pieces))
}
}
}
data class TileRenderMatchedPiece(
val piece: TileRenderPiece,
val offset: Vector2i
) {
companion object {
fun fromJson(input: JsonArray, tilePieces: Map<String, TileRenderPiece>): TileRenderMatchedPiece {
val piece = input[0].asString.let {
return@let tilePieces[it] ?: throw IllegalArgumentException("Unable to find render piece $it")
}
val offset = Vector2i.fromJson(input[1].asJsonArray)
return TileRenderMatchedPiece(piece, offset)
}
}
}
data class TileRenderMatchPositioned(
val condition: TileRenderRule,
val offset: Vector2i
) {
/**
* Состояние [condition] на [thisPos] с [offset]
*/
fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i): Boolean {
return condition.test(getter, thisRef, thisPos, offset)
}
companion object {
fun fromJson(input: JsonArray, rulePieces: Map<String, TileRenderRule>): TileRenderMatchPositioned {
val offset = Vector2i.fromJson(input[0].asJsonArray)
val condition = rulePieces[input[1].asString] ?: throw IllegalArgumentException("Rule ${input[1].asString} is missing!")
return TileRenderMatchPositioned(condition, offset)
}
}
}
data class TileRenderMatchPiece(
val pieces: List<TileRenderMatchedPiece>,
val matchAllPoints: List<TileRenderMatchPositioned>,
val subMatches: List<TileRenderMatchPiece>
) {
/**
* Возвращает, сработали ли ВСЕ [matchAllPoints]
*
* Если хотя бы один из них вернул false, весь тест возвращает false
*
* [subMatches] стоит итерировать только если это вернуло true
*/
fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i): Boolean {
for (matcher in matchAllPoints) {
if (!matcher.test(getter, thisRef, thisPos)) {
return false
}
}
return true
}
companion object {
fun fromJson(input: JsonObject, tilePieces: Map<String, TileRenderPiece>, rulePieces: Map<String, TileRenderRule>): TileRenderMatchPiece {
val pieces = input["pieces"]?.asJsonArray?.let {
val list = ArrayList<TileRenderMatchedPiece>()
for (thisPiece in it) {
list.add(TileRenderMatchedPiece.fromJson(thisPiece.asJsonArray, tilePieces))
}
return@let ImmutableList.copyOf(list)
} ?: listOf()
val matchAllPoints = input["matchAllPoints"]?.asJsonArray?.let {
val list = ArrayList<TileRenderMatchPositioned>()
for (thisPiece in it) {
list.add(TileRenderMatchPositioned.fromJson(thisPiece.asJsonArray, rulePieces))
}
return@let ImmutableList.copyOf(list)
} ?: listOf()
val subMatches = input["subMatches"]?.asJsonArray?.let {
val list = ArrayList<TileRenderMatchPiece>()
for (thisPiece in it) {
list.add(fromJson(thisPiece.asJsonObject, tilePieces, rulePieces))
}
return@let ImmutableList.copyOf(list)
} ?: listOf()
return TileRenderMatchPiece(pieces, matchAllPoints, subMatches)
}
}
}
data class TileRenderMatch(
val name: String,
val pieces: List<TileRenderMatchPiece>,
) {
companion object {
fun fromJson(input: JsonArray, tilePieces: Map<String, TileRenderPiece>, rulePieces: Map<String, TileRenderRule>): TileRenderMatch {
val name = input[0].asString
val pieces = ArrayList<TileRenderMatchPiece>()
for (elem in input[1].asJsonArray) {
pieces.add(TileRenderMatchPiece.fromJson(elem.asJsonObject, tilePieces, rulePieces))
}
return TileRenderMatch(name, ImmutableList.copyOf(pieces))
}
}
}
class TileRenderTemplateParseException(message: String, cause: Throwable?) : IllegalStateException(message, cause)
data class TileRenderTemplate(
val representativePiece: String,
val pieces: Map<String, TileRenderPiece>,
val rules: Map<String, TileRenderRule>,
val matches: Map<String, TileRenderMatch>,
) {
companion object {
val map = HashMap<String, TileRenderTemplate>()
fun load(path: String): TileRenderTemplate {
return map.computeIfAbsent(path) {
try {
val json = Starbound.loadJson(path).asJsonObject
return@computeIfAbsent fromJson(json)
} catch (err: Throwable) {
throw TileRenderTemplateParseException("Failed to load tile render definition from $path", err)
}
}
}
fun fromJson(input: JsonObject): TileRenderTemplate {
val representativePiece = input["representativePiece"].asString
val pieces = HashMap<String, TileRenderPiece>()
val rules = HashMap<String, TileRenderRule>()
val matches = HashMap<String, TileRenderMatch>()
val jPieces = input["pieces"] as JsonObject
for (key in jPieces.keySet()) {
pieces[key] = TileRenderPiece.fromJson(key, jPieces[key] as JsonObject)
}
val jRules = input["rules"] as JsonObject
for (key in jRules.keySet()) {
rules[key] = TileRenderRule.fromJson(key, jRules[key] as JsonObject)
}
val jMatches = input["matches"] as JsonArray
for (instance in jMatches) {
val deserialized = TileRenderMatch.fromJson(instance.asJsonArray, pieces, rules)
matches[deserialized.name] = deserialized
}
return TileRenderTemplate(representativePiece, ImmutableMap.copyOf(pieces), ImmutableMap.copyOf(rules), ImmutableMap.copyOf(matches))
}
}
}
data class TileRenderDefinition(
val texture: File,
val variants: Int,
val lightTransparent: Boolean,
val occludesBelow: Boolean,
val multiColored: Boolean,
val zLevel: Int,
val renderTemplate: TileRenderTemplate?
)
class TileRenderDefinitionBuilder {
var texture = ""
var variants = 1
var lightTransparent = false
var occludesBelow = false
var multiColored = false
var zLevel = 0
var renderTemplate: TileRenderTemplate? = null
fun build(directory: String? = null): TileRenderDefinition {
val newtexture: File
if (texture[0] == '/') {
// путь абсолютен
newtexture = File(texture)
} else {
if (directory != null) {
newtexture = File(directory, texture)
} else {
newtexture = File(texture)
}
}
return TileRenderDefinition(
texture = newtexture,
variants = variants,
lightTransparent = lightTransparent,
occludesBelow = occludesBelow,
multiColored = multiColored,
zLevel = zLevel,
renderTemplate = renderTemplate,
)
}
}