diff --git a/src/main/kotlin/com/lambda/config/blocks/BuildConfig.kt b/src/main/kotlin/com/lambda/config/blocks/BuildConfig.kt
index 7e98fc835..807f46b1f 100644
--- a/src/main/kotlin/com/lambda/config/blocks/BuildConfig.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/BuildConfig.kt
@@ -34,6 +34,7 @@ interface BuildConfig {
val collectDrops: Boolean
val spleefEntities: Boolean
val cautionDoubleBlocks: Boolean
+ val stripLogs: Boolean
val maxPendingActions: Int
val actionTimeout: Int
val maxBuildDependencies: Int
diff --git a/src/main/kotlin/com/lambda/config/blocks/BuildSettings.kt b/src/main/kotlin/com/lambda/config/blocks/BuildSettings.kt
index b03efbe5c..63102e414 100644
--- a/src/main/kotlin/com/lambda/config/blocks/BuildSettings.kt
+++ b/src/main/kotlin/com/lambda/config/blocks/BuildSettings.kt
@@ -38,6 +38,7 @@ class BuildSettings(override val c: Config) : BuildConfig, ConfigBlock {
@Group(GENERAL_GROUP) override val collectDrops by c.setting("Collect All Drops", false, "Collect all drops when breaking blocks")
@Group(GENERAL_GROUP) override val spleefEntities by c.setting("Spleef Entities", false, "Breaks blocks beneath entities blocking placements to get them out of the way")
@Group(GENERAL_GROUP) override val cautionDoubleBlocks by c.setting("Caution Double Blocks", true, "Prevents spamming double blocks like doors, chests, etc when configured to interact more than once per tick")
+ @Group(GENERAL_GROUP) override val stripLogs by c.setting("Strip Logs", true, "Builds stripped blocks by placing the unstripped variant and stripping it with an axe when the stripped item isn't carried")
@Group(GENERAL_GROUP) override val maxPendingActions by c.setting("Max Pending Actions", 59, 1..60, 1, "The maximum count of pending interactions to allow before pausing future interactions")
@Group(GENERAL_GROUP) override val actionTimeout by c.setting("Action Timeout", 10, 1..30, 1, "Timeout for block breaks in ticks", unit = " ticks")
@Group(GENERAL_GROUP) override val maxBuildDependencies by c.setting("Max Sim Dependencies", 3, 0..10, 1, "Maximum dependency build results")
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/PlacementProcessor.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/PlacementProcessor.kt
index 841ca28c9..332ef30ad 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/PlacementProcessor.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/PlacementProcessor.kt
@@ -17,6 +17,7 @@
package com.lambda.interaction.construction.simulation.processing
+import com.lambda.context.AutomatedSafeContext
import com.lambda.context.SafeContext
import net.minecraft.block.BlockState
import net.minecraft.util.math.BlockPos
@@ -27,6 +28,10 @@ import net.minecraft.util.math.BlockPos
* unnecessary to scan all of them, for example.
*/
interface StateProcessor {
+ /** Checked before [acceptsState], so a disabled processor still falls back to breaking. */
+ context(_: AutomatedSafeContext)
+ fun isEnabled(): Boolean = true
+
fun acceptsState(state: BlockState, targetState: BlockState): Boolean
context(safeContext: SafeContext)
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/ProcessorRegistry.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/ProcessorRegistry.kt
index 3ec075142..9aaf2aadd 100644
--- a/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/ProcessorRegistry.kt
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/ProcessorRegistry.kt
@@ -18,7 +18,6 @@
package com.lambda.interaction.construction.simulation.processing
import com.lambda.context.AutomatedSafeContext
-import com.lambda.context.SafeContext
import com.lambda.core.Loadable
import com.lambda.interaction.construction.simulation.SimDsl
import com.lambda.interaction.construction.verify.TargetState
@@ -144,15 +143,19 @@ object ProcessorRegistry : Loadable {
return PreProcessingData(preProcessingInfo, pos)
}
- context(safeContext: SafeContext)
+ context(_: AutomatedSafeContext)
private fun preProcess(pos: BlockPos, state: BlockState, targetState: BlockState, itemStack: ItemStack) =
PreProcessingInfoAccumulator(targetState, itemStack.item).run {
var stateProcessing = false
stateProcessors.forEach { processor ->
- if (processor.acceptsState(state, targetState)) {
- with(processor) { preProcess(state, targetState, pos) }
- stateProcessing = true
+ if (!processor.acceptsState(state, targetState)) return@forEach
+ if (!processor.isEnabled()) {
+ // The verdict now hinges on a setting that can be toggled at any time.
+ noCaching()
+ return@forEach
}
+ with(processor) { preProcess(state, targetState, pos) }
+ stateProcessing = true
}
if (!omitInteraction) {
if (state.block != expectedState.block) {
diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/state/StrippedStateProcessor.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/state/StrippedStateProcessor.kt
new file mode 100644
index 000000000..ee405aef5
--- /dev/null
+++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/state/StrippedStateProcessor.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2026 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.processing.preprocessors.state
+
+import com.lambda.context.AutomatedSafeContext
+import com.lambda.context.SafeContext
+import com.lambda.interaction.construction.simulation.processing.PreProcessingInfoAccumulator
+import com.lambda.interaction.construction.simulation.processing.StateProcessor
+import com.lambda.util.item.ItemUtils
+import net.minecraft.block.Block
+import net.minecraft.block.BlockState
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.item.AxeItem
+import net.minecraft.item.Item
+import net.minecraft.state.property.Properties
+import net.minecraft.util.math.BlockPos
+
+@Suppress("unused")
+object StrippedStateProcessor : StateProcessor {
+ // [AxeItem.STRIPPED_BLOCKS] maps unstripped to stripped, we need the other direction
+ private val unstrippedToStripped: Map by lazy {
+ AxeItem.STRIPPED_BLOCKS.entries.associate { (from, to) -> to to from }
+ }
+
+ context(automatedSafeContext: AutomatedSafeContext)
+ override fun isEnabled() = automatedSafeContext.buildConfig.stripLogs
+
+ override fun acceptsState(state: BlockState, targetState: BlockState): Boolean {
+ val unstrippedVariant = unstrippedToStripped[targetState.block] ?: return false
+ return state.isReplaceable || state.block == unstrippedVariant
+ }
+
+ context(safeContext: SafeContext)
+ override fun PreProcessingInfoAccumulator.preProcess(state: BlockState, targetState: BlockState, pos: BlockPos) {
+ val unstrippedVariant = unstrippedToStripped[targetState.block] ?: return
+ val player = safeContext.player
+
+ // Every branch below depends on the current inventory, so this must never be cached.
+ noCaching()
+
+ if (state.isReplaceable){
+ // Don't use unstripped logs if you already have stripped ones
+ if (player.carries(targetState.block.asItem())) return
+
+ val sourceItem = unstrippedVariant.asItem()
+ if (!player.carries(sourceItem)) return
+
+ setExpectedState(unstrippedVariant.withAxisOf(targetState))
+ setItem(sourceItem)
+ } else if (state.block == unstrippedVariant) {
+ val axe = player.findAxe() ?: return
+ setItem(axe)
+ setPlacing(false)
+ // InteractSim refuses to interact while sneaking, so ask for it explicitly.
+ setSneak(false)
+ }
+ }
+
+ private fun Block.withAxisOf(targetState: BlockState) = defaultState.let { placed ->
+ if (Properties.AXIS in placed && Properties.AXIS in targetState) {
+ placed.with(Properties.AXIS, targetState.get(Properties.AXIS))
+ } else placed
+ }
+
+ private fun PlayerEntity.findAxe(): Item? {
+ val held = mainHandStack.item
+ if (held in ItemUtils.axes) return held
+ return inventory.mainStacks.firstOrNull { it.item in ItemUtils.axes }?.item
+ }
+
+ private fun PlayerEntity.carries(item: Item) =
+ inventory.mainStacks.any { it.item == item }
+}
diff --git a/src/main/resources/lambda.accesswidener b/src/main/resources/lambda.accesswidener
index c446edbb0..f887bb3c4 100644
--- a/src/main/resources/lambda.accesswidener
+++ b/src/main/resources/lambda.accesswidener
@@ -150,6 +150,7 @@ transitive-accessible method net/minecraft/item/DebugStickItem cycle (Lnet/minec
transitive-accessible field net/minecraft/structure/StructureTemplate blockInfoLists Ljava/util/List;
transitive-accessible method net/minecraft/item/BlockItem getPlacementState (Lnet/minecraft/item/ItemPlacementContext;)Lnet/minecraft/block/BlockState;
transitive-accessible method net/minecraft/block/AbstractBlock getPickStack (Lnet/minecraft/world/WorldView;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Z)Lnet/minecraft/item/ItemStack;
+transitive-accessible field net/minecraft/item/AxeItem STRIPPED_BLOCKS Ljava/util/Map;
transitive-accessible field net/minecraft/client/gui/screen/ingame/HandledScreen focusedSlot Lnet/minecraft/screen/slot/Slot;
transitive-accessible field net/minecraft/registry/SimpleRegistry frozen Z
transitive-accessible field net/minecraft/client/gui/screen/ingame/AbstractSignEditScreen messages [Ljava/lang/String;