TileModification packets

This commit is contained in:
DBotThePony 2024-04-10 22:41:41 +07:00
parent b3b51aefa5
commit 7cd0f5e173
Signed by: DBot
GPG Key ID: DCC23B5715498507
32 changed files with 636 additions and 147 deletions

View File

@ -61,3 +61,4 @@ val color: TileColor = TileColor.DEFAULT
* Implemented `isConnectable`, which was planned by original developers, but scrapped in process (defaults to `true`, by default only next meta-materials have it set to false: `empty`, `null` and `boundary`) * Implemented `isConnectable`, which was planned by original developers, but scrapped in process (defaults to `true`, by default only next meta-materials have it set to false: `empty`, `null` and `boundary`)
* Used by object and plant anchoring code to determine valid placement * Used by object and plant anchoring code to determine valid placement
* 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)

View File

@ -15,7 +15,7 @@ import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
class ChunkCellsPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClientPacket { class ChunkCellsPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readChunkPos(), stream.readCollection { MutableCell().read(stream).immutable() }) constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readChunkPos(), stream.readCollection { MutableCell().readLegacy(stream).immutable() })
constructor(chunk: Chunk<*, *>) : this(chunk.pos, ArrayList<ImmutableCell>(CHUNK_SIZE * CHUNK_SIZE).also { constructor(chunk: Chunk<*, *>) : this(chunk.pos, ArrayList<ImmutableCell>(CHUNK_SIZE * CHUNK_SIZE).also {
for (x in 0 until CHUNK_SIZE) { for (x in 0 until CHUNK_SIZE) {
for (y in 0 until CHUNK_SIZE) { for (y in 0 until CHUNK_SIZE) {

View File

@ -260,7 +260,7 @@ class DungeonPart(data: JsonData) {
// Mark entities for removal, and remove them when dungeon is actually placed in world // Mark entities for removal, and remove them when dungeon is actually placed in world
world.waitForRegionAndJoin(Vector2i(x, y), reader.size) { world.waitForRegionAndJoin(Vector2i(x, y), reader.size) {
val entities = world.parent.entityIndex.query(AABBi(Vector2i(x, y), Vector2i(x, y) + reader.size)) val entities = world.parent.entityIndex.query(AABBi(Vector2i(x, y), Vector2i(x, y) + reader.size).toDoubleAABB())
for (entity in entities) { for (entity in entities) {
if (entity !is TileEntity) if (entity !is TileEntity)

View File

@ -34,7 +34,6 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.Side
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import kotlin.math.PI import kotlin.math.PI

View File

@ -57,12 +57,18 @@ val Registry.Entry<TileDefinition>.isObjectPlatformTile: Boolean
val Registry.Entry<TileDefinition>.supportsModifiers: Boolean val Registry.Entry<TileDefinition>.supportsModifiers: Boolean
get() = !value.isMeta && value.supportsMods get() = !value.isMeta && value.supportsMods
/**
* whenever modifier is empty (always supported), or material supports it
*/
fun Registry.Entry<TileDefinition>.supportsModifier(modifier: Registry.Entry<TileModifierDefinition>): Boolean { fun Registry.Entry<TileDefinition>.supportsModifier(modifier: Registry.Entry<TileModifierDefinition>): Boolean {
return !value.isMeta && value.supportsMods && !modifier.value.isMeta return modifier == BuiltinMetaMaterials.EMPTY_MOD || value.supportsModifier(modifier)
} }
/**
* whenever modifier is empty (always supported), or material supports it
*/
fun Registry.Entry<TileDefinition>.supportsModifier(modifier: Registry.Ref<TileModifierDefinition>): Boolean { fun Registry.Entry<TileDefinition>.supportsModifier(modifier: Registry.Ref<TileModifierDefinition>): Boolean {
return !value.isMeta && value.supportsMods && modifier.isPresent && !modifier.value!!.isMeta return !modifier.isPresent || value.supportsModifier(modifier.entry!!)
} }
val Registry.Entry<LiquidDefinition>.isEmptyLiquid: Boolean val Registry.Entry<LiquidDefinition>.isEmptyLiquid: Boolean
@ -80,6 +86,18 @@ val Registry.Ref<LiquidDefinition>.orEmptyLiquid: Registry.Entry<LiquidDefinitio
val Registry.Ref<LiquidDefinition>.isNotEmptyLiquid: Boolean val Registry.Ref<LiquidDefinition>.isNotEmptyLiquid: Boolean
get() = !isEmptyLiquid get() = !isEmptyLiquid
val Registry.Ref<TileModifierDefinition>.isEmptyModifier: Boolean
get() = entry == null || entry == BuiltinMetaMaterials.EMPTY_MOD
val Registry.Ref<TileModifierDefinition>.isNotEmptyModifier: Boolean
get() = !isEmptyModifier
val Registry.Entry<TileModifierDefinition>.isEmptyModifier: Boolean
get() = this == BuiltinMetaMaterials.EMPTY_MOD
val Registry.Entry<TileModifierDefinition>.isNotEmptyModifier: Boolean
get() = !isEmptyModifier
val Registry.Ref<TileModifierDefinition>.orEmptyModifier: Registry.Entry<TileModifierDefinition> val Registry.Ref<TileModifierDefinition>.orEmptyModifier: Registry.Entry<TileModifierDefinition>
get() = entry ?: BuiltinMetaMaterials.EMPTY_MOD get() = entry ?: BuiltinMetaMaterials.EMPTY_MOD

View File

@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
@ -56,6 +57,10 @@ data class TileDefinition(
require(materialId > 0) { "Invalid tile ID $materialId" } require(materialId > 0) { "Invalid tile ID $materialId" }
} }
fun supportsModifier(modifier: Registry.Entry<TileModifierDefinition>): Boolean {
return !isMeta && !modifier.value.isMeta && supportsMods
}
val actualDamageTable: TileDamageConfig by lazy { val actualDamageTable: TileDamageConfig by lazy {
val dmg = damageTable.value ?: TileDamageConfig.EMPTY val dmg = damageTable.value ?: TileDamageConfig.EMPTY

View File

@ -52,6 +52,7 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemShipDestroyPa
import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SystemWorldUpdatePacket
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.UniverseTimeUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UniverseTimeUpdatePacket
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
@ -63,6 +64,7 @@ import ru.dbotthepony.kstarbound.network.packets.serverbound.DamageTileGroupPack
import ru.dbotthepony.kstarbound.network.packets.serverbound.EntityInteractPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.EntityInteractPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.FlyShipPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.FlyShipPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ModifyTileListPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.PlayerWarpPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.PlayerWarpPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.RequestDropPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.RequestDropPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket
@ -443,7 +445,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(LegacyTileUpdatePacket::read) LEGACY.add(LegacyTileUpdatePacket::read)
LEGACY.skip("TileLiquidUpdate") LEGACY.skip("TileLiquidUpdate")
LEGACY.add(::TileDamageUpdatePacket) LEGACY.add(::TileDamageUpdatePacket)
LEGACY.skip("TileModificationFailure") LEGACY.add(::TileModificationFailurePacket)
LEGACY.add(::GiveItemPacket) LEGACY.add(::GiveItemPacket)
LEGACY.add(::EnvironmentUpdatePacket) LEGACY.add(::EnvironmentUpdatePacket)
LEGACY.skip("UpdateTileProtection") LEGACY.skip("UpdateTileProtection")
@ -454,7 +456,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.add(PongPacket::read) LEGACY.add(PongPacket::read)
// Packets sent world client -> world server // Packets sent world client -> world server
LEGACY.skip("ModifyTileList") LEGACY.add(::ModifyTileListPacket)
LEGACY.add(::DamageTileGroupPacket) LEGACY.add(::DamageTileGroupPacket)
LEGACY.skip("CollectLiquid") LEGACY.skip("CollectLiquid")
LEGACY.add(::RequestDropPacket) LEGACY.add(::RequestDropPacket)

View File

@ -0,0 +1,24 @@
package ru.dbotthepony.kstarbound.network.packets.clientbound
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readVector2i
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.world.TileModification
import java.io.DataInputStream
import java.io.DataOutputStream
class TileModificationFailurePacket(val modifications: Collection<Pair<Vector2i, TileModification>>) : IClientPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readCollection { stream.readVector2i() to TileModification.read(stream, isLegacy) })
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeCollection(modifications) { stream.writeStruct2i(it.first); it.second.write(stream, isLegacy) }
}
override fun play(connection: ClientConnection) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,36 @@
package ru.dbotthepony.kstarbound.network.packets.serverbound
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readVector2i
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileModificationFailurePacket
import ru.dbotthepony.kstarbound.world.TileModification
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
class ModifyTileListPacket(val modifications: Collection<Pair<Vector2i, TileModification>>, val allowEntityOverlap: Boolean) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readCollection { stream.readVector2i() to TileModification.read(stream, isLegacy) }, stream.readBoolean())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeCollection(modifications) { stream.writeStruct2i(it.first); it.second.write(stream, isLegacy) }
stream.writeBoolean(allowEntityOverlap)
}
override fun play(connection: ServerConnection) {
val inWorld = connection.enqueue {
val unapplied = applyTileModifications(modifications, allowEntityOverlap)
if (unapplied.isNotEmpty()) {
connection.send(TileModificationFailurePacket(unapplied))
}
}
if (!inWorld) {
connection.send(TileModificationFailurePacket(modifications))
}
}
}

View File

@ -53,8 +53,14 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
// packets which interact with world must be // packets which interact with world must be
// executed on world's thread // executed on world's thread
fun enqueue(task: ServerWorld.() -> Unit) { fun enqueue(task: ServerWorld.() -> Unit): Boolean {
return tracker?.enqueue(task) ?: LOGGER.warn("$this tried to interact with world, but they are not in one.") val isInWorld = tracker?.enqueue(task) != null
if (!isInWorld) {
LOGGER.warn("$this tried to interact with world, but they are not in one.")
}
return isInWorld
} }
lateinit var shipWorld: ServerWorld lateinit var shipWorld: ServerWorld

View File

@ -45,13 +45,14 @@ class LegacyWorldStorage(val loader: Loader) : WorldStorage() {
return loader(key).thenApplyAsync(Function { return loader(key).thenApplyAsync(Function {
it.map { it.map {
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it)))) val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
reader.skipBytes(3) val generationLevel = reader.readVarInt()
val tileSerializationVersion = reader.readVarInt()
val result = Object2DArray.nulls<ImmutableCell>(CHUNK_SIZE, CHUNK_SIZE) val result = Object2DArray.nulls<ImmutableCell>(CHUNK_SIZE, CHUNK_SIZE)
for (y in 0 until CHUNK_SIZE) { for (y in 0 until CHUNK_SIZE) {
for (x in 0 until CHUNK_SIZE) { for (x in 0 until CHUNK_SIZE) {
result[x, y] = MutableCell().read(reader).immutable() result[x, y] = MutableCell().readLegacy(reader, tileSerializationVersion).immutable()
} }
} }

View File

@ -2,7 +2,6 @@ package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -38,7 +37,6 @@ import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
@ -404,7 +402,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): DamageResult { fun damageTile(pos: Vector2i, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): DamageResult {
val cell = cells.value[pos.x, pos.y] val cell = cells.value[pos.x, pos.y]
if (cell.isIndestructible || cell.tile(isBackground).material.value.isMeta) { if (cell.tile(isBackground).material.value.isMeta) {
return DamageResult(TileDamageResult.NONE) return DamageResult(TileDamageResult.NONE)
} }
@ -426,7 +424,6 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
health.damage(params, sourcePosition, damage) health.damage(params, sourcePosition, damage)
onTileHealthUpdate(pos.x, pos.y, isBackground, health)
if (health.isDead) { if (health.isDead) {
val drops = ArrayList<ItemDescriptor>() val drops = ArrayList<ItemDescriptor>()
@ -466,8 +463,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
setCell(pos.x, pos.y, mCell.immutable()) setCell(pos.x, pos.y, mCell.immutable())
health.reset()
onTileHealthUpdate(pos.x, pos.y, isBackground, health)
return DamageResult(result, copyHealth, cell) return DamageResult(result, copyHealth, cell)
} else { } else {
onTileHealthUpdate(pos.x, pos.y, isBackground, health)
return DamageResult(result, health, cell) return DamageResult(result, health, cell)
} }
} }
@ -549,9 +549,9 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
if (state == ChunkState.FULL) { if (state == ChunkState.FULL) {
val unloadable = world.entityIndex val unloadable = world.entityIndex
.query( .query(
aabb, aabbd,
filter = Predicate { it.isApplicableForUnloading && !it.isRemote && aabbd.isInside(it.position) }, filter = Predicate { it.isApplicableForUnloading && !it.isRemote && aabbd.isInside(it.position) },
distinct = true, withEdges = false) withEdges = false)
world.storage.saveCells(pos, copyCells()) world.storage.saveCells(pos, copyCells())
world.storage.saveEntities(pos, unloadable) world.storage.saveEntities(pos, unloadable)

View File

@ -12,6 +12,7 @@ import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAction
@ -26,6 +27,7 @@ import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.jsonArrayOf import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
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.UpdateWorldPropertiesPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.UpdateWorldPropertiesPacket
@ -174,7 +176,7 @@ class ServerWorld private constructor(
damage = damage.copy(type = TileDamageType.PROTECTED) damage = damage.copy(type = TileDamageType.PROTECTED)
if (!isBackground) { if (!isBackground) {
for (entity in entitiesAtTile(pos, distinct = false)) { for (entity in entitiesAtTile(pos)) {
if (!damagedEntities.add(entity)) continue if (!damagedEntities.add(entity)) continue
val occupySpaces = entity.occupySpaces.stream() val occupySpaces = entity.occupySpaces.stream()
@ -217,6 +219,30 @@ class ServerWorld private constructor(
return topMost return topMost
} }
fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false): List<Pair<Vector2i, TileModification>> {
val unapplied = ArrayList(modifications)
var size: Int
do {
size = unapplied.size
val itr = unapplied.iterator()
for ((pos, modification) in itr) {
val cell = getCell(pos)
if (!ignoreTileProtection && cell.dungeonId in protectedDungeonIDs)
continue
if (modification.allowed(this, pos, allowEntityOverlap)) {
modification.apply(this, pos, allowEntityOverlap)
itr.remove()
}
}
} while(unapplied.isNotEmpty() && size != unapplied.size)
return unapplied
}
override fun tick() { override fun tick() {
try { try {
if (clients.isEmpty() && isBusy <= 0) { if (clients.isEmpty() && isBusy <= 0) {
@ -352,8 +378,8 @@ class ServerWorld private constructor(
tickets.addAll(region) tickets.addAll(region)
region.forEach { it.chunk.await() } region.forEach { it.chunk.await() }
foundGround = matchCells(spawnRect) { foundGround = anyCellSatisfies(spawnRect) { tx, ty, tcell ->
it.foreground.material.value.collisionKind != CollisionType.NONE tcell.foreground.material.value.collisionKind != CollisionType.NONE
} }
if (foundGround) { if (foundGround) {
@ -382,7 +408,7 @@ class ServerWorld private constructor(
tickets.addAll(region) tickets.addAll(region)
region.forEach { it.chunk.await() } region.forEach { it.chunk.await() }
if (!matchCells(spawnRect) { it.foreground.material.value.collisionKind != CollisionType.NONE } && spawnRect.maxs.y < geometry.size.y) { if (!anyCellSatisfies(spawnRect) { tx, ty, tcell -> tcell.foreground.material.value.collisionKind != CollisionType.NONE } && spawnRect.maxs.y < geometry.size.y) {
LOGGER.info("Found appropriate spawn position at $pos") LOGGER.info("Found appropriate spawn position at $pos")
return pos return pos
} }

View File

@ -205,9 +205,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
val trackingEntities = ObjectAVLTreeSet<AbstractEntity>() val trackingEntities = ObjectAVLTreeSet<AbstractEntity>()
for (region in trackingRegions) { for (region in trackingRegions) {
// we don't care about distinct values here, since trackingEntities.addAll(world.entityIndex.query(region.toDoubleAABB(), filter = { it.connectionID != client.connectionID }))
// we handle this by ourselves
trackingEntities.addAll(world.entityIndex.query(region, filter = { it.connectionID != client.connectionID }, distinct = false))
} }
val unseen = IntArrayList(entityVersions.keys) val unseen = IntArrayList(entityVersions.keys)

View File

@ -47,15 +47,15 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
protected val g3_1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) } protected val g3_1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) }
protected val g3_2 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) } protected val g3_2 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) }
private var init = false
private val initLock = Any()
init { init {
if (parameters.seed != null && parameters.type != PerlinNoiseParameters.Type.UNITIALIZED) { if (parameters.seed != null && parameters.type != PerlinNoiseParameters.Type.UNITIALIZED) {
init(parameters.seed) init(parameters.seed)
} }
} }
private var init = false
private val initLock = Any()
protected fun checkInit() { protected fun checkInit() {
check(hasSeedSpecified) { "No noise seed specified" } check(hasSeedSpecified) { "No noise seed specified" }

View File

@ -1,6 +0,0 @@
package ru.dbotthepony.kstarbound.world
enum class Side {
LEFT,
RIGHT;
}

View File

@ -1,16 +1,15 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
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 it.unimi.dsi.fastutil.longs.LongArrayList import it.unimi.dsi.fastutil.longs.LongArrayList
import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.KOptional
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
import kotlin.concurrent.withLock
// After some thinking, I decided to go with separate spatial index over // After some thinking, I decided to go with separate spatial index over
// using chunk/chunkmap as spatial indexing of entities (just like original engine does). // using chunk/chunkmap as spatial indexing of entities (just like original engine does).
@ -58,7 +57,7 @@ class SpatialIndex<T>(val geometry: WorldGeometry) {
inner class Entry(val value: T) : Comparable<Entry> { inner class Entry(val value: T) : Comparable<Entry> {
private val sectors = Object2IntAVLTreeMap<Sector>() private val sectors = Object2IntAVLTreeMap<Sector>()
private val id = counter.getAndIncrement() val id = counter.getAndIncrement()
private val fixtures = ArrayList<Fixture>(1) private val fixtures = ArrayList<Fixture>(1)
// default fixture since in most cases it should be enough // default fixture since in most cases it should be enough
@ -247,19 +246,34 @@ class SpatialIndex<T>(val geometry: WorldGeometry) {
} }
} }
/** fun query(rect: AABB, filter: Predicate<T> = Predicate { true }, withEdges: Boolean = true): List<T> {
* [filter] might be invoked for same entry multiple times, regardless of [distinct] val entriesDirect = ArrayList<T>()
*/
fun query(rect: AABBi, filter: Predicate<T> = Predicate { true }, distinct: Boolean = true, withEdges: Boolean = true): List<T> { iterate(rect, withEdges = withEdges, visitor = {
return query(rect.toDoubleAABB(), filter, distinct, withEdges) if (filter.test(it)) entriesDirect.add(it)
})
return entriesDirect
} }
/** fun any(rect: AABB, filter: Predicate<T> = Predicate { true }, withEdges: Boolean = true): Boolean {
* [filter] might be invoked for same entry multiple times, regardless of [distinct] return walk(rect, withEdges = withEdges, visitor = {
*/ if (filter.test(it)) KOptional(true) else KOptional()
fun query(rect: AABB, filter: Predicate<T> = Predicate { true }, distinct: Boolean = true, withEdges: Boolean = true): List<T> { }).orElse(false)
val entries = ArrayList<Entry>() }
val entriesDirect = ArrayList<T>()
fun all(rect: AABB, filter: Predicate<T> = Predicate { true }, withEdges: Boolean = true): Boolean {
return walk(rect, withEdges = withEdges, visitor = {
if (!filter.test(it)) KOptional(false) else KOptional()
}).orElse(true)
}
fun iterate(rect: AABB, visitor: (T) -> Unit, withEdges: Boolean = true) {
walk<Unit>(rect, { visitor(it); KOptional() }, withEdges)
}
fun <V> walk(rect: AABB, visitor: (T) -> KOptional<V>, withEdges: Boolean = true): KOptional<V> {
val seen = IntAVLTreeSet()
for (actualRegion in geometry.split(rect).first) { for (actualRegion in geometry.split(rect).first) {
val xMin = geometry.x.chunkFromCell(actualRegion.mins.x) val xMin = geometry.x.chunkFromCell(actualRegion.mins.x)
@ -272,42 +286,18 @@ class SpatialIndex<T>(val geometry: WorldGeometry) {
for (y in yMin .. yMax) { for (y in yMin .. yMax) {
val sector = map[index(x, y)] ?: continue val sector = map[index(x, y)] ?: continue
if (distinct) {
for (entry in sector.entries) { for (entry in sector.entries) {
if (filter.test(entry.value) && entry.intersects(actualRegion, withEdges)) { if (entry.intersects(actualRegion, withEdges) && seen.add(entry.id)) {
entries.add(entry) val visit = visitor(entry.value)
}
} if (visit.isPresent)
} else { return visit
for (entry in sector.entries) {
if (filter.test(entry.value) && entry.intersects(actualRegion, withEdges)) {
entriesDirect.add(entry.value)
}
} }
} }
} }
} }
} }
if (distinct) { return KOptional()
if (entries.isEmpty())
return listOf()
entries.sort()
val entries0 = ArrayList<T>(entries.size)
var previous: Entry? = null
for (entry in entries) {
if (entry != previous) {
previous = entry
entries0.add(entry.value)
}
}
return entries0
} else {
return entriesDirect
}
} }
} }

View File

@ -0,0 +1,313 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.ARTIFICIAL_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.orEmptyModifier
import ru.dbotthepony.kstarbound.defs.tile.supportsModifier
import ru.dbotthepony.kstarbound.io.readNullable
import ru.dbotthepony.kstarbound.io.readNullableFloat
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.DynamicEntity
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Predicate
sealed class TileModification {
object Invalid : TileModification() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(0)
}
override fun apply(
world: World<*, *>,
position: Vector2i,
allowEntityOverlap: Boolean,
) {
// do nothing
}
override fun allowed(
world: World<*, *>,
position: Vector2i,
allowEntityOverlap: Boolean,
perhaps: Boolean
): Boolean {
return true
}
}
abstract fun write(stream: DataOutputStream, isLegacy: Boolean)
abstract fun allowed(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean, perhaps: Boolean = false): Boolean
abstract fun apply(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean)
data class PlaceMaterial(val isBackground: Boolean, val material: Registry.Ref<TileDefinition>, val hueShift: Float?) : TileModification() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), if (isLegacy) Registries.tiles.ref(stream.readUnsignedShort()) else TODO(), if (isLegacy) stream.readNullable { stream.readUnsignedByte() / 255f } else stream.readNullableFloat())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(1)
stream.writeBoolean(isBackground)
if (isLegacy) {
val id = material.key.right.orNull() ?: material.entry?.id ?: throw IllegalStateException("Can't network $material over legacy protocol because it has no pre-defined ID")
if (id !in 0 .. 65535) {
throw IllegalStateException("Can't network $material over legacy protocol because its pre-defined ID is bigger than uint16_t")
}
stream.writeShort(id)
stream.writeBoolean(hueShift != null)
if (hueShift != null) {
stream.writeByte((hueShift * 360f / 255f).toInt())
}
} else {
// registries name->id mapping should be networked on join
TODO()
// stream.writeNullableFloat(hueShift)
}
}
override fun allowed(
world: World<*, *>,
position: Vector2i,
allowEntityOverlap: Boolean,
perhaps: Boolean
): Boolean {
if (material.isEmptyTile || material.value!!.isMeta)
return false
if (isBackground && material.value!!.foregroundOnly)
return false
val (x, y) = position
val cell = world.getCell(x, y)
val tile = cell.tile(isBackground)
if (tile.material.isNotEmptyTile)
return false
if (!isBackground) {
val rect = AABB(Vector2d(x.toDouble(), y.toDouble()), Vector2d(x + 1.0, y + 1.0))
if (world.entityIndex.any(rect, Predicate { it is TileEntity && position in it.occupySpaces })) {
return false
}
if (!allowEntityOverlap && world.entityIndex.any(rect, Predicate { it is DynamicEntity && it.movement.computeCollisionAABB().intersect(rect) })) {
return false
}
}
return perhaps ||
world.geometry.x.cell(x - 1) == x ||
world.geometry.y.cell(x + 1) == x ||
world.geometry.y.cell(y - 1) == y ||
world.geometry.y.cell(y + 1) == y ||
world.anyCellSatisfies(x, y, 1) { tx, ty, tcell ->
tx != x && ty != y && (tcell.foreground.material.value.isConnectable || tcell.background.material.value.isConnectable)
}
}
override fun apply(
world: World<*, *>,
position: Vector2i,
allowEntityOverlap: Boolean,
) {
val material = material.entry!!
val cell = world.getCell(position).mutable()
val tile = cell.tile(isBackground)
tile.material = material
tile.hueShift = hueShift ?: world.template.cellInfo(position).blockBiome?.hueShift(material) ?: 0f
tile.color = TileColor.DEFAULT
if (material.isEmptyTile) {
// remove modifier if removing tile
tile.modifier = BuiltinMetaMaterials.EMPTY_MOD
tile.modifierHueShift = 0f
} else if (isBackground && cell.liquid.isInfinite) {
cell.liquid.isInfinite = false
cell.liquid.pressure = 1f
} else if (!isBackground && material.value.blocksLiquidFlow) {
cell.liquid.reset()
}
cell.dungeonId = ARTIFICIAL_DUNGEON_ID
world.setCell(position, cell.immutable())
}
}
data class PlaceModifier(val isBackground: Boolean, val modifier: Registry.Ref<TileModifierDefinition>, val hueShift: Float?) : TileModification() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), if (isLegacy) Registries.tileModifiers.ref(stream.readUnsignedShort()) else TODO(), if (isLegacy) stream.readNullable { stream.readUnsignedByte() / 255f } else stream.readNullableFloat())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(2)
stream.writeBoolean(isBackground)
if (isLegacy) {
val id = modifier.key.right.orNull() ?: modifier.entry?.id ?: throw IllegalStateException("Can't network $modifier over legacy protocol because it has no pre-defined ID")
if (id !in 0 .. 65535) {
throw IllegalStateException("Can't network $modifier over legacy protocol because its pre-defined ID is bigger than uint16_t")
}
stream.writeShort(id)
stream.writeBoolean(hueShift != null)
if (hueShift != null) {
stream.writeByte((hueShift * 360f / 255f).toInt())
}
} else {
// registries name->id mapping should be networked on join
TODO()
// stream.writeNullableFloat(hueShift)
}
}
override fun allowed(
world: World<*, *>,
position: Vector2i,
allowEntityOverlap: Boolean,
perhaps: Boolean
): Boolean {
val cell = world.getCell(position)
val tile = cell.tile(isBackground)
return modifier.isNotEmptyModifier &&
!modifier.value!!.isMeta &&
tile.modifier.isEmptyModifier &&
tile.material.supportsModifier(modifier)
}
override fun apply(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean) {
val cell = world.getCell(position).mutable()
val tile = cell.tile(isBackground)
tile.modifier = modifier.orEmptyModifier
world.setCell(position, cell)
}
}
data class Paint(val isBackground: Boolean, val color: TileColor) : TileModification() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBoolean(), TileColor.entries[stream.readUnsignedByte()])
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(3)
stream.writeBoolean(isBackground)
stream.writeByte(color.ordinal)
}
override fun allowed(
world: World<*, *>,
position: Vector2i,
allowEntityOverlap: Boolean,
perhaps: Boolean
): Boolean {
val tile = world.getCell(position).tile(isBackground)
val material = tile.material.value
return tile.hueShift != 0f || tile.color != this.color && material.renderParameters.multiColored
}
override fun apply(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean) {
val cell = world.getCell(position)
val tile = cell.tile(isBackground).mutable()
tile.hueShift = 0f
tile.color = this.color
world.setCell(position, cell)
}
}
data class Pour(val state: Registry.Ref<LiquidDefinition>, val level: Float) : TileModification() {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) Registries.liquid.ref(stream.readUnsignedByte()) else TODO(), stream.readFloat())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(4)
if (isLegacy) {
val id = state.key.right.orNull() ?: state.entry?.id ?: throw IllegalStateException("Can't network $state over legacy protocol because it has no pre-defined ID")
if (id !in 0..255) {
throw IllegalStateException("Can't network $state over legacy protocol because its pre-defined ID is bigger than uint8_t")
}
stream.writeByte(id)
} else {
// registries name->id mapping should be networked on join
TODO()
}
stream.writeFloat(level)
}
override fun allowed(
world: World<*, *>,
position: Vector2i,
allowEntityOverlap: Boolean,
perhaps: Boolean
): Boolean {
if (state.isEmpty)
return false
val cell = world.getCell(position)
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.isInfinite)
return false // it makes no sense to try to pour liquid into infinite source
if (cell.liquid.state.isNotEmptyLiquid && cell.liquid.state != state.entry)
return false // it makes also makes no sense to magically replace liquid what is already there
// while checks above makes vanilla client look stupid when it tries to pour liquids into other
// liquids, we must think better than vanilla client.
return !cell.foreground.material.value.collisionKind.isSolidCollision
}
override fun apply(world: World<*, *>, position: Vector2i, allowEntityOverlap: Boolean) {
if (state.isEmpty)
return
val state = state.entry!!
val cell = world.getCell(position).mutable()
if (cell.liquid.state == state) {
cell.liquid.level += level
} else {
cell.liquid.reset()
cell.liquid.state = state
cell.liquid.level = level
cell.liquid.pressure = 1f
}
world.setCell(position, cell)
}
}
companion object {
fun read(stream: DataInputStream, isLegacy: Boolean): TileModification {
return when (val type = stream.readUnsignedByte()) {
0 -> Invalid
1 -> PlaceMaterial(stream, isLegacy)
2 -> PlaceModifier(stream, isLegacy)
3 -> Paint(stream, isLegacy)
4 -> Pour(stream, isLegacy)
else -> throw IllegalArgumentException("Unknown tile modification type $type!")
}
}
}
}

