Terrain generation, staged async chunk loading

This commit is contained in:
DBotThePony 2024-04-05 18:30:45 +07:00
parent 31ea958304
commit c3c928de92
Signed by: DBot
GPG Key ID: DCC23B5715498507
71 changed files with 2560 additions and 678 deletions

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.11.1 kommonsVersion=2.12.1
ffiVersion=2.2.13 ffiVersion=2.2.13
lwjglVersion=3.3.0 lwjglVersion=3.3.0

View File

@ -8,6 +8,7 @@ import ru.dbotthepony.kstarbound.defs.ClientConfig
import ru.dbotthepony.kstarbound.defs.CurrencyDefinition import ru.dbotthepony.kstarbound.defs.CurrencyDefinition
import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.UniverseServerConfig import ru.dbotthepony.kstarbound.defs.UniverseServerConfig
import ru.dbotthepony.kstarbound.defs.WorldServerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig
@ -16,6 +17,7 @@ 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.CelestialNames import ru.dbotthepony.kstarbound.defs.world.CelestialNames
import ru.dbotthepony.kstarbound.defs.world.DungeonWorldsConfig import ru.dbotthepony.kstarbound.defs.world.DungeonWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.InstanceWorldConfig
import ru.dbotthepony.kstarbound.defs.world.SkyGlobalConfig import ru.dbotthepony.kstarbound.defs.world.SkyGlobalConfig
import ru.dbotthepony.kstarbound.defs.world.SystemWorldConfig import ru.dbotthepony.kstarbound.defs.world.SystemWorldConfig
import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig
@ -72,6 +74,9 @@ object Globals {
var universeServer by Delegates.notNull<UniverseServerConfig>() var universeServer by Delegates.notNull<UniverseServerConfig>()
private set private set
var worldServer by Delegates.notNull<WorldServerConfig>()
private set
var currencies by Delegates.notNull<ImmutableMap<String, CurrencyDefinition>>() var currencies by Delegates.notNull<ImmutableMap<String, CurrencyDefinition>>()
private set private set
@ -90,6 +95,9 @@ object Globals {
var celestialNames by Delegates.notNull<CelestialNames>() var celestialNames by Delegates.notNull<CelestialNames>()
private set private set
var instanceWorlds by Delegates.notNull<ImmutableMap<String, InstanceWorldConfig>>()
private set
private object EmptyTask : ForkJoinTask<Unit>() { private object EmptyTask : ForkJoinTask<Unit>() {
private fun readResolve(): Any = EmptyTask private fun readResolve(): Any = EmptyTask
override fun getRawResult() { override fun getRawResult() {
@ -138,6 +146,7 @@ object Globals {
tasks.add(load("/world_template.config", ::worldTemplate)) tasks.add(load("/world_template.config", ::worldTemplate))
tasks.add(load("/sky.config", ::sky)) tasks.add(load("/sky.config", ::sky))
tasks.add(load("/universe_server.config", ::universeServer)) tasks.add(load("/universe_server.config", ::universeServer))
tasks.add(load("/worldserver.config", ::worldServer))
tasks.add(load("/player.config", ::player)) tasks.add(load("/player.config", ::player))
tasks.add(load("/systemworld.config", ::systemWorld)) tasks.add(load("/systemworld.config", ::systemWorld))
tasks.add(load("/celestial.config", ::celestialBaseInformation)) tasks.add(load("/celestial.config", ::celestialBaseInformation))
@ -152,6 +161,7 @@ object Globals {
tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter())) tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter()))
tasks.add(load("/currencies.config", ::currencies, Starbound.gson.mapAdapter())) tasks.add(load("/currencies.config", ::currencies, Starbound.gson.mapAdapter()))
tasks.add(load("/system_objects.config", ::systemObjects, Starbound.gson.mapAdapter())) tasks.add(load("/system_objects.config", ::systemObjects, Starbound.gson.mapAdapter()))
tasks.add(load("/instance_worlds.config", ::instanceWorlds, Starbound.gson.mapAdapter()))
return tasks return tasks
} }

View File

@ -30,101 +30,15 @@ fun main() {
Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak")) Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak"))
Starbound.doBootstrap() Starbound.doBootstrap()
if (false) {
val data = ServerUniverse()
val t = System.nanoTime()
val result = Starbound.COROUTINES.future {
val systems = data.findSystems(AABBi(Vector2i(-50, -50), Vector2i(50, 50)), setOf("whitestar"))
for (system in systems) {
for (children in data.children(system)) {
if (children.isPlanet) {
val params = data.parameters(children)!!
if (params.visitableParameters != null) {
//val write = params.visitableParameters!!.toJson(false)
//println(write)
//println(Starbound.gson.fromJson(write, VisitableWorldParameters::class.java))
}
}
}
}
systems
}.get()
println(System.nanoTime() - t)
data.close()
return
}
/*val db0 = BTreeDB5(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world"))
val db2 = BTreeDB6.create(File("testdb.bdb"), sync = false)
for (key in db0.findAllKeys()) {
db2.write(key, db0.read(key).get())
}
db2.close()*/
LOGGER.info("Running LWJGL ${Version.getVersion()}") LOGGER.info("Running LWJGL ${Version.getVersion()}")
//Thread.sleep(6_000L)
//val db = BTreeDB6(File("testdb.bdb"))
val db = BTreeDB5(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world"))
//val db = BTreeDB(File("world.world"))
val meta = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(db.read(ByteKey(0, 0, 0, 0, 0)).get()), Inflater())))
println(meta.readInt())
println(meta.readInt())
// println(VersionedJson(meta)) // println(VersionedJson(meta))
val client = StarboundClient.create().get() val client = StarboundClient.create().get()
//val client2 = StarboundClient.create().get()
//val world = ServerWorld.load(server, LegacyWorldStorage.file(db)).get()
//Starbound.addFilePath(File("./unpacked_assets/"))
/*for (folder in File("J:\\Steam\\steamapps\\workshop\\content\\211820").list()!!) {
val f = File("J:\\Steam\\steamapps\\workshop\\content\\211820\\$folder\\contents.pak")
if (f.exists()) {
Starbound.addPakPath(f)
}
}*/
//Starbound.addPakPath(File("packed.pak"))
Starbound.initializeGame() Starbound.initializeGame()
Starbound.mailboxInitialized.submit { Starbound.mailboxInitialized.submit {
val server = IntegratedStarboundServer(client, File("./")) val server = IntegratedStarboundServer(client, File("./"))
val world = ServerWorld.load(server, LegacyWorldStorage.file(db)).get()
world.thread.start()
//ply = PlayerEntity(client.world!!)
//ply!!.position = Vector2d(225.0, 680.0)
//ply!!.spawn()
//client.world!!.parallax = Starbound.parallaxAccess["garden"]
//client.connectToLocalServer(server.channels.createLocalChannel())
//client.connectToRemoteServer(InetSocketAddress("127.0.0.1", 21025))
//client2.connectToLocalServer(server.channels.createLocalChannel(), UUID.randomUUID())
server.channels.createChannel(InetSocketAddress(21060)) server.channels.createChannel(InetSocketAddress(21060))
} }
//ent.position += Vector2d(y = 14.0, x = -10.0)
client.camera.pos = Vector2d(238.0, 685.0)
//client2.camera.pos = Vector2d(238.0, 685.0)
//client.camera.pos = Vector2f(0f, 0f)
//ent.spawn()
} }

View File

@ -141,7 +141,7 @@ object Registries {
tasks.addAll(loadItemDefinitions(fileTree)) tasks.addAll(loadItemDefinitions(fileTree))
tasks.addAll(loadTerrainSelectors(fileTree["terrain"] ?: listOf())) tasks.addAll(loadTerrainSelectors(fileTree))
tasks.addAll(loadRegistry(tiles, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId))) tasks.addAll(loadRegistry(tiles, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId)))
tasks.addAll(loadRegistry(tileModifiers, fileTree["matmod"] ?: listOf(), key(TileModifierDefinition::modName, TileModifierDefinition::modId))) tasks.addAll(loadRegistry(tileModifiers, fileTree["matmod"] ?: listOf(), key(TileModifierDefinition::modName, TileModifierDefinition::modId)))
@ -238,21 +238,38 @@ object Registries {
} }
} }
private fun loadTerrainSelectors(files: Collection<IStarboundFile>): List<Future<*>> { private fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?) {
return files.map { listedFile -> try {
Starbound.EXECUTOR.submit { val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true })
try { val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field")
val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true }) val factory = TerrainSelectorType.factory(json, false, type)
val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field")
val factory = TerrainSelectorType.factory(json, false)
terrainSelectors.add { terrainSelectors.add {
terrainSelectors.add(name, factory) terrainSelectors.add(name, factory)
}
} catch (err: Exception) {
LOGGER.error("Loading terrain selector $listedFile", err)
}
} }
} catch (err: Exception) {
LOGGER.error("Loading terrain selector $listedFile", err)
} }
} }
private fun loadTerrainSelectors(files: Map<String, Collection<IStarboundFile>>): List<Future<*>> {
val tasks = ArrayList<Future<*>>()
tasks.addAll((files["terrain"] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit {
loadTerrainSelector(listedFile, null)
}
})
// legacy files
for (type in TerrainSelectorType.entries) {
tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit {
loadTerrainSelector(listedFile, type)
}
})
}
return tasks
}
} }

View File

