Central structures placement and replacement, minimally working shipworld saving, some more save format inconsistencies dug up

This commit is contained in:
DBotThePony 2024-05-10 20:10:12 +07:00
parent 9eaa6ea5f1
commit 2d3c080002
Signed by: DBot
GPG Key ID: DCC23B5715498507
30 changed files with 714 additions and 177 deletions

View File

@ -41,6 +41,10 @@ val color: TileColor = TileColor.DEFAULT
* Tiled map behavior is unchanged, and marks their position only. * Tiled map behavior is unchanged, and marks their position only.
## .terrain ## .terrain
Please keep in mind that if you use new format or new terrain selectors original clients will
probably explode upon joining worlds where new terrain selectors are utilized.
* All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them * All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them
* They can be referenced by either specifying corresponding field as string, or as object like so: `{"name": "namedselector"}` * They can be referenced by either specifying corresponding field as string, or as object like so: `{"name": "namedselector"}`
* `min`, `max` and `minmax` terrain selectors now also accept next format: `{"name": "namedselector", "seedBias": 4}` * `min`, `max` and `minmax` terrain selectors now also accept next format: `{"name": "namedselector", "seedBias": 4}`
@ -48,8 +52,8 @@ val color: TileColor = TileColor.DEFAULT
* `displacement` terrain selector has `seedBias` added, which deviate seed of `source` selector (default to `0`) * `displacement` terrain selector has `seedBias` added, which deviate seed of `source` selector (default to `0`)
* `displacement` terrain selector has `xClamp` added, works like `yClamp` * `displacement` terrain selector has `xClamp` added, works like `yClamp`
* `rotate` terrain selector has `rotationWidth` (defaults to `0.5`) and `rotationHeight` (defaults to `0.0`) added, which are multiplied by world's size and world's height respectively to determine rotation point center * `rotate` terrain selector has `rotationWidth` (defaults to `0.5`) and `rotationHeight` (defaults to `0.0`) added, which are multiplied by world's size and world's height respectively to determine rotation point center
* `min` terrain selector added, opposite of existing `max` (json format is the same as `max`) * Added `min` terrain selector, opposite of existing `max` (json format is the same as `max`)
* `cache` terrain selector removed due it not being documented, and having little practical value * Removed `cache` terrain selector due it not being documented, and having little practical value
* `perlin` terrain selector now accepts `type`, `frequency` and `amplitude` values (naming inconsistency fix) * `perlin` terrain selector now accepts `type`, `frequency` and `amplitude` values (naming inconsistency fix)
* `ridgeblocks` terrain selector now accepts `amplitude` and `frequency` values (naming inconsistency fix); * `ridgeblocks` terrain selector now accepts `amplitude` and `frequency` values (naming inconsistency fix);
* `ridgeblocks` has `octaves` added (defaults to `2`), `perlinOctaves` (defaults to `1`) * `ridgeblocks` has `octaves` added (defaults to `2`), `perlinOctaves` (defaults to `1`)

View File

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

View File

@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.UniverseServerConfig import ru.dbotthepony.kstarbound.defs.UniverseServerConfig
import ru.dbotthepony.kstarbound.defs.WorldServerConfig import ru.dbotthepony.kstarbound.defs.WorldServerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig
import ru.dbotthepony.kstarbound.defs.item.ItemGlobalConfig import ru.dbotthepony.kstarbound.defs.item.ItemGlobalConfig
import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
@ -114,6 +115,9 @@ object Globals {
var itemParameters by Delegates.notNull<ItemGlobalConfig>() var itemParameters by Delegates.notNull<ItemGlobalConfig>()
private set private set
var shipUpgrades by Delegates.notNull<ShipUpgrades>()
private set
private var profanityFilterInternal by Delegates.notNull<ImmutableList<String>>() private var profanityFilterInternal by Delegates.notNull<ImmutableList<String>>()
val profanityFilter: ImmutableSet<String> by lazy { val profanityFilter: ImmutableSet<String> by lazy {
@ -219,6 +223,7 @@ object Globals {
tasks.add(load("/plants/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage))
tasks.add(load("/plants/bushDamage.config", ::bushDamage)) tasks.add(load("/plants/bushDamage.config", ::bushDamage))
tasks.add(load("/tiles/defaultDamage.config", ::tileDamage)) tasks.add(load("/tiles/defaultDamage.config", ::tileDamage))
tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades))
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/dungeon_worlds.config", ::dungeonWorlds, mapAdapter("/dungeon_worlds.config")) }.asCompletableFuture()) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/dungeon_worlds.config", ::dungeonWorlds, mapAdapter("/dungeon_worlds.config")) }.asCompletableFuture())
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/currencies.config", ::currencies, mapAdapter("/currencies.config")) }.asCompletableFuture()) tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/currencies.config", ::currencies, mapAdapter("/currencies.config")) }.asCompletableFuture())

View File

@ -294,7 +294,9 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
val ELEMENTS_ADAPTER = InternedJsonElementAdapter(STRINGS) val ELEMENTS_ADAPTER = InternedJsonElementAdapter(STRINGS)
val gson: Gson = with(GsonBuilder()) { val gson: Gson = with(GsonBuilder()) {
// serializeNulls() // serialize explicit nulls, FactoryAdapter don't serialize null fields
// This is required because some fucktard fucking fuck put get/optX combo in some places instead of optX directly, or even opt/optX
serializeNulls()
setDateFormat(DateFormat.LONG) setDateFormat(DateFormat.LONG)
setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
setPrettyPrinting() setPrettyPrinting()

View File

@ -131,14 +131,6 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn
} }
} }
override fun onChannelClosed() {
super.onChannelClosed()
if (pendingDisconnect) {
disconnectNow()
}
}
fun bootstrap(address: SocketAddress = channel.remoteAddress(), asLegacy: Boolean = false) { fun bootstrap(address: SocketAddress = channel.remoteAddress(), asLegacy: Boolean = false) {
LOGGER.info("Trying to connect to remote server at $address with ${if (asLegacy) "legacy" else "native"} protocol") LOGGER.info("Trying to connect to remote server at $address with ${if (asLegacy) "legacy" else "native"} protocol")

View File

@ -26,6 +26,7 @@ import ru.dbotthepony.kstarbound.client.render.RenderLayer
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
@ -72,6 +73,12 @@ class ClientWorld(
override val connectionID: Int override val connectionID: Int
get() = client.activeConnection?.connectionID ?: throw IllegalStateException("ClientWorld exists without active connection") get() = client.activeConnection?.connectionID ?: throw IllegalStateException("ClientWorld exists without active connection")
public override var centralStructure: WorldStructure
get() = super.centralStructure
set(value) {
super.centralStructure = value
}
val renderRegionWidth = determineChunkSize(geometry.size.x) val renderRegionWidth = determineChunkSize(geometry.size.x)
val renderRegionHeight = determineChunkSize(geometry.size.y) val renderRegionHeight = determineChunkSize(geometry.size.y)
val renderRegionsX = geometry.size.x / renderRegionWidth val renderRegionsX = geometry.size.x / renderRegionWidth

View File

@ -14,6 +14,8 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.FastJsonTreeReader
import ru.dbotthepony.kstarbound.json.popJsonElement
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.util.sbIntern
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
@ -59,7 +61,7 @@ class AssetReference<V> {
companion object : TypeAdapterFactory { companion object : TypeAdapterFactory {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
val EMPTY = AssetReference(null, null, null, null) private val EMPTY = AssetReference(null, null, null, null)
fun <V> empty() = EMPTY as AssetReference<V> fun <V> empty() = EMPTY as AssetReference<V>
@ -117,8 +119,8 @@ class AssetReference<V> {
return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second) return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second)
} else { } else {
val json = Starbound.ELEMENTS_ADAPTER.read(`in`) val json = `in`.popJsonElement()
val value = adapter.read(JsonTreeReader(json)) ?: return null val value = adapter.read(FastJsonTreeReader(json)) ?: return null
return AssetReference(null, null, value, json) return AssetReference(null, null, value, json)
} }
} }

View File

@ -1,9 +1,11 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.util.function.Predicate import java.util.function.Predicate
@ -16,6 +18,7 @@ data class UniverseServerConfig(
val queuedFlightWaitTime: Double = 0.0, val queuedFlightWaitTime: Double = 0.0,
val useNewWireProcessing: Boolean = true, val useNewWireProcessing: Boolean = true,
val speciesShips: ImmutableMap<String, ImmutableList<AssetReference<WorldStructure>>>,
) { ) {
@JsonFactory @JsonFactory
data class WorldPredicate( data class WorldPredicate(

View File

@ -40,10 +40,10 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : Par
val offset = (x + y * image.width) * 4 val offset = (x + y * image.width) * 4
// flip image as we go // flip image as we go
tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or // red
bytes[offset + 1].toInt().and(0xFF).shl(8) or bytes[offset + 1].toInt().and(0xFF).shl(8) or // green
bytes[offset + 2].toInt().and(0xFF).shl(16) or bytes[offset + 2].toInt().and(0xFF).shl(16) or // blue
bytes[offset + 3].toInt().and(0xFF).shl(24) bytes[offset + 3].toInt().and(0xFF).shl(24) // alpha
} }
} }

View File

@ -151,6 +151,10 @@ class Image private constructor(
val whole = Sprite("this", 0, 0, width, height) val whole = Sprite("this", 0, 0, width, height)
val nonEmptyRegion get() = whole.nonEmptyRegion val nonEmptyRegion get() = whole.nonEmptyRegion
override fun toString(): String {
return "Image[$source of $width, $height]"
}
/** /**
* returns integer in ABGR format * returns integer in ABGR format
*/ */
@ -201,9 +205,12 @@ class Image private constructor(
override val u1: Float = (x.toFloat() + this.width.toFloat()) / this@Image.width override val u1: Float = (x.toFloat() + this.width.toFloat()) / this@Image.width
override val v0: Float = (y.toFloat() + this.height.toFloat()) / this@Image.height override val v0: Float = (y.toFloat() + this.height.toFloat()) / this@Image.height
override fun toString(): String {
return "Sprite[at $x, $y -> $width, $height of ${this@Image}]"
}
/** /**
* returns integer in big-endian ABGR format if it is RGB or RGBA picture, * returns integer in little-endian RGBA/big-endian ABGR format
* otherwise returns pixels as-is
*/ */
operator fun get(x: Int, y: Int, data: ByteBuffer = this@Image.data): Int { operator fun get(x: Int, y: Int, data: ByteBuffer = this@Image.data): Int {
require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" } require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" }
@ -217,7 +224,7 @@ class Image private constructor(
} }
/** /**
* returns integer in ABGR format * returns integer in little-endian RGBA/big-endian ABGR format
*/ */
operator fun get(x: Int, y: Int, flip: Boolean, data: ByteBuffer = this@Image.data): Int { operator fun get(x: Int, y: Int, flip: Boolean, data: ByteBuffer = this@Image.data): Int {
if (flip) { if (flip) {

View File

@ -175,7 +175,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
val pool = ImmutableList.Builder<Pair<Double, ItemOrPool>>() val pool = ImmutableList.Builder<Pair<Double, ItemOrPool>>()
val fill = ImmutableList.Builder<Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>>() val fill = ImmutableList.Builder<Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>>()
var poolRounds: IPoolRounds = OneRound var poolRounds: IPoolRounds = OneRound
val allowDuplication = things["allowDuplication"]?.asBoolean ?: false val allowDuplication = things["allowDuplication"]?.asBoolean ?: true
things["poolRounds"]?.let { things["poolRounds"]?.let {
if (it is JsonPrimitive) { if (it is JsonPrimitive) {

View File

@ -91,6 +91,7 @@ data class ObjectDefinition(
val damageConfig: CompletableFuture<TileDamageParameters>, val damageConfig: CompletableFuture<TileDamageParameters>,
val flickerPeriod: PeriodicFunction? = null, val flickerPeriod: PeriodicFunction? = null,
val orientations: ImmutableList<Supplier<ObjectOrientation>>, val orientations: ImmutableList<Supplier<ObjectOrientation>>,
val uniqueId: String? = null,
) { ) {
fun findValidOrientation(world: World<*, *>, position: Vector2i, directionAffinity: Direction? = null, ignoreProtectedDungeons: Boolean = false): Int { fun findValidOrientation(world: World<*, *>, position: Vector2i, directionAffinity: Direction? = null, ignoreProtectedDungeons: Boolean = false): Int {
// If we are given a direction affinity, try and find an orientation with a // If we are given a direction affinity, try and find an orientation with a
@ -159,6 +160,7 @@ data class ObjectDefinition(
val health: Double = 1.0, val health: Double = 1.0,
val rooting: Boolean = false, val rooting: Boolean = false,
val biomePlaced: Boolean = false, val biomePlaced: Boolean = false,
val uniqueId: String? = null,
) )
private val basic = gson.getAdapter(PlainData::class.java) private val basic = gson.getAdapter(PlainData::class.java)
@ -283,6 +285,7 @@ data class ObjectDefinition(
damageConfig = damageConfig, damageConfig = damageConfig,
flickerPeriod = flickerPeriod, flickerPeriod = flickerPeriod,
orientations = orientations.build(), orientations = orientations.build(),
uniqueId = basic.uniqueId,
) )
} }
} }

View File

@ -66,15 +66,17 @@ data class ObjectOrientation(
if (occupySpaces.isEmpty()) if (occupySpaces.isEmpty())
return true return true
var valid = occupySpaces.all { val localSpaces = occupySpaces.map { it + position }
val cell = world.chunkMap.getCell(it + position)
var valid = localSpaces.all {
val cell = world.chunkMap.getCell(it)
//if (!cell.foreground.material.isEmptyTile) println("not empty tile: ${it + position}, space $it, pos $position") //if (!cell.foreground.material.isEmptyTile) println("not empty tile: ${it + position}, space $it, pos $position")
//if (cell.dungeonId in world.protectedDungeonIDs) println("position is protected: ${it + position}") //if (cell.dungeonId in world.protectedDungeonIDs) println("position is protected: ${it + position}")
cell.foreground.material.isEmptyTile && (ignoreProtectedDungeons || !world.isDungeonIDProtected(cell.dungeonId)) cell.foreground.material.isEmptyTile && (ignoreProtectedDungeons || !world.isDungeonIDProtected(cell.dungeonId))
} }
if (valid) { if (valid) {
valid = !world.entityIndex.any(AABB.ofPoints(occupySpaces), Predicate { it is WorldObject && occupySpaces.any { s -> s in it.occupySpaces } }) valid = !world.entityIndex.any(AABB.ofPoints(localSpaces), Predicate { it is WorldObject && localSpaces.any { s -> s in it.occupySpaces } })
} }
return valid return valid
@ -189,7 +191,7 @@ data class ObjectOrientation(
var occupySpaces = obj["spaces"]?.let { spaces.fromJsonTree(it) } ?: ImmutableSet.of(Vector2i.ZERO) var occupySpaces = obj["spaces"]?.let { spaces.fromJsonTree(it) } ?: ImmutableSet.of(Vector2i.ZERO)
if ("spaceScan" in obj) { if ("spaceScan" in obj) {
occupySpaces = ImmutableSet.of() // occupySpaces = ImmutableSet.of()
for (drawable in drawables) { for (drawable in drawables) {
if (drawable is Drawable.Image) { if (drawable is Drawable.Image) {
@ -239,7 +241,7 @@ data class ObjectOrientation(
"right" -> occupySpaces.stream().filter { it.x == maxX }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "right" -> occupySpaces.stream().filter { it.x == maxX }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"top" -> occupySpaces.stream().filter { it.y == maxY }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "top" -> occupySpaces.stream().filter { it.y == maxY }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"bottom" -> occupySpaces.stream().filter { it.y == minY }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "bottom" -> occupySpaces.stream().filter { it.y == minY }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"background" -> occupySpaces.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "background" -> occupySpaces.forEach { anchors.add(Anchor(true, it, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
else -> throw JsonSyntaxException("Unknown anchor type $v") else -> throw JsonSyntaxException("Unknown anchor type $v")
} }
} }

View File

@ -2,47 +2,175 @@ package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.JsonAlias
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
@JsonFactory @JsonFactory
data class WorldStructure( data class WorldStructure(
val region: AABBi = AABBi(Vector2i.ZERO, Vector2i.ZERO), val region: AABBi = AABBi(Vector2i.ZERO, Vector2i.ZERO),
val anchorPosition: Vector2i = Vector2i.ZERO, val anchorPosition: Vector2i = Vector2i.ZERO,
var config: JsonElement = JsonObject(), val config: JsonObject = JsonObject(),
val backgroundOverlays: ImmutableList<Overlay> = ImmutableList.of(), val backgroundOverlays: ImmutableList<Overlay> = ImmutableList.of(),
val foregroundOverlays: ImmutableList<Overlay> = ImmutableList.of(), val foregroundOverlays: ImmutableList<Overlay> = ImmutableList.of(),
val blockKey: AssetReference<ImmutableList<BlockKey>> = AssetReference.empty(),
val blockImage: SpriteReference? = null,
val backgroundBlocks: ImmutableList<Block> = ImmutableList.of(), val backgroundBlocks: ImmutableList<Block> = ImmutableList.of(),
val foregroundBlocks: ImmutableList<Block> = ImmutableList.of(), val foregroundBlocks: ImmutableList<Block> = ImmutableList.of(),
val objects: ImmutableList<Obj> = ImmutableList.of(), val objects: ImmutableList<Obj> = ImmutableList.of(),
val flaggedBlocks: ImmutableMap<String, ImmutableList<Vector2i>> = ImmutableMap.of(), val flaggedBlocks: ImmutableMap<String, ImmutableSet<Vector2i>> = ImmutableMap.of(),
) { ) {
init { @JsonFactory
if (config == JsonNull.INSTANCE) { data class Overlay(
// so it is not omitted @JsonAlias("position")
config = JsonObject() val min: Vector2d,
} val image: AssetPath,
} val fullbright: Boolean = false)
@JsonFactory @JsonFactory
data class Overlay(val min: Vector2d, val image: String, val fullbright: Boolean) data class Block(val position: Vector2i, val materialId: NativeLegacy.Tile, val residual: Boolean)
@JsonFactory
data class Block(val position: Vector2i, val materialId: Either<Int, String>, val residual: Boolean)
@JsonFactory @JsonFactory
data class Obj( data class Obj(
val position: Vector2i, val position: Vector2i = Vector2i.ZERO,
val name: String, val name: Registry.Ref<ObjectDefinition>,
val direction: Direction, val direction: Direction = Direction.LEFT,
val parameters: JsonElement, val parameters: JsonObject = JsonObject(),
val residual: Boolean = false, val residual: Boolean = false,
) )
@JsonFactory
data class BlockKey(
val value: RGBAColor,
val foregroundBlock: Boolean = false,
val backgroundBlock: Boolean = false,
val foregroundMat: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.STRUCTURE.ref,
val backgroundMat: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.STRUCTURE.ref,
val foregroundResidual: Boolean = false,
val backgroundResidual: Boolean = false,
val `object`: Registry.Ref<ObjectDefinition> = Registries.worldObjects.emptyRef,
val objectDirection: Direction = Direction.LEFT,
val objectParameters: JsonObject = JsonObject(),
val objectResidual: Boolean = false,
val flags: ImmutableSet<String> = ImmutableSet.of(),
val anchor: Boolean = false,
)
fun resolve(position: Vector2i): WorldStructure {
val blockKey = blockKey.value.get() ?: return copy(
anchorPosition = position,
backgroundBlocks = ImmutableList.of(),
foregroundBlocks = ImmutableList.of(),
objects = ImmutableList.of(),
flaggedBlocks = ImmutableMap.of(),
)
val sprite = blockImage?.sprite ?: return copy(
anchorPosition = position,
backgroundBlocks = ImmutableList.of(),
foregroundBlocks = ImmutableList.of(),
objects = ImmutableList.of(),
flaggedBlocks = ImmutableMap.of(),
)
var anchorPosition: Vector2i? = null
val backgroundBlocks = ArrayList<Block>()
val foregroundBlocks = ArrayList<Block>()
val objects = ArrayList<Obj>()
val flaggedBlocks = HashMap<String, HashSet<Vector2i>>()
val mapped = Int2ObjectOpenHashMap<BlockKey>()
for (v in blockKey) {
mapped[v.value.toRGBA()] = v
}
for (x in 0 until sprite.width) {
for (y in sprite.height - 1 downTo 0) { // reverses order of objects
// flip image so image visible top maps to world's top
val color = sprite[x, sprite.height - y - 1]
val block = mapped[color]
if (block == null) {
LOGGER.error("No such block with color index $color at $x, $y in $sprite")
continue
}
val pos = Vector2i(x, y)
if (block.foregroundBlock) {
foregroundBlocks.add(Block(pos, NativeLegacy.Tile(block.foregroundMat), block.foregroundResidual))
}
if (block.backgroundBlock) {
backgroundBlocks.add(Block(pos, NativeLegacy.Tile(block.backgroundMat), block.backgroundResidual))
}
if (block.`object`.isPresent) {
objects.add(Obj(
pos,
block.`object`,
block.objectDirection,
block.objectParameters,
block.objectResidual
))
}
if (block.anchor) {
check(anchorPosition == null) { "Duplicate anchor position (previous was at ${anchorPosition!! - position}, new at $x, $y)" }
anchorPosition = Vector2i(x, y)
}
for (flag in block.flags)
flaggedBlocks.computeIfAbsent(flag) { HashSet() }.add(pos)
}
}
val diff = position - (anchorPosition ?: position)
return copy(
anchorPosition = (anchorPosition ?: position) + diff,
backgroundBlocks = backgroundBlocks.stream().map { it.copy(position = it.position + diff) }.collect(ImmutableList.toImmutableList()),
foregroundBlocks = foregroundBlocks.stream().map { it.copy(position = it.position + diff) }.collect(ImmutableList.toImmutableList()),
backgroundOverlays = backgroundOverlays.stream().map { it.copy(min = it.min + diff) }.collect(ImmutableList.toImmutableList()),
foregroundOverlays = foregroundOverlays.stream().map { it.copy(min = it.min + diff) }.collect(ImmutableList.toImmutableList()),
objects = objects.stream().map { it.copy(position = it.position + diff) }.collect(ImmutableList.toImmutableList()),
flaggedBlocks = flaggedBlocks.entries
.stream()
.map { it.key to it.value.stream().map { it + diff }.collect(ImmutableSet.toImmutableSet()) }
.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })),
)
}
companion object {
private val LOGGER = LogManager.getLogger()
}
} }

View File

@ -165,6 +165,9 @@ class FactoryAdapter<T : Any> private constructor(
out.beginObject() out.beginObject()
for (type in types) { for (type in types) {
if (type.isIgnored)
continue
if (type.isFlat) { if (type.isFlat) {
check(!asJsonArray) check(!asJsonArray)
@ -183,12 +186,17 @@ class FactoryAdapter<T : Any> private constructor(
} }
} else { } else {
val (field, adapter) = type val (field, adapter) = type
val getValue = field.get(value)
// god fucking damn it
if (!asJsonArray && getValue === null)
continue
if (!asJsonArray) if (!asJsonArray)
out.name(field.name) out.name(field.name)
@Suppress("unchecked_cast") @Suppress("unchecked_cast")
(adapter as TypeAdapter<Any>).write(out, (field as KProperty1<T, Any>).get(value)) (adapter as TypeAdapter<Any>).write(out, field.get(value))
} }
} }

View File

@ -234,6 +234,8 @@ fun luaFunctionN(name: String, callable: ExecutionContext.(ArgumentIterator) ->
} }
override fun invoke(context: ExecutionContext, args: Array<out Any>) { override fun invoke(context: ExecutionContext, args: Array<out Any>) {
context.returnBuffer.setTo()
try { try {
callable.invoke(context, ArgumentIterator.of(context, name, args)) callable.invoke(context, ArgumentIterator.of(context, name, args))
} catch (err: ClassCastException) { } catch (err: ClassCastException) {
@ -252,6 +254,8 @@ fun luaFunctionNS(name: String, callable: ExecutionContext.(ArgumentIterator) ->
} }
override fun invoke(context: ExecutionContext, args: Array<out Any>) { override fun invoke(context: ExecutionContext, args: Array<out Any>) {
context.returnBuffer.setTo()
try { try {
callable.invoke(context, ArgumentIterator.of(context, name, args)).run(context) callable.invoke(context, ArgumentIterator.of(context, name, args)).run(context)
} catch (err: ClassCastException) { } catch (err: ClassCastException) {
@ -270,6 +274,8 @@ fun luaFunctionArray(callable: ExecutionContext.(Array<out Any?>) -> Unit): LuaF
} }
override fun invoke(context: ExecutionContext, args: Array<out Any?>) { override fun invoke(context: ExecutionContext, args: Array<out Any?>) {
context.returnBuffer.setTo()
try { try {
callable.invoke(context, args) callable.invoke(context, args)
} catch (err: ClassCastException) { } catch (err: ClassCastException) {
@ -288,6 +294,8 @@ fun luaFunction(callable: ExecutionContext.() -> Unit): LuaFunction<*, *, *, *,
} }
override fun invoke(context: ExecutionContext) { override fun invoke(context: ExecutionContext) {
context.returnBuffer.setTo()
try { try {
callable.invoke(context) callable.invoke(context)
} catch (err: ClassCastException) { } catch (err: ClassCastException) {
@ -306,6 +314,8 @@ fun <T> luaFunction(callable: ExecutionContext.(T) -> Unit): LuaFunction<T, *, *
} }
override fun invoke(context: ExecutionContext, arg1: T) { override fun invoke(context: ExecutionContext, arg1: T) {
context.returnBuffer.setTo()
try { try {
callable.invoke(context, arg1) callable.invoke(context, arg1)
} catch (err: ClassCastException) { } catch (err: ClassCastException) {
@ -324,6 +334,8 @@ fun <T, T2> luaFunction(callable: ExecutionContext.(T, T2) -> Unit): LuaFunction
} }
override fun invoke(context: ExecutionContext, arg1: T, arg2: T2) { override fun invoke(context: ExecutionContext, arg1: T, arg2: T2) {
context.returnBuffer.setTo()
try { try {
callable.invoke(context, arg1, arg2) callable.invoke(context, arg1, arg2)
} catch (err: ClassCastException) { } catch (err: ClassCastException) {
@ -342,6 +354,8 @@ fun <T, T2, T3> luaFunction(callable: ExecutionContext.(T, T2, T3) -> Unit): Lua
} }
override fun invoke(context: ExecutionContext, arg1: T, arg2: T2, arg3: T3) { override fun invoke(context: ExecutionContext, arg1: T, arg2: T2, arg3: T3) {
context.returnBuffer.setTo()
try { try {
callable.invoke(context, arg1, arg2, arg3) callable.invoke(context, arg1, arg2, arg3)
} catch (err: ClassCastException) { } catch (err: ClassCastException) {
@ -360,6 +374,8 @@ fun <T, T2, T3, T4> luaFunction(callable: ExecutionContext.(T, T2, T3, T4) -> Un
} }
override fun invoke(context: ExecutionContext, arg1: T, arg2: T2, arg3: T3, arg4: T4) { override fun invoke(context: ExecutionContext, arg1: T, arg2: T2, arg3: T3, arg4: T4) {
context.returnBuffer.setTo()
try { try {
callable.invoke(context, arg1, arg2, arg3, arg4) callable.invoke(context, arg1, arg2, arg3, arg4)
} catch (err: ClassCastException) { } catch (err: ClassCastException) {

View File

@ -459,7 +459,10 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
returnBuffer.setTo(tableOf(*entities.toTypedArray())) returnBuffer.setTo(tableOf(*entities.toTypedArray()))
} }
callbacks["spawnMonster"] = luaStub("spawnMonster") callbacks["spawnMonster"] = luaFunction {
// TODO
returnBuffer.setTo(0)
}
callbacks["spawnNpc"] = luaStub("spawnNpc") callbacks["spawnNpc"] = luaStub("spawnNpc")
callbacks["spawnStagehand"] = luaStub("spawnStagehand") callbacks["spawnStagehand"] = luaStub("spawnStagehand")
callbacks["spawnProjectile"] = luaStub("spawnProjectile") callbacks["spawnProjectile"] = luaStub("spawnProjectile")

View File

@ -551,7 +551,7 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn
callbacks["callScriptedEntity"] = luaFunctionN("callScriptedEntity") { callbacks["callScriptedEntity"] = luaFunctionN("callScriptedEntity") {
val id = it.nextInteger() val id = it.nextInteger()
val function = it.nextString().decode() val function = it.nextString().decode()
val entity = self.entities[id.toInt()] ?: throw LuaRuntimeException("Entity with ID $id does not exist") val entity = self.entities[id.toInt()] ?: return@luaFunctionN //?: throw LuaRuntimeException("Entity with ID $id does not exist")
if (entity !is ScriptedEntity) if (entity !is ScriptedEntity)
throw LuaRuntimeException("$entity is not scripted entity") throw LuaRuntimeException("$entity is not scripted entity")

View File

@ -119,12 +119,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
inGame() inGame()
} }
protected open fun onChannelClosed() {
isConnected = false
LOGGER.info("$this is terminated")
scope.cancel("$this is terminated")
}
fun bind(channel: Channel) { fun bind(channel: Channel) {
scope = CoroutineScope(channel.eventLoop().asCoroutineDispatcher() + SupervisorJob() + CoroutineExceptionHandler { coroutineContext, throwable -> scope = CoroutineScope(channel.eventLoop().asCoroutineDispatcher() + SupervisorJob() + CoroutineExceptionHandler { coroutineContext, throwable ->
disconnect("Uncaught exception in one of connection' coroutines: $throwable") disconnect("Uncaught exception in one of connection' coroutines: $throwable")
@ -142,7 +136,11 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
channel.pipeline().addLast(this) channel.pipeline().addLast(this)
channel.closeFuture().addListener { channel.closeFuture().addListener {
onChannelClosed() isConnected = false
LOGGER.info("$channel is closed")
scope.cancel("$channel is closed")
disconnect("Connection closed")
} }
} }

View File

@ -68,9 +68,11 @@ data class ClientConnectPacket(
connection.nickname = connection.server.reserveNickname(playerName, "Player_${connection.connectionID}") connection.nickname = connection.server.reserveNickname(playerName, "Player_${connection.connectionID}")
connection.shipUpgrades = shipUpgrades connection.shipUpgrades = shipUpgrades
connection.shipUpgradesQueue.trySend(shipUpgrades)
connection.uuid = playerUuid connection.uuid = playerUuid
connection.playerSpecies = playerSpecies
connection.receiveShipChunks(shipChunks) connection.loadShipChunks(shipChunks)
connection.send(ConnectSuccessPacket(connection.connectionID, connection.server.serverUUID, connection.server.universe.baseInformation)) connection.send(ConnectSuccessPacket(connection.connectionID, connection.server.serverUUID, connection.server.universe.baseInformation))
connection.send(UniverseTimeUpdatePacket(connection.server.universeClock.time)) connection.send(UniverseTimeUpdatePacket(connection.server.universeClock.time))
connection.channel.flush() connection.channel.flush()

View File

@ -1,9 +1,12 @@
package ru.dbotthepony.kstarbound.server package ru.dbotthepony.kstarbound.server
import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -21,9 +24,12 @@ import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.defs.WarpAlias
import ru.dbotthepony.kstarbound.defs.WarpMode import ru.dbotthepony.kstarbound.defs.WarpMode
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.world.SkyType import ru.dbotthepony.kstarbound.defs.world.SkyType
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.ConnectionSide
@ -35,17 +41,20 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPac
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker
import ru.dbotthepony.kstarbound.server.world.WorldStorage
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld import ru.dbotthepony.kstarbound.server.world.ServerSystemWorld
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.ActionPacer import ru.dbotthepony.kstarbound.util.ActionPacer
import ru.dbotthepony.kstarbound.world.SystemWorld
import ru.dbotthepony.kstarbound.world.SystemWorldLocation import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import java.util.UUID import java.util.UUID
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.stream.Collectors
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.math.min
import kotlin.properties.Delegates import kotlin.properties.Delegates
// serverside part of connection // serverside part of connection
@ -61,6 +70,9 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
// packets which interact with world must be // packets which interact with world must be
// executed on world's thread // executed on world's thread
fun enqueue(task: ServerWorld.(ServerWorldTracker) -> Unit): Boolean { fun enqueue(task: ServerWorld.(ServerWorldTracker) -> Unit): Boolean {
if (isDisconnecting.get())
return false
val isInWorld = tracker?.enqueue(task) != null val isInWorld = tracker?.enqueue(task) != null
if (!isInWorld) { if (!isInWorld) {
@ -73,7 +85,37 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
lateinit var shipWorld: ServerWorld lateinit var shipWorld: ServerWorld
private set private set
var playerSpecies: String by Delegates.notNull()
var uuid: UUID? = null var uuid: UUID? = null
var systemWorldShip: SystemWorld.Ship? = null
private set
val shipUpgradesQueue = Channel<ShipUpgrades>(256)
private suspend fun shipUpgradesLoop() {
while (true) {
var upgrades = shipUpgradesQueue.receive()
val ships = Globals.universeServer.speciesShips[playerSpecies] ?: continue
val oldLevel = shipWorld.getProperty("ship.level", JsonPrimitive(0)).asInt
val newLevel = min(ships.size - 1, upgrades.shipLevel)
if (oldLevel < newLevel) {
for (i in oldLevel + 1 .. newLevel) {
val structure = ships[i].value.await() ?: continue
shipWorld.replaceCentralStructure(structure).join()
upgrades = upgrades.apply(Starbound.gson.fromJson(structure.config["shipUpgrades"] ?: JsonNull.INSTANCE) ?: continue)
}
}
shipWorld.setProperty("ship.level", JsonPrimitive(upgrades.shipLevel))
shipWorld.setProperty("ship.maxFuel", JsonPrimitive(upgrades.maxFuel))
shipWorld.setProperty("ship.crewSize", JsonPrimitive(upgrades.crewSize))
shipWorld.setProperty("ship.fuelEfficiency", JsonPrimitive(upgrades.fuelEfficiency))
systemWorldShip?.speed = upgrades.shipSpeed
shipUpgrades = upgrades
}
}
init { init {
connectionID = server.channels.nextConnectionID() connectionID = server.channels.nextConnectionID()
@ -81,6 +123,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
rpc.add("team.fetchTeamStatus") { rpc.add("team.fetchTeamStatus") {
JsonObject() JsonObject()
} }
rpc.add("ship.applyShipUpgrades") {
// we must consider upgrades sequentially since upgrading the ship will modify ship upgrades data
shipUpgradesQueue.trySend(shipUpgrades.apply(Starbound.gson.fromJson(it) ?: return@add InternedJsonElementAdapter.of(true)))
InternedJsonElementAdapter.of(true)
}
} }
override fun toString(): String { override fun toString(): String {
@ -93,61 +141,26 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
return "$nickname <$connectionID/$uuid>" return "$nickname <$connectionID/$uuid>"
} }
private val shipChunks = HashMap<ByteKey, KOptional<ByteArray>>() private val legacyWorldStorage = LegacyWorldStorage.Memory {
private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>() // TODO: uncomment this once ALL entity types are implemented
var shipChunkSource by Delegates.notNull<WorldStorage>() // sendContextUpdates(it)
private set }
override fun setupLegacy() { override fun setupLegacy() {
super.setupLegacy() super.setupLegacy()
shipChunkSource = LegacyWorldStorage.Memory({ shipChunks[it]?.orNull() }, { key, value -> shipChunks[key] = KOptional(value) })
} }
override fun setupNative() { override fun setupNative() {
super.setupNative() super.setupNative()
shipChunkSource = LegacyWorldStorage.Memory({ shipChunks[it]?.orNull() }, { key, value -> shipChunks[key] = KOptional(value) })
} }
fun receiveShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) { fun loadShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) {
check(shipChunks.isEmpty()) { "Already has ship chunks" } legacyWorldStorage.load(chunks.entries.stream().map { it.key to it.value.orNull() }.filter { it.second != null }.collect(Collectors.toMap({ it.first }, { it.second!! })))
shipChunks.putAll(chunks)
} }
private var remoteVersion = 0L private var remoteVersion = 0L
private var saveClientContextTask: Future<*>? = null private var saveClientContextTask: Future<*>? = null
override fun onChannelClosed() {
tickTask?.cancel(false)
sendUniverseTimeTask?.cancel(false)
playerEntity = null
saveClientContextTask?.cancel(false)
tracker?.remove("Connection channel closed")
tracker = null
saveClientContext()
super.onChannelClosed()
warpQueue.close()
server.channels.freeConnectionID(connectionID)
server.channels.connections.remove(this)
server.freeNickname(nickname)
systemWorld?.removeClient(this)
systemWorld = null
announceDisconnect("Connection to remote host is lost.")
if (::shipWorld.isInitialized) {
shipWorld.eventLoop.shutdown()
}
if (countedTowardsPlayerCount) {
countedTowardsPlayerCount = false
server.channels.decrementPlayerCount()
}
}
private data class WarpRequest(val action: WarpAction, val deploy: Boolean, val ifFailed: WarpAction?) private data class WarpRequest(val action: WarpAction, val deploy: Boolean, val ifFailed: WarpAction?)
private val warpQueue = Channel<WarpRequest>(capacity = 10) private val warpQueue = Channel<WarpRequest>(capacity = 10)
@ -296,6 +309,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
this.systemWorld = world this.systemWorld = world
var ship = world.addClient(this, location = actualInWorldLocation).await() var ship = world.addClient(this, location = actualInWorldLocation).await()
systemWorldShip = ship
shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world)) shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world))
shipCoordinate = UniversePos(world.location) shipCoordinate = UniversePos(world.location)
systemWorldLocation = actualInWorldLocation systemWorldLocation = actualInWorldLocation
@ -375,6 +389,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipCoordinate = UniversePos(world.location) // update ship coordinate after we have successfully travelled to destination shipCoordinate = UniversePos(world.location) // update ship coordinate after we have successfully travelled to destination
this.systemWorld = world this.systemWorld = world
ship = world.addClient(this).await() ship = world.addClient(this).await()
systemWorldShip = ship
val newParams = ship.location.skyParameters(world) val newParams = ship.location.skyParameters(world)
@ -486,67 +501,122 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
warpQueue.trySend(WarpRequest(destination, deploy, ifFailed)) warpQueue.trySend(WarpRequest(destination, deploy, ifFailed))
} }
private var tickTask: Future<*>? = null private var sendContextUpdatesTask: Future<*>? = null
private var sendUniverseTimeTask: Future<*>? = null private var sendUniverseTimeTask: Future<*>? = null
private fun tick() { private fun sendContextUpdates(shipWorldChanges: Map<ByteKey, KOptional<ByteArray>> = mapOf()) {
if (isConnected && isReady) { if (isConnected && isReady) {
val entries = rpc.write() val entries = rpc.write()
if (entries != null || modifiedShipChunks.isNotEmpty() || server2clientGroup.upstream.hasChangedSince(remoteVersion)) { if (entries != null || shipWorldChanges.isNotEmpty() || server2clientGroup.upstream.hasChangedSince(remoteVersion)) {
val (data, version) = server2clientGroup.write(remoteVersion, isLegacy) val (data, version) = server2clientGroup.write(remoteVersion, isLegacy)
remoteVersion = version remoteVersion = version
send(ClientContextUpdatePacket( send(ClientContextUpdatePacket(
entries ?: listOf(), entries ?: listOf(),
KOptional(modifiedShipChunks.associateWith { shipChunks[it]!! }), KOptional(shipWorldChanges),
KOptional(data))) KOptional(data)))
modifiedShipChunks.clear()
} }
} }
} }
// set to true so failed connection attempts don't appear in chat
private var announcedDisconnect = true private var announcedDisconnect = true
private val isDisconnecting = AtomicBoolean()
private fun announceDisconnect(reason: String) { private suspend fun disconnect0(reason: String) {
if (!announcedDisconnect && nickname.isNotBlank()) { // initiate shipworld shutdown
if (reason.isBlank()) { if (::shipWorld.isInitialized) {
server.chat.systemMessage("Player '$nickname' disconnected") shipWorld.eventLoop.shutdown()
} else { }
server.chat.systemMessage("Player '$nickname' disconnected ($reason)")
// stop being counted towards player count, vanish from connection list
server.channels.freeConnectionID(connectionID)
server.channels.connections.remove(this)
server.freeNickname(nickname)
if (countedTowardsPlayerCount) {
countedTowardsPlayerCount = false
server.channels.decrementPlayerCount()
}
// big try-catch block to cleanup mess if we had exception during disconnection
try {
// announce disconnect
if (!announcedDisconnect) {
if (reason.isBlank()) {
server.chat.systemMessage("Player '$nickname' disconnected")
} else {
server.chat.systemMessage("Player '$nickname' disconnected ($reason)")
}
announcedDisconnect = true
} }
announcedDisconnect = true // cancel periodic tasks
sendContextUpdatesTask?.cancel(false)
sendUniverseTimeTask?.cancel(false)
saveClientContextTask?.cancel(false)
// remove from world before saving client context
tracker?.remove("Disconnect: $reason")
tracker = null
// write server state
saveClientContext()
// remove from system world
systemWorld?.removeClient(this)
systemWorld = null
playerEntity = null
// stop coroutines
scope.cancel(CancellationException("Client disconnect: $reason"))
if (channel.isOpen && ::shipWorld.isInitialized) {
// if channel is still open, send one last update packet
shipWorld.eventLoop.shutdown()
while (!shipWorld.eventLoop.isTerminated) {
delay(10L)
}
legacyWorldStorage.commit()
legacyWorldStorage.waitAsync()
// send pending updates
sendContextUpdates()
channel.flush()
}
isReady = false
if (channel.isOpen) {
// say goodbye, if channel is still open
channel.write(ServerDisconnectPacket(reason))
channel.flush()
channel.close()
}
} catch (err: Throwable) {
LOGGER.error("Exception while disconnecting $this", err)
} finally {
// don't leave residue entities in worlds if we encountered an exception
tracker?.remove("Disconnect: $reason")
tracker = null
systemWorld?.removeClient(this)
systemWorld = null
} }
} }
override fun disconnect(reason: String) { override fun disconnect(reason: String) {
LOGGER.info("${alias()} disconnect initiated with reason $reason") if (isDisconnecting.compareAndSet(false, true)) {
announceDisconnect(reason) server.scope.launch { disconnect0(reason) }
if (channel.isOpen) {
// send pending updates
tick()
channel.flush()
}
isReady = false
tracker?.remove("Disconnect")
tracker = null
saveClientContext()
if (channel.isOpen) {
// say goodbye
channel.write(ServerDisconnectPacket(reason))
channel.flush()
channel.close()
} }
} }
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (!channel.isOpen) if (!channel.isOpen || isDisconnecting.get())
return return
if (msg is IServerPacket) { if (msg is IServerPacket) {
@ -587,10 +657,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
null null
} }
shipUpgrades = shipUpgrades.addCapability("planetTravel")
shipUpgrades = shipUpgrades.addCapability("teleport")
shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 3)
scope.launch { warpEventLoop() } scope.launch { warpEventLoop() }
if (context == null) { if (context == null) {
@ -620,8 +686,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
countedTowardsPlayerCount = true countedTowardsPlayerCount = true
server.channels.incrementPlayerCount() server.channels.incrementPlayerCount()
tickTask = channel.eventLoop().scheduleWithFixedDelay(Runnable { sendContextUpdatesTask = channel.eventLoop().scheduleWithFixedDelay(Runnable {
tick() sendContextUpdates()
}, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS) }, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
sendUniverseTimeTask = channel.eventLoop().scheduleWithFixedDelay(Runnable { sendUniverseTimeTask = channel.eventLoop().scheduleWithFixedDelay(Runnable {
@ -629,10 +695,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
send(UniverseTimeUpdatePacket(server.universeClock.time), false) send(UniverseTimeUpdatePacket(server.universeClock.time), false)
}, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS) }, Globals.universeServer.clockUpdatePacketInterval, Globals.universeServer.clockUpdatePacketInterval, TimeUnit.MILLISECONDS)
if (isLegacy) { scope.launch { celestialRequestsHandler() }
scope.launch { celestialRequestsHandler() }
server.loadShipWorld(this, shipChunkSource).thenAccept { if (isLegacy) {
server.loadShipWorld(this, legacyWorldStorage).thenAccept {
if (!isConnected || !channel.isOpen) { if (!isConnected || !channel.isOpen) {
LOGGER.warn("$this disconnected before loaded their ShipWorld") LOGGER.warn("$this disconnected before loaded their ShipWorld")
it.eventLoop.shutdown() it.eventLoop.shutdown()
@ -640,7 +706,11 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipWorld = it shipWorld = it
shipWorld.sky.referenceClock = server.universeClock shipWorld.sky.referenceClock = server.universeClock
// shipWorld.sky.startFlying(true, true) // shipWorld.sky.startFlying(true, true)
shipWorld.eventLoop.start()
if (!shipWorld.eventLoop.isAlive)
shipWorld.eventLoop.start()
scope.launch { shipUpgradesLoop() }
scope.launch { loadDataAndDispatchEventLoops() } scope.launch { loadDataAndDispatchEventLoops() }
} }
}.exceptionally { }.exceptionally {

View File

@ -22,12 +22,14 @@ import ru.dbotthepony.kstarbound.defs.world.AsteroidsWorldParameters
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonElementInflated import ru.dbotthepony.kstarbound.json.readJsonElementInflated
import ru.dbotthepony.kstarbound.json.readJsonObject import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElementDeflated import ru.dbotthepony.kstarbound.json.writeJsonElementDeflated
import ru.dbotthepony.kstarbound.json.writeJsonObject import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage import ru.dbotthepony.kstarbound.server.world.NativeLocalWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerUniverse import ru.dbotthepony.kstarbound.server.world.ServerUniverse
@ -39,6 +41,7 @@ import ru.dbotthepony.kstarbound.util.JVMClock
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.toStarboundString import ru.dbotthepony.kstarbound.util.toStarboundString
import ru.dbotthepony.kstarbound.util.uuidFromStarboundString import ru.dbotthepony.kstarbound.util.uuidFromStarboundString
import ru.dbotthepony.kstarbound.world.WorldGeometry
import java.io.File import java.io.File
import java.sql.DriverManager import java.sql.DriverManager
import java.util.Collections import java.util.Collections
@ -419,17 +422,48 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
} }
fun loadShipWorld(connection: ServerConnection, storage: WorldStorage): CompletableFuture<ServerWorld> { fun loadShipWorld(connection: ServerConnection, storage: WorldStorage): CompletableFuture<ServerWorld> {
return supplyAsync { return scope.async {
val id = WorldID.ShipWorld(connection.uuid ?: throw NullPointerException("Connection UUID is null")) val id = WorldID.ShipWorld(connection.uuid ?: throw NullPointerException("Connection UUID is null"))
val existing = worlds[id] val existing = worlds[id]
if (existing != null) if (existing != null)
throw IllegalStateException("Already has $id!") throw IllegalStateException("Already has $id!")
val world = ServerWorld.load(this, storage, id) try {
worlds[id] = world val world = ServerWorld.load(this@StarboundServer, storage, id)
world worlds[id] = world
}.thenCompose { it } return@async world.await()
} catch (err: ServerWorld.WorldMetadataMissingException) {
LOGGER.info("Creating new client shipworld for $connection")
val world = ServerWorld.create(this@StarboundServer, WorldGeometry(Vector2i(2048, 2048)), storage, id)
try {
val structure = Globals.universeServer.speciesShips[connection.playerSpecies]?.firstOrNull()?.value?.get() ?: throw NoSuchElementException("No ship structure for species ${connection.playerSpecies}")
world.eventLoop.start()
world.replaceCentralStructure(structure).join()
val currentUpgrades = connection.shipUpgrades
.apply(Globals.shipUpgrades)
.apply(Starbound.gson.fromJson(structure.config.get("shipUpgrades") ?: throw NoSuchElementException("No shipUpgrades element in world structure config for species ${connection.playerSpecies}")) ?: throw NullPointerException("World structure config.shipUpgrades is null for species ${connection.playerSpecies}"))
connection.shipUpgrades = currentUpgrades
world.setProperty("invinciblePlayers", JsonPrimitive(true))
world.setProperty("ship.level", JsonPrimitive(0))
world.setProperty("ship.fuel", JsonPrimitive(0))
world.setProperty("ship.maxFuel", JsonPrimitive(currentUpgrades.maxFuel))
world.setProperty("ship.crewSize", JsonPrimitive(currentUpgrades.crewSize))
world.setProperty("ship.fuelEfficiency", JsonPrimitive(currentUpgrades.fuelEfficiency))
world.saveMetadata()
} catch (err: Throwable) {
world.eventLoop.shutdown()
throw err
}
worlds[id] = CompletableFuture.completedFuture(world)
return@async world
}
}.asCompletableFuture()
} }
fun notifyWorldUnloaded(worldID: WorldID) { fun notifyWorldUnloaded(worldID: WorldID) {
@ -453,7 +487,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
isDaemon = false isDaemon = false
} }
private val occupiedNicknames = ObjectArraySet<String>() private val occupiedNicknames = ObjectOpenHashSet<String>()
fun reserveNickname(name: String, alternative: String): String { fun reserveNickname(name: String, alternative: String): String {
synchronized(occupiedNicknames) { synchronized(occupiedNicknames) {

View File

@ -5,6 +5,7 @@ import com.github.benmanes.caffeine.cache.Caffeine
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
@ -19,6 +20,7 @@ import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.xxhash32 import ru.dbotthepony.kommons.util.xxhash32
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.VersionRegistry import ru.dbotthepony.kstarbound.VersionRegistry
@ -31,6 +33,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.util.CarriedExecutor import ru.dbotthepony.kstarbound.util.CarriedExecutor
import ru.dbotthepony.kstarbound.util.ScheduledCoroutineExecutor import ru.dbotthepony.kstarbound.util.ScheduledCoroutineExecutor
import ru.dbotthepony.kstarbound.util.supplyAsync
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.ChunkState
@ -47,9 +50,7 @@ import java.io.DataOutputStream
import java.io.File import java.io.File
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
import java.sql.DriverManager import java.sql.DriverManager
import java.time.Duration
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.function.Function import java.util.function.Function
import java.util.function.Supplier import java.util.function.Supplier
@ -61,7 +62,7 @@ import java.util.zip.InflaterInputStream
sealed class LegacyWorldStorage() : WorldStorage() { sealed class LegacyWorldStorage() : WorldStorage() {
protected abstract fun load(at: ByteKey): CompletableFuture<ByteArray?> protected abstract fun load(at: ByteKey): CompletableFuture<ByteArray?>
protected abstract fun write(at: ByteKey, value: ByteArray) protected abstract fun write(at: ByteKey, value: ByteArray)
protected abstract val executor: Executor protected val executor = CarriedExecutor(Starbound.IO_EXECUTOR)
protected val scope by lazy { protected val scope by lazy {
CoroutineScope(ScheduledCoroutineExecutor(executor) + SupervisorJob()) CoroutineScope(ScheduledCoroutineExecutor(executor) + SupervisorJob())
@ -357,30 +358,51 @@ sealed class LegacyWorldStorage() : WorldStorage() {
write(metadataKey, buff.array.copyOf(buff.length)) write(metadataKey, buff.array.copyOf(buff.length))
} }
class Memory(private val get: (ByteKey) -> ByteArray?, private val set: (ByteKey, ByteArray) -> Unit) : LegacyWorldStorage() { class Memory(private val listener: (changes: Map<ByteKey, KOptional<ByteArray>>) -> Unit) : LegacyWorldStorage() {
private val pending = HashMap<ByteKey, ByteArray>() private val changed = ObjectOpenHashSet<ByteKey>()
override val executor: Executor = Executor { it.run() } private val memory = HashMap<ByteKey, ByteArray>()
fun load(memory: Map<ByteKey, ByteArray>) {
executor.execute {
changed.clear()
this.memory.clear()
this.memory.putAll(memory)
}
}
override fun load(at: ByteKey): CompletableFuture<ByteArray?> { override fun load(at: ByteKey): CompletableFuture<ByteArray?> {
return CompletableFuture.completedFuture(pending[at] ?: get(at)) return executor.supplyAsync { memory[at] }
} }
override fun write(at: ByteKey, value: ByteArray) { override fun write(at: ByteKey, value: ByteArray) {
// set(at, value) executor.execute {
pending[at] = value if (at !in memory || !memory[at].contentEquals(value)) {
memory[at] = value
changed.add(at)
}
}
} }
override fun close() {} override fun close() {
executor.execute { commit() }
executor.wait(300L, TimeUnit.SECONDS)
}
suspend fun waitAsync() {
executor.execute { commit() }
executor.waitAsync()
}
override fun commit() { override fun commit() {
pending.entries.forEach { (k, v) -> set(k, v) } executor.execute {
pending.clear() val result = changed.associateWith { KOptional.ofNullable(memory[it]) }
changed.clear()
listener(result)
}
} }
} }
class DB5(private val database: BTreeDB5) : LegacyWorldStorage() { class DB5(private val database: BTreeDB5) : LegacyWorldStorage() {
override val executor = CarriedExecutor(Starbound.IO_EXECUTOR)
override fun load(at: ByteKey): CompletableFuture<ByteArray?> { override fun load(at: ByteKey): CompletableFuture<ByteArray?> {
return CompletableFuture.supplyAsync(Supplier { database.read(at).orNull() }, executor) return CompletableFuture.supplyAsync(Supplier { database.read(at).orNull() }, executor)
} }
@ -400,7 +422,6 @@ sealed class LegacyWorldStorage() : WorldStorage() {
} }
class SQL(path: File) : LegacyWorldStorage() { class SQL(path: File) : LegacyWorldStorage() {
override val executor = CarriedExecutor(Starbound.IO_EXECUTOR)
private val connection = DriverManager.getConnection("jdbc:sqlite:${path.canonicalPath.replace('\\', '/')}") private val connection = DriverManager.getConnection("jdbc:sqlite:${path.canonicalPath.replace('\\', '/')}")
private val cleaner: Cleaner.Cleanable private val cleaner: Cleaner.Cleanable
@ -464,10 +485,5 @@ sealed class LegacyWorldStorage() : WorldStorage() {
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val metadataKey = ByteKey(0, 0, 0, 0, 0) private val metadataKey = ByteKey(0, 0, 0, 0, 0)
fun memory(): Memory {
val map = HashMap<ByteKey, ByteArray>()
return Memory(map::get, map::set)
}
} }
} }

View File

@ -12,6 +12,7 @@ import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.DESTROYED_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.DESTROYED_DUNGEON_ID
@ -557,7 +558,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
world.storage.saveEntities(pos, unloadable.filter { it.isPersistent }) world.storage.saveEntities(pos, unloadable.filter { it.isPersistent })
if (!isUnloadingWorld) if (!isUnloadingWorld && world.worldID !is WorldID.ShipWorld)
world.storage.commit() world.storage.commit()
} }

View File

@ -4,16 +4,21 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
@ -25,14 +30,18 @@ import ru.dbotthepony.kstarbound.VersionRegistry
import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.defs.WarpAlias
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceablesDefinition import ru.dbotthepony.kstarbound.defs.world.BiomePlaceablesDefinition
import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.jsonArrayOf import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.Connection
@ -67,10 +76,12 @@ import java.util.PriorityQueue
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.math.max
class ServerWorld private constructor( class ServerWorld private constructor(
val server: StarboundServer, val server: StarboundServer,
@ -142,14 +153,28 @@ class ServerWorld private constructor(
playerStart = playerSpawnPosition, playerStart = playerSpawnPosition,
respawnInWorld = respawnInWorld, respawnInWorld = respawnInWorld,
adjustPlayerStart = adjustPlayerSpawn, adjustPlayerStart = adjustPlayerSpawn,
worldTemplate = if (storage is LegacyWorldStorage) Starbound.legacyJson { template.toJson() } else template.toJson(), worldTemplate = if (storage is LegacyWorldStorage) Starbound.legacyStoreJson { template.toJson() } else template.toJson(),
centralStructure = centralStructure, centralStructure = centralStructure,
protectedDungeonIds = ImmutableSet.copyOf(protectedDungeonIDsInternal), protectedDungeonIds = ImmutableSet.copyOf(protectedDungeonIDsInternal),
worldProperties = copyProperties(), worldProperties = copyProperties(),
spawningEnabled = true spawningEnabled = true,
dungeonIdBreathable = dungeonBreathableInternal.entries.stream().map { it.key to it.value }.collect(ImmutableList.toImmutableList()),
dungeonIdGravity = if (storage is LegacyWorldStorage) {
dungeonGravityInternal.entries.stream().map { it.key to Either.left<Double, Vector2d>(it.value.y) }.collect(ImmutableList.toImmutableList())
} else {
dungeonGravityInternal.entries.stream().map { it.key to Either.right<Double, Vector2d>(it.value) }.collect(ImmutableList.toImmutableList())
}
) )
storage.saveMetadata(WorldStorage.Metadata(geometry, VersionRegistry.make("WorldMetadata", Starbound.gson.toJsonTree(metadata)))) if (storage is LegacyWorldStorage) {
Starbound.legacyStoreJson {
storage.saveMetadata(WorldStorage.Metadata(geometry, VersionRegistry.make("WorldMetadata", Starbound.gson.toJsonTree(metadata))))
}
} else {
Starbound.storeJson {
storage.saveMetadata(WorldStorage.Metadata(geometry, VersionRegistry.make("WorldMetadata", Starbound.gson.toJsonTree(metadata))))
}
}
} }
private var uncleanShutdown = false private var uncleanShutdown = false
@ -199,6 +224,14 @@ class ServerWorld private constructor(
eventLoop.scheduleAtFixedRate(Runnable { eventLoop.scheduleAtFixedRate(Runnable {
tick(Starbound.TIMESTEP) tick(Starbound.TIMESTEP)
}, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS) }, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
if (worldID is WorldID.ShipWorld) {
// due to dumb logic on legacy client side, committing frequently causes very noticeable performance degradation
eventLoop.scheduleWithFixedDelay(Runnable {
if (!uncleanShutdown)
storage.commit()
}, 20L, 20L, TimeUnit.SECONDS)
}
} }
private var placementTaskID = 0L private var placementTaskID = 0L
@ -289,6 +322,117 @@ class ServerWorld private constructor(
override val connectionID: Int override val connectionID: Int
get() = 0 get() = 0
private suspend fun replaceCentralStructure0(structure: WorldStructure) {
val tickets = ArrayList<ServerChunk.ITicket>()
try {
var region = AABB
.ofPoints(centralStructure.objects.map { it.position })
.combine(AABB.ofPoints(centralStructure.foregroundBlocks.map { it.position }))
.combine(AABB.ofPoints(centralStructure.backgroundBlocks.map { it.position }))
.enlarge(4.0, 4.0)
run {
val getTickets = permanentChunkTicket(region, ChunkState.FULL).await()
tickets.addAll(getTickets)
getTickets.forEach { it.chunk.await() }
}
// remove old structure
for (obj in centralStructure.objects) {
if (!obj.residual) {
val entities = entityIndex.tileEntitiesAt(obj.position)
for (entity in entities) {
if (entity is WorldObject && entity.config == obj.name.entry && entity.tilePosition == obj.position) {
entity.remove(AbstractEntity.RemovalReason.REMOVED)
}
}
}
}
for (block in centralStructure.backgroundBlocks) {
if (!block.residual) {
val cell = getCell(block.position).mutable()
if (cell.background.material.ref == block.materialId.native) {
cell.background.empty()
check(setCell(block.position, cell))
}
}
}
for (block in centralStructure.foregroundBlocks) {
if (!block.residual) {
val cell = getCell(block.position).mutable()
if (cell.foreground.material.ref == block.materialId.native) {
cell.foreground.empty()
check(setCell(block.position, cell))
}
}
}
// put new structure
centralStructure = structure.resolve(geometry.size / 2)
region = AABB
.ofPoints(centralStructure.objects.map { it.position })
.combine(AABB.ofPoints(centralStructure.foregroundBlocks.map { it.position }))
.combine(AABB.ofPoints(centralStructure.backgroundBlocks.map { it.position }))
.enlarge(4.0, 4.0)
run {
val getTickets = permanentChunkTicket(region, ChunkState.FULL).await()
tickets.addAll(getTickets)
getTickets.forEach { it.chunk.await() }
}
playerSpawnPosition = centralStructure.flaggedBlocks["playerSpawn"]?.firstOrNull()?.toDoubleVector() ?: (geometry.size.toDoubleVector() / 2.0)
for (block in centralStructure.backgroundBlocks) {
val cell = getCell(block.position).mutable()
if (cell.background.material.isEmptyTile) {
cell.background.material = block.materialId.native.entry ?: BuiltinMetaMaterials.EMPTY
check(setCell(block.position, cell))
}
}
for (block in centralStructure.foregroundBlocks) {
val cell = getCell(block.position).mutable()
if (cell.foreground.material.isEmptyTile) {
cell.foreground.material = block.materialId.native.entry ?: BuiltinMetaMaterials.EMPTY
check(setCell(block.position, cell))
}
}
for (obj in centralStructure.objects) {
val config = obj.name.entry ?: continue
val orientation = config.value.findValidOrientation(this, obj.position, obj.direction, true)
if (orientation == -1) {
if (obj.residual) {
LOGGER.debug("Tried to put residual object '{}' at {} for central structure, but it can't be placed there!", config.key, obj.position)
} else {
LOGGER.error("Tried to put object '${config.key}' at ${obj.position} for central structure, but it can't be placed there!")
}
} else {
val create = WorldObject.create(config, obj.position, obj.parameters.deepCopy()) ?: continue
create.orientationIndex = orientation.toLong()
create.joinWorld(this)
}
}
} finally {
tickets.forEach { it.cancel() }
}
}
fun replaceCentralStructure(structure: WorldStructure): Job {
return eventLoop.scope.launch { replaceCentralStructure0(structure) }
}
override fun switchDungeonIDProtection(id: Int, enable: Boolean): Boolean { override fun switchDungeonIDProtection(id: Int, enable: Boolean): Boolean {
val updated = super.switchDungeonIDProtection(id, enable) val updated = super.switchDungeonIDProtection(id, enable)
@ -717,8 +861,14 @@ class ServerWorld private constructor(
} }
} }
private var scheduledMetadataSave: ScheduledFuture<*>? = null
override fun setProperty0(key: String, value: JsonElement) { override fun setProperty0(key: String, value: JsonElement) {
broadcast(UpdateWorldPropertiesPacket(JsonObject().apply { add(key, value) })) broadcast(UpdateWorldPropertiesPacket(JsonObject().apply { add(key, value) }))
if (scheduledMetadataSave == null || scheduledMetadataSave!!.isDone) {
scheduledMetadataSave = eventLoop.schedule(Runnable { saveMetadata() }, 10L, TimeUnit.SECONDS)
}
} }
override fun chunkFactory(pos: ChunkPos): ServerChunk { override fun chunkFactory(pos: ChunkPos): ServerChunk {
@ -831,9 +981,13 @@ class ServerWorld private constructor(
val centralStructure: WorldStructure, val centralStructure: WorldStructure,
val protectedDungeonIds: ImmutableSet<Int>, val protectedDungeonIds: ImmutableSet<Int>,
val worldProperties: JsonObject, val worldProperties: JsonObject,
val spawningEnabled: Boolean val spawningEnabled: Boolean,
val dungeonIdGravity: ImmutableList<Pair<Int, Either<Double, Vector2d>>> = ImmutableList.of(),
val dungeonIdBreathable: ImmutableList<Pair<Int, Boolean>> = ImmutableList.of(),
) )
class WorldMetadataMissingException : NoSuchElementException("No world metadata is present")
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
@ -868,7 +1022,8 @@ class ServerWorld private constructor(
LOGGER.info("Attempting to load world at $worldID") LOGGER.info("Attempting to load world at $worldID")
return storage.loadMetadata().thenApply { return storage.loadMetadata().thenApply {
it ?: throw NoSuchElementException("No world metadata is present") it ?: throw WorldMetadataMissingException()
LOGGER.info("Loading world at $worldID") LOGGER.info("Loading world at $worldID")
AssetPathStack("/") { _ -> AssetPathStack("/") { _ ->
val meta = Starbound.gson.fromJson(it.data.content, MetadataJson::class.java) val meta = Starbound.gson.fromJson(it.data.content, MetadataJson::class.java)

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.util package ru.dbotthepony.kstarbound.util
import kotlinx.coroutines.delay
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import java.lang.ref.Reference import java.lang.ref.Reference
import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.ConcurrentLinkedDeque
@ -85,6 +86,12 @@ class CarriedExecutor(private val parent: Executor, private val allowExecutionIn
} }
} }
suspend fun waitAsync() {
while (isCarried.get()) {
delay(10L)
}
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
} }

View File

@ -80,10 +80,18 @@ class RelativeClock() : IClock {
) )
fun toJson(): JsonElement { fun toJson(): JsonElement {
return Starbound.gson.toJsonTree(JsonData(time, if (pointOfReferenceSet) pointOfReference else null)) return JsonObject().also {
it["elapsedTime"] = time
it["lastEpochTime"] = if (pointOfReferenceSet) JsonPrimitive(pointOfReference) else JsonNull.INSTANCE
}
} }
fun fromJson(json: JsonElement) { fun fromJson(json: JsonElement) {
if (json.isJsonNull) {
pointOfReferenceSet = false
return
}
val data = Starbound.gson.fromJson(json, JsonData::class.java) val data = Starbound.gson.fromJson(json, JsonData::class.java)
time = data.elapsedTime time = data.elapsedTime

View File

@ -306,7 +306,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
var centralStructure: WorldStructure = WorldStructure() open var centralStructure: WorldStructure = WorldStructure()
protected set
protected val protectedDungeonIDsInternal = IntOpenHashSet() protected val protectedDungeonIDsInternal = IntOpenHashSet()
protected val dungeonGravityInternal = Int2ObjectOpenHashMap<Vector2d>() protected val dungeonGravityInternal = Int2ObjectOpenHashMap<Vector2d>()

View File

@ -132,6 +132,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
craftingProgress = data.get("craftingProgress", 0.0) craftingProgress = data.get("craftingProgress", 0.0)
isInitialized = data.get("initialized", true) isInitialized = data.get("initialized", true)
items.fromJson(data.get("items", JsonArray()), resize = true) items.fromJson(data.get("items", JsonArray()), resize = true)
ageItemsTimer.fromJson(data.get("ageItemsTimer") ?: JsonNull.INSTANCE)
} }
override fun serialize(data: JsonObject) { override fun serialize(data: JsonObject) {
@ -144,6 +145,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
data["craftingProgress"] = craftingProgress data["craftingProgress"] = craftingProgress
data["initialized"] = isInitialized data["initialized"] = isInitialized
data["items"] = items.toJson(true) data["items"] = items.toJson(true)
data["ageItemsTimer"] = ageItemsTimer.toJson()
} }
private fun randomizeContents(random: RandomGenerator, threatLevel: Double) { private fun randomizeContents(random: RandomGenerator, threatLevel: Double) {
@ -197,7 +199,6 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
if (!isRemote) { if (!isRemote) {
if (isInitialized) return if (isInitialized) return
isInitialized = true
val seed = lookupProperty("treasureSeed") val seed = lookupProperty("treasureSeed")

View File

@ -14,6 +14,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString import org.classdump.luna.ByteString
import org.classdump.luna.Table import org.classdump.luna.Table
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
@ -47,6 +48,7 @@ import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.io.Vector2iCodec import ru.dbotthepony.kstarbound.io.Vector2iCodec
import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
@ -110,6 +112,28 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
if ("uniqueId" in data) if ("uniqueId" in data)
uniqueID.accept(data["uniqueId"]?.asStringOrNull) uniqueID.accept(data["uniqueId"]?.asStringOrNull)
if ("inputWireNodes" in data) {
val inputWireNodes = data.getAsJsonArray("inputWireNodes")
for ((i, value) in inputWireNodes.withIndex()) {
if (i >= inputNodes.size)
break
inputNodes[i].deserialize(value)
}
}
if ("outputWireNodes" in data) {
val outputWireNodes = data.getAsJsonArray("outputWireNodes")
for ((i, value) in outputWireNodes.withIndex()) {
if (i >= outputNodes.size)
break
outputNodes[i].deserialize(value)
}
}
} }
open fun loadParameters(parameters: JsonObject) { open fun loadParameters(parameters: JsonObject) {
@ -129,6 +153,9 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
data["orientationIndex"] = orientationIndex data["orientationIndex"] = orientationIndex
data["interactive"] = isInteractive data["interactive"] = isInteractive
data["inputWireNodes"] = inputNodes.stream().map { it.serialize() }.collect(JsonArrayCollector)
data["outputWireNodes"] = outputNodes.stream().map { it.serialize() }.collect(JsonArrayCollector)
val scriptStorage = lua.globals["storage"] val scriptStorage = lua.globals["storage"]
if (scriptStorage != null && scriptStorage is Table) { if (scriptStorage != null && scriptStorage is Table) {
@ -159,6 +186,12 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
} }
init {
if (config.value.uniqueId != null) {
uniqueID.accept(config.value.uniqueId)
}
}
protected val orientationLazies = ArrayList<ManualLazy<*>>() protected val orientationLazies = ArrayList<ManualLazy<*>>()
protected val parametersLazies = ArrayList<ManualLazy<*>>() protected val parametersLazies = ArrayList<ManualLazy<*>>()
protected val spacesLazies = ArrayList<ManualLazy<*>>() protected val spacesLazies = ArrayList<ManualLazy<*>>()
@ -304,6 +337,31 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
lua.invokeGlobal("onNodeConnectionChange") lua.invokeGlobal("onNodeConnectionChange")
} }
} }
fun serialize(): JsonObject {
return JsonObject().also {
it["connections"] = connectionsInternal.stream().map { jsonArrayOf(it.entityLocation, it.index) }.collect(JsonArrayCollector)
it["state"] = state
}
}
fun deserialize(value: JsonElement) {
connectionsInternal.clear()
state = false
if (value is JsonObject) {
state = value.get("state", false)
val connections = value.get("connections", JsonArray())
for (obj in connections) {
if (obj is JsonArray) {
connectionsInternal.add(WireConnection(vectors.fromJsonTree(obj[0]), obj[1].asInt))
}
}
}
}
} }
val inputNodes: ImmutableList<WireNode> = lookupProperty("inputNodes") { JsonArray() } val inputNodes: ImmutableList<WireNode> = lookupProperty("inputNodes") { JsonArray() }
@ -813,6 +871,10 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
isInteractive = !interactAction.isJsonNull isInteractive = !interactAction.isJsonNull
} }
override fun toString(): String {
return "WorldObject[${config.key}, at $tilePosition]"
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val lightColorPath = JsonPath("lightColor") private val lightColorPath = JsonPath("lightColor")