519 lines
15 KiB
Kotlin
519 lines
15 KiB
Kotlin
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,
|
||
)
|
||
}
|
||
}
|