diff --git a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt index eb2ddfdbc..0b82986e4 100644 --- a/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/BreakSettings.kt @@ -50,9 +50,10 @@ class BreakSettings( // Fixes / Delays override val breakThreshold by c.setting("Break Threshold", 0.70f, 0.1f..1.0f, 0.01f, "The break amount at which the block is considered broken", visibility = vis).group(groupPath, Group.General) - override val fudgeFactor by c.setting("Fudge Factor", 2, 0..5, 1, "The amount of ticks to give double, aka secondary breaks extra for the server to recognise the break", visibility = vis).group(groupPath, Group.General) + override val fudgeFactor by c.setting("Fudge Factor", 1, 0..5, 1, "The number of ticks to add to the break time, usually to account for server lag", visibility = vis).group(groupPath, Group.General) + override val serverSwapTicks by c.setting("Server Swap", 2, 0..5, 1, "The number of ticks to give the server time to recognize the player attributes on the swapped item", " tick(s)", visibility = vis).group(groupPath, Group.General) // override val desyncFix by c.setting("Desync Fix", false, "Predicts if the players breaking will be slowed next tick as block break packets are processed using the players next position") { vis() && page == Page.General } - override val breakDelay by c.setting("Break Delay", 0, 0..6, 1, "The delay between breaking blocks", " ticks", visibility = vis).group(groupPath, Group.General) + override val breakDelay by c.setting("Break Delay", 0, 0..6, 1, "The delay between breaking blocks", " tick(s)", visibility = vis).group(groupPath, Group.General) // Timing override val breakStageMask by c.setting("Break Stage Mask", setOf(TickEvent.Input.Post, TickEvent.Player.Post), description = "The sub-tick timing at which break actions can be performed", visibility = vis).group(groupPath, Group.General) 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 921a0b4cc..ea05b6026 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/BuildSimulator.kt @@ -38,7 +38,6 @@ 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.breaking.BreakConfig -import com.lambda.interaction.request.breaking.BreakManager import com.lambda.interaction.request.inventory.InventoryConfig import com.lambda.interaction.request.placing.PlaceConfig import com.lambda.interaction.request.rotating.Rotation.Companion.rotation @@ -850,7 +849,6 @@ object BuildSimulator { val swapStack = swapCandidates.map { it.matchingStacks(stackSelection) } .asSequence() .flatten() - .filter { BreakManager.currentStackSelection.filterStack(it) } .let { containerStacks -> var bestStack = ItemStack.EMPTY var bestBreakDelta = -1f 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 7d95dfc26..275944380 100644 --- a/src/main/kotlin/com/lambda/interaction/request/breaking/BreakConfig.kt +++ b/src/main/kotlin/com/lambda/interaction/request/breaking/BreakConfig.kt @@ -28,13 +28,14 @@ import java.awt.Color interface BreakConfig : RequestConfig { val breakMode: BreakMode val sorter: SortMode - val breakThreshold: Float val rebreak: Boolean val doubleBreak: Boolean val unsafeCancels: Boolean + val breakThreshold: Float val fudgeFactor: Int + val serverSwapTicks: Int //ToDo: Needs a more advanced player simulation implementation to predict the next ticks onGround / submerged status // abstract val desyncFix: Boolean val breakDelay: Int 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 efa8f4a90..c76542d78 100644 --- a/src/main/kotlin/com/lambda/interaction/request/breaking/BreakInfo.kt +++ b/src/main/kotlin/com/lambda/interaction/request/breaking/BreakInfo.kt @@ -19,6 +19,8 @@ package com.lambda.interaction.request.breaking import com.lambda.interaction.construction.context.BreakContext import com.lambda.interaction.request.ActionInfo +import com.lambda.interaction.request.breaking.BreakInfo.BreakType.Primary +import com.lambda.interaction.request.breaking.BreakInfo.BreakType.Rebreak import com.lambda.util.BlockUtils.calcItemBlockBreakingDelta import com.lambda.util.Describable import com.lambda.util.NamedEnum @@ -31,7 +33,6 @@ import net.minecraft.entity.player.PlayerEntity import net.minecraft.item.ItemStack import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket.Action -import net.minecraft.world.BlockView data class BreakInfo( override var context: BreakContext, @@ -44,10 +45,9 @@ data class BreakInfo( // Pre Processing var shouldProgress = false - var couldReBreak by OneSetPerTick(value = RebreakManager.RebreakPotential.None, throwOnLimitBreach = true) - var shouldSwap by OneSetPerTick(value = false, throwOnLimitBreach = true) + var rebreakPotential by OneSetPerTick(value = RebreakManager.RebreakPotential.None, throwOnLimitBreach = true) + var swapInfo by OneSetPerTick(value = SwapInfo.EMPTY, throwOnLimitBreach = true) var swapStack: ItemStack by OneSetPerTick(ItemStack.EMPTY, true) - var minSwapTicks by OneSetPerTick(0, true) // BreakInfo Specific var updatedThisTick by OneSetPerTick(false, resetAfterTick = true).apply { set(true) } @@ -60,7 +60,7 @@ data class BreakInfo( var breakingTicks by OneSetPerTick(0, true) var soundsCooldown by OneSetPerTick(0f, true) var vanillaInstantBreakable = false - val rebreakable get() = !vanillaInstantBreakable && type == BreakType.Primary + val rebreakable get() = !vanillaInstantBreakable && type == Primary enum class BreakType( override val displayName: String, @@ -70,12 +70,6 @@ data class BreakInfo( Secondary("Secondary", "A second block broken at the same time (when double‑break is enabled)."), RedundantSecondary("Redundant Secondary", "A previously started secondary break that’s now ignored/monitored only (no new actions)."), Rebreak("Rebreak", "A previously broken block which new breaks in the same position can compound progression on. Often rebreaking instantly."); - - fun getBreakThreshold(breakConfig: BreakConfig) = - when (this) { - Primary -> breakConfig.breakThreshold - else -> 1.0f - } } // Post Processing @@ -113,20 +107,6 @@ data class BreakInfo( item = null } - fun shouldSwap(player: ClientPlayerEntity, world: BlockView): Boolean { - val breakDelta = context.cachedState.calcItemBlockBreakingDelta(player, world, context.blockPos, swapStack) - val breakProgress = breakDelta * (breakingTicks + 1) - return if (couldReBreak == RebreakManager.RebreakPotential.Instant) - breakConfig.swapMode.isEnabled() - else when (breakConfig.swapMode) { - BreakConfig.SwapMode.None -> false - BreakConfig.SwapMode.Start -> !breaking - BreakConfig.SwapMode.End -> breakProgress >= getBreakThreshold() - BreakConfig.SwapMode.StartAndEnd -> !breaking || breakProgress >= getBreakThreshold() - BreakConfig.SwapMode.Constant -> true - } - } - fun setBreakingTextureStage( player: ClientPlayerEntity, world: ClientWorld, @@ -144,7 +124,12 @@ data class BreakInfo( return if (progress > 0.0f) (progress * 10.0f).toInt().coerceAtMost(9) else -1 } - fun getBreakThreshold() = type.getBreakThreshold(breakConfig) + fun getBreakThreshold() = + when (type) { + Primary, + Rebreak-> breakConfig.breakThreshold + else -> 1.0f + } fun startBreakPacket(world: ClientWorld, interaction: ClientPlayerInteractionManager) = breakPacket(Action.START_DESTROY_BLOCK, world, interaction) 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 1891c13e8..917b5342c 100644 --- a/src/main/kotlin/com/lambda/interaction/request/breaking/BreakManager.kt +++ b/src/main/kotlin/com/lambda/interaction/request/breaking/BreakManager.kt @@ -65,6 +65,7 @@ import com.lambda.interaction.request.breaking.BrokenBlockHandler.destroyBlock import com.lambda.interaction.request.breaking.BrokenBlockHandler.pendingActions import com.lambda.interaction.request.breaking.BrokenBlockHandler.setPendingConfigs import com.lambda.interaction.request.breaking.BrokenBlockHandler.startPending +import com.lambda.interaction.request.breaking.SwapInfo.Companion.getSwapInfo import com.lambda.interaction.request.interacting.InteractionManager import com.lambda.interaction.request.placing.PlaceManager import com.lambda.interaction.request.rotating.RotationRequest @@ -138,15 +139,15 @@ object BreakManager : RequestHandler( private var rotationRequest: RotationRequest? = null private val rotated get() = rotationRequest?.done != false - var swappedThisTick = false - var heldTicks = 0 - var swappedStack: ItemStack = ItemStack.EMPTY + var currentStack: ItemStack = ItemStack.EMPTY set(value) { if (value != field) heldTicks = 0 swappedThisTick = true field = value } + var heldTicks = 0 + var swappedThisTick = false private var breakCooldown = 0 var breaksThisTick = 0 private var maxBreaksThisTick = 0 @@ -173,7 +174,7 @@ object BreakManager : RequestHandler( listen(priority = Int.MIN_VALUE) { if (!swappedThisTick) { - swappedStack = player.mainHandStack + currentStack = player.mainHandStack } swappedThisTick = false heldTicks++ @@ -314,9 +315,12 @@ object BreakManager : RequestHandler( * @see updateBreakProgress */ private fun SafeContext.processRequest(breakRequest: BreakRequest?) { + breakRequest?.let { request -> + if (request.fresh) populateFrom(request) + } + repeat(2) { breakRequest?.let { request -> - if (request.fresh) populateFrom(request) if (performInstantBreaks(request)) { processNewBreaks(request) } @@ -421,12 +425,12 @@ object BreakManager : RequestHandler( infos.forEach { it.updatePreProcessing(player, world) } infos.firstOrNull()?.let { info -> - infos.lastOrNull { it.shouldSwap && it.shouldProgress }?.let { last -> - val minSwapTicks = max(info.minSwapTicks, last.minSwapTicks) + infos.lastOrNull { it.swapInfo.swap && it.shouldProgress }?.let { last -> + val minSwapTicks = max(info.swapInfo.minKeepTicks, last.swapInfo.minKeepTicks) if (!info.context.requestSwap(info.request, minSwapTicks)) return false if (minSwapTicks > 0) - swappedStack = info.swapStack + currentStack = info.swapStack } } } @@ -603,31 +607,8 @@ object BreakManager : RequestHandler( updatedPreProcessingThisTick = true swapStack = player.inventory.getStack(context.hotbarIndex) - couldReBreak = RebreakManager.couldRebreak(this, player, world) - shouldSwap = shouldSwap(player, world) - - val cachedState = context.cachedState - - val breakTicks = (breakingTicks + 1 - breakConfig.fudgeFactor).coerceAtLeast(1) - val breakAmount = cachedState.calcBreakDelta( - player, - world, - context.blockPos, - breakConfig, - swapStack - ) * breakTicks - val breakAmountNoEfficiency = cachedState.calcBreakDelta( - player, - world, - context.blockPos, - breakConfig, - swapStack, - ignoreEfficiency = true - ) * breakTicks - - minSwapTicks = if ((breakAmount >= getBreakThreshold() || couldReBreak == RebreakManager.RebreakPotential.Instant) && - (breakAmountNoEfficiency < getBreakThreshold() || type == Secondary)) 1 - else 0 + rebreakPotential = RebreakManager.getRebreakPotential(this, player, world) + swapInfo = getSwapInfo(this, player, world) } /** @@ -737,8 +718,6 @@ object BreakManager : RequestHandler( config ) * (info.breakingTicks - config.fudgeFactor) - val overBreakThreshold = progress >= info.getBreakThreshold() - if (config.sounds) { if (info.soundsCooldown % 4.0f == 0.0f) { val blockSoundGroup = blockState.soundGroup @@ -765,7 +744,7 @@ object BreakManager : RequestHandler( } val swing = config.swing - if (overBreakThreshold && heldTicks + 1 >= info.breakConfig.fudgeFactor) { + if (progress >= info.getBreakThreshold() && info.swapInfo.canCompleteBreak) { if (info.type == Primary) { onBlockBreak(info) info.stopBreakPacket(world, interaction) @@ -791,7 +770,7 @@ object BreakManager : RequestHandler( private fun SafeContext.startBreaking(info: BreakInfo): Boolean { val ctx = info.context - if (info.couldReBreak.isPossible()) { + if (info.rebreakPotential.isPossible()) { when (val rebreakResult = RebreakManager.handleUpdate(info.context, info.request)) { is RebreakResult.StillBreaking -> { primaryBreak = rebreakResult.breakInfo.apply { @@ -801,6 +780,7 @@ object BreakManager : RequestHandler( } primaryBreak?.let { primary -> + if (!handlePreProcessing()) return false updateBreakProgress(primary) } return true @@ -832,14 +812,13 @@ object BreakManager : RequestHandler( lastPosStarted = ctx.blockPos val blockState = blockState(ctx.blockPos) - val notEmpty = blockState.isNotEmpty - if (notEmpty && info.breakingTicks == 0) { + if (info.breakingTicks == 0) { blockState.onBlockBreakStart(world, ctx.blockPos, player) } - val breakDelta = blockState.calcBreakDelta(player, world, ctx.blockPos, info.breakConfig) - info.vanillaInstantBreakable = breakDelta >= 1 - if (notEmpty && breakDelta >= info.getBreakThreshold() && heldTicks + 1 >= info.breakConfig.fudgeFactor) { + val progress = blockState.calcBreakDelta(player, world, ctx.blockPos, info.breakConfig) + info.vanillaInstantBreakable = progress >= 1 && info.swapInfo.canCompleteBreak + if (progress >= info.getBreakThreshold() && info.swapInfo.canCompleteBreak) { onBlockBreak(info) if (!info.vanillaInstantBreakable) breakCooldown = info.breakConfig.breakDelay } else { @@ -859,7 +838,7 @@ object BreakManager : RequestHandler( info.startBreakPacket(world, interaction) - if (info.type == Secondary || (!info.vanillaInstantBreakable && breakDelta >= info.breakConfig.breakThreshold)) { + if (info.type == Secondary || (!info.vanillaInstantBreakable && progress >= info.breakConfig.breakThreshold)) { info.stopBreakPacket(world, interaction) } diff --git a/src/main/kotlin/com/lambda/interaction/request/breaking/RebreakManager.kt b/src/main/kotlin/com/lambda/interaction/request/breaking/RebreakManager.kt index 368214676..be5edcb56 100644 --- a/src/main/kotlin/com/lambda/interaction/request/breaking/RebreakManager.kt +++ b/src/main/kotlin/com/lambda/interaction/request/breaking/RebreakManager.kt @@ -35,7 +35,7 @@ object RebreakManager { var rebreak: BreakInfo? = null init { - listen(priority = Int.MIN_VALUE) { + listen(priority = Int.MIN_VALUE + 1) { rebreak?.run { if (!progressedThisTick) { breakingTicks++ @@ -64,7 +64,7 @@ object RebreakManager { rebreak = null } - fun couldRebreak(info: BreakInfo, player: ClientPlayerEntity, world: BlockView) = + fun getRebreakPotential(info: BreakInfo, player: ClientPlayerEntity, world: BlockView) = rebreak?.let { reBreak -> val stack = if (info.breakConfig.swapMode.isEnabled()) info.swapStack @@ -89,7 +89,8 @@ object RebreakManager { val context = reBreak.context val breakDelta = context.cachedState.calcBreakDelta(player, world, context.blockPos, reBreak.breakConfig) - return@runSafe if ((reBreak.breakingTicks - reBreak.breakConfig.fudgeFactor) * breakDelta >= reBreak.breakConfig.breakThreshold) { + val breakTicks = reBreak.breakingTicks - reBreak.breakConfig.fudgeFactor + return@runSafe if (breakTicks * breakDelta >= reBreak.getBreakThreshold() && reBreak.swapInfo.canCompleteBreak) { if (reBreak.breakConfig.breakConfirmation != BreakConfig.BreakConfirmationMode.AwaitThenBreak) { destroyBlock(reBreak) } @@ -109,6 +110,6 @@ object RebreakManager { PartialProgress, None; - fun isPossible() = this == Instant || this == PartialProgress + fun isPossible() = this != None } } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/request/breaking/SwapInfo.kt b/src/main/kotlin/com/lambda/interaction/request/breaking/SwapInfo.kt new file mode 100644 index 000000000..53e2770b4 --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/request/breaking/SwapInfo.kt @@ -0,0 +1,94 @@ +/* + * 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.request.breaking + +import com.lambda.interaction.request.breaking.BreakInfo.BreakType.Primary +import com.lambda.interaction.request.breaking.BreakInfo.BreakType.Rebreak +import com.lambda.interaction.request.breaking.BreakManager.currentStack +import com.lambda.module.modules.client.TaskFlowModule +import com.lambda.util.BlockUtils.calcItemBlockBreakingDelta +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.world.BlockView + +data class SwapInfo( + val type: BreakInfo.BreakType, + val breakConfig: BreakConfig = TaskFlowModule.build.breaking, + val swap: Boolean = false, + val minKeepTicks: Int = 0, +) { + val canCompleteBreak + get() = (BreakManager.heldTicks + 1) >= if (type == Primary || type == Rebreak) breakConfig.serverSwapTicks + else breakConfig.serverSwapTicks.coerceAtLeast(3) + + companion object { + val EMPTY = SwapInfo(Primary) + + fun getSwapInfo( + info: BreakInfo, + player: ClientPlayerEntity, + world: BlockView + ): SwapInfo = with(info) { + val breakDelta = context.cachedState + .calcItemBlockBreakingDelta(player, world, context.blockPos, swapStack) + val breakDeltaNoEfficiency = context.cachedState + .calcItemBlockBreakingDelta(player, world, context.blockPos, swapStack, ignoreEfficiency = true) + val breakTicks = (if (rebreakPotential.isPossible()) RebreakManager.rebreak?.breakingTicks + ?: throw IllegalStateException("Rebreak BreakInfo was null when rebreak was considered possible") + else breakingTicks).let { + // Plus one as this is calculated before this ticks progress is calculated and the breakingTicks are incremented + (it + 1) - breakConfig.fudgeFactor + } + val threshold = getBreakThreshold() + + val minKeepTicks = run { + if (type == Primary) { + val swapTickProgress = breakDelta * (breakTicks + breakConfig.serverSwapTicks - 1) + val withoutEfficiency = breakDeltaNoEfficiency * breakTicks >= threshold + if (swapTickProgress >= threshold && + !withoutEfficiency) 1 + else 0 + } else { + val serverSwapTicks = breakConfig.serverSwapTicks.coerceAtLeast(3) + val swapTickProgress = breakDelta * (breakTicks + serverSwapTicks - 1) + if (swapTickProgress >= threshold && swapStack.heldTicks < serverSwapTicks) 1 + else 0 + } + } + + val swapAtEnd = breakDelta * breakTicks >= threshold || minKeepTicks > 0 + + val swap = if (rebreakPotential == RebreakManager.RebreakPotential.Instant) + breakConfig.swapMode.isEnabled() + else when (breakConfig.swapMode) { + BreakConfig.SwapMode.None -> false + BreakConfig.SwapMode.Start -> !breaking + BreakConfig.SwapMode.End -> swapAtEnd + BreakConfig.SwapMode.StartAndEnd -> !breaking || swapAtEnd + BreakConfig.SwapMode.Constant -> true + } + + return SwapInfo(info.type, breakConfig, swap, minKeepTicks) + } + + private val ItemStack.heldTicks + get() = if (currentStack == this) + BreakManager.heldTicks + else 0 + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt b/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt index ccb117983..5b71300df 100644 --- a/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt +++ b/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt @@ -29,7 +29,6 @@ import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStruc 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.BreakContext import com.lambda.interaction.construction.context.BuildContext import com.lambda.interaction.construction.result.BreakResult import com.lambda.interaction.construction.result.BuildResult @@ -140,24 +139,14 @@ class BuildTask @Ta5kBuilder constructor( if (atMaxPendingInteractions) return@listen when (bestResult) { is BreakResult.Break -> { - val breakResults = resultsNotBlocked.filterIsInstance() - val requestContexts = arrayListOf() - - if (build.breaking.breaksPerTick > 1) { - breakResults - .filter { it.context.instantBreak } - .take(emptyPendingInteractionSlots) - .let { instantBreakResults -> - requestContexts.addAll(instantBreakResults.map { it.context }) - } - } - - if (requestContexts.isEmpty()) { - requestContexts.addAll(breakResults.map { it.context }) - } + val breakResults = resultsNotBlocked + .filterIsInstance() + .distinctBy { it.blockPos } + .take(emptyPendingInteractionSlots) + .map { it.context } breakRequest( - requestContexts, pendingInteractions, rotation, hotbar, interactionConfig, inventory, build, + breakResults, pendingInteractions, rotation, hotbar, interactionConfig, inventory, build, ) { onStop { breaks++ } onItemDrop?.let { onItemDrop -> @@ -171,8 +160,9 @@ class BuildTask @Ta5kBuilder constructor( .filterIsInstance() .distinctBy { it.blockPos } .take(emptyPendingInteractionSlots) + .map { it.context } - PlaceRequest(placeResults.map { it.context }, build, rotation, hotbar, pendingInteractions) { placements++ }.submit() + PlaceRequest(placeResults, build, rotation, hotbar, pendingInteractions) { placements++ }.submit() } is InteractResult.Interact -> { val interactResults = resultsNotBlocked