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, 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>() var footstepSound: String? = null var health = 0 var category = "generic" val render = TileRenderDefinitionBuilder() fun build(directory: String? = null): TileDefinition { return TileDefinition( racialDescription = ImmutableMap.builder().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) { 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): 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() 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) : 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) : RenderRule(params) { override fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean { return false // TODO } } class AlwaysPassingRenderRule(params: Map) : RenderRule(params) { override fun test(getter: IChunk, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean { return inverse } } class AlwaysFailingRenderRule(params: Map) : 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 ) { 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(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): 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): 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, val matchAllPoints: List, val subMatches: List ) { /** * Возвращает, сработали ли ВСЕ [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, rulePieces: Map): TileRenderMatchPiece { val pieces = input["pieces"]?.asJsonArray?.let { val list = ArrayList() 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() 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() 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, ) { companion object { fun fromJson(input: JsonArray, tilePieces: Map, rulePieces: Map): TileRenderMatch { val name = input[0].asString val pieces = ArrayList() 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, val rules: Map, val matches: Map, ) { companion object { val map = HashMap() 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() val rules = HashMap() val matches = HashMap() 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, ) } }