diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt new file mode 100644 index 00000000..86d2162d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt @@ -0,0 +1,8 @@ +package ru.dbotthepony.kstarbound + +import com.google.gson.GsonBuilder +import com.google.gson.TypeAdapter + +inline fun <reified T> GsonBuilder.registerTypeAdapter(adapter: TypeAdapter<T>): GsonBuilder { + return registerTypeAdapter(T::class.java, adapter) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 0a749737..91deb112 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -12,6 +12,7 @@ import java.io.ByteArrayInputStream import java.io.DataInputStream import java.io.File import java.util.zip.Inflater +import kotlin.math.roundToInt private val LOGGER = LogManager.getLogger() @@ -75,12 +76,17 @@ fun main() { hitTile = true } - reader.skipBytes(1) // Foreground hue shift - reader.skipBytes(1) // Foreground color variant + // reader.skipBytes(1) // Foreground hue shift + // reader.skipBytes(1) // Foreground color variant + val colorShift = reader.readUnsignedByte() + val colorVariant = reader.readUnsignedByte() val modifier = reader.readUnsignedShort() val getModifier = Starbound.tileModifiersByIDAccess[modifier] + chunk.foreground[x, y]?.color = colorVariant + chunk.foreground[x, y]?.setHueShift(colorShift) + if (getModifier != null && getMat != null) { chunk.foreground[x, y]?.modifier = getModifier } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index d5e1695a..9dc3fe94 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -8,6 +8,7 @@ import ru.dbotthepony.kstarbound.api.PhysicalFS import ru.dbotthepony.kstarbound.api.getPathFilename import ru.dbotthepony.kstarbound.api.getPathFolder import ru.dbotthepony.kstarbound.defs.* +import ru.dbotthepony.kstarbound.defs.liquid.LiquidDefinition import ru.dbotthepony.kstarbound.defs.projectile.* import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.RenderParameters @@ -57,10 +58,15 @@ object Starbound : IVFS { private val tileModifiers = HashMap<String, MaterialModifier>() private val tileModifiersByID = Int2ObjectAVLTreeMap<MaterialModifier>() + private val liquid = HashMap<String, LiquidDefinition>() + private val liquidByID = Int2ObjectAVLTreeMap<LiquidDefinition>() + private val projectiles = HashMap<String, ConfiguredProjectile>() private val parallax = HashMap<String, ParallaxPrototype>() private val functions = HashMap<String, JsonFunction>() + val liquidAccess: Map<String, LiquidDefinition> = Collections.unmodifiableMap(liquid) + val liquidByIDAccess: Map<Int, LiquidDefinition> = Collections.unmodifiableMap(liquidByID) val tileModifiersAccess: Map<String, MaterialModifier> = Collections.unmodifiableMap(tileModifiers) val tileModifiersByIDAccess: Map<Int, MaterialModifier> = Collections.unmodifiableMap(tileModifiersByID) val tilesAccess: Map<String, TileDefinition> = Collections.unmodifiableMap(tiles) @@ -94,6 +100,7 @@ object Starbound : IVFS { .also(RenderParameters::registerGson) .also(RenderTemplate::registerGson) .also(TileDefinition::registerGson) + .also(LiquidDefinition::registerGson) .registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe()) @@ -183,6 +190,7 @@ object Starbound : IVFS { loadStage(callback, this::loadProjectiles, "projectiles") loadStage(callback, this::loadParallax, "parallax definitions") loadStage(callback, this::loadMaterialModifiers, "material modifier definitions") + loadStage(callback, this::loadLiquidDefinitions, "liquid definitions") initializing = false initialized = true @@ -350,4 +358,25 @@ object Starbound : IVFS { readingFolder = null } + + private fun loadLiquidDefinitions(callback: (String) -> Unit) { + for (fs in fileSystems) { + for (listedFile in fs.listAllFilesWithExtension("liquid")) { + try { + callback("Loading $listedFile") + + readingFolder = getPathFolder(listedFile) + val liquidDef = gson.fromJson(getReader(listedFile), LiquidDefinition::class.java) + + check(liquid.put(liquidDef.name, liquidDef) == null) { "Already has liquid with name ${liquidDef.name} loaded!" } + check(liquidByID.put(liquidDef.liquidId, liquidDef) == null) { "Already has liquid with ID ${liquidDef.liquidId} loaded!" } + } catch (err: Throwable) { + //throw TileDefLoadingException("Loading tile file $listedFile", err) + LOGGER.error("Loading liquid definition 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 b5792416..04ba733b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt @@ -47,12 +47,12 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client for ((pos, tile) in view.posToTile) { if (tile != null) { - state.tileRenderers.getTileRenderer(tile.def.materialName).tesselate(view, layers, pos, background = isBackground) + state.tileRenderers.getTileRenderer(tile.def.materialName).tesselate(tile, view, layers, pos, background = isBackground) val modifier = tile.modifier if (modifier != null) { - state.tileRenderers.getModifierRenderer(modifier.modName).tesselate(view, layers, pos, background = isBackground) + state.tileRenderers.getModifierRenderer(modifier.modName).tesselate(tile, view, layers, pos, background = isBackground, isModifier = true) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt index 974c246a..ba85fb90 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLAttributeList.kt @@ -23,6 +23,9 @@ enum class GLType(val identity: Int, val typeIndentity: Int, val byteSize: Int, } interface IGLAttributeList { + /** + * Применяет список атрибутов к заданному [GLVertexArrayObject] (попутно включая или отключая их через [enable]) + */ fun apply(target: GLVertexArrayObject, enable: Boolean = false) } @@ -85,10 +88,15 @@ class GLFlatAttributeList(builder: GLFlatAttributeListBuilder) : IGLAttributeLis } companion object { - val VEC2F = GLFlatAttributeListBuilder().also {it.push(GLType.VEC2F)}.build() - val VEC3F = GLFlatAttributeListBuilder().also {it.push(GLType.VEC3F)}.build() - val VERTEX_TEXTURE = GLFlatAttributeListBuilder().also {it.push(GLType.VEC3F).push(GLType.VEC2F)}.build() - val VERTEX_2D_TEXTURE = GLFlatAttributeListBuilder().also {it.push(GLType.VEC2F).push(GLType.VEC2F)}.build() + val VEC2F = GLFlatAttributeListBuilder().push(GLType.VEC2F).build() + val VEC3F = GLFlatAttributeListBuilder().push(GLType.VEC3F).build() + + val VERTEX_TEXTURE = GLFlatAttributeListBuilder().push(GLType.VEC3F).push(GLType.VEC2F).build() + + val VERTEX_HSV_TEXTURE = GLFlatAttributeListBuilder() + .push(GLType.VEC3F, GLType.VEC2F, GLType.FLOAT).build() + + val VERTEX_2D_TEXTURE = GLFlatAttributeListBuilder().push(GLType.VEC2F).push(GLType.VEC2F).build() } } @@ -109,6 +117,11 @@ class GLFlatAttributeListBuilder : IGLAttributeList { return push("$type#${attributes.size}", type) } + fun push(vararg types: GLType): GLFlatAttributeListBuilder { + for (type in types) push(type) + return this + } + fun push(name: String, type: GLType): GLFlatAttributeListBuilder { check(!findName(name)) { "Already has named attribute $name!" } attributes.add(name to type) @@ -117,7 +130,7 @@ class GLFlatAttributeListBuilder : IGLAttributeList { fun build() = GLFlatAttributeList(this) - @Deprecated("Используй build()") + @Deprecated("Используй build()", replaceWith = ReplaceWith("build()")) override fun apply(target: GLVertexArrayObject, enable: Boolean) { var offset = 0L var stride = 0 @@ -136,9 +149,4 @@ class GLFlatAttributeListBuilder : IGLAttributeList { } } } - - companion object { - val VEC3F = GLFlatAttributeList.VEC3F - val VERTEX_TEXTURE = GLFlatAttributeList.VERTEX_TEXTURE - } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt index 620b6a2e..0b2ab8bd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt @@ -227,7 +227,7 @@ class GLStateTracker { checkForGLError() } - val thread = Thread.currentThread() + val thread: Thread = Thread.currentThread() val tileRenderers = TileRenderers(this) fun ensureSameThread() { @@ -356,18 +356,25 @@ class GLStateTracker { val shaderVertexTexture: GLTransformableProgram val shaderVertexTextureColor: GLTransformableColorableProgram + val shaderVertexTextureHSVColor: GLTransformableColorableProgram init { val textureF = GLShader.internalFragment("shaders/fragment/texture.glsl") val textureColorF = GLShader.internalFragment("shaders/fragment/texture_color.glsl") val textureV = GLShader.internalVertex("shaders/vertex/texture.glsl") + val textureFragmentHSV = GLShader.internalFragment("shaders/fragment/texture_color_per_vertex.glsl") + val textureVertexHSV = GLShader.internalVertex("shaders/vertex/texture_hsv.glsl") + shaderVertexTexture = GLTransformableProgram(this, textureF, textureV) shaderVertexTextureColor = GLTransformableColorableProgram(this, textureColorF, textureV) + shaderVertexTextureHSVColor = GLTransformableColorableProgram(this, textureFragmentHSV, textureVertexHSV) textureF.unlink() textureColorF.unlink() textureV.unlink() + textureFragmentHSV.unlink() + textureVertexHSV.unlink() } val fontProgram: GLTransformableColorableProgram diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt index 04ade50b..f9c5933d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt @@ -125,6 +125,7 @@ interface IVertex<This : IVertex<This, VertexBuilderType>, VertexBuilderType> { fun expect(type: GLType): This fun pushVec3f(x: Float, y: Float, z: Float): This fun pushVec2f(x: Float, y: Float): This + fun push(value: Float): This fun end(): VertexBuilderType } @@ -132,6 +133,20 @@ typealias VertexTransformer = (IVertex<*, *>, Int) -> IVertex<*, *> private val emptyTransform: VertexTransformer = { it, _ -> it } private val EMPTY_BUFFER = ByteBuffer.allocateDirect(0) +fun VertexTransformer.before(other: VertexTransformer): VertexTransformer { + return { a, b -> + other.invoke(a, b) + this.invoke(a, b) + } +} + +fun VertexTransformer.after(other: VertexTransformer): VertexTransformer { + return { a, b -> + this.invoke(a, b) + other.invoke(a, b) + } +} + object VertexTransformers { fun uv(u0: Float, v0: Float, @@ -279,6 +294,7 @@ class DynamicVertexBuilder(val attributes: GLFlatAttributeList, override val typ is IntArray -> for (i in element) bytes.putInt(i) is ByteArray -> for (i in element) bytes.put(i) is DoubleArray -> for (i in element) bytes.putDouble(i) + is Float -> bytes.putFloat(element) else -> throw IllegalStateException("Unknown element $element") } } @@ -291,6 +307,7 @@ class DynamicVertexBuilder(val attributes: GLFlatAttributeList, override val typ is IntArray -> it.joinToString(", ") is ByteArray -> it.joinToString(", ") is DoubleArray -> it.joinToString(", ") + is Float -> it else -> "null" } }.joinToString("; ")})" } @@ -331,6 +348,12 @@ class DynamicVertexBuilder(val attributes: GLFlatAttributeList, override val typ return this } + override fun push(value: Float): Vertex { + expect(GLType.FLOAT) + store[index++] = value + return this + } + override fun checkValid() { for (elem in store.indices) { if (store[elem] == null) { @@ -521,6 +544,15 @@ class StreamVertexBuilder( return this } + override fun push(value: Float): Vertex { + expect(GLType.FLOAT) + vertexBuffer.position(bufferPosition) + vertexBuffer.putFloat(value) + index++ + bufferPosition += 4 + return this + } + override fun end(): StreamVertexBuilder { check(index == attributes.size) { "Vertex $vertexIndex is not fully filled (only $index attributes provided, ${attributes.size} required)" } return this@StreamVertexBuilder diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt index 039aeb9b..a1cacabf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.gl.* import ru.dbotthepony.kstarbound.defs.tile.* -import ru.dbotthepony.kstarbound.world.ChunkTile +import ru.dbotthepony.kstarbound.world.TileState import ru.dbotthepony.kstarbound.world.ITileChunk import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.nint.Vector2i @@ -80,7 +80,7 @@ class TileRenderers(val state: GLStateTracker) { } } - private inner class ForegroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.shaderVertexTexture) { + private inner class ForegroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.shaderVertexTextureHSVColor) { override fun setup() { super.setup() state.activeTexture = 0 @@ -89,6 +89,8 @@ class TileRenderers(val state: GLStateTracker) { texture.bind() texture.textureMagFilter = GL_NEAREST texture.textureMinFilter = GL_NEAREST + + program["_color"] = FOREGROUND_COLOR } override fun equals(other: Any?): Boolean { @@ -108,7 +110,7 @@ class TileRenderers(val state: GLStateTracker) { } } - private inner class BackgroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.shaderVertexTextureColor) { + private inner class BackgroundTileProgram(private val texture: GLTexture2D) : ConfiguredShaderProgram(state.shaderVertexTextureHSVColor) { override fun setup() { super.setup() state.activeTexture = 0 @@ -154,6 +156,7 @@ class TileRenderers(val state: GLStateTracker) { companion object { val BACKGROUND_COLOR = Color(0.4f, 0.4f, 0.4f) + val FOREGROUND_COLOR = Color(1f, 1f, 1f) } } @@ -163,16 +166,16 @@ private enum class TileRenderTesselateResult { HALT } -private fun vertexTextureBuilder() = DynamicVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS) +private fun vertexTextureBuilder() = DynamicVertexBuilder(GLFlatAttributeList.VERTEX_HSV_TEXTURE, VertexType.QUADS) private class TileEqualityTester(val definition: TileDefinition) : EqualityRuleTester { - override fun test(tile: ChunkTile?): Boolean { + override fun test(tile: TileState?): Boolean { return tile?.def == definition } } private class ModifierEqualityTester(val definition: MaterialModifier) : EqualityRuleTester { - override fun test(tile: ChunkTile?): Boolean { + override fun test(tile: TileState?): Boolean { return tile?.modifier == definition } } @@ -191,7 +194,7 @@ class TileRenderer(val state: GLStateTracker, val def: IRenderableTile) { val bakedBackgroundProgramState = state.tileRenderers.background(texture) // private var notifiedDepth = false - private fun tesselateAt(piece: RenderPiece, getter: ITileChunk, builder: DynamicVertexBuilder, pos: Vector2i, offset: Vector2i = Vector2i.ZERO) { + private fun tesselateAt(self: TileState, piece: RenderPiece, getter: ITileChunk, builder: DynamicVertexBuilder, pos: Vector2i, offset: Vector2i = Vector2i.ZERO, isModifier: Boolean) { val fx = pos.x.toFloat() val fy = pos.y.toFloat() @@ -212,38 +215,35 @@ class TileRenderer(val state: GLStateTracker, val def: IRenderableTile) { d += offset.y / PIXELS_IN_STARBOUND_UNITf } - 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) + var mins = piece.texturePosition + var maxs = piece.texturePosition + piece.textureSize - builder.quadZ( - a, - b, - c, - d, - Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) - } else { + if (def.renderParameters.variants != 0 && piece.variantStride != null && piece.texture == null) { 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) - - builder.quadZ( - a, - b, - c, - d, - Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0)) + mins += piece.variantStride * variant + maxs += piece.variantStride * variant } + + if (def.renderParameters.multiColored && piece.colorStride != null && self.color != 0) { + mins += piece.colorStride * self.color + maxs += piece.colorStride * self.color + } + + val (u0, v0) = texture.pixelToUV(mins) + val (u1, v1) = texture.pixelToUV(maxs) + + builder.quadZ(a, b, c, d, Z_LEVEL, VertexTransformers.uv(u0, v1, u1, v0).after { it, _ -> it.push(if (!isModifier || self.modifier?.grass == true) self.hueShift else 0f) }) } private fun tesselatePiece( + self: TileState, matchPiece: RenderMatch, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, thisBuilder: DynamicVertexBuilder, - background: Boolean + background: Boolean, + isModifier: Boolean, ): TileRenderTesselateResult { if (matchPiece.test(getter, equalityTester, pos)) { for (renderPiece in matchPiece.pieces) { @@ -254,14 +254,14 @@ class TileRenderer(val state: GLStateTracker, val def: IRenderableTile) { state.tileRenderers.foreground(state.loadNamedTexture(renderPiece.piece.texture!!)) } - tesselateAt(renderPiece.piece, getter, layers.computeIfAbsent(program, def.renderParameters.zLevel, ::vertexTextureBuilder), pos, renderPiece.offset) + tesselateAt(self, renderPiece.piece, getter, layers.computeIfAbsent(program, def.renderParameters.zLevel, ::vertexTextureBuilder), pos, renderPiece.offset, isModifier) } else { - tesselateAt(renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset) + tesselateAt(self, renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset, isModifier) } } for (subPiece in matchPiece.subMatches) { - val matched = tesselatePiece(subPiece, getter, layers, pos, thisBuilder, background) + val matched = tesselatePiece(self, subPiece, getter, layers, pos, thisBuilder, background, isModifier) if (matched == TileRenderTesselateResult.HALT || matched == TileRenderTesselateResult.CONTINUE && matchPiece.haltOnSubMatch) { return TileRenderTesselateResult.HALT @@ -287,7 +287,7 @@ class TileRenderer(val state: GLStateTracker, val def: IRenderableTile) { * * Тесселирует тайлы в нужный VertexBuilder с масштабом согласно константе [PIXELS_IN_STARBOUND_UNITf] */ - fun tesselate(getter: ITileChunk, layers: TileLayerList, pos: Vector2i, background: Boolean = false) { + fun tesselate(self: TileState, getter: ITileChunk, layers: TileLayerList, pos: Vector2i, background: Boolean = false, isModifier: Boolean = false) { // если у нас нет renderTemplate // то мы просто не можем его отрисовать val template = def.renderTemplate @@ -296,7 +296,7 @@ class TileRenderer(val state: GLStateTracker, val def: IRenderableTile) { for ((_, matcher) in template.matches) { for (matchPiece in matcher) { - val matched = tesselatePiece(matchPiece, getter, layers, pos, vertexBuilder, background) + val matched = tesselatePiece(self, matchPiece, getter, layers, pos, vertexBuilder, background, isModifier) if (matched == TileRenderTesselateResult.HALT) { break diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/liquid/LiquidDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/liquid/LiquidDefinition.kt new file mode 100644 index 00000000..ccf0c0ea --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/liquid/LiquidDefinition.kt @@ -0,0 +1,53 @@ +package ru.dbotthepony.kstarbound.defs.liquid + +import com.google.gson.GsonBuilder +import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter +import ru.dbotthepony.kstarbound.registerTypeAdapter +import ru.dbotthepony.kvector.vector.Color + +data class LiquidDefinition( + val name: String, + val liquidId: Int, + val description: String = "...", + val tickDelta: Int = 1, + val color: Color, + val itemDrop: String? = null, + val statusEffects: List<String> = listOf(), + val interactions: List<Interaction> = listOf(), + val texture: String, + val bottomLightMix: Color, + val textureMovementFactor: Double, +) { + data class Interaction(val liquid: Int, val liquidResult: Int? = null, val materialResult: String? = null) { + init { + require(liquidResult != null || materialResult != null) { "Both liquidResult and materialResult are missing" } + } + } + + companion object { + val ADAPTER = KConcreteTypeAdapter.Builder(LiquidDefinition::class) + .plain(LiquidDefinition::name) + .plain(LiquidDefinition::liquidId) + .plain(LiquidDefinition::description) + .plain(LiquidDefinition::tickDelta) + .plain(LiquidDefinition::color) + .plain(LiquidDefinition::itemDrop) + .list(LiquidDefinition::statusEffects) + .list(LiquidDefinition::interactions) + .plain(LiquidDefinition::texture) + .plain(LiquidDefinition::bottomLightMix) + .plain(LiquidDefinition::textureMovementFactor) + .build() + + val INTERACTION_ADAPTER = KConcreteTypeAdapter.Builder(Interaction::class) + .plain(Interaction::liquid) + .plain(Interaction::liquidResult) + .plain(Interaction::materialResult) + .build() + + fun registerGson(gsonBuilder: GsonBuilder) { + gsonBuilder.registerTypeAdapter(ADAPTER) + gsonBuilder.registerTypeAdapter(INTERACTION_ADAPTER) + } + } +} 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 7eb0847f..49dc0e0d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/MaterialModifier.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/MaterialModifier.kt @@ -11,6 +11,7 @@ data class MaterialModifier( val health: Int = 0, val harvestLevel: Int = 0, val breaksWithTile: Boolean = true, + val grass: Boolean = false, val miningSounds: List<String> = listOf(), val miningParticle: String? = null, override val renderTemplate: RenderTemplate, @@ -29,6 +30,7 @@ data class MaterialModifier( .plain(MaterialModifier::health) .plain(MaterialModifier::harvestLevel) .plain(MaterialModifier::breaksWithTile) + .plain(MaterialModifier::grass) .list(MaterialModifier::miningSounds) .plain(MaterialModifier::miningParticle) .plain(MaterialModifier::renderTemplate, RenderTemplate.CACHE) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt index 48b80930..23eab3a0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt @@ -4,6 +4,8 @@ import com.google.gson.GsonBuilder import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.io.KConcreteTypeAdapter +const val TILE_COLOR_VARIANTS = 9 + data class RenderParameters( val texture: String, val variants: Int = 0, 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 ef47ea20..57b272e6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt @@ -12,7 +12,7 @@ import ru.dbotthepony.kstarbound.Starbound 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.TileState import ru.dbotthepony.kstarbound.world.ITileGetter import ru.dbotthepony.kvector.vector.nint.Vector2i import java.util.concurrent.ConcurrentHashMap @@ -38,7 +38,7 @@ data class RenderPiece( } fun interface EqualityRuleTester { - fun test(tile: ChunkTile?): Boolean + fun test(tile: TileState?): Boolean } data class RenderRuleList( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt index 41a0be89..815df8d0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KConcreteTypeAdapter.kt @@ -279,7 +279,7 @@ class KConcreteTypeAdapter<T : Any>( val name = fieldId.toString() if (loggedMisses.add(name)) { - LOGGER.warn("Skipping JSON field with name $name because ${bound.qualifiedName} has no such field") + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field") } reader.skipValue() @@ -306,7 +306,7 @@ class KConcreteTypeAdapter<T : Any>( if (fieldId == -1) { if (loggedMisses.add(name)) { - LOGGER.warn("Skipping JSON field with name $name because ${bound.qualifiedName} has no such field") + LOGGER.warn("Skipping JSON field with name $name because ${bound.simpleName} has no such field") } reader.skipValue() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/TwoDimensionalArray.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/TwoDimensionalArray.kt new file mode 100644 index 00000000..4e0dca75 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/TwoDimensionalArray.kt @@ -0,0 +1,35 @@ +package ru.dbotthepony.kstarbound.util + +import kotlin.reflect.KClass + +class TwoDimensionalArray<T : Any>(clazz: KClass<T>, private val width: Int, private val height: Int) { + private val memory: Array<T?> = java.lang.reflect.Array.newInstance(clazz.java, width * height) as Array<T?> + + operator fun get(x: Int, y: Int): T? { + if (x !in 0 until width) { + throw IndexOutOfBoundsException("X $x is out of bounds between 0 and $width") + } + + if (y !in 0 until height) { + throw IndexOutOfBoundsException("Y $y is out of bounds between 0 and $height") + } + + return memory[x + y * width] + } + + operator fun set(x: Int, y: Int, value: T): T? { + if (x !in 0 until width) { + throw IndexOutOfBoundsException("X $x is out of bounds between 0 and $width") + } + + if (y !in 0 until height) { + throw IndexOutOfBoundsException("Y $y is out of bounds between 0 and $height") + } + + val old = memory[x + y * width] + memory[x + y * width] = value + return old + } +} + +inline fun <reified T : Any> TwoDimensionalArray(width: Int, height: Int) = TwoDimensionalArray(T::class, width, height) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 98988ff4..ea8dfbbf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -4,8 +4,11 @@ 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.liquid.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier +import ru.dbotthepony.kstarbound.defs.tile.TILE_COLOR_VARIANTS import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import ru.dbotthepony.kstarbound.util.TwoDimensionalArray import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.phys.RectTileFlooderDepthFirst import ru.dbotthepony.kstarbound.world.phys.RectTileFlooderSizeFirst @@ -18,19 +21,46 @@ import kotlin.collections.HashSet /** * Представляет из себя класс, который содержит состояние тайла на заданной позиции */ -class ChunkTile(val chunk: Chunk<*, *>.TileLayer, val def: TileDefinition) { +class TileState(val chunk: Chunk<*, *>.TileLayer, val def: TileDefinition) { var color = 0 set(value) { if (value != field) { + if (!def.renderParameters.multiColored) { + throw IllegalStateException("${def.materialName} can't be colored") + } + + if (value !in 0 until TILE_COLOR_VARIANTS) { + throw IndexOutOfBoundsException("Tile variant $value is out of possible range 0 to $TILE_COLOR_VARIANTS") + } + field = value chunk.incChangeset() } } - var variant = -1 + /** + * Выставляет hue shift как байтовое значение в диапазоне 0 .. 255 + */ + fun setHueShift(value: Int) { + if (value < 0) { + hueShift = 0f + } else if (value > 255) { + hueShift = 360f + } else { + hueShift = (value / 255f) * 360f + } + } + + var hueShift = 0f set(value) { - if (value != field) { - field = value + var newValue = value % 360f + + if (newValue < 0f) { + newValue += 360f + } + + if (newValue != field) { + field = newValue chunk.incChangeset() } } @@ -44,23 +74,28 @@ class ChunkTile(val chunk: Chunk<*, *>.TileLayer, val def: TileDefinition) { } override fun equals(other: Any?): Boolean { - return other is ChunkTile && other.color == color && other.variant == variant && other.modifier === modifier && other.def === def + return other is TileState && other.color == color && other.modifier === modifier && other.def === def } override fun toString(): String { - return "ChunkTile[$chunk, material = ${def.materialName}, color = $color, variant = $variant, modifier = ${modifier?.modName}]" + return "ChunkTile[$chunk, material = ${def.materialName}, color = $color, 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 } } +data class LiquidState(val chunk: Chunk<*, *>.TileLayer, val def: LiquidDefinition) { + var pressure: Float = 0f + var level: Float = 1f + var isInfinite: Boolean = false +} + private fun ccwSortScore(point: Vector2d, axis: Vector2d): Double { if (point.x > 0.0) { return point.dot(axis) @@ -308,16 +343,16 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, /** * Хранит тайлы как x + y * CHUNK_SIZE */ - private val tiles = arrayOfNulls<ChunkTile>(CHUNK_SIZE * CHUNK_SIZE) + private val tiles = arrayOfNulls<TileState>(CHUNK_SIZE * CHUNK_SIZE) - override operator fun get(x: Int, y: Int): ChunkTile? { + override operator fun get(x: Int, y: Int): TileState? { if (isOutside(x, y)) return null return tiles[x or (y shl CHUNK_SHIFT)] } - operator fun set(x: Int, y: Int, tile: ChunkTile?) { + operator fun set(x: Int, y: Int, tile: TileState?) { if (isOutside(x, y)) throw IndexOutOfBoundsException("Trying to set tile ${tile?.def?.materialName} at $x $y, but that is outside of chunk's range") @@ -326,11 +361,11 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, markPhysicsDirty() } - override operator fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? { + override operator fun set(x: Int, y: Int, tile: TileDefinition?): TileState? { if (isOutside(x, y)) throw IndexOutOfBoundsException("Trying to set tile ${tile?.materialName} at $x $y, but that is outside of chunk's range") - val chunkTile = if (tile != null) ChunkTile(this, tile) else null + val chunkTile = if (tile != null) TileState(this, tile) else null this[x, y] = chunkTile changeset++ markPhysicsDirty() @@ -344,8 +379,11 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, val foreground = TileLayer() val background = TileLayer() + + protected val liquidStates: TwoDimensionalArray<LiquidState> = TwoDimensionalArray(CHUNK_SIZE, CHUNK_SIZE) + protected val entities = HashSet<Entity>() - val entitiesAccess = Collections.unmodifiableSet(entities) + val entitiesAccess: Set<Entity> = Collections.unmodifiableSet(entities) protected abstract fun onEntityAdded(entity: Entity) protected abstract fun onEntityTransferedToThis(entity: Entity, otherChunk: This) @@ -396,8 +434,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, val EMPTY = object : IMutableTileChunk { override val pos = ChunkPos(0, 0) - override fun get(x: Int, y: Int): ChunkTile? = null - override fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? = null + override fun get(x: Int, y: Int): TileState? = null + override fun set(x: Int, y: Int, tile: TileDefinition?): TileState? = null } private val aabbBase = AABB( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkAPI.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkAPI.kt index c951244c..96c14b8b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkAPI.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkAPI.kt @@ -26,7 +26,7 @@ interface ITileGetter : ITileMap { /** * Возвращает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка */ - operator fun get(x: Int, y: Int): ChunkTile? + operator fun get(x: Int, y: Int): TileState? /** * Возвращает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка @@ -38,8 +38,8 @@ interface ITileGetter : ITileMap { * * Вектор имеет ОТНОСИТЕЛЬНЫЕ значения внутри самого чанка */ - val posToTile: Iterator<Pair<Vector2i, ChunkTile?>> get() { - return object : Iterator<Pair<Vector2i, ChunkTile?>> { + val posToTile: Iterator<Pair<Vector2i, TileState?>> get() { + return object : Iterator<Pair<Vector2i, TileState?>> { private var x = 0 private var y = 0 @@ -49,7 +49,7 @@ interface ITileGetter : ITileMap { return idx() < CHUNK_SIZE * CHUNK_SIZE } - override fun next(): Pair<Vector2i, ChunkTile?> { + override fun next(): Pair<Vector2i, TileState?> { if (!hasNext()) { throw IllegalStateException("Already iterated everything!") } @@ -121,7 +121,7 @@ interface ITileSetter : ITileMap { /** * Устанавливает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка */ - operator fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? + operator fun set(x: Int, y: Int, tile: TileDefinition?): TileState? /** * Устанавливает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка */ diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileView.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileView.kt index b83db973..681b891f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileView.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileView.kt @@ -22,7 +22,7 @@ open class TileView( open val bottomLeft: ITileChunk?, open val bottomRight: ITileChunk?, ) : ITileChunk { - override fun get(x: Int, y: Int): ChunkTile? { + override fun get(x: Int, y: Int): TileState? { if (x in 0 ..CHUNK_SIZE_FF) { if (y in 0 ..CHUNK_SIZE_FF) { return center[x, y] @@ -75,7 +75,7 @@ class MutableTileView( override val bottomLeft: IMutableTileChunk?, override val bottomRight: IMutableTileChunk?, ) : TileView(center, right, top, topRight, topLeft, left, bottom, bottomLeft, bottomRight), IMutableTileChunk { - override fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? { + override fun set(x: Int, y: Int, tile: TileDefinition?): TileState? { if (x in 0 .. CHUNK_SIZE_FF) { if (y in 0 .. CHUNK_SIZE_FF) { return center.set(x, y, tile) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index e96ebe2e..5a0a118a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -323,7 +323,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun ) } - fun getTile(pos: Vector2i): ChunkTile? { + fun getTile(pos: Vector2i): TileState? { return get(ChunkPos.fromTilePosition(pos))?.foreground?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)) } @@ -333,7 +333,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun return chunk } - fun getBackgroundTile(pos: Vector2i): ChunkTile? { + fun getBackgroundTile(pos: Vector2i): TileState? { return get(ChunkPos.fromTilePosition(pos))?.background?.get(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y)) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderDepthFirst.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderDepthFirst.kt index 3fb686ff..e33a29cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderDepthFirst.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderDepthFirst.kt @@ -2,11 +2,11 @@ package ru.dbotthepony.kstarbound.world.phys import ru.dbotthepony.kstarbound.world.CHUNK_SHIFT import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF -import ru.dbotthepony.kstarbound.world.ChunkTile +import ru.dbotthepony.kstarbound.world.TileState import ru.dbotthepony.kvector.vector.nint.Vector2i class RectTileFlooderDepthFirst( - private val tiles: Array<ChunkTile?>, + private val tiles: Array<TileState?>, private val seen: BooleanArray, rootx: Int, rooty: Int diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt index e32c97a4..c6f7a6f1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt @@ -2,11 +2,11 @@ package ru.dbotthepony.kstarbound.world.phys import ru.dbotthepony.kstarbound.world.CHUNK_SHIFT import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF -import ru.dbotthepony.kstarbound.world.ChunkTile +import ru.dbotthepony.kstarbound.world.TileState import ru.dbotthepony.kvector.vector.nint.Vector2i class RectTileFlooderSizeFirst( - private val tiles: Array<ChunkTile?>, + private val tiles: Array<TileState?>, private val seen: BooleanArray, private val rootx: Int, private val rooty: Int diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/TileFlooder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/TileFlooder.kt index 68eb397b..415db2b0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/TileFlooder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/TileFlooder.kt @@ -2,7 +2,7 @@ package ru.dbotthepony.kstarbound.world.phys import ru.dbotthepony.kstarbound.world.CHUNK_SHIFT import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF -import ru.dbotthepony.kstarbound.world.ChunkTile +import ru.dbotthepony.kstarbound.world.TileState import ru.dbotthepony.kvector.vector.ndouble.Vector2d private data class TileExposure( @@ -14,7 +14,7 @@ private data class TileExposure( ) private class TileFlooder( - private val tiles: Array<ChunkTile?>, + private val tiles: Array<TileState?>, private val seen: BooleanArray, rootx: Int, rooty: Int diff --git a/src/main/resources/shaders/fragment/texture_color_per_vertex.glsl b/src/main/resources/shaders/fragment/texture_color_per_vertex.glsl new file mode 100644 index 00000000..ad64b66d --- /dev/null +++ b/src/main/resources/shaders/fragment/texture_color_per_vertex.glsl @@ -0,0 +1,38 @@ + +#version 460 + +uniform sampler2D _texture; +uniform vec4 _color; + +in vec2 _uv_out; +in float _hsv_vertex; +out vec4 _color_out; + +// https://gist.github.com/983/e170a24ae8eba2cd174f +vec3 rgb2hsv(vec3 c) +{ + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +vec3 hsv2rgb(vec3 c) +{ + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +void main() { + vec4 texel = texture(_texture, _uv_out); + + vec3 hsv2 = rgb2hsv(vec3(texel[0], texel[1], texel[2])); + hsv2[0] += _hsv_vertex / 360; + vec4 rgb2 = vec4(hsv2rgb(hsv2), texel[3]); + + _color_out = _color * rgb2; +} diff --git a/src/main/resources/shaders/vertex/texture_hsv.glsl b/src/main/resources/shaders/vertex/texture_hsv.glsl new file mode 100644 index 00000000..86728b0b --- /dev/null +++ b/src/main/resources/shaders/vertex/texture_hsv.glsl @@ -0,0 +1,16 @@ + +#version 460 + +layout (location = 0) in vec3 _pos; +layout (location = 1) in vec2 _uv_in; +layout (location = 2) in float _hsv_in; + +out vec2 _uv_out; +out float _hsv_vertex; +uniform mat4 _transform; + +void main() { + _uv_out = _uv_in; + _hsv_vertex = _hsv_in; + gl_Position = _transform * vec4(_pos, 1.0); +}