Move tile definition and render template to KConcreteTypeAdapter

This commit is contained in:
DBotThePony 2022-08-26 22:30:32 +07:00
parent d3396ddb7c
commit 7c318966d5
Signed by: DBot
GPG Key ID: DCC23B5715498507
15 changed files with 351 additions and 755 deletions

View File

@ -19,61 +19,9 @@ private val LOGGER = LogManager.getLogger()
fun main() {
LOGGER.info("Running LWJGL ${Version.getVersion()}")
if (true) {
//val pak = StarboundPak(File("J:\\SteamLibrary\\steamapps\\common\\Starbound\\assets\\packed.pak"))
//val json = JsonParser.parseReader(pak.getReader("/projectiles/traps/lowgravboostergas/lowgravboostergas.projectile"))
//val obj = Gson().fromJson(json, ProjectileDefinitionBuilder::class.java)
//println(obj.build())
//return
}
val db = BTreeDB(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world"))
//val db = BTreeDB(File("world.world"))
/*if (true) {
val a = System.currentTimeMillis()
val worldMeta = db.read(byteArrayOf(0, 0, 0, 0, 0))
println(System.currentTimeMillis() - a)
val inflater = Inflater()
inflater.setInput(worldMeta!!)
val output = ByteArray(1_000_000)
inflater.inflate(output)
val stream = DataInputStream(ByteArrayInputStream(output))
println("X tiles ${stream.readInt()}")
println("Y tiles ${stream.readInt()}")
val metadata = VersionedJSON(stream)
println(metadata.data)
return
}*/
/*if (true) {
val data = db.read(byteArrayOf(1, 0, 61, 0, 23))
val inflater = Inflater()
inflater.setInput(data!!)
val output = ByteArray(64_000)
val actual = inflater.inflate(output)
File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\tiles.dat").writeBytes(output)
val reader = DataInputStream(ByteArrayInputStream(output))
reader.skipBytes(3)
for (y in 0 .. 31) {
for (x in 0 .. 31) {
println("$x $y ${reader.readShort()}")
reader.skipBytes(29)
}
}
return
}*/
val client = StarboundClient()
//Starbound.addFilePath(File("./unpacked_assets/"))
@ -91,34 +39,6 @@ fun main() {
val ent = PlayerEntity(client.world!!)
Starbound.onInitialize {
if (true) {
val input = "{\n" +
" \"modId\" : 26,\n" +
" \"modName\" : \"aegisalt\",\n" +
" \"itemDrop\" : \"aegisaltore\",\n" +
" \"description\" : \"Aegisalt.\",\n" +
" \"health\" : 5,\n" +
" \"harvestLevel\" : 5,\n" +
" \"breaksWithTile\" : true,\n" +
"\n" +
" \"miningSounds\" : [ \"/sfx/tools/pickaxe_ore.ogg\", \"/sfx/tools/pickaxe_ore2.ogg\" ],\n" +
" \"miningParticle\" : \"orespark\",\n" +
"\n" +
" \"renderTemplate\" : \"/tiles/classicmaterialtemplate.config\",\n" +
" \"renderParameters\" : {\n" +
" \"texture\" : \"aegisalt.png\",\n" +
" \"variants\" : 8,\n" +
" \"multiColored\" : false,\n" +
" \"zLevel\" : 0\n" +
" }\n" +
"}\n"
val json = Starbound.gson.fromJson(input, MaterialModifier::class.java)
println(json)
return@onInitialize
}
var find = 0L
var set = 0L
var parse = 0L

View File

@ -12,6 +12,7 @@ import ru.dbotthepony.kstarbound.defs.projectile.*
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
import ru.dbotthepony.kstarbound.defs.tile.RenderParameters
import ru.dbotthepony.kstarbound.defs.tile.RenderTemplate
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.world.SkyParameters
import ru.dbotthepony.kstarbound.defs.world.dungeon.DungeonWorldDef
import ru.dbotthepony.kstarbound.io.*
@ -41,6 +42,15 @@ const val PIXELS_IN_STARBOUND_UNITf = 8.0f
object Starbound : IVFS {
private val LOGGER = LogManager.getLogger()
private val _readingFolder = ThreadLocal<String>()
/**
* Случит переменной для указания из какой папки происходит чтение asset'а в данном потоке
*/
var readingFolder: String?
get() = _readingFolder.get()
set(value) { _readingFolder.set(value) }
private val tiles = HashMap<String, TileDefinition>()
private val tilesByMaterialID = Int2ObjectAVLTreeMap<TileDefinition>()
private val projectiles = HashMap<String, ConfiguredProjectile>()
@ -77,6 +87,7 @@ object Starbound : IVFS {
.also(MaterialModifier::registerGson)
.also(RenderParameters::registerGson)
.also(RenderTemplate::registerGson)
.also(TileDefinition::registerGson)
.registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe())
@ -231,12 +242,15 @@ object Starbound : IVFS {
}
private fun loadTileMaterials(callback: (String) -> Unit) {
readingFolder = "/tiles/materials"
for (fs in fileSystems) {
for (listedFile in fs.listAllFilesWithExtension("material")) {
try {
callback("Loading $listedFile")
val tileDef = TileDefinitionBuilder.fromJson(JsonParser.parseReader(getReader(listedFile)) as JsonObject).build("/tiles/materials")
readingFolder = getPathFolder(listedFile)
val tileDef = gson.fromJson(getReader(listedFile), TileDefinition::class.java)
check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" }
check(tilesByMaterialID[tileDef.materialId] == null) { "Already has material with ID ${tileDef.materialId} loaded!" }
@ -248,6 +262,8 @@ object Starbound : IVFS {
}
}
}
readingFolder = null
}
private fun loadProjectiles(callback: (String) -> Unit) {

View File

@ -5,9 +5,9 @@ import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.gl.*
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.defs.TileRenderMatchPiece
import ru.dbotthepony.kstarbound.defs.TileRenderPiece
import ru.dbotthepony.kstarbound.defs.tile.RenderMatch
import ru.dbotthepony.kstarbound.defs.tile.RenderPiece
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.world.ITileChunk
import ru.dbotthepony.kvector.vector.Color
import ru.dbotthepony.kvector.vector.nint.Vector2i
@ -152,7 +152,7 @@ private enum class TileRenderTesselateResult {
}
class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
val texture = state.loadNamedTexture(tile.render.texture).also {
val texture = state.loadNamedTexture(tile.renderParameters.absoluteTexturePath).also {
it.textureMagFilter = GL_NEAREST
}
@ -160,7 +160,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
val bakedBackgroundProgramState = state.tileRenderers.background(texture)
// private var notifiedDepth = false
private fun tesselateAt(piece: TileRenderPiece, getter: ITileChunk, builder: DynamicVertexBuilder, pos: Vector2i, offset: Vector2i = Vector2i.ZERO) {
private fun tesselateAt(piece: RenderPiece, getter: ITileChunk, builder: DynamicVertexBuilder, pos: Vector2i, offset: Vector2i = Vector2i.ZERO) {
val fx = pos.x.toFloat()
val fy = pos.y.toFloat()
@ -173,7 +173,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
if (offset != Vector2i.ZERO) {
a += offset.x / PIXELS_IN_STARBOUND_UNITf
// в json файлах y указан как положительный вверх,
// в json файлах Y указан как положительный вверх,
// что соответствует нашему миру
b += offset.y / PIXELS_IN_STARBOUND_UNITf
@ -181,7 +181,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
d += offset.y / PIXELS_IN_STARBOUND_UNITf
}
if (tile.render.variants == 0 || piece.texture != null || piece.variantStride == null) {
if (tile.renderParameters.variants == 0 || piece.texture != null || piece.variantStride == null) {
val (u0, v0) = texture.pixelToUV(piece.texturePosition)
val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize)
@ -192,7 +192,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
d,
Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0))
} else {
val variant = (getter.randomDoubleFor(pos) * tile.render.variants).toInt()
val variant = (getter.randomDoubleFor(pos) * tile.renderParameters.variants).toInt()
val (u0, v0) = texture.pixelToUV(piece.texturePosition + piece.variantStride * variant)
val (u1, v1) = texture.pixelToUV(piece.texturePosition + piece.textureSize + piece.variantStride * variant)
@ -206,19 +206,19 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
}
}
private fun tesselatePiece(matchPiece: TileRenderMatchPiece, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, thisBuilder: DynamicVertexBuilder, background: Boolean): TileRenderTesselateResult {
private fun tesselatePiece(matchPiece: RenderMatch, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, thisBuilder: DynamicVertexBuilder, background: Boolean): TileRenderTesselateResult {
if (matchPiece.test(getter, tile, pos)) {
for (renderPiece in matchPiece.pieces) {
if (renderPiece.piece.texture != null) {
val program: BakedProgramState
if (background) {
program = state.tileRenderers.background(state.loadNamedTexture(renderPiece.piece.texture))
program = state.tileRenderers.background(state.loadNamedTexture(renderPiece.piece.texture!!))
} else {
program = state.tileRenderers.foreground(state.loadNamedTexture(renderPiece.piece.texture))
program = state.tileRenderers.foreground(state.loadNamedTexture(renderPiece.piece.texture!!))
}
tesselateAt(renderPiece.piece, getter, layers.getLayer(program, tile.render.zLevel) {
tesselateAt(renderPiece.piece, getter, layers.getLayer(program, tile.renderParameters.zLevel) {
return@getLayer DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS)
}, pos, renderPiece.offset)
} else {
@ -256,14 +256,14 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) {
fun tesselate(getter: ITileChunk, layers: TileLayerList, pos: Vector2i, background: Boolean = false) {
// если у нас нет renderTemplate
// то мы просто не можем его отрисовать
tile.render.renderTemplate ?: return
val template = tile.renderTemplate
val builder = layers.getLayer(if (background) bakedBackgroundProgramState else bakedProgramState, tile.render.zLevel) {
val builder = layers.getLayer(if (background) bakedBackgroundProgramState else bakedProgramState, tile.renderParameters.zLevel) {
return@getLayer DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS)
}
for ((_, matcher) in tile.render.renderTemplate.matches) {
for (matchPiece in matcher.pieces) {
for ((_, matcher) in template.matches) {
for (matchPiece in matcher) {
val matched = tesselatePiece(matchPiece, getter, layers, pos, builder, background)
if (matched == TileRenderTesselateResult.HALT) {

View File

@ -1,587 +0,0 @@
package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.world.ITileGetter
import ru.dbotthepony.kvector.vector.Color
import ru.dbotthepony.kvector.vector.nint.Vector2i
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 = Starbound.gson.fromJson(input["particleColor"], Color::class.java)
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: String?,
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 it
}
val textureSize = Starbound.gson.fromJson(input["textureSize"], Vector2i::class.java)
val texturePosition = Starbound.gson.fromJson(input["texturePosition"], Vector2i::class.java)
val colorStride = input["colorStride"]?.let { Starbound.gson.fromJson(it, Vector2i::class.java) }
val variantStride = input["variantStride"]?.let { Starbound.gson.fromJson(it, Vector2i::class.java) }
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: ITileGetter, 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" -> RenderRuleConnects(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: ITileGetter, 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: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
return false // TODO
}
}
class RenderRuleConnects(params: Map<String, Any>) : RenderRule(params) {
override fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
if (inverse)
return getter[thisPos + offsetPos] == null
return getter[thisPos + offsetPos] != null
}
}
class AlwaysPassingRenderRule(params: Map<String, Any>) : RenderRule(params) {
override fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
return inverse
}
}
class AlwaysFailingRenderRule(params: Map<String, Any>) : RenderRule(params) {
override fun test(getter: ITileGetter, 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: ITileGetter, 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 = Starbound.gson.fromJson(input[1], Vector2i::class.java)
return TileRenderMatchedPiece(piece, offset)
}
}
}
data class TileRenderMatchPositioned(
val condition: TileRenderRule,
val offset: Vector2i
) {
/**
* Состояние [condition] на [thisPos] с [offset]
*/
fun test(getter: ITileGetter, 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 = Starbound.gson.fromJson(input[0], Vector2i::class.java)
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 matchAnyPoints: List<TileRenderMatchPositioned>,
val subMatches: List<TileRenderMatchPiece>,
val haltOnSubMatch: Boolean,
val haltOnMatch: Boolean
) {
init {
if (matchAnyPoints.isNotEmpty() || matchAllPoints.isNotEmpty()) {
require(matchAnyPoints.isEmpty() || matchAllPoints.isEmpty()) { "Both matchAllPoints and matchAnyPoints are present, this is not valid." }
}
}
/**
* Возвращает, сработали ли ВСЕ [matchAllPoints] или ЛЮБОЙ ИЗ [matchAnyPoints]
*
* Если хотя бы один из них вернул false, весь тест возвращает false
*
* [subMatches] стоит итерировать только если это вернуло true
*/
fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i): Boolean {
for (matcher in matchAllPoints) {
if (!matcher.test(getter, thisRef, thisPos)) {
return false
}
}
if (matchAnyPoints.isEmpty()) {
return true
}
for (matcher in matchAnyPoints) {
if (matcher.test(getter, thisRef, thisPos)) {
return true
}
}
return false
}
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 matchAnyPoints = input["matchAnyPoints"]?.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()
val haltOnSubMatch = input["haltOnSubMatch"]?.asBoolean ?: false
val haltOnMatch = input["haltOnMatch"]?.asBoolean ?: false
return TileRenderMatchPiece(
pieces = pieces,
matchAllPoints = matchAllPoints,
matchAnyPoints = matchAnyPoints,
subMatches = subMatches,
haltOnSubMatch = haltOnSubMatch,
haltOnMatch = haltOnMatch,
)
}
}
}
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>()
try {
for (elem in input[1].asJsonArray) {
pieces.add(TileRenderMatchPiece.fromJson(elem.asJsonObject, tilePieces, rulePieces))
}
} catch(err: Throwable) {
throw IllegalArgumentException("Failed to deserialize render match rule $name", err)
}
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 register(builder: GsonBuilder) {
builder.registerTypeAdapter(TileRenderTemplate::class.java, object : TypeAdapter<TileRenderTemplate>() {
override fun write(out: JsonWriter?, value: TileRenderTemplate?) {
TODO("Not yet implemented")
}
override fun read(`in`: JsonReader): TileRenderTemplate {
return fromJson(TypeAdapters.JSON_ELEMENT.read(`in`) as JsonObject)
}
})
}
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: String,
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: String
if (texture[0] == '/') {
// путь абсолютен
newtexture = texture
} else {
if (directory != null) {
newtexture = "$directory/$texture"
} else {
newtexture = texture
}
}
return TileRenderDefinition(
texture = newtexture,
variants = variants,
lightTransparent = lightTransparent,
occludesBelow = occludesBelow,
multiColored = multiColored,
zLevel = zLevel,
renderTemplate = renderTemplate,
)
}
}

View File

@ -16,6 +16,10 @@ data class MaterialModifier(
val renderTemplate: RenderTemplate,
val renderParameters: RenderParameters
) {
init {
require(modId > 0) { "Invalid material modifier ID $modId" }
}
companion object {
val ADAPTER = KConcreteTypeAdapter.Builder(MaterialModifier::class)
.plain(MaterialModifier::modId)
@ -25,7 +29,7 @@ data class MaterialModifier(
.plain(MaterialModifier::health)
.plain(MaterialModifier::harvestLevel)
.plain(MaterialModifier::breaksWithTile)
.list(MaterialModifier::miningSounds, String::class.java)
.list(MaterialModifier::miningSounds, String::class)
.plain(MaterialModifier::miningParticle)
.plain(MaterialModifier::renderTemplate, RenderTemplate.CACHE)
.plain(MaterialModifier::renderParameters)

View File

@ -1,20 +1,37 @@
package ru.dbotthepony.kstarbound.defs.tile
import com.google.gson.GsonBuilder
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter
data class RenderParameters(
val texture: String,
val variants: Int,
val multiColored: Boolean,
val variants: Int = 0,
val multiColored: Boolean = false,
val occludesBelow: Boolean = false,
val lightTransparent: Boolean = false,
val zLevel: Int,
) {
val absoluteTexturePath: String
init {
val dir = Starbound.readingFolder
if (dir == null || texture[0] == '/') {
absoluteTexturePath = texture
} else {
absoluteTexturePath = "$dir/$texture"
}
}
companion object {
val ADAPTER = KConcreteTypeAdapter.Builder(RenderParameters::class)
.plain(
RenderParameters::texture,
RenderParameters::variants,
RenderParameters::multiColored,
RenderParameters::occludesBelow,
RenderParameters::lightTransparent,
RenderParameters::zLevel,
)
.build()

View File

@ -6,9 +6,13 @@ 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.defs.TileDefinition
import ru.dbotthepony.kstarbound.io.CustomEnumTypeAdapter
import ru.dbotthepony.kstarbound.io.EnumAdapter
import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter
import ru.dbotthepony.kstarbound.util.WriteOnce
import ru.dbotthepony.kstarbound.world.ITileGetter
import ru.dbotthepony.kvector.vector.nint.Vector2i
import java.util.concurrent.ConcurrentHashMap
@ -33,80 +37,184 @@ data class RenderPiece(
}
}
data class RenderRule(
val type: String,
val matchHue: Boolean = false,
val inverse: Boolean = false,
) {
private fun doTest(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
return when (type) {
"EqualsSelf" -> getter[thisPos + offsetPos]?.def == thisRef
"Connects" -> getter[thisPos + offsetPos] != null
else -> false
}
}
fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
if (inverse) {
return !doTest(getter, thisRef, thisPos, offsetPos)
}
return doTest(getter, thisRef, thisPos, offsetPos)
}
companion object {
val ADAPTER = KConcreteTypeAdapter.Builder(RenderRule::class)
.plain(
RenderRule::type,
RenderRule::matchHue,
RenderRule::inverse,
)
.build()
}
}
data class RenderRuleList(
val entries: List<RenderRule>
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, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
return when (type) {
"EqualsSelf" -> getter[thisPos + offsetPos]?.def == thisRef
"Connects" -> getter[thisPos + offsetPos] != null
else -> {
if (LOGGED.add(type)) {
LOGGER.error("Unknown render rule test $type!")
}
false
}
}
}
fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
if (inverse) {
return !doTest(getter, thisRef, thisPos, offsetPos)
}
return doTest(getter, thisRef, thisPos, offsetPos)
}
companion object {
private val LOGGER = LogManager.getLogger()
private val LOGGED = ObjectArraySet<String>()
val ADAPTER = KConcreteTypeAdapter.Builder(Entry::class)
.plain(
Entry::type,
Entry::matchHue,
Entry::inverse,
)
.build()
}
}
fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offset: Vector2i): Boolean {
when (join) {
Combination.ALL -> {
for (entry in entries) {
if (!entry.test(getter, thisRef, thisPos, offset)) {
return false
}
}
return true
}
Combination.ANY -> {
for (entry in entries) {
if (entry.test(getter, thisRef, thisPos, offset)) {
return true
}
}
return false
}
}
}
companion object {
val ADAPTER = KConcreteTypeAdapter.Builder(RenderRuleList::class)
.list(RenderRuleList::entries, RenderRule::class)
.list(RenderRuleList::entries, Entry::class)
.plain(RenderRuleList::join)
.build()
}
}
data class RenderMatch(
val pieces: List<Piece>? = null,
val matchAllPoints: List<Matcher>? = null,
val subMatches: List<RenderMatch>? = null,
val haltOnMatch: Boolean = false
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 point: Vector2i
)
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 point: Vector2i,
val offset: Vector2i,
val ruleName: String
)
) {
var rule by WriteOnce<RenderRuleList>()
fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i): Boolean {
return rule.test(getter, thisRef, 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
*/
fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i): Boolean {
for (matcher in matchAllPoints) {
if (!matcher.test(getter, thisRef, thisPos)) {
return false
}
}
if (matchAnyPoints.isEmpty()) {
return true
}
for (matcher in matchAnyPoints) {
if (matcher.test(getter, thisRef, thisPos)) {
return true
}
}
return false
}
companion object {
val ADAPTER = KConcreteTypeAdapter.Builder(RenderMatch::class)
.list(RenderMatch::pieces, Piece::class)
.list(RenderMatch::matchAllPoints, Matcher::class)
.list(RenderMatch::matchAnyPoints, Matcher::class)
.list(RenderMatch::subMatches, RenderMatch::class)
.plain(RenderMatch::haltOnMatch)
.plain(RenderMatch::haltOnSubMatch)
.build()
val PIECE_ADAPTER = KConcreteTypeAdapter.Builder(Piece::class)
.plain(Piece::name)
.plain(Piece::point)
.plain(Piece::offset)
.build(asList = true)
val MATCHER_ADAPTER = KConcreteTypeAdapter.Builder(Matcher::class)
.plain(Matcher::point)
.plain(Matcher::offset)
.plain(Matcher::ruleName)
.build(asList = true)
}
@ -116,6 +224,12 @@ 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 = KConcreteTypeAdapter.Builder(RenderMatchList::class)
.plain(RenderMatchList::name)
@ -127,18 +241,26 @@ data class RenderMatchList(
data class RenderTemplate(
val pieces: Map<String, RenderPiece>,
val representativePiece: String,
val matches: List<RenderMatchList>
val matches: List<RenderMatchList>,
val rules: Map<String, RenderRuleList>,
) {
init {
for (value in matches) {
value.resolve(this)
}
}
companion object {
val ADAPTER = KConcreteTypeAdapter.Builder(RenderTemplate::class)
.map(RenderTemplate::pieces, RenderPiece::class.java)
.plain(RenderTemplate::representativePiece)
.list(RenderTemplate::matches, RenderMatchList::class.java)
.map(RenderTemplate::rules, RenderRuleList::class.java)
.build()
fun registerGson(gsonBuilder: GsonBuilder) {
RenderPiece.ADAPTER.register(gsonBuilder)
RenderRule.ADAPTER.register(gsonBuilder)
RenderRuleList.Entry.ADAPTER.register(gsonBuilder)
RenderRuleList.ADAPTER.register(gsonBuilder)
RenderMatch.ADAPTER.register(gsonBuilder)
RenderMatch.PIECE_ADAPTER.register(gsonBuilder)
@ -146,17 +268,11 @@ data class RenderTemplate(
RenderMatchList.ADAPTER.register(gsonBuilder)
ADAPTER.register(gsonBuilder)
gsonBuilder.registerTypeAdapter(RenderRuleList.Combination::class.java, EnumAdapter(RenderRuleList.Combination::class.java))
}
private val cache = ConcurrentHashMap<String, RenderTemplate>()
private val _readingFolder = ThreadLocal<String>()
/**
* Случит переменной для указания из какой папки происходит чтение в данном потоке
*/
var readingFolder: String?
get() = _readingFolder.get()
set(value) { _readingFolder.set(value) }
val CACHE = object : TypeAdapter<RenderTemplate>() {
override fun write(out: JsonWriter, value: RenderTemplate) {
@ -173,7 +289,7 @@ data class RenderTemplate(
if (path[0] != '/') {
// относительный путь
val readingFolder = readingFolder ?: throw NullPointerException("Currently read folder is not specified")
val readingFolder = Starbound.readingFolder ?: throw NullPointerException("Currently read folder is not specified")
path = "$readingFolder/$path"
}

View File

@ -0,0 +1,50 @@
package ru.dbotthepony.kstarbound.defs.tile
import com.google.gson.GsonBuilder
import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter
import ru.dbotthepony.kvector.vector.Color
data class TileDefinition(
val materialId: Int,
val materialName: String,
val particleColor: Color? = null,
val itemDrop: String? = null,
val description: String = "...",
val shortdescription: String = "...",
val footstepSound: String? = null,
val blocksLiquidFlow: Boolean = true,
val soil: Boolean = false,
val health: Double = 0.0,
val category: String,
val renderTemplate: RenderTemplate,
val renderParameters: RenderParameters,
) {
companion object {
val ADAPTER = KConcreteTypeAdapter.Builder(TileDefinition::class)
.plain(
TileDefinition::materialId,
TileDefinition::materialName,
TileDefinition::particleColor,
TileDefinition::itemDrop,
TileDefinition::description,
TileDefinition::shortdescription,
TileDefinition::footstepSound,
TileDefinition::blocksLiquidFlow,
TileDefinition::soil,
TileDefinition::health,
TileDefinition::category
)
.plain(TileDefinition::renderTemplate, RenderTemplate.CACHE)
.plain(
TileDefinition::renderParameters,
)
.build()
fun registerGson(gsonBuilder: GsonBuilder) {
ADAPTER.register(gsonBuilder)
}
}
}

View File

@ -0,0 +1,40 @@
package ru.dbotthepony.kstarbound.io
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
class EnumAdapter<T : Enum<T>>(private val enum: Class<T>) : TypeAdapter<T>() {
private val mapping: ImmutableMap<String, T> = Object2ObjectArrayMap<String, T>().let {
for (value in enum.enumConstants) {
it[value.name] = value
it[value.name.uppercase()] = value
it[value.name.lowercase()] = value
val spaced = value.name.replace('_', ' ')
val stitched = value.name.replace("_", "")
it[spaced] = value
it[spaced.uppercase()] = value
it[spaced.lowercase()] = value
it[stitched] = value
it[stitched.uppercase()] = value
it[stitched.lowercase()] = value
}
ImmutableMap.copyOf(it)
}
override fun write(out: JsonWriter, value: T) {
out.value(value.name)
}
override fun read(`in`: JsonReader): T {
val key = `in`.nextString()
return mapping[key] ?: throw JsonSyntaxException("Unable to match '$key' against ${enum.canonicalName}")
}
}

View File

@ -294,7 +294,7 @@ class KConcreteTypeAdapter<T : Any>(
readValues[fieldId] = adapter.read(reader)
presentValues[fieldId] = true
} catch(err: Throwable) {
throw JsonSyntaxException("Exception reading field ${field.name}", err)
throw JsonSyntaxException("Exception reading field ${field.name} for ${bound.qualifiedName}", err)
}
fieldId++
@ -317,7 +317,7 @@ class KConcreteTypeAdapter<T : Any>(
readValues[fieldId] = adapter.read(reader)
presentValues[fieldId] = true
} catch(err: Throwable) {
throw JsonSyntaxException("Exception reading field ${field.name}", err)
throw JsonSyntaxException("Exception reading field ${field.name} for ${bound.qualifiedName}", err)
}
}
}
@ -341,11 +341,11 @@ class KConcreteTypeAdapter<T : Any>(
if (readValues[i] == null) {
if (!returnTypeCache[field]!!.isMarkedNullable) {
throw JsonSyntaxException("Field ${field.name} does not accept nulls")
throw JsonSyntaxException("Field ${field.name} of ${bound.qualifiedName} does not accept nulls")
}
if (!regularFactory.parameters[i].isOptional && !presentValues[i]) {
throw JsonSyntaxException("Field ${field.name} must be defined (even just as null)")
throw JsonSyntaxException("Field ${field.name} of ${bound.qualifiedName} must be defined (even just as null)")
}
}
}
@ -388,7 +388,7 @@ class KConcreteTypeAdapter<T : Any>(
val param = regularFactory.parameters[i]
if (!param.isOptional && !presentValues[i]) {
throw JsonSyntaxException("Field ${field.name} is missing")
throw JsonSyntaxException("Field ${field.name} of ${bound.qualifiedName} is missing")
}
if (returnTypeCache[field]!!.isMarkedNullable) {
@ -396,11 +396,11 @@ class KConcreteTypeAdapter<T : Any>(
}
if (param.isOptional && !presentValues[i]) {
copied[i] = syntheticPrimitives[i] ?: throw NullPointerException("HOW $i")
copied[i] = syntheticPrimitives[i]
continue
}
throw JsonSyntaxException("Field ${field.name} does not accept nulls")
throw JsonSyntaxException("Field ${field.name} of ${bound.qualifiedName} does not accept nulls")
}
return syntheticFactory.newInstance(*copied)

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.util
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class WriteOnce<V> : ReadWriteProperty<Any, V> {
private var value: V? = null
override fun getValue(thisRef: Any, property: KProperty<*>): V {
return value ?: throw IllegalStateException("Property ${property.name} is not initialized")
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: V) {
if (this.value != null) {
throw IllegalStateException("Already initialized")
}
this.value = value
}
}

View File

@ -4,7 +4,7 @@ import ru.dbotthepony.kbox2d.api.BodyDef
import ru.dbotthepony.kbox2d.api.FixtureDef
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
import ru.dbotthepony.kbox2d.dynamics.B2Fixture
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kstarbound.world.phys.RectTileFlooderDepthFirst
import ru.dbotthepony.kstarbound.world.phys.RectTileFlooderSizeFirst

View File

@ -1,6 +1,6 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kvector.vector.nint.Vector2i
const val CHUNK_SHIFT = 5

View File

@ -1,6 +1,6 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
/**
* Предоставляет доступ к чанку и его соседям

View File

@ -10,7 +10,7 @@ import ru.dbotthepony.kbox2d.dynamics.B2Fixture
import ru.dbotthepony.kbox2d.dynamics.B2World
import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.util.Timer
import ru.dbotthepony.kstarbound.world.entities.CollisionResolution