Server world Lua bindings

This commit is contained in:
DBotThePony 2024-05-05 22:34:18 +07:00
parent 9104c7a46f
commit 2a4b5ffb03
Signed by: DBot
GPG Key ID: DCC23B5715498507
26 changed files with 1006 additions and 157 deletions

View File

@ -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 * `treasurechests` now can specify `treasurePool` as array
* `damageTable` can be defined directly, without referencing other JSON file (experimental feature) * `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]`) * Tree biome placeables now have `variantsRange` (defaults to `[1, 1]`) and `subVariantsRange` (defaults to `[2, 2]`)
* `variantsRange` is responsible for "stem-foliage" combinations * `variantsRange` is responsible for "stem-foliage" combinations
* `subVariantsRange` is responsible for "stem-foliage" hue shift 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 * 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. * 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): * `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 ```kotlin
val material: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref val material: Registry.Ref<TileDefinition> = 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"]`) * 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. * 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 * 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}`
@ -54,11 +54,11 @@ val color: TileColor = TileColor.DEFAULT
* `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`)
### player.config ## player.config
* Inventory bags are no longer limited to 255 slots * 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) * 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: * `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`. * `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]`) * `color` (defaults to white `[255, 255, 255, 255]`)
@ -67,7 +67,7 @@ val color: TileColor = TileColor.DEFAULT
* `centered` (defaults to `true`) * `centered` (defaults to `true`)
* `fullbright` (defaults to `false`) * `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 * `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: * 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.) * 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 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. * 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. * 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` 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. * `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`) * 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) * 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 * `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)) * `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. * `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`) * 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 * 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` * Removed `random:addEntropy`
#### animator ## animator
* Added `animator.targetRotationAngle(rotationGroup: string): double` * Added `animator.targetRotationAngle(rotationGroup: string): double`
* Added `animator.hasRotationGroup(rotationGroup: string): boolean` * Added `animator.hasRotationGroup(rotationGroup: string): boolean`
@ -117,7 +117,9 @@ val color: TileColor = TileColor.DEFAULT
* Added `animator.hasEffect(effect: string): boolean` * Added `animator.hasEffect(effect: string): boolean`
* Added `animator.parts(): List<string>` * Added `animator.parts(): List<string>`
#### world ## world
#### Additions
* Added `world.liquidNamesAlongLine(start: Vector2d, end: Vector2d): List<LiquidState>`, will return Liquid' name instead of its ID * Added `world.liquidNamesAlongLine(start: Vector2d, end: Vector2d): List<LiquidState>`, will return Liquid' name instead of its ID
* Added `world.liquidNameAt(at: Vector2i): LiquidState?`, 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<EntityID>` * Added `world.playerLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>` * Added `world.objectLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>` * Added `world.loungeableLineQuery(p0: Vector2d, p1: Vector2d, options: Table?): List<EntityID>`
* `world.entityCanDamage(source: EntityID, target: EntityID): Boolean` now properly accounts for case when `source == target` * Added `world.loadUniqueEntityAsync(id: String): RpcPromise<EntityID>`
* Added `world.findUniqueEntityAsync(id: String): RpcPromise<Vector2d?>`
* `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<Boolean>`
* Added `world.tryPlaceDungeonAsync(name: String, position: Vector2d, dungeonID: Int?, seed: Long?): RpcPromise<Boolean>`
#### Changes
* `world.entityHandItem(id: EntityID, hand: String): String` now accepts `"secondary"` as `hand` argument (in addition to `"primary"`/`"alt"`) * `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.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<nil>` 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<EntityID>`
* `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<Double, Vector2d>)` 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` * `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) * **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 * 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 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 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` * 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) * 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. * 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. * 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 * Collision is now determined using hull instead of rectangle

View File

@ -8,15 +8,19 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import java.util.Collections import java.util.Collections
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.collections.set import kotlin.collections.set
import kotlin.concurrent.read
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.concurrent.write
class Registry<T : Any>(val name: String) { class Registry<T : Any>(val name: String) {
private val keysInternal = HashMap<String, Impl>() private val keysInternal = HashMap<String, Impl>()
@ -25,13 +29,18 @@ class Registry<T : Any>(val name: String) {
private val idRefs = Int2ObjectOpenHashMap<RefImpl>() private val idRefs = Int2ObjectOpenHashMap<RefImpl>()
private val backlog = ConcurrentLinkedQueue<Runnable>() private val backlog = ConcurrentLinkedQueue<Runnable>()
private val lock = ReentrantReadWriteLock()
private var hasBeenValidated = false
private val loggedMisses = ObjectOpenHashSet<String>()
// it is much cheaper to queue registry additions rather than locking during high congestion // it is much cheaper to queue registry additions rather than locking during high congestion
fun add(task: Runnable) { fun add(task: Runnable) {
backlog.add(task) backlog.add(task)
} }
fun finishLoad() { fun finishLoad() {
lock.withLock { lock.write {
var next = backlog.poll() var next = backlog.poll()
while (next != null) { while (next != null) {
@ -41,8 +50,6 @@ class Registry<T : Any>(val name: String) {
} }
} }
private val lock = ReentrantLock()
val keys: Map<String, Entry<T>> = Collections.unmodifiableMap(keysInternal) val keys: Map<String, Entry<T>> = Collections.unmodifiableMap(keysInternal)
val ids: Int2ObjectMap<out Entry<T>> = Int2ObjectMaps.unmodifiable(idsInternal) val ids: Int2ObjectMap<out Entry<T>> = Int2ObjectMaps.unmodifiable(idsInternal)
@ -127,8 +134,33 @@ class Registry<T : Any>(val name: String) {
get() = this@Registry get() = this@Registry
} }
operator fun get(index: String): Entry<T>? = lock.withLock { keysInternal[index] } operator fun get(index: String): Entry<T>? {
operator fun get(index: Int): Entry<T>? = lock.withLock { idsInternal[index] } 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<T>? {
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<T> { fun getOrThrow(index: String): Entry<T> {
return get(index) ?: throw NoSuchElementException("No such $name: $index") return get(index) ?: throw NoSuchElementException("No such $name: $index")
@ -138,47 +170,61 @@ class Registry<T : Any>(val name: String) {
return get(index) ?: throw NoSuchElementException("No such $name: $index") return get(index) ?: throw NoSuchElementException("No such $name: $index")
} }
fun ref(index: String): Ref<T> = lock.withLock { fun ref(index: String): Ref<T> = lock.write {
keyRefs.computeIfAbsent(index, Object2ObjectFunction { keyRefs.computeIfAbsent(index, Object2ObjectFunction {
val ref = RefImpl(Either.left(it as String)) val ref = RefImpl(Either.left(it as String))
ref.entry = keysInternal[it] ref.entry = keysInternal[it]
if (hasBeenValidated && ref.entry == null && loggedMisses.add(it)) {
LOGGER.warn("No such $name: $it")
}
ref ref
}).also { }).also {
it.references++ it.references++
} }
} }
fun ref(index: Int): Ref<T> = lock.withLock { fun ref(index: Int): Ref<T> = lock.write {
idRefs.computeIfAbsent(index, Int2ObjectFunction { idRefs.computeIfAbsent(index, Int2ObjectFunction {
val ref = RefImpl(Either.right(it)) val ref = RefImpl(Either.right(it))
ref.entry = idsInternal[it] ref.entry = idsInternal[it]
if (hasBeenValidated && ref.entry == null && loggedMisses.add(it.toString())) {
LOGGER.warn("No such $name: ID $it")
}
ref ref
}).also { }).also {
it.references++ it.references++
} }
} }
operator fun contains(index: String) = lock.withLock { index in keysInternal } operator fun contains(index: String) = lock.read { index in keysInternal }
operator fun contains(index: Int) = lock.withLock { index in idsInternal } operator fun contains(index: Int) = lock.read { index in idsInternal }
fun validate(): Boolean { fun validate(): Boolean {
var valid = true hasBeenValidated = true
keyRefs.values.forEach { lock.read {
if (!it.isPresent && it.key.left() != "") { var valid = true
LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)")
valid = false 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 { idRefs.values.forEach {
if (!it.isPresent) { if (!it.isPresent) {
LOGGER.warn("Registry '$name' reference with ID '${it.key.right()}' is not bound to value, expect problems (referenced ${it.references} times)") LOGGER.warn("Registry '$name' reference with ID '${it.key.right()}' is not bound to value, expect problems (referenced ${it.references} times)")
valid = false valid = false
}
} }
}
return valid return valid
}
} }
fun add( fun add(
@ -191,7 +237,7 @@ class Registry<T : Any>(val name: String) {
): Entry<T> { ): Entry<T> {
require(key != "") { "Adding $name with empty name (empty name is reserved)" } require(key != "") { "Adding $name with empty name (empty name is reserved)" }
lock.withLock { lock.write {
if (key in keysInternal) { if (key in keysInternal) {
LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})") LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
} }

View File

@ -205,7 +205,17 @@ data class DungeonDefinition(
world.applyFinalTouches() 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<DungeonWorld> { 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<DungeonWorld> {
require(dungeonID in 0 .. NO_DUNGEON_ID) { "Dungeon ID out of range: $dungeonID" } require(dungeonID in 0 .. NO_DUNGEON_ID) { "Dungeon ID out of range: $dungeonID" }
val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends) val dungeonWorld = DungeonWorld(world, random, if (markSurfaceAndTerrain) y else null, terrainSurfaceSpaceExtends)
@ -218,36 +228,32 @@ data class DungeonDefinition(
val anchor = validAnchors.random(world.random) val anchor = validAnchors.random(world.random)
return CoroutineScope(Starbound.COROUTINES) return scope.async {
.async { if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, dungeonWorld)) {
if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, dungeonWorld)) { generate0(anchor, dungeonWorld, x, y - anchor.placementLevelConstraint, forcePlacement, dungeonID)
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<DungeonWorld> {
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)
if (commit) { if (commit) {
dungeonWorld.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<DungeonWorld> {
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 { companion object {

View File

@ -3,7 +3,6 @@ package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.math.AABBi 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.setPlayerSpawn(playerStart!!, false)
parent.eventLoop.supplyAsync { parent.eventLoop.supplyAsync {
parent.enableDungeonTileProtection = false
for ((obj, direction) in placedObjects) { for ((obj, direction) in placedObjects) {
try { try {
val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction, true) 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) LOGGER.error("Exception while applying dungeon wiring group", err)
} }
} }
parent.enableDungeonTileProtection = true
}.await() }.await()
} finally { } finally {
tickets.forEach { it.cancel() } tickets.forEach { it.cancel() }

View File

@ -70,7 +70,7 @@ data class ObjectOrientation(
val cell = world.chunkMap.getCell(it + position) val cell = world.chunkMap.getCell(it + position)
//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 || cell.dungeonId !in world.protectedDungeonIDs) cell.foreground.material.isEmptyTile && (ignoreProtectedDungeons || world.isDungeonIDProtected(cell.dungeonId))
} }
if (valid) { if (valid) {

View File

@ -110,7 +110,7 @@ const val MICRO_DUNGEON_ID = 65533
// meta dungeon signalling player built structures // meta dungeon signalling player built structures
const val ARTIFICIAL_DUNGEON_ID = 65532 const val ARTIFICIAL_DUNGEON_ID = 65532
// indicates a block that has been destroyed // 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 // dungeonId for zero-g areas with and without tile protection
const val ZERO_GRAVITY_DUNGEON_ID = 65525 const val ZERO_GRAVITY_DUNGEON_ID = 65525
const val PROTECTED_ZERO_GRAVITY_DUNGEON_ID = 65524 const val PROTECTED_ZERO_GRAVITY_DUNGEON_ID = 65524

View File

@ -50,10 +50,10 @@ enum class FlyingType(override val jsonName: String) : IStringSerializable {
} }
} }
enum class WarpPhase { enum class WarpPhase(override val jsonName: String) : IStringSerializable {
SLOWING_DOWN, SLOWING_DOWN("slowingdown"),
MAINTAIN, MAINTAIN("maintain"),
SPEEDING_UP; SPEEDING_UP("speedingup");
} }
enum class SkyOrbiterType(override val jsonName: String) : IStringSerializable { enum class SkyOrbiterType(override val jsonName: String) : IStringSerializable {

View File

@ -171,19 +171,19 @@ class WorldTemplate(val geometry: WorldGeometry) {
class PotentialBiomeItems( class PotentialBiomeItems(
// Potential items that would spawn at the given block assuming it is at // Potential items that would spawn at the given block assuming it is at
val surfaceBiomeItems: List<BiomePlaceables.Placement>, val surfaceBiomeItems: List<BiomePlaceables.Placement> = listOf(),
// ... Or on a cave surface. // ... Or on a cave surface.
val caveSurfaceBiomeItems: List<BiomePlaceables.Placement>, val caveSurfaceBiomeItems: List<BiomePlaceables.Placement> = listOf(),
// ... Or on a cave ceiling. // ... Or on a cave ceiling.
val caveCeilingBiomeItems: List<BiomePlaceables.Placement>, val caveCeilingBiomeItems: List<BiomePlaceables.Placement> = listOf(),
// ... Or on a cave background wall. // ... Or on a cave background wall.
val caveBackgroundBiomeItems: List<BiomePlaceables.Placement>, val caveBackgroundBiomeItems: List<BiomePlaceables.Placement> = listOf(),
// ... Or in the ocean // ... Or in the ocean
val oceanItems: List<BiomePlaceables.Placement>, val oceanItems: List<BiomePlaceables.Placement> = listOf(),
) )
fun potentialBiomeItemsAt(x: Int, y: Int): PotentialBiomeItems { fun potentialBiomeItemsAt(x: Int, y: Int): PotentialBiomeItems {
@ -201,7 +201,7 @@ class WorldTemplate(val geometry: WorldGeometry) {
) )
} }
fun validBiomeItemsAt(x: Int, y: Int): List<BiomePlaceables.Placement> { fun validBiomeItemsAt(x: Int, y: Int, potential: PotentialBiomeItems = potentialBiomeItemsAt(x, y)): List<BiomePlaceables.Placement> {
val thisBlock = cellInfo(geometry.x.cell(x), geometry.y.cell(y)) val thisBlock = cellInfo(geometry.x.cell(x), geometry.y.cell(y))
if (thisBlock.biomeTransition) if (thisBlock.biomeTransition)
@ -210,7 +210,6 @@ class WorldTemplate(val geometry: WorldGeometry) {
val result = ObjectArrayList<BiomePlaceables.Placement>() val result = ObjectArrayList<BiomePlaceables.Placement>()
val lowerBlock = cellInfo(geometry.x.cell(x), geometry.y.cell(y - 1)) 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 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) if (!lowerBlock.biomeTransition && lowerBlock.terrain && !thisBlock.terrain && !lowerBlock.foregroundCave)
result.addAll(potential.surfaceBiomeItems) result.addAll(potential.surfaceBiomeItems)

View File

@ -81,7 +81,7 @@ enum class JsonPatch(val key: String) {
abstract fun apply(base: JsonElement, data: JsonObject): JsonElement 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 { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()

View File

@ -27,6 +27,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2f import ru.dbotthepony.kstarbound.math.vector.Vector2f
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.world.physics.Poly 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())) 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 { fun toJsonFromLua(value: Any?): JsonElement {
return when (value) { return when (value) {
null, is JsonNull -> JsonNull.INSTANCE null, is JsonNull -> JsonNull.INSTANCE

View File

@ -1,9 +1,339 @@
package ru.dbotthepony.kstarbound.lua.bindings 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.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.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.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<ByteString>() {
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<AbstractEntity?>
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) { 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<BiomePlaceables.DistributionItem>()
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
)
)
}
} }

View File

@ -168,6 +168,14 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
returnBuffer.setTo(self.sky.flyingType.jsonName) 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? -> callbacks["magnitude"] = luaFunction { arg1: Table, arg2: Table? ->
if (arg2 == null) { if (arg2 == null) {
returnBuffer.setTo(toVector2d(arg1).length) returnBuffer.setTo(toVector2d(arg1).length)
@ -533,7 +541,7 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
returnLiquid(cell.liquid, true) 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["findPlatformerPath"] = luaStub("findPlatformerPath")
callbacks["platformerPathStart"] = luaStub("platformerPathStart") callbacks["platformerPathStart"] = luaStub("platformerPathStart")

View File

@ -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") { callbacks["sendEntityMessage"] = luaFunctionN("sendEntityMessage") {
val id = it.nextAny() val id = it.nextAny()
val func = it.nextString().decode() val func = it.nextString().decode()

View File

@ -34,8 +34,8 @@ data class LegacyNetworkTileState(
} }
companion object { companion object {
val EMPTY = LegacyNetworkTileState(0, 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, 0, 0) val NULL = LegacyNetworkTileState(BuiltinMetaMaterials.NULL.id!!, 0, 0, BuiltinMetaMaterials.EMPTY_MOD.id!!, 0)
fun read(stream: DataInputStream): LegacyNetworkTileState { fun read(stream: DataInputStream): LegacyNetworkTileState {
val tile = stream.readUnsignedShort() val tile = stream.readUnsignedShort()

View File

@ -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.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileModificationFailurePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.TileModificationFailurePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket 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.UpdateWorldPropertiesPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket
@ -418,10 +421,10 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("ProtocolResponse") LEGACY.skip("ProtocolResponse")
// Packets sent universe server -> universe client // Packets sent universe server -> universe client
LEGACY.add(::ServerDisconnectPacket) // ServerDisconnect LEGACY.add(::ServerDisconnectPacket)
LEGACY.add(::ConnectSuccessPacket) // ConnectSuccess LEGACY.add(::ConnectSuccessPacket)
LEGACY.add(::ConnectFailurePacket) LEGACY.add(::ConnectFailurePacket)
LEGACY.add(::HandshakeChallengePacket) // HandshakeChallenge LEGACY.add(::HandshakeChallengePacket)
LEGACY.add(::ChatReceivePacket) LEGACY.add(::ChatReceivePacket)
LEGACY.add(::UniverseTimeUpdatePacket) LEGACY.add(::UniverseTimeUpdatePacket)
LEGACY.add(::CelestialResponsePacket) LEGACY.add(::CelestialResponsePacket)
@ -431,9 +434,9 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(::ServerInfoPacket) LEGACY.add(::ServerInfoPacket)
// Packets sent universe client -> universe server // Packets sent universe client -> universe server
LEGACY.add(::ClientConnectPacket) // ClientConnect LEGACY.add(::ClientConnectPacket)
LEGACY.add(ClientDisconnectRequestPacket::read) LEGACY.add(ClientDisconnectRequestPacket::read)
LEGACY.add(::HandshakeResponsePacket) // HandshakeResponse LEGACY.add(::HandshakeResponsePacket)
LEGACY.add(::PlayerWarpPacket) LEGACY.add(::PlayerWarpPacket)
LEGACY.add(::FlyShipPacket) LEGACY.add(::FlyShipPacket)
LEGACY.add(::ChatSendPacket) LEGACY.add(::ChatSendPacket)
@ -444,7 +447,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(ClientContextUpdatePacket::read) LEGACY.add(ClientContextUpdatePacket::read)
// Packets sent world server -> world client // Packets sent world server -> world client
LEGACY.add(::WorldStartPacket) // WorldStart LEGACY.add(::WorldStartPacket)
LEGACY.add(::WorldStopPacket) LEGACY.add(::WorldStopPacket)
LEGACY.skip("WorldLayoutUpdate") LEGACY.skip("WorldLayoutUpdate")
LEGACY.skip("WorldParametersUpdate") LEGACY.skip("WorldParametersUpdate")
@ -456,9 +459,9 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(::TileModificationFailurePacket) LEGACY.add(::TileModificationFailurePacket)
LEGACY.add(::GiveItemPacket) LEGACY.add(::GiveItemPacket)
LEGACY.add(::EnvironmentUpdatePacket) LEGACY.add(::EnvironmentUpdatePacket)
LEGACY.skip("UpdateTileProtection") LEGACY.add(::UpdateDungeonProtectionPacket)
LEGACY.skip("SetDungeonGravity") LEGACY.add(UpdateDungeonGravityPacket::read)
LEGACY.skip("SetDungeonBreathable") LEGACY.add(::UpdateDungeonBreathablePacket)
LEGACY.add(::SetPlayerStartPacket) LEGACY.add(::SetPlayerStartPacket)
LEGACY.add(FindUniqueEntityResponsePacket::read) LEGACY.add(FindUniqueEntityResponsePacket::read)
LEGACY.add(PongPacket::read) LEGACY.add(PongPacket::read)

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -5,6 +5,7 @@ import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
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.async import kotlinx.coroutines.async
@ -40,6 +41,7 @@ import ru.dbotthepony.kstarbound.util.toStarboundString
import ru.dbotthepony.kstarbound.util.uuidFromStarboundString import ru.dbotthepony.kstarbound.util.uuidFromStarboundString
import java.io.File import java.io.File
import java.sql.DriverManager import java.sql.DriverManager
import java.util.Collections
import java.util.UUID import java.util.UUID
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -73,21 +75,62 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
database.createStatement().use { database.createStatement().use {
it.execute("PRAGMA journal_mode=WAL") it.execute("PRAGMA journal_mode=WAL")
it.execute("PRAGMA synchronous=NORMAL") 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("""
it.execute("CREATE TABLE IF NOT EXISTS `client_context` (`uuid` VARCHAR NOT NULL PRIMARY KEY, `data` BLOB NOT NULL)") CREATE TABLE IF NOT EXISTS "metadata" (
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`))") "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 database.autoCommit = false
} }
private val lookupMetadata = database.prepareStatement("SELECT `value` FROM `metadata` WHERE `key` = ?") private val lookupMetadata = database.prepareStatement("""
private val writeMetadata = database.prepareStatement("REPLACE INTO `metadata` (`key`, `value`) VALUES (?, ?)") SELECT "value" FROM "metadata" WHERE "key" = ?
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 (?, ?)")
private val lookupSystemWorld = database.prepareStatement("SELECT `data` FROM `system_worlds` WHERE `x` = ? AND `y` = ? AND `z` = ?") private val writeMetadata = database.prepareStatement("""
private val writeSystemWorld = database.prepareStatement("REPLACE INTO `system_worlds` (`x`, `y`, `z`, `data`) VALUES (?, ?, ?, ?)") 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<JsonElement> { private fun getMetadata(key: String): KOptional<JsonElement> {
lookupMetadata.setString(1, key) lookupMetadata.setString(1, key)
@ -129,6 +172,64 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
} }
} }
private val universeFlags = Collections.synchronizedSet(ObjectOpenHashSet<String>())
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<String> {
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<JsonElement?> { fun loadServerWorldData(pos: Vector3i): CompletableFuture<JsonElement?> {
return supplyAsync { return supplyAsync {
lookupSystemWorld.setInt(1, pos.x) lookupSystemWorld.setInt(1, pos.x)

View File

@ -50,7 +50,6 @@ import java.util.zip.InflaterInputStream
class NativeLocalWorldStorage(file: File?) : WorldStorage() { class NativeLocalWorldStorage(file: File?) : WorldStorage() {
private val connection: Connection private val connection: Connection
private val executor = CarriedExecutor(Starbound.IO_EXECUTOR) private val executor = CarriedExecutor(Starbound.IO_EXECUTOR)
private val cleaner: Cleaner.Cleanable
init { init {
if (file == null) { if (file == null) {
@ -58,14 +57,10 @@ class NativeLocalWorldStorage(file: File?) : WorldStorage() {
} else { } else {
connection = DriverManager.getConnection("jdbc:sqlite:${file.absolutePath.replace('\\', '/')}") 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() { override fun commit() {
executor.execute { connection.commit() } executor.execute { connection.commit() }
} }

View File

@ -14,6 +14,7 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
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.FIRST_RESERVED_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.MICRO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
@ -408,7 +409,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
var damage = damage var damage = damage
var result = TileDamageResult.NORMAL var result = TileDamageResult.NORMAL
if (cell.dungeonId in world.protectedDungeonIDs) { if (world.isDungeonIDProtected(cell.dungeonId)) {
damage = damage.copy(type = TileDamageType.PROTECTED) damage = damage.copy(type = TileDamageType.PROTECTED)
result = TileDamageResult.PROTECTED result = TileDamageResult.PROTECTED
} }
@ -430,6 +431,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
val copyHealth = health.copy() val copyHealth = health.copy()
val mCell = cell.mutable() val mCell = cell.mutable()
val mTile = mCell.tile(isBackground) val mTile = mCell.tile(isBackground)
mCell.dungeonId = DESTROYED_DUNGEON_ID
if (health.isHarvested && mTile.material.value.itemDrop != null) { if (health.isHarvested && mTile.material.value.itemDrop != null) {
drops.add(ItemDescriptor(mTile.material.value.itemDrop!!, 1L)) drops.add(ItemDescriptor(mTile.material.value.itemDrop!!, 1L))
@ -728,17 +730,24 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
private val missingDungeonNames = ObjectAVLTreeSet<String>() private val missingDungeonNames = ObjectAVLTreeSet<String>()
private data class PotentialMicrodungeon(val future: CompletableFuture<Vector2i>?, val item: BiomePlaceables.Placement, val dungeonId: Int)
private suspend fun placeMicroDungeons() { private suspend fun placeMicroDungeons() {
val placements = CompletableFuture.supplyAsync(Supplier { val placements = CompletableFuture.supplyAsync(Supplier {
val placements = ArrayList<BiomePlaceables.Placement>() val placements = ArrayList<PotentialMicrodungeon>()
for (x in 0 until width) { for (x in 0 until width) {
for (y in 0 until height) { 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 placements
}, Starbound.EXECUTOR).await() }, Starbound.EXECUTOR).await()
@ -749,7 +758,9 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
val random = random(staticRandom64(world.template.seed, pos.x, pos.y, "microdungeon placement")) val random = random(staticRandom64(world.template.seed, pos.x, pos.y, "microdungeon placement"))
for (placement in placements) { for ((future, placement, dungeonId) in placements) {
if (future?.isDone == true) continue
if (placement.item is BiomePlaceables.MicroDungeon) { if (placement.item is BiomePlaceables.MicroDungeon) {
if (placement.item.microdungeons.isEmpty()) if (placement.item.microdungeons.isEmpty())
continue // ??? continue // ???
@ -773,28 +784,30 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
if (!bounds.isInside(pos) || !bounds.isInside(pos + anchor.reader.size - Vector2i.POSITIVE_XY)) if (!bounds.isInside(pos) || !bounds.isInside(pos + anchor.reader.size - Vector2i.POSITIVE_XY))
continue continue
val placed = world.queueMicrodungeonPlacement(pos.x, pos.y) { val placed = world.scheduleMicrodungeonPlacement(pos.x, pos.y) {
// this is quite ugly code flow, but we should try to avoid double-walking // this is quite ugly code flow, but we should try to avoid double-walking
// over all dungeon tiles (DungeonTile#canPlace already checks for absence of other dungeons on their place, // over all dungeon tiles (DungeonTile#canPlace already checks for absence of other dungeons on their place,
// so we only need to tell DungeonPart to not force-place) // so we only need to tell DungeonPart to not force-place)
if (it.measureAndSuspend { anchor.canPlace(pos.x, pos.y, world, false) }) { if (it.measureAndSuspend { anchor.canPlace(pos.x, pos.y, world, false) }) {
try { try {
val dungeonWorld = dungeon.value!!.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID, commit = false).await() val dungeonWorld = dungeon.value!!.build(anchor, world, random, pos.x, pos.y, dungeonID = dungeonId, commit = false).await()
val placementIsFree = dungeonWorld.touchedTiles.all { world.getCell(it).dungeonId == NO_DUNGEON_ID } val placementIsFree = dungeonWorld.touchedTiles.all { world.getCell(it).dungeonId == NO_DUNGEON_ID }
if (placementIsFree) { if (placementIsFree) {
dungeonWorld.commit() dungeonWorld.commit()
future?.complete(pos)
} else { } else {
LOGGER.debug("Dungeons overlap somewhere around {} after built new dungeon, not placing {}", pos, dungeon.key.left()) LOGGER.debug("Dungeons overlap somewhere around {} after built new dungeon, not placing {}", pos, dungeon.key.left())
} }
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Error while generating microdungeon ${dungeon.key.left()} at $pos", err) LOGGER.error("Error while generating microdungeon ${dungeon.key.left()} at $pos", err)
future?.completeExceptionally(err)
} }
return@queueMicrodungeonPlacement true return@scheduleMicrodungeonPlacement true
} }
return@queueMicrodungeonPlacement false return@scheduleMicrodungeonPlacement false
} }
if (placed) { if (placed) {
@ -1123,39 +1136,54 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
private data class Determined(val future: CompletableFuture<Vector2i>?, val item: BiomePlaceables.Placement, val pos: Vector2i)
private data class Prepared(val future: CompletableFuture<Vector2i>?, val item: () -> Unit, val pos: Vector2i)
private suspend fun placeBiomeItems() { private suspend fun placeBiomeItems() {
val placements = CompletableFuture.supplyAsync(Supplier { val placements = CompletableFuture.supplyAsync(Supplier {
val placements = ArrayList<BiomePlaceables.Placement>() val placements = ArrayList<Determined>()
for (x in 0 until width) { for (x in 0 until width) {
for (y in 0 until height) { for (y in 0 until height) {
if (cells.value[x, y].dungeonId == NO_DUNGEON_ID) { 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 random = random(staticRandom64(world.template.seed, pos.x, pos.y, "biome placement"))
val funcs = ArrayList<() -> Unit>() val funcs = ArrayList<Prepared>()
for ((future, placement, pos) in placements) {
if (future?.isDone == true) continue
for (placement in placements) {
try { 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) { } catch (err: Throwable) {
LOGGER.error("Exception while evaluating biome placeables for chunk $pos in $world", err) LOGGER.error("Exception while evaluating biome placeables for chunk $pos in $world", err)
future?.completeExceptionally(err)
} }
} }
funcs funcs
}, Starbound.EXECUTOR).await() }, Starbound.EXECUTOR).await()
for (placement in placements) { for ((future, placement, pos) in placements) {
if (future?.isDone == true) continue
try { try {
placement() placement()
future?.complete(pos)
} catch (err: Throwable) { } catch (err: Throwable) {
LOGGER.error("Exception while placing biome placeables for chunk $pos in $world", err) LOGGER.error("Exception while placing biome placeables for chunk $pos in $world", err)
future?.completeExceptionally(err)
} }
} }
} }

View File

@ -1,9 +1,10 @@
package ru.dbotthepony.kstarbound.server.world 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.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject 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.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
@ -28,6 +29,8 @@ 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.world.BiomePlaceables
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.json.builder.JsonFactory 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.world.TileModification
import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket 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.network.packets.clientbound.UpdateWorldPropertiesPacket
import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.server.ServerConnection 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.TileEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.util.Collections
import java.util.PriorityQueue import java.util.PriorityQueue
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
@ -137,7 +144,7 @@ class ServerWorld private constructor(
adjustPlayerStart = adjustPlayerSpawn, adjustPlayerStart = adjustPlayerSpawn,
worldTemplate = if (storage is LegacyWorldStorage) Starbound.legacyJson { template.toJson() } else template.toJson(), worldTemplate = if (storage is LegacyWorldStorage) Starbound.legacyJson { template.toJson() } else template.toJson(),
centralStructure = centralStructure, centralStructure = centralStructure,
protectedDungeonIds = protectedDungeonIDs, protectedDungeonIds = ImmutableSet.copyOf(protectedDungeonIDsInternal),
worldProperties = copyProperties(), worldProperties = copyProperties(),
spawningEnabled = true spawningEnabled = true
) )
@ -223,7 +230,7 @@ class ServerWorld private constructor(
// this is used for scheduling and resolving microdungeon placement // 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 // tries to early-resolve artifacts like this: https://i.dbotthepony.ru/2024/04/28/gb6GdbLox7.png
suspend fun <T> queueMicrodungeonPlacement(x: Int, y: Int, callback: suspend (ExecutionTimePacer) -> T): T { suspend fun <T> scheduleMicrodungeonPlacement(x: Int, y: Int, callback: suspend (ExecutionTimePacer) -> T): T {
return suspendCoroutine { return suspendCoroutine {
placementQueue.add(PlacementElement(x, y, placementTaskID++, callback, it)) 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<BiomePlaceables.DistributionItem>,
val future: CompletableFuture<Vector2i>,
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<QueuedPlacement>()
val queuedPlacements: List<QueuedPlacement> = Collections.unmodifiableList(queuedPlacementsInternal)
fun enqueuePlacement(placement: List<BiomePlaceables.DistributionItem>, dungeonId: Int? = null): CompletableFuture<Vector2i> {
val future = CompletableFuture<Vector2i>()
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 { override fun toString(): String {
return "Server World $worldID" return "Server World $worldID"
} }
@ -245,6 +289,54 @@ class ServerWorld private constructor(
override val connectionID: Int override val connectionID: Int
get() = 0 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 {}) * 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) { for (pos in damagePositions) {
val cell = getCell(pos) val cell = getCell(pos)
if (cell.dungeonId in protectedDungeonIDs) { if (cell.dungeonId in protectedDungeonIDsInternal) {
actualDamage = actualDamage.copy(type = TileDamageType.PROTECTED) actualDamage = actualDamage.copy(type = TileDamageType.PROTECTED)
entityDamageResults[pos] = TileDamageResult.PROTECTED entityDamageResults[pos] = TileDamageResult.PROTECTED
} else { } else {
@ -339,7 +431,7 @@ class ServerWorld private constructor(
val getCell = chunk?.getCell(pos - chunk.pos.tile) val getCell = chunk?.getCell(pos - chunk.pos.tile)
if (getCell != null) { if (getCell != null) {
if (getCell.dungeonId in protectedDungeonIDs) { if (getCell.dungeonId in protectedDungeonIDsInternal) {
actualDamage = actualDamage.copy(type = TileDamageType.PROTECTED) actualDamage = actualDamage.copy(type = TileDamageType.PROTECTED)
} }
} }
@ -383,7 +475,7 @@ class ServerWorld private constructor(
for ((pos, modification) in itr) { for ((pos, modification) in itr) {
val cell = getCell(pos) val cell = getCell(pos)
if (!ignoreTileProtection && cell.dungeonId in protectedDungeonIDs) if (!ignoreTileProtection && isDungeonIDProtected(cell.dungeonId))
continue continue
if (modification.allowed(this, pos, allowEntityOverlap)) { 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}") LOGGER.info("Placed dungeon ${dungeon.dungeon.key} at $x, ${dungeon.baseHeight}")
if (dungeon.dungeon.value.metadata.protected) { if (dungeon.dungeon.value.metadata.protected) {
protectedDungeonIDs.add(currentDungeonID) protectedDungeonIDsInternal.add(currentDungeonID)
} }
if (dungeon.dungeon.value.metadata.gravity != null) { if (dungeon.dungeon.value.metadata.gravity != null) {
@ -671,23 +763,16 @@ class ServerWorld private constructor(
return storage.findUniqueEntity(id).thenApply { it?.pos } return storage.findUniqueEntity(id).thenApply { it?.pos }
} }
override fun dispatchEntityMessage( fun loadUniqueEntity(entityID: String): CompletableFuture<AbstractEntity?> {
sourceConnection: Int,
entityID: String,
message: String,
arguments: JsonArray
): CompletableFuture<JsonElement> {
var loaded = uniqueEntities[entityID] var loaded = uniqueEntities[entityID]
if (loaded != null) { 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 { return eventLoop.scope.async {
val (chunk) = storage.findUniqueEntity(entityID).await() ?: throw MessageCallException("No such entity $entityID") val (chunk) = storage.findUniqueEntity(entityID).await() ?: return@async null
val ticket = permanentChunkTicket(chunk).await() ?: throw MessageCallException("Internal server error") val ticket = permanentChunkTicket(chunk).await() ?: throw RuntimeException("how did we end up here")
try { try {
ticket.chunk.await() ticket.chunk.await()
@ -696,16 +781,27 @@ class ServerWorld private constructor(
if (loaded == null) { if (loaded == null) {
// How? // 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") 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 { } finally {
ticket.cancel() ticket.cancel()
} }
}.asCompletableFuture() }.asCompletableFuture()
} }
override fun dispatchEntityMessage(
sourceConnection: Int,
entityID: String,
message: String,
arguments: JsonArray
): CompletableFuture<JsonElement> {
return loadUniqueEntity(entityID).thenCompose {
it ?: throw MessageCallException("No such unique entity $entityID")
it.dispatchMessage(sourceConnection, message, arguments)
}
}
@JsonFactory @JsonFactory
data class MetadataJson( data class MetadataJson(
val playerStart: Vector2d, val playerStart: Vector2d,
@ -713,7 +809,7 @@ class ServerWorld private constructor(
val adjustPlayerStart: Boolean, val adjustPlayerStart: Boolean,
val worldTemplate: JsonObject, val worldTemplate: JsonObject,
val centralStructure: WorldStructure, val centralStructure: WorldStructure,
val protectedDungeonIds: IntArraySet, val protectedDungeonIds: ImmutableSet<Int>,
val worldProperties: JsonObject, val worldProperties: JsonObject,
val spawningEnabled: Boolean val spawningEnabled: Boolean
) )
@ -743,7 +839,7 @@ class ServerWorld private constructor(
world.setProperty(k, v) world.setProperty(k, v)
} }
world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) world.protectedDungeonIDsInternal.addAll(meta.protectedDungeonIds)
world world
} }
} }
@ -767,7 +863,7 @@ class ServerWorld private constructor(
world.setProperty(k, v) world.setProperty(k, v)
} }
world.protectedDungeonIDs.addAll(meta.protectedDungeonIds) world.protectedDungeonIDsInternal.addAll(meta.protectedDungeonIds)
world world
} }
}.also { }.also {

View File

@ -25,6 +25,7 @@ import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WorldID import ru.dbotthepony.kstarbound.defs.WorldID
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.world.FlyingType import ru.dbotthepony.kstarbound.defs.world.FlyingType
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket 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()) } 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 { fun isTracking(entity: AbstractEntity): Boolean {
return entityVersions.containsKey(entity.entityID) return entityVersions.containsKey(entity.entityID)
} }

View File

@ -6,8 +6,11 @@ import com.google.gson.JsonArray
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.Int2BooleanOpenHashMap
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap 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.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArrayList 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.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.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.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.world.WorldStructure import ru.dbotthepony.kstarbound.defs.world.WorldStructure
@ -300,9 +305,72 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
var centralStructure: WorldStructure = WorldStructure() var centralStructure: WorldStructure = WorldStructure()
val protectedDungeonIDs = IntArraySet() protected val protectedDungeonIDsInternal = IntOpenHashSet()
protected val dungeonGravityInternal = Int2ObjectOpenHashMap<Vector2d>()
protected val dungeonBreathableInternal = Int2BooleanOpenHashMap()
protected val worldProperties = JsonObject() 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 copyProperties(): JsonObject = worldProperties.deepCopy()
fun updateProperties(properties: JsonObject) { fun updateProperties(properties: JsonObject) {
@ -645,6 +713,12 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
abstract fun findUniqueEntity(id: String): CompletableFuture<Vector2d?> abstract fun findUniqueEntity(id: String): CompletableFuture<Vector2d?>
fun isPlayerModified(region: AABBi): Boolean {
return anyCellSatisfies(region) { _, _, cell ->
cell.dungeonId == ARTIFICIAL_DUNGEON_ID || cell.dungeonId == DESTROYED_DUNGEON_ID
}
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
@ -662,7 +736,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
* on main world thread, so concurrent access is not needed for now. * on main world thread, so concurrent access is not needed for now.
* *
* ArrayChunkMap does ~not~ need synchronization, because 2D array is thread-safe to be read * ArrayChunkMap does ~not~ need synchronization, because 2D array is thread-safe to be read
* by multiple thread while one is writing to it (but it might leave to race condition if * by multiple thread while one is writing to it (but it might lead to race condition if
* we try to read chunks which are currently being initialized). * we try to read chunks which are currently being initialized).
*/ */
private const val CONCURRENT_SPARSE_CHUNK_MAP = false private const val CONCURRENT_SPARSE_CHUNK_MAP = false

View File

@ -39,11 +39,11 @@ sealed class AbstractTileState {
} }
fun writeLegacy(stream: DataOutputStream) { fun writeLegacy(stream: DataOutputStream) {
stream.writeShort(material.id ?: 0) stream.writeShort(material.id ?: BuiltinMetaMaterials.EMPTY.id!!)
stream.writeByte(byteHueShift()) stream.writeByte(byteHueShift())
stream.writeByte(color.ordinal) stream.writeByte(color.ordinal)
stream.writeShort(modifier.id ?: 0) stream.writeShort(modifier.id ?: BuiltinMetaMaterials.EMPTY_MOD.id!!)
stream.writeByte(byteModifierHueShift()) stream.writeByte(byteModifierHueShift())
} }

View File

@ -137,17 +137,21 @@ class ItemDropEntity() : DynamicEntity() {
return state == State.AVAILABLE && owningEntity == 0 && item.isNotEmpty return state == State.AVAILABLE && owningEntity == 0 && item.isNotEmpty
} }
fun take(by: AbstractEntity): ItemStack { fun take(by: Int): ItemStack {
if (canTake) { if (canTake) {
state = State.TAKEN state = State.TAKEN
age.set(0.0) age.set(0.0)
owningEntity = by.entityID owningEntity = by
return item.copy() return item.copy()
} }
return ItemStack.EMPTY return ItemStack.EMPTY
} }
fun take(by: AbstractEntity?): ItemStack {
return take(by?.entityID ?: 0)
}
private var stayAliveFor = -1.0 private var stayAliveFor = -1.0
override val metaBoundingBox: AABB override val metaBoundingBox: AABB