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
kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.11.1
kommonsVersion=2.12.1
ffiVersion=2.2.13
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.MovementParameters
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.tile.TileDamageConfig
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.CelestialNames
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.SystemWorldConfig
import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig
@ -72,6 +74,9 @@ object Globals {
var universeServer by Delegates.notNull<UniverseServerConfig>()
private set
var worldServer by Delegates.notNull<WorldServerConfig>()
private set
var currencies by Delegates.notNull<ImmutableMap<String, CurrencyDefinition>>()
private set
@ -90,6 +95,9 @@ object Globals {
var celestialNames by Delegates.notNull<CelestialNames>()
private set
var instanceWorlds by Delegates.notNull<ImmutableMap<String, InstanceWorldConfig>>()
private set
private object EmptyTask : ForkJoinTask<Unit>() {
private fun readResolve(): Any = EmptyTask
override fun getRawResult() {
@ -138,6 +146,7 @@ object Globals {
tasks.add(load("/world_template.config", ::worldTemplate))
tasks.add(load("/sky.config", ::sky))
tasks.add(load("/universe_server.config", ::universeServer))
tasks.add(load("/worldserver.config", ::worldServer))
tasks.add(load("/player.config", ::player))
tasks.add(load("/systemworld.config", ::systemWorld))
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("/currencies.config", ::currencies, 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
}

View File

@ -30,101 +30,15 @@ fun main() {
Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak"))
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()}")
//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))
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.mailboxInitialized.submit {
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))
}
//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(loadTerrainSelectors(fileTree["terrain"] ?: listOf()))
tasks.addAll(loadTerrainSelectors(fileTree))
tasks.addAll(loadRegistry(tiles, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId)))
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<*>> {
return files.map { listedFile ->
Starbound.EXECUTOR.submit {
try {
val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true })
val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field")
val factory = TerrainSelectorType.factory(json, false)
private fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?) {
try {
val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true })
val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field")
val factory = TerrainSelectorType.factory(json, false, type)
terrainSelectors.add {
terrainSelectors.add(name, factory)
}
} catch (err: Exception) {
LOGGER.error("Loading terrain selector $listedFile", err)
}
terrainSelectors.add {
terrainSelectors.add(name, factory)
}
} 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 file: IStarboundFile?
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 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>>() {
override fun write(out: JsonWriter, value: Registry.Ref<S>?) {
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 {
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.Vector4fTypeAdapter
import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.*
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.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.world.physics.Poly
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.Matrix3fStack
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2f
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.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.Direction
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.Vector2i
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.render.ConfiguredMesh
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.ChunkPos
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.OffsetCellAccess
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.TimeUnit
import java.util.function.Consumer
import kotlin.concurrent.withLock
class ClientWorld(
val client: StarboundClient,
@ -164,7 +161,7 @@ class ClientWorld(
for (x in 0 until renderRegionWidth) {
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) {
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() + 1f, y.toFloat())
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 it.unimi.dsi.fastutil.objects.Object2DoubleMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.world.physics.CollisionType
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 {
private fun make(id: Int, name: String, collisionType: CollisionType) = Registries.tiles.add(name, id, TileDefinition(
materialId = id,
@ -19,6 +61,7 @@ object BuiltinMetaMaterials {
renderTemplate = AssetReference.empty(),
renderParameters = RenderParameters.META,
isMeta = true,
supportsMods = false,
collisionKind = collisionType,
damageTable = AssetReference(TileDamageConfig(
damageFactors = ImmutableMap.of(),
@ -27,7 +70,16 @@ object BuiltinMetaMaterials {
totalHealth = Double.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
@ -37,7 +89,7 @@ object BuiltinMetaMaterials {
/**
* 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 BIOME = make(65527, "biome", CollisionType.BLOCK)
@ -64,4 +116,18 @@ object BuiltinMetaMaterials {
OBJECT_SOLID,
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.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
@JsonFactory
data class LiquidDefinition(
@ -19,6 +20,8 @@ data class LiquidDefinition(
val texture: String,
val bottomLightMix: RGBAColor,
val textureMovementFactor: Double,
@JsonIgnore
val isMeta: Boolean = false,
) {
@JsonFactory
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 miningSounds: Either<ImmutableList<String>, String> = Either.left(ImmutableList.of()),
val blocksLiquidFlow: Boolean = true,
val soil: Boolean = false,
val category: String,
@ -44,6 +43,12 @@ data class TileDefinition(
override val renderTemplate: AssetReference<RenderTemplate>,
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 {
init {
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.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
@JsonFactory
data class TileModifierDefinition(
@ -28,6 +29,11 @@ data class TileModifierDefinition(
@JsonFlat
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 renderParameters: RenderParameters
) : 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 java.io.DataInputStream
import java.io.DataOutputStream
import java.util.random.RandomGenerator
import kotlin.properties.Delegates
class AsteroidsWorldParameters : VisitableWorldParameters() {
@ -93,8 +94,7 @@ class AsteroidsWorldParameters : VisitableWorldParameters() {
stream.writeColor(ambientLightLevel)
}
override fun createLayout(seed: Long): WorldLayout {
val random = random(seed)
override fun createLayout(random: RandomGenerator): WorldLayout {
val terrain = Globals.asteroidWorlds.terrains.random(random)
val layout = WorldLayout()
@ -145,9 +145,7 @@ class AsteroidsWorldParameters : VisitableWorldParameters() {
}
companion object {
fun generate(seed: Long): AsteroidsWorldParameters {
val random = random(seed)
fun generate(random: RandomGenerator): AsteroidsWorldParameters {
val parameters = AsteroidsWorldParameters()
parameters.threatLevel = random.nextRange(Globals.asteroidWorlds.threatRange)
@ -168,5 +166,9 @@ class AsteroidsWorldParameters : VisitableWorldParameters() {
return parameters
}
fun generate(seed: Long): AsteroidsWorldParameters {
return generate(random(seed))
}
}
}

View File

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

View File

@ -217,7 +217,7 @@ data class BiomePlaceablesDefinition(
name.entry!!,
biome.random.nextDouble(-1.0, 1.0) * baseHueShiftMax,
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 java.io.DataInputStream
import java.io.DataOutputStream
import java.util.random.RandomGenerator
import kotlin.properties.Delegates
class FloatingDungeonWorldParameters : VisitableWorldParameters() {
@ -44,9 +45,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
override val type: VisitableWorldParametersType
get() = VisitableWorldParametersType.FLOATING_DUNGEON
override fun createLayout(seed: Long): WorldLayout {
val random = random(seed)
override fun createLayout(random: RandomGenerator): WorldLayout {
val layout = WorldLayout()
layout.worldSize = worldSize
@ -146,6 +145,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
val config = Globals.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!")
val parameters = FloatingDungeonWorldParameters()
parameters.worldSize = config.worldSize
parameters.threatLevel = config.threatLevel
parameters.gravity = config.gravity.map({ Vector2d(y = it) }, { it })
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 modCount: Int = 0,
val parallax: Either<Double, Vector2d>,
val repeatY: Boolean = true,
val repeatY: Boolean = false,
val repeatX: Boolean = true,
val tileLimitTop: Double? = null,
val tileLimitBottom: Double? = null,
@ -117,7 +117,7 @@ class Parallax(
return Layer(
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,
tileLimitBottom = tileLimitBottom,
verticalOrigin = verticalOrigin,
@ -141,7 +141,8 @@ class Parallax(
val textures: ImmutableList<String>,
val alpha: Double = 1.0,
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 tileLimitBottom: Double? = null,
val verticalOrigin: Double,
@ -155,7 +156,7 @@ class Parallax(
) {
fun fadeToSkyColor(color: RGBAColor) {
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
// 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()) {
layers.add(Layer(liquidImages.replace("<liquid>", surfaceLiquid), imageScale))
@ -293,7 +293,7 @@ data class SkyParameters(
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) {
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.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList
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.io.readInternedString
import ru.dbotthepony.kstarbound.io.readVector2d
@ -74,9 +78,9 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
val bgOreSelector: String,
val subBlockSelector: String,
val caveLiquid: Either<Int, String>?,
val caveLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref,
val caveLiquidSeedDensity: Double,
val oceanLiquid: Either<Int, String>?,
val oceanLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref,
val oceanLiquidLevel: Int,
val encloseLiquids: Boolean,
@ -92,10 +96,10 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
stream.readInternedString(),
stream.readInternedString(),
Either.left(stream.readUnsignedByte()),
Registries.liquid.ref(stream.readUnsignedByte()),
stream.readFloat().toDouble(),
Either.left(stream.readUnsignedByte()),
Registries.liquid.ref(stream.readUnsignedByte()),
stream.readInt(),
stream.readBoolean(),
@ -112,9 +116,9 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
stream.writeBinaryString(bgOreSelector)
stream.writeBinaryString(subBlockSelector)
stream.writeByte(caveLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0)
stream.writeByte(caveLiquid.entry?.id ?: 0)
stream.writeFloat(caveLiquidSeedDensity.toFloat())
stream.writeByte(oceanLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0)
stream.writeByte(oceanLiquid.entry?.id ?: 0)
stream.writeInt(oceanLiquidLevel)
stream.writeBoolean(encloseLiquids)
@ -179,7 +183,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
val read = Starbound.gson.fromJson(data, StoreData::class.java)
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
hueShift = read.hueShift
skyColoring = read.skyColoring
@ -203,12 +207,10 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
// original engine operate on liquids solely with IDs
// and we also need to network this json to legacy clients.
// what a shame :JC:.
if (this.surfaceLiquid == null) {
primarySurfaceLiquid = if (isLegacy) Either.left(0) else null
} else if (isLegacy) {
primarySurfaceLiquid = this.surfaceLiquid!!.map({ it }, { Registries.liquid.get(it)!!.id })?.let { Either.left(it) }
if (isLegacy) {
primarySurfaceLiquid = this.surfaceLiquid.entry?.id?.let { Either.left(it) } ?: Either.left(0)
} else {
primarySurfaceLiquid = this.surfaceLiquid
primarySurfaceLiquid = this.surfaceLiquid.entry?.let { Either.right(it.key) } ?: this.surfaceLiquid.key.swap()
}
val store = StoreData(
@ -255,7 +257,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
var primaryBiome: String by Delegates.notNull()
private set
var surfaceLiquid: Either<Int, String>? = null
var surfaceLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref
private set
var sizeName: String by Delegates.notNull()
private set
@ -292,7 +294,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
super.read0(stream)
primaryBiome = stream.readInternedString()
surfaceLiquid = Either.left(stream.readUnsignedByte())
surfaceLiquid = Registries.liquid.ref(stream.readUnsignedByte())
sizeName = stream.readInternedString()
hueShift = stream.readFloat().toDouble()
skyColoring = SkyColoring.read(stream, true)
@ -314,7 +316,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
super.write0(stream)
stream.writeBinaryString(primaryBiome)
stream.writeByte(surfaceLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0)
stream.writeByte(surfaceLiquid.entry?.id ?: 0)
stream.writeBinaryString(sizeName)
stream.writeFloat(hueShift.toFloat())
skyColoring.write(stream, true)
@ -335,12 +337,10 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
}
// why
override fun createLayout(seed: Long): WorldLayout {
override fun createLayout(random: RandomGenerator): WorldLayout {
val layout = WorldLayout()
layout.worldSize = worldSize
val random = random(seed)
fun addLayer(layer: Layer) {
fun bake(region: Region): WorldLayout.RegionParameters {
return WorldLayout.RegionParameters(
@ -386,7 +386,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
layout.regionBlending = blendSize
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)
@ -470,9 +470,9 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
fgOreSelector = fgOreSelector,
bgOreSelector = bgOreSelector,
subBlockSelector = subBlockSelector,
caveLiquid = caveLiquid?.let { Either.right(it) },
caveLiquid = caveLiquid?.let { Registries.liquid.ref(it) } ?: BuiltinMetaMaterials.NO_LIQUID.ref,
caveLiquidSeedDensity = caveLiquidSeedDensity,
oceanLiquid = oceanLiquid?.let { Either.right(it) },
oceanLiquid = oceanLiquid?.let { Registries.liquid.ref(it) } ?: BuiltinMetaMaterials.NO_LIQUID.ref,
oceanLiquidLevel = oceanLiquidLevel,
encloseLiquids = encloseLiquids,
fillMicrodungeons = fillMicrodungeons,
@ -565,7 +565,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
parameters.threatLevel = threadLevel
parameters.typeName = typeName
parameters.worldSize = params.size
parameters.gravity = Vector2d(y = -random.nextRange(params.gravityRange))
parameters.gravity = Vector2d(y = 10.0)
parameters.airless = primaryBiome.value.airless
parameters.environmentStatusEffects = primaryBiome.value.statusEffects
parameters.overrideTech = params.overrideTech
@ -578,7 +578,11 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
parameters.sizeName = sizeName
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.dayLength = random.nextRange(params.dayLengthRange)

View File

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

View File

@ -1,10 +1,14 @@
package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.random
import java.util.random.RandomGenerator
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
@JsonFactory
data class BlockNoiseConfig(
@ -30,4 +34,17 @@ data class BlockNoise(
val verticalNoise: AbstractPerlinNoise,
val xNoise: 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.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.util.random.random
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.random.RandomGenerator
import kotlin.properties.Delegates
// uint8_t
@ -100,7 +102,6 @@ enum class VisitableWorldParametersType(override val jsonName: String, val token
// if done as immutable class
abstract class VisitableWorldParameters {
var threatLevel: Double = 0.0
protected set
var typeName: String by Delegates.notNull()
protected set
var worldSize: Vector2i by Delegates.notNull()
@ -116,17 +117,18 @@ abstract class VisitableWorldParameters {
var globalDirectives: Set<String>? = null
protected set
var beamUpRule: BeamUpRule by Delegates.notNull()
protected set
var disableDeathDrops: Boolean = false
protected set
var terraformed: Boolean = false
protected set
var worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.NONE
protected set
var weatherPool: WeightedList<String>? = null
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

View File

@ -22,12 +22,16 @@ import ru.dbotthepony.kommons.vector.Vector2d
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.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.world.terrain.AbstractTerrainSelector
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.ListInterner
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import java.util.random.RandomGenerator
import kotlin.math.roundToInt
@ -36,9 +40,9 @@ import kotlin.properties.Delegates
/**
* While this class seems redundant, it is not.
*
* Every world must be represented by layers and regions.
* And some worlds don't have visitable world parameters because they are custom-made,
* such as dungeon worlds.
* Every world must be represented by layers and regions,
* hence this class serves as mutual point of reference between different
* world types.
*
* 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.
@ -68,6 +72,13 @@ class WorldLayout {
val playerStartSearchRegions = ArrayList<AABBi>()
val layers = ArrayList<Layer>()
var loopX = true
var loopY = false
val worldGeometry by lazy {
WorldGeometry(worldSize, loopX, loopY)
}
private object StartingRegionsToken : TypeToken<ArrayList<AABBi>>()
@JsonFactory
@ -94,14 +105,57 @@ class WorldLayout {
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
data class RegionLiquids(
val caveLiquid: Either<Int, String>? = null,
val caveLiquid: Registry.Ref<LiquidDefinition> = BuiltinMetaMaterials.NO_LIQUID.ref,
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 encloseLiquids: Boolean = false,
@ -109,9 +163,9 @@ class WorldLayout {
) {
fun toLegacy(): RegionLiquidsLegacy {
return RegionLiquidsLegacy(
caveLiquid = caveLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0,
caveLiquid = caveLiquid.value?.liquidId ?: 0,
caveLiquidSeedDensity = caveLiquidSeedDensity,
oceanLiquid = oceanLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0,
oceanLiquid = oceanLiquid.value?.liquidId ?: 0,
oceanLiquidLevel = oceanLiquidLevel,
encloseLiquids = encloseLiquids,
fillMicrodungeons = fillMicrodungeons,
@ -143,7 +197,20 @@ class WorldLayout {
val subBlockSelectorIndexes: IntArrayList,
val foregroundOreSelectorIndexes: 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(
val terrainSelector: AbstractTerrainSelector<*>?,
@ -159,16 +226,37 @@ class WorldLayout {
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 {
val data = SerializedRegion(
terrainSelectorIndex = terrainSelectors.list.indexOf(terrainSelector),
foregroundCaveSelectorIndex = terrainSelectors.list.indexOf(foregroundCaveSelector),
backgroundCaveSelectorIndex = terrainSelectors.list.indexOf(backgroundCaveSelector),
blockBiomeIndex = biomes.list.indexOf(blockBiome),
environmentBiomeIndex = biomes.list.indexOf(environmentBiome),
subBlockSelectorIndexes = subBlockSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) }.filter { 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),
backgroundOreSelectorIndexes = backgroundOreSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) }.filter { it != -1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll),
terrainSelectorIndex = terrainSelectors.list.indexOf(terrainSelector) + 1,
foregroundCaveSelectorIndex = terrainSelectors.list.indexOf(foregroundCaveSelector) + 1,
backgroundCaveSelectorIndex = terrainSelectors.list.indexOf(backgroundCaveSelector) + 1,
blockBiomeIndex = biomes.list.indexOf(blockBiome) + 1,
environmentBiomeIndex = biomes.list.indexOf(environmentBiome) + 1,
subBlockSelectorIndexes = subBlockSelector.stream().mapToInt { terrainSelectors.list.indexOf(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) + 1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll),
)
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 terrainSelectors: List<AbstractTerrainSelector<*>>,
val layers: JsonArray,
val loopX: Boolean = true,
val loopY: Boolean = false,
)
fun toJson(): JsonObject {
return Starbound.gson.toJsonTree(SerializedForm(
worldSize, regionBlending, blockNoise, blendNoise,
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
}
@ -222,6 +313,8 @@ class WorldLayout {
regionBlending = load.regionBlending
blockNoise = load.blockNoise
blendNoise = load.blendNoise
loopX = load.loopX
loopY = load.loopY
playerStartSearchRegions.addAll(load.playerStartSearchRegions)
load.layers.forEach {
@ -232,15 +325,18 @@ class WorldLayout {
datalayer.cells.forEach {
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(
terrainSelector = load.terrainSelectors.getOrNull(region.terrainSelectorIndex)?.let { terrainSelectors.intern(it) },
foregroundCaveSelector = load.terrainSelectors.getOrNull(region.foregroundCaveSelectorIndex)?.let { terrainSelectors.intern(it) },
backgroundCaveSelector = load.terrainSelectors.getOrNull(region.backgroundCaveSelectorIndex)?.let { terrainSelectors.intern(it) },
blockBiome = load.biomes.getOrNull(region.blockBiomeIndex)?.let { biomes.intern(it) },
environmentBiome = load.biomes.getOrNull(region.environmentBiomeIndex)?.let { biomes.intern(it) },
subBlockSelector = region.subBlockSelectorIndexes.map { load.terrainSelectors.getOrNull(it)?.let { terrainSelectors.intern(it) } }.filterNotNull(),
foregroundOreSelector = region.foregroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it)?.let { terrainSelectors.intern(it) } }.filterNotNull(),
backgroundOreSelector = region.backgroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it)?.let { terrainSelectors.intern(it) } }.filterNotNull(),
terrainSelector = load.terrainSelectors.getOrNull(region.terrainSelectorIndex - 1),
foregroundCaveSelector = load.terrainSelectors.getOrNull(region.foregroundCaveSelectorIndex - 1),
backgroundCaveSelector = load.terrainSelectors.getOrNull(region.backgroundCaveSelectorIndex - 1),
blockBiome = load.biomes.getOrNull(region.blockBiomeIndex - 1),
environmentBiome = load.biomes.getOrNull(region.environmentBiomeIndex - 1),
subBlockSelector = region.subBlockSelectorIndexes.map { load.terrainSelectors.getOrNull(it - 1) }.filterNotNull(),
foregroundOreSelector = region.foregroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it - 1) }.filterNotNull(),
backgroundOreSelector = region.backgroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it - 1) }.filterNotNull(),
regionLiquids = Starbound.gson.fromJson(it, RegionLiquids::class.java),
))
}
@ -338,9 +434,11 @@ class WorldLayout {
if (!Globals.terrestrialWorlds.useSecondaryEnvironmentBiomeIndex) {
region.environmentBiome = primaryEnvironment.environmentBiome
region.environmentBiomeIndex = primaryEnvironment.environmentBiomeIndex
}
subRegion.environmentBiome = region.environmentBiome
subRegion.environmentBiomeIndex = region.environmentBiomeIndex
if (params.biomeName == primaryBiome && region.blockBiome != null)
spawnBiomes.add(region.blockBiome)
@ -375,8 +473,8 @@ class WorldLayout {
var nextBoundary = random.nextInt(0, worldSize.x)
layer.boundaries.add(nextBoundary)
for (v in relativeRegionSizes) {
nextBoundary += (worldSize.x * v / totalRelativeSize).roundToInt()
for (i in 0 until relativeRegionSizes.size - 1) {
nextBoundary += (worldSize.x * relativeRegionSizes.getDouble(i) / totalRelativeSize).roundToInt()
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>() {
private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }

View File

@ -1,13 +1,33 @@
package ru.dbotthepony.kstarbound.defs.world
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.vector.Vector2d
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.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.math.quintic2
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos
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) {
var seed: Long = 0L
@ -23,12 +43,20 @@ class WorldTemplate(val geometry: WorldGeometry) {
var celestialParameters: CelestialParameters? = null
private set
val customTerrainRegions = ArrayList<CustomTerrainRegion>()
constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, seed: Long) : this(WorldGeometry(worldParameters.worldSize, true, false)) {
this.seed = seed
this.skyParameters = skyParameters
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() {
if (celestialParameters != null) {
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
data class SerializedForm(
val celestialParameters: CelestialParameters? = null,
@ -47,19 +83,295 @@ class WorldTemplate(val geometry: WorldGeometry) {
val seed: Long = 0L,
val size: Either<WorldGeometry, Vector2i>,
val regionData: WorldLayout? = null,
//val customTerrainRegions:
val customTerrainRegions: List<CustomTerrainRegion>,
)
fun toJson(): JsonObject {
val data = Starbound.gson.toJsonTree(SerializedForm(
celestialParameters, worldParameters, skyParameters, seed,
if (Starbound.IS_LEGACY_JSON) Either.right(geometry.size) else Either.left(geometry),
worldLayout
worldLayout, customTerrainRegions
)) as JsonObject
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 {
suspend fun create(coordinate: UniversePos, universe: Universe): WorldTemplate {
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.seed = load.seed
template.worldLayout = load.regionData
template.customTerrainRegions.addAll(load.customTerrainRegions)
template.determineName()

View File

@ -5,4 +5,13 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class WorldTemplateConfig(
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.Registry
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.TileDefinition
import java.lang.reflect.Constructor
@ -68,7 +69,7 @@ abstract class NativeLegacy<NATIVE, LEGACY> {
}
override fun computeNative(value: Int?): Registry.Ref<TileDefinition> {
value ?: return Registries.tiles.emptyRef
value ?: return BuiltinMetaMaterials.EMPTY.ref
return Registries.tiles.ref(value)
}
}
@ -93,7 +94,7 @@ abstract class NativeLegacy<NATIVE, LEGACY> {
}
override fun computeNative(value: Int?): Registry.Ref<TileModifierDefinition> {
value ?: return Registries.tileModifiers.emptyRef
value ?: return BuiltinMetaMaterials.EMPTY_MOD.ref
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> {
types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable))
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, isIgnored = isIgnored))
return this
}
@ -511,6 +511,7 @@ class FactoryAdapter<T : Any> private constructor(
builder.add(
property,
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,
)

View File

@ -2,18 +2,16 @@ package ru.dbotthepony.kstarbound.math
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
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.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.math.absoluteValue
@ -25,9 +23,32 @@ private operator fun Vector2d.compareTo(other: Vector2d): Int {
}
@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))
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) {
companion object {
val EMPTY = Intersection(false, KOptional(), KOptional(), false, false)
@ -35,16 +56,16 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
}
fun difference(): Vector2d {
return b - a
return p1 - p0
}
fun reverse(): Line2d {
return Line2d(b, a)
return Line2d(p1, p0)
}
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeStruct2d(a, isLegacy)
stream.writeStruct2d(b, isLegacy)
stream.writeStruct2d(p0, isLegacy)
stream.writeStruct2d(p1, isLegacy)
}
// original source of this intersection algorithm:
@ -56,7 +77,7 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
val ab = difference()
val cd = other.difference()
val abCross = a.cross(b)
val abCross = p0.cross(p1)
val cdCross = c.cross(d)
val denominator = ab.cross(cd)
@ -65,7 +86,7 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
if (denominator.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 t = 0.0
@ -73,21 +94,21 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
if (infinite) {
point = Vector2d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)
} else {
point = if (a < c) c else a
point = if (p0 < c) c else p0
}
}
if (a < c) {
if (c.x != a.x) {
t = (c.x - a.x) / ab.x
if (p0 < c) {
if (c.x != p0.x) {
t = (c.x - p0.x) / ab.x
} else {
t = (c.y - a.y) / ab.y
t = (c.y - p0.y) / ab.y
}
} else if (a > d) {
if (d.x != a.x) {
t = (d.x - a.x) / ab.x
} else if (p0 > d) {
if (d.x != p0.x) {
t = (d.x - p0.x) / ab.x
} 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
}
} else {
val ta = (c - a).cross(cd) / denominator
val tb = (c - a).cross(ab) / denominator
val ta = (c - p0).cross(cd) / denominator
val tb = (c - p0).cross(ab) / denominator
val intersects = infinite || (ta in 0.0 .. 1.0 && tb in 0.0 .. 1.0)
return Intersection(
intersects = intersects,
t = KOptional(ta),
point = KOptional((b - a) * ta + a),
point = KOptional((p1 - p0) * ta + p0),
coincides = false,
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()
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 {
@ -122,14 +153,14 @@ data class Line2d(val a: Vector2d, val b: Vector2d) {
if (!infinite)
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>() {
private val pair = gson.getAdapter<Pair<Vector2d, Vector2d>>()
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 {

View File

@ -100,3 +100,11 @@ fun normalizeAngle(angle: Double): Double {
fun approachAngle(target: Double, current: Double, limit: Double): Double {
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.nio.NioEventLoopGroup
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.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.IntValueCodec
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.VarIntValueCodec
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.NetworkedGroup
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.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
@ -122,7 +127,10 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
}
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)
this.channel = channel
@ -181,6 +189,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
var windowWidth by client2serverGroup.upstream.add(networkedSignedInt())
var windowHeight 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
val server2clientGroup = MasterElement(NetworkedGroup())
@ -193,9 +202,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
var playerEntity: PlayerEntity? = null
// holy shit
val clientSpectatingEntities = BasicNetworkedElement(IntAVLTreeSet(), StreamCodec.Collection(VarIntValueCodec) { IntAVLTreeSet() })
// in tiles
fun trackingTileRegions(): List<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
}

View File

@ -11,6 +11,7 @@ import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.world.Chunk
import java.io.DataInputStream
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 {
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) {
stream.writeSignedVarInt(origin.x)
stream.writeSignedVarInt(origin.y)
stream.writeVarInt(data.rows)
stream.writeVarInt(data.columns)
stream.writeVarInt(data.rows)
for (x in data.rowIndices) {
for (y in data.columnIndices) {
data[y, x].write(stream)
for (y in data.rowIndices) {
for (x in data.columnIndices) {
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" }
val origin = Vector2i(stream.readSignedVarInt(), stream.readSignedVarInt())
val rows = stream.readVarInt()
val columns = stream.readVarInt()
val rows = stream.readVarInt()
val data = Object2DArray.nulls<LegacyNetworkCellState>(columns, rows)
for (x in data.rowIndices) {
for (y in data.columnIndices) {
data[y, x] = LegacyNetworkCellState.read(stream)
for (y in data.rowIndices) {
for (x in data.columnIndices) {
data[x, y] = LegacyNetworkCellState.read(stream)
}
}

View File

@ -18,6 +18,6 @@ class WorldClientStateUpdatePacket(val deltas: ByteArrayList) : IServerPacket {
}
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.WorldID
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.network.Connection
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.WorldStorage
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.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos
@ -44,6 +46,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
var tracker: ServerWorldTracker? = null
var worldStartAcknowledged = false
var returnWarp: WarpAction? = null
private var systemWorld: ServerSystemWorld? = null
val world: ServerWorld?
get() = tracker?.world
@ -127,6 +130,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
server.channels.freeConnectionID(connectionID)
server.channels.connections.remove(this)
server.freeNickname(nickname)
systemWorld?.removeClient(this)
systemWorld = null
announceDisconnect("Connection to remote host is lost.")
@ -149,20 +154,21 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
if (request is WarpAlias)
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)
if (resolve.isLimbo) {
send(PlayerWarpResultPacket(false, request, true))
} 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))
} else {
val world = server.worlds[resolve]
if (world == null) {
val world = try {
server.loadWorld(resolve).await()
} catch (err: Throwable) {
send(PlayerWarpResultPacket(false, request, false))
LOGGER.error("Unable to wark ${alias()} to $request", err)
continue
}
@ -246,13 +252,24 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
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()
shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world))
shipCoordinate = found
run {
val action = ship.location.orbitalAction(world)
currentOrbitalWarpAction = action
orbitalWarpAction = action
for (client in shipWorld.clients) {
@ -319,6 +336,14 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
client.client.orbitalWarpAction = KOptional()
}
newSystem.thenApply {
systemWorld = it
if (!isConnected) {
it.removeClient(this)
}
}
world = newSystem.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) {
if (!channel.isOpen)
return
if (msg is IServerPacket) {
try {
msg.play(this)
@ -460,23 +488,24 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
if (isLegacy) {
scope.launch { celestialRequestsHandler() }
ServerWorld.load(server, shipChunkSource, WorldID.ShipWorld(uuid!!)).thenAccept {
server.loadShipWorld(this, shipChunkSource).thenAccept {
if (!isConnected || !channel.isOpen) {
LOGGER.warn("$this disconnected before loaded their ShipWorld")
it.close()
} else {
shipWorld = it
// shipWorld.sky.startFlying(true, true)
shipWorld.thread.start()
enqueueWarp(WarpAlias.OwnShip)
shipUpgrades = shipUpgrades.addCapability("planetTravel")
shipUpgrades = shipUpgrades.addCapability("teleport")
shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 1)
scope.launch { warpEventLoop() }
shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 3)
scope.launch { shipFlightEventLoop() }
scope.launch { warpEventLoop() }
if (server.channels.connections.size > 1) {
enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!))
}
//if (server.channels.connections.size > 1) {
// enqueueWarp(WarpAction.Player(server.channels.connections.first().uuid!!))
//}
}
}.exceptionally {
LOGGER.error("Error while initializing shipworld for $this", it)

View File

@ -1,34 +1,40 @@
package ru.dbotthepony.kstarbound.server
import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound
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.server.world.ServerUniverse
import ru.dbotthepony.kstarbound.server.world.ServerWorld
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.ExceptionLogger
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 java.io.Closeable
import java.io.File
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Supplier
@ -41,15 +47,13 @@ sealed class StarboundServer(val root: File) : Closeable {
}
}
val limboWorldIndex = AtomicInteger()
val limboWorlds = CopyOnWriteArrayList<ServerWorld>()
val worlds = ConcurrentHashMap<WorldID, ServerWorld>()
private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>()
val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) }
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::tick, Starbound.TIMESTEP_NANOS)
val thread = Thread(spinner, "Server Thread")
val universe = ServerUniverse()
val chat = ChatHandler(this)
val context = CoroutineScope(Starbound.COROUTINE_EXECUTOR)
val scope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>()
@ -60,11 +64,107 @@ sealed class StarboundServer(val root: File) : Closeable {
fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> {
return CompletableFuture.supplyAsync(Supplier {
systemWorlds.computeIfAbsent(location) {
context.async { loadSystemWorld0(location) }.asCompletableFuture()
scope.async { loadSystemWorld0(location) }.asCompletableFuture()
}
}, 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> {
return loadSystemWorld(location.location)
}
@ -140,24 +240,26 @@ sealed class StarboundServer(val root: File) : Closeable {
// TODO: schedule to thread pool?
// right now, system worlds are rather lightweight, and having separate threads for them is overkill
runBlocking {
systemWorlds.values.removeIf {
if (it.isCompletedExceptionally) {
return@removeIf true
}
if (systemWorlds.isNotEmpty()) {
runBlocking {
systemWorlds.values.removeIf {
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
}
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
isClosed = true
context.cancel("Server shutting down")
scope.cancel("Server shutting down")
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()
close0()
}

View File

@ -2,13 +2,57 @@ package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.objects.ObjectArraySet
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
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.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) {
/**
* 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> {
if (cells.isInitialized()) {
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)
}
}
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 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> {
@ -268,6 +272,7 @@ class ServerSystemWorld : SystemWorld {
val packet = SystemObjectDestroyPacket(it.uuid)
ships.values.forEach { ship ->
ship.forget(it.uuid)
ship.client.send(packet)
}
@ -379,6 +384,10 @@ class ServerSystemWorld : SystemWorld {
client.send(SystemWorldUpdatePacket(objects, ships))
}
fun forget(id: UUID) {
netVersions.removeLong(id)
}
private var destinationFuture = 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.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
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 ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2i
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.defs.WarpAction
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.TileDamageResult
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.WorldTemplate
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.tile.TileEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer
import java.util.function.Predicate
import java.util.function.Supplier
import kotlin.concurrent.withLock
import kotlin.properties.Delegates
class ServerWorld private constructor(
val server: StarboundServer,
@ -61,43 +72,42 @@ class ServerWorld private constructor(
init {
if (server.isClosed)
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 shouldStopOnIdle = worldID !is WorldID.ShipWorld
val scope = CoroutineScope(mailbox.asCoroutineDispatcher() + SupervisorJob())
private fun doAcceptClient(client: ServerConnection, action: WarpAction?) {
if (clients.any { it.client == client })
throw IllegalStateException("$client is already in $this")
try {
isBusy++
if (!client.isConnected)
throw IllegalStateException("$client disconnected while joining $this")
if (clients.any { it.client == client })
throw IllegalStateException("$client is already in $this")
val start = if (action is WarpAction.Player)
clients.firstOrNull { it.client.uuid == action.uuid }?.client?.playerEntity?.position
else if (action is WarpAction.World)
action.target.resolve(this)
else
playerSpawnPosition
if (!client.isConnected)
throw IllegalStateException("$client disconnected while joining $this")
if (start == null) {
client.send(PlayerWarpResultPacket(false, action!!, true))
throw IllegalStateException("Not a valid spawn target: $action")
val start = if (action is WarpAction.Player)
clients.firstOrNull { it.client.uuid == action.uuid }?.client?.playerEntity?.position
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> {
@ -105,20 +115,32 @@ class ServerWorld private constructor(
unpause()
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)
}
return future
} catch (err: RejectedExecutionException) {
return CompletableFuture.failedFuture(err)
}
}
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)
init {
mailbox.thread = thread
}
private val isClosed = AtomicBoolean()
fun isClosed(): Boolean {
return isClosed.get()
}
init {
thread.isDaemon = true
}
@ -143,24 +165,47 @@ class ServerWorld private constructor(
LOGGER.info("Shutting down $this")
if (isClosed.compareAndSet(false, true)) {
server.notifyWorldUnloaded(worldID)
super.close()
spinner.unpause()
clients.forEach { it.remove() }
if (worldID != WorldID.Limbo)
server.worlds.remove(worldID)
else
server.limboWorlds.remove(this)
ticketListLock.withLock {
ticketLists.forEach { it.scope.cancel() }
}
clients.forEach {
it.remove()
it.client.enqueueWarp(WarpAlias.Return)
}
LockSupport.unpark(thread)
}
}
private var idleTicks = 0
private var isBusy = 0
private fun spin(): Boolean {
if (isClosed.get()) return false
try {
if (clients.isEmpty() && isBusy <= 0) {
idleTicks++
} else {
idleTicks = 0
}
tick()
if (idleTicks >= 600) {
if (shouldStopOnIdle) {
close()
return false
} else {
pause()
}
}
return true
} catch (err: Throwable) {
LOGGER.fatal("Exception in world tick loop", err)
@ -260,35 +305,7 @@ class ServerWorld private constructor(
}
ticketListLock.withLock {
ticketLists.removeIf {
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
}
ticketLists.removeIf { it.tick() }
}
}
@ -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) {
super.setProperty0(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) })
}
fun permanentChunkTicket(pos: ChunkPos): ITicket {
fun permanentChunkTicket(pos: ChunkPos, target: ServerChunk.State = ServerChunk.State.FULL): ITicket {
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" }
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) {
ticketMap[chunk.pos.toLong()]?.let { chunk.addListener(it) }
ticketListLock.withLock {
ticketMap[chunk.pos.toLong()]?.let { chunk.addListener(it) }
}
}
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 {
@ -342,7 +491,7 @@ class ServerWorld private constructor(
val isCanceled: Boolean
val pos: ChunkPos
val id: Int
val chunk: ServerChunk?
val chunk: CompletableFuture<ServerChunk>
var listener: IChunkListener?
}
@ -360,14 +509,163 @@ class ServerWorld private constructor(
private inner class TicketList(val pos: ChunkPos) : IChunkListener {
constructor(pos: Long) : this(ChunkPos(pos))
private var first = true
private var calledLoadChunk = true
private val permanent = ArrayList<Ticket>()
private val temporary = ObjectAVLTreeSet<TimedTicket>()
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
get() = temporary.isNotEmpty() || permanent.isNotEmpty()
private suspend fun chunkGeneratorLoop() {
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 {
ticks++
@ -378,60 +676,79 @@ class ServerWorld private constructor(
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) {
permanent.forEach { it.listener?.onCellChanges(x, y, cell) }
temporary.forEach { it.listener?.onCellChanges(x, y, cell) }
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.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) {
permanent.forEach { it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
temporary.forEach { it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
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.listener?.onTileHealthUpdate(x, y, isBackground, health) }
temporary.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
}
abstract inner class AbstractTicket : ITicket {
final override val id: Int = nextTicketID.getAndIncrement()
abstract inner class AbstractTicket(val targetState: ServerChunk.State) : ITicket {
final override val id: Int = nextTicketID++
final override val pos: ChunkPos
get() = this@TicketList.pos
final override var isCanceled: Boolean = false
private var loadFuture: CompletableFuture<*>? = null
final override val chunk = CompletableFuture<ServerChunk>()
fun init() {
if (first) {
first = false
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)
}
init {
isBusy = true
this@TicketList.targetState.trySend(targetState)
}
final override fun cancel() {
@ -440,23 +757,20 @@ class ServerWorld private constructor(
ticketListLock.withLock {
if (isCanceled) return
isCanceled = true
loadFuture?.cancel(false)
chunk.cancel(false)
listener = null
cancel0()
}
}
protected abstract fun cancel0()
final override val chunk: ServerChunk?
get() = chunkMap[pos]
final override var listener: IChunkListener? = null
}
inner class Ticket : AbstractTicket() {
inner class Ticket(state: ServerChunk.State) : AbstractTicket(state) {
init {
permanent.add(this)
init()
loadChunk()
}
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
override val timeRemaining: Int
@ -472,7 +786,7 @@ class ServerWorld private constructor(
init {
temporary.add(this)
init()
loadChunk()
}
override fun cancel0() {
@ -516,6 +830,25 @@ class ServerWorld private constructor(
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> {
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
class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, playerStart: Vector2d) {
init {
LOGGER.info("$client is joining $world")
LOGGER.info("Accepted ${client.alias()}")
client.worldStartAcknowledged = false
client.tracker = this
@ -57,7 +57,6 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
private val isRemoved = AtomicBoolean()
private var isActuallyRemoved = false
private val tickets = HashMap<ChunkPos, Ticket>()
private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>()
private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>()
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 {
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) {
@ -178,7 +175,6 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
for ((pos, ticket) in itr) {
if (pos !in newTrackedChunks) {
pendingSend.remove(pos)
ticket.ticket.cancel()
itr.remove()
}
@ -188,30 +184,22 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
if (pos !in tickets) {
val ticket = world.permanentChunkTicket(pos)
val thisTicket = Ticket(ticket, pos)
tickets[pos] = 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 {
val trackingEntities = ObjectAVLTreeSet<AbstractEntity>()
@ -287,7 +275,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
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))
}
@ -295,6 +283,10 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
client.playerEntity = null
client.worldID = WorldID.Limbo
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 (
intersection.intersects &&
intersection.point.get() != proposed.a &&
intersection.point.get() != proposed.b
intersection.point.get() != proposed.p0 &&
intersection.point.get() != proposed.p1
) {
valid = false
break
@ -321,7 +321,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
if (
proposed != existingLine &&
proposed.distanceTo(existingLine.a) < universe.generationInformation.minimumConstellationLineCloseness
proposed.distanceTo(existingLine.p0) < universe.generationInformation.minimumConstellationLineCloseness
) {
valid = false
break
@ -329,7 +329,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
if (
proposed != existingLine.reverse() &&
proposed.distanceTo(existingLine.b) < universe.generationInformation.minimumConstellationLineCloseness
proposed.distanceTo(existingLine.p1) < universe.generationInformation.minimumConstellationLineCloseness
) {
valid = false
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 {
val NULL: WorldStorage = Fixed(AbstractCell.NULL)
val EMPTY: WorldStorage = Fixed(AbstractCell.EMPTY)

View File

@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager
import org.lwjgl.system.MemoryStack
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.WindowsBindings
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
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 carrier: Thread? = null
@Volatile
private var isPaused = false
private val pause = AtomicInteger()
fun pause() {
isPaused = true
pause.incrementAndGet()
}
fun unpause() {
if (isPaused) {
isPaused = false
if (pause.addAndGet(-100) <= 0) {
carrier?.let { LockSupport.unpark(it) }
}
}
@ -69,7 +68,7 @@ class ExecutionSpinner(private val waiter: Runnable, private val spinner: Boolea
fun spin(): Boolean {
carrier = Thread.currentThread()
while (isPaused) {
while (pause.get() > 0) {
waiter.run()
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.set
import ru.dbotthepony.kommons.gson.value
import java.util.concurrent.CompletableFuture
import kotlin.math.floor
import kotlin.math.sqrt
@ -30,6 +31,7 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
var hasSeedSpecified = false
private set
private var initializationTask: CompletableFuture<Unit>? = null
private var isInitialized = false
private val initLock = Any()
@ -52,6 +54,11 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
}
private fun doInit(seed: Long) {
val p = p
val g1 = g1
val g2 = g2
val g3 = g3
p.fill(0)
g1.fill(0.0)
g2.fill(0.0)
@ -256,11 +263,7 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
out.nullValue()
else {
val json = parent.toJsonTree(value.parameters) as JsonObject
if (value.seed != 0L) {
json["seed"] = value.seed
}
json["seed"] = value.seed
out.value(json)
}
}

View File

@ -1,23 +1,14 @@
package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.util.AABB
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.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
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.entities.AbstractEntity
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() }
}
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>) {
val ours = cells.value
source.checkSizeEquals(ours)
@ -286,28 +165,11 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
}
open fun remove() {
}
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 {

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.math.divideUp
import kotlin.math.absoluteValue
import kotlin.math.pow
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: 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() {
override val chunks = divideUp(cells, CHUNK_SIZE)
private val cellsD = cells.toDouble()
@ -121,6 +125,22 @@ abstract class CoordinateMapper {
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 {
return chunk(value shr CHUNK_SIZE_BITS)
}
@ -272,5 +292,13 @@ abstract class CoordinateMapper {
override fun chunk(value: Int): Int {
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()
if (inOrbit) {
skyType = SkyType.ORBITAL
} else {
skyType = parameters.skyType
}
skyType = parameters.skyType
}
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.AABB
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
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.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.ParallelPerform
import ru.dbotthepony.kstarbound.util.random.random
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.random.RandomGenerator
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 {
val background = TileView.Background(this)
val foreground = TileView.Foreground(this)
val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) }
val sky = Sky()
val sky = Sky(template.skyParameters)
val geometry: WorldGeometry = template.geometry
val nextEntityID = AtomicInteger()
@ -311,6 +312,34 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
) 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> {
val result = ArrayList<CollisionPoly>()
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.vector.Vector2d
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.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())
init {
@ -214,6 +217,11 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool
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 {
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 {
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 ru.dbotthepony.kstarbound.Starbound
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.DataOutputStream
@ -13,16 +11,28 @@ sealed class AbstractCell {
abstract val background: AbstractTileState
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 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
// whenever if cell ignores any attempts to damage it
abstract val isIndestructible: Boolean
abstract fun immutable(): ImmutableCell
abstract fun mutable(): MutableCell
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
@ -35,7 +45,7 @@ sealed class AbstractCell {
stream.write(0) // collisionMap
stream.writeShort(dungeonId)
stream.writeByte(biome)
stream.writeByte(blockBiome)
stream.writeByte(envBiome)
stream.writeBoolean(isIndestructible)

View File

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

View File

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

View File

@ -8,7 +8,7 @@ data class ImmutableCell(
override val liquid: ImmutableLiquidState = AbstractLiquidState.EMPTY,
override val dungeonId: Int = 0,
override val biome: Int = 0,
override val blockBiome: Int = 0,
override val envBiome: Int = 0,
override val isIndestructible: Boolean = false,
) : AbstractCell() {
@ -30,6 +30,6 @@ data class ImmutableCell(
}
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
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
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 pressure: Float = 0f,
override val isInfinite: Boolean = false,
) : AbstractLiquidState() {
override fun mutable(): MutableLiquidState {
return MutableLiquidState(def, level, pressure, isInfinite)
return MutableLiquidState(state, level, pressure, isInfinite)
}
override fun immutable(): ImmutableLiquidState {

View File

@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.network.LegacyNetworkTileState
data class ImmutableTileState(
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 hueShift: Float = 0f,
override var modifierHueShift: Float = 0f,

View File

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

View File

@ -2,28 +2,36 @@ package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import java.io.DataInputStream
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 pressure: Float = 0f,
override var isInfinite: Boolean = false,
) : AbstractLiquidState() {
fun read(stream: DataInputStream): MutableLiquidState {
def = Registries.liquid[stream.readUnsignedByte()]
state = Registries.liquid[stream.readUnsignedByte()] ?: BuiltinMetaMaterials.NO_LIQUID
level = stream.readFloat()
pressure = stream.readFloat()
isInfinite = stream.readBoolean()
return this
}
fun reset() {
state = BuiltinMetaMaterials.NO_LIQUID
level = 0f
pressure = 0f
isInfinite = false
}
override fun mutable(): MutableLiquidState {
return this
}
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(
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 hueShift: Float = 0f,
override var modifierHueShift: Float = 0f,
@ -48,10 +48,10 @@ data class MutableTileState(
fun read(stream: DataInputStream): MutableTileState {
material = Registries.tiles[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY
setHueShift(stream.read())
color = TileColor.of(stream.read())
modifier = Registries.tileModifiers[stream.readUnsignedShort()]
setModHueShift(stream.read())
setHueShift(stream.readUnsignedByte())
color = TileColor.of(stream.readUnsignedByte())
modifier = Registries.tileModifiers[stream.readUnsignedShort()] ?: BuiltinMetaMaterials.EMPTY_MOD
setModHueShift(stream.readUnsignedByte())
return this
}
}

View File

@ -6,7 +6,6 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayList
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
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.networkedData
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.SpatialIndex
import ru.dbotthepony.kstarbound.world.World

View File

@ -1,15 +1,15 @@
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
NULL(true),
NULL(true, true, false),
// air
NONE(true),
NONE(true, false, false),
// including stairs made of platforms
PLATFORM(false),
DYNAMIC(false),
SLIPPERY(false),
BLOCK(false);
PLATFORM(false, false, false),
DYNAMIC(false, true, false),
SLIPPERY(false, true, true),
BLOCK(false, true, true);
fun maxOf(other: CollisionType): CollisionType {
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.writeStruct2f
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.nativeCodec
import java.io.DataInputStream
@ -34,7 +35,7 @@ import kotlin.math.absoluteValue
import kotlin.math.cos
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)" }
if (points.size == 2) {
@ -56,22 +57,23 @@ private fun calculateEdges(points: List<Vector2d>): Pair<ImmutableList<Poly.Edge
val (f) = calculateEdges(newPoints.build())
return f to ImmutableList.copyOf(points)
} else {
val edges = ImmutableList.Builder<Poly.Edge>()
val edges = ImmutableList.Builder<Line2d>()
for (i in points.indices) {
val p0 = points[i]
val p1 = points[(i + 1) % points.size]
val diff = (p1 - p0).unitVector
val normal = Vector2d(-diff.y, diff.x)
edges.add(Poly.Edge(p0, p1, normal))
edges.add(Line2d(p0, p1))
}
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 {
return Vector2d(
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
*/
class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: ImmutableList<Vector2d>) {
private constructor(pair: Pair<ImmutableList<Edge>, ImmutableList<Vector2d>>) : this(pair.first, pair.second)
class Poly private constructor(val edges: ImmutableList<Line2d>, val vertices: ImmutableList<Vector2d>) {
private constructor(pair: Pair<ImmutableList<Line2d>, ImmutableList<Vector2d>>) : this(pair.first, pair.second)
constructor(points: List<Vector2d>) : this(calculateEdges(points))
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) {
operator fun plus(other: IStruct2d): Edge {
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)
}
val centre by lazy {
vertices.reduce { acc, vector2d -> acc + vector2d } / vertices.size
}
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 {
if (isEmpty) return this
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.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 {
if (isEmpty) return this
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.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 {
if (isEmpty) return this
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.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 {
if (isEmpty) return this
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.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 {
if (isEmpty) return this
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.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 cos = cos(radians)
val edges = ImmutableList.Builder<Edge>()
val edges = ImmutableList.Builder<Line2d>()
val vertices = ImmutableList.Builder<Vector2d>()
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) {
@ -223,6 +211,58 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
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.
*/

View File

@ -81,7 +81,7 @@ class DisplacementTerrainSelector(data: Data, parameters: TerrainSelectorParamet
}
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 {

View File

@ -90,7 +90,7 @@ class IslandSurfaceTerrainSelector(data: Data, parameters: TerrainSelectorParame
}
private val cache = Caffeine.newBuilder()
.maximumSize(512L)
.maximumSize(2048L)
.executor(Starbound.EXECUTOR)
.scheduler(Scheduler.systemScheduler())
.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.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomFloat
import ru.dbotthepony.kstarbound.world.positiveModulo
import java.time.Duration
import kotlin.math.PI
@ -21,7 +22,9 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
@JsonFactory
data class Data(
val sectorSize: Int = 64,
// we don't actually care
val layerPerlinsCacheSize: Int = 32,
// we don't actually care
val sectorCacheSize: Int = 32,
val layerResolution: Int,
val layerDensity: Double,
@ -49,13 +52,14 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
}
val cave = AbstractPerlinNoise.of(data.caveDecision).also { it.init(caveSeed) }
val layerHeightVariation by lazy { AbstractPerlinNoise.of(data.layerHeightVariation).also { it.init(layerHeightVariationSeed) } }
val caveHeightVariation by lazy { AbstractPerlinNoise.of(data.caveHeightVariation).also { it.init(caveHeightVariationSeed) } }
val caveFloorVariation by lazy { AbstractPerlinNoise.of(data.caveFloorVariation).also { it.init(caveFloorVariationSeed) } }
val layerHeightVariation = AbstractPerlinNoise.of(data.layerHeightVariation).also { it.init(layerHeightVariationSeed) }
val caveHeightVariation = AbstractPerlinNoise.of(data.caveHeightVariation).also { it.init(caveHeightVariationSeed) }
val caveFloorVariation = AbstractPerlinNoise.of(data.caveFloorVariation).also { it.init(caveFloorVariationSeed) }
}
private val layers = Caffeine.newBuilder()
.maximumSize(data.layerPerlinsCacheSize.toLong())
.maximumSize(2048L)
.softValues()
.expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler())
.executor(Starbound.EXECUTOR)
@ -66,13 +70,11 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
private val values = Double2DArray.allocate(data.sectorSize, data.sectorSize)
init {
val random = random(parameters.seed)
for (y in sector.y - data.bufferHeight until sector.y + data.sectorSize + data.bufferHeight) {
val layerChance = data.layerDensity * data.layerResolution
// 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
val layer = layers[y]
@ -98,7 +100,7 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
maxValue = maxValue.coerceAtLeast(halfHeight)
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)
}
}
@ -125,7 +127,8 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
}
private val sectors = Caffeine.newBuilder()
.maximumSize(data.sectorCacheSize.toLong())
.maximumSize(2048L)
.softValues()
.expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler())
.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.world.TerrainSelectorParameters
import ru.dbotthepony.kstarbound.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
class PerlinTerrainSelector(data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector<PerlinTerrainSelector.Data>(data, parameters) {
@JsonFactory
data class Data(
@JsonAlias("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`))
}
fun factory(json: JsonObject, isSerializedForm: Boolean): Factory<*, *> {
val type = json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json")
fun factory(json: JsonObject, isSerializedForm: Boolean, type: TerrainSelectorType? = null): Factory<*, *> {
val type = type?.jsonName ?: json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json")
if (isSerializedForm) {
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()
.maximumSize(data.lruCacheSize.toLong())
.maximumSize(2048L)
.softValues()
.expireAfterAccess(Duration.ofMinutes(5))
.scheduler(Scheduler.systemScheduler())
.executor(Starbound.EXECUTOR)