diff --git a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt index 704ca6fea..c4a630a7a 100644 --- a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt @@ -76,10 +76,10 @@ class BreakSettings( // Block override val avoidLiquids by c.setting("Avoid Liquids", true, "Avoids breaking blocks that would cause liquid to spill", visibility = vis).group(baseGroup, Group.General) override val avoidSupporting by c.setting("Avoid Supporting", true, "Avoids breaking the block supporting the player", visibility = vis).group(baseGroup, Group.General) - override val breakWeakBlocks by c.setting("Break Weak Blocks", false, "Break blocks that dont have structural integrity (e.g: grass)", visibility = vis).group(baseGroup, Group.General) override val ignoredBlocks by c.setting("Ignored Blocks", allSigns, description = "Blocks that wont be broken", visibility = vis).group(baseGroup, Group.General) // Tool + override val efficientOnly by c.setting("Efficient Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { vis() && swapMode.isEnabled() }.group(baseGroup, Group.General) override val suitableToolsOnly by c.setting("Suitable Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { vis() && swapMode.isEnabled() }.group(baseGroup, Group.General) override val forceSilkTouch by c.setting("Force Silk Touch", false, "Force silk touch when breaking blocks") { vis() && swapMode.isEnabled() }.group(baseGroup, Group.General) override val forceFortunePickaxe by c.setting("Force Fortune Pickaxe", false, "Force fortune pickaxe when breaking blocks") { vis() && swapMode.isEnabled() }.group(baseGroup, Group.General) diff --git a/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt b/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt index 4205fe605..b8f4397aa 100644 --- a/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt @@ -47,8 +47,8 @@ class BuildSettings( override val scanReach: Double get() = max(attackReach, interactReach) - override val strictRayCast by c.setting("Strict Raycast", false, "Whether to include the environment to the ray cast context", vis).group(*baseGroup, Group.Scan) override val checkSideVisibility by c.setting("Visibility Check", true, "Whether to check if an AABB side is visible", vis).group(*baseGroup, Group.Scan) + override val strictRayCast by c.setting("Strict Raycast", false, "Whether to include the environment to the ray cast context", vis).group(*baseGroup, Group.Scan) override val resolution by c.setting("Resolution", 5, 1..20, 1, "The amount of grid divisions per surface of the hit box", "", vis).group(*baseGroup, Group.Scan) override val pointSelection by c.setting("Point Selection", PointSelection.Optimum, "The strategy to select the best hit point", vis).group(*baseGroup, Group.Scan) diff --git a/src/main/kotlin/com/lambda/context/AutomationConfig.kt b/src/main/kotlin/com/lambda/context/AutomationConfig.kt index 03f4f20b2..8925a5804 100644 --- a/src/main/kotlin/com/lambda/context/AutomationConfig.kt +++ b/src/main/kotlin/com/lambda/context/AutomationConfig.kt @@ -62,6 +62,7 @@ object AutomationConfig : Configurable(LambdaConfig), Automated { val showAllEntries by setting("Show All Entries", false, "Show all entries in the task tree").group(Group.Debug) val shrinkFactor by setting("Shrink Factor", 0.001, 0.0..1.0, 0.001).group(Group.Debug) val ignoreItemDropWarnings by setting("Ignore Drop Warnings", false, "Hides the item drop warnings from the break manager").group(Group.Debug) + val maxSimDependencies by setting("Max Sim Dependencies", 3, 0..10, 1, "Maximum dependency build results").group(Group.Debug) @Volatile var drawables = listOf() diff --git a/src/main/kotlin/com/lambda/interaction/construction/context/BreakContext.kt b/src/main/kotlin/com/lambda/interaction/construction/context/BreakContext.kt index e6df5e6b9..a6f6c17d2 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/context/BreakContext.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/context/BreakContext.kt @@ -41,19 +41,18 @@ import kotlin.math.sqrt import kotlin.random.Random data class BreakContext( - override val result: BlockHitResult, - override val rotation: RotationRequest, - override var hotbarIndex: Int, - var itemSelection: StackSelection, - var instantBreak: Boolean, + override val hitResult: BlockHitResult, + override val rotationRequest: RotationRequest, + override val hotbarIndex: Int, + val itemSelection: StackSelection, + val instantBreak: Boolean, override var cachedState: BlockState, - val sortMode: BreakConfig.SortMode, private val automated: Automated ) : BuildContext(), LogContext, Automated by automated { private val baseColor = Color(222, 0, 0, 25) private val sideColor = Color(222, 0, 0, 100) - override val blockPos: BlockPos = result.blockPos + override val blockPos: BlockPos = hitResult.blockPos override val expectedState = cachedState.emptyState val random = Random.nextDouble() @@ -61,14 +60,15 @@ data class BreakContext( override fun compareTo(other: BuildContext): Int = runSafe { return when (other) { is BreakContext -> compareByDescending { - if (sortMode == BreakConfig.SortMode.Tool) it.hotbarIndex == HotbarManager.serverSlot + if (breakConfig.sorter == BreakConfig.SortMode.Tool) + it.hotbarIndex == HotbarManager.serverSlot else 0 }.thenBy { - when (sortMode) { + when (breakConfig.sorter) { BreakConfig.SortMode.Tool, - BreakConfig.SortMode.Closest -> player.eyePos.distance(it.result.pos, it.cachedState.block) - BreakConfig.SortMode.Farthest -> -player.eyePos.distance(it.result.pos, it.cachedState.block) - BreakConfig.SortMode.Rotation -> it.rotation.target.angleDistance + BreakConfig.SortMode.Closest -> player.eyePos.distance(it.hitResult.pos, it.cachedState.block) + BreakConfig.SortMode.Farthest -> -player.eyePos.distance(it.hitResult.pos, it.cachedState.block) + BreakConfig.SortMode.Rotation -> it.rotationRequest.target.angleDistance BreakConfig.SortMode.Random -> it.random } }.thenByDescending { @@ -92,19 +92,18 @@ data class BreakContext( } override fun ShapeBuilder.buildRenderer() { - box(blockPos, cachedState, baseColor, sideColor, DirectionMask.ALL.exclude(result.side)) + box(blockPos, cachedState, baseColor, sideColor, DirectionMask.ALL.exclude(hitResult.side)) } override fun getLogContextBuilder(): LogContextBuilder.() -> Unit = { group("Break Context") { text(blockPos.getLogContextBuilder()) - text(result.getLogContextBuilder()) - text(rotation.getLogContextBuilder()) + text(hitResult.getLogContextBuilder()) + text(rotationRequest.getLogContextBuilder()) value("Hotbar Index", hotbarIndex) value("Instant Break", instantBreak) value("Cached State", cachedState) value("Expected State", expectedState) - value("Sort Mode", sortMode) } } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/context/BuildContext.kt b/src/main/kotlin/com/lambda/interaction/construction/context/BuildContext.kt index 3a4598d3b..2cd5893b1 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/context/BuildContext.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/context/BuildContext.kt @@ -26,14 +26,14 @@ import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.math.BlockPos abstract class BuildContext : Comparable, Drawable, Automated { - abstract val result: BlockHitResult - abstract val rotation: RotationRequest + abstract val hitResult: BlockHitResult + abstract val rotationRequest: RotationRequest abstract val hotbarIndex: Int abstract val cachedState: BlockState abstract val expectedState: BlockState abstract val blockPos: BlockPos val distance by lazy { - runSafe { player.eyePos.distanceTo(result.pos) } ?: Double.MAX_VALUE + runSafe { player.eyePos.distanceTo(hitResult.pos) } ?: Double.MAX_VALUE } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/context/InteractionContext.kt b/src/main/kotlin/com/lambda/interaction/construction/context/InteractionContext.kt index 8e2bd04e2..720bb69c6 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/context/InteractionContext.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/context/InteractionContext.kt @@ -35,8 +35,8 @@ import net.minecraft.util.math.BlockPos import java.awt.Color class InteractionContext( - override val result: BlockHitResult, - override val rotation: RotationRequest, + override val hitResult: BlockHitResult, + override val rotationRequest: RotationRequest, override var hotbarIndex: Int, override var cachedState: BlockState, override val expectedState: BlockState, @@ -45,7 +45,7 @@ class InteractionContext( private val baseColor = Color(35, 254, 79, 25) private val sideColor = Color(35, 254, 79, 100) - override val blockPos: BlockPos = result.blockPos + override val blockPos: BlockPos = hitResult.blockPos override fun compareTo(other: BuildContext) = when { @@ -54,7 +54,7 @@ class InteractionContext( }.thenByDescending { it.cachedState.fluidState.level }.thenBy { - it.rotation.target.angleDistance + it.rotationRequest.target.angleDistance }.thenBy { it.hotbarIndex == HotbarManager.serverSlot }.thenBy { @@ -65,20 +65,20 @@ class InteractionContext( } override fun ShapeBuilder.buildRenderer() { - box(blockPos, expectedState, baseColor, sideColor, result.side.mask) + box(blockPos, expectedState, baseColor, sideColor, hitResult.side.mask) } fun requestDependencies(request: InteractRequest): Boolean { val hotbarRequest = submit(HotbarRequest(hotbarIndex, request), false) - val validRotation = if (request.interactConfig.rotate) submit(rotation, false).done else true + val validRotation = if (request.interactConfig.rotate) submit(rotationRequest, false).done else true return hotbarRequest.done && validRotation } override fun getLogContextBuilder(): LogContextBuilder.() -> Unit = { group("Interaction Context") { text(blockPos.getLogContextBuilder()) - text(result.getLogContextBuilder()) - text(rotation.getLogContextBuilder()) + text(hitResult.getLogContextBuilder()) + text(rotationRequest.getLogContextBuilder()) value("Hotbar Index", hotbarIndex) value("Cached State", cachedState) value("Expected State", expectedState) diff --git a/src/main/kotlin/com/lambda/interaction/construction/context/PlaceContext.kt b/src/main/kotlin/com/lambda/interaction/construction/context/PlaceContext.kt index 9d70e202a..57aa55a69 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/context/PlaceContext.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/context/PlaceContext.kt @@ -36,8 +36,8 @@ import net.minecraft.util.math.BlockPos import java.awt.Color data class PlaceContext( - override val result: BlockHitResult, - override val rotation: RotationRequest, + override val hitResult: BlockHitResult, + override val rotationRequest: RotationRequest, override var hotbarIndex: Int, override val blockPos: BlockPos, override var cachedState: BlockState, @@ -61,7 +61,7 @@ data class PlaceContext( }.thenBy { it.sneak == (mc.player?.isSneaking ?: false) }.thenBy { - it.rotation.target.angleDistance + it.rotationRequest.target.angleDistance }.thenBy { it.hotbarIndex == HotbarManager.serverSlot }.thenBy { @@ -74,13 +74,13 @@ data class PlaceContext( } override fun ShapeBuilder.buildRenderer() { - box(blockPos, expectedState, baseColor, sideColor, result.side.mask) + box(blockPos, expectedState, baseColor, sideColor, hitResult.side.mask) } fun requestDependencies(request: PlaceRequest): Boolean { val hotbarRequest = submit(HotbarRequest(hotbarIndex, this), false) val validRotation = if (request.placeConfig.rotateForPlace) { - submit(rotation, false).done && currentDirIsValid + submit(rotationRequest, false).done && currentDirIsValid } else true return hotbarRequest.done && validRotation } @@ -88,8 +88,8 @@ data class PlaceContext( override fun getLogContextBuilder(): LogContextBuilder.() -> Unit = { group("Place Context") { text(blockPos.getLogContextBuilder()) - text(result.getLogContextBuilder()) - text(rotation.getLogContextBuilder()) + text(hitResult.getLogContextBuilder()) + text(rotationRequest.getLogContextBuilder()) value("Hotbar Index", hotbarIndex) value("Cached State", cachedState) value("Expected State", expectedState) diff --git a/src/main/kotlin/com/lambda/interaction/construction/processing/ProcessorRegistry.kt b/src/main/kotlin/com/lambda/interaction/construction/processing/ProcessorRegistry.kt index a11e7bb04..1d219c4d6 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/processing/ProcessorRegistry.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/processing/ProcessorRegistry.kt @@ -21,14 +21,13 @@ import com.lambda.core.Loadable import com.lambda.interaction.construction.verify.TargetState import com.lambda.util.reflections.getInstances import net.minecraft.block.BlockState -import net.minecraft.block.SlabBlock -import net.minecraft.block.enums.SlabType import net.minecraft.state.property.Properties import net.minecraft.util.math.BlockPos +import java.util.* object ProcessorRegistry : Loadable { private val processors = getInstances() - private val processorCache = mutableMapOf() + private val processorCache = Collections.synchronizedMap(mutableMapOf()) val postProcessedProperties = setOf( Properties.EXTENDED, @@ -107,30 +106,21 @@ object ProcessorRegistry : Loadable { override fun load() = "Loaded ${processors.size} pre processors" - fun TargetState.getProcessingInfo(pos: BlockPos) = - if (this is TargetState.State) { + fun TargetState.getProcessingInfo(pos: BlockPos): PreProcessingInfo? = + if (this !is TargetState.State) PreProcessingInfo.DEFAULT + else { val get: () -> PreProcessingInfo? = get@{ val infoAccumulator = PreProcessingInfoAccumulator() processors.forEach { processor -> if (!processor.acceptsState(blockState)) return@forEach processor.preProcess(blockState, pos, infoAccumulator) - if (infoAccumulator.shouldBeOmitted) { + if (infoAccumulator.shouldBeOmitted) return@get null - } } infoAccumulator.complete() } - if (isExemptFromCache()) { - get() - } else { - processorCache.getOrPut(blockState, get) - } - } else { - PreProcessingInfo.DEFAULT + processorCache.getOrPut(blockState, get) } - - private fun TargetState.State.isExemptFromCache() = - blockState.block is SlabBlock && blockState.get(Properties.SLAB_TYPE) == SlabType.DOUBLE } diff --git a/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/BlockHalfPreProcessor.kt b/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/BlockHalfPreProcessor.kt index ef4ea9515..4b883645f 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/BlockHalfPreProcessor.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/BlockHalfPreProcessor.kt @@ -37,8 +37,8 @@ object BlockHalfPreProcessor : PlacementProcessor() { val slab = state.get(Properties.BLOCK_HALF) ?: return val surfaceScan = when (slab) { - BlockHalf.BOTTOM -> SurfaceScan(ScanMode.LESSER_BLOCK_HALF, Direction.Axis.Y) - BlockHalf.TOP -> SurfaceScan(ScanMode.GREATER_BLOCK_HALF, Direction.Axis.Y) + BlockHalf.BOTTOM -> SurfaceScan(ScanMode.LesserBlockHalf, Direction.Axis.Y) + BlockHalf.TOP -> SurfaceScan(ScanMode.GreaterBlockHalf, Direction.Axis.Y) } accumulator.offerSurfaceScan(surfaceScan) diff --git a/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/DoorHingePreProcessor.kt b/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/DoorHingePreProcessor.kt index 07abad61d..251965755 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/DoorHingePreProcessor.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/DoorHingePreProcessor.kt @@ -39,19 +39,19 @@ object DoorHingePreProcessor : PlacementProcessor() { val side = state.get(Properties.DOOR_HINGE) ?: return@runSafe val scanner = when (state.get(Properties.HORIZONTAL_FACING) ?: return@runSafe) { Direction.NORTH -> - if (side == DoorHinge.LEFT) SurfaceScan(ScanMode.LESSER_BLOCK_HALF, Direction.Axis.X) - else SurfaceScan(ScanMode.GREATER_BLOCK_HALF, Direction.Axis.X) + if (side == DoorHinge.LEFT) SurfaceScan(ScanMode.LesserBlockHalf, Direction.Axis.X) + else SurfaceScan(ScanMode.GreaterBlockHalf, Direction.Axis.X) Direction.EAST -> - if (side == DoorHinge.LEFT) SurfaceScan(ScanMode.LESSER_BLOCK_HALF, Direction.Axis.Z) - else SurfaceScan(ScanMode.GREATER_BLOCK_HALF, Direction.Axis.Z) + if (side == DoorHinge.LEFT) SurfaceScan(ScanMode.LesserBlockHalf, Direction.Axis.Z) + else SurfaceScan(ScanMode.GreaterBlockHalf, Direction.Axis.Z) Direction.SOUTH -> - if (side == DoorHinge.LEFT) SurfaceScan(ScanMode.GREATER_BLOCK_HALF, Direction.Axis.X) - else SurfaceScan(ScanMode.LESSER_BLOCK_HALF, Direction.Axis.X) + if (side == DoorHinge.LEFT) SurfaceScan(ScanMode.GreaterBlockHalf, Direction.Axis.X) + else SurfaceScan(ScanMode.LesserBlockHalf, Direction.Axis.X) Direction.DOWN, Direction.UP, Direction.WEST -> - if (side == DoorHinge.LEFT) SurfaceScan(ScanMode.GREATER_BLOCK_HALF, Direction.Axis.Z) - else SurfaceScan(ScanMode.LESSER_BLOCK_HALF, Direction.Axis.Z) + if (side == DoorHinge.LEFT) SurfaceScan(ScanMode.GreaterBlockHalf, Direction.Axis.Z) + else SurfaceScan(ScanMode.LesserBlockHalf, Direction.Axis.Z) } accumulator.offerSurfaceScan(scanner) } ?: Unit diff --git a/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/SlabPreProcessor.kt b/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/SlabPreProcessor.kt index 08bb334ca..d1fefe8ba 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/SlabPreProcessor.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/processing/preprocessors/SlabPreProcessor.kt @@ -40,14 +40,14 @@ object SlabPreProcessor : PlacementProcessor() { val currentState = runSafe { blockState(pos) } ?: return val surfaceScan = when (slab) { - SlabType.BOTTOM -> SurfaceScan(ScanMode.LESSER_BLOCK_HALF, Direction.Axis.Y) - SlabType.TOP -> SurfaceScan(ScanMode.GREATER_BLOCK_HALF, Direction.Axis.Y) + SlabType.BOTTOM -> SurfaceScan(ScanMode.LesserBlockHalf, Direction.Axis.Y) + SlabType.TOP -> SurfaceScan(ScanMode.GreaterBlockHalf, Direction.Axis.Y) SlabType.DOUBLE -> { accumulator.addIgnores(Properties.SLAB_TYPE) if (currentState.block !is SlabBlock) SurfaceScan.DEFAULT else when (currentState.get(Properties.SLAB_TYPE)) { - SlabType.BOTTOM -> SurfaceScan(ScanMode.GREATER_BLOCK_HALF, Direction.Axis.Y) - else -> SurfaceScan(ScanMode.LESSER_BLOCK_HALF, Direction.Axis.Y) + SlabType.BOTTOM -> SurfaceScan(ScanMode.GreaterBlockHalf, Direction.Axis.Y) + else -> SurfaceScan(ScanMode.LesserBlockHalf, Direction.Axis.Y) } } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/BuildResult.kt b/src/main/kotlin/com/lambda/interaction/construction/result/BuildResult.kt index 9b2c98e7e..7f1158d68 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/result/BuildResult.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/result/BuildResult.kt @@ -17,281 +17,12 @@ package com.lambda.interaction.construction.result -import baritone.api.pathing.goals.GoalBlock -import baritone.api.pathing.goals.GoalNear -import com.lambda.context.Automated -import com.lambda.graphics.renderer.esp.ShapeBuilder -import com.lambda.interaction.construction.context.BuildContext -import com.lambda.interaction.material.StackSelection -import com.lambda.interaction.material.StackSelection.Companion.select -import com.lambda.interaction.material.container.ContainerManager.transfer -import com.lambda.interaction.material.container.MaterialContainer -import com.lambda.interaction.material.container.containers.MainHandContainer import com.lambda.util.Nameable -import net.minecraft.block.BlockState -import net.minecraft.item.ItemStack import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Direction -import net.minecraft.util.math.Vec3d -import java.awt.Color -abstract class BuildResult : ComparableResult, Nameable { - abstract val blockPos: BlockPos - open val pausesParent = false - override val name: String get() = "${this::class.simpleName} at ${blockPos.toShortString()}" +abstract class BuildResult : Nameable, ComparableResult { + abstract val pos: BlockPos + override val compareBy = this - interface Contextual { - val context: BuildContext - } - - /** - * The build action is done. - */ - data class Done( - override val blockPos: BlockPos, - ) : BuildResult() { - override val name: String - get() = "Build at $blockPos is done." - override val rank = Rank.DONE - } - - /** - * The build action is ignored. - */ - data class Ignored( - override val blockPos: BlockPos, - ) : BuildResult() { - override val name: String - get() = "Build at $blockPos is ignored." - override val rank = Rank.IGNORED - } - - /** - * The chunk at the target is not loaded. - * @param blockPos The position of the block that is in an unloaded chunk. - */ - data class ChunkNotLoaded( - override val blockPos: BlockPos, - ) : Navigable, Drawable, BuildResult() { - override val name: String get() = "Chunk at $blockPos is not loaded." - override val rank = Rank.CHUNK_NOT_LOADED - private val color = Color(252, 165, 3, 100) - - override val goal = GoalBlock(blockPos) - - override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) - } - - override fun compareTo(other: ComparableResult): Int { - return when (other) { - is ChunkNotLoaded -> blockPos.compareTo(other.blockPos) - else -> super.compareTo(other) - } - } - } - - /** - * The player has no permission to interact with the block. (E.g.: Adventure mode) - * @param blockPos The position of the block that is restricted. - */ - data class Restricted( - override val blockPos: BlockPos, - ) : Drawable, BuildResult() { - override val name: String get() = "Restricted at $blockPos." - override val rank = Rank.BREAK_RESTRICTED - private val color = Color(255, 0, 0, 100) - - override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) - } - } - - /** - * The block needs server permission to be broken. (Needs op) - * @param blockPos The position of the block that needs permission. - * @param blockState The state of the block that needs permission. - */ - data class NoPermission( - override val blockPos: BlockPos, - val blockState: BlockState, - ) : Drawable, BuildResult() { - override val name: String get() = "No permission at $blockPos." - override val rank get() = Rank.BREAK_NO_PERMISSION - private val color = Color(255, 0, 0, 100) - - override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) - } - } - - /** - * The break target is out of the world border or height limit. - * @param blockPos The position of the block that is out of the world. - */ - data class OutOfWorld( - override val blockPos: BlockPos, - ) : Drawable, BuildResult() { - override val name: String get() = "$blockPos is out of the world." - override val rank = Rank.OUT_OF_WORLD - private val color = Color(3, 148, 252, 100) - - override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) - } - } - - /** - * The block is unbreakable. - * @param blockPos The position of the block that is unbreakable. - * @param blockState The state of the block that is unbreakable. - */ - data class Unbreakable( - override val blockPos: BlockPos, - val blockState: BlockState, - ) : Drawable, BuildResult() { - override val name: String get() = "Unbreakable at $blockPos." - override val rank = Rank.UNBREAKABLE - private val color = Color(11, 11, 11, 100) - - override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) - } - } - - /** - * The checked configuration hits on a side not in the player direction. - * @param blockPos The position of the block that is not exposed. - * @param side The side that is not exposed. - */ - data class NotVisible( - override val blockPos: BlockPos, - val hitPos: BlockPos, - val side: Direction, - val distance: Double, - ) : Drawable, BuildResult() { - override val name: String get() = "Not visible at $blockPos." - override val rank = Rank.NOT_VISIBLE - private val color = Color(46, 0, 0, 80) - - override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) - } - - override fun compareTo(other: ComparableResult): Int { - return when (other) { - is NotVisible -> distance.compareTo(other.distance) - else -> super.compareTo(other) - } - } - } - - /** - * Player has an inefficient tool equipped. - * @param neededSelection The best tool for the block state. - */ - data class WrongItemSelection( - override val blockPos: BlockPos, - val context: BuildContext, - val neededSelection: StackSelection, - val currentItem: ItemStack - ) : Drawable, Resolvable, BuildResult() { - override val name: String get() = "Wrong item ($currentItem) for ${blockPos.toShortString()} need $neededSelection" - override val rank = Rank.WRONG_ITEM - private val color = Color(3, 252, 169, 25) - - override val pausesParent get() = true - - context(automated: Automated) - override fun resolve() = - neededSelection.transfer(MainHandContainer) - ?: MaterialContainer.AwaitItemTask( - "Couldn't find $neededSelection anywhere.", - neededSelection, - automated - ) - - override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) - } - - override fun compareTo(other: ComparableResult): Int { - return when (other) { - is WrongItemSelection -> context.compareTo(other.context) - else -> super.compareTo(other) - } - } - } - - /** - * The Player has the wrong item stack selected. - * @param blockPos The position of the block that needs a different tool. - * @param neededStack The best tool for the block state. - */ - data class WrongStack( - override val blockPos: BlockPos, - val context: BuildContext, - val neededStack: ItemStack - ) : Drawable, Resolvable, BuildResult() { - override val name: String get() = "Wrong stack for ${blockPos.toShortString()} need $neededStack." - override val rank = Rank.WRONG_ITEM - private val color = Color(3, 252, 169, 25) - - override val pausesParent get() = true - - context(automated: Automated) - override fun resolve() = - neededStack.select().let { selection -> - selection.transfer(MainHandContainer) - ?: MaterialContainer.AwaitItemTask( - "Couldn't find ${neededStack.item.name.string} anywhere.", - selection, - automated - ) - } - - override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) - } - - override fun compareTo(other: ComparableResult): Int { - return when (other) { - is WrongItemSelection -> context.compareTo(other.context) - else -> super.compareTo(other) - } - } - } - - /** - * Represents a break out of reach. - * @param blockPos The position of the block that is out of reach. - * @param pov The point of view of the player. - * @param misses The points that are out of reach. - */ - data class OutOfReach( - override val blockPos: BlockPos, - val pov: Vec3d, - val misses: Set, - ) : Navigable, Drawable, BuildResult() { - override val name: String get() = "Out of reach at $blockPos." - override val rank = Rank.OUT_OF_REACH - private val color = Color(252, 3, 207, 25) - - val distance: Double by lazy { - misses.minOfOrNull { pov.distanceTo(it) } ?: 0.0 - } - - override val goal = GoalNear(blockPos, 3) - - override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) - } - - override fun compareTo(other: ComparableResult): Int { - return when (other) { - is OutOfReach -> distance.compareTo(other.distance) - else -> super.compareTo(other) - } - } - } -} + final override fun compareTo(other: ComparableResult) = super.compareTo(other) +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/ComparableResult.kt b/src/main/kotlin/com/lambda/interaction/construction/result/ComparableResult.kt index 26639b67e..efd8c1c49 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/result/ComparableResult.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/result/ComparableResult.kt @@ -17,10 +17,13 @@ package com.lambda.interaction.construction.result -sealed interface ComparableResult> : Comparable> { +interface ComparableResult> : Comparable> { val rank: T + val compareBy: ComparableResult - override fun compareTo(other: ComparableResult): Int { - return rank.compareTo(other.rank) - } + override fun compareTo(other: ComparableResult) = + compareBy.compareResult(other.compareBy) + + fun compareResult(other: ComparableResult): Int = + compareBy.rank.compareTo(other.compareBy.rank) } diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/Contextual.kt b/src/main/kotlin/com/lambda/interaction/construction/result/Contextual.kt new file mode 100644 index 000000000..a95f01cb5 --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/result/Contextual.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.result + +import com.lambda.interaction.construction.context.BuildContext +import com.lambda.interaction.request.hotbar.HotbarManager + +interface Contextual : ComparableResult { + val context: BuildContext + + override fun compareResult(other: ComparableResult) = + when (other) { + + is Contextual -> compareByDescending { + it.context.hotbarIndex == HotbarManager.serverSlot + }.thenBy { + it.compareBy.rank + }.compare(this, other) + + else -> compareBy.rank.compareTo(other.compareBy.rank) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/Dependent.kt b/src/main/kotlin/com/lambda/interaction/construction/result/Dependent.kt new file mode 100644 index 000000000..d688892bc --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/result/Dependent.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.result + +interface Dependent { + val dependency: BuildResult + val lastDependency: BuildResult + + companion object { + val Dependent.iterator + get() = generateSequence(dependency) { (it as? Dependent)?.dependency } + } + + class Nested(override val dependency: BuildResult) : Dependent { + override val lastDependency = iterator.last() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/Rank.kt b/src/main/kotlin/com/lambda/interaction/construction/result/Rank.kt index 3581b5853..c199cf41b 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/result/Rank.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/result/Rank.kt @@ -19,38 +19,35 @@ package com.lambda.interaction.construction.result enum class Rank { // solvable - BREAK_SUCCESS, - INTERACT_SUCCESS, - PLACE_SUCCESS, - WRONG_ITEM, - BREAK_ITEM_CANT_MINE, - PLACE_BLOCKED_BY_PLAYER, - NOT_VISIBLE, - OUT_OF_REACH, - PLACE_BLOCKED_BY_ENTITY, - BREAK_NOT_EXPOSED, - CHUNK_NOT_LOADED, - PLACE_CANT_REPLACE, - BREAK_PLAYER_ON_TOP, - PLACE_NOT_ITEM_BLOCK, + PlaceSuccess, + BreakSuccess, + InteractSuccess, + WrongItem, + BreakItemCantMine, + PlaceBlockedByPlayer, + NotVisible, + OutOfReach, + PlaceBlockedByEntity, + BreakNotExposed, + ChunkNotLoaded, + PlaceCantReplace, + BreakPlayerOnTop, // not solvable - OUT_OF_WORLD, - BREAK_RESTRICTED, - PLACE_NO_INTEGRITY, - BREAK_SUBMERGE, - BREAK_IS_BLOCKED_BY_FLUID, - UNBREAKABLE, - BREAK_NO_PERMISSION, - PLACE_SCAFFOLD_EXCEEDED, - PLACE_BLOCK_FEATURE_DISABLED, - UNEXPECTED_POSITION, - PLACE_ILLEGAL_USAGE, + OutOfWorld, + BreakRestricted, + PlaceNoIntegrity, + BreakSubmerge, + BreakIsBlockedByFluid, + Unbreakable, + BreakNoPermission, + PlaceScaffoldExceeded, + PlaceBlockFeatureDisabled, + UnexpectedPosition, + PlaceIllegalUsage, // not an issue - DONE, - IGNORED; - - val solvable: Boolean - get() = ordinal < PLACE_NOT_ITEM_BLOCK.ordinal + Done, + Ignored, + NoMatch; } diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/BreakResult.kt b/src/main/kotlin/com/lambda/interaction/construction/result/results/BreakResult.kt similarity index 63% rename from src/main/kotlin/com/lambda/interaction/construction/result/BreakResult.kt rename to src/main/kotlin/com/lambda/interaction/construction/result/results/BreakResult.kt index b7b8ef340..d442fba00 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/result/BreakResult.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/result/results/BreakResult.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.lambda.interaction.construction.result +package com.lambda.interaction.construction.result.results import baritone.api.pathing.goals.GoalBlock import baritone.api.pathing.goals.GoalInverted @@ -23,6 +23,14 @@ import com.lambda.context.Automated import com.lambda.graphics.renderer.esp.DirectionMask.mask import com.lambda.graphics.renderer.esp.ShapeBuilder import com.lambda.interaction.construction.context.BreakContext +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.ComparableResult +import com.lambda.interaction.construction.result.Contextual +import com.lambda.interaction.construction.result.Dependent +import com.lambda.interaction.construction.result.Drawable +import com.lambda.interaction.construction.result.Navigable +import com.lambda.interaction.construction.result.Rank +import com.lambda.interaction.construction.result.Resolvable import com.lambda.interaction.material.StackSelection.Companion.selectStack import com.lambda.interaction.material.container.ContainerManager.transfer import com.lambda.interaction.material.container.MaterialContainer @@ -34,51 +42,50 @@ import net.minecraft.util.math.Direction import java.awt.Color sealed class BreakResult : BuildResult() { + override val name: String get() = "${this::class.simpleName} at ${pos.toShortString()}" /** * Represents a successful break. All checks have been passed. * @param context The context of the break. */ data class Break( - override val blockPos: BlockPos, + override val pos: BlockPos, override val context: BreakContext, - ) : Drawable, Contextual, BreakResult() { - override val rank = Rank.BREAK_SUCCESS + ) : Contextual, Drawable, BreakResult() { + override val rank = Rank.BreakSuccess override fun ShapeBuilder.buildRenderer() { with(context) { buildRenderer() } } - override fun compareTo(other: ComparableResult): Int { - return when (other) { + override fun compareResult(other: ComparableResult) = + when (other) { is Break -> context.compareTo(other.context) - else -> super.compareTo(other) + else -> super.compareResult(other) } - } } /** * Represents a break configuration where the hit side is not exposed to air. - * @param blockPos The position of the block that is not exposed. + * @param pos The position of the block that is not exposed. * @param side The side that is not exposed. */ data class NotExposed( - override val blockPos: BlockPos, + override val pos: BlockPos, val side: Direction, ) : Drawable, BreakResult() { - override val rank = Rank.BREAK_NOT_EXPOSED + override val rank = Rank.BreakNotExposed private val color = Color(46, 0, 0, 30) override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color, side.mask) + box(pos, color, color, side.mask) } - override fun compareTo(other: ComparableResult): Int { - return when (other) { - is NotExposed -> blockPos.compareTo(other.blockPos) - else -> super.compareTo(other) + override fun compareResult(other: ComparableResult) = + when (other) { + is NotExposed -> pos.compareTo(other.pos) + else -> super.compareResult(other) } - } } /** @@ -87,15 +94,13 @@ sealed class BreakResult : BuildResult() { * @param badItem The item that is being used. */ data class ItemCantMine( - override val blockPos: BlockPos, + override val pos: BlockPos, val blockState: BlockState, val badItem: Item ) : Drawable, Resolvable, BreakResult() { - override val rank = Rank.BREAK_ITEM_CANT_MINE + override val rank = Rank.BreakItemCantMine private val color = Color(255, 0, 0, 100) - override val pausesParent get() = true - context(automated: Automated) override fun resolve() = selectStack { @@ -110,31 +115,29 @@ sealed class BreakResult : BuildResult() { } override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) + box(pos, color, color) } - override fun compareTo(other: ComparableResult): Int { - return when (other) { + override fun compareResult(other: ComparableResult) = + when (other) { is ItemCantMine -> badItem.name.string.compareTo(other.badItem.name.string) - else -> super.compareTo(other) + else -> super.compareResult(other) } - } } /** * The block is a liquid and first has to be submerged. - * @param blockPos The position of the block that is a liquid. + * @param pos The position of the block that is a liquid. */ data class Submerge( - override val blockPos: BlockPos, - val blockState: BlockState, - val submerge: Set, + override val pos: BlockPos, + val blockState: BlockState ) : Drawable, BreakResult() { - override val rank = Rank.BREAK_SUBMERGE + override val rank = Rank.BreakSubmerge private val color = Color(114, 27, 255, 100) override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) + box(pos, color, color) } } @@ -142,14 +145,14 @@ sealed class BreakResult : BuildResult() { * The block is blocked by another liquid block that first has to be submerged. */ data class BlockedByFluid( - override val blockPos: BlockPos, + override val pos: BlockPos, val blockState: BlockState, ) : Drawable, BreakResult() { - override val rank = Rank.BREAK_IS_BLOCKED_BY_FLUID + override val rank = Rank.BreakIsBlockedByFluid private val color = Color(50, 12, 112, 100) override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) + box(pos, color, color) } } @@ -157,16 +160,24 @@ sealed class BreakResult : BuildResult() { * The player is standing on the block. */ data class PlayerOnTop( - override val blockPos: BlockPos, + override val pos: BlockPos, val blockState: BlockState, ) : Navigable, Drawable, BreakResult() { - override val rank = Rank.BREAK_PLAYER_ON_TOP + override val rank = Rank.BreakPlayerOnTop private val color = Color(252, 3, 207, 100) - override val goal = GoalInverted(GoalBlock(blockPos)) + override val goal = GoalInverted(GoalBlock(pos)) override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) + box(pos, color, color) } } + + data class Dependency( + override val pos: BlockPos, + override val dependency: BuildResult + ) : BreakResult(), Dependent by Dependent.Nested(dependency) { + override val rank = dependency.rank + override val compareBy = lastDependency + } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/results/GenericResult.kt b/src/main/kotlin/com/lambda/interaction/construction/result/results/GenericResult.kt new file mode 100644 index 000000000..e54216738 --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/result/results/GenericResult.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.result.results + +import baritone.api.pathing.goals.GoalNear +import com.lambda.context.Automated +import com.lambda.graphics.renderer.esp.ShapeBuilder +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.ComparableResult +import com.lambda.interaction.construction.result.Drawable +import com.lambda.interaction.construction.result.Navigable +import com.lambda.interaction.construction.result.Rank +import com.lambda.interaction.construction.result.Resolvable +import com.lambda.interaction.material.StackSelection +import com.lambda.interaction.material.container.ContainerManager.transfer +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.interaction.material.container.containers.MainHandContainer +import net.minecraft.item.ItemStack +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import java.awt.Color + +sealed class GenericResult : BuildResult() { + /** + * The checked configuration hits on a side not in the player direction. + * @param pos The position of the block that is not exposed. + * @param side The side that is not exposed. + */ + data class NotVisible( + override val pos: BlockPos, + val hitPos: BlockPos, + val distance: Double + ) : Drawable, GenericResult() { + override val name: String get() = "Not visible at $pos." + override val rank = Rank.NotVisible + private val color = Color(46, 0, 0, 80) + + override fun ShapeBuilder.buildRenderer() { + box(pos, color, color) + } + + override fun compareResult(other: ComparableResult): Int { + return when (other) { + is NotVisible -> distance.compareTo(other.distance) + else -> super.compareResult(other) + } + } + } + + /** + * The build action is ignored. + */ + data class Ignored( + override val pos: BlockPos, + ) : GenericResult() { + override val name: String + get() = "Build at $pos is ignored." + override val rank = Rank.Ignored + } + + /** + * Player has an inefficient tool equipped. + * @param neededSelection The best tool for the block state. + */ + data class WrongItemSelection( + override val pos: BlockPos, + val neededSelection: StackSelection, + val currentItem: ItemStack + ) : Drawable, Resolvable, GenericResult() { + override val name: String get() = "Wrong item ($currentItem) for ${pos.toShortString()} need $neededSelection" + override val rank = Rank.WrongItem + private val color = Color(3, 252, 169, 25) + + context(automated: Automated) + override fun resolve() = + neededSelection.transfer(MainHandContainer) + ?: MaterialContainer.AwaitItemTask( + "Couldn't find $neededSelection anywhere.", + neededSelection, + automated + ) + + override fun ShapeBuilder.buildRenderer() { + box(pos, color, color) + } + } + + /** + * Represents a break out of reach. + * @param pos The position of the block that is out of reach. + * @param pov The point of view of the player. + * @param misses The points that are out of reach. + */ + data class OutOfReach( + override val pos: BlockPos, + val pov: Vec3d, + val misses: Set, + ) : Navigable, Drawable, GenericResult() { + override val name: String get() = "Out of reach at $pos." + override val rank = Rank.OutOfReach + private val color = Color(252, 3, 207, 25) + + val distance: Double by lazy { + misses.minOfOrNull { pov.distanceTo(it) } ?: 0.0 + } + + override val goal = GoalNear(pos, 3) + + override fun ShapeBuilder.buildRenderer() { + box(pos, color, color) + } + + override fun compareResult(other: ComparableResult): Int { + return when (other) { + is OutOfReach -> distance.compareTo(other.distance) + else -> super.compareResult(other) + } + } + } +} diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/InteractResult.kt b/src/main/kotlin/com/lambda/interaction/construction/result/results/InteractResult.kt similarity index 55% rename from src/main/kotlin/com/lambda/interaction/construction/result/InteractResult.kt rename to src/main/kotlin/com/lambda/interaction/construction/result/results/InteractResult.kt index 6b05538d4..67a6012ad 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/result/InteractResult.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/result/results/InteractResult.kt @@ -15,27 +15,43 @@ * along with this program. If not, see . */ -package com.lambda.interaction.construction.result +package com.lambda.interaction.construction.result.results import com.lambda.graphics.renderer.esp.ShapeBuilder import com.lambda.interaction.construction.context.InteractionContext +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.ComparableResult +import com.lambda.interaction.construction.result.Contextual +import com.lambda.interaction.construction.result.Dependent +import com.lambda.interaction.construction.result.Drawable +import com.lambda.interaction.construction.result.Rank import net.minecraft.util.math.BlockPos sealed class InteractResult : BuildResult() { + override val name: String get() = "${this::class.simpleName} at ${pos.toShortString()}" + data class Interact( - override val blockPos: BlockPos, + override val pos: BlockPos, override val context: InteractionContext ) : Contextual, Drawable, InteractResult() { - override val rank = Rank.INTERACT_SUCCESS + override val rank = Rank.InteractSuccess override fun ShapeBuilder.buildRenderer() { with(context) { buildRenderer() } } - override fun compareTo(other: ComparableResult) = + override fun compareResult(other: ComparableResult) = when (other) { is Interact -> context.compareTo(other.context) - else -> super.compareTo(other) + else -> super.compareResult(other) } } + + data class Dependency( + override val pos: BlockPos, + override val dependency: BuildResult + ) : InteractResult(), Dependent by Dependent.Nested(dependency) { + override val rank = dependency.rank + override val compareBy = lastDependency + } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/PlaceResult.kt b/src/main/kotlin/com/lambda/interaction/construction/result/results/PlaceResult.kt similarity index 63% rename from src/main/kotlin/com/lambda/interaction/construction/result/PlaceResult.kt rename to src/main/kotlin/com/lambda/interaction/construction/result/results/PlaceResult.kt index 9905cf0c7..187e769f5 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/result/PlaceResult.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/result/results/PlaceResult.kt @@ -15,17 +15,22 @@ * along with this program. If not, see . */ -package com.lambda.interaction.construction.result +package com.lambda.interaction.construction.result.results import baritone.api.pathing.goals.GoalBlock import baritone.api.pathing.goals.GoalInverted import com.lambda.context.Automated import com.lambda.graphics.renderer.esp.ShapeBuilder import com.lambda.interaction.construction.context.PlaceContext -import com.lambda.interaction.construction.verify.TargetState -import com.lambda.task.Task +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.ComparableResult +import com.lambda.interaction.construction.result.Contextual +import com.lambda.interaction.construction.result.Dependent +import com.lambda.interaction.construction.result.Drawable +import com.lambda.interaction.construction.result.Navigable +import com.lambda.interaction.construction.result.Rank +import com.lambda.interaction.construction.result.Resolvable import com.lambda.task.tasks.BuildTask.Companion.breakBlock -import com.lambda.task.tasks.BuildTask.Companion.build import net.minecraft.block.BlockState import net.minecraft.entity.Entity import net.minecraft.item.ItemPlacementContext @@ -36,29 +41,31 @@ import java.awt.Color /** * [PlaceResult] represents the result of a placement simulation. * Holds data about the placement and the result of the simulation. - * Every [BuildResult] can [resolve] its own problem. - * Every [BuildResult] can be compared to another [BuildResult]. - * First based on the context, then based on the [Rank]. + * Every [GenericResult] can [resolve] its own problem. + * Every [GenericResult] can be compared to another [GenericResult]. + * First based on the context, then based on the [com.lambda.interaction.construction.result.Rank]. */ sealed class PlaceResult : BuildResult() { + override val name: String get() = "${this::class.simpleName} at ${pos.toShortString()}" + /** * Represents a successful placement. All checks have been passed. * @param context The context of the placement. */ data class Place( - override val blockPos: BlockPos, + override val pos: BlockPos, override val context: PlaceContext, ) : Contextual, Drawable, PlaceResult() { - override val rank = Rank.PLACE_SUCCESS + override val rank = Rank.PlaceSuccess override fun ShapeBuilder.buildRenderer() { with(context) { buildRenderer() } } - override fun compareTo(other: ComparableResult) = + override fun compareResult(other: ComparableResult) = when (other) { is Place -> context.compareTo(other.context) - else -> super.compareTo(other) + else -> super.compareResult(other) } } @@ -68,52 +75,52 @@ sealed class PlaceResult : BuildResult() { * This class is used to provide details about a block placement issue in which the actual block * placed does not match the expected state, or additional integrity conditions are not met. * - * @property blockPos The position of the block being inspected or placed. + * @property pos The position of the block being inspected or placed. * @property expected The expected state of the block. * @property simulated The context of the item placement simulation. * @property actual The expected */ data class NoIntegrity( - override val blockPos: BlockPos, + override val pos: BlockPos, val expected: BlockState, val simulated: ItemPlacementContext, val actual: BlockState? = null, ) : Drawable, PlaceResult() { - override val rank = Rank.PLACE_NO_INTEGRITY + override val rank = Rank.PlaceNoIntegrity private val color = Color(252, 3, 3, 100) override fun ShapeBuilder.buildRenderer() { - box(blockPos, expected, color, color) + box(pos, expected, color, color) } } /** * Represents a scenario where block placement is obstructed by the player itself. * - * @property blockPos The position of the block that was attempted to be placed. + * @property pos The position of the block that was attempted to be placed. */ data class BlockedBySelf( - override val blockPos: BlockPos + override val pos: BlockPos ) : Drawable, Navigable, PlaceResult() { - override val rank = Rank.PLACE_BLOCKED_BY_PLAYER + override val rank = Rank.PlaceBlockedByPlayer private val color = Color(252, 3, 3, 100) - override val goal = GoalInverted(GoalBlock(blockPos)) + override val goal = GoalInverted(GoalBlock(pos)) override fun ShapeBuilder.buildRenderer() { - box(blockPos, color, color) + box(pos, color, color) } } /** * Represents a scenario where block placement is obstructed by an entity. * - * @property blockPos The position of the block that was attempted to be placed. + * @property pos The position of the block that was attempted to be placed. */ data class BlockedByEntity( - override val blockPos: BlockPos, + override val pos: BlockPos, val entities: List ) : Drawable, PlaceResult() { - override val rank = Rank.PLACE_BLOCKED_BY_ENTITY + override val rank = Rank.PlaceBlockedByEntity private val color = Color(252, 3, 3, 100) override fun ShapeBuilder.buildRenderer() { @@ -124,82 +131,76 @@ sealed class PlaceResult : BuildResult() { /** * Represents a result indicating that a block cannot be replaced during a placement operation. * - * @property blockPos The position of the block that cannot be replaced. + * @property pos The position of the block that cannot be replaced. * @property simulated The context of the item placement simulation. */ data class CantReplace( - override val blockPos: BlockPos, + override val pos: BlockPos, val simulated: ItemPlacementContext, ) : Resolvable, PlaceResult() { - override val rank = Rank.PLACE_CANT_REPLACE + override val rank = Rank.PlaceCantReplace context(automated: Automated) - override fun resolve() = automated.breakBlock(blockPos) + override fun resolve() = automated.breakBlock(pos) } /** * Represents a placement result indicating that the scaffolding placement has exceeded the allowed limits. * - * @property blockPos The position of the block where the placement attempt occurred. + * @property pos The position of the block where the placement attempt occurred. * @property simulated The context of the simulated item placement attempt. */ data class ScaffoldExceeded( - override val blockPos: BlockPos, - val simulated: ItemPlacementContext, + override val pos: BlockPos ) : PlaceResult() { - override val rank = Rank.PLACE_SCAFFOLD_EXCEEDED + override val rank = Rank.PlaceScaffoldExceeded } /** * Represents a result where a block placement operation was prevented because * the relevant block feature is disabled. * - * @property blockPos The position of the block that could not be placed. + * @property pos The position of the block that could not be placed. * @property itemStack The item stack associated with the attempted placement. */ data class BlockFeatureDisabled( - override val blockPos: BlockPos, + override val pos: BlockPos, val itemStack: ItemStack, ) : PlaceResult() { - override val rank = Rank.PLACE_BLOCK_FEATURE_DISABLED + override val rank = Rank.PlaceBlockFeatureDisabled } /** * Represents a result state where the placement or manipulation of a block resulted in an unexpected position. * - * @property blockPos The intended position of the block. + * @property pos The intended position of the block. * @property actualPos The actual position of the block, which differs from the intended position. */ data class UnexpectedPosition( - override val blockPos: BlockPos, + override val pos: BlockPos, val actualPos: BlockPos, ) : PlaceResult() { - override val rank = Rank.UNEXPECTED_POSITION + override val rank = Rank.UnexpectedPosition } /** * Represents a result indicating an illegal usage during a placement operation. * E.g., the player can't modify the world or the block cannot be placed against the surface. * - * @property blockPos The position of the block associated with the illegal usage result. + * @property pos The position of the block associated with the illegal usage result. * @property rank The ranking of this result, which is always `PLACE_ILLEGAL_USAGE`. */ data class IllegalUsage( - override val blockPos: BlockPos, + override val pos: BlockPos, ) : PlaceResult() { - override val rank = Rank.PLACE_ILLEGAL_USAGE + override val rank = Rank.PlaceIllegalUsage } - /** - * Represents the result of a place operation where the provided item does not match the expected item block type. - * - * @property blockPos The position of the block where the operation was attempted. - * @property itemStack The item stack that was checked during the place operation. - */ - data class NotItemBlock( - override val blockPos: BlockPos, - val itemStack: ItemStack, - ) : PlaceResult() { - override val rank = Rank.PLACE_NOT_ITEM_BLOCK + data class Dependency( + override val pos: BlockPos, + override val dependency: BuildResult + ) : PlaceResult(), Dependent by Dependent.Nested(dependency) { + override val rank = lastDependency.rank + override val compareBy = lastDependency } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/results/PostSimResult.kt b/src/main/kotlin/com/lambda/interaction/construction/result/results/PostSimResult.kt new file mode 100644 index 000000000..2f016a0f7 --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/result/results/PostSimResult.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.result.results + +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.Rank +import net.minecraft.util.math.BlockPos + +sealed class PostSimResult : BuildResult() { + override val name: String get() = "${this::class.simpleName} at ${pos.toShortString()}" + + data class NoMatch( + override val pos: BlockPos, + ) : PostSimResult() { + override val rank = Rank.NoMatch + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/result/results/PreSimResult.kt b/src/main/kotlin/com/lambda/interaction/construction/result/results/PreSimResult.kt new file mode 100644 index 000000000..29d72968e --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/result/results/PreSimResult.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.result.results + +import baritone.api.pathing.goals.GoalBlock +import com.lambda.graphics.renderer.esp.ShapeBuilder +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.ComparableResult +import com.lambda.interaction.construction.result.Drawable +import com.lambda.interaction.construction.result.Navigable +import com.lambda.interaction.construction.result.Rank +import net.minecraft.block.BlockState +import net.minecraft.util.math.BlockPos +import java.awt.Color + +sealed class PreSimResult : BuildResult() { + override val name: String get() = "${this::class.simpleName} at ${pos.toShortString()}" + + /** + * The build action is done. + */ + data class Done( + override val pos: BlockPos, + ) : PreSimResult() { + override val name: String + get() = "Build at $pos is done." + override val rank = Rank.Done + } + + /** + * The chunk at the target is not loaded. + * @param pos The position of the block that is in an unloaded chunk. + */ + data class ChunkNotLoaded( + override val pos: BlockPos, + ) : Navigable, Drawable, PreSimResult() { + override val name: String get() = "Chunk at $pos is not loaded." + override val rank = Rank.ChunkNotLoaded + private val color = Color(252, 165, 3, 100) + + override val goal = GoalBlock(pos) + + override fun ShapeBuilder.buildRenderer() { + box(pos, color, color) + } + + override fun compareResult(other: ComparableResult) = + when (other) { + is ChunkNotLoaded -> pos.compareTo(other.pos) + else -> super.compareResult(other) + } + } + + /** + * The player has no permission to interact with the block. (E.g.: Adventure mode) + * @param pos The position of the block that is restricted. + */ + data class Restricted( + override val pos: BlockPos, + ) : Drawable, PreSimResult() { + override val name: String get() = "Restricted at $pos." + override val rank = Rank.BreakRestricted + private val color = Color(255, 0, 0, 100) + + override fun ShapeBuilder.buildRenderer() { + box(pos, color, color) + } + } + + /** + * The block needs server permission to be broken. (Needs op) + * @param pos The position of the block that needs permission. + * @param blockState The state of the block that needs permission. + */ + data class NoPermission( + override val pos: BlockPos, + val blockState: BlockState, + ) : Drawable, PreSimResult() { + override val name: String get() = "No permission at $pos." + override val rank get() = Rank.BreakNoPermission + private val color = Color(255, 0, 0, 100) + + override fun ShapeBuilder.buildRenderer() { + box(pos, color, color) + } + } + + /** + * The break target is out of the world border or height limit. + * @param pos The position of the block that is out of the world. + */ + data class OutOfWorld( + override val pos: BlockPos, + ) : Drawable, PreSimResult() { + override val name: String get() = "$pos is out of the world." + override val rank = Rank.OutOfWorld + private val color = Color(3, 148, 252, 100) + + override fun ShapeBuilder.buildRenderer() { + box(pos, color, color) + } + } + + /** + * The block is unbreakable. + * @param pos The position of the block that is unbreakable. + * @param blockState The state of the block that is unbreakable. + */ + data class Unbreakable( + override val pos: BlockPos, + val blockState: BlockState, + ) : Drawable, PreSimResult() { + override val name: String get() = "Unbreakable at $pos." + override val rank = Rank.Unbreakable + private val color = Color(11, 11, 11, 100) + + override fun ShapeBuilder.buildRenderer() { + box(pos, color, color) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt index 38315dd56..78612b403 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt @@ -19,893 +19,47 @@ package com.lambda.interaction.construction.simulation import com.lambda.context.AutomatedSafeContext import com.lambda.interaction.construction.blueprint.Blueprint -import com.lambda.interaction.construction.context.BreakContext -import com.lambda.interaction.construction.context.InteractionContext -import com.lambda.interaction.construction.context.PlaceContext -import com.lambda.interaction.construction.processing.PreProcessingInfo -import com.lambda.interaction.construction.processing.ProcessorRegistry.getProcessingInfo -import com.lambda.interaction.construction.result.BreakResult import com.lambda.interaction.construction.result.BuildResult -import com.lambda.interaction.construction.result.InteractResult -import com.lambda.interaction.construction.result.PlaceResult +import com.lambda.interaction.construction.result.results.PostSimResult +import com.lambda.interaction.construction.simulation.ISimInfo.Companion.sim +import com.lambda.interaction.construction.simulation.checks.BreakSim.Companion.simBreak +import com.lambda.interaction.construction.simulation.checks.PlaceSim.Companion.simPlacement +import com.lambda.interaction.construction.simulation.checks.PostProcessingSim.Companion.simPostProcessing import com.lambda.interaction.construction.verify.TargetState -import com.lambda.interaction.material.ContainerSelection.Companion.selectContainer -import com.lambda.interaction.material.StackSelection -import com.lambda.interaction.material.StackSelection.Companion.select -import com.lambda.interaction.material.StackSelection.Companion.selectStack -import com.lambda.interaction.material.container.ContainerManager.containerWithMaterial -import com.lambda.interaction.material.container.MaterialContainer -import com.lambda.interaction.request.rotating.Rotation.Companion.rotation -import com.lambda.interaction.request.rotating.Rotation.Companion.rotationTo -import com.lambda.interaction.request.rotating.RotationManager -import com.lambda.interaction.request.rotating.RotationRequest -import com.lambda.interaction.request.rotating.visibilty.PlaceDirection -import com.lambda.interaction.request.rotating.visibilty.VisibilityChecker.CheckedHit -import com.lambda.interaction.request.rotating.visibilty.VisibilityChecker.getVisibleSurfaces -import com.lambda.interaction.request.rotating.visibilty.VisibilityChecker.scanSurfaces -import com.lambda.interaction.request.rotating.visibilty.lookAt -import com.lambda.interaction.request.rotating.visibilty.lookAtBlock -import com.lambda.interaction.request.rotating.visibilty.lookInDirection -import com.lambda.util.BlockUtils import com.lambda.util.BlockUtils.blockState -import com.lambda.util.BlockUtils.calcItemBlockBreakingDelta -import com.lambda.util.BlockUtils.hasFluid -import com.lambda.util.BlockUtils.instantBreakable -import com.lambda.util.BlockUtils.isNotEmpty -import com.lambda.util.Communication.info -import com.lambda.util.Communication.warn -import com.lambda.util.math.distSq -import com.lambda.util.math.vec3d -import com.lambda.util.player.SlotUtils.hotbar -import com.lambda.util.player.copyPlayer -import com.lambda.util.player.gamemode -import com.lambda.util.world.WorldUtils.isLoaded -import com.lambda.util.world.raycast.RayCastUtils.blockResult +import io.ktor.util.collections.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import net.minecraft.block.BlockState -import net.minecraft.block.FallingBlock -import net.minecraft.block.OperatorBlock -import net.minecraft.block.ShapeContext -import net.minecraft.block.SlabBlock -import net.minecraft.block.Waterloggable -import net.minecraft.block.enums.SlabType -import net.minecraft.block.pattern.CachedBlockPosition -import net.minecraft.enchantment.Enchantments -import net.minecraft.entity.Entity -import net.minecraft.fluid.FlowableFluid -import net.minecraft.fluid.LavaFluid -import net.minecraft.fluid.WaterFluid -import net.minecraft.item.BlockItem -import net.minecraft.item.Item -import net.minecraft.item.ItemPlacementContext -import net.minecraft.item.ItemStack -import net.minecraft.item.ItemUsageContext -import net.minecraft.predicate.entity.EntityPredicates -import net.minecraft.registry.tag.ItemTags.DIAMOND_TOOL_MATERIALS -import net.minecraft.registry.tag.ItemTags.GOLD_TOOL_MATERIALS -import net.minecraft.registry.tag.ItemTags.IRON_TOOL_MATERIALS -import net.minecraft.registry.tag.ItemTags.NETHERITE_TOOL_MATERIALS -import net.minecraft.registry.tag.ItemTags.STONE_TOOL_MATERIALS -import net.minecraft.registry.tag.ItemTags.WOODEN_TOOL_MATERIALS -import net.minecraft.state.property.Properties -import net.minecraft.util.Hand -import net.minecraft.util.hit.BlockHitResult -import net.minecraft.util.math.BlockPos -import net.minecraft.util.math.Box -import net.minecraft.util.math.Direction +import kotlinx.coroutines.supervisorScope import net.minecraft.util.math.Vec3d -import net.minecraft.util.shape.VoxelShapes -import kotlin.math.pow -object BuildSimulator { - context(context: AutomatedSafeContext) - fun Blueprint.simulate(eye: Vec3d): Set = - runBlocking(Dispatchers.Default) { - structure.entries - .map { (pos, target) -> - async { - val preProcessing = target.getProcessingInfo(pos) ?: return@async emptySet() - - with(context) { - checkRequirements(pos, target).let { results -> - if (results.isNotEmpty()) return@async results - } - checkPostProcessResults(pos, eye, preProcessing, target).let { results -> - if (results.isNotEmpty()) return@async results - } - checkPlaceResults(pos, eye, preProcessing, target).let { results -> - if (results.isNotEmpty()) return@async results - } - checkBreakResults(pos, eye, preProcessing).let { results -> - if (results.isNotEmpty()) return@async results - } - } - - warn("Nothing matched $pos $target") - emptySet() - } - } - .awaitAll() - .flatMap { it } - .toSet() - } - - private fun AutomatedSafeContext.checkRequirements(pos: BlockPos, target: TargetState): Set { - val acc = mutableSetOf() - - /* the chunk is not loaded */ - if (!isLoaded(pos)) { - acc.add(BuildResult.ChunkNotLoaded(pos)) - return acc - } - - val state = blockState(pos) - - /* block is already in the correct state */ - if (target.matches(state, pos, world)) { - acc.add(BuildResult.Done(pos)) - return acc - } - - /* block should be ignored */ - if (state.block in breakConfig.ignoredBlocks && target.type == TargetState.Type.AIR) { - acc.add(BuildResult.Ignored(pos)) - return acc - } - - /* the player is in the wrong game mode to alter the block state */ - if (player.isBlockBreakingRestricted(world, pos, gamemode)) { - acc.add(BuildResult.Restricted(pos)) - return acc - } - - /* the player has no permissions to alter the block state */ - if (state.block is OperatorBlock && !player.isCreativeLevelTwoOp) { - acc.add(BuildResult.NoPermission(pos, state)) - return acc - } - - /* block is outside the world so it cant be altered */ - if (!world.worldBorder.contains(pos) || world.isOutOfHeightLimit(pos)) { - acc.add(BuildResult.OutOfWorld(pos)) - return acc - } - - /* block is unbreakable, so it cant be broken or replaced */ - if (state.getHardness(world, pos) < 0 && !gamemode.isCreative) { - acc.add(BuildResult.Unbreakable(pos, state)) - return acc - } - - return acc - } - - private fun AutomatedSafeContext.checkPostProcessResults( - pos: BlockPos, - eye: Vec3d, - preProcessing: PreProcessingInfo, - targetState: TargetState - ): Set { - if (targetState !is TargetState.State) return emptySet() - - val acc = mutableSetOf() - - val state = blockState(pos) - if (!targetState.matches(state, pos, world, preProcessing.ignore)) - return acc - - val interactBlock: (BlockState, Set?, Item?, Boolean) -> Unit = - interactBlock@ { expectedState, sides, item, placing -> - val boxes = state.getOutlineShape(world, pos).boundingBoxes.map { it.offset(pos) } - val validHits = mutableListOf() - val blockedHits = mutableSetOf() - val misses = mutableSetOf() - val airPlace = placing && placeConfig.airPlace.isEnabled - - boxes.forEach { box -> - val refinedSides = if (buildConfig.checkSideVisibility) { - box.getVisibleSurfaces(eye).let { visibleSides -> - sides?.let { specific -> - visibleSides.intersect(specific) - } ?: visibleSides.toSet() - } - } else sides ?: Direction.entries.toSet() - - scanSurfaces( - box, - refinedSides, - buildConfig.resolution, - preProcessing.surfaceScan - ) { hitSide, vec -> - val distSquared = eye distSq vec - if (distSquared > buildConfig.interactReach.pow(2)) { - misses.add(vec) - return@scanSurfaces - } - - val newRotation = eye.rotationTo(vec) - - val hit = if (buildConfig.strictRayCast) { - val rayCast = newRotation.rayCast(buildConfig.interactReach, eye) - when { - rayCast != null && (!airPlace || eye distSq rayCast.pos <= distSquared) -> - rayCast.blockResult - - airPlace -> { - val hitVec = newRotation.castBox(box, buildConfig.interactReach, eye) - BlockHitResult(hitVec, hitSide, pos, false) - } - - else -> null - } - } else { - val hitVec = newRotation.castBox(box, buildConfig.interactReach, eye) - BlockHitResult(hitVec, hitSide, pos, false) - } ?: return@scanSurfaces - - val checked = CheckedHit(hit, newRotation, buildConfig.interactReach) - if (hit.blockResult?.blockPos != pos) { - blockedHits.add(vec) - return@scanSurfaces - } - - validHits.add(checked) - } - } - - if (validHits.isEmpty()) { - if (misses.isNotEmpty()) { - acc.add(BuildResult.OutOfReach(pos, eye, misses)) - } else { - //ToDo: Must clean up surface scan usage / renders. Added temporary direction until changes are made - acc.add( - BuildResult.NotVisible( - pos, - pos, - Direction.UP, - eye.distanceTo(pos.offset(Direction.UP).vec3d) - ) - ) - } - return@interactBlock - } - - buildConfig.pointSelection.select(validHits)?.let { checkedHit -> - val checkedResult = checkedHit.hit - val rotationTarget = lookAt(checkedHit.targetRotation, 0.001) - val context = InteractionContext( - checkedResult.blockResult ?: return@interactBlock, - RotationRequest(rotationTarget, this), - player.inventory.selectedSlot, - state, - expectedState, - this - ) - - val stackSelection = (item ?: player.mainHandStack.item).select() - val hotbarCandidates = selectContainer { - matches(stackSelection) and ofAnyType(MaterialContainer.Rank.HOTBAR) - }.let { predicate -> - stackSelection.containerWithMaterial( predicate) - } - - if (hotbarCandidates.isEmpty()) { - acc.add( - BuildResult.WrongItemSelection( - pos, - context, - stackSelection, - player.mainHandStack - ) - ) - return@interactBlock - } else { - context.hotbarIndex = - player.hotbar.indexOf(hotbarCandidates.first().matchingStacks(stackSelection).first()) - } - - acc.add(InteractResult.Interact(pos, context)) - } - } - - val mismatchedProperties = state.properties.filter { state.get(it) != targetState.blockState.get(it) } - mismatchedProperties.forEach { property -> - when (property) { - Properties.EYE -> { - if (state.get(Properties.EYE)) return@forEach - val expectedState = state.with(Properties.EYE, true) - interactBlock(expectedState, null, null, false) - } - - Properties.INVERTED -> { - val expectedState = state.with(Properties.INVERTED, !state.get(Properties.INVERTED)) - interactBlock(expectedState, null, null, false) - } - - Properties.DELAY -> { - val expectedState = - state.with(Properties.DELAY, state.cycle(Properties.DELAY).get(Properties.DELAY)) - interactBlock(expectedState, null, null, false) - } - - Properties.COMPARATOR_MODE -> { - val expectedState = state.with( - Properties.COMPARATOR_MODE, - state.cycle(Properties.COMPARATOR_MODE).get(Properties.COMPARATOR_MODE) - ) - interactBlock(expectedState, null, null, false) - } - - Properties.OPEN -> { - val expectedState = state.with(Properties.OPEN, !state.get(Properties.OPEN)) - interactBlock(expectedState, null, null, false) - } - - Properties.SLAB_TYPE -> { - if (targetState.blockState.get(Properties.SLAB_TYPE) != SlabType.DOUBLE) return@forEach - checkPlaceResults( - pos, - eye, - preProcessing, - targetState - ).let { placeResults -> - acc.addAll(placeResults) - } - } - } - } - - if (acc.isEmpty()) { - acc.add(BuildResult.Done(pos)) - } - - return acc - } - - private fun AutomatedSafeContext.checkPlaceResults( - pos: BlockPos, - eye: Vec3d, - preProcessing: PreProcessingInfo, - targetState: TargetState - ): Set { - val acc = mutableSetOf() - val currentState = blockState(pos) - - val statePromoting = currentState.block is SlabBlock - && targetState.matches(currentState, pos, world, preProcessing.ignore) - // If the target state is air then the only possible blocks it could place are to remove liquids so we use the Solid TargetState - val nextTargetState = if (targetState is TargetState.Air) { - if (currentState.hasFluid) TargetState.Solid - else return acc - } else if (targetState.isEmpty()) { - // Otherwise if the target state is empty, there's no situation where placement would be required so we can return - return acc - } else targetState - // For example, slabs count as state promoting because you are placing another block to promote the current state to the target state - if (!currentState.isReplaceable && !statePromoting) return acc - - preProcessing.sides.forEach { neighbor -> - val hitPos = if (!placeConfig.airPlace.isEnabled && (currentState.isAir || statePromoting)) - pos.offset(neighbor) else pos - val hitSide = neighbor.opposite - if (!world.worldBorder.contains(hitPos)) return@forEach - - val voxelShape = blockState(hitPos).getOutlineShape(world, hitPos).let { outlineShape -> - if (!outlineShape.isEmpty || !placeConfig.airPlace.isEnabled) outlineShape - else VoxelShapes.fullCube() - } - if (voxelShape.isEmpty) return@forEach - - val boxes = voxelShape.boundingBoxes.map { it.offset(hitPos) } - val verify: CheckedHit.() -> Boolean = { - hit.blockResult?.blockPos == hitPos && hit.blockResult?.side == hitSide - } - - val validHits = mutableListOf() - val misses = mutableSetOf() - val reachSq = buildConfig.interactReach.pow(2) - - boxes.forEach { box -> - val sides = if (buildConfig.checkSideVisibility) { - box.getVisibleSurfaces(eye).intersect(setOf(hitSide)) - } else setOf(hitSide) - - scanSurfaces(box, sides, buildConfig.resolution, preProcessing.surfaceScan) { _, vec -> - val distSquared = eye distSq vec - if (distSquared > reachSq) { - misses.add(vec) - return@scanSurfaces - } - - val newRotation = eye.rotationTo(vec) - - val hit = if (buildConfig.strictRayCast) { - val rayCast = newRotation.rayCast(buildConfig.interactReach, eye) - when { - rayCast != null && (!placeConfig.airPlace.isEnabled || eye distSq rayCast.pos <= distSquared) -> - rayCast.blockResult - - placeConfig.airPlace.isEnabled -> { - val hitVec = newRotation.castBox(box, buildConfig.interactReach, eye) - BlockHitResult(hitVec, hitSide, hitPos, false) - } - - else -> null - } - } else { - val hitVec = newRotation.castBox(box, buildConfig.interactReach, eye) - BlockHitResult(hitVec, hitSide, hitPos, false) - } ?: return@scanSurfaces - - val checked = CheckedHit(hit, newRotation, buildConfig.interactReach) - if (!checked.verify()) return@scanSurfaces - - validHits.add(checked) - } - } - - if (validHits.isEmpty()) { - if (misses.isNotEmpty()) { - acc.add(BuildResult.OutOfReach(pos, eye, misses)) - return@forEach - } - - acc.add(BuildResult.NotVisible(pos, hitPos, hitSide, eye.distanceTo(hitPos.offset(hitSide).vec3d))) - return@forEach - } - - buildConfig.pointSelection.select(validHits)?.let { checkedHit -> - val optimalStack = nextTargetState.getStack(world, pos, this) - - // ToDo: For each hand and sneak or not? - val fakePlayer = copyPlayer(player).apply { - this.rotation = RotationManager.serverRotation - } - - val checkedResult = checkedHit.hit - - // ToDo: Override the stack used for this to account for blocks where replaceability is dependent on the held item - val usageContext = ItemUsageContext( - fakePlayer, - Hand.MAIN_HAND, - checkedResult.blockResult, - ) - val cachePos = CachedBlockPosition( - usageContext.world, usageContext.blockPos, false - ) - val canBePlacedOn = optimalStack.canPlaceOn(cachePos) - if (!player.abilities.allowModifyWorld && !canBePlacedOn) { - acc.add(PlaceResult.IllegalUsage(pos)) - return@forEach - } - - var context = ItemPlacementContext(usageContext) - - if (context.blockPos != pos) { - acc.add(PlaceResult.UnexpectedPosition(pos, context.blockPos)) - return@forEach - } - - if (!optimalStack.item.isEnabled(world.enabledFeatures)) { - acc.add(PlaceResult.BlockFeatureDisabled(pos, optimalStack)) - return@forEach - } - - if (!context.canPlace() && !statePromoting) { - acc.add(PlaceResult.CantReplace(pos, context)) - return@forEach - } - - val blockItem = optimalStack.item as? BlockItem ?: run { - acc.add(PlaceResult.NotItemBlock(pos, optimalStack)) - return@forEach - } - - context = blockItem.getPlacementContext(context) - ?: run { - acc.add(PlaceResult.ScaffoldExceeded(pos, context)) - return@forEach - } - - lateinit var resultState: BlockState - var rot = fakePlayer.rotation - - val simulatePlaceState = placeState@{ - resultState = blockItem.getPlacementState(context) - ?: run { - val theoreticalState = blockItem.block.getPlacementState(context) - ?: return@placeState PlaceResult.BlockedBySelf(pos) - val shapeContext = ShapeContext.ofPlacement(player) - val collisionShape = theoreticalState.getCollisionShape(world, context.blockPos, shapeContext) - .offset(context.blockPos) - val collidingEntities = collisionShape.boundingBoxes.flatMap { box -> - world.entities.filter { it.boundingBox.intersects(box) } - } - if (collidingEntities.isNotEmpty()) { - collidingEntities - .mapNotNull { it.supportingBlockPos.orElse(null) } - .forEach { support -> - acc.addAll(checkBreakResults(support, eye, preProcessing)) - } - return@placeState PlaceResult.BlockedByEntity(pos, collidingEntities) - } else { - return@placeState PlaceResult.BlockedBySelf(pos) - } - } - - return@placeState if (!nextTargetState.matches(resultState, pos, world, preProcessing.ignore)) - PlaceResult.NoIntegrity( - pos, - resultState, - context, - (nextTargetState as? TargetState.State)?.blockState - ) - else null - } - - val currentDirIsValid = simulatePlaceState()?.let { basePlaceResult -> - if (!placeConfig.rotateForPlace) { - acc.add(basePlaceResult) - return@forEach - } - false - } != false - - run rotate@{ - if (!placeConfig.axisRotate) { - fakePlayer.rotation = checkedHit.targetRotation - simulatePlaceState()?.let { rotatedPlaceResult -> - acc.add(rotatedPlaceResult) - return@forEach - } - rot = fakePlayer.rotation - return@rotate - } - - fakePlayer.rotation = player.rotation - if (simulatePlaceState() == null) { - rot = fakePlayer.rotation - return@rotate - } - - PlaceDirection.entries.asReversed().forEachIndexed direction@{ index, direction -> - fakePlayer.rotation = direction.rotation - when (val placeResult = simulatePlaceState()) { - is PlaceResult.BlockedByEntity -> { - acc.add(placeResult) - return@forEach - } - - is PlaceResult.NoIntegrity -> { - if (index != PlaceDirection.entries.lastIndex) return@direction - acc.add(placeResult) - return@forEach +object BuildSimulator : Sim() { + context(automatedSafeContext: AutomatedSafeContext) + fun Blueprint.simulate( + pov: Vec3d = automatedSafeContext.player.eyePos + ): Set = runBlocking(Dispatchers.Default) { + supervisorScope { + val concurrentSet = ConcurrentSet() + + with(automatedSafeContext) { + structure.forEach { (pos, targetState) -> + launch { + sim(pos, blockState(pos), targetState, pov, concurrentSet) { + if (targetState is TargetState.State && + targetState.matches(state, pos, preProcessing.ignore) + ) { + simPostProcessing() + return@sim } - - else -> { - rot = fakePlayer.rotation - return@rotate - } - } - } - } - - val blockHit = checkedResult.blockResult ?: return@forEach - val hitBlock = blockState(blockHit.blockPos).block - val shouldSneak = hitBlock::class in BlockUtils.interactionBlocks - - val rotationRequest = if (placeConfig.axisRotate) { - lookInDirection(PlaceDirection.fromRotation(rot)) - } else lookAt(rot, 0.001) - - val placeContext = PlaceContext( - blockHit, - RotationRequest(rotationRequest, this), - player.inventory.selectedSlot, - context.blockPos, - blockState(context.blockPos), - resultState, - shouldSneak, - false, - currentDirIsValid, - this - ) - - val selection = optimalStack.item.select() - val containerSelection = selectContainer { ofAnyType(MaterialContainer.Rank.HOTBAR) } - val container = selection.containerWithMaterial(containerSelection).firstOrNull() ?: run { - acc.add( - BuildResult.WrongItemSelection( - pos, - placeContext, - optimalStack.item.select(), - player.mainHandStack - ) - ) - return acc - } - val stack = selection.filterStacks(container.stacks).run { - firstOrNull { player.inventory.getSlotWithStack(it) == player.inventory.selectedSlot } - ?: first() - } - - placeContext.hotbarIndex = player.inventory.getSlotWithStack(stack) - - acc.add(PlaceResult.Place(pos, placeContext)) - } - } - - return acc - } - - private fun AutomatedSafeContext.checkBreakResults( - pos: BlockPos, - eye: Vec3d, - preProcessing: PreProcessingInfo - ): Set { - val acc = mutableSetOf() - val state = blockState(pos) - - /* is a block that will be destroyed by breaking adjacent blocks */ - if (!breakConfig.breakWeakBlocks && state.block.hardness == 0f && !state.isAir && state.isNotEmpty) { - acc.add(BuildResult.Ignored(pos)) - return acc - } - - /* player is standing on top of the block */ - if (breakConfig.avoidSupporting) player.supportingBlockPos.orElse(null)?.let { support -> - if (support != pos) return@let - acc.add(BreakResult.PlayerOnTop(pos, state)) - return acc - } - - /* liquid needs to be submerged first to be broken */ - if (!state.fluidState.isEmpty && state.isReplaceable) { - val submerge = checkPlaceResults(pos, eye, preProcessing, TargetState.Solid) - acc.add(BreakResult.Submerge(pos, state, submerge)) - acc.addAll(submerge) - return acc - } - - if (breakConfig.avoidLiquids) { - val affectedBlocks = hashSetOf(pos) - val checkQueue = hashSetOf(pos) - - while (checkQueue.isNotEmpty()) { - val checkPos = checkQueue.first() - checkQueue.remove(checkPos) - for (offset in Direction.entries) { - val adjacentPos = checkPos.offset(offset) - - if (blockState(adjacentPos).block !is FallingBlock) continue - if (adjacentPos in affectedBlocks) continue - - if (offset == Direction.UP || FallingBlock.canFallThrough(blockState(adjacentPos.down()))) { - checkQueue.add(adjacentPos) - affectedBlocks.add(adjacentPos) - } - } - } - - val affectedFluids = affectedBlocks.fold(hashMapOf()) { accumulator, affectedPos -> - Direction.entries.forEach { offset -> - if (offset == Direction.DOWN) return@forEach - - val offsetPos = affectedPos.offset(offset) - val offsetState = blockState(offsetPos) - val fluidState = offsetState.fluidState - val fluid = fluidState.fluid - - if (fluidState.isEmpty || fluid !is FlowableFluid) return@forEach - - if (offset == Direction.UP) { - accumulator[offsetPos] = offsetState - return@fold accumulator - } - - if (offsetState.block is Waterloggable && !fluidState.isEmpty) { - accumulator[offsetPos] = offsetState - return@fold accumulator - } - - val levelDecreasePerBlock = - when (fluid) { - is WaterFluid -> fluid.getLevelDecreasePerBlock(world) - is LavaFluid -> fluid.getLevelDecreasePerBlock(world) - else -> 0 + if (!targetState.isEmpty() && state.isReplaceable) simPlacement() + else simBreak() } - - if (fluidState.level - levelDecreasePerBlock > 0) { - accumulator[offsetPos] = offsetState - return@fold accumulator - } - } - - return@fold accumulator - } - - if (affectedFluids.isNotEmpty()) { - val liquidOutOfBounds = affectedFluids.any { !world.worldBorder.contains(it.key) } - if (liquidOutOfBounds) { - acc.add(BuildResult.Ignored(pos)) - return acc - } - - affectedFluids.forEach { (liquidPos, liquidState) -> - val submerge = checkPlaceResults(liquidPos, eye, preProcessing, TargetState.Solid) - acc.add(BreakResult.Submerge(liquidPos, liquidState, submerge)) - acc.addAll(submerge) - } - acc.add(BreakResult.BlockedByFluid(pos, state)) - return acc - } - } - - val currentRotation = RotationManager.activeRotation - val currentCast = currentRotation.rayCast(buildConfig.interactReach, eye) - - val voxelShape = state.getOutlineShape(world, pos) - voxelShape.getClosestPointTo(eye).ifPresent { - // ToDo: Use closest point of shape of only visible faces - } - - val boxes = voxelShape.boundingBoxes.map { it.offset(pos) } - val verify: CheckedHit.() -> Boolean = { - hit.blockResult?.blockPos == pos - } - - // ToDo: Move this to a location where more of the context parameters can be properly set - /* the player is buried inside the block */ - if (boxes.any { it.contains(eye) }) { - currentCast?.blockResult?.let { blockHit -> - val rotationRequest = RotationRequest( - lookAtBlock(pos), this - ) - val breakContext = BreakContext( - blockHit, - rotationRequest, - player.inventory.selectedSlot, - StackSelection.EVERYTHING.select(), - instantBreakable(state, pos, breakConfig.breakThreshold), - state, - breakConfig.sorter, - this - ) - acc.add(BreakResult.Break(pos, breakContext)) - return acc - } - } - - val validHits = mutableListOf() - val misses = mutableSetOf() - val reachSq = buildConfig.interactReach.pow(2) - - boxes.forEach { box -> - val sides = if (buildConfig.checkSideVisibility) { - box.getVisibleSurfaces(eye).intersect(Direction.entries) - } else Direction.entries.toSet() - // ToDo: Rewrite Rotation request system to allow support for all sim features and use the rotation finder - scanSurfaces(box, sides, buildConfig.resolution) { side, vec -> - if (eye distSq vec > reachSq) { - misses.add(vec) - return@scanSurfaces - } - - val newRotation = eye.rotationTo(vec) - - val hit = if (buildConfig.strictRayCast) { - newRotation.rayCast(buildConfig.interactReach, eye)?.blockResult - } else { - val hitVec = newRotation.castBox(box, buildConfig.interactReach, eye) - BlockHitResult(hitVec, side, pos, false) - } ?: return@scanSurfaces - - val checked = CheckedHit(hit, newRotation, buildConfig.interactReach) - if (!checked.verify()) return@scanSurfaces - - validHits.add(checked) - } - } - - if (validHits.isEmpty()) { - // ToDo: If we can only mine exposed surfaces we need to add not visible result here - acc.add(BuildResult.OutOfReach(pos, eye, misses)) - return acc - } - - val bestHit = buildConfig.pointSelection.select(validHits) ?: return acc - val blockHit = bestHit.hit.blockResult ?: return acc - val target = lookAt(bestHit.targetRotation, 0.001) - val rotationRequest = RotationRequest(target, this) - val instant = instantBreakable(state, pos, breakConfig.breakThreshold) - - val breakContext = BreakContext( - blockHit, - rotationRequest, - player.inventory.selectedSlot, - StackSelection.EVERYTHING.select(), - instant, - state, - breakConfig.sorter, - this - ) - - if (gamemode.isCreative) { - acc.add(BreakResult.Break(pos, breakContext)) - return acc - } - - val stackSelection = selectStack( - sorter = compareByDescending { - it.canBreak(CachedBlockPosition(world, pos, false)) - }.thenByDescending { - state.calcItemBlockBreakingDelta(pos, it) - } - ) { - isTool() and if (breakConfig.suitableToolsOnly) { - isSuitableForBreaking(state) - } else any() and if (breakConfig.forceSilkTouch) { - hasEnchantment(Enchantments.SILK_TOUCH) - } else any() and if (breakConfig.forceFortunePickaxe) { - hasEnchantment(Enchantments.FORTUNE) - } else any() and if (!breakConfig.useWoodenTools) { - hasTag(WOODEN_TOOL_MATERIALS).not() - } else any() and if (!breakConfig.useStoneTools) { - hasTag(STONE_TOOL_MATERIALS).not() - } else any() and if (!breakConfig.useIronTools) { - hasTag(IRON_TOOL_MATERIALS).not() - } else any() and if (!breakConfig.useDiamondTools) { - hasTag(DIAMOND_TOOL_MATERIALS).not() - } else any() and if (!breakConfig.useGoldTools) { - hasTag(GOLD_TOOL_MATERIALS).not() - } else any() and if (!breakConfig.useNetheriteTools) { - hasTag(NETHERITE_TOOL_MATERIALS).not() - } else any() - } - - val silentSwapSelection = selectContainer { - ofAnyType(MaterialContainer.Rank.HOTBAR) - } - - val swapCandidates = stackSelection.containerWithMaterial(silentSwapSelection) - if (swapCandidates.isEmpty()) { - acc.add(BuildResult.WrongItemSelection(pos, breakContext, stackSelection, player.mainHandStack)) - return acc - } - - val swapStack = swapCandidates - .map { it.matchingStacks(stackSelection) } - .asSequence() - .flatten() - .let { containerStacks -> - var bestStack = ItemStack.EMPTY - var bestBreakDelta = -1f - containerStacks.forEach { stack -> - val breakDelta = state.calcItemBlockBreakingDelta(pos, stack) - if (breakDelta > bestBreakDelta || - (stack == player.mainHandStack && breakDelta >= bestBreakDelta) - ) { - bestBreakDelta = breakDelta - bestStack = stack } } - bestStack } - breakContext.apply { - hotbarIndex = player.hotbar.indexOf(swapStack) - itemSelection = stackSelection - instantBreak = instantBreakable( - state, - pos, - if (breakConfig.swapMode.isEnabled()) swapStack else player.mainHandStack, - breakConfig.breakThreshold - ) + concurrentSet } - acc.add(BreakResult.Break(pos, breakContext)) - return acc } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/ISimInfo.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/ISimInfo.kt new file mode 100644 index 000000000..3a460ecd4 --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/ISimInfo.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.simulation + +import com.lambda.context.Automated +import com.lambda.context.AutomatedSafeContext +import com.lambda.interaction.construction.processing.PreProcessingInfo +import com.lambda.interaction.construction.processing.ProcessorRegistry.getProcessingInfo +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.simulation.checks.BasicChecker.hasBasicRequirements +import com.lambda.interaction.construction.verify.TargetState +import net.minecraft.block.BlockState +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import java.util.* + +interface ISimInfo : Automated { + val pos: BlockPos + val state: BlockState + val targetState: TargetState + val preProcessing: PreProcessingInfo + val pov: Vec3d + val concurrentResults: MutableSet + val dependencyStack: Stack> + + companion object { + @SimDsl + context(_: BuildSimulator) + suspend fun AutomatedSafeContext.sim( + pos: BlockPos, + state: BlockState, + targetState: TargetState, + pov: Vec3d, + concurrentResults: MutableSet, + simBuilder: suspend SimInfo.() -> Unit + ) { + SimInfo( + pos, state, targetState, + targetState.getProcessingInfo(pos) ?: return, + pov, Stack(), concurrentResults, this + ).takeIf { it.hasBasicRequirements() }?.run { simBuilder() } + } + + @SimDsl + context(_: AutomatedSafeContext) + suspend fun ISimInfo.sim( + pos: BlockPos = this.pos, + state: BlockState = this.state, + targetState: TargetState = this.targetState, + pov: Vec3d = this.pov, + simBuilder: suspend SimInfo.() -> Unit + ) { + SimInfo( + pos, state, targetState, + targetState.getProcessingInfo(pos) ?: return, + pov, Stack>().apply { addAll(dependencyStack) }, concurrentResults, this + ).takeIf { it.hasBasicRequirements() }?.run { simBuilder() } + } + } +} + +data class SimInfo( + override val pos: BlockPos, + override val state: BlockState, + override val targetState: TargetState, + override val preProcessing: PreProcessingInfo, + override val pov: Vec3d, + override val dependencyStack: Stack>, + override val concurrentResults: MutableSet, + private val automated: Automated +) : ISimInfo, Automated by automated \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/Results.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/Results.kt new file mode 100644 index 000000000..4111218ac --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/Results.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.simulation + +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.results.GenericResult + +@SimDsl +interface Results { + fun ISimInfo.result(result: GenericResult) = addResult(result) + fun ISimInfo.result(result: T) = addResult(result) + + private fun ISimInfo.addResult(result: BuildResult) { + concurrentResults.add( + dependencyStack + .asReversed() + .fold(result) { acc, dependent -> + with(dependent) { dependentUpon(acc) } + } + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/Sim.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/Sim.kt new file mode 100644 index 000000000..1f47b9b8c --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/Sim.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.simulation + +import com.lambda.context.AutomationConfig.maxSimDependencies +import com.lambda.interaction.construction.processing.PreProcessingInfo +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.results.GenericResult +import com.lambda.interaction.request.rotating.Rotation.Companion.rotationTo +import com.lambda.interaction.request.rotating.visibilty.VisibilityChecker.CheckedHit +import com.lambda.interaction.request.rotating.visibilty.VisibilityChecker.getVisibleSurfaces +import com.lambda.interaction.request.rotating.visibilty.VisibilityChecker.scanSurfaces +import com.lambda.util.math.distSq +import com.lambda.util.math.vec3d +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import io.ktor.util.collections.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.util.math.Vec3d +import net.minecraft.util.shape.VoxelShape +import kotlin.math.pow + +@DslMarker +annotation class SimDsl + +@SimDsl +abstract class Sim : Results { + @SimDsl + open fun dependentUpon(buildResult: BuildResult): BuildResult = buildResult + + protected suspend fun ISimInfo.withDependent(dependent: Sim<*>, block: suspend () -> Unit) { + // +1 because the build sim counts as a dependent + if (dependencyStack.size >= maxSimDependencies + 1) return + dependencyStack.push(dependent) + block() + dependencyStack.pop() + } + + suspend fun ISimInfo.scanShape( + pov: Vec3d, + voxelShape: VoxelShape, + pos: BlockPos, + sides: Set, + preProcessing: PreProcessingInfo + ): Set? { + val boxes = voxelShape.boundingBoxes.map { it.offset(pos) } + + val reachSq = buildConfig.interactReach.pow(2) + + val validHits = ConcurrentSet() + val misses = ConcurrentSet() + + supervisorScope { + boxes.forEach { box -> + launch { + val sides = if (buildConfig.checkSideVisibility || buildConfig.strictRayCast) { + sides.intersect(box.getVisibleSurfaces(pov)) + } else sides + + scanSurfaces(box, sides, buildConfig.resolution, preProcessing.surfaceScan) { side, vec -> + if (pov distSq vec > reachSq) { + misses.add(vec) + return@scanSurfaces + } + + val newRotation = pov.rotationTo(vec) + + val hit = if (buildConfig.strictRayCast) { + newRotation.rayCast(buildConfig.interactReach, pov)?.blockResult ?: return@scanSurfaces + } else { + val hitVec = newRotation.castBox(box, buildConfig.interactReach, pov) ?: return@scanSurfaces + BlockHitResult(hitVec, side, pos, false) + } + + if (hit.blockPos != pos || hit.side != side) return@scanSurfaces + val checked = CheckedHit(hit, newRotation, buildConfig.interactReach) + + validHits.add(checked) + } + } + } + } + + if (validHits.isEmpty()) { + if (misses.isNotEmpty()) { + result(GenericResult.OutOfReach(pos, pov, misses)) + return null + } + + result(GenericResult.NotVisible(pos, pos, pov.distanceTo(pos.vec3d))) + return null + } + + return validHits + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt index 1fda4fbf8..df216b8c3 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/Simulation.kt @@ -43,21 +43,20 @@ data class Simulation( private val cache: MutableMap> = mutableMapOf() private fun FastVector.toView(): Vec3d = toVec3d().add(0.5, ClientPlayerEntity.DEFAULT_EYE_HEIGHT.toDouble(), 0.5) - fun simulate( - pos: FastVector, - ) = cache.getOrPut(pos) { - val view = pos.toView() - val isOutOfBounds = blueprint.isOutOfBounds(view) - val isTooFar = blueprint.getClosestPointTo(view).distanceTo(view) > 10.0 - return runSafeAutomated { - if (isOutOfBounds && isTooFar) return@getOrPut emptySet() - val blockPos = pos.toBlockPos() - val isWalkable = blockState(blockPos.down()).isSideSolidFullSquare(world, blockPos, Direction.UP) - if (!isWalkable) return@getOrPut emptySet() - if (!playerFitsIn(blockPos)) return@getOrPut emptySet() - blueprint.simulate(view) - } ?: emptySet() - } + fun simulate(pos: FastVector) = + cache.getOrPut(pos) { + val pov = pos.toView() + val isOutOfBounds = blueprint.isOutOfBounds(pov) + val isTooFar = blueprint.getClosestPointTo(pov).distanceTo(pov) > 10.0 + return@getOrPut runSafeAutomated { + if (isOutOfBounds && isTooFar) return@getOrPut emptySet() + val blockPos = pos.toBlockPos() + val isWalkable = blockState(blockPos.down()).isSideSolidFullSquare(world, blockPos, Direction.UP) + if (!isWalkable) return@getOrPut emptySet() + if (!playerFitsIn(blockPos)) return@getOrPut emptySet() + blueprint.simulate(pov) + } ?: emptySet() + } fun goodPositions() = cache .filter { entry -> entry.value.any { it.rank.ordinal < 4 } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/BasicChecker.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/BasicChecker.kt new file mode 100644 index 000000000..ec61a518c --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/BasicChecker.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.simulation.checks + +import com.lambda.context.AutomatedSafeContext +import com.lambda.interaction.construction.result.results.GenericResult +import com.lambda.interaction.construction.result.results.PreSimResult +import com.lambda.interaction.construction.simulation.Results +import com.lambda.interaction.construction.simulation.SimDsl +import com.lambda.interaction.construction.simulation.SimInfo +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.util.player.gamemode +import com.lambda.util.world.WorldUtils.isLoaded +import net.minecraft.block.OperatorBlock + +object BasicChecker : Results { + @SimDsl + context(automatedSafeContext: AutomatedSafeContext) + fun SimInfo.hasBasicRequirements(): Boolean = with(automatedSafeContext) { + // the chunk is not loaded + if (!isLoaded(pos)) { + result(PreSimResult.ChunkNotLoaded(pos)) + return false + } + + // block is already in the correct state + if (targetState.matches(state, pos)) { + result(PreSimResult.Done(pos)) + return false + } + + // block should be ignored + if (state.block in breakConfig.ignoredBlocks && targetState.type == TargetState.Type.Air) { + result(GenericResult.Ignored(pos)) + return false + } + + // the player is in the wrong game mode to alter the block state + if (player.isBlockBreakingRestricted(world, pos, gamemode)) { + result(PreSimResult.Restricted(pos)) + return false + } + + // the player has no permissions to alter the block state + if (state.block is OperatorBlock && !player.isCreativeLevelTwoOp) { + result(PreSimResult.NoPermission(pos, state)) + return false + } + + // block is outside the world so it cant be altered + if (!world.worldBorder.contains(pos) || world.isOutOfHeightLimit(pos)) { + result(PreSimResult.OutOfWorld(pos)) + return false + } + + // block is unbreakable, so it cant be broken or replaced + if (state.getHardness(world, pos) < 0 && !gamemode.isCreative) { + result(PreSimResult.Unbreakable(pos, state)) + return false + } + + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/BreakSim.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/BreakSim.kt new file mode 100644 index 000000000..b9179123d --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/BreakSim.kt @@ -0,0 +1,285 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.simulation.checks + +import com.lambda.context.AutomatedSafeContext +import com.lambda.interaction.construction.context.BreakContext +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.results.BreakResult +import com.lambda.interaction.construction.result.results.GenericResult +import com.lambda.interaction.construction.simulation.ISimInfo +import com.lambda.interaction.construction.simulation.ISimInfo.Companion.sim +import com.lambda.interaction.construction.simulation.Sim +import com.lambda.interaction.construction.simulation.SimDsl +import com.lambda.interaction.construction.simulation.SimInfo +import com.lambda.interaction.construction.simulation.checks.PlaceSim.Companion.simPlacement +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.interaction.material.ContainerSelection.Companion.selectContainer +import com.lambda.interaction.material.StackSelection +import com.lambda.interaction.material.StackSelection.Companion.EVERYTHING +import com.lambda.interaction.material.StackSelection.Companion.selectStack +import com.lambda.interaction.material.container.ContainerManager.containerWithMaterial +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.interaction.request.hotbar.HotbarManager +import com.lambda.interaction.request.rotating.RotationManager +import com.lambda.interaction.request.rotating.RotationRequest +import com.lambda.interaction.request.rotating.visibilty.lookAt +import com.lambda.interaction.request.rotating.visibilty.lookAtBlock +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.BlockUtils.calcItemBlockBreakingDelta +import com.lambda.util.BlockUtils.instantBreakable +import com.lambda.util.item.ItemStackUtils.inventoryIndex +import com.lambda.util.item.ItemStackUtils.inventoryIndexOrSelected +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import net.minecraft.block.BlockState +import net.minecraft.block.FallingBlock +import net.minecraft.block.Waterloggable +import net.minecraft.block.pattern.CachedBlockPosition +import net.minecraft.enchantment.Enchantments +import net.minecraft.fluid.FlowableFluid +import net.minecraft.fluid.LavaFluid +import net.minecraft.fluid.WaterFluid +import net.minecraft.item.ItemStack +import net.minecraft.registry.tag.ItemTags.DIAMOND_TOOL_MATERIALS +import net.minecraft.registry.tag.ItemTags.GOLD_TOOL_MATERIALS +import net.minecraft.registry.tag.ItemTags.IRON_TOOL_MATERIALS +import net.minecraft.registry.tag.ItemTags.NETHERITE_TOOL_MATERIALS +import net.minecraft.registry.tag.ItemTags.STONE_TOOL_MATERIALS +import net.minecraft.registry.tag.ItemTags.WOODEN_TOOL_MATERIALS +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import kotlin.jvm.optionals.getOrNull + +class BreakSim private constructor(simInfo: ISimInfo) + : Sim(), + ISimInfo by simInfo +{ + override fun dependentUpon(buildResult: BuildResult) = + BreakResult.Dependency(pos, buildResult) + + companion object { + @SimDsl + context(automatedSafeContext: AutomatedSafeContext, dependent: Sim<*>) + suspend fun SimInfo.simBreak() = + BreakSim(this).run { + withDependent(dependent) { + automatedSafeContext.simBreaks() + } + } + } + + private suspend fun AutomatedSafeContext.simBreaks() { + if (breakConfig.avoidSupporting) player.supportingBlockPos.getOrNull()?.let { support -> + if (support != pos) return@let + result(BreakResult.PlayerOnTop(pos, state)) + return + } + + if (targetState.getState(pos).isAir && !state.fluidState.isEmpty && state.isReplaceable) { + result(BreakResult.Submerge(pos, state)) + sim(pos, state, TargetState.Solid(emptySet())) { simPlacement() } + return + } + + if (breakConfig.avoidLiquids && affectsFluids()) return + + val (swapStack, stackSelection) = getSwapStack() ?: return + val instant = instantBreakable( + state, pos, + if (breakConfig.swapMode.isEnabled()) swapStack else player.mainHandStack, + breakConfig.breakThreshold + ) + + val shape = state.getOutlineShape(world, pos) + + if (shape.boundingBoxes.map { it.offset(pos) }.any { it.contains(pov) }) { + val currentCast = RotationManager.activeRotation.rayCast(buildConfig.interactReach, pov) + currentCast?.blockResult?.let { blockHit -> + val rotationRequest = RotationRequest(lookAtBlock(pos), this) + val breakContext = BreakContext( + blockHit, + rotationRequest, + swapStack.inventoryIndexOrSelected, + stackSelection, + instant, + state, + this + ) + result(BreakResult.Break(pos, breakContext)) + } + return + } + + val validHits = scanShape(pov, shape, pos, Direction.entries.toSet(), preProcessing) ?: return + + val bestHit = buildConfig.pointSelection.select(validHits) ?: return + val target = lookAt(bestHit.targetRotation, 0.001) + val rotationRequest = RotationRequest(target, this) + + val breakContext = BreakContext( + bestHit.hit.blockResult ?: return, + rotationRequest, + swapStack.inventoryIndexOrSelected, + stackSelection, + instant, + state, + this + ) + + result(BreakResult.Break(pos, breakContext)) + return + } + + private fun AutomatedSafeContext.getSwapStack(): Pair? { + val stackSelection = selectStack( + count = 0, + sorter = compareByDescending { + it.canBreak(CachedBlockPosition(world, pos, false)) + }.thenByDescending { + state.calcItemBlockBreakingDelta(pos, it) + }.thenByDescending { + it.inventoryIndex == HotbarManager.serverSlot + } + ) { + EVERYTHING + .andIf(breakConfig.efficientOnly) { + isEfficientForBreaking(state) + }.andIf(breakConfig.suitableToolsOnly) { + isSuitableForBreaking(state) + }.andIf(breakConfig.forceSilkTouch) { + hasEnchantment(Enchantments.SILK_TOUCH) + }.andIf(breakConfig.forceFortunePickaxe) { + hasEnchantment(Enchantments.FORTUNE) + }.andIf(!breakConfig.useWoodenTools) { + hasTag(WOODEN_TOOL_MATERIALS).not() + }.andIf(!breakConfig.useStoneTools) { + hasTag(STONE_TOOL_MATERIALS).not() + }.andIf(!breakConfig.useIronTools) { + hasTag(IRON_TOOL_MATERIALS).not() + }.andIf(!breakConfig.useDiamondTools) { + hasTag(DIAMOND_TOOL_MATERIALS).not() + }.andIf(!breakConfig.useGoldTools) { + hasTag(GOLD_TOOL_MATERIALS).not() + }.andIf(!breakConfig.useNetheriteTools) { + hasTag(NETHERITE_TOOL_MATERIALS).not() + } + } + + val silentSwapSelection = selectContainer { + ofAnyType(MaterialContainer.Rank.HOTBAR) + } + + val swapCandidates = stackSelection + .containerWithMaterial(silentSwapSelection) + .map { it.matchingStacks(stackSelection) } + .flatten() + if (swapCandidates.isEmpty()) { + result(GenericResult.WrongItemSelection(pos, stackSelection, player.mainHandStack)) + return null + } + + var bestStack = ItemStack.EMPTY + var bestBreakDelta = -1f + swapCandidates.forEach { stack -> + val breakDelta = state.calcItemBlockBreakingDelta(pos, stack) + if (breakDelta > bestBreakDelta || + (stack == player.mainHandStack && breakDelta >= bestBreakDelta) + ) { + bestBreakDelta = breakDelta + bestStack = stack + } + } + return if (bestBreakDelta == -1f) null + else Pair(bestStack, stackSelection) + } + + private suspend fun AutomatedSafeContext.affectsFluids(): Boolean { + val affectedBlocks = hashSetOf(pos) + val checkQueue = hashSetOf(pos) + + while (checkQueue.isNotEmpty()) { + val checkPos = checkQueue.first() + checkQueue.remove(checkPos) + for (offset in Direction.entries) { + val adjacentPos = checkPos.offset(offset) + + if (blockState(adjacentPos).block !is FallingBlock) continue + if (adjacentPos in affectedBlocks) continue + + if (offset == Direction.UP || FallingBlock.canFallThrough(blockState(adjacentPos.down()))) { + checkQueue.add(adjacentPos) + affectedBlocks.add(adjacentPos) + } + } + } + + val affectedFluids = affectedBlocks.fold(hashMapOf()) { accumulator, affectedPos -> + Direction.entries.forEach { offset -> + if (offset == Direction.DOWN) return@forEach + + val offsetPos = affectedPos.offset(offset) + val offsetState = blockState(offsetPos) + val fluidState = offsetState.fluidState + val fluid = fluidState.fluid + + if (fluidState.isEmpty || fluid !is FlowableFluid) return@forEach + + if (offset == Direction.UP) { + accumulator[offsetPos] = offsetState + return@fold accumulator + } + + if (offsetState.block is Waterloggable && !fluidState.isEmpty) { + accumulator[offsetPos] = offsetState + return@fold accumulator + } + + val levelDecreasePerBlock = + when (fluid) { + is WaterFluid -> fluid.getLevelDecreasePerBlock(world) + is LavaFluid -> fluid.getLevelDecreasePerBlock(world) + else -> 0 + } + + if (fluidState.level - levelDecreasePerBlock > 0) { + accumulator[offsetPos] = offsetState + return@fold accumulator + } + } + + return@fold accumulator + } + + if (affectedFluids.isNotEmpty()) { + val liquidOutOfBounds = affectedFluids.any { !world.worldBorder.contains(it.key) } + if (liquidOutOfBounds) { + result(GenericResult.Ignored(pos)) + return true + } + + affectedFluids.forEach { (liquidPos, liquidState) -> + result(BreakResult.Submerge(liquidPos, liquidState)) + sim(liquidPos, liquidState, TargetState.Solid(emptySet())) { simPlacement() } + } + result(BreakResult.BlockedByFluid(pos, state)) + return true + } + + return false + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/PlaceSim.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/PlaceSim.kt new file mode 100644 index 000000000..df5a0a644 --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/PlaceSim.kt @@ -0,0 +1,311 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.simulation.checks + +import com.lambda.context.AutomatedSafeContext +import com.lambda.interaction.construction.context.PlaceContext +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.results.GenericResult +import com.lambda.interaction.construction.result.results.PlaceResult +import com.lambda.interaction.construction.simulation.ISimInfo +import com.lambda.interaction.construction.simulation.ISimInfo.Companion.sim +import com.lambda.interaction.construction.simulation.Sim +import com.lambda.interaction.construction.simulation.SimDsl +import com.lambda.interaction.construction.simulation.SimInfo +import com.lambda.interaction.construction.simulation.checks.BreakSim.Companion.simBreak +import com.lambda.interaction.construction.simulation.checks.PlaceSim.RotatePlaceTest.Companion.rotatePlaceTest +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.interaction.material.ContainerSelection.Companion.selectContainer +import com.lambda.interaction.material.StackSelection.Companion.select +import com.lambda.interaction.material.container.ContainerManager.containerWithMaterial +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.interaction.request.rotating.Rotation +import com.lambda.interaction.request.rotating.Rotation.Companion.rotation +import com.lambda.interaction.request.rotating.RotationManager +import com.lambda.interaction.request.rotating.RotationRequest +import com.lambda.interaction.request.rotating.visibilty.PlaceDirection +import com.lambda.interaction.request.rotating.visibilty.VisibilityChecker.CheckedHit +import com.lambda.interaction.request.rotating.visibilty.lookAt +import com.lambda.interaction.request.rotating.visibilty.lookInDirection +import com.lambda.util.BlockUtils +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.EntityUtils.getPositionsWithinHitboxXZ +import com.lambda.util.item.ItemStackUtils.inventoryIndex +import com.lambda.util.item.ItemUtils.blockItem +import com.lambda.util.math.MathUtils.floorToInt +import com.lambda.util.math.minus +import com.lambda.util.player.MovementUtils.sneaking +import com.lambda.util.player.copyPlayer +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import net.minecraft.block.BlockState +import net.minecraft.block.ShapeContext +import net.minecraft.block.pattern.CachedBlockPosition +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.entity.Entity +import net.minecraft.item.ItemPlacementContext +import net.minecraft.item.ItemStack +import net.minecraft.util.Hand +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.util.shape.VoxelShapes + +class PlaceSim private constructor(simInfo: ISimInfo) + : Sim(), + ISimInfo by simInfo +{ + override fun dependentUpon(buildResult: BuildResult) = + PlaceResult.Dependency(pos, buildResult) + + companion object { + context(automatedSafeContext: AutomatedSafeContext, dependent: Sim<*>) + @SimDsl + suspend fun SimInfo.simPlacement() = + PlaceSim(this).run { + withDependent(dependent) { + automatedSafeContext.simPlacements() + } + } + } + + private suspend fun AutomatedSafeContext.simPlacements() = + supervisorScope { + preProcessing.sides.forEach { side -> + launch { + val neighborPos = pos.offset(side) + val neighborSide = side.opposite + if (!placeConfig.airPlace.isEnabled) + testBlock(neighborPos, neighborSide, this@supervisorScope) + testBlock(pos, side, this@supervisorScope) + } + } + } + + private suspend fun AutomatedSafeContext.testBlock(pos: BlockPos, side: Direction, supervisorScope: CoroutineScope) { + if (!world.worldBorder.contains(pos)) return + + val testBlockState = blockState(pos) + val shape = testBlockState.getOutlineShape(world, pos).let { outlineShape -> + if (!outlineShape.isEmpty || !placeConfig.airPlace.isEnabled) outlineShape + else VoxelShapes.fullCube() + } + if (shape.isEmpty) return + + // ToDo: For each hand + val fakePlayer = copyPlayer(player).apply { + val newPos = pov - (this.eyePos - this.pos) + setPos(newPos.x, newPos.y, newPos.z) + if (testBlockState.block::class in BlockUtils.interactionBlocks) { + input.sneaking = true + updatePose() + } + } + val pov = fakePlayer.eyePos + + val validHits = scanShape(pov, shape, pos, setOf(side), preProcessing) ?: return + + val swapStack = getSwapStack() ?: return + if (!swapStack.item.isEnabled(world.enabledFeatures)) { + result(PlaceResult.BlockFeatureDisabled(pos, swapStack)) + supervisorScope.cancel() + return + } + + selectHitPos(validHits, fakePlayer, swapStack) + } + + private fun AutomatedSafeContext.getSwapStack(): ItemStack? { + val optimalStack = targetState.getStack(pos) + val stackSelection = optimalStack.item.select() + val containerSelection = selectContainer { ofAnyType(MaterialContainer.Rank.HOTBAR) } + val container = stackSelection.containerWithMaterial(containerSelection).firstOrNull() ?: run { + result(GenericResult.WrongItemSelection(pos, optimalStack.item.select(), player.mainHandStack)) + return null + } + return stackSelection.filterStacks(container.stacks).run { + firstOrNull { it.inventoryIndex == player.inventory.selectedSlot } + ?: firstOrNull() + } + } + + private suspend fun AutomatedSafeContext.selectHitPos( + validHits: Collection, + fakePlayer: ClientPlayerEntity, + swapStack: ItemStack + ) { + buildConfig.pointSelection.select(validHits)?.let { checkedHit -> + val hitResult = checkedHit.hit.blockResult ?: return + + val context = swapStack.blockItem.getPlacementContext( + ItemPlacementContext( + world, + fakePlayer, + Hand.MAIN_HAND, + swapStack, + hitResult, + ) + ) ?: run { + result(PlaceResult.ScaffoldExceeded(pos)) + return + } + + if (context.blockPos != pos) { + result(PlaceResult.UnexpectedPosition(pos, context.blockPos)) + return + } + + val cachePos = CachedBlockPosition(context.world, context.blockPos, false) + if (!player.abilities.allowModifyWorld && !swapStack.canPlaceOn(cachePos)) { + result(PlaceResult.IllegalUsage(pos)) + return + } + + if (!context.canPlace()) { + result(PlaceResult.CantReplace(pos, context)) + return + } + + val rotatePlaceTest = simRotatePlace(fakePlayer, checkedHit, context) ?: return + + val rotationRequest = if (placeConfig.axisRotate) { + lookInDirection(PlaceDirection.fromRotation(rotatePlaceTest.rotation)) + } else lookAt(rotatePlaceTest.rotation, 0.001) + + val placeContext = PlaceContext( + hitResult, + RotationRequest(rotationRequest, this@PlaceSim), + swapStack.inventoryIndex, + pos, + state, + rotatePlaceTest.resultState, + fakePlayer.isSneaking, + false, + rotatePlaceTest.currentDirIsValid, + this@PlaceSim + ) + + result(PlaceResult.Place(pos, placeContext)) + } + + return + } + + private suspend fun AutomatedSafeContext.simRotatePlace( + fakePlayer: ClientPlayerEntity, + checkedHit: CheckedHit, + context: ItemPlacementContext + ): RotatePlaceTest? { + fakePlayer.rotation = RotationManager.serverRotation + val currentDirIsValid = testPlaceState(context).testResult == PlaceTestResult.Success + + if (!placeConfig.axisRotate) { + fakePlayer.rotation = checkedHit.targetRotation + return rotatePlaceTest(testPlaceState(context), currentDirIsValid, fakePlayer.rotation) + } + + fakePlayer.rotation = player.rotation + testPlaceState(context).takeIf { it.testResult == PlaceTestResult.Success }?.let { playerRotTest -> + return rotatePlaceTest(playerRotTest, currentDirIsValid, fakePlayer.rotation) + } + + PlaceDirection.entries.asReversed().forEach direction@{ direction -> + fakePlayer.rotation = direction.rotation + testPlaceState(context).takeIf { it.testResult == PlaceTestResult.Success }?.let { axisRotateTest -> + return rotatePlaceTest(axisRotateTest, currentDirIsValid, fakePlayer.rotation) + } + } + + return null + } + + private suspend fun AutomatedSafeContext.testPlaceState(context: ItemPlacementContext): PlaceTest { + val resultState = context.stack.blockItem.getPlacementState(context) + ?: run { + val blockingEntities = handleEntityBlockage(context) + result(PlaceResult.BlockedByEntity(pos, blockingEntities)) + return PlaceTest(state, PlaceTestResult.BlockedByEntity) + } + + return if (!targetState.matches(resultState, pos, preProcessing.ignore)) { + result(PlaceResult.NoIntegrity(pos, resultState, context, (targetState as? TargetState.State)?.blockState)) + PlaceTest(resultState, PlaceTestResult.NoIntegrity) + } else PlaceTest(resultState, PlaceTestResult.Success) + } + + private suspend fun AutomatedSafeContext.handleEntityBlockage(context: ItemPlacementContext): List { + val pos = context.blockPos + val theoreticalState = context.stack.blockItem.block.getPlacementState(context) + ?: return emptyList() + + val collisionShape = theoreticalState.getCollisionShape( + world, pos, ShapeContext.ofPlacement(player) + ).offset(pos) + + val collidingEntities = collisionShape.boundingBoxes.flatMap { box -> + world.entities.filter { it.boundingBox.intersects(box) } + } + + if (collidingEntities.isNotEmpty()) { + collidingEntities + .mapNotNull { entity -> + if (entity === player) { + result(PlaceResult.BlockedBySelf(pos)) + return@mapNotNull null + } + val hitbox = entity.boundingBox + entity.getPositionsWithinHitboxXZ( + (pos.y - (hitbox.maxY - hitbox.minY)).floorToInt(), + pos.y + ) + } + .flatten() + .forEach { support -> + sim(support, blockState(support), TargetState.Empty) { simBreak() } + } + result(PlaceResult.BlockedByEntity(pos, collidingEntities)) + } + + return collidingEntities + } + + private class RotatePlaceTest private constructor( + val resultState: BlockState, + val currentDirIsValid: Boolean, + val rotation: Rotation + ) { + companion object { + fun rotatePlaceTest( + placeTest: PlaceTest, + currentDirIsValid: Boolean, + rotation: Rotation + ): RotatePlaceTest? { + return if (placeTest.testResult != PlaceTestResult.Success) null + else RotatePlaceTest(placeTest.resultState, currentDirIsValid, rotation) + } + } + } + private data class PlaceTest(val resultState: BlockState, val testResult: PlaceTestResult) + private enum class PlaceTestResult { + Success, + BlockedByEntity, + NoIntegrity + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/PostProcessingSim.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/PostProcessingSim.kt new file mode 100644 index 000000000..b285f9bae --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/PostProcessingSim.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.interaction.construction.simulation.checks + +import com.lambda.context.AutomatedSafeContext +import com.lambda.interaction.construction.context.InteractionContext +import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.results.GenericResult +import com.lambda.interaction.construction.result.results.InteractResult +import com.lambda.interaction.construction.simulation.ISimInfo +import com.lambda.interaction.construction.simulation.ISimInfo.Companion.sim +import com.lambda.interaction.construction.simulation.Sim +import com.lambda.interaction.construction.simulation.SimDsl +import com.lambda.interaction.construction.simulation.SimInfo +import com.lambda.interaction.construction.simulation.checks.PlaceSim.Companion.simPlacement +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.interaction.material.ContainerSelection.Companion.selectContainer +import com.lambda.interaction.material.StackSelection.Companion.select +import com.lambda.interaction.material.container.ContainerManager.containerWithMaterial +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.interaction.request.rotating.RotationRequest +import com.lambda.interaction.request.rotating.visibilty.VisibilityChecker.CheckedHit +import com.lambda.interaction.request.rotating.visibilty.lookAt +import com.lambda.util.item.ItemStackUtils.inventoryIndex +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import net.minecraft.block.BlockState +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.state.property.Properties +import net.minecraft.util.math.Direction + +class PostProcessingSim private constructor(simInfo: ISimInfo) + : Sim(), + ISimInfo by simInfo +{ + override fun dependentUpon(buildResult: BuildResult) = + InteractResult.Dependency(pos, buildResult) + + companion object { + context(automatedSafeContext: AutomatedSafeContext, dependent: Sim<*>) + @SimDsl + suspend fun SimInfo.simPostProcessing() = + PostProcessingSim(this).run { + withDependent(dependent) { + automatedSafeContext.simPostProcessing() + } + } + } + + private suspend fun AutomatedSafeContext.simPostProcessing() { + val targetState = (targetState as? TargetState.State) ?: return + + val mismatchedProperties = state.properties.filter { state.get(it) != targetState.blockState.get(it) } + mismatchedProperties.forEach { property -> + when (property) { + Properties.EYE -> { + if (state.get(Properties.EYE)) return@forEach + val expectedState = state.with(Properties.EYE, true) + simInteraction(expectedState) + } + + Properties.INVERTED -> { + val expectedState = state.with(Properties.INVERTED, !state.get(Properties.INVERTED)) + simInteraction(expectedState) + } + + Properties.DELAY -> { + val expectedState = + state.with(Properties.DELAY, state.cycle(Properties.DELAY).get(Properties.DELAY)) + simInteraction(expectedState) + } + + Properties.COMPARATOR_MODE -> { + val expectedState = state.with( + Properties.COMPARATOR_MODE, + state.cycle(Properties.COMPARATOR_MODE).get(Properties.COMPARATOR_MODE) + ) + simInteraction(expectedState) + } + + Properties.OPEN -> { + val expectedState = state.with(Properties.OPEN, !state.get(Properties.OPEN)) + simInteraction(expectedState) + } + + Properties.SLAB_TYPE -> sim { simPlacement() } + } + } + } + + private suspend fun AutomatedSafeContext.simInteraction( + expectedState: BlockState, + sides: Set = Direction.entries.toSet(), + item: Item? = null + ) { + val validHits = scanShape(pov, state.getOutlineShape(world, pos), pos, sides, preProcessing) + ?: return + + val swapStack = getSwapStack(item ?: player.mainHandStack.item) ?: return + + selectHit(validHits, expectedState, swapStack) + } + + private fun AutomatedSafeContext.getSwapStack(item: Item): ItemStack? { + val stackSelection = item.select() + val hotbarCandidates = selectContainer { + ofAnyType(MaterialContainer.Rank.HOTBAR) + }.let { predicate -> + stackSelection.containerWithMaterial( predicate) + } + + if (hotbarCandidates.isEmpty()) { + result(GenericResult.WrongItemSelection(pos, stackSelection, player.mainHandStack)) + return null + } + + return hotbarCandidates.first().matchingStacks(stackSelection).first() + } + + private fun AutomatedSafeContext.selectHit( + validHits: Collection, + expectedState: BlockState, + swapStack: ItemStack + ) { + buildConfig.pointSelection.select(validHits)?.let { checkedHit -> + val checkedResult = checkedHit.hit.blockResult ?: return + val rotationTarget = lookAt(checkedHit.targetRotation, 0.001) + val context = InteractionContext( + checkedResult, + RotationRequest(rotationTarget, this), + swapStack.inventoryIndex, + state, + expectedState, + this + ) + + result(InteractResult.Interact(pos, context)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/construction/verify/ScanMode.kt b/src/main/kotlin/com/lambda/interaction/construction/verify/ScanMode.kt index ba83a1d8d..79a02b0e6 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/verify/ScanMode.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/verify/ScanMode.kt @@ -18,7 +18,7 @@ package com.lambda.interaction.construction.verify enum class ScanMode(val priority: Int) { - GREATER_BLOCK_HALF(1), - LESSER_BLOCK_HALF(1), - FULL(0) + GreaterBlockHalf(1), + LesserBlockHalf(1), + Full(0) } diff --git a/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt b/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt index 25eab2808..6121c6288 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/verify/StateMatcher.kt @@ -17,21 +17,24 @@ package com.lambda.interaction.construction.verify -import com.lambda.context.Automated +import com.lambda.context.AutomatedSafeContext +import com.lambda.context.SafeContext import net.minecraft.block.BlockState -import net.minecraft.client.world.ClientWorld import net.minecraft.item.ItemStack import net.minecraft.state.property.Property import net.minecraft.util.math.BlockPos interface StateMatcher { + context(safeContext: SafeContext) fun matches( state: BlockState, pos: BlockPos, - world: ClientWorld, ignoredProperties: Collection> = emptySet() ): Boolean - fun getStack(world: ClientWorld, pos: BlockPos, automated: Automated): ItemStack + context(automatedSafeContext: AutomatedSafeContext) + fun getStack(pos: BlockPos): ItemStack + context(automatedSafeContext: AutomatedSafeContext) + fun getState(pos: BlockPos): BlockState fun isEmpty(): Boolean } diff --git a/src/main/kotlin/com/lambda/interaction/construction/verify/SurfaceScan.kt b/src/main/kotlin/com/lambda/interaction/construction/verify/SurfaceScan.kt index c8d9d507e..4a0a79d4a 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/verify/SurfaceScan.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/verify/SurfaceScan.kt @@ -24,6 +24,6 @@ data class SurfaceScan( val axis: Direction.Axis, ) { companion object { - val DEFAULT = SurfaceScan(ScanMode.FULL, Direction.Axis.Y) + val DEFAULT = SurfaceScan(ScanMode.Full, Direction.Axis.Y) } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt b/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt index af52771d9..5a438a193 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/verify/TargetState.kt @@ -17,14 +17,17 @@ package com.lambda.interaction.construction.verify -import com.lambda.context.Automated +import com.lambda.context.AutomatedSafeContext +import com.lambda.context.SafeContext import com.lambda.interaction.material.container.ContainerManager.findDisposable +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.BlockUtils.emptyState import com.lambda.util.BlockUtils.isEmpty import com.lambda.util.BlockUtils.matches import com.lambda.util.StringUtils.capitalize import com.lambda.util.item.ItemUtils.block import net.minecraft.block.BlockState -import net.minecraft.client.world.ClientWorld +import net.minecraft.block.Blocks import net.minecraft.item.ItemStack import net.minecraft.item.Items import net.minecraft.state.property.Property @@ -34,135 +37,159 @@ import net.minecraft.util.math.Direction sealed class TargetState(val type: Type) : StateMatcher { enum class Type { - EMPTY, AIR, SOLID, SUPPORT, STATE, BLOCK, STACK + Empty, Air, Solid, Support, State, Block, Stack } - data object Empty : TargetState(Type.EMPTY) { + data object Empty : TargetState(Type.Empty) { override fun toString() = "Empty" + context(safeContext: SafeContext) override fun matches( state: BlockState, pos: BlockPos, - world: ClientWorld, ignoredProperties: Collection> ) = state.isEmpty - override fun getStack(world: ClientWorld, pos: BlockPos, automated: Automated): ItemStack = - ItemStack.EMPTY + context(_: AutomatedSafeContext) + override fun getStack(pos: BlockPos): ItemStack = ItemStack.EMPTY + + context(automatedSafeContext: AutomatedSafeContext) + override fun getState(pos: BlockPos) = automatedSafeContext.blockState(pos).emptyState override fun isEmpty() = true } - data object Air : TargetState(Type.AIR) { + data object Air : TargetState(Type.Air) { override fun toString() = "Air" + context(safeContext: SafeContext) override fun matches( state: BlockState, pos: BlockPos, - world: ClientWorld, ignoredProperties: Collection> ) = state.isAir - override fun getStack(world: ClientWorld, pos: BlockPos, automated: Automated): ItemStack = - ItemStack.EMPTY + context(_: AutomatedSafeContext) + override fun getStack(pos: BlockPos): ItemStack = ItemStack.EMPTY + + context(_: AutomatedSafeContext) + override fun getState(pos: BlockPos): BlockState = Blocks.AIR.defaultState override fun isEmpty() = true } - data object Solid : TargetState(Type.SOLID) { + data class Solid(val replace: Set) : TargetState(Type.Solid) { override fun toString() = "Solid" + context(safeContext: SafeContext) override fun matches( state: BlockState, pos: BlockPos, - world: ClientWorld, ignoredProperties: Collection> - ) = - state.isSolidBlock(world, pos) + ) = with(safeContext) { state.isSolidBlock(world, pos) && state.block !in replace } - override fun getStack(world: ClientWorld, pos: BlockPos, automated: Automated) = - with(automated) { + context(automatedSafeContext: AutomatedSafeContext) + override fun getStack(pos: BlockPos) = + with(automatedSafeContext) { findDisposable()?.stacks?.firstOrNull { - it.item.block in inventoryConfig.disposables + it.item.block in inventoryConfig.disposables && it.item.block !in replace } ?: ItemStack(Items.NETHERRACK) } + context(_: AutomatedSafeContext) + override fun getState(pos: BlockPos): BlockState = getStack(pos).item.block.defaultState + override fun isEmpty() = false } - data class Support(val direction: Direction) : TargetState(Type.SUPPORT) { + data class Support(val direction: Direction) : TargetState(Type.Support) { override fun toString() = "Support for ${direction.name}" + context(safeContext: SafeContext) override fun matches( state: BlockState, pos: BlockPos, - world: ClientWorld, ignoredProperties: Collection> - ) = + ) = with(safeContext) { world.getBlockState(pos.offset(direction)).isSolidBlock(world, pos.offset(direction)) || state.isSolidBlock(world, pos) + } - override fun getStack(world: ClientWorld, pos: BlockPos, automated: Automated) = - with(automated) { + context(automatedSafeContext: AutomatedSafeContext) + override fun getStack(pos: BlockPos) = + with(automatedSafeContext) { findDisposable()?.stacks?.firstOrNull { it.item.block in inventoryConfig.disposables } ?: ItemStack(Items.NETHERRACK) } + context(_: AutomatedSafeContext) + override fun getState(pos: BlockPos): BlockState = getStack(pos).item.block.defaultState + override fun isEmpty() = false } - data class State(val blockState: BlockState) : TargetState(Type.STATE) { + data class State(val blockState: BlockState) : TargetState(Type.State) { override fun toString() = "State of $blockState" + context(safeContext: SafeContext) override fun matches( state: BlockState, pos: BlockPos, - world: ClientWorld, ignoredProperties: Collection> ) = state.matches(blockState, ignoredProperties) - override fun getStack(world: ClientWorld, pos: BlockPos, automated: Automated): ItemStack = - blockState.block.getPickStack(world, pos, blockState, true) + context(automatedSafeContext: AutomatedSafeContext) + override fun getStack(pos: BlockPos): ItemStack = + blockState.block.getPickStack(automatedSafeContext.world, pos, blockState, true) + + context(_: AutomatedSafeContext) + override fun getState(pos: BlockPos): BlockState = blockState override fun isEmpty() = blockState.isEmpty } - data class Block(val block: net.minecraft.block.Block) : TargetState(Type.BLOCK) { + data class Block(val block: net.minecraft.block.Block) : TargetState(Type.Block) { override fun toString() = "Block of ${block.name.string.capitalize()}" + context(safeContext: SafeContext) override fun matches( state: BlockState, pos: BlockPos, - world: ClientWorld, ignoredProperties: Collection> - ) = - state.block == block + ) = state.block == block - override fun getStack(world: ClientWorld, pos: BlockPos, automated: Automated): ItemStack = - block.getPickStack(world, pos, block.defaultState, true) + context(automatedSafeContext: AutomatedSafeContext) + override fun getStack(pos: BlockPos): ItemStack = + block.getPickStack(automatedSafeContext.world, pos, block.defaultState, true) + + context(_: AutomatedSafeContext) + override fun getState(pos: BlockPos): BlockState = block.defaultState override fun isEmpty() = block.defaultState.isEmpty } - data class Stack(val itemStack: ItemStack) : TargetState(Type.STACK) { + data class Stack(val itemStack: ItemStack) : TargetState(Type.Stack) { private val startStack: ItemStack = itemStack.copy() override fun toString() = "Stack of ${startStack.item.name.string.capitalize()}" private val block = itemStack.item.block + context(safeContext: SafeContext) override fun matches( state: BlockState, pos: BlockPos, - world: ClientWorld, ignoredProperties: Collection> - ) = - state.block == block + ) = state.block == block - override fun getStack(world: ClientWorld, pos: BlockPos, automated: Automated): ItemStack = + context(automatedSafeContext: AutomatedSafeContext) + override fun getStack(pos: BlockPos): ItemStack = itemStack + context(_: AutomatedSafeContext) + override fun getState(pos: BlockPos): BlockState = block.defaultState + override fun isEmpty() = false } } diff --git a/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt b/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt index a1a5e7c9b..869c7678b 100644 --- a/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt +++ b/src/main/kotlin/com/lambda/interaction/material/StackSelection.kt @@ -20,6 +20,7 @@ package com.lambda.interaction.material import com.lambda.interaction.material.StackSelection.Companion.StackSelectionDsl import com.lambda.util.EnchantmentUtils.getEnchantment import com.lambda.util.item.ItemStackUtils.shulkerBoxContents +import com.lambda.util.item.ItemUtils import net.minecraft.block.Block import net.minecraft.block.BlockState import net.minecraft.component.ComponentType @@ -27,11 +28,11 @@ import net.minecraft.component.DataComponentTypes import net.minecraft.enchantment.Enchantment import net.minecraft.item.Item import net.minecraft.item.ItemStack -import net.minecraft.item.ToolMaterial import net.minecraft.item.consume.UseAction import net.minecraft.registry.RegistryKey import net.minecraft.registry.tag.TagKey import net.minecraft.screen.slot.Slot +import java.util.* import kotlin.reflect.KClass /** @@ -57,8 +58,7 @@ class StackSelection { /** * Filters the given [stacks], sorts them with the [comparator] and returns the first value */ - fun bestItemMatch(stacks: List): ItemStack? = - filterStacks(stacks).firstOrNull() + fun bestItemMatch(stacks: List): ItemStack? = filterStacks(stacks).firstOrNull() fun matches(stack: ItemStack): Boolean = filterStack(stack) @@ -160,7 +160,15 @@ class StackSelection { */ fun isOneOfStacks(stacks: Collection): (ItemStack) -> Boolean = stacks::contains - fun isSuitableForBreaking(blockState: BlockState): (ItemStack) -> Boolean = { it.isSuitableFor(blockState) } + fun isEfficientForBreaking(blockState: BlockState): (ItemStack) -> Boolean = { itemStack -> + val hasEfficientTool = efficientToolCache.getOrPut(blockState) { + ItemUtils.tools.any { it.getMiningSpeed(it.defaultStack, blockState) > 1f } + } + if (hasEfficientTool) itemStack.item.getMiningSpeed(itemStack, blockState) > 1f + else false + } + + fun isSuitableForBreaking(blockState: BlockState): (ItemStack) -> Boolean = { !blockState.isToolRequired || it.isSuitableFor(blockState) } fun hasTag(tag: TagKey): (ItemStack) -> Boolean = { it.isIn(tag) } @@ -230,27 +238,24 @@ class StackSelection { * Returns the negation of the original predicate. * @return A new predicate that matches if the original predicate does not match. */ - fun ((ItemStack) -> Boolean).not(): (ItemStack) -> Boolean { - return { !this(it) } - } + fun ((ItemStack) -> Boolean).not(): (ItemStack) -> Boolean = { !this(it) } /** * Combines two predicates using the logical AND operator. * @param otherPredicate The second predicate. * @return A new predicate that matches if both inputs predicate match. */ - infix fun ((ItemStack) -> Boolean).and(otherPredicate: (ItemStack) -> Boolean): (ItemStack) -> Boolean { - return { this(it) && otherPredicate(it) } - } + infix fun ((ItemStack) -> Boolean).and(otherPredicate: (ItemStack) -> Boolean): (ItemStack) -> Boolean = { this(it) && otherPredicate(it) } /** * Combines two predicates using the logical OR operator. * @param otherPredicate The second predicate. * @return A new predicate that matches if either input predicate matches. */ - infix fun ((ItemStack) -> Boolean).or(otherPredicate: (ItemStack) -> Boolean): (ItemStack) -> Boolean { - return { this(it) || otherPredicate(it) } - } + infix fun ((ItemStack) -> Boolean).or(otherPredicate: (ItemStack) -> Boolean): (ItemStack) -> Boolean = { this(it) || otherPredicate(it) } + + fun ((ItemStack) -> Boolean).andIf(predicate: Boolean, otherPredicate: () -> (ItemStack) -> Boolean): (ItemStack) -> Boolean = + if (predicate) { { this(it) && otherPredicate()(it) } } else this override fun toString() = buildString { append("selection of ${count}x ") @@ -279,6 +284,8 @@ class StackSelection { val NOTHING: (ItemStack) -> Boolean = { false } val NO_COMPARE: Comparator = Comparator { _, _ -> 0 } + val efficientToolCache: MutableMap = Collections.synchronizedMap(mutableMapOf()) + @StackSelectionDsl fun Item.select() = selectStack { isItem(this@select) } diff --git a/src/main/kotlin/com/lambda/interaction/request/breaking/BreakConfig.kt b/src/main/kotlin/com/lambda/interaction/request/breaking/BreakConfig.kt index 7ba6875d3..c46df9408 100644 --- a/src/main/kotlin/com/lambda/interaction/request/breaking/BreakConfig.kt +++ b/src/main/kotlin/com/lambda/interaction/request/breaking/BreakConfig.kt @@ -55,9 +55,9 @@ interface BreakConfig : RequestConfig { val avoidLiquids: Boolean val avoidSupporting: Boolean - val breakWeakBlocks: Boolean val ignoredBlocks: Set + val efficientOnly: Boolean val suitableToolsOnly: Boolean val forceSilkTouch: Boolean val forceFortunePickaxe: Boolean diff --git a/src/main/kotlin/com/lambda/interaction/request/breaking/BreakInfo.kt b/src/main/kotlin/com/lambda/interaction/request/breaking/BreakInfo.kt index 98d9379cd..731d9e00c 100644 --- a/src/main/kotlin/com/lambda/interaction/request/breaking/BreakInfo.kt +++ b/src/main/kotlin/com/lambda/interaction/request/breaking/BreakInfo.kt @@ -156,7 +156,7 @@ data class BreakInfo( PlayerActionC2SPacket( action, context.blockPos, - context.result.side, + context.hitResult.side, sequence ) } diff --git a/src/main/kotlin/com/lambda/interaction/request/breaking/BreakManager.kt b/src/main/kotlin/com/lambda/interaction/request/breaking/BreakManager.kt index c9b7b449c..fc9fdfd9a 100644 --- a/src/main/kotlin/com/lambda/interaction/request/breaking/BreakManager.kt +++ b/src/main/kotlin/com/lambda/interaction/request/breaking/BreakManager.kt @@ -33,7 +33,7 @@ import com.lambda.graphics.renderer.esp.DynamicAABB import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint import com.lambda.interaction.construction.context.BreakContext -import com.lambda.interaction.construction.result.BreakResult +import com.lambda.interaction.construction.result.results.BreakResult import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.verify.TargetState import com.lambda.interaction.material.StackSelection @@ -433,7 +433,7 @@ object BreakManager : RequestHandler( .let { infos -> rotationRequest = infos.firstOrNull { info -> info.breakConfig.rotateForBreak } ?.let { info -> - val rotation = info.context.rotation + val rotation = info.context.rotationRequest logger.debug("Requesting rotation", rotation) rotation.submit(false) } @@ -528,7 +528,7 @@ object BreakManager : RequestHandler( abandonedInfo.context.blockPos .toStructure(TargetState.Empty) .toBlueprint() - .simulate(player.eyePos) + .simulate() .asSequence() .filterIsInstance() .filter { canAccept(it.context) } @@ -685,7 +685,7 @@ object BreakManager : RequestHandler( return } - val hitResult = ctx.result + val hitResult = ctx.hitResult if (gamemode.isCreative && world.worldBorder.contains(ctx.blockPos)) { breakCooldown = breakConfig.breakDelay diff --git a/src/main/kotlin/com/lambda/interaction/request/interacting/InteractionManager.kt b/src/main/kotlin/com/lambda/interaction/request/interacting/InteractionManager.kt index 927827f7a..f5fc73080 100644 --- a/src/main/kotlin/com/lambda/interaction/request/interacting/InteractionManager.kt +++ b/src/main/kotlin/com/lambda/interaction/request/interacting/InteractionManager.kt @@ -122,10 +122,10 @@ object InteractionManager : RequestHandler( InteractionInfo(ctx, request.pendingInteractionsList, request).startPending() } if (interactConfig.interactConfirmationMode != InteractConfig.InteractConfirmationMode.AwaitThenInteract) { - interaction.interactBlock(player, Hand.MAIN_HAND, ctx.result) + interaction.interactBlock(player, Hand.MAIN_HAND, ctx.hitResult) } else { interaction.sendSequencedPacket(world) { sequence -> - PlayerInteractBlockC2SPacket(Hand.MAIN_HAND, ctx.result, sequence) + PlayerInteractBlockC2SPacket(Hand.MAIN_HAND, ctx.hitResult, sequence) } } if (interactConfig.swingHand) { diff --git a/src/main/kotlin/com/lambda/interaction/request/placing/PlaceManager.kt b/src/main/kotlin/com/lambda/interaction/request/placing/PlaceManager.kt index 9bfc7ded4..e1d262a13 100644 --- a/src/main/kotlin/com/lambda/interaction/request/placing/PlaceManager.kt +++ b/src/main/kotlin/com/lambda/interaction/request/placing/PlaceManager.kt @@ -212,7 +212,7 @@ object PlaceManager : RequestHandler( */ private fun AutomatedSafeContext.placeBlock(placeContext: PlaceContext, request: PlaceRequest, hand: Hand): ActionResult { interaction.syncSelectedSlot() - val hitResult = placeContext.result + val hitResult = placeContext.hitResult if (!world.worldBorder.contains(hitResult.blockPos)) { logger.error("Placement position outside the world border", placeContext, request) return ActionResult.FAIL diff --git a/src/main/kotlin/com/lambda/interaction/request/rotating/Rotation.kt b/src/main/kotlin/com/lambda/interaction/request/rotating/Rotation.kt index 7d17bbe20..d10326ba7 100644 --- a/src/main/kotlin/com/lambda/interaction/request/rotating/Rotation.kt +++ b/src/main/kotlin/com/lambda/interaction/request/rotating/Rotation.kt @@ -17,11 +17,9 @@ package com.lambda.interaction.request.rotating -import com.lambda.Lambda.mc import com.lambda.threading.runSafe import com.lambda.util.math.MathUtils.toDegree import com.lambda.util.math.MathUtils.toRadian -import com.lambda.util.math.Vec2d import com.lambda.util.math.plus import com.lambda.util.math.times import com.lambda.util.world.raycast.InteractionMask diff --git a/src/main/kotlin/com/lambda/interaction/request/rotating/visibilty/VisibilityChecker.kt b/src/main/kotlin/com/lambda/interaction/request/rotating/visibilty/VisibilityChecker.kt index 31aad1e65..684f4650a 100644 --- a/src/main/kotlin/com/lambda/interaction/request/rotating/visibilty/VisibilityChecker.kt +++ b/src/main/kotlin/com/lambda/interaction/request/rotating/visibilty/VisibilityChecker.kt @@ -165,51 +165,54 @@ object VisibilityChecker { sides: Set = emptySet(), resolution: Int = 5, scan: SurfaceScan = SurfaceScan.DEFAULT, - check: (Direction, Vec3d) -> Unit, + check: (Direction, Vec3d) -> Unit ) { sides.forEach { side -> - val (minX, minY, minZ, maxX, maxY, maxZ) = box.contract(AutomationConfig.shrinkFactor).bounds(side) + val (minX, minY, minZ, maxX, maxY, maxZ) = box + .contract(AutomationConfig.shrinkFactor) + .offset(side.doubleVector.multiply(AutomationConfig.shrinkFactor)) + .bounds(side) // Determine the bounds to scan based on the axis and mode. Skip if no part of the face is in the desired bounds val (startX, endX) = if (scan.axis == Direction.Axis.X && maxX != minX) { when (scan.mode) { - ScanMode.GREATER_BLOCK_HALF -> (floor(minX) + 0.501).let { center -> + ScanMode.GreaterBlockHalf -> (floor(minX) + 0.501).let { center -> if (maxX < center) return@forEach minX.coerceAtLeast(center) to maxX } - ScanMode.LESSER_BLOCK_HALF -> (floor(maxX) + 0.499).let { center -> + ScanMode.LesserBlockHalf -> (floor(maxX) + 0.499).let { center -> if (minX > center) return@forEach minX to maxX.coerceAtMost(center) } - ScanMode.FULL -> minX to maxX + ScanMode.Full -> minX to maxX } } else minX to maxX val (startY, endY) = if (scan.axis == Direction.Axis.Y && maxY != minY) { when (scan.mode) { - ScanMode.GREATER_BLOCK_HALF -> (floor(minY) + 0.501).let { center -> + ScanMode.GreaterBlockHalf -> (floor(minY) + 0.501).let { center -> if (maxY < center) return@forEach minY.coerceAtLeast(center) to maxY } - ScanMode.LESSER_BLOCK_HALF -> (floor(maxY) + 0.499).let { center -> + ScanMode.LesserBlockHalf -> (floor(maxY) + 0.499).let { center -> if (minY > center) return@forEach minY to maxY.coerceAtMost(center) } - ScanMode.FULL -> minY to maxY + ScanMode.Full -> minY to maxY } } else minY to maxY val (startZ, endZ) = if (scan.axis == Direction.Axis.Z && maxZ != minZ) { when (scan.mode) { - ScanMode.GREATER_BLOCK_HALF -> (floor(minZ) + 0.501).let { center -> + ScanMode.GreaterBlockHalf -> (floor(minZ) + 0.501).let { center -> if (maxZ < center) return@forEach minZ.coerceAtLeast(center) to maxZ } - ScanMode.LESSER_BLOCK_HALF -> (floor(maxZ) + 0.499).let { center -> + ScanMode.LesserBlockHalf -> (floor(maxZ) + 0.499).let { center -> if (minZ > center) return@forEach minZ to maxZ.coerceAtMost(center) } - ScanMode.FULL -> minZ to maxZ + ScanMode.Full -> minZ to maxZ } } else minZ to maxZ diff --git a/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt b/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt index 2b1cd3998..8081c0215 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/FastBreak.kt @@ -66,19 +66,18 @@ object FastBreak : Module( editTyped( ::avoidLiquids, ::avoidSupporting, + ::efficientOnly, ::suitableToolsOnly, ::rotateForBreak, ::doubleBreak ) { defaultValue(false) } ::breaksPerTick.edit { defaultValue(1) } - ::breakWeakBlocks.edit { defaultValue(true) } hide( ::sorter, ::doubleBreak, ::unsafeCancels, ::rotateForBreak, ::breaksPerTick, - ::breakWeakBlocks ) } override val inventoryConfig = InventorySettings(this, Group.Inventory).apply { @@ -174,7 +173,6 @@ object FastBreak : Module( breakConfig.breakThreshold ), state, - breakConfig.sorter, this@FastBreak ) diff --git a/src/main/kotlin/com/lambda/module/modules/player/HighwayTools.kt b/src/main/kotlin/com/lambda/module/modules/player/HighwayTools.kt index 63fb744af..46327ce69 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/HighwayTools.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/HighwayTools.kt @@ -81,6 +81,7 @@ object HighwayTools : Module( private val wallMaterial by setting("Wall Material", Blocks.NETHERRACK, "Material to build the walls with") { rightWall == Material.Block || leftWall == Material.Block }.group(Group.Structure) private val ceiling by setting("Ceiling", Material.None, "Material for the ceiling").group(Group.Structure) private val ceilingMaterial by setting("Ceiling Material", Blocks.OBSIDIAN, "Material to build the ceiling with") { ceiling == Material.Block }.group(Group.Structure) + private val replaceableSolids by setting("Replaceable Solids", setOf(Blocks.MAGMA_BLOCK, Blocks.SOUL_SAND)).group(Group.Structure) private val distance by setting("Distance", -1, -1..1000000, 1, "Distance to build the highway/tunnel (negative for infinite)").group(Group.Structure) private val sliceSize by setting("Slice Size", 3, 1..5, 1, "Number of slices to build at once").group(Group.Structure) @@ -265,11 +266,10 @@ object HighwayTools : Module( return transformed } - private fun target(target: Material, material: net.minecraft.block.Block): TargetState { - return when (target) { - Material.Solid -> TargetState.Solid + private fun target(target: Material, material: net.minecraft.block.Block) = + when (target) { + Material.Solid -> TargetState.Solid(replaceableSolids) Material.Block -> TargetState.Block(material) else -> throw IllegalStateException("Invalid material") } - } } diff --git a/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt b/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt index df042db97..a967db0b4 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/Nuker.kt @@ -17,7 +17,6 @@ package com.lambda.module.modules.player -import com.lambda.context.AutomationConfig import com.lambda.interaction.BaritoneManager import com.lambda.interaction.construction.blueprint.TickingBlueprint.Companion.tickingBlueprint import com.lambda.interaction.construction.verify.TargetState @@ -27,7 +26,7 @@ import com.lambda.task.RootTask.run import com.lambda.task.Task import com.lambda.task.tasks.BuildTask.Companion.build import com.lambda.util.BlockUtils.blockPos -import com.lambda.util.BlockUtils.blockState +import net.minecraft.block.Blocks import net.minecraft.util.math.BlockPos object Nuker : Module( @@ -39,7 +38,6 @@ object Nuker : Module( private val width by setting("Width", 4, 1..8, 1) private val flatten by setting("Flatten", true) private val fillFluids by setting("Fill Fluids", false, "Removes liquids by filling them in before breaking") - private val instantOnly by setting("Instant Only", false) private val fillFloor by setting("Fill Floor", false) private val baritoneSelection by setting("Baritone Selection", false, "Restricts nuker to your baritone selection") @@ -53,7 +51,6 @@ object Nuker : Module( .map { it.blockPos } .filter { !world.isAir(it) } .filter { !flatten || it.y >= player.blockPos.y } - .filter { !instantOnly || blockState(it).getHardness(world, it) <= AutomationConfig.breakConfig.breakThreshold } .filter { pos -> if (!baritoneSelection) true else BaritoneManager.primary.selectionManager.selections.any { @@ -69,7 +66,7 @@ object Nuker : Module( if (fillFloor) { val floor = BlockPos.iterateOutwards(player.blockPos.down(), width, 0, width) .map { it.blockPos } - .associateWith { TargetState.Solid } + .associateWith { TargetState.Solid(setOf(Blocks.MAGMA_BLOCK)) } return@tickingBlueprint selection + floor } diff --git a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt index dca765b20..1d0df02dc 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/PacketMine.kt @@ -31,7 +31,7 @@ import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint import com.lambda.interaction.construction.context.BreakContext import com.lambda.interaction.construction.context.BuildContext -import com.lambda.interaction.construction.result.BreakResult +import com.lambda.interaction.construction.result.results.BreakResult import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.verify.TargetState import com.lambda.interaction.request.breaking.BreakConfig @@ -79,8 +79,12 @@ object PacketMine : Module( private val endColor by setting("End Color", Color(255, 0, 0, 60).brighter(), "The color of the end (farthest from breaking) of the queue") { renderQueue && dynamicColor }.group(Group.Break, BreakSettings.Group.Cosmetic) override val breakConfig = BreakSettings(this, Group.Break).apply { - editTyped(::avoidLiquids, ::avoidSupporting, ::suitableToolsOnly) { defaultValue(false) } - ::breakWeakBlocks.edit { defaultValue(true) } + editTyped( + ::avoidLiquids, + ::avoidSupporting, + ::efficientOnly, + ::suitableToolsOnly + ) { defaultValue(false) } ::swing.edit { defaultValue(BreakConfig.SwingMode.Start) } ::rebreak.insert(::rebreakMode, SettingGroup.InsertMode.Below) @@ -250,7 +254,7 @@ object PacketMine : Module( .filterNotNull() .associateWith { TargetState.State(blockState(it).fluidState.blockState) } .toBlueprint() - .simulate(player.eyePos) + .simulate() .asSequence() .filterIsInstance() .map { it.context } diff --git a/src/main/kotlin/com/lambda/module/modules/player/Scaffold.kt b/src/main/kotlin/com/lambda/module/modules/player/Scaffold.kt index 8b4059e6e..d70eada13 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/Scaffold.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/Scaffold.kt @@ -27,7 +27,7 @@ import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint import com.lambda.interaction.construction.context.BuildContext -import com.lambda.interaction.construction.result.PlaceResult +import com.lambda.interaction.construction.result.results.PlaceResult import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.verify.TargetState import com.lambda.interaction.request.Request.Companion.submit @@ -92,11 +92,11 @@ object Scaffold : Module( val beneath = playerSupport.down(offset) runSafeAutomated { scaffoldPositions(beneath) - .associateWith { TargetState.Solid } + .associateWith { TargetState.Solid(emptySet()) } .toBlueprint() - .simulate(player.eyePos) + .simulate() .filterIsInstance() - .minByOrNull { it.blockPos distSq beneath } + .minByOrNull { it.pos distSq beneath } ?.let { result -> submit(PlaceRequest( setOf(result.context), diff --git a/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt b/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt index 106780268..4319e3887 100644 --- a/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt +++ b/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt @@ -32,13 +32,17 @@ import com.lambda.interaction.construction.blueprint.PropagatingBlueprint import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint import com.lambda.interaction.construction.blueprint.TickingBlueprint import com.lambda.interaction.construction.context.BuildContext -import com.lambda.interaction.construction.result.BreakResult import com.lambda.interaction.construction.result.BuildResult +import com.lambda.interaction.construction.result.Contextual +import com.lambda.interaction.construction.result.Dependent import com.lambda.interaction.construction.result.Drawable -import com.lambda.interaction.construction.result.InteractResult import com.lambda.interaction.construction.result.Navigable -import com.lambda.interaction.construction.result.PlaceResult import com.lambda.interaction.construction.result.Resolvable +import com.lambda.interaction.construction.result.results.BreakResult +import com.lambda.interaction.construction.result.results.GenericResult +import com.lambda.interaction.construction.result.results.InteractResult +import com.lambda.interaction.construction.result.results.PlaceResult +import com.lambda.interaction.construction.result.results.PreSimResult import com.lambda.interaction.construction.simulation.BuildGoal import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.simulation.Simulation.Companion.simulation @@ -50,7 +54,6 @@ import com.lambda.interaction.request.placing.PlaceRequest import com.lambda.task.Task import com.lambda.task.tasks.EatTask.Companion.eat import com.lambda.threading.runSafeAutomated -import com.lambda.util.Communication.info import com.lambda.util.Formatting.string import com.lambda.util.extension.Structure import com.lambda.util.extension.inventorySlots @@ -106,97 +109,112 @@ class BuildTask private constructor( if (collectDrops()) return@listen - val results = runSafeAutomated { blueprint.simulate(player.eyePos) } + simulateAndProcess() + } - AutomationConfig.drawables = results - .filterIsInstance() - .plus(pendingInteractions.toList()) + listen { + if (finishOnDone && blueprint.structure.isEmpty()) { + failure("Structure is empty") + return@listen + } + } + } - val resultsNotBlocked = results - .filter { result -> pendingInteractions.none { it.blockPos == result.blockPos } } - .sorted() + private fun SafeContext.simulateAndProcess() { + val results = runSafeAutomated { blueprint.simulate() } - val bestResult = resultsNotBlocked.firstOrNull() ?: return@listen - if (bestResult !is BuildResult.Contextual && pendingInteractions.isNotEmpty()) - return@listen - info("Best result: $bestResult") - when (bestResult) { - is BuildResult.Done, - is BuildResult.Ignored, - is BuildResult.Unbreakable, - is BuildResult.Restricted, - is BuildResult.NoPermission -> { - if (iteratePropagating()) return@listen - - if (finishOnDone) success(blueprint.structure) - } + AutomationConfig.drawables = results + .filterIsInstance() + .plus(pendingInteractions.toList()) - is BuildResult.NotVisible, - is PlaceResult.NoIntegrity -> { - if (!buildConfig.pathing) return@listen - val sim = blueprint.simulation() - val goal = BuildGoal(sim, player.blockPos) - BaritoneManager.setGoalAndPath(goal) - } + val resultsNotBlocked = results + .filter { result -> pendingInteractions.none { it.blockPos == result.pos } } + .sorted() - is Navigable -> { - if (buildConfig.pathing) BaritoneManager.setGoalAndPath(bestResult.goal) - } + val bestResult = resultsNotBlocked.firstOrNull() ?: return + handleResult(bestResult, resultsNotBlocked) + } - is BuildResult.Contextual -> { - if (atMaxPendingInteractions) return@listen - when (bestResult) { - is BreakResult.Break -> { - val breakResults = resultsNotBlocked - .filterIsInstance() - .map { it.context } - - breakRequest(breakResults, pendingInteractions) { - onStop { breaks++ } - onItemDrop?.let { onItemDrop -> - onItemDrop { onItemDrop(it) } - } - }.submit() - return@listen - } - is PlaceResult.Place -> { - val placeResults = resultsNotBlocked - .filterIsInstance() - .map { it.context } - - PlaceRequest( - placeResults, - pendingInteractions, - this@BuildTask - ) { placements++ }.submit() - } - is InteractResult.Interact -> { - val interactResults = resultsNotBlocked - .filterIsInstance() - .map { it.context } - - InteractRequest( - interactResults, - pendingInteractions, - this@BuildTask, - null - ).submit() - } - } + private fun SafeContext.handleResult(result: BuildResult, allResults: List) { + if (result !is Contextual && pendingInteractions.isNotEmpty()) + return + + when (result) { + is PreSimResult.Done, + is PreSimResult.Unbreakable, + is PreSimResult.Restricted, + is PreSimResult.NoPermission, + is GenericResult.Ignored -> { + if (iteratePropagating()) { + simulateAndProcess() + return } - is Resolvable -> { - LOG.info("Resolving: ${bestResult.name}") + if (finishOnDone) success(blueprint.structure) + } + + is GenericResult.NotVisible, + is PlaceResult.NoIntegrity -> { + if (!buildConfig.pathing) return + val sim = blueprint.simulation() + val goal = BuildGoal(sim, player.blockPos) + BaritoneManager.setGoalAndPath(goal) + } - bestResult.resolve().execute(this@BuildTask) + is Navigable -> { + if (buildConfig.pathing) BaritoneManager.setGoalAndPath(result.goal) + } + + is Contextual -> { + if (atMaxPendingInteractions) return + when (result) { + is BreakResult.Break -> { + val breakResults = allResults + .map { if (it is Dependent) it.lastDependency else it } + .filterIsInstance() + .map { it.context } + + breakRequest(breakResults, pendingInteractions) { + onStop { breaks++ } + onItemDrop?.let { onItemDrop -> + onItemDrop { onItemDrop(it) } + } + }.submit() + return + } + is PlaceResult.Place -> { + val placeResults = allResults + .map { if (it is Dependent) it.lastDependency else it } + .filterIsInstance() + .map { it.context } + + PlaceRequest( + placeResults, + pendingInteractions, + this@BuildTask + ) { placements++ }.submit() + } + is InteractResult.Interact -> { + val interactResults = allResults + .map { if (it is Dependent) it.lastDependency else it } + .filterIsInstance() + .map { it.context } + + InteractRequest( + interactResults, + pendingInteractions, + this@BuildTask, + null + ).submit() + } } } - } - listen { - if (finishOnDone && blueprint.structure.isEmpty()) { - failure("Structure is empty") - return@listen + is Dependent -> handleResult(result.lastDependency, allResults) + + is Resolvable -> { + LOG.info("Resolving: ${result.name}") + result.resolve().execute(this@BuildTask) } } } diff --git a/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt b/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt index a8b59dbf7..9e7aabc49 100644 --- a/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt +++ b/src/main/kotlin/com/lambda/task/tasks/PlaceContainer.kt @@ -21,8 +21,8 @@ import com.lambda.context.Automated import com.lambda.context.SafeContext import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint -import com.lambda.interaction.construction.result.BuildResult -import com.lambda.interaction.construction.result.PlaceResult +import com.lambda.interaction.construction.result.results.GenericResult +import com.lambda.interaction.construction.result.results.PlaceResult import com.lambda.interaction.construction.simulation.BuildSimulator.simulate import com.lambda.interaction.construction.verify.TargetState import com.lambda.interaction.request.ManagerUtils @@ -55,19 +55,18 @@ class PlaceContainer @Ta5kBuilder constructor( .flatMap { it.toStructure(TargetState.Stack(startStack)) .toBlueprint() - .simulate(player.eyePos) + .simulate() } } val options = results.filterIsInstance().filter { - canBeOpened(startStack, it.blockPos, it.context.result.side) - } + results.filterIsInstance().filter { - canBeOpened(startStack, it.blockPos, it.context.result.side) - } + canBeOpened(startStack, it.pos, it.context.hitResult.side) + } + results.filterIsInstance() + val containerPosition = options.filter { // ToDo: Check based on if we can move the player close enough rather than y level once the custom pathfinder is merged - it.blockPos.y == player.blockPos.y - }.minByOrNull { it.blockPos distSq player.pos }?.blockPos ?: run { + it.pos.y == player.blockPos.y + }.minByOrNull { it.pos distSq player.pos }?.pos ?: run { failure("Couldn't find a valid container placement position for ${startStack.name.string}") return@onStart } diff --git a/src/main/kotlin/com/lambda/util/BlockUtils.kt b/src/main/kotlin/com/lambda/util/BlockUtils.kt index 0423c8e6d..a500dce1e 100644 --- a/src/main/kotlin/com/lambda/util/BlockUtils.kt +++ b/src/main/kotlin/com/lambda/util/BlockUtils.kt @@ -305,7 +305,7 @@ object BlockUtils { return speedMultiplier } - val BlockState.isEmpty get() = matches(emptyState) + val BlockState.isEmpty get() = isAir || matches(emptyState) val BlockState.isNotEmpty get() = !isEmpty val BlockState.hasFluid get() = !fluidState.isEmpty val BlockState.emptyState: BlockState get() = fluidState.blockState diff --git a/src/main/kotlin/com/lambda/util/EnchantmentUtils.kt b/src/main/kotlin/com/lambda/util/EnchantmentUtils.kt index 903585ae3..a7d2541e1 100644 --- a/src/main/kotlin/com/lambda/util/EnchantmentUtils.kt +++ b/src/main/kotlin/com/lambda/util/EnchantmentUtils.kt @@ -46,7 +46,6 @@ object EnchantmentUtils { fun ItemStack.getEnchantment(key: RegistryKey) = enchantments.enchantmentEntries.find { it.key?.matchesKey(key) == true }?.intValue ?: 0 - /** * Iterates over all the enchantments for the given [ItemStack] */ diff --git a/src/main/kotlin/com/lambda/util/EntityUtils.kt b/src/main/kotlin/com/lambda/util/EntityUtils.kt new file mode 100644 index 000000000..ebecce7da --- /dev/null +++ b/src/main/kotlin/com/lambda/util/EntityUtils.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.util + +import com.lambda.util.math.MathUtils.floorToInt +import net.minecraft.entity.Entity +import net.minecraft.util.math.BlockPos + +object EntityUtils { + fun Entity.getPositionsWithinHitboxXZ(minY: Int, maxY: Int): Set { + val hitbox = boundingBox + val minX = hitbox.minX.floorToInt() + val maxX = hitbox.maxX.floorToInt() + val minZ = hitbox.minZ.floorToInt() + val maxZ = hitbox.maxZ.floorToInt() + val positions = mutableSetOf() + (minX..maxX).forEach { x -> + (minY..maxY).forEach { y -> + (minZ..maxZ).forEach { z -> + positions.add(BlockPos(x, y, z)) + } + } + } + return positions + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt b/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt index b060b3aa0..e67ec2c48 100644 --- a/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt +++ b/src/main/kotlin/com/lambda/util/item/ItemStackUtils.kt @@ -17,8 +17,8 @@ package com.lambda.util.item +import com.lambda.context.SafeContext import com.lambda.util.collections.Cacheable.Companion.cacheable -import net.minecraft.component.ComponentType import net.minecraft.component.DataComponentTypes import net.minecraft.component.type.AttributeModifiersComponent import net.minecraft.entity.EquipmentSlot @@ -79,6 +79,15 @@ object ItemStackUtils { val List.count: Int get() = sumOf { it.count } val List.copy: List get() = map { it.copy() } + context(safeContext: SafeContext) + val ItemStack.inventoryIndex get() = safeContext.player.inventory.getSlotWithStack(this) + context(safeContext: SafeContext) + val ItemStack.inventoryIndexOrSelected get() = with(safeContext) { + player.inventory.getSlotWithStack(this@inventoryIndexOrSelected).let { + if (it == -1) player.inventory.selectedSlot else it + } + } + val List.compressed: List get() = fold(mutableListOf()) { acc, itemStack -> @@ -86,11 +95,7 @@ object ItemStackUtils { acc } - infix fun List.merge(other: ItemStack): List { - return flatMap { - it merge other - } - } + infix fun List.merge(other: ItemStack): List = flatMap { it merge other } infix fun ItemStack.merge(other: ItemStack): List { if (!isStackable || !other.isStackable) { diff --git a/src/main/kotlin/com/lambda/util/item/ItemUtils.kt b/src/main/kotlin/com/lambda/util/item/ItemUtils.kt index 2364191f4..c91b3776a 100644 --- a/src/main/kotlin/com/lambda/util/item/ItemUtils.kt +++ b/src/main/kotlin/com/lambda/util/item/ItemUtils.kt @@ -20,7 +20,9 @@ package com.lambda.util.item import net.minecraft.block.Block import net.minecraft.block.Blocks import net.minecraft.component.DataComponentTypes +import net.minecraft.item.BlockItem import net.minecraft.item.Item +import net.minecraft.item.ItemStack import net.minecraft.item.Items object ItemUtils { @@ -121,6 +123,7 @@ object ItemUtils { ) val Item.block: Block get() = Block.getBlockFromItem(this) + val ItemStack.blockItem: BlockItem get() = (item as? BlockItem ?: Items.AIR) as BlockItem val Item.nutrition: Int get() = components.get(DataComponentTypes.FOOD)?.nutrition ?: 0 diff --git a/src/main/kotlin/com/lambda/util/player/SlotUtils.kt b/src/main/kotlin/com/lambda/util/player/SlotUtils.kt index 9d56a4650..70f3753be 100644 --- a/src/main/kotlin/com/lambda/util/player/SlotUtils.kt +++ b/src/main/kotlin/com/lambda/util/player/SlotUtils.kt @@ -29,7 +29,6 @@ object SlotUtils { val ClientPlayerEntity.hotbarAndStorage: List get() = inventory.mainStacks val ClientPlayerEntity.combined: List get() = hotbarAndStorage + equipment - fun SafeContext.clickSlot( slotId: Int, button: Int, diff --git a/src/main/resources/lambda.accesswidener b/src/main/resources/lambda.accesswidener index 0cc97047a..b62668c1e 100644 --- a/src/main/resources/lambda.accesswidener +++ b/src/main/resources/lambda.accesswidener @@ -48,6 +48,7 @@ transitive-accessible method net/minecraft/entity/LivingEntity getHandSwingDurat transitive-accessible method net/minecraft/client/network/ClientPlayerInteractionManager syncSelectedSlot ()V transitive-accessible method net/minecraft/util/math/Direction listClosest (Lnet/minecraft/util/math/Direction;Lnet/minecraft/util/math/Direction;Lnet/minecraft/util/math/Direction;)[Lnet/minecraft/util/math/Direction; transitive-accessible field net/minecraft/client/network/ClientPlayerInteractionManager gameMode Lnet/minecraft/world/GameMode; +transitive-accessible method net/minecraft/entity/player/PlayerEntity updatePose ()V # Camera transitive-accessible method net/minecraft/client/render/Camera setPos (DDD)V