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