package ru.dbotthepony.kstarbound.defs.tile import com.google.gson.GsonBuilder import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.ObjectArraySet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.io.json.builder.EnumAdapter import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.registerTypeAdapter import ru.dbotthepony.kstarbound.util.WriteOnce import ru.dbotthepony.kstarbound.world.ITileGetter import ru.dbotthepony.kstarbound.world.ITileState import ru.dbotthepony.kvector.vector.nint.Vector2i import java.util.concurrent.ConcurrentHashMap data class RenderPiece( val texture: String? = null, val textureSize: Vector2i, val texturePosition: Vector2i, val colorStride: Vector2i? = null, val variantStride: Vector2i? = null, ) { companion object { val ADAPTER = FactoryAdapter.Builder(RenderPiece::class) .auto( RenderPiece::texture, RenderPiece::textureSize, RenderPiece::texturePosition, RenderPiece::colorStride, RenderPiece::variantStride, ) .build() } } fun interface EqualityRuleTester { fun test(thisTile: ITileState, otherTile: ITileState): Boolean } data class RenderRuleList( val entries: List, val join: Combination = Combination.ALL ) { enum class Combination { ALL, ANY; } data class Entry( val type: String, val matchHue: Boolean = false, val inverse: Boolean = false, ) { private fun doTest(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { return when (type) { "EqualsSelf" -> equalityTester.test(getter[thisPos], getter[thisPos + offsetPos]) "Connects" -> getter[thisPos + offsetPos].material != null else -> { if (LOGGED.add(type)) { LOGGER.error("Unknown render rule test $type!") } false } } } fun test(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { if (inverse) { return !doTest(getter, equalityTester, thisPos, offsetPos) } return doTest(getter, equalityTester, thisPos, offsetPos) } companion object { private val LOGGER = LogManager.getLogger() private val LOGGED = ObjectArraySet() val ADAPTER = FactoryAdapter.Builder(Entry::class) .auto( Entry::type, Entry::matchHue, Entry::inverse, ) .build() } } fun test(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i, offset: Vector2i): Boolean { when (join) { Combination.ALL -> { for (entry in entries) { if (!entry.test(getter, equalityTester, thisPos, offset)) { return false } } return true } Combination.ANY -> { for (entry in entries) { if (entry.test(getter, equalityTester, thisPos, offset)) { return true } } return false } } } companion object { val ADAPTER = FactoryAdapter.Builder(RenderRuleList::class) .autoList(RenderRuleList::entries) .auto(RenderRuleList::join) .build() } } data class RenderMatch( val pieces: List = listOf(), val matchAllPoints: List = listOf(), val matchAnyPoints: List = listOf(), val subMatches: List = listOf(), val haltOnMatch: Boolean = false, val haltOnSubMatch: Boolean = false, ) { data class Piece( val name: String, val offset: Vector2i ) { var piece by WriteOnce() fun resolve(template: RenderTemplate) { piece = template.pieces[name] ?: throw IllegalStateException("Unable to find render piece $name") } } data class Matcher( val offset: Vector2i, val ruleName: String ) { var rule by WriteOnce() fun test(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { return rule.test(getter, equalityTester, thisPos, offset) } fun resolve(template: RenderTemplate) { rule = template.rules[ruleName] ?: throw IllegalStateException("Unable to find render rule $ruleName") } } fun resolve(template: RenderTemplate) { for (value in matchAllPoints) { value.resolve(template) } for (value in pieces) { value.resolve(template) } for (value in matchAnyPoints) { value.resolve(template) } for (value in subMatches) { value.resolve(template) } } /** * Возвращает, сработали ли ВСЕ [matchAllPoints] или ЛЮБОЙ ИЗ [matchAnyPoints] * * Если хотя бы один из них вернул false, весь тест возвращает false * * [subMatches] стоит итерировать только если это вернуло true * * [equalityTester] требуется для проверки раенства между "этим" тайлом и другим */ fun test(tileAccess: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { for (matcher in matchAllPoints) { if (!matcher.test(tileAccess, equalityTester, thisPos)) { return false } } if (matchAnyPoints.isEmpty()) { return true } for (matcher in matchAnyPoints) { if (matcher.test(tileAccess, equalityTester, thisPos)) { return true } } return false } companion object { val ADAPTER = FactoryAdapter.Builder(RenderMatch::class) .autoList(RenderMatch::pieces) .autoList(RenderMatch::matchAllPoints) .autoList(RenderMatch::matchAnyPoints) .autoList(RenderMatch::subMatches) .auto(RenderMatch::haltOnMatch) .auto(RenderMatch::haltOnSubMatch) .build() val PIECE_ADAPTER = FactoryAdapter.Builder(Piece::class) .auto(Piece::name) .auto(Piece::offset) .inputAsList() .build() val MATCHER_ADAPTER = FactoryAdapter.Builder(Matcher::class) .auto(Matcher::offset) .auto(Matcher::ruleName) .inputAsList() .build() } } data class RenderMatchList( val name: String, val list: List ) { fun resolve(template: RenderTemplate) { for (value in list) { value.resolve(template) } } companion object { val ADAPTER = FactoryAdapter.Builder(RenderMatchList::class) .auto(RenderMatchList::name) .autoList(RenderMatchList::list) .inputAsList() .build() } } data class RenderTemplate( val pieces: Map, val representativePiece: String, val matches: List, val rules: Map, ) { init { for (value in matches) { value.resolve(this) } } companion object { val ADAPTER = FactoryAdapter.Builder(RenderTemplate::class) .mapAsObject(RenderTemplate::pieces, RenderPiece::class.java) .auto(RenderTemplate::representativePiece) .list(RenderTemplate::matches, RenderMatchList::class.java) .mapAsObject(RenderTemplate::rules, RenderRuleList::class.java) .build() fun registerGson(gsonBuilder: GsonBuilder) { gsonBuilder.registerTypeAdapter(RenderPiece.ADAPTER) gsonBuilder.registerTypeAdapter(RenderRuleList.ADAPTER) gsonBuilder.registerTypeAdapter(RenderMatch.ADAPTER) gsonBuilder.registerTypeAdapter(RenderMatch.PIECE_ADAPTER) gsonBuilder.registerTypeAdapter(RenderMatch.MATCHER_ADAPTER) gsonBuilder.registerTypeAdapter(RenderMatchList.ADAPTER) gsonBuilder.registerTypeAdapter(RenderRuleList.Entry.ADAPTER) gsonBuilder.registerTypeAdapter(ADAPTER) gsonBuilder.registerTypeAdapter(EnumAdapter(RenderRuleList.Combination::class.java)) } private val cache = ConcurrentHashMap() val CACHE = object : TypeAdapter() { override fun write(out: JsonWriter, value: RenderTemplate) { ADAPTER.write(out, value) } override fun read(reader: JsonReader): RenderTemplate { if (reader.peek() != JsonToken.STRING) { throw JsonSyntaxException("Expected string as input for render template cache retriever") } var path = reader.nextString() if (path[0] != '/') { // относительный путь val readingFolder = Starbound.assetFolder ?: throw NullPointerException("Currently read folder is not specified") path = "$readingFolder/$path" } return cache.computeIfAbsent(path) { return@computeIfAbsent Starbound.GSON.fromJson(Starbound.locate(it).reader(), RenderTemplate::class.java) } } } } }