Skip to content

Commit b98ed2b

Browse files
committed
30bps placements
1 parent e194c92 commit b98ed2b

12 files changed

Lines changed: 290 additions & 133 deletions

File tree

src/main/kotlin/com/lambda/config/groups/BuildConfig.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ interface BuildConfig : ISettingGroup {
3434
val actionTimeout: Int
3535
val maxBuildDependencies: Int
3636

37+
val limitTimeframe: Int
38+
val actionPacketLimit: Int
39+
val interactionPacketLimit: Int
40+
3741
val blockReach: Double
3842
val entityReach: Double
3943
val scanReach: Double

src/main/kotlin/com/lambda/config/groups/BuildSettings.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class BuildSettings(
3131
) : SettingGroup(c), BuildConfig {
3232
enum class Group(override val displayName: String) : NamedEnum {
3333
General("General"),
34+
PacketLimits("Packet Limits"),
3435
Reach("Reach"),
3536
Scan("Scan")
3637
}
@@ -46,6 +47,10 @@ class BuildSettings(
4647
override val actionTimeout by c.setting("${prefix}Action Timeout", 10, 1..30, 1, "Timeout for block breaks in ticks", unit = " ticks", visibility = visibility).group(*baseGroup, Group.General).index()
4748
override val maxBuildDependencies by c.setting("${prefix}Max Sim Dependencies", 3, 0..10, 1, "Maximum dependency build results", visibility = visibility).group(*baseGroup, Group.General).index()
4849

50+
override val limitTimeframe by c.setting("${prefix}Limit Timeframe", 6, 1..30, 1, "The timeframe in which the limit is bound to", "ticks", visibility = visibility).group(*baseGroup, Group.PacketLimits).index()
51+
override val actionPacketLimit by c.setting("${prefix}Action Packet Limit", 55, 1..100, 1, "The maximum allowed action packets to be sent to the server per given timeframe", visibility = visibility).group(*baseGroup, Group.PacketLimits).index()
52+
override val interactionPacketLimit by c.setting("Interaction Limit", 9, 1..20, 1, "The maximum allowed interaction packets to be sent to the server per given timeframe", visibility = visibility).group(*baseGroup, Group.PacketLimits).index()
53+
4954
override var blockReach by c.setting("${prefix}Interact Reach", 4.5, 1.0..7.0, 0.01, "Maximum block interaction distance", visibility = visibility).group(*baseGroup, Group.Reach).index()
5055
override var entityReach by c.setting("${prefix}Attack Reach", 3.0, 1.0..7.0, 0.01, "Maximum entity interaction distance", visibility = visibility).group(*baseGroup, Group.Reach).index()
5156
override val scanReach: Double get() = max(entityReach, blockReach)

src/main/kotlin/com/lambda/config/groups/InteractSettings.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class InteractSettings(
3939
override val tickStageMask by c.setting("${prefix}Interaction Stage Mask", setOf(TickEvent.Input.Post), ALL_STAGES.toSet(), "The sub-tick timing at which place actions are performed", displayClassName = true, visibility = visibility).group(*baseGroup).index()
4040
override val interactConfirmationMode by c.setting("${prefix}Interact Confirmation", InteractConfirmationMode.PlaceThenAwait, "Wait for block placement confirmation", visibility = visibility).group(*baseGroup).index()
4141
override val interactDelay by c.setting("${prefix}Interact Delay", 0, 0..3, 1, "Tick delay between interacting with another block", visibility = visibility).group(*baseGroup).index()
42-
override val interactionsPerTick by c.setting("${prefix}Interactions Per Tick", 1, 1..30, 1, "Maximum instant block places per tick", visibility = visibility).group(*baseGroup).index()
42+
override val interactionsPerTick by c.setting("${prefix}Interactions Per Tick", 9, 1..30, 1, "Maximum instant block places per tick", visibility = visibility).group(*baseGroup).index()
4343
override val swing by c.setting("${prefix}Swing On Interact", true, "Swings the players hand when placing", visibility = visibility).group(*baseGroup).index()
4444
override val swingType by c.setting("${prefix}Interact Swing Type", BuildConfig.SwingType.Vanilla, "The style of swing") { visibility() && swing }.group(*baseGroup).index()
4545
override val sounds by c.setting("${prefix}Place Sounds", true, "Plays the placing sounds", visibility = visibility).group(*baseGroup).index()

src/main/kotlin/com/lambda/interaction/construction/simulation/context/InteractContext.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,9 @@ data class InteractContext(
6060
}
6161

6262
fun requestDependencies(request: InteractRequest): Boolean {
63-
val hotbarRequest = HotbarRequest(hotbarIndex, this).submit(queueIfMismatchedStage = false)
6463
val validRotation = if (request.interactConfig.rotate) {
6564
(rotationRequest.submit(queueIfMismatchedStage = false).done || interactConfig.airPlace.isEnabled) && currentDirIsValid
6665
} else true
67-
return hotbarRequest.done && validRotation
66+
return validRotation
6867
}
6968
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2026 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.interaction.managers
19+
20+
import com.lambda.config.groups.BuildConfig
21+
import com.lambda.context.Automated
22+
import com.lambda.event.events.TickEvent
23+
import com.lambda.event.listener.SafeListener.Companion.listen
24+
import com.lambda.util.TickTimer
25+
26+
object PacketLimitHandler {
27+
private val packetLimitMap = PacketType.entries.associateWith { LimitHandler(0, 0, 0) }
28+
29+
init {
30+
listen<TickEvent.Pre>(priority = { Int.MIN_VALUE }) {
31+
packetLimitMap.values.forEach { limitHandler ->
32+
with(limitHandler) {
33+
tickTimer.tick()
34+
if (tickTimer.hasSurpassed(timeframe)) {
35+
packetsThisTimeframe = 0
36+
tickTimer.reset()
37+
}
38+
}
39+
}
40+
}
41+
}
42+
43+
context(automated: Automated)
44+
fun canSendPackets(packetCount: Int, packetType: PacketType) =
45+
packetLimitMap[packetType]?.let {
46+
it.maxPacketsThisTimeframe = packetType.maxPacketsPerTimeframe(automated.buildConfig)
47+
it.timeframe = automated.buildConfig.limitTimeframe
48+
it.packetsThisTimeframe + packetCount <= it.maxPacketsThisTimeframe
49+
} ?: false
50+
51+
fun sentPackets(packetCount: Int, packetType: PacketType) =
52+
packetLimitMap[packetType]?.let { limitHandler ->
53+
if (limitHandler.packetsThisTimeframe == 0) limitHandler.tickTimer.reset()
54+
limitHandler.packetsThisTimeframe += packetCount
55+
}
56+
57+
private data class LimitHandler(var maxPacketsThisTimeframe: Int, var packetsThisTimeframe: Int, var timeframe: Int) {
58+
val tickTimer = TickTimer()
59+
}
60+
}
61+
62+
enum class PacketType(val maxPacketsPerTimeframe: BuildConfig.() -> Int) {
63+
PlayerAction({ actionPacketLimit }),
64+
Interaction({ interactionPacketLimit })
65+
}

src/main/kotlin/com/lambda/interaction/managers/breaking/BreakManager.kt

Lines changed: 93 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import com.lambda.interaction.construction.simulation.result.results.BreakResult
3333
import com.lambda.interaction.construction.verify.TargetState
3434
import com.lambda.interaction.managers.Manager
3535
import com.lambda.interaction.managers.ManagerUtils.isPosBlocked
36+
import com.lambda.interaction.managers.PacketLimitHandler
37+
import com.lambda.interaction.managers.PacketType
3638
import com.lambda.interaction.managers.PositionBlocking
3739
import com.lambda.interaction.managers.breaking.BreakConfig.BreakConfirmationMode
3840
import com.lambda.interaction.managers.breaking.BreakConfig.BreakMode
@@ -496,6 +498,7 @@ object BreakManager : Manager<BreakRequest>(
496498
val breakInfo = BreakInfo(requestCtx, Primary, request)
497499
primaryBreak?.let { primaryInfo ->
498500
if (tickStage !in primaryInfo.breakConfig.tickStageMask) return null
501+
if (!PacketLimitHandler.canSendPackets(1, PacketType.PlayerAction)) return null
499502

500503
if (!primaryInfo.breakConfig.doubleBreak || secondaryBreak != null) {
501504
if (!primaryInfo.updatedThisTick) {
@@ -508,6 +511,7 @@ object BreakManager : Manager<BreakRequest>(
508511

509512
secondaryBreak = primaryInfo.apply { type = Secondary }
510513
secondaryBreak?.stopBreakPacket()
514+
PacketLimitHandler.sentPackets(1, PacketType.PlayerAction)
511515
return@let
512516
}
513517

@@ -625,13 +629,17 @@ object BreakManager : Manager<BreakRequest>(
625629
* If not, the break continues.
626630
*/
627631
context(safeContext: SafeContext)
628-
private fun BreakInfo.cancelBreak() = with(safeContext) {
629-
if (type == RedundantSecondary || abandoned) return@with
632+
private fun BreakInfo.cancelBreak() = with(safeContext) safeContext@{
633+
if (type == RedundantSecondary || abandoned) return@safeContext
630634
when (type) {
631635
Primary -> {
636+
with(request) {
637+
if (!PacketLimitHandler.canSendPackets(1, PacketType.PlayerAction)) return@safeContext
638+
}
632639
nullify()
633640
setBreakingTextureStage(player, world, -1)
634641
abortBreakPacket()
642+
PacketLimitHandler.sentPackets(1, PacketType.PlayerAction)
635643
request.onCancel?.invoke(this, context.blockPos)
636644
}
637645
Secondary -> {
@@ -658,83 +666,82 @@ object BreakManager : Manager<BreakRequest>(
658666
*
659667
* @see net.minecraft.client.network.ClientPlayerInteractionManager.updateBlockBreakingProgress
660668
*/
661-
private fun SafeContext.updateBreakProgress(info: BreakInfo): Unit = info.request.runSafeAutomated {
662-
val ctx = info.context
669+
private fun SafeContext.updateBreakProgress(info: BreakInfo): Unit =
670+
info.request.runSafeAutomated {
671+
val ctx = info.context
672+
info.progressedThisTick = true
673+
674+
if (!info.breaking) {
675+
if (info.swapInfo.swap && !swapped) return
676+
if (!startBreaking(info)) {
677+
info.nullify()
678+
info.request.onCancel?.invoke(this, ctx.blockPos)
679+
}
680+
return
681+
}
663682

664-
info.progressedThisTick = true
683+
val hitResult = ctx.hitResult
665684

666-
if (!info.breaking) {
667-
if (info.swapInfo.swap && !swapped) return
668-
if (!startBreaking(info)) {
685+
if (gamemode.isCreative && world.worldBorder.contains(ctx.blockPos)) {
686+
if (!PacketLimitHandler.canSendPackets(1, PacketType.PlayerAction)) return
687+
breakCooldown = breakConfig.breakDelay
688+
lastPosStarted = ctx.blockPos
689+
onBlockBreak(info)
690+
info.startBreakPacket()
691+
PacketLimitHandler.sentPackets(1, PacketType.PlayerAction)
692+
if (breakConfig.swing.isEnabled()) swingHand(breakConfig.swingType, Hand.MAIN_HAND)
693+
return
694+
}
695+
696+
val blockState = blockState(ctx.blockPos)
697+
if (blockState.isEmpty) {
669698
info.nullify()
670699
info.request.onCancel?.invoke(this, ctx.blockPos)
700+
return
671701
}
672-
return
673-
}
674-
675-
val hitResult = ctx.hitResult
676702

677-
if (gamemode.isCreative && world.worldBorder.contains(ctx.blockPos)) {
678-
breakCooldown = breakConfig.breakDelay
679-
lastPosStarted = ctx.blockPos
680-
onBlockBreak(info)
681-
info.startBreakPacket()
682-
if (breakConfig.swing.isEnabled()) swingHand(breakConfig.swingType, Hand.MAIN_HAND)
683-
return
684-
}
685-
686-
val blockState = blockState(ctx.blockPos)
687-
if (blockState.isEmpty) {
688-
info.nullify()
689-
info.request.onCancel?.invoke(this, ctx.blockPos)
690-
return
691-
}
692-
693-
if (breakConfig.swapMode == BreakConfig.SwapMode.Constant && !swapped) return
694-
695-
info.breakingTicks++
696-
val breakDelta = blockState.calcBreakDelta(ctx.blockPos)
697-
val progress = breakDelta * (info.breakingTicks - breakConfig.fudgeFactor)
698-
699-
if (breakConfig.sounds) {
700-
if (info.soundsCooldown % 4.0f == 0.0f) {
701-
val blockSoundGroup = blockState.soundGroup
702-
mc.soundManager.play(
703-
PositionedSoundInstance(
704-
blockSoundGroup.hitSound,
705-
SoundCategory.BLOCKS,
706-
(blockSoundGroup.getVolume() + 1.0f) / 8.0f,
707-
blockSoundGroup.getPitch() * 0.5f,
708-
SoundInstance.createRandom(),
709-
ctx.blockPos
703+
if (breakConfig.swapMode == BreakConfig.SwapMode.Constant && !swapped) return
704+
705+
info.breakingTicks++
706+
val breakDelta = blockState.calcBreakDelta(ctx.blockPos)
707+
val progress = breakDelta * (info.breakingTicks - breakConfig.fudgeFactor)
708+
709+
if (breakConfig.sounds) {
710+
if (info.soundsCooldown % 4.0f == 0.0f) {
711+
val blockSoundGroup = blockState.soundGroup
712+
mc.soundManager.play(
713+
PositionedSoundInstance(
714+
blockSoundGroup.hitSound,
715+
SoundCategory.BLOCKS,
716+
(blockSoundGroup.getVolume() + 1.0f) / 8.0f,
717+
blockSoundGroup.getPitch() * 0.5f,
718+
SoundInstance.createRandom(),
719+
ctx.blockPos
720+
)
710721
)
711-
)
722+
}
723+
info.soundsCooldown++
712724
}
713-
info.soundsCooldown++
714-
}
715725

716-
if (breakConfig.particles) {
717-
world.spawnBlockBreakingParticle(ctx.blockPos, hitResult.side)
718-
}
719-
720-
if (breakConfig.breakingTexture) {
721-
info.setBreakingTextureStage(player, world)
722-
}
726+
if (breakConfig.particles) world.spawnBlockBreakingParticle(ctx.blockPos, hitResult.side)
727+
if (breakConfig.breakingTexture) info.setBreakingTextureStage(player, world)
723728

724-
val swing = breakConfig.swing
725-
if (progress >= info.getBreakThreshold()) {
726-
if (info.swapInfo.swap && !swapped) return
729+
val swing = breakConfig.swing
730+
if (progress >= info.getBreakThreshold()) {
731+
if (info.swapInfo.swap && !swapped) return
732+
if (info.type == Primary && !PacketLimitHandler.canSendPackets(1, PacketType.PlayerAction)) return
727733

728-
onBlockBreak(info)
729-
if (info.type == Primary) info.stopBreakPacket()
730-
if (swing.isEnabled() && swing != BreakConfig.SwingMode.Start)
731-
swingHand(breakConfig.swingType, Hand.MAIN_HAND)
732-
breakCooldown = breakConfig.breakDelay
733-
} else {
734-
if (swing == BreakConfig.SwingMode.Constant)
735-
swingHand(breakConfig.swingType, Hand.MAIN_HAND)
734+
onBlockBreak(info)
735+
if (info.type == Primary) {
736+
info.stopBreakPacket()
737+
PacketLimitHandler.sentPackets(1, PacketType.PlayerAction)
738+
}
739+
if (swing.isEnabled() && swing != BreakConfig.SwingMode.Start) swingHand(breakConfig.swingType, Hand.MAIN_HAND)
740+
breakCooldown = breakConfig.breakDelay
741+
} else {
742+
if (swing == BreakConfig.SwingMode.Constant) swingHand(breakConfig.swingType, Hand.MAIN_HAND)
743+
}
736744
}
737-
}
738745

739746
/**
740747
* A modified version of the minecraft attackBlock method.
@@ -744,6 +751,7 @@ object BreakManager : Manager<BreakRequest>(
744751
* @see net.minecraft.client.network.ClientPlayerInteractionManager.attackBlock
745752
*/
746753
private fun AutomatedSafeContext.startBreaking(info: BreakInfo): Boolean {
754+
if (!PacketLimitHandler.canSendPackets(1, PacketType.PlayerAction)) return false
747755
val ctx = info.context
748756

749757
if (info.rebreakPotential.isPossible()) {
@@ -779,49 +787,51 @@ object BreakManager : Manager<BreakRequest>(
779787
info.request.onStart?.invoke(this, ctx.blockPos)
780788
onBlockBreak(info)
781789
info.startBreakPacket()
790+
PacketLimitHandler.sentPackets(1, PacketType.PlayerAction)
782791
breakCooldown = breakConfig.breakDelay
783792
if (breakConfig.swing.isEnabled()) swingHand(breakConfig.swingType, Hand.MAIN_HAND)
784793
return true
785794
}
786795
if (info.breaking) return false
796+
797+
val blockState = blockState(ctx.blockPos)
798+
val progress = blockState.calcBreakDelta(ctx.blockPos)
799+
val instantBreakable = progress >= info.getBreakThreshold()
800+
801+
var packetCount = 1
802+
if (breakConfig.breakMode == BreakMode.Packet) packetCount++
803+
val requiresSecondStop = info.type == Secondary || (instantBreakable && !info.vanillaInstantBreakable)
804+
if (requiresSecondStop) packetCount++
805+
806+
if (!PacketLimitHandler.canSendPackets(packetCount, PacketType.PlayerAction)) return false
807+
787808
info.request.onStart?.invoke(this, ctx.blockPos)
788809

789810
lastPosStarted = ctx.blockPos
790811

791-
val blockState = blockState(ctx.blockPos)
792812
if (info.breakingTicks == 0) {
793813
blockState.onBlockBreakStart(world, ctx.blockPos, player)
794814
}
795815

796-
val progress = blockState.calcBreakDelta(ctx.blockPos)
797-
798-
val instantBreakable = progress >= info.getBreakThreshold()
799816
if (instantBreakable) {
800817
info.vanillaInstantBreakable = progress >= 1
801818
onBlockBreak(info)
802819
val breakDelay = breakConfig.breakDelay
803-
if (!info.vanillaInstantBreakable)
804-
breakCooldown = if (breakDelay == 0) 0 else breakDelay + 1
820+
if (!info.vanillaInstantBreakable) breakCooldown = if (breakDelay == 0) 0 else breakDelay + 1
805821
} else {
806822
info.apply {
807823
breaking = true
808824
breakingTicks = 1
809825
soundsCooldown = 0.0f
810-
if (breakConfig.breakingTexture) {
811-
setBreakingTextureStage(player, world)
812-
}
826+
if (breakConfig.breakingTexture) setBreakingTextureStage(player, world)
813827
}
814828
}
815829

816-
if (breakConfig.breakMode == BreakMode.Packet) {
817-
info.stopBreakPacket()
818-
}
819-
830+
if (breakConfig.breakMode == BreakMode.Packet) info.stopBreakPacket()
820831
info.startBreakPacket()
832+
if (requiresSecondStop) info.stopBreakPacket()
821833

822-
if (info.type == Secondary || (instantBreakable && !info.vanillaInstantBreakable)) {
823-
info.stopBreakPacket()
824-
}
834+
PacketLimitHandler.sentPackets(packetCount, PacketType.PlayerAction)
825835

826836
if (breakConfig.swing.isEnabled() && (breakConfig.swing != BreakConfig.SwingMode.End || instantBreakable)) {
827837
swingHand(breakConfig.swingType, Hand.MAIN_HAND)

0 commit comments

Comments
 (0)