View File

@ -17,7 +17,10 @@ import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
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.mergeJson import ru.dbotthepony.kstarbound.json.mergeJson
@ -36,8 +39,6 @@ import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@ -87,6 +88,11 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return chunk.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) return chunk.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK)
} }
fun getCellDirect(ix: Int, iy: Int): AbstractCell {
val chunk = get(geometry.x.chunkFromCell(ix), geometry.y.chunkFromCell(iy)) ?: return AbstractCell.NULL
return chunk.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK)
}
fun setCell(x: Int, y: Int, cell: AbstractCell, state: ChunkState): Boolean { fun setCell(x: Int, y: Int, cell: AbstractCell, state: ChunkState): Boolean {
val ix = geometry.x.cell(x) val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y) val iy = geometry.y.cell(y)
@ -302,21 +308,26 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
abstract val eventLoop: BlockableEventLoop abstract val eventLoop: BlockableEventLoop
fun entitiesAtTile(pos: Vector2i, filter: Predicate<TileEntity> = Predicate { true }, distinct: Boolean = true): List<TileEntity> { fun entitiesAtTile(pos: Vector2i, filter: Predicate<TileEntity> = Predicate { true }): List<TileEntity> {
return entityIndex.query( return entityIndex.query(
AABBi(pos, pos + Vector2i.POSITIVE_XY), AABBi(pos, pos + Vector2i.POSITIVE_XY).toDoubleAABB(),
distinct = distinct,
filter = { it is TileEntity && pos in it.occupySpaces && filter.test(it) } filter = { it is TileEntity && pos in it.occupySpaces && filter.test(it) }
) as List<TileEntity> ) as List<TileEntity>
} }
fun matchCells(aabb: AABBi, predicate: Predicate<AbstractCell>): Boolean { fun interface CellPredicate {
for (split in geometry.split(aabb).first) { fun test(x: Int, y: Int, cell: AbstractCell): Boolean
for (x in split.mins.x .. split.maxs.x) {
for (y in split.mins.x .. split.maxs.x) {
if (predicate.test(chunkMap.getCell(x, y))) {
return true
} }
fun anyCellSatisfies(aabb: AABBi, predicate: CellPredicate): Boolean {
for (x in aabb.mins.x .. aabb.maxs.x) {
for (y in aabb.mins.x .. aabb.maxs.x) {
val ix = geometry.x.cell(x)
val iy = geometry.x.cell(y)
if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) {
return true
} }
} }
} }
@ -324,15 +335,31 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return false return false
} }
fun matchCells(aabb: AABB, predicate: Predicate<AbstractCell>): Boolean { fun anyCellSatisfies(aabb: AABB, predicate: CellPredicate): Boolean {
for (split in geometry.split(aabb).first) { for (x in aabb.mins.x.toInt() .. aabb.maxs.x.roundToInt()) {
for (x in split.mins.x.toInt() .. split.maxs.x.roundToInt()) { for (y in aabb.mins.y.toInt() .. aabb.maxs.y.roundToInt()) {
for (y in split.mins.y.toInt() .. split.maxs.y.roundToInt()) { val ix = geometry.x.cell(x)
if (predicate.test(chunkMap.getCell(x, y))) { val iy = geometry.x.cell(y)
if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) {
return true return true
} }
} }
} }
return false
}
fun anyCellSatisfies(x: Int, y: Int, distance: Int, predicate: CellPredicate): Boolean {
for (tx in x - distance .. x + distance) {
for (ty in y - distance .. y + distance) {
val ix = geometry.x.cell(tx)
val iy = geometry.x.cell(ty)
if (predicate.test(ix, iy, chunkMap.getCellDirect(ix, iy))) {
return true
}
}
} }
return false return false
@ -394,8 +421,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
* of world generation, everything else is kinda okay to be performed * of world generation, everything else is kinda okay to be performed
* 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 since concurrent write * ArrayChunkMap does ~not need~ needs synchronization too, unless we use CopyOnWriteArrayList
* won't cause any observable side effects by concurrent readers. * for "existing" chunks list.
*/ */
private const val CONCURRENT_SPARSE_CHUNK_MAP = false private const val CONCURRENT_SPARSE_CHUNK_MAP = false
} }

View File

@ -27,8 +27,7 @@ sealed class AbstractCell {
// value of 0 points to null, 1 points to 0, and so on // value of 0 points to null, 1 points to 0, and so on
abstract val envBiome: Int abstract val envBiome: Int
// whenever if cell ignores any attempts to damage it abstract val biomeTransition: Boolean
abstract val isIndestructible: Boolean
abstract fun immutable(): ImmutableCell abstract fun immutable(): ImmutableCell
abstract fun mutable(): MutableCell abstract fun mutable(): MutableCell
@ -59,7 +58,7 @@ sealed class AbstractCell {
stream.writeShort(dungeonId) stream.writeShort(dungeonId)
stream.writeByte(blockBiome) stream.writeByte(blockBiome)
stream.writeByte(envBiome) stream.writeByte(envBiome)
stream.writeBoolean(isIndestructible) stream.writeBoolean(biomeTransition)
stream.write(0) // unknown stream.write(0) // unknown
} }

View File

@ -11,7 +11,7 @@ data class ImmutableCell(
override val dungeonId: Int = NO_DUNGEON_ID, override val dungeonId: Int = NO_DUNGEON_ID,
override val blockBiome: Int = 0, override val blockBiome: Int = 0,
override val envBiome: Int = 0, override val envBiome: Int = 0,
override val isIndestructible: Boolean = false, override val biomeTransition: Boolean = false,
) : AbstractCell() { ) : AbstractCell() {
override fun immutable(): ImmutableCell { override fun immutable(): ImmutableCell {
return this return this
@ -31,6 +31,6 @@ data class ImmutableCell(
} }
override fun mutable(): MutableCell { override fun mutable(): MutableCell {
return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, blockBiome, envBiome, isIndestructible) return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, blockBiome, envBiome, biomeTransition)
} }
} }

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world.api package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kommons.io.readVector2i
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import java.io.DataInputStream import java.io.DataInputStream
@ -11,21 +12,34 @@ data class MutableCell(
override var dungeonId: Int = NO_DUNGEON_ID, override var dungeonId: Int = NO_DUNGEON_ID,
override var blockBiome: Int = 0, override var blockBiome: Int = 0,
override var envBiome: Int = 0, override var envBiome: Int = 0,
override var isIndestructible: Boolean = false, override var biomeTransition: Boolean = false,
) : AbstractCell() { ) : AbstractCell() {
fun read(stream: DataInputStream): MutableCell { fun readLegacy(stream: DataInputStream, version: Int = 419): MutableCell {
foreground.read(stream) foreground.read(stream)
background.read(stream) background.read(stream)
liquid.read(stream) liquid.read(stream)
stream.skipNBytes(1) // collisionMap stream.skipNBytes(1) // foreground.material.value.collisionKind
dungeonId = stream.readUnsignedShort() dungeonId = stream.readUnsignedShort()
blockBiome = stream.readUnsignedByte() blockBiome = stream.readUnsignedByte()
envBiome = stream.readUnsignedByte() envBiome = stream.readUnsignedByte()
isIndestructible = stream.readBoolean()
stream.skipNBytes(1) // unknown if (version < 417) {
biomeTransition = false
} else {
biomeTransition = stream.readBoolean()
}
if (version < 418) {
stream.skipNBytes(1) // leftover
} else {
// TODO: root source
if (stream.readBoolean()) {
stream.readVector2i()
}
}
return this return this
} }
@ -37,7 +51,7 @@ data class MutableCell(
} }
override fun immutable(): ImmutableCell { override fun immutable(): ImmutableCell {
return POOL.intern(ImmutableCell(foreground.immutable(), background.immutable(), liquid.immutable(), dungeonId, blockBiome, envBiome, isIndestructible)) return POOL.intern(ImmutableCell(foreground.immutable(), background.immutable(), liquid.immutable(), dungeonId, blockBiome, envBiome, biomeTransition))
} }
override fun mutable(): MutableCell { override fun mutable(): MutableCell {

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.api
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
// uint8_t
enum class TileColor { enum class TileColor {
DEFAULT, DEFAULT,
RED, RED,

View File

@ -4,6 +4,7 @@ import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -106,6 +107,11 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
protected var spatialEntry: SpatialIndex<AbstractEntity>.Entry? = null protected var spatialEntry: SpatialIndex<AbstractEntity>.Entry? = null
private set private set
/**
* Used for spatial index
*/
abstract val metaBoundingBox: AABB
open fun onNetworkUpdate() { open fun onNetworkUpdate() {
} }

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.RenderLayer import ru.dbotthepony.kstarbound.client.render.RenderLayer
@ -27,25 +28,38 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
movement.updateFixtures() movement.updateFixtures()
} }
private var fixturesChangeset = -1
override fun tick() { override fun tick() {
super.tick() super.tick()
if (isRemote && networkGroup.upstream.isInterpolating) { if (isRemote && networkGroup.upstream.isInterpolating) {
movement.updateFixtures() movement.updateFixtures()
} }
if (fixturesChangeset != movement.fixturesChangeset) {
fixturesChangeset = movement.fixturesChangeset
metaFixture!!.move(metaBoundingBox)
} }
}
protected var metaFixture: SpatialIndex<AbstractEntity>.Entry.Fixture? = null
private set
override fun onJoinWorld(world: World<*, *>) { override fun onJoinWorld(world: World<*, *>) {
super.onJoinWorld(world) super.onJoinWorld(world)
world.dynamicEntities.add(this) world.dynamicEntities.add(this)
movement.initialize(world, spatialEntry) movement.initialize(world, spatialEntry)
forceChunkRepos = true forceChunkRepos = true
metaFixture = spatialEntry!!.Fixture()
} }
override fun onRemove(world: World<*, *>, reason: RemovalReason) { override fun onRemove(world: World<*, *>, reason: RemovalReason) {
super.onRemove(world, reason) super.onRemove(world, reason)
world.dynamicEntities.remove(this) world.dynamicEntities.remove(this)
movement.remove() movement.remove()
metaFixture?.remove()
metaFixture = null
} }
override fun render(client: StarboundClient, layers: LayeredRenderer) { override fun render(client: StarboundClient, layers: LayeredRenderer) {

View File

@ -121,6 +121,9 @@ class ItemDropEntity() : DynamicEntity("/") {
private var stayAliveFor = -1.0 private var stayAliveFor = -1.0
override val metaBoundingBox: AABB
get() = AABB(position - Vector2d(0.5, 0.5), position + Vector2d(0.5, 0.5))
override fun tick() { override fun tick() {
super.tick() super.tick()

View File

@ -71,6 +71,33 @@ open class MovementController() {
} }
} }
fun computeCollisionAABB(): AABB {
val poly = movementParameters.collisionPoly ?: return AABB.ZERO + position
if (poly.isLeft) {
if (poly.left().isEmpty)
return AABB.ZERO + position
return (poly.left().rotate(rotation) + position).aabb
} else {
if (poly.right().isEmpty())
return AABB.ZERO + position
else if (poly.right().first().isEmpty)
return AABB.ZERO + position
var build = (poly.right().first().rotate(rotation) + position).aabb
for (i in 1 until poly.right().size) {
val gPoly = poly.right()[i]
if (gPoly.isNotEmpty)
build = build.combine((gPoly.rotate(rotation) + position).aabb)
}
return build
}
}
open fun shouldCollideWithType(type: CollisionType): Boolean { open fun shouldCollideWithType(type: CollisionType): Boolean {
return type !== CollisionType.NONE return type !== CollisionType.NONE
} }
@ -117,6 +144,10 @@ open class MovementController() {
fun updateFixtures() { fun updateFixtures() {
val spatialEntry = spatialEntry ?: return val spatialEntry = spatialEntry ?: return
fixturesChangeset++ fixturesChangeset++
val poly = movementParameters.collisionPoly ?: return
if (poly.isLeft && poly.left().isEmpty) return
if (poly.isRight && poly.right().none { it.isNotEmpty }) return
val localHitboxes = computeLocalHitboxes() val localHitboxes = computeLocalHitboxes()
while (fixtures.size > localHitboxes.size) { while (fixtures.size > localHitboxes.size) {

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.entities.player
import com.google.gson.JsonElement import com.google.gson.JsonElement
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
@ -102,28 +103,8 @@ class PlayerEntity() : HumanoidActorEntity("/") {
networkGroup.upstream.add(techController.networkGroup) networkGroup.upstream.add(techController.networkGroup)
} }
private var fixturesChangeset = -1 override val metaBoundingBox: AABB
private var metaFixture: SpatialIndex<AbstractEntity>.Entry.Fixture? = null get() = Globals.player.metaBoundBox + position
override fun onJoinWorld(world: World<*, *>) {
super.onJoinWorld(world)
metaFixture = spatialEntry!!.Fixture()
}
override fun onRemove(world: World<*, *>, reason: RemovalReason) {
super.onRemove(world, reason)
metaFixture?.remove()
metaFixture = null
}
override fun tick() {
super.tick()
if (fixturesChangeset != movement.fixturesChangeset) {
fixturesChangeset = movement.fixturesChangeset
metaFixture!!.move(Globals.player.metaBoundBox + position)
}
}
override val aimPosition: Vector2d override val aimPosition: Vector2d
get() = Vector2d(xAimPosition, yAimPosition) get() = Vector2d(xAimPosition, yAimPosition)

View File

@ -31,8 +31,6 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
yTilePositionNet.addListener(::onPositionUpdated) yTilePositionNet.addListener(::onPositionUpdated)
} }
abstract val metaBoundingBox: AABB
var xTilePosition: Int var xTilePosition: Int
get() = xTilePositionNet.get() get() = xTilePositionNet.get()
set(value) { set(value) {

View File

@ -94,6 +94,8 @@ class Poly private constructor(val edges: ImmutableList<Line2d>, val vertices: I
val aabb: AABB val aabb: AABB
val isEmpty: Boolean val isEmpty: Boolean
get() = vertices.isEmpty() get() = vertices.isEmpty()
val isNotEmpty: Boolean
get() = vertices.isNotEmpty()
init { init {
if (vertices.isEmpty()) { if (vertices.isEmpty()) {

View File

@ -58,11 +58,11 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
} }
private val layers = Caffeine.newBuilder() private val layers = Caffeine.newBuilder()
.maximumSize(256L) .maximumSize(512L)
.softValues() .softValues()
.expireAfterAccess(Duration.ofMinutes(2)) .expireAfterAccess(Duration.ofSeconds(10))
//.scheduler(Starbound) .scheduler(Starbound)
//.executor(Starbound.EXECUTOR) .executor(Starbound.EXECUTOR)
.build<Int, Layer>(::Layer) .build<Int, Layer>(::Layer)
private inner class Sector(val sector: Vector2i) { private inner class Sector(val sector: Vector2i) {
@ -127,11 +127,11 @@ class KarstCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters
} }
private val sectors = Caffeine.newBuilder() private val sectors = Caffeine.newBuilder()
.maximumSize(64L) .maximumSize(256L)
.softValues() .softValues()
.expireAfterAccess(Duration.ofMinutes(2)) .expireAfterAccess(Duration.ofSeconds(10))
//.scheduler(Starbound) .scheduler(Starbound)
//.executor(Starbound.EXECUTOR) .executor(Starbound.EXECUTOR)
.build<Vector2i, Sector>(::Sector) .build<Vector2i, Sector>(::Sector)
override fun get(x: Int, y: Int): Double { override fun get(x: Int, y: Int): Double {

View File

@ -178,11 +178,11 @@ class WormCaveTerrainSelector(data: Data, parameters: TerrainSelectorParameters)
} }
private val sectors = Caffeine.newBuilder() private val sectors = Caffeine.newBuilder()
.maximumSize(64L) .maximumSize(256L)
.softValues() .softValues()
.expireAfterAccess(Duration.ofMinutes(2)) .expireAfterAccess(Duration.ofSeconds(10))
//.scheduler(Starbound) .scheduler(Starbound)
//.executor(Starbound.EXECUTOR) .executor(Starbound.EXECUTOR)
.build<Vector2i, Sector>(::Sector) .build<Vector2i, Sector>(::Sector)
override fun get(x: Int, y: Int): Double { override fun get(x: Int, y: Int): Double {