Central structures placement and replacement, minimally working shipworld saving, some more save format inconsistencies dug up
This commit is contained in:
parent
9eaa6ea5f1
commit
2d3c080002
@ -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`)
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
|
@ -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()
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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>()
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user