From ad8910d098d7902f77b479df7bf4825355948b37 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Thu, 10 Feb 2022 22:16:17 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B6=D0=B5=D0=BA=D1=82=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B8=20=D0=B8=D1=85=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=20=D1=80=D0=B5=D0=BD=D0=B4=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 +- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 33 +- .../ru/dbotthepony/kstarbound/Starbound.kt | 103 ++++- .../ru/dbotthepony/kstarbound/api/IVFS.kt | 70 +++- .../kstarbound/client/ClientChunk.kt | 5 +- .../kstarbound/client/gl/ErrorCheck.kt | 2 +- .../kstarbound/client/gl/GLStateTracker.kt | 33 +- .../kstarbound/client/gl/GLTexture.kt | 16 + .../client/render/EntityRenderer.kt | 47 ++- .../client/render/FrameSetAnimator.kt | 69 ++++ .../dbotthepony/kstarbound/defs/Animation.kt | 14 + .../kstarbound/defs/ConfigurableDefinition.kt | 269 +++++++++++++ .../dbotthepony/kstarbound/defs/FrameGrid.kt | 355 ++++++++++++++++++ .../ru/dbotthepony/kstarbound/defs/Helpers.kt | 8 + .../kstarbound/defs/ParticleDefinition.kt | 8 + .../defs/projectile/Configurable.kt | 74 ++++ .../kstarbound/defs/projectile/Configured.kt | 33 ++ .../defs/projectile/ProjectilePhysics.kt | 113 ++++++ .../dbotthepony/kstarbound/io/StarboundPak.kt | 16 +- .../ru/dbotthepony/kstarbound/math/AABB.kt | 58 ++- .../ru/dbotthepony/kstarbound/math/Poly.kt | 41 ++ .../ru/dbotthepony/kstarbound/math/Vector.kt | 64 ++++ .../ru/dbotthepony/kstarbound/util/Color.kt | 106 +++++- .../kstarbound/util/CustomEnumTypeAdapter.kt | 31 ++ .../ru/dbotthepony/kstarbound/world/Chunk.kt | 4 + .../kstarbound/world/entities/AliveEntity.kt | 145 +++++-- .../kstarbound/world/entities/Entity.kt | 137 ++----- .../world/entities/MovementController.kt | 141 +++++++ .../kstarbound/world/entities/PlayerEntity.kt | 16 +- .../kstarbound/world/entities/Projectile.kt | 12 + 30 files changed, 1835 insertions(+), 191 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/client/render/FrameSetAnimator.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/Animation.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/ConfigurableDefinition.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/Helpers.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/ParticleDefinition.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configured.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/math/Poly.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/util/CustomEnumTypeAdapter.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt diff --git a/build.gradle.kts b/build.gradle.kts index 64762d9f..7b12a8a5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,11 +31,12 @@ tasks.compileKotlin { dependencies { implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) implementation("org.apache.logging.log4j:log4j-api:2.17.1") implementation("org.apache.logging.log4j:log4j-core:2.17.1") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") implementation("com.google.code.gson:gson:2.8.9") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index da74c58b..9bb4b0dd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -3,16 +3,15 @@ package ru.dbotthepony.kstarbound import org.apache.logging.log4j.LogManager import org.lwjgl.Version import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose -import ru.dbotthepony.kstarbound.api.PhysicalFS import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.defs.TileDefinition -import ru.dbotthepony.kstarbound.io.StarboundPak import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.entities.Move import ru.dbotthepony.kstarbound.world.entities.PlayerEntity +import ru.dbotthepony.kstarbound.world.entities.Projectile import java.io.File private val LOGGER = LogManager.getLogger() @@ -20,6 +19,14 @@ 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 client = StarboundClient() //Starbound.addFilePath(File("./unpacked_assets/")) @@ -108,7 +115,13 @@ fun main() { chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile }*/ - ent.dropToFloor() + ent.movement.dropToFloor() + + for ((i, proj) in Starbound.projectilesAccess.values.withIndex()) { + val projEnt = Projectile(client.world!!, proj) + projEnt.pos = Vector2d(i * 2.0, 10.0) + projEnt.spawn() + } } //val rand = Random() @@ -117,7 +130,7 @@ fun main() { client.onDrawGUI { client.gl.font.render("${ent.pos}", y = 100f, scale = 0.25f) - client.gl.font.render("${ent.velocity}", y = 120f, scale = 0.25f) + client.gl.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f) } client.onPreDrawWorld { @@ -140,17 +153,17 @@ fun main() { //ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1 if (client.input.KEY_LEFT_DOWN) { - ent.moveDirection = Move.MOVE_LEFT + ent.movement.moveDirection = Move.MOVE_LEFT } else if (client.input.KEY_RIGHT_DOWN) { - ent.moveDirection = Move.MOVE_RIGHT + ent.movement.moveDirection = Move.MOVE_RIGHT } else { - ent.moveDirection = Move.STAND_STILL + ent.movement.moveDirection = Move.STAND_STILL } - if (client.input.KEY_SPACE_PRESSED && ent.onGround) { - ent.requestJump() + if (client.input.KEY_SPACE_PRESSED && ent.movement.onGround) { + ent.movement.requestJump() } else if (client.input.KEY_SPACE_RELEASED) { - ent.recallJump() + ent.movement.recallJump() } if (client.input.KEY_ESCAPE_PRESSED) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 16b1cdc7..b66b6507 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -1,16 +1,22 @@ package ru.dbotthepony.kstarbound -import com.google.gson.JsonElement -import com.google.gson.JsonObject -import com.google.gson.JsonParser +import com.google.gson.* +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.api.IVFS import ru.dbotthepony.kstarbound.api.PhysicalFS -import ru.dbotthepony.kstarbound.defs.TileDefinition -import ru.dbotthepony.kstarbound.defs.TileDefinitionBuilder +import ru.dbotthepony.kstarbound.api.getPathFolder +import ru.dbotthepony.kstarbound.defs.* +import ru.dbotthepony.kstarbound.defs.projectile.ConfigurableProjectile +import ru.dbotthepony.kstarbound.defs.projectile.ConfiguredProjectile +import ru.dbotthepony.kstarbound.defs.projectile.ProjectilePhysics import ru.dbotthepony.kstarbound.io.StarboundPak -import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.math.* +import ru.dbotthepony.kstarbound.util.Color +import ru.dbotthepony.kstarbound.util.ColorTypeAdapter +import ru.dbotthepony.kstarbound.util.CustomEnumTypeAdapter import java.io.* import java.nio.ByteBuffer +import java.text.DateFormat import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap @@ -22,10 +28,31 @@ const val PIXELS_IN_STARBOUND_UNIT = 8.0 const val PIXELS_IN_STARBOUND_UNITf = 8.0f class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause) +class ProjectileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause) object Starbound : IVFS { + private val LOGGER = LogManager.getLogger() private val tiles = HashMap() - val tilesAccess = object : Map by tiles {} + private val projectiles = HashMap() + val tilesAccess = Collections.unmodifiableMap(tiles) + val projectilesAccess = Collections.unmodifiableMap(projectiles) + + val gson = GsonBuilder() + .enableComplexMapKeySerialization() + .serializeNulls() + .setDateFormat(DateFormat.LONG) + .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) + .setPrettyPrinting() + .registerTypeAdapter(Color::class.java, ColorTypeAdapter.nullSafe()) + .registerTypeAdapter(ProjectilePhysics::class.java, CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe()) + .registerTypeAdapter(AABB::class.java, AABBTypeAdapter) + .registerTypeAdapter(AABBi::class.java, AABBiTypeAdapter) + .registerTypeAdapter(Vector2d::class.java, Vector2dTypeAdapter) + .registerTypeAdapter(Vector2f::class.java, Vector2fTypeAdapter) + .registerTypeAdapter(Vector2i::class.java, Vector2iTypeAdapter) + .registerTypeAdapter(Poly::class.java, PolyTypeAdapter) + .registerTypeAdapter(ConfigurableProjectile::class.java, ConfigurableProjectile.ADAPTER) + .create() var initializing = false private set @@ -87,17 +114,36 @@ object Starbound : IVFS { } } - callback(false, false, "Loading materials...") + run { + val localTime = System.currentTimeMillis() - loadTileMaterials { - if (terminateLoading) { - throw InterruptedException("Game is terminating") + callback(false, false, "Loading materials...") + + loadTileMaterials { + if (terminateLoading) { + throw InterruptedException("Game is terminating") + } + + callback(false, true, it) } - callback(false, true, it) + callback(false, true, "Loaded materials in ${System.currentTimeMillis() - localTime}ms") } - callback(false, true, "Loaded materials") + run { + val localTime = System.currentTimeMillis() + callback(false, false, "Loading projectiles...") + + loadProjectiles { + if (terminateLoading) { + throw InterruptedException("Game is terminating") + } + + callback(false, true, it) + } + + callback(false, true, "Loaded Projectiles in ${System.currentTimeMillis() - localTime}ms") + } initializing = false initialized = true @@ -135,6 +181,16 @@ object Starbound : IVFS { return listing } + override fun listDirectories(path: String): Collection { + val listing = mutableListOf() + + for (fs in fileSystems) { + listing.addAll(fs.listDirectories(path)) + } + + return listing + } + fun onInitialize(callback: () -> Unit) { if (initialized) { callback() @@ -155,7 +211,7 @@ object Starbound : IVFS { private fun loadTileMaterials(callback: (String) -> Unit) { for (fs in fileSystems) { - for (listedFile in fs.listFiles("tiles/materials")) { + for (listedFile in fs.listAllFiles("tiles/materials")) { if (listedFile.endsWith(".material")) { try { callback("Loading $listedFile") @@ -171,4 +227,23 @@ object Starbound : IVFS { } } } + + private fun loadProjectiles(callback: (String) -> Unit) { + for (fs in fileSystems) { + for (listedFile in fs.listAllFiles("projectiles")) { + if (listedFile.endsWith(".projectile")) { + try { + callback("Loading $listedFile") + + val def = gson.fromJson(getReader(listedFile), ConfigurableProjectile::class.java).configure(getPathFolder(listedFile)) + check(projectiles[def.projectileName] == null) { "Already has projectile with ID ${def.projectileName} loaded!" } + projectiles[def.projectileName] = def + } catch(err: Throwable) { + //throw ProjectileDefLoadingException("Loading projectile file $listedFile", err) + LOGGER.error("Loading projectile file $listedFile", err) + } + } + } + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt index f7ef0f13..6ac18706 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt @@ -6,6 +6,29 @@ import java.nio.ByteBuffer interface IVFS { fun pathExists(path: String): Boolean + fun pathExistsOrElse(path: String, orElse: String): String { + if (pathExists(path)) + return path + + return orElse + } + + fun firstExisting(vararg pathList: String): String { + for (path in pathList) + if (pathExists(path)) + return path + + throw FileNotFoundException("Unable to find any of files specified") + } + + fun firstExistingOrNull(vararg pathList: String): String? { + for (path in pathList) + if (pathExists(path)) + return path + + return null + } + fun read(path: String): ByteBuffer { return readOrNull(path) ?: throw FileNotFoundException("No such file $path") } @@ -21,6 +44,35 @@ interface IVFS { } fun listFiles(path: String): Collection + fun listDirectories(path: String): Collection + + fun listFilesAndDirectories(path: String): Collection { + val a = listFiles(path) + val b = listDirectories(path) + + return ArrayList(a.size + b.size).also { it.addAll(a); it.addAll(b) } + } + + fun listAllFiles(path: String): Collection { + val lists = mutableListOf>() + + lists.add(listFiles(path)) + + for (dir in listDirectories(path)) { + lists.add(listAllFiles(dir)) + } + + // flatten медленный + // return lists.flatten() + + var size = 0 + + for (list in lists) { + size += list.size + } + + return ArrayList(size).also { lists.forEach(it::addAll) } + } fun readDirect(path: String): ByteBuffer { val read = read(path) @@ -39,6 +91,10 @@ interface IVFS { } } +fun getPathFolder(path: String): String { + return path.substring(0, path.lastIndexOf('/')) +} + class PhysicalFS(root: File) : IVFS { val root: File = root.absoluteFile @@ -71,8 +127,20 @@ class PhysicalFS(root: File) : IVFS { if (path.contains("..")) { return listOf() } + val fpath = File(root.absolutePath, path) - return fpath.listFiles()?.map { + return fpath.listFiles()?.filter { it.isFile }?.map { + it.path.replace('\\', '/').substring(root.path.length) + } ?: return listOf() + } + + override fun listDirectories(path: String): Collection { + if (path.contains("..")) { + return listOf() + } + + val fpath = File(root.absolutePath, path) + return fpath.listFiles()?.filter { it.isDirectory }?.map { it.path.replace('\\', '/').substring(root.path.length) } ?: return listOf() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt index 49be590e..a4aa74b5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt @@ -266,6 +266,9 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk val relative = renderer.renderPos - posVector2d @@ -284,7 +287,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk() override fun onEntityAdded(entity: Entity) { - entityRenderers[entity] = EntityRenderer(state, entity, this) + entityRenderers[entity] = EntityRenderer.getRender(state, entity, this) } override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/ErrorCheck.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/ErrorCheck.kt index 27f0e5fa..b439ee9b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/ErrorCheck.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/ErrorCheck.kt @@ -9,7 +9,7 @@ import org.lwjgl.opengl.GL46.* // GL_STACK_UNDERFLOW // GL_OUT_OF_MEMORY -sealed class OpenGLError(message: String, val statusCode: Int) : Throwable(message) +sealed class OpenGLError(message: String, val statusCode: Int) : RuntimeException(message) class OpenGLUnknownError(statusCode: Int, message: String = "Unknown OpenGL error occured: $statusCode") : OpenGLError(message, statusCode) 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 0b862ae1..693a1fa0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt @@ -253,11 +253,30 @@ class GLStateTracker { private val named2DTextures = HashMap() fun loadNamedTexture(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D { - if (!Starbound.pathExists(path)) { - throw FileNotFoundException("Unable to locate $path") + return named2DTextures.computeIfAbsent(path) { + if (!Starbound.pathExists(path)) { + throw FileNotFoundException("Unable to locate $path") + } + + return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path), memoryFormat, fileFormat).generateMips() + } + } + + private var loadedEmptyTexture = false + private val missingTexturePath = "/assetmissing.png" + + fun loadNamedTextureSafe(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D { + if (!loadedEmptyTexture) { + loadedEmptyTexture = true + named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), memoryFormat, fileFormat).generateMips() } return named2DTextures.computeIfAbsent(path) { + if (!Starbound.pathExists(path)) { + LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath) + return@computeIfAbsent named2DTextures[missingTexturePath]!! + } + return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path), memoryFormat, fileFormat).generateMips() } } @@ -359,6 +378,16 @@ class GLStateTracker { } } + val flat2DTexturedQuads = object : GLStreamBuilderList { + override val small by lazy { + return@lazy StreamVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS, 1024) + } + + override val statefulSmall by lazy { + return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small) + } + } + val flat2DQuadLines = object : GLStreamBuilderList { override val small by lazy { return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt index f52e6bb6..a55a41ff 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt @@ -45,6 +45,22 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "") : A var uploaded = false private set + val aspectRatioWH: Float get() { + if (height == 0) { + return 1f + } + + return width.toFloat() / height.toFloat() + } + + val aspectRatioHW: Float get() { + if (width == 0) { + return 1f + } + + return height.toFloat() / width.toFloat() + } + private var mipsWarning = 2 var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_LINEAR) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt index 7ef388bc..4421f8fe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt @@ -1,10 +1,13 @@ package ru.dbotthepony.kstarbound.client.render +import org.lwjgl.opengl.GL46.* import ru.dbotthepony.kstarbound.client.ClientChunk import ru.dbotthepony.kstarbound.client.gl.GLStateTracker +import ru.dbotthepony.kstarbound.client.gl.VertexTransformers import ru.dbotthepony.kstarbound.math.Matrix4fStack import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.world.entities.Entity +import ru.dbotthepony.kstarbound.world.entities.Projectile import java.io.Closeable /** @@ -21,7 +24,7 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va open fun renderDebug() { if (chunk?.world?.client?.settings?.debugCollisions == true) { - state.quadWireframe(entity.worldaabb) + state.quadWireframe(entity.movement.worldAABB) } } @@ -30,4 +33,46 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va override fun close() { } + + companion object { + fun getRender(state: GLStateTracker, entity: Entity, chunk: ClientChunk? = null): EntityRenderer { + return when (entity) { + is Projectile -> ProjectileRenderer(state, entity, chunk) + else -> EntityRenderer(state, entity, chunk) + } + } + } +} + +open class ProjectileRenderer(state: GLStateTracker, entity: Projectile, chunk: ClientChunk?) : EntityRenderer(state, entity, chunk) { + private val def = entity.def + private val texture = state.loadNamedTextureSafe(def.image.texture) + private val animator = FrameSetAnimator(def.image, def.animationCycle, true) + + init { + texture.textureMagFilter = GL_NEAREST + } + + override fun render(stack: Matrix4fStack) { + state.shaderVertexTexture.use() + state.shaderVertexTexture.transform.set(stack.last) + state.activeTexture = 0 + state.shaderVertexTexture["_texture"] = 0 + texture.bind() + + animator.advance() + + val stateful = state.flat2DTexturedQuads.statefulSmall + val builder = stateful.builder + + builder.begin() + + val (u0, v0) = texture.pixelToUV(def.image.frames[animator.frame].texturePosition) + val (u1, v1) = texture.pixelToUV(def.image.frames[animator.frame].textureEndPosition) + + builder.quadZ(0f, 0f, 1f, def.image.frames[animator.frame].aspectRatioHW, 5f, VertexTransformers.uv(u0, v0, u1, v1)) + + stateful.upload() + stateful.draw() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/FrameSetAnimator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/FrameSetAnimator.kt new file mode 100644 index 00000000..5f0c448e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/FrameSetAnimator.kt @@ -0,0 +1,69 @@ +package ru.dbotthepony.kstarbound.client.render + +import org.lwjgl.glfw.GLFW.glfwGetTime +import ru.dbotthepony.kstarbound.defs.FrameSet + +/** + * Анимирует заданный FrameSet + */ +class FrameSetAnimator( + val set: FrameSet, + + /** + * Сколько времени занимает один кадр + */ + var animationCycle: Double, + + /** + * Зациклить ли анимацию + */ + var animationLoops: Boolean, +) { + /** + * Последний кадр анимации + */ + var lastFrame = set.frameCount - 1 + + /** + * Первый кадр анимации + */ + var firstFrame = 0 + + var frame = 0 + private set + + /** + * Возвращает разницу между последним и первым кадром анимации + */ + val frameDiff get() = lastFrame - firstFrame + + private val initial = glfwGetTime() + private var lastRender = initial + + /** + * Сколько времени прошло с момента последнего кадра + */ + val delta get() = glfwGetTime() - lastRender + + private var counter = 0.0 + + /** + * Проверяет glfw таймер и продвигает фрейм анимации + */ + fun advance() { + if (frameDiff == 0) + return + + if (frame + frameDiff >= lastFrame && !animationLoops) { + return + } + + counter += delta / animationCycle + lastRender = glfwGetTime() + + if (counter >= 1.0) { + frame = (frame + counter.toInt()) % frameDiff + counter %= 1.0 + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Animation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Animation.kt new file mode 100644 index 00000000..81937520 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Animation.kt @@ -0,0 +1,14 @@ +package ru.dbotthepony.kstarbound.defs + +import ru.dbotthepony.kstarbound.math.Vector2i + +class AnimationDefinitionBuilder { + var frames: String? = null + var variants: Int? = null + var frameNumber: Int? = null + var animationCycle: Double? = null + var offset: Vector2i? = null +} + +class AnimationDefinition { +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ConfigurableDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ConfigurableDefinition.kt new file mode 100644 index 00000000..7d37ee6c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ConfigurableDefinition.kt @@ -0,0 +1,269 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.gson.* +import com.google.gson.internal.bind.TypeAdapters +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.Starbound +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KType +import kotlin.reflect.full.isSuperclassOf + +private fun flattenJsonPrimitive(input: JsonPrimitive): Any { + if (input.isNumber) { + return input.asNumber + } else if (input.isString) { + return input.asString.intern() + } else { + return input.asBoolean + } +} + +private fun flattenJsonArray(input: JsonArray): ArrayList { + val flattened = ArrayList(input.size()) + + for (v in input) { + when (v) { + is JsonObject -> flattened.add(flattenJsonObject(v)) + is JsonArray -> flattened.add(flattenJsonArray(v)) + is JsonPrimitive -> flattened.add(flattenJsonPrimitive(v)) + // is JsonNull -> baked.add(null) + } + } + + return flattened +} + +private fun flattenJsonObject(input: JsonObject): Object2ObjectArrayMap { + val flattened = Object2ObjectArrayMap() + + for ((k, v) in input.entrySet()) { + when (v) { + is JsonObject -> flattened[k] = flattenJsonObject(v) + is JsonArray -> flattened[k] = flattenJsonArray(v) + is JsonPrimitive -> flattened[k] = flattenJsonPrimitive(v) + } + } + + return flattened +} + +fun flattenJsonElement(input: JsonElement): Any? { + return when (input) { + is JsonObject -> flattenJsonObject(input) + is JsonArray -> flattenJsonArray(input) + is JsonPrimitive -> flattenJsonPrimitive(input) + is JsonNull -> null + else -> throw IllegalArgumentException("Unknown argument $input") + } +} + +/** + * Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов + */ +fun enrollList(input: List): ImmutableList { + val builder = ImmutableList.builder() + + for (v in input) { + when (v) { + is Map<*, *> -> builder.add(enrollMap(v as Map)) + is List<*> -> builder.add(enrollList(v as List)) + else -> builder.add((v as? String)?.intern() ?: v) + } + } + + return builder.build() +} + +/** + * Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов + */ +fun enrollMap(input: Map): ImmutableMap { + val builder = ImmutableMap.builder() + + for ((k, v) in input) { + when (v) { + is Map<*, *> -> builder.put(k.intern(), enrollMap(v as Map)) + is List<*> -> builder.put(k.intern(), enrollList(v as List)) + else -> builder.put(k.intern(), (v as? String)?.intern() ?: v) + } + } + + return builder.build() +} + +/** + * Возвращает глубокую изменяемую копию [input] примитивов/List'ов/Map'ов + */ +fun flattenList(input: List): ArrayList { + val list = ArrayList(input.size) + + for (v in input) { + when (v) { + is Map<*, *> -> list.add(flattenMap(v as Map)) + is List<*> -> list.add(flattenList(v as List)) + else -> list.add(v) + } + } + + return list +} + +fun flattenMap(input: Map): Object2ObjectArrayMap { + val map = Object2ObjectArrayMap() + + for ((k, v) in input) { + when (v) { + is Map<*, *> -> map[k] = flattenMap(v as Map) + is List<*> -> map[k] = flattenList(v as List) + else -> map[k] = v + } + } + + return map +} + +/** + * Базовый класс описания прототипа игрового объекта + * + * Должен иметь все (или больше) поля объекта, который он будет создавать + * + * Поля должны иметь базовые ограничения (т.е. ограничения, которые применимы для всех конфигураций прототипа). + * Если границы поля зависят от других полей, то проверка такого поля должна осуществляться уже при самой + * сборке прототипа. + */ +abstract class ConfigurableDefinition, Configured : ConfiguredDefinition> { + val json = Object2ObjectArrayMap() + fun enroll() = enrollMap(json) + abstract fun configure(directory: String = ""): Configured +} + +/** + * Базовый класс описанного прототипа игрового объекта + * + * Должен иметь все поля объекта, которые будут использоваться движком напрямую + * + * Создается соответствующим [ConfigurableDefinition], который проверил уже все поля + * на их правильность. + */ +abstract class ConfiguredDefinition, Configurator : ConfigurableDefinition>( + val json: ImmutableMap +) { + open fun getParameter(key: String): Any? = json[key] + fun flatten() = flattenMap(json) + abstract fun reconfigure(): Configurator +} + +class ConfigurableTypeAdapter>(val factory: () -> T, vararg fields: KMutableProperty1) : TypeAdapter() { + private val mappedFields = Object2ObjectArrayMap>() + // потому что returnType медленный + private val mappedFieldsReturnTypes = Object2ObjectArrayMap() + private val loggedMisses = ObjectArraySet() + + init { + for (field in fields) { + // потому что в котлине нет понятия KProperty который не имеет getter'а, только setter + require(mappedFields.put(field.name, field as KMutableProperty1) == null) { "${field.name} is defined twice" } + mappedFieldsReturnTypes[field.name] = field.returnType + } + } + + val fields: Array> get() { + val iterator = mappedFields.values.iterator() + return Array(mappedFields.size) { iterator.next() } + } + + override fun write(writer: JsonWriter, value: T) { + TODO("Not yet implemented") + } + + override fun read(reader: JsonReader): T? { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull() + return null + } + + reader.beginObject() + val instance = factory.invoke() + + while (reader.hasNext()) { + val name = reader.nextName() + val field = mappedFields[name] + + if (field != null) { + try { + val peek = reader.peek() + val expectedType = mappedFieldsReturnTypes[name]!! + + if (!expectedType.isMarkedNullable && peek == JsonToken.NULL) { + throw IllegalArgumentException("Property ${field.name} of ${instance::class.qualifiedName} does not accept nulls") + } else if (peek == JsonToken.NULL) { + field.set(instance, null) + reader.nextNull() + } else { + val classifier = expectedType.classifier + + if (classifier is KClass<*>) { + if (classifier.isSuperclassOf(Float::class)) { + val read = reader.nextDouble() + instance.json[name] = read + field.set(instance, read.toFloat()) + } else if (classifier.isSuperclassOf(Double::class)) { + val read = reader.nextDouble() + instance.json[name] = read + field.set(instance, read) + } else if (classifier.isSuperclassOf(Int::class)) { + val read = reader.nextInt() + instance.json[name] = read + field.set(instance, read) + } else if (classifier.isSuperclassOf(Long::class)) { + val read = reader.nextLong() + instance.json[name] = read + field.set(instance, read) + } else if (classifier.isSuperclassOf(String::class)) { + val read = reader.nextString() + instance.json[name] = read + field.set(instance, read) + } else if (classifier.isSuperclassOf(Boolean::class)) { + val read = reader.nextBoolean() + instance.json[name] = read + field.set(instance, read) + } else { + val readElement = TypeAdapters.JSON_ELEMENT.read(reader) + instance.json[name] = flattenJsonElement(readElement) + field.set(instance, Starbound.gson.fromJson(readElement, classifier.java)) + } + } else { + throw IllegalStateException("Expected ${field.name} classifier to be KClass, got $classifier") + } + } + } catch(err: Throwable) { + throw JsonSyntaxException("Reading property ${field.name} of ${instance::class.qualifiedName} near ${reader.path}", err) + } + } else { + instance.json[name] = TypeAdapters.JSON_ELEMENT.read(reader) + + if (!loggedMisses.contains(name)) { + loggedMisses.add(name) + LOGGER.warn("{} has no property for storing {}, this value will be visible to Lua scripts only", instance::class.qualifiedName, name) + } + } + } + + reader.endObject() + return instance + } + + companion object { + private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt new file mode 100644 index 00000000..0a5e22fe --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt @@ -0,0 +1,355 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.common.collect.ImmutableList +import com.google.gson.* +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.math.Vector2i +import java.io.FileNotFoundException +import kotlin.collections.HashMap + +class MalformedFrameGridException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) + +data class Frame( + val texture: String, + val texturePosition: Vector2i, + val textureSize: Vector2i, +) { + val textureEndPosition = texturePosition + textureSize + val width get() = textureSize.x + val height get() = textureSize.y + + val aspectRatioWH: Float get() { + if (height == 0) { + return 1f + } + + return width.toFloat() / height.toFloat() + } + + val aspectRatioHW: Float get() { + if (width == 0) { + return 1f + } + + return height.toFloat() / width.toFloat() + } +} + +data class FrameSet( + val texture: String, + val name: String, + val frames: List, +) { + val frameCount get() = frames.size + fun frame(num: Int) = frames[num] +} + +private class FrameSetBuilder(val name: String) { + val frames = Object2ObjectArrayMap() + + fun build(texture: String): FrameSet { + val list = ImmutableList.builder() + val rebuild = Int2ObjectArrayMap() + + for ((k, v) in frames) { + val int = k.toIntOrNull() + + if (int != null) { + rebuild[int] = v + } + } + + if (rebuild.size == 0) { + throw IllegalStateException("Frame Set $name is empty") + } + + for (i in 0 until rebuild.size) { + list.add(rebuild[i] ?: throw IllegalStateException("Frame Set $name has gap at $i")) + } + + return FrameSet( + texture = texture, + name = name, + frames = list.build() + ) + } +} + +interface IFrameGrid { + val texture: String + val size: Vector2i + val dimensions: Vector2i + val frames: List + val frameCount get() = frames.size + val isResolved: Boolean + + val textureSize get() = size * dimensions + + fun resolve(determinedSize: Vector2i) + + operator fun get(index: Int) = frames[index] + + operator fun get(index: String): FrameSet { + for (frame in frames) { + if (index == frame.name) { + return frame + } + } + + throw IndexOutOfBoundsException("No such frame strip with name $index") + } + + companion object { + private fun splitName(textureName: String, input: String, warn: Boolean, lazy: () -> String): Pair { + val split = input.split(':') + val frameName: String + val setName: String + + when (split.size) { + 1 -> { + setName = "root" + frameName = split[0] + } + 2 -> { + setName = split[0] + frameName = split[1] + } + else -> throw IllegalArgumentException("${lazy.invoke()}: Malformed frame name $input") + } + + val frameNumber = frameName.toIntOrNull() + + if (frameNumber == null) { + if (warn) + LOGGER.warn("{}: Frame {} will be discarded after frame grid is built, because it is not an integer", textureName, frameName) + } else { + require(frameNumber >= 0) { "${lazy.invoke()}: Frame number of $frameNumber does not make any sense" } + } + + return setName to frameName + } + + private fun generateFakeNames(dimensions: Vector2i): JsonArray { + return JsonArray(dimensions.y).also { + var stripElem = 0 + + for (stripNum in 0 until dimensions.y) { + val strip = JsonArray(dimensions.x) + + for (i in 0 until dimensions.x) { + strip.add(stripElem.toString()) + stripElem++ + } + + it.add(strip) + } + } + } + + /** + * Так как объект довольно сложный для автоматической десериализации через gson + * поэтому вот так + */ + fun fromJson(input: JsonObject, texture: String): IFrameGrid { + val frameGrid: JsonObject + val aliases: JsonObject + val texturePath = "$texture.png".intern() + + if (input["frameGrid"] is JsonObject) { + frameGrid = input["frameGrid"] as JsonObject + } else { + frameGrid = input + } + + if (input["aliases"] is JsonObject) { + aliases = input["aliases"] as JsonObject + } else { + aliases = JsonObject() + } + + val size = Starbound.gson.fromJson(frameGrid["size"], Vector2i::class.java) ?: throw IllegalArgumentException("Size is missing") + val dimensions = Starbound.gson.fromJson(frameGrid["dimensions"], Vector2i::class.java) ?: throw IllegalArgumentException("Dimensions are missing") + + require(size.x > 0) { "Invalid texture width of ${size.x}" } + require(size.y > 0) { "Invalid texture height of ${size.y}" } + + require(dimensions.x > 0) { "Invalid texture frame count of ${dimensions.x}" } + require(dimensions.y > 0) { "Invalid texture stripe count of ${dimensions.y}" } + + val names = frameGrid["names"] as? JsonArray ?: generateFakeNames(dimensions) + + if (names.size() != dimensions.y) { + LOGGER.warn("{} inconsistency: it has Y frame span of {}, but {} name strips are defined", texture, dimensions.y, names.size()) + } + + val frameSets = Object2ObjectArrayMap() + + for (yPosition in 0 until names.size()) { + val list = names[yPosition] as? JsonArray ?: throw IllegalArgumentException("names->$yPosition is not an array") + + if (list.size() != dimensions.x) { + LOGGER.warn("{} inconsistency: it has X frame span of {}, but strip at {} has {} names defined", texture, dimensions.x, yPosition, list.size()) + } + + for (xPosition in 0 until list.size()) { + val fullName = list[xPosition] + + if (fullName is JsonNull) { + continue + } + + fullName as? JsonPrimitive ?: throw IllegalArgumentException("names->$yPosition->$xPosition: Illegal value $fullName") + + val (setName, frameNumber) = splitName(texture, fullName.asString, true) { "names->$yPosition->$xPosition" } + val frameSet = frameSets.computeIfAbsent(setName, ::FrameSetBuilder) + + frameSet.frames[frameNumber] = Frame( + texture = texturePath, + textureSize = size, + texturePosition = Vector2i(x = size.x * xPosition, y = size.y * yPosition)) + } + } + + for ((newName, originalName) in aliases.entrySet()) { + originalName as? JsonPrimitive ?: throw IllegalArgumentException("aliases->$newName: Illegal value $originalName") + + val (oSetName, oFrameNumber) = splitName(texture, originalName.asString, false) { "alias->$newName" } + val (nSetName, nFrameNumber) = splitName(texture, newName, true) { "alias->$newName" } + + val oFrameSet = frameSets.computeIfAbsent(oSetName, ::FrameSetBuilder) + val nFrameSet = frameSets.computeIfAbsent(nSetName, ::FrameSetBuilder) + + nFrameSet.frames[nFrameNumber] = requireNotNull(oFrameSet.frames[oFrameNumber]) { "alias->$newName points to nothing" } + } + + val frameSetList = ImmutableList.builder() + + for (frameSet in frameSets.values) { + frameSetList.add(frameSet.build(texturePath)) + } + + return ResolvedFrameGrid(texturePath, size, dimensions, frameSetList.build()) + } + + fun singleFrame(texturePath: String, size: Vector2i = Vector2i.ZERO): IFrameGrid { + val frame = Frame( + texture = texturePath, + textureSize = size, + texturePosition = Vector2i.ZERO) + + return ResolvedFrameGrid( + texture = texturePath, + size = size, + dimensions = Vector2i.ONE_ONE, + frames = listOf(FrameSet( + name = "root", + frames = listOf(frame), + texture = texturePath)) + ) + } + + private val cache = HashMap() + + fun loadCached(path: String, weak: Boolean = false, weakSize: Vector2i = Vector2i.ZERO): IFrameGrid { + if (path[0] != '/') + throw IllegalArgumentException("Path must be absolute") + + val splitPath = path.split('/').toMutableList() + val last = splitPath.last() + splitPath.removeLast() + val splitLast = last.split('.') + + try { + if (splitLast.size == 1) { + // имя уже абсолютное + return cache.computeIfAbsent(path) { + val frames = Starbound.firstExistingOrNull("$path.frames", "${splitPath.joinToString("/")}/default.frames") + + if (weak && frames == null) { + LOGGER.warn("Expected animated texture at {}, but .frames metafile is missing.", path) + + return@computeIfAbsent singleFrame("$path.png", weakSize) + } + + return@computeIfAbsent fromJson(Starbound.loadJson(frames ?: throw FileNotFoundException("Unable to find .frames meta for $path")) as JsonObject, path) + } + } + + val newPath = "${splitPath.joinToString("/")}/${splitLast[0]}" + + return cache.computeIfAbsent(newPath) { + val frames = Starbound.firstExistingOrNull("$newPath.frames", "${splitPath.joinToString("/")}/default.frames") + + if (weak && frames == null) { + LOGGER.warn("Expected animated texture at {}, but .frames metafile is missing.", newPath) + + return@computeIfAbsent singleFrame("$newPath.png", weakSize) + } + + return@computeIfAbsent fromJson(Starbound.loadJson(frames ?: throw FileNotFoundException("Unable to find .frames meta for $path")) as JsonObject, newPath) + } + } catch (err: Throwable) { + throw MalformedFrameGridException("Reading animated texture definition $path", err) + } + } + + fun loadFrameStrip(path: String, weak: Boolean = false, weakSize: Vector2i = Vector2i.ZERO): FrameSet { + if (path[0] != '/') + throw IllegalArgumentException("Path must be absolute") + + val split = path.split(':') + + if (split.size == 1) { + // мы хотим получить главный кадр, который является анонимным + val load = loadCached(path, weak, weakSize) + check(load.frameCount == 1) { "$path has ${load.frameCount} frame strips, but we want exactly one!" } + return load[0] + } + + val load = loadCached(split[0], weak, weakSize) + return load[split[1]] + } + + private val LOGGER = LogManager.getLogger() + } +} + +data class ResolvedFrameGrid( + override val texture: String, + override val size: Vector2i, + override val dimensions: Vector2i, + + override val frames: List +) : IFrameGrid { + override val isResolved = true + + override fun resolve(determinedSize: Vector2i) { + require(determinedSize.x > 0) { "Invalid image width ${determinedSize.x}" } + require(determinedSize.y > 0) { "Invalid image height ${determinedSize.y}" } + // no-op + } +} + +data class LazyFrameGrid( + override val texture: String, + override val size: Vector2i, + override val dimensions: Vector2i, +) : IFrameGrid { + private var _frames: List? = null + + override val frames: List + get() = _frames ?: throw IllegalStateException("Call resolve() first") + + override var isResolved = false + private set + + override fun resolve(determinedSize: Vector2i) { + if (_frames != null) + return + + check(dimensions == determinedSize) { "$texture was expected to have dimensions of $dimensions, $determinedSize given" } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Helpers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Helpers.kt new file mode 100644 index 00000000..c2bf8f0c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Helpers.kt @@ -0,0 +1,8 @@ +package ru.dbotthepony.kstarbound.defs + +fun ensureAbsolutePath(path: String, parent: String): String { + if (path[0] == '/') + return path + + return "$parent/$path" +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ParticleDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ParticleDefinition.kt new file mode 100644 index 00000000..3fdcab53 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ParticleDefinition.kt @@ -0,0 +1,8 @@ +package ru.dbotthepony.kstarbound.defs + +class ParticleDefinitionBuilder { + var kind: String? = null + var animation: String? = null + var size: Double? = null + var timeToLive: Double? = null +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt new file mode 100644 index 00000000..d9361a39 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt @@ -0,0 +1,74 @@ +package ru.dbotthepony.kstarbound.defs.projectile + +import ru.dbotthepony.kstarbound.defs.ConfigurableDefinition +import ru.dbotthepony.kstarbound.defs.ConfigurableTypeAdapter +import ru.dbotthepony.kstarbound.defs.IFrameGrid +import ru.dbotthepony.kstarbound.defs.ensureAbsolutePath +import ru.dbotthepony.kstarbound.util.Color + +class ConfigurableProjectile : ConfigurableDefinition() { + var projectileName: String? = null + var physics: ProjectilePhysics = ProjectilePhysics.DEFAULT + var damageKindImage: String? = null + var damageType: String? = null + var damageKind: String? = null + + var pointLight: Boolean = false + var lightColor: Color? = null + + var onlyHitTerrain: Boolean = false + var orientationLocked: Boolean = false + + var image: String? = null + + var timeToLive: Double = Double.POSITIVE_INFINITY + var animationCycle: Double = Double.POSITIVE_INFINITY + var bounces: Int = -1 + var frameNumber: Int = 1 + + var scripts: Array = Array(0) { "" } + + var hydrophobic: Boolean = false + + override fun configure(directory: String): ConfiguredProjectile { + return ConfiguredProjectile( + json = enroll(), + projectileName = checkNotNull(projectileName) { "projectileName is null" }, + physics = physics, + damageKindImage = damageKindImage, + damageType = damageType, + damageKind = damageKind, + pointLight = pointLight, + lightColor = lightColor, + onlyHitTerrain = onlyHitTerrain, + orientationLocked = orientationLocked, + image = IFrameGrid.loadFrameStrip(ensureAbsolutePath(requireNotNull(image) { "image is null" }, directory), weak = true), + timeToLive = timeToLive, + animationCycle = animationCycle, + bounces = bounces, + frameNumber = frameNumber, + scripts = scripts, + ) + } + + companion object { + val ADAPTER = ConfigurableTypeAdapter( + ::ConfigurableProjectile, + ConfigurableProjectile::projectileName, + ConfigurableProjectile::physics, + ConfigurableProjectile::damageKindImage, + ConfigurableProjectile::damageType, + ConfigurableProjectile::damageKind, + ConfigurableProjectile::pointLight, + ConfigurableProjectile::lightColor, + ConfigurableProjectile::onlyHitTerrain, + ConfigurableProjectile::orientationLocked, + ConfigurableProjectile::image, + ConfigurableProjectile::timeToLive, + ConfigurableProjectile::animationCycle, + ConfigurableProjectile::bounces, + ConfigurableProjectile::frameNumber, + ConfigurableProjectile::scripts, + ) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configured.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configured.kt new file mode 100644 index 00000000..7a5dd141 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configured.kt @@ -0,0 +1,33 @@ +package ru.dbotthepony.kstarbound.defs.projectile + +import com.google.common.collect.ImmutableMap +import ru.dbotthepony.kstarbound.defs.ConfiguredDefinition +import ru.dbotthepony.kstarbound.defs.FrameSet +import ru.dbotthepony.kstarbound.util.Color + +class ConfiguredProjectile( + json: ImmutableMap, + val projectileName: String, + val physics: ProjectilePhysics, + val damageKindImage: String?, + val damageType: String?, + val damageKind: String?, + val pointLight: Boolean, + val lightColor: Color?, + val onlyHitTerrain: Boolean, + val orientationLocked: Boolean, + val image: FrameSet, + val timeToLive: Double, + val animationCycle: Double, + val bounces: Int, + val frameNumber: Int, + val scripts: Array, +) : ConfiguredDefinition(json) { + override fun reconfigure(): ConfigurableProjectile { + TODO("Not yet implemented") + } + + override fun toString(): String { + return "ConfiguredProjectile($projectileName)" + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt new file mode 100644 index 00000000..ecd55cfe --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt @@ -0,0 +1,113 @@ +package ru.dbotthepony.kstarbound.defs.projectile + +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kstarbound.util.IStringSerializable + +enum class ProjectilePhysics(private vararg val aliases: String) : IStringSerializable { + GAS, + LASER, + BOOMERANG, + DEFAULT, + BULLET, + STICKY_BULLET("STICKYBULLET"), + ARROW, + UNDERWATER_ARROW("UNDERWATERARROW"), + UNDERWATER_ARROW_NO_STICKY("UNDERWATERARROWNOSTICKY"), + ROCKET, + GRAVITY_BULLET("GRAVITYBULLET"), + FLAME, + ARROW_NO_STICKY("ARROWNOSTICKY"), + + SQUIRT, + FLYBUG, + ROLLER, + BOWLDER, + SMOOTH_ROLLING_BOULDER("SMOOTHROLLINGBOULDER"), + ROLLING_BOULDER("ROLLINGBOULDER"), + + DRAGON_BONE("DRAGONBONE"), + DRAGON_HEAD("DRAGONHEAD"), + + STICKY, + BOWLING_BALL("BOWLINGBALL"), + PAPER_PLANE("PAPERPLANE"), + BOULDER, + + STATUS_POD("STATUSPOD"), + + // ??? + ILLUSION, + ILLUSION_ROCKET("ROCKETILLUSION"), + + // ????????????? + FRIENDLY_BUBBLE("FRIENDLYBUBBLE"), + + STICKY_HEAVY_GAS("STICKYHEAVYGAS"), + HEAVY_GAS("HEAVYGAS"), + BOUNCY_GAS("BOUNCYGAS"), + FIREBALL, + SLIDER, + GOOP, + HOVER, + + BONE_THORN("BONETHORN"), + + BIG_BUBBLE("BIGBUBBLE"), + FIREWORK_FALL("FIREWORKFALL"), + LIGHTNING_BOLT("LIGHTNINGBOLT"), + SIMPLE_ARC("SIMPLEARC"), + LOW_GRAVITY_ARC("LOWGRAVARC"), + + SPIKE_BALL("SPIKEBALL"), + SHRAPNEL, + + // что + WEATHER, + + FIRE_SPREAD("FIRESPREAD"), + + GRAPPLE_HOOK("GRAPPLEHOOK"), + BALLISTIC_GRAPPLE_HOOK("BALLISTICGRAPPLEHOOK"), + + FLOATY_STICKY_BOMB("FLOATYSTICKYBOMB"), + STICKY_BOMB("STICKYBOMB"), + BOUNCY, + GRAVITY_BOMB("GRAVITYBOMB"), + DISC, + HEAVY_BOUNCER("HEAVYBOUNCER"), + + WALL_STICKY("WALLSTICKY"), + FISHING_LURE_SINKING("FISHINGLURESINKING"), + FISHING_LURE("FISHINGLURE"), + RAIN("RAIN"), + + PET_BALL("PETBALL"), + BOUNCY_BALL("BOUNCYBALL"), + BEACH_BALL("BEACHBALL"), + NOVELTY_BANANA("NOVELTYBANANA"), + + SPACE_MINE("SPACEMINE"), + MECH_BATTERY("MECHBATTERY"), + + GRENADE, + GRENADE_LARGE("LARGEGRENADE"), + GRENADE_Z_BOMB("GRENADEZBOMB"), + GRENADE_STICKY("STICKYGRENADE"), + GRENADE_SUPER_GRAVITY("SUPERHIGHGRAVGRENADE"), + GRENADE_HIGH_GRAVITY_V("VHIGHGRAVGRENADE"), + GRENADE_HIGH_GRAVITY("HIGHGRAVGRENADE"), + GRENADE_LOW_BOUNCE("GRENADELOWBOUNCE"), + GRENADE_NO_BOUNCE("GRENADENOBOUNCE"); + + override fun match(name: String): Boolean { + for (alias in aliases) + if (name == alias) + return true + + return name == this.name + } + + override fun write(out: JsonWriter) { + out.value(this.name) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt index 204fd8d2..c134e5b0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt @@ -70,13 +70,12 @@ class StarboundPakFile( } class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory? = null) { - private val files = HashMap() - private val directories = HashMap() + val files = HashMap() + val directories = HashMap() fun resolve(path: Array, level: Int = 0): StarboundPakDirectory { - if (path.size == level) { + if (path.size == level) return this - } if (level == 0 && path[0] == "" && name == "/") return resolve(path, 1) @@ -94,6 +93,7 @@ class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory? fun getDirectory(name: String) = directories[name] fun listFiles(): Collection = Collections.unmodifiableCollection(files.values) + fun listDirectories(): Collection = Collections.unmodifiableCollection(directories.values) fun writeFile(file: StarboundPakFile) { files[file.name.split('/').last()] = file @@ -105,9 +105,9 @@ class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory? while (getParent != null) { if (getParent.parent != null) { - build = "${getParent.name}/$name" + build = "${getParent.name}/$build" } else { - build = "/$name" + build = "/$build" break } @@ -196,6 +196,10 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String) return root.resolve(path.split("/").toTypedArray()).listFiles().map { it.name } } + override fun listDirectories(path: String): Collection { + return root.resolve(path.split("/").toTypedArray()).listDirectories().map { it.fullName() } + } + override fun pathExists(path: String): Boolean { return indexNodes.containsKey(path) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt index 61c335b1..6888fbe7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/AABB.kt @@ -1,5 +1,8 @@ package ru.dbotthepony.kstarbound.math +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.api.IStruct2d import ru.dbotthepony.kstarbound.world.ChunkPos import kotlin.math.absoluteValue @@ -33,8 +36,8 @@ data class SweepResult( */ data class AABB(val mins: Vector2d, val maxs: Vector2d) { init { - require(mins.x < maxs.x) { "mins.x ${mins.x} is more or equal to maxs.x ${maxs.x}" } - require(mins.y < maxs.y) { "mins.y ${mins.y} is more or equal to maxs.y ${maxs.y}" } + require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" } + require(mins.y <= maxs.y) { "mins.y ${mins.y} is more than maxs.y ${maxs.y}" } } operator fun plus(other: AABB) = AABB(mins + other.mins, maxs + other.maxs) @@ -367,6 +370,31 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) { } } +object AABBTypeAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: AABB) { + `out`.beginArray() + Vector2dTypeAdapter.write(out, value.mins) + Vector2dTypeAdapter.write(out, value.maxs) + `out`.endArray() + } + + override fun read(`in`: JsonReader): AABB { + val (x1, x2) = Vector2dTypeAdapter.read(`in`) + val (y1, y2) = Vector2dTypeAdapter.read(`in`) + + val xMins = x1.coerceAtMost(x2) + val xMaxs = x1.coerceAtLeast(x2) + + val yMins = y1.coerceAtMost(y2) + val yMaxs = y1.coerceAtLeast(y2) + + return AABB( + Vector2d(xMins, yMins), + Vector2d(xMaxs, yMaxs), + ) + } +} + data class AABBi(val mins: Vector2i, val maxs: Vector2i) { init { require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" } @@ -499,3 +527,29 @@ data class AABBi(val mins: Vector2i, val maxs: Vector2i) { val vectors: kotlin.collections.Iterator get() = Iterator(::Vector2i) val chunkPositions: kotlin.collections.Iterator get() = Iterator(::ChunkPos) } + +object AABBiTypeAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: AABBi) { + `out`.beginArray() + Vector2iTypeAdapter.write(out, value.mins) + Vector2iTypeAdapter.write(out, value.maxs) + `out`.endArray() + } + + override fun read(`in`: JsonReader): AABBi { + val (x1, x2) = Vector2iTypeAdapter.read(`in`) + val (y1, y2) = Vector2iTypeAdapter.read(`in`) + + val xMins = x1.coerceAtMost(x2) + val xMaxs = x1.coerceAtLeast(x2) + + val yMins = y1.coerceAtMost(y2) + val yMaxs = y1.coerceAtLeast(y2) + + return AABBi( + Vector2i(xMins, yMins), + Vector2i(xMaxs, yMaxs), + ) + } +} + diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Poly.kt new file mode 100644 index 00000000..43173e85 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Poly.kt @@ -0,0 +1,41 @@ +package ru.dbotthepony.kstarbound.math + +import com.google.common.collect.ImmutableList +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter + +class Poly(vararg points: Vector2d) { + val points: List = ImmutableList.copyOf(points) + + override fun toString(): String { + return "Poly($points)" + } +} + +object PolyTypeAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: Poly) { + `out`.beginArray() + + for (point in value.points) { + Vector2dTypeAdapter.write(out, point) + } + + `out`.endArray() + } + + override fun read(`in`: JsonReader): Poly { + `in`.beginArray() + + val points = mutableListOf() + + while (`in`.peek() == JsonToken.BEGIN_ARRAY) { + points.add(Vector2dTypeAdapter.read(`in`)) + } + + `in`.endArray() + + return Poly(*points.toTypedArray()) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt index e11e4ccc..af26d9f1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Vector.kt @@ -1,6 +1,9 @@ package ru.dbotthepony.kstarbound.math import com.google.gson.JsonArray +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.api.* import kotlin.math.cos import kotlin.math.pow @@ -131,6 +134,27 @@ data class Vector2i(override val x: Int = 0, override val y: Int = 0) : IVector2 val RIGHT = Vector2i().right() val UP = Vector2i().up() val DOWN = Vector2i().down() + val ONE_ONE = Vector2i(1, 1) + } +} + +object Vector2iTypeAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: Vector2i) { + `out`.beginArray() + `out`.value(value.x) + `out`.value(value.y) + `out`.endArray() + } + + override fun read(`in`: JsonReader): Vector2i { + `in`.beginArray() + + val x = `in`.nextInt() + val y = `in`.nextInt() + + `in`.endArray() + + return Vector2i(x, y) } } @@ -248,6 +272,26 @@ data class Vector2f(override val x: Float = 0f, override val y: Float = 0f) : IV } } +object Vector2fTypeAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: Vector2f) { + `out`.beginArray() + `out`.value(value.x) + `out`.value(value.y) + `out`.endArray() + } + + override fun read(`in`: JsonReader): Vector2f { + `in`.beginArray() + + val x = `in`.nextDouble().toFloat() + val y = `in`.nextDouble().toFloat() + + `in`.endArray() + + return Vector2f(x, y) + } +} + data class MutableVector2f(override var x: Float = 0f, override var y: Float = 0f) : IVector2f() { override fun make(x: Float, y: Float): MutableVector2f { this.x = x @@ -367,6 +411,26 @@ data class Vector2d(override val x: Double = 0.0, override val y: Double = 0.0) } } +object Vector2dTypeAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: Vector2d) { + `out`.beginArray() + `out`.value(value.x) + `out`.value(value.y) + `out`.endArray() + } + + override fun read(`in`: JsonReader): Vector2d { + `in`.beginArray() + + val x = `in`.nextDouble() + val y = `in`.nextDouble() + + `in`.endArray() + + return Vector2d(x, y) + } +} + data class MutableVector2d(override var x: Double = 0.0, override var y: Double = 0.0) : IVector2d() { override fun make(x: Double, y: Double): MutableVector2d { this.x = x diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Color.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Color.kt index 99cc4b9e..451d5195 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Color.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Color.kt @@ -1,12 +1,22 @@ package ru.dbotthepony.kstarbound.util import com.google.common.collect.ImmutableList -import com.google.gson.JsonArray +import com.google.gson.* +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.api.IStruct4f +import java.lang.reflect.Type data class Color(val red: Float, val green: Float, val blue: Float, val alpha: Float = 1f) : IStruct4f { constructor(input: JsonArray) : this(input[0].asFloat / 255f, input[1].asFloat / 255f, input[2].asFloat / 255f, if (input.size() >= 4) input[3].asFloat / 255f else 1f) + constructor(input: Long) : this( + ((input ushr 16) and 0xFFL).toFloat() / 255f, + ((input ushr 8) and 0xFFL).toFloat() / 255f, + (input and 0xFFL).toFloat() / 255f, + ) + companion object { val WHITE = Color(1f, 1f, 1f) @@ -16,6 +26,12 @@ data class Color(val red: Float, val green: Float, val blue: Float, val alpha: F val SLATE_GREY = Color(0.2f, 0.2f, 0.2f) + val PRE_DEFINED_MAP = mapOf( + "red" to RED, + "green" to GREEN, + "blue" to BLUE, + ) + val SHADES_OF_GRAY = ArrayList().let { for (i in 0 .. 256) { it.add(Color(i / 256f, i / 256f, i / 256f)) @@ -25,3 +41,91 @@ data class Color(val red: Float, val green: Float, val blue: Float, val alpha: F } } } + +object ColorTypeAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: Color) { + TODO("Not yet implemented") + } + + override fun read(`in`: JsonReader): Color { + when (val type = `in`.peek()) { + JsonToken.BEGIN_ARRAY -> { + `in`.beginArray() + val red = `in`.nextDouble() + val green = `in`.nextDouble() + val blue = `in`.nextDouble() + + if (red % 1.0 == 0.0 && green % 1.0 == 0.0 && blue % 1.0 == 0.0) { + val alpha = `in`.peek().let { if (it == JsonToken.END_ARRAY) 255.0 else `in`.nextDouble() } + `in`.endArray() + + return Color( + red.toFloat() / 255f, + green.toFloat() / 255f, + blue.toFloat() / 255f, + alpha.toFloat() / 255f, + ) + } else { + val alpha = `in`.peek().let { if (it == JsonToken.END_ARRAY) 1.0 else `in`.nextDouble() } + `in`.endArray() + + return Color( + red.toFloat(), + green.toFloat(), + blue.toFloat(), + alpha.toFloat(), + ) + } + } + + JsonToken.BEGIN_OBJECT -> { + `in`.beginObject() + + val keyed = mutableMapOf() + + while (`in`.peek() != JsonToken.END_OBJECT) { + keyed[`in`.nextName()] = `in`.nextDouble() + } + + if (keyed.isEmpty()) + throw IllegalArgumentException("Object is empty") + + var values = 0 + + val red = keyed["red"]?.also { values++ } ?: keyed["r"]?.also { values++ } ?: 255.0 + val green = keyed["green"]?.also { values++ } ?: keyed["g"]?.also { values++ } ?: 255.0 + val blue = keyed["blue"]?.also { values++ } ?: keyed["b"]?.also { values++ } ?: 255.0 + val alpha = keyed["alpha"]?.also { values++ } ?: keyed["a"]?.also { values++ } ?: 255.0 + `in`.endObject() + + if (values == 0) { + throw IllegalArgumentException("Object is not a color") + } + + if (red % 1.0 == 0.0 && green % 1.0 == 0.0 && blue % 1.0 == 0.0 && alpha % 1.0 == 0.0) { + return Color( + red.toFloat() / 255f, + green.toFloat() / 255f, + blue.toFloat() / 255f, + alpha.toFloat() / 255f, + ) + } else { + return Color( + red.toFloat(), + green.toFloat(), + blue.toFloat(), + alpha.toFloat(), + ) + } + } + + JsonToken.NUMBER -> return Color(`in`.nextLong()) + JsonToken.STRING -> { + val name = `in`.nextString() + return Color.PRE_DEFINED_MAP[name] ?: throw IllegalArgumentException("Unknown pre defined color name $name") + } + + else -> throw IllegalArgumentException("Expected array, object or number; got $type") + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/CustomEnumTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/CustomEnumTypeAdapter.kt new file mode 100644 index 00000000..ade9adae --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/CustomEnumTypeAdapter.kt @@ -0,0 +1,31 @@ +package ru.dbotthepony.kstarbound.util + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +interface IStringSerializable { + fun match(name: String): Boolean + fun write(out: JsonWriter) +} + +class CustomEnumTypeAdapter>(private val clazz: Array) : TypeAdapter() { + override fun write(out: JsonWriter, value: T) { + if (value is IStringSerializable) + value.write(out) + else + out.value(value.name) + } + + override fun read(`in`: JsonReader): T { + val str = `in`.nextString().uppercase() + + for (value in clazz) { + if (value is IStringSerializable && value.match(str) || value.name == str) { + return value + } + } + + throw IllegalArgumentException("${clazz[0]::class.java.name} does not have value for $str") + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 67dbd2c2..c791d196 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -492,6 +492,10 @@ abstract class Chunk, This : Chunk) : Entity(world) { - open var maxHealth = 10.0 - open var health = 10.0 - open val moveDirection = Move.STAND_STILL - override val collisionResolution = CollisionResolution.SLIDE - - open val aabbDucked get() = aabb - - override val currentaabb: AABB get() { - if (isDucked) { - return aabbDucked - } - - return super.currentaabb - } - - var wantsToDuck = false - var isDucked = false - protected set +interface IWalkableEntity : IEntity { + /** + * AABB сущности, которая стоит + */ + val standingAABB: AABB /** - * Максимальная скорость передвижения этого существа в Starbound Units/секунда + * AABB сущности, которая присела + */ + val duckingAABB: AABB + + /** + * Максимальная скорость передвижения этого AliveMovementController в Starbound Units/секунда * * Скорость передвижения: Это скорость вдоль земли (или в воздухе, если парит) при ходьбе. * * Если вектор скорости вдоль поверхности (или в воздухе, если парит) больше заданного значения, * то сущность быстро тормозит (учитывая силу трения) */ - open val topSpeed = 20.0 + val topSpeed: Double /** * Скорость ускорения сущности в Starbound Units/секунда^2 * - * Если сущность хочет двигаться вправо или влево (а также вверх или вниз, если парит), + * Если сущность хочет двигаться вправо или влево, * то она разгоняется с данной скоростью. */ - open val moveSpeed = 64.0 + val moveSpeed: Double /** * То, как сущность может влиять на свою скорость в Starbound Units/секунда^2 @@ -56,33 +48,83 @@ open class AliveEntity(world: World<*, *>) : Entity(world) { * * Позволяет в т.ч. игрокам изменять свою траекторию полёта в стиле Quake. */ - open val freeFallMoveSpeed = 8.0 + val freeFallMoveSpeed: Double /** * "Сила", с которой сущность останавливается, если не хочет двигаться. * * Зависит от текущего трения, так как технически является множителем трения поверхности, - * на которой стоит сущность. Если сущность парит, то сила трения является константой и не зависит от её окружения. + * на которой стоит сущность. */ - open val brakeForce = 32.0 - - /** - * Импульс прыжка данной сущности. Если сущность парит, то данное значение не несёт никакой - * полезной нагрузки. - */ - open val jumpForce = 20.0 + val brakeForce: Double /** * Высота шага данной сущности. Данное значение отвечает за то, на сколько блоков * сможет подняться сущность просто двигаясь в одном направлении без необходимости прыгнуть. */ - open val stepSize = 1.1 + val jumpForce: Double + + /** + * Импульс прыжка данной сущности. Если сущность парит, то данное значение не несёт никакой + * полезной нагрузки. + */ + val stepSize: Double +} + +/** + * Базовый абстрактный класс, реализующий сущность, которая ходит по земле + */ +abstract class WalkableMovementController(entity: T) : MovementController(entity) { + protected abstract val moveDirection: Move + override val collisionResolution = CollisionResolution.SLIDE + + var wantsToDuck = false + var isDucked = false + protected set + + override val currentAABB: AABB get() { + if (isDucked) { + return entity.duckingAABB + } + + return entity.standingAABB + } override fun thinkPhysics(delta: Double) { super.thinkPhysics(delta) thinkMovement(delta) } + /** + * Смотрим [IWalkableEntity.topSpeed] + */ + open val topSpeed by entity::topSpeed + + /** + * Смотрим [IWalkableEntity.moveSpeed] + */ + open val moveSpeed by entity::moveSpeed + + /** + * Смотрим [IWalkableEntity.freeFallMoveSpeed] + */ + open val freeFallMoveSpeed by entity::freeFallMoveSpeed + + /** + * Смотрим [IWalkableEntity.brakeForce] + */ + open val brakeForce by entity::brakeForce + + /** + * Смотрим [IWalkableEntity.stepSize] + */ + open val stepSize by entity::stepSize + + /** + * Смотрим [IWalkableEntity.jumpForce] + */ + open val jumpForce by entity::jumpForce + protected var jumpRequested = false protected var nextJump = 0.0 @@ -101,14 +143,13 @@ open class AliveEntity(world: World<*, *>) : Entity(world) { } } - open fun thinkMovement(delta: Double) { + protected open fun thinkMovement(delta: Double) { if (onGround || !affectedByGravity) { - var add: Vector2d + var add = Vector2d.ZERO if (isDucked) { thinkFriction(delta * brakeForce) - add = Vector2d.ZERO - } else { + } else if (velocity.y.absoluteValue < 1 && !jumpRequested) { when (moveDirection) { Move.STAND_STILL -> { thinkFriction(delta * brakeForce) @@ -147,7 +188,7 @@ open class AliveEntity(world: World<*, *>) : Entity(world) { if (world is ClientWorld && world.client.settings.debugCollisions) { world.client.onPostDrawWorldOnce { - world.client.gl.quadWireframe(worldaabb + velocity * delta * 4.0 + sweep.hitPosition, Color.RED) + world.client.gl.quadWireframe(worldAABB + velocity * delta * 4.0 + sweep.hitPosition, Color.RED) } } } else { @@ -165,14 +206,14 @@ open class AliveEntity(world: World<*, *>) : Entity(world) { if (world is ClientWorld && world.client.settings.debugCollisions) { world.client.onPostDrawWorldOnce { - world.client.gl.quadWireframe(worldaabb + sweep.hitPosition + sweep2.hitPosition, Color.GREEN) + world.client.gl.quadWireframe(worldAABB + sweep.hitPosition + sweep2.hitPosition, Color.GREEN) } } } if (world is ClientWorld && world.client.settings.debugCollisions) { world.client.onPostDrawWorldOnce { - world.client.gl.quadWireframe(worldaabb + sweep.hitPosition, Color.BLUE) + world.client.gl.quadWireframe(worldAABB + sweep.hitPosition, Color.BLUE) } } } @@ -210,9 +251,31 @@ open class AliveEntity(world: World<*, *>) : Entity(world) { if (wantsToDuck && onGround) { isDucked = true } else if (isDucked) { - if (world.isSpaceEmptyFromTiles(aabb + pos)) { + if (world.isSpaceEmptyFromTiles(entity.standingAABB + pos)) { isDucked = false } } } } + +abstract class AliveEntity(world: World<*, *>) : Entity(world) { + open var maxHealth = 10.0 + open var health = 10.0 +} + +abstract class AliveWalkingEntity(world: World<*, *>) : AliveEntity(world), IWalkableEntity { + abstract override val movement: WalkableMovementController<*> + + override val topSpeed = 20.0 + override val moveSpeed = 64.0 + override val freeFallMoveSpeed = 8.0 + override val brakeForce = 32.0 + override val jumpForce = 20.0 + override val stepSize = 1.1 + + open var wantsToDuck + get() = movement.wantsToDuck + set(value) { movement.wantsToDuck = value } + + open val isDucked get() = movement.isDucked +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt index 22b0560a..7ace1eab 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt @@ -1,7 +1,6 @@ package ru.dbotthepony.kstarbound.world.entities import org.apache.logging.log4j.LogManager -import ru.dbotthepony.kstarbound.client.render.EntityRenderer import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.lerp @@ -9,18 +8,29 @@ import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.World -enum class CollisionResolution { - STOP, - BOUNCE, - PUSH, - SLIDE, +/** + * Интерфейс служит лишь для убирания жёсткой зависимости от класса Entity + */ +interface IEntity { + val world: World<*, *> + var chunk: Chunk<*, *>? + var pos: Vector2d + var rotation: Double + val movement: MovementController<*> + val isSpawned: Boolean + val isRemoved: Boolean + + fun spawn() + fun remove() + fun think(delta: Double) + fun onTouchSurface(velocity: Vector2d, normal: Vector2d) } /** * Определяет из себя сущность в мире, которая имеет позицию, скорость и коробку столкновений */ -open class Entity(val world: World<*, *>) { - var chunk: Chunk<*, *>? = null +abstract class Entity(override val world: World<*, *>) : IEntity { + override var chunk: Chunk<*, *>? = null set(value) { if (!isSpawned) { throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world") @@ -50,10 +60,7 @@ open class Entity(val world: World<*, *>) { } } - open val currentaabb: AABB get() = aabb - open val worldaabb: AABB get() = currentaabb + pos - - var pos = Vector2d() + override var pos = Vector2d() set(value) { if (field == value) return @@ -71,13 +78,14 @@ open class Entity(val world: World<*, *>) { } } - var velocity = Vector2d() - var isSpawned = false + override var rotation: Double = 0.0 + + final override var isSpawned = false private set - var isRemoved = false + final override var isRemoved = false private set - fun spawn() { + override fun spawn() { if (isSpawned) throw IllegalStateException("Already spawned") @@ -90,7 +98,7 @@ open class Entity(val world: World<*, *>) { } } - fun remove() { + override fun remove() { if (isRemoved) throw IllegalStateException("Already removed") @@ -103,107 +111,24 @@ open class Entity(val world: World<*, *>) { } /** - * Касается ли сущность земли - * - * Данный флаг выставляется при обработке скорости, если данный флаг не будет выставлен - * правильно, то сущность будет иметь очень плохое движение в стороны - * - * Так же от него зависит то, может ли сущность двигаться, если она не парит - * - * Если сущность касается земли, то на неё не действует гравитация + * Контроллер перемещения данной сущности */ - var onGround = false - protected set(value) { - field = value - nextOnGroundUpdate = world.timer + 0.1 - } - - protected var nextOnGroundUpdate = 0.0 - - var groundNormal = Vector2d.ZERO - protected set - - protected var isHuggingAWall = false - - // наследуемые свойства - open val aabb = AABB.rectangle(Vector2d.ZERO, 0.9, 0.9) - open val affectedByGravity = true - open val collisionResolution = CollisionResolution.STOP - - protected open fun onTouchGround(velocity: Vector2d, normal: Vector2d) { - - } - - protected fun sweepRelative(velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(worldaabb, velocity, collisionResolution, delta) - protected fun sweepAbsolute(from: Vector2d, velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(aabb + from, velocity, collisionResolution, delta) - protected fun isSpaceOpen(relative: Vector2d, delta: Double) = !sweepRelative(relative, delta).hitAnything - - fun dropToFloor() { - val sweep = sweepRelative(Vector2d.DROP_TO_FLOOR, 1.0, CollisionResolution.STOP) - - if (!sweep.hitAnything) - return - - pos += sweep.hitPosition - } - - protected open fun propagateVelocity(delta: Double) { - if (velocity.length == 0.0) - return - - val sweep = sweepRelative(velocity * delta, delta) - this.velocity = sweep.hitPosition / delta - this.pos += this.velocity * delta - - if (nextOnGroundUpdate <= world.timer || !onGround) { - onGround = sweep.hitNormal.dotProduct(world.gravity.normalized) <= -0.98 - groundNormal = sweep.hitNormal - - if (!onGround) { - val sweepGround = sweepRelative(world.gravity * delta, delta) - onGround = sweepGround.hitAnything && sweepGround.hitNormal.dotProduct(world.gravity.normalized) <= -0.98 - groundNormal = sweepGround.hitNormal - } - } - } - - protected open fun thinkGravity(delta: Double) { - velocity += world.gravity * delta - } - - protected open fun thinkFriction(delta: Double) { - velocity *= Vector2d(lerp(delta, 1.0, 0.01), 1.0) - } - - protected open fun thinkPhysics(delta: Double) { - if (!onGround && affectedByGravity) - thinkGravity(delta) - - propagateVelocity(delta) - - if (affectedByGravity && onGround) - thinkFriction(delta) - - //dropToFloor() - } + abstract override val movement: MovementController<*> + protected abstract fun thinkAI(delta: Double) /** * Заставляет сущность "думать". */ - fun think(delta: Double) { + final override fun think(delta: Double) { if (!isSpawned) { throw IllegalStateException("Tried to think before spawning in world") } - thinkPhysics(delta) + movement.thinkPhysics(delta) thinkAI(delta) } - protected open fun thinkAI(delta: Double) { + override fun onTouchSurface(velocity: Vector2d, normal: Vector2d) { } - - companion object { - private val LOGGER = LogManager.getLogger(Entity::class.java) - } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt new file mode 100644 index 00000000..b3621e63 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt @@ -0,0 +1,141 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.Vector2d +import ru.dbotthepony.kstarbound.math.lerp + +enum class CollisionResolution { + STOP, + BOUNCE, + PUSH, + SLIDE, +} + +abstract class MovementController(val entity: T) { + val world = entity.world + var pos by entity::pos + var rotation by entity::rotation + + open val mass = 1.0 + + // наследуемые свойства + open val affectedByGravity = true + open val collisionResolution = CollisionResolution.STOP + + var velocity = Vector2d() + + /** + * Касается ли AABB сущности земли + * + * Данный флаг выставляется при обработке скорости, если данный флаг не будет выставлен + * правильно, то сущность будет иметь очень плохое движение в стороны + * + * Так же от него зависит то, может ли сущность двигаться, если она не парит + * + * Если сущность касается земли, то на неё не действует гравитация + */ + var onGround = false + protected set(value) { + field = value + nextOnGroundUpdate = world.timer + 0.1 + } + + /** + * Текущий AABB этого Movement Controller + * + * Это может быть как и статичное значение (для данного типа сущности), так и динамичное + * (к примеру, присевший игрок) + * + * Данное значение, хоть и является val, НЕ ЯВЛЯЕТСЯ КОНСТАНТОЙ! + */ + abstract val currentAABB: AABB + + /** + * Текущий AABB в отображении на мировые координаты + */ + val worldAABB: AABB get() = currentAABB + pos + + protected var nextOnGroundUpdate = 0.0 + + protected fun sweepRelative(velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(worldAABB, velocity, collisionResolution, delta) + protected fun sweepAbsolute(from: Vector2d, velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(currentAABB + from, velocity, collisionResolution, delta) + protected fun isSpaceOpen(relative: Vector2d, delta: Double) = !sweepRelative(relative, delta).hitAnything + + fun dropToFloor() { + val sweep = sweepRelative(Vector2d.DROP_TO_FLOOR, 1.0, CollisionResolution.STOP) + + if (!sweep.hitAnything) + return + + pos += sweep.hitPosition + } + + var groundNormal = Vector2d.ZERO + protected set + + protected open fun propagateVelocity(delta: Double) { + if (velocity.length == 0.0) + return + + val sweep = sweepRelative(velocity * delta, delta) + this.velocity = sweep.hitPosition / delta + this.pos += this.velocity * delta + + if (nextOnGroundUpdate <= world.timer || !onGround) { + onGround = sweep.hitNormal.dotProduct(world.gravity.normalized) <= -0.98 + groundNormal = sweep.hitNormal + + if (!onGround) { + val sweepGround = sweepRelative(world.gravity * delta, delta) + onGround = sweepGround.hitAnything && sweepGround.hitNormal.dotProduct(world.gravity.normalized) <= -0.98 + groundNormal = sweepGround.hitNormal + } + } + } + + protected open fun thinkGravity(delta: Double) { + velocity += world.gravity * delta + } + + protected open fun thinkFriction(delta: Double) { + velocity *= Vector2d(lerp(delta, 1.0, 0.01), 1.0) + } + + open fun thinkPhysics(delta: Double) { + if (!onGround && affectedByGravity) + thinkGravity(delta) + + propagateVelocity(delta) + + if (affectedByGravity && onGround) + thinkFriction(delta) + } + + protected open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) { + entity.onTouchSurface(velocity, normal) + } +} + +/** + * MovementController который ничего не делает (прям совсем) + */ +class DummyMovementController(entity: Entity) : MovementController(entity) { + override val currentAABB = DUMMY_AABB + override val affectedByGravity = false + + override fun propagateVelocity(delta: Double) { + // no-op + } + + override fun thinkGravity(delta: Double) { + // no-op + } + + override fun thinkFriction(delta: Double) { + // no-op + } + + companion object { + private val DUMMY_AABB = AABB.rectangle(Vector2d.ZERO, 0.1, 0.1) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt index 85a0dd7f..8c9b4f1c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt @@ -4,11 +4,19 @@ import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.world.World +class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController(entity) { + public override var moveDirection = Move.STAND_STILL +} + /** * Физический аватар игрока в мире */ -open class PlayerEntity(world: World<*, *>) : AliveEntity(world) { - override val aabb = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7) - override val aabbDucked: AABB = AABB.rectangle(Vector2d.ZERO, 1.8, 1.8) + Vector2d(y = -0.9) - override var moveDirection = Move.STAND_STILL +open class PlayerEntity(world: World<*, *>) : AliveWalkingEntity(world) { + override val standingAABB = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7) + override val duckingAABB = AABB.rectangle(Vector2d.ZERO, 1.8, 1.8) + Vector2d(y = -0.9) + override val movement = PlayerMovementController(this) + + override fun thinkAI(delta: Double) { + + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt new file mode 100644 index 00000000..ae78dabf --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt @@ -0,0 +1,12 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.defs.projectile.ConfiguredProjectile +import ru.dbotthepony.kstarbound.world.World + +class Projectile(world: World<*, *>, val def: ConfiguredProjectile) : Entity(world) { + override val movement: MovementController<*> = DummyMovementController(this) + + override fun thinkAI(delta: Double) { + + } +}