diff --git a/gradle.properties b/gradle.properties index 2739ff64..38bf5df3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt index 42be9d79..a75f1189 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt @@ -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() private set + var worldServer by Delegates.notNull() + private set + var currencies by Delegates.notNull>() private set @@ -90,6 +95,9 @@ object Globals { var celestialNames by Delegates.notNull() private set + var instanceWorlds by Delegates.notNull>() + private set + private object EmptyTask : ForkJoinTask() { 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 } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index d4da0f74..df936628 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index 84373d3f..d23d8045 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -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): List> { - 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>): List> { + val tasks = ArrayList>() + + 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 + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index f7208311..f5d5ea38 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -71,6 +71,7 @@ class Registry(val name: String) { abstract val json: JsonElement abstract val file: IStarboundFile? abstract val registry: Registry + @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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt index 2dc4bb5a..a6884732 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt @@ -55,7 +55,24 @@ class RegistryTypeAdapterFactory(private val registry: Registry, pri private inner class RefImpl(gson: Gson) : TypeAdapter>() { override fun write(out: JsonWriter, value: Registry.Ref?) { 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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 260de41b..d636742f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -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.* diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 8beeb78f..3a3ec6cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index 4ebbf0ad..f09d4aa0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt new file mode 100644 index 00000000..2ee3e088 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/WorldServerConfig.kt @@ -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, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt index 624bcb91..67f38429 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt @@ -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.isEmptyTile: Boolean + get() = entry == BuiltinMetaMaterials.EMPTY + +val Registry.Ref.isNullTile: Boolean + get() = entry == BuiltinMetaMaterials.NULL || entry == null + +val Registry.Ref.isObjectSolidTile: Boolean + get() = entry == BuiltinMetaMaterials.OBJECT_SOLID + +val Registry.Ref.isObjectPlatformTile: Boolean + get() = entry == BuiltinMetaMaterials.OBJECT_PLATFORM + +val Registry.Entry.isEmptyTile: Boolean + get() = this == BuiltinMetaMaterials.EMPTY + +val Registry.Entry.isNullTile: Boolean + get() = this == BuiltinMetaMaterials.NULL + +val Registry.Entry.isObjectSolidTile: Boolean + get() = this == BuiltinMetaMaterials.OBJECT_SOLID + +val Registry.Entry.isObjectPlatformTile: Boolean + get() = this == BuiltinMetaMaterials.OBJECT_PLATFORM + +val Registry.Entry.supportsModifiers: Boolean + get() = !value.isMeta && value.supportsMods + +fun Registry.Entry.supportsModifier(modifier: Registry.Entry): Boolean { + return !value.isMeta && value.supportsMods && !modifier.value.isMeta +} + +fun Registry.Entry.supportsModifier(modifier: Registry.Ref): Boolean { + return !value.isMeta && value.supportsMods && modifier.isPresent && !modifier.value!!.isMeta +} + +val Registry.Entry.isEmptyLiquid: Boolean + get() = this == BuiltinMetaMaterials.NO_LIQUID + +val Registry.Ref.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) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/LiquidDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/LiquidDefinition.kt index f9cf6fe1..d1ddcf71 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/LiquidDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/LiquidDefinition.kt @@ -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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt index f77505a1..efb32e51 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt @@ -21,7 +21,6 @@ data class TileDefinition( val footstepSound: Either, String> = Either.left(ImmutableList.of()), val miningSounds: Either, 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, 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" } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt index dca264e9..94c886de 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt @@ -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, override val renderParameters: RenderParameters ) : IRenderableTile, IThingWithDescription by descriptionData { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt index 97b5a620..fd710525 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt @@ -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)) + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Biome.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Biome.kt index 8067d142..06c39cb9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Biome.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Biome.kt @@ -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): Float { + if (block == mainBlock.native.entry) { + return hueShift.toFloat() + } + + return 0f + } + + @JvmName("hueShiftTile") + fun hueShift(block: Registry.Ref): Float { + if (block == mainBlock.native) { + return hueShift.toFloat() + } + + return 0f + } + + @JvmName("hueShiftMod") + fun hueShift(mod: Registry.Entry): 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): Float { + if ( + mod == surfacePlaceables.grassMod.native || + mod == undergroundPlaceables.grassMod.native || + mod == surfacePlaceables.ceilingGrassMod.native || + mod == undergroundPlaceables.ceilingGrassMod.native + ) { + return hueShift.toFloat() + } + + return 0f + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt index a7803904..6c6b4e4f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt @@ -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?), - val ceilingGrassMod: NativeLegacy.TileMod = NativeLegacy.TileMod(null as Registry.Ref?), + 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 = ImmutableList.of(), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt index 63df6551..70796813 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt @@ -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 ) ) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt index aac3d41f..b4356e8e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/InstanceWorldConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/InstanceWorldConfig.kt new file mode 100644 index 00000000..4ba6f557 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/InstanceWorldConfig.kt @@ -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") + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt index 4c3f5f34..9a1f6843 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt @@ -54,7 +54,7 @@ class Parallax( val baseCount: Int = 1, val modCount: Int = 0, val parallax: Either, - 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, val alpha: Double = 1.0, val parallaxValue: Vector2d, - val repeat: Either, Pair>, + // treat as booleans because original engine stuff. + val repeat: Pair, 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") } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt index 37e98158..f45d4f42 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt @@ -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("", 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt index da8366f8..882b74ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt @@ -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?, + val caveLiquid: Registry.Ref = BuiltinMetaMaterials.NO_LIQUID.ref, val caveLiquidSeedDensity: Double, - val oceanLiquid: Either?, + val oceanLiquid: Registry.Ref = 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? = null + var surfaceLiquid: Registry.Ref = 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt index 6f707a1f..2b97f256 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt @@ -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 = "/", diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Utils.kt index f6142472..2d085ffd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Utils.kt @@ -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), + ) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt index f903230f..80bafbce 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt @@ -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? = 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? = 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt index ad259843..7fe13885 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt @@ -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() val layers = ArrayList() + var loopX = true + var loopY = false + + val worldGeometry by lazy { + WorldGeometry(worldSize, loopX, loopY) + } + private object StartingRegionsToken : TypeToken>() @JsonFactory @@ -94,14 +105,57 @@ class WorldLayout { return data } + + fun findContainingCell(x: Int): Pair { + 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 { + 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 { + 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? = null, + val caveLiquid: Registry.Ref = BuiltinMetaMaterials.NO_LIQUID.ref, val caveLiquidSeedDensity: Double = 0.0, - val oceanLiquid: Either? = null, + val oceanLiquid: Registry.Ref = 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, val terrainSelectors: List>, 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 { + val weighting = ArrayList() + + 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() { private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index 2175e1b2..fe35f891 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -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() + 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, val regionData: WorldLayout? = null, - //val customTerrainRegions: + val customTerrainRegions: List, ) 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 = BuiltinMetaMaterials.EMPTY.ref + var foregroundMod: Registry.Ref = BuiltinMetaMaterials.EMPTY_MOD.ref + var background: Registry.Ref = BuiltinMetaMaterials.EMPTY.ref + var backgroundMod: Registry.Ref = BuiltinMetaMaterials.EMPTY_MOD.ref + var fillMicrodungeons: Boolean = false + var encloseLiquids: Boolean = false + var oceanLiquidLevel: Int = 0 + var oceanLiquid: Registry.Ref = BuiltinMetaMaterials.NO_LIQUID.ref + var caveLiquidSeedDensity: Double = 0.0 + var caveLiquid: Registry.Ref = 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 + val transitionWeighting: List + + 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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt index 126d4ce8..db413d3a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt @@ -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, ) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/NativeLegacy.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/NativeLegacy.kt index 26add45e..753d640f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/NativeLegacy.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/NativeLegacy.kt @@ -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 { } override fun computeNative(value: Int?): Registry.Ref { - value ?: return Registries.tiles.emptyRef + value ?: return BuiltinMetaMaterials.EMPTY.ref return Registries.tiles.ref(value) } } @@ -93,7 +94,7 @@ abstract class NativeLegacy { } override fun computeNative(value: Int?): Registry.Ref { - value ?: return Registries.tileModifiers.emptyRef + value ?: return BuiltinMetaMaterials.EMPTY_MOD.ref return Registries.tileModifiers.ref(value) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt index dc967cf6..796d8491 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt @@ -436,8 +436,8 @@ class FactoryAdapter private constructor( ) } - fun add(field: KProperty1, isFlat: Boolean = false, isMarkedNullable: Boolean? = null): Builder { - types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable)) + fun add(field: KProperty1, isFlat: Boolean = false, isMarkedNullable: Boolean? = null, isIgnored: Boolean = false): Builder { + types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable, isIgnored = isIgnored)) return this } @@ -511,6 +511,7 @@ class FactoryAdapter 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, ) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt index ea5cc062..c9f4f2ee 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Line2d.kt @@ -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, val t: KOptional, 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() { private val pair = gson.getAdapter>() 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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt index d290dc43..ee8e3419 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/math/Utils.kt @@ -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) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index c23b09c9..308275fe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -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 { val result = ArrayList() @@ -239,7 +245,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : )) } - for (entity in clientSpectatingEntities.get()) { + for (entity in clientSpectatingEntities) { // TODO } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt index f858fbc6..b411ae0f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt @@ -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) : 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(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) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldClientStateUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldClientStateUpdatePacket.kt index ed4da350..15b16921 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldClientStateUpdatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/WorldClientStateUpdatePacket.kt @@ -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) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index a047ef33..ab37d10d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 2102bf30..c49d7273 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -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() - val worlds = ConcurrentHashMap() + private val worlds = HashMap>() 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>() @@ -60,11 +64,107 @@ sealed class StarboundServer(val root: File) : Closeable { fun loadSystemWorld(location: Vector3i): CompletableFuture { 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 { + 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 { + 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 { 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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 3fcde367..7bb09384 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -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(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 { if (cells.isInitialized()) { return Object2DArray(cells.value) @@ -16,4 +60,205 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk() + private val damagedTilesBackground = ObjectArraySet() + + fun tileDamagePackets(): List { + val result = ArrayList() + + 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 { + 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() + } + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt index 257549a6..b8809282 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerSystemWorld.kt @@ -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 { @@ -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() fun destination(destination: SystemWorldLocation, future: CompletableFuture) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index a3f3a650..1b88c3fb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -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() + 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 { @@ -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() + + 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 { + ticketListLock.withLock { + return geometry.region2Chunks(region).map { getTicketList(it).Ticket(target) } + } + } + + fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List { + 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 { + 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 { + 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 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() private val temporary = ObjectAVLTreeSet() private var ticks = 0 - private var nextTicketID = AtomicInteger() + private var nextTicketID = 0 + private var isBusy = false + private var chunk by Delegates.notNull() + private val targetState = Channel(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 + val temporary: List + + 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 + val temporary: List + + 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 + val temporary: List + + 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() - 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 { LOGGER.info("Attempting to load world at $worldID") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index a4ebf9d8..a8dc369e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -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() - private val pendingSend = ObjectLinkedOpenHashSet() private val tasks = ConcurrentLinkedQueue 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() @@ -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() } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt index 10b51c7a..32e35cc7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldGenerator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldGenerator.kt new file mode 100644 index 00000000..f1c512d3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldGenerator.kt @@ -0,0 +1,7 @@ +package ru.dbotthepony.kstarbound.server.world + +import ru.dbotthepony.kstarbound.defs.world.WorldTemplate + +class WorldGenerator(val template: WorldTemplate) { + +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldStorage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldStorage.kt index 621c6d19..5b6a1bbc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldStorage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/WorldStorage.kt @@ -50,6 +50,20 @@ abstract class WorldStorage : Closeable { } } + object Nothing : WorldStorage() { + override fun loadCells(pos: ChunkPos): CompletableFuture>> { + return CompletableFuture.completedFuture(KOptional()) + } + + override fun loadEntities(pos: ChunkPos): CompletableFuture>> { + return CompletableFuture.completedFuture(KOptional()) + } + + override fun loadMetadata(): CompletableFuture> { + return CompletableFuture.completedFuture(KOptional()) + } + } + companion object { val NULL: WorldStorage = Fixed(AbstractCell.NULL) val EMPTY: WorldStorage = Fixed(AbstractCell.EMPTY) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt index b3160ffc..03ec23f4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt @@ -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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt new file mode 100644 index 00000000..251245b1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/MailboxExecutorService.kt @@ -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 > LinkedList.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>() + + private val timers = LinkedList>() + private val repeatableTimers = LinkedList() + + @Volatile + private var isShutdown = false + @Volatile + private var isTerminated = false + + private val timeOrigin = JVMTimeSource() + + var exceptionHandler: Consumer? = null + + private inner class Timer(task: Callable, val executeAt: Long) : FutureTask(task), ScheduledFuture { + 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(private val value: T) : Future { + 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({ task.run() }), ScheduledFuture { + 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() + + 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 { + if (isTerminated) return listOf() + isShutdown = true + isTerminated = true + + val result = ArrayList() + + 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 submit(task: Callable): Future { + 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 submit(task: Runnable, result: T): Future { + 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 invokeAll(tasks: Collection>): List> { + 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 invokeAll( + tasks: Collection>, + timeout: Long, + unit: TimeUnit + ): List> { + 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 invokeAny(tasks: Collection>): 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 invokeAny(tasks: Collection>, 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 join(future: Future): 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 schedule(callable: Callable, delay: Long, unit: TimeUnit): ScheduledFuture { + 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) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt index 225e71a9..15dd2d4c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt @@ -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? = 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) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 7b582021..43df2362 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -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, This : Chunk 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() - protected val damagedTilesBackground = ObjectArraySet() - - fun legacyNetworkCells(): Object2DArray { - 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 { - val result = ArrayList() - - 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) { val ours = cells.value source.checkSizeEquals(ours) @@ -286,28 +165,11 @@ abstract class Chunk, This : Chunk - 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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt index acf6abc8..ac5983db 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CoordinateMapper.kt @@ -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) + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt index 6ea80382..b10cf894 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt @@ -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) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index babc94f8..032636b3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -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, ChunkType : Chunk>(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, ChunkType : Chunk } + fun matchCells(aabb: AABBi, predicate: Predicate): 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): 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 { val result = ArrayList() val tiles = aabb.encasingIntAABB() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt index 93dda28e..452b6966 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt @@ -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)) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt index a2533e7b..a4eb403c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt index b8b858d2..5eb461f1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt @@ -9,7 +9,7 @@ import java.io.DataInputStream import java.io.DataOutputStream sealed class AbstractLiquidState { - abstract val def: Registry.Entry? + abstract val state: Registry.Entry 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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt index 5cd21818..88a135f0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt @@ -12,7 +12,7 @@ import java.io.DataOutputStream sealed class AbstractTileState { abstract val material: Registry.Entry - abstract val modifier: Registry.Entry? + abstract val modifier: Registry.Entry 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()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt index 775cd4a4..bb631637 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt @@ -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) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableLiquidState.kt index d7a1f3c1..8725fb2a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableLiquidState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableLiquidState.kt @@ -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? = null, + override val state: Registry.Entry = 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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt index 61b2877d..3ca47a36 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt @@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.network.LegacyNetworkTileState data class ImmutableTileState( override var material: Registry.Entry = BuiltinMetaMaterials.NULL, - override var modifier: Registry.Entry? = null, + override var modifier: Registry.Entry = BuiltinMetaMaterials.EMPTY_MOD, override var color: TileColor = TileColor.DEFAULT, override var hueShift: Float = 0f, override var modifierHueShift: Float = 0f, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt index a562ca0b..4a323279 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt index a274323c..1ff0be98 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt @@ -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? = null, + override var state: Registry.Entry = 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)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableTileState.kt index 01aaafcf..e40a03b5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableTileState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableTileState.kt @@ -9,7 +9,7 @@ import java.io.DataInputStream data class MutableTileState( override var material: Registry.Entry = BuiltinMetaMaterials.NULL, - override var modifier: Registry.Entry? = null, + override var modifier: Registry.Entry = 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 } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index cc7b2afb..d0b67172 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt index d3c731f5..dfa7f036 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt index 362f3748..973fa3c5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -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): Pair, ImmutableList> { +private fun calculateEdges(points: List): Pair, ImmutableList> { 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): Pair() + val edges = ImmutableList.Builder() 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, val vertices: ImmutableList) { - private constructor(pair: Pair, ImmutableList>) : this(pair.first, pair.second) +class Poly private constructor(val edges: ImmutableList, val vertices: ImmutableList) { + private constructor(pair: Pair, ImmutableList>) : this(pair.first, pair.second) constructor(points: List) : 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, 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 { @@ -133,7 +121,7 @@ class Poly private constructor(val edges: ImmutableList, val vertices: Imm operator fun plus(value: Penetration): Poly { if (isEmpty) return this val vertices = ImmutableList.Builder() - val edges = ImmutableList.Builder() + val edges = ImmutableList.Builder() 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, val vertices: Imm operator fun plus(value: IStruct2d): Poly { if (isEmpty) return this val vertices = ImmutableList.Builder() - val edges = ImmutableList.Builder() + val edges = ImmutableList.Builder() 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, val vertices: Imm operator fun minus(value: IStruct2d): Poly { if (isEmpty) return this val vertices = ImmutableList.Builder() - val edges = ImmutableList.Builder() + val edges = ImmutableList.Builder() 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, val vertices: Imm operator fun times(value: IStruct2d): Poly { if (isEmpty) return this val vertices = ImmutableList.Builder() - val edges = ImmutableList.Builder() + val edges = ImmutableList.Builder() 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, val vertices: Imm operator fun times(value: Double): Poly { if (isEmpty) return this val vertices = ImmutableList.Builder() - val edges = ImmutableList.Builder() + val edges = ImmutableList.Builder() 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, val vertices: Imm val sin = sin(radians) val cos = cos(radians) - val edges = ImmutableList.Builder() + val edges = ImmutableList.Builder() val vertices = ImmutableList.Builder() 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, 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. */ diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt index 38052d24..d56a7f90 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/IslandSurfaceTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/IslandSurfaceTerrainSelector.kt index 905230a1..4b5583e9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/IslandSurfaceTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/IslandSurfaceTerrainSelector.kt @@ -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(::compute) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt index 60686b4c..8931aadf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/KarstCaveTerrainSelector.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/PerlinTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/PerlinTerrainSelector.kt index 0076bf9b..72b04f72 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/PerlinTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/PerlinTerrainSelector.kt @@ -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(data, parameters) { + @JsonFactory data class Data( @JsonAlias("type") val function: PerlinNoiseParameters.Type, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt index e28a96ca..cb85c3b8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt @@ -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") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt index 1d5abaec..2f98ec28 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/WormCaveTerrainSelector.kt @@ -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)