From 2a4b5ffb03b0de033d252d77c6b2f2a62d5aada8 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 5 May 2024 22:34:18 +0700 Subject: [PATCH] Server world Lua bindings --- ADDITIONS.md | 66 +++- .../ru/dbotthepony/kstarbound/Registry.kt | 90 +++-- .../defs/dungeon/DungeonDefinition.kt | 58 +-- .../kstarbound/defs/dungeon/DungeonWorld.kt | 5 +- .../defs/object/ObjectOrientation.kt | 2 +- .../defs/tile/BuiltinMetaMaterials.kt | 2 +- .../dbotthepony/kstarbound/defs/world/Sky.kt | 8 +- .../kstarbound/defs/world/WorldTemplate.kt | 13 +- .../dbotthepony/kstarbound/json/JsonPatch.kt | 2 +- .../dbotthepony/kstarbound/lua/Conversions.kt | 15 + .../lua/bindings/ServerWorldBindings.kt | 330 ++++++++++++++++++ .../kstarbound/lua/bindings/WorldBindings.kt | 10 +- .../lua/bindings/WorldEntityBindings.kt | 7 + .../network/LegacyNetworkCellState.kt | 4 +- .../kstarbound/network/PacketRegistry.kt | 21 +- .../UpdateDungeonBreathablePacket.kt | 23 ++ .../clientbound/UpdateDungeonGravityPacket.kt | 52 +++ .../UpdateDungeonProtectionPacket.kt | 21 ++ .../kstarbound/server/StarboundServer.kt | 121 ++++++- .../server/world/NativeLocalWorldStorage.kt | 9 +- .../kstarbound/server/world/ServerChunk.kt | 60 +++- .../kstarbound/server/world/ServerWorld.kt | 142 ++++++-- .../server/world/ServerWorldTracker.kt | 10 + .../ru/dbotthepony/kstarbound/world/World.kt | 80 ++++- .../kstarbound/world/api/AbstractTileState.kt | 4 +- .../world/entities/ItemDropEntity.kt | 8 +- 26 files changed, 1006 insertions(+), 157 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonBreathablePacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonGravityPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonProtectionPacket.kt diff --git a/ADDITIONS.md b/ADDITIONS.md index 2153faea..60ff9ef0 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -8,12 +8,12 @@ but listing all of them will be a hassle, and will pollute actually useful chang --------------- -## Prototypes +# Prototypes * `treasurechests` now can specify `treasurePool` as array * `damageTable` can be defined directly, without referencing other JSON file (experimental feature) -#### Biomes +## Biomes * Tree biome placeables now have `variantsRange` (defaults to `[1, 1]`) and `subVariantsRange` (defaults to `[2, 2]`) * `variantsRange` is responsible for "stem-foliage" combinations * `subVariantsRange` is responsible for "stem-foliage" hue shift combinations @@ -21,7 +21,7 @@ but listing all of them will be a hassle, and will pollute actually useful chang * Also two more properties were added: `sameStemHueShift` (defaults to `true`) and `sameFoliageHueShift` (defaults to `false`), which fixate hue shifts within same "stem-foliage" combination * Original engine always generates two tree types when processing placeable items, new engine however, allows to generate any number of trees. -#### Dungeons +## Dungeons * `front` and `back` brushes now can properly accept detailed data as json object on second position (e.g. `["front", { "material": ... }]`), with following structure (previously, due to oversight in code, it was impossible to specify this structure through any means, because brush definition itself can't be an object): ```kotlin val material: Registry.Ref = BuiltinMetaMaterials.EMPTY.ref @@ -40,7 +40,7 @@ val color: TileColor = TileColor.DEFAULT * By default, they mark entire _part_ of dungeon with their ID. To mark specific tile inside dungeon with its own Dungeon ID, supply `true` as third value to brush (e.g `["dungeonid", 40000, true"]`) * Tiled map behavior is unchanged, and marks their position only. -#### .terrain +## .terrain * All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them * They can be referenced by either specifying corresponding field as string, or as object like so: `{"name": "namedselector"}` * `min`, `max` and `minmax` terrain selectors now also accept next format: `{"name": "namedselector", "seedBias": 4}` @@ -54,11 +54,11 @@ val color: TileColor = TileColor.DEFAULT * `ridgeblocks` terrain selector now accepts `amplitude` and `frequency` values (naming inconsistency fix); * `ridgeblocks` has `octaves` added (defaults to `2`), `perlinOctaves` (defaults to `1`) -### player.config +## player.config * Inventory bags are no longer limited to 255 slots * However, when joining original servers with mod which increase bag size past 255 slots will result in undefined behavior (joining servers with inventory size bag mods will already result in nearly instant desync though, so you may not ever live to see the side effects; and if original server installs said mod, original clients and original server will experience severe desyncs/undefined behavior too) -#### .item +## .item * `inventoryIcon` additions if specified as array: * `scale`, either as float or as vector (for x and y scales); both in prototype file and in `parameters`. * `color` (defaults to white `[255, 255, 255, 255]`) @@ -67,7 +67,7 @@ val color: TileColor = TileColor.DEFAULT * `centered` (defaults to `true`) * `fullbright` (defaults to `false`) -#### .liquid +## .liquid * `liquidId` is no longer essential and can be skipped; engine **will not** assign it to anything, but liquid will still be fully functional from engine's point of view * However, this has serious implications: * Liquid will become "invisible" to legacy clients (this is not guaranteed, and if it ever "bleeds" into structures sent to legacy clients due to missed workarounds in code, legacy client will blow up.) @@ -76,7 +76,7 @@ val color: TileColor = TileColor.DEFAULT * This will make liquid "invisible" to original clients only, Lua code should continue to function normally * This is not guaranteed, and if it ever "bleeds" into structures sent to legacy clients due to missed workarounds in code, legacy client will blow up. -#### .matierial +## .matierial * Meta-materials are no longer treated uniquely, and are defined as "real" materials, just like every other material, but still preserve unique interactions. * `materialId` is no longer essential and can be skipped, with same notes as described in `liquidId`. * `materialId` can be specified as any number in 1 -> 2^31 - 1 (softly excluding reserved "meta materials" ID range, since this range is not actually reserved, but is expected to be used solely by meta materials), with legacy client implications only. @@ -85,24 +85,24 @@ val color: TileColor = TileColor.DEFAULT * Used by world tile rendering code (render piece rule `Connects`) * And finally, used by `canPlaceMaterial` to determine whenever player can place blocks next to it (at least one such tile should be present for player to be able to place blocks next to it) -#### .object +## .object * `breakDropOptions` and `smashDropOptions` items created now obey world's threat level * `smashDropPool`, `breakDropPool`, `breakDropOptions` and `smashDropOptions` are now deterministic (see [worldgen section](#Deterministic_world_generation)) -#### .matmod +## .matmod * `modId` is no longer essential and can be skipped, or specified as any number in 1 -> 2^31 range, with notes of `materialId` and `liquidId` apply. --------------- -## Scripting +# Scripting * In DamageSource, `sourceEntityId` combination with `rayCheck` has been fixed, and check for tile collision between victim and inflictor (this entity), not between victim and attacker (`sourceEntityId`) -#### Random +### Random * Added `random:randn(deviation: double, mean: double): double`, returns normally distributed double, where `deviation` stands for [Standard deviation](https://en.wikipedia.org/wiki/Standard_deviation), and `mean` specifies middle point * Removed `random:addEntropy` -#### animator +## animator * Added `animator.targetRotationAngle(rotationGroup: string): double` * Added `animator.hasRotationGroup(rotationGroup: string): boolean` @@ -117,7 +117,9 @@ val color: TileColor = TileColor.DEFAULT * Added `animator.hasEffect(effect: string): boolean` * Added `animator.parts(): List` -#### world +## world + +#### Additions * Added `world.liquidNamesAlongLine(start: Vector2d, end: Vector2d): List`, will return Liquid' name instead of its ID * Added `world.liquidNameAt(at: Vector2i): LiquidState?`, will return Liquid' name instead of its ID @@ -128,9 +130,35 @@ val color: TileColor = TileColor.DEFAULT * Added `world.playerLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` * Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` * Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List` - * `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target` + * Added `world.loadUniqueEntityAsync(id: String): RpcPromise` + * Added `world.findUniqueEntityAsync(id: String): RpcPromise` + * `world.findUniqueEntity` is legacy function, and to retain legacy behavior it will **block** world thread upon `result()` call if entity has not been found yet + * `world.findUniqueEntity` won't block world thread if called on client worlds, though, and will behave equal to `world.findUniqueEntityAsync` + * Added `world.unsetUniverseFlag(flag: String): Boolean` + * Added `world.placeDungeonAsync(name: String, position: Vector2d, dungeonID: Int?, seed: Long?): RpcPromise` + * Added `world.tryPlaceDungeonAsync(name: String, position: Vector2d, dungeonID: Int?, seed: Long?): RpcPromise` + +#### Changes + * `world.entityHandItem(id: EntityID, hand: String): String` now accepts `"secondary"` as `hand` argument (in addition to `"primary"`/`"alt"`) * `world.containerConsume(id: EntityID, item: ItemDescriptor, exact: Boolean?): Boolean?` now accepts `exact` which forces exact match on item descriptor (default `false`) + * `world.flyingType(): String` has been made shared (previously was server world only) + * `world.warpPhase(): String` has been made shared (previously was server world only) + * `world.skyTime(): Double` has been made shared (previously was server world only) + * `world.loadRegion(region: AABB): RpcPromise` now returns promise for region load + * Due to how engine handles world loading and unloading (it is completely async), mods which expect `loadRegion` to instantaneously load required regions **will break**. This will not be changed, mods must be adapted to new behavior + * `world.breakObject(id: EntityID, smash: Boolean = false): Boolean` argument `smash` is now optional + * `world.loadUniqueEntity(id: String): EntityID` **is busted in new engine** and will cause major issues if used (because engine is incapable for synchronous loading of world chunks, everything is async) + * If your mod is using it **PLEASE** switch to `world.loadUniqueEntityAsync(id: String): RpcPromise` + * `world.placeDungeon(name: String, position: Vector2d, dungeonID: Int?, seed: Long?): Boolean` now accepts optional `seed`. If not specified, engine will determine one (like original engine does). + * Please update code to use `world.placeDungeonAsync`, because there are absolutely no guarantees dungeon will be generated the moment `world.placeDungeon` call returns + * `world.tryPlaceDungeon(name: String, position: Vector2d, dungeonID: Int?, seed: Long?): Boolean` now accepts optional `seed`. If not specified, engine will determine one (like original engine does). + * Please update code to use `world.tryPlaceDungeonAsync`, because there are absolutely no guarantees dungeon will be generated the moment `world.tryPlaceDungeon` call returns + * `world.setDungeonGravity(id: Int, gravity: Either)` now accept directional vector. **Attention:** Directional gravity is WIP. + +#### Fixes + + * `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target` * `world.containerStackItems(id: EntityID, items: ItemDescriptor): ItemDescriptor` now actually does what it says on tin, instead of being equal to `world.containerAddItems` * **ONLY** for local entities, or when using native protocol (but why would you ever mutate containers over network in first place) * Remote entities on legacy protocol will behave like `world.containerAddItems` has been called @@ -141,7 +169,7 @@ val color: TileColor = TileColor.DEFAULT --------------- -## Deterministic world generation +# Deterministic world generation In new engine, entirety of world generation is made deterministic. What this means that given one world seed, engine will generate _exactly the same_ (on best effort*) world each time it is requested to generate one (given prototype definitions which influence @@ -177,13 +205,13 @@ and which one gets placed is determined by who finishes generating first. --------------- -## Behavior +# Behavior -### universe_server.config +## universe_server.config * Added `useNewWireProcessing`, which defaults to `true` * New wire updating system is insanely fast (because wiring is updated along entity ticking, and doesn't involve intense entity map lookups) * However, it is not a complete replacement for legacy system, because some mods might rely on fact that in legacy system when wired entities update, they load all other endpoints into memory (basically, chunkload all connected entities). In new system if wired entity references unloaded entities it simply does not update its state. * If specified as `false`, original behavior will be restored, but beware of performance degradation! If you are a modder, **PLEASE** consider other ways around instead of enabling the old behavior, because performance cost of using old system is almost always gonna outweight "benefits" of chunkloaded wiring systems. -#### Plant drop entities (vines or steps dropping on ground) +## Plant drop entities (vines or steps dropping on ground) * Collision is now determined using hull instead of rectangle diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index dfbe8bf1..f4f03272 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -8,15 +8,19 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectFunction +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.KOptional import java.util.Collections import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.locks.ReentrantLock +import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.function.Supplier import kotlin.collections.set +import kotlin.concurrent.read import kotlin.concurrent.withLock +import kotlin.concurrent.write class Registry(val name: String) { private val keysInternal = HashMap() @@ -25,13 +29,18 @@ class Registry(val name: String) { private val idRefs = Int2ObjectOpenHashMap() private val backlog = ConcurrentLinkedQueue() + private val lock = ReentrantReadWriteLock() + + private var hasBeenValidated = false + private val loggedMisses = ObjectOpenHashSet() + // it is much cheaper to queue registry additions rather than locking during high congestion fun add(task: Runnable) { backlog.add(task) } fun finishLoad() { - lock.withLock { + lock.write { var next = backlog.poll() while (next != null) { @@ -41,8 +50,6 @@ class Registry(val name: String) { } } - private val lock = ReentrantLock() - val keys: Map> = Collections.unmodifiableMap(keysInternal) val ids: Int2ObjectMap> = Int2ObjectMaps.unmodifiable(idsInternal) @@ -127,8 +134,33 @@ class Registry(val name: String) { get() = this@Registry } - operator fun get(index: String): Entry? = lock.withLock { keysInternal[index] } - operator fun get(index: Int): Entry? = lock.withLock { idsInternal[index] } + operator fun get(index: String): Entry? { + val result = lock.read { keysInternal[index] } + + if (result == null && hasBeenValidated) { + lock.write { + if (loggedMisses.add(index)) { + LOGGER.warn("No such $name: $index") + } + } + } + + return result + } + + operator fun get(index: Int): Entry? { + val result = lock.read { idsInternal[index] } + + if (result == null && hasBeenValidated) { + lock.write { + if (loggedMisses.add(index.toString())) { + LOGGER.warn("No such $name: ID $index") + } + } + } + + return result + } fun getOrThrow(index: String): Entry { return get(index) ?: throw NoSuchElementException("No such $name: $index") @@ -138,47 +170,61 @@ class Registry(val name: String) { return get(index) ?: throw NoSuchElementException("No such $name: $index") } - fun ref(index: String): Ref = lock.withLock { + fun ref(index: String): Ref = lock.write { keyRefs.computeIfAbsent(index, Object2ObjectFunction { val ref = RefImpl(Either.left(it as String)) ref.entry = keysInternal[it] + + if (hasBeenValidated && ref.entry == null && loggedMisses.add(it)) { + LOGGER.warn("No such $name: $it") + } + ref }).also { it.references++ } } - fun ref(index: Int): Ref = lock.withLock { + fun ref(index: Int): Ref = lock.write { idRefs.computeIfAbsent(index, Int2ObjectFunction { val ref = RefImpl(Either.right(it)) ref.entry = idsInternal[it] + + if (hasBeenValidated && ref.entry == null && loggedMisses.add(it.toString())) { + LOGGER.warn("No such $name: ID $it") + } + ref }).also { it.references++ } } - operator fun contains(index: String) = lock.withLock { index in keysInternal } - operator fun contains(index: Int) = lock.withLock { index in idsInternal } + operator fun contains(index: String) = lock.read { index in keysInternal } + operator fun contains(index: Int) = lock.read { index in idsInternal } fun validate(): Boolean { - var valid = true + hasBeenValidated = true - keyRefs.values.forEach { - if (!it.isPresent && it.key.left() != "") { - LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)") - valid = false + lock.read { + var valid = true + + keyRefs.values.forEach { + if (!it.isPresent && it.key.left() != "") { + LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)") + valid = false + } } - } - idRefs.values.forEach { - if (!it.isPresent) { - LOGGER.warn("Registry '$name' reference with ID '${it.key.right()}' is not bound to value, expect problems (referenced ${it.references} times)") - valid = false + idRefs.values.forEach { + if (!it.isPresent) { + LOGGER.warn("Registry '$name' reference with ID '${it.key.right()}' is not bound to value, expect problems (referenced ${it.references} times)") + valid = false + } } - } - return valid + return valid + } } fun add( @@ -191,7 +237,7 @@ class Registry(val name: String) { ): Entry { require(key != "") { "Adding $name with empty name (empty name is reserved)" } - lock.withLock { + lock.write { if (key in keysInternal) { LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt index 0ba74e15..80a9e81d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonDefinition.kt @@ -205,7 +205,17 @@ data class DungeonDefinition( world.applyFinalTouches() } - fun generate(world: ServerWorld, random: RandomGenerator, x: Int, y: Int, markSurfaceAndTerrain: Boolean, forcePlacement: Boolean, dungeonID: Int = 0, terrainSurfaceSpaceExtends: Int = 0, commit: Boolean = true): CompletableFuture { + fun generate( + world: ServerWorld, + random: RandomGenerator, + x: Int, y: Int, + markSurfaceAndTerrain: Boolean, + forcePlacement: Boolean, + dungeonID: Int = 0, + terrainSurfaceSpaceExtends: Int = 0, + commit: Boolean = true, + scope: CoroutineScope = Starbound.GLOBAL_SCOPE + ): CompletableFuture { require(dungeonID in 0 .. NO_DUNGEON_ID) { "Dungeon ID out of range: $dungeonID" } val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends) @@ -218,36 +228,32 @@ data class DungeonDefinition( val anchor = validAnchors.random(world.random) - return CoroutineScope(Starbound.COROUTINES) - .async { - if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, dungeonWorld)) { - generate0(anchor, dungeonWorld, x, y - anchor.placementLevelConstraint, forcePlacement, dungeonID) - - if (commit) { - dungeonWorld.commit() - } - } - - dungeonWorld - } - .asCompletableFuture() - } - - fun build(anchor: DungeonPart, world: ServerWorld, random: RandomGenerator, x: Int, y: Int, dungeonID: Int = NO_DUNGEON_ID, markSurfaceAndTerrain: Boolean = false, forcePlacement: Boolean = false, terrainSurfaceSpaceExtends: Int = 0, commit: Boolean = true): CompletableFuture { - require(anchor in anchorParts) { "$anchor does not belong to $name" } - val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends) - - return CoroutineScope(Starbound.COROUTINES) - .async { - generate0(anchor, dungeonWorld, x, y, forcePlacement, dungeonID) + return scope.async { + if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, dungeonWorld)) { + generate0(anchor, dungeonWorld, x, y - anchor.placementLevelConstraint, forcePlacement, dungeonID) if (commit) { dungeonWorld.commit() } - - dungeonWorld } - .asCompletableFuture() + + dungeonWorld + }.asCompletableFuture() + } + + fun build(anchor: DungeonPart, world: ServerWorld, random: RandomGenerator, x: Int, y: Int, dungeonID: Int = NO_DUNGEON_ID, markSurfaceAndTerrain: Boolean = false, forcePlacement: Boolean = false, terrainSurfaceSpaceExtends: Int = 0, commit: Boolean = true, scope: CoroutineScope = Starbound.GLOBAL_SCOPE): CompletableFuture { + require(anchor in anchorParts) { "$anchor does not belong to $name" } + val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends) + + return scope.async { + generate0(anchor, dungeonWorld, x, y, forcePlacement, dungeonID) + + if (commit) { + dungeonWorld.commit() + } + + dungeonWorld + }.asCompletableFuture() } companion object { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt index a95dc18f..183bb5e6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt @@ -3,7 +3,6 @@ package ru.dbotthepony.kstarbound.defs.dungeon import com.google.gson.JsonObject import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.future.await import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.math.AABBi @@ -583,6 +582,8 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar parent.setPlayerSpawn(playerStart!!, false) parent.eventLoop.supplyAsync { + parent.enableDungeonTileProtection = false + for ((obj, direction) in placedObjects) { try { val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction, true) @@ -623,6 +624,8 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar LOGGER.error("Exception while applying dungeon wiring group", err) } } + + parent.enableDungeonTileProtection = true }.await() } finally { tickets.forEach { it.cancel() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt index 8397fba3..80a8ef47 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -70,7 +70,7 @@ data class ObjectOrientation( val cell = world.chunkMap.getCell(it + 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}") - cell.foreground.material.isEmptyTile && (ignoreProtectedDungeons || cell.dungeonId !in world.protectedDungeonIDs) + cell.foreground.material.isEmptyTile && (ignoreProtectedDungeons || world.isDungeonIDProtected(cell.dungeonId)) } if (valid) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt index 8c17e353..0e633c8b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt @@ -110,7 +110,7 @@ const val MICRO_DUNGEON_ID = 65533 // meta dungeon signalling player built structures const val ARTIFICIAL_DUNGEON_ID = 65532 // indicates a block that has been destroyed -const val DESTROYED_BLOCK_ID = 65531 +const val DESTROYED_DUNGEON_ID = 65531 // dungeonId for zero-g areas with and without tile protection const val ZERO_GRAVITY_DUNGEON_ID = 65525 const val PROTECTED_ZERO_GRAVITY_DUNGEON_ID = 65524 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt index 45c27885..2d750a13 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt @@ -50,10 +50,10 @@ enum class FlyingType(override val jsonName: String) : IStringSerializable { } } -enum class WarpPhase { - SLOWING_DOWN, - MAINTAIN, - SPEEDING_UP; +enum class WarpPhase(override val jsonName: String) : IStringSerializable { + SLOWING_DOWN("slowingdown"), + MAINTAIN("maintain"), + SPEEDING_UP("speedingup"); } enum class SkyOrbiterType(override val jsonName: String) : IStringSerializable { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index a707f2a9..e3c6653b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -171,19 +171,19 @@ class WorldTemplate(val geometry: WorldGeometry) { class PotentialBiomeItems( // Potential items that would spawn at the given block assuming it is at - val surfaceBiomeItems: List, + val surfaceBiomeItems: List = listOf(), // ... Or on a cave surface. - val caveSurfaceBiomeItems: List, + val caveSurfaceBiomeItems: List = listOf(), // ... Or on a cave ceiling. - val caveCeilingBiomeItems: List, + val caveCeilingBiomeItems: List = listOf(), // ... Or on a cave background wall. - val caveBackgroundBiomeItems: List, + val caveBackgroundBiomeItems: List = listOf(), // ... Or in the ocean - val oceanItems: List, + val oceanItems: List = listOf(), ) fun potentialBiomeItemsAt(x: Int, y: Int): PotentialBiomeItems { @@ -201,7 +201,7 @@ class WorldTemplate(val geometry: WorldGeometry) { ) } - fun validBiomeItemsAt(x: Int, y: Int): List { + fun validBiomeItemsAt(x: Int, y: Int, potential: PotentialBiomeItems = potentialBiomeItemsAt(x, y)): List { val thisBlock = cellInfo(geometry.x.cell(x), geometry.y.cell(y)) if (thisBlock.biomeTransition) @@ -210,7 +210,6 @@ class WorldTemplate(val geometry: WorldGeometry) { val result = ObjectArrayList() val lowerBlock = cellInfo(geometry.x.cell(x), geometry.y.cell(y - 1)) val upperBlock = cellInfo(geometry.x.cell(x), geometry.y.cell(y + 1)) - val potential = potentialBiomeItemsAt(x, y) if (!lowerBlock.biomeTransition && lowerBlock.terrain && !thisBlock.terrain && !lowerBlock.foregroundCave) result.addAll(potential.surfaceBiomeItems) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt index 5c724ab0..68736c6b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt @@ -81,7 +81,7 @@ enum class JsonPatch(val key: String) { abstract fun apply(base: JsonElement, data: JsonObject): JsonElement - class JsonTestException(message: String) : IllegalStateException(message) + class JsonTestException(message: String) : RuntimeException(message, null, false, false) companion object { private val LOGGER = LogManager.getLogger() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt index 85c19d9f..3002e451 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Conversions.kt @@ -27,6 +27,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2f import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter +import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.world.physics.Poly @@ -104,6 +105,20 @@ fun ExecutionContext.toAABB(table: Any): AABB { return AABB(Vector2d(x.toDouble(), y.toDouble()), Vector2d(z.toDouble(), w.toDouble())) } +fun ExecutionContext.toAABBi(table: Any): AABBi { + val x = indexNoYield(table, 1L) + val y = indexNoYield(table, 2L) + val z = indexNoYield(table, 3L) + val w = indexNoYield(table, 4L) + + if (x !is Number) throw ClassCastException("Expected table representing a AABBi, but value at [1] is not a number: $x") + if (y !is Number) throw ClassCastException("Expected table representing a AABBi, but value at [2] is not a number: $y") + if (z !is Number) throw ClassCastException("Expected table representing a AABBi, but value at [3] is not a number: $z") + if (w !is Number) throw ClassCastException("Expected table representing a AABBi, but value at [4] is not a number: $w") + + return AABBi(Vector2i(x.toInt(), y.toInt()), Vector2i(z.toInt(), w.toInt())) +} + fun toJsonFromLua(value: Any?): JsonElement { return when (value) { null, is JsonNull -> JsonNull.INSTANCE diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt index 759ecac5..f676154f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt @@ -1,9 +1,339 @@ package ru.dbotthepony.kstarbound.lua.bindings +import org.apache.logging.log4j.LogManager +import org.classdump.luna.ByteString +import org.classdump.luna.LuaRuntimeException import org.classdump.luna.Table +import org.classdump.luna.runtime.AbstractFunction1 +import org.classdump.luna.runtime.ExecutionContext +import org.classdump.luna.runtime.UnresolvedControlThrowable +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.EntityType +import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID +import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid +import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition +import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables +import ru.dbotthepony.kstarbound.defs.world.BiomePlaceablesDefinition import ru.dbotthepony.kstarbound.lua.LuaEnvironment +import ru.dbotthepony.kstarbound.lua.iterator +import ru.dbotthepony.kstarbound.lua.luaFunction +import ru.dbotthepony.kstarbound.lua.luaStub +import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.tableOf +import ru.dbotthepony.kstarbound.lua.toAABB +import ru.dbotthepony.kstarbound.lua.toAABBi +import ru.dbotthepony.kstarbound.lua.toJsonFromLua +import ru.dbotthepony.kstarbound.lua.toVector2d +import ru.dbotthepony.kstarbound.lua.toVector2i +import ru.dbotthepony.kstarbound.lua.userdata.LuaFuture import ru.dbotthepony.kstarbound.server.world.ServerWorld +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.util.random.staticRandom64 +import ru.dbotthepony.kstarbound.util.sbIntern +import ru.dbotthepony.kstarbound.world.ChunkState +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity +import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +private val LOGGER = LogManager.getLogger() + +private class LoadUniqueEntityFunction(val self: ServerWorld) : AbstractFunction1() { + override fun invoke(context: ExecutionContext, arg1: ByteString) { + // no, I give up, this shit is beyond bonkers + // I don't care that mods could break + // mods WILL break + // just please, I beg you, for the love of God and Jesus, + // stop fucking designing async functions as sync ones + LOGGER.warn("Lua function world.loadUniqueEntity() has been called. If after this line world stop responding, this is the reason. Called in ${LuaRuntimeException("Calling world.loadUniqueEntity()").errorLocation}") + LOGGER.warn("To modders: Please refer to Lua docs for information on non-busted version of this function, world.loadUniqueEntityAsync()") + + val promise = self.loadUniqueEntity(arg1.decode()) + + if (promise.isDone) { + context.returnBuffer.setTo(promise.get()?.entityID) + } else { + try { + context.pause() + } catch (err: UnresolvedControlThrowable) { + err.resolve(this, promise) + } + } + } + + override fun resume(context: ExecutionContext, suspendedState: Any) { + val promise = suspendedState as CompletableFuture + + if (promise.isDone) { + context.returnBuffer.setTo(promise.get()?.entityID) + } else { + try { + context.pause() + } catch (err: UnresolvedControlThrowable) { + err.resolve(this, promise) + } + } + } +} + +private fun ExecutionContext.placeDungeonImpl(self: ServerWorld, force: Boolean, async: Boolean, name: ByteString, position: Table, dungeonID: Number?, seed: Number?) { + val pos = toVector2i(position) + val actualSeed = seed?.toLong() ?: staticRandom64(pos.x, pos.y, "DungeonPlacement") + val dungeonDef = Registries.dungeons[name.decode()] + + if (dungeonDef == null) { + LOGGER.error("Lua script tried to spawn dungeon $name, however, no such dungeon exist") + + if (async) { + returnBuffer.setTo(LuaFuture( + future = CompletableFuture.failedFuture(NoSuchElementException("No such dungeon: $name")), + isLocal = false + )) + } else { + returnBuffer.setTo(false) + } + } else { + val future = dungeonDef.value.generate( + self, + random(actualSeed), + pos.x, pos.y, + markSurfaceAndTerrain = false, + forcePlacement = force, + dungeonID = dungeonID?.toInt() ?: NO_DUNGEON_ID, + // Setting scope to eventloop will make dungeon generate synchronous if it is possible + // It is still possible, though, for dungeon to not generate synchronously if it need to load chunks + // which are currently not loaded + scope = if (async) Starbound.GLOBAL_SCOPE else self.eventLoop.scope).thenApply { it.hasGenerated } + + if (async) { + returnBuffer.setTo(LuaFuture( + future = future, + isLocal = false + )) + } else { + returnBuffer.setTo(future.getNow(true)) + } + } +} fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvironment) { + callbacks["breakObject"] = luaFunction { id: Number, smash: Boolean? -> + val entity = self.entities[id.toInt()] as? WorldObject ?: return@luaFunction returnBuffer.setTo(false) + if (entity.isRemote) { + // we can't break objects now owned by us + returnBuffer.setTo(false) + } else { + if (smash == true) + entity.health = 0.0 + + entity.remove(AbstractEntity.RemovalReason.DYING) + returnBuffer.setTo(true) + } + } + + callbacks["isVisibleToPlayer"] = luaFunction { region: Table -> + val parse = toAABB(region) + returnBuffer.setTo(self.clients.any { it.isTracking(parse) }) + } + + callbacks["loadRegion"] = luaFunction { region: Table -> + // FIXME: in original engine this supposedly blocks everything else from happening until region is loaded + // And we can't block event loop, because this will make region be unable to be loaded in first place + // god damn it + val parse = toAABB(region) + // keep in ram for at most 2400 ticks + val tickets = self.temporaryChunkTicket(parse, 2400).get() + val future = CompletableFuture.allOf(*tickets.map { it.chunk }.toTypedArray()) + + future.thenApply { + self.eventLoop.schedule(Runnable { + tickets.forEach { it.cancel() } + }, 4L, TimeUnit.SECONDS) + }.exceptionally { + self.eventLoop.schedule(Runnable { + tickets.forEach { it.cancel() } + }, 4L, TimeUnit.SECONDS) + } + + returnBuffer.setTo(LuaFuture( + future = future, + isLocal = false + )) + } + + callbacks["regionActive"] = luaFunction { region: Table -> + val chunks = self.geometry.region2Chunks(toAABB(region)) + // TODO: i have no idea what mods expect this to return, so i'll have to guess + returnBuffer.setTo(chunks.all { self.chunkMap[it]?.state == ChunkState.FULL }) + } + + callbacks["setTileProtection"] = luaFunction { id: Number, enable: Boolean -> + self.switchDungeonIDProtection(id.toInt(), enable) + } + + callbacks["isPlayerModified"] = luaFunction { region: Table -> + returnBuffer.setTo(self.isPlayerModified(toAABBi(region))) + } + + callbacks["forceDestroyLiquid"] = luaFunction { position: Table -> + val parse = toVector2i(position) + val cell = self.getCell(parse).mutable() + + if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.level > 0f) { + returnBuffer.setTo(cell.liquid.level.toDouble()) + + cell.liquid.reset() + self.setCell(parse, cell.immutable()) + } + } + + callbacks["loadUniqueEntity"] = LoadUniqueEntityFunction(self) + + callbacks["loadUniqueEntityAsync"] = luaFunction { name: ByteString -> + returnBuffer.setTo(LuaFuture( + future = self.loadUniqueEntity(name.decode()), + isLocal = false + )) + } + + callbacks["setUniqueId"] = luaFunction { id: Number, name: ByteString -> + val entity = self.entities[id.toInt()] ?: throw LuaRuntimeException("No such entity with ID $id (tried to set unique id to $name)") + + if (entity.isRemote) + throw LuaRuntimeException("Entity $entity is not owned by this side") + + if ( + entity.type == EntityType.NPC || + entity.type == EntityType.STAGEHAND || + entity.type == EntityType.MONSTER || + entity.type == EntityType.OBJECT + ) { + entity.uniqueID.accept(name.decode().sbIntern()) + } else { + throw LuaRuntimeException("Entity type is restricted from having unique ID: $entity (tried to set unique id to $name)") + } + } + + callbacks["takeItemDrop"] = luaFunction { id: Number, takenBy: Number? -> + val entity = self.entities[id.toInt()] as? ItemDropEntity ?: return@luaFunction returnBuffer.setTo() + + if (!entity.isRemote && entity.canTake) { + returnBuffer.setTo(entity.take(takenBy?.toInt() ?: 0).toTable(this)) + } + } + + callbacks["setPlayerStart"] = luaFunction { position: Table, respawnInWorld: Boolean? -> + self.setPlayerSpawn(toVector2d(position), respawnInWorld == true) + } + + callbacks["players"] = luaFunction { + returnBuffer.setTo(tableOf(*self.clients.map { it.client.playerID }.toTypedArray())) + } + + callbacks["fidelity"] = luaFunction { + returnBuffer.setTo("high") + } + + callbacks["setSkyTime"] = luaFunction { newTime: Number -> + val cast = newTime.toDouble() + + if (cast < 0.0) + throw LuaRuntimeException("Negative time? $cast") + + self.sky.time = cast + } + + callbacks["setUniverseFlag"] = luaFunction { flag: ByteString -> + returnBuffer.setTo(self.server.addUniverseFlag(flag.decode())) + } + + callbacks["unsetUniverseFlag"] = luaFunction { flag: ByteString -> + returnBuffer.setTo(self.server.removeUniverseFlag(flag.decode())) + } + + callbacks["universeFlags"] = luaFunction { + returnBuffer.setTo(tableOf(*self.server.getUniverseFlags().toTypedArray())) + } + + callbacks["universeFlagSet"] = luaFunction { flag: ByteString -> + returnBuffer.setTo(self.server.hasUniverseFlag(flag.decode())) + } + + callbacks["placeDungeon"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> + placeDungeonImpl(self, true, false, name, position, dungeonID, seed) + } + + callbacks["tryPlaceDungeon"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> + placeDungeonImpl(self, false, false, name, position, dungeonID, seed) + } + + callbacks["placeDungeonAsync"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> + placeDungeonImpl(self, true, true, name, position, dungeonID, seed) + } + + callbacks["tryPlaceDungeonAsync"] = luaFunction { name: ByteString, position: Table, dungeonID: Number?, seed: Number? -> + placeDungeonImpl(self, false, true, name, position, dungeonID, seed) + } + + // terraforming + callbacks["addBiomeRegion"] = luaStub("addBiomeRegion") + callbacks["expandBiomeRegion"] = luaStub("expandBiomeRegion") + callbacks["pregenerateAddBiome"] = luaStub("pregenerateAddBiome") + callbacks["pregenerateExpandBiome"] = luaStub("pregenerateExpandBiome") + callbacks["setLayerEnvironmentBiome"] = luaStub("setLayerEnvironmentBiome") + callbacks["setPlanetType"] = luaStub("setPlanetType") + + callbacks["setDungeonGravity"] = luaFunction { id: Number, gravity: Any? -> + if (gravity == null) { + self.setDungeonGravity(id.toInt(), null) + } else if (gravity is Table) { + self.setDungeonGravity(id.toInt(), toVector2d(gravity)) + } else if (gravity is Number) { + self.setDungeonGravity(id.toInt(), gravity.toDouble()) + } else { + throw LuaRuntimeException("Illegal argument to setDungeonGravity: $gravity") + } + } + + callbacks["setDungeonBreathable"] = luaFunction { id: Number, breathable: Boolean? -> + self.setDungeonBreathable(id.toInt(), breathable) + } + + callbacks["setDungeonId"] = luaFunction { region: Table, id: Number -> + val parse = toAABBi(region) + val actualId = id.toInt() + + for (x in parse.mins.x .. parse.maxs.x) { + for (y in parse.mins.y .. parse.maxs.y) { + val cell = self.getCell(x, y) + + if (cell.dungeonId != actualId) { + self.setCell(x, y, cell.mutable().also { it.dungeonId = actualId }) + } + } + } + } + + callbacks["enqueuePlacement"] = luaFunction { distributions: Table, dungeonId: Number? -> + val items = ArrayList() + + for ((_, v) in distributions) { + // original engine treats distributions table as if it was originating from biome json files + val unprepared = Starbound.gson.fromJson(toJsonFromLua(v), BiomePlaceablesDefinition.DistributionItem::class.java) + val prepared = unprepared.create(BiomeDefinition.CreationParams(hueShift = 0.0, random = lua.random)) + items.add(prepared) + } + + // TODO: Enqueued placements are lost on world shutdown. + // Shouldn't we serialize them to persistent storage? + returnBuffer.setTo( + LuaFuture( + future = self.enqueuePlacement(items, dungeonId?.toInt()), + isLocal = false + ) + ) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt index 99d68bd2..3b3005f6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -168,6 +168,14 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { returnBuffer.setTo(self.sky.flyingType.jsonName) } + callbacks["warpPhase"] = luaFunction { + returnBuffer.setTo(self.sky.warpPhase.jsonName) + } + + callbacks["skyTime"] = luaFunction { + returnBuffer.setTo(self.sky.time) + } + callbacks["magnitude"] = luaFunction { arg1: Table, arg2: Table? -> if (arg2 == null) { returnBuffer.setTo(toVector2d(arg1).length) @@ -533,7 +541,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { returnLiquid(cell.liquid, true) } - callbacks["isTileProtected"] = luaFunction { pos: Table -> returnBuffer.setTo(self.getCell(toVector2i(pos)).dungeonId in self.protectedDungeonIDs) } + callbacks["isTileProtected"] = luaFunction { pos: Table -> returnBuffer.setTo(self.isDungeonIDProtected(self.getCell(toVector2i(pos)).dungeonId)) } callbacks["findPlatformerPath"] = luaStub("findPlatformerPath") callbacks["platformerPathStart"] = luaStub("platformerPathStart") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt index 116f2aa5..72272497 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldEntityBindings.kt @@ -569,6 +569,13 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn )) } + callbacks["findUniqueEntityAsync"] = luaFunction { id: ByteString -> + returnBuffer.setTo(LuaFuture( + future = self.findUniqueEntity(id.decode()).thenApply { from(it) }, + isLocal = false + )) + } + callbacks["sendEntityMessage"] = luaFunctionN("sendEntityMessage") { val id = it.nextAny() val func = it.nextString().decode() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/LegacyNetworkCellState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/LegacyNetworkCellState.kt index 202ee7a3..a97e328c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/LegacyNetworkCellState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/LegacyNetworkCellState.kt @@ -34,8 +34,8 @@ data class LegacyNetworkTileState( } companion object { - val EMPTY = LegacyNetworkTileState(0, 0, 0, 0, 0) - val NULL = LegacyNetworkTileState(BuiltinMetaMaterials.NULL.id!!, 0, 0, 0, 0) + val EMPTY = LegacyNetworkTileState(BuiltinMetaMaterials.EMPTY.id!!, 0, 0, BuiltinMetaMaterials.EMPTY_MOD.id!!, 0) + val NULL = LegacyNetworkTileState(BuiltinMetaMaterials.NULL.id!!, 0, 0, BuiltinMetaMaterials.EMPTY_MOD.id!!, 0) fun read(stream: DataInputStream): LegacyNetworkTileState { val tile = stream.readUnsignedShort() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index b9814528..7df86afb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -59,6 +59,9 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldUpdatePa import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.TileModificationFailurePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateDungeonBreathablePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateDungeonGravityPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateDungeonProtectionPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateWorldPropertiesPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket @@ -418,10 +421,10 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.skip("ProtocolResponse") // Packets sent universe server -> universe client - LEGACY.add(::ServerDisconnectPacket) // ServerDisconnect - LEGACY.add(::ConnectSuccessPacket) // ConnectSuccess + LEGACY.add(::ServerDisconnectPacket) + LEGACY.add(::ConnectSuccessPacket) LEGACY.add(::ConnectFailurePacket) - LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge + LEGACY.add(::HandshakeChallengePacket) LEGACY.add(::ChatReceivePacket) LEGACY.add(::UniverseTimeUpdatePacket) LEGACY.add(::CelestialResponsePacket) @@ -431,9 +434,9 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::ServerInfoPacket) // Packets sent universe client -> universe server - LEGACY.add(::ClientConnectPacket) // ClientConnect + LEGACY.add(::ClientConnectPacket) LEGACY.add(ClientDisconnectRequestPacket::read) - LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse + LEGACY.add(::HandshakeResponsePacket) LEGACY.add(::PlayerWarpPacket) LEGACY.add(::FlyShipPacket) LEGACY.add(::ChatSendPacket) @@ -444,7 +447,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(ClientContextUpdatePacket::read) // Packets sent world server -> world client - LEGACY.add(::WorldStartPacket) // WorldStart + LEGACY.add(::WorldStartPacket) LEGACY.add(::WorldStopPacket) LEGACY.skip("WorldLayoutUpdate") LEGACY.skip("WorldParametersUpdate") @@ -456,9 +459,9 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::TileModificationFailurePacket) LEGACY.add(::GiveItemPacket) LEGACY.add(::EnvironmentUpdatePacket) - LEGACY.skip("UpdateTileProtection") - LEGACY.skip("SetDungeonGravity") - LEGACY.skip("SetDungeonBreathable") + LEGACY.add(::UpdateDungeonProtectionPacket) + LEGACY.add(UpdateDungeonGravityPacket::read) + LEGACY.add(::UpdateDungeonBreathablePacket) LEGACY.add(::SetPlayerStartPacket) LEGACY.add(FindUniqueEntityResponsePacket::read) LEGACY.add(PongPacket::read) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonBreathablePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonBreathablePacket.kt new file mode 100644 index 00000000..3b1aea66 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonBreathablePacket.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.io.readNullable +import ru.dbotthepony.kstarbound.io.writeNullable +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class UpdateDungeonBreathablePacket(val id: Int, val breathable: Boolean?) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUnsignedShort(), stream.readNullable { readBoolean() }) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeShort(id) + stream.writeNullable(breathable) { writeBoolean(it) } + } + + override fun play(connection: ClientConnection) { + connection.enqueue { + world?.setDungeonBreathable(this@UpdateDungeonBreathablePacket.id, breathable) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonGravityPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonGravityPacket.kt new file mode 100644 index 00000000..f3daa411 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonGravityPacket.kt @@ -0,0 +1,52 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kommons.io.writeStruct2d +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.io.readVector2d +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class UpdateDungeonGravityPacket(val id: Int, val gravity: Vector2d?) : IClientPacket { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeShort(id) + + if (gravity == null) { + stream.writeBoolean(false) + } else { + stream.writeBoolean(true) + + if (isLegacy) { + stream.writeFloat(gravity.y.toFloat()) + } else { + stream.writeStruct2d(gravity) + } + } + } + + override fun play(connection: ClientConnection) { + connection.enqueue { + world?.setDungeonGravity(this@UpdateDungeonGravityPacket.id, gravity) + } + } + + companion object { + fun read(stream: DataInputStream, isLegacy: Boolean): UpdateDungeonGravityPacket { + val id = stream.readUnsignedShort() + val gravity: Vector2d? + + if (stream.readBoolean()) { + if (isLegacy) { + gravity = Vector2d(y = stream.readFloat().toDouble()) + } else { + gravity = stream.readVector2d() + } + } else { + gravity = null + } + + return UpdateDungeonGravityPacket(id, gravity) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonProtectionPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonProtectionPacket.kt new file mode 100644 index 00000000..77decee6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/UpdateDungeonProtectionPacket.kt @@ -0,0 +1,21 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class UpdateDungeonProtectionPacket(val id: Int, val isProtected: Boolean) : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUnsignedShort(), stream.readBoolean()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeShort(id) + stream.writeBoolean(isProtected) + } + + override fun play(connection: ClientConnection) { + connection.enqueue { + world?.switchDungeonIDProtection(this@UpdateDungeonProtectionPacket.id, isProtected) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 1812ba2d..2729b194 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -5,6 +5,7 @@ import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArraySet +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async @@ -40,6 +41,7 @@ import ru.dbotthepony.kstarbound.util.toStarboundString import ru.dbotthepony.kstarbound.util.uuidFromStarboundString import java.io.File import java.sql.DriverManager +import java.util.Collections import java.util.UUID import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit @@ -73,21 +75,62 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread database.createStatement().use { it.execute("PRAGMA journal_mode=WAL") it.execute("PRAGMA synchronous=NORMAL") - it.execute("CREATE TABLE IF NOT EXISTS `metadata` (`key` VARCHAR NOT NULL PRIMARY KEY, `value` BLOB NOT NULL)") - it.execute("CREATE TABLE IF NOT EXISTS `universe_flags` (`flag` VARCHAR NOT NULL PRIMARY KEY)") - it.execute("CREATE TABLE IF NOT EXISTS `client_context` (`uuid` VARCHAR NOT NULL PRIMARY KEY, `data` BLOB NOT NULL)") - it.execute("CREATE TABLE IF NOT EXISTS `system_worlds` (`x` INTEGER NOT NULL, `y` INTEGER NOT NULL, `z` INTEGER NOT NULL, `data` BLOB NOT NULL, PRIMARY KEY (`x`, `y`, `z`))") + + it.execute(""" + CREATE TABLE IF NOT EXISTS "metadata" ( + "key" VARCHAR NOT NULL PRIMARY KEY, + "value" BLOB NOT NULL + ) + """.trimIndent()) + + it.execute(""" + CREATE TABLE IF NOT EXISTS "universe_flags" ("flag" VARCHAR NOT NULL PRIMARY KEY) + """.trimIndent()) + + it.execute(""" + CREATE TABLE IF NOT EXISTS "client_context" ( + "uuid" VARCHAR NOT NULL PRIMARY KEY, + "data" BLOB NOT NULL + ) + """.trimIndent()) + + it.execute(""" + CREATE TABLE IF NOT EXISTS "system_worlds" ( + "x" INTEGER NOT NULL, + "y" INTEGER NOT NULL, + "z" INTEGER NOT NULL, + "data" BLOB NOT NULL, + PRIMARY KEY ("x", "y", "z") + ) + """.trimIndent()) } database.autoCommit = false } - private val lookupMetadata = database.prepareStatement("SELECT `value` FROM `metadata` WHERE `key` = ?") - private val writeMetadata = database.prepareStatement("REPLACE INTO `metadata` (`key`, `value`) VALUES (?, ?)") - private val lookupClientContext = database.prepareStatement("SELECT `data` FROM `client_context` WHERE `uuid` = ?") - private val writeClientContext = database.prepareStatement("REPLACE INTO `client_context` (`uuid`, `data`) VALUES (?, ?)") - private val lookupSystemWorld = database.prepareStatement("SELECT `data` FROM `system_worlds` WHERE `x` = ? AND `y` = ? AND `z` = ?") - private val writeSystemWorld = database.prepareStatement("REPLACE INTO `system_worlds` (`x`, `y`, `z`, `data`) VALUES (?, ?, ?, ?)") + private val lookupMetadata = database.prepareStatement(""" + SELECT "value" FROM "metadata" WHERE "key" = ? + """.trimIndent()) + + private val writeMetadata = database.prepareStatement(""" + REPLACE INTO "metadata" ("key", "value") VALUES (?, ?) + """.trimIndent()) + + private val lookupClientContext = database.prepareStatement(""" + SELECT "data" FROM "client_context" WHERE "uuid" = ? + """.trimIndent()) + + private val writeClientContext = database.prepareStatement(""" + REPLACE INTO "client_context" ("uuid", "data") VALUES (?, ?) + """.trimIndent()) + + private val lookupSystemWorld = database.prepareStatement(""" + SELECT "data" FROM "system_worlds" WHERE "x" = ? AND "y" = ? AND "z" = ? + """.trimIndent()) + + private val writeSystemWorld = database.prepareStatement(""" + REPLACE INTO "system_worlds" ("x", "y", "z", "data") VALUES (?, ?, ?, ?) + """.trimIndent()) private fun getMetadata(key: String): KOptional { lookupMetadata.setString(1, key) @@ -129,6 +172,64 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread } } + private val universeFlags = Collections.synchronizedSet(ObjectOpenHashSet()) + + init { + database.createStatement().use { + it.executeQuery("""SELECT "flag" FROM "universe_flags"""").use { + while (it.next()) { + universeFlags.add(it.getString(1)) + } + } + } + } + + private val insertUniverseFlag = database.prepareStatement(""" + INSERT INTO "universe_flags" ("flag") VALUES (?) + """.trimIndent()) + + private val removeUniverseFlag = database.prepareStatement(""" + DELETE FROM "universe_flags" WHERE "flag" = ? + """.trimIndent()) + + fun hasUniverseFlag(flag: String): Boolean { + return flag in universeFlags + } + + fun getUniverseFlags(): Set { + return Collections.unmodifiableSet(universeFlags) + } + + fun addUniverseFlag(flag: String): Boolean { + if (universeFlags.add(flag)) { + LOGGER.info("Added universe flag '$flag'") + + execute { + insertUniverseFlag.setString(1, flag) + insertUniverseFlag.execute() + } + + return true + } + + return false + } + + fun removeUniverseFlag(flag: String): Boolean { + if (universeFlags.remove(flag)) { + LOGGER.info("Removed universe flag '$flag'") + + execute { + removeUniverseFlag.setString(1, flag) + removeUniverseFlag.execute() + } + + return true + } + + return false + } + fun loadServerWorldData(pos: Vector3i): CompletableFuture { return supplyAsync { lookupSystemWorld.setInt(1, pos.x) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/NativeLocalWorldStorage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/NativeLocalWorldStorage.kt index 41c560d1..efb71e54 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/NativeLocalWorldStorage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/NativeLocalWorldStorage.kt @@ -50,7 +50,6 @@ import java.util.zip.InflaterInputStream class NativeLocalWorldStorage(file: File?) : WorldStorage() { private val connection: Connection private val executor = CarriedExecutor(Starbound.IO_EXECUTOR) - private val cleaner: Cleaner.Cleanable init { if (file == null) { @@ -58,14 +57,10 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() { } else { connection = DriverManager.getConnection("jdbc:sqlite:${file.absolutePath.replace('\\', '/')}") } - - val connection = connection - - cleaner = Starbound.CLEANER.register(this) { - connection.close() - } } + private val cleaner: Cleaner.Cleanable = Starbound.CLEANER.register(this, connection::close) + override fun commit() { executor.execute { connection.commit() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 91b4be8f..5b24c179 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -14,6 +14,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials +import ru.dbotthepony.kstarbound.defs.tile.DESTROYED_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.FIRST_RESERVED_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID @@ -408,7 +409,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk() + private data class PotentialMicrodungeon(val future: CompletableFuture?, val item: BiomePlaceables.Placement, val dungeonId: Int) + private suspend fun placeMicroDungeons() { val placements = CompletableFuture.supplyAsync(Supplier { - val placements = ArrayList() + val placements = ArrayList() for (x in 0 until width) { for (y in 0 until height) { - placements.addAll(world.template.validBiomeItemsAt(pos.tileX + x, pos.tileY + y)) + for (p in world.template.validBiomeItemsAt(pos.tileX + x, pos.tileY + y)) + placements.add(PotentialMicrodungeon(null, p, MICRO_DUNGEON_ID)) + + for (enqueued in world.queuedPlacements) + for (p in world.template.validBiomeItemsAt(pos.tileX + x, pos.tileY + y, enqueued.evaluate(pos.tileX + x, pos.tileY + y))) + placements.add(PotentialMicrodungeon(enqueued.future, p, enqueued.dungeonId ?: MICRO_DUNGEON_ID)) } } - placements.sortBy { it.priority } + placements.sortBy { it.item.priority } placements }, Starbound.EXECUTOR).await() @@ -749,7 +758,9 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk?, val item: BiomePlaceables.Placement, val pos: Vector2i) + private data class Prepared(val future: CompletableFuture?, val item: () -> Unit, val pos: Vector2i) + private suspend fun placeBiomeItems() { val placements = CompletableFuture.supplyAsync(Supplier { - val placements = ArrayList() + val placements = ArrayList() for (x in 0 until width) { for (y in 0 until height) { if (cells.value[x, y].dungeonId == NO_DUNGEON_ID) { - placements.addAll(world.template.validBiomeItemsAt(pos.tileX + x, pos.tileY + y)) + for (p in world.template.validBiomeItemsAt(pos.tileX + x, pos.tileY + y)) + placements.add(Determined(null, p, Vector2i(pos.tileX + x, pos.tileY + y))) + + for (enqueued in world.queuedPlacements) + for (p in world.template.validBiomeItemsAt(pos.tileX + x, pos.tileY + y, enqueued.evaluate(pos.tileX + x, pos.tileY + y))) + placements.add(Determined(enqueued.future, p, Vector2i(pos.tileX + x, pos.tileY + y))) } } } - placements.sortBy { it.priority } + placements.sortBy { it.item.priority } val random = random(staticRandom64(world.template.seed, pos.x, pos.y, "biome placement")) - val funcs = ArrayList<() -> Unit>() + val funcs = ArrayList() + + for ((future, placement, pos) in placements) { + if (future?.isDone == true) continue - for (placement in placements) { try { - funcs.add(placement.item.createPlacementFunc(world, random, placement.position)) + funcs.add(Prepared(future, placement.item.createPlacementFunc(world, random, placement.position), pos)) } catch (err: Throwable) { LOGGER.error("Exception while evaluating biome placeables for chunk $pos in $world", err) + future?.completeExceptionally(err) } } funcs }, Starbound.EXECUTOR).await() - for (placement in placements) { + for ((future, placement, pos) in placements) { + if (future?.isDone == true) continue + try { placement() + future?.complete(pos) } catch (err: Throwable) { LOGGER.error("Exception while placing biome placeables for chunk $pos in $world", err) + future?.completeExceptionally(err) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 5ae9ade4..bf6bc243 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -1,9 +1,10 @@ package ru.dbotthepony.kstarbound.server.world +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableSet import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject -import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.ObjectArraySet @@ -28,6 +29,8 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageType import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid +import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables +import ru.dbotthepony.kstarbound.defs.world.BiomePlaceablesDefinition import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -40,6 +43,9 @@ import ru.dbotthepony.kstarbound.network.packets.HitRequestPacket import ru.dbotthepony.kstarbound.world.TileModification import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateDungeonBreathablePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateDungeonGravityPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateDungeonProtectionPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateWorldPropertiesPacket import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.ServerConnection @@ -56,6 +62,7 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.physics.CollisionType +import java.util.Collections import java.util.PriorityQueue import java.util.concurrent.CompletableFuture import java.util.concurrent.CopyOnWriteArrayList @@ -137,7 +144,7 @@ class ServerWorld private constructor( adjustPlayerStart = adjustPlayerSpawn, worldTemplate = if (storage is LegacyWorldStorage) Starbound.legacyJson { template.toJson() } else template.toJson(), centralStructure = centralStructure, - protectedDungeonIds = protectedDungeonIDs, + protectedDungeonIds = ImmutableSet.copyOf(protectedDungeonIDsInternal), worldProperties = copyProperties(), spawningEnabled = true ) @@ -223,7 +230,7 @@ class ServerWorld private constructor( // this is used for scheduling and resolving microdungeon placement // tries to early-resolve artifacts like this: https://i.dbotthepony.ru/2024/04/28/gb6GdbLox7.png - suspend fun queueMicrodungeonPlacement(x: Int, y: Int, callback: suspend (ExecutionTimePacer) -> T): T { + suspend fun scheduleMicrodungeonPlacement(x: Int, y: Int, callback: suspend (ExecutionTimePacer) -> T): T { return suspendCoroutine { placementQueue.add(PlacementElement(x, y, placementTaskID++, callback, it)) @@ -233,6 +240,43 @@ class ServerWorld private constructor( } } + // not data class for identity equals() + class QueuedPlacement( + val placeables: ImmutableList, + val future: CompletableFuture, + val dungeonId: Int? + ) { + fun evaluate(x: Int, y: Int): WorldTemplate.PotentialBiomeItems { + val surfaceBiomeItems = placeables.filter { it.mode == BiomePlaceablesDefinition.Placement.FLOOR }.map { it.itemToPlace(x, y) }.filterNotNull() + val oceanItems = placeables.filter { it.mode == BiomePlaceablesDefinition.Placement.OCEAN }.map { it.itemToPlace(x, y) }.filterNotNull() + + return WorldTemplate.PotentialBiomeItems( + surfaceBiomeItems = surfaceBiomeItems, + oceanItems = oceanItems, + ) + } + } + + // we don't care about other threads reading stale data, because they will check future's completion + // before executing actual placement + private val queuedPlacementsInternal = CopyOnWriteArrayList() + val queuedPlacements: List = Collections.unmodifiableList(queuedPlacementsInternal) + + fun enqueuePlacement(placement: List, dungeonId: Int? = null): CompletableFuture { + val future = CompletableFuture() + val entry = QueuedPlacement(placeables = ImmutableList.copyOf(placement), future = future, dungeonId = dungeonId) + queuedPlacementsInternal.add(entry) + + future.thenRun { + queuedPlacementsInternal.remove(entry) + }.exceptionally { + queuedPlacementsInternal.remove(entry) + null + } + + return future + } + override fun toString(): String { return "Server World $worldID" } @@ -245,6 +289,54 @@ class ServerWorld private constructor( override val connectionID: Int get() = 0 + override fun switchDungeonIDProtection(id: Int, enable: Boolean): Boolean { + val updated = super.switchDungeonIDProtection(id, enable) + + if (updated) { + if (enable) { + LOGGER.info("Dungeon ID $id is now protected") + } else { + LOGGER.info("Dungeon ID $id is no longer protected") + } + + broadcast(UpdateDungeonProtectionPacket(id, enable)) + } + + return updated + } + + override fun setDungeonGravity(id: Int, gravity: Vector2d?): Boolean { + val updated = super.setDungeonGravity(id, gravity) + + if (updated) { + if (gravity != null) { + LOGGER.info("Dungeon ID $id now has gravity: $gravity") + } else { + LOGGER.info("Dungeon ID $id no longer has special gravity") + } + + broadcast(UpdateDungeonGravityPacket(id, gravity)) + } + + return updated + } + + override fun setDungeonBreathable(id: Int, breathable: Boolean?): Boolean { + val updated = super.setDungeonBreathable(id, breathable) + + if (updated) { + if (breathable != null) { + LOGGER.info("Dungeon ID $id breathable set to: $breathable") + } else { + LOGGER.info("Dungeon ID $id no longer has special breathable flag") + } + + broadcast(UpdateDungeonBreathablePacket(id, breathable)) + } + + return updated + } + /** * this method does not block if pacer is null (safe to use with runBlocking {}) */ @@ -300,7 +392,7 @@ class ServerWorld private constructor( for (pos in damagePositions) { val cell = getCell(pos) - if (cell.dungeonId in protectedDungeonIDs) { + if (cell.dungeonId in protectedDungeonIDsInternal) { actualDamage = actualDamage.copy(type = TileDamageType.PROTECTED) entityDamageResults[pos] = TileDamageResult.PROTECTED } else { @@ -339,7 +431,7 @@ class ServerWorld private constructor( val getCell = chunk?.getCell(pos - chunk.pos.tile) if (getCell != null) { - if (getCell.dungeonId in protectedDungeonIDs) { + if (getCell.dungeonId in protectedDungeonIDsInternal) { actualDamage = actualDamage.copy(type = TileDamageType.PROTECTED) } } @@ -383,7 +475,7 @@ class ServerWorld private constructor( for ((pos, modification) in itr) { val cell = getCell(pos) - if (!ignoreTileProtection && cell.dungeonId in protectedDungeonIDs) + if (!ignoreTileProtection && isDungeonIDProtected(cell.dungeonId)) continue if (modification.allowed(this, pos, allowEntityOverlap)) { @@ -487,7 +579,7 @@ class ServerWorld private constructor( LOGGER.info("Placed dungeon ${dungeon.dungeon.key} at $x, ${dungeon.baseHeight}") if (dungeon.dungeon.value.metadata.protected) { - protectedDungeonIDs.add(currentDungeonID) + protectedDungeonIDsInternal.add(currentDungeonID) } if (dungeon.dungeon.value.metadata.gravity != null) { @@ -671,23 +763,16 @@ class ServerWorld private constructor( return storage.findUniqueEntity(id).thenApply { it?.pos } } - override fun dispatchEntityMessage( - sourceConnection: Int, - entityID: String, - message: String, - arguments: JsonArray - ): CompletableFuture { + fun loadUniqueEntity(entityID: String): CompletableFuture { var loaded = uniqueEntities[entityID] if (loaded != null) { - return loaded.dispatchMessage(sourceConnection, message, arguments) + return CompletableFuture.completedFuture(loaded) } - // very well. - // I accept the requirement to load the chunk that contains aforementioned entity return eventLoop.scope.async { - val (chunk) = storage.findUniqueEntity(entityID).await() ?: throw MessageCallException("No such entity $entityID") - val ticket = permanentChunkTicket(chunk).await() ?: throw MessageCallException("Internal server error") + val (chunk) = storage.findUniqueEntity(entityID).await() ?: return@async null + val ticket = permanentChunkTicket(chunk).await() ?: throw RuntimeException("how did we end up here") try { ticket.chunk.await() @@ -696,16 +781,27 @@ class ServerWorld private constructor( if (loaded == null) { // How? LOGGER.warn("Expected unique entity $entityID to be present inside $chunk, but after loading said chunk required entity is missing; world storage might be in corrupt state after unclean shutdown") - throw MessageCallException("No such entity $entityID") } - loaded!!.dispatchMessage(sourceConnection, message, arguments).await() + loaded } finally { ticket.cancel() } }.asCompletableFuture() } + override fun dispatchEntityMessage( + sourceConnection: Int, + entityID: String, + message: String, + arguments: JsonArray + ): CompletableFuture { + return loadUniqueEntity(entityID).thenCompose { + it ?: throw MessageCallException("No such unique entity $entityID") + it.dispatchMessage(sourceConnection, message, arguments) + } + } + @JsonFactory data class MetadataJson( val playerStart: Vector2d, @@ -713,7 +809,7 @@ class ServerWorld private constructor( val adjustPlayerStart: Boolean, val worldTemplate: JsonObject, val centralStructure: WorldStructure, - val protectedDungeonIds: IntArraySet, + val protectedDungeonIds: ImmutableSet, val worldProperties: JsonObject, val spawningEnabled: Boolean ) @@ -743,7 +839,7 @@ class ServerWorld private constructor( world.setProperty(k, v) } - world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) + world.protectedDungeonIDsInternal.addAll(meta.protectedDungeonIds) world } } @@ -767,7 +863,7 @@ class ServerWorld private constructor( world.setProperty(k, v) } - world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) + world.protectedDungeonIDsInternal.addAll(meta.protectedDungeonIds) world } }.also { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index 248957b1..0b2c30d5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -25,6 +25,7 @@ import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.world.FlyingType +import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket @@ -260,6 +261,15 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p return currentlyTrackingRegions.any { it.isInside(pos.x.roundToInt(), pos.y.roundToInt()) } } + fun isTracking(aabb: AABB): Boolean { + val cast = aabb.encasingIntAABB() + return currentlyTrackingRegions.any { it.intersect(cast) } + } + + fun isTracking(aabb: AABBi): Boolean { + return currentlyTrackingRegions.any { it.intersect(aabb) } + } + fun isTracking(entity: AbstractEntity): Boolean { return entityVersions.containsKey(entity.entityID) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 6c2439bb..56f4bb8d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -6,8 +6,11 @@ import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.ints.Int2BooleanOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap -import it.unimi.dsi.fastutil.ints.IntArraySet +import it.unimi.dsi.fastutil.ints.IntOpenHashSet +import it.unimi.dsi.fastutil.ints.IntSet +import it.unimi.dsi.fastutil.ints.IntSets import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArrayList @@ -24,6 +27,8 @@ import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.tile.ARTIFICIAL_DUNGEON_ID +import ru.dbotthepony.kstarbound.defs.tile.DESTROYED_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.defs.world.WorldStructure @@ -300,9 +305,72 @@ abstract class World, ChunkType : Chunk() + protected val dungeonBreathableInternal = Int2BooleanOpenHashMap() protected val worldProperties = JsonObject() + val protectedDungeonIDs: IntSet = IntSets.unmodifiable(protectedDungeonIDsInternal) + + var enableDungeonTileProtection = true + + fun isDungeonIDProtected(id: Int): Boolean { + if (!enableDungeonTileProtection) + return false + + return id in protectedDungeonIDsInternal + } + + fun isDungeonIDActuallyProtected(id: Int): Boolean { + return id in protectedDungeonIDsInternal + } + + open fun switchDungeonIDProtection(id: Int, enable: Boolean): Boolean { + return if (enable) { + protectedDungeonIDsInternal.remove(id) + } else { + protectedDungeonIDsInternal.add(id) + } + } + + fun enableDungeonIDProtection(id: Int): Boolean { + return switchDungeonIDProtection(id, true) + } + + fun disableDungeonIDProtection(id: Int): Boolean { + return switchDungeonIDProtection(id, false) + } + + open fun setDungeonGravity(id: Int, gravity: Vector2d?): Boolean { + if (gravity == null) { + return dungeonGravityInternal.remove(id) != null + } else { + return dungeonGravityInternal.put(id, gravity) != gravity + } + } + + fun setDungeonGravity(id: Int, gravity: Double): Boolean { + return setDungeonGravity(id, Vector2d(y = gravity)) + } + + fun unsetDungeonGravity(id: Int): Boolean { + return setDungeonGravity(id, null) + } + + open fun setDungeonBreathable(id: Int, breathable: Boolean?): Boolean { + if (breathable == null) { + val had = dungeonBreathableInternal.containsKey(id) + if (had) dungeonBreathableInternal.remove(id) + return had + } else { + return dungeonBreathableInternal.put(id, breathable) != breathable + } + } + + fun unsetDungeonBreathable(id: Int): Boolean { + return setDungeonBreathable(id, null) + } + fun copyProperties(): JsonObject = worldProperties.deepCopy() fun updateProperties(properties: JsonObject) { @@ -645,6 +713,12 @@ abstract class World, ChunkType : Chunk + fun isPlayerModified(region: AABBi): Boolean { + return anyCellSatisfies(region) { _, _, cell -> + cell.dungeonId == ARTIFICIAL_DUNGEON_ID || cell.dungeonId == DESTROYED_DUNGEON_ID + } + } + companion object { private val LOGGER = LogManager.getLogger() @@ -662,7 +736,7 @@ abstract class World, ChunkType : Chunk