package dev.cobblesurvival.machine import net.minecraft.core.BlockPos import net.minecraft.core.registries.BuiltInRegistries import net.minecraft.core.registries.Registries import net.minecraft.resources.ResourceLocation import net.minecraft.tags.TagKey import net.minecraft.world.level.Level import net.minecraft.world.level.block.Rotation import net.minecraft.world.level.block.state.BlockState /** * A machine's shell: what must stand at each offset from the core, written with the core in the south wall, and the * chamber where the Pokémon stands. [match] tries the four horizontal rotations around the core. */ class Shell(private val parts: Map, private val chamber: BlockPos) { class BlockRule(val text: String, val test: (BlockState) -> Boolean) /** The shell found around a core: where the chamber is and the shell's blocks (the core not included). */ class Match(val chamber: BlockPos, val blocks: Set) fun match(level: Level, core: BlockPos): Match? = ROTATIONS.firstNotNullOfOrNull { rotation -> if (parts.all { (offset, rule) -> rule.test(level.getBlockState(core.offset(offset.rotate(rotation)))) }) { Match(core.offset(chamber.rotate(rotation)), parts.keys.filter { it != chamber }.mapTo(HashSet()) { core.offset(it.rotate(rotation)) }) } else null } /** For a message: the first wrong block in the rotation that comes closest, and what belongs there. */ fun firstMismatch(level: Level, core: BlockPos): Pair? = ROTATIONS .map { rotation -> parts.entries.filterNot { (offset, rule) -> rule.test(level.getBlockState(core.offset(offset.rotate(rotation)))) } .map { (offset, rule) -> core.offset(offset.rotate(rotation)) to rule.text } } .minByOrNull { it.size }?.firstOrNull() /** Every block position of the shell in the orientation where the core is in the south wall (for building it). */ fun layout(): Map = parts.mapValues { it.value.text } companion object { private val ROTATIONS = listOf(Rotation.NONE, Rotation.CLOCKWISE_90, Rotation.CLOCKWISE_180, Rotation.COUNTERCLOCKWISE_90) private const val CORE = 'C' private const val CHAMBER = '.' const val AIR = "air" /** [layers] bottom to top, each a list of rows north to south, each row west to east; ' ' = anything. A key * lists the blocks (ids or #tags) allowed for its character. */ fun parse(layers: List>, keys: Map>): Shell { val cells = layers.flatMapIndexed { y, rows -> rows.flatMapIndexed { z, row -> row.mapIndexed { x, c -> BlockPos(x, y, z) to c } } } val core = cells.single { it.second == CORE }.first val chamber = cells.single { it.second == CHAMBER }.first.subtract(core) val parts = cells.filter { it.second != CORE && it.second != ' ' }.associate { (pos, c) -> pos.subtract(core) to when (c) { CHAMBER -> BlockRule(AIR) { it.isAir } else -> anyOf(keys[c] ?: error("no key for '$c'")) } } return Shell(parts, chamber) } private fun anyOf(specs: List): BlockRule { val rules = specs.map(::rule) return BlockRule(specs.joinToString(" or ")) { state -> rules.any { it.test(state) } } } private fun rule(spec: String): BlockRule = if (spec.startsWith("#")) { val tag = TagKey.create(Registries.BLOCK, ResourceLocation.parse(spec.substring(1))) BlockRule(spec) { it.`is`(tag) } } else { val id = ResourceLocation.parse(spec) BlockRule(spec) { BuiltInRegistries.BLOCK.getKey(it.block) == id } } } }