@ -71,6 +71,7 @@ class Registry<T : Any>(val name: String) {
abstract val json: JsonElement abstract val json: JsonElement
abstract val file: IStarboundFile? abstract val file: IStarboundFile?
abstract val registry: Registry<T> abstract val registry: Registry<T>
@Deprecated("Careful! This might be confused with 'isMeta' of some entries (such as tiles, in such case use entry.isMeta)")
abstract val isBuiltin: Boolean abstract val isBuiltin: Boolean
abstract val ref: Ref<T> abstract val ref: Ref<T>

View File

@ -55,7 +55,24 @@ class RegistryTypeAdapterFactory<S : Any>(private val registry: Registry<S>, pri
private inner class RefImpl(gson: Gson) : TypeAdapter<Registry.Ref<S>>() { private inner class RefImpl(gson: Gson) : TypeAdapter<Registry.Ref<S>>() {
override fun write(out: JsonWriter, value: Registry.Ref<S>?) { override fun write(out: JsonWriter, value: Registry.Ref<S>?) {
if (value != null) { if (value != null) {
value.key.map(out::value, out::value) if (Starbound.IS_LEGACY_JSON) {
if (value.isPresent) {
if (value.entry!!.id != null) {
out.value(value.entry!!.id)
} else {
out.value(value.entry!!.key)
}
} else {
value.key.map(out::value, out::value)
}
} else {
// always write string ID if we know it
if (value.isPresent) {
out.value(value.entry!!.key)
} else {
value.key.map(out::value, out::value)
}
}
} else { } else {
out.nullValue() out.nullValue()
} }

View File

@ -21,7 +21,6 @@ import ru.dbotthepony.kommons.gson.Vector3iTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.defs.image.Image
@ -62,6 +61,7 @@ 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
import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.* import java.io.*

View File

@ -24,7 +24,6 @@ import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.matrix.Matrix3f import ru.dbotthepony.kommons.matrix.Matrix3f
import ru.dbotthepony.kommons.matrix.Matrix3fStack import ru.dbotthepony.kommons.matrix.Matrix3fStack
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2f import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector4f import ru.dbotthepony.kommons.vector.Vector4f
@ -65,6 +64,7 @@ import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.util.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.ExecutionSpinner 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.LightCalculator import ru.dbotthepony.kstarbound.world.LightCalculator

View File

@ -11,7 +11,6 @@ import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2f import ru.dbotthepony.kommons.vector.Vector2f
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.ConfiguredMesh import ru.dbotthepony.kstarbound.client.render.ConfiguredMesh
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
@ -24,7 +23,6 @@ import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
@ -33,7 +31,6 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.function.Consumer import java.util.function.Consumer
import kotlin.concurrent.withLock
class ClientWorld( class ClientWorld(
val client: StarboundClient, val client: StarboundClient,
@ -164,7 +161,7 @@ class ClientWorld(
for (x in 0 until renderRegionWidth) { for (x in 0 until renderRegionWidth) {
for (y in 0 until renderRegionHeight) { for (y in 0 until renderRegionHeight) {
view.getCell(x, y).liquid.def?.let { liquidTypes.add(it) } view.getCell(x, y).liquid.state?.let { liquidTypes.add(it) }
} }
} }
@ -177,7 +174,7 @@ class ClientWorld(
for (y in 0 until renderRegionHeight) { for (y in 0 until renderRegionHeight) {
val state = view.getCell(x, y) val state = view.getCell(x, y)
if (state.liquid.def == type) { if (state.liquid.state == type) {
builder.vertex(x.toFloat(), y.toFloat()) builder.vertex(x.toFloat(), y.toFloat())
builder.vertex(x.toFloat() + 1f, y.toFloat()) builder.vertex(x.toFloat() + 1f, y.toFloat())
builder.vertex(x.toFloat() + 1f, y.toFloat() + 1f) builder.vertex(x.toFloat() + 1f, y.toFloat() + 1f)

View File

@ -0,0 +1,10 @@
package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
class WorldServerConfig(
val playerStartRegionMaximumTries: Int = 1,
val playerStartRegionMaximumVerticalSearch: Int = 1,
val playerStartRegionSize: Vector2d,
)

View File

@ -4,12 +4,54 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMap import it.unimi.dsi.fastutil.objects.Object2DoubleMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMaps import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
val Registry.Ref<TileDefinition>.isEmptyTile: Boolean
get() = entry == BuiltinMetaMaterials.EMPTY
val Registry.Ref<TileDefinition>.isNullTile: Boolean
get() = entry == BuiltinMetaMaterials.NULL || entry == null
val Registry.Ref<TileDefinition>.isObjectSolidTile: Boolean
get() = entry == BuiltinMetaMaterials.OBJECT_SOLID
val Registry.Ref<TileDefinition>.isObjectPlatformTile: Boolean
get() = entry == BuiltinMetaMaterials.OBJECT_PLATFORM
val Registry.Entry<TileDefinition>.isEmptyTile: Boolean
get() = this == BuiltinMetaMaterials.EMPTY
val Registry.Entry<TileDefinition>.isNullTile: Boolean
get() = this == BuiltinMetaMaterials.NULL
val Registry.Entry<TileDefinition>.isObjectSolidTile: Boolean
get() = this == BuiltinMetaMaterials.OBJECT_SOLID
val Registry.Entry<TileDefinition>.isObjectPlatformTile: Boolean
get() = this == BuiltinMetaMaterials.OBJECT_PLATFORM
val Registry.Entry<TileDefinition>.supportsModifiers: Boolean
get() = !value.isMeta && value.supportsMods
fun Registry.Entry<TileDefinition>.supportsModifier(modifier: Registry.Entry<TileModifierDefinition>): Boolean {
return !value.isMeta && value.supportsMods && !modifier.value.isMeta
}
fun Registry.Entry<TileDefinition>.supportsModifier(modifier: Registry.Ref<TileModifierDefinition>): Boolean {
return !value.isMeta && value.supportsMods && modifier.isPresent && !modifier.value!!.isMeta
}
val Registry.Entry<LiquidDefinition>.isEmptyLiquid: Boolean
get() = this == BuiltinMetaMaterials.NO_LIQUID
val Registry.Ref<LiquidDefinition>.isEmptyLiquid: Boolean
get() = entry == null || entry == BuiltinMetaMaterials.NO_LIQUID
object BuiltinMetaMaterials { object BuiltinMetaMaterials {
private fun make(id: Int, name: String, collisionType: CollisionType) = Registries.tiles.add(name, id, TileDefinition( private fun make(id: Int, name: String, collisionType: CollisionType) = Registries.tiles.add(name, id, TileDefinition(
materialId = id, materialId = id,
@ -19,6 +61,7 @@ object BuiltinMetaMaterials {
renderTemplate = AssetReference.empty(), renderTemplate = AssetReference.empty(),
renderParameters = RenderParameters.META, renderParameters = RenderParameters.META,
isMeta = true, isMeta = true,
supportsMods = false,
collisionKind = collisionType, collisionKind = collisionType,
damageTable = AssetReference(TileDamageConfig( damageTable = AssetReference(TileDamageConfig(
damageFactors = ImmutableMap.of(), damageFactors = ImmutableMap.of(),
@ -27,7 +70,16 @@ object BuiltinMetaMaterials {
totalHealth = Double.MAX_VALUE, totalHealth = Double.MAX_VALUE,
harvestLevel = Int.MAX_VALUE, harvestLevel = Int.MAX_VALUE,
)) ))
)) ), isBuiltin = true)
private fun makeMod(id: Int, name: String) = Registries.tileModifiers.add(name, id, TileModifierDefinition(
modId = id,
modName = "metamod:$name",
descriptionData = ThingDescription.EMPTY,
renderParameters = RenderParameters.META,
isMeta = true,
renderTemplate = AssetReference.empty(),
), isBuiltin = true)
/** /**
* air * air
@ -37,7 +89,7 @@ object BuiltinMetaMaterials {
/** /**
* not set / out of bounds * not set / out of bounds
*/ */
val NULL = make(65534, "null", CollisionType.BLOCK) val NULL = make(65534, "null", CollisionType.NULL)
val STRUCTURE = make(65533, "structure", CollisionType.BLOCK) val STRUCTURE = make(65533, "structure", CollisionType.BLOCK)
val BIOME = make(65527, "biome", CollisionType.BLOCK) val BIOME = make(65527, "biome", CollisionType.BLOCK)
@ -64,4 +116,18 @@ object BuiltinMetaMaterials {
OBJECT_SOLID, OBJECT_SOLID,
OBJECT_PLATFORM, OBJECT_PLATFORM,
) )
val EMPTY_MOD = makeMod(65535, "empty")
val BIOME_MOD = makeMod(65534, "biome")
val UNDERGROUND_BIOME_MOD = makeMod(65533, "underground_biome")
val NO_LIQUID = Registries.liquid.add("empty", 0, LiquidDefinition(
name = "metaliquid:empty",
liquidId = 0,
color = RGBAColor.TRANSPARENT_BLACK,
texture = "",
isMeta = true,
textureMovementFactor = 0.0,
bottomLightMix = RGBAColor.TRANSPARENT_BLACK
), isBuiltin = true)
} }

View File

@ -5,6 +5,7 @@ import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
@JsonFactory @JsonFactory
data class LiquidDefinition( data class LiquidDefinition(
@ -19,6 +20,8 @@ data class LiquidDefinition(
val texture: String, val texture: String,
val bottomLightMix: RGBAColor, val bottomLightMix: RGBAColor,
val textureMovementFactor: Double, val textureMovementFactor: Double,
@JsonIgnore
val isMeta: Boolean = false,
) { ) {
@JsonFactory @JsonFactory
data class Interaction(val liquid: Int, val liquidResult: Int? = null, val materialResult: String? = null) { data class Interaction(val liquid: Int, val liquidResult: Int? = null, val materialResult: String? = null) {

View File

@ -21,7 +21,6 @@ data class TileDefinition(
val footstepSound: Either<ImmutableList<String>, String> = Either.left(ImmutableList.of()), val footstepSound: Either<ImmutableList<String>, String> = Either.left(ImmutableList.of()),
val miningSounds: Either<ImmutableList<String>, String> = Either.left(ImmutableList.of()), val miningSounds: Either<ImmutableList<String>, String> = Either.left(ImmutableList.of()),
val blocksLiquidFlow: Boolean = true,
val soil: Boolean = false, val soil: Boolean = false,
val category: String, val category: String,
@ -44,6 +43,12 @@ data class TileDefinition(
override val renderTemplate: AssetReference<RenderTemplate>, override val renderTemplate: AssetReference<RenderTemplate>,
override val renderParameters: RenderParameters, override val renderParameters: RenderParameters,
val cascading: Boolean = false,
val falling: Boolean = false,
val foregroundOnly: Boolean = collisionKind != CollisionType.BLOCK,
val supportsMods: Boolean = !(falling || !cascading || collisionKind != CollisionType.BLOCK),
val blocksLiquidFlow: Boolean = collisionKind.isSolidCollision,
) : IRenderableTile, IThingWithDescription by descriptionData { ) : IRenderableTile, IThingWithDescription by descriptionData {
init { init {
require(materialId > 0) { "Invalid tile ID $materialId" } require(materialId > 0) { "Invalid tile ID $materialId" }

View File

@ -7,6 +7,7 @@ import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.json.builder.JsonFactory 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.builder.JsonIgnore
@JsonFactory @JsonFactory
data class TileModifierDefinition( data class TileModifierDefinition(
@ -28,6 +29,11 @@ data class TileModifierDefinition(
@JsonFlat @JsonFlat
val descriptionData: ThingDescription, val descriptionData: ThingDescription,
// meta tiles are treated uniquely by many systems
// such as players/projectiles unable to break them, etc
@JsonIgnore
val isMeta: Boolean = false,
override val renderTemplate: AssetReference<RenderTemplate>, override val renderTemplate: AssetReference<RenderTemplate>,
override val renderParameters: RenderParameters override val renderParameters: RenderParameters
) : IRenderableTile, IThingWithDescription by descriptionData { ) : IRenderableTile, IThingWithDescription by descriptionData {

View File

@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.random.RandomGenerator
import kotlin.properties.Delegates import kotlin.properties.Delegates
class AsteroidsWorldParameters : VisitableWorldParameters() { class AsteroidsWorldParameters : VisitableWorldParameters() {
@ -93,8 +94,7 @@ class AsteroidsWorldParameters : VisitableWorldParameters() {
stream.writeColor(ambientLightLevel) stream.writeColor(ambientLightLevel)
} }
override fun createLayout(seed: Long): WorldLayout { override fun createLayout(random: RandomGenerator): WorldLayout {
val random = random(seed)
val terrain = Globals.asteroidWorlds.terrains.random(random) val terrain = Globals.asteroidWorlds.terrains.random(random)
val layout = WorldLayout() val layout = WorldLayout()
@ -145,9 +145,7 @@ class AsteroidsWorldParameters : VisitableWorldParameters() {
} }
companion object { companion object {
fun generate(seed: Long): AsteroidsWorldParameters { fun generate(random: RandomGenerator): AsteroidsWorldParameters {
val random = random(seed)
val parameters = AsteroidsWorldParameters() val parameters = AsteroidsWorldParameters()
parameters.threatLevel = random.nextRange(Globals.asteroidWorlds.threatRange) parameters.threatLevel = random.nextRange(Globals.asteroidWorlds.threatRange)
@ -168,5 +166,9 @@ class AsteroidsWorldParameters : VisitableWorldParameters() {
return parameters return parameters
} }
fun generate(seed: Long): AsteroidsWorldParameters {
return generate(random(seed))
}
} }
} }

View File

@ -1,6 +1,9 @@
package ru.dbotthepony.kstarbound.defs.world package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -18,4 +21,50 @@ data class Biome(
val surfacePlaceables: BiomePlaceables = BiomePlaceables(), val surfacePlaceables: BiomePlaceables = BiomePlaceables(),
val undergroundPlaceables: BiomePlaceables = BiomePlaceables(), val undergroundPlaceables: BiomePlaceables = BiomePlaceables(),
val parallax: Parallax? = null, val parallax: Parallax? = null,
) ) {
@JvmName("hueShiftTile")
fun hueShift(block: Registry.Entry<TileDefinition>): Float {
if (block == mainBlock.native.entry) {
return hueShift.toFloat()
}
return 0f
}
@JvmName("hueShiftTile")
fun hueShift(block: Registry.Ref<TileDefinition>): Float {
if (block == mainBlock.native) {
return hueShift.toFloat()
}
return 0f
}
@JvmName("hueShiftMod")
fun hueShift(mod: Registry.Entry<TileModifierDefinition>): Float {
if (
mod == surfacePlaceables.grassMod.native.entry ||
mod == undergroundPlaceables.grassMod.native.entry ||
mod == surfacePlaceables.ceilingGrassMod.native.entry ||
mod == undergroundPlaceables.ceilingGrassMod.native.entry
) {
return hueShift.toFloat()
}
return 0f
}
@JvmName("hueShiftMod")
fun hueShift(mod: Registry.Ref<TileModifierDefinition>): Float {
if (
mod == surfacePlaceables.grassMod.native ||
mod == undergroundPlaceables.grassMod.native ||
mod == surfacePlaceables.ceilingGrassMod.native ||
mod == undergroundPlaceables.ceilingGrassMod.native
) {
return hueShift.toFloat()
}
return 0f
}
}

View File

@ -20,6 +20,7 @@ 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
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -30,8 +31,8 @@ import java.util.stream.Stream
@JsonFactory @JsonFactory
data class BiomePlaceables( data class BiomePlaceables(
val grassMod: NativeLegacy.TileMod = NativeLegacy.TileMod(null as Registry.Ref<TileModifierDefinition>?), val grassMod: NativeLegacy.TileMod = NativeLegacy.TileMod(BuiltinMetaMaterials.EMPTY_MOD),
val ceilingGrassMod: NativeLegacy.TileMod = NativeLegacy.TileMod(null as Registry.Ref<TileModifierDefinition>?), val ceilingGrassMod: NativeLegacy.TileMod = NativeLegacy.TileMod(BuiltinMetaMaterials.EMPTY_MOD),
val grassModDensity: Double = 0.0, val grassModDensity: Double = 0.0,
val ceilingGrassModDensity: Double = 0.0, val ceilingGrassModDensity: Double = 0.0,
val itemDistributions: ImmutableList<DistributionItem> = ImmutableList.of(), val itemDistributions: ImmutableList<DistributionItem> = ImmutableList.of(),

View File

@ -217,7 +217,7 @@ data class BiomePlaceablesDefinition(
name.entry!!, name.entry!!,
biome.random.nextDouble(-1.0, 1.0) * baseHueShiftMax, biome.random.nextDouble(-1.0, 1.0) * baseHueShiftMax,
mod, mod,
biome.random.nextDouble(-1.0 * 1.0) * modHueShiftMax biome.random.nextDouble(-1.0, 1.0) * modHueShiftMax
) )
) )
} }

View File

@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.random.RandomGenerator
import kotlin.properties.Delegates import kotlin.properties.Delegates
class FloatingDungeonWorldParameters : VisitableWorldParameters() { class FloatingDungeonWorldParameters : VisitableWorldParameters() {
@ -44,9 +45,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
override val type: VisitableWorldParametersType override val type: VisitableWorldParametersType
get() = VisitableWorldParametersType.FLOATING_DUNGEON get() = VisitableWorldParametersType.FLOATING_DUNGEON
override fun createLayout(seed: Long): WorldLayout { override fun createLayout(random: RandomGenerator): WorldLayout {
val random = random(seed)
val layout = WorldLayout() val layout = WorldLayout()
layout.worldSize = worldSize layout.worldSize = worldSize
@ -146,6 +145,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
val config = Globals.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!") val config = Globals.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!")
val parameters = FloatingDungeonWorldParameters() val parameters = FloatingDungeonWorldParameters()
parameters.worldSize = config.worldSize
parameters.threatLevel = config.threatLevel parameters.threatLevel = config.threatLevel
parameters.gravity = config.gravity.map({ Vector2d(y = it) }, { it }) parameters.gravity = config.gravity.map({ Vector2d(y = it) }, { it })
parameters.airless = config.airless parameters.airless = config.airless

View File

@ -0,0 +1,39 @@
package ru.dbotthepony.kstarbound.defs.world
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class InstanceWorldConfig(
val worldProperties: JsonObject = JsonObject(),
val spawningEnabled: Boolean = true,
val persistent: Boolean = false,
val useUniverseClock: Boolean = false,
val skyParameters: SkyParameters = SkyParameters(),
val disableDeathDrops: Boolean = false,
val beamUpRule: BeamUpRule? = null,
val type: String,
val seed: Long? = null,
// terrestrial
val planetType: String? = null,
val planetSize: String? = null,
// floating dungeon
val dungeonWorld: String? = null,
) {
init {
when (type.lowercase()) {
"terrestrial" -> {
requireNotNull(planetSize) { "World has 'type' specified as $type, but is missing 'planetSize' parameter" }
requireNotNull(planetType) { "World has 'type' specified as $type, but is missing 'planetType' parameter" }
}
"floatingdungeon" -> requireNotNull(dungeonWorld) { "World has 'type' specified as $type, but is missing 'dungeonWorld' parameter" }
"asteroids" -> {}
else -> throw IllegalArgumentException("Invalid instance world type $type")
}
}
}

View File

@ -54,7 +54,7 @@ class Parallax(
val baseCount: Int = 1, val baseCount: Int = 1,
val modCount: Int = 0, val modCount: Int = 0,
val parallax: Either<Double, Vector2d>, val parallax: Either<Double, Vector2d>,
val repeatY: Boolean = true, val repeatY: Boolean = false,
val repeatX: Boolean = true, val repeatX: Boolean = true,
val tileLimitTop: Double? = null, val tileLimitTop: Double? = null,
val tileLimitBottom: Double? = null, val tileLimitBottom: Double? = null,
@ -117,7 +117,7 @@ class Parallax(
return Layer( return Layer(
parallaxValue = parallax.map({ Vector2d(it, it) }, { it }), parallaxValue = parallax.map({ Vector2d(it, it) }, { it }),
repeat = Either.left(repeatX to repeatY), repeat = (if (repeatX) 1 else 0) to (if (repeatY) 1 else 0),
tileLimitTop = tileLimitTop, tileLimitTop = tileLimitTop,
tileLimitBottom = tileLimitBottom, tileLimitBottom = tileLimitBottom,
verticalOrigin = verticalOrigin, verticalOrigin = verticalOrigin,
@ -141,7 +141,8 @@ class Parallax(
val textures: ImmutableList<String>, val textures: ImmutableList<String>,
val alpha: Double = 1.0, val alpha: Double = 1.0,
val parallaxValue: Vector2d, val parallaxValue: Vector2d,
val repeat: Either<Pair<Boolean, Boolean>, Pair<Int, Int>>, // treat as booleans because original engine stuff.
val repeat: Pair<Int, Int>,
val tileLimitTop: Double? = null, val tileLimitTop: Double? = null,
val tileLimitBottom: Double? = null, val tileLimitBottom: Double? = null,
val verticalOrigin: Double, val verticalOrigin: Double,
@ -155,7 +156,7 @@ class Parallax(
) { ) {
fun fadeToSkyColor(color: RGBAColor) { fun fadeToSkyColor(color: RGBAColor) {
if (fadePercent > 0.0) { if (fadePercent > 0.0) {
directives = directives.add("fade", color.toHexStringRGB() + "=$fadePercent") directives = directives.add("fade", color.toHexStringRGB().substring(1) + "=$fadePercent")
} }
} }
} }

View File

@ -178,7 +178,7 @@ data class SkyParameters(
// If the planet has water, then draw the corresponding water image as the // If the planet has water, then draw the corresponding water image as the
// base layer, otherwise use the bottom most mask image. // base layer, otherwise use the bottom most mask image.
val surfaceLiquid = visitable.surfaceLiquid?.map({ Registries.liquid[it] }, { Registries.liquid[it] })?.key val surfaceLiquid = visitable.surfaceLiquid.entry?.key
if (surfaceLiquid != null && liquidImages.isNotBlank()) { if (surfaceLiquid != null && liquidImages.isNotBlank()) {
layers.add(Layer(liquidImages.replace("<liquid>", surfaceLiquid), imageScale)) layers.add(Layer(liquidImages.replace("<liquid>", surfaceLiquid), imageScale))
@ -293,7 +293,7 @@ data class SkyParameters(
val biomeHueShift = "?hueshift=${visitable.hueShift.toInt()}" val biomeHueShift = "?hueshift=${visitable.hueShift.toInt()}"
val surfaceLiquid = visitable.surfaceLiquid?.map({ Registries.liquid[it] }, { Registries.liquid[it] })?.key val surfaceLiquid = visitable.surfaceLiquid.entry?.key
if (surfaceLiquid != null) { if (surfaceLiquid != null) {
val random = random(parameters.seed) val random = random(parameters.seed)

View File

@ -21,9 +21,13 @@ import ru.dbotthepony.kommons.vector.Vector2d
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.Registries import ru.dbotthepony.kstarbound.Registries
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
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readVector2d import ru.dbotthepony.kstarbound.io.readVector2d
@ -74,9 +78,9 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
val bgOreSelector: String, val bgOreSelector: String,
val subBlockSelector: String, val subBlockSelector: String,
val caveLiquid: Either<Int, String>?, val caveLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref,
val caveLiquidSeedDensity: Double, val caveLiquidSeedDensity: Double,
val oceanLiquid: Either<Int, String>?, val oceanLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref,
val oceanLiquidLevel: Int, val oceanLiquidLevel: Int,
val encloseLiquids: Boolean, val encloseLiquids: Boolean,
@ -92,10 +96,10 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
stream.readInternedString(), stream.readInternedString(),
stream.readInternedString(), stream.readInternedString(),
Either.left(stream.readUnsignedByte()), Registries.liquid.ref(stream.readUnsignedByte()),
stream.readFloat().toDouble(), stream.readFloat().toDouble(),
Either.left(stream.readUnsignedByte()), Registries.liquid.ref(stream.readUnsignedByte()),
stream.readInt(), stream.readInt(),
stream.readBoolean(), stream.readBoolean(),
@ -112,9 +116,9 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
stream.writeBinaryString(bgOreSelector) stream.writeBinaryString(bgOreSelector)
stream.writeBinaryString(subBlockSelector) stream.writeBinaryString(subBlockSelector)
stream.writeByte(caveLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0) stream.writeByte(caveLiquid.entry?.id ?: 0)
stream.writeFloat(caveLiquidSeedDensity.toFloat()) stream.writeFloat(caveLiquidSeedDensity.toFloat())
stream.writeByte(oceanLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0) stream.writeByte(oceanLiquid.entry?.id ?: 0)
stream.writeInt(oceanLiquidLevel) stream.writeInt(oceanLiquidLevel)
stream.writeBoolean(encloseLiquids) stream.writeBoolean(encloseLiquids)
@ -179,7 +183,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
val read = Starbound.gson.fromJson(data, StoreData::class.java) val read = Starbound.gson.fromJson(data, StoreData::class.java)
primaryBiome = read.primaryBiome primaryBiome = read.primaryBiome
surfaceLiquid = read.surfaceLiquid surfaceLiquid = read.surfaceLiquid?.map({ Registries.liquid.ref(it) }, { Registries.liquid.ref(it) }) ?: BuiltinMetaMaterials.NO_LIQUID.ref
sizeName = read.sizeName sizeName = read.sizeName
hueShift = read.hueShift hueShift = read.hueShift
skyColoring = read.skyColoring skyColoring = read.skyColoring
@ -203,12 +207,10 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
// original engine operate on liquids solely with IDs // original engine operate on liquids solely with IDs
// and we also need to network this json to legacy clients. // and we also need to network this json to legacy clients.
// what a shame :JC:. // what a shame :JC:.
if (this.surfaceLiquid == null) { if (isLegacy) {
primarySurfaceLiquid = if (isLegacy) Either.left(0) else null primarySurfaceLiquid = this.surfaceLiquid.entry?.id?.let { Either.left(it) } ?: Either.left(0)
} else if (isLegacy) {
primarySurfaceLiquid = this.surfaceLiquid!!.map({ it }, { Registries.liquid.get(it)!!.id })?.let { Either.left(it) }
} else { } else {
primarySurfaceLiquid = this.surfaceLiquid primarySurfaceLiquid = this.surfaceLiquid.entry?.let { Either.right(it.key) } ?: this.surfaceLiquid.key.swap()
} }
val store = StoreData( val store = StoreData(
@ -255,7 +257,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
var primaryBiome: String by Delegates.notNull() var primaryBiome: String by Delegates.notNull()
private set private set
var surfaceLiquid: Either<Int, String>? = null var surfaceLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref
private set private set
var sizeName: String by Delegates.notNull() var sizeName: String by Delegates.notNull()
private set private set
@ -292,7 +294,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
super.read0(stream) super.read0(stream)
primaryBiome = stream.readInternedString() primaryBiome = stream.readInternedString()
surfaceLiquid = Either.left(stream.readUnsignedByte()) surfaceLiquid = Registries.liquid.ref(stream.readUnsignedByte())
sizeName = stream.readInternedString() sizeName = stream.readInternedString()
hueShift = stream.readFloat().toDouble() hueShift = stream.readFloat().toDouble()
skyColoring = SkyColoring.read(stream, true) skyColoring = SkyColoring.read(stream, true)
@ -314,7 +316,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
super.write0(stream) super.write0(stream)
stream.writeBinaryString(primaryBiome) stream.writeBinaryString(primaryBiome)
stream.writeByte(surfaceLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0) stream.writeByte(surfaceLiquid.entry?.id ?: 0)
stream.writeBinaryString(sizeName) stream.writeBinaryString(sizeName)
stream.writeFloat(hueShift.toFloat()) stream.writeFloat(hueShift.toFloat())
skyColoring.write(stream, true) skyColoring.write(stream, true)
@ -335,12 +337,10 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
} }
// why // why
override fun createLayout(seed: Long): WorldLayout { override fun createLayout(random: RandomGenerator): WorldLayout {
val layout = WorldLayout() val layout = WorldLayout()
layout.worldSize = worldSize layout.worldSize = worldSize
val random = random(seed)
fun addLayer(layer: Layer) { fun addLayer(layer: Layer) {
fun bake(region: Region): WorldLayout.RegionParameters { fun bake(region: Region): WorldLayout.RegionParameters {
return WorldLayout.RegionParameters( return WorldLayout.RegionParameters(
@ -386,7 +386,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
layout.regionBlending = blendSize layout.regionBlending = blendSize
layout.blockNoise = blockNoiseConfig?.build(random) layout.blockNoise = blockNoiseConfig?.build(random)
layout.blendNoise = blendNoiseConfig?.let { AbstractPerlinNoise.of(it).also { it.init(seed) } } layout.blendNoise = blendNoiseConfig?.let { AbstractPerlinNoise.of(it).also { it.init(random.nextLong()) } }
layout.finalize(skyColoring.mainColor) layout.finalize(skyColoring.mainColor)
@ -470,9 +470,9 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
fgOreSelector = fgOreSelector, fgOreSelector = fgOreSelector,
bgOreSelector = bgOreSelector, bgOreSelector = bgOreSelector,
subBlockSelector = subBlockSelector, subBlockSelector = subBlockSelector,
caveLiquid = caveLiquid?.let { Either.right(it) }, caveLiquid = caveLiquid?.let { Registries.liquid.ref(it) } ?: BuiltinMetaMaterials.NO_LIQUID.ref,
caveLiquidSeedDensity = caveLiquidSeedDensity, caveLiquidSeedDensity = caveLiquidSeedDensity,
oceanLiquid = oceanLiquid?.let { Either.right(it) }, oceanLiquid = oceanLiquid?.let { Registries.liquid.ref(it) } ?: BuiltinMetaMaterials.NO_LIQUID.ref,
oceanLiquidLevel = oceanLiquidLevel, oceanLiquidLevel = oceanLiquidLevel,
encloseLiquids = encloseLiquids, encloseLiquids = encloseLiquids,
fillMicrodungeons = fillMicrodungeons, fillMicrodungeons = fillMicrodungeons,
@ -565,7 +565,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
parameters.threatLevel = threadLevel parameters.threatLevel = threadLevel
parameters.typeName = typeName parameters.typeName = typeName
parameters.worldSize = params.size parameters.worldSize = params.size
parameters.gravity = Vector2d(y = -random.nextRange(params.gravityRange)) parameters.gravity = Vector2d(y = 10.0)
parameters.airless = primaryBiome.value.airless parameters.airless = primaryBiome.value.airless
parameters.environmentStatusEffects = primaryBiome.value.statusEffects parameters.environmentStatusEffects = primaryBiome.value.statusEffects
parameters.overrideTech = params.overrideTech parameters.overrideTech = params.overrideTech
@ -578,7 +578,11 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
parameters.sizeName = sizeName parameters.sizeName = sizeName
parameters.hueShift = primaryBiome.value.hueShift(random) parameters.hueShift = primaryBiome.value.hueShift(random)
parameters.surfaceLiquid = surfaceLayer.primaryRegion.oceanLiquid ?: surfaceLayer.primaryRegion.caveLiquid parameters.surfaceLiquid = surfaceLayer.primaryRegion.oceanLiquid
if (parameters.surfaceLiquid.isEmptyLiquid)
parameters.surfaceLiquid = surfaceLayer.primaryRegion.caveLiquid
parameters.skyColoring = primaryBiome.value.skyColoring(random) parameters.skyColoring = primaryBiome.value.skyColoring(random)
parameters.dayLength = random.nextRange(params.dayLengthRange) parameters.dayLength = random.nextRange(params.dayLengthRange)

View File

@ -83,7 +83,7 @@ data class TreeVariant(
ephemeral = data.value.ephemeral, ephemeral = data.value.ephemeral,
tileDamageParameters = (data.value.damageTable?.value ?: Globals.treeDamage).copy(totalHealth = data.value.health), tileDamageParameters = (data.value.damageTable?.value ?: Globals.treeDamage).copy(totalHealth = data.value.health),
foliageSettings = JsonNull.INSTANCE, foliageSettings = JsonObject(),
foliageDropConfig = JsonObject(), foliageDropConfig = JsonObject(),
foliageName = "", foliageName = "",
foliageDirectory = "/", foliageDirectory = "/",

View File

@ -1,10 +1,14 @@
package ru.dbotthepony.kstarbound.defs.world package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
@JsonFactory @JsonFactory
data class BlockNoiseConfig( data class BlockNoiseConfig(
@ -30,4 +34,17 @@ data class BlockNoise(
val verticalNoise: AbstractPerlinNoise, val verticalNoise: AbstractPerlinNoise,
val xNoise: AbstractPerlinNoise, val xNoise: AbstractPerlinNoise,
val yNoise: AbstractPerlinNoise, val yNoise: AbstractPerlinNoise,
) ) {
fun apply(x: Int, y: Int, width: Int, height: Int): Vector2i {
val angle = (x.toDouble() / width.toDouble()) * 2.0 * PI
// eek?
val xc = sin(angle) / (2.0 * PI) * width
val zc = cos(angle) / (2.0 * PI) * width
return Vector2i(
(x + horizontalNoise[y.toDouble()] + xNoise[xc, y.toDouble(), zc]).toInt(),
(y + verticalNoise[xc, zc] + yNoise[xc, y.toDouble(), zc]).toInt().coerceIn(0, height),
)
}
}

View File

@ -39,8 +39,10 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonObject import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.util.random.random
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.random.RandomGenerator
import kotlin.properties.Delegates import kotlin.properties.Delegates
// uint8_t // uint8_t
@ -100,7 +102,6 @@ enum class VisitableWorldParametersType(override val jsonName: String, val token
// if done as immutable class // if done as immutable class
abstract class VisitableWorldParameters { abstract class VisitableWorldParameters {
var threatLevel: Double = 0.0 var threatLevel: Double = 0.0
protected set
var typeName: String by Delegates.notNull() var typeName: String by Delegates.notNull()
protected set protected set
var worldSize: Vector2i by Delegates.notNull() var worldSize: Vector2i by Delegates.notNull()
@ -116,17 +117,18 @@ abstract class VisitableWorldParameters {
var globalDirectives: Set<String>? = null var globalDirectives: Set<String>? = null
protected set protected set
var beamUpRule: BeamUpRule by Delegates.notNull() var beamUpRule: BeamUpRule by Delegates.notNull()
protected set
var disableDeathDrops: Boolean = false var disableDeathDrops: Boolean = false
protected set
var terraformed: Boolean = false var terraformed: Boolean = false
protected set
var worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.NONE var worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.NONE
protected set protected set
var weatherPool: WeightedList<String>? = null var weatherPool: WeightedList<String>? = null
protected set protected set
abstract fun createLayout(seed: Long): WorldLayout fun createLayout(seed: Long): WorldLayout {
return createLayout(random(seed))
}
abstract fun createLayout(random: RandomGenerator): WorldLayout
abstract val type: VisitableWorldParametersType abstract val type: VisitableWorldParametersType

View File

@ -22,12 +22,16 @@ import ru.dbotthepony.kommons.vector.Vector2d
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.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.world.terrain.AbstractTerrainSelector import ru.dbotthepony.kstarbound.world.terrain.AbstractTerrainSelector
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.ListInterner import ru.dbotthepony.kstarbound.util.ListInterner
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.nextRange import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -36,9 +40,9 @@ import kotlin.properties.Delegates
/** /**
* While this class seems redundant, it is not. * While this class seems redundant, it is not.
* *
* Every world must be represented by layers and regions. * Every world must be represented by layers and regions,
* And some worlds don't have visitable world parameters because they are custom-made, * hence this class serves as mutual point of reference between different
* such as dungeon worlds. * world types.
* *
* Layer is a "line" than spans from 0 to world width, and has determined height. * Layer is a "line" than spans from 0 to world width, and has determined height.
* Region, on other hand, is 1D section (slice) of layer, representing unique biome. * Region, on other hand, is 1D section (slice) of layer, representing unique biome.
@ -68,6 +72,13 @@ class WorldLayout {
val playerStartSearchRegions = ArrayList<AABBi>() val playerStartSearchRegions = ArrayList<AABBi>()
val layers = ArrayList<Layer>() val layers = ArrayList<Layer>()
var loopX = true
var loopY = false
val worldGeometry by lazy {
WorldGeometry(worldSize, loopX, loopY)
}
private object StartingRegionsToken : TypeToken<ArrayList<AABBi>>() private object StartingRegionsToken : TypeToken<ArrayList<AABBi>>()
@JsonFactory @JsonFactory
@ -94,14 +105,57 @@ class WorldLayout {
return data return data
} }
fun findContainingCell(x: Int): Pair<Int, Int> {
val wx = worldGeometry.x.cell(x)
if (wx < boundaries.first()) {
return 0 to wx
} else if (wx > boundaries.last()) {
return boundaries.size to wx
} else {
return (boundaries.indexOfLast { it <= wx } + 1) to wx
}
}
fun leftCell(index: Int, x: Int): Pair<Int, Int> {
if (index == 0) {
// wrap around
return (cells.size - 1) to (x + worldSize.x)
} else {
// normal left cell
return (index - 1) to x
}
}
fun rightCell(index: Int, x: Int): Pair<Int, Int> {
if (index >= cells.size - 1) {
// wrap around
return 0 to (x - worldSize.x)
} else {
// normal right cell
return (index + 1) to x
}
}
fun weight(cellIndex: Int, x: Int): Double {
val xMin = if (cellIndex > 0) boundaries.getInt(cellIndex - 1) else 0
val xMax = if (cellIndex < boundaries.size) boundaries.getInt(cellIndex) else worldSize.x
if (x > (xMin + xMax) / 2.0) {
return (0.5 - (x - xMax) / regionBlending).coerceIn(0.0, 1.0)
} else {
return (0.5 - (xMin - x) / regionBlending).coerceIn(0.0, 1.0)
}
}
} }
@JsonFactory @JsonFactory
data class RegionLiquids( data class RegionLiquids(
val caveLiquid: Either<Int, String>? = null, val caveLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref,
val caveLiquidSeedDensity: Double = 0.0, val caveLiquidSeedDensity: Double = 0.0,
val oceanLiquid: Either<Int, String>? = null, val oceanLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref,
val oceanLiquidLevel: Int = 0, val oceanLiquidLevel: Int = 0,
val encloseLiquids: Boolean = false, val encloseLiquids: Boolean = false,
@ -109,9 +163,9 @@ class WorldLayout {
) { ) {
fun toLegacy(): RegionLiquidsLegacy { fun toLegacy(): RegionLiquidsLegacy {
return RegionLiquidsLegacy( return RegionLiquidsLegacy(
caveLiquid = caveLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0, caveLiquid = caveLiquid.value?.liquidId ?: 0,
caveLiquidSeedDensity = caveLiquidSeedDensity, caveLiquidSeedDensity = caveLiquidSeedDensity,
oceanLiquid = oceanLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0, oceanLiquid = oceanLiquid.value?.liquidId ?: 0,
oceanLiquidLevel = oceanLiquidLevel, oceanLiquidLevel = oceanLiquidLevel,
encloseLiquids = encloseLiquids, encloseLiquids = encloseLiquids,
fillMicrodungeons = fillMicrodungeons, fillMicrodungeons = fillMicrodungeons,
@ -143,7 +197,20 @@ class WorldLayout {
val subBlockSelectorIndexes: IntArrayList, val subBlockSelectorIndexes: IntArrayList,
val foregroundOreSelectorIndexes: IntArrayList, val foregroundOreSelectorIndexes: IntArrayList,
val backgroundOreSelectorIndexes: IntArrayList, val backgroundOreSelectorIndexes: IntArrayList,
) ) {
init {
check(terrainSelectorIndex != -1) { "terrainSelectorIndex is -1" }
check(foregroundCaveSelectorIndex != -1) { "foregroundCaveSelectorIndex is -1" }
check(backgroundCaveSelectorIndex != -1) { "backgroundCaveSelectorIndex is -1" }
check(blockBiomeIndex != -1) { "blockBiomeIndex is -1" }
check(environmentBiomeIndex != -1) { "environmentBiomeIndex is -1" }
check(subBlockSelectorIndexes.none { it == -1 }) { "subBlockSelectorIndexes contains -1" }
check(foregroundOreSelectorIndexes.none { it == -1 }) { "foregroundOreSelectorIndexes contains -1" }
check(backgroundOreSelectorIndexes.none { it == -1 }) { "backgroundOreSelectorIndexes contains -1" }
}
}
inner class Region( inner class Region(
val terrainSelector: AbstractTerrainSelector<*>?, val terrainSelector: AbstractTerrainSelector<*>?,
@ -159,16 +226,37 @@ class WorldLayout {
val regionLiquids: RegionLiquids, val regionLiquids: RegionLiquids,
) { ) {
val blockBiomeIndex: Int
var environmentBiomeIndex: Int
init {
if (blockBiome == null) {
blockBiomeIndex = 0
} else {
val index = biomes.list.indexOf(blockBiome)
check(index != -1) { "blockBiome != null but indexOf returned -1" }
blockBiomeIndex = index + 1
}
if (environmentBiome == null) {
environmentBiomeIndex = 0
} else {
val index = biomes.list.indexOf(environmentBiome)
check(index != -1) { "environmentBiome != null but indexOf returned -1" }
environmentBiomeIndex = index + 1
}
}
fun toJson(isLegacy: Boolean): JsonObject { fun toJson(isLegacy: Boolean): JsonObject {
val data = SerializedRegion( val data = SerializedRegion(
terrainSelectorIndex = terrainSelectors.list.indexOf(terrainSelector), terrainSelectorIndex = terrainSelectors.list.indexOf(terrainSelector) + 1,
foregroundCaveSelectorIndex = terrainSelectors.list.indexOf(foregroundCaveSelector), foregroundCaveSelectorIndex = terrainSelectors.list.indexOf(foregroundCaveSelector) + 1,
backgroundCaveSelectorIndex = terrainSelectors.list.indexOf(backgroundCaveSelector), backgroundCaveSelectorIndex = terrainSelectors.list.indexOf(backgroundCaveSelector) + 1,
blockBiomeIndex = biomes.list.indexOf(blockBiome), blockBiomeIndex = biomes.list.indexOf(blockBiome) + 1,
environmentBiomeIndex = biomes.list.indexOf(environmentBiome), environmentBiomeIndex = biomes.list.indexOf(environmentBiome) + 1,
subBlockSelectorIndexes = subBlockSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) }.filter { it != -1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll), subBlockSelectorIndexes = subBlockSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) + 1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll),
foregroundOreSelectorIndexes = foregroundOreSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) }.filter { it != -1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll), foregroundOreSelectorIndexes = foregroundOreSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) + 1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll),
backgroundOreSelectorIndexes = backgroundOreSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) }.filter { it != -1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll), backgroundOreSelectorIndexes = backgroundOreSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) + 1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll),
) )
val liquidData = (if (isLegacy) Starbound.gson.toJsonTree(regionLiquids.toLegacy()) else Starbound.gson.toJsonTree(regionLiquids)) as JsonObject val liquidData = (if (isLegacy) Starbound.gson.toJsonTree(regionLiquids.toLegacy()) else Starbound.gson.toJsonTree(regionLiquids)) as JsonObject
@ -205,13 +293,16 @@ class WorldLayout {
val biomes: List<Biome>, val biomes: List<Biome>,
val terrainSelectors: List<AbstractTerrainSelector<*>>, val terrainSelectors: List<AbstractTerrainSelector<*>>,
val layers: JsonArray, val layers: JsonArray,
val loopX: Boolean = true,
val loopY: Boolean = false,
) )
fun toJson(): JsonObject { fun toJson(): JsonObject {
return Starbound.gson.toJsonTree(SerializedForm( return Starbound.gson.toJsonTree(SerializedForm(
worldSize, regionBlending, blockNoise, blendNoise, worldSize, regionBlending, blockNoise, blendNoise,
playerStartSearchRegions, biomes.list, terrainSelectors.list, playerStartSearchRegions, biomes.list, terrainSelectors.list,
layers = layers.stream().map { it.toJson(Starbound.IS_LEGACY_JSON) }.collect(JsonArrayCollector) layers = layers.stream().map { it.toJson(Starbound.IS_LEGACY_JSON) }.collect(JsonArrayCollector),
loopX, loopY
)) as JsonObject )) as JsonObject
} }
@ -222,6 +313,8 @@ class WorldLayout {
regionBlending = load.regionBlending regionBlending = load.regionBlending
blockNoise = load.blockNoise blockNoise = load.blockNoise
blendNoise = load.blendNoise blendNoise = load.blendNoise
loopX = load.loopX
loopY = load.loopY
playerStartSearchRegions.addAll(load.playerStartSearchRegions) playerStartSearchRegions.addAll(load.playerStartSearchRegions)
load.layers.forEach { load.layers.forEach {
@ -232,15 +325,18 @@ class WorldLayout {
datalayer.cells.forEach { datalayer.cells.forEach {
val region = Starbound.gson.fromJson(it, SerializedRegion::class.java) val region = Starbound.gson.fromJson(it, SerializedRegion::class.java)
load.terrainSelectors.forEach { terrainSelectors.intern(it) }
load.biomes.forEach { biomes.intern(it) }
layer.cells.add(Region( layer.cells.add(Region(
terrainSelector = load.terrainSelectors.getOrNull(region.terrainSelectorIndex)?.let { terrainSelectors.intern(it) }, terrainSelector = load.terrainSelectors.getOrNull(region.terrainSelectorIndex - 1),
foregroundCaveSelector = load.terrainSelectors.getOrNull(region.foregroundCaveSelectorIndex)?.let { terrainSelectors.intern(it) }, foregroundCaveSelector = load.terrainSelectors.getOrNull(region.foregroundCaveSelectorIndex - 1),
backgroundCaveSelector = load.terrainSelectors.getOrNull(region.backgroundCaveSelectorIndex)?.let { terrainSelectors.intern(it) }, backgroundCaveSelector = load.terrainSelectors.getOrNull(region.backgroundCaveSelectorIndex - 1),
blockBiome = load.biomes.getOrNull(region.blockBiomeIndex)?.let { biomes.intern(it) }, blockBiome = load.biomes.getOrNull(region.blockBiomeIndex - 1),
environmentBiome = load.biomes.getOrNull(region.environmentBiomeIndex)?.let { biomes.intern(it) }, environmentBiome = load.biomes.getOrNull(region.environmentBiomeIndex - 1),
subBlockSelector = region.subBlockSelectorIndexes.map { load.terrainSelectors.getOrNull(it)?.let { terrainSelectors.intern(it) } }.filterNotNull(), subBlockSelector = region.subBlockSelectorIndexes.map { load.terrainSelectors.getOrNull(it - 1) }.filterNotNull(),
foregroundOreSelector = region.foregroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it)?.let { terrainSelectors.intern(it) } }.filterNotNull(), foregroundOreSelector = region.foregroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it - 1) }.filterNotNull(),
backgroundOreSelector = region.backgroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it)?.let { terrainSelectors.intern(it) } }.filterNotNull(), backgroundOreSelector = region.backgroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it - 1) }.filterNotNull(),
regionLiquids = Starbound.gson.fromJson(it, RegionLiquids::class.java), regionLiquids = Starbound.gson.fromJson(it, RegionLiquids::class.java),
)) ))
} }
@ -338,9 +434,11 @@ class WorldLayout {
if (!Globals.terrestrialWorlds.useSecondaryEnvironmentBiomeIndex) { if (!Globals.terrestrialWorlds.useSecondaryEnvironmentBiomeIndex) {
region.environmentBiome = primaryEnvironment.environmentBiome region.environmentBiome = primaryEnvironment.environmentBiome
region.environmentBiomeIndex = primaryEnvironment.environmentBiomeIndex
} }
subRegion.environmentBiome = region.environmentBiome subRegion.environmentBiome = region.environmentBiome
subRegion.environmentBiomeIndex = region.environmentBiomeIndex
if (params.biomeName == primaryBiome && region.blockBiome != null) if (params.biomeName == primaryBiome && region.blockBiome != null)
spawnBiomes.add(region.blockBiome) spawnBiomes.add(region.blockBiome)
@ -375,8 +473,8 @@ class WorldLayout {
var nextBoundary = random.nextInt(0, worldSize.x) var nextBoundary = random.nextInt(0, worldSize.x)
layer.boundaries.add(nextBoundary) layer.boundaries.add(nextBoundary)
for (v in relativeRegionSizes) { for (i in 0 until relativeRegionSizes.size - 1) {
nextBoundary += (worldSize.x * v / totalRelativeSize).roundToInt() nextBoundary += (worldSize.x * relativeRegionSizes.getDouble(i) / totalRelativeSize).roundToInt()
layer.boundaries.add(nextBoundary) layer.boundaries.add(nextBoundary)
} }
@ -416,6 +514,83 @@ class WorldLayout {
} }
} }
data class RegionWeighting(val weight: Double, val xValue: Int, val region: Region)
fun getWeighting(x: Int, y: Int): List<RegionWeighting> {
val weighting = ArrayList<RegionWeighting>()
fun addLayerWeighting(layer: Layer, x: Int, weightFactor: Double) {
if (layer.cells.isEmpty())
return
val (innerIndex, innerX) = layer.findContainingCell(x)
var innerWeight = layer.weight(innerIndex, innerX)
val (leftIndex, leftX) = layer.leftCell(innerIndex, innerX)
var leftWeight = layer.weight(leftIndex, leftX)
val (rightIndex, rightX) = layer.leftCell(innerIndex, innerX)
var rightWeight = layer.weight(rightIndex, rightX)
val totalWeight = innerWeight + leftWeight + rightWeight
if (totalWeight <= 0.0)
return
val factor = weightFactor / totalWeight
innerWeight *= factor
leftWeight *= factor
rightWeight *= factor
if (innerWeight > 0.0)
weighting.add(RegionWeighting(innerWeight, innerX, layer.cells[innerIndex]))
if (leftWeight > 0.0)
weighting.add(RegionWeighting(leftWeight, leftX, layer.cells[leftIndex]))
if (rightWeight > 0.0)
weighting.add(RegionWeighting(rightWeight, rightX, layer.cells[rightIndex]))
}
val yi: Int
if (y < layers.first().yStart) {
return emptyList()
} else if (y > layers.last().yStart) {
yi = layers.size
} else {
yi = layers.indexOfFirst { it.yStart >= y } - 1
}
val layer = layers[yi]
if (y - layer.yStart < regionBlending / 2.0) {
if (yi == 0) {
addLayerWeighting(layer, x, 1.0)
} else {
val weight = 0.5 + (y - layer.yStart) / regionBlending
addLayerWeighting(layer, x, weight)
addLayerWeighting(layers[yi - 1], x, 1.0 - weight)
}
} else {
val nextLayer = layers.getOrNull(yi + 1)
if (nextLayer == null || y <= nextLayer.yStart - (regionBlending / 2.0)) {
addLayerWeighting(layer, x, 1.0)
} else {
val weight = 0.5 + (nextLayer.yStart - y) / regionBlending
addLayerWeighting(layer, x, 1.0 - weight)
addLayerWeighting(nextLayer, x, weight)
}
}
// Need to return weighting in order of greatest to least
weighting.sortWith { o1, o2 ->
o2.weight.compareTo(o1.weight)
}
return weighting
}
companion object : TypeAdapter<WorldLayout>() { companion object : TypeAdapter<WorldLayout>() {
private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) } private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }

View File

@ -1,13 +1,33 @@
package ru.dbotthepony.kstarbound.defs.world package ru.dbotthepony.kstarbound.defs.world
import com.google.gson.JsonObject import com.google.gson.JsonObject
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNullTile
import ru.dbotthepony.kstarbound.defs.tile.supportsModifier
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.quintic2
import ru.dbotthepony.kstarbound.util.random.random
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
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.random.RandomGenerator
class WorldTemplate(val geometry: WorldGeometry) { class WorldTemplate(val geometry: WorldGeometry) {
var seed: Long = 0L var seed: Long = 0L
@ -23,12 +43,20 @@ class WorldTemplate(val geometry: WorldGeometry) {
var celestialParameters: CelestialParameters? = null var celestialParameters: CelestialParameters? = null
private set private set
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
this.skyParameters = skyParameters this.skyParameters = skyParameters
this.worldLayout = worldParameters.createLayout(seed) this.worldLayout = worldParameters.createLayout(seed)
} }
constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, random: RandomGenerator) : this(WorldGeometry(worldParameters.worldSize, true, false)) {
this.seed = random.nextLong()
this.skyParameters = skyParameters
this.worldLayout = worldParameters.createLayout(random)
}
fun determineName() { fun determineName() {
if (celestialParameters != null) { if (celestialParameters != null) {
worldName = celestialParameters!!.name worldName = celestialParameters!!.name
@ -39,6 +67,14 @@ class WorldTemplate(val geometry: WorldGeometry) {
} }
} }
@JsonFactory
data class CustomTerrainRegion(
val region: Poly,
val solid: Boolean
) {
val aabb = region.aabb.enlarge(Globals.worldTemplate.customTerrainBlendSize, Globals.worldTemplate.customTerrainBlendSize)
}
@JsonFactory @JsonFactory
data class SerializedForm( data class SerializedForm(
val celestialParameters: CelestialParameters? = null, val celestialParameters: CelestialParameters? = null,
@ -47,19 +83,295 @@ class WorldTemplate(val geometry: WorldGeometry) {
val seed: Long = 0L, val seed: Long = 0L,
val size: Either<WorldGeometry, Vector2i>, val size: Either<WorldGeometry, Vector2i>,
val regionData: WorldLayout? = null, val regionData: WorldLayout? = null,
//val customTerrainRegions: val customTerrainRegions: List<CustomTerrainRegion>,
) )
fun toJson(): JsonObject { fun toJson(): JsonObject {
val data = Starbound.gson.toJsonTree(SerializedForm( val data = Starbound.gson.toJsonTree(SerializedForm(
celestialParameters, worldParameters, skyParameters, seed, celestialParameters, worldParameters, skyParameters, seed,
if (Starbound.IS_LEGACY_JSON) Either.right(geometry.size) else Either.left(geometry), if (Starbound.IS_LEGACY_JSON) Either.right(geometry.size) else Either.left(geometry),
worldLayout worldLayout, customTerrainRegions
)) as JsonObject )) as JsonObject
return data return data
} }
fun findSensiblePlayerStart(): Vector2d? {
val layout = worldLayout ?: return null
if (layout.playerStartSearchRegions.isEmpty())
return null
val random = random()
for (i in 0 until Globals.worldTemplate.playerStartSearchTries) {
val region = layout.playerStartSearchRegions.random(random)
val x = random.nextInt(region.mins.x, region.maxs.x)
for (y in region.maxs.y - 1 downTo region.mins.y) {
val info = cellInfo(x, y)
if (info.terrain && !cellInfo(x, y + 1).terrain) {
var isFree = true
for (x2 in x - Globals.worldTemplate.playerStartFreeBlocksRadius .. x + Globals.worldTemplate.playerStartFreeBlocksRadius) {
for (y2 in y + 1 .. y + Globals.worldTemplate.playerStartFreeBlocksHeight) {
isFree = isFree && isFree(x2, y2)
}
}
if (isFree) {
return Vector2d(x.toDouble(), y + 1.0)
}
}
}
}
return null
}
fun isFree(x: Int, y: Int): Boolean {
return !cellInfo(geometry.x.cell(x), geometry.y.cell(y)).terrain
}
fun isFree(region: AABBi): Boolean {
for (x in region.mins.x .. region.maxs.x) {
for (y in region.mins.y .. region.maxs.y) {
if (cellInfo(geometry.x.cell(x), geometry.y.cell(y)).terrain) {
return false
}
}
}
return true
}
fun surfaceLevel(): Int {
val parameters = worldParameters
if (parameters is TerrestrialWorldParameters) {
return parameters.surfaceLayer.layerBaseHeight
}
return geometry.size.y / 2
}
class CellInfo(val x: Int, val y: Int) {
var foreground: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref
var foregroundMod: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref
var background: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref
var backgroundMod: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref
var fillMicrodungeons: Boolean = false
var encloseLiquids: Boolean = false
var oceanLiquidLevel: Int = 0
var oceanLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref
var caveLiquidSeedDensity: Double = 0.0
var caveLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref
var blockBiome: Biome? = null
var blockBiomeIndex: Int = 0
var environmentBiome: Biome? = null
var environmentBiomeIndex: Int = 0
var biomeTransition = false
var terrain = false
var foregroundCave = false
var backgroundCave = false
}
fun cellInfo(x: Int, y: Int): CellInfo {
val info = CellInfo(x, y)
val layout = worldLayout ?: return info
// The environment biome is calculated with weighting based on the flat coordinates.
val flatWeighting = layout.getWeighting(x, y)
// The block biome is calculated optionally with higher frequency noise
// added to prevent straight lines appearing on the boundaries of
// regions.
var blendNoiseOffset = 0
if (layout.blendNoise != null)
blendNoiseOffset = layout.blendNoise!![x.toDouble(), y.toDouble()].toInt()
val blockPos: Vector2i
val blockWeighting: List<WorldLayout.RegionWeighting>
val transitionWeighting: List<WorldLayout.RegionWeighting>
val blockNoise = layout.blockNoise
if (blockNoise != null) {
blockPos = blockNoise.apply(x, y, geometry.size.x, geometry.size.y)
blockWeighting = layout.getWeighting(blockPos.x + blendNoiseOffset, blockPos.y)
transitionWeighting = layout.getWeighting(blockPos.x, blockPos.y)
} else {
blockPos = Vector2i(x, y)
blockWeighting = flatWeighting
transitionWeighting = flatWeighting
}
if (flatWeighting.isEmpty() || blockWeighting.isEmpty())
return info
val primaryFlatWeighting = flatWeighting.first()
val primaryBlockWeighting = blockWeighting.first()
info.blockBiome = primaryBlockWeighting.region.blockBiome
info.blockBiomeIndex = primaryBlockWeighting.region.blockBiomeIndex
info.environmentBiome = primaryFlatWeighting.region.environmentBiome
info.environmentBiomeIndex = primaryFlatWeighting.region.environmentBiomeIndex
val config = Globals.worldTemplate
info.biomeTransition = transitionWeighting.isNotEmpty() && transitionWeighting.first().weight < config.biomeTransitionThreshold
var terrainSelect = 0.0
var foregroundCaveSelect = 0.0
var backgroundCaveSelect = 0.0
// Terrain weighting uses the flat weighting, and weights each selector
// to blend among them.
for (weighting in flatWeighting) {
val terrain = weighting.region.terrainSelector ?: continue
terrainSelect += terrain[weighting.xValue, y] * weighting.weight
}
// This is a bit of a cheat. Since customTerrainWeighting is always flat,
// there are some odd effects that come from linearly interpolating from
// the generally non-flat terrain sources to flat regions of space. By
// using an interpolator that has an exaggerated S curve between the
// points, this hides some of these effects.
var smoothedValue = 0.0
var smoothFactor = 0.0
run {
var minimumDistance = Double.MAX_VALUE
var finalSolidWeight = 0.0
var totalWeight = 0.0
val point = Vector2d(x.toDouble(), y.toDouble())
for (region in customTerrainRegions) {
if (!geometry.rectContains(region.aabb, point)) {
continue
}
val distance = geometry.polyDistance(region.region, point)
if (distance >= config.customTerrainBlendSize)
continue
var weight = 1.0 - distance / config.customTerrainBlendSize
totalWeight += weight
if (!region.solid)
weight *= -1.0
finalSolidWeight += weight
minimumDistance = minimumDistance.coerceAtMost(distance)
}
if (minimumDistance <= config.customTerrainBlendSize) {
finalSolidWeight /= totalWeight
smoothedValue = finalSolidWeight * config.customTerrainBlendSize
smoothFactor = 1.0 - minimumDistance / config.customTerrainBlendSize
}
}
terrainSelect = quintic2(smoothFactor, terrainSelect, smoothedValue)
if (terrainSelect > 0.0) {
info.terrain = true
for (weighting in flatWeighting) {
val fg = weighting.region.foregroundCaveSelector
val bg = weighting.region.backgroundCaveSelector
if (fg != null) {
foregroundCaveSelect += fg[weighting.xValue, y] * weighting.weight
}
if (bg != null) {
backgroundCaveSelect += bg[weighting.xValue, y] * weighting.weight
}
}
if (terrainSelect < config.surfaceCaveAttenuationDist) {
val factor = (config.surfaceCaveAttenuationDist - terrainSelect) * config.surfaceCaveAttenuationFactor
foregroundCaveSelect -= factor
backgroundCaveSelect -= factor
}
}
info.foregroundCave = foregroundCaveSelect > 0.0
info.backgroundCave = backgroundCaveSelect > 0.0
// println("$x $y $terrainSelect $foregroundCaveSelect $backgroundCaveSelect")
info.caveLiquid = primaryFlatWeighting.region.regionLiquids.caveLiquid
info.caveLiquidSeedDensity = primaryFlatWeighting.region.regionLiquids.caveLiquidSeedDensity
info.oceanLiquid = primaryFlatWeighting.region.regionLiquids.oceanLiquid
info.oceanLiquidLevel = primaryFlatWeighting.region.regionLiquids.oceanLiquidLevel
info.encloseLiquids = primaryFlatWeighting.region.regionLiquids.encloseLiquids
info.fillMicrodungeons = primaryFlatWeighting.region.regionLiquids.fillMicrodungeons
if (!info.terrain && info.encloseLiquids && y < info.oceanLiquidLevel) {
info.terrain = true
info.foregroundCave = true
}
if (info.terrain) {
val biome = info.blockBiome
if (biome != null) {
if (!info.foregroundCave) {
info.foreground = biome.mainBlock.native
info.background = biome.mainBlock.native
} else if (!info.backgroundCave) {
info.background = biome.mainBlock.native
}
// subBlock, foregroundOre, and backgroundOre selectors can be empty
// if they are not enabled, otherwise they will always have the
// correct count
if (primaryBlockWeighting.region.subBlockSelector.isNotEmpty()) {
for (i in biome.subBlocks.indices) {
val block = biome.subBlocks[i].native
val selector = primaryBlockWeighting.region.subBlockSelector[i]
if (selector[primaryBlockWeighting.xValue - blendNoiseOffset, blockPos.y] > 0.0) {
if (!info.foregroundCave) {
info.foreground = block
info.background = block
} else if (!info.backgroundCave) {
info.background = block
}
break
}
}
}
if (!info.foregroundCave && primaryBlockWeighting.region.foregroundOreSelector.isNotEmpty()) {
for ((i, ore) in biome.ores.withIndex()) {
if (primaryBlockWeighting.region.foregroundOreSelector[i][x, y] > 0.0) {
info.foregroundMod = ore.first.native
break
}
}
}
if (!info.backgroundCave && primaryBlockWeighting.region.backgroundOreSelector.isNotEmpty()) {
for ((i, ore) in biome.ores.withIndex()) {
if (primaryBlockWeighting.region.backgroundOreSelector[i][x, y] > 0.0) {
info.backgroundMod = ore.first.native
break
}
}
}
}
}
return info
}
companion object { companion object {
suspend fun create(coordinate: UniversePos, universe: Universe): WorldTemplate { suspend fun create(coordinate: UniversePos, universe: Universe): WorldTemplate {
val params = universe.parameters(coordinate) ?: throw IllegalArgumentException("$universe has nothing at $coordinate!") val params = universe.parameters(coordinate) ?: throw IllegalArgumentException("$universe has nothing at $coordinate!")
@ -87,6 +399,7 @@ class WorldTemplate(val geometry: WorldGeometry) {
template.skyParameters = load.skyParameters template.skyParameters = load.skyParameters
template.seed = load.seed template.seed = load.seed
template.worldLayout = load.regionData template.worldLayout = load.regionData
template.customTerrainRegions.addAll(load.customTerrainRegions)
template.determineName() template.determineName()

View File

@ -5,4 +5,13 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
data class WorldTemplateConfig( data class WorldTemplateConfig(
val playerStartSearchYRange: Int, val playerStartSearchYRange: Int,
val playerStartSearchTries: Int,
val playerStartFreeBlocksRadius: Int,
val playerStartFreeBlocksHeight: Int,
val biomeTransitionThreshold: Double = 0.0,
val customTerrainBlendSize: Double = 0.0,
val surfaceCaveAttenuationDist: Double = 0.0,
val surfaceCaveAttenuationFactor: Double = 1.0,
) )

View File

@ -12,6 +12,7 @@ import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
@ -68,7 +69,7 @@ abstract class NativeLegacy<NATIVE, LEGACY> {
} }
override fun computeNative(value: Int?): Registry.Ref<TileDefinition> { override fun computeNative(value: Int?): Registry.Ref<TileDefinition> {
value ?: return Registries.tiles.emptyRef value ?: return BuiltinMetaMaterials.EMPTY.ref
return Registries.tiles.ref(value) return Registries.tiles.ref(value)
} }
} }
@ -93,7 +94,7 @@ abstract class NativeLegacy<NATIVE, LEGACY> {
} }
override fun computeNative(value: Int?): Registry.Ref<TileModifierDefinition> { override fun computeNative(value: Int?): Registry.Ref<TileModifierDefinition> {
value ?: return Registries.tileModifiers.emptyRef value ?: return BuiltinMetaMaterials.EMPTY_MOD.ref
return Registries.tileModifiers.ref(value) return Registries.tileModifiers.ref(value)
} }
} }

View File

@ -436,8 +436,8 @@ class FactoryAdapter<T : Any> private constructor(
) )
} }
fun <V> add(field: KProperty1<T, V>, isFlat: Boolean = false, isMarkedNullable: Boolean? = null): Builder<T> { fun <V> add(field: KProperty1<T, V>, isFlat: Boolean = false, isMarkedNullable: Boolean? = null, isIgnored: Boolean = false): Builder<T> {
types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable)) types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable, isIgnored = isIgnored))
return this return this
} }
@ -511,6 +511,7 @@ class FactoryAdapter<T : Any> private constructor(
builder.add( builder.add(
property, property,
isFlat = property.annotations.any { it.annotationClass == JsonFlat::class }, isFlat = property.annotations.any { it.annotationClass == JsonFlat::class },
isIgnored = property.annotations.any { it.annotationClass == JsonIgnore::class },
isMarkedNullable = if (property.annotations.any { it.annotationClass == JsonNotNull::class }) false else null, isMarkedNullable = if (property.annotations.any { it.annotationClass == JsonNotNull::class }) false else null,
) )

View File

@ -2,18 +2,16 @@ package ru.dbotthepony.kstarbound.math
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readVector2d import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.io.writeStruct2d import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.getAdapter import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -25,9 +23,32 @@ private operator fun Vector2d.compareTo(other: Vector2d): Int {
} }
@JsonAdapter(Line2d.Adapter::class) @JsonAdapter(Line2d.Adapter::class)
data class Line2d(val a: Vector2d, val b: Vector2d) { data class Line2d(val p0: Vector2d, val p1: Vector2d) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector2d(isLegacy), stream.readVector2d(isLegacy)) constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector2d(isLegacy), stream.readVector2d(isLegacy))
val normal: Vector2d
init {
val diff = (p1 - p0).unitVector
normal = Vector2d(-diff.y, diff.x)
}
operator fun plus(other: IStruct2d): Line2d {
return Line2d(p0 + other, p1 + other)
}
operator fun minus(other: IStruct2d): Line2d {
return Line2d(p0 - other, p1 - other)
}
operator fun times(other: IStruct2d): Line2d {
return Line2d(p0 * other, p1 * other)
}
operator fun times(other: Double): Line2d {
return Line2d(p0 * other, p1 * other)
}
data class Intersection(val intersects: Boolean, val point: KOptional<Vector2d>, val t: KOptional<Double>, val coincides: Boolean, val glances: Boolean) { data class Intersection(val intersects: Boolean, val point: KOptional<Vector2d>, val t: KOptional<Double>, val coincides: Boolean, val glances: Boolean) {
companion object { companion object {
val EMPTY = Intersection(false, KOptional(), KOptional(), false, false) val EMPTY = Intersection(false, KOptional(), KOptional(), false, false)
@ -35,16 +56,16 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
} }
fun difference(): Vector2d { fun difference(): Vector2d {
return b - a return p1 - p0
} }
fun reverse(): Line2d { fun reverse(): Line2d {
return Line2d(b, a) return Line2d(p1, p0)
} }
fun write(stream: DataOutputStream, isLegacy: Boolean) { fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeStruct2d(a, isLegacy) stream.writeStruct2d(p0, isLegacy)
stream.writeStruct2d(b, isLegacy) stream.writeStruct2d(p1, isLegacy)
} }
// original source of this intersection algorithm: // original source of this intersection algorithm:
@ -56,7 +77,7 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
val ab = difference() val ab = difference()
val cd = other.difference() val cd = other.difference()
val abCross = a.cross(b) val abCross = p0.cross(p1)
val cdCross = c.cross(d) val cdCross = c.cross(d)
val denominator = ab.cross(cd) val denominator = ab.cross(cd)
@ -65,7 +86,7 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
if (denominator.absoluteValue <= NEAR_ZERO) { if (denominator.absoluteValue <= NEAR_ZERO) {
if (xNumber.absoluteValue <= NEAR_ZERO && yNumber.absoluteValue <= NEAR_ZERO) { if (xNumber.absoluteValue <= NEAR_ZERO && yNumber.absoluteValue <= NEAR_ZERO) {
val intersects = infinite || (a >= c && a <= d) || (c >= a && c <= b) val intersects = infinite || (p0 >= c && p0 <= d) || (c >= p0 && c <= p1)
var point: Vector2d? = null var point: Vector2d? = null
var t = 0.0 var t = 0.0
@ -73,21 +94,21 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
if (infinite) { if (infinite) {
point = Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY) point = Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)
} else { } else {
point = if (a < c) c else a point = if (p0 < c) c else p0
} }
} }
if (a < c) { if (p0 < c) {
if (c.x != a.x) { if (c.x != p0.x) {
t = (c.x - a.x) / ab.x t = (c.x - p0.x) / ab.x
} else { } else {
t = (c.y - a.y) / ab.y t = (c.y - p0.y) / ab.y
} }
} else if (a > d) { } else if (p0 > d) {
if (d.x != a.x) { if (d.x != p0.x) {
t = (d.x - a.x) / ab.x t = (d.x - p0.x) / ab.x
} else { } else {
t = (d.y - a.y) / ab.y t = (d.y - p0.y) / ab.y
} }
} }
@ -96,24 +117,34 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
return Intersection.EMPTY return Intersection.EMPTY
} }
} else { } else {
val ta = (c - a).cross(cd) / denominator val ta = (c - p0).cross(cd) / denominator
val tb = (c - a).cross(ab) / denominator val tb = (c - p0).cross(ab) / denominator
val intersects = infinite || (ta in 0.0 .. 1.0 && tb in 0.0 .. 1.0) val intersects = infinite || (ta in 0.0 .. 1.0 && tb in 0.0 .. 1.0)
return Intersection( return Intersection(
intersects = intersects, intersects = intersects,
t = KOptional(ta), t = KOptional(ta),
point = KOptional((b - a) * ta + a), point = KOptional((p1 - p0) * ta + p0),
coincides = false, coincides = false,
glances = !infinite && intersects && (ta <= NEAR_ZERO || ta >= NEAR_ONE || tb <= NEAR_ZERO || tb >= NEAR_ONE) glances = !infinite && intersects && (ta <= NEAR_ZERO || ta >= NEAR_ONE || tb <= NEAR_ZERO || tb >= NEAR_ONE)
) )
} }
} }
fun project(axis: Vector2d): Double { fun project(axis: IStruct2d): Double {
val diff = difference() val diff = difference()
return ((axis.x - a.x) * diff.x + (axis.y - a.y) * diff.y) / diff.lengthSquared val (x, y) = axis
return ((x - p0.x) * diff.x + (y - p0.y) * diff.y) / diff.lengthSquared
}
fun distanceTo(other: IStruct2d, infinite: Boolean = false): Double {
var proj = project(other)
if (!infinite)
proj = proj.coerceIn(0.0, 1.0)
return (Vector2d(other) - p0 + difference() * proj).length
} }
fun distanceTo(other: Vector2d, infinite: Boolean = false): Double { fun distanceTo(other: Vector2d, infinite: Boolean = false): Double {
@ -122,14 +153,14 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
if (!infinite) if (!infinite)
proj = proj.coerceIn(0.0, 1.0) proj = proj.coerceIn(0.0, 1.0)
return (other - a + difference() * proj).length return (other - p0 + difference() * proj).length
} }
class Adapter(gson: Gson) : TypeAdapter<Line2d>() { class Adapter(gson: Gson) : TypeAdapter<Line2d>() {
private val pair = gson.getAdapter<Pair<Vector2d, Vector2d>>() private val pair = gson.getAdapter<Pair<Vector2d, Vector2d>>()
override fun write(out: JsonWriter, value: Line2d) { override fun write(out: JsonWriter, value: Line2d) {
pair.write(out, value.a to value.b) pair.write(out, value.p0 to value.p1)
} }
override fun read(`in`: JsonReader): Line2d { override fun read(`in`: JsonReader): Line2d {

View File

@ -100,3 +100,11 @@ fun normalizeAngle(angle: Double): Double {
fun approachAngle(target: Double, current: Double, limit: Double): Double { fun approachAngle(target: Double, current: Double, limit: Double): Double {
return normalizeAngle(current + angleDifference(current, target).coerceIn(-limit, limit)) return normalizeAngle(current + angleDifference(current, target).coerceIn(-limit, limit))
} }
fun quintic2(t: Float, a: Float, b: Float): Float {
return a + (b - a) * t * t * t * (t * (t * 6f - 15f) + 10f)
}
fun quintic2(t: Double, a: Double, b: Double): Double {
return a + (b - a) * t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
}

View File

@ -7,10 +7,14 @@ import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOption import io.netty.channel.ChannelOption
import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.nio.NioEventLoopGroup
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
import it.unimi.dsi.fastutil.ints.IntArrayList
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.IntValueCodec
import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.VarIntValueCodec import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.io.koptional
@ -30,6 +34,7 @@ import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedList
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
@ -122,7 +127,10 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
} }
fun bind(channel: Channel) { fun bind(channel: Channel) {
scope = CoroutineScope(channel.eventLoop().asCoroutineDispatcher()) scope = CoroutineScope(channel.eventLoop().asCoroutineDispatcher() + SupervisorJob() + CoroutineExceptionHandler { coroutineContext, throwable ->
disconnect("Uncaught exception in one of connection' coroutines: $throwable")
LOGGER.fatal("Uncaught exception in one of $this coroutines", throwable)
})
channel.config().setOption(ChannelOption.TCP_NODELAY, true) channel.config().setOption(ChannelOption.TCP_NODELAY, true)
this.channel = channel this.channel = channel
@ -181,6 +189,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
var windowWidth by client2serverGroup.upstream.add(networkedSignedInt()) var windowWidth by client2serverGroup.upstream.add(networkedSignedInt())
var windowHeight by client2serverGroup.upstream.add(networkedSignedInt()) var windowHeight by client2serverGroup.upstream.add(networkedSignedInt())
var playerID by client2serverGroup.upstream.add(networkedSignedInt()) var playerID by client2serverGroup.upstream.add(networkedSignedInt())
val clientSpectatingEntities = NetworkedList(IntValueCodec, elementsFactory = ::IntArrayList).also { client2serverGroup.upstream.add(it) }
// serverside variables // serverside variables
val server2clientGroup = MasterElement(NetworkedGroup()) val server2clientGroup = MasterElement(NetworkedGroup())
@ -193,9 +202,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
var playerEntity: PlayerEntity? = null var playerEntity: PlayerEntity? = null
// holy shit
val clientSpectatingEntities = BasicNetworkedElement(IntAVLTreeSet(), StreamCodec.Collection(VarIntValueCodec) { IntAVLTreeSet() })
// in tiles // in tiles
fun trackingTileRegions(): List<AABBi> { fun trackingTileRegions(): List<AABBi> {
val result = ArrayList<AABBi>() val result = ArrayList<AABBi>()
@ -239,7 +245,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
)) ))
} }
for (entity in clientSpectatingEntities.get()) { for (entity in clientSpectatingEntities) {
// TODO // TODO
} }

View File

@ -11,6 +11,7 @@ import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.ClientConnection import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
@ -35,17 +36,17 @@ class LegacyTileUpdatePacket(val position: Vector2i, val tile: LegacyNetworkCell
} }
class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray<LegacyNetworkCellState>) : IClientPacket { class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray<LegacyNetworkCellState>) : IClientPacket {
constructor(chunk: Chunk<*, *>) : this(chunk.pos.tile, chunk.legacyNetworkCells()) constructor(chunk: ServerChunk) : this(chunk.pos.tile, chunk.legacyNetworkCells())
override fun write(stream: DataOutputStream, isLegacy: Boolean) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeSignedVarInt(origin.x) stream.writeSignedVarInt(origin.x)
stream.writeSignedVarInt(origin.y) stream.writeSignedVarInt(origin.y)
stream.writeVarInt(data.rows)
stream.writeVarInt(data.columns) stream.writeVarInt(data.columns)
stream.writeVarInt(data.rows)
for (x in data.rowIndices) { for (y in data.rowIndices) {
for (y in data.columnIndices) { for (x in data.columnIndices) {
data[y, x].write(stream) data[x, y].write(stream)
} }
} }
} }
@ -59,14 +60,14 @@ class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray<
check(isLegacy) { "Using legacy packet in native protocol" } check(isLegacy) { "Using legacy packet in native protocol" }
val origin = Vector2i(stream.readSignedVarInt(), stream.readSignedVarInt()) val origin = Vector2i(stream.readSignedVarInt(), stream.readSignedVarInt())
val rows = stream.readVarInt()
val columns = stream.readVarInt() val columns = stream.readVarInt()
val rows = stream.readVarInt()
val data = Object2DArray.nulls<LegacyNetworkCellState>(columns, rows) val data = Object2DArray.nulls<LegacyNetworkCellState>(columns, rows)
for (x in data.rowIndices) { for (y in data.rowIndices) {
for (y in data.columnIndices) { for (x in data.columnIndices) {
data[y, x] = LegacyNetworkCellState.read(stream) data[x, y] = LegacyNetworkCellState.read(stream)
} }
} }

View File

@ -18,6 +18,6 @@ class WorldClientStateUpdatePacket(val deltas: ByteArrayList) : IServerPacket {
} }
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {
connection.client2serverGroup.read(deltas.elements(), 0, deltas.size) connection.client2serverGroup.read(deltas.elements(), 0, deltas.size, isLegacy = connection.isLegacy)
} }
} }

View File

@ -20,6 +20,7 @@ import ru.dbotthepony.kstarbound.defs.WarpAlias
import ru.dbotthepony.kstarbound.defs.WarpMode import ru.dbotthepony.kstarbound.defs.WarpMode
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.SkyType
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters
import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.ConnectionSide
@ -32,6 +33,7 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPac
import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker
import ru.dbotthepony.kstarbound.server.world.WorldStorage import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.SystemWorldLocation import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
@ -44,6 +46,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
var tracker: ServerWorldTracker? = null var tracker: ServerWorldTracker? = null
var worldStartAcknowledged = false var worldStartAcknowledged = false
var returnWarp: WarpAction? = null var returnWarp: WarpAction? = null
private var systemWorld: ServerSystemWorld? = null
val world: ServerWorld? val world: ServerWorld?
get() = tracker?.world get() = tracker?.world
@ -127,6 +130,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
server.channels.freeConnectionID(connectionID) server.channels.freeConnectionID(connectionID)
server.channels.connections.remove(this) server.channels.connections.remove(this)
server.freeNickname(nickname) server.freeNickname(nickname)
systemWorld?.removeClient(this)
systemWorld = null
announceDisconnect("Connection to remote host is lost.") announceDisconnect("Connection to remote host is lost.")
@ -149,20 +154,21 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
if (request is WarpAlias) if (request is WarpAlias)
request = request.remap(this) request = request.remap(this)
LOGGER.info("Trying to warp $this to $request") LOGGER.info("Trying to warp ${alias()} to $request")
val resolve = request.resolve(this) val resolve = request.resolve(this)
if (resolve.isLimbo) { if (resolve.isLimbo) {
send(PlayerWarpResultPacket(false, request, true)) send(PlayerWarpResultPacket(false, request, true))
} else if (tracker?.world?.worldID == resolve) { } else if (tracker?.world?.worldID == resolve) {
LOGGER.info("$this tried to warp into world they are already in.") LOGGER.info("${alias()} tried to warp into world they are already in.")
send(PlayerWarpResultPacket(true, request, false)) send(PlayerWarpResultPacket(true, request, false))
} else { } else {
val world = server.worlds[resolve] val world = try {
server.loadWorld(resolve).await()
if (world == null) { } catch (err: Throwable) {
send(PlayerWarpResultPacket(false, request, false)) send(PlayerWarpResultPacket(false, request, false))
LOGGER.error("Unable to wark ${alias()} to $request", err)
continue continue
} }
@ -246,13 +252,24 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
LOGGER.info("Found appropriate starter world at $found for ${alias()}") LOGGER.info("Found appropriate starter world at $found for ${alias()}")
var world = server.loadSystemWorld(found.location).await() val worldPromise = server.loadSystemWorld(found.location)
worldPromise.thenApply {
systemWorld = it
if (!isConnected) {
it.removeClient(this)
}
}
var world = worldPromise.await()
var ship = world.addClient(this, location = SystemWorldLocation.Celestial(found)).await() var ship = world.addClient(this, location = SystemWorldLocation.Celestial(found)).await()
shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world)) shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world))
shipCoordinate = found shipCoordinate = found
run { run {
val action = ship.location.orbitalAction(world) val action = ship.location.orbitalAction(world)
currentOrbitalWarpAction = action
orbitalWarpAction = action orbitalWarpAction = action
for (client in shipWorld.clients) { for (client in shipWorld.clients) {
@ -319,6 +336,14 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
client.client.orbitalWarpAction = KOptional() client.client.orbitalWarpAction = KOptional()
} }
newSystem.thenApply {
systemWorld = it
if (!isConnected) {
it.removeClient(this)
}
}
world = newSystem.await() world = newSystem.await()
ship = world.addClient(this).await() ship = world.addClient(this).await()
@ -436,6 +461,9 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (!channel.isOpen)
return
if (msg is IServerPacket) { if (msg is IServerPacket) {
try { try {
msg.play(this) msg.play(this)
@ -460,23 +488,24 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
if (isLegacy) { if (isLegacy) {
scope.launch { celestialRequestsHandler() } scope.launch { celestialRequestsHandler() }
ServerWorld.load(server, shipChunkSource, WorldID.ShipWorld(uuid!!)).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.close()
} else { } else {
shipWorld = it shipWorld = it
// shipWorld.sky.startFlying(true, true)
shipWorld.thread.start() shipWorld.thread.start()
enqueueWarp(WarpAlias.OwnShip) enqueueWarp(WarpAlias.OwnShip)
shipUpgrades = shipUpgrades.addCapability("planetTravel") shipUpgrades = shipUpgrades.addCapability("planetTravel")
shipUpgrades = shipUpgrades.addCapability("teleport") shipUpgrades = shipUpgrades.addCapability("teleport")
shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 1) shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 3)
scope.launch { warpEventLoop() }
scope.launch { shipFlightEventLoop() } scope.launch { shipFlightEventLoop() }
scope.launch { warpEventLoop() }
if (server.channels.connections.size > 1) { //if (server.channels.connections.size > 1) {
enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!)) // enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!))
} //}
} }
}.exceptionally { }.exceptionally {
LOGGER.error("Error while initializing shipworld for $this", it) LOGGER.error("Error while initializing shipworld for $this", it)

View File

@ -1,34 +1,40 @@
package ru.dbotthepony.kstarbound.server package ru.dbotthepony.kstarbound.server
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.async import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.world.AsteroidsWorldParameters
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.server.world.ServerUniverse 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.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
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Supplier import java.util.function.Supplier
@ -41,15 +47,13 @@ sealed class StarboundServer(val root: File) : Closeable {
} }
} }
val limboWorldIndex = AtomicInteger() private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>()
val limboWorlds = CopyOnWriteArrayList<ServerWorld>()
val worlds = ConcurrentHashMap<WorldID, ServerWorld>()
val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) } val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) }
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::tick, Starbound.TIMESTEP_NANOS) val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::tick, Starbound.TIMESTEP_NANOS)
val thread = Thread(spinner, "Server Thread") val thread = Thread(spinner, "Server Thread")
val universe = ServerUniverse() val universe = ServerUniverse()
val chat = ChatHandler(this) val chat = ChatHandler(this)
val context = CoroutineScope(Starbound.COROUTINE_EXECUTOR) val scope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>() private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>()
@ -60,11 +64,107 @@ sealed class StarboundServer(val root: File) : Closeable {
fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> { fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> {
return CompletableFuture.supplyAsync(Supplier { return CompletableFuture.supplyAsync(Supplier {
systemWorlds.computeIfAbsent(location) { systemWorlds.computeIfAbsent(location) {
context.async { loadSystemWorld0(location) }.asCompletableFuture() scope.async { loadSystemWorld0(location) }.asCompletableFuture()
} }
}, mailbox).thenCompose { it } }, mailbox).thenCompose { it }
} }
private suspend fun loadCelestialWorld(location: WorldID.Celestial): ServerWorld {
LOGGER.info("Creating celestial world $location")
val template = WorldTemplate.create(location.pos, universe)
val world = ServerWorld.create(this, template, WorldStorage.Nothing, location)
try {
world.thread.start()
world.prepare().await()
} catch (err: Throwable) {
LOGGER.fatal("Exception while creating celestial world at $location!", err)
world.close()
throw err
}
return world
}
private suspend fun loadInstanceWorld(location: WorldID.Instance): ServerWorld {
val config = Globals.instanceWorlds[location.name] ?: throw NoSuchElementException("No such instance world ${location.name}")
val random = random(config.seed ?: System.nanoTime())
val visitable = when (config.type.lowercase()) {
"terrestrial" -> TerrestrialWorldParameters.generate(config.planetType!!, config.planetSize!!, random)
"asteroids" -> AsteroidsWorldParameters.generate(random)
"floatingdungeon" -> FloatingDungeonWorldParameters.generate(config.dungeonWorld!!)
else -> throw RuntimeException()
}
if (location.threatLevel != null) {
visitable.threatLevel = location.threatLevel
}
if (config.beamUpRule != null) {
visitable.beamUpRule = config.beamUpRule
}
visitable.disableDeathDrops = config.disableDeathDrops
val template = WorldTemplate(visitable, config.skyParameters, random)
val world = ServerWorld.create(this, template, WorldStorage.NULL, location)
world.setProperty("ephemeral", JsonPrimitive(!config.persistent))
world.thread.start()
return world
}
private suspend fun loadWorld0(location: WorldID): ServerWorld {
return when (location) {
is WorldID.ShipWorld -> throw IllegalArgumentException("Can't create ship worlds out of thin air")
is WorldID.Instance -> loadInstanceWorld(location)
is WorldID.Celestial -> loadCelestialWorld(location)
is WorldID.Limbo -> throw IllegalArgumentException("Limbo was supplied as world ID")
}
}
fun loadWorld(location: WorldID): CompletableFuture<ServerWorld> {
return CompletableFuture.supplyAsync(Supplier {
var world = worlds[location]
if (world != null && world.isCompletedExceptionally) {
worlds.remove(location)
world = null
}
if (world != null) {
world
} else {
val future = scope.async { loadWorld0(location) }.asCompletableFuture()
worlds[location] = future
future
}
}, mailbox).thenCompose { it }
}
fun loadShipWorld(connection: ServerConnection, storage: WorldStorage): CompletableFuture<ServerWorld> {
return CompletableFuture.supplyAsync(Supplier {
val id = WorldID.ShipWorld(connection.uuid ?: throw NullPointerException("Connection UUID is null"))
val existing = worlds[id]
if (existing != null)
throw IllegalStateException("Already has $id!")
val world = ServerWorld.load(this, storage, id)
worlds[id] = world
world
}, mailbox).thenCompose { it }
}
fun notifyWorldUnloaded(worldID: WorldID) {
mailbox.execute {
worlds.remove(worldID)
}
}
fun loadSystemWorld(location: UniversePos): CompletableFuture<ServerSystemWorld> { fun loadSystemWorld(location: UniversePos): CompletableFuture<ServerSystemWorld> {
return loadSystemWorld(location.location) return loadSystemWorld(location.location)
} }
@ -140,24 +240,26 @@ sealed class StarboundServer(val root: File) : Closeable {
// TODO: schedule to thread pool? // TODO: schedule to thread pool?
// right now, system worlds are rather lightweight, and having separate threads for them is overkill // right now, system worlds are rather lightweight, and having separate threads for them is overkill
runBlocking { if (systemWorlds.isNotEmpty()) {
systemWorlds.values.removeIf { runBlocking {
if (it.isCompletedExceptionally) { systemWorlds.values.removeIf {
return@removeIf true if (it.isCompletedExceptionally) {
} return@removeIf true
}
if (!it.isDone) {
return@removeIf false
}
launch { it.get().tick() }
if (it.get().shouldClose()) {
LOGGER.info("Stopping idling ${it.get()}")
return@removeIf true
}
if (!it.isDone) {
return@removeIf false return@removeIf false
} }
launch { it.get().tick() }
if (it.get().shouldClose()) {
LOGGER.info("Stopping idling ${it.get()}")
return@removeIf true
}
return@removeIf false
} }
} }
@ -169,10 +271,16 @@ sealed class StarboundServer(val root: File) : Closeable {
if (isClosed) return if (isClosed) return
isClosed = true isClosed = true
context.cancel("Server shutting down") scope.cancel("Server shutting down")
channels.close() channels.close()
worlds.values.forEach { it.close() }
limboWorlds.forEach { it.close() } worlds.values.forEach {
if (it.isDone && !it.isCompletedExceptionally)
it.get().close()
it.cancel(true)
}
universe.close() universe.close()
close0() close0()
} }

View File

@ -2,13 +2,57 @@ package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNullTile
import ru.dbotthepony.kstarbound.defs.tile.supportsModifier
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.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.TileColor
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) { class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
/**
* Determines the state chunk is in, chunks are written to persistent storage
* only if they are in [FULL] state to avoid partially loaded or partially generated
* chunks from making its way into persistent storage.
*/
enum class State {
FRESH, // Nothing is loaded
TILES,
MICRO_DUNGEONS,
CAVE_LIQUID,
TILE_ENTITIES,
ENTITIES,
FULL; // indicates everything has been loaded
}
var state: State = State.FRESH
private set
fun bumpState(newState: State) {
require(newState >= state) { "Tried to downgrade $this state from $state to $newState" }
if (newState >= State.ENTITIES) {
this.state = State.FULL
} else {
this.state = newState
}
}
fun copyCells(): Object2DArray<ImmutableCell> { fun copyCells(): Object2DArray<ImmutableCell> {
if (cells.isInitialized()) { if (cells.isInitialized()) {
return Object2DArray(cells.value) return Object2DArray(cells.value)
@ -16,4 +60,205 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
return Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY) return Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY)
} }
} }
data class DamageResult(val result: TileDamageResult, val health: TileHealth? = null, val stateBefore: AbstractCell? = null)
fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): DamageResult {
if (!cells.isInitialized()) {
return DamageResult(TileDamageResult.NONE)
}
val cell = cells.value[pos.x, pos.y]
if (cell.isIndestructible || cell.tile(isBackground).material.value.isMeta) {
return DamageResult(TileDamageResult.NONE)
}
var damage = damage
var result = TileDamageResult.NORMAL
if (cell.dungeonId in world.protectedDungeonIDs) {
damage = damage.copy(type = TileDamageType.PROTECTED)
result = TileDamageResult.PROTECTED
}
val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y]
val tile = cell.tile(isBackground)
val params = if (!damage.type.isPenetrating && tile.modifier != null && tile.modifier!!.value.breaksWithTile) {
tile.material.value.actualDamageTable + tile.modifier!!.value.actualDamageTable
} else {
tile.material.value.actualDamageTable
}
health.damage(params, sourcePosition, damage)
subscribers.forEach { it.onTileHealthUpdate(pos.x, pos.y, isBackground, health) }
if (health.isDead) {
if (isBackground) {
damagedTilesBackground.remove(pos)
} else {
damagedTilesForeground.remove(pos)
}
val copyHealth = health.copy()
val mCell = cell.mutable()
val mTile = mCell.tile(isBackground)
mTile.material = BuiltinMetaMaterials.EMPTY
mTile.color = TileColor.DEFAULT
mTile.hueShift = 0f
if (tile.modifier.value.breaksWithTile) {
mTile.modifier = BuiltinMetaMaterials.EMPTY_MOD
}
setCell(pos.x, pos.y, mCell.immutable())
health.reset()
return DamageResult(result, copyHealth, cell)
} else {
if (isBackground) {
damagedTilesBackground.add(pos)
} else {
damagedTilesForeground.add(pos)
}
return DamageResult(result, health, cell)
}
}
private val damagedTilesForeground = ObjectArraySet<Vector2i>()
private val damagedTilesBackground = ObjectArraySet<Vector2i>()
fun tileDamagePackets(): List<TileDamageUpdatePacket> {
val result = ArrayList<TileDamageUpdatePacket>()
if (tileHealthBackground.isInitialized()) {
val tileHealthBackground = tileHealthBackground.value
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
val health = tileHealthBackground[x, y]
if (!health.isHealthy) {
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, true, health))
}
}
}
}
if (tileHealthForeground.isInitialized()) {
val tileHealthForeground = tileHealthForeground.value
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
val health = tileHealthForeground[x, y]
if (!health.isHealthy) {
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, false, health))
}
}
}
}
return result
}
override fun tick() {
if (state != State.FULL)
return
super.tick()
if (cells.isInitialized() && (damagedTilesBackground.isNotEmpty() || damagedTilesForeground.isNotEmpty())) {
val tileHealthBackground = tileHealthBackground.value
val tileHealthForeground = tileHealthForeground.value
val cells = cells.value
damagedTilesBackground.removeIf { (x, y) ->
val health = tileHealthBackground[x, y]
val result = !health.tick(cells[x, y].background.material.value.actualDamageTable)
subscribers.forEach { it.onTileHealthUpdate(x, y, true, health) }
result
}
damagedTilesForeground.removeIf { (x, y) ->
val health = tileHealthForeground[x, y]
val result = !health.tick(cells[x, y].foreground.material.value.actualDamageTable)
subscribers.forEach { it.onTileHealthUpdate(x, y, false, health) }
result
}
}
}
fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> {
val width = (world.geometry.size.x - pos.tileX).coerceAtMost(CHUNK_SIZE)
val height = (world.geometry.size.y - pos.tileY).coerceAtMost(CHUNK_SIZE)
if (cells.isInitialized()) {
val cells = cells.value
return Object2DArray(width, height) { a, b -> cells[a, b].toLegacyNet() }
} else {
return Object2DArray(width, height, LegacyNetworkCellState.NULL)
}
}
fun prepareCells() {
val cells = cells.value
val width = (world.geometry.size.x - pos.tileX).coerceAtMost(cells.columns)
val height = (world.geometry.size.y - pos.tileY).coerceAtMost(cells.rows)
for (x in 0 until width) {
for (y in 0 until height) {
val info = world.template.cellInfo(pos.tileX + x, pos.tileY + y)
val state = cells[x, y].mutable()
state.blockBiome = info.blockBiomeIndex
state.envBiome = info.environmentBiomeIndex
if (state.foreground.material.isNullTile) {
state.foreground.material = info.foreground.entry ?: BuiltinMetaMaterials.EMPTY
state.foreground.color = TileColor.DEFAULT
state.foreground.hueShift = info.blockBiome?.hueShift(state.foreground.material) ?: 0f
if (state.foreground.material.supportsModifier(info.foregroundMod)) {
state.foreground.modifier = info.foregroundMod.entry ?: BuiltinMetaMaterials.EMPTY_MOD
state.foreground.hueShift = info.blockBiome?.hueShift(info.foregroundMod) ?: 0f
}
}
if (state.background.material.isNullTile) {
state.background.material = info.background.entry ?: BuiltinMetaMaterials.EMPTY
state.background.color = TileColor.DEFAULT
state.background.hueShift = info.blockBiome?.hueShift(state.background.material) ?: 0f
if (state.background.material.supportsModifier(info.backgroundMod)) {
state.background.modifier = info.backgroundMod.entry ?: BuiltinMetaMaterials.EMPTY_MOD
state.background.hueShift = info.blockBiome?.hueShift(info.backgroundMod) ?: 0f
}
}
if (!state.foreground.material.isEmptyTile) {
state.liquid.reset()
} else if (y < info.oceanLiquidLevel && info.oceanLiquid.isPresent && !info.oceanLiquid.value!!.isMeta) {
val pressure = (info.oceanLiquidLevel - y).toFloat()
if (state.background.material.isEmptyTile) {
state.liquid.pressure = pressure
state.liquid.isInfinite = true
state.liquid.state = info.oceanLiquid.entry!!
} else if (info.encloseLiquids) {
state.liquid.pressure = pressure
state.liquid.isInfinite = false
state.liquid.level = 1f
state.liquid.state = info.oceanLiquid.entry!!
}
}
cells[x, y] = state.immutable()
}
}
}
} }

View File

@ -111,7 +111,11 @@ class ServerSystemWorld : SystemWorld {
val ship = ships.remove(client.uuid) ?: throw IllegalStateException("No client $client in $this!") val ship = ships.remove(client.uuid) ?: throw IllegalStateException("No client $client in $this!")
val packet = SystemShipDestroyPacket(ship.uuid) val packet = SystemShipDestroyPacket(ship.uuid)
ships.values.forEach { it.client.send(packet) }
ships.values.forEach {
it.forget(ship.uuid)
it.client.send(packet)
}
} }
fun addClient(client: ServerConnection, shipSpeed: Double = Globals.systemWorld.clientShip.speed, location: SystemWorldLocation = SystemWorldLocation.Transit): CompletableFuture<ServerShip> { fun addClient(client: ServerConnection, shipSpeed: Double = Globals.systemWorld.clientShip.speed, location: SystemWorldLocation = SystemWorldLocation.Transit): CompletableFuture<ServerShip> {
@ -268,6 +272,7 @@ class ServerSystemWorld : SystemWorld {
val packet = SystemObjectDestroyPacket(it.uuid) val packet = SystemObjectDestroyPacket(it.uuid)
ships.values.forEach { ship -> ships.values.forEach { ship ->
ship.forget(it.uuid)
ship.client.send(packet) ship.client.send(packet)
} }
@ -379,6 +384,10 @@ class ServerSystemWorld : SystemWorld {
client.send(SystemWorldUpdatePacket(objects, ships)) client.send(SystemWorldUpdatePacket(objects, ships))
} }
fun forget(id: UUID) {
netVersions.removeLong(id)
}
private var destinationFuture = CompletableFuture<SystemWorldLocation>() private var destinationFuture = CompletableFuture<SystemWorldLocation>()
fun destination(destination: SystemWorldLocation, future: CompletableFuture<SystemWorldLocation>) { fun destination(destination: SystemWorldLocation, future: CompletableFuture<SystemWorldLocation>) {

View File

@ -6,12 +6,22 @@ import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
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.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
@ -19,6 +29,7 @@ import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult 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.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -40,17 +51,17 @@ import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
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.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer
import java.util.function.Predicate import java.util.function.Predicate
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.properties.Delegates
class ServerWorld private constructor( class ServerWorld private constructor(
val server: StarboundServer, val server: StarboundServer,
@ -61,43 +72,42 @@ class ServerWorld private constructor(
init { init {
if (server.isClosed) if (server.isClosed)
throw RuntimeException() throw RuntimeException()
if (worldID != WorldID.Limbo) {
if (server.worlds.containsKey(worldID))
throw IllegalStateException("Duplicate world ID: $worldID")
server.worlds[worldID] = this
} else {
server.limboWorlds.add(this)
}
} }
val clients = CopyOnWriteArrayList<ServerWorldTracker>() val clients = CopyOnWriteArrayList<ServerWorldTracker>()
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?) {
if (clients.any { it.client == client }) try {
throw IllegalStateException("$client is already in $this") isBusy++
if (!client.isConnected) if (clients.any { it.client == client })
throw IllegalStateException("$client disconnected while joining $this") throw IllegalStateException("$client is already in $this")
val start = if (action is WarpAction.Player) if (!client.isConnected)
clients.firstOrNull { it.client.uuid == action.uuid }?.client?.playerEntity?.position throw IllegalStateException("$client disconnected while joining $this")
else if (action is WarpAction.World)
action.target.resolve(this)
else
playerSpawnPosition
if (start == null) { val start = if (action is WarpAction.Player)
client.send(PlayerWarpResultPacket(false, action!!, true)) clients.firstOrNull { it.client.uuid == action.uuid }?.client?.playerEntity?.position
throw IllegalStateException("Not a valid spawn target: $action") else if (action is WarpAction.World)
action.target.resolve(this)
else
playerSpawnPosition
if (start == null) {
client.send(PlayerWarpResultPacket(false, action!!, true))
throw IllegalStateException("Not a valid spawn target: $action")
}
if (action != null)
client.send(PlayerWarpResultPacket(true, action, false))
client.tracker?.remove("Transiting to new world")
clients.add(ServerWorldTracker(this, client, start))
} finally {
isBusy--
} }
if (action != null)
client.send(PlayerWarpResultPacket(true, action, false))
client.tracker?.remove("Transiting to new world")
clients.add(ServerWorldTracker(this, client, start))
} }
fun acceptClient(player: ServerConnection, action: WarpAction? = null): CompletableFuture<Unit> { fun acceptClient(player: ServerConnection, action: WarpAction? = null): CompletableFuture<Unit> {
@ -105,20 +115,32 @@ class ServerWorld private constructor(
unpause() unpause()
try { try {
return CompletableFuture.supplyAsync(Supplier { doAcceptClient(player, action) }, mailbox).exceptionally { val future = CompletableFuture.supplyAsync(Supplier { doAcceptClient(player, action) }, mailbox)
future.exceptionally {
LOGGER.error("Error while accepting new player into world", it) LOGGER.error("Error while accepting new player into world", it)
} }
return future
} catch (err: RejectedExecutionException) { } catch (err: RejectedExecutionException) {
return CompletableFuture.failedFuture(err) return CompletableFuture.failedFuture(err)
} }
} }
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS) val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS)
private val str = "Server World ${if (worldID == WorldID.Limbo) "limbo(${server.limboWorldIndex.getAndIncrement()})" else worldID.toString()}" private val str = "Server World ${worldID.toString()}"
val thread = Thread(spinner, str) val thread = Thread(spinner, str)
init {
mailbox.thread = thread
}
private val isClosed = AtomicBoolean() private val isClosed = AtomicBoolean()
fun isClosed(): Boolean {
return isClosed.get()
}
init { init {
thread.isDaemon = true thread.isDaemon = true
} }
@ -143,24 +165,47 @@ class ServerWorld private constructor(
LOGGER.info("Shutting down $this") LOGGER.info("Shutting down $this")
if (isClosed.compareAndSet(false, true)) { if (isClosed.compareAndSet(false, true)) {
server.notifyWorldUnloaded(worldID)
super.close() super.close()
spinner.unpause() spinner.unpause()
clients.forEach { it.remove() }
if (worldID != WorldID.Limbo) ticketListLock.withLock {
server.worlds.remove(worldID) ticketLists.forEach { it.scope.cancel() }
else }
server.limboWorlds.remove(this)
clients.forEach {
it.remove()
it.client.enqueueWarp(WarpAlias.Return)
}
LockSupport.unpark(thread) LockSupport.unpark(thread)
} }
} }
private var idleTicks = 0
private var isBusy = 0
private fun spin(): Boolean { private fun spin(): Boolean {
if (isClosed.get()) return false if (isClosed.get()) return false
try { try {
if (clients.isEmpty() && isBusy <= 0) {
idleTicks++
} else {
idleTicks = 0
}
tick() tick()
if (idleTicks >= 600) {
if (shouldStopOnIdle) {
close()
return false
} else {
pause()
}
}
return true return true
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.fatal("Exception in world tick loop", err) LOGGER.fatal("Exception in world tick loop", err)
@ -260,35 +305,7 @@ class ServerWorld private constructor(
} }
ticketListLock.withLock { ticketListLock.withLock {
ticketLists.removeIf { ticketLists.removeIf { it.tick() }
val valid = it.tick()
if (!valid) {
val removed = ticketMap.remove(it.pos.toLong())
check(removed == it) { "Expected to remove $it, but removed $removed" }
val chunk = chunkMap[it.pos]
if (chunk != null) {
val unloadable = entityIndex
.query(
chunk.aabb,
filter = Predicate { it.isApplicableForUnloading && chunk.aabb.isInside(it.position) },
distinct = true, withEdges = false)
storage.saveCells(it.pos, chunk.copyCells())
storage.saveEntities(it.pos, unloadable)
unloadable.forEach {
it.remove()
}
chunkMap.remove(it.pos)
}
}
!valid
}
} }
} }
@ -298,6 +315,106 @@ class ServerWorld private constructor(
} }
} }
private suspend fun prepare0() {
try {
isBusy++
if (playerSpawnPosition == Vector2d.ZERO) {
playerSpawnPosition = findPlayerStart()
}
} finally {
isBusy--
}
}
// done as completable future because we must do
// everything inside our own thread, not anywhere else
// This way, external callers can properly wait for preparations to complete
fun prepare(): CompletableFuture<*> {
return CompletableFuture.supplyAsync(Supplier {
scope.launch { prepare0() }.asCompletableFuture()
}, mailbox).thenCompose { it }
}
private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d {
val tickets = ArrayList<ITicket>()
try {
LOGGER.info("Trying to find player spawn position...")
var pos = hint ?: CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart() }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble())
var previous = pos
LOGGER.info("Trying to find player spawn position near $pos...")
for (t in 0 until Globals.worldServer.playerStartRegionMaximumTries) {
var foundGround = false
// First go downward until we collide with terrain
for (i in 0 until Globals.worldServer.playerStartRegionMaximumVerticalSearch) {
val spawnRect = AABB(
Vector2d(pos.x - Globals.worldServer.playerStartRegionSize.x / 2, pos.y),
Vector2d(pos.x + Globals.worldServer.playerStartRegionSize.x / 2, pos.y + Globals.worldServer.playerStartRegionSize.y),
)
val region = permanentChunkTicket(spawnRect)
tickets.addAll(region)
region.forEach { it.chunk.await() }
foundGround = matchCells(spawnRect) {
it.foreground.material.value.collisionKind != CollisionType.NONE
}
if (foundGround) {
break
} else {
pos += Vector2d.NEGATIVE_Y
}
}
if (foundGround) {
// Then go up until our spawn region is no longer in the terrain, but bail
// out and try again if we can't signal the region or we are stuck in a
// dungeon.
for (i in 0 until Globals.worldServer.playerStartRegionMaximumVerticalSearch) {
if (!chunkMap.getCell(pos.x.toInt(), pos.y.toInt()).liquid.state.isEmptyLiquid) {
break
}
val spawnRect = AABB(
Vector2d(pos.x - Globals.worldServer.playerStartRegionSize.x / 2, pos.y),
Vector2d(pos.x + Globals.worldServer.playerStartRegionSize.x / 2, pos.y + Globals.worldServer.playerStartRegionSize.y),
)
val region = permanentChunkTicket(spawnRect)
tickets.addAll(region)
region.forEach { it.chunk.await() }
if (!matchCells(spawnRect) { it.foreground.material.value.collisionKind != CollisionType.NONE } && spawnRect.maxs.y < geometry.size.y) {
LOGGER.info("Found appropriate spawn position at $pos")
return pos
}
pos += Vector2d.POSITIVE_Y
}
}
pos = CompletableFuture.supplyAsync(Supplier { template.findSensiblePlayerStart() }, Starbound.EXECUTOR).await() ?: Vector2d(0.0, template.surfaceLevel().toDouble())
if (previous != pos) {
LOGGER.info("Still trying to find player spawn position near $pos...")
previous = pos
} else {
break
}
}
LOGGER.warn("Unable to find proper player start location, will use $pos")
return pos
} finally {
tickets.forEach { it.cancel() }
}
}
override fun setProperty0(key: String, value: JsonElement) { override fun setProperty0(key: String, value: JsonElement) {
super.setProperty0(key, value) super.setProperty0(key, value)
broadcast(UpdateWorldPropertiesPacket(JsonObject().apply { add(key, value) })) broadcast(UpdateWorldPropertiesPacket(JsonObject().apply { add(key, value) }))
@ -315,26 +432,58 @@ class ServerWorld private constructor(
return ticketMap.computeIfAbsent(geometry.wrapToLong(pos), Long2ObjectFunction { TicketList(it) }) return ticketMap.computeIfAbsent(geometry.wrapToLong(pos), Long2ObjectFunction { TicketList(it) })
} }
fun permanentChunkTicket(pos: ChunkPos): ITicket { fun permanentChunkTicket(pos: ChunkPos, target: ServerChunk.State = ServerChunk.State.FULL): ITicket {
ticketListLock.withLock { ticketListLock.withLock {
return getTicketList(pos).Ticket() return getTicketList(pos).Ticket(target)
} }
} }
fun temporaryChunkTicket(pos: ChunkPos, time: Int): ITicket { fun permanentChunkTicket(region: AABBi, target: ServerChunk.State = ServerChunk.State.FULL): List<ITicket> {
ticketListLock.withLock {
return geometry.region2Chunks(region).map { getTicketList(it).Ticket(target) }
}
}
fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List<ITicket> {
ticketListLock.withLock {
return geometry.region2Chunks(region).map { getTicketList(it).Ticket(target) }
}
}
fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): ITimedTicket {
require(time > 0) { "Invalid ticket time: $time" } require(time > 0) { "Invalid ticket time: $time" }
ticketListLock.withLock { ticketListLock.withLock {
return getTicketList(pos).TimedTicket(time) return getTicketList(pos).TimedTicket(time, target)
}
}
fun temporaryChunkTicket(region: AABBi, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ITimedTicket> {
require(time > 0) { "Invalid ticket time: $time" }
ticketListLock.withLock {
return geometry.region2Chunks(region).map { getTicketList(it).TimedTicket(time, target) }
}
}
fun temporaryChunkTicket(region: AABB, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ITimedTicket> {
require(time > 0) { "Invalid ticket time: $time" }
ticketListLock.withLock {
return geometry.region2Chunks(region).map { getTicketList(it).TimedTicket(time, target) }
} }
} }
override fun onChunkCreated(chunk: ServerChunk) { override fun onChunkCreated(chunk: ServerChunk) {
ticketMap[chunk.pos.toLong()]?.let { chunk.addListener(it) } ticketListLock.withLock {
ticketMap[chunk.pos.toLong()]?.let { chunk.addListener(it) }
}
} }
override fun onChunkRemoved(chunk: ServerChunk) { override fun onChunkRemoved(chunk: ServerChunk) {
ticketMap[chunk.pos.toLong()]?.let { chunk.removeListener(it) } ticketListLock.withLock {
ticketMap[chunk.pos.toLong()]?.let { chunk.removeListener(it) }
}
} }
interface ITicket { interface ITicket {
@ -342,7 +491,7 @@ class ServerWorld private constructor(
val isCanceled: Boolean val isCanceled: Boolean
val pos: ChunkPos val pos: ChunkPos
val id: Int val id: Int
val chunk: ServerChunk? val chunk: CompletableFuture<ServerChunk>
var listener: IChunkListener? var listener: IChunkListener?
} }
@ -360,14 +509,163 @@ class ServerWorld private constructor(
private inner class TicketList(val pos: ChunkPos) : IChunkListener { private inner class TicketList(val pos: ChunkPos) : IChunkListener {
constructor(pos: Long) : this(ChunkPos(pos)) constructor(pos: Long) : this(ChunkPos(pos))
private var first = true private var calledLoadChunk = true
private val permanent = ArrayList<Ticket>() private val permanent = ArrayList<Ticket>()
private val temporary = ObjectAVLTreeSet<TimedTicket>() private val temporary = ObjectAVLTreeSet<TimedTicket>()
private var ticks = 0 private var ticks = 0
private var nextTicketID = AtomicInteger() private var nextTicketID = 0
private var isBusy = false
private var chunk by Delegates.notNull<ServerChunk>()
private val targetState = Channel<ServerChunk.State>(Int.MAX_VALUE)
val scope = CoroutineScope(mailbox.asCoroutineDispatcher())
private var idleTicks = 0
private var isRemoved = false
val isValid: Boolean private suspend fun chunkGeneratorLoop() {
get() = temporary.isNotEmpty() || permanent.isNotEmpty() while (true) {
if (chunk.state == ServerChunk.State.FULL) {
break
}
val targetState = targetState.receive()
while (chunk.state < targetState) {
isBusy = true
val nextState = ServerChunk.State.entries[chunk.state.ordinal + 1]
try {
when (nextState) {
ServerChunk.State.TILES -> {
// tiles can be generated concurrently without any consequences
CompletableFuture.runAsync(Runnable { chunk.prepareCells() }, Starbound.EXECUTOR).await()
}
ServerChunk.State.MICRO_DUNGEONS -> {
//LOGGER.error("NYI: Generating microdungeons for $chunk")
}
ServerChunk.State.CAVE_LIQUID -> {
//LOGGER.error("NYI: Generating cave liquids for $chunk")
}
ServerChunk.State.TILE_ENTITIES -> {
//LOGGER.error("NYI: Generating tile entities for $chunk")
}
ServerChunk.State.ENTITIES -> {
//LOGGER.error("NYI: Placing entities for $chunk")
}
else -> {}
}
chunk.bumpState(nextState)
fulfilFutures()
} catch (err: Throwable) {
LOGGER.error("Exception while propagating $chunk to next generation state $nextState", err)
break
}
}
isBusy = false
}
isBusy = false
}
private suspend fun loadChunk0() {
try {
val cells = storage.loadCells(pos).await()
// very good.
if (cells.isPresent) {
chunk.loadCells(cells.value)
chunk.bumpState(ServerChunk.State.CAVE_LIQUID)
fulfilFutures()
storage.loadEntities(pos).await().ifPresent {
for (obj in it) {
obj.joinWorld(this@ServerWorld)
}
}
chunk.bumpState(ServerChunk.State.FULL)
fulfilFutures()
isBusy = false
return
} else {
// generate.
chunkGeneratorLoop()
}
} catch (err: Throwable) {
LOGGER.error("Exception while loading chunk $chunk", err)
}
}
private fun loadChunk() {
if (!calledLoadChunk)
return
calledLoadChunk = true
if (geometry.x.inBoundsChunk(pos.x) && geometry.y.inBoundsChunk(pos.y)) {
ticketListLock.withLock {
ticketLists.add(this)
}
val existing = chunkMap[pos]
if (existing == null) {
// fresh chunk
val chunk = chunkMap.compute(pos) ?: return ticketListLock.withLock {
isRemoved = true
ticketLists.remove(this)
ticketMap.remove(pos.toLong())
}
this.chunk = chunk
chunk.addListener(this)
isBusy = true
scope.launch { loadChunk0() }
fulfilFutures()
} else {
chunk = existing
existing.addListener(this)
fulfilFutures()
}
}
}
private fun unload() {
if (isRemoved)
return
isRemoved = true
scope.cancel()
targetState.close()
val removed = ticketMap.remove(pos.toLong())
check(removed == this) { "Expected to remove $this, but removed $removed" }
if (chunk.state == ServerChunk.State.FULL) {
val unloadable = entityIndex
.query(
chunk.aabb,
filter = Predicate { it.isApplicableForUnloading && chunk.aabb.isInside(it.position) },
distinct = true, withEdges = false)
storage.saveCells(pos, chunk.copyCells())
storage.saveEntities(pos, unloadable)
unloadable.forEach {
it.remove()
}
}
chunkMap.remove(pos)
}
fun tick(): Boolean { fun tick(): Boolean {
ticks++ ticks++
@ -378,60 +676,79 @@ class ServerWorld private constructor(
temporary.remove(ticket) temporary.remove(ticket)
} }
return temporary.isNotEmpty() || permanent.isNotEmpty() var shouldUnload = !isBusy && temporary.isEmpty() && permanent.isEmpty()
if (shouldUnload) {
idleTicks++
// don't load-save-load-save too frequently
shouldUnload = idleTicks > 600
} else {
idleTicks = 0
}
if (shouldUnload) {
unload()
}
return shouldUnload
}
private fun fulfilFutures() {
val permanent: List<Ticket>
val temporary: List<Ticket>
ticketListLock.withLock {
permanent = ObjectArrayList(this.permanent)
temporary = ObjectArrayList(this.permanent)
}
val state = chunk.state
permanent.forEach { if (it.targetState <= state) it.chunk.complete(chunk) }
temporary.forEach { if (it.targetState <= state) it.chunk.complete(chunk) }
} }
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
permanent.forEach { it.listener?.onCellChanges(x, y, cell) } val permanent: List<Ticket>
temporary.forEach { it.listener?.onCellChanges(x, y, cell) } val temporary: List<Ticket>
ticketListLock.withLock {
permanent = ObjectArrayList(this.permanent)
temporary = ObjectArrayList(this.permanent)
}
val state = chunk.state
permanent.forEach { if (it.targetState <= state) it.listener?.onCellChanges(x, y, cell) }
temporary.forEach { if (it.targetState <= state) it.listener?.onCellChanges(x, y, cell) }
} }
override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) { override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {
permanent.forEach { it.listener?.onTileHealthUpdate(x, y, isBackground, health) } val permanent: List<Ticket>
temporary.forEach { it.listener?.onTileHealthUpdate(x, y, isBackground, health) } val temporary: List<Ticket>
ticketListLock.withLock {
permanent = ObjectArrayList(this.permanent)
temporary = ObjectArrayList(this.permanent)
}
val state = chunk.state
permanent.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
temporary.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
} }
abstract inner class AbstractTicket : ITicket { abstract inner class AbstractTicket(val targetState: ServerChunk.State) : ITicket {
final override val id: Int = nextTicketID.getAndIncrement() final override val id: Int = nextTicketID++
final override val pos: ChunkPos final override val pos: ChunkPos
get() = this@TicketList.pos get() = this@TicketList.pos
final override var isCanceled: Boolean = false final override var isCanceled: Boolean = false
private var loadFuture: CompletableFuture<*>? = null final override val chunk = CompletableFuture<ServerChunk>()
fun init() { init {
if (first) { isBusy = true
first = false this@TicketList.targetState.trySend(targetState)
if (geometry.x.inBoundsChunk(pos.x) && geometry.y.inBoundsChunk(pos.y)) {
ticketListLock.withLock {
ticketLists.add(this@TicketList)
}
val existing = chunkMap[pos]
if (existing == null) {
loadFuture = storage.loadCells(pos).thenAccept { tiles ->
if (!tiles.isPresent) return@thenAccept
storage.loadEntities(pos).thenAcceptAsync(Consumer { ents ->
val chunk = chunkMap.compute(pos) ?: return@Consumer
chunk.loadCells(tiles.value)
ents.ifPresent {
for (obj in it) {
obj.joinWorld(this@ServerWorld)
}
}
}, mailbox)
}
} else {
existing.addListener(this@TicketList)
}
}
} else {
chunkMap[pos]?.addListener(this@TicketList)
}
} }
final override fun cancel() { final override fun cancel() {
@ -440,23 +757,20 @@ class ServerWorld private constructor(
ticketListLock.withLock { ticketListLock.withLock {
if (isCanceled) return if (isCanceled) return
isCanceled = true isCanceled = true
loadFuture?.cancel(false) chunk.cancel(false)
listener = null listener = null
cancel0() cancel0()
} }
} }
protected abstract fun cancel0() protected abstract fun cancel0()
final override val chunk: ServerChunk?
get() = chunkMap[pos]
final override var listener: IChunkListener? = null final override var listener: IChunkListener? = null
} }
inner class Ticket : AbstractTicket() { inner class Ticket(state: ServerChunk.State) : AbstractTicket(state) {
init { init {
permanent.add(this) permanent.add(this)
init() loadChunk()
} }
override fun cancel0() { override fun cancel0() {
@ -464,7 +778,7 @@ class ServerWorld private constructor(
} }
} }
inner class TimedTicket(expiresAt: Int) : AbstractTicket(), ITimedTicket { inner class TimedTicket(expiresAt: Int, state: ServerChunk.State) : AbstractTicket(state), ITimedTicket {
var expiresAt = expiresAt + ticks var expiresAt = expiresAt + ticks
override val timeRemaining: Int override val timeRemaining: Int
@ -472,7 +786,7 @@ class ServerWorld private constructor(
init { init {
temporary.add(this) temporary.add(this)
init() loadChunk()
} }
override fun cancel0() { override fun cancel0() {
@ -516,6 +830,25 @@ class ServerWorld private constructor(
return ServerWorld(server, WorldTemplate(geometry), storage, worldID) return ServerWorld(server, WorldTemplate(geometry), storage, worldID)
} }
fun create(server: StarboundServer, metadata: WorldStorage.Metadata, storage: WorldStorage, worldID: WorldID = WorldID.Limbo): ServerWorld {
return AssetPathStack("/") { _ ->
val meta = Starbound.gson.fromJson(metadata.data.content, MetadataJson::class.java)
val world = ServerWorld(server, WorldTemplate.fromJson(meta.worldTemplate), storage, worldID)
world.playerSpawnPosition = meta.playerStart
world.respawnInWorld = meta.respawnInWorld
world.adjustPlayerSpawn = meta.adjustPlayerStart
world.centralStructure = meta.centralStructure
for ((k, v) in meta.worldProperties.entrySet()) {
world.setProperty(k, v)
}
world.protectedDungeonIDs.addAll(meta.protectedDungeonIds)
world
}
}
fun load(server: StarboundServer, storage: WorldStorage, worldID: WorldID = WorldID.Limbo): CompletableFuture<ServerWorld> { fun load(server: StarboundServer, storage: WorldStorage, worldID: WorldID = WorldID.Limbo): CompletableFuture<ServerWorld> {
LOGGER.info("Attempting to load world at $worldID") LOGGER.info("Attempting to load world at $worldID")

View File

@ -42,7 +42,7 @@ import java.util.concurrent.atomic.AtomicBoolean
// allowing ServerConnection client to track ServerWorld state // allowing ServerConnection client to track ServerWorld state
class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, playerStart: Vector2d) { class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, playerStart: Vector2d) {
init { init {
LOGGER.info("$client is joining $world") LOGGER.info("Accepted ${client.alias()}")
client.worldStartAcknowledged = false client.worldStartAcknowledged = false
client.tracker = this client.tracker = this
@ -57,7 +57,6 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
private val isRemoved = AtomicBoolean() private val isRemoved = AtomicBoolean()
private var isActuallyRemoved = false private var isActuallyRemoved = false
private val tickets = HashMap<ChunkPos, Ticket>() private val tickets = HashMap<ChunkPos, Ticket>()
private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>()
private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>() private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>()
private val entityVersions = Int2LongOpenHashMap() private val entityVersions = Int2LongOpenHashMap()
@ -76,9 +75,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener { private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener {
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
if (pos !in pendingSend) { send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet()))
send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet()))
}
} }
override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) { override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {
@ -178,7 +175,6 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
for ((pos, ticket) in itr) { for ((pos, ticket) in itr) {
if (pos !in newTrackedChunks) { if (pos !in newTrackedChunks) {
pendingSend.remove(pos)
ticket.ticket.cancel() ticket.ticket.cancel()
itr.remove() itr.remove()
} }
@ -188,30 +184,22 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
if (pos !in tickets) { if (pos !in tickets) {
val ticket = world.permanentChunkTicket(pos) val ticket = world.permanentChunkTicket(pos)
val thisTicket = Ticket(ticket, pos) val thisTicket = Ticket(ticket, pos)
tickets[pos] = thisTicket tickets[pos] = thisTicket
ticket.listener = thisTicket ticket.listener = thisTicket
pendingSend.add(pos)
ticket.chunk.thenAccept {
if (client.isLegacy) {
send(LegacyTileArrayUpdatePacket(it))
it.tileDamagePackets().forEach { send(it) }
} else {
send(ChunkCellsPacket(it))
}
}
} }
} }
} }
run {
val itr = pendingSend.iterator()
for (pos in itr) {
val chunk = world.chunkMap[pos] ?: continue
if (client.isLegacy) {
send(LegacyTileArrayUpdatePacket(chunk))
chunk.tileDamagePackets().forEach { send(it) }
} else {
send(ChunkCellsPacket(chunk))
}
itr.remove()
}
}
run { run {
val trackingEntities = ObjectAVLTreeSet<AbstractEntity>() val trackingEntities = ObjectAVLTreeSet<AbstractEntity>()
@ -287,7 +275,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
val playerEntity = client.playerEntity val playerEntity = client.playerEntity
if (playerEntity != null) { if (playerEntity != null && world.worldID is WorldID.Celestial) {
client.returnWarp = WarpAction.World(world.worldID, SpawnTarget.Position(playerEntity.position)) client.returnWarp = WarpAction.World(world.worldID, SpawnTarget.Position(playerEntity.position))
} }
@ -295,6 +283,10 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
client.playerEntity = null client.playerEntity = null
client.worldID = WorldID.Limbo client.worldID = WorldID.Limbo
client.send(WorldStopPacket(reason)) client.send(WorldStopPacket(reason))
// this handles case where player is removed from world and
// instantly added back because new world rejected us
world.mailbox.execute { remove0() }
} }
} }

View File

@ -312,8 +312,8 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
if ( if (
intersection.intersects && intersection.intersects &&
intersection.point.get() != proposed.a && intersection.point.get() != proposed.p0 &&
intersection.point.get() != proposed.b intersection.point.get() != proposed.p1
) { ) {
valid = false valid = false
break break
@ -321,7 +321,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
if ( if (
proposed != existingLine && proposed != existingLine &&
proposed.distanceTo(existingLine.a) < universe.generationInformation.minimumConstellationLineCloseness proposed.distanceTo(existingLine.p0) < universe.generationInformation.minimumConstellationLineCloseness
) { ) {
valid = false valid = false
break break
@ -329,7 +329,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
if ( if (
proposed != existingLine.reverse() && proposed != existingLine.reverse() &&
proposed.distanceTo(existingLine.b) < universe.generationInformation.minimumConstellationLineCloseness proposed.distanceTo(existingLine.p1) < universe.generationInformation.minimumConstellationLineCloseness
) { ) {
valid = false valid = false
break break

View File

@ -0,0 +1,7 @@
package ru.dbotthepony.kstarbound.server.world
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
class WorldGenerator(val template: WorldTemplate) {
}

View File

@ -50,6 +50,20 @@ abstract class WorldStorage : Closeable {
} }
} }
object Nothing : WorldStorage() {
override fun loadCells(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
return CompletableFuture.completedFuture(KOptional())
}
override fun loadEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<AbstractEntity>>> {
return CompletableFuture.completedFuture(KOptional())
}
override fun loadMetadata(): CompletableFuture<KOptional<Metadata>> {
return CompletableFuture.completedFuture(KOptional())
}
}
companion object { companion object {
val NULL: WorldStorage = Fixed(AbstractCell.NULL) val NULL: WorldStorage = Fixed(AbstractCell.NULL)
val EMPTY: WorldStorage = Fixed(AbstractCell.EMPTY) val EMPTY: WorldStorage = Fixed(AbstractCell.EMPTY)

View File

@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager
import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryStack
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.WindowsBindings import ru.dbotthepony.kstarbound.WindowsBindings
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.function.BooleanSupplier import java.util.function.BooleanSupplier
@ -52,16 +53,14 @@ class ExecutionSpinner(private val waiter: Runnable, private val spinner: Boolea
private var compensate = 0L private var compensate = 0L
private var carrier: Thread? = null private var carrier: Thread? = null
@Volatile private val pause = AtomicInteger()
private var isPaused = false
fun pause() { fun pause() {
isPaused = true pause.incrementAndGet()
} }
fun unpause() { fun unpause() {
if (isPaused) { if (pause.addAndGet(-100) <= 0) {
isPaused = false
carrier?.let { LockSupport.unpark(it) } carrier?.let { LockSupport.unpark(it) }
} }
} }
@ -69,7 +68,7 @@ class ExecutionSpinner(private val waiter: Runnable, private val spinner: Boolea
fun spin(): Boolean { fun spin(): Boolean {
carrier = Thread.currentThread() carrier = Thread.currentThread()
while (isPaused) { while (pause.get() > 0) {
waiter.run() waiter.run()
LockSupport.park() LockSupport.park()
} }

View File

@ -0,0 +1,443 @@
package ru.dbotthepony.kstarbound.util
import ru.dbotthepony.kommons.util.JVMTimeSource
import java.util.*
import java.util.concurrent.Callable
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Delayed
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.FutureTask
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.LockSupport
import java.util.function.Consumer
import kotlin.NoSuchElementException
import kotlin.collections.ArrayList
private fun <E : Comparable<E>> LinkedList<E>.enqueue(value: E) {
if (isEmpty()) {
add(value)
} else if (first >= value) {
addFirst(value)
} else if (last <= value) {
addLast(value)
} else {
val iterator = listIterator()
while (iterator.hasNext()) {
val i = iterator.next()
if (i >= value) {
iterator.previous()
iterator.add(value)
break
}
}
}
}
/**
* [ScheduledExecutorService] which act as a mailbox, [executeQueuedTasks] must be called from main thread.
*
* [submit], [execute], etc can be called on any thread. If any of enqueueing methods are called on the same thread
* as where [executeQueuedTasks] was called, executes provided lambda immediately and returns completed future.
*/
class MailboxExecutorService(@Volatile var thread: Thread = Thread.currentThread()) : ScheduledExecutorService {
private val futureQueue = ConcurrentLinkedQueue<FutureTask<*>>()
private val timers = LinkedList<Timer<*>>()
private val repeatableTimers = LinkedList<RepeatableTimer>()
@Volatile
private var isShutdown = false
@Volatile
private var isTerminated = false
private val timeOrigin = JVMTimeSource()
var exceptionHandler: Consumer<Throwable>? = null
private inner class Timer<T>(task: Callable<T>, val executeAt: Long) : FutureTask<T>(task), ScheduledFuture<T> {
override fun compareTo(other: Delayed): Int {
return getDelay(TimeUnit.NANOSECONDS).compareTo(other.getDelay(TimeUnit.NANOSECONDS))
}
override fun getDelay(unit: TimeUnit): Long {
return unit.convert(executeAt, TimeUnit.NANOSECONDS) - timeOrigin.nanos
}
}
private data class CompletedFuture<T>(private val value: T) : Future<T> {
override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
return false
}
override fun isCancelled(): Boolean {
return false
}
override fun isDone(): Boolean {
return true
}
override fun get(): T {
return value
}
override fun get(timeout: Long, unit: TimeUnit): T {
return value
}
companion object {
val VOID = CompletedFuture(Unit)
}
}
private inner class RepeatableTimer(
task: Runnable,
initialDelay: Long,
val period: Long,
val fixedDelay: Boolean,
): FutureTask<Unit>({ task.run() }), ScheduledFuture<Unit> {
var next = initialDelay
private set
public override fun runAndReset(): Boolean {
if (fixedDelay) {
next += period
return super.runAndReset()
} else {
try {
return super.runAndReset()
} finally {
next += period
}
}
}
override fun compareTo(other: Delayed): Int {
return getDelay(TimeUnit.NANOSECONDS).compareTo(other.getDelay(TimeUnit.NANOSECONDS))
}
override fun getDelay(unit: TimeUnit): Long {
return unit.convert(next, TimeUnit.NANOSECONDS) - timeOrigin.nanos
}
}
fun isSameThread(): Boolean {
return Thread.currentThread() === thread
}
fun executeQueuedTasks() {
thread = Thread.currentThread()
if (isShutdown) {
if (!isTerminated) {
isTerminated = true
futureQueue.forEach {
it.cancel(false)
}
futureQueue.clear()
timers.clear()
repeatableTimers.clear()
return
}
}
var next = futureQueue.poll()
while (next != null) {
if (isTerminated) return
next.run()
Thread.interrupted()
try {
next.get()
} catch (err: ExecutionException) {
exceptionHandler?.accept(err)
}
next = futureQueue.poll()
}
while (!timers.isEmpty()) {
if (isTerminated) return
val first = timers.first
if (first.isCancelled) {
timers.removeFirst()
} else if (first.executeAt <= timeOrigin.nanos) {
first.run()
Thread.interrupted()
try {
first.get()
} catch (err: ExecutionException) {
exceptionHandler?.accept(err)
}
timers.removeFirst()
} else {
break
}
}
if (repeatableTimers.isNotEmpty()) {
val executed = LinkedList<RepeatableTimer>()
while (repeatableTimers.isNotEmpty()) {
if (isTerminated) return
val first = repeatableTimers.first
if (first.isDone) {
repeatableTimers.removeFirst()
} else if (first.next <= timeOrigin.nanos) {
if (first.runAndReset()) {
executed.add(first)
}
repeatableTimers.removeFirst()
} else {
break
}
}
executed.forEach { repeatableTimers.enqueue(it) }
}
}
override fun execute(command: Runnable) {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
command.run()
} else {
futureQueue.add(FutureTask(command, Unit))
LockSupport.unpark(thread)
}
}
override fun shutdown() {
isShutdown = true
}
override fun shutdownNow(): List<Runnable> {
if (isTerminated) return listOf()
isShutdown = true
isTerminated = true
val result = ArrayList<Runnable>()
futureQueue.forEach {
it.cancel(false)
result.add(it)
}
futureQueue.clear()
timers.forEach { it.cancel(false) }
repeatableTimers.forEach { it.cancel(false) }
timers.clear()
repeatableTimers.clear()
return result
}
override fun isShutdown(): Boolean {
return isShutdown
}
override fun isTerminated(): Boolean {
return isTerminated
}
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {
throw UnsupportedOperationException()
}
override fun <T : Any?> submit(task: Callable<T>): Future<T> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) return CompletedFuture(task.call())
return FutureTask(task).also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun <T : Any?> submit(task: Runnable, result: T): Future<T> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { task.run(); return CompletedFuture(result) }
return FutureTask { task.run(); result }.also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun submit(task: Runnable): Future<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) { task.run(); return CompletedFuture.VOID }
return FutureTask { task.run() }.also { futureQueue.add(it); LockSupport.unpark(thread) }
}
override fun <T : Any?> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.map { CompletedFuture(it.call()) }
} else {
return tasks.map { submit(it) }.onEach { it.get() }
}
}
override fun <T : Any?> invokeAll(
tasks: Collection<Callable<T>>,
timeout: Long,
unit: TimeUnit
): List<Future<T>> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.map { CompletedFuture(it.call()) }
} else {
return tasks.map { submit(it) }.onEach { it.get(timeout, unit) }
}
}
override fun <T : Any?> invokeAny(tasks: Collection<Callable<T>>): T {
if (tasks.isEmpty())
throw NoSuchElementException("Provided task list is empty")
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.first().call()
} else {
return submit(tasks.first()).get()
}
}
override fun <T : Any?> invokeAny(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): T {
if (tasks.isEmpty())
throw NoSuchElementException("Provided task list is empty")
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (isSameThread()) {
return tasks.first().call()
} else {
return submit(tasks.first()).get(timeout, unit)
}
}
fun <V> join(future: Future<V>): V {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
if (!isSameThread())
return future.get()
while (!future.isDone) {
executeQueuedTasks()
LockSupport.parkNanos(1_000_000L)
}
return future.get()
}
override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
val timer = Timer({ command.run() }, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit))
if (isSameThread() && delay <= 0L) {
timer.run()
Thread.interrupted()
} else if (isSameThread()) {
timers.enqueue(timer)
} else {
execute {
if (timer.isCancelled) {
// do nothing
} else if (timer.executeAt <= timeOrigin.nanos) {
timer.run()
Thread.interrupted()
} else {
timers.enqueue(timer)
}
}
}
return timer
}
override fun <V : Any?> schedule(callable: Callable<V>, delay: Long, unit: TimeUnit): ScheduledFuture<V> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
val timer = Timer(callable, timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(delay, unit))
if (isSameThread() && delay <= 0L) {
timer.run()
Thread.interrupted()
} else if (isSameThread()) {
timers.enqueue(timer)
} else {
execute {
if (timer.isCancelled) {
// do nothing
} else if (timer.executeAt <= timeOrigin.nanos) {
timer.run()
Thread.interrupted()
} else {
timers.enqueue(timer)
}
}
}
return timer
}
override fun scheduleAtFixedRate(
command: Runnable,
initialDelay: Long,
period: Long,
unit: TimeUnit
): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
return RepeatableTimer(
command,
timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit),
TimeUnit.NANOSECONDS.convert(period, unit), true)
.also {
execute {
if (it.isCancelled) {
// do nothing
} else {
repeatableTimers.enqueue(it)
}
}
}
}
override fun scheduleWithFixedDelay(
command: Runnable,
initialDelay: Long,
delay: Long,
unit: TimeUnit
): ScheduledFuture<*> {
if (isShutdown) throw RejectedExecutionException("This mailbox is shutting down")
return RepeatableTimer(
command,
timeOrigin.nanos + TimeUnit.NANOSECONDS.convert(initialDelay, unit),
TimeUnit.NANOSECONDS.convert(delay, unit), false)
.also {
execute {
if (it.isCancelled) {
// do nothing
} else {
repeatableTimers.enqueue(it)
}
}
}
}
}

View File

@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.gson.value
import java.util.concurrent.CompletableFuture
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.sqrt import kotlin.math.sqrt
@ -30,6 +31,7 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
var hasSeedSpecified = false var hasSeedSpecified = false
private set private set
private var initializationTask: CompletableFuture<Unit>? = null
private var isInitialized = false private var isInitialized = false
private val initLock = Any() private val initLock = Any()
@ -52,6 +54,11 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
} }
private fun doInit(seed: Long) { private fun doInit(seed: Long) {
val p = p
val g1 = g1
val g2 = g2
val g3 = g3
p.fill(0) p.fill(0)
g1.fill(0.0) g1.fill(0.0)
g2.fill(0.0) g2.fill(0.0)
@ -256,11 +263,7 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
out.nullValue() out.nullValue()
else { else {
val json = parent.toJsonTree(value.parameters) as JsonObject val json = parent.toJsonTree(value.parameters) as JsonObject
json["seed"] = value.seed
if (value.seed != 0L) {
json["seed"] = value.seed
}
out.value(json) out.value(json)
} }
} }

View File

@ -1,23 +1,14 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.objects.ObjectArraySet
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.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
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
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
/** /**
@ -74,118 +65,6 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() } Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { _, _ -> TileHealth.Tile() }
} }
data class DamageResult(val result: TileDamageResult, val health: TileHealth? = null, val stateBefore: AbstractCell? = null)
fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): DamageResult {
if (!cells.isInitialized()) {
return DamageResult(TileDamageResult.NONE)
}
val cell = cells.value[pos.x, pos.y]
if (cell.isIndestructible || cell.tile(isBackground).material.isBuiltin) {
return DamageResult(TileDamageResult.NONE)
}
var damage = damage
var result = TileDamageResult.NORMAL
if (cell.dungeonId in world.protectedDungeonIDs) {
damage = damage.copy(type = TileDamageType.PROTECTED)
result = TileDamageResult.PROTECTED
}
val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y]
val tile = cell.tile(isBackground)
val params = if (!damage.type.isPenetrating && tile.modifier != null && tile.modifier!!.value.breaksWithTile) {
tile.material.value.actualDamageTable + tile.modifier!!.value.actualDamageTable
} else {
tile.material.value.actualDamageTable
}
health.damage(params, sourcePosition, damage)
subscribers.forEach { it.onTileHealthUpdate(pos.x, pos.y, isBackground, health) }
if (health.isDead) {
if (isBackground) {
damagedTilesBackground.remove(pos)
} else {
damagedTilesForeground.remove(pos)
}
val copyHealth = health.copy()
val mCell = cell.mutable()
val mTile = mCell.tile(isBackground)
mTile.material = BuiltinMetaMaterials.EMPTY
mTile.color = TileColor.DEFAULT
mTile.hueShift = 0f
if (tile.modifier != null && mTile.modifier!!.value.breaksWithTile) {
mTile.modifier = null
}
setCell(pos.x, pos.y, mCell.immutable())
health.reset()
return DamageResult(result, copyHealth, cell)
} else {
if (isBackground) {
damagedTilesBackground.add(pos)
} else {
damagedTilesForeground.add(pos)
}
return DamageResult(result, health, cell)
}
}
protected val damagedTilesForeground = ObjectArraySet<Vector2i>()
protected val damagedTilesBackground = ObjectArraySet<Vector2i>()
fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> {
if (cells.isInitialized()) {
val cells = cells.value
return Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { a, b -> cells[a, b].toLegacyNet() }
} else {
return Object2DArray(CHUNK_SIZE, CHUNK_SIZE, LegacyNetworkCellState.NULL)
}
}
fun tileDamagePackets(): List<TileDamageUpdatePacket> {
val result = ArrayList<TileDamageUpdatePacket>()
if (tileHealthBackground.isInitialized()) {
val tileHealthBackground = tileHealthBackground.value
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
val health = tileHealthBackground[x, y]
if (!health.isHealthy) {
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, true, health))
}
}
}
}
if (tileHealthForeground.isInitialized()) {
val tileHealthForeground = tileHealthForeground.value
for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) {
val health = tileHealthForeground[x, y]
if (!health.isHealthy) {
result.add(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, false, health))
}
}
}
}
return result
}
fun loadCells(source: Object2DArray<out AbstractCell>) { fun loadCells(source: Object2DArray<out AbstractCell>) {
val ours = cells.value val ours = cells.value
source.checkSizeEquals(ours) source.checkSizeEquals(ours)
@ -286,28 +165,11 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
open fun remove() { open fun remove() {
} }
open fun tick() { open fun tick() {
if (cells.isInitialized() && (damagedTilesBackground.isNotEmpty() || damagedTilesForeground.isNotEmpty())) {
val tileHealthBackground = tileHealthBackground.value
val tileHealthForeground = tileHealthForeground.value
val cells = cells.value
damagedTilesBackground.removeIf { (x, y) ->
val health = tileHealthBackground[x, y]
val result = !health.tick(cells[x, y].background.material.value.actualDamageTable)
subscribers.forEach { it.onTileHealthUpdate(x, y, true, health) }
result
}
damagedTilesForeground.removeIf { (x, y) ->
val health = tileHealthForeground[x, y]
val result = !health.tick(cells[x, y].foreground.material.value.actualDamageTable)
subscribers.forEach { it.onTileHealthUpdate(x, y, false, health) }
result
}
}
} }
companion object { companion object {

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.world
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.math.divideUp import ru.dbotthepony.kstarbound.math.divideUp
import kotlin.math.absoluteValue
import kotlin.math.pow import kotlin.math.pow
fun positiveModulo(a: Int, b: Int): Int { fun positiveModulo(a: Int, b: Int): Int {
@ -68,6 +69,9 @@ abstract class CoordinateMapper {
abstract fun diff(a: Int, b: Int): Int abstract fun diff(a: Int, b: Int): Int
abstract fun diff(a: Double, b: Double): Double abstract fun diff(a: Double, b: Double): Double
abstract fun nearestTo(source: Int, target: Int): Int
abstract fun nearestTo(source: Double, target: Double): Double
class Wrapper(private val cells: Int) : CoordinateMapper() { class Wrapper(private val cells: Int) : CoordinateMapper() {
override val chunks = divideUp(cells, CHUNK_SIZE) override val chunks = divideUp(cells, CHUNK_SIZE)
private val cellsD = cells.toDouble() private val cellsD = cells.toDouble()
@ -121,6 +125,22 @@ abstract class CoordinateMapper {
return diff return diff
} }
override fun nearestTo(source: Int, target: Int): Int {
if ((target - source).absoluteValue < cells / 2) {
return target
} else {
return diff(target, source) + source
}
}
override fun nearestTo(source: Double, target: Double): Double {
if ((target - source).absoluteValue < cells / 2) {
return target
} else {
return diff(target, source) + source
}
}
override fun chunkFromCell(value: Int): Int { override fun chunkFromCell(value: Int): Int {
return chunk(value shr CHUNK_SIZE_BITS) return chunk(value shr CHUNK_SIZE_BITS)
} }
@ -272,5 +292,13 @@ abstract class CoordinateMapper {
override fun chunk(value: Int): Int { override fun chunk(value: Int): Int {
return value.coerceIn(0, chunks - 1) return value.coerceIn(0, chunks - 1)
} }
override fun nearestTo(source: Int, target: Int): Int {
return diff(source, target)
}
override fun nearestTo(source: Double, target: Double): Double {
return diff(source, target)
}
} }
} }

View File

@ -89,14 +89,9 @@ class Sky() {
} }
} }
constructor(parameters: SkyParameters, inOrbit: Boolean) : this() { constructor(parameters: SkyParameters) : this() {
skyParametersNetState.value = parameters.copy() skyParametersNetState.value = parameters.copy()
skyType = parameters.skyType
if (inOrbit) {
skyType = SkyType.ORBITAL
} else {
skyType = parameters.skyType
}
} }
fun startFlying(enterHyperspace: Boolean, startInWarp: Boolean = false) { fun startFlying(enterHyperspace: Boolean, startInWarp: Boolean = false) {

View File

@ -14,7 +14,6 @@ import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.MailboxExecutorService
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.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -25,6 +24,7 @@ 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.ExceptionLogger import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.util.ParallelPerform
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
@ -44,12 +44,13 @@ import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import java.util.stream.Stream import java.util.stream.Stream
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, Closeable {
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 mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) }
val sky = Sky() val sky = Sky(template.skyParameters)
val geometry: WorldGeometry = template.geometry val geometry: WorldGeometry = template.geometry
val nextEntityID = AtomicInteger() val nextEntityID = AtomicInteger()
@ -311,6 +312,34 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
) as List<TileEntity> ) as List<TileEntity>
} }
fun matchCells(aabb: AABBi, predicate: Predicate<AbstractCell>): Boolean {
for (split in geometry.split(aabb).first) {
for (x in split.mins.x .. split.maxs.x) {
for (y in split.mins.x .. split.maxs.x) {
if (predicate.test(chunkMap.getCell(x, y))) {
return true
}
}
}
}
return false
}
fun matchCells(aabb: AABB, predicate: Predicate<AbstractCell>): Boolean {
for (split in geometry.split(aabb).first) {
for (x in split.mins.x.toInt() .. split.maxs.x.roundToInt()) {
for (y in split.mins.y.toInt() .. split.maxs.y.roundToInt()) {
if (predicate.test(chunkMap.getCell(x, y))) {
return true
}
}
}
}
return false
}
fun queryTileCollisions(aabb: AABB): MutableList<CollisionPoly> { fun queryTileCollisions(aabb: AABB): MutableList<CollisionPoly> {
val result = ArrayList<CollisionPoly>() val result = ArrayList<CollisionPoly>()
val tiles = aabb.encasingIntAABB() val tiles = aabb.encasingIntAABB()

View File

@ -11,10 +11,13 @@ import ru.dbotthepony.kommons.util.IStruct2f
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.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Boolean) { @JsonFactory
data class WorldGeometry(val size: Vector2i, val loopX: Boolean = true, val loopY: Boolean = false) {
constructor(buff: DataInputStream) : this(buff.readVector2i(), buff.readBoolean(), buff.readBoolean()) constructor(buff: DataInputStream) : this(buff.readVector2i(), buff.readBoolean(), buff.readBoolean())
init { init {
@ -214,6 +217,11 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool
return ObjectArraySet.ofUnchecked(*result.toTypedArray()) return ObjectArraySet.ofUnchecked(*result.toTypedArray())
} }
fun rectContains(rect: AABB, point: IStruct2d): Boolean {
val wrap = wrap(point)
return split(rect).first.any { it.isInside(wrap) }
}
fun diff(a: Vector2i, b: Vector2i): Vector2i { fun diff(a: Vector2i, b: Vector2i): Vector2i {
return Vector2i(x.diff(a.x, b.x), y.diff(a.y, b.y)) return Vector2i(x.diff(a.x, b.x), y.diff(a.y, b.y))
} }
@ -221,4 +229,11 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool
fun diff(a: Vector2d, b: Vector2d): Vector2d { fun diff(a: Vector2d, b: Vector2d): Vector2d {
return Vector2d(x.diff(a.x, b.x), y.diff(a.y, b.y)) return Vector2d(x.diff(a.x, b.x), y.diff(a.y, b.y))
} }
fun nearestTo(source: IStruct2i, target: IStruct2i) = Vector2i(x.nearestTo(source.component1(), target.component1()), y.nearestTo(source.component2(), target.component2()))
fun nearestTo(source: IStruct2d, target: IStruct2d) = Vector2d(x.nearestTo(source.component1(), target.component1()), y.nearestTo(source.component2(), target.component2()))
fun polyDistance(poly: Poly, point: IStruct2d): Double {
return poly.distance(nearestTo(poly.centre, point))
}
} }

View File

@ -3,8 +3,6 @@ package ru.dbotthepony.kstarbound.world.api
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
@ -13,16 +11,28 @@ sealed class AbstractCell {
abstract val background: AbstractTileState abstract val background: AbstractTileState
abstract val liquid: AbstractLiquidState abstract val liquid: AbstractLiquidState
// ushort, can be anything (but 65535), mostly used by placed dungeons
// this value determines special logic behind this cell, such as if it is protected from modifications
// by players or other means
abstract val dungeonId: Int abstract val dungeonId: Int
abstract val biome: Int
// ubyte, points at biome from current WorldLayout
// in context of block
// value of 0 points to null, 1 points to 0, and so on
abstract val blockBiome: Int
// ubyte, points at biome from current WorldLayout
// in context of environment
// value of 0 points to null, 1 points to 0, and so on
abstract val envBiome: Int abstract val envBiome: Int
// whenever if cell ignores any attempts to damage it
abstract val isIndestructible: Boolean abstract val isIndestructible: Boolean
abstract fun immutable(): ImmutableCell abstract fun immutable(): ImmutableCell
abstract fun mutable(): MutableCell abstract fun mutable(): MutableCell
open fun toLegacyNet(): LegacyNetworkCellState { open fun toLegacyNet(): LegacyNetworkCellState {
return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, biome, envBiome, liquid.toLegacyNet(), dungeonId) return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, blockBiome, envBiome, liquid.toLegacyNet(), dungeonId)
} }
abstract fun tile(background: Boolean): AbstractTileState abstract fun tile(background: Boolean): AbstractTileState
@ -35,7 +45,7 @@ sealed class AbstractCell {
stream.write(0) // collisionMap stream.write(0) // collisionMap
stream.writeShort(dungeonId) stream.writeShort(dungeonId)
stream.writeByte(biome) stream.writeByte(blockBiome)
stream.writeByte(envBiome) stream.writeByte(envBiome)
stream.writeBoolean(isIndestructible) stream.writeBoolean(isIndestructible)

View File

@ -9,7 +9,7 @@ import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
sealed class AbstractLiquidState { sealed class AbstractLiquidState {
abstract val def: Registry.Entry<LiquidDefinition>? abstract val state: Registry.Entry<LiquidDefinition>
abstract val level: Float abstract val level: Float
abstract val pressure: Float abstract val pressure: Float
abstract val isInfinite: Boolean abstract val isInfinite: Boolean
@ -18,15 +18,15 @@ sealed class AbstractLiquidState {
abstract fun immutable(): ImmutableLiquidState abstract fun immutable(): ImmutableLiquidState
fun toLegacyNet(): LegacyNetworkLiquidState { fun toLegacyNet(): LegacyNetworkLiquidState {
if (def?.id != null && def!!.id!! in 1 .. 255) { if (state.id != null && state.id!! in 1 .. 255) {
return LegacyNetworkLiquidState(def!!.id!!, (level * 255f).toInt().coerceIn(0, 255)) return LegacyNetworkLiquidState(state.id!!, (level * 255f).toInt().coerceIn(0, 255))
} else { } else {
return LegacyNetworkLiquidState.EMPTY return LegacyNetworkLiquidState.EMPTY
} }
} }
fun write(stream: DataOutputStream) { fun write(stream: DataOutputStream) {
stream.writeByte(def?.id ?: 0) stream.writeByte(state.id ?: 0)
stream.writeFloat(level) stream.writeFloat(level)
stream.writeFloat(pressure) stream.writeFloat(pressure)
stream.writeBoolean(isInfinite) stream.writeBoolean(isInfinite)

View File

@ -12,7 +12,7 @@ import java.io.DataOutputStream
sealed class AbstractTileState { sealed class AbstractTileState {
abstract val material: Registry.Entry<TileDefinition> abstract val material: Registry.Entry<TileDefinition>
abstract val modifier: Registry.Entry<TileModifierDefinition>? abstract val modifier: Registry.Entry<TileModifierDefinition>
abstract val color: TileColor abstract val color: TileColor
abstract val hueShift: Float abstract val hueShift: Float
abstract val modifierHueShift: Float abstract val modifierHueShift: Float
@ -30,8 +30,8 @@ sealed class AbstractTileState {
open fun toLegacyNet(): LegacyNetworkTileState { open fun toLegacyNet(): LegacyNetworkTileState {
if (material.id != null && material.id in 0 .. 65535) { if (material.id != null && material.id in 0 .. 65535) {
val validMod = modifier?.id != null && modifier!!.id!! in 0 .. 65535 val validMod = modifier.id != null && modifier.id!! in 0 .. 65535
return LegacyNetworkTileState(material.id!!, byteHueShift(), color.ordinal, if (validMod) modifier!!.id!! else 0, if (validMod) byteModifierHueShift() else 0) return LegacyNetworkTileState(material.id!!, byteHueShift(), color.ordinal, if (validMod) modifier.id!! else 0, if (validMod) byteModifierHueShift() else 0)
} else { } else {
return LegacyNetworkTileState.EMPTY return LegacyNetworkTileState.EMPTY
} }
@ -39,10 +39,10 @@ sealed class AbstractTileState {
fun write(stream: DataOutputStream) { fun write(stream: DataOutputStream) {
stream.writeShort(material.id ?: 0) stream.writeShort(material.id ?: 0)
stream.writeBoolean(modifier != null) stream.writeByte(byteHueShift())
stream.writeShort(modifier?.id ?: 0)
stream.writeByte(color.ordinal) stream.writeByte(color.ordinal)
stream.write(byteHueShift())
stream.writeShort(modifier.id ?: 0)
stream.write(byteModifierHueShift()) stream.write(byteModifierHueShift())
} }

View File

@ -8,7 +8,7 @@ data class ImmutableCell(
override val liquid: ImmutableLiquidState = AbstractLiquidState.EMPTY, override val liquid: ImmutableLiquidState = AbstractLiquidState.EMPTY,
override val dungeonId: Int = 0, override val dungeonId: Int = 0,
override val biome: Int = 0, override val blockBiome: Int = 0,
override val envBiome: Int = 0, override val envBiome: Int = 0,
override val isIndestructible: Boolean = false, override val isIndestructible: Boolean = false,
) : AbstractCell() { ) : AbstractCell() {
@ -30,6 +30,6 @@ data class ImmutableCell(
} }
override fun mutable(): MutableCell { override fun mutable(): MutableCell {
return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, biome, envBiome, isIndestructible) return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, blockBiome, envBiome, isIndestructible)
} }
} }

View File

@ -1,16 +1,17 @@
package ru.dbotthepony.kstarbound.world.api package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
data class ImmutableLiquidState( data class ImmutableLiquidState(
override val def: Registry.Entry<LiquidDefinition>? = null, override val state: Registry.Entry<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID,
override val level: Float = 0f, override val level: Float = 0f,
override val pressure: Float = 0f, override val pressure: Float = 0f,
override val isInfinite: Boolean = false, override val isInfinite: Boolean = false,
) : AbstractLiquidState() { ) : AbstractLiquidState() {
override fun mutable(): MutableLiquidState { override fun mutable(): MutableLiquidState {
return MutableLiquidState(def, level, pressure, isInfinite) return MutableLiquidState(state, level, pressure, isInfinite)
} }
override fun immutable(): ImmutableLiquidState { override fun immutable(): ImmutableLiquidState {

View File

@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.network.LegacyNetworkTileState
data class ImmutableTileState( data class ImmutableTileState(
override var material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.NULL, override var material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.NULL,
override var modifier: Registry.Entry<TileModifierDefinition>? = null, override var modifier: Registry.Entry<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD,
override var color: TileColor = TileColor.DEFAULT, override var color: TileColor = TileColor.DEFAULT,
override var hueShift: Float = 0f, override var hueShift: Float = 0f,
override var modifierHueShift: Float = 0f, override var modifierHueShift: Float = 0f,

View File

@ -3,12 +3,12 @@ package ru.dbotthepony.kstarbound.world.api
import java.io.DataInputStream import java.io.DataInputStream
data class MutableCell( data class MutableCell(
override var foreground: MutableTileState = MutableTileState(), override val foreground: MutableTileState = MutableTileState(),
override var background: MutableTileState = MutableTileState(), override val background: MutableTileState = MutableTileState(),
override var liquid: MutableLiquidState = MutableLiquidState(), override val liquid: MutableLiquidState = MutableLiquidState(),
override var dungeonId: Int = 0, override var dungeonId: Int = 0,
override var biome: Int = 0, override var blockBiome: Int = 0,
override var envBiome: Int = 0, override var envBiome: Int = 0,
override var isIndestructible: Boolean = false, override var isIndestructible: Boolean = false,
) : AbstractCell() { ) : AbstractCell() {
@ -20,7 +20,7 @@ data class MutableCell(
stream.skipNBytes(1) // collisionMap stream.skipNBytes(1) // collisionMap
dungeonId = stream.readUnsignedShort() dungeonId = stream.readUnsignedShort()
biome = stream.readUnsignedByte() blockBiome = stream.readUnsignedByte()
envBiome = stream.readUnsignedByte() envBiome = stream.readUnsignedByte()
isIndestructible = stream.readBoolean() isIndestructible = stream.readBoolean()
@ -36,7 +36,7 @@ data class MutableCell(
} }
override fun immutable(): ImmutableCell { override fun immutable(): ImmutableCell {
return POOL.intern(ImmutableCell(foreground.immutable(), background.immutable(), liquid.immutable(), dungeonId, biome, envBiome, isIndestructible)) return POOL.intern(ImmutableCell(foreground.immutable(), background.immutable(), liquid.immutable(), dungeonId, blockBiome, envBiome, isIndestructible))
} }
override fun mutable(): MutableCell { override fun mutable(): MutableCell {

View File

@ -2,28 +2,36 @@ package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import java.io.DataInputStream import java.io.DataInputStream
data class MutableLiquidState( data class MutableLiquidState(
override var def: Registry.Entry<LiquidDefinition>? = null, override var state: Registry.Entry<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID,
override var level: Float = 0f, override var level: Float = 0f,
override var pressure: Float = 0f, override var pressure: Float = 0f,
override var isInfinite: Boolean = false, override var isInfinite: Boolean = false,
) : AbstractLiquidState() { ) : AbstractLiquidState() {
fun read(stream: DataInputStream): MutableLiquidState { fun read(stream: DataInputStream): MutableLiquidState {
def = Registries.liquid[stream.readUnsignedByte()] state = Registries.liquid[stream.readUnsignedByte()] ?: BuiltinMetaMaterials.NO_LIQUID
level = stream.readFloat() level = stream.readFloat()
pressure = stream.readFloat() pressure = stream.readFloat()
isInfinite = stream.readBoolean() isInfinite = stream.readBoolean()
return this return this
} }
fun reset() {
state = BuiltinMetaMaterials.NO_LIQUID
level = 0f
pressure = 0f
isInfinite = false
}
override fun mutable(): MutableLiquidState { override fun mutable(): MutableLiquidState {
return this return this
} }
override fun immutable(): ImmutableLiquidState { override fun immutable(): ImmutableLiquidState {
return POOL.intern(ImmutableLiquidState(def, level, pressure, isInfinite)) return POOL.intern(ImmutableLiquidState(state, level, pressure, isInfinite))
} }
} }

View File

@ -9,7 +9,7 @@ import java.io.DataInputStream
data class MutableTileState( data class MutableTileState(
override var material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.NULL, override var material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.NULL,
override var modifier: Registry.Entry<TileModifierDefinition>? = null, override var modifier: Registry.Entry<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD,
override var color: TileColor = TileColor.DEFAULT, override var color: TileColor = TileColor.DEFAULT,
override var hueShift: Float = 0f, override var hueShift: Float = 0f,
override var modifierHueShift: Float = 0f, override var modifierHueShift: Float = 0f,
@ -48,10 +48,10 @@ data class MutableTileState(
fun read(stream: DataInputStream): MutableTileState { fun read(stream: DataInputStream): MutableTileState {
material = Registries.tiles[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY material = Registries.tiles[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY
setHueShift(stream.read()) setHueShift(stream.readUnsignedByte())
color = TileColor.of(stream.read()) color = TileColor.of(stream.readUnsignedByte())
modifier = Registries.tileModifiers[stream.readUnsignedShort()] modifier = Registries.tileModifiers[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY_MOD
setModHueShift(stream.read()) setModHueShift(stream.readUnsignedByte())
return this return this
} }
} }

View File

@ -6,7 +6,6 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayList
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
@ -22,6 +21,7 @@ import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.SpatialIndex import ru.dbotthepony.kstarbound.world.SpatialIndex
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World

View File

@ -1,15 +1,15 @@
package ru.dbotthepony.kstarbound.world.physics package ru.dbotthepony.kstarbound.world.physics
enum class CollisionType(val isEmpty: Boolean) { enum class CollisionType(val isEmpty: Boolean, val isSolidCollision: Boolean, val isTileCollision: Boolean) {
// not loaded, block collisions by default // not loaded, block collisions by default
NULL(true), NULL(true, true, false),
// air // air
NONE(true), NONE(true, false, false),
// including stairs made of platforms // including stairs made of platforms
PLATFORM(false), PLATFORM(false, false, false),
DYNAMIC(false), DYNAMIC(false, true, false),
SLIPPERY(false), SLIPPERY(false, true, true),
BLOCK(false); BLOCK(false, true, true);
fun maxOf(other: CollisionType): CollisionType { fun maxOf(other: CollisionType): CollisionType {
if (this === NULL || other === NULL) if (this === NULL || other === NULL)

View File

@ -26,6 +26,7 @@ import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kstarbound.json.listAdapter import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec 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
@ -34,7 +35,7 @@ import kotlin.math.absoluteValue
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
private fun calculateEdges(points: List<Vector2d>): Pair<ImmutableList<Poly.Edge>, ImmutableList<Vector2d>> { private fun calculateEdges(points: List<Vector2d>): Pair<ImmutableList<Line2d>, ImmutableList<Vector2d>> {
require(points.size >= 2) { "Provided poly is invalid (only ${points.size} points are defined)" } require(points.size >= 2) { "Provided poly is invalid (only ${points.size} points are defined)" }
if (points.size == 2) { if (points.size == 2) {
@ -56,22 +57,23 @@ private fun calculateEdges(points: List<Vector2d>): Pair<ImmutableList<Poly.Edge
val (f) = calculateEdges(newPoints.build()) val (f) = calculateEdges(newPoints.build())
return f to ImmutableList.copyOf(points) return f to ImmutableList.copyOf(points)
} else { } else {
val edges = ImmutableList.Builder<Poly.Edge>() val edges = ImmutableList.Builder<Line2d>()
for (i in points.indices) { for (i in points.indices) {
val p0 = points[i] val p0 = points[i]
val p1 = points[(i + 1) % points.size] val p1 = points[(i + 1) % points.size]
val diff = (p1 - p0).unitVector edges.add(Line2d(p0, p1))
val normal = Vector2d(-diff.y, diff.x)
edges.add(Poly.Edge(p0, p1, normal))
} }
return edges.build() to ImmutableList.copyOf(points) return edges.build() to ImmutableList.copyOf(points)
} }
} }
private fun isLeft(p0: IStruct2d, p1: IStruct2d, p2: IStruct2d): Double {
return (p1.component1() - p0.component1()) * (p2.component2() - p0.component2()) - (p2.component1() - p0.component1()) * (p1.component2() - p0.component2())
}
private fun rotate(point: Vector2d, sin: Double, cos: Double): Vector2d { private fun rotate(point: Vector2d, sin: Double, cos: Double): Vector2d {
return Vector2d( return Vector2d(
point.x * cos + point.y * sin, point.x * cos + point.y * sin,
@ -84,8 +86,8 @@ private fun rotate(point: Vector2d, sin: Double, cos: Double): Vector2d {
* *
* If poly shape is not convex behavior of SAT is undefined * If poly shape is not convex behavior of SAT is undefined
*/ */
class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: ImmutableList<Vector2d>) { class Poly private constructor(val edges: ImmutableList<Line2d>, val vertices: ImmutableList<Vector2d>) {
private constructor(pair: Pair<ImmutableList<Edge>, ImmutableList<Vector2d>>) : this(pair.first, pair.second) private constructor(pair: Pair<ImmutableList<Line2d>, ImmutableList<Vector2d>>) : this(pair.first, pair.second)
constructor(points: List<Vector2d>) : this(calculateEdges(points)) constructor(points: List<Vector2d>) : this(calculateEdges(points))
constructor(aabb: AABB) : this(listOf(aabb.bottomLeft, aabb.topLeft, aabb.topRight, aabb.bottomRight)) constructor(aabb: AABB) : this(listOf(aabb.bottomLeft, aabb.topLeft, aabb.topRight, aabb.bottomRight))
@ -104,22 +106,8 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
} }
} }
data class Edge(val p0: Vector2d, val p1: Vector2d, val normal: Vector2d) { val centre by lazy {
operator fun plus(other: IStruct2d): Edge { vertices.reduce { acc, vector2d -> acc + vector2d } / vertices.size
return Edge(p0 + other, p1 + other, normal)
}
operator fun minus(other: IStruct2d): Edge {
return Edge(p0 - other, p1 - other, normal)
}
operator fun times(other: IStruct2d): Edge {
return Edge(p0 * other, p1 * other, (normal * other).unitVector)
}
operator fun times(other: Double): Edge {
return Edge(p0 * other, p1 * other, (normal * other).unitVector)
}
} }
data class Penetration(val axis: Vector2d, val penetration: Double) : Comparable<Penetration> { data class Penetration(val axis: Vector2d, val penetration: Double) : Comparable<Penetration> {
@ -133,7 +121,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
operator fun plus(value: Penetration): Poly { operator fun plus(value: Penetration): Poly {
if (isEmpty) return this if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>() val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>() val edges = ImmutableList.Builder<Line2d>()
for (v in this.vertices) vertices.add(v + value.vector) for (v in this.vertices) vertices.add(v + value.vector)
for (v in this.edges) edges.add(v + value.vector) for (v in this.edges) edges.add(v + value.vector)
@ -144,7 +132,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
operator fun plus(value: IStruct2d): Poly { operator fun plus(value: IStruct2d): Poly {
if (isEmpty) return this if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>() val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>() val edges = ImmutableList.Builder<Line2d>()
for (v in this.vertices) vertices.add(v + value) for (v in this.vertices) vertices.add(v + value)
for (v in this.edges) edges.add(v + value) for (v in this.edges) edges.add(v + value)
@ -155,7 +143,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
operator fun minus(value: IStruct2d): Poly { operator fun minus(value: IStruct2d): Poly {
if (isEmpty) return this if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>() val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>() val edges = ImmutableList.Builder<Line2d>()
for (v in this.vertices) vertices.add(v - value) for (v in this.vertices) vertices.add(v - value)
for (v in this.edges) edges.add(v - value) for (v in this.edges) edges.add(v - value)
@ -166,7 +154,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
operator fun times(value: IStruct2d): Poly { operator fun times(value: IStruct2d): Poly {
if (isEmpty) return this if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>() val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>() val edges = ImmutableList.Builder<Line2d>()
for (v in this.vertices) vertices.add(v * value) for (v in this.vertices) vertices.add(v * value)
for (v in this.edges) edges.add(v * value) for (v in this.edges) edges.add(v * value)
@ -177,7 +165,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
operator fun times(value: Double): Poly { operator fun times(value: Double): Poly {
if (isEmpty) return this if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>() val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>() val edges = ImmutableList.Builder<Line2d>()
for (v in this.vertices) vertices.add(v * value) for (v in this.vertices) vertices.add(v * value)
for (v in this.edges) edges.add(v * value) for (v in this.edges) edges.add(v * value)
@ -192,11 +180,11 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
val sin = sin(radians) val sin = sin(radians)
val cos = cos(radians) val cos = cos(radians)
val edges = ImmutableList.Builder<Edge>() val edges = ImmutableList.Builder<Line2d>()
val vertices = ImmutableList.Builder<Vector2d>() val vertices = ImmutableList.Builder<Vector2d>()
for (edge in this.edges) { for (edge in this.edges) {
edges.add(Edge(rotate(edge.p0, sin, cos), rotate(edge.p1, sin, cos), rotate(edge.normal, sin, cos))) edges.add(Line2d(rotate(edge.p0, sin, cos), rotate(edge.p1, sin, cos)))
} }
for (vertex in this.vertices) { for (vertex in this.vertices) {
@ -223,6 +211,58 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
return Vector2d(min, max) return Vector2d(min, max)
} }
fun windingNumber(point: IStruct2d): Int {
val (x, y) = point
// the winding number counter
var wn = 0
// loop through all edges of the polygon
for (edge in edges) {
// start y <= p[1]
if (edge.p0.y <= y) {
if (edge.p1.y > y) {
// an upward crossing
if (isLeft(edge.p0, edge.p1, point) > 0.0) {
// p left of edge
// have a valid up intersect
++wn
}
}
} else {
// start y > p[1] (no test needed)
if (edge.p1.y <= y) {
// a downward crossing
if (isLeft(edge.p0, edge.p1, point) < 0.0) {
// p right of edge
// have a valid down intersect
--wn
}
}
}
}
return wn
}
fun contains(point: IStruct2d): Boolean {
return windingNumber(point) != 0
}
fun distance(point: IStruct2d): Double {
if (contains(point))
return 0.0
return edges.minOf { it.distanceTo(point) }
}
fun distance(point: Vector2d): Double {
if (contains(point))
return 0.0
return edges.minOf { it.distanceTo(point) }
}
/** /**
* @param axis separate ONLY along specified axis, that said, if axis is positive Y and we have collision, and closest separation axis is to up right, we instead separate only up until we no longer collide. * @param axis separate ONLY along specified axis, that said, if axis is positive Y and we have collision, and closest separation axis is to up right, we instead separate only up until we no longer collide.
*/ */

View File

@ -81,7 +81,7 @@ class DisplacementTerrainSelector(data: Data, parameters: TerrainSelectorParamet
} }
override fun get(x: Int, y: Int): Double { override fun get(x: Int, y: Int): Double {
return source[clampX(xFn[x * data.xXInfluence, y * data.xYInfluence]).roundToInt(), clampY(yFn[x * data.yXInfluence, y * data.yYInfluence]).roundToInt()] return source[x + clampX(xFn[x * data.xXInfluence, y * data.xYInfluence]).roundToInt(), y + clampY(yFn[x * data.yXInfluence, y * data.yYInfluence]).roundToInt()]
} }
private fun clampX(v: Double): Double { private fun clampX(v: Double): Double {

View File

@ -90,7 +90,7 @@ class IslandSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParame
} }
private val cache = Caffeine.newBuilder() private val cache = Caffeine.newBuilder()
.maximumSize(512L) .maximumSize(2048L)
.executor(Starbound.EXECUTOR) .executor(Starbound.EXECUTOR)
.scheduler(Scheduler.systemScheduler()) .scheduler(Scheduler.systemScheduler())
.build<Int, Column>(::compute) .build<Int, Column>(::compute)

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomFloat
import ru.dbotthepony.kstarbound.world.positiveModulo import ru.dbotthepony.kstarbound.world.positiveModulo
import java.time.Duration import java.time.Duration
import kotlin.math.PI import kotlin.math.PI
@ -21,7 +22,9 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
@JsonFactory @JsonFactory
data class Data( data class Data(
val sectorSize: Int = 64, val sectorSize: Int = 64,
// we don't actually care
val layerPerlinsCacheSize: Int = 32, val layerPerlinsCacheSize: Int = 32,
// we don't actually care
val sectorCacheSize: Int = 32, val sectorCacheSize: Int = 32,
val layerResolution: Int, val layerResolution: Int,
val layerDensity: Double, val layerDensity: Double,
@ -49,13 +52,14 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
} }
val cave = AbstractPerlinNoise.of(data.caveDecision).also { it.init(caveSeed) } val cave = AbstractPerlinNoise.of(data.caveDecision).also { it.init(caveSeed) }
val layerHeightVariation by lazy { AbstractPerlinNoise.of(data.layerHeightVariation).also { it.init(layerHeightVariationSeed) } } val layerHeightVariation = AbstractPerlinNoise.of(data.layerHeightVariation).also { it.init(layerHeightVariationSeed) }
val caveHeightVariation by lazy { AbstractPerlinNoise.of(data.caveHeightVariation).also { it.init(caveHeightVariationSeed) } } val caveHeightVariation = AbstractPerlinNoise.of(data.caveHeightVariation).also { it.init(caveHeightVariationSeed) }
val caveFloorVariation by lazy { AbstractPerlinNoise.of(data.caveFloorVariation).also { it.init(caveFloorVariationSeed) } } val caveFloorVariation = AbstractPerlinNoise.of(data.caveFloorVariation).also { it.init(caveFloorVariationSeed) }
} }
private val layers = Caffeine.newBuilder() private val layers = Caffeine.newBuilder()
.maximumSize(data.layerPerlinsCacheSize.toLong()) .maximumSize(2048L)
.softValues()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler()) .scheduler(Scheduler.systemScheduler())
.executor(Starbound.EXECUTOR) .executor(Starbound.EXECUTOR)
@ -66,13 +70,11 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
private val values = Double2DArray.allocate(data.sectorSize, data.sectorSize) private val values = Double2DArray.allocate(data.sectorSize, data.sectorSize)
init { init {
val random = random(parameters.seed)
for (y in sector.y - data.bufferHeight until sector.y + data.sectorSize + data.bufferHeight) { for (y in sector.y - data.bufferHeight until sector.y + data.sectorSize + data.bufferHeight) {
val layerChance = data.layerDensity * data.layerResolution val layerChance = data.layerDensity * data.layerResolution
// determine whether this layer has caves // determine whether this layer has caves
if (y % data.layerResolution != 0 && random.nextDouble() > layerChance) if (y % data.layerResolution != 0 || staticRandomFloat(parameters.seed, y, "karst") > layerChance)
continue continue
val layer = layers[y] val layer = layers[y]
@ -98,7 +100,7 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
maxValue = maxValue.coerceAtLeast(halfHeight) maxValue = maxValue.coerceAtLeast(halfHeight)
for (pointY in floorY.roundToInt() until ceilingY.roundToInt()) { for (pointY in floorY.roundToInt() until ceilingY.roundToInt()) {
if (isInside(x, y)) { if (isInside(x, pointY)) {
this[x, pointY] = this[x, pointY].coerceAtLeast(halfHeight - (midpointY - pointY).absoluteValue) this[x, pointY] = this[x, pointY].coerceAtLeast(halfHeight - (midpointY - pointY).absoluteValue)
} }
} }
@ -125,7 +127,8 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
} }
private val sectors = Caffeine.newBuilder() private val sectors = Caffeine.newBuilder()
.maximumSize(data.sectorCacheSize.toLong()) .maximumSize(2048L)
.softValues()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler()) .scheduler(Scheduler.systemScheduler())
.executor(Starbound.EXECUTOR) .executor(Starbound.EXECUTOR)

View File

@ -3,9 +3,11 @@ package ru.dbotthepony.kstarbound.world.terrain
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters
import ru.dbotthepony.kstarbound.json.builder.JsonAlias import ru.dbotthepony.kstarbound.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
class PerlinTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<PerlinTerrainSelector.Data>(data, parameters) { class PerlinTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<PerlinTerrainSelector.Data>(data, parameters) {
@JsonFactory
data class Data( data class Data(
@JsonAlias("type") @JsonAlias("type")
val function: PerlinNoiseParameters.Type, val function: PerlinNoiseParameters.Type,

View File

@ -75,8 +75,8 @@ enum class TerrainSelectorType(val jsonName: String, private val data: Data<*, *
return load(objects.read(`in`)) return load(objects.read(`in`))
} }
fun factory(json: JsonObject, isSerializedForm: Boolean): Factory<*, *> { fun factory(json: JsonObject, isSerializedForm: Boolean, type: TerrainSelectorType? = null): Factory<*, *> {
val type = json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json") val type = type?.jsonName ?: json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json")
if (isSerializedForm) { if (isSerializedForm) {
val config = json["config"]?.asJsonObject ?: throw JsonSyntaxException("Missing 'config' element of terrain json") val config = json["config"]?.asJsonObject ?: throw JsonSyntaxException("Missing 'config' element of terrain json")

View File

@ -178,7 +178,8 @@ class WormCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters)
} }
private val sectors = Caffeine.newBuilder() private val sectors = Caffeine.newBuilder()
.maximumSize(data.lruCacheSize.toLong()) .maximumSize(2048L)
.softValues()
.expireAfterAccess(Duration.ofMinutes(5)) .expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler()) .scheduler(Scheduler.systemScheduler())
.executor(Starbound.EXECUTOR) .executor(Starbound.EXECUTOR)