diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index eeadb111..823758fd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -4,11 +4,11 @@ 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.api.getPathFilename import ru.dbotthepony.kstarbound.api.getPathFolder import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.projectile.* import ru.dbotthepony.kstarbound.defs.world.SkyParameters -import ru.dbotthepony.kstarbound.defs.world.SkyType import ru.dbotthepony.kstarbound.defs.world.dungeon.DungeonWorldDef import ru.dbotthepony.kstarbound.io.* import ru.dbotthepony.kstarbound.math.* @@ -25,8 +25,8 @@ import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap -const val METRES_IN_STARBOUND_UNIT = 0.25 -const val METRES_IN_STARBOUND_UNITf = 0.25f +const val METRES_IN_STARBOUND_UNIT = 0.5 +const val METRES_IN_STARBOUND_UNITf = 0.5f const val PIXELS_IN_STARBOUND_UNIT = 8.0 const val PIXELS_IN_STARBOUND_UNITf = 8.0f @@ -36,12 +36,16 @@ class ProjectileDefLoadingException(message: String, cause: Throwable? = null) : object Starbound : IVFS { private val LOGGER = LogManager.getLogger() + private val tiles = HashMap() private val projectiles = HashMap() - val tilesAccess = Collections.unmodifiableMap(tiles) - val projectilesAccess = Collections.unmodifiableMap(projectiles) + private val parallax = HashMap() - val gson = GsonBuilder() + val tilesAccess: Map = Collections.unmodifiableMap(tiles) + val projectilesAccess: Map = Collections.unmodifiableMap(projectiles) + val parallaxAccess: Map = Collections.unmodifiableMap(parallax) + + val gson: Gson = GsonBuilder() .enableComplexMapKeySerialization() .serializeNulls() .setDateFormat(DateFormat.LONG) @@ -60,6 +64,7 @@ object Starbound : IVFS { .also(ConfigurableProjectile::registerGson) .also(SkyParameters::registerGson) .also(DungeonWorldDef::registerGson) + .also(Parallax::registerGson) .registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe()) @@ -99,6 +104,25 @@ object Starbound : IVFS { fun getTileDefinition(name: String) = tiles[name] private val initCallbacks = ArrayList<() -> Unit>() + private fun loadStage( + callback: (Boolean, Boolean, String) -> Unit, + loader: ((String) -> Unit) -> Unit, + name: String, + ) { + val time = System.currentTimeMillis() + callback(false, false, "Loading $name...") + + loader { + if (terminateLoading) { + throw InterruptedException("Game is terminating") + } + + callback(false, true, it) + } + + callback(false, true, "Loaded $name in ${System.currentTimeMillis() - time}ms") + } + fun initializeGame(callback: (finished: Boolean, replaceStatus: Boolean, status: String) -> Unit) { if (initializing) { throw IllegalStateException("Already initializing!") @@ -125,36 +149,9 @@ object Starbound : IVFS { } } - run { - val localTime = System.currentTimeMillis() - - callback(false, false, "Loading materials...") - - loadTileMaterials { - if (terminateLoading) { - throw InterruptedException("Game is terminating") - } - - callback(false, true, it) - } - - callback(false, true, "Loaded materials in ${System.currentTimeMillis() - localTime}ms") - } - - 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") - } + loadStage(callback, this::loadTileMaterials, "materials") + loadStage(callback, this::loadProjectiles, "projectiles") + loadStage(callback, this::loadParallax, "parallax definitions") initializing = false initialized = true @@ -257,4 +254,21 @@ object Starbound : IVFS { } } } + + private fun loadParallax(callback: (String) -> Unit) { + for (fs in fileSystems) { + for (listedFile in fs.listAllFiles("parallax")) { + if (listedFile.endsWith(".parallax")) { + try { + callback("Loading $listedFile") + + val def = gson.fromJson(getReader(listedFile), Parallax::class.java) + parallax[getPathFilename(listedFile).substringBefore('.')] = def + } catch(err: Throwable) { + LOGGER.error("Loading parallax file $listedFile", err) + } + } + } + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt index 6ac18706..5a03f851 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt @@ -92,7 +92,11 @@ interface IVFS { } fun getPathFolder(path: String): String { - return path.substring(0, path.lastIndexOf('/')) + return path.substringBeforeLast('/') +} + +fun getPathFilename(path: String): String { + return path.substringAfterLast('/') } class PhysicalFS(root: File) : IVFS { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index de813094..1a8c6141 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -1,11 +1,39 @@ package ru.dbotthepony.kstarbound.client +import org.lwjgl.opengl.GL46.* +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf +import ru.dbotthepony.kstarbound.client.gl.VertexTransformers import ru.dbotthepony.kstarbound.client.render.renderLayeredList +import ru.dbotthepony.kstarbound.defs.Parallax import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kvector.util2d.AABB +class DoubleEdgeProgression : Iterator { + var value = 0 + + override fun hasNext(): Boolean { + return true + } + + override fun next(): Int { + return nextInt() + } + + fun nextInt(): Int { + return if (value > 0) { + val ret = value + value = -value + ret + } else { + val ret = value + value = -value + 1 + ret + } + } +} + class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World(seed) { init { physics.debugDraw = client.gl.box2dRenderer @@ -18,17 +46,77 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World() for (chunk in collectInternal(size.encasingChunkPosAABB())) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index e807bfea..b999bebb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -216,9 +216,9 @@ class StarboundClient : AutoCloseable { world?.render( AABB.rectangle( - camera.pos.toDoubleVector(), - viewportWidth / settings.scale / PIXELS_IN_STARBOUND_UNIT, - viewportHeight / settings.scale / PIXELS_IN_STARBOUND_UNIT)) + camera.pos.toDoubleVector(), + viewportWidth / settings.scale / PIXELS_IN_STARBOUND_UNIT, + viewportHeight / settings.scale / PIXELS_IN_STARBOUND_UNIT)) for (lambda in onPostDrawWorld) { lambda.invoke() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt index ae0fd7bb..620b6a2e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt @@ -252,7 +252,7 @@ class GLStateTracker { private val named2DTextures = HashMap() - fun loadNamedTexture(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D { + fun loadNamedTexture(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D { return named2DTextures.computeIfAbsent(path) { if (!Starbound.pathExists(path)) { throw FileNotFoundException("Unable to locate $path") @@ -262,10 +262,20 @@ class GLStateTracker { } } + fun loadNamedTexture(path: String): GLTexture2D { + return named2DTextures.computeIfAbsent(path) { + if (!Starbound.pathExists(path)) { + throw FileNotFoundException("Unable to locate $path") + } + + return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path)).generateMips() + } + } + private var loadedEmptyTexture = false private val missingTexturePath = "/assetmissing.png" - fun loadNamedTextureSafe(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D { + fun loadNamedTextureSafe(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D { if (!loadedEmptyTexture) { loadedEmptyTexture = true named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), memoryFormat, fileFormat).generateMips() @@ -281,6 +291,22 @@ class GLStateTracker { } } + fun loadNamedTextureSafe(path: String): GLTexture2D { + if (!loadedEmptyTexture) { + loadedEmptyTexture = true + named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).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)).generateMips() + } + } + fun bind(obj: GLVertexBufferObject): GLVertexBufferObject { if (obj.type == VBOType.ARRAY) VBO = obj diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt index 0adaf522..81ace5ce 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLTexture.kt @@ -162,6 +162,40 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "") : A return this } + fun upload(path: File): GLTexture2D { + state.ensureSameThread() + + if (!path.exists()) { + throw FileNotFoundException("${path.absolutePath} does not exist") + } + + if (!path.isFile) { + throw FileNotFoundException("${path.absolutePath} is not a file") + } + + val getwidth = intArrayOf(0) + val getheight = intArrayOf(0) + val getchannels = intArrayOf(0) + + val bytes = STBImage.stbi_load(path.absolutePath, getwidth, getheight, getchannels, 0) ?: throw TextureLoadingException("Unable to load ${path.absolutePath}. Is it a valid image?") + + require(getwidth[0] > 0) { "Image ${path.absolutePath} has bad width of ${getwidth[0]}" } + require(getheight[0] > 0) { "Image ${path.absolutePath} has bad height of ${getheight[0]}" } + + val bufferFormat = when (val numChannels = getchannels[0]) { + 1 -> GL_R + 2 -> GL_RG + 3 -> GL_RGB + 4 -> GL_RGBA + else -> throw IllegalArgumentException("Weird amount of channels in file: $numChannels") + } + + upload(bufferFormat, getwidth[0], getheight[0], bufferFormat, GL_UNSIGNED_BYTE, bytes) + STBImage.stbi_image_free(bytes) + + return this + } + fun upload(buff: ByteBuffer, memoryFormat: Int, bufferFormat: Int): GLTexture2D { state.ensureSameThread() @@ -180,6 +214,32 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "") : A return this } + fun upload(buff: ByteBuffer): GLTexture2D { + state.ensureSameThread() + + val getwidth = intArrayOf(0) + val getheight = intArrayOf(0) + val getchannels = intArrayOf(0) + + val bytes = STBImage.stbi_load_from_memory(buff, getwidth, getheight, getchannels, 0) ?: throw TextureLoadingException("Unable to load ${buff}. Is it a valid image?") + + require(getwidth[0] > 0) { "Image $name has bad width of ${getwidth[0]}" } + require(getheight[0] > 0) { "Image $name has bad height of ${getheight[0]}" } + + val bufferFormat = when (val numChannels = getchannels[0]) { + 1 -> GL_R + 2 -> GL_RG + 3 -> GL_RGB + 4 -> GL_RGBA + else -> throw IllegalArgumentException("Weird amount of channels in file: $numChannels") + } + + upload(bufferFormat, getwidth[0], getheight[0], bufferFormat, GL_UNSIGNED_BYTE, bytes) + STBImage.stbi_image_free(bytes) + + return this + } + var isValid = true private set diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Parallax.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Parallax.kt new file mode 100644 index 00000000..04e9bdd6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Parallax.kt @@ -0,0 +1,103 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.gson.GsonBuilder +import com.google.gson.JsonSyntaxException +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kstarbound.io.KTypeAdapter +import ru.dbotthepony.kvector.vector.ndouble.Vector2d +import kotlin.properties.Delegates + +class Parallax { + var verticalOrigin = 0.0 + + var layers = Array(0) { ParallaxLayer() } + + companion object { + val ADAPTER = KTypeAdapter(::Parallax, + Parallax::verticalOrigin, + Parallax::layers, + ) + + fun registerGson(gsonBuilder: GsonBuilder) { + gsonBuilder.registerTypeAdapter(Parallax::class.java, ADAPTER) + gsonBuilder.registerTypeAdapter(ParallaxLayer::class.java, ParallaxLayer.ADAPTER) + gsonBuilder.registerTypeAdapter(ParallaxLayer.Parallax::class.java, ParallaxLayer.LAYER_PARALLAX_ADAPTER) + } + } +} + +class ParallaxLayer { + class Parallax(val x: Double, val y: Double) + + var timeOfDayCorrelation: String? = null + var offset = Vector2d.ZERO + var repeatY = false + var lightMapped = false + var tileLimitTop: Int? = null + var parallax by Delegates.notNull() + + var unlit = false + var nohueshift = false + var minSpeed = 0 + var maxSpeed = 0 + var fadePercent = 0.0 + + var frequency = 1.0 + + var modCount = 0 + + var noRandomOffset = false + var directives: String? = null + + var kind by Delegates.notNull() + var baseCount = 1 + + companion object { + val LAYER_PARALLAX_ADAPTER = object : TypeAdapter() { + override fun write(out: JsonWriter?, value: Parallax?) { + TODO("Not yet implemented") + } + + override fun read(reader: JsonReader): Parallax { + return when (val type = reader.peek()) { + JsonToken.BEGIN_ARRAY -> { + reader.beginArray() + val instance = Parallax(reader.nextDouble(), reader.nextDouble()) + reader.endArray() + instance + } + + JsonToken.NUMBER -> { + val num = reader.nextDouble() + Parallax(num, num) + } + + else -> throw JsonSyntaxException("Unexpected token $type") + } + } + } + + val ADAPTER = KTypeAdapter(::ParallaxLayer, + ParallaxLayer::timeOfDayCorrelation, + ParallaxLayer::offset, + ParallaxLayer::repeatY, + ParallaxLayer::lightMapped, + ParallaxLayer::tileLimitTop, + ParallaxLayer::parallax, + ParallaxLayer::unlit, + ParallaxLayer::nohueshift, + ParallaxLayer::minSpeed, + ParallaxLayer::maxSpeed, + ParallaxLayer::fadePercent, + ParallaxLayer::kind, + ParallaxLayer::baseCount, + ParallaxLayer::noRandomOffset, + ParallaxLayer::directives, + ParallaxLayer::frequency, + ParallaxLayer::modCount, + ) + } +}