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

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 {
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")

View File

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

View File

@ -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<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
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<String> {
val listing = mutableListOf<String>()
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)
}
}
}
}
}
}

View File

@ -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<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 {
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<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)
} ?: return listOf()
}

View File

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

View File

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

View File

@ -253,11 +253,30 @@ class GLStateTracker {
private val named2DTextures = HashMap<String, GLTexture2D>()
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)

View File

@ -45,6 +45,22 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : 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)

View File

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

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) {
private val files = HashMap<String, StarboundPakFile>()
private val directories = HashMap<String, StarboundPakDirectory>()
val files = HashMap<String, StarboundPakFile>()
val directories = HashMap<String, StarboundPakDirectory>()
fun resolve(path: Array<String>, 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<StarboundPakFile> = Collections.unmodifiableCollection(files.values)
fun listDirectories(): Collection<StarboundPakDirectory> = 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<String> {
return root.resolve(path.split("/").toTypedArray()).listDirectories().map { it.fullName() }
}
override fun pathExists(path: String): Boolean {
return indexNodes.containsKey(path)
}

View File

@ -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<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) {
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<Vector2i> get() = Iterator(::Vector2i)
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
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<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>() {
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<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>() {
override fun make(x: Double, y: Double): MutableVector2d {
this.x = x

View File

@ -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<Color>().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<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)
}
override fun toString(): String {
return "Chunk(pos=$pos, entityCount=${entities.size}, world=$world)"
}
companion object {
val EMPTY = object : IMutableTileChunk {
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.util.Color
import ru.dbotthepony.kstarbound.world.World
import kotlin.math.absoluteValue
enum class Move {
STAND_STILL,
@ -12,43 +13,34 @@ enum class Move {
MOVE_RIGHT
}
open class AliveEntity(world: World<*, *>) : 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<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) {
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
}

View File

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

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.world.World
class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController<PlayerEntity>(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) {
}
}

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