Multithreaded game loading

This commit is contained in:
DBotThePony 2023-09-25 18:59:43 +07:00
parent 5901d756ee
commit 97d441deba
Signed by: DBot
GPG Key ID: DCC23B5715498507
10 changed files with 275 additions and 134 deletions

View File

@ -0,0 +1,71 @@
package ru.dbotthepony.kstarbound
import it.unimi.dsi.fastutil.ints.IntIterators
import it.unimi.dsi.fastutil.objects.ObjectIterators
interface ILoadingLog : Iterable<String> {
interface ILine {
var text: String
}
fun line(text: String): ILine
companion object : ILoadingLog, ILine {
override var text: String
get() = ""
set(value) {}
override fun line(text: String): ILine {
return this
}
override fun iterator(): Iterator<String> {
return ObjectIterators.emptyIterator()
}
}
}
class LoadingLog : ILoadingLog {
private val lines = arrayOfNulls<Line>(128)
private var index = 0
private val lock = Any()
private var size = 0
var lastActivity: Long = System.nanoTime()
private set
override fun line(text: String): ILoadingLog.ILine {
return Line(text)
}
inner class Line(text: String) : ILoadingLog.ILine {
override var text: String = text
set(value) {
field = value
lastActivity = System.nanoTime()
}
init {
lastActivity = System.nanoTime()
synchronized(lock) {
lines[index++ and 127] = this
size = (size + 1).coerceAtMost(127)
}
}
}
override fun iterator(): Iterator<String> {
return object : Iterator<String> {
private val index = (this@LoadingLog.index - 1) and 127
private val parent = IntIterators.fromTo(0, size)
override fun hasNext(): Boolean {
return parent.hasNext()
}
override fun next(): String {
return lines[(index - parent.nextInt()) and 127]!!.text
}
}
}
}

View File

@ -52,9 +52,7 @@ fun main() {
//Starbound.addPakPath(File("packed.pak")) //Starbound.addPakPath(File("packed.pak"))
Starbound.initializeGame { finished, replaceStatus, status -> Starbound.initializeGame(client.loadingLog)
client.putDebugLog(status, replaceStatus)
}
client.onTermination { client.onTermination {
Starbound.terminateLoading = true Starbound.terminateLoading = true
@ -159,7 +157,8 @@ fun main() {
client.font.render("${ent.position}", y = 100f, scale = 0.25f) client.font.render("${ent.position}", y = 100f, scale = 0.25f)
client.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f) client.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f)
client.font.render("Camera: ${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f) client.font.render("Camera: ${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f)
client.font.render("World chunk: ${client.world!!.chunkFromCell(client.camera.pos)}", y = 160f, scale = 0.25f) client.font.render("Cursor: ${client.mouseCoordinates} -> ${client.screenToWorld(client.mouseCoordinates)}", y = 160f, scale = 0.25f)
client.font.render("World chunk: ${client.world!!.chunkFromCell(client.camera.pos)}", y = 180f, scale = 0.25f)
} }
client.onPreDrawWorld { client.onPreDrawWorld {

View File

@ -7,14 +7,15 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import ru.dbotthepony.kstarbound.api.IStarboundFile import ru.dbotthepony.kstarbound.api.IStarboundFile
import ru.dbotthepony.kstarbound.defs.player.RecipeDefinition import ru.dbotthepony.kstarbound.defs.player.RecipeDefinition
import java.util.Collections import java.util.Collections
import java.util.LinkedList
class RecipeRegistry { class RecipeRegistry {
private val recipesInternal = ArrayList<RegistryObject<RecipeDefinition>>() private val recipesInternal = ArrayList<RegistryObject<RecipeDefinition>>()
private val group2recipesInternal = Object2ObjectOpenHashMap<String, ArrayList<RegistryObject<RecipeDefinition>>>() private val group2recipesInternal = Object2ObjectOpenHashMap<String, LinkedList<RegistryObject<RecipeDefinition>>>()
private val group2recipesBacking = Object2ObjectOpenHashMap<String, List<RegistryObject<RecipeDefinition>>>() private val group2recipesBacking = Object2ObjectOpenHashMap<String, List<RegistryObject<RecipeDefinition>>>()
private val output2recipesInternal = Object2ObjectOpenHashMap<String, ArrayList<RegistryObject<RecipeDefinition>>>() private val output2recipesInternal = Object2ObjectOpenHashMap<String, LinkedList<RegistryObject<RecipeDefinition>>>()
private val output2recipesBacking = Object2ObjectOpenHashMap<String, List<RegistryObject<RecipeDefinition>>>() private val output2recipesBacking = Object2ObjectOpenHashMap<String, List<RegistryObject<RecipeDefinition>>>()
private val input2recipesInternal = Object2ObjectOpenHashMap<String, ArrayList<RegistryObject<RecipeDefinition>>>() private val input2recipesInternal = Object2ObjectOpenHashMap<String, LinkedList<RegistryObject<RecipeDefinition>>>()
private val input2recipesBacking = Object2ObjectOpenHashMap<String, List<RegistryObject<RecipeDefinition>>>() private val input2recipesBacking = Object2ObjectOpenHashMap<String, List<RegistryObject<RecipeDefinition>>>()
val recipes: List<RegistryObject<RecipeDefinition>> = Collections.unmodifiableList(recipesInternal) val recipes: List<RegistryObject<RecipeDefinition>> = Collections.unmodifiableList(recipesInternal)
@ -28,21 +29,21 @@ class RecipeRegistry {
for (group in value.groups) { for (group in value.groups) {
group2recipesInternal.computeIfAbsent(group, Object2ObjectFunction { p -> group2recipesInternal.computeIfAbsent(group, Object2ObjectFunction { p ->
ArrayList<RegistryObject<RecipeDefinition>>().also { LinkedList<RegistryObject<RecipeDefinition>>().also {
group2recipesBacking[p as String] = Collections.unmodifiableList(it) group2recipesBacking[p as String] = Collections.unmodifiableList(it)
} }
}).add(recipe) }).add(recipe)
} }
output2recipesInternal.computeIfAbsent(value.output.item.name, Object2ObjectFunction { p -> output2recipesInternal.computeIfAbsent(value.output.item.name, Object2ObjectFunction { p ->
ArrayList<RegistryObject<RecipeDefinition>>().also { LinkedList<RegistryObject<RecipeDefinition>>().also {
output2recipesBacking[p as String] = Collections.unmodifiableList(it) output2recipesBacking[p as String] = Collections.unmodifiableList(it)
} }
}).add(recipe) }).add(recipe)
for (input in value.input) { for (input in value.input) {
input2recipesInternal.computeIfAbsent(input.item.name, Object2ObjectFunction { p -> input2recipesInternal.computeIfAbsent(input.item.name, Object2ObjectFunction { p ->
ArrayList<RegistryObject<RecipeDefinition>>().also { LinkedList<RegistryObject<RecipeDefinition>>().also {
input2recipesBacking[p as String] = Collections.unmodifiableList(it) input2recipesBacking[p as String] = Collections.unmodifiableList(it)
} }
}).add(recipe) }).add(recipe)

View File

@ -85,6 +85,8 @@ import ru.dbotthepony.kstarbound.util.set
import ru.dbotthepony.kstarbound.util.traverseJsonPath import ru.dbotthepony.kstarbound.util.traverseJsonPath
import java.io.* import java.io.*
import java.text.DateFormat import java.text.DateFormat
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
import java.util.function.BiConsumer import java.util.function.BiConsumer
import java.util.function.BinaryOperator import java.util.function.BinaryOperator
import java.util.function.Function import java.util.function.Function
@ -901,7 +903,7 @@ object Starbound : ISBFileLocator {
private set private set
private fun loadStage( private fun loadStage(
callback: (Boolean, Boolean, String) -> Unit, log: ILoadingLog,
loader: ((String) -> Unit) -> Unit, loader: ((String) -> Unit) -> Unit,
name: String, name: String,
) { ) {
@ -909,27 +911,25 @@ object Starbound : ISBFileLocator {
return return
val time = System.currentTimeMillis() val time = System.currentTimeMillis()
callback(false, false, "Loading $name...") val line = log.line("Loading $name...".also(logger::info))
logger.info("Loading $name...")
loader { loader {
if (terminateLoading) { if (terminateLoading) {
throw InterruptedException("Game is terminating") throw InterruptedException("Game is terminating")
} }
callback(false, true, it) line.text = it
} }
callback(false, true, "Loaded $name in ${System.currentTimeMillis() - time}ms") line.text = ("Loaded $name in ${System.currentTimeMillis() - time}ms".also(logger::info))
logger.info("Loaded $name in ${System.currentTimeMillis() - time}ms")
} }
private fun <T : Any> loadStage( private fun <T : Any> loadStage(
callback: (Boolean, Boolean, String) -> Unit, log: ILoadingLog,
registry: ObjectRegistry<T>, registry: ObjectRegistry<T>,
files: List<IStarboundFile>, files: List<IStarboundFile>,
) { ) {
loadStage(callback, loader = { loadStage(log, loader = {
for (listedFile in files) { for (listedFile in files) {
try { try {
it("Loading $listedFile") it("Loading $listedFile")
@ -945,24 +945,24 @@ object Starbound : ISBFileLocator {
}, registry.name) }, registry.name)
} }
private fun doInitialize(callback: (finished: Boolean, replaceStatus: Boolean, status: String) -> Unit) { private fun doInitialize(log: ILoadingLog) {
var time = System.currentTimeMillis() var time = System.currentTimeMillis()
if (archivePaths.isNotEmpty()) { if (archivePaths.isNotEmpty()) {
callback(false, false, "Searching for pak archives...".also(logger::info)) log.line("Searching for pak archives...".also(logger::info))
for (path in archivePaths) { for (path in archivePaths) {
callback(false, false, "Reading index of ${path}...".also(logger::info)) val line = log.line("Reading index of ${path}...".also(logger::info))
addPak(StarboundPak(path) { _, status -> addPak(StarboundPak(path) { _, status ->
callback(false, true, "${path.parent}/${path.name}: $status") line.text = ("${path.parent}/${path.name}: $status")
}) })
} }
} }
callback(false, false, "Finished reading pak archives in ${System.currentTimeMillis() - time}ms".also(logger::info)) log.line("Finished reading pak archives in ${System.currentTimeMillis() - time}ms".also(logger::info))
time = System.currentTimeMillis() time = System.currentTimeMillis()
callback(false, false, "Building file index...".also(logger::info)) log.line("Building file index...".also(logger::info))
val ext2files = fileSystems.parallelStream() val ext2files = fileSystems.parallelStream()
.flatMap { it.explore() } .flatMap { it.explore() }
@ -998,39 +998,45 @@ object Starbound : ISBFileLocator {
} }
}) })
callback(false, false, "Finished building file index in ${System.currentTimeMillis() - time}ms".also(logger::info)) log.line("Finished building file index in ${System.currentTimeMillis() - time}ms".also(logger::info))
loadStage(callback, { loadItemDefinitions(it, ext2files) }, "item definitions") val pool = ForkJoinPool.commonPool()
loadStage(callback, { loadJsonFunctions(it, ext2files["functions"] ?: listOf()) }, "json functions") val tasks = ArrayList<ForkJoinTask<*>>()
loadStage(callback, { loadJson2Functions(it, ext2files["2functions"] ?: listOf()) }, "json 2functions")
loadStage(callback, { loadRecipes(it, ext2files["recipe"] ?: listOf()) }, "recipes")
loadStage(callback, { loadTreasurePools(it, ext2files["treasurepools"] ?: listOf()) }, "treasure pools")
loadStage(callback, _tiles, ext2files["material"] ?: listOf()) tasks.add(pool.submit { loadStage(log, { loadItemDefinitions(it, ext2files) }, "item definitions") })
loadStage(callback, _tileModifiers, ext2files["matmod"] ?: listOf())
loadStage(callback, _liquid, ext2files["liquid"] ?: listOf()) tasks.add(pool.submit { loadStage(log, { loadJsonFunctions(it, ext2files["functions"] ?: listOf()) }, "json functions") })
loadStage(callback, _worldObjects, ext2files["object"] ?: listOf()) tasks.add(pool.submit { loadStage(log, { loadJson2Functions(it, ext2files["2functions"] ?: listOf()) }, "json 2functions") })
loadStage(callback, _statusEffects, ext2files["statuseffect"] ?: listOf()) tasks.add(pool.submit { loadStage(log, { loadRecipes(it, ext2files["recipe"] ?: listOf()) }, "recipes") })
loadStage(callback, _species, ext2files["species"] ?: listOf()) tasks.add(pool.submit { loadStage(log, { loadTreasurePools(it, ext2files["treasurepools"] ?: listOf()) }, "treasure pools") })
loadStage(callback, _particles, ext2files["particle"] ?: listOf())
loadStage(callback, _questTemplates, ext2files["questtemplate"] ?: listOf()) tasks.add(pool.submit { loadStage(log, _tiles, ext2files["material"] ?: listOf()) })
loadStage(callback, _techs, ext2files["tech"] ?: listOf()) tasks.add(pool.submit { loadStage(log, _tileModifiers, ext2files["matmod"] ?: listOf()) })
loadStage(callback, _npcTypes, ext2files["npctype"] ?: listOf()) tasks.add(pool.submit { loadStage(log, _liquid, ext2files["liquid"] ?: listOf()) })
//loadStage(callback, _projectiles, ext2files["projectile"] ?: listOf()) tasks.add(pool.submit { loadStage(log, _worldObjects, ext2files["object"] ?: listOf()) })
//loadStage(callback, _tenants, ext2files["tenant"] ?: listOf()) tasks.add(pool.submit { loadStage(log, _statusEffects, ext2files["statuseffect"] ?: listOf()) })
loadStage(callback, _monsterSkills, ext2files["monsterskill"] ?: listOf()) tasks.add(pool.submit { loadStage(log, _species, ext2files["species"] ?: listOf()) })
//loadStage(callback, _monsterTypes, ext2files["monstertype"] ?: listOf()) tasks.add(pool.submit { loadStage(log, _particles, ext2files["particle"] ?: listOf()) })
tasks.add(pool.submit { loadStage(log, _questTemplates, ext2files["questtemplate"] ?: listOf()) })
tasks.add(pool.submit { loadStage(log, _techs, ext2files["tech"] ?: listOf()) })
tasks.add(pool.submit { loadStage(log, _npcTypes, ext2files["npctype"] ?: listOf()) })
// tasks.add(pool.submit { loadStage(log, _projectiles, ext2files["projectile"] ?: listOf()) })
// tasks.add(pool.submit { loadStage(log, _tenants, ext2files["tenant"] ?: listOf()) })
tasks.add(pool.submit { loadStage(log, _monsterSkills, ext2files["monsterskill"] ?: listOf()) })
// tasks.add(pool.submit { loadStage(log, _monsterTypes, ext2files["monstertype"] ?: listOf()) })
AssetPathStack.block("/") { AssetPathStack.block("/") {
//playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java) //playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java)
} }
tasks.forEach { it.join() }
initializing = false initializing = false
initialized = true initialized = true
callback(true, false, "Finished loading in ${System.currentTimeMillis() - time}ms") log.line("Finished loading in ${System.currentTimeMillis() - time}ms")
} }
fun initializeGame(callback: (finished: Boolean, replaceStatus: Boolean, status: String) -> Unit) { fun initializeGame(log: ILoadingLog) {
if (initializing) { if (initializing) {
throw IllegalStateException("Already initializing!") throw IllegalStateException("Already initializing!")
} }
@ -1040,7 +1046,7 @@ object Starbound : ISBFileLocator {
} }
initializing = true initializing = true
Thread({ doInitialize(callback) }, "Asset Loader").also { Thread({ doInitialize(log) }, "Asset Loader").also {
it.isDaemon = true it.isDaemon = true
it.start() it.start()
} }

View File

@ -12,6 +12,7 @@ import org.lwjgl.opengl.GL45.*
import org.lwjgl.opengl.GLCapabilities import org.lwjgl.opengl.GLCapabilities
import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil import org.lwjgl.system.MemoryUtil
import ru.dbotthepony.kstarbound.LoadingLog
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -109,7 +110,7 @@ class StarboundClient : Closeable {
var viewportTopRight = Vector2d() var viewportTopRight = Vector2d()
private set private set
var fullbright = true var fullbright = false
var clientTerminated = false var clientTerminated = false
private set private set
@ -133,8 +134,7 @@ class StarboundClient : Closeable {
private val onPostDrawWorldOnce = ArrayList<(LayeredRenderer) -> Unit>() private val onPostDrawWorldOnce = ArrayList<(LayeredRenderer) -> Unit>()
private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>() private val onViewportChanged = ArrayList<(width: Int, height: Int) -> Unit>()
private val terminateCallbacks = ArrayList<() -> Unit>() private val terminateCallbacks = ArrayList<() -> Unit>()
private val startupTextList = ArrayList<String>() val loadingLog = LoadingLog()
private var finishStartupRendering = System.currentTimeMillis() + 4000L
private val cleaner = Cleaner.create { r -> private val cleaner = Cleaner.create { r ->
val thread = Thread(r, "OpenGL Cleaner for '${thread.name}'") val thread = Thread(r, "OpenGL Cleaner for '${thread.name}'")
@ -180,7 +180,7 @@ class StarboundClient : Closeable {
window = GLFW.glfwCreateWindow(800, 600, "KStarbound", MemoryUtil.NULL, MemoryUtil.NULL) window = GLFW.glfwCreateWindow(800, 600, "KStarbound", MemoryUtil.NULL, MemoryUtil.NULL)
require(window != MemoryUtil.NULL) { "Unable to create GLFW window" } require(window != MemoryUtil.NULL) { "Unable to create GLFW window" }
startupTextList.add("Created GLFW window") loadingLog.line("Created GLFW window")
input.installCallback(window) input.installCallback(window)
@ -235,7 +235,7 @@ class StarboundClient : Closeable {
GLFW.glfwSwapInterval(0) GLFW.glfwSwapInterval(0)
GLFW.glfwShowWindow(window) GLFW.glfwShowWindow(window)
putDebugLog("Initialized GLFW window") loadingLog.line("Initialized GLFW window")
} }
val maxTextureBlocks = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS) val maxTextureBlocks = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS)
@ -521,6 +521,20 @@ class StarboundClient : Closeable {
builder.draw(GL_LINES) builder.draw(GL_LINES)
} }
inline fun lines(color: RGBAColor = RGBAColor.WHITE, lambda: (VertexBuilder) -> Unit) {
val builder = programs.position.builder
builder.builder.begin(GeometryType.LINES)
lambda.invoke(builder.builder)
builder.upload()
programs.position.use()
programs.position.colorMultiplier = color
programs.position.modelMatrix = stack.last()
builder.draw(GL_LINES)
}
fun vertex(file: File) = GLShader(file, GL_VERTEX_SHADER) fun vertex(file: File) = GLShader(file, GL_VERTEX_SHADER)
fun fragment(file: File) = GLShader(file, GL_FRAGMENT_SHADER) fun fragment(file: File) = GLShader(file, GL_FRAGMENT_SHADER)
@ -531,20 +545,6 @@ class StarboundClient : Closeable {
fun internalFragment(file: String) = GLShader(readInternal(file), GL_FRAGMENT_SHADER) fun internalFragment(file: String) = GLShader(readInternal(file), GL_FRAGMENT_SHADER)
fun internalGeometry(file: String) = GLShader(readInternal(file), GL_GEOMETRY_SHADER) fun internalGeometry(file: String) = GLShader(readInternal(file), GL_GEOMETRY_SHADER)
fun putDebugLog(text: String, replace: Boolean = false) {
if (replace) {
if (startupTextList.isEmpty()) {
startupTextList.add(text)
} else {
startupTextList[startupTextList.size - 1] = text
}
} else {
startupTextList.add(text)
}
finishStartupRendering = System.currentTimeMillis() + 4000L
}
private fun isMe(state: StarboundClient?) { private fun isMe(state: StarboundClient?) {
if (state != null && state != this) { if (state != null && state != this) {
throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)") throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)")
@ -569,7 +569,7 @@ class StarboundClient : Closeable {
xMousePos.position(0) xMousePos.position(0)
yMousePos.position(0) yMousePos.position(0)
return Vector2d(xMousePos.get(), yMousePos.get()) return Vector2d(xMousePos.get(), viewportHeight - yMousePos.get())
} }
val mouseCoordinatesF: Vector2f get() { val mouseCoordinatesF: Vector2f get() {
@ -579,7 +579,7 @@ class StarboundClient : Closeable {
xMousePos.position(0) xMousePos.position(0)
yMousePos.position(0) yMousePos.position(0)
return Vector2f(xMousePos.get().toFloat(), yMousePos.get().toFloat()) return Vector2f(xMousePos.get().toFloat(), viewportHeight - yMousePos.get().toFloat())
} }
fun screenToWorld(x: Double, y: Double): Vector2d { fun screenToWorld(x: Double, y: Double): Vector2d {
@ -600,7 +600,6 @@ class StarboundClient : Closeable {
var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true) var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true)
init { init {
putDebugLog("Initialized OpenGL context")
clearColor = RGBAColor.SLATE_GRAY clearColor = RGBAColor.SLATE_GRAY
blend = true blend = true
@ -798,6 +797,10 @@ class StarboundClient : Closeable {
size = viewportRectangle) size = viewportRectangle)
if (viewportLightingMem != null && !fullbright) { if (viewportLightingMem != null && !fullbright) {
val spos = screenToWorld(mouseCoordinates)
viewportLighting.addPointLight(roundTowardsPositiveInfinity(spos.x - viewportCellX), roundTowardsPositiveInfinity(spos.y - viewportCellY), 1f, 1f, 1f)
viewportLightingMem.position(0) viewportLightingMem.position(0)
BufferUtils.zeroBuffer(viewportLightingMem) BufferUtils.zeroBuffer(viewportLightingMem)
viewportLightingMem.position(0) viewportLightingMem.position(0)
@ -844,21 +847,19 @@ class StarboundClient : Closeable {
uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen } uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen }
fontShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen } fontShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen }
val thisTime = System.currentTimeMillis() if (System.nanoTime() - loadingLog.lastActivity <= 4_000_000_000L) {
if (startupTextList.isNotEmpty() && thisTime <= finishStartupRendering) {
var alpha = 1f var alpha = 1f
if (finishStartupRendering - thisTime < 1000L) { if (System.nanoTime() - loadingLog.lastActivity >= 3_000_000_000L) {
alpha = (finishStartupRendering - thisTime) / 1000f alpha = 1f - (System.nanoTime() - loadingLog.lastActivity - 3_000_000_000L) / 1_000_000_000f
} }
stack.push() stack.push()
stack.last().translate(y = viewportHeight.toFloat()) stack.last().translate(y = viewportHeight.toFloat())
var shade = 255 var shade = 255
for (i in startupTextList.size - 1 downTo 0) { for (line in loadingLog) {
val size = font.render(startupTextList[i], alignY = TextAlignY.BOTTOM, scale = 0.4f, color = RGBAColor(shade / 255f, shade / 255f, shade / 255f, alpha)) val size = font.render(line, alignY = TextAlignY.BOTTOM, scale = 0.4f, color = RGBAColor(shade / 255f, shade / 255f, shade / 255f, alpha))
stack.last().translate(y = -size.height * 1.2f) stack.last().translate(y = -size.height * 1.2f)
if (shade > 120) { if (shade > 120) {

View File

@ -2,6 +2,8 @@ package ru.dbotthepony.kstarbound.client.render
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.world.api.ITileState
import ru.dbotthepony.kstarbound.world.api.TileColor
enum class RenderLayer { enum class RenderLayer {
BackgroundOverlay, BackgroundOverlay,
@ -29,33 +31,49 @@ enum class RenderLayer {
FrontParticle, FrontParticle,
Overlay; Overlay;
val base = Point(this) private val base = Point(this)
fun point(offset: Long = 0L): Point { fun point(offset: Long = 0L, index: Long = 0L, hueShift: Float = 0f, colorVariant: TileColor = TileColor.DEFAULT): Point {
return if (offset == 0L) return if (offset == 0L && index == 0L && hueShift == 0f && colorVariant === TileColor.DEFAULT)
base base
else else
Point(this, offset) Point(this, offset, index, hueShift, colorVariant)
} }
data class Point(val base: RenderLayer, val offset: Long = 0L) : Comparable<Point> { fun point(): Point {
return base
}
data class Point(val base: RenderLayer, val offset: Long = 0L, val index: Long = 0L, val hueShift: Float = 0f, val colorVariant: TileColor = TileColor.DEFAULT) : Comparable<Point> {
override fun compareTo(other: Point): Int { override fun compareTo(other: Point): Int {
if (this === other) return 0
var cmp = base.compareTo(other.base) var cmp = base.compareTo(other.base)
if (cmp == 0) cmp = offset.compareTo(other.offset) if (cmp == 0) cmp = offset.compareTo(other.offset)
if (cmp == 0) cmp = index.compareTo(other.index)
if (cmp == 0) cmp = hueShift.compareTo(other.hueShift)
if (cmp == 0) cmp = colorVariant.compareTo(other.colorVariant)
return cmp return cmp
} }
} }
companion object { companion object {
fun tileLayer(isBackground: Boolean, isModifier: Boolean, offset: Long = 0L): Point { fun tileLayer(isBackground: Boolean, isModifier: Boolean, offset: Long = 0L, index: Long = 0L, hueShift: Float = 0f, colorVariant: TileColor = TileColor.DEFAULT): Point {
if (isBackground && isModifier) { if (isBackground && isModifier) {
return BackgroundTileMod.point(offset) return BackgroundTileMod.point(offset, index, hueShift, colorVariant)
} else if (isBackground) { } else if (isBackground) {
return BackgroundTile.point(offset) return BackgroundTile.point(offset, index, hueShift, colorVariant)
} else if (isModifier) { } else if (isModifier) {
return ForegroundTileMod.point(offset) return ForegroundTileMod.point(offset, index, hueShift, colorVariant)
} else { } else {
return ForegroundTile.point(offset) return ForegroundTile.point(offset, index, hueShift, colorVariant)
}
}
fun tileLayer(isBackground: Boolean, isModifier: Boolean, tile: ITileState): Point {
if (isModifier) {
return tileLayer(isBackground, true, tile.modifier?.renderParameters?.zLevel ?: 0L, tile.modifier?.modId?.toLong() ?: 0L, tile.modifierHueShift)
} else {
return tileLayer(isBackground, false, tile.material.renderParameters.zLevel, tile.material.materialId.toLong(), tile.hueShift)
} }
} }

View File

@ -1,5 +1,8 @@
package ru.dbotthepony.kstarbound.client.render package ru.dbotthepony.kstarbound.client.render
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL45.* import org.lwjgl.opengl.GL45.*
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
@ -15,6 +18,7 @@ import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kvector.arrays.Matrix3f import ru.dbotthepony.kvector.arrays.Matrix3f
import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import java.time.Duration
import kotlin.collections.HashMap import kotlin.collections.HashMap
/** /**
@ -23,22 +27,37 @@ import kotlin.collections.HashMap
* Создаётся единожды как потомок [Graphics] * Создаётся единожды как потомок [Graphics]
*/ */
class TileRenderers(val client: StarboundClient) { class TileRenderers(val client: StarboundClient) {
private val foreground = HashMap<GLTexture2D, Config>() private val foreground: Cache<GLTexture2D, Config> = Caffeine.newBuilder()
private val background = HashMap<GLTexture2D, Config>() .expireAfterAccess(Duration.ofMinutes(5))
private val matCache = HashMap<String, TileRenderer>() .scheduler(Scheduler.systemScheduler())
private val modCache = HashMap<String, TileRenderer>() .build()
private val background: Cache<GLTexture2D, Config> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler())
.build()
private val matCache: Cache<String, TileRenderer> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler())
.build()
private val modCache: Cache<String, TileRenderer> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler())
.build()
fun getMaterialRenderer(defName: String): TileRenderer { fun getMaterialRenderer(defName: String): TileRenderer {
return matCache.computeIfAbsent(defName) { return matCache.get(defName) {
val def = Starbound.tiles[defName] // TODO: Пустой рендерер val def = Starbound.tiles[defName] // TODO: Пустой рендерер
return@computeIfAbsent TileRenderer(this, def!!.value) TileRenderer(this, def!!.value)
} }
} }
fun getModifierRenderer(defName: String): TileRenderer { fun getModifierRenderer(defName: String): TileRenderer {
return modCache.computeIfAbsent(defName) { return modCache.get(defName) {
val def = Starbound.tileModifiers[defName] // TODO: Пустой рендерер val def = Starbound.tileModifiers[defName] // TODO: Пустой рендерер
return@computeIfAbsent TileRenderer(this, def!!.value) TileRenderer(this, def!!.value)
} }
} }
@ -62,11 +81,11 @@ class TileRenderers(val client: StarboundClient) {
} }
fun foreground(texture: GLTexture2D): RenderConfig<UberShader> { fun foreground(texture: GLTexture2D): RenderConfig<UberShader> {
return foreground.computeIfAbsent(texture) { Config(it, FOREGROUND_COLOR) } return foreground.get(texture) { Config(it, FOREGROUND_COLOR) }
} }
fun background(texture: GLTexture2D): RenderConfig<UberShader> { fun background(texture: GLTexture2D): RenderConfig<UberShader> {
return background.computeIfAbsent(texture) { Config(it, BACKGROUND_COLOR) } return background.get(texture) { Config(it, BACKGROUND_COLOR) }
} }
companion object { companion object {
@ -175,7 +194,7 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
tesselateAt( tesselateAt(
self, renderPiece.piece, getter, self, renderPiece.piece, getter,
meshBuilder.getBuilder(RenderLayer.tileLayer(isBackground, isModifier, def.renderParameters.zLevel), program).mode(GeometryType.QUADS), meshBuilder.getBuilder(RenderLayer.tileLayer(isBackground, isModifier, self), program).mode(GeometryType.QUADS),
pos, renderPiece.offset, isModifier) pos, renderPiece.offset, isModifier)
} else { } else {
tesselateAt(self, renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset, isModifier) tesselateAt(self, renderPiece.piece, getter, thisBuilder, pos, renderPiece.offset, isModifier)
@ -217,7 +236,7 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
val template = def.renderTemplate.value ?: return val template = def.renderTemplate.value ?: return
val vertexBuilder = meshBuilder val vertexBuilder = meshBuilder
.getBuilder(RenderLayer.tileLayer(isBackground, isModifier, def.renderParameters.zLevel), if (isBackground) bakedBackgroundProgramState!! else bakedProgramState!!) .getBuilder(RenderLayer.tileLayer(isBackground, isModifier, self), if (isBackground) bakedBackgroundProgramState!! else bakedProgramState!!)
.mode(GeometryType.QUADS) .mode(GeometryType.QUADS)
for ((_, matcher) in template.matches) { for ((_, matcher) in template.matches) {

View File

@ -16,6 +16,8 @@ import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.NonSolidRayFilter
import ru.dbotthepony.kstarbound.world.SolidRayFilter
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
@ -24,8 +26,12 @@ import ru.dbotthepony.kstarbound.world.positiveModulo
import ru.dbotthepony.kvector.api.IStruct2i import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2f
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
class ClientWorld( class ClientWorld(
val client: StarboundClient, val client: StarboundClient,
@ -282,26 +288,42 @@ class ClientWorld(
} }
} }
/*layers.add(-999999) { /*layers.add(RenderLayer.Overlay.base) {
val rayFan = ArrayList<Vector2d>() val rayFan = ArrayList<Vector2d>()
val pos = client.screenToWorld(client.mouseCoordinates)
for (i in 0 .. 359) { //for (i in 0 .. 359) {
rayFan.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI))) // rayFan.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI)))
//}
rayFan.add(Vector2d(0.5, 0.7).unitVector)
client.quadWireframe(RGBAColor(1f, 1f, 1f, 0.4f)) {
for (x in -20 .. 20) {
for (y in -20 .. 20) {
it.vertex(pos.x.toInt().toFloat() + x, pos.y.toInt().toFloat() + y)
it.vertex(pos.x.toInt().toFloat() + x + 1f, pos.y.toInt().toFloat() + y)
it.vertex(pos.x.toInt().toFloat() + x + 1f, pos.y.toInt().toFloat() + 1f + y)
it.vertex(pos.x.toInt().toFloat() + x, pos.y.toInt().toFloat() + 1f + y)
}
}
} }
for (ray in rayFan) { client.lines {
val trace = castRayNaive(pos, ray, 16.0) for (ray in rayFan) {
val trace = castRayExact(pos, ray, 16.0, NonSolidRayFilter)
client.gl.quadWireframe { it.vertex(pos.x.toFloat(), pos.y.toFloat())
for ((tpos, tile) in trace.traversedTiles) { it.vertex(pos.x.toFloat() + ray.x.toFloat() * trace.fraction.toFloat() * 16f, pos.y.toFloat() + ray.y.toFloat() * trace.fraction.toFloat() * 16f)
if (tile.foreground.material != null)
it.quad( /*for ((tpos, tile) in trace.traversedTiles) {
tpos.x.toFloat(), if (!tile.foreground.material.renderParameters.lightTransparent) {
tpos.y.toFloat(), it.vertex(tpos.x.toFloat(), tpos.y.toFloat())
tpos.x + 1f, it.vertex(tpos.x.toFloat() + 1f, tpos.y.toFloat())
tpos.y + 1f it.vertex(tpos.x.toFloat() + 1f, tpos.y.toFloat() + 1f)
) it.vertex(tpos.x.toFloat(), tpos.y.toFloat() + 1f)
} }
}*/
} }
} }
}*/ }*/

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs.image
import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonNull import com.google.gson.JsonNull
@ -274,10 +275,10 @@ class Image private constructor(
private val cleaner = Cleaner.create { Thread(it, "STB Image Cleaner") } private val cleaner = Cleaner.create { Thread(it, "STB Image Cleaner") }
private val dataCache: Cache<String, ByteBuffer> = Caffeine.newBuilder() private val dataCache: Cache<String, ByteBuffer> = Caffeine.newBuilder()
.softValues() .expireAfterAccess(Duration.ofMinutes(1))
.expireAfterAccess(Duration.ofMinutes(5))
.weigher<String, ByteBuffer> { key, value -> value.capacity() } .weigher<String, ByteBuffer> { key, value -> value.capacity() }
.maximumWeight(1_024L * 1_024L * 256L /* 256 МиБ */) .maximumWeight(1_024L * 1_024L * 256L /* 256 МиБ */)
.scheduler(Scheduler.systemScheduler())
.build() .build()
@JvmStatic @JvmStatic

View File

@ -97,9 +97,11 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
return -1 return -1
} }
reader.seek(innerOffset + offset) synchronized(lock) {
innerOffset++ reader.seek(innerOffset + offset)
return reader.read() innerOffset++
return reader.read()
}
} }
override fun readNBytes(len: Int): ByteArray { override fun readNBytes(len: Int): ByteArray {
@ -110,15 +112,13 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
if (readMax == 0) if (readMax == 0)
return ByteArray(0) return ByteArray(0)
val b = ByteArray(readMax) synchronized(lock) {
reader.seek(innerOffset + offset) val b = ByteArray(readMax)
val readBytes = reader.read(b) reader.seek(innerOffset + offset)
reader.readFully(b)
if (readBytes != readMax) innerOffset += readMax
throw IOError(RuntimeException("Reading $readMax bytes returned only $readBytes bytes")) return b
}
innerOffset += readBytes
return b
} }
override fun read(b: ByteArray, off: Int, len: Int): Int { override fun read(b: ByteArray, off: Int, len: Int): Int {
@ -133,14 +133,16 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
if (readMax <= 0) if (readMax <= 0)
return -1 return -1
reader.seek(innerOffset + offset) synchronized(lock) {
val readBytes = reader.read(b, off, readMax) reader.seek(innerOffset + offset)
val readBytes = reader.read(b, off, readMax)
if (readBytes == -1) if (readBytes == -1)
throw RuntimeException("Unexpected EOF, want to read $readMax bytes from starting $offset in $path") throw RuntimeException("Unexpected EOF, want to read $readMax bytes from starting $offset in $path")
innerOffset += readBytes innerOffset += readBytes
return readBytes return readBytes
}
} }
} }
} }
@ -150,7 +152,8 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
} }
} }
val reader = RandomAccessFile(path, "r") private val reader = RandomAccessFile(path, "r")
private val lock = Any()
init { init {
readHeader(reader, 0x53) // S readHeader(reader, 0x53) // S