314 lines
10 KiB
Kotlin
314 lines
10 KiB
Kotlin
package ru.dbotthepony.kstarbound.world
|
|
|
|
import ru.dbotthepony.kstarbound.math.AABB
|
|
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
|
import ru.dbotthepony.kstarbound.math.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 * 360f } 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.collisionArea.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 * 360f } 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).mutable()
|
|
val tile = cell.tile(isBackground)
|
|
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!")
|
|
}
|
|
}
|
|
}
|
|
}
|