Подгрузка описания прожектайлов и их тест рендер

This commit is contained in:
DBotThePony 2022-02-10 22:16:17 +07:00
parent 4fc51530f7
commit ad8910d098
Signed by: DBot
GPG Key ID: DCC23B5715498507
30 changed files with 1835 additions and 191 deletions

View File

@ -31,11 +31,12 @@ tasks.compileKotlin {
dependencies { dependencies {
implementation(kotlin("stdlib")) implementation(kotlin("stdlib"))
implementation(kotlin("reflect"))
implementation("org.apache.logging.log4j:log4j-api:2.17.1") implementation("org.apache.logging.log4j:log4j-api:2.17.1")
implementation("org.apache.logging.log4j:log4j-core: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") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
implementation("com.google.code.gson:gson:2.8.9") implementation("com.google.code.gson:gson:2.8.9")

View File

@ -3,16 +3,15 @@ package ru.dbotthepony.kstarbound
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.Version import org.lwjgl.Version
import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
import ru.dbotthepony.kstarbound.api.PhysicalFS
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.io.StarboundPak
import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.entities.Move import ru.dbotthepony.kstarbound.world.entities.Move
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import ru.dbotthepony.kstarbound.world.entities.Projectile
import java.io.File import java.io.File
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
@ -20,6 +19,14 @@ private val LOGGER = LogManager.getLogger()
fun main() { fun main() {
LOGGER.info("Running LWJGL ${Version.getVersion()}") 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() val client = StarboundClient()
//Starbound.addFilePath(File("./unpacked_assets/")) //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 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() //val rand = Random()
@ -117,7 +130,7 @@ fun main() {
client.onDrawGUI { client.onDrawGUI {
client.gl.font.render("${ent.pos}", y = 100f, scale = 0.25f) 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 { client.onPreDrawWorld {
@ -140,17 +153,17 @@ fun main() {
//ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1 //ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1
if (client.input.KEY_LEFT_DOWN) { if (client.input.KEY_LEFT_DOWN) {
ent.moveDirection = Move.MOVE_LEFT ent.movement.moveDirection = Move.MOVE_LEFT
} else if (client.input.KEY_RIGHT_DOWN) { } else if (client.input.KEY_RIGHT_DOWN) {
ent.moveDirection = Move.MOVE_RIGHT ent.movement.moveDirection = Move.MOVE_RIGHT
} else { } else {
ent.moveDirection = Move.STAND_STILL ent.movement.moveDirection = Move.STAND_STILL
} }
if (client.input.KEY_SPACE_PRESSED && ent.onGround) { if (client.input.KEY_SPACE_PRESSED && ent.movement.onGround) {
ent.requestJump() ent.movement.requestJump()
} else if (client.input.KEY_SPACE_RELEASED) { } else if (client.input.KEY_SPACE_RELEASED) {
ent.recallJump() ent.movement.recallJump()
} }
if (client.input.KEY_ESCAPE_PRESSED) { if (client.input.KEY_ESCAPE_PRESSED) {

View File

@ -1,16 +1,22 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.google.gson.JsonElement import com.google.gson.*
import com.google.gson.JsonObject import org.apache.logging.log4j.LogManager
import com.google.gson.JsonParser
import ru.dbotthepony.kstarbound.api.IVFS import ru.dbotthepony.kstarbound.api.IVFS
import ru.dbotthepony.kstarbound.api.PhysicalFS import ru.dbotthepony.kstarbound.api.PhysicalFS
import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.api.getPathFolder
import ru.dbotthepony.kstarbound.defs.TileDefinitionBuilder 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.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.io.*
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.text.DateFormat
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.HashMap import kotlin.collections.HashMap
@ -22,10 +28,31 @@ const val PIXELS_IN_STARBOUND_UNIT = 8.0
const val PIXELS_IN_STARBOUND_UNITf = 8.0f const val PIXELS_IN_STARBOUND_UNITf = 8.0f
class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause) class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause)
class ProjectileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause)
object Starbound : IVFS { object Starbound : IVFS {
private val LOGGER = LogManager.getLogger()
private val tiles = HashMap<String, TileDefinition>() private val tiles = HashMap<String, TileDefinition>()
val tilesAccess = object : Map<String, TileDefinition> by tiles {} private val projectiles = HashMap<String, ConfiguredProjectile>()
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 var initializing = false
private set private set
@ -87,17 +114,36 @@ object Starbound : IVFS {
} }
} }
callback(false, false, "Loading materials...") run {
val localTime = System.currentTimeMillis()
loadTileMaterials { callback(false, false, "Loading materials...")
if (terminateLoading) {
throw InterruptedException("Game is terminating") 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 initializing = false
initialized = true initialized = true
@ -135,6 +181,16 @@ object Starbound : IVFS {
return listing return listing
} }
override fun listDirectories(path: String): Collection<String> {
val listing = mutableListOf<String>()
for (fs in fileSystems) {
listing.addAll(fs.listDirectories(path))
}
return listing
}
fun onInitialize(callback: () -> Unit) { fun onInitialize(callback: () -> Unit) {
if (initialized) { if (initialized) {
callback() callback()
@ -155,7 +211,7 @@ object Starbound : IVFS {
private fun loadTileMaterials(callback: (String) -> Unit) { private fun loadTileMaterials(callback: (String) -> Unit) {
for (fs in fileSystems) { for (fs in fileSystems) {
for (listedFile in fs.listFiles("tiles/materials")) { for (listedFile in fs.listAllFiles("tiles/materials")) {
if (listedFile.endsWith(".material")) { if (listedFile.endsWith(".material")) {
try { try {
callback("Loading $listedFile") 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)
}
}
}
}
}
} }

View File

@ -6,6 +6,29 @@ import java.nio.ByteBuffer
interface IVFS { interface IVFS {
fun pathExists(path: String): Boolean 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 { fun read(path: String): ByteBuffer {
return readOrNull(path) ?: throw FileNotFoundException("No such file $path") return readOrNull(path) ?: throw FileNotFoundException("No such file $path")
} }
@ -21,6 +44,35 @@ interface IVFS {
} }
fun listFiles(path: String): Collection<String> fun listFiles(path: String): Collection<String>
fun listDirectories(path: String): Collection<String>
fun listFilesAndDirectories(path: String): Collection<String> {
val a = listFiles(path)
val b = listDirectories(path)
return ArrayList<String>(a.size + b.size).also { it.addAll(a); it.addAll(b) }
}
fun listAllFiles(path: String): Collection<String> {
val lists = mutableListOf<Collection<String>>()
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<String>(size).also { lists.forEach(it::addAll) }
}
fun readDirect(path: String): ByteBuffer { fun readDirect(path: String): ByteBuffer {
val read = read(path) 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 { class PhysicalFS(root: File) : IVFS {
val root: File = root.absoluteFile val root: File = root.absoluteFile
@ -71,8 +127,20 @@ class PhysicalFS(root: File) : IVFS {
if (path.contains("..")) { if (path.contains("..")) {
return listOf() return listOf()
} }
val fpath = File(root.absolutePath, path) 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<String> {
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) it.path.replace('\\', '/').substring(root.path.length)
} ?: return listOf() } ?: return listOf()
} }

View File

@ -266,6 +266,9 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
layerQueue.add(baked::renderStacked to zLevel) layerQueue.add(baked::renderStacked to zLevel)
} }
//println("${entityRenderers.size} at $pos")
//println("${entities.size} at $pos")
for (renderer in entityRenderers.values) { for (renderer in entityRenderers.values) {
layerQueue.add(lambda@{ it: Matrix4fStack -> layerQueue.add(lambda@{ it: Matrix4fStack ->
val relative = renderer.renderPos - posVector2d val relative = renderer.renderPos - posVector2d
@ -284,7 +287,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
private val entityRenderers = HashMap<Entity, EntityRenderer>() private val entityRenderers = HashMap<Entity, EntityRenderer>()
override fun onEntityAdded(entity: Entity) { 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) { override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {

View File

@ -9,7 +9,7 @@ import org.lwjgl.opengl.GL46.*
// GL_STACK_UNDERFLOW // GL_STACK_UNDERFLOW
// GL_OUT_OF_MEMORY // 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) class OpenGLUnknownError(statusCode: Int, message: String = "Unknown OpenGL error occured: $statusCode") : OpenGLError(message, statusCode)

View File

@ -253,11 +253,30 @@ class GLStateTracker {
private val named2DTextures = HashMap<String, GLTexture2D>() private val named2DTextures = HashMap<String, GLTexture2D>()
fun loadNamedTexture(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D { fun loadNamedTexture(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D {
if (!Starbound.pathExists(path)) { return named2DTextures.computeIfAbsent(path) {
throw FileNotFoundException("Unable to locate $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) { 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() 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 { val flat2DQuadLines = object : GLStreamBuilderList {
override val small by lazy { override val small by lazy {
return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024) return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024)

View File

@ -45,6 +45,22 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
var uploaded = false var uploaded = false
private set 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 private var mipsWarning = 2
var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_LINEAR) var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_LINEAR)

View File

@ -1,10 +1,13 @@
package ru.dbotthepony.kstarbound.client.render package ru.dbotthepony.kstarbound.client.render
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.client.ClientChunk import ru.dbotthepony.kstarbound.client.ClientChunk
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker 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.Matrix4fStack
import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kstarbound.world.entities.Projectile
import java.io.Closeable import java.io.Closeable
/** /**
@ -21,7 +24,7 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va
open fun renderDebug() { open fun renderDebug() {
if (chunk?.world?.client?.settings?.debugCollisions == true) { 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() { 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()
}
} }

View File

@ -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
}
}
}

View File

@ -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 {
}

View File

@ -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<Any> {
val flattened = ArrayList<Any>(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<String, Any> {
val flattened = Object2ObjectArrayMap<String, Any>()
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<Any>): ImmutableList<Any> {
val builder = ImmutableList.builder<Any>()
for (v in input) {
when (v) {
is Map<*, *> -> builder.add(enrollMap(v as Map<String, Any>))
is List<*> -> builder.add(enrollList(v as List<Any>))
else -> builder.add((v as? String)?.intern() ?: v)
}
}
return builder.build()
}
/**
* Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов
*/
fun enrollMap(input: Map<String, Any>): ImmutableMap<String, Any> {
val builder = ImmutableMap.builder<String, Any>()
for ((k, v) in input) {
when (v) {
is Map<*, *> -> builder.put(k.intern(), enrollMap(v as Map<String, Any>))
is List<*> -> builder.put(k.intern(), enrollList(v as List<Any>))
else -> builder.put(k.intern(), (v as? String)?.intern() ?: v)
}
}
return builder.build()
}
/**
* Возвращает глубокую изменяемую копию [input] примитивов/List'ов/Map'ов
*/
fun flattenList(input: List<Any>): ArrayList<Any> {
val list = ArrayList<Any>(input.size)
for (v in input) {
when (v) {
is Map<*, *> -> list.add(flattenMap(v as Map<String, Any>))
is List<*> -> list.add(flattenList(v as List<Any>))
else -> list.add(v)
}
}
return list
}
fun flattenMap(input: Map<String, Any>): Object2ObjectArrayMap<String, Any> {
val map = Object2ObjectArrayMap<String, Any>()
for ((k, v) in input) {
when (v) {
is Map<*, *> -> map[k] = flattenMap(v as Map<String, Any>)
is List<*> -> map[k] = flattenList(v as List<Any>)
else -> map[k] = v
}
}
return map
}
/**
* Базовый класс описания прототипа игрового объекта
*
* Должен иметь все (или больше) поля объекта, который он будет создавать
*
* Поля должны иметь базовые ограничения (т.е. ограничения, которые применимы для всех конфигураций прототипа).
* Если границы поля зависят от других полей, то проверка такого поля должна осуществляться уже при самой
* сборке прототипа.
*/
abstract class ConfigurableDefinition<Configurable : ConfigurableDefinition<Configurable, Configured>, Configured : ConfiguredDefinition<Configured, Configurable>> {
val json = Object2ObjectArrayMap<String, Any>()
fun enroll() = enrollMap(json)
abstract fun configure(directory: String = ""): Configured
}
/**
* Базовый класс описанного прототипа игрового объекта
*
* Должен иметь все поля объекта, которые будут использоваться движком напрямую
*
* Создается соответствующим [ConfigurableDefinition], который проверил уже все поля
* на их правильность.
*/
abstract class ConfiguredDefinition<Configured : ConfiguredDefinition<Configured, Configurator>, Configurator : ConfigurableDefinition<Configurator, Configured>>(
val json: ImmutableMap<String, Any>
) {
open fun getParameter(key: String): Any? = json[key]
fun flatten() = flattenMap(json)
abstract fun reconfigure(): Configurator
}
class ConfigurableTypeAdapter<T : ConfigurableDefinition<*, *>>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapter<T>() {
private val mappedFields = Object2ObjectArrayMap<String, KMutableProperty1<T, in Any?>>()
// потому что returnType медленный
private val mappedFieldsReturnTypes = Object2ObjectArrayMap<String, KType>()
private val loggedMisses = ObjectArraySet<String>()
init {
for (field in fields) {
// потому что в котлине нет понятия KProperty который не имеет getter'а, только setter
require(mappedFields.put(field.name, field as KMutableProperty1<T, in Any?>) == null) { "${field.name} is defined twice" }
mappedFieldsReturnTypes[field.name] = field.returnType
}
}
val fields: Array<KMutableProperty1<T, in Any?>> 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)
}
}

View File

@ -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<Frame>,
) {
val frameCount get() = frames.size
fun frame(num: Int) = frames[num]
}
private class FrameSetBuilder(val name: String) {
val frames = Object2ObjectArrayMap<String, Frame>()
fun build(texture: String): FrameSet {
val list = ImmutableList.builder<Frame>()
val rebuild = Int2ObjectArrayMap<Frame>()
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<FrameSet>
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<String, String> {
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<String, FrameSetBuilder>()
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<FrameSet>()
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<String, IFrameGrid>()
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<FrameSet>
) : 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<FrameSet>? = null
override val frames: List<FrameSet>
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" }
}
}

View File

@ -0,0 +1,8 @@
package ru.dbotthepony.kstarbound.defs
fun ensureAbsolutePath(path: String, parent: String): String {
if (path[0] == '/')
return path
return "$parent/$path"
}

View File

@ -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
}

View File

@ -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<ConfigurableProjectile, ConfiguredProjectile>() {
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<String> = 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,
)
}
}

View File

@ -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<String, Any>,
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<String>,
) : ConfiguredDefinition<ConfiguredProjectile, ConfigurableProjectile>(json) {
override fun reconfigure(): ConfigurableProjectile {
TODO("Not yet implemented")
}
override fun toString(): String {
return "ConfiguredProjectile($projectileName)"
}
}

View File

@ -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)
}
}

View File

@ -70,13 +70,12 @@ class StarboundPakFile(
} }
class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory? = null) { class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory? = null) {
private val files = HashMap<String, StarboundPakFile>() val files = HashMap<String, StarboundPakFile>()
private val directories = HashMap<String, StarboundPakDirectory>() val directories = HashMap<String, StarboundPakDirectory>()
fun resolve(path: Array<String>, level: Int = 0): StarboundPakDirectory { fun resolve(path: Array<String>, level: Int = 0): StarboundPakDirectory {
if (path.size == level) { if (path.size == level)
return this return this
}
if (level == 0 && path[0] == "" && name == "/") if (level == 0 && path[0] == "" && name == "/")
return resolve(path, 1) return resolve(path, 1)
@ -94,6 +93,7 @@ class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory?
fun getDirectory(name: String) = directories[name] fun getDirectory(name: String) = directories[name]
fun listFiles(): Collection<StarboundPakFile> = Collections.unmodifiableCollection(files.values) fun listFiles(): Collection<StarboundPakFile> = Collections.unmodifiableCollection(files.values)
fun listDirectories(): Collection<StarboundPakDirectory> = Collections.unmodifiableCollection(directories.values)
fun writeFile(file: StarboundPakFile) { fun writeFile(file: StarboundPakFile) {
files[file.name.split('/').last()] = file files[file.name.split('/').last()] = file
@ -105,9 +105,9 @@ class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory?
while (getParent != null) { while (getParent != null) {
if (getParent.parent != null) { if (getParent.parent != null) {
build = "${getParent.name}/$name" build = "${getParent.name}/$build"
} else { } else {
build = "/$name" build = "/$build"
break 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 } return root.resolve(path.split("/").toTypedArray()).listFiles().map { it.name }
} }
override fun listDirectories(path: String): Collection<String> {
return root.resolve(path.split("/").toTypedArray()).listDirectories().map { it.fullName() }
}
override fun pathExists(path: String): Boolean { override fun pathExists(path: String): Boolean {
return indexNodes.containsKey(path) return indexNodes.containsKey(path)
} }

View File

@ -1,5 +1,8 @@
package ru.dbotthepony.kstarbound.math 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.api.IStruct2d
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -33,8 +36,8 @@ data class SweepResult(
*/ */
data class AABB(val mins: Vector2d, val maxs: Vector2d) { data class AABB(val mins: Vector2d, val maxs: Vector2d) {
init { init {
require(mins.x < maxs.x) { "mins.x ${mins.x} is more or equal to maxs.x ${maxs.x}" } 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 or equal to maxs.y ${maxs.y}" } 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) 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<AABB>() {
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) { data class AABBi(val mins: Vector2i, val maxs: Vector2i) {
init { init {
require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" } 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<Vector2i> get() = Iterator(::Vector2i) val vectors: kotlin.collections.Iterator<Vector2i> get() = Iterator(::Vector2i)
val chunkPositions: kotlin.collections.Iterator<ChunkPos> get() = Iterator(::ChunkPos) val chunkPositions: kotlin.collections.Iterator<ChunkPos> get() = Iterator(::ChunkPos)
} }
object AABBiTypeAdapter : TypeAdapter<AABBi>() {
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),
)
}
}

View File

@ -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<Vector2d> = ImmutableList.copyOf(points)
override fun toString(): String {
return "Poly($points)"
}
}
object PolyTypeAdapter : TypeAdapter<Poly>() {
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<Vector2d>()
while (`in`.peek() == JsonToken.BEGIN_ARRAY) {
points.add(Vector2dTypeAdapter.read(`in`))
}
`in`.endArray()
return Poly(*points.toTypedArray())
}
}

View File

@ -1,6 +1,9 @@
package ru.dbotthepony.kstarbound.math package ru.dbotthepony.kstarbound.math
import com.google.gson.JsonArray 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 ru.dbotthepony.kstarbound.api.*
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.pow 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 RIGHT = Vector2i().right()
val UP = Vector2i().up() val UP = Vector2i().up()
val DOWN = Vector2i().down() val DOWN = Vector2i().down()
val ONE_ONE = Vector2i(1, 1)
}
}
object Vector2iTypeAdapter : TypeAdapter<Vector2i>() {
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<Vector2f>() {
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<MutableVector2f>() { data class MutableVector2f(override var x: Float = 0f, override var y: Float = 0f) : IVector2f<MutableVector2f>() {
override fun make(x: Float, y: Float): MutableVector2f { override fun make(x: Float, y: Float): MutableVector2f {
this.x = x 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<Vector2d>() {
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<MutableVector2d>() { data class MutableVector2d(override var x: Double = 0.0, override var y: Double = 0.0) : IVector2d<MutableVector2d>() {
override fun make(x: Double, y: Double): MutableVector2d { override fun make(x: Double, y: Double): MutableVector2d {
this.x = x this.x = x

View File

@ -1,12 +1,22 @@
package ru.dbotthepony.kstarbound.util package ru.dbotthepony.kstarbound.util
import com.google.common.collect.ImmutableList 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 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 { 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: 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 { companion object {
val WHITE = Color(1f, 1f, 1f) 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 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<Color>().let { val SHADES_OF_GRAY = ArrayList<Color>().let {
for (i in 0 .. 256) { for (i in 0 .. 256) {
it.add(Color(i / 256f, i / 256f, i / 256f)) 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<Color>() {
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<String, Double>()
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")
}
}
}

View File

@ -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<T : Enum<T>>(private val clazz: Array<T>) : TypeAdapter<T>() {
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")
}
}

View File

@ -492,6 +492,10 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
onEntityRemoved(entity) onEntityRemoved(entity)
} }
override fun toString(): String {
return "Chunk(pos=$pos, entityCount=${entities.size}, world=$world)"
}
companion object { companion object {
val EMPTY = object : IMutableTileChunk { val EMPTY = object : IMutableTileChunk {
override val pos = ChunkPos(0, 0) override val pos = ChunkPos(0, 0)

View File

@ -5,6 +5,7 @@ import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.util.Color import ru.dbotthepony.kstarbound.util.Color
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import kotlin.math.absoluteValue
enum class Move { enum class Move {
STAND_STILL, STAND_STILL,
@ -12,43 +13,34 @@ enum class Move {
MOVE_RIGHT MOVE_RIGHT
} }
open class AliveEntity(world: World<*, *>) : Entity(world) { interface IWalkableEntity : IEntity {
open var maxHealth = 10.0 /**
open var health = 10.0 * AABB сущности, которая стоит
open val moveDirection = Move.STAND_STILL */
override val collisionResolution = CollisionResolution.SLIDE val standingAABB: AABB
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
/** /**
* Максимальная скорость передвижения этого существа в Starbound Units/секунда * AABB сущности, которая присела
*/
val duckingAABB: AABB
/**
* Максимальная скорость передвижения этого AliveMovementController в Starbound Units/секунда
* *
* Скорость передвижения: Это скорость вдоль земли (или в воздухе, если парит) при ходьбе. * Скорость передвижения: Это скорость вдоль земли (или в воздухе, если парит) при ходьбе.
* *
* Если вектор скорости вдоль поверхности (или в воздухе, если парит) больше заданного значения, * Если вектор скорости вдоль поверхности (или в воздухе, если парит) больше заданного значения,
* то сущность быстро тормозит (учитывая силу трения) * то сущность быстро тормозит (учитывая силу трения)
*/ */
open val topSpeed = 20.0 val topSpeed: Double
/** /**
* Скорость ускорения сущности в Starbound Units/секунда^2 * Скорость ускорения сущности в Starbound Units/секунда^2
* *
* Если сущность хочет двигаться вправо или влево (а также вверх или вниз, если парит), * Если сущность хочет двигаться вправо или влево,
* то она разгоняется с данной скоростью. * то она разгоняется с данной скоростью.
*/ */
open val moveSpeed = 64.0 val moveSpeed: Double
/** /**
* То, как сущность может влиять на свою скорость в Starbound Units/секунда^2 * То, как сущность может влиять на свою скорость в Starbound Units/секунда^2
@ -56,33 +48,83 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
* *
* Позволяет в т.ч. игрокам изменять свою траекторию полёта в стиле Quake. * Позволяет в т.ч. игрокам изменять свою траекторию полёта в стиле Quake.
*/ */
open val freeFallMoveSpeed = 8.0 val freeFallMoveSpeed: Double
/** /**
* "Сила", с которой сущность останавливается, если не хочет двигаться. * "Сила", с которой сущность останавливается, если не хочет двигаться.
* *
* Зависит от текущего трения, так как технически является множителем трения поверхности, * Зависит от текущего трения, так как технически является множителем трения поверхности,
* на которой стоит сущность. Если сущность парит, то сила трения является константой и не зависит от её окружения. * на которой стоит сущность.
*/ */
open val brakeForce = 32.0 val brakeForce: Double
/**
* Импульс прыжка данной сущности. Если сущность парит, то данное значение не несёт никакой
* полезной нагрузки.
*/
open val jumpForce = 20.0
/** /**
* Высота шага данной сущности. Данное значение отвечает за то, на сколько блоков * Высота шага данной сущности. Данное значение отвечает за то, на сколько блоков
* сможет подняться сущность просто двигаясь в одном направлении без необходимости прыгнуть. * сможет подняться сущность просто двигаясь в одном направлении без необходимости прыгнуть.
*/ */
open val stepSize = 1.1 val jumpForce: Double
/**
* Импульс прыжка данной сущности. Если сущность парит, то данное значение не несёт никакой
* полезной нагрузки.
*/
val stepSize: Double
}
/**
* Базовый абстрактный класс, реализующий сущность, которая ходит по земле
*/
abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : MovementController<T>(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) { override fun thinkPhysics(delta: Double) {
super.thinkPhysics(delta) super.thinkPhysics(delta)
thinkMovement(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 jumpRequested = false
protected var nextJump = 0.0 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) { if (onGround || !affectedByGravity) {
var add: Vector2d var add = Vector2d.ZERO
if (isDucked) { if (isDucked) {
thinkFriction(delta * brakeForce) thinkFriction(delta * brakeForce)
add = Vector2d.ZERO } else if (velocity.y.absoluteValue < 1 && !jumpRequested) {
} else {
when (moveDirection) { when (moveDirection) {
Move.STAND_STILL -> { Move.STAND_STILL -> {
thinkFriction(delta * brakeForce) thinkFriction(delta * brakeForce)
@ -147,7 +188,7 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
if (world is ClientWorld && world.client.settings.debugCollisions) { if (world is ClientWorld && world.client.settings.debugCollisions) {
world.client.onPostDrawWorldOnce { 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 { } else {
@ -165,14 +206,14 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
if (world is ClientWorld && world.client.settings.debugCollisions) { if (world is ClientWorld && world.client.settings.debugCollisions) {
world.client.onPostDrawWorldOnce { 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) { if (world is ClientWorld && world.client.settings.debugCollisions) {
world.client.onPostDrawWorldOnce { 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) { if (wantsToDuck && onGround) {
isDucked = true isDucked = true
} else if (isDucked) { } else if (isDucked) {
if (world.isSpaceEmptyFromTiles(aabb + pos)) { if (world.isSpaceEmptyFromTiles(entity.standingAABB + pos)) {
isDucked = false 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
}

View File

@ -1,7 +1,6 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.client.render.EntityRenderer
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.math.lerp 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.ChunkPos
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
enum class CollisionResolution { /**
STOP, * Интерфейс служит лишь для убирания жёсткой зависимости от класса Entity
BOUNCE, */
PUSH, interface IEntity {
SLIDE, 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<*, *>) { abstract class Entity(override val world: World<*, *>) : IEntity {
var chunk: Chunk<*, *>? = null override var chunk: Chunk<*, *>? = null
set(value) { set(value) {
if (!isSpawned) { if (!isSpawned) {
throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world") 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 override var pos = Vector2d()
open val worldaabb: AABB get() = currentaabb + pos
var pos = Vector2d()
set(value) { set(value) {
if (field == value) if (field == value)
return return
@ -71,13 +78,14 @@ open class Entity(val world: World<*, *>) {
} }
} }
var velocity = Vector2d() override var rotation: Double = 0.0
var isSpawned = false
final override var isSpawned = false
private set private set
var isRemoved = false final override var isRemoved = false
private set private set
fun spawn() { override fun spawn() {
if (isSpawned) if (isSpawned)
throw IllegalStateException("Already spawned") throw IllegalStateException("Already spawned")
@ -90,7 +98,7 @@ open class Entity(val world: World<*, *>) {
} }
} }
fun remove() { override fun remove() {
if (isRemoved) if (isRemoved)
throw IllegalStateException("Already removed") throw IllegalStateException("Already removed")
@ -103,107 +111,24 @@ open class Entity(val world: World<*, *>) {
} }
/** /**
* Касается ли сущность земли * Контроллер перемещения данной сущности
*
* Данный флаг выставляется при обработке скорости, если данный флаг не будет выставлен
* правильно, то сущность будет иметь очень плохое движение в стороны
*
* Так же от него зависит то, может ли сущность двигаться, если она не парит
*
* Если сущность касается земли, то на неё не действует гравитация
*/ */
var onGround = false abstract override val movement: MovementController<*>
protected set(value) { protected abstract fun thinkAI(delta: Double)
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()
}
/** /**
* Заставляет сущность "думать". * Заставляет сущность "думать".
*/ */
fun think(delta: Double) { final override fun think(delta: Double) {
if (!isSpawned) { if (!isSpawned) {
throw IllegalStateException("Tried to think before spawning in world") throw IllegalStateException("Tried to think before spawning in world")
} }
thinkPhysics(delta) movement.thinkPhysics(delta)
thinkAI(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)
}
} }

View File

@ -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<T : IEntity>(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>(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)
}
}

View File

@ -4,11 +4,19 @@ import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.Vector2d import ru.dbotthepony.kstarbound.math.Vector2d
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController<PlayerEntity>(entity) {
public override var moveDirection = Move.STAND_STILL
}
/** /**
* Физический аватар игрока в мире * Физический аватар игрока в мире
*/ */
open class PlayerEntity(world: World<*, *>) : AliveEntity(world) { open class PlayerEntity(world: World<*, *>) : AliveWalkingEntity(world) {
override val aabb = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7) override val standingAABB = 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 val duckingAABB = AABB.rectangle(Vector2d.ZERO, 1.8, 1.8) + Vector2d(y = -0.9)
override var moveDirection = Move.STAND_STILL override val movement = PlayerMovementController(this)
override fun thinkAI(delta: Double) {
}
} }

View File

@ -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) {
}
}