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.
## .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
* 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}`
@ -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 `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
* `min` terrain selector added, 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
* Added `min` terrain selector, opposite of existing `max` (json format is the same as `max`)
* 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)
* `ridgeblocks` terrain selector now accepts `amplitude` and `frequency` values (naming inconsistency fix);
* `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
kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.16.1
kommonsVersion=2.17.0
ffiVersion=2.2.13
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.WorldServerConfig
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.ItemGlobalConfig
import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
@ -114,6 +115,9 @@ object Globals {
var itemParameters by Delegates.notNull<ItemGlobalConfig>()
private set
var shipUpgrades by Delegates.notNull<ShipUpgrades>()
private set
private var profanityFilterInternal by Delegates.notNull<ImmutableList<String>>()
val profanityFilter: ImmutableSet<String> by lazy {
@ -219,6 +223,7 @@ object Globals {
tasks.add(load("/plants/treeDamage.config", ::treeDamage))
tasks.add(load("/plants/bushDamage.config", ::bushDamage))
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("/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 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)
setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
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) {
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.TileDamage
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.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
@ -72,6 +73,12 @@ class ClientWorld(
override val connectionID: Int
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 renderRegionHeight = determineChunkSize(geometry.size.y)
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 ru.dbotthepony.kommons.gson.consumeNull
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.sbIntern
import java.lang.reflect.ParameterizedType
@ -59,7 +61,7 @@ class AssetReference<V> {
companion object : TypeAdapterFactory {
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>
@ -117,8 +119,8 @@ class AssetReference<V> {
return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second)
} else {
val json = Starbound.ELEMENTS_ADAPTER.read(`in`)
val value = adapter.read(JsonTreeReader(json)) ?: return null
val json = `in`.popJsonElement()
val value = adapter.read(FastJsonTreeReader(json)) ?: return null
return AssetReference(null, null, value, json)
}
}

View File

@ -1,9 +1,11 @@
package ru.dbotthepony.kstarbound.defs
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.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.util.function.Predicate
@ -16,6 +18,7 @@ data class UniverseServerConfig(
val queuedFlightWaitTime: Double = 0.0,
val useNewWireProcessing: Boolean = true,
val speciesShips: ImmutableMap<String, ImmutableList<AssetReference<WorldStructure>>>,
) {
@JsonFactory
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
// flip image as we go
tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or
bytes[offset + 1].toInt().and(0xFF).shl(8) or
bytes[offset + 2].toInt().and(0xFF).shl(16) or
bytes[offset + 3].toInt().and(0xFF).shl(24)
tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or // red
bytes[offset + 1].toInt().and(0xFF).shl(8) or // green
bytes[offset + 2].toInt().and(0xFF).shl(16) or // blue
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 nonEmptyRegion get() = whole.nonEmptyRegion
override fun toString(): String {
return "Image[$source of $width, $height]"
}
/**
* 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 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,
* otherwise returns pixels as-is
* returns integer in little-endian RGBA/big-endian ABGR format
*/
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" }
@ -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 {
if (flip) {

View File

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

View File

@ -91,6 +91,7 @@ data class ObjectDefinition(
val damageConfig: CompletableFuture<TileDamageParameters>,
val flickerPeriod: PeriodicFunction? = null,
val orientations: ImmutableList<Supplier<ObjectOrientation>>,
val uniqueId: String? = null,
) {
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
@ -159,6 +160,7 @@ data class ObjectDefinition(
val health: Double = 1.0,
val rooting: Boolean = false,
val biomePlaced: Boolean = false,
val uniqueId: String? = null,
)
private val basic = gson.getAdapter(PlainData::class.java)
@ -283,6 +285,7 @@ data class ObjectDefinition(
damageConfig = damageConfig,
flickerPeriod = flickerPeriod,
orientations = orientations.build(),
uniqueId = basic.uniqueId,
)
}
}

View File

@ -66,15 +66,17 @@ data class ObjectOrientation(
if (occupySpaces.isEmpty())
return true
var valid = occupySpaces.all {
val cell = world.chunkMap.getCell(it + position)
val localSpaces = occupySpaces.map { 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.dungeonId in world.protectedDungeonIDs) println("position is protected: ${it + position}")
cell.foreground.material.isEmptyTile && (ignoreProtectedDungeons || !world.isDungeonIDProtected(cell.dungeonId))
}
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
@ -189,7 +191,7 @@ data class ObjectOrientation(
var occupySpaces = obj["spaces"]?.let { spaces.fromJsonTree(it) } ?: ImmutableSet.of(Vector2i.ZERO)
if ("spaceScan" in obj) {
occupySpaces = ImmutableSet.of()
// occupySpaces = ImmutableSet.of()
for (drawable in drawables) {
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)) }
"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)) }
"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")
}
}

View File

@ -2,47 +2,175 @@ package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement
import com.google.gson.JsonNull
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.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.Vector2i
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
import ru.dbotthepony.kstarbound.world.Direction
@JsonFactory
data class WorldStructure(
val region: AABBi = AABBi(Vector2i.ZERO, Vector2i.ZERO),
val anchorPosition: Vector2i = Vector2i.ZERO,
var config: JsonElement = JsonObject(),
val config: JsonObject = JsonObject(),
val backgroundOverlays: 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 foregroundBlocks: ImmutableList<Block> = 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 {
if (config == JsonNull.INSTANCE) {
// so it is not omitted
config = JsonObject()
}
}
@JsonFactory
data class Overlay(
@JsonAlias("position")
val min: Vector2d,
val image: AssetPath,
val fullbright: Boolean = false)
@JsonFactory
data class Overlay(val min: Vector2d, val image: String, val fullbright: Boolean)
@JsonFactory
data class Block(val position: Vector2i, val materialId: Either<Int, String>, val residual: Boolean)
data class Block(val position: Vector2i, val materialId: NativeLegacy.Tile, val residual: Boolean)
@JsonFactory
data class Obj(
val position: Vector2i,
val name: String,
val direction: Direction,
val parameters: JsonElement,
val position: Vector2i = Vector2i.ZERO,
val name: Registry.Ref<ObjectDefinition>,
val direction: Direction = Direction.LEFT,
val parameters: JsonObject = JsonObject(),
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()
for (type in types) {
if (type.isIgnored)
continue
if (type.isFlat) {
check(!asJsonArray)
@ -183,12 +186,17 @@ class FactoryAdapter<T : Any> private constructor(
}
} else {
val (field, adapter) = type
val getValue = field.get(value)
// god fucking damn it
if (!asJsonArray && getValue === null)
continue
if (!asJsonArray)
out.name(field.name)
@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>) {
context.returnBuffer.setTo()
try {
callable.invoke(context, ArgumentIterator.of(context, name, args))
} catch (err: ClassCastException) {
@ -252,6 +254,8 @@ fun luaFunctionNS(name: String, callable: ExecutionContext.(ArgumentIterator) ->
}
override fun invoke(context: ExecutionContext, args: Array<out Any>) {
context.returnBuffer.setTo()
try {
callable.invoke(context, ArgumentIterator.of(context, name, args)).run(context)
} 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?>) {
context.returnBuffer.setTo()
try {
callable.invoke(context, args)
} catch (err: ClassCastException) {
@ -288,6 +294,8 @@ fun luaFunction(callable: ExecutionContext.() -> Unit): LuaFunction<*, *, *, *,
}
override fun invoke(context: ExecutionContext) {
context.returnBuffer.setTo()
try {
callable.invoke(context)
} catch (err: ClassCastException) {
@ -306,6 +314,8 @@ fun <T> luaFunction(callable: ExecutionContext.(T) -> Unit): LuaFunction<T, *, *
}
override fun invoke(context: ExecutionContext, arg1: T) {
context.returnBuffer.setTo()
try {
callable.invoke(context, arg1)
} 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) {
context.returnBuffer.setTo()
try {
callable.invoke(context, arg1, arg2)
} 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) {
context.returnBuffer.setTo()
try {
callable.invoke(context, arg1, arg2, arg3)
} 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) {
context.returnBuffer.setTo()
try {
callable.invoke(context, arg1, arg2, arg3, arg4)
} catch (err: ClassCastException) {

View File

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

View File

@ -551,7 +551,7 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn
callbacks["callScriptedEntity"] = luaFunctionN("callScriptedEntity") {
val id = it.nextInteger()
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)
throw LuaRuntimeException("$entity is not scripted entity")

View File

@ -119,12 +119,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
inGame()
}
protected open fun onChannelClosed() {
isConnected = false
LOGGER.info("$this is terminated")
scope.cancel("$this is terminated")
}
fun bind(channel: Channel) {
scope = CoroutineScope(channel.eventLoop().asCoroutineDispatcher() + SupervisorJob() + CoroutineExceptionHandler { coroutineContext, 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.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.shipUpgrades = shipUpgrades
connection.shipUpgradesQueue.trySend(shipUpgrades)
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(UniverseTimeUpdatePacket(connection.server.universeClock.time))
connection.channel.flush()

View File

@ -1,9 +1,12 @@
package ru.dbotthepony.kstarbound.server
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import io.netty.channel.ChannelHandlerContext
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
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.WarpMode
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.SkyType
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.network.Connection
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.UniverseTimeUpdatePacket
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.util.ActionPacer
import ru.dbotthepony.kstarbound.world.SystemWorld
import ru.dbotthepony.kstarbound.world.SystemWorldLocation
import ru.dbotthepony.kstarbound.world.UniversePos
import java.util.UUID
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.stream.Collectors
import kotlin.collections.HashMap
import kotlin.math.min
import kotlin.properties.Delegates
// serverside part of connection
@ -61,6 +70,9 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
// packets which interact with world must be
// executed on world's thread
fun enqueue(task: ServerWorld.(ServerWorldTracker) -> Unit): Boolean {
if (isDisconnecting.get())
return false
val isInWorld = tracker?.enqueue(task) != null
if (!isInWorld) {
@ -73,7 +85,37 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
lateinit var shipWorld: ServerWorld
private set
var playerSpecies: String by Delegates.notNull()
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 {
connectionID = server.channels.nextConnectionID()
@ -81,6 +123,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
rpc.add("team.fetchTeamStatus") {
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 {
@ -93,61 +141,26 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
return "$nickname <$connectionID/$uuid>"
}
private val shipChunks = HashMap<ByteKey, KOptional<ByteArray>>()
private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>()
var shipChunkSource by Delegates.notNull<WorldStorage>()
private set
private val legacyWorldStorage = LegacyWorldStorage.Memory {
// TODO: uncomment this once ALL entity types are implemented
// sendContextUpdates(it)
}
override fun setupLegacy() {
super.setupLegacy()
shipChunkSource = LegacyWorldStorage.Memory({ shipChunks[it]?.orNull() }, { key, value -> shipChunks[key] = KOptional(value) })
}
override fun setupNative() {
super.setupNative()
shipChunkSource = LegacyWorldStorage.Memory({ shipChunks[it]?.orNull() }, { key, value -> shipChunks[key] = KOptional(value) })
}
fun receiveShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) {
check(shipChunks.isEmpty()) { "Already has ship chunks" }
shipChunks.putAll(chunks)
fun loadShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) {
legacyWorldStorage.load(chunks.entries.stream().map { it.key to it.value.orNull() }.filter { it.second != null }.collect(Collectors.toMap({ it.first }, { it.second!! })))
}
private var remoteVersion = 0L
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 val warpQueue = Channel<WarpRequest>(capacity = 10)
@ -296,6 +309,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
this.systemWorld = world
var ship = world.addClient(this, location = actualInWorldLocation).await()
systemWorldShip = ship
shipWorld.sky.stopFlyingAt(ship.location.skyParameters(world))
shipCoordinate = UniversePos(world.location)
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
this.systemWorld = world
ship = world.addClient(this).await()
systemWorldShip = ship
val newParams = ship.location.skyParameters(world)
@ -486,67 +501,122 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
warpQueue.trySend(WarpRequest(destination, deploy, ifFailed))
}
private var tickTask: Future<*>? = null
private var sendContextUpdatesTask: Future<*>? = null
private var sendUniverseTimeTask: Future<*>? = null
private fun tick() {
private fun sendContextUpdates(shipWorldChanges: Map<ByteKey, KOptional<ByteArray>> = mapOf()) {
if (isConnected && isReady) {
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)
remoteVersion = version
send(ClientContextUpdatePacket(
entries ?: listOf(),
KOptional(modifiedShipChunks.associateWith { shipChunks[it]!! }),
KOptional(shipWorldChanges),
KOptional(data)))
modifiedShipChunks.clear()
}
}
}
// set to true so failed connection attempts don't appear in chat
private var announcedDisconnect = true
private val isDisconnecting = AtomicBoolean()
private fun announceDisconnect(reason: String) {
if (!announcedDisconnect && nickname.isNotBlank()) {
if (reason.isBlank()) {
server.chat.systemMessage("Player '$nickname' disconnected")
} else {
server.chat.systemMessage("Player '$nickname' disconnected ($reason)")
private suspend fun disconnect0(reason: String) {
// initiate shipworld shutdown
if (::shipWorld.isInitialized) {
shipWorld.eventLoop.shutdown()
}
// 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) {
LOGGER.info("${alias()} disconnect initiated with reason $reason")
announceDisconnect(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()
if (isDisconnecting.compareAndSet(false, true)) {
server.scope.launch { disconnect0(reason) }
}
}
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (!channel.isOpen)
if (!channel.isOpen || isDisconnecting.get())
return
if (msg is IServerPacket) {
@ -587,10 +657,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
null
}
shipUpgrades = shipUpgrades.addCapability("planetTravel")
shipUpgrades = shipUpgrades.addCapability("teleport")
shipUpgrades = shipUpgrades.copy(maxFuel = 10000, shipLevel = 3)
scope.launch { warpEventLoop() }
if (context == null) {
@ -620,8 +686,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
countedTowardsPlayerCount = true
server.channels.incrementPlayerCount()
tickTask = channel.eventLoop().scheduleWithFixedDelay(Runnable {
tick()
sendContextUpdatesTask = channel.eventLoop().scheduleWithFixedDelay(Runnable {
sendContextUpdates()
}, Starbound.TIMESTEP_NANOS, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
sendUniverseTimeTask = channel.eventLoop().scheduleWithFixedDelay(Runnable {
@ -629,10 +695,10 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
send(UniverseTimeUpdatePacket(server.universeClock.time), false)
}, 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) {
LOGGER.warn("$this disconnected before loaded their ShipWorld")
it.eventLoop.shutdown()
@ -640,7 +706,11 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipWorld = it
shipWorld.sky.referenceClock = server.universeClock
// shipWorld.sky.startFlying(true, true)
shipWorld.eventLoop.start()
if (!shipWorld.eventLoop.isAlive)
shipWorld.eventLoop.start()
scope.launch { shipUpgradesLoop() }
scope.launch { loadDataAndDispatchEventLoops() }
}
}.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.TerrestrialWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonElementInflated
import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElementDeflated
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.NativeLocalWorldStorage
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.toStarboundString
import ru.dbotthepony.kstarbound.util.uuidFromStarboundString
import ru.dbotthepony.kstarbound.world.WorldGeometry
import java.io.File
import java.sql.DriverManager
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> {
return supplyAsync {
return scope.async {
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
}.thenCompose { it }
try {
val world = ServerWorld.load(this@StarboundServer, storage, id)
worlds[id] = world
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) {
@ -453,7 +487,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
isDaemon = false
}
private val occupiedNicknames = ObjectArraySet<String>()
private val occupiedNicknames = ObjectOpenHashSet<String>()
fun reserveNickname(name: String, alternative: String): String {
synchronized(occupiedNicknames) {

View File

@ -5,6 +5,7 @@ import com.github.benmanes.caffeine.cache.Caffeine
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
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.writeStruct2f
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.xxhash32
import ru.dbotthepony.kstarbound.Starbound
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.util.CarriedExecutor
import ru.dbotthepony.kstarbound.util.ScheduledCoroutineExecutor
import ru.dbotthepony.kstarbound.util.supplyAsync
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
@ -47,9 +50,7 @@ import java.io.DataOutputStream
import java.io.File
import java.lang.ref.Cleaner
import java.sql.DriverManager
import java.time.Duration
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import java.util.function.Function
import java.util.function.Supplier
@ -61,7 +62,7 @@ import java.util.zip.InflaterInputStream
sealed class LegacyWorldStorage() : WorldStorage() {
protected abstract fun load(at: ByteKey): CompletableFuture<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 {
CoroutineScope(ScheduledCoroutineExecutor(executor) + SupervisorJob())
@ -357,30 +358,51 @@ sealed class LegacyWorldStorage() : WorldStorage() {
write(metadataKey, buff.array.copyOf(buff.length))
}
class Memory(private val get: (ByteKey) -> ByteArray?, private val set: (ByteKey, ByteArray) -> Unit) : LegacyWorldStorage() {
private val pending = HashMap<ByteKey, ByteArray>()
override val executor: Executor = Executor { it.run() }
class Memory(private val listener: (changes: Map<ByteKey, KOptional<ByteArray>>) -> Unit) : LegacyWorldStorage() {
private val changed = ObjectOpenHashSet<ByteKey>()
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?> {
return CompletableFuture.completedFuture(pending[at] ?: get(at))
return executor.supplyAsync { memory[at] }
}
override fun write(at: ByteKey, value: ByteArray) {
// set(at, value)
pending[at] = value
executor.execute {
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() {
pending.entries.forEach { (k, v) -> set(k, v) }
pending.clear()
executor.execute {
val result = changed.associateWith { KOptional.ofNullable(memory[it]) }
changed.clear()
listener(result)
}
}
}
class DB5(private val database: BTreeDB5) : LegacyWorldStorage() {
override val executor = CarriedExecutor(Starbound.IO_EXECUTOR)
override fun load(at: ByteKey): CompletableFuture<ByteArray?> {
return CompletableFuture.supplyAsync(Supplier { database.read(at).orNull() }, executor)
}
@ -400,7 +422,6 @@ sealed class LegacyWorldStorage() : WorldStorage() {
}
class SQL(path: File) : LegacyWorldStorage() {
override val executor = CarriedExecutor(Starbound.IO_EXECUTOR)
private val connection = DriverManager.getConnection("jdbc:sqlite:${path.canonicalPath.replace('\\', '/')}")
private val cleaner: Cleaner.Cleanable
@ -464,10 +485,5 @@ sealed class LegacyWorldStorage() : WorldStorage() {
companion object {
private val LOGGER = LogManager.getLogger()
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.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
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 })
if (!isUnloadingWorld)
if (!isUnloadingWorld && world.worldID !is WorldID.ShipWorld)
world.storage.commit()
}

View File

@ -4,16 +4,21 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
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.Either
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.AABBi
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.WarpAlias
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.TileDamageResult
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
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.BiomePlaceablesDefinition
import ru.dbotthepony.kstarbound.defs.world.WorldStructure
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.network.Connection
@ -67,10 +76,12 @@ import java.util.PriorityQueue
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.function.Supplier
import kotlin.coroutines.Continuation
import kotlin.coroutines.suspendCoroutine
import kotlin.math.max
class ServerWorld private constructor(
val server: StarboundServer,
@ -142,14 +153,28 @@ class ServerWorld private constructor(
playerStart = playerSpawnPosition,
respawnInWorld = respawnInWorld,
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,
protectedDungeonIds = ImmutableSet.copyOf(protectedDungeonIDsInternal),
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
@ -199,6 +224,14 @@ class ServerWorld private constructor(
eventLoop.scheduleAtFixedRate(Runnable {
tick(Starbound.TIMESTEP)
}, 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
@ -289,6 +322,117 @@ class ServerWorld private constructor(
override val connectionID: Int
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 {
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) {
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 {
@ -831,9 +981,13 @@ class ServerWorld private constructor(
val centralStructure: WorldStructure,
val protectedDungeonIds: ImmutableSet<Int>,
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 {
private val LOGGER = LogManager.getLogger()
@ -868,7 +1022,8 @@ class ServerWorld private constructor(
LOGGER.info("Attempting to load world at $worldID")
return storage.loadMetadata().thenApply {
it ?: throw NoSuchElementException("No world metadata is present")
it ?: throw WorldMetadataMissingException()
LOGGER.info("Loading world at $worldID")
AssetPathStack("/") { _ ->
val meta = Starbound.gson.fromJson(it.data.content, MetadataJson::class.java)

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.util
import kotlinx.coroutines.delay
import org.apache.logging.log4j.LogManager
import java.lang.ref.Reference
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 {
private val LOGGER = LogManager.getLogger()
}

View File

@ -80,10 +80,18 @@ class RelativeClock() : IClock {
)
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) {
if (json.isJsonNull) {
pointOfReferenceSet = false
return
}
val data = Starbound.gson.fromJson(json, JsonData::class.java)
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 dungeonGravityInternal = Int2ObjectOpenHashMap<Vector2d>()

View File

@ -132,6 +132,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
craftingProgress = data.get("craftingProgress", 0.0)
isInitialized = data.get("initialized", true)
items.fromJson(data.get("items", JsonArray()), resize = true)
ageItemsTimer.fromJson(data.get("ageItemsTimer") ?: JsonNull.INSTANCE)
}
override fun serialize(data: JsonObject) {
@ -144,6 +145,7 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
data["craftingProgress"] = craftingProgress
data["initialized"] = isInitialized
data["items"] = items.toJson(true)
data["ageItemsTimer"] = ageItemsTimer.toJson()
}
private fun randomizeContents(random: RandomGenerator, threatLevel: Double) {
@ -197,7 +199,6 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
if (!isRemote) {
if (isInitialized) return
isInitialized = true
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.classdump.luna.ByteString
import org.classdump.luna.Table
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.math.RGBAColor
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.io.Vector2iCodec
import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.stream
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
@ -110,6 +112,28 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
if ("uniqueId" in data)
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) {
@ -129,6 +153,9 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
data["orientationIndex"] = orientationIndex
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"]
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 parametersLazies = ArrayList<ManualLazy<*>>()
protected val spacesLazies = ArrayList<ManualLazy<*>>()
@ -304,6 +337,31 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
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() }
@ -813,6 +871,10 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
isInteractive = !interactAction.isJsonNull
}
override fun toString(): String {
return "WorldObject[${config.key}, at $tilePosition]"
}
companion object {
private val LOGGER = LogManager.getLogger()
private val lightColorPath = JsonPath("lightColor")