KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileModification.kt

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!")
}
}
}
}