diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 1cd080a3..2fa1768b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -4,7 +4,6 @@ import org.apache.logging.log4j.LogManager import org.lwjgl.Version import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.io.* import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.entities.PlayerEntity @@ -76,7 +75,17 @@ fun main() { hitTile = true } - reader.skipBytes(5) + reader.skipBytes(1) // Foreground hue shift + reader.skipBytes(1) // Foreground color variant + + val modifier = reader.readShort() + val getModifier = Starbound.tileModifiersByIDAccess[modifier.toInt()] + + if (getModifier != null && getMat != null) { + chunk.foreground[x, y]?.modifier = getModifier + } + + reader.skipBytes(1) // Foreground mod hue shift val materialID2 = reader.readShort() val getMat2 = Starbound.tilesAccessID[materialID2.toInt()] @@ -86,7 +95,17 @@ fun main() { hitTile = true } - reader.skipBytes(22) + reader.skipBytes(1) // Background hue shift + reader.skipBytes(1) // Background color variant + + val modifier2 = reader.readShort() + val getModifier2 = Starbound.tileModifiersByIDAccess[modifier2.toInt()] + + if (getModifier2 != null && getMat2 != null) { + chunk.background[x, y]?.modifier = getModifier2 + } + + reader.skipBytes(18) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 98c3fd22..d5e1695a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -53,10 +53,16 @@ object Starbound : IVFS { private val tiles = HashMap() private val tilesByMaterialID = Int2ObjectAVLTreeMap() + + private val tileModifiers = HashMap() + private val tileModifiersByID = Int2ObjectAVLTreeMap() + private val projectiles = HashMap() private val parallax = HashMap() private val functions = HashMap() + val tileModifiersAccess: Map = Collections.unmodifiableMap(tileModifiers) + val tileModifiersByIDAccess: Map = Collections.unmodifiableMap(tileModifiersByID) val tilesAccess: Map = Collections.unmodifiableMap(tiles) val tilesAccessID: Map = Collections.unmodifiableMap(tilesByMaterialID) val projectilesAccess: Map = Collections.unmodifiableMap(projectiles) @@ -176,6 +182,7 @@ object Starbound : IVFS { loadStage(callback, this::loadTileMaterials, "materials") loadStage(callback, this::loadProjectiles, "projectiles") loadStage(callback, this::loadParallax, "parallax definitions") + loadStage(callback, this::loadMaterialModifiers, "material modifier definitions") initializing = false initialized = true @@ -320,8 +327,27 @@ object Starbound : IVFS { } private fun loadMaterialModifiers(callback: (String) -> Unit) { - for (fs in fileSystems) { + readingFolder = "/tiles/materials" + for (fs in fileSystems) { + for (listedFile in fs.listAllFilesWithExtension("matmod")) { + try { + callback("Loading $listedFile") + + readingFolder = getPathFolder(listedFile) + val tileDef = gson.fromJson(getReader(listedFile), MaterialModifier::class.java) + + check(tileModifiers[tileDef.modName] == null) { "Already has material with name ${tileDef.modName} loaded!" } + check(tileModifiersByID[tileDef.modId] == null) { "Already has material with ID ${tileDef.modId} loaded!" } + tileModifiersByID[tileDef.modId] = tileDef + tileModifiers[tileDef.modName] = tileDef + } catch (err: Throwable) { + //throw TileDefLoadingException("Loading tile file $listedFile", err) + LOGGER.error("Loading tile modifier file $listedFile", err) + } + } } + + readingFolder = null } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt index b93856f9..28129620 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt @@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kvector.matrix.Matrix4fStack import ru.dbotthepony.kvector.vector.ndouble.Vector2d import java.io.Closeable +import java.util.LinkedList /** * Псевдо zPos у фоновых тайлов @@ -24,7 +25,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk Int, private val isBackground: Boolean) : AutoCloseable { private val layers = TileLayerList() - val bakedMeshes = ArrayList>() + val bakedMeshes = LinkedList>() private var changeset = -1 fun bake(view: ITileChunk) { @@ -46,8 +47,13 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk>() + private val layers = HashMap>() - fun getLayer(programState: BakedProgramState, zLevel: Int, compute: () -> DynamicVertexBuilder): DynamicVertexBuilder { - val list = layers.computeIfAbsent(programState) {ArrayList()} - - for (layer in list) { - if (layer.zPos == zLevel) { - return layer.vertexBuilder - } - } - - val computed = TileLayer(programState, compute.invoke(), zLevel) - list.add(computed) - return computed.vertexBuilder + /** + * Получает геометрию слоя ([DynamicVertexBuilder]), который имеет программу для отрисовки [program] и располагается на [zLevel]. + * + * Если такого слоя нет, вызывается [compute] и создаётся новый [TileLayer], затем возвращается результат [compute]. + */ + fun computeIfAbsent(program: ConfiguredShaderProgram, zLevel: Int, compute: () -> DynamicVertexBuilder): DynamicVertexBuilder { + return layers.computeIfAbsent(program) { Int2ObjectAVLTreeMap() }.computeIfAbsent(zLevel, Int2ObjectFunction { + return@Int2ObjectFunction TileLayer(program, compute.invoke(), zLevel) + }).vertexBuilder } - fun buildList(): List { + fun buildSortedLayerList(): List { val list = ArrayList() for (getList in layers.values) { - list.addAll(getList) + list.addAll(getList.values) } list.sortBy { @@ -56,19 +55,32 @@ class TileLayerList { val isNotEmpty get() = layers.isNotEmpty() } +/** + * Хранит в себе программы для отрисовки определённых [TileDefinition] + * + * Создаётся единожды как потомок [GLStateTracker] + */ class TileRenderers(val state: GLStateTracker) { private val foregroundTilePrograms = HashMap() private val backgroundTilePrograms = HashMap() - private val tileRenderers = HashMap() + private val tileRenderersCache = HashMap() + private val modifierRenderersCache = HashMap() - operator fun get(tile: String): TileRenderer { - return tileRenderers.computeIfAbsent(tile) { - val def = Starbound.getTileDefinition(tile) // TODO: Пустой рендерер + fun getTileRenderer(defName: String): TileRenderer { + return tileRenderersCache.computeIfAbsent(defName) { + val def = Starbound.tilesAccess[defName] // TODO: Пустой рендерер return@computeIfAbsent TileRenderer(state, def!!) } } - private inner class ForegroundTileProgram(private val texture: GLTexture2D) : BakedProgramState(state.shaderVertexTexture) { + fun getModifierRenderer(defName: String): TileRenderer { + return modifierRenderersCache.computeIfAbsent(defName) { + val def = Starbound.tileModifiersAccess[defName] // TODO: Пустой рендерер + return@computeIfAbsent TileRenderer(state, def!!) + } + } + + private inner class ForegroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.shaderVertexTexture) { override fun setup() { super.setup() state.activeTexture = 0 @@ -96,7 +108,7 @@ class TileRenderers(val state: GLStateTracker) { } } - private inner class BackgroundTileProgram(private val texture: GLTexture2D) : BakedProgramState(state.shaderVertexTextureColor) { + private inner class BackgroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.shaderVertexTextureColor) { override fun setup() { super.setup() state.activeTexture = 0 @@ -127,16 +139,16 @@ class TileRenderers(val state: GLStateTracker) { } /** - * Возвращает запечённое состояние shaderVertexTexture с данной текстурой + * Возвращает запечённое состояние шейдера shaderVertexTexture с данной текстурой */ - fun foreground(texture: GLTexture2D): BakedProgramState { + fun foreground(texture: GLTexture2D): ConfiguredShaderProgram { return foregroundTilePrograms.computeIfAbsent(texture, ::ForegroundTileProgram) } /** - * Возвращает запечённое состояние shaderVertexTextureColor с данной текстурой + * Возвращает запечённое состояние шейдера shaderVertexTextureColor с данной текстурой */ - fun background(texture: GLTexture2D): BakedProgramState { + fun background(texture: GLTexture2D): ConfiguredShaderProgram { return backgroundTilePrograms.computeIfAbsent(texture, ::BackgroundTileProgram) } @@ -151,11 +163,30 @@ private enum class TileRenderTesselateResult { HALT } -class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { - val texture = state.loadNamedTexture(tile.renderParameters.absoluteTexturePath).also { +private fun vertexTextureBuilder() = DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) + +private class TileEqualityTester(val definition: TileDefinition) : EqualityRuleTester { + override fun test(tile: ChunkTile?): Boolean { + return tile?.def == definition + } +} + +private class ModifierEqualityTester(val definition: MaterialModifier) : EqualityRuleTester { + override fun test(tile: ChunkTile?): Boolean { + return tile?.modifier == definition + } +} + +class TileRenderer(val state: GLStateTracker, val def: IRenderableTile) { + val texture = state.loadNamedTexture(def.renderParameters.absoluteTexturePath).also { it.textureMagFilter = GL_NEAREST } + val equalityTester: EqualityRuleTester = when (def) { + is TileDefinition -> TileEqualityTester(def) + is MaterialModifier -> ModifierEqualityTester(def) + } + val bakedProgramState = state.tileRenderers.foreground(texture) val bakedBackgroundProgramState = state.tileRenderers.background(texture) // private var notifiedDepth = false @@ -181,7 +212,7 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { d += offset.y / PIXELS_IN_STARBOUND_UNITf } - if (tile.renderParameters.variants == 0 || piece.texture != null || piece.variantStride == null) { + if (def.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 +223,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.renderParameters.variants).toInt() + val variant = (getter.randomDoubleFor(pos) * def.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,21 +237,24 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { } } - private fun tesselatePiece(matchPiece: RenderMatch, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, thisBuilder: DynamicVertexBuilder, background: Boolean): TileRenderTesselateResult { - if (matchPiece.test(getter, tile, pos)) { + private fun tesselatePiece( + matchPiece: RenderMatch, + getter: ITileChunk, + layers: TileLayerList, + pos: Vector2i, + thisBuilder: DynamicVertexBuilder, + background: Boolean + ): TileRenderTesselateResult { + if (matchPiece.test(getter, equalityTester, 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!!)) + val program = if (background) { + state.tileRenderers.background(state.loadNamedTexture(renderPiece.piece.texture!!)) } else { - program = state.tileRenderers.foreground(state.loadNamedTexture(renderPiece.piece.texture!!)) + state.tileRenderers.foreground(state.loadNamedTexture(renderPiece.piece.texture!!)) } - tesselateAt(renderPiece.piece, getter, layers.getLayer(program, tile.renderParameters.zLevel) { - return@getLayer DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) - }, pos, renderPiece.offset) + tesselateAt(renderPiece.piece, getter, layers.computeIfAbsent(program, def.renderParameters.zLevel, ::vertexTextureBuilder), pos, renderPiece.offset) } else { tesselateAt(renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset) } @@ -256,15 +290,13 @@ class TileRenderer(val state: GLStateTracker, val tile: TileDefinition) { fun tesselate(getter: ITileChunk, layers: TileLayerList, pos: Vector2i, background: Boolean = false) { // если у нас нет renderTemplate // то мы просто не можем его отрисовать - val template = tile.renderTemplate + val template = def.renderTemplate - val builder = layers.getLayer(if (background) bakedBackgroundProgramState else bakedProgramState, tile.renderParameters.zLevel) { - return@getLayer DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) - } + val vertexBuilder = layers.computeIfAbsent(if (background) bakedBackgroundProgramState else bakedProgramState, def.renderParameters.zLevel, ::vertexTextureBuilder) for ((_, matcher) in template.matches) { for (matchPiece in matcher) { - val matched = tesselatePiece(matchPiece, getter, layers, pos, builder, background) + val matched = tesselatePiece(matchPiece, getter, layers, pos, vertexBuilder, background) if (matched == TileRenderTesselateResult.HALT) { break diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/IRenderableTile.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/IRenderableTile.kt new file mode 100644 index 00000000..efe28c76 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/IRenderableTile.kt @@ -0,0 +1,6 @@ +package ru.dbotthepony.kstarbound.defs.tile + +sealed interface IRenderableTile { + val renderTemplate: RenderTemplate + val renderParameters: RenderParameters +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/MaterialModifier.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/MaterialModifier.kt index 78fd8ad8..7eb0847f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/MaterialModifier.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/MaterialModifier.kt @@ -6,16 +6,16 @@ import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter data class MaterialModifier( val modId: Int, val modName: String, - val itemDrop: String, - val description: String, - val health: Int, - val harvestLevel: Int, - val breaksWithTile: Boolean, + val itemDrop: String? = null, + val description: String = "...", + val health: Int = 0, + val harvestLevel: Int = 0, + val breaksWithTile: Boolean = true, val miningSounds: List = listOf(), val miningParticle: String? = null, - val renderTemplate: RenderTemplate, - val renderParameters: RenderParameters -) { + override val renderTemplate: RenderTemplate, + override val renderParameters: RenderParameters +) : IRenderableTile { init { require(modId > 0) { "Invalid material modifier ID $modId" } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt index 05a92dc2..ef47ea20 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt @@ -9,10 +9,10 @@ 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.CustomEnumTypeAdapter import ru.dbotthepony.kstarbound.io.EnumAdapter import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter import ru.dbotthepony.kstarbound.util.WriteOnce +import ru.dbotthepony.kstarbound.world.ChunkTile import ru.dbotthepony.kstarbound.world.ITileGetter import ru.dbotthepony.kvector.vector.nint.Vector2i import java.util.concurrent.ConcurrentHashMap @@ -37,6 +37,10 @@ data class RenderPiece( } } +fun interface EqualityRuleTester { + fun test(tile: ChunkTile?): Boolean +} + data class RenderRuleList( val entries: List, val join: Combination = Combination.ALL @@ -50,9 +54,9 @@ data class RenderRuleList( val matchHue: Boolean = false, val inverse: Boolean = false, ) { - private fun doTest(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + private fun doTest(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { return when (type) { - "EqualsSelf" -> getter[thisPos + offsetPos]?.def == thisRef + "EqualsSelf" -> equalityTester.test(getter[thisPos + offsetPos]) "Connects" -> getter[thisPos + offsetPos] != null else -> { @@ -65,12 +69,12 @@ data class RenderRuleList( } } - fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + fun test(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { if (inverse) { - return !doTest(getter, thisRef, thisPos, offsetPos) + return !doTest(getter, equalityTester, thisPos, offsetPos) } - return doTest(getter, thisRef, thisPos, offsetPos) + return doTest(getter, equalityTester, thisPos, offsetPos) } companion object { @@ -87,11 +91,11 @@ data class RenderRuleList( } } - fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i, offset: Vector2i): Boolean { + fun test(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i, offset: Vector2i): Boolean { when (join) { Combination.ALL -> { for (entry in entries) { - if (!entry.test(getter, thisRef, thisPos, offset)) { + if (!entry.test(getter, equalityTester, thisPos, offset)) { return false } } @@ -101,7 +105,7 @@ data class RenderRuleList( Combination.ANY -> { for (entry in entries) { - if (entry.test(getter, thisRef, thisPos, offset)) { + if (entry.test(getter, equalityTester, thisPos, offset)) { return true } } @@ -144,8 +148,8 @@ data class RenderMatch( ) { var rule by WriteOnce() - fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i): Boolean { - return rule.test(getter, thisRef, thisPos, offset) + fun test(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { + return rule.test(getter, equalityTester, thisPos, offset) } fun resolve(template: RenderTemplate) { @@ -177,10 +181,12 @@ data class RenderMatch( * Если хотя бы один из них вернул false, весь тест возвращает false * * [subMatches] стоит итерировать только если это вернуло true + * + * [equalityTester] требуется для проверки раенства между "этим" тайлом и другим */ - fun test(getter: ITileGetter, thisRef: TileDefinition, thisPos: Vector2i): Boolean { + fun test(tileAccess: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { for (matcher in matchAllPoints) { - if (!matcher.test(getter, thisRef, thisPos)) { + if (!matcher.test(tileAccess, equalityTester, thisPos)) { return false } } @@ -190,7 +196,7 @@ data class RenderMatch( } for (matcher in matchAnyPoints) { - if (matcher.test(getter, thisRef, thisPos)) { + if (matcher.test(tileAccess, equalityTester, thisPos)) { return true } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt index 6125c8d9..47068311 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt @@ -19,9 +19,9 @@ data class TileDefinition( val health: Double = 0.0, val category: String, - val renderTemplate: RenderTemplate, - val renderParameters: RenderParameters, -) { + override val renderTemplate: RenderTemplate, + override val renderParameters: RenderParameters, +) : IRenderableTile { companion object { val ADAPTER = KConcreteTypeAdapter.Builder(TileDefinition::class) .plain( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 9f4167a2..98988ff4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -4,6 +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.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.phys.RectTileFlooderDepthFirst @@ -17,18 +18,47 @@ import kotlin.collections.HashSet /** * Представляет из себя класс, который содержит состояние тайла на заданной позиции */ -data class ChunkTile(val chunk: Chunk<*, *>.TileLayer, val def: TileDefinition) { +class ChunkTile(val chunk: Chunk<*, *>.TileLayer, val def: TileDefinition) { var color = 0 set(value) { - field = value - chunk.incChangeset() + if (value != field) { + field = value + chunk.incChangeset() + } } - var forceVariant = -1 + var variant = -1 set(value) { - field = value - chunk.incChangeset() + if (value != field) { + field = value + chunk.incChangeset() + } } + + var modifier: MaterialModifier? = null + set(value) { + if (value != field) { + field = value + chunk.incChangeset() + } + } + + override fun equals(other: Any?): Boolean { + return other is ChunkTile && other.color == color && other.variant == variant && other.modifier === modifier && other.def === def + } + + override fun toString(): String { + return "ChunkTile[$chunk, material = ${def.materialName}, color = $color, variant = $variant, modifier = ${modifier?.modName}]" + } + + override fun hashCode(): Int { + var result = chunk.hashCode() + result = 31 * result + def.hashCode() + result = 31 * result + color + result = 31 * result + variant + result = 31 * result + (modifier?.hashCode() ?: 0) + return result + } } private fun ccwSortScore(point: Vector2d, axis: Vector2d): Double {