314 lines
8.3 KiB
Kotlin
314 lines
8.3 KiB
Kotlin
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<Entry>,
|
||
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<String>()
|
||
|
||
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<Piece> = listOf(),
|
||
val matchAllPoints: List<Matcher> = listOf(),
|
||
val matchAnyPoints: List<Matcher> = listOf(),
|
||
val subMatches: List<RenderMatch> = listOf(),
|
||
val haltOnMatch: Boolean = false,
|
||
val haltOnSubMatch: Boolean = false,
|
||
) {
|
||
data class Piece(
|
||
val name: String,
|
||
val offset: Vector2i
|
||
) {
|
||
var piece by WriteOnce<RenderPiece>()
|
||
|
||
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<RenderRuleList>()
|
||
|
||
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<RenderMatch>
|
||
) {
|
||
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<String, RenderPiece>,
|
||
val representativePiece: String,
|
||
val matches: List<RenderMatchList>,
|
||
val rules: Map<String, RenderRuleList>,
|
||
) {
|
||
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<String, RenderTemplate>()
|
||
|
||
val CACHE = object : TypeAdapter<RenderTemplate>() {
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|