Dungeons generation (need optimizing), BlockableEventLoop (finally normal event loop)

This commit is contained in:
DBotThePony 2024-04-08 12:14:42 +07:00
parent 68f3d6aa29
commit 53bb3bd843
Signed by: DBot
GPG Key ID: DCC23B5715498507
61 changed files with 4842 additions and 768 deletions

View File

@ -27,6 +27,26 @@
* Also two more properties were added: `sameStemHueShift` (defaults to `true`) and `sameFoliageHueShift` (defaults to `false`), which fixate hue shifts within same "stem-foliage" combination * Also two more properties were added: `sameStemHueShift` (defaults to `true`) and `sameFoliageHueShift` (defaults to `false`), which fixate hue shifts within same "stem-foliage" combination
* Original engine always generates two tree types when processing placeable items, new engine however, allows to generate any number of trees. * Original engine always generates two tree types when processing placeable items, new engine however, allows to generate any number of trees.
#### Dungeons
* All brushes are now deterministic, and will produce _exact_ results given same seed (this fixes dungeons being generated differently on each machine despite players visiting exactly same coordinates in universe)
* `front` and `back` brushes now can properly accept detailed data as json object on second position (e.g. `["front", { "material": ... }]`), with following structure (previously, due to oversight in code, it was impossible to specify this structure through any means, because brush definition itself can't be an object):
```kotlin
val material: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref
val modifier: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref
val hueShift: Float = 0f
val modHueShift: Float = 0f
val color: TileColor = TileColor.DEFAULT
```
* `item` brush now can accept proper item descriptors (in json object tag),
* Previous behavior remains unchanged (if specified as string, creates _randomized_ item, if as object, creates _exactly_ what have been specified)
* To stop randomizing as Tiled tileset brush, specify `"randomize"` as `false`
* `liquid` brush now can accept 'level' as second argument
* Previous behavior is unchanged, `["liquid", "water", true]` will result into infinite water as before, but `["liquid", "water", 0.5, false]` will spawn half-filled water
* In tiled, you already can do this using `"quantity"` property
* `dungeonid` brush has been hooked up to legacy dungeons and now can be directly specified inside `"brush"` (previously they were only accessible when using Tiled' tilesets).
* By default, they mark entire _part_ of dungeon with their ID. To mark specific tile inside dungeon with its own Dungeon ID, supply `true` as third value to brush (e.g `["dungeonid", 40000, true"]`)
* Tiled map behavior is unchanged, and marks their position only.
--------------- ---------------
### player.config ### player.config

View File

@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.10 kotlinVersion=1.9.10
kotlinCoroutinesVersion=1.8.0 kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.12.2 kommonsVersion=2.12.3
ffiVersion=2.2.13 ffiVersion=2.2.13
lwjglVersion=3.3.0 lwjglVersion=3.3.0

View File

@ -44,7 +44,7 @@ operator fun <T> ThreadLocal<T>.setValue(thisRef: Any, property: KProperty<*>, v
set(value) set(value)
} }
operator fun <K, V> ImmutableMap.Builder<K, V>.set(key: K, value: V): ImmutableMap.Builder<K, V> = put(key, value) operator fun <K : Any, V : Any> ImmutableMap.Builder<K, V>.set(key: K, value: V): ImmutableMap.Builder<K, V> = put(key, value)
fun String.sintern(): String = Starbound.STRINGS.intern(this) fun String.sintern(): String = Starbound.STRINGS.intern(this)

View File

@ -1,28 +1,11 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import kotlinx.coroutines.future.future
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.Version import org.lwjgl.Version
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.world.WorldGeometry
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.DataInputStream
import java.io.File import java.io.File
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
@ -32,12 +15,9 @@ fun main() {
LOGGER.info("Running LWJGL ${Version.getVersion()}") LOGGER.info("Running LWJGL ${Version.getVersion()}")
// println(VersionedJson(meta)) val client = StarboundClient()
val client = StarboundClient.create().get() Starbound.initializeGame().thenApply {
Starbound.initializeGame()
Starbound.mailboxInitialized.submit {
val server = IntegratedStarboundServer(client, File("./")) val server = IntegratedStarboundServer(client, File("./"))
server.channels.createChannel(InetSocketAddress(21060)) server.channels.createChannel(InetSocketAddress(21060))
} }

View File

@ -31,6 +31,7 @@ import ru.dbotthepony.kstarbound.defs.npc.TenantDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition
import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.projectile.ProjectileDefinition import ru.dbotthepony.kstarbound.defs.projectile.ProjectileDefinition
import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
@ -83,6 +84,7 @@ object Registries {
val treeStemVariants = Registry<TreeVariant.StemData>("tree stem variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val treeStemVariants = Registry<TreeVariant.StemData>("tree stem variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treeFoliageVariants = Registry<TreeVariant.FoliageData>("tree foliage variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val treeFoliageVariants = Registry<TreeVariant.FoliageData>("tree foliage variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val bushVariants = Registry<BushVariant.Data>("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val bushVariants = Registry<BushVariant.Data>("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val dungeons = Registry<DungeonDefinition>("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) }
private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, Int?> { private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, Int?> {
return { mapper.invoke(it) to null } return { mapper.invoke(it) to null }
@ -160,6 +162,7 @@ object Registries {
tasks.addAll(loadRegistry(treeStemVariants, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name))) tasks.addAll(loadRegistry(treeStemVariants, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name)))
tasks.addAll(loadRegistry(treeFoliageVariants, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name))) tasks.addAll(loadRegistry(treeFoliageVariants, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name)))
tasks.addAll(loadRegistry(bushVariants, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name))) tasks.addAll(loadRegistry(bushVariants, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name)))
tasks.addAll(loadRegistry(dungeons, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::name)))
tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf())) tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf()))
tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf())) tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf()))

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import com.github.benmanes.caffeine.cache.Scheduler
import com.google.gson.* import com.google.gson.*
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
@ -57,6 +58,7 @@ import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory
import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.Directives import ru.dbotthepony.kstarbound.util.Directives
import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
@ -68,6 +70,7 @@ import java.io.*
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
import java.text.DateFormat import java.text.DateFormat
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.Future import java.util.concurrent.Future
@ -85,12 +88,14 @@ import java.util.stream.Collector
import kotlin.NoSuchElementException import kotlin.NoSuchElementException
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
object Starbound : ISBFileLocator { object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLocator {
const val ENGINE_VERSION = "0.0.1" const val ENGINE_VERSION = "0.0.1"
const val NATIVE_PROTOCOL_VERSION = 748 const val NATIVE_PROTOCOL_VERSION = 748
const val LEGACY_PROTOCOL_VERSION = 747 const val LEGACY_PROTOCOL_VERSION = 747
const val TIMESTEP = 1.0 / 60.0 const val TIMESTEP = 1.0 / 60.0
const val SYSTEM_WORLD_TIMESTEP = 1.0 / 20.0
const val TIMESTEP_NANOS = (TIMESTEP * 1_000_000_000L).toLong() const val TIMESTEP_NANOS = (TIMESTEP * 1_000_000_000L).toLong()
const val SYSTEM_WORLD_TIMESTEP_NANOS = (SYSTEM_WORLD_TIMESTEP * 1_000_000_000L).toLong()
// compile flags. uuuugh // compile flags. uuuugh
const val DEDUP_CELL_STATES = true const val DEDUP_CELL_STATES = true
@ -106,14 +111,13 @@ object Starbound : ISBFileLocator {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
val thread = Thread(::universeThread, "Universe Thread") override fun schedule(executor: Executor, command: Runnable, delay: Long, unit: TimeUnit): Future<*> {
val mailbox = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } return schedule(Runnable { executor.execute(command) }, delay, unit)
val mailboxBootstrapped = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } }
val mailboxInitialized = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) }
init { init {
thread.isDaemon = true isDaemon = true
thread.start() start()
} }
private val ioPoolCounter = AtomicInteger() private val ioPoolCounter = AtomicInteger()
@ -135,8 +139,6 @@ object Starbound : ISBFileLocator {
val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool() val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool()
@JvmField @JvmField
val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher() val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher()
@JvmField
val COROUTINES = CoroutineScope(COROUTINE_EXECUTOR)
@JvmField @JvmField
val CLEANER: Cleaner = Cleaner.create { val CLEANER: Cleaner = Cleaner.create {
@ -512,8 +514,6 @@ object Starbound : ISBFileLocator {
LOGGER.info("Finished reading PAK archives") LOGGER.info("Finished reading PAK archives")
bootstrapped = true bootstrapped = true
bootstrapping = false bootstrapping = false
checkMailbox()
} }
private fun doInitialize() { private fun doInitialize() {
@ -559,8 +559,6 @@ object Starbound : ISBFileLocator {
} }
}) })
checkMailbox()
val tasks = ArrayList<Future<*>>() val tasks = ArrayList<Future<*>>()
tasks.addAll(Registries.load(ext2files)) tasks.addAll(Registries.load(ext2files))
@ -573,7 +571,6 @@ object Starbound : ISBFileLocator {
while (tasks.isNotEmpty()) { while (tasks.isNotEmpty()) {
tasks.removeIf { it.isDone } tasks.removeIf { it.isDone }
checkMailbox()
loaded = toLoad - tasks.size loaded = toLoad - tasks.size
loadingProgress = (total - tasks.size) / total loadingProgress = (total - tasks.size) / total
LockSupport.parkNanos(5_000_000L) LockSupport.parkNanos(5_000_000L)
@ -588,22 +585,12 @@ object Starbound : ISBFileLocator {
initialized = true initialized = true
} }
fun initializeGame(): Future<*> { fun initializeGame(): CompletableFuture<*> {
return mailbox.submit { doInitialize() } return submit { doInitialize() }
} }
fun bootstrapGame(): Future<*> { fun bootstrapGame(): CompletableFuture<*> {
return mailbox.submit { doBootstrap() } return submit { doBootstrap() }
}
private fun checkMailbox() {
mailbox.executeQueuedTasks()
if (bootstrapped)
mailboxBootstrapped.executeQueuedTasks()
if (initialized)
mailboxInitialized.executeQueuedTasks()
} }
private var fontPath: File? = null private var fontPath: File? = null
@ -614,11 +601,11 @@ object Starbound : ISBFileLocator {
if (fontPath != null) if (fontPath != null)
return CompletableFuture.completedFuture(fontPath) return CompletableFuture.completedFuture(fontPath)
return CompletableFuture.supplyAsync(Supplier { return supplyAsync {
val fontPath = Starbound.fontPath val fontPath = Starbound.fontPath
if (fontPath != null) if (fontPath != null)
return@Supplier fontPath return@supplyAsync fontPath
val file = locate("/hobo.ttf") val file = locate("/hobo.ttf")
@ -630,15 +617,8 @@ object Starbound : ISBFileLocator {
val tempPath = File(System.getProperty("java.io.tmpdir"), "sb-hobo.ttf") val tempPath = File(System.getProperty("java.io.tmpdir"), "sb-hobo.ttf")
tempPath.writeBytes(file.read().array()) tempPath.writeBytes(file.read().array())
Starbound.fontPath = tempPath Starbound.fontPath = tempPath
return@Supplier tempPath return@supplyAsync tempPath
} }
}, mailboxBootstrapped)
}
private fun universeThread() {
while (true) {
checkMailbox()
LockSupport.park()
} }
} }
} }

View File

@ -2,7 +2,6 @@ package ru.dbotthepony.kstarbound.client
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 io.netty.channel.Channel import io.netty.channel.Channel
import io.netty.channel.local.LocalAddress import io.netty.channel.local.LocalAddress
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
@ -61,12 +60,10 @@ import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.RayDirection
import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
@ -80,9 +77,9 @@ import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinWorkerThread import java.util.concurrent.ForkJoinWorkerThread
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer import java.util.function.Consumer
@ -93,11 +90,14 @@ import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.properties.Delegates import kotlin.properties.Delegates
class StarboundClient private constructor(val clientID: Int) : Closeable { class StarboundClient private constructor(val clientID: Int) : BlockableEventLoop("Client Thread $clientID"), Closeable {
val window: Long constructor() : this(COUNTER.getAndIncrement())
var window: Long = 0L
private set
val camera = Camera(this) val camera = Camera(this)
val input = UserInput() val input = UserInput()
val thread: Thread = Thread.currentThread() val thread: Thread = this
private val threadCounter = AtomicInteger() private val threadCounter = AtomicInteger()
// client specific executor which will accept tasks which involve probable // client specific executor which will accept tasks which involve probable
@ -119,8 +119,10 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
} }
}, null, false) }, null, false)
val mailbox = MailboxExecutorService(thread).also { it.exceptionHandler = ExceptionLogger(LOGGER) } @Deprecated("Use this directly", replaceWith = ReplaceWith("this"))
val capabilities: GLCapabilities val mailbox = this
var capabilities: GLCapabilities by Delegates.notNull()
private set
var viewportX: Int = 0 var viewportX: Int = 0
private set private set
@ -150,14 +152,11 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
var fullbright = true var fullbright = true
var shouldTerminate = false var viewportMatrixScreen: Matrix3f = Matrix3f.rowMajor()
private set
var viewportMatrixScreen: Matrix3f
private set private set
get() = Matrix3f.unmodifiable(field) get() = Matrix3f.unmodifiable(field)
var viewportMatrixWorld: Matrix3f var viewportMatrixWorld: Matrix3f = Matrix3f.rowMajor()
private set private set
get() = Matrix3f.unmodifiable(field) get() = Matrix3f.unmodifiable(field)
@ -185,7 +184,6 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
private val scissorStack = LinkedList<ScissorRect>() private val scissorStack = LinkedList<ScissorRect>()
private val onDrawGUI = ArrayList<() -> Unit>() private val onDrawGUI = ArrayList<() -> 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 openglCleanQueue = ReferenceQueue<Any>() private val openglCleanQueue = ReferenceQueue<Any>()
private var openglCleanQueueHead: CleanRef? = null private var openglCleanQueueHead: CleanRef? = null
@ -201,126 +199,18 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
var openglObjectsCleaned = 0L var openglObjectsCleaned = 0L
private set private set
var maxTextureBlocks: Int = 0
private set
var maxUserTextureBlocks: Int = 0 // available textures blocks for generic use
private set
var maxVertexAttribBindPoints: Int = 0
private set
var lightMapLocation = maxTextureBlocks - 1
private set
init { init {
check(CLIENTS.get() == null) { "Already has OpenGL context existing at ${Thread.currentThread()}!" }
CLIENTS.set(this) CLIENTS.set(this)
lock.lock()
try {
clients++
if (!glfwInitialized) {
check(GLFW.glfwInit()) { "Unable to initialize GLFW" }
glfwInitialized = true
GLFWErrorCallback.create { error, description ->
LOGGER.error("LWJGL error {}: {}", error, description)
}.set()
}
} finally {
lock.unlock()
}
GLFW.glfwDefaultWindowHints()
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE)
GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE)
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 4)
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 5)
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE)
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE)
window = GLFW.glfwCreateWindow(800, 600, "KStarbound: Locating files...", MemoryUtil.NULL, MemoryUtil.NULL)
require(window != MemoryUtil.NULL) { "Unable to create GLFW window" }
input.installCallback(window)
GLFW.glfwMakeContextCurrent(window)
// This line is critical for LWJGL's interoperation with GLFW's
// OpenGL context, or any context that is managed externally.
// LWJGL detects the context that is current in the current thread,
// creates the GLCapabilities instance and makes the OpenGL
// bindings available for use.
capabilities = GL.createCapabilities()
GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h ->
if (w == 0 || h == 0) {
isRenderingGame = false
} else {
isRenderingGame = true
setViewport(0, 0, w, h)
viewportMatrixScreen = updateViewportMatrixScreen()
viewportMatrixWorld = updateViewportMatrixWorld()
for (callback in onViewportChanged) {
callback.invoke(w, h)
}
}
}
var stack = MemoryStack.stackPush()
try {
val pWidth = stack.mallocInt(1)
val pHeight = stack.mallocInt(1)
GLFW.glfwGetWindowSize(window, pWidth, pHeight)
val vidmode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor())!!
GLFW.glfwSetWindowPos(
window,
(vidmode.width() - pWidth[0]) / 2,
(vidmode.height() - pHeight[0]) / 2
)
setViewport(0, 0, pWidth[0], pHeight[0])
viewportMatrixScreen = updateViewportMatrixScreen()
viewportMatrixWorld = updateViewportMatrixWorld()
} finally {
stack.close()
}
stack = MemoryStack.stackPush()
try {
val pWidth = stack.mallocInt(1)
val pHeight = stack.mallocInt(1)
val pChannels = stack.mallocInt(1)
val readFromDisk = readInternalBytes("starbound_icon.png")
val buff = ByteBuffer.allocateDirect(readFromDisk.size)
buff.put(readFromDisk)
buff.position(0)
val data = STBImage.stbi_load_from_memory(buff, pWidth, pHeight, pChannels, 4) ?: throw IllegalStateException("Unable to decode starbound_icon.png")
val img = GLFWImage.malloc()
img.set(pWidth[0], pHeight[0], data)
GLFW.nglfwSetWindowIcon(window, 1, memAddressSafe(img))
img.free()
} finally {
stack.close()
}
// vsync
GLFW.glfwSwapInterval(0)
GLFW.glfwShowWindow(window)
}
val maxTextureBlocks = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS)
val maxUserTextureBlocks = maxTextureBlocks - 1 // available textures blocks for generic use
val maxVertexAttribBindPoints = glGetInteger(GL_MAX_VERTEX_ATTRIB_BINDINGS)
init {
LOGGER.info("OpenGL Version: ${glGetString(GL_VERSION)}")
LOGGER.info("OpenGL Vendor: ${glGetString(GL_VENDOR)}")
LOGGER.info("OpenGL Renderer: ${glGetString(GL_RENDERER)}")
LOGGER.debug("Max supported texture image units: $maxTextureBlocks")
LOGGER.debug("Max supported vertex attribute bind points: $maxVertexAttribBindPoints")
} }
val stack = Matrix3fStack() val stack = Matrix3fStack()
@ -328,7 +218,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
// минимальное время хранения 5 минут и... // минимальное время хранения 5 минут и...
val named2DTextures0: Cache<Image, GLTexture2D> = Caffeine.newBuilder() val named2DTextures0: Cache<Image, GLTexture2D> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(1)) .expireAfterAccess(Duration.ofMinutes(1))
.scheduler(Scheduler.systemScheduler()) .scheduler(Starbound)
.build() .build()
// ...бесконечное хранение пока кто-то все ещё использует текстуру // ...бесконечное хранение пока кто-то все ещё использует текстуру
@ -340,8 +230,6 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
private val fontShaderPrograms = ArrayList<WeakReference<FontProgram>>() private val fontShaderPrograms = ArrayList<WeakReference<FontProgram>>()
private val uberShaderPrograms = ArrayList<WeakReference<UberShader>>() private val uberShaderPrograms = ArrayList<WeakReference<UberShader>>()
val lightMapLocation = maxTextureBlocks - 1
fun addShaderProgram(program: GLShaderProgram) { fun addShaderProgram(program: GLShaderProgram) {
if (program is UberShader) { if (program is UberShader) {
uberShaderPrograms.add(WeakReference(program)) uberShaderPrograms.add(WeakReference(program))
@ -365,9 +253,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
} }
} }
private fun executeQueuedTasks() { private fun performOpenGLCleanup() {
mailbox.executeQueuedTasks()
var next = openglCleanQueue.poll() as CleanRef? var next = openglCleanQueue.poll() as CleanRef?
var i = 0 var i = 0
@ -414,7 +300,8 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
var framebuffer by GLObjectTracker<GLFrameBuffer>(::glBindFramebuffer, GL_FRAMEBUFFER) var framebuffer by GLObjectTracker<GLFrameBuffer>(::glBindFramebuffer, GL_FRAMEBUFFER)
var program by GLObjectTracker<GLShaderProgram>(::glUseProgram) var program by GLObjectTracker<GLShaderProgram>(::glUseProgram)
val textures2D = GLTexturesTracker<GLTexture2D>(maxTextureBlocks) var textures2D: GLTexturesTracker<GLTexture2D> by Delegates.notNull()
private set
var clearColor by GLGenericTracker<IStruct4f>(RGBAColor.WHITE) { var clearColor by GLGenericTracker<IStruct4f>(RGBAColor.WHITE) {
val (r, g, b, a) = it val (r, g, b, a) = it
@ -432,37 +319,23 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
private var fontInitialized = false private var fontInitialized = false
init {
Starbound.loadFont().thenAcceptAsync(Consumer {
try {
font = Font(it.canonicalPath)
fontInitialized = true
} catch (err: Throwable) {
LOGGER.fatal("Unable to load font", err)
}
}, mailbox)
}
val programs = GLPrograms() val programs = GLPrograms()
init { val whiteTexture by lazy(LazyThreadSafetyMode.NONE) {
glActiveTexture(GL_TEXTURE0) val texture = GLTexture2D(1, 1, GL_RGB8)
checkForGLError()
}
val whiteTexture = GLTexture2D(1, 1, GL_RGB8)
val missingTexture = GLTexture2D(8, 8, GL_RGB8)
init {
val buffer = ByteBuffer.allocateDirect(3) val buffer = ByteBuffer.allocateDirect(3)
buffer.put(0xFF.toByte()) buffer.put(0xFF.toByte())
buffer.put(0xFF.toByte()) buffer.put(0xFF.toByte())
buffer.put(0xFF.toByte()) buffer.put(0xFF.toByte())
buffer.position(0) buffer.position(0)
whiteTexture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer) texture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer)
texture
} }
init { val missingTexture by lazy(LazyThreadSafetyMode.NONE) {
val texture = GLTexture2D(8, 8, GL_RGB8)
val buffer = ByteBuffer.allocateDirect(3 * 8 * 8) val buffer = ByteBuffer.allocateDirect(3 * 8 * 8)
for (row in 0 until 4) { for (row in 0 until 4) {
@ -494,7 +367,8 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
} }
buffer.position(0) buffer.position(0)
missingTexture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer) texture.upload(GL_RGB, GL_UNSIGNED_BYTE, buffer)
texture
} }
fun setViewport(x: Int, y: Int, width: Int, height: Int) { fun setViewport(x: Int, y: Int, width: Int, height: Int) {
@ -557,14 +431,6 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
val currentScissorRect get() = scissorStack.lastOrNull() val currentScissorRect get() = scissorStack.lastOrNull()
fun ensureSameThread() {
if (thread !== Thread.currentThread()) {
throw IllegalAccessException("Trying to access $this outside of $thread!")
}
}
fun isSameThread() = thread === Thread.currentThread()
fun newEBO() = BufferObject.EBO() fun newEBO() = BufferObject.EBO()
fun newVBO() = BufferObject.VBO() fun newVBO() = BufferObject.VBO()
fun newVAO() = VertexArrayObject() fun newVAO() = VertexArrayObject()
@ -675,14 +541,6 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
val tileRenderers = TileRenderers(this) val tileRenderers = TileRenderers(this)
var world: ClientWorld? = null var world: ClientWorld? = null
init {
clearColor = RGBAColor.SLATE_GRAY
blend = true
blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA
}
val spinner = ExecutionSpinner(::executeQueuedTasks, ::renderFrame, Starbound.TIMESTEP_NANOS)
val settings = ClientSettings() val settings = ClientSettings()
val viewportCells: ICellAccess = object : ICellAccess { val viewportCells: ICellAccess = object : ICellAccess {
@ -702,7 +560,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight) var viewportLighting = LightCalculator(viewportCells, viewportCellWidth, viewportCellHeight)
private set private set
var viewportLightingTexture = GLTexture2D(1, 1, GL_RGB8) var viewportLightingTexture: GLTexture2D by Delegates.notNull()
private set private set
private var viewportLightingMem: ByteBuffer? = null private var viewportLightingMem: ByteBuffer? = null
@ -754,8 +612,8 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
private fun drawPerformanceBasic(onlyMemory: Boolean) { private fun drawPerformanceBasic(onlyMemory: Boolean) {
val runtime = Runtime.getRuntime() val runtime = Runtime.getRuntime()
if (!onlyMemory) font.render("Latency: ${(spinner.averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f) //if (!onlyMemory) font.render("Latency: ${(spinner.averageRenderWait * 1_00000.0).toInt() / 100f}ms", scale = 0.4f)
if (!onlyMemory) font.render("Frame: ${(spinner.averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f) //if (!onlyMemory) font.render("Frame: ${(spinner.averageRenderTime * 1_00000.0).toInt() / 100f}ms", y = font.lineHeight * 0.6f, scale = 0.4f)
font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 1.2f, scale = 0.4f) font.render("JVM Heap: ${formatBytesShort(runtime.totalMemory() - runtime.freeMemory())}", y = font.lineHeight * 1.2f, scale = 0.4f)
if (!onlyMemory) font.render("OGL C: $openglObjectsCreated D: $openglObjectsCleaned A: ${openglObjectsCreated - openglObjectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f) if (!onlyMemory) font.render("OGL C: $openglObjectsCreated D: $openglObjectsCleaned A: ${openglObjectsCreated - openglObjectsCleaned}", y = font.lineHeight * 1.8f, scale = 0.4f)
} }
@ -763,7 +621,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
private var renderedLoadingScreen = false private var renderedLoadingScreen = false
private fun renderLoadingScreen() { private fun renderLoadingScreen() {
executeQueuedTasks() performOpenGLCleanup()
updateViewportParams() updateViewportParams()
clearColor = RGBAColor.BLACK clearColor = RGBAColor.BLACK
@ -905,29 +763,24 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
} }
} }
private fun renderFrame(): Boolean { private fun renderFrame() {
if (GLFW.glfwWindowShouldClose(window)) {
close()
return false
}
val world = world val world = world
if (!isRenderingGame) { if (!isRenderingGame) {
executeQueuedTasks() performOpenGLCleanup()
GLFW.glfwPollEvents() GLFW.glfwPollEvents()
if (world != null && Starbound.initialized) if (world != null && Starbound.initialized)
world.tick() world.tick()
activeConnection?.flush() activeConnection?.flush()
return true return
} }
if (!Starbound.initialized || !fontInitialized) { if (!Starbound.initialized || !fontInitialized) {
executeQueuedTasks() performOpenGLCleanup()
renderLoadingScreen() renderLoadingScreen()
return true return
} }
if (renderedLoadingScreen) { if (renderedLoadingScreen) {
@ -937,7 +790,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
input.think() input.think()
camera.think(Starbound.TIMESTEP) camera.think(Starbound.TIMESTEP)
executeQueuedTasks() performOpenGLCleanup()
layers.clear() layers.clear()
@ -978,78 +831,216 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
GLFW.glfwSwapBuffers(window) GLFW.glfwSwapBuffers(window)
GLFW.glfwPollEvents() GLFW.glfwPollEvents()
executeQueuedTasks() performOpenGLCleanup()
activeConnection?.flush() activeConnection?.flush()
return true
} }
private fun spin() { private fun tick() {
try { try {
while (!shouldTerminate && spinner.spin()) { val ply = activeConnection?.character
val ply = activeConnection?.character
if (ply != null) { if (ply != null) {
camera.pos = ply.position camera.pos = ply.position
ply.movement.controlMove = if (input.KEY_A_DOWN) Direction.LEFT else if (input.KEY_D_DOWN) Direction.RIGHT else null ply.movement.controlMove = if (input.KEY_A_DOWN) Direction.LEFT else if (input.KEY_D_DOWN) Direction.RIGHT else null
ply.movement.controlJump = input.KEY_SPACE_DOWN ply.movement.controlJump = input.KEY_SPACE_DOWN
ply.movement.controlRun = !input.KEY_LEFT_SHIFT_DOWN ply.movement.controlRun = !input.KEY_LEFT_SHIFT_DOWN
} else { } else {
camera.pos += Vector2d( camera.pos += Vector2d(
(if (input.KEY_A_DOWN) -Starbound.TIMESTEP * 32f / settings.zoom else 0.0) + (if (input.KEY_D_DOWN) Starbound.TIMESTEP * 32f / settings.zoom else 0.0), (if (input.KEY_A_DOWN) -Starbound.TIMESTEP * 32f / settings.zoom else 0.0) + (if (input.KEY_D_DOWN) Starbound.TIMESTEP * 32f / settings.zoom else 0.0),
(if (input.KEY_W_DOWN) Starbound.TIMESTEP * 32f / settings.zoom else 0.0) + (if (input.KEY_S_DOWN) -Starbound.TIMESTEP * 32f / settings.zoom else 0.0) (if (input.KEY_W_DOWN) Starbound.TIMESTEP * 32f / settings.zoom else 0.0) + (if (input.KEY_S_DOWN) -Starbound.TIMESTEP * 32f / settings.zoom else 0.0)
) )
camera.pos = world?.geometry?.wrap(camera.pos) ?: camera.pos camera.pos = world?.geometry?.wrap(camera.pos) ?: camera.pos
} }
if (input.KEY_ESCAPE_PRESSED) { renderFrame()
GLFW.glfwSetWindowShouldClose(window, true) performOpenGLCleanup()
}
if (input.KEY_ESCAPE_PRESSED) {
GLFW.glfwSetWindowShouldClose(window, true)
}
if (GLFW.glfwWindowShouldClose(window)) {
close()
} }
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.fatal("Exception in client loop", err) LOGGER.fatal("Exception in main game logic", err)
} finally { shutdownNow()
executor.shutdown()
lock.lock()
try {
if (window != MemoryUtil.NULL) {
Callbacks.glfwFreeCallbacks(window)
GLFW.glfwDestroyWindow(window)
}
if (--clients == 0) {
GLFW.glfwTerminate()
GLFW.glfwSetErrorCallback(null)?.free()
glfwInitialized = false
}
shouldTerminate = true
for (callback in terminateCallbacks) {
callback.invoke()
}
} catch (err: Throwable) {
LOGGER.fatal("Exception while destroying client", err)
} finally {
lock.unlock()
}
} }
} }
fun onTermination(lambda: () -> Unit) { override fun performShutdown() {
terminateCallbacks.add(lambda) executor.shutdown()
lock.lock()
try {
if (window != MemoryUtil.NULL) {
Callbacks.glfwFreeCallbacks(window)
GLFW.glfwDestroyWindow(window)
}
if (--clients == 0) {
GLFW.glfwTerminate()
GLFW.glfwSetErrorCallback(null)?.free()
glfwInitialized = false
}
} catch (err: Throwable) {
LOGGER.fatal("Exception while destroying client", err)
} finally {
lock.unlock()
}
} }
override fun close() { override fun close() {
shouldTerminate = true shutdown()
}
private fun initialize() {
check(CLIENTS.get() == null) { "Already has OpenGL context existing at ${currentThread()}!" }
CLIENTS.set(this)
lock.lock()
try {
clients++
if (!glfwInitialized) {
check(GLFW.glfwInit()) { "Unable to initialize GLFW" }
glfwInitialized = true
GLFWErrorCallback.create { error, description ->
LOGGER.error("LWJGL error {}: {}", error, description)
}.set()
}
} finally {
lock.unlock()
}
GLFW.glfwDefaultWindowHints()
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE)
GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE)
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 4)
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 5)
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE)
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE)
window = GLFW.glfwCreateWindow(800, 600, "KStarbound: Locating files...", MemoryUtil.NULL, MemoryUtil.NULL)
require(window != MemoryUtil.NULL) { "Unable to create GLFW window" }
input.installCallback(window)
GLFW.glfwMakeContextCurrent(window)
// This line is critical for LWJGL's interoperation with GLFW's
// OpenGL context, or any context that is managed externally.
// LWJGL detects the context that is current in the current thread,
// creates the GLCapabilities instance and makes the OpenGL
// bindings available for use.
capabilities = GL.createCapabilities()
GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h ->
if (w == 0 || h == 0) {
isRenderingGame = false
} else {
isRenderingGame = true
setViewport(0, 0, w, h)
viewportMatrixScreen = updateViewportMatrixScreen()
viewportMatrixWorld = updateViewportMatrixWorld()
for (callback in onViewportChanged) {
callback.invoke(w, h)
}
}
}
var stack = MemoryStack.stackPush()
try {
val pWidth = stack.mallocInt(1)
val pHeight = stack.mallocInt(1)
GLFW.glfwGetWindowSize(window, pWidth, pHeight)
val vidmode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor())!!
GLFW.glfwSetWindowPos(
window,
(vidmode.width() - pWidth[0]) / 2,
(vidmode.height() - pHeight[0]) / 2
)
setViewport(0, 0, pWidth[0], pHeight[0])
viewportMatrixScreen = updateViewportMatrixScreen()
viewportMatrixWorld = updateViewportMatrixWorld()
} finally {
stack.close()
}
stack = MemoryStack.stackPush()
try {
val pWidth = stack.mallocInt(1)
val pHeight = stack.mallocInt(1)
val pChannels = stack.mallocInt(1)
val readFromDisk = readInternalBytes("starbound_icon.png")
val buff = ByteBuffer.allocateDirect(readFromDisk.size)
buff.put(readFromDisk)
buff.position(0)
val data = STBImage.stbi_load_from_memory(buff, pWidth, pHeight, pChannels, 4) ?: throw IllegalStateException("Unable to decode starbound_icon.png")
val img = GLFWImage.malloc()
img.set(pWidth[0], pHeight[0], data)
GLFW.nglfwSetWindowIcon(window, 1, memAddressSafe(img))
img.free()
} finally {
stack.close()
}
// vsync
GLFW.glfwSwapInterval(0)
GLFW.glfwShowWindow(window)
maxTextureBlocks = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS)
maxUserTextureBlocks = maxTextureBlocks - 1 // available textures blocks for generic use
maxVertexAttribBindPoints = glGetInteger(GL_MAX_VERTEX_ATTRIB_BINDINGS)
textures2D = GLTexturesTracker(maxTextureBlocks)
LOGGER.info("OpenGL Version: ${glGetString(GL_VERSION)}")
LOGGER.info("OpenGL Vendor: ${glGetString(GL_VENDOR)}")
LOGGER.info("OpenGL Renderer: ${glGetString(GL_RENDERER)}")
LOGGER.debug("Max supported texture image units: $maxTextureBlocks")
LOGGER.debug("Max supported vertex attribute bind points: $maxVertexAttribBindPoints")
lightMapLocation = maxTextureBlocks - 1
clearColor = RGBAColor.SLATE_GRAY
blend = true
blendFunc = BlendFunc.MULTIPLY_WITH_ALPHA
viewportLightingTexture = GLTexture2D(1, 1, GL_RGB8)
Starbound.loadFont().thenAcceptAsync(Consumer {
try {
font = Font(it.canonicalPath)
fontInitialized = true
} catch (err: Throwable) {
LOGGER.fatal("Unable to load font", err)
}
}, this)
} }
init { init {
CLIENTS.remove()
input.addScrollCallback { _, x, y -> input.addScrollCallback { _, x, y ->
if (y > 0.0) { if (y > 0.0) {
settings.zoom *= y.toFloat() * 2f settings.zoom *= y.toFloat() * 2f
@ -1057,30 +1048,17 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
settings.zoom /= -y.toFloat() * 2f settings.zoom /= -y.toFloat() * 2f
} }
} }
execute { initialize() }
scheduleAtFixedRate(::tick, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
start()
}
override fun toString(): String {
return "StarboundClient[$clientID]"
} }
companion object { companion object {
fun create(): CompletableFuture<StarboundClient> {
val future = CompletableFuture<StarboundClient>()
val clientID = COUNTER.getAndIncrement()
val thread = Thread(Runnable {
val client = try {
StarboundClient(clientID)
} catch (err: Throwable) {
future.completeExceptionally(err)
throw err
}
future.complete(client)
client.spin()
}, "Client Thread $clientID")
thread.start()
return future
}
private val COUNTER = AtomicInteger() private val COUNTER = AtomicInteger()
private val LOGGER = LogManager.getLogger(StarboundClient::class.java) private val LOGGER = LogManager.getLogger(StarboundClient::class.java)
private val CLIENTS = ThreadLocal<StarboundClient>() private val CLIENTS = ThreadLocal<StarboundClient>()

View File

@ -9,6 +9,7 @@ import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.* import ru.dbotthepony.kstarbound.client.gl.*
import ru.dbotthepony.kstarbound.client.gl.shader.UberShader import ru.dbotthepony.kstarbound.client.gl.shader.UberShader
@ -30,22 +31,22 @@ import kotlin.math.roundToInt
class TileRenderers(val client: StarboundClient) { class TileRenderers(val client: StarboundClient) {
private val foreground: Cache<GLTexture2D, Config> = Caffeine.newBuilder() private val foreground: Cache<GLTexture2D, Config> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler()) .scheduler(Starbound)
.build() .build()
private val background: Cache<GLTexture2D, Config> = Caffeine.newBuilder() private val background: Cache<GLTexture2D, Config> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler()) .scheduler(Starbound)
.build() .build()
private val matCache: Cache<String, TileRenderer> = Caffeine.newBuilder() private val matCache: Cache<String, TileRenderer> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler()) .scheduler(Starbound)
.build() .build()
private val modCache: Cache<String, TileRenderer> = Caffeine.newBuilder() private val modCache: Cache<String, TileRenderer> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler()) .scheduler(Starbound)
.build() .build()
fun getMaterialRenderer(defName: String): TileRenderer { fun getMaterialRenderer(defName: String): TileRenderer {

View File

@ -20,6 +20,7 @@ import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
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.World import ru.dbotthepony.kstarbound.world.World
@ -62,9 +63,8 @@ class ClientWorld(
return geometry.loopY || value in 0 .. renderRegionsY return geometry.loopY || value in 0 .. renderRegionsY
} }
override fun isSameThread(): Boolean { override val eventLoop: BlockableEventLoop
return client.isSameThread() get() = client
}
inner class RenderRegion(val x: Int, val y: Int) { inner class RenderRegion(val x: Int, val y: Int) {
inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) { inner class Layer(private val view: ITileAccess, private val isBackground: Boolean) {

View File

@ -0,0 +1,321 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isRealModifier
import ru.dbotthepony.kstarbound.defs.tile.orEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.orEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.api.MutableLiquidState
import ru.dbotthepony.kstarbound.world.api.TileColor
@JsonAdapter(DungeonBrush.Adapter::class)
abstract class DungeonBrush {
enum class Phase {
CLEAR,
DUNGEON_ID, // move dungeon id to very top so it takes effect right away
PLACE_WALLS,
PLACE_MODS,
PLACE_OBJECTS,
PLACE_BIOME_TREES,
PLACE_BIOME_ITEMS,
DO_WIRING,
DROP_ITEMS,
PLACE_NPCS;
}
class Adapter(gson: Gson) : TypeAdapter<DungeonBrush>() {
private val arrays = gson.getAdapter(JsonArray::class.java)
override fun write(out: JsonWriter, value: DungeonBrush) {
TODO("Not yet implemented")
}
override fun read(`in`: JsonReader): DungeonBrush {
val read = arrays.read(`in`)
// don't delegate to EnumAdapter since this can have bad consequences
// such as defaulting to CLEAR action and printing a warning in console
val type = DungeonBrushType.entries.firstOrNull { it.jsonName == read[0].asString } ?: throw NoSuchElementException("Unknown brush type ${read[0].asString}!")
try {
return type.createLegacy(read)
} catch (err: Throwable) {
throw JsonSyntaxException("Reading dungeon brush $type", err)
}
}
}
abstract fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld)
object Invalid : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
// do nothing
}
}
// in original engine this brush is non-deterministic.
data class Random(val children: ImmutableList<DungeonBrush>) : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (children.isEmpty())
return
children.random(world.random).execute(x, y, phase, world)
}
}
object Clear : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase !== Phase.CLEAR)
return
// TODO: delete objects too?
world.setLiquid(x, y, AbstractLiquidState.EMPTY)
world.setForeground(x, y, BuiltinMetaMaterials.EMPTY)
world.setForeground(x, y, BuiltinMetaMaterials.EMPTY_MOD)
world.setBackground(x, y, BuiltinMetaMaterials.EMPTY)
world.setBackground(x, y, BuiltinMetaMaterials.EMPTY_MOD)
world.setDungeonID(x, y)
}
override fun toString(): String {
return "Clear"
}
}
// In original engine due to oversight in Brush::parse and parseFrontBrush
// detailed
data class Tile(
val isBackground: Boolean,
val material: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref,
val modifier: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref,
val hueShift: Float = 0f,
val modHueShift: Float = 0f,
val color: TileColor = TileColor.DEFAULT,
) : DungeonBrush() {
constructor(isBackground: Boolean, data: JsonData) : this(
isBackground,
data.material, data.modifier,
data.modHueShift, data.hueShift,
TileColor.entries.firstOrNull { it.lowercase == data.colorVariant.lowercase() } ?: TileColor.entries[data.colorVariant.toIntOrNull() ?: throw JsonSyntaxException("Invalid color variant: ${data.colorVariant}")]
)
constructor(isBackground: Boolean, material: Registry.Ref<TileDefinition>, data: TiledData) : this(
isBackground, material,
data.modifier, data.hueshift,
data.modhueshift,
TileColor.entries.firstOrNull { it.lowercase == data.colorVariant.lowercase() } ?: TileColor.entries[data.colorVariant.toIntOrNull() ?: throw JsonSyntaxException("Invalid color variant: ${data.colorVariant}")]
)
@JsonFactory
data class JsonData(
val material: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref,
val modifier: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref,
val hueShift: Float = 0f,
val modHueShift: Float = 0f,
val colorVariant: String = "0", // HOLY FUCKING SHIT
)
@JsonFactory
data class TiledData(
val modifier: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref,
val hueshift: Float = 0f,
val modhueshift: Float = 0f,
val colorVariant: String = "0", // HOLY FUCKING SHIT
)
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase !== Phase.PLACE_WALLS)
return
if (isBackground)
world.setBackground(x, y, material.orEmptyTile, hueShift, color)
else
world.setForeground(x, y, material.orEmptyTile, hueShift, color)
if (!isBackground && material.orEmptyTile.value.collisionKind.isSolidCollision) {
world.setLiquid(x, y, AbstractLiquidState.EMPTY)
}
if (modifier.isRealModifier) {
if (isBackground)
world.setBackground(x, y, modifier.orEmptyModifier, modHueShift)
else
world.setForeground(x, y, modifier.orEmptyModifier, modHueShift)
}
}
}
data class WorldObject(
val obj: Registry.Ref<ObjectDefinition>,
val direction: Direction = Direction.LEFT,
val parameters: JsonObject = JsonObject(),
) : DungeonBrush() {
constructor(obj: Registry.Ref<ObjectDefinition>, extra: Extra) : this(obj, extra.direction, extra.parameters)
@JsonFactory
data class Extra(
val direction: Direction = Direction.LEFT,
val parameters: JsonObject = JsonObject(),
)
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase == Phase.PLACE_OBJECTS && obj.isPresent) {
world.placeObject(x, y, obj.entry!!, direction, parameters)
}
}
}
data class Vehicle(val vehicle: String, val parameters: JsonObject = JsonObject()) : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
LOGGER.warn("NYI: Vehicle at $x, $y")
}
}
object BiomeItems : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase === Phase.PLACE_BIOME_ITEMS) {
world.placeBiomeItems(x, y)
}
}
override fun toString(): String {
return "Biome item"
}
}
object BiomeTree : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase === Phase.PLACE_BIOME_TREES) {
world.placeBiomeTree(x, y)
}
}
override fun toString(): String {
return "Biome tree"
}
}
data class DropItem(val item: ItemDescriptor, val randomize: Boolean) : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase === Phase.DROP_ITEMS) {
if (randomize) {
world.dropRandomizedItem(x, y, item)
} else {
world.dropItem(x, y, item)
}
}
}
}
data class NPC(val data: JsonObject) : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase === Phase.PLACE_NPCS) {
LOGGER.warn("NYI: NPC at $x, $y")
}
}
}
data class Stagehand(val data: JsonObject) : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase === Phase.PLACE_NPCS) {
LOGGER.warn("NYI: Stagehand at $x, $y")
}
}
}
@JsonFactory
data class Surface(val variant: Int = 0, val mod: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref, @JsonIgnore val isBackground: Boolean = false) : DungeonBrush() {
val material = BuiltinMetaMaterials.BIOME_META_MATERIALS.getOrNull(variant) ?: throw IllegalArgumentException("Invalid biome metamaterial $variant")
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase == Phase.PLACE_WALLS) {
if (!isBackground) // TODO: is this intentional?
world.setForeground(x, y, material)
world.setBackground(x, y, material)
} else if (phase == Phase.PLACE_MODS) {
if (mod.isRealModifier) {
if (isBackground)
world.setBackground(x, y, mod.orEmptyModifier)
else
world.setForeground(x, y, mod.orEmptyModifier)
} else if (!isBackground && world.needsForegroundBiomeMod(x, y)) {
world.setForeground(x, y, BuiltinMetaMaterials.BIOME_MOD)
} else if (isBackground && world.needsBackgroundBiomeMod(x, y)) {
world.setBackground(x, y, BuiltinMetaMaterials.BIOME_MOD)
}
}
}
}
data class Liquid(val liquid: Registry.Ref<LiquidDefinition>, val level: Float = 1f, val isInfinite: Boolean = false) : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase == Phase.PLACE_WALLS) {
world.requestLiquid(x, y, MutableLiquidState(liquid.orEmptyLiquid, level, 1f, isInfinite))
}
}
}
@JsonFactory
data class Wire(val group: String, val local: Boolean = false) : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase === Phase.DO_WIRING) {
world.placeWiring(x, y, group, local)
}
}
}
object info_player_start : DungeonBrush() {
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase === Phase.PLACE_NPCS) {
world.playerStart = Vector2d(x.toDouble(), y.toDouble())
}
}
override fun toString(): String {
return "info_player_start"
}
}
data class DungeonID(val id: Int, val perTile: Boolean = false) : DungeonBrush() {
init {
require(id in 0 .. NO_DUNGEON_ID) { "Dungeon ID out of range: $id" }
}
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
if (phase == Phase.DUNGEON_ID) {
if (perTile)
world.setDungeonID(x, y, id)
else
world.setDungeonID(id)
}
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -0,0 +1,468 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.world.Direction
enum class DungeonBrushType(override val jsonName: String) : IStringSerializable {
INVALID("invalid") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.Invalid
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("invalid" in json) {
return DungeonBrush.Invalid
}
return null
}
},
CLEAR("clear") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.Clear
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if (json["clear"]?.asString == "true") {
return DungeonBrush.Clear
}
return null
}
},
RANDOM("random") {
private val adapter by lazy {
Starbound.gson.getAdapter(object : TypeToken<ImmutableList<DungeonBrush>>() {})
}
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.Random(adapter.fromJsonTree(json[1]))
}
override fun readTiled(json: JsonObject): DungeonBrush? {
return null
}
},
FOREGROUND("front") {
private val adapter by lazy {
Starbound.gson.getAdapter(DungeonBrush.Tile.JsonData::class.java)
}
private val adapterTiled by lazy {
Starbound.gson.getAdapter(DungeonBrush.Tile.TiledData::class.java)
}
private val adapter0 by lazy {
Starbound.gson.getAdapter(object : TypeToken<Registry.Ref<TileDefinition>>() {})
}
private val adapter1 by lazy {
Starbound.gson.getAdapter(object : TypeToken<Registry.Ref<TileModifierDefinition>>() {})
}
override fun createLegacy(json: JsonArray): DungeonBrush {
if (json.size() == 3) {
return DungeonBrush.Tile(false, adapter0.fromJsonTree(json[1]), adapter1.fromJsonTree(json[2]))
} else if (json.size() == 2) {
if (json[1] is JsonObject) {
return DungeonBrush.Tile(false, adapter.fromJsonTree(json[1]))
} else {
return DungeonBrush.Tile(false, adapter0.fromJsonTree(json[1]))
}
}
throw IllegalArgumentException("Invalid tile modifier brush: $json")
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("front" in json) {
return DungeonBrush.Tile(false, Registries.tiles.ref(json["front"].asString), adapterTiled.fromJsonTree(json))
} else if (json["layer"].asString == "front" && "material" in json) {
return DungeonBrush.Tile(false, Registries.tiles.ref(json["material"].asString), adapterTiled.fromJsonTree(json))
}
return null
}
},
BACKGROUND("back") {
private val adapter by lazy {
Starbound.gson.getAdapter(DungeonBrush.Tile.JsonData::class.java)
}
private val adapterTiled by lazy {
Starbound.gson.getAdapter(DungeonBrush.Tile.TiledData::class.java)
}
private val adapter0 by lazy {
Starbound.gson.getAdapter(object : TypeToken<Registry.Ref<TileDefinition>>() {})
}
private val adapter1 by lazy {
Starbound.gson.getAdapter(object : TypeToken<Registry.Ref<TileModifierDefinition>>() {})
}
override fun createLegacy(json: JsonArray): DungeonBrush {
if (json.size() == 3) {
return DungeonBrush.Tile(true, adapter0.fromJsonTree(json[1]), adapter1.fromJsonTree(json[2]))
} else if (json.size() == 2) {
if (json[1] is JsonObject) {
return DungeonBrush.Tile(true, adapter.fromJsonTree(json[1]))
} else {
return DungeonBrush.Tile(true, adapter0.fromJsonTree(json[1]))
}
}
throw IllegalArgumentException("Invalid tile modifier brush: $json")
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("back" in json) {
return DungeonBrush.Tile(true, Registries.tiles.ref(json["back"].asString), adapterTiled.fromJsonTree(json))
} else if (json["layer"].asString == "back" && "material" in json) {
return DungeonBrush.Tile(true, Registries.tiles.ref(json["material"].asString), adapterTiled.fromJsonTree(json))
}
return null
}
},
OBJECT("object") {
private val adapter0 by lazy {
Starbound.gson.getAdapter(object : TypeToken<Registry.Ref<ObjectDefinition>>() {})
}
private val adapter1 by lazy {
Starbound.gson.getAdapter(DungeonBrush.WorldObject.Extra::class.java)
}
private val adapterObject by lazy {
Starbound.gson.getAdapter(JsonObject::class.java)
}
override fun createLegacy(json: JsonArray): DungeonBrush {
if (json.size() > 2) {
return DungeonBrush.WorldObject(adapter0.fromJsonTree(json[1]), adapter1.fromJsonTree(json[2]))
}
return DungeonBrush.WorldObject(adapter0.fromJsonTree(json[1]))
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("object" in json) {
val ref = Registries.worldObjects.ref(json["object"].asString)
var direction = if ("tilesetDirection" in json) Direction.entries.first { it.jsonName == json["tilesetDirection"].asString } else Direction.RIGHT
if ("flipX" in json)
direction = direction.opposite
var parameters = json.get("parameters")
if (parameters == null || parameters.isJsonNull)
parameters = JsonObject()
else if (parameters is JsonPrimitive)
parameters = adapterObject.fromJson(parameters.asString)
return DungeonBrush.WorldObject(ref, direction, parameters as JsonObject)
}
return null
}
},
VEHICLE("vehicle") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.Vehicle(json[1].asString, if (json.size() >= 3) json[2].asJsonObject else JsonObject())
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("vehicle" in json) {
return DungeonBrush.Vehicle(json["vehicle"].asString, json.get("parameters") { JsonObject() })
}
return null
}
},
BIOME_ITEMS("biomeitems") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.BiomeItems
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("biomeitems" in json)
return DungeonBrush.BiomeItems
return null
}
},
BIOME_TREE("biometree") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.BiomeTree
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("biometree" in json)
return DungeonBrush.BiomeTree
return null
}
},
DROP_ITEM("item") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.DropItem(ItemDescriptor(json[1]), json[1] !is JsonObject)
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("item" in json) {
return DungeonBrush.DropItem(ItemDescriptor(json["item"].asString, json.get("count", 1L), json.get("parameters") { JsonObject() }), json.get("randomize", true))
}
return null
}
},
NPC("npc") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.NPC(json[1].asJsonObject)
}
private val adapterObject by lazy {
Starbound.gson.getAdapter(JsonObject::class.java)
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("npc" in json) {
val brush = JsonObject()
brush["kind"] = "npc"
brush["species"] = json["npc"] // this may be a single species or a comma
// separated list to be parsed later
if ("seed" in json) {
brush["seed"] = json["seed"]
} else {
brush["seed"] = "stable"
}
if ("typeName" in json) {
brush["typeName"] = json["typeName"]
}
var parameters = json.get("parameters")
if (parameters == null || parameters.isJsonNull)
parameters = JsonObject()
else if (parameters is JsonPrimitive)
parameters = adapterObject.fromJson(parameters.asString)
brush["parameters"] = parameters
return DungeonBrush.NPC(brush)
} else if ("monster" in json) {
val brush = JsonObject()
brush["kind"] = "monster"
brush["typeName"] = json["monster"]
if ("seed" in json) {
brush["seed"] = json["seed"]
} else {
brush["seed"] = "stable"
}
var parameters = json.get("parameters")
if (parameters == null || parameters.isJsonNull)
parameters = JsonObject()
else if (parameters is JsonPrimitive)
parameters = adapterObject.fromJson(parameters.asString)
brush["parameters"] = parameters
return DungeonBrush.NPC(brush)
}
return null
}
},
STAGEHAND("stagehand") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.Stagehand(json[1].asJsonObject)
}
private val adapterObject by lazy {
Starbound.gson.getAdapter(JsonObject::class.java)
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("stagehand" in json) {
val brush = JsonObject()
brush["type"] = json["stagehand"]
var parameters = json.get("parameters")
if (parameters == null || parameters.isJsonNull)
parameters = JsonObject()
else if (parameters is JsonPrimitive)
parameters = adapterObject.fromJson(parameters.asString)
brush["parameters"] = parameters
if ("broadcastArea" in json) // this is set properly as array inside TiledMap
brush["parameters"].asJsonObject["broadcastArea"] = json["broadcastArea"]
if (json["stagehand"].asString == "radiomessage" && "radiomessage" in json) // why lock behind radiomessage type?
brush["parameters"].asJsonObject["radiomessage"] = json["radiomessage"]
return DungeonBrush.Stagehand(brush)
}
return null
}
},
SURFACE("surface") {
private val adapter0 by lazy {
Starbound.gson.getAdapter(DungeonBrush.Surface::class.java)
}
override fun createLegacy(json: JsonArray): DungeonBrush {
return adapter0.fromJsonTree(if (json.size() == 1) JsonObject() else json[1])
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if (json["layer"].asString == "front" && "surface" in json) {
val variant = json["surface"].asString.toIntOrNull() ?: 0
val mod = json["mod"]?.asString?.let { Registries.tileModifiers.ref(it) } ?: BuiltinMetaMaterials.EMPTY_MOD.ref
return DungeonBrush.Surface(variant, mod, false)
}
return null
}
},
SURFACE_BACKGROUND("surfacebackground") {
private val adapter0 by lazy {
Starbound.gson.getAdapter(DungeonBrush.Surface::class.java)
}
override fun createLegacy(json: JsonArray): DungeonBrush {
return adapter0.fromJsonTree(if (json.size() == 1) JsonObject() else json[1]).copy(isBackground = true)
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if (json["layer"].asString == "back" && "surface" in json) {
val variant = json["surface"].asString.toIntOrNull() ?: 0
val mod = json["mod"]?.asString?.let { Registries.tileModifiers.ref(it) } ?: BuiltinMetaMaterials.EMPTY_MOD.ref
return DungeonBrush.Surface(variant, mod, true)
}
return null
}
},
LIQUID("liquid") {
private val adapter0 by lazy {
Starbound.gson.getAdapter(object : TypeToken<Registry.Ref<LiquidDefinition>>() {})
}
override fun createLegacy(json: JsonArray): DungeonBrush {
if (json.size() == 1)
throw JsonSyntaxException("No liquid specified")
if (json.size() == 2)
return DungeonBrush.Liquid(adapter0.fromJsonTree(json[1]))
else if (json.size() == 3)
return DungeonBrush.Liquid(adapter0.fromJsonTree(json[1]), 1f, json[2].asBoolean)
else if (json.size() == 4)
return DungeonBrush.Liquid(adapter0.fromJsonTree(json[1]), json[2].asFloat, json[3].asBoolean)
else
throw JsonSyntaxException("Invalid liquid data: $json")
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("liquid" in json) {
val quantity = json["quantity"]?.asFloat ?: 1f
val source = "source" in json
return DungeonBrush.Liquid(Registries.liquid.ref(json["liquid"].asString), quantity, source)
}
return null
}
},
WIRE("wire") {
private val adapter0 by lazy {
Starbound.gson.getAdapter(DungeonBrush.Wire::class.java)
}
override fun createLegacy(json: JsonArray): DungeonBrush {
return adapter0.fromJsonTree(if (json.size() == 1) JsonObject() else json[1])
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("wire" in json) {
return DungeonBrush.Wire(json["wire"].asString, "local" in json)
}
return null
}
},
PLAYER_START("playerstart") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.info_player_start
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("playerstart" in json) {
return DungeonBrush.info_player_start
}
return null
}
},
DUNGEON_ID("dungeonid") {
override fun createLegacy(json: JsonArray): DungeonBrush {
return DungeonBrush.DungeonID(json[1].asInt, if (json.size() >= 3) json[2].asBoolean else false)
}
override fun readTiled(json: JsonObject): DungeonBrush? {
if ("dungeonid" in json) {
return DungeonBrush.DungeonID(json["dungeonid"].asString.toIntOrNull() ?: NO_DUNGEON_ID, false)
}
return null
}
}
;
abstract fun createLegacy(json: JsonArray): DungeonBrush
abstract fun readTiled(json: JsonObject): DungeonBrush?
}

View File

@ -0,0 +1,237 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonSyntaxException
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.random.random
import java.util.concurrent.CompletableFuture
import java.util.random.RandomGenerator
// Dungeons in Starbound are separated into two categories:
// A. Dungeons described using specific tileset (palette, defined through JSON) and corresponding image maps (chunks)
// this is the legacy (original) way to define dungeons
// B. Dungeons described using Tiled's format stored as JSON
// this is the new way to define dungeons
// There is no preference how you define dungeons in new engine, both are handled
// with equal care. Tiled dungeons are good for cases where you manually place stuff (actual dungeons),
// image maps dungeons are good for creating terrain features using automated tools (since
// making automated tools output images is way easier than making them output TMX format).
// Example of above is using third-party complex noise algorithms
// which are not available in game engine. This way you can generate a variety of custom
// terrain features without having to add new terrain selector into engine.
// But keep in mind that adding dungeons into game is not free and
// comes with memory cost.
@JsonFactory
data class DungeonDefinition(
val metadata: Metadata,
// relevant for PNG defined dungeons
val tiles: ImageTileSet = ImageTileSet(),
val parts: ImmutableList<DungeonPart>,
) {
@JsonFactory
data class Metadata(
val name: String,
val displayName: String = "",
val species: String = "", // why is it required to be present in original code?
val protected: Boolean = false,
val maxRadius: Double = 100.0,
val maxParts: Int = 100,
val extendSurfaceFreeSpace: Int = 0,
val rules: ImmutableList<DungeonRule> = ImmutableList.of(),
val anchor: ImmutableSet<String> = ImmutableSet.of(),
val gravity: Either<Vector2d, Double>? = null,
val breathable: Boolean? = null,
) {
init {
require(maxRadius > 0.0) { "Non positive maxRadius. What are you trying to achieve?" }
require(anchor.isNotEmpty()) { "No anchors are specified, dungeon won't be able to spawn in world" }
}
}
val name: String
get() = metadata.name
init {
parts.forEach { it.bind(this) }
for (anchor in metadata.anchor) {
if (!parts.any { it.name == anchor }) {
throw JsonSyntaxException("Dungeon contains $anchor as anchor, but there is no such part")
}
}
}
val partMap: ImmutableMap<String, DungeonPart> = parts.stream().map { it.name to it }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))
val anchorParts: ImmutableList<DungeonPart> = metadata.anchor.stream().map { anchor -> parts.first { it.name == anchor } }.collect(ImmutableList.toImmutableList())
private fun connectableParts(connector: DungeonPart.JigsawConnector): List<DungeonPart.JigsawConnector> {
val result = ArrayList<DungeonPart.JigsawConnector>()
for (part in parts) {
if (!part.doesNotConnectTo(connector.part)) {
for (pconnector in part.connectors) {
if (pconnector.connectsTo(connector)) {
result.add(pconnector)
}
}
}
}
return result
}
private fun choosePart(parts: MutableList<DungeonPart.JigsawConnector>, random: RandomGenerator): DungeonPart.JigsawConnector {
val sum = parts.sumOf { it.part.chance }
val sample = random.nextDouble(sum)
var weighting = 0.0
val itr = parts.iterator()
for (part in itr) {
weighting += part.part.chance
if (weighting >= sample) {
itr.remove()
return part
}
}
return parts.removeLast()
}
fun validAnchors(world: ServerWorld): List<DungeonPart> {
return anchorParts.filter { world.template.threatLevel in it.minimumThreatLevel .. it.maximumThreatLevel }
}
private suspend fun generate0(anchor: DungeonPart, world: DungeonWorld, x: Int, y: Int, forcePlacement: Boolean, dungeonID: Int) {
val placementCounter = Object2IntArrayMap<String>()
val basePos = Vector2i(x, y)
val openSet = ArrayDeque<Pair<Vector2i, DungeonPart>>()
anchor.place(basePos, world, dungeonID)
var piecesPlaced = 1
placementCounter[anchor.name] = 1
openSet.add(basePos to anchor)
val origin = basePos + anchor.reader.size / 2
val closedConnectors = HashSet<Vector2i>()
while (openSet.isNotEmpty()) {
val (parentPos, parent) = openSet.removeFirst()
for (connector in parent.connectors) {
val connectorPos = parentPos + connector.offset
if (!closedConnectors.add(connectorPos))
continue
val candidates = connectableParts(connector)
.filter { world.parent.template.threatLevel in it.part.minimumThreatLevel .. it.part.maximumThreatLevel }
.toMutableList()
while (candidates.isNotEmpty()) {
val candidate = choosePart(candidates, world.random)
val partPos = connectorPos - candidate.offset + candidate.direction.positionAdjustment
val optionPos = connectorPos + candidate.direction.positionAdjustment
if (!candidate.part.ignoresPartMaximum) {
if (piecesPlaced >= metadata.maxParts) {
continue
}
if ((partPos - origin).length > metadata.maxRadius) {
continue
}
}
if (!candidate.part.allowsPlacement(placementCounter.getInt(candidate.part.name))) {
continue
} else if (!candidate.part.checkPartCombinationsAllowed(placementCounter)) {
continue
} else if (candidate.part.collidesWithPlaces(partPos.x, partPos.y, world)) {
continue
}
if (forcePlacement || candidate.part.canPlace(partPos.x, partPos.y, world)) {
candidate.part.place(partPos, world, dungeonID)
piecesPlaced++
placementCounter[candidate.part.name] = placementCounter.getInt(candidate.part.name) + 1
closedConnectors.add(partPos)
closedConnectors.add(optionPos)
openSet.add(partPos to candidate.part)
}
}
}
}
world.pressurizeLiquids()
}
fun generate(world: ServerWorld, random: RandomGenerator, x: Int, y: Int, markSurfaceAndTerrain: Boolean, forcePlacement: Boolean, dungeonID: Int = 0, terrainSurfaceSpaceExtends: Int = 0, commit: Boolean = true): CompletableFuture<DungeonWorld> {
require(dungeonID in 0 .. NO_DUNGEON_ID) { "Dungeon ID out of range: $dungeonID" }
val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends)
val validAnchors = anchorParts.filter { world.template.threatLevel in it.minimumThreatLevel .. it.maximumThreatLevel }
if (validAnchors.isEmpty()) {
LOGGER.error("Can't place dungeon ${metadata.name} because it has no valid anchors for threat level ${world.template.threatLevel}")
return CompletableFuture.completedFuture(dungeonWorld)
}
val anchor = validAnchors.random(world.random)
return CoroutineScope(Starbound.COROUTINE_EXECUTOR)
.async {
if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, world)) {
generate0(anchor, dungeonWorld, x, y - anchor.placementLevelConstraint, forcePlacement, dungeonID)
if (commit) {
dungeonWorld.commit()
}
}
dungeonWorld
}
.asCompletableFuture()
}
fun build(anchor: DungeonPart, world: ServerWorld, random: RandomGenerator, x: Int, y: Int, dungeonID: Int = NO_DUNGEON_ID, markSurfaceAndTerrain: Boolean = false, forcePlacement: Boolean = false, terrainSurfaceSpaceExtends: Int = 0, commit: Boolean = true): CompletableFuture<DungeonWorld> {
require(anchor in anchorParts) { "$anchor does not belong to $name" }
val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends)
return CoroutineScope(Starbound.COROUTINE_EXECUTOR)
.async {
generate0(anchor, dungeonWorld, x, y, forcePlacement, dungeonID)
if (commit) {
dungeonWorld.commit()
}
dungeonWorld
}
.asCompletableFuture()
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -0,0 +1,33 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class DungeonDirection(override val jsonName: String, val positionAdjustment: Vector2i) : IStringSerializable {
LEFT("left", Vector2i.NEGATIVE_X) {
override val opposite: DungeonDirection
get() = RIGHT
},
RIGHT("right", Vector2i.POSITIVE_X) {
override val opposite: DungeonDirection
get() = LEFT
},
UP("up", Vector2i.POSITIVE_Y) {
override val opposite: DungeonDirection
get() = UP
},
DOWN("down", Vector2i.NEGATIVE_Y) {
override val opposite: DungeonDirection
get() = DOWN
},
UNKNOWN("unknown", Vector2i.ZERO) {
override val opposite: DungeonDirection
get() = throw NoSuchElementException()
},
ANY("any", Vector2i.ZERO) {
override val opposite: DungeonDirection
get() = this
};
abstract val opposite: DungeonDirection
}

View File

@ -0,0 +1,381 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.io.FileNotFoundException
import java.util.stream.Stream
import kotlin.properties.Delegates
@JsonAdapter(DungeonPart.Adapter::class)
class DungeonPart(data: JsonData) {
@JsonFactory
data class JsonData(
val name: String,
val rules: ImmutableList<DungeonRule>,
val chance: Double = 1.0,
val markDungeonId: Boolean = true,
val overrideAllowAlways: Boolean = false,
val minimumThreatLevel: Double = Double.NEGATIVE_INFINITY,
val maximumThreatLevel: Double = Double.POSITIVE_INFINITY,
val clearAnchoredObjects: Boolean = true,
val def: JsonArray,
)
val name = data.name
val rules = data.rules
val chance = data.chance.coerceIn(0.0001, 1.0)
val markDungeonId = data.markDungeonId
val overrideAllowAlways = data.overrideAllowAlways
val minimumThreatLevel = data.minimumThreatLevel
val maximumThreatLevel = data.maximumThreatLevel
val clearAnchoredObjects = data.clearAnchoredObjects
data class JigsawConnector(val part: DungeonPart, val index: String, val forwardOnly: Boolean, val direction: DungeonDirection, val offset: Vector2i) {
fun connectsTo(other: JigsawConnector): Boolean {
if (forwardOnly || index != other.index)
return false
if (direction == DungeonDirection.ANY || other.direction == DungeonDirection.ANY)
return true
return direction == other.direction.opposite
}
}
var connectors: ImmutableList<JigsawConnector> by Delegates.notNull()
private set
var anchor: Vector2i by Delegates.notNull()
private set
// should be used only when placing actual dungeons, and not microdungeons
val placementLevelConstraint: Int by lazy {
var air = reader.size.y
var airX = 0
var ground = 0
var groundX = 0
var liquid = 0
var liquidX = 0
reader.iterateTiles { x, y, tile ->
for (rule in tile.rules) {
if (rule.requiresSolid && y > ground) {
ground = y
groundX = x
}
if (rule.requiresOpen && y < air) {
air = y
airX = x
}
if ((rule === DungeonRule.MustContainLiquid || rule === DungeonRule.MustNotContainLiquid) && y > liquid) {
liquid = y
liquidX = x
}
}
}
ground = ground.coerceAtLeast(liquid)
if (air < ground) {
throw IllegalArgumentException("Invalid ground vs air constraint, ground at: $groundX,$ground; air at: $airX,$air; liquid at: $liquidX,$liquid for part $name")
} else {
air
}
}
val ignoresPartMaximum: Boolean = rules.any { it.ignorePartMaximum }
var dungeon by Delegates.notNull<DungeonDefinition>()
private set
val reader: PartReader
init {
if (data.def[0].asString == "image") {
if (data.def[1].isJsonPrimitive) {
reader = ImagePartReader(this, ImmutableList.of(Image.get(AssetPathStack.remap(data.def[1].asString)) ?: throw FileNotFoundException("Unable to locate image file ${data.def[1].asString} (${AssetPathStack.remap(data.def[1].asString)}) for dungeon part $name!")))
} else {
// assume array of images
reader = ImagePartReader(
this,
data.def[1].asJsonArray
.stream()
.map { it.asString }
.map { Image.get(AssetPathStack.remap(it)) ?: throw FileNotFoundException("Unable to locate image file $it (${AssetPathStack.remap(it)}) for dungeon part $name!") }
.collect(ImmutableList.toImmutableList())
)
}
} else if (data.def[0].asString == "tmx") {
if (data.def[1].isJsonPrimitive) {
reader = TiledPartReader(this, Stream.of(data.def[1].asString))
} else {
reader = TiledPartReader(this, data.def[1].asJsonArray.stream().map { it.asString })
}
} else {
throw IllegalArgumentException("Unknown part type ${data.def[0].asString}!")
}
}
fun bind(def: DungeonDefinition) {
dungeon = def
reader.bind(def)
val connectors = ArrayList<JigsawConnector>()
var cx = 0
var cy = 0
var cc = 0
var lowestAir = reader.size.y
var highestGround = -1
var highestLiquid = -1
reader.iterateTiles { x, y, tile ->
if (tile.connector != null) {
var direction = tile.connector.direction
if (direction == DungeonDirection.UNKNOWN)
direction = pickByNeighbours(x, y)
if (direction == DungeonDirection.UNKNOWN)
direction = pickByEdge(x, y)
connectors.add(JigsawConnector(this, tile.connector.index, tile.connector.forward, direction, Vector2i(x, y)))
}
if (tile.collidesWithPlaces) {
cx += x
cy += y
cc++
}
if (tile.requiresOpen && y < lowestAir) {
lowestAir = y
}
if (tile.requiresSolid && y > highestGround) {
highestGround = y
}
if (tile.requiresLiquid && y > highestLiquid) {
highestLiquid = y
}
}
this.connectors = ImmutableList.copyOf(connectors)
highestGround = highestGround.coerceAtLeast(highestLiquid)
if (highestGround == -1)
highestGround = lowestAir - 1
if (lowestAir == reader.size.y)
lowestAir = highestGround + 1
if (cc == 0) {
cx = reader.size.x / 2
cy = reader.size.y / 2
} else {
cx /= cc
cy /= cc
}
if (highestGround != -1) {
cy = highestGround + 1
}
anchor = Vector2i(cx, cy)
}
fun doesNotConnectTo(other: DungeonPart): Boolean {
return rules.any { it.doesNotConnectToPart(other.name) } || other.rules.any { it.doesNotConnectToPart(name) }
}
fun checkPartCombinationsAllowed(counter: Map<String, Int>): Boolean {
return rules.all { it.checkPartCombinationsAllowed(counter) }
}
fun allowsPlacement(spawnCount: Int): Boolean {
return rules.all { it.allowSpawnCount(spawnCount) }
}
suspend fun canPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
if (overrideAllowAlways || reader.size.x == 0 || reader.size.y == 0)
return true
return world.waitForRegionAndJoin(Vector2i(x, y), reader.size) {
reader.walkTiles<Boolean> { tx, ty, tile ->
if (!tile.canPlace(x + tx, y + ty, world)) {
return@walkTiles KOptional(false)
}
return@walkTiles KOptional()
}
}.orElse(true)
}
fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean {
if (overrideAllowAlways || reader.size.x == 0 || reader.size.y == 0)
return true
return reader.walkTiles<Boolean> { tx, ty, tile ->
if (!tile.canPlace(x + tx, y + ty, world)) {
return@walkTiles KOptional(false)
}
return@walkTiles KOptional()
}.orElse(true)
}
suspend fun place(pos: Vector2i, world: DungeonWorld, dungeonID: Int) {
val (x, y) = pos
val markForRemoval = HashSet<Vector2i>()
reader.iterateTiles { tx, ty, tile ->
if (tile.hasBrushes) {
markForRemoval.add(world.geometry.wrap(Vector2i(x + tx, y + ty)))
}
}
// Mark entities for removal, and remove them when dungeon is actually placed in world
world.waitForRegionAndJoin(Vector2i(x, y), reader.size) {
val entities = world.parent.entityIndex.query(AABBi(Vector2i(x, y), Vector2i(x, y) + reader.size))
for (entity in entities) {
if (entity !is TileEntity)
continue
var markForDeath = false
if (entity.occupySpaces.any { it in markForRemoval }) {
markForDeath = true
} else if (clearAnchoredObjects) {
if (entity.roots.any { it in markForRemoval }) {
markForDeath = true
} else if (entity is WorldObject && entity.anchorPositions.any { it in markForRemoval }) {
markForDeath = true
}
}
if (markForDeath) {
// mark all spaces for removal so "is tile empty" placement rules properly account
// for marked-for-death tiles occupied by existing entities
entity.occupySpaces.forEach { world.clearTileEntityAt(it) }
world.clearTileEntity(entity)
}
}
}
if (markDungeonId) {
world.setDungeonID(dungeonID)
} else {
world.setDungeonID()
}
for (phase in DungeonBrush.Phase.entries) {
reader.iterateTiles { tx, ty, tile ->
if (tile.usesPlaces || !world.hasPlacement(x + tx, y + ty)) {
tile.place(x + tx, y + ty, phase, world)
}
}
}
reader.iterateTiles { tx, ty, tile ->
if (tile.usesPlaces) {
world.touchPlacement(x + tx, y + ty)
}
if (tile.hasBrushes) {
world.touch(x + tx, y + ty)
}
}
world.finishPart()
}
fun tileUsesPlaces(x: Int, y: Int): Boolean {
return reader.walkTilesAt(x, y) { x, y, tile -> if (tile.usesPlaces) KOptional(true) else KOptional() }.orElse(false)
}
fun collidesWithPlaces(x: Int, y: Int, world: DungeonWorld): Boolean {
return reader.walkTiles { tx, ty, tile -> if (tile.collidesWithPlaces && world.hasPlacement(tx + x, ty + y)) KOptional(true) else KOptional() }.orElse(false)
}
fun pickByNeighbours(x: Int, y: Int): DungeonDirection {
// if on a border use that, corners use the left/right direction
if (x == 0)
return DungeonDirection.LEFT
else if (x == reader.size.x - 1)
return DungeonDirection.RIGHT
else if (y == 0)
return DungeonDirection.DOWN
else if (y == reader.size.y - 1)
return DungeonDirection.UP
// scans around the connector, the direction where it finds a solid is where
// it assume the connection comes from
if (tileUsesPlaces(x + 1, y) && !tileUsesPlaces(x - 1, y))
return DungeonDirection.LEFT
if (tileUsesPlaces(x - 1, y) && !tileUsesPlaces(x + 1, y))
return DungeonDirection.RIGHT
if (tileUsesPlaces(x, y + 1) && !tileUsesPlaces(x, y - 1))
return DungeonDirection.DOWN
if (tileUsesPlaces(x, y - 1) && !tileUsesPlaces(x, y + 1))
return DungeonDirection.UP
return DungeonDirection.UNKNOWN
}
fun pickByEdge(x: Int, y: Int): DungeonDirection {
val dxb = reader.size.x - x
val dyb = reader.size.y - y
return when (x.coerceAtMost(dxb).coerceAtMost(y).coerceAtMost(dyb)) {
x -> DungeonDirection.LEFT
dxb -> DungeonDirection.RIGHT
y -> DungeonDirection.DOWN
dyb -> DungeonDirection.UP
else -> throw RuntimeException()
}
}
class Adapter(gson: Gson) : TypeAdapter<DungeonPart>() {
private val data = gson.getAdapter(JsonData::class.java)
override fun write(out: JsonWriter, value: DungeonPart) {
TODO("Not yet implemented")
}
override fun read(`in`: JsonReader): DungeonPart {
return DungeonPart(data.read(`in`))
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -0,0 +1,357 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isObjectTile
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.server.world.ServerWorld
@JsonAdapter(DungeonRule.Adapter::class)
abstract class DungeonRule {
enum class Type(override val jsonName: String, val factory: (JsonArray) -> DungeonRule) : IStringSerializable {
NOOP("", { Noop }) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
return null
}
},
MUST_CONTAIN_LIQUID("worldGenMustContainLiquid", { MustContainLiquid }) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
if ("worldGenMustContainLiquid" in json)
return MustContainLiquid
return null
}
},
MUST_NOT_CONTAIN_LIQUID("worldGenMustNotContainLiquid", { MustNotContainLiquid }) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
if ("worldGenMustNotContainLiquid" in json)
return MustNotContainLiquid
return null
}
},
HAVE_SOLID_FOREGROUND("worldGenMustContainSolidForeground", { HaveSolidForeground }) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
if (json["layer"].asString == "front" && "worldGenMustContainSolid" in json)
return HaveSolidForeground
return null
}
},
HAVE_EMPTY_FOREGROUND("worldGenMustContainAirForeground", { HaveEmptyForeground }) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
if (json["layer"].asString == "front" && "worldGenMustContainAir" in json)
return HaveEmptyForeground
return null
}
},
HAVE_SOLID_BACKGROUND("worldGenMustContainSolidBackground", { HaveSolidBackground }) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
if (json["layer"].asString == "back" && "worldGenMustContainSolid" in json)
return HaveSolidBackground
return null
}
},
HAVE_EMPTY_BACKGROUND("worldGenMustContainAirBackground", { HaveEmptyBackground }) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
if (json["layer"].asString == "back" && "worldGenMustContainAir" in json)
return HaveEmptyBackground
return null
}
},
ALLOW_OVERDRAWING("allowOverdrawing", { AllowOverdrawing }) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
if ("allowOverdrawing" in json)
return AllowOverdrawing
return null
}
},
IGNORE_PART_MAXIMUM("ignorePartMaximumRule", { IgnorePartMaximum }) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
return null
}
},
MAX_SPAWN_COUNT("maxSpawnCount", ::MaxSpawnCount) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
return null
}
},
DO_NOT_CONNECT_TO_PART("doNotConnectToPart", ::DoNotConnectToPart) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
return null
}
},
DO_NOT_COMBINE_WITH("doNotCombineWith", ::DoNotCombineWith) {
override fun readTiled(json: JsonObject, flipX: Boolean, flipY: Boolean): DungeonRule? {
return null
}
};
abstract fun readTiled(json: JsonObject, flipX: Boolean = false, flipY: Boolean = false): DungeonRule?
}
open val requiresSolid: Boolean
get() = false
open val requiresLiquid: Boolean
get() = false
open val requiresOpen: Boolean
get() = false
open val allowOverdrawing: Boolean
get() = false
open val ignorePartMaximum: Boolean
get() = false
open fun doesNotConnectToPart(name: String): Boolean {
return false
}
open fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
return true
}
open fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
return true
}
open fun checkPartCombinationsAllowed(placements: Map<String, Int>): Boolean {
return true
}
open fun allowSpawnCount(currentCount: Int): Boolean {
return true
}
object Noop : DungeonRule()
object MustContainLiquid : DungeonRule() {
override val requiresLiquid: Boolean
get() = true
override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
val cell = world.parent.template.cellInfo(x, y)
return cell.oceanLiquid.isNotEmptyLiquid && cell.oceanLiquidLevel > y
}
override fun toString(): String {
return "Must contain liquid"
}
}
object MustNotContainLiquid : DungeonRule() {
override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
val cell = world.parent.template.cellInfo(x, y)
return cell.oceanLiquid.isEmptyLiquid || cell.oceanLiquidLevel <= y
}
override fun toString(): String {
return "Must not contain liquid"
}
}
object HaveSolidForeground : DungeonRule() {
override val requiresSolid: Boolean
get() = true
override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
if (world.markSurfaceLevel != null)
return y < world.markSurfaceLevel
val cell = world.parent.chunkMap.getCell(x, y)
if (cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y))
return false
return cell.foreground.material.isNotEmptyTile && !world.isClearingTileEntityAt(x, y)
}
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y)
return cell.foreground.material.isNotEmptyTile
}
override fun toString(): String {
return "Solid foreground"
}
}
object HaveEmptyForeground : DungeonRule() {
override val requiresOpen: Boolean
get() = true
override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
if (world.markSurfaceLevel != null)
return y >= world.markSurfaceLevel
val cell = world.parent.chunkMap.getCell(x, y)
return cell.foreground.material.isEmptyTile || cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y)
}
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y)
return cell.foreground.material.isEmptyTile
}
override fun toString(): String {
return "Empty foreground"
}
}
object HaveSolidBackground : DungeonRule() {
override val requiresSolid: Boolean
get() = true
override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
if (world.markSurfaceLevel != null)
return y < world.markSurfaceLevel
val cell = world.parent.chunkMap.getCell(x, y)
if (cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y))
return false
return cell.background.material.isNotEmptyTile
}
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y)
return cell.background.material.isNotEmptyTile
}
override fun toString(): String {
return "Solid background"
}
}
object HaveEmptyBackground : DungeonRule() {
override val requiresOpen: Boolean
get() = true
override fun checkTileCanPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
if (world.markSurfaceLevel != null)
return y >= world.markSurfaceLevel
val cell = world.parent.chunkMap.getCell(x, y)
return cell.background.material.isEmptyTile || cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y)
}
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y)
return cell.background.material.isEmptyTile
}
override fun toString(): String {
return "Empty background"
}
}
object AllowOverdrawing : DungeonRule() {
override val allowOverdrawing: Boolean
get() = true
override fun toString(): String {
return "Allow overdrawing"
}
}
object IgnorePartMaximum : DungeonRule() {
override val ignorePartMaximum: Boolean
get() = true
override fun toString(): String {
return "Ignore part maximum"
}
}
data class MaxSpawnCount(val count: Int) : DungeonRule() {
constructor(json: JsonArray) : this(json[1].asJsonArray[0].asInt)
override fun allowSpawnCount(currentCount: Int): Boolean {
return currentCount < count
}
override fun toString(): String {
return "Max spawn count = $count"
}
}
data class DoNotConnectToPart(val parts: ImmutableSet<String>) : DungeonRule() {
constructor(json: JsonArray) : this(json[1].asJsonArray.stream().map { it.asString }.collect(ImmutableSet.toImmutableSet()))
override fun doesNotConnectToPart(name: String): Boolean {
return name in parts
}
override fun toString(): String {
return "Do not connect to $parts"
}
}
data class DoNotCombineWith(val parts: ImmutableSet<String>) : DungeonRule() {
constructor(json: JsonArray) : this(json[1].asJsonArray.stream().map { it.asString }.collect(ImmutableSet.toImmutableSet()))
override fun checkPartCombinationsAllowed(placements: Map<String, Int>): Boolean {
return placements.keys.none { it in parts }
}
override fun toString(): String {
return "Do not combine with $parts"
}
}
class Adapter(gson: Gson) : TypeAdapter<DungeonRule>() {
private val arrays = gson.getAdapter(JsonArray::class.java)
private val types = gson.getAdapter(Type::class.java)
override fun write(out: JsonWriter, value: DungeonRule) {
throw UnsupportedOperationException("Dungeon Rules can't be serialized at this moment")
}
override fun read(`in`: JsonReader): DungeonRule {
val read = arrays.read(`in`)
if (read.isEmpty) {
throw JsonSyntaxException("Empty rule")
}
val type = types.fromJsonTree(read[0])
return type.factory(read)
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -0,0 +1,150 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.github.benmanes.caffeine.cache.Interner
import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector4i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.server.world.ServerWorld
@JsonAdapter(DungeonTile.Adapter::class)
data class DungeonTile(
val brushes: ImmutableList<DungeonBrush>,
val rules: ImmutableList<DungeonRule>,
val index: Int,
val connector: JigsawConnector?,
) {
data class JigsawConnector(
val index: String,
val forward: Boolean,
val direction: DungeonDirection
)
@JsonFactory
data class BasicData(
val brush: ImmutableList<DungeonBrush> = ImmutableList.of(),
val rules: ImmutableList<DungeonRule> = ImmutableList.of(),
val direction: DungeonDirection = DungeonDirection.UNKNOWN,
// original engine supports specifying pixels as strings
// but I don't see it being used anywhere in original game assets
// /shrug lets support anyway
val value: Either<Vector4i, String>,
val connector: Boolean = false,
val connectForwardOnly: Boolean = false,
)
// whenever this tile CAN BE overdrawn by other tile
val allowOverdrawing: Boolean = rules.any { it.allowOverdrawing }
// "modifyPlaces" in original code
val hasBrushes: Boolean get() = brushes.isNotEmpty()
val hasRules: Boolean get() = rules.isNotEmpty()
val usesPlaces: Boolean get() = hasBrushes && !allowOverdrawing
val collidesWithPlaces: Boolean get() = usesPlaces
val requiresOpen: Boolean get() = rules.any { it.requiresOpen }
val requiresLiquid: Boolean get() = rules.any { it.requiresLiquid }
val requiresSolid: Boolean get() = rules.any { it.requiresSolid }
// empty tiles can be cut off from canPlace and place loops
// (since they don't do anything other than padding region coordinates)
val isEmpty: Boolean get() = !hasBrushes && !hasRules
// sucks that we have to join world thread when doing this check
// this hinders parallelized computations by noticeable margin,
// and also makes dungeon placement in world completely serial
// (one dungeon can be in process of generating at time)
// TODO: find a way around this, to make dungeons less restricted by this
// but thats also not a priority, since this check happens quite quickly
// to have any noticeable impact on world's performance
fun canPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
val cell = world.parent.chunkMap.getCell(x, y)
if (cell.dungeonId != NO_DUNGEON_ID)
return false
if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y))
return false
return rules.none { !it.checkTileCanPlace(x, y, world) }
}
fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y)
if (cell.dungeonId != NO_DUNGEON_ID)
return false
if (!world.geometry.x.isValidCellIndex(x) || !world.geometry.y.isValidCellIndex(y))
return false
return rules.none { !it.checkTileCanPlace(x, y, world) }
}
fun place(x: Int, y: Int, phase: DungeonBrush.Phase, world: DungeonWorld) {
brushes.forEach { it.execute(x, y, phase, world) }
}
// weird custom parsing rules but ok
class Adapter(gson: Gson) : TypeAdapter<DungeonTile>() {
private val objects = gson.getAdapter(JsonObject::class.java)
private val data = gson.getAdapter(BasicData::class.java)
private val values = gson.getAdapter<Either<Vector4i, String>>()
private fun parseIndex(value: String): Int {
val split = value.split(',')
require(split.size == 4) { "Invalid color string: $value" }
var index = 0
for (v in split.map { it.toInt() }.asReversed())
index = index.shl(8) or v.and(0xFF)
return index
}
override fun write(out: JsonWriter, value: DungeonTile) {
TODO("Not yet implemented")
}
override fun read(`in`: JsonReader): DungeonTile {
val read = objects.read(`in`)
val (brushes, rules, direction, rawIndex, connector, connectForwardOnly) = data.fromJsonTree(read)
var connectorIndex = rawIndex
if ("connector-value" in read) {
connectorIndex = values.fromJsonTree(read["connector-value"])
}
val index = rawIndex.map({ it.w.and(0xFF).shl(24) or it.z.and(0xFF).shl(16) or it.y.and(0xFF).shl(8) or it.x.and(0xFF) }, { parseIndex(it) })
if (connector) {
return INTERNER.intern(DungeonTile(brushes, rules, index, JigsawConnector(
index = connectorIndex.map({ it.w.and(0xFF).shl(24) or it.z.and(0xFF).shl(16) or it.y.and(0xFF).shl(8) or it.x.and(0xFF) }, { parseIndex(it) }).toString(),
forward = connectForwardOnly,
direction = direction
)))
} else {
return INTERNER.intern(DungeonTile(brushes, rules, index, null))
}
}
}
companion object {
val INTERNER: Interner<DungeonTile> = if (Starbound.DEDUP_CELL_STATES) Starbound.interner(5) else Interner { it }
val EMPTY: DungeonTile = INTERNER.intern(DungeonTile(ImmutableList.of(), ImmutableList.of(), 0, null))
val CLEAR: DungeonTile = INTERNER.intern(DungeonTile(ImmutableList.of(DungeonBrush.Clear), ImmutableList.of(), 0, null))
}
}

View File

@ -0,0 +1,485 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import kotlinx.coroutines.future.await
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.api.MutableTileState
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.Collections
import java.util.function.Consumer
import java.util.random.RandomGenerator
// Facade world for generating dungeons, so generation can be performed without affecting world state,
// and if error occurs, won't require world's rollback, as well allowing dungeon to be generated mostly
// off world's thread.
class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val markSurfaceLevel: Int? = null, val terrainSurfaceSpaceExtends: Int = 0) {
val geometry = parent.geometry
data class Material(
val material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.EMPTY,
val hueShift: Float = 0f,
val color: TileColor = TileColor.DEFAULT,
) {
fun apply(to: MutableTileState) {
to.material = material
to.hueShift = hueShift
to.color = color
}
}
data class Modifier(
val modifier: Registry.Entry<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD,
val hueShift: Float = 0f
) {
fun apply(to: MutableTileState) {
to.modifier = modifier
to.modifierHueShift = hueShift
}
}
data class PlacedObject(
val prototype: Registry.Entry<ObjectDefinition>,
val direction: Direction = Direction.LEFT,
val parameters: JsonObject = JsonObject()
)
private val liquid = HashMap<Vector2i, AbstractLiquidState>()
private val foregroundMaterial = HashMap<Vector2i, Material>()
private val foregroundModifier = HashMap<Vector2i, Modifier>()
private val backgroundMaterial = HashMap<Vector2i, Material>()
private val backgroundModifier = HashMap<Vector2i, Modifier>()
// for entity spaces which should be considered empty if they
// are occupied by tile entity
private val clearTileEntitiesAt = HashSet<Vector2i>()
// entities themselves to be removed
private val tileEntitiesToRemove = HashSet<TileEntity>()
fun clearTileEntityAt(x: Int, y: Int) {
clearTileEntitiesAt.add(geometry.wrap(Vector2i(x, y)))
}
fun clearTileEntityAt(position: Vector2i) {
clearTileEntitiesAt.add(geometry.wrap(position))
}
fun isClearingTileEntityAt(x: Int, y: Int): Boolean {
return geometry.wrap(Vector2i(x, y)) in clearTileEntitiesAt
}
fun clearTileEntity(entity: TileEntity) {
tileEntitiesToRemove.add(entity)
}
private val touchedTiles = HashSet<Vector2i>()
private val protectTile = HashSet<Vector2i>()
private val boundingBoxes = ArrayList<AABBi>()
private var currentBoundingBox: AABBi? = null
fun touched(): Set<Vector2i> = Collections.unmodifiableSet(touchedTiles)
fun touch(x: Int, y: Int) {
val wrapped = geometry.wrap(Vector2i(x, y))
touchedTiles.add(wrapped)
if (currentBoundingBox == null) {
currentBoundingBox = AABBi(wrapped, wrapped)
} else {
currentBoundingBox = currentBoundingBox!!.expand(wrapped)
}
if (dungeonID != -1 && wrapped !in dungeonIDs) {
dungeonIDs[wrapped] = dungeonID
}
}
fun isTouched(x: Int, y: Int): Boolean {
return touchedTiles.contains(geometry.wrap(Vector2i(x, y)))
}
fun touchPlacement(x: Int, y: Int) {
protectTile.add(geometry.wrap(Vector2i(x, y)))
}
fun hasPlacement(x: Int, y: Int): Boolean {
return protectTile.contains(geometry.wrap(Vector2i(x, y)))
}
private val biomeItems = HashSet<Vector2i>()
private val biomeTrees = HashSet<Vector2i>()
private val itemDrops = HashMap<Vector2i, ArrayList<ItemDescriptor>>()
private val randomizedItemDrops = HashMap<Vector2i, ArrayList<ItemDescriptor>>()
private val dungeonIDs = HashMap<Vector2i, Int>()
private var dungeonID = -1
private val pendingLiquids = HashMap<Vector2i, AbstractLiquidState>()
private val openLocalWires = HashMap<String, HashSet<Vector2i>>()
private val globalWires = HashMap<String, HashSet<Vector2i>>()
private val localWires = ArrayList<HashSet<Vector2i>>()
private val placedObjects = HashMap<Vector2i, PlacedObject>()
var playerStart: Vector2d? = null
fun finishPart() {
localWires.addAll(openLocalWires.values)
openLocalWires.clear()
if (currentBoundingBox != null) {
boundingBoxes.add(currentBoundingBox!!)
currentBoundingBox = null
}
}
fun placeObject(x: Int, y: Int, prototype: Registry.Entry<ObjectDefinition>, direction: Direction = Direction.LEFT, parameters: JsonObject = JsonObject()) {
placedObjects[geometry.wrap(Vector2i(x, y))] = PlacedObject(prototype, direction, parameters)
}
fun placeWiring(x: Int, y: Int, group: String, partLocal: Boolean) {
val table = if (partLocal) openLocalWires else globalWires
table.computeIfAbsent(group) { HashSet() }.add(geometry.wrap(Vector2i(x, y)))
}
fun requestLiquid(x: Int, y: Int, liquid: AbstractLiquidState) {
pendingLiquids[geometry.wrap(Vector2i(x, y))] = liquid
}
fun setDungeonID(x: Int, y: Int, id: Int) {
dungeonIDs[geometry.wrap(Vector2i(x, y))] = id
}
fun setDungeonID(x: Int, y: Int) {
dungeonIDs.remove(geometry.wrap(Vector2i(x, y)))
}
fun setDungeonID(id: Int) {
require(id in 0 .. NO_DUNGEON_ID) { "Dungeon ID out of range: $id" }
dungeonID = id
}
fun setDungeonID() {
dungeonID = -1
}
fun placeBiomeTree(x: Int, y: Int) {
biomeTrees.add(geometry.wrap(Vector2i(x, y)))
}
fun placeBiomeItems(x: Int, y: Int) {
biomeItems.add(geometry.wrap(Vector2i(x, y)))
}
fun dropItem(x: Int, y: Int, item: ItemDescriptor) {
itemDrops.computeIfAbsent(geometry.wrap(Vector2i(x, y))) { ArrayList() }.add(item)
}
fun dropRandomizedItem(x: Int, y: Int, item: ItemDescriptor) {
randomizedItemDrops.computeIfAbsent(geometry.wrap(Vector2i(x, y))) { ArrayList() }.add(item)
}
fun setLiquid(x: Int, y: Int, liquid: AbstractLiquidState) {
this.liquid[geometry.wrap(Vector2i(x, y))] = liquid
}
fun getLiquid(x: Int, y: Int): AbstractLiquidState {
return this.liquid[geometry.wrap(Vector2i(x, y))] ?: AbstractLiquidState.EMPTY
}
fun setForeground(x: Int, y: Int, tile: Material) {
this.foregroundMaterial[geometry.wrap(Vector2i(x, y))] = tile
}
fun setForeground(x: Int, y: Int, tile: Modifier) {
this.foregroundModifier[geometry.wrap(Vector2i(x, y))] = tile
}
fun setForeground(x: Int, y: Int, material: Registry.Entry<TileDefinition>, hueShift: Float = 0f, color: TileColor = TileColor.DEFAULT) {
setForeground(x, y, Material(material, hueShift, color))
}
fun setForeground(x: Int, y: Int, modifier: Registry.Entry<TileModifierDefinition>, hueShift: Float = 0f) {
setForeground(x, y, Modifier(modifier, hueShift))
}
fun setBackground(x: Int, y: Int, tile: Material) {
this.backgroundMaterial[geometry.wrap(Vector2i(x, y))] = tile
}
fun setBackground(x: Int, y: Int, tile: Modifier) {
this.backgroundModifier[geometry.wrap(Vector2i(x, y))] = tile
}
fun setBackground(x: Int, y: Int, material: Registry.Entry<TileDefinition>, hueShift: Float = 0f, color: TileColor = TileColor.DEFAULT) {
setBackground(x, y, Material(material, hueShift, color))
}
fun setBackground(x: Int, y: Int, modifier: Registry.Entry<TileModifierDefinition>, hueShift: Float = 0f) {
setBackground(x, y, Modifier(modifier, hueShift))
}
fun needsForegroundBiomeMod(x: Int, y: Int): Boolean {
val pos = geometry.wrap(Vector2i(x, y))
val material = foregroundMaterial[pos] ?: return false
if (material.material !in BuiltinMetaMaterials.BIOME_META_MATERIALS)
return false
val above = geometry.wrap(Vector2i(x, y + 1))
return foregroundMaterial[above]?.material?.isNotEmptyTile == false
}
fun needsBackgroundBiomeMod(x: Int, y: Int): Boolean {
val pos = geometry.wrap(Vector2i(x, y))
val material = backgroundMaterial[pos] ?: return false
if (material.material !in BuiltinMetaMaterials.BIOME_META_MATERIALS)
return false
val above = geometry.wrap(Vector2i(x, y + 1))
return backgroundMaterial[above]?.material?.isNotEmptyTile == false
}
suspend inline fun <T> waitForRegion(region: AABBi, block: () -> T): T {
val tickets = ArrayList<ServerChunk.ITicket>()
return try {
tickets.addAll(parent.permanentChunkTicket(region, ServerChunk.State.TERRAIN))
tickets.forEach { it.chunk.await() }
block()
} finally {
tickets.forEach { it.cancel() }
}
}
suspend inline fun <T> waitForRegionAndJoin(region: AABBi, crossinline block: () -> T): T {
return waitForRegion(region) {
parent.eventLoop.supplyAsync { block() }.await()
}
}
suspend inline fun <T> waitForRegion(position: Vector2i, size: Vector2i, block: () -> T): T {
return waitForRegion(AABBi(position, position + size), block)
}
suspend inline fun <T> waitForRegionAndJoin(position: Vector2i, size: Vector2i, crossinline block: () -> T): T {
return waitForRegionAndJoin(AABBi(position, position + size), block)
}
fun pressurizeLiquids() {
// For each liquid type, find each contiguous region of liquid, then
// pressurize that region based on the highest position in the region
val unpressurizedLiquids = HashMap<Registry.Entry<LiquidDefinition>, HashSet<Vector2i>>()
for ((pos, liquid) in pendingLiquids) {
unpressurizedLiquids.computeIfAbsent(liquid.state) { HashSet() }.add(pos)
}
for (unpressurized in unpressurizedLiquids.values) {
while (unpressurized.isNotEmpty()) {
// Start with the first unpressurized block as the open set.
val firstBlock = unpressurized.first()
unpressurized.remove(firstBlock)
var openSet = ArrayList<Vector2i>()
val contiguousRegion = HashSet<Vector2i>()
openSet.add(firstBlock)
contiguousRegion.add(firstBlock)
// For each element in the previous open set, add all connected blocks
// in
// the unpressurized set to the new open set and to the total contiguous
// region, taking them from the unpressurized set.
while (openSet.isNotEmpty()) {
val oldOpenSet = openSet
openSet = ArrayList()
for (node in oldOpenSet) {
for (dir in offsets) {
val pos = node + dir
if (unpressurized.remove(pos)) {
contiguousRegion.add(pos)
openSet.add(pos)
}
}
}
}
// Once we have found no more blocks in the unpressurized set to add to
// the open set, then we have taken a contiguous region out of the
// unpressurized set. Pressurize it based on the highest point.
val highestPoint = contiguousRegion.maxOf { it.y }
contiguousRegion.forEach {
val state = pendingLiquids[it]!!.mutable()
state.pressure = 1f + highestPoint - it.y
pendingLiquids[it] = state.immutable()
}
}
}
for ((pos, liquid) in pendingLiquids.entries) {
setLiquid(pos.x, pos.y, liquid)
}
pendingLiquids.clear()
}
private fun applyCellChangesAt(pos: Vector2i, chunk: ServerChunk) {
val cell = chunk.getCell(pos - chunk.pos.tile).mutable()
backgroundMaterial[pos]?.apply(cell.background)
foregroundMaterial[pos]?.apply(cell.foreground)
backgroundModifier[pos]?.apply(cell.background)
foregroundModifier[pos]?.apply(cell.foreground)
val dungeonId = dungeonIDs[pos]
val liquid = liquid[pos]
if (liquid != null) {
cell.liquid.from(liquid)
}
if (dungeonId != null) {
cell.dungeonId = dungeonId
}
chunk.replaceBiomeBlocks(cell, parent.template.cellInfo(pos.x, pos.y))
chunk.setCell(pos - chunk.pos.tile, cell)
}
suspend fun commit() {
val tickets = ArrayList<ServerChunk.ITicket>()
try {
val terrainBlendingVertexes = ArrayList<Vector2d>()
val spaceBlendingVertexes = ArrayList<Vector2d>()
for (box in boundingBoxes) {
// don't schedule generating terrain until we have specified custom terrain regions! (they affect terrain generation)
// tickets.addAll(parent.permanentChunkTicket(box, ServerChunk.State.TERRAIN))
if (markSurfaceLevel != null) {
// Mark the regions of the dungeon above the dungeon surface as needing
// space, and the regions below the surface as needing terrain
if (box.mins.y < markSurfaceLevel) {
val mins = box.mins
val maxs = box.maxs.copy(y = box.maxs.y.coerceAtMost(markSurfaceLevel))
terrainBlendingVertexes.add(Vector2d(mins.x.toDouble(), mins.y.toDouble()))
terrainBlendingVertexes.add(Vector2d(maxs.x.toDouble(), mins.y.toDouble()))
terrainBlendingVertexes.add(Vector2d(maxs.x.toDouble(), maxs.y.toDouble()))
terrainBlendingVertexes.add(Vector2d(mins.x.toDouble(), maxs.y.toDouble()))
}
if (box.maxs.y > markSurfaceLevel) {
val mins = box.mins.copy(y = box.mins.y.coerceAtLeast(markSurfaceLevel))
val maxs = box.maxs
spaceBlendingVertexes.add(Vector2d(mins.x.toDouble(), mins.y.toDouble()))
spaceBlendingVertexes.add(Vector2d(maxs.x.toDouble(), mins.y.toDouble()))
spaceBlendingVertexes.add(Vector2d(maxs.x.toDouble(), maxs.y.toDouble() + terrainSurfaceSpaceExtends))
spaceBlendingVertexes.add(Vector2d(mins.x.toDouble(), maxs.y.toDouble() + terrainSurfaceSpaceExtends))
}
}
}
parent.eventLoop.supplyAsync {
if (terrainBlendingVertexes.isNotEmpty()) {
parent.template.addCustomTerrainRegion(Poly.quickhull(terrainBlendingVertexes))
}
if (spaceBlendingVertexes.isNotEmpty()) {
parent.template.addCustomSpaceRegion(Poly.quickhull(spaceBlendingVertexes))
}
}.await()
for (box in boundingBoxes) {
tickets.addAll(parent.permanentChunkTicket(box, ServerChunk.State.TERRAIN))
}
// apply tiles to world per-chunk
// this way we don't need to wait on all chunks to be loaded
// and apply changes chunks which have been loaded right away
val tilePositionsRaw = ArrayList<Vector2i>()
tilePositionsRaw.addAll(foregroundMaterial.keys)
tilePositionsRaw.addAll(foregroundModifier.keys)
tilePositionsRaw.addAll(backgroundMaterial.keys)
tilePositionsRaw.addAll(backgroundModifier.keys)
tilePositionsRaw.sortWith { o1, o2 ->
val cmp = o1.x.compareTo(o2.x)
if (cmp == 0) o1.y.compareTo(o2.y) else cmp
}
val regions = Long2ObjectOpenHashMap<ArrayList<Vector2i>>()
var previous: Vector2i? = null
for (pos in tilePositionsRaw) {
if (pos != previous) {
regions.computeIfAbsent(ChunkPos.toLong(geometry.x.chunkFromCell(pos.x), geometry.y.chunkFromCell(pos.y)), Long2ObjectFunction { ArrayList() }).add(pos)
previous = pos
}
}
val seenTickets = HashSet<ChunkPos>()
for (ticket in tickets.filter { seenTickets.add(it.pos) }) {
// make changes to chunk only inside world's thread once it has reached TILES state
ticket.chunk.thenAcceptAsync(Consumer {
regions.get(ticket.pos.toLong())?.forEach { applyCellChangesAt(it, ticket.chunk.get()) }
}, parent.eventLoop)
}
// wait for all chunks to be loaded
tickets.forEach { it.chunk.await() }
// at this point all chunks are available, and we applied changes to tiles
// and finally, schedule chunks to be loaded into FULL state
// this way, big dungeons won't get cut off when chunks being saved
// to disk because of multiple chunks outside player tracking area
// But this might trigger cascading world generation
// (big dungeon generates another big dungeon, and another, and so on),
// tough, so need to take care!
for (box in boundingBoxes) {
// specify timer as 0 so ticket gets removed on next world tick
parent.temporaryChunkTicket(box, 0, ServerChunk.State.FULL)
}
} finally {
tickets.forEach { it.cancel() }
}
}
companion object{
private val offsets = listOf(Vector2i.POSITIVE_Y, Vector2i.NEGATIVE_Y, Vector2i.POSITIVE_X, Vector2i.NEGATIVE_X)
}
}

View File

@ -0,0 +1,67 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.image.Image
class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : PartReader(part) {
override val size: Vector2i
get() = if (images.isEmpty()) Vector2i.ZERO else images.first().size
// it is much cheaper to just read all images and store 2D array
// of references than loading / keeping images themselves around
// `Image` class doesn't actually keep pixel data around for too long,
// if it doesn't get accessed in some time it gets purged from ram
private val layers = Array(images.size) {
Object2DArray.nulls<DungeonTile>(images[it].width, images[it].height)
} as Array<Object2DArray<DungeonTile>>
override fun bind(def: DungeonDefinition) {
check(def.tiles.isNotEmpty) { "Image parts require 'tiles' palette to be present in .dungeon definition" }
for ((i, image) in images.withIndex()) {
val layer = layers[i]
for (y in 0 until image.height) {
for (x in 0 until image.width) {
val color = image[x, y]
val tile = part.dungeon.tiles[color]
if (tile == null) {
val parse = RGBAColor.abgr(color)
throw IllegalStateException("Unknown tile on ${image.path} at $x, $y: [${parse.redInt}, ${parse.greenInt}, ${parse.blueInt}, ${parse.alphaInt}] (index $color)")
}
layer[x, y] = tile
}
}
}
}
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
for (layer in layers) {
for (y in 0 until layer.rows) {
for (x in 0 until layer.columns) {
val get = callback(x, y, layer[x, y])
if (get.isPresent) return get
}
}
}
return KOptional()
}
override fun <T> walkTilesAt(x: Int, y: Int, callback: TileCallback<T>): KOptional<T> {
for (layer in layers) {
if (x in 0 until layer.columns && y in 0 until layer.rows) {
val get = callback(x, y, layer[x, y])
if (get.isPresent) return get
}
}
return KOptional()
}
}

View File

@ -0,0 +1,64 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.json.getAdapter
// dungeons are stored as images, and each pixel
// represents a different tile. To make sense
// of pixel's color, this class is used to lookup
// what to do.
@JsonAdapter(ImageTileSet.Adapter::class)
class ImageTileSet(list: List<DungeonTile> = listOf()) {
private val mapping = Int2ObjectOpenHashMap<DungeonTile>()
init {
for ((i, it) in list.withIndex()) {
val replaced = mapping.put(it.index, it)
// allow duplicates of same entry because vanilla files have them.
if (replaced != null && replaced != it) {
val color = RGBAColor.abgr(it.index)
throw IllegalArgumentException("Two tiles are trying to take same place with index ${it.index} [${color.redInt}, ${color.greenInt}, ${color.blueInt}, ${color.alphaInt}] (list index $i):\ntile 1: $replaced\ntile 2: $it")
}
}
}
operator fun get(index: Int): DungeonTile? {
return mapping.get(index)
}
operator fun get(red: Int, green: Int, blue: Int, alpha: Int): DungeonTile? {
return mapping.get(red.and(0xFF).shl(24) or green.and(0xFF).shl(16) or blue.and(0xFF).shl(8) or alpha.and(0xFF))
}
operator fun get(index: RGBAColor): DungeonTile? {
return this[index.redInt, index.greenInt, index.blueInt, index.alphaInt]
}
val isNotEmpty: Boolean
get() = mapping.isNotEmpty()
class Adapter(gson: Gson) : TypeAdapter<ImageTileSet>() {
private val list = gson.getAdapter<ImmutableList<DungeonTile>>()
override fun write(out: JsonWriter, value: ImageTileSet) {
TODO("Not yet implemented")
}
override fun read(`in`: JsonReader): ImageTileSet {
return ImageTileSet(list.read(`in`))
}
}
companion object {
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -0,0 +1,9 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import ru.dbotthepony.kommons.vector.Vector2i
abstract class PartReader(val part: DungeonPart) : TileMap() {
abstract val size: Vector2i
abstract fun bind(def: DungeonDefinition)
}

View File

@ -0,0 +1,24 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import ru.dbotthepony.kommons.util.KOptional
abstract class TileMap {
fun interface TileCallback<T> {
operator fun invoke(x: Int, y: Int, tile: DungeonTile): KOptional<T>
}
fun interface TileCallback0 {
operator fun invoke(x: Int, y: Int, tile: DungeonTile)
}
abstract fun <T> walkTiles(callback: TileCallback<T>): KOptional<T>
abstract fun <T> walkTilesAt(x: Int, y: Int, callback: TileCallback<T>): KOptional<T>
fun iterateTiles(callback: TileCallback0) {
walkTiles<Unit> { x, y, tile -> callback(x, y, tile); KOptional() }
}
fun iterateTilesAt(x: Int, y: Int, callback: TileCallback0) {
walkTilesAt<Unit>(x, y) { x, y, tile -> callback(x, y, tile); KOptional() }
}
}

View File

@ -0,0 +1,449 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITi
import java.io.BufferedInputStream
import java.io.EOFException
import java.util.Base64
import java.util.zip.InflaterInputStream
import kotlin.math.roundToInt
class TiledMap(data: JsonData) : TileMap() {
@JsonFactory
data class JsonData(
val tileheight: Int,
val tilewidth: Int,
val width: Int,
val height: Int,
val tilesets: ImmutableList<TiledTileSets.Entry>,
val layers: ImmutableList<JsonObject>,
)
init {
require(data.tilewidth == 8) { "Tile width is not equal to 8 (${data.tilewidth})" }
require(data.tileheight == 8) { "Tile height is not equal to 8 (${data.tileheight})" }
require(data.width > 0) { "Non-positive map width: ${data.width}" }
require(data.height > 0) { "Non-positive map height: ${data.height}" }
}
val size = Vector2i(data.width, data.height)
val tileSets = TiledTileSets(data.tilesets)
var frontLayer: TileLayer? = null
private set
var backLayer: TileLayer? = null
private set
val objectLayers: ImmutableList<ObjectLayer>
init {
val objectLayers = ArrayList<ObjectLayer>()
for (layer in data.layers) {
val type = layer["type"].asString
if (type == "tilelayer") {
val read = Starbound.gson.fromJson(layer, TileLayerData::class.java)
when (read.name.lowercase()) {
"front" -> {
if (frontLayer != null) {
throw IllegalArgumentException("Duplicate front layer")
}
frontLayer = TileLayer(read, false)
}
"back" -> {
if (backLayer != null) {
throw IllegalArgumentException("Duplicate back layer")
}
backLayer = TileLayer(read, true)
}
else -> throw UnsupportedOperationException("Unknown tile layer '${read.name}'! Must be either 'front' or 'back'")
}
} else if (type == "objectgroup") {
objectLayers.add(ObjectLayer(Starbound.gson.fromJson(layer, ObjectLayerData::class.java)))
} else {
throw UnsupportedOperationException("Unknown layer type $type!")
}
}
this.objectLayers = ImmutableList.copyOf(objectLayers)
}
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
var result = frontLayer?.walkTiles(callback) ?: KOptional()
if (result.isPresent) return result
result = backLayer?.walkTiles(callback) ?: KOptional()
if (result.isPresent) return result
for (layer in objectLayers) {
result = layer.walkTiles(callback)
if (result.isPresent) return result
}
return KOptional()
}
override fun <T> walkTilesAt(x: Int, y: Int, callback: TileCallback<T>): KOptional<T> {
var result = frontLayer?.walkTilesAt(x, y, callback) ?: KOptional()
if (result.isPresent) return result
result = backLayer?.walkTilesAt(x, y, callback) ?: KOptional()
if (result.isPresent) return result
for (layer in objectLayers) {
result = layer.walkTilesAt(x, y, callback)
if (result.isPresent) return result
}
return KOptional()
}
@JsonFactory
data class TileLayerData(
val width: Int,
val height: Int,
val x: Int = 0,
val y: Int = 0,
val name: String,
val compression: String? = null,
val data: JsonElement
) {
init {
require(width > 0) { "Non-positive tile layer width: $width" }
require(height > 0) { "Non-positive tile layer height: $height" }
}
}
inner class TileLayer(data: TileLayerData, val isBackground: Boolean) : TileMap() {
val width: Int = data.width
val height: Int = data.height
val x: Int = data.x
val y: Int = data.y
// this eats ram, need to use cache or huffman encoding with bitset
private val tileData: IntArray
init {
if (data.compression == "zlib") {
val stream = BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(Base64.getDecoder().decode(data.data.asString))))
tileData = IntArray(data.width * data.height) {
val a = stream.read()
val b = stream.read()
val c = stream.read()
val d = stream.read()
if (a or b or c or d < 0)
throw EOFException("Reached end of stream before read all tiles from layer")
(a or b.shl(8) or c.shl(16) or d.shl(24)) and FLAG_BITS.inv()
}
} else if (data.compression == null) {
require(data.data.asJsonArray.size() == data.width * data.height) {
"'data' does not contain enough tiles to fill ${data.width} x ${data.height} array"
}
tileData = IntArray(data.width * data.height)
for ((i, index) in data.data.asJsonArray.withIndex()) {
tileData[i] = index.asInt and FLAG_BITS.inv()
}
} else {
throw IllegalArgumentException("Unsupported compression mode: ${data.compression}")
}
}
private fun get0(x: Int, y: Int): DungeonTile {
val actualX = x - this.x
var actualY = y - this.y
actualY = this.height - actualY - 1
if (isBackground)
return tileSets.getBack(tileData[actualX + actualY * width])
else
return tileSets.getFront(tileData[actualX + actualY * width])
}
operator fun get(x: Int, y: Int): DungeonTile {
if (x !in this.x until this.x + width || y !in this.y until this.y + height)
return DungeonTile.EMPTY
return get0(x, height - y + 1)
}
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
for (x in this.x until this.x + width) {
for (y in this.y until this.y + height) {
val result = callback(x, y, get0(x, y))
if (result.isPresent) return result
}
}
return KOptional()
}
override fun <T> walkTilesAt(x: Int, y: Int, callback: TileCallback<T>): KOptional<T> {
if (x !in this.x until this.x + width || y !in this.y until this.y + height)
return KOptional()
return callback(x, y, get0(x, y))
}
}
@JsonFactory
data class OProperty(val name: String, val value: JsonElement)
@JsonFactory
data class ObjectLayerData(
val name: String,
val properties: Either<JsonObject, ImmutableList<OProperty>> = Either.left(JsonObject()),
val objects: ImmutableList<ObjectData>,
)
inner class ObjectLayer(data: ObjectLayerData) : TileMap() {
private val objects: ImmutableList<MapObject>
init {
val properties = data.properties.map({ it }, { p -> JsonObject().also { for ((k, v) in p) it[k] = v } })
val objects = ArrayList<MapObject>()
for (o in data.objects) {
objects.add(MapObject(properties, o))
}
this.objects = ImmutableList.copyOf(objects)
}
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
for (o in objects) {
val result = o.walkTiles(callback)
if (result.isPresent) return result
}
return KOptional()
}
override fun <T> walkTilesAt(x: Int, y: Int, callback: TileCallback<T>): KOptional<T> {
for (o in objects) {
val result = o.walkTilesAt(x, y, callback)
if (result.isPresent) return result
}
return KOptional()
}
}
@JsonFactory
data class ObjectData(
val id: Int,
val gid: Int? = null,
val properties: Either<JsonObject, ImmutableList<OProperty>> = Either.left(JsonObject()),
val x: Int,
val y: Int,
val width: Int = 0,
val height: Int = 0,
val rotation: Double = 0.0,
val polyline: ImmutableList<PolylineEntry>? = null,
val ellipse: JsonElement = JsonNull.INSTANCE,
val polygon: JsonElement = JsonNull.INSTANCE,
)
@JsonFactory
data class PolylineEntry(val x: Int, val y: Int)
enum class ObjType {
STAGEHAND, TILE, WIRING, RECTANGLE;
}
inner class MapObject(layerProperties: JsonObject, data: ObjectData) : TileMap() {
val id = data.id
val type: ObjType
val polyline: ImmutableList<Vector2i>
val pos: Vector2i
val size: Vector2i
val tile: DungeonTile
init {
val properties = data.properties.map({ it }, { p -> JsonObject().also { for ((k, v) in p) it[k] = v } })
val merged0 = mergeJson(layerProperties.deepCopy(), properties)
val isBackground = if ("layer" in merged0) merged0["layer"].asString == "back" else false
val merged = layerProperties.deepCopy()
var flipX = false
if (data.gid != null) {
if (isBackground) {
mergeJson(merged, tileSets.getBackData(data.gid and FLAG_BITS.inv()))
} else {
mergeJson(merged, tileSets.getFrontData(data.gid and FLAG_BITS.inv()))
}
if (data.gid and (VERTICAL_FLIP or DIAGONAL_FLIP) != 0)
throw UnsupportedOperationException("Object with GID ${data.gid and FLAG_BITS.inv()} (id $id) has either vertical flip or diagonal flip set, which is not supported")
flipX = data.gid and HORIZONTAL_FLIP != 0
}
mergeJson(merged, properties)
if (isBackground)
merged["layer"] = "back"
else
merged["layer"] = "front"
if (flipX)
merged["flipX"] = "true"
if ("stagehand" in merged)
type = ObjType.STAGEHAND
else if (data.gid != null)
type = ObjType.TILE
else if (data.polyline != null)
type = ObjType.WIRING
else if (!data.ellipse.isJsonNull)
throw UnsupportedOperationException("Object has ellipse shape, which is not supported")
else if (!data.polygon.isJsonNull)
throw UnsupportedOperationException("Object has polygon shape, which is not supported")
else
type = ObjType.RECTANGLE
if (data.rotation != 0.0)
throw UnsupportedOperationException("Object has rotation: ${merged["rotation"].asDouble}, which is not supported")
if (data.polyline != null) {
merged["wire"] = "_polylineWire$id"
merged["local"] = true
this.polyline = ImmutableList.copyOf(data.polyline.map { Vector2i(it.x / PIXELS_IN_STARBOUND_UNITi, this@TiledMap.size.y - it.y / PIXELS_IN_STARBOUND_UNITi - 1) })
} else {
this.polyline = ImmutableList.of()
}
val calcPos = Vector2i(data.x, data.y) / PIXELS_IN_STARBOUND_UNITi + Vector2i(((merged["imagePositionX"]?.asDouble ?: 0.0) / PIXELS_IN_STARBOUND_UNIT).toInt(), -((merged["imagePositionY"]?.asDouble ?: 0.0) / PIXELS_IN_STARBOUND_UNIT).toInt())
pos = calcPos.copy(y = this@TiledMap.size.y - calcPos.y - 1)
size = Vector2i(data.width, data.height) / PIXELS_IN_STARBOUND_UNITi
if (type == ObjType.STAGEHAND) {
val center = pos + size / 2
// TODO: what?
val broadcastMins = pos - center
val broadcastMaxs = pos + size - center
merged["broadcastArea"] = jsonArrayOf(
JsonPrimitive(broadcastMins.x),
JsonPrimitive(broadcastMins.y),
JsonPrimitive(broadcastMaxs.x),
JsonPrimitive(broadcastMaxs.y))
}
tile = TiledTileSet.makeTile(merged)
}
private fun stagehandPosition() = Vector2i(pos.x + size.x / 2, pos.y + (size.y.toDouble() / 2.0).roundToInt())
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
return when (type) {
ObjType.STAGEHAND -> {
val (x, y) = stagehandPosition()
callback(x, y, tile)
}
ObjType.TILE -> {
// Used for placing Starbound-Tiles and Starbound-Objects
callback(pos.x, pos.y + 1, tile) // i don't know why it needs 1 tile offset though
}
ObjType.WIRING -> {
// Used for wiring. Treat each vertex in the polyline as a tile with the
// wire brush.
for ((x, y) in polyline) {
val result = callback(this.pos.x + x, this.pos.y + y, tile)
if (result.isPresent) return result
}
KOptional()
}
ObjType.RECTANGLE -> {
// Used for creating custom brushes and rules
for (x in this.pos.x until this.pos.x + this.size.x) {
for (y in this.pos.y until this.pos.y + this.size.y) {
val result = callback(x, y, tile)
if (result.isPresent) return result
}
}
KOptional()
}
}
}
override fun <T> walkTilesAt(x: Int, y: Int, callback: TileCallback<T>): KOptional<T> {
return when(type) {
ObjType.STAGEHAND -> {
val (sx, sy) = stagehandPosition()
if (sx == x && sy == y) return callback(x, y, tile)
KOptional()
}
ObjType.TILE -> {
if (x == pos.x && y == pos.y + 1) return callback(x, y, tile)
KOptional()
}
ObjType.WIRING -> {
for ((px, py) in polyline) {
val ox = this.pos.x + px
val oy = this.pos.y + py
if (ox == x && oy == y) {
val result = callback(x, y, tile)
if (result.isPresent) return result
}
}
KOptional()
}
ObjType.RECTANGLE -> {
if (x in this.pos.x until this.pos.x + this.size.x && y in this.pos.y until this.pos.y + this.size.y) {
return callback(x, y, tile)
}
KOptional()
}
}
}
}
companion object {
const val HORIZONTAL_FLIP = 1 shl 31
const val VERTICAL_FLIP = 1 shl 30
const val DIAGONAL_FLIP = 1 shl 29
const val FLAG_BITS = HORIZONTAL_FLIP or VERTICAL_FLIP or DIAGONAL_FLIP
}
}

View File

@ -0,0 +1,53 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import com.google.gson.JsonSyntaxException
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.AssetPathStack
import java.util.stream.Stream
class TiledPartReader(part: DungeonPart, parts: Stream<String>) : PartReader(part) {
val maps: ImmutableList<TiledMap> = parts
.map { Starbound.locate(AssetPathStack.remap(it)) }
.peek { check(it.exists) { "Dungeon references part which does not exist: $it" } }
// well, i don't think anyone gonna patch THESE json files, i guess
// so ignore json patches and just read directly
.map {
try {
TiledMap(Starbound.gson.fromJson(it.jsonReader(), TiledMap.JsonData::class.java))
} catch (err: Throwable) {
throw JsonSyntaxException("Exception while reading tile map $it", err)
}
}
.collect(ImmutableList.toImmutableList())
// also why would you ever want multiple maps specified lmao
// it already has layers and everything else you would ever need
override val size: Vector2i
get() = maps.firstOrNull()?.size ?: Vector2i.ZERO
override fun bind(def: DungeonDefinition) {
}
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
for (map in maps) {
val result = map.walkTiles(callback)
if (result.isPresent) return result
}
return KOptional()
}
override fun <T> walkTilesAt(x: Int, y: Int, callback: TileCallback<T>): KOptional<T> {
for (map in maps) {
val result = map.walkTilesAt(x, y, callback)
if (result.isPresent) return result
}
return KOptional()
}
}

View File

@ -0,0 +1,101 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.set
import java.util.concurrent.ConcurrentHashMap
class TiledTileSet private constructor(
val front: ImmutableMap<Int, Pair<DungeonTile, JsonObject>>,
val back: ImmutableMap<Int, Pair<DungeonTile, JsonObject>>,
) {
@JsonFactory
data class JsonData(
val properties: JsonObject = JsonObject(),
// val tilecount: Int, // we don't care
val tileproperties: JsonObject = JsonObject(), // empty tileset?
)
val size: Int
get() = front.size
val isEmpty: Boolean
get() = size == 0
val isNotEmpty: Boolean
get() = size != 0
companion object {
private val cache = ConcurrentHashMap<String, Either<TiledTileSet, Throwable>>()
fun makeTile(data: JsonObject): DungeonTile {
val brushes = DungeonBrushType.entries.mapNotNull { it.readTiled(data) }
val rules = DungeonRule.Type.entries.mapNotNull { it.readTiled(data) }
var connector: DungeonTile.JigsawConnector? = null
if ("connector" in data) {
val name = data["connector"].asString
val connectForwardOnly = "connectForwardOnly" in data
val connectDirection = DungeonDirection.entries.first { it.jsonName == data.get("connectDirection", "any") }
connector = DungeonTile.JigsawConnector(name, connectForwardOnly, connectDirection)
}
return DungeonTile.INTERNER.intern(DungeonTile(ImmutableList.copyOf(brushes), ImmutableList.copyOf(rules), 0, connector))
}
private fun load0(location: String): Either<TiledTileSet, Throwable> {
val locate = Starbound.loadJsonAsset(location)
?: return Either.right(NoSuchElementException("Tileset at $location does not exist"))
try {
val data = Starbound.gson.fromJson(locate, JsonData::class.java)
val front = ImmutableMap.Builder<Int, Pair<DungeonTile, JsonObject>>()
val back = ImmutableMap.Builder<Int, Pair<DungeonTile, JsonObject>>()
for ((key, value) in data.tileproperties.entrySet()) {
if (value !is JsonObject)
throw JsonSyntaxException("Tile at $key is invalid: $value")
val index = key.toIntOrNull() ?: throw JsonSyntaxException("Invalid tile ID in $location: $key")
val merge = mergeJson(data.properties.deepCopy(), value)
val mergeBack = merge.deepCopy()
if ("layer" !in merge) {
merge["layer"] = "front"
}
if ("layer" !in mergeBack) {
mergeBack["layer"] = "back"
}
if ("clear" !in merge) {
// The magic pink tile/brush has the clear property set to "false". All
// other tiles default to clear="true".
mergeBack["clear"] = "true"
}
front[index] = makeTile(merge) to merge
back[index] = makeTile(mergeBack) to mergeBack
}
return Either.left(TiledTileSet(front.build(), back.build()))
} catch (err: Throwable) {
return Either.right(err)
}
}
fun load(location: String): TiledTileSet {
return cache.computeIfAbsent(location, Companion::load0).map({ it }, { throw it })
}
}
}

View File

@ -0,0 +1,63 @@
package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.AssetPathStack
class TiledTileSets(entries: List<Entry>) {
@JsonFactory
data class Entry(
val firstgid: Int,
// not asset path because of funky names which can't be properly
// untangled by AssetPath's adapter code
val source: String,
)
private val front = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>()
private val back = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>()
init {
for ((firstgid, source) in entries) {
// Tiled stores tileset paths relative to the map file, which can go below
// the assets root if it's referencing a tileset in another asset package.
// The solution chosen here is to ignore everything in the path up until a
// known path segment, e.g.:
// "source" : "..\/..\/..\/..\/packed\/tilesets\/packed\/materials.json"
// We ignore everything up until the 'tilesets' path segment, and the asset
// we actually load is located at:
// /tilesets/packed/materials.json
val actualSource: String
val split = source.lowercase().lastIndexOf("/tilesets/")
if (split != -1) {
actualSource = source.substring(split)
} else {
actualSource = AssetPathStack.remap(source)
}
val set = TiledTileSet.load(actualSource)
for (i in 0 until set.size) {
front[firstgid + i] = set.front[i] ?: throw NullPointerException("aeiou")
back[firstgid + i] = set.back[i] ?: throw NullPointerException("aeiou")
}
}
}
fun getFront(gid: Int): DungeonTile {
return front[gid]?.first ?: DungeonTile.EMPTY
}
fun getBack(gid: Int): DungeonTile {
return back[gid]?.first ?: DungeonTile.CLEAR
}
fun getFrontData(gid: Int): JsonObject {
return front[gid]?.second ?: JsonObject()
}
fun getBackData(gid: Int): JsonObject {
return back[gid]?.second ?: JsonObject()
}
}

View File

@ -63,7 +63,7 @@ class Image private constructor(
private val spritesInternal = LinkedHashMap<String, Sprite>() private val spritesInternal = LinkedHashMap<String, Sprite>()
private var dataRef: WeakReference<ByteBuffer>? = null private var dataRef: WeakReference<ByteBuffer>? = null
private val lock = ReentrantLock() private val lock = Any()
//private val _texture = ThreadLocal<WeakReference<GLTexture2D>>() //private val _texture = ThreadLocal<WeakReference<GLTexture2D>>()
init { init {
@ -110,9 +110,7 @@ class Image private constructor(
if (get != null) if (get != null)
return CompletableFuture.completedFuture(get) return CompletableFuture.completedFuture(get)
lock.lock() synchronized(lock) {
try {
get = dataRef?.get() get = dataRef?.get()
if (get != null) if (get != null)
@ -124,8 +122,6 @@ class Image private constructor(
dataRef = WeakReference(f.get()) dataRef = WeakReference(f.get())
return f.copy() return f.copy()
} finally {
lock.unlock()
} }
} }
@ -156,7 +152,7 @@ class Image private constructor(
tex.textureMinFilter = GL45.GL_NEAREST tex.textureMinFilter = GL45.GL_NEAREST
tex.textureMagFilter = GL45.GL_NEAREST tex.textureMagFilter = GL45.GL_NEAREST
}, client.mailbox) }, client)
tex tex
} }
@ -172,10 +168,16 @@ class Image private constructor(
val whole = Sprite("this", 0, 0, width, height) val whole = Sprite("this", 0, 0, width, height)
val nonEmptyRegion get() = whole.nonEmptyRegion val nonEmptyRegion get() = whole.nonEmptyRegion
/**
* returns integer in ABGR format
*/
operator fun get(x: Int, y: Int): Int { operator fun get(x: Int, y: Int): Int {
return whole[x, y] return whole[x, y]
} }
/**
* returns integer in ABGR format
*/
operator fun get(x: Int, y: Int, flip: Boolean): Int { operator fun get(x: Int, y: Int, flip: Boolean): Int {
return whole[x, y, flip] return whole[x, y, flip]
} }
@ -201,6 +203,10 @@ class Image private constructor(
override val u1: Float = (x.toFloat() + this.width.toFloat()) / this@Image.width override val u1: Float = (x.toFloat() + this.width.toFloat()) / this@Image.width
override val v0: Float = (y.toFloat() + this.height.toFloat()) / this@Image.height override val v0: Float = (y.toFloat() + this.height.toFloat()) / this@Image.height
/**
* returns integer in ABGR format if it is RGB or RGBA picture,
* otherwise returns pixels as-is
*/
operator fun get(x: Int, y: Int): Int { operator fun get(x: Int, y: Int): Int {
require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" } require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" }
@ -208,17 +214,17 @@ class Image private constructor(
val data = data.join() val data = data.join()
when (amountOfChannels) { when (amountOfChannels) {
4 -> return data[offset].toInt() or 4 -> return data[offset].toInt().and(0xFF) or
data[offset + 1].toInt().shl(8) or data[offset + 1].toInt().and(0xFF).shl(8) or
data[offset + 2].toInt().shl(16) or data[offset + 2].toInt().and(0xFF).shl(16) or
data[offset + 3].toInt().shl(24) data[offset + 3].toInt().and(0xFF).shl(24)
3 -> return data[offset].toInt() or 3 -> return data[offset].toInt().and(0xFF) or
data[offset + 1].toInt().shl(8) or data[offset + 1].toInt().and(0xFF).shl(8) or
data[offset + 2].toInt().shl(16) data[offset + 2].toInt().and(0xFF).shl(16) or -0x1000000 // leading alpha as 255
2 -> return data[offset].toInt() or 2 -> return data[offset].toInt().and(0xFF) or
data[offset + 1].toInt().shl(8) data[offset + 1].toInt().and(0xFF).shl(8)
1 -> return data[offset].toInt() 1 -> return data[offset].toInt()
@ -226,6 +232,9 @@ class Image private constructor(
} }
} }
/**
* returns integer in ABGR format
*/
operator fun get(x: Int, y: Int, flip: Boolean): Int { operator fun get(x: Int, y: Int, flip: Boolean): Int {
if (flip) { if (flip) {
require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" } require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" }
@ -336,7 +345,7 @@ class Image private constructor(
.expireAfterAccess(Duration.ofMinutes(1)) .expireAfterAccess(Duration.ofMinutes(1))
.weigher<IStarboundFile, ByteBuffer> { key, value -> value.capacity() } .weigher<IStarboundFile, ByteBuffer> { key, value -> value.capacity() }
.maximumWeight((Runtime.getRuntime().maxMemory() / 4L).coerceIn(1_024L * 1_024L * 32L /* 32 МиБ */, 1_024L * 1_024L * 256L /* 256 МиБ */)) .maximumWeight((Runtime.getRuntime().maxMemory() / 4L).coerceIn(1_024L * 1_024L * 32L /* 32 МиБ */, 1_024L * 1_024L * 256L /* 256 МиБ */))
.scheduler(Scheduler.systemScheduler()) .scheduler(Starbound)
.executor(Starbound.EXECUTOR) .executor(Starbound.EXECUTOR)
.buildAsync(CacheLoader { .buildAsync(CacheLoader {
val getWidth = intArrayOf(0) val getWidth = intArrayOf(0)

View File

@ -17,6 +17,12 @@ val Registry.Ref<TileDefinition>.isEmptyTile: Boolean
val Registry.Ref<TileDefinition>.isNullTile: Boolean val Registry.Ref<TileDefinition>.isNullTile: Boolean
get() = entry == BuiltinMetaMaterials.NULL || entry == null get() = entry == BuiltinMetaMaterials.NULL || entry == null
val Registry.Ref<TileDefinition>.orEmptyTile: Registry.Entry<TileDefinition>
get() = entry ?: BuiltinMetaMaterials.EMPTY
val Registry.Ref<TileDefinition>.orNullTile: Registry.Entry<TileDefinition>
get() = entry ?: BuiltinMetaMaterials.NULL
val Registry.Ref<TileDefinition>.isObjectSolidTile: Boolean val Registry.Ref<TileDefinition>.isObjectSolidTile: Boolean
get() = entry == BuiltinMetaMaterials.OBJECT_SOLID get() = entry == BuiltinMetaMaterials.OBJECT_SOLID
@ -26,9 +32,18 @@ val Registry.Ref<TileDefinition>.isObjectPlatformTile: Boolean
val Registry.Entry<TileDefinition>.isEmptyTile: Boolean val Registry.Entry<TileDefinition>.isEmptyTile: Boolean
get() = this == BuiltinMetaMaterials.EMPTY || this == BuiltinMetaMaterials.NULL get() = this == BuiltinMetaMaterials.EMPTY || this == BuiltinMetaMaterials.NULL
val Registry.Entry<TileDefinition>.isRealTile: Boolean
get() = !value.isMeta
val Registry.Entry<TileDefinition>.isMetaTile: Boolean
get() = value.isMeta
val Registry.Entry<TileDefinition>.isNotEmptyTile: Boolean val Registry.Entry<TileDefinition>.isNotEmptyTile: Boolean
get() = !isEmptyTile get() = !isEmptyTile
val Registry.Entry<TileDefinition>.isObjectTile: Boolean
get() = this === BuiltinMetaMaterials.OBJECT_SOLID || this === BuiltinMetaMaterials.OBJECT_PLATFORM
val Registry.Entry<TileDefinition>.isNullTile: Boolean val Registry.Entry<TileDefinition>.isNullTile: Boolean
get() = this == BuiltinMetaMaterials.NULL get() = this == BuiltinMetaMaterials.NULL
@ -52,9 +67,24 @@ fun Registry.Entry<TileDefinition>.supportsModifier(modifier: Registry.Ref<TileM
val Registry.Entry<LiquidDefinition>.isEmptyLiquid: Boolean val Registry.Entry<LiquidDefinition>.isEmptyLiquid: Boolean
get() = this == BuiltinMetaMaterials.NO_LIQUID get() = this == BuiltinMetaMaterials.NO_LIQUID
val Registry.Entry<LiquidDefinition>.isNotEmptyLiquid: Boolean
get() = !isEmptyLiquid
val Registry.Ref<LiquidDefinition>.isEmptyLiquid: Boolean val Registry.Ref<LiquidDefinition>.isEmptyLiquid: Boolean
get() = entry == null || entry == BuiltinMetaMaterials.NO_LIQUID get() = entry == null || entry == BuiltinMetaMaterials.NO_LIQUID
val Registry.Ref<LiquidDefinition>.orEmptyLiquid: Registry.Entry<LiquidDefinition>
get() = entry ?: BuiltinMetaMaterials.NO_LIQUID
val Registry.Ref<LiquidDefinition>.isNotEmptyLiquid: Boolean
get() = !isEmptyLiquid
val Registry.Ref<TileModifierDefinition>.orEmptyModifier: Registry.Entry<TileModifierDefinition>
get() = entry ?: BuiltinMetaMaterials.EMPTY_MOD
val Registry.Ref<TileModifierDefinition>.isRealModifier: Boolean
get() = entry?.value?.isMeta == false
// these are hardcoded way harder than any Hard-Coder:tm: // these are hardcoded way harder than any Hard-Coder:tm:
// considering there is no way you gonna mod-in this many (16 bit uint) dungeons // considering there is no way you gonna mod-in this many (16 bit uint) dungeons
const val NO_DUNGEON_ID = 65535 const val NO_DUNGEON_ID = 65535
@ -120,20 +150,7 @@ object BuiltinMetaMaterials {
val OBJECT_SOLID = make(65500, "objectsolid", CollisionType.BLOCK) val OBJECT_SOLID = make(65500, "objectsolid", CollisionType.BLOCK)
val OBJECT_PLATFORM = make(65501, "objectplatform", CollisionType.PLATFORM) val OBJECT_PLATFORM = make(65501, "objectplatform", CollisionType.PLATFORM)
val MATERIALS: ImmutableList<Registry.Entry<TileDefinition>> = ImmutableList.of( val BIOME_META_MATERIALS: ImmutableList<Registry.Entry<TileDefinition>> = ImmutableList.of(BIOME, BIOME1, BIOME2, BIOME3, BIOME4, BIOME5)
EMPTY,
NULL,
STRUCTURE,
BIOME,
BIOME1,
BIOME2,
BIOME3,
BIOME4,
BIOME5,
BOUNDARY,
OBJECT_SOLID,
OBJECT_PLATFORM,
)
val EMPTY_MOD = makeMod(65535, "none") val EMPTY_MOD = makeMod(65535, "none")
val BIOME_MOD = makeMod(65534, "biome") val BIOME_MOD = makeMod(65534, "biome")

View File

@ -16,6 +16,7 @@ import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.stream import ru.dbotthepony.kommons.gson.stream
import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
@ -27,6 +28,8 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat import ru.dbotthepony.kstarbound.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.json.listAdapter import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.util.random.staticRandomInt
import java.util.stream.Stream import java.util.stream.Stream
@JsonFactory @JsonFactory
@ -55,7 +58,13 @@ data class BiomePlaceables(
val mode: BiomePlaceablesDefinition.Placement = BiomePlaceablesDefinition.Placement.FLOOR, val mode: BiomePlaceablesDefinition.Placement = BiomePlaceablesDefinition.Placement.FLOOR,
@JsonFlat @JsonFlat
val data: DistributionData, val data: DistributionData,
) ) {
fun itemToPlace(x: Int, y: Int): Placement? {
return data.itemToPlace(x, y, priority)
}
}
data class Placement(val item: Item, val position: Vector2i, val priority: Double)
abstract class Item { abstract class Item {
abstract val type: BiomePlacementItemType abstract val type: BiomePlacementItemType
@ -213,5 +222,39 @@ data class BiomePlaceables(
return weightedItems.stream().map { it.first } return weightedItems.stream().map { it.first }
} }
} }
fun itemToPlace(x: Int, y: Int, priority: Double): Placement? {
if (distribution == BiomePlacementDistributionType.RANDOM) {
if (randomItems.isEmpty())
return null // whut
if (staticRandomDouble(x, y, blockSeed) <= blockProbability) {
return Placement(randomItems[staticRandomInt(0, randomItems.size, x, y, blockSeed)], Vector2i(x, y), priority)
}
} else {
if (weightedItems.isEmpty())
return null // whut
if (densityFunction[x.toDouble(), y.toDouble()] > 0.0 && (x + modulusOffset + modulusDistortion[x.toDouble(), y.toDouble()]).toInt() % modulus == 0) {
var maxWeight = Double.NEGATIVE_INFINITY
var choosen: Item? = null
for ((item, distrb) in weightedItems) {
val weight = distrb[x.toDouble(), y.toDouble()]
if (weight > maxWeight) {
maxWeight = weight
choosen = item
}
}
if (choosen != null) {
return Placement(choosen, Vector2i(x, y), priority)
}
}
}
return null
}
} }
} }

View File

@ -70,7 +70,18 @@ class WorldLayout {
val biomes = ListInterner<Biome>() val biomes = ListInterner<Biome>()
val playerStartSearchRegions = ArrayList<AABBi>() val playerStartSearchRegions = ArrayList<AABBi>()
val layers = ArrayList<Layer>() var layers: MutableList<Layer> = ArrayList()
private set
private fun optimizeLayers() {
// replaces arraylist with immutable list
// which is faster when determining layer weighting
// because it doesn't check for concurrent modifications
// TODO: add binary search of layers
// binary searching layers will save CPU cycles
// when there are a lot of them
layers = ImmutableList.copyOf(layers)
}
var loopX = true var loopX = true
var loopY = false var loopY = false
@ -342,6 +353,8 @@ class WorldLayout {
} }
} }
optimizeLayers()
return this return this
} }
@ -512,6 +525,8 @@ class WorldLayout {
for (biome in biomes) { for (biome in biomes) {
biome.parallax?.fadeToSkyColor(skyColoring) biome.parallax?.fadeToSkyColor(skyColoring)
} }
optimizeLayers()
} }
data class RegionWeighting(val weight: Double, val xValue: Int, val region: Region) data class RegionWeighting(val weight: Double, val xValue: Int, val region: Region)
@ -558,7 +573,7 @@ class WorldLayout {
} else if (y < layers.first().yStart) { } else if (y < layers.first().yStart) {
return emptyList() return emptyList()
} else if (y >= layers.last().yStart) { } else if (y >= layers.last().yStart) {
yi = layers.size yi = layers.size - 1
} else { } else {
yi = layers.indexOfFirst { it.yStart >= y } - 1 yi = layers.indexOfFirst { it.yStart >= y } - 1
} }

View File

@ -10,7 +10,7 @@ import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.world.Direction1D import ru.dbotthepony.kstarbound.world.Direction
@JsonFactory @JsonFactory
data class WorldStructure( data class WorldStructure(
@ -41,7 +41,7 @@ data class WorldStructure(
data class Obj( data class Obj(
val position: Vector2i, val position: Vector2i,
val name: String, val name: String,
val direction: Direction1D, val direction: Direction,
val parameters: JsonElement, val parameters: JsonElement,
val residual: Boolean = false, val residual: Boolean = false,
) )

View File

@ -14,9 +14,11 @@ import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.quintic2 import ru.dbotthepony.kstarbound.math.quintic2
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
@ -38,7 +40,10 @@ class WorldTemplate(val geometry: WorldGeometry) {
var celestialParameters: CelestialParameters? = null var celestialParameters: CelestialParameters? = null
private set private set
val customTerrainRegions = ArrayList<CustomTerrainRegion>() val threatLevel: Double
get() = worldParameters?.threatLevel ?: 0.0
private val customTerrainRegions = ArrayList<CustomTerrainRegion>()
constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, seed: Long) : this(WorldGeometry(worldParameters.worldSize, true, false)) { constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, seed: Long) : this(WorldGeometry(worldParameters.worldSize, true, false)) {
this.seed = seed this.seed = seed
@ -70,6 +75,14 @@ class WorldTemplate(val geometry: WorldGeometry) {
val aabb = region.aabb.enlarge(Globals.worldTemplate.customTerrainBlendSize, Globals.worldTemplate.customTerrainBlendSize) val aabb = region.aabb.enlarge(Globals.worldTemplate.customTerrainBlendSize, Globals.worldTemplate.customTerrainBlendSize)
} }
fun addCustomTerrainRegion(region: Poly) {
customTerrainRegions.add(CustomTerrainRegion(region, true))
}
fun addCustomSpaceRegion(region: Poly) {
customTerrainRegions.add(CustomTerrainRegion(region, false))
}
@JsonFactory @JsonFactory
data class SerializedForm( data class SerializedForm(
val celestialParameters: CelestialParameters? = null, val celestialParameters: CelestialParameters? = null,
@ -151,21 +164,23 @@ class WorldTemplate(val geometry: WorldGeometry) {
return geometry.size.y / 2 return geometry.size.y / 2
} }
data class PotentialBiomeItems( fun seedFor(x: Int, y: Int) = staticRandom64(geometry.x.cell(x), geometry.y.cell(y), seed, "Block")
class PotentialBiomeItems(
// Potential items that would spawn at the given block assuming it is at // Potential items that would spawn at the given block assuming it is at
val surfaceBiomeItems: List<BiomePlaceables.DistributionItem>, val surfaceBiomeItems: List<BiomePlaceables.Placement>,
// ... Or on a cave surface. // ... Or on a cave surface.
val caveSurfaceBiomeItems: List<BiomePlaceables.DistributionItem>, val caveSurfaceBiomeItems: List<BiomePlaceables.Placement>,
// ... Or on a cave ceiling. // ... Or on a cave ceiling.
val caveCeilingBiomeItems: List<BiomePlaceables.DistributionItem>, val caveCeilingBiomeItems: List<BiomePlaceables.Placement>,
// ... Or on a cave background wall. // ... Or on a cave background wall.
val caveBackgroundBiomeItems: List<BiomePlaceables.DistributionItem>, val caveBackgroundBiomeItems: List<BiomePlaceables.Placement>,
// ... Or in the ocean // ... Or in the ocean
val oceanItems: List<BiomePlaceables.DistributionItem>, val oceanItems: List<BiomePlaceables.Placement>,
) )
fun potentialBiomeItemsAt(x: Int, y: Int): PotentialBiomeItems { fun potentialBiomeItemsAt(x: Int, y: Int): PotentialBiomeItems {
@ -174,15 +189,44 @@ class WorldTemplate(val geometry: WorldGeometry) {
val upperBlockBiome = cellInfo(geometry.x.cell(x), geometry.y.cell(y + 1)).blockBiome val upperBlockBiome = cellInfo(geometry.x.cell(x), geometry.y.cell(y + 1)).blockBiome
return PotentialBiomeItems( return PotentialBiomeItems(
surfaceBiomeItems = lowerBlockBiome?.surfacePlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.FLOOR } ?: listOf(), surfaceBiomeItems = lowerBlockBiome?.surfacePlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.FLOOR }?.map { it.itemToPlace(x, y) }?.filterNotNull() ?: listOf(),
oceanItems = thisBlockBiome?.surfacePlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.OCEAN } ?: listOf(), oceanItems = thisBlockBiome?.surfacePlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.OCEAN }?.map { it.itemToPlace(x, y) }?.filterNotNull() ?: listOf(),
caveSurfaceBiomeItems = lowerBlockBiome?.undergroundPlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.FLOOR } ?: listOf(), caveSurfaceBiomeItems = lowerBlockBiome?.undergroundPlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.FLOOR }?.map { it.itemToPlace(x, y) }?.filterNotNull() ?: listOf(),
caveCeilingBiomeItems = upperBlockBiome?.undergroundPlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.CEILING } ?: listOf(), caveCeilingBiomeItems = upperBlockBiome?.undergroundPlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.CEILING }?.map { it.itemToPlace(x, y) }?.filterNotNull() ?: listOf(),
caveBackgroundBiomeItems = thisBlockBiome?.undergroundPlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.BACKGROUND } ?: listOf(), caveBackgroundBiomeItems = thisBlockBiome?.undergroundPlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.BACKGROUND }?.map { it.itemToPlace(x, y) }?.filterNotNull() ?: listOf(),
) )
} }
fun validBiomeItemsAt(x: Int, y: Int): List<BiomePlaceables.Placement> {
val thisBlock = cellInfo(geometry.x.cell(x), geometry.y.cell(y))
if (thisBlock.biomeTransition)
return emptyList()
val result = ArrayList<BiomePlaceables.Placement>()
val lowerBlock = cellInfo(geometry.x.cell(x), geometry.y.cell(y - 1))
val upperBlock = cellInfo(geometry.x.cell(x), geometry.y.cell(y + 1))
val potential = potentialBiomeItemsAt(x, y)
if (!lowerBlock.biomeTransition && lowerBlock.terrain && !thisBlock.terrain && !lowerBlock.foregroundCave)
result.addAll(potential.surfaceBiomeItems)
if (!lowerBlock.biomeTransition && lowerBlock.terrain && thisBlock.terrain && !lowerBlock.foregroundCave && thisBlock.foregroundCave)
result.addAll(potential.caveSurfaceBiomeItems)
if (!upperBlock.biomeTransition && upperBlock.terrain && thisBlock.terrain && !upperBlock.foregroundCave && thisBlock.foregroundCave)
result.addAll(potential.caveCeilingBiomeItems)
if (thisBlock.terrain && thisBlock.foregroundCave && !thisBlock.backgroundCave)
result.addAll(potential.caveBackgroundBiomeItems)
if (thisBlock.oceanLiquid.isNotEmptyLiquid && y == thisBlock.oceanLiquidLevel)
result.addAll(potential.oceanItems)
return result
}
class CellInfo(val x: Int, val y: Int) { class CellInfo(val x: Int, val y: Int) {
var foreground: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref var foreground: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref
var foregroundMod: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref var foregroundMod: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref
@ -204,19 +248,11 @@ class WorldTemplate(val geometry: WorldGeometry) {
var backgroundCave = false var backgroundCave = false
} }
// making cache big enough to make
// serial generation stages fast enough,
// since sampling noise is costly
// TODO: Don't specify scheduler and executor since
// G1GC doesn't like this and will refuse to clean up
// memory retained by this cache until G1GC feels like it
// (needs more profiling)
private val cellCache = Caffeine.newBuilder() private val cellCache = Caffeine.newBuilder()
.maximumSize(1_000_000L) .maximumSize(50_000L)
.expireAfterAccess(Duration.ofMinutes(2)) //.expireAfterAccess(Duration.ofMinutes(1))
.executor(Starbound.EXECUTOR) //.executor(Starbound.EXECUTOR)
.scheduler(Scheduler.systemScheduler()) //.scheduler(Starbound) // don't specify scheduler since this cache is accessed very frequently
.build<Vector2i, CellInfo> { (x, y) -> cellInfo0(x, y) } .build<Vector2i, CellInfo> { (x, y) -> cellInfo0(x, y) }
fun cellInfo(x: Int, y: Int): CellInfo { fun cellInfo(x: Int, y: Int): CellInfo {

View File

@ -107,10 +107,11 @@ data class LegacyNetworkLiquidState(
val level: Int, // ubyte val level: Int, // ubyte
) { ) {
fun write(stream: DataOutputStream) { fun write(stream: DataOutputStream) {
stream.write(liquid)
if (liquid in 1 .. 255) { // empty or can't be represented by legacy protocol if (liquid in 1 .. 255) { // empty or can't be represented by legacy protocol
stream.write(liquid)
stream.write(level) stream.write(level)
} else {
stream.write(0)
} }
} }

View File

@ -195,15 +195,26 @@ class PacketRegistry(val isLegacy: Boolean) {
stream = FastByteArrayInputStream(packetReadBuffer.elements(), 0, packetReadBuffer.size) stream = FastByteArrayInputStream(packetReadBuffer.elements(), 0, packetReadBuffer.size)
} }
var i = -1
// legacy protocol allows to stitch multiple packets of same type together without // legacy protocol allows to stitch multiple packets of same type together without
// separate headers for each // separate headers for each
// Due to nature of netty pipeline, we can't do the same on native protocol; // Due to nature of netty pipeline, we can't do the same on native protocol;
// so don't do that when on native protocol // so don't do that when on native protocol
do { do {
i++
try { try {
ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy, side)) ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy, side))
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err) LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type}; packet No. $i in stream)", err)
}
// are u foking serious, InflaterInputStream?
if (stream is BufferedInputStream && stream.available() > 0 && isLegacy) {
stream.mark(1)
stream.read()
stream.reset()
} }
} while (stream.available() > 0 && isLegacy) } while (stream.available() > 0 && isLegacy)

View File

@ -4,14 +4,14 @@ import ru.dbotthepony.kstarbound.client.StarboundClient
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
class IntegratedStarboundServer(val client: StarboundClient, root: File) : StarboundServer(root), Closeable { class IntegratedStarboundServer(val client: StarboundClient, root: File) : StarboundServer(root) {
init { init {
channels.createLocalChannel() channels.createLocalChannel()
} }
override fun tick0() { override fun tick0() {
if (client.shouldTerminate) { if (client.isShutdown) {
close() shutdown()
} }
} }

View File

@ -136,7 +136,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
announceDisconnect("Connection to remote host is lost.") announceDisconnect("Connection to remote host is lost.")
if (::shipWorld.isInitialized) { if (::shipWorld.isInitialized) {
shipWorld.close() shipWorld.eventLoop.shutdown()
} }
if (countedTowardsPlayerCount) { if (countedTowardsPlayerCount) {
@ -288,7 +288,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
currentFlightJob?.cancel() currentFlightJob?.cancel()
val flight = world.flyShip(this, location) val flight = world.flyShip(this, location)
shipWorld.mailbox.execute { shipWorld.eventLoop.execute {
shipWorld.sky.startFlying(false) shipWorld.sky.startFlying(false)
} }
@ -303,7 +303,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
val sky = coords.skyParameters(world) val sky = coords.skyParameters(world)
shipWorld.mailbox.execute { shipWorld.eventLoop.execute {
shipWorld.sky.stopFlyingAt(sky) shipWorld.sky.stopFlyingAt(sky)
} }
} }
@ -323,7 +323,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
currentFlightJob?.cancel() currentFlightJob?.cancel()
world.removeClient(this) world.removeClient(this)
shipWorld.mailbox.execute { shipWorld.eventLoop.execute {
shipWorld.sky.startFlying(true) shipWorld.sky.startFlying(true)
} }
@ -350,7 +350,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
val newParams = ship.location.skyParameters(world) val newParams = ship.location.skyParameters(world)
shipWorld.mailbox.execute { shipWorld.eventLoop.execute {
shipWorld.sky.stopFlyingAt(newParams) shipWorld.sky.stopFlyingAt(newParams)
} }
@ -450,7 +450,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
tracker = null tracker = null
if (::shipWorld.isInitialized) { if (::shipWorld.isInitialized) {
shipWorld.close() shipWorld.eventLoop.shutdown()
} }
if (channel.isOpen) { if (channel.isOpen) {
@ -492,11 +492,11 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
server.loadShipWorld(this, shipChunkSource).thenAccept { server.loadShipWorld(this, shipChunkSource).thenAccept {
if (!isConnected || !channel.isOpen) { if (!isConnected || !channel.isOpen) {
LOGGER.warn("$this disconnected before loaded their ShipWorld") LOGGER.warn("$this disconnected before loaded their ShipWorld")
it.close() it.eventLoop.shutdown()
} else { } else {
shipWorld = it shipWorld = it
// shipWorld.sky.startFlying(true, true) // shipWorld.sky.startFlying(true, true)
shipWorld.thread.start() shipWorld.eventLoop.start()
enqueueWarp(WarpAlias.OwnShip) enqueueWarp(WarpAlias.OwnShip)
shipUpgrades = shipUpgrades.addCapability("planetTravel") shipUpgrades = shipUpgrades.addCapability("planetTravel")
shipUpgrades = shipUpgrades.addCapability("teleport") shipUpgrades = shipUpgrades.addCapability("teleport")

View File

@ -4,6 +4,7 @@ import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
@ -24,6 +25,7 @@ import ru.dbotthepony.kstarbound.server.world.ServerUniverse
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.Clock import ru.dbotthepony.kstarbound.util.Clock
import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.util.ExecutionSpinner
@ -38,7 +40,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Supplier import java.util.function.Supplier
sealed class StarboundServer(val root: File) : Closeable { sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread") {
init { init {
if (!root.exists()) { if (!root.exists()) {
check(root.mkdirs()) { "Unable to create ${root.absolutePath}" } check(root.mkdirs()) { "Unable to create ${root.absolutePath}" }
@ -48,12 +50,10 @@ sealed class StarboundServer(val root: File) : Closeable {
} }
private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>() private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>()
val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) }
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::tick, Starbound.TIMESTEP_NANOS)
val thread = Thread(spinner, "Server Thread")
val universe = ServerUniverse() val universe = ServerUniverse()
val chat = ChatHandler(this) val chat = ChatHandler(this)
val scope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob()) val scope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
val eventLoopScope = CoroutineScope(asCoroutineDispatcher() + SupervisorJob())
private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>() private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>()
@ -62,11 +62,11 @@ sealed class StarboundServer(val root: File) : Closeable {
} }
fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> { fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> {
return CompletableFuture.supplyAsync(Supplier { return supplyAsync {
systemWorlds.computeIfAbsent(location) { systemWorlds.computeIfAbsent(location) {
scope.async { loadSystemWorld0(location) }.asCompletableFuture() scope.async { loadSystemWorld0(location) }.asCompletableFuture()
} }
}, mailbox).thenCompose { it } }.thenCompose { it }
} }
private suspend fun loadCelestialWorld(location: WorldID.Celestial): ServerWorld { private suspend fun loadCelestialWorld(location: WorldID.Celestial): ServerWorld {
@ -76,11 +76,11 @@ sealed class StarboundServer(val root: File) : Closeable {
val world = ServerWorld.create(this, template, WorldStorage.Nothing, location) val world = ServerWorld.create(this, template, WorldStorage.Nothing, location)
try { try {
world.thread.start() world.eventLoop.start()
world.prepare().await() world.prepare().await()
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.fatal("Exception while creating celestial world at $location!", err) LOGGER.fatal("Exception while creating celestial world at $location!", err)
world.close() world.eventLoop.shutdown()
throw err throw err
} }
@ -113,7 +113,7 @@ sealed class StarboundServer(val root: File) : Closeable {
world.setProperty("ephemeral", JsonPrimitive(!config.persistent)) world.setProperty("ephemeral", JsonPrimitive(!config.persistent))
world.thread.start() world.eventLoop.start()
return world return world
} }
@ -127,7 +127,7 @@ sealed class StarboundServer(val root: File) : Closeable {
} }
fun loadWorld(location: WorldID): CompletableFuture<ServerWorld> { fun loadWorld(location: WorldID): CompletableFuture<ServerWorld> {
return CompletableFuture.supplyAsync(Supplier { return supplyAsync {
var world = worlds[location] var world = worlds[location]
if (world != null && world.isCompletedExceptionally) { if (world != null && world.isCompletedExceptionally) {
@ -142,11 +142,11 @@ sealed class StarboundServer(val root: File) : Closeable {
worlds[location] = future worlds[location] = future
future future
} }
}, mailbox).thenCompose { it } }.thenCompose { it }
} }
fun loadShipWorld(connection: ServerConnection, storage: WorldStorage): CompletableFuture<ServerWorld> { fun loadShipWorld(connection: ServerConnection, storage: WorldStorage): CompletableFuture<ServerWorld> {
return CompletableFuture.supplyAsync(Supplier { return supplyAsync {
val id = WorldID.ShipWorld(connection.uuid ?: throw NullPointerException("Connection UUID is null")) val id = WorldID.ShipWorld(connection.uuid ?: throw NullPointerException("Connection UUID is null"))
val existing = worlds[id] val existing = worlds[id]
@ -156,11 +156,11 @@ sealed class StarboundServer(val root: File) : Closeable {
val world = ServerWorld.load(this, storage, id) val world = ServerWorld.load(this, storage, id)
worlds[id] = world worlds[id] = world
world world
}, mailbox).thenCompose { it } }.thenCompose { it }
} }
fun notifyWorldUnloaded(worldID: WorldID) { fun notifyWorldUnloaded(worldID: WorldID) {
mailbox.execute { execute {
worlds.remove(worldID) worlds.remove(worldID)
} }
} }
@ -181,17 +181,20 @@ sealed class StarboundServer(val root: File) : Closeable {
val universeClock = Clock() val universeClock = Clock()
init { init {
mailbox.scheduleAtFixedRate(Runnable { scheduleAtFixedRate(Runnable {
channels.broadcast(UniverseTimeUpdatePacket(universeClock.seconds)) channels.broadcast(UniverseTimeUpdatePacket(universeClock.seconds))
}, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS) }, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS)
thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e -> scheduleAtFixedRate(Runnable {
LOGGER.fatal("Unexpected exception in server execution loop, shutting down", e) tickNormal()
actuallyClose() }, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
}
// thread.isDaemon = this is IntegratedStarboundServer scheduleAtFixedRate(Runnable {
thread.start() tickSystemWorlds()
}, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, Starbound.SYSTEM_WORLD_TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
isDaemon = false
start()
} }
private val occupiedNicknames = ObjectArraySet<String>() private val occupiedNicknames = ObjectArraySet<String>()
@ -226,57 +229,67 @@ sealed class StarboundServer(val root: File) : Closeable {
protected abstract fun close0() protected abstract fun close0()
protected abstract fun tick0() protected abstract fun tick0()
private fun tick(): Boolean { private fun tickSystemWorlds() {
if (isClosed) return false systemWorlds.values.removeIf {
if (it.isCompletedExceptionally) {
channels.connections.forEach { return@removeIf true
try {
it.tick()
} catch (err: Throwable) {
LOGGER.error("Exception while ticking client connection", err)
it.disconnect("Exception while ticking client connection: $err")
} }
}
// TODO: schedule to thread pool? if (!it.isDone) {
// right now, system worlds are rather lightweight, and having separate threads for them is overkill return@removeIf false
if (systemWorlds.isNotEmpty()) { }
runBlocking {
systemWorlds.values.removeIf {
if (it.isCompletedExceptionally) {
return@removeIf true
}
if (!it.isDone) { eventLoopScope.launch {
return@removeIf false try {
} it.get().tick()
} catch (err: Throwable) {
launch { it.get().tick() } LOGGER.fatal("Exception in system world $it event loop", err)
if (it.get().shouldClose()) {
LOGGER.info("Stopping idling ${it.get()}")
return@removeIf true
}
return@removeIf false
} }
} }
}
tick0() if (it.get().shouldClose()) {
return !isClosed LOGGER.info("Stopping idling ${it.get()}")
return@removeIf true
}
return@removeIf false
}
} }
private fun actuallyClose() { private fun tickNormal() {
if (isClosed) return try {
isClosed = true channels.connections.forEach {
try {
it.tick()
} catch (err: Throwable) {
LOGGER.error("Exception while ticking client connection", err)
it.disconnect("Exception while ticking client connection: $err")
}
}
tick0()
} catch (err: Throwable) {
LOGGER.fatal("Exception in main server event loop", err)
shutdown()
}
}
override fun performShutdown() {
super.performShutdown()
scope.cancel("Server shutting down") scope.cancel("Server shutting down")
channels.close() channels.close()
worlds.values.forEach { worlds.values.forEach {
if (it.isDone && !it.isCompletedExceptionally) if (it.isDone && !it.isCompletedExceptionally) {
it.get().close() it.get().eventLoop.shutdown()
}
}
worlds.values.forEach {
if (it.isDone && !it.isCompletedExceptionally) {
it.get().eventLoop.awaitTermination(10L, TimeUnit.SECONDS)
}
it.cancel(true) it.cancel(true)
} }
@ -285,14 +298,6 @@ sealed class StarboundServer(val root: File) : Closeable {
close0() close0()
} }
final override fun close() {
if (Thread.currentThread() == thread) {
actuallyClose()
} else {
mailbox.execute { actuallyClose() }
}
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
} }

View File

@ -4,16 +4,21 @@ import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonRule
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.FIRST_RESERVED_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
@ -21,12 +26,19 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isMetaTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNullTile import ru.dbotthepony.kstarbound.defs.tile.isNullTile
import ru.dbotthepony.kstarbound.defs.tile.supportsModifier import ru.dbotthepony.kstarbound.defs.tile.supportsModifier
import ru.dbotthepony.kstarbound.defs.world.Biome
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.util.random.staticRandomInt
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
@ -35,13 +47,19 @@ import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.api.MutableTileState
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
import java.util.function.Supplier
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) { class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
/** /**
@ -52,10 +70,10 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
enum class State { enum class State {
FRESH, // Nothing is loaded FRESH, // Nothing is loaded
TILES, TERRAIN,
MICRO_DUNGEONS, MICRO_DUNGEONS,
CAVE_LIQUID, CAVE_LIQUID,
FULL; // indicates everything has been loaded FULL;
} }
var state: State = State.FRESH var state: State = State.FRESH
@ -107,14 +125,29 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
for (neighbour in neighbours) { for (neighbour in neighbours) {
var i = 0 if (neighbour.chunk.isDone)
continue
while (!neighbour.chunk.isDone && ++i < 20) { suspendCancellableCoroutine<Unit> { block ->
delay(500L) val future = world.eventLoop.schedule(Runnable {
} if (!neighbour.chunk.isDone) {
LOGGER.error("Giving up waiting on ${neighbour.pos} while advancing generation stage of $pos to $nextState (neighbour chunk was in state ${world.chunkMap[neighbour.pos]?.state}, expected $state)")
block.resume(Unit)
}
}, 30L, TimeUnit.SECONDS)
if (!neighbour.chunk.isDone) { neighbour.chunk.thenAccept {
LOGGER.error("Giving up waiting on ${neighbour.pos} while advancing generation stage of $this to $nextState (neighbour chunk was in state ${world.chunkMap[neighbour.pos]?.state}, expected $state)") future.cancel(false)
block.resume(Unit)
}.exceptionally {
future.cancel(false)
block.resumeWithException(it); null
}
block.invokeOnCancellation {
future.cancel(false)
neighbour.cancel()
}
} }
} }
} catch (err: Throwable) { } catch (err: Throwable) {
@ -126,23 +159,37 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
when (nextState) { when (nextState) {
State.TILES -> { State.TERRAIN -> {
// tiles can be generated concurrently without any consequences if (world.template.worldLayout == null) {
CompletableFuture.runAsync(Runnable { prepareCells() }, Starbound.EXECUTOR).await() // skip since no cells will be generated anyway
cells.value.fill(AbstractCell.EMPTY)
} else {
// tiles can be generated concurrently without any consequences
CompletableFuture.runAsync(Runnable { prepareCells() }, Starbound.EXECUTOR).await()
}
} }
State.MICRO_DUNGEONS -> { State.MICRO_DUNGEONS -> {
//LOGGER.error("NYI: Generating microdungeons for $chunk") // skip if we have no layout
if (world.template.worldLayout != null) {
placeMicroDungeons()
}
} }
State.CAVE_LIQUID -> { State.CAVE_LIQUID -> {
// not thread safe, but takes very little time to execute // skip if we have no layout
generateLiquid() if (world.template.worldLayout != null) {
generateLiquid()
}
} }
State.FULL -> { State.FULL -> {
// CompletableFuture.runAsync(Runnable { placeGrass() }, Starbound.EXECUTOR).await() CompletableFuture.runAsync(Runnable { finalizeCells() }, Starbound.EXECUTOR).await()
placeGrass()
// skip if we have no layout
if (world.template.worldLayout != null) {
placeGrass()
}
} }
State.FRESH -> throw RuntimeException() State.FRESH -> throw RuntimeException()
@ -168,7 +215,10 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
// very good. // very good.
if (cells.isPresent) { if (cells.isPresent) {
loadCells(cells.value) loadCells(cells.value)
bumpState(State.CAVE_LIQUID) // bumping state while loading chunk might have
// undesired consequences, such as if chunk requester
// is pessimistic and want "fully loaded chunk or chunk generated least X stage"
// bumpState(State.CAVE_LIQUID)
world.storage.loadEntities(pos).await().ifPresent { world.storage.loadEntities(pos).await().ifPresent {
for (obj in it) { for (obj in it) {
@ -189,6 +239,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} else { } else {
LOGGER.error("Exception while loading chunk $this", err) LOGGER.error("Exception while loading chunk $this", err)
} }
} finally {
isBusy = false
} }
} }
@ -199,7 +251,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
fun temporaryTicket(time: Int, target: State = State.FULL): ITimedTicket { fun temporaryTicket(time: Int, target: State = State.FULL): ITimedTicket {
require(time > 0) { "Invalid ticket time: $time" } require(time >= 0) { "Invalid ticket time: $time" }
ticketsLock.withLock { ticketsLock.withLock {
return TimedTicket(time, target) return TimedTicket(time, target)
@ -263,8 +315,6 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
final override val chunk = CompletableFuture<ServerChunk>() final override val chunk = CompletableFuture<ServerChunk>()
init { init {
isBusy = true
if (this@ServerChunk.state >= targetState) { if (this@ServerChunk.state >= targetState) {
chunk.complete(this@ServerChunk) chunk.complete(this@ServerChunk)
} }
@ -520,7 +570,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
val unloadable = world.entityIndex val unloadable = world.entityIndex
.query( .query(
aabb, aabb,
filter = Predicate { it.isApplicableForUnloading && aabb.isInside(it.position) }, filter = Predicate { it.isApplicableForUnloading && aabbd.isInside(it.position) },
distinct = true, withEdges = false) distinct = true, withEdges = false)
world.storage.saveCells(pos, copyCells()) world.storage.saveCells(pos, copyCells())
@ -603,83 +653,215 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
private fun placeGrass() { private fun finalizeCells() {
val cells = cells.value
for (x in 0 until width) { for (x in 0 until width) {
for (y in 0 until height) { for (y in 0 until height) {
placeGrass(x, y) val cell = cells[x, y].mutable()
val info by lazy { world.template.cellInfo(pos.tileX + x, pos.tileY + y) }
if (cell.liquid.isInfinite) {
// make sure that ocean liquid never exists on tiles without empty
// background (except in real dungeons)
if (cell.dungeonId < FIRST_RESERVED_DUNGEON_ID && cell.background.material.isNotEmptyTile) {
cell.liquid.isInfinite = false
}
// pressurize liquid under the ocean
if (info.oceanLiquid.isNotEmptyLiquid && pos.tileY + y < info.oceanLiquidLevel) {
cell.liquid.pressure = cell.liquid.pressure.coerceAtLeast((info.oceanLiquidLevel - (pos.tileY + y)).toFloat())
}
}
if (cell.foreground.material.isMetaTile) {
cell.foreground.color = TileColor.DEFAULT
}
if (cell.background.material.isMetaTile) {
cell.background.color = TileColor.DEFAULT
}
replaceBiomeBlocks(cell, info)
cells[x, y] = cell.immutable()
} }
} }
} }
private fun placeGrass(x: Int, y: Int) { private fun doReplaceBiomeTile(tile: MutableTileState, biome: Biome?) {
val biome = world.template.cellInfo(pos.tileX + x, pos.tileY + y).blockBiome ?: return // TODO: Maybe somehow expand this biome meta material list?
val cell = cells.value[x, y] // that's only 6 meta blocks in total!
// determine layer for grass mod calculation when (tile.material) {
val isBackground = cell.foreground.material.isEmptyTile BuiltinMetaMaterials.BIOME -> tile.material = biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME1 -> tile.material = biome?.subBlocks?.getOrNull(0)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME2 -> tile.material = biome?.subBlocks?.getOrNull(1)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME3 -> tile.material = biome?.subBlocks?.getOrNull(2)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME4 -> tile.material = biome?.subBlocks?.getOrNull(3)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME5 -> tile.material = biome?.subBlocks?.getOrNull(4)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
else -> {}
}
val tile = cell.tile(isBackground) tile.hueShift = biome?.hueShift(tile.material) ?: 0f
val tileInv = cell.tile(!isBackground)
// don't place mods in dungeons unless explicitly specified, also don't if (biome == null && tile.modifier == BuiltinMetaMaterials.BIOME_MOD) {
// touch non-grass mods tile.modifier = BuiltinMetaMaterials.EMPTY_MOD
if ( tile.modifierHueShift = 0f
tile.modifier == BuiltinMetaMaterials.BIOME_MOD || }
tile.modifier == BuiltinMetaMaterials.UNDERGROUND_BIOME_MOD || }
(cell.dungeonId == NO_DUNGEON_ID && tile.modifier == BuiltinMetaMaterials.EMPTY_MOD)
) {
// check whether we're floor or ceiling
// NOTE: we are querying other chunks while generating, fun replaceBiomeBlocks(cell: MutableCell, info: WorldTemplate.CellInfo) {
// and we might read stale data if we are reading neighbouring chunks doReplaceBiomeTile(cell.foreground, info.blockBiome)
// since they might be in process of generation, too doReplaceBiomeTile(cell.background, info.blockBiome)
// (only if they are in process of generating someing significant, which modify terrain) }
// shouldn't be an issue though
val cellAbove = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y + 1)
val cellBelow = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y - 1)
val isFloor = (!cell.foreground.material.isEmptyTile && cellAbove.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellAbove.background.material.isEmptyTile) private fun replaceBiomeBlocks() {
val isCeiling = !isFloor && ((!cell.foreground.material.isEmptyTile && cellBelow.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellBelow.background.material.isEmptyTile)) val cells = cells.value
// I might be stupid, but in original code the check above is completely wrong for (x in 0 until width) {
// because it will result in buried glass under tiles for (y in 0 until height) {
//val isFloor = !tile.material.isEmptyTile && tileAbove.material.isEmptyTile val cell = cells[x, y].mutable()
//val isCeiling = !isFloor && !tile.material.isEmptyTile && tileBelow.material.isEmptyTile replaceBiomeBlocks(cell, world.template.cellInfo(pos.tileX + x, pos.tileY + y))
cells[x, y] = cell.immutable()
// get the appropriate placeables for above/below ground
val placeables = if (isFloor && !cellAbove.background.material.isEmptyTile || isCeiling && !cellBelow.background.material.isEmptyTile) {
biome.undergroundPlaceables
} else {
biome.surfacePlaceables
} }
}
}
// determine the proper grass mod or lack thereof private suspend fun placeMicroDungeons() {
var grassMod = BuiltinMetaMaterials.EMPTY_MOD val placements = CompletableFuture.supplyAsync(Supplier {
val placements = ArrayList<BiomePlaceables.Placement>()
if (isFloor) { for (x in 0 until width) {
if (staticRandomDouble(world.template.seed, pos.tileX + x, pos.tileY + y, "grass") <= placeables.grassModDensity) { for (y in 0 until height) {
grassMod = placeables.grassMod.native.entry ?: BuiltinMetaMaterials.EMPTY_MOD placements.addAll(world.template.validBiomeItemsAt(pos.tileX + x, pos.tileY + y))
}
} else if (isCeiling) {
if (staticRandomDouble(world.template.seed, pos.tileX + x, pos.tileY + y, "grass") <= placeables.ceilingGrassModDensity) {
grassMod = placeables.ceilingGrassMod.native.entry ?: BuiltinMetaMaterials.EMPTY_MOD
} }
} }
val modify = cell.mutable() placements.sortByDescending { it.priority }
placements
}, Starbound.EXECUTOR).await()
if (isBackground) { val bounds = AABBi(
modify.background.modifier = grassMod pos.tile - Vector2i(CHUNK_SIZE_FF, CHUNK_SIZE_FF),
modify.foreground.modifier = BuiltinMetaMaterials.EMPTY_MOD pos.tile + Vector2i(width + CHUNK_SIZE_FF, height + CHUNK_SIZE_FF)
} else { )
modify.foreground.modifier = grassMod
modify.background.modifier = BuiltinMetaMaterials.EMPTY_MOD for (placement in placements) {
if (placement.item is BiomePlaceables.MicroDungeon) {
if (placement.item.microdungeons.isEmpty())
continue // ???
val seed = world.template.seedFor(placement.position.x, placement.position.y)
val random = random(seed)
val dungeon = placement.item.microdungeons.elementAt(random.nextInt(placement.item.microdungeons.size))
val def = Registries.dungeons[dungeon]
if (def == null) {
LOGGER.error("Unknown dungeon type $dungeon!")
} else {
val anchors = def.value.validAnchors(world)
if (anchors.isEmpty())
continue
val anchor = anchors.random(random)
for (dy in MICRODUNGEON_PLACEMENT_SHIFTS) {
val pos = placement.position - anchor.anchor + Vector2i(y = dy)
if (!bounds.isInside(pos) || !bounds.isInside(pos + anchor.reader.size - Vector2i.POSITIVE_XY))
continue
val collision = anchor.reader.walkTiles<Boolean> { x, y, tile ->
if (tile.usesPlaces && world.getCell(pos.x + x, pos.y + y).dungeonId != NO_DUNGEON_ID) {
return@walkTiles KOptional(true)
}
return@walkTiles KOptional()
}.orElse(false)
if (!collision && anchor.canPlace(pos.x, pos.y, world)) {
def.value.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID).await()
LOGGER.info("Placed dungeon $dungeon at $pos")
break
}
}
}
} }
}
}
modify.background.modifierHueShift = biome.hueShift(modify.background.modifier) private fun placeGrass() {
modify.foreground.modifierHueShift = biome.hueShift(modify.foreground.modifier) val cells = cells.value
cells.value[x, y] = modify.immutable() for (x in 0 until width) {
for (y in 0 until height) {
val biome = world.template.cellInfo(pos.tileX + x, pos.tileY + y).blockBiome ?: continue
val cell = cells[x, y]
// determine layer for grass mod calculation
val isBackground = cell.foreground.material.isEmptyTile
val tile = cell.tile(isBackground)
val tileInv = cell.tile(!isBackground)
// don't place mods in dungeons unless explicitly specified, also don't
// touch non-grass mods
if (
tile.modifier == BuiltinMetaMaterials.BIOME_MOD ||
tile.modifier == BuiltinMetaMaterials.UNDERGROUND_BIOME_MOD ||
(cell.dungeonId == NO_DUNGEON_ID && tile.modifier == BuiltinMetaMaterials.EMPTY_MOD)
) {
// check whether we're floor or ceiling
// NOTE: we are querying other chunks while generating,
// and we might read stale data if we are reading neighbouring chunks
// since they might be in process of generation, too
// (only if they are in process of generating someing significant, which modify terrain)
// shouldn't be an issue though
val cellAbove = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y + 1)
val cellBelow = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y - 1)
val isFloor = (!cell.foreground.material.isEmptyTile && cellAbove.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellAbove.background.material.isEmptyTile)
val isCeiling = !isFloor && ((!cell.foreground.material.isEmptyTile && cellBelow.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellBelow.background.material.isEmptyTile))
// get the appropriate placeables for above/below ground
val placeables = if (isFloor && !cellAbove.background.material.isEmptyTile || isCeiling && !cellBelow.background.material.isEmptyTile) {
biome.undergroundPlaceables
} else {
biome.surfacePlaceables
}
// determine the proper grass mod or lack thereof
var grassMod = BuiltinMetaMaterials.EMPTY_MOD
if (isFloor) {
if (staticRandomDouble(world.template.seed, pos.tileX + x, pos.tileY + y, "grass") <= placeables.grassModDensity) {
grassMod = placeables.grassMod.native.entry ?: BuiltinMetaMaterials.EMPTY_MOD
}
} else if (isCeiling) {
if (staticRandomDouble(world.template.seed, pos.tileX + x, pos.tileY + y, "grass") <= placeables.ceilingGrassModDensity) {
grassMod = placeables.ceilingGrassMod.native.entry ?: BuiltinMetaMaterials.EMPTY_MOD
}
}
val modify = cell.mutable()
if (isBackground) {
modify.background.modifier = grassMod
modify.foreground.modifier = BuiltinMetaMaterials.EMPTY_MOD
} else {
modify.foreground.modifier = grassMod
modify.background.modifier = BuiltinMetaMaterials.EMPTY_MOD
}
modify.background.modifierHueShift = biome.hueShift(modify.background.modifier)
modify.foreground.modifierHueShift = biome.hueShift(modify.foreground.modifier)
cells[x, y] = modify.immutable()
}
}
} }
} }
@ -927,5 +1109,12 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val MICRODUNGEON_PLACEMENT_SHIFTS = immutableList {
accept(0)
for (i in 1 .. 4) accept(i)
for (i in 1 .. 4) accept(-i)
}
} }
} }

View File

@ -256,7 +256,7 @@ class ServerSystemWorld : SystemWorld {
// in original engine, ticking happens at 20 updates per second // in original engine, ticking happens at 20 updates per second
// Since there is no Lua driven code, we can tick as fast as we want // Since there is no Lua driven code, we can tick as fast as we want
suspend fun tick(delta: Double = Starbound.TIMESTEP) { suspend fun tick(delta: Double = Starbound.SYSTEM_WORLD_TIMESTEP) {
var next = tasks.poll() var next = tasks.poll()
while (next != null) { while (next != null) {
@ -299,7 +299,7 @@ class ServerSystemWorld : SystemWorld {
private val netVersions = Object2LongOpenHashMap<UUID>() private val netVersions = Object2LongOpenHashMap<UUID>()
suspend fun tick(delta: Double = Starbound.TIMESTEP) { suspend fun tick(delta: Double = Starbound.SYSTEM_WORLD_TIMESTEP) {
val orbit = destination as? SystemWorldLocation.Orbit val orbit = destination as? SystemWorldLocation.Orbit
// if destination is an orbit we haven't started orbiting yet, update the time // if destination is an orbit we haven't started orbiting yet, update the time
@ -446,7 +446,7 @@ class ServerSystemWorld : SystemWorld {
var hasExpired = false var hasExpired = false
private set private set
suspend fun tick(delta: Double = Starbound.TIMESTEP) { suspend fun tick(delta: Double = Starbound.SYSTEM_WORLD_TIMESTEP) {
if (!data.permanent && spawnTime > 0.0 && clock.seconds > spawnTime + data.lifeTime) if (!data.permanent && spawnTime > 0.0 && clock.seconds > spawnTime + data.lifeTime)
hasExpired = true hasExpired = true

View File

@ -13,6 +13,7 @@ import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.world.CelestialConfig import ru.dbotthepony.kstarbound.defs.world.CelestialConfig
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
@ -219,7 +220,8 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
.expireAfterAccess(Duration.ofMinutes(10L)) .expireAfterAccess(Duration.ofMinutes(10L))
.maximumSize(1024L) .maximumSize(1024L)
.softValues() .softValues()
.scheduler(Scheduler.systemScheduler()) .scheduler(Starbound)
.executor(Starbound.EXECUTOR)
.build() .build()
fun getChunkFuture(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> { fun getChunkFuture(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> {

View File

@ -15,7 +15,9 @@ import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.defs.WarpAlias
@ -35,7 +37,9 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateWorldProperti
import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
@ -46,6 +50,7 @@ import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.function.Supplier import java.util.function.Supplier
@ -64,7 +69,6 @@ class ServerWorld private constructor(
val clients = CopyOnWriteArrayList<ServerWorldTracker>() val clients = CopyOnWriteArrayList<ServerWorldTracker>()
val shouldStopOnIdle = worldID !is WorldID.ShipWorld val shouldStopOnIdle = worldID !is WorldID.ShipWorld
val scope = CoroutineScope(mailbox.asCoroutineDispatcher() + SupervisorJob())
private fun doAcceptClient(client: ServerConnection, action: WarpAction?) { private fun doAcceptClient(client: ServerConnection, action: WarpAction?) {
try { try {
@ -93,17 +97,19 @@ class ServerWorld private constructor(
client.tracker?.remove("Transiting to new world", false) client.tracker?.remove("Transiting to new world", false)
clients.add(ServerWorldTracker(this, client, start)) clients.add(ServerWorldTracker(this, client, start))
//if (worldID is WorldID.Celestial)
//Registries.dungeons["gardenmicrodungeons"]?.value?.generate(this@ServerWorld, random(), start.x.toInt(), start.y.toInt(), markSurfaceAndTerrain = false, forcePlacement = true)
} finally { } finally {
isBusy-- isBusy--
} }
} }
fun acceptClient(player: ServerConnection, action: WarpAction? = null): CompletableFuture<Unit> { fun acceptClient(player: ServerConnection, action: WarpAction? = null): CompletableFuture<Unit> {
check(!isClosed.get()) { "$this is invalid" } check(!eventLoop.isShutdown) { "$this is invalid" }
unpause()
try { try {
val future = CompletableFuture.supplyAsync(Supplier { doAcceptClient(player, action) }, mailbox) val future = eventLoop.supplyAsync { doAcceptClient(player, action) }
future.exceptionally { future.exceptionally {
LOGGER.error("Error while accepting new player into world", it) LOGGER.error("Error while accepting new player into world", it)
@ -115,47 +121,19 @@ class ServerWorld private constructor(
} }
} }
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS) override val eventLoop = object : BlockableEventLoop("Server World $worldID") {
private val str = "Server World ${worldID.toString()}" init {
val thread = Thread(spinner, str) isDaemon = true
}
init { override fun performShutdown() {
mailbox.thread = thread LOGGER.info("Shutting down ${this@ServerWorld}")
}
private val isClosed = AtomicBoolean() try {
server.notifyWorldUnloaded(worldID)
fun isClosed(): Boolean { } catch (err: RejectedExecutionException) {
return isClosed.get() // do nothing
} }
init {
thread.isDaemon = true
}
fun pause() {
if (!isClosed.get()) spinner.pause()
}
fun unpause() {
if (!isClosed.get()) spinner.unpause()
}
override fun toString(): String {
if (isClosed.get())
return "NULL $str"
else
return str
}
override fun close() {
if (!isClosed.get())
LOGGER.info("Shutting down $this")
if (isClosed.compareAndSet(false, true)) {
server.notifyWorldUnloaded(worldID)
super.close()
spinner.unpause()
chunkMap.chunks().forEach { chunkMap.chunks().forEach {
it.cancelLoadJob() it.cancelLoadJob()
@ -165,50 +143,25 @@ class ServerWorld private constructor(
it.remove() it.remove()
it.client.enqueueWarp(WarpAlias.Return) it.client.enqueueWarp(WarpAlias.Return)
} }
LockSupport.unpark(thread)
} }
} }
init {
eventLoop.scheduleAtFixedRate(::tick, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
}
val scope = CoroutineScope(eventLoop.asCoroutineDispatcher() + SupervisorJob())
override fun toString(): String {
return "Server World $worldID"
}
private var idleTicks = 0 private var idleTicks = 0
private var isBusy = 0 private var isBusy = 0
private fun spin(): Boolean {
if (isClosed.get()) return false
try {
if (clients.isEmpty() && isBusy <= 0) {
idleTicks++
} else {
idleTicks = 0
}
tick()
if (idleTicks >= 600) {
if (shouldStopOnIdle) {
close()
return false
} else {
pause()
}
}
return true
} catch (err: Throwable) {
LOGGER.fatal("Exception in world tick loop", err)
close()
return false
}
}
override val isRemote: Boolean override val isRemote: Boolean
get() = false get() = false
override fun isSameThread(): Boolean {
return Thread.currentThread() === thread
}
fun damageTiles(positions: Collection<IStruct2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult { fun damageTiles(positions: Collection<IStruct2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult {
if (damage.amount <= 0.0) if (damage.amount <= 0.0)
return TileDamageResult.NONE return TileDamageResult.NONE
@ -235,8 +188,7 @@ class ServerWorld private constructor(
if (!damagedEntities.add(entity)) continue if (!damagedEntities.add(entity)) continue
val occupySpaces = entity.occupySpaces.stream() val occupySpaces = entity.occupySpaces.stream()
.map { geometry.wrap(it + entity.tilePosition) } .filter { p -> actualPositions.any { it.first == p } }
.filter { it in positions }
.toList() .toList()
val broken = entity.damage(occupySpaces, sourcePosition, damage) val broken = entity.damage(occupySpaces, sourcePosition, damage)
@ -276,12 +228,26 @@ class ServerWorld private constructor(
} }
override fun tick() { override fun tick() {
super.tick() try {
if (clients.isEmpty() && isBusy <= 0) {
idleTicks++
} else {
idleTicks = 0
}
val packet = StepUpdatePacket(ticks) if (idleTicks >= 600) {
if (shouldStopOnIdle) {
eventLoop.shutdown()
}
clients.forEach { return
if (!isClosed.get()) { }
super.tick()
val packet = StepUpdatePacket(ticks)
clients.forEach {
it.send(packet) it.send(packet)
try { try {
@ -291,6 +257,9 @@ class ServerWorld private constructor(
//it.disconnect("Exception while ticking player: $err") //it.disconnect("Exception while ticking player: $err")
} }
} }
} catch (err: Throwable) {
LOGGER.fatal("Exception in world tick loop", err)
eventLoop.shutdown()
} }
} }
@ -316,9 +285,7 @@ class ServerWorld private constructor(
// everything inside our own thread, not anywhere else // everything inside our own thread, not anywhere else
// This way, external callers can properly wait for preparations to complete // This way, external callers can properly wait for preparations to complete
fun prepare(): CompletableFuture<*> { fun prepare(): CompletableFuture<*> {
return CompletableFuture.supplyAsync(Supplier { return scope.launch { prepare0() }.asCompletableFuture()
scope.launch { prepare0() }.asCompletableFuture()
}, mailbox).thenCompose { it }
} }
private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d { private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d {
@ -430,13 +397,13 @@ class ServerWorld private constructor(
} }
fun temporaryChunkTicket(region: AABBi, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITimedTicket> { fun temporaryChunkTicket(region: AABBi, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITimedTicket> {
require(time > 0) { "Invalid ticket time: $time" } require(time >= 0) { "Invalid ticket time: $time" }
return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull() return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull()
} }
fun temporaryChunkTicket(region: AABB, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITimedTicket> { fun temporaryChunkTicket(region: AABB, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITimedTicket> {
require(time > 0) { "Invalid ticket time: $time" } require(time >= 0) { "Invalid ticket time: $time" }
return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull() return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull()
} }

View File

@ -289,7 +289,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
// this handles case where player is removed from world and // this handles case where player is removed from world and
// instantly added back because new world rejected us // instantly added back because new world rejected us
world.mailbox.execute { remove0() } world.eventLoop.execute { remove0() }
} }
} }

View File

@ -0,0 +1,450 @@
package ru.dbotthepony.kstarbound.util
import org.apache.logging.log4j.LogManager
import java.util.PriorityQueue
import java.util.concurrent.Callable
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Delayed
import java.util.concurrent.Future
import java.util.concurrent.FutureTask
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.LockSupport
import java.util.function.Supplier
// I tried to make use of Netty's event loops, but they seem to be a bit overcomplicated
// if you try to use them by yourself :(
// so I made my own
open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorService {
private class ScheduledTask<T>(callable: Callable<T>, var executeAt: Long, val repeat: Boolean, val timeDelay: Long, val isFixedDelay: Boolean) : FutureTask<T>(callable), ScheduledFuture<T> {
override fun compareTo(other: Delayed): Int {
return getDelay(TimeUnit.NANOSECONDS).compareTo(other.getDelay(TimeUnit.NANOSECONDS))
}
override fun getDelay(unit: TimeUnit): Long {
return executeAt - System.nanoTime()
}
fun shouldEnqueue(): Boolean {
if (executeAt <= System.nanoTime())
return perform()
return true
}
fun perform(): Boolean {
if (repeat) {
if (isFixedDelay) {
// fixed delay
val deadlineMargin = executeAt - System.nanoTime()
if (deadlineMargin <= -5_000_000_000L) {
if (!IS_IN_IDE) // since this will get spammed if debugging using breakpoints
LOGGER.warn("Event loop missed scheduled deadline by ${-deadlineMargin / 1_000_000L} milliseconds")
}
runAndReset()
executeAt = System.nanoTime() + timeDelay
} else {
// fixed rate
val timeBefore = System.nanoTime()
var deadlineMargin = executeAt - System.nanoTime()
if (deadlineMargin <= -5_000_000_000L) {
if (!IS_IN_IDE) // since this will get spammed if debugging using breakpoints
LOGGER.warn("Event loop missed scheduled deadline by ${-deadlineMargin / 1_000_000L} milliseconds, clamping to 5 seconds")
deadlineMargin = -5_000_000_000L
}
runAndReset()
val now = System.nanoTime()
executeAt = now + timeDelay + deadlineMargin - (now - timeBefore)
}
return true
} else {
run()
return false
}
}
}
private class TaskPair<T>(val future: CompletableFuture<T>, var supplier: Callable<T>?)
private val eventQueue = LinkedBlockingQueue<TaskPair<*>>()
private val scheduledQueue = PriorityQueue<ScheduledTask<*>>()
private fun nextDeadline(): Long {
if (isShutdown)
return 0L
val poll = scheduledQueue.peek()
if (poll == null) {
return Long.MAX_VALUE
} else {
return poll.executeAt - System.nanoTime()
}
}
@Volatile
private var isShutdown = false
private var isRunning = true
private fun eventLoopIteration(): Boolean {
var executedAnything = false
val next = eventQueue.poll(nextDeadline(), TimeUnit.NANOSECONDS)
if (next != null) {
executedAnything = true
try {
val callable = next.supplier
if (callable != null) {
(next.future as CompletableFuture<Any?>).complete(callable.call())
}
} catch (err: Throwable) {
LOGGER.error("Error executing scheduled task", err)
try {
next.future.completeExceptionally(err)
} catch (err: Throwable) {
LOGGER.error("Caught an exception while propagating CompletableFuture to completeExceptionally stage", err)
}
}
}
if (scheduledQueue.isNotEmpty() && !isShutdown) {
val executed = ArrayList<ScheduledTask<*>>()
var lastSize: Int
do {
lastSize = executed.size
while (scheduledQueue.isNotEmpty() && scheduledQueue.peek()!!.executeAt <= System.nanoTime() && !isShutdown) {
executedAnything = true
val poll = scheduledQueue.poll()!!
if (poll.perform()) {
executed.add(poll)
}
}
} while (lastSize != executed.size && !isShutdown)
scheduledQueue.addAll(executed)
}
return executedAnything
}
final override fun run() {
while (isRunning) {
eventLoopIteration()
if (isShutdown && isRunning) {
while (eventLoopIteration()) {}
isRunning = false
performShutdown()
}
}
LOGGER.info("Thread ${this.name} stopped gracefully")
}
final override fun execute(command: Runnable) {
if (isShutdown)
throw RejectedExecutionException("EventLoop is shutting down")
if (currentThread() === this) {
command.run()
} else {
val future = CompletableFuture<Unit>()
val pair = TaskPair(future) { command.run() }
future.exceptionally {
pair.supplier = null
}
eventQueue.add(pair)
}
}
final override fun <T> submit(task: Callable<T>): CompletableFuture<T> {
if (isShutdown)
throw RejectedExecutionException("EventLoop is shutting down")
if (currentThread() === this) {
try {
return CompletableFuture.completedFuture(task.call())
} catch (err: Throwable) {
return CompletableFuture.failedFuture(err)
}
} else {
val future = CompletableFuture<T>()
val pair = TaskPair(future, task)
future.exceptionally {
pair.supplier = null
null
}
eventQueue.add(pair)
return future
}
}
fun <T> supplyAsync(task: Supplier<T>): CompletableFuture<T> {
return submit(task::get)
}
fun <T> supplyAsync(task: () -> T): CompletableFuture<T> {
return submit(task::invoke)
}
fun ensureSameThread() {
check(this === currentThread()) { "Performing non-threadsafe operation outside of event loop thread" }
}
fun isSameThread() = this === currentThread()
final override fun submit(task: Runnable): CompletableFuture<*> {
if (isShutdown)
throw RejectedExecutionException("EventLoop is shutting down")
if (currentThread() === this) {
try {
return CompletableFuture.completedFuture(task.run())
} catch (err: Throwable) {
return CompletableFuture.failedFuture<Unit>(err)
}
} else {
val future = CompletableFuture<Unit>()
val pair = TaskPair(future) { task.run() }
future.exceptionally {
pair.supplier = null
}
eventQueue.add(pair)
return future
}
}
final override fun <T> submit(task: Runnable, result: T): CompletableFuture<T> {
if (isShutdown)
throw RejectedExecutionException("EventLoop is shutting down")
if (currentThread() === this) {
try {
task.run()
return CompletableFuture.completedFuture(result)
} catch (err: Throwable) {
return CompletableFuture.failedFuture(err)
}
} else {
val future = CompletableFuture<T>()
val pair = TaskPair(future) { task.run(); result }
future.exceptionally {
pair.supplier = null
null
}
eventQueue.add(pair)
return future
}
}
final override fun <T> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> {
if (isShutdown)
throw RejectedExecutionException("EventLoop is shutting down")
return tasks.map { submit(it) }
}
final override fun <T> invokeAll(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): List<Future<T>> {
if (isShutdown)
throw RejectedExecutionException("EventLoop is shutting down")
val futures = tasks.map { submit(it) }
CompletableFuture.allOf(*futures.toTypedArray()).get(timeout, unit)
return futures
}
final override fun <T> invokeAny(tasks: Collection<Callable<T>>): T {
if (isShutdown)
throw RejectedExecutionException("EventLoop is shutting down")
return submit(tasks.first()).get()
}
final override fun <T> invokeAny(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): T {
if (isShutdown)
throw RejectedExecutionException("EventLoop is shutting down")
return submit(tasks.first()).get(timeout, unit)
}
final override fun shutdown() {
if (!isShutdown) {
isShutdown = true
if (currentThread() === this || state == State.NEW) {
while (eventLoopIteration()) {}
while (scheduledQueue.isNotEmpty()) {
val remove = scheduledQueue.remove()
try {
remove.cancel(false)
} catch (err: Throwable) {
LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err)
}
}
isRunning = false
performShutdown()
} else {
// wake up thread
eventQueue.add(TaskPair(CompletableFuture()) { })
}
}
}
protected open fun performShutdown() {
}
private fun shutdownNow0() {
while (eventQueue.isNotEmpty()) {
val remove = eventQueue.remove()
try {
remove.future.cancel(false)
} catch (err: Throwable) {
LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err)
}
}
while (scheduledQueue.isNotEmpty()) {
val remove = scheduledQueue.remove()
try {
remove.cancel(false)
} catch (err: Throwable) {
LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err)
}
}
isRunning = false
performShutdown()
}
final override fun shutdownNow(): List<Runnable> {
if (!isShutdown) {
isShutdown = true
if (currentThread() === this) {
shutdownNow0()
} else {
eventQueue.add(TaskPair(CompletableFuture()) { })
}
}
return emptyList()
}
final override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
// lazy wait loop
var budget = TimeUnit.NANOSECONDS.convert(timeout, unit)
var origin = System.nanoTime()
while (budget > 0L && isRunning) {
val new = System.nanoTime()
budget -= new - origin
origin = new
LockSupport.parkNanos(budget.coerceAtMost(500_000L))
if (interrupted()) {
return !isRunning
}
}
return !isRunning
}
final override fun isShutdown(): Boolean {
return isShutdown
}
final override fun isTerminated(): Boolean {
return !isRunning
}
final override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> {
val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false)
execute {
if (task.shouldEnqueue())
scheduledQueue.add(task)
}
return task
}
final override fun <V> schedule(callable: Callable<V>, delay: Long, unit: TimeUnit): ScheduledFuture<V> {
val task = ScheduledTask(callable, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false)
execute {
if (task.shouldEnqueue())
scheduledQueue.add(task)
}
return task
}
final override fun scheduleAtFixedRate(
command: Runnable,
initialDelay: Long,
period: Long,
unit: TimeUnit
): ScheduledFuture<*> {
val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(period, unit), false)
execute {
if (task.shouldEnqueue())
scheduledQueue.add(task)
}
return task
}
final override fun scheduleWithFixedDelay(
command: Runnable,
initialDelay: Long,
delay: Long,
unit: TimeUnit
): ScheduledFuture<*> {
val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(delay, unit), true)
execute {
if (task.shouldEnqueue())
scheduledQueue.add(task)
}
return task
}
companion object {
private val LOGGER = LogManager.getLogger()
private val IS_IN_IDE = java.lang.management.ManagementFactory.getRuntimeMXBean().inputArguments.toString().contains("-agentlib:jdwp")
}
}

View File

@ -3,10 +3,11 @@ package ru.dbotthepony.kstarbound.util
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import it.unimi.dsi.fastutil.HashCommon import it.unimi.dsi.fastutil.HashCommon
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.stream import ru.dbotthepony.kstarbound.stream
import java.lang.ref.ReferenceQueue import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.locks.LockSupport import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.math.log import kotlin.math.log
@ -19,46 +20,40 @@ class HashTableInterner<T : Any>(private val segmentBits: Int = log(Runtime.getR
companion object { companion object {
private val interners = ArrayList<WeakReference<HashTableInterner<*>>>() private val interners = ArrayList<WeakReference<HashTableInterner<*>>>()
private fun run() { private var wait = 1_000_000L
var wait = 1_000_000L private const val minWait = 1_000_000L
val minWait = 1_000_000L private const val maxWait = 1_000_000_000L
val maxWait = 1_000_000_000L
while (true) { private fun cleanupCycle() {
var any = 0 var any = 0
synchronized(interners) { synchronized(interners) {
val i = interners.iterator() val i = interners.iterator()
for (v in i) { for (v in i) {
val get = v.get() val get = v.get()
if (get == null) { if (get == null) {
i.remove() i.remove()
} else { } else {
for (segment in get.segments) { for (segment in get.segments) {
any += segment.cleanup() any += segment.cleanup()
}
} }
} }
} }
if (any != 0) {
wait = (wait - 1_000_000L * any).coerceAtLeast(minWait)
} else {
wait = (wait + 1_000_000L).coerceAtMost(maxWait)
}
LockSupport.parkNanos(wait)
} }
if (any != 0) {
wait = (wait - 1_000_000L * any).coerceAtLeast(minWait)
} else {
wait = (wait + 1_000_000L).coerceAtMost(maxWait)
}
Starbound.schedule(::cleanupCycle, wait, TimeUnit.NANOSECONDS)
} }
private val thread = Thread(::run, "Interner Cleanup")
init { init {
thread.priority = 2 cleanupCycle()
thread.isDaemon = true
thread.start()
} }
} }

View File

@ -83,6 +83,11 @@ fun staticRandomDouble(vararg values: Any): Double {
return staticRandom64(*values).ushr(11) * 1.1102230246251565E-16 return staticRandom64(*values).ushr(11) * 1.1102230246251565E-16
} }
fun staticRandomInt(origin: Int, bound: Int, vararg values: Any): Int {
val rand = staticRandomDouble(*values)
return origin + ((bound - origin) * rand).toInt()
}
fun staticRandom64(vararg values: Any): Long { fun staticRandom64(vararg values: Any): Long {
val digest = XXHash64(1997293021376312589L) val digest = XXHash64(1997293021376312589L)

View File

@ -2,7 +2,9 @@ package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
@ -52,7 +54,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
val worldBackgroundView = TileView.Background(worldView) val worldBackgroundView = TileView.Background(worldView)
val worldForegroundView = TileView.Foreground(worldView) val worldForegroundView = TileView.Foreground(worldView)
val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble()) val aabb = AABBi(pos.tile, pos.tile + Vector2i(width, height))
val aabbd = aabb.toDoubleAABB()
// TODO: maybe fit them into "width" and "height" variables added recently? // TODO: maybe fit them into "width" and "height" variables added recently?
protected val cells = lazy { protected val cells = lazy {
@ -158,11 +161,4 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
open fun tick() { open fun tick() {
} }
companion object {
private val aabbBase = AABB(
Vector2d.ZERO,
Vector2d(CHUNK_SIZE.toDouble(), CHUNK_SIZE.toDouble()),
)
}
} }

View File

@ -1,17 +1,22 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class Direction(val normal: Vector2d, override val jsonName: String) : IStringSerializable { enum class Direction(val normal: Vector2d, override val jsonName: String) : IStringSerializable {
LEFT(Vector2d.NEGATIVE_X, "left"), LEFT(Vector2d.NEGATIVE_X, "left") {
RIGHT(Vector2d.POSITIVE_X, "right"), override val opposite: Direction
get() = RIGHT
},
RIGHT(Vector2d.POSITIVE_X, "right") {
override val opposite: Direction
get() = LEFT
};
UP(Vector2d.POSITIVE_Y, "up"), abstract val opposite: Direction
DOWN(Vector2d.NEGATIVE_Y, "down"),
NONE(Vector2d.ZERO, "any"); operator fun unaryPlus() = opposite
override fun match(name: String): Boolean { override fun match(name: String): Boolean {
return name.lowercase() == jsonName return name.lowercase() == jsonName
@ -21,16 +26,3 @@ enum class Direction(val normal: Vector2d, override val jsonName: String) : IStr
val CODEC = StreamCodec.Enum(Direction::class.java) val CODEC = StreamCodec.Enum(Direction::class.java)
} }
} }
enum class Direction1D(val normal: Vector2d, override val jsonName: String) : IStringSerializable {
LEFT(Vector2d.NEGATIVE_X, "left"),
RIGHT(Vector2d.POSITIVE_X, "right");
override fun match(name: String): Boolean {
return name.lowercase() == jsonName
}
companion object {
val CODEC = StreamCodec.Enum(Direction1D::class.java)
}
}

View File

@ -0,0 +1,22 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class RayDirection(val normal: Vector2d, override val jsonName: String) : IStringSerializable {
LEFT(Vector2d.NEGATIVE_X, "left"),
RIGHT(Vector2d.POSITIVE_X, "right"),
UP(Vector2d.POSITIVE_Y, "up"),
DOWN(Vector2d.NEGATIVE_Y, "down"),
NONE(Vector2d.ZERO, "any");
override fun match(name: String): Boolean {
return name.lowercase() == jsonName
}
companion object {
val CODEC = StreamCodec.Enum(RayDirection::class.java)
}
}

View File

@ -19,7 +19,7 @@ data class RayCastResult(
) { ) {
constructor(startPos: Vector2d, direction: Vector2d) : this(listOf(), null, 0.0, startPos, startPos, direction) constructor(startPos: Vector2d, direction: Vector2d) : this(listOf(), null, 0.0, startPos, startPos, direction)
data class HitCell(val pos: Vector2i, val normal: Direction, val borderCross: Vector2d, val cell: AbstractCell) data class HitCell(val pos: Vector2i, val normal: RayDirection, val borderCross: Vector2d, val cell: AbstractCell)
} }
enum class RayFilterResult(val hit: Boolean, val write: Boolean) { enum class RayFilterResult(val hit: Boolean, val write: Boolean) {
@ -43,7 +43,7 @@ fun interface TileRayFilter {
/** /**
* [x] and [y] are wrapped around positions * [x] and [y] are wrapped around positions
*/ */
fun test(cell: AbstractCell, fraction: Double, x: Int, y: Int, normal: Direction, borderX: Double, borderY: Double): RayFilterResult fun test(cell: AbstractCell, fraction: Double, x: Int, y: Int, normal: RayDirection, borderX: Double, borderY: Double): RayFilterResult
} }
val NeverFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.CONTINUE } val NeverFilter = TileRayFilter { state, fraction, x, y, normal, borderX, borderY -> RayFilterResult.CONTINUE }
@ -67,9 +67,9 @@ fun ICellAccess.castRay(
val direction = (end - start).unitVector val direction = (end - start).unitVector
var result = filter.test(cell, 0.0, cellPosX, cellPosY, Direction.NONE, start.x, start.y) var result = filter.test(cell, 0.0, cellPosX, cellPosY, RayDirection.NONE, start.x, start.y)
if (result.write) hitTiles.add(RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), Direction.NONE, start, cell)) if (result.write) hitTiles.add(RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), RayDirection.NONE, start, cell))
if (result.hit) return RayCastResult(hitTiles, RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), Direction.NONE, start, cell), 0.0, start, start, direction) if (result.hit) return RayCastResult(hitTiles, RayCastResult.HitCell(Vector2i(cellPosX, cellPosY), RayDirection.NONE, start, cell), 0.0, start, start, direction)
val distance = start.distance(end) val distance = start.distance(end)
var travelled = 0.0 var travelled = 0.0
@ -79,8 +79,8 @@ fun ICellAccess.castRay(
val stepX: Int val stepX: Int
val stepY: Int val stepY: Int
val xNormal: Direction val xNormal: RayDirection
val yNormal: Direction val yNormal: RayDirection
var rayLengthX: Double var rayLengthX: Double
var rayLengthY: Double var rayLengthY: Double
@ -88,25 +88,25 @@ fun ICellAccess.castRay(
if (direction.x < 0.0) { if (direction.x < 0.0) {
stepX = -1 stepX = -1
rayLengthX = (start.x - cellPosX) * unitStepSizeX rayLengthX = (start.x - cellPosX) * unitStepSizeX
xNormal = Direction.RIGHT xNormal = RayDirection.RIGHT
} else { } else {
stepX = 1 stepX = 1
rayLengthX = (cellPosX - start.x + 1) * unitStepSizeX rayLengthX = (cellPosX - start.x + 1) * unitStepSizeX
xNormal = Direction.LEFT xNormal = RayDirection.LEFT
} }
if (direction.y < 0.0) { if (direction.y < 0.0) {
stepY = -1 stepY = -1
rayLengthY = (start.y - cellPosY) * unitStepSizeY rayLengthY = (start.y - cellPosY) * unitStepSizeY
yNormal = Direction.UP yNormal = RayDirection.UP
} else { } else {
stepY = 1 stepY = 1
rayLengthY = (cellPosY - start.y + 1) * unitStepSizeY rayLengthY = (cellPosY - start.y + 1) * unitStepSizeY
yNormal = Direction.DOWN yNormal = RayDirection.DOWN
} }
while (travelled < distance) { while (travelled < distance) {
val normal: Direction val normal: RayDirection
if (rayLengthX < rayLengthY) { if (rayLengthX < rayLengthY) {
cellPosX += stepX cellPosX += stepX

View File

@ -25,6 +25,7 @@ import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.MailboxExecutorService import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.util.ParallelPerform
@ -49,10 +50,9 @@ import java.util.stream.Stream
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.math.roundToInt import kotlin.math.roundToInt
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess, Closeable { abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess {
val background = TileView.Background(this) val background = TileView.Background(this)
val foreground = TileView.Foreground(this) val foreground = TileView.Foreground(this)
val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) }
val sky = Sky(template.skyParameters) val sky = Sky(template.skyParameters)
val geometry: WorldGeometry = template.geometry val geometry: WorldGeometry = template.geometry
@ -270,15 +270,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
broadcast(SetPlayerStartPacket(position, respawnInWorld)) broadcast(SetPlayerStartPacket(position, respawnInWorld))
} }
abstract fun isSameThread(): Boolean
fun ensureSameThread() {
check(isSameThread()) { "Trying to access $this from ${Thread.currentThread()}" }
}
open fun tick() { open fun tick() {
ticks++ ticks++
mailbox.executeQueuedTasks()
Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), { Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), {
if (!it.isRemote) { if (!it.isRemote) {
@ -286,29 +279,23 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
})).join() })).join()
mailbox.executeQueuedTasks()
entities.values.forEach { it.tick() } entities.values.forEach { it.tick() }
mailbox.executeQueuedTasks()
for (chunk in chunkMap.chunks()) for (chunk in chunkMap.chunks())
chunk.tick() chunk.tick()
mailbox.executeQueuedTasks()
sky.tick() sky.tick()
} }
protected abstract fun chunkFactory(pos: ChunkPos): ChunkType protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
override fun close() { abstract val eventLoop: BlockableEventLoop
mailbox.shutdownNow()
}
fun entitiesAtTile(pos: Vector2i, filter: Predicate<TileEntity> = Predicate { true }, distinct: Boolean = true): List<TileEntity> { fun entitiesAtTile(pos: Vector2i, filter: Predicate<TileEntity> = Predicate { true }, distinct: Boolean = true): List<TileEntity> {
return entityIndex.query( return entityIndex.query(
AABBi(pos, pos + Vector2i.POSITIVE_XY), AABBi(pos, pos + Vector2i.POSITIVE_XY),
distinct = distinct, distinct = distinct,
filter = { it is TileEntity && (pos - it.tilePosition) in it.occupySpaces && filter.test(it) } filter = { it is TileEntity && pos in it.occupySpaces && filter.test(it) }
) as List<TileEntity> ) as List<TileEntity>
} }

View File

@ -20,5 +20,6 @@ interface ICellAccess {
* whenever cell was set * whenever cell was set
*/ */
fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean
fun setCell(pos: IStruct2i, cell: AbstractCell): Boolean = setCell(pos.component1(), pos.component2(), cell)
} }

View File

@ -12,6 +12,13 @@ data class MutableLiquidState(
override var pressure: Float = 0f, override var pressure: Float = 0f,
override var isInfinite: Boolean = false, override var isInfinite: Boolean = false,
) : AbstractLiquidState() { ) : AbstractLiquidState() {
fun from(other: AbstractLiquidState) {
state = other.state
level = other.level
pressure = other.pressure
isInfinite = other.isInfinite
}
fun read(stream: DataInputStream): MutableLiquidState { fun read(stream: DataInputStream): MutableLiquidState {
state = Registries.liquid[stream.readUnsignedByte()] ?: BuiltinMetaMaterials.NO_LIQUID state = Registries.liquid[stream.readUnsignedByte()] ?: BuiltinMetaMaterials.NO_LIQUID
level = stream.readFloat() level = stream.readFloat()

View File

@ -112,7 +112,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
if (entityID == 0) if (entityID == 0)
entityID = world.nextEntityID.incrementAndGet() entityID = world.nextEntityID.incrementAndGet()
world.ensureSameThread() world.eventLoop.ensureSameThread()
check(!world.entities.containsKey(entityID)) { "Duplicate entity ID: $entityID" } check(!world.entities.containsKey(entityID)) { "Duplicate entity ID: $entityID" }
@ -127,7 +127,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
fun remove(isDeath: Boolean = false) { fun remove(isDeath: Boolean = false) {
val world = innerWorld ?: throw IllegalStateException("Not in world") val world = innerWorld ?: throw IllegalStateException("Not in world")
world.ensureSameThread() world.eventLoop.ensureSameThread()
mailbox.shutdownNow() mailbox.shutdownNow()
check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" } check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" }

View File

@ -15,7 +15,6 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.util.GameTimer import ru.dbotthepony.kstarbound.util.GameTimer
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.Direction1D
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -27,16 +26,16 @@ class ActorMovementController() : MovementController() {
var controlDown: Boolean = false var controlDown: Boolean = false
var lastControlDown: Boolean = false var lastControlDown: Boolean = false
var controlFly: Vector2d? = null var controlFly: Vector2d? = null
var controlFace: Direction1D? = null var controlFace: Direction? = null
var isWalking: Boolean by networkGroup.add(networkedBoolean()) var isWalking: Boolean by networkGroup.add(networkedBoolean())
private set private set
var isRunning: Boolean by networkGroup.add(networkedBoolean()) var isRunning: Boolean by networkGroup.add(networkedBoolean())
private set private set
var movingDirection: Direction1D by networkGroup.add(networkedEnum(Direction1D.RIGHT)) var movingDirection: Direction by networkGroup.add(networkedEnum(Direction.RIGHT))
private set private set
var facingDirection: Direction1D by networkGroup.add(networkedEnum(Direction1D.RIGHT)) var facingDirection: Direction by networkGroup.add(networkedEnum(Direction.RIGHT))
private set private set
var isCrouching: Boolean by networkGroup.add(networkedBoolean()) var isCrouching: Boolean by networkGroup.add(networkedBoolean())
@ -282,7 +281,7 @@ class ActorMovementController() : MovementController() {
isLiquidMovement = liquidPercentage >= (actorMovementParameters.minimumLiquidPercentage ?: 0.0) isLiquidMovement = liquidPercentage >= (actorMovementParameters.minimumLiquidPercentage ?: 0.0)
val liquidImpedance = liquidPercentage * (actorMovementParameters.liquidImpedance ?: 0.0) val liquidImpedance = liquidPercentage * (actorMovementParameters.liquidImpedance ?: 0.0)
var updatedMovingDirection: Direction1D? = null var updatedMovingDirection: Direction? = null
val isRunning = controlRun && !movementModifiers.runningSuppressed val isRunning = controlRun && !movementModifiers.runningSuppressed
if (controlFly != null) { if (controlFly != null) {
@ -297,9 +296,9 @@ class ActorMovementController() : MovementController() {
approachVelocity(flyVelocity * movementModifiers.speedModifier, movementParameters.airForce ?: 0.0) approachVelocity(flyVelocity * movementModifiers.speedModifier, movementParameters.airForce ?: 0.0)
if (flyVelocity.x > 0.0) if (flyVelocity.x > 0.0)
updatedMovingDirection = Direction1D.RIGHT updatedMovingDirection = Direction.RIGHT
else if (flyVelocity.x < 0.0) else if (flyVelocity.x < 0.0)
updatedMovingDirection = Direction1D.LEFT updatedMovingDirection = Direction.LEFT
groundMovementSustainTimer = GameTimer(0.0) groundMovementSustainTimer = GameTimer(0.0)
} else { } else {
@ -379,10 +378,10 @@ class ActorMovementController() : MovementController() {
} }
if (controlMove == Direction.LEFT) { if (controlMove == Direction.LEFT) {
updatedMovingDirection = Direction1D.LEFT updatedMovingDirection = Direction.LEFT
targetHorizontalAmbulatingVelocity = -1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier targetHorizontalAmbulatingVelocity = -1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier
} else if (controlMove == Direction.RIGHT) { } else if (controlMove == Direction.RIGHT) {
updatedMovingDirection = Direction1D.RIGHT updatedMovingDirection = Direction.RIGHT
targetHorizontalAmbulatingVelocity = 1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier targetHorizontalAmbulatingVelocity = 1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier
} }

View File

@ -1,7 +1,7 @@
package ru.dbotthepony.kstarbound.world.entities package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.world.Direction1D import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
class PathController(val world: World<*, *>, var edgeTimer: Double = 0.0) { class PathController(val world: World<*, *>, var edgeTimer: Double = 0.0) {
@ -9,7 +9,7 @@ class PathController(val world: World<*, *>, var edgeTimer: Double = 0.0) {
private set private set
var endPosition: Vector2d? = null var endPosition: Vector2d? = null
private set private set
var controlFace: Direction1D? = null var controlFace: Direction? = null
private set private set
fun reset() { fun reset() {

View File

@ -22,7 +22,7 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
abstract val metaBoundingBox: AABB abstract val metaBoundingBox: AABB
private fun updateSpatialIndex() { protected open fun updateSpatialIndex() {
val spatialEntry = spatialEntry ?: return val spatialEntry = spatialEntry ?: return
spatialEntry.fixture.move(metaBoundingBox + position) spatialEntry.fixture.move(metaBoundingBox + position)
} }
@ -57,7 +57,16 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
override val position: Vector2d override val position: Vector2d
get() = Vector2d(xTilePosition.toDouble(), yTilePosition.toDouble()) get() = Vector2d(xTilePosition.toDouble(), yTilePosition.toDouble())
/**
* Tile positions this entity occupies in world (in world coordinates, not relative)
*/
abstract val occupySpaces: Set<Vector2i> abstract val occupySpaces: Set<Vector2i>
/**
* Tile positions this entity is rooted in world (in world coordinates, not relative)
*/
abstract val roots: Set<Vector2i>
abstract fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean abstract fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean
override fun onJoinWorld(world: World<*, *>) { override fun onJoinWorld(world: World<*, *>) {

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.entities.tile
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
@ -22,6 +23,7 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.guava.immutableSet
import ru.dbotthepony.kommons.io.RGBACodec import ru.dbotthepony.kommons.io.RGBACodec
import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.Vector2iCodec import ru.dbotthepony.kommons.io.Vector2iCodec
@ -32,7 +34,6 @@ import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.world.ClientWorld
import ru.dbotthepony.kstarbound.defs.DamageSource import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractAction
@ -250,7 +251,28 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
val drawables: List<Drawable> by drawablesCache val drawables: List<Drawable> by drawablesCache
override val occupySpaces get() = orientation?.occupySpaces ?: setOf() private val occupySpaces0 = LazyData {
(orientation?.occupySpaces ?: setOf()).stream().map { world.geometry.wrap(it + tilePosition) }.collect(ImmutableSet.toImmutableSet())
}
override val occupySpaces: ImmutableSet<Vector2i> by occupySpaces0
override val roots: Set<Vector2i>
get() = setOf()
private val anchorPositions0 = LazyData {
immutableSet {
orientation?.anchors?.forEach { accept(it.pos + tilePosition) }
}
}
val anchorPositions: ImmutableSet<Vector2i> by anchorPositions0
override fun updateSpatialIndex() {
super.updateSpatialIndex()
occupySpaces0.invalidate()
anchorPositions0.invalidate()
}
fun getRenderParam(key: String): String? { fun getRenderParam(key: String): String? {
return localRenderKeys[key] ?: networkedRenderKeys[key] ?: "default" return localRenderKeys[key] ?: networkedRenderKeys[key] ?: "default"

View File

@ -18,7 +18,6 @@ import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readCollection import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readVector2d import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.readVector2f import ru.dbotthepony.kommons.io.readVector2f
@ -31,6 +30,7 @@ import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.LinkedList
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
@ -245,7 +245,7 @@ class Poly private constructor(val edges: ImmutableList<Line2d>, val vertices: I
return wn return wn
} }
fun contains(point: IStruct2d): Boolean { operator fun contains(point: IStruct2d): Boolean {
return windingNumber(point) != 0 return windingNumber(point) != 0
} }
@ -433,5 +433,42 @@ class Poly private constructor(val edges: ImmutableList<Line2d>, val vertices: I
return null return null
} }
fun quickhull(points: Collection<Vector2d>): Poly {
val sorted = ArrayList(points)
sorted.sortWith { o1, o2 ->
val cmp = o1.x.compareTo(o2.x)
if (cmp == 0) o1.y.compareTo(o2.y) else cmp
}
// calculates on which side b is lying on o->a line
fun cross(o: Vector2d, a: Vector2d, b: Vector2d): Double {
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
}
val lowerPoints = LinkedList<Vector2d>()
val upperPoints = LinkedList<Vector2d>()
for (point in sorted) {
while (lowerPoints.size > 1 && cross(lowerPoints[lowerPoints.size - 2], lowerPoints[lowerPoints.size - 1], point) <= 0)
lowerPoints.removeLast()
lowerPoints.add(point)
}
for (point in sorted.asReversed()) {
while (upperPoints.size > 1 && cross(upperPoints[upperPoints.size - 2], upperPoints[upperPoints.size - 1], point) <= 0)
upperPoints.removeLast()
upperPoints.add(point)
}
lowerPoints.removeLast()
upperPoints.removeLast()
lowerPoints.addAll(upperPoints)
return Poly(lowerPoints)
}
} }
} }

View File

@ -90,9 +90,9 @@ class IslandSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParame
} }
private val cache = Caffeine.newBuilder() private val cache = Caffeine.newBuilder()
.maximumSize(2048L) .maximumSize(512L)
.executor(Starbound.EXECUTOR) //.executor(Starbound.EXECUTOR)
.scheduler(Scheduler.systemScheduler()) //.scheduler(Starbound)
.build<Int, Column>(::compute) .build<Int, Column>(::compute)
override fun get(x: Int, y: Int): Double { override fun get(x: Int, y: Int): Double {

View File

@ -58,11 +58,11 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
} }
private val layers = Caffeine.newBuilder() private val layers = Caffeine.newBuilder()
.maximumSize(2048L) .maximumSize(256L)
.softValues() .softValues()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(2))
.scheduler(Scheduler.systemScheduler()) //.scheduler(Starbound)
.executor(Starbound.EXECUTOR) //.executor(Starbound.EXECUTOR)
.build<Int, Layer>(::Layer) .build<Int, Layer>(::Layer)
private inner class Sector(val sector: Vector2i) { private inner class Sector(val sector: Vector2i) {
@ -127,11 +127,11 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
} }
private val sectors = Caffeine.newBuilder() private val sectors = Caffeine.newBuilder()
.maximumSize(2048L) .maximumSize(64L)
.softValues() .softValues()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(2))
.scheduler(Scheduler.systemScheduler()) //.scheduler(Starbound)
.executor(Starbound.EXECUTOR) //.executor(Starbound.EXECUTOR)
.build<Vector2i, Sector>(::Sector) .build<Vector2i, Sector>(::Sector)
override fun get(x: Int, y: Int): Double { override fun get(x: Int, y: Int): Double {

View File

@ -178,11 +178,11 @@ class WormCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters)
} }
private val sectors = Caffeine.newBuilder() private val sectors = Caffeine.newBuilder()
.maximumSize(2048L) .maximumSize(64L)
.softValues() .softValues()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(2))
.scheduler(Scheduler.systemScheduler()) //.scheduler(Starbound)
.executor(Starbound.EXECUTOR) //.executor(Starbound.EXECUTOR)
.build<Vector2i, Sector>(::Sector) .build<Vector2i, Sector>(::Sector)
override fun get(x: Int, y: Int): Double { override fun get(x: Int, y: Int): Double {

View File

@ -1,11 +1,18 @@
package ru.dbotthepony.kstarbound.test package ru.dbotthepony.kstarbound.test
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.physics.Poly
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
object MathTests { object MathTests {
@Test @Test
@ -47,4 +54,30 @@ object MathTests {
check(roundTowardsPositiveInfinity(-1.1) == -1) check(roundTowardsPositiveInfinity(-1.1) == -1)
check(roundTowardsPositiveInfinity(-1.6) == -1) check(roundTowardsPositiveInfinity(-1.6) == -1)
} }
@Test
@DisplayName("Quickhull test")
fun quickhullTest() {
// this test is fragile since it is at discretion of "random" how it goes
val random = random(28383384L)
val vertices = ArrayList<Vector2d>()
for (i in 0 until 1000) {
val angle = random.nextDouble(PI * 2.0)
vertices.add(Vector2d(sin(angle), cos(angle)))
}
val hull = Poly.quickhull(vertices)
val testPoints = ArrayList<Vector2d>()
for (i in 0 until 1000) {
val angle = random.nextDouble(PI * 2.0)
testPoints.add(Vector2d(sin(angle) * 0.8, cos(angle) * 0.8))
}
for (point in testPoints) {
assertTrue(point in hull)
}
}
} }