From 7f4573b85e8fe82bc42c2ff57b39373a894c5d3d Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Fri, 25 Jul 2025 11:50:42 -0400 Subject: [PATCH 01/29] scaffold_b.go: impl (open src since it just enforces vanilla) --- player/detection/register.go | 5 + player/detection/scaffold_b.go | 162 +++++++++++++++++++++++++++++++++ player/packet.go | 6 +- 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 player/detection/scaffold_b.go diff --git a/player/detection/register.go b/player/detection/register.go index 603b6c2b..e4623fd3 100644 --- a/player/detection/register.go +++ b/player/detection/register.go @@ -15,8 +15,13 @@ func Register(p *player.Player) { p.RegisterDetection(New_EditionFakerB(p)) p.RegisterDetection(New_EditionFakerC(p)) + // inv move detections p.RegisterDetection(New_InvMoveA(p)) + // scaffold detections + p.RegisterDetection(New_ScaffoldA(p)) + p.RegisterDetection(New_ScaffoldB(p)) + //p.RegisterDetection(New_NukerA(p)) // killaura detections diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go new file mode 100644 index 00000000..62c83404 --- /dev/null +++ b/player/detection/scaffold_b.go @@ -0,0 +1,162 @@ +package detection + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl32" + "github.com/oomph-ac/oomph/game" + "github.com/oomph-ac/oomph/player" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type ScaffoldB struct { + mPlayer *player.Player + metadata *player.DetectionMetadata + + initialFace cube.Face +} + +func New_ScaffoldB(p *player.Player) *ScaffoldB { + return &ScaffoldB{ + mPlayer: p, + metadata: &player.DetectionMetadata{ + MaxViolations: 10, + }, + initialFace: -1, + } +} + +func (d *ScaffoldB) Type() string { + return "Scaffold" +} + +func (d *ScaffoldB) SubType() string { + return "B" +} + +func (d *ScaffoldB) Description() string { + return "Checks if the block face the player is placing against is valid." +} + +func (d *ScaffoldB) Punishable() bool { + return true +} + +func (d *ScaffoldB) Metadata() *player.DetectionMetadata { + return d.metadata +} + +func (d *ScaffoldB) Detect(pk packet.Packet) { + tr, ok := pk.(*packet.InventoryTransaction) + if !ok { + return + } + + dat, ok := tr.TransactionData.(*protocol.UseItemTransactionData) + if !ok || dat.ActionType != protocol.UseItemActionClickBlock || dat.ClientPrediction == protocol.ClientPredictionFailure || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) { + return + } + + eyeOffset := game.DefaultPlayerHeightOffset + if d.mPlayer.Movement().Sneaking() { + eyeOffset = game.SneakingPlayerHeightOffset + } + prevEyePos := d.mPlayer.Movement().Client().LastPos() + currEyePos := d.mPlayer.Movement().Client().Pos() + prevEyePos[1] += eyeOffset + currEyePos[1] += eyeOffset + blockFace := cube.Face(dat.BlockFace) + if dat.TriggerType == protocol.TriggerTypePlayerInput { + d.initialFace = -1 + } else if d.initialFace == -1 && blockFace != cube.FaceDown && blockFace != cube.FaceUp { + d.initialFace = blockFace + } + blockPos := cube.Pos{int(dat.BlockPosition[0]), int(dat.BlockPosition[1]), int(dat.BlockPosition[2])} + if !d.isFaceInteractable(prevEyePos, currEyePos, blockPos, blockFace, dat.TriggerType == protocol.TriggerTypePlayerInput) { + d.mPlayer.FailDetection(d, nil) + } +} + +func (d *ScaffoldB) isFaceInteractable( + startPos, + endPos mgl32.Vec3, + blockPos cube.Pos, + targetFace cube.Face, + isClientInput bool, +) bool { + interactableFaces := make(map[cube.Face]struct{}, 6) + blockX, blockY, blockZ := blockPos[0], blockPos[1], blockPos[2] + + if !isClientInput { + interactableFaces[cube.FaceDown] = struct{}{} + interactableFaces[cube.FaceUp] = struct{}{} + interactableFaces[d.initialFace] = struct{}{} + interactableFaces[d.initialFace.Opposite()] = struct{}{} + } else { + yFloorStart := int(startPos[1]) + yFloorEnd := int(endPos[1]) + xFloorStart := int(startPos[0]) + xFloorEnd := int(endPos[0]) + zFloorStart := int(startPos[2]) + zFloorEnd := int(endPos[2]) + + // Check for the Y-axis faces first. + // If floor(eyePos.Y) < blockPos.Y -> the bottom face is interactable. + // If floor(eyePos.Y) > blockPos.Y -> the top face is interactable. + isBelowBlock := yFloorStart < blockY || yFloorEnd < blockY + isAboveBlock := yFloorStart > blockY || yFloorEnd > blockY + if isBelowBlock { + interactableFaces[cube.FaceDown] = struct{}{} + } + if isAboveBlock { + interactableFaces[cube.FaceUp] = struct{}{} + + startXDelta := game.AbsNum(xFloorStart - blockX) + endXDelta := game.AbsNum(xFloorEnd - blockX) + if startXDelta <= 1 || endXDelta <= 1 { + interactableFaces[cube.FaceWest] = struct{}{} + interactableFaces[cube.FaceEast] = struct{}{} + } + + startZDelta := game.AbsNum(zFloorStart - blockZ) + endZDelta := game.AbsNum(zFloorEnd - blockZ) + if startZDelta <= 1 || endZDelta <= 1 { + interactableFaces[cube.FaceNorth] = struct{}{} + interactableFaces[cube.FaceSouth] = struct{}{} + } + } + + // Check for the X-axis faces. + // If floor(eyePos.X) < blockPos.X -> the west face is interactable. + // If floor(eyePos.X) > blockPos.X -> the east face is interactable. + if (xFloorStart == blockX || xFloorEnd == blockX) && isBelowBlock { + interactableFaces[cube.FaceWest] = struct{}{} + interactableFaces[cube.FaceEast] = struct{}{} + } else { + if xFloorStart < blockX || xFloorEnd < blockX { + interactableFaces[cube.FaceWest] = struct{}{} + } + if xFloorStart > blockX || xFloorEnd > blockX { + interactableFaces[cube.FaceEast] = struct{}{} + } + } + + // Check for the Z-axis faces. + // If floor(eyePos.Z) < blockPos.Z -> the north face is interactable. + // If floor(eyePos.Z) > blockPos.Z -> the south face is interactable. + if (zFloorStart == blockZ || zFloorEnd == blockZ) && isBelowBlock { + interactableFaces[cube.FaceNorth] = struct{}{} + interactableFaces[cube.FaceSouth] = struct{}{} + } else { + if zFloorStart < blockZ || zFloorEnd < blockZ { + interactableFaces[cube.FaceNorth] = struct{}{} + } + if zFloorStart > blockZ || zFloorEnd > blockZ { + interactableFaces[cube.FaceSouth] = struct{}{} + } + } + } + + _, interactable := interactableFaces[targetFace] + return interactable +} diff --git a/player/packet.go b/player/packet.go index 41fa4e84..2d3da2d4 100644 --- a/player/packet.go +++ b/player/packet.go @@ -219,7 +219,11 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { } */ } else if tr.ActionType == protocol.UseItemActionBreakBlock && (p.GameMode == packet.GameTypeAdventure || p.GameMode == packet.GameTypeSurvival) { ctx.Cancel() - return + } else if tr.ActionType == protocol.UseItemActionClickBlock { + if p.VersionInRange(GameVersion1_21_20, protocol.CurrentProtocol) && tr.TriggerType != protocol.TriggerTypePlayerInput && tr.TriggerType != protocol.TriggerTypeSimulationTick { + p.Log().Debug("unknown trigger type", "triggerType", tr.TriggerType) + ctx.Cancel() + } } } else if tr, ok := pk.TransactionData.(*protocol.ReleaseItemTransactionData); ok { p.inventory.SetHeldSlot(int32(tr.HotBarSlot)) From 0372434f3d67a51e5e93ff148dfda416fbff75bd Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sat, 26 Jul 2025 16:30:50 -0400 Subject: [PATCH 02/29] detection/scaffold_b.go: fixes :) --- player/detection/scaffold_b.go | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index 62c83404..07ce8b74 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -1,7 +1,7 @@ package detection import ( - "github.com/df-mc/dragonfly/server/block/cube" + "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" "github.com/oomph-ac/oomph/game" "github.com/oomph-ac/oomph/player" @@ -9,6 +9,8 @@ import ( "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) +var faceNotSet cube.Face = -1 + type ScaffoldB struct { mPlayer *player.Player metadata *player.DetectionMetadata @@ -22,7 +24,7 @@ func New_ScaffoldB(p *player.Player) *ScaffoldB { metadata: &player.DetectionMetadata{ MaxViolations: 10, }, - initialFace: -1, + initialFace: faceNotSet, } } @@ -67,8 +69,8 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { currEyePos[1] += eyeOffset blockFace := cube.Face(dat.BlockFace) if dat.TriggerType == protocol.TriggerTypePlayerInput { - d.initialFace = -1 - } else if d.initialFace == -1 && blockFace != cube.FaceDown && blockFace != cube.FaceUp { + d.initialFace = faceNotSet + } else if d.initialFace == faceNotSet { d.initialFace = blockFace } blockPos := cube.Pos{int(dat.BlockPosition[0]), int(dat.BlockPosition[1]), int(dat.BlockPosition[2])} @@ -129,7 +131,7 @@ func (d *ScaffoldB) isFaceInteractable( // Check for the X-axis faces. // If floor(eyePos.X) < blockPos.X -> the west face is interactable. // If floor(eyePos.X) > blockPos.X -> the east face is interactable. - if (xFloorStart == blockX || xFloorEnd == blockX) && isBelowBlock { + /* if (xFloorStart == blockX || xFloorEnd == blockX) && isBelowBlock { interactableFaces[cube.FaceWest] = struct{}{} interactableFaces[cube.FaceEast] = struct{}{} } else { @@ -139,12 +141,24 @@ func (d *ScaffoldB) isFaceInteractable( if xFloorStart > blockX || xFloorEnd > blockX { interactableFaces[cube.FaceEast] = struct{}{} } + } */ + if xFloorStart <= blockX || xFloorEnd <= blockX { + interactableFaces[cube.FaceWest] = struct{}{} + } + if xFloorStart >= blockX || xFloorEnd >= blockX { + interactableFaces[cube.FaceEast] = struct{}{} } // Check for the Z-axis faces. // If floor(eyePos.Z) < blockPos.Z -> the north face is interactable. // If floor(eyePos.Z) > blockPos.Z -> the south face is interactable. - if (zFloorStart == blockZ || zFloorEnd == blockZ) && isBelowBlock { + if zFloorStart <= blockZ || zFloorEnd <= blockZ { + interactableFaces[cube.FaceNorth] = struct{}{} + } + if zFloorStart >= blockZ || zFloorEnd >= blockZ { + interactableFaces[cube.FaceSouth] = struct{}{} + } + /* if (zFloorStart == blockZ || zFloorEnd == blockZ) && isBelowBlock { interactableFaces[cube.FaceNorth] = struct{}{} interactableFaces[cube.FaceSouth] = struct{}{} } else { @@ -154,9 +168,10 @@ func (d *ScaffoldB) isFaceInteractable( if zFloorStart > blockZ || zFloorEnd > blockZ { interactableFaces[cube.FaceSouth] = struct{}{} } - } + } */ } _, interactable := interactableFaces[targetFace] + //fmt.Println(blockPos, cube.PosFromVec3(startPos), cube.PosFromVec3(endPos), isClientInput, targetFace, interactableFaces, interactable) return interactable } From f4f1c3902e4a5a1a1f63f472ae14bb0cf1582c7b Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sat, 26 Jul 2025 16:43:23 -0400 Subject: [PATCH 03/29] detection/scaffold_b.go: ffs?! --- player/detection/scaffold_b.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index 07ce8b74..81e11541 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -70,7 +70,8 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { blockFace := cube.Face(dat.BlockFace) if dat.TriggerType == protocol.TriggerTypePlayerInput { d.initialFace = faceNotSet - } else if d.initialFace == faceNotSet { + } else if d.initialFace == faceNotSet && blockFace != cube.FaceDown && blockFace != cube.FaceUp { + // weird bedrock client behavior.. I LOVE THIS GAME! d.initialFace = blockFace } blockPos := cube.Pos{int(dat.BlockPosition[0]), int(dat.BlockPosition[1]), int(dat.BlockPosition[2])} @@ -92,8 +93,10 @@ func (d *ScaffoldB) isFaceInteractable( if !isClientInput { interactableFaces[cube.FaceDown] = struct{}{} interactableFaces[cube.FaceUp] = struct{}{} - interactableFaces[d.initialFace] = struct{}{} - interactableFaces[d.initialFace.Opposite()] = struct{}{} + if d.initialFace != -1 { + interactableFaces[d.initialFace] = struct{}{} + interactableFaces[d.initialFace.Opposite()] = struct{}{} + } } else { yFloorStart := int(startPos[1]) yFloorEnd := int(endPos[1]) From 6bc9142479e87aaec81d195855fe5da07e5ad549 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sat, 26 Jul 2025 22:53:02 -0400 Subject: [PATCH 04/29] scaffold_b.go: actually FIX TS godbless --- game/math.go | 10 ++++ player/component/inventory.go | 2 +- player/component/world.go | 2 +- player/detection/scaffold_b.go | 90 ++++++++++++++-------------------- player/packet.go | 4 +- 5 files changed, 53 insertions(+), 55 deletions(-) diff --git a/game/math.go b/game/math.go index b6898cef..fbdcc743 100755 --- a/game/math.go +++ b/game/math.go @@ -54,6 +54,16 @@ func AbsNum[T uint | int | uint8 | int8 | uint16 | int16 | uint32 | int32 | uint return a } +// Round will round a number to the nearest integer. +func Round[T float32 | float64, V uint | int | uint8 | int8 | uint16 | int16 | uint32 | int32 | uint64 | int64](a T) V { + baseFloat := a - T(V(a)) + base := V(a) + if baseFloat >= 0.5 { + base++ + } + return base +} + // AbsInt64 will return the absolute value of an int64. func AbsInt64(a int64) int64 { if a < 0 { diff --git a/player/component/inventory.go b/player/component/inventory.go index 9e0eac3c..b45f2d6e 100644 --- a/player/component/inventory.go +++ b/player/component/inventory.go @@ -15,7 +15,7 @@ import ( const ( inventorySizeArmour uint32 = 4 inventorySizePlayer uint32 = 36 - inventorySizeHotbar uint32 = 10 + inventorySizeHotbar uint32 = 9 inventorySizeOffhand uint32 = 1 inventorySizeUI uint32 = 54 ) diff --git a/player/component/world.go b/player/component/world.go index 03c68ac2..5c8203ea 100644 --- a/player/component/world.go +++ b/player/component/world.go @@ -109,7 +109,7 @@ func (c *WorldUpdaterComponent) AttemptItemInteractionWithBlock(pk *packet.Inven } heldItem := holding.Item() - c.mPlayer.Dbg.Notify(player.DebugModeBlockPlacement, true, "item in hand: %T", heldItem) + c.mPlayer.Dbg.Notify(player.DebugModeBlockPlacement, true, "item in hand (slot %d): %T", c.mPlayer.Inventory().HeldSlot(), heldItem) switch heldItem := heldItem.(type) { case *block.Air: // This only happens when Dragonfly is unsure of what the item is (unregistered), so we use the client-authoritative block in hand. diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index 81e11541..e5f3c20e 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -9,7 +9,10 @@ import ( "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) -var faceNotSet cube.Face = -1 +var ( + faceNotSet cube.Face = -1 + faceNotInit cube.Face = -2 +) type ScaffoldB struct { mPlayer *player.Player @@ -24,12 +27,12 @@ func New_ScaffoldB(p *player.Player) *ScaffoldB { metadata: &player.DetectionMetadata{ MaxViolations: 10, }, - initialFace: faceNotSet, + initialFace: faceNotInit, } } func (d *ScaffoldB) Type() string { - return "Scaffold" + return TypeScaffold } func (d *ScaffoldB) SubType() string { @@ -55,7 +58,22 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { } dat, ok := tr.TransactionData.(*protocol.UseItemTransactionData) - if !ok || dat.ActionType != protocol.UseItemActionClickBlock || dat.ClientPrediction == protocol.ClientPredictionFailure || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) { + if !ok || dat.ActionType != protocol.UseItemActionClickBlock || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) { + return + } + + // We have to check this regardless of whether the client predicted the interaction failed or not - otherwise we get false positives when + // checking during when the trigger type is of TriggerTypeSimulationTick (player frame). + blockFace := cube.Face(dat.BlockFace) + if dat.TriggerType == protocol.TriggerTypePlayerInput { + d.initialFace = faceNotSet + } else if d.initialFace == faceNotSet { + d.initialFace = blockFace + } else if d.initialFace == faceNotInit { + d.mPlayer.Log().Debug("scaffold_b", "initFace", "faceNotInit", "face", blockFace) + d.mPlayer.FailDetection(d, nil) + } + if dat.ClientPrediction == protocol.ClientPredictionFailure { return } @@ -67,13 +85,6 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { currEyePos := d.mPlayer.Movement().Client().Pos() prevEyePos[1] += eyeOffset currEyePos[1] += eyeOffset - blockFace := cube.Face(dat.BlockFace) - if dat.TriggerType == protocol.TriggerTypePlayerInput { - d.initialFace = faceNotSet - } else if d.initialFace == faceNotSet && blockFace != cube.FaceDown && blockFace != cube.FaceUp { - // weird bedrock client behavior.. I LOVE THIS GAME! - d.initialFace = blockFace - } blockPos := cube.Pos{int(dat.BlockPosition[0]), int(dat.BlockPosition[1]), int(dat.BlockPosition[2])} if !d.isFaceInteractable(prevEyePos, currEyePos, blockPos, blockFace, dat.TriggerType == protocol.TriggerTypePlayerInput) { d.mPlayer.FailDetection(d, nil) @@ -89,42 +100,37 @@ func (d *ScaffoldB) isFaceInteractable( ) bool { interactableFaces := make(map[cube.Face]struct{}, 6) blockX, blockY, blockZ := blockPos[0], blockPos[1], blockPos[2] + floorPosStart := cube.PosFromVec3(startPos) + floorPosEnd := cube.PosFromVec3(endPos) if !isClientInput { interactableFaces[cube.FaceDown] = struct{}{} interactableFaces[cube.FaceUp] = struct{}{} - if d.initialFace != -1 { + if d.initialFace != faceNotSet { interactableFaces[d.initialFace] = struct{}{} interactableFaces[d.initialFace.Opposite()] = struct{}{} } } else { - yFloorStart := int(startPos[1]) - yFloorEnd := int(endPos[1]) - xFloorStart := int(startPos[0]) - xFloorEnd := int(endPos[0]) - zFloorStart := int(startPos[2]) - zFloorEnd := int(endPos[2]) - // Check for the Y-axis faces first. // If floor(eyePos.Y) < blockPos.Y -> the bottom face is interactable. // If floor(eyePos.Y) > blockPos.Y -> the top face is interactable. - isBelowBlock := yFloorStart < blockY || yFloorEnd < blockY - isAboveBlock := yFloorStart > blockY || yFloorEnd > blockY + isBelowBlock := floorPosStart[1] < blockY || floorPosEnd[1] < blockY + isAboveBlock := floorPosStart[1] > blockY || floorPosEnd[1] > blockY if isBelowBlock { interactableFaces[cube.FaceDown] = struct{}{} } if isAboveBlock { interactableFaces[cube.FaceUp] = struct{}{} - startXDelta := game.AbsNum(xFloorStart - blockX) - endXDelta := game.AbsNum(xFloorEnd - blockX) + startXDelta := game.AbsNum(floorPosStart[0] - blockX) + endXDelta := game.AbsNum(floorPosEnd[0] - blockX) if startXDelta <= 1 || endXDelta <= 1 { interactableFaces[cube.FaceWest] = struct{}{} interactableFaces[cube.FaceEast] = struct{}{} } - startZDelta := game.AbsNum(zFloorStart - blockZ) - endZDelta := game.AbsNum(zFloorEnd - blockZ) + startZDelta := game.AbsNum(floorPosStart[2] - blockZ) + endZDelta := game.AbsNum(floorPosEnd[2] - blockZ) if startZDelta <= 1 || endZDelta <= 1 { interactableFaces[cube.FaceNorth] = struct{}{} interactableFaces[cube.FaceSouth] = struct{}{} @@ -134,47 +140,27 @@ func (d *ScaffoldB) isFaceInteractable( // Check for the X-axis faces. // If floor(eyePos.X) < blockPos.X -> the west face is interactable. // If floor(eyePos.X) > blockPos.X -> the east face is interactable. - /* if (xFloorStart == blockX || xFloorEnd == blockX) && isBelowBlock { - interactableFaces[cube.FaceWest] = struct{}{} - interactableFaces[cube.FaceEast] = struct{}{} - } else { - if xFloorStart < blockX || xFloorEnd < blockX { - interactableFaces[cube.FaceWest] = struct{}{} - } - if xFloorStart > blockX || xFloorEnd > blockX { - interactableFaces[cube.FaceEast] = struct{}{} - } - } */ - if xFloorStart <= blockX || xFloorEnd <= blockX { + if floorPosStart[0] < blockX || floorPosEnd[0] < blockX { interactableFaces[cube.FaceWest] = struct{}{} } - if xFloorStart >= blockX || xFloorEnd >= blockX { + if floorPosStart[0] > blockX || floorPosEnd[0] > blockX { interactableFaces[cube.FaceEast] = struct{}{} } // Check for the Z-axis faces. // If floor(eyePos.Z) < blockPos.Z -> the north face is interactable. // If floor(eyePos.Z) > blockPos.Z -> the south face is interactable. - if zFloorStart <= blockZ || zFloorEnd <= blockZ { + if floorPosStart[2] < blockZ || floorPosEnd[2] < blockZ { interactableFaces[cube.FaceNorth] = struct{}{} } - if zFloorStart >= blockZ || zFloorEnd >= blockZ { + if floorPosStart[2] > blockZ || floorPosEnd[2] > blockZ { interactableFaces[cube.FaceSouth] = struct{}{} } - /* if (zFloorStart == blockZ || zFloorEnd == blockZ) && isBelowBlock { - interactableFaces[cube.FaceNorth] = struct{}{} - interactableFaces[cube.FaceSouth] = struct{}{} - } else { - if zFloorStart < blockZ || zFloorEnd < blockZ { - interactableFaces[cube.FaceNorth] = struct{}{} - } - if zFloorStart > blockZ || zFloorEnd > blockZ { - interactableFaces[cube.FaceSouth] = struct{}{} - } - } */ } _, interactable := interactableFaces[targetFace] - //fmt.Println(blockPos, cube.PosFromVec3(startPos), cube.PosFromVec3(endPos), isClientInput, targetFace, interactableFaces, interactable) + if !interactable { + d.mPlayer.Log().Debug("scaffold_b", "blockPos", blockPos, "startPos", startPos, "endPos", endPos, "isClientInput", isClientInput, "targetFace", targetFace, "interactableFaces", interactableFaces) + } return interactable } diff --git a/player/packet.go b/player/packet.go index 2d3da2d4..25e4ff30 100644 --- a/player/packet.go +++ b/player/packet.go @@ -169,7 +169,9 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { case *packet.RequestChunkRadius: p.worldUpdater.SetChunkRadius(pk.ChunkRadius + 4) case *packet.InventoryTransaction: - if _, ok := pk.TransactionData.(*protocol.UseItemOnEntityTransactionData); ok { + if tr, ok := pk.TransactionData.(*protocol.UseItemOnEntityTransactionData); ok { + p.inventory.SetHeldSlot(int32(tr.HotBarSlot)) + // The reason we cancel here is because Oomph also utlizes a full-authoritative system for combat. We need to wait for the // next movement (PlayerAuthInputPacket) the client sends so that we can accurately calculate if the hit is valid. p.combat.Attack(pk) From 425e073420584381ab3f5c0a9b0998a480047f71 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sun, 27 Jul 2025 15:50:03 -0400 Subject: [PATCH 05/29] scaffold_b.go: little leniency for ts because MCBE client weird --- player/detection/scaffold_b.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index e5f3c20e..145be8cc 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -25,6 +25,9 @@ func New_ScaffoldB(p *player.Player) *ScaffoldB { return &ScaffoldB{ mPlayer: p, metadata: &player.DetectionMetadata{ + FailBuffer: 1.01, + MaxBuffer: 1.5, + MaxViolations: 10, }, initialFace: faceNotInit, @@ -67,7 +70,7 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { blockFace := cube.Face(dat.BlockFace) if dat.TriggerType == protocol.TriggerTypePlayerInput { d.initialFace = faceNotSet - } else if d.initialFace == faceNotSet { + } else if d.initialFace == faceNotSet && blockFace != cube.FaceUp && blockFace != cube.FaceDown { d.initialFace = blockFace } else if d.initialFace == faceNotInit { d.mPlayer.Log().Debug("scaffold_b", "initFace", "faceNotInit", "face", blockFace) @@ -88,6 +91,8 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { blockPos := cube.Pos{int(dat.BlockPosition[0]), int(dat.BlockPosition[1]), int(dat.BlockPosition[2])} if !d.isFaceInteractable(prevEyePos, currEyePos, blockPos, blockFace, dat.TriggerType == protocol.TriggerTypePlayerInput) { d.mPlayer.FailDetection(d, nil) + } else { + d.mPlayer.PassDetection(d, 0.5) } } From 3f793296db22b32fbec9bb0844815de575599a67 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sat, 16 Aug 2025 00:02:07 -0400 Subject: [PATCH 06/29] detection/scaffold_b.go: make more strict --- player/detection/scaffold_b.go | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index 145be8cc..7529e0cd 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -1,6 +1,8 @@ package detection import ( + "fmt" + "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" "github.com/oomph-ac/oomph/game" @@ -121,25 +123,28 @@ func (d *ScaffoldB) isFaceInteractable( // If floor(eyePos.Y) > blockPos.Y -> the top face is interactable. isBelowBlock := floorPosStart[1] < blockY || floorPosEnd[1] < blockY isAboveBlock := floorPosStart[1] > blockY || floorPosEnd[1] > blockY + isOnBlock := floorPosStart[1] == blockY+2 || floorPosEnd[1] == blockY+2 if isBelowBlock { interactableFaces[cube.FaceDown] = struct{}{} } if isAboveBlock { interactableFaces[cube.FaceUp] = struct{}{} - - startXDelta := game.AbsNum(floorPosStart[0] - blockX) - endXDelta := game.AbsNum(floorPosEnd[0] - blockX) - if startXDelta <= 1 || endXDelta <= 1 { - interactableFaces[cube.FaceWest] = struct{}{} - interactableFaces[cube.FaceEast] = struct{}{} - } - - startZDelta := game.AbsNum(floorPosStart[2] - blockZ) - endZDelta := game.AbsNum(floorPosEnd[2] - blockZ) - if startZDelta <= 1 || endZDelta <= 1 { - interactableFaces[cube.FaceNorth] = struct{}{} - interactableFaces[cube.FaceSouth] = struct{}{} + if isOnBlock { + startXDelta := game.AbsNum(floorPosStart[0] - blockX) + endXDelta := game.AbsNum(floorPosEnd[0] - blockX) + if startXDelta <= 1 || endXDelta <= 1 { + interactableFaces[cube.FaceWest] = struct{}{} + interactableFaces[cube.FaceEast] = struct{}{} + } + + startZDelta := game.AbsNum(floorPosStart[2] - blockZ) + endZDelta := game.AbsNum(floorPosEnd[2] - blockZ) + if startZDelta <= 1 || endZDelta <= 1 { + interactableFaces[cube.FaceNorth] = struct{}{} + interactableFaces[cube.FaceSouth] = struct{}{} + } } + fmt.Println(isOnBlock, floorPosStart[1], floorPosEnd[1], blockY) } // Check for the X-axis faces. From f2239f3ffdd79985d89b91390d795f7e4d6d5e50 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sat, 16 Aug 2025 00:19:12 -0400 Subject: [PATCH 07/29] detection/scaffold_b.go: remove debug --- player/detection/scaffold_b.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index 7529e0cd..c2a3ff3f 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -1,8 +1,6 @@ package detection import ( - "fmt" - "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" "github.com/oomph-ac/oomph/game" @@ -144,7 +142,7 @@ func (d *ScaffoldB) isFaceInteractable( interactableFaces[cube.FaceSouth] = struct{}{} } } - fmt.Println(isOnBlock, floorPosStart[1], floorPosEnd[1], blockY) + //fmt.Println(isOnBlock, floorPosStart[1], floorPosEnd[1], blockY) } // Check for the X-axis faces. From d89dbf4e5239ce9e1d6c3d43ca012f09cb967fd7 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Mon, 18 Aug 2025 22:25:08 -0400 Subject: [PATCH 08/29] detection/scaffold_b.go: only run if client is holding block and if client prediction is positive --- game/aabb.go | 12 +++++++----- player/detection/register.go | 1 + player/detection/scaffold_b.go | 7 ++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/game/aabb.go b/game/aabb.go index dfc510c6..f810883e 100755 --- a/game/aabb.go +++ b/game/aabb.go @@ -118,13 +118,13 @@ func BBHasZeroVolume(bb cube.BBox) bool { return bb.Min() == bb.Max() } -func ClosestPointInLineToPoint(origin, end mgl32.Vec3, point mgl32.Vec3) mgl32.Vec3 { - line := end.Sub(origin) +func ClosestPointInLineToPoint(start, end, point mgl32.Vec3) mgl32.Vec3 { + line := end.Sub(start) if line.LenSqr() <= 1e-4 { - return origin + return start } - t := (point.Sub(origin)).Dot(line) / line.LenSqr() + t := (point.Sub(start)).Dot(line) / line.LenSqr() // Clamp to stay on the line segment if t < 0 { t = 0 @@ -132,7 +132,7 @@ func ClosestPointInLineToPoint(origin, end mgl32.Vec3, point mgl32.Vec3) mgl32.V t = 1 } - return origin.Add(line.Mul(t)) + return start.Add(line.Mul(t)) } // ClosestPointToBBox returns the shortest point from a given origin to a given bounding box. @@ -194,6 +194,8 @@ func ClosestPointToBBoxDirectional(origin, startLook, endLook mgl32.Vec3, bb cub return point2, true } return point1, true + } else if point1.X() == point2.X() || point1.Y() == point2.Y() || point1.Z() == point2.Z() { + return mgl32.Vec3{}, false } possibleBB := cube.Box(point1.X(), point1.Y(), point1.Z(), point2.X(), point2.Y(), point2.Z()) diff --git a/player/detection/register.go b/player/detection/register.go index 10038aba..d79c6b84 100644 --- a/player/detection/register.go +++ b/player/detection/register.go @@ -23,6 +23,7 @@ func Register(p *player.Player) { // scaffold detections p.RegisterDetection(New_ScaffoldA(p)) p.RegisterDetection(New_ScaffoldB(p)) + //p.RegisterDetection(New_ScaffoldC(p)) //p.RegisterDetection(New_NukerA(p)) diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index c2a3ff3f..4789d2f3 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -1,6 +1,7 @@ package detection import ( + "github.com/df-mc/dragonfly/server/world" "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" "github.com/oomph-ac/oomph/game" @@ -61,7 +62,11 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { } dat, ok := tr.TransactionData.(*protocol.UseItemTransactionData) - if !ok || dat.ActionType != protocol.UseItemActionClickBlock || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) { + if !ok || dat.ActionType != protocol.UseItemActionClickBlock || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) || dat.ClientPrediction != protocol.ClientPredictionSuccess { + return + } + inHand := d.mPlayer.Inventory().Holding() + if _, isBlock := inHand.Item().(world.Block); !isBlock { return } From 114d101e38e684ed524a14655dec7a17926ea588 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Mon, 18 Aug 2025 22:53:18 -0400 Subject: [PATCH 09/29] we already check the client prediction here ffs --- player/detection/scaffold_b.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index 4789d2f3..6c07a4fa 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -62,7 +62,7 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { } dat, ok := tr.TransactionData.(*protocol.UseItemTransactionData) - if !ok || dat.ActionType != protocol.UseItemActionClickBlock || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) || dat.ClientPrediction != protocol.ClientPredictionSuccess { + if !ok || dat.ActionType != protocol.UseItemActionClickBlock || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) { return } inHand := d.mPlayer.Inventory().Holding() From 3a75afa083a2fa106f07e0f816ce919bff90b336 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Tue, 19 Aug 2025 21:12:13 -0400 Subject: [PATCH 10/29] detection/scaffold_b.go: get rid of faceNotInit and don't use eyePos --- game/aabb.go | 16 ++++++---------- game/math.go | 10 ++++++++++ player/detection/scaffold_b.go | 31 +++++++++++++------------------ 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/game/aabb.go b/game/aabb.go index f810883e..738f3816 100755 --- a/game/aabb.go +++ b/game/aabb.go @@ -185,19 +185,15 @@ func ClosestPointToBBoxDirectional(origin, startLook, endLook mgl32.Vec3, bb cub point2 = ClosestPointToBBox(point2, bb) } - if point1 == point2 { - if !hit1 { - if !hit2 { - // Here, there is no possible way that any point between the two rays can intersect with the bounding box. - return mgl32.Vec3{}, false - } - return point2, true + if !hit1 && !hit2 { + if point1 == point2 || point1.Y() == point2.Y() || (point1.X() == point2.X() && point1.Z() == point2.Z()) { + return mgl32.Vec3{}, false } + } else if hit1 { return point1, true - } else if point1.X() == point2.X() || point1.Y() == point2.Y() || point1.Z() == point2.Z() { - return mgl32.Vec3{}, false + } else if hit2 { + return point2, true } - possibleBB := cube.Box(point1.X(), point1.Y(), point1.Z(), point2.X(), point2.Y(), point2.Z()) return ClosestPointToBBox(origin, possibleBB), true } diff --git a/game/math.go b/game/math.go index fbdcc743..6a757b4c 100755 --- a/game/math.go +++ b/game/math.go @@ -64,6 +64,16 @@ func Round[T float32 | float64, V uint | int | uint8 | int8 | uint16 | int16 | u return base } +// PrecisionFloor32 floors a number to the given precision. +func PrecisionFloor32(a float32, precision float32) int { + increaseValueAt := float32(1) - precision + floorVal := int(a) + if a-float32(floorVal) >= increaseValueAt { + floorVal++ + } + return floorVal +} + // AbsInt64 will return the absolute value of an int64. func AbsInt64(a int64) int64 { if a < 0 { diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index 6c07a4fa..c91bfcc3 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -11,8 +11,7 @@ import ( ) var ( - faceNotSet cube.Face = -1 - faceNotInit cube.Face = -2 + faceNotSet cube.Face = -1 ) type ScaffoldB struct { @@ -31,7 +30,7 @@ func New_ScaffoldB(p *player.Player) *ScaffoldB { MaxViolations: 10, }, - initialFace: faceNotInit, + initialFace: faceNotSet, } } @@ -77,24 +76,19 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { d.initialFace = faceNotSet } else if d.initialFace == faceNotSet && blockFace != cube.FaceUp && blockFace != cube.FaceDown { d.initialFace = blockFace - } else if d.initialFace == faceNotInit { - d.mPlayer.Log().Debug("scaffold_b", "initFace", "faceNotInit", "face", blockFace) - d.mPlayer.FailDetection(d, nil) } if dat.ClientPrediction == protocol.ClientPredictionFailure { return } - eyeOffset := game.DefaultPlayerHeightOffset - if d.mPlayer.Movement().Sneaking() { - eyeOffset = game.SneakingPlayerHeightOffset - } - prevEyePos := d.mPlayer.Movement().Client().LastPos() - currEyePos := d.mPlayer.Movement().Client().Pos() - prevEyePos[1] += eyeOffset - currEyePos[1] += eyeOffset blockPos := cube.Pos{int(dat.BlockPosition[0]), int(dat.BlockPosition[1]), int(dat.BlockPosition[2])} - if !d.isFaceInteractable(prevEyePos, currEyePos, blockPos, blockFace, dat.TriggerType == protocol.TriggerTypePlayerInput) { + if !d.isFaceInteractable( + d.mPlayer.Movement().Client().LastPos(), + d.mPlayer.Movement().Client().Pos(), + blockPos, + blockFace, + dat.TriggerType == protocol.TriggerTypePlayerInput, + ) { d.mPlayer.FailDetection(d, nil) } else { d.mPlayer.PassDetection(d, 0.5) @@ -126,23 +120,24 @@ func (d *ScaffoldB) isFaceInteractable( // If floor(eyePos.Y) > blockPos.Y -> the top face is interactable. isBelowBlock := floorPosStart[1] < blockY || floorPosEnd[1] < blockY isAboveBlock := floorPosStart[1] > blockY || floorPosEnd[1] > blockY - isOnBlock := floorPosStart[1] == blockY+2 || floorPosEnd[1] == blockY+2 + isOnBlock := floorPosStart[1] == blockY+1 || floorPosEnd[1] == blockY+1 if isBelowBlock { interactableFaces[cube.FaceDown] = struct{}{} } if isAboveBlock { + //d.mPlayer.Message("isOnBlock=%t prevY=%f currY=%f", isOnBlock, startPos[1], endPos[1]) interactableFaces[cube.FaceUp] = struct{}{} if isOnBlock { startXDelta := game.AbsNum(floorPosStart[0] - blockX) endXDelta := game.AbsNum(floorPosEnd[0] - blockX) - if startXDelta <= 1 || endXDelta <= 1 { + if startXDelta == 0 || endXDelta == 0 { interactableFaces[cube.FaceWest] = struct{}{} interactableFaces[cube.FaceEast] = struct{}{} } startZDelta := game.AbsNum(floorPosStart[2] - blockZ) endZDelta := game.AbsNum(floorPosEnd[2] - blockZ) - if startZDelta <= 1 || endZDelta <= 1 { + if startZDelta == 0 || endZDelta == 0 { interactableFaces[cube.FaceNorth] = struct{}{} interactableFaces[cube.FaceSouth] = struct{}{} } From 1dd7734aaa5d53c5492304e0dffe3401d9c596b9 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Wed, 20 Aug 2025 15:10:06 -0400 Subject: [PATCH 11/29] add CPS limits (check oconfig) --- player/clicks.go | 30 ++++++++ player/combat.go | 6 +- player/component/clicks.go | 112 ++++++++++++++++++++++++++++++ player/component/register.go | 1 + player/detection/autoclicker_a.go | 73 +++++++++++++++++++ player/detection/register.go | 3 + player/detection/types.go | 1 + player/packet.go | 11 +-- player/player.go | 3 + utils/circular_queue.go | 9 +++ 10 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 player/clicks.go create mode 100644 player/component/clicks.go create mode 100644 player/detection/autoclicker_a.go diff --git a/player/clicks.go b/player/clicks.go new file mode 100644 index 00000000..5b448711 --- /dev/null +++ b/player/clicks.go @@ -0,0 +1,30 @@ +package player + +import "github.com/sandertv/gophertunnel/minecraft/protocol" + +type ClicksComponent interface { + HandleAttack(*protocol.UseItemOnEntityTransactionData) + HandleSwing() + HandleRight(*protocol.UseItemTransactionData) + + DelayLeft() int64 + DelayRight() int64 + + CPSLeft() int64 + CPSRight() int64 + + AddLeftHook(hook ClickHook) + AddRightHook(hook ClickHook) + + Tick() +} + +type ClickHook func() + +func (p *Player) SetClicks(c ClicksComponent) { + p.clicks = c +} + +func (p *Player) Clicks() ClicksComponent { + return p.clicks +} diff --git a/player/combat.go b/player/combat.go index 77e77751..2602cc7d 100644 --- a/player/combat.go +++ b/player/combat.go @@ -54,7 +54,11 @@ func (p *Player) ClientCombat() CombatComponent { return p.clientCombat } -func (p *Player) tryRunningClientCombat() { +func (p *Player) tryRunningClientCombat(pk *packet.PlayerAuthInput) { + if pk.InputData.Load(packet.InputFlagMissedSwing) { + p.Clicks().HandleSwing() + } + p.Clicks().Tick() if p.opts.Combat.EnableClientEntityTracking { p.clientEntTracker.Tick(p.ClientTick) _ = p.clientCombat.Calculate() diff --git a/player/component/clicks.go b/player/component/clicks.go new file mode 100644 index 00000000..f4417637 --- /dev/null +++ b/player/component/clicks.go @@ -0,0 +1,112 @@ +package component + +import ( + "github.com/oomph-ac/oomph/player" + "github.com/oomph-ac/oomph/utils" + "github.com/sandertv/gophertunnel/minecraft/protocol" +) + +type ClicksComponent struct { + mPlayer *player.Player + + clicksLeft *utils.CircularQueue[int64] + clicksRight *utils.CircularQueue[int64] + + delayLeft int64 + delayRight int64 + + lastLeftClick int64 + lastRightClick int64 + + cpsLeft int64 + cpsRight int64 + + hooksLeft []player.ClickHook + hooksRight []player.ClickHook +} + +func NewClicksComponent(p *player.Player) *ClicksComponent { + return &ClicksComponent{ + mPlayer: p, + clicksLeft: utils.NewCircularQueue[int64](player.TicksPerSecond), + clicksRight: utils.NewCircularQueue[int64](player.TicksPerSecond), + } +} + +func (c *ClicksComponent) HandleAttack(dat *protocol.UseItemOnEntityTransactionData) { + if dat.ActionType == protocol.UseItemOnEntityActionAttack { + c.clickLeft() + } +} + +func (c *ClicksComponent) HandleSwing() { + c.clickLeft() +} + +func (c *ClicksComponent) HandleRight(dat *protocol.UseItemTransactionData) { + // On versions before 1.21.20, we cannot determine if the right click action was caused by a player input or due to MCBE's assisted simulation actions. + // This isn't much of a problem for Oomph specifically - as it *officially* supports 1.21.20+ + if !c.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) || dat.TriggerType != protocol.TriggerTypePlayerInput { + return + } + c.clickRight() +} + +func (c *ClicksComponent) DelayLeft() int64 { + return c.delayLeft +} + +func (c *ClicksComponent) DelayRight() int64 { + return c.delayRight +} + +func (c *ClicksComponent) CPSLeft() int64 { + return c.cpsLeft +} + +func (c *ClicksComponent) CPSRight() int64 { + return c.cpsRight +} + +func (c *ClicksComponent) AddLeftHook(hook player.ClickHook) { + c.hooksLeft = append(c.hooksLeft, hook) +} + +func (c *ClicksComponent) AddRightHook(hook player.ClickHook) { + c.hooksRight = append(c.hooksRight, hook) +} + +func (c *ClicksComponent) Tick() { + if c.clicksLeft.Len() == c.clicksLeft.Cap() { + c.cpsLeft -= c.clicksLeft.Get(0) + } + if c.clicksRight.Len() == c.clicksRight.Cap() { + c.cpsRight -= c.clicksRight.Get(0) + } + c.clicksLeft.Append(0) + c.clicksRight.Append(0) +} + +func (c *ClicksComponent) clickLeft() { + index := c.clicksLeft.Cap() - 1 + c.clicksLeft.Set(index, c.clicksLeft.Get(index)+1) + c.cpsLeft++ + c.delayLeft = c.mPlayer.InputCount - c.lastLeftClick + c.lastLeftClick = c.mPlayer.InputCount + + for _, hook := range c.hooksLeft { + hook() + } +} + +func (c *ClicksComponent) clickRight() { + index := c.clicksRight.Cap() - 1 + c.clicksRight.Set(index, c.clicksRight.Get(index)+1) + c.cpsRight++ + c.delayRight = c.mPlayer.InputCount - c.lastRightClick + c.lastRightClick = c.mPlayer.InputCount + + for _, hook := range c.hooksRight { + hook() + } +} diff --git a/player/component/register.go b/player/component/register.go index 15d28218..624d28a3 100644 --- a/player/component/register.go +++ b/player/component/register.go @@ -6,6 +6,7 @@ import "github.com/oomph-ac/oomph/player" func Register(p *player.Player) { p.SetCombat(NewAuthoritativeCombatComponent(p, false)) p.SetClientCombat(NewAuthoritativeCombatComponent(p, true)) + p.SetClicks(NewClicksComponent(p)) p.SetEntityTracker(NewEntityTrackerComponent(p, false)) p.SetClientEntityTracker(NewEntityTrackerComponent(p, true)) p.SetEffects(NewEffectsComponent()) diff --git a/player/detection/autoclicker_a.go b/player/detection/autoclicker_a.go new file mode 100644 index 00000000..46b5aa7a --- /dev/null +++ b/player/detection/autoclicker_a.go @@ -0,0 +1,73 @@ +package detection + +import ( + "github.com/elliotchance/orderedmap/v2" + "github.com/oomph-ac/oomph/player" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type AutoclickerA struct { + mPlayer *player.Player + metadata *player.DetectionMetadata +} + +func New_AutoclickerA(p *player.Player) *AutoclickerA { + d := &AutoclickerA{ + mPlayer: p, + metadata: &player.DetectionMetadata{ + FailBuffer: 4, + MaxBuffer: 4, + MaxViolations: 20, + TrustDuration: -1, + }, + } + p.Clicks().AddLeftHook(d.hookLeft) + p.Clicks().AddRightHook(d.hookRight) + return d +} + +func (*AutoclickerA) Type() string { + return TypeAutoclicker +} + +func (*AutoclickerA) SubType() string { + return "A" +} + +func (*AutoclickerA) Description() string { + return "Checks if the player is clicking above the limit set in Oomph's configuration." +} + +func (*AutoclickerA) Punishable() bool { + return true +} + +func (d *AutoclickerA) Metadata() *player.DetectionMetadata { + return d.metadata +} + +func (d *AutoclickerA) Detect(pk packet.Packet) {} + +func (d *AutoclickerA) hookLeft() { + limit := d.mPlayer.Opts().Combat.LeftCPSLimit + if d.mPlayer.InputMode == packet.InputModeTouch { + limit = d.mPlayer.Opts().Combat.LeftCPSLimitMobile + } + if cps := d.mPlayer.Clicks().CPSLeft(); cps > limit { + dat := orderedmap.NewOrderedMap[string, any]() + dat.Set("left_cps", cps) + d.mPlayer.FailDetection(d, dat) + } +} + +func (d *AutoclickerA) hookRight() { + limit := d.mPlayer.Opts().Combat.RightCPSLimit + if d.mPlayer.InputMode == packet.InputModeTouch { + limit = d.mPlayer.Opts().Combat.RightCPSLimitMobile + } + if cps := d.mPlayer.Clicks().CPSRight(); cps > limit { + dat := orderedmap.NewOrderedMap[string, any]() + dat.Set("right_cps", cps) + d.mPlayer.FailDetection(d, dat) + } +} diff --git a/player/detection/register.go b/player/detection/register.go index d79c6b84..63ae25fe 100644 --- a/player/detection/register.go +++ b/player/detection/register.go @@ -3,6 +3,9 @@ package detection import "github.com/oomph-ac/oomph/player" func Register(p *player.Player) { + // autoclicker detections + p.RegisterDetection(New_AutoclickerA(p)) + // aim detections p.RegisterDetection(New_AimA(p)) diff --git a/player/detection/types.go b/player/detection/types.go index 481a29fb..8263f1c6 100644 --- a/player/detection/types.go +++ b/player/detection/types.go @@ -2,6 +2,7 @@ package detection const ( TypeAim = "Aim" + TypeAutoclicker = "Autoclicker" TypeBadPacket = "BadPacket" TypeEditionFaker = "EditionFaker" TypeKillaura = "Killaura" diff --git a/player/packet.go b/player/packet.go index c17764e9..56c06fea 100644 --- a/player/packet.go +++ b/player/packet.go @@ -92,7 +92,7 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { case *packet.PlayerAuthInput: if !p.movement.InputAcceptable() { p.Popup("input rate-limited (%d)", p.SimulationFrame) - p.tryRunningClientCombat() + p.tryRunningClientCombat(pk) ctx.Cancel() return } @@ -115,7 +115,7 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { p.handleBlockActions(pk) p.handleMovement(pk) - p.tryRunningClientCombat() + p.tryRunningClientCombat(pk) var serverVerifiedHit bool if !p.blockBreakInProgress { @@ -140,14 +140,16 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { if tr.ActionType == protocol.UseItemOnEntityActionAttack { // The reason we cancel here is because Oomph also utlizes a full-authoritative system for combat. We need to wait for the // next movement (PlayerAuthInputPacket) the client sends so that we can accurately calculate if the hit is valid. - p.combat.Attack(pk) + p.Combat().Attack(pk) + p.Clicks().HandleAttack(tr) if p.opts.Combat.EnableClientEntityTracking { - p.clientCombat.Attack(pk) + p.ClientCombat().Attack(pk) } ctx.Cancel() } } else if tr, ok := pk.TransactionData.(*protocol.UseItemTransactionData); ok { p.inventory.SetHeldSlot(int32(tr.HotBarSlot)) + p.Clicks().HandleRight(tr) if tr.ActionType == protocol.UseItemActionClickAir { // If the client is gliding and uses a firework, it predicts a boost on it's own side, although the entity may not exist on the server. // This is very stange, as the gliding boost (in bedrock) is supplied by FireworksRocketActor::normalTick() which is similar to MC:JE logic. @@ -248,6 +250,7 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { case *packet.LevelSoundEvent: if pk.SoundType == packet.SoundEventAttackNoDamage { p.Combat().Attack(nil) + p.Clicks().HandleSwing() } } p.RunDetections(pk) diff --git a/player/player.go b/player/player.go index ca94f8b8..211740dd 100755 --- a/player/player.go +++ b/player/player.go @@ -181,6 +181,9 @@ type Player struct { // clientCombat is the component that handles validating combat with the client-state entity tracker. clientCombat CombatComponent + // clicks is the component that handles click actions from the player. + clicks ClicksComponent + // eventHandler is a handler that handles events such as punishments and flags from detections. eventHandler EventHandler diff --git a/utils/circular_queue.go b/utils/circular_queue.go index d76c4401..44185965 100644 --- a/utils/circular_queue.go +++ b/utils/circular_queue.go @@ -45,6 +45,15 @@ func (q *CircularQueue[T]) Get(index int) T { return q.items[(q.head+index)%len(q.items)] } +// Set sets the element at logical position index (0 = oldest). +// It panics if index is out of range. +func (q *CircularQueue[T]) Set(index int, item T) { + if index < 0 || index >= q.size { + panic("circular queue: index out of range") + } + q.items[(q.head+index)%len(q.items)] = item +} + func (q *CircularQueue[T]) Iter() iter.Seq[T] { return func(yield func(T) bool) { for index := range q.size { From ef604dd0597c58c501b9c589b02c34c29e9f6da1 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Wed, 20 Aug 2025 15:54:59 -0400 Subject: [PATCH 12/29] re-introduce no-slowdown & fast-eat mitigations --- entity/metadata.go | 130 ++++++++++++++++++- player/component/acknowledgement/movement.go | 6 + player/component/movement.go | 14 +- player/packet.go | 24 ++-- player/player.go | 4 +- 5 files changed, 159 insertions(+), 19 deletions(-) diff --git a/entity/metadata.go b/entity/metadata.go index ff0d9e21..ceac6088 100755 --- a/entity/metadata.go +++ b/entity/metadata.go @@ -11,7 +11,131 @@ const ( ) const ( - DataFlagSneaking = 1 - DataFlagSprinting = 3 - DataFlagImmobile = 16 + DataFlagOnFire = iota + DataFlagSneaking + DataFlagRiding + DataFlagSprinting + DataFlagAction + DataFlagInvisible + DataFlagTempted + DataFlagInLove + DataFlagSaddled + DataFlagPowered + DataFlagIgnited + DataFlagBaby + DataFlagConverting + DataFlagCritical + DataFlagCanShowNameTag + DataFlagAlwaysShowNameTag + DataFlagImmobile + DataFlagSilent + DataFlagWallClimbing + DataFlagCanClimb + DataFlagSwimmer + DataFlagCanFly + DataFlagWalker + DataFlagResting + DataFlagSitting + DataFlagAngry + DataFlagInterested + DataFlagCharged + DataFlagTamed + DataFlagOrphaned + DataFlagLeashed + DataFlagSheared + DataFlagGliding + DataFlagElder + DataFlagMoving + DataFlagBreathing + DataFlagChested + DataFlagStackable + DataFlagShowBase + DataFlagRearing + DataFlagVibrating + DataFlagIdling + DataFlagEvokerSpell + DataFlagChargeAttack + DataFlagWASDControlled + DataFlagCanPowerJump + DataFlagCanDash + DataFlagLinger + DataFlagHasCollision + DataFlagAffectedByGravity + DataFlagFireImmune + DataFlagDancing + DataFlagEnchanted + DataFlagShowTridentRope // tridents show an animated rope when enchanted with loyalty after they are thrown and return to their owner. To be combined with DATA_OWNER_EID + DataFlagContainerPrivate // inventory is private, doesn't drop contents when killed if true + DataFlagTransforming + DataFlagSpinAttack + DataFlagSwimming + DataFlagBribed // dolphins have this set when they go to find treasure for the player + DataFlagPregnant + DataFlagLayingEgg + DataFlagRiderCanPick // ??? + DataFlagTransitionSitting + DataFlagEating + DataFlagLayingDown + DataFlagSneezing + DataFlagTrusting + DataFlagRolling + DataFlagScared + DataFlagInScaffolding + DataFlagOverScaffolding + DataFlagFallThroughScaffolding + DataFlagBlocking // shield + DataFlagTransitionBlocking + DataFlagBlockedUsingShield + DataFlagBlockedUsingDamagedShield + DataFlagSleeping + DataFlagWantsToWake + DataFlagTradeInterest + DataFlagDoorBreaker // ... + DataFlagBreakingObstruction + DataFlagDoorOpener // ... + DataFlagIllagerCaptain + DataFlagStunned + DataFlagRoaring + DataFlagDelayedAttacking + DataFlagAvoidingMobs + DataFlagAvoidingBlock + DataFlagFacingTargetToRangeAttack + DataFlagHiddenWhenInvisible // ??????????????????? + DataFlagIsInUI + DataFlagStalking + DataFlagEmoting + DataFlagCelebrating + DataFlagAdmiring + DataFlagCelebratingSpecial + DataFlagOutOfControl + DataFlagRamAttack + DataFlagPlayingDead + DataFlagInAscendableBlock + DataFlagOverDescendableBlock + DataFlagCroaking + DataFlagEatMob + DataFlagJumpGoalJump + DataFlagEmerging + DataFlagSniffing + DataFlagDigging + DataFlagSonicBoom + DataFlagHasDashCooldown + DataFlagPushTowardsClosestSpace + DataFlagScenting + DataFlagRising + DataFlagHappy + DataFlagSearching + DataFlagCrawling + DataFlagTimerFlag1 + DataFlagTimerFlag2 + DataFlagTimerFlag3 + DataFlagBodyRotationBlocked + DataFlagRenderWhenInvisible + DataFlagRotationAxisAligned + DataFlagCollidable + DataFlagWASDAirControlled + DataFlagDoesServerAuthOnlyDismount + DataFlagBodyRotationAlwaysFollowsHead + + DataFlagNumberOfFlags ) diff --git a/player/component/acknowledgement/movement.go b/player/component/acknowledgement/movement.go index 92ee8a04..df5dc3e7 100644 --- a/player/component/acknowledgement/movement.go +++ b/player/component/acknowledgement/movement.go @@ -160,6 +160,12 @@ func (ack *PlayerUpdateActorData) Run() { flags := f.(int64) ack.mPlayer.Movement().SetImmobile(utils.HasDataFlag(entity.DataFlagImmobile, flags)) ack.mPlayer.Movement().SetServerSprint(utils.HasDataFlag(entity.DataFlagSprinting, flags)) + + // Forcefully remove this flag so that the client doesn't end up with any weird desync. + ack.metadata[entity.DataKeyFlags] = utils.RemoveDataFlag(flags, entity.DataFlagAction) + /*if utils.HasDataFlag(entity.DataFlagAction, flags) { + //ack.mPlayer.Message("hasAction? (%d)", ack.mPlayer.InputCount) + }*/ } } diff --git a/player/component/movement.go b/player/component/movement.go index 3e41e036..f1bb3119 100644 --- a/player/component/movement.go +++ b/player/component/movement.go @@ -654,7 +654,6 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { //assert.IsTrue(mc.mPlayer != nil, "parent player is null") //assert.IsTrue(pk != nil, "given player input is nil") //assert.IsTrue(mc.nonAuthoritative != nil, "non-authoritative data is null") - mc.nonAuthoritative.horizontalCollision = pk.InputData.Load(packet.InputFlagHorizontalCollision) mc.nonAuthoritative.verticalCollision = pk.InputData.Load(packet.InputFlagVerticalCollision) @@ -665,8 +664,6 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.nonAuthoritative.lastMov = mc.nonAuthoritative.mov mc.nonAuthoritative.mov = mc.nonAuthoritative.pos.Sub(mc.nonAuthoritative.lastPos) - mc.impulse = pk.MoveVector.Mul(0.98) - if pk.InputData.Load(packet.InputFlagStartFlying) { mc.nonAuthoritative.toggledFly = true if mc.trustFlyStatus { @@ -760,6 +757,16 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.sneaking = pk.InputData.Load(packet.InputFlagSneakDown) } + if mc.mPlayer.StartUseConsumableTick != 0 { + baseConsumeSpeed := float32(0.3) + if mc.sneaking { + baseConsumeSpeed *= 0.3 + } + pk.MoveVector[0] = game.ClampFloat(pk.MoveVector[0], -baseConsumeSpeed, baseConsumeSpeed) + pk.MoveVector[1] = game.ClampFloat(pk.MoveVector[1], -baseConsumeSpeed, baseConsumeSpeed) + //mc.mPlayer.Message("consuming (curr=%d start=%d)", mc.mPlayer.InputCount, mc.mPlayer.StartUseConsumableTick) + } + mc.jumping = pk.InputData.Load(packet.InputFlagStartJumping) mc.pressingJump = pk.InputData.Load(packet.InputFlagJumping) mc.jumpHeight = game.DefaultJumpHeight @@ -782,6 +789,7 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.gliding = true } + mc.impulse = pk.MoveVector.Mul(0.98) simulation.SimulatePlayerMovement(mc.mPlayer, mc) // On older versions, there seems to be a delay before the sprinting status is actually applied. diff --git a/player/packet.go b/player/packet.go index 56c06fea..63059254 100644 --- a/player/packet.go +++ b/player/packet.go @@ -168,28 +168,30 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { p.lastUseProjectileTick = p.InputCount inv, _ := p.inventory.WindowFromWindowID(protocol.WindowIDInventory) inv.SetSlot(int(tr.HotBarSlot), held.Grow(-1)) - } /* else if c, ok := held.Item().(item.Consumable); ok { - if p.startUseConsumableTick == 0 { - p.startUseConsumableTick = p.InputCount + } else if c, ok := held.Item().(item.Consumable); ok { + if p.StartUseConsumableTick == 0 { + p.StartUseConsumableTick = p.InputCount p.consumedSlot = int(tr.HotBarSlot) } else { - duration := p.InputCount - p.startUseConsumableTick + duration := p.InputCount - p.StartUseConsumableTick if duration < ((c.ConsumeDuration().Milliseconds() / 50) - 1) { - p.startUseConsumableTick = p.InputCount + p.StartUseConsumableTick = p.InputCount ctx.Cancel() p.inventory.ForceSync() - p.Message("item cooldown (attempted to consume in %d ticks, %d required)", duration, (c.ConsumeDuration().Milliseconds()/50)-1) + //p.Message("item cooldown (attempted to consume in %d ticks, %d required)", duration, (c.ConsumeDuration().Milliseconds()/50)-1) //_ = p.inventory.SyncSlot(protocol.WindowIDInventory, int(tr.HotBarSlot)) - //p.Popup("Item consumption cooldown") + p.Popup("Item consumption cooldown") return } - p.startUseConsumableTick = 0 + p.StartUseConsumableTick = 0 p.consumedSlot = 0 } - } */ + } } } else if tr, ok := pk.TransactionData.(*protocol.ReleaseItemTransactionData); ok { p.inventory.SetHeldSlot(int32(tr.HotBarSlot)) + p.StartUseConsumableTick = 0 + //p.Message("released item") } else if _, ok := pk.TransactionData.(*protocol.NormalTransactionData); ok { if len(pk.Actions) != 2 { p.Log().Debug("drop action should have exactly 2 actions, got different amount", "actionCount", len(pk.Actions)) @@ -237,8 +239,8 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { case *packet.MobEquipment: p.LastEquipmentData = pk p.inventory.SetHeldSlot(int32(pk.HotBarSlot)) - if p.startUseConsumableTick != 0 && p.consumedSlot != int(pk.HotBarSlot) { - p.startUseConsumableTick = 0 + if p.StartUseConsumableTick != 0 && p.consumedSlot != int(pk.HotBarSlot) { + p.StartUseConsumableTick = 0 p.consumedSlot = 0 } case *packet.Animate: diff --git a/player/player.go b/player/player.go index 211740dd..4edec3d1 100755 --- a/player/player.go +++ b/player/player.go @@ -123,8 +123,8 @@ type Player struct { // lastUseProjectileTick is the last tick the player used a projectile item. lastUseProjectileTick int64 - // startUseConsumableTick is the tick that the player started using a consumable item. - startUseConsumableTick int64 + // StartUseConsumableTick is the tick that the player started using a consumable item. + StartUseConsumableTick int64 // consumedSlot is the slot of the item that the player started consuming. consumedSlot int From f3a2a79886b7a6b906e205205843907e8a545877 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Wed, 20 Aug 2025 16:34:01 -0400 Subject: [PATCH 13/29] added two badpackets detections --- example/spectrum/spectrum.go | 4 +- player/component/inventory.go | 3 +- player/detection/bad_packet_e.go | 56 ++++++++++++++++++++++++ player/detection/bad_packet_f.go | 75 ++++++++++++++++++++++++++++++++ player/detection/register.go | 2 + 5 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 player/detection/bad_packet_e.go create mode 100644 player/detection/bad_packet_f.go diff --git a/example/spectrum/spectrum.go b/example/spectrum/spectrum.go index 0a596d6e..89a50353 100644 --- a/example/spectrum/spectrum.go +++ b/example/spectrum/spectrum.go @@ -157,9 +157,9 @@ func main() { proc.Player().SetCloser(func() { f.Close() }) - proc.Player().SetRecoverFunc(func(p *player.Player, err any) { + /* proc.Player().SetRecoverFunc(func(p *player.Player, err any) { debug.PrintStack() - }) + }) */ proc.Player().AddPerm(player.PermissionDebug) proc.Player().AddPerm(player.PermissionAlerts) proc.Player().AddPerm(player.PermissionLogs) diff --git a/player/component/inventory.go b/player/component/inventory.go index 16f12ebc..c4e29726 100644 --- a/player/component/inventory.go +++ b/player/component/inventory.go @@ -7,7 +7,6 @@ import ( "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/item/recipe" "github.com/df-mc/dragonfly/server/world" - "github.com/oomph-ac/oomph/game" "github.com/oomph-ac/oomph/player" "github.com/oomph-ac/oomph/player/component/acknowledgement" "github.com/oomph-ac/oomph/utils" @@ -186,7 +185,7 @@ func (c *InventoryComponent) HeldSlot() int32 { func (c *InventoryComponent) SetHeldSlot(heldSlot int32) { if heldSlot < 0 || heldSlot >= int32(inventorySizeHotbar) { - c.mPlayer.Disconnect(game.ErrorInvalidInventorySlot) + //c.mPlayer.Disconnect(game.ErrorInvalidInventorySlot) return } c.heldSlot = heldSlot diff --git a/player/detection/bad_packet_e.go b/player/detection/bad_packet_e.go new file mode 100644 index 00000000..0938189c --- /dev/null +++ b/player/detection/bad_packet_e.go @@ -0,0 +1,56 @@ +package detection + +import ( + "github.com/oomph-ac/oomph/player" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type BadPacketE struct { + mPlayer *player.Player + metadata *player.DetectionMetadata +} + +func New_BadPacketE(p *player.Player) *BadPacketE { + return &BadPacketE{ + mPlayer: p, + metadata: &player.DetectionMetadata{ + FailBuffer: 1, + MaxBuffer: 1, + MaxViolations: 1, + TrustDuration: -1, + }, + } +} + +func (*BadPacketE) Type() string { + return TypeBadPacket +} + +func (*BadPacketE) SubType() string { + return "E" +} + +func (*BadPacketE) Description() string { + return "Checks if the player is sending an invalid value for their MoveVector on client-side." +} + +func (*BadPacketE) Punishable() bool { + return true +} + +func (d *BadPacketE) Metadata() *player.DetectionMetadata { + return d.metadata +} + +func (d *BadPacketE) Detect(pk packet.Packet) { + i, ok := pk.(*packet.PlayerAuthInput) + if !ok { + return + } + + for index := range 2 { + if v := i.MoveVector[index]; v < -1 || v > 1 { + d.mPlayer.FailDetection(d, nil) + } + } +} diff --git a/player/detection/bad_packet_f.go b/player/detection/bad_packet_f.go new file mode 100644 index 00000000..29550bc1 --- /dev/null +++ b/player/detection/bad_packet_f.go @@ -0,0 +1,75 @@ +package detection + +import ( + "github.com/oomph-ac/oomph/player" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type BadPacketF struct { + mPlayer *player.Player + metadata *player.DetectionMetadata +} + +func New_BadPacketF(p *player.Player) *BadPacketF { + return &BadPacketF{ + mPlayer: p, + metadata: &player.DetectionMetadata{ + FailBuffer: 1, + MaxBuffer: 1, + MaxViolations: 1, + TrustDuration: -1, + }, + } +} + +func (*BadPacketF) Type() string { + return TypeBadPacket +} + +func (*BadPacketF) SubType() string { + return "F" +} + +func (*BadPacketF) Description() string { + return "Checks if the player's TriggerType is valid" +} + +func (*BadPacketF) Punishable() bool { + return true +} + +func (d *BadPacketF) Metadata() *player.DetectionMetadata { + return d.metadata +} + +func (d *BadPacketF) Detect(pk packet.Packet) { + tr, ok := pk.(*packet.InventoryTransaction) + if !ok || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) { + return + } + + switch dat := tr.TransactionData.(type) { + case *protocol.ReleaseItemTransactionData: + d.checkHotbarSlot(dat.HotBarSlot) + case *protocol.UseItemOnEntityTransactionData: + d.checkHotbarSlot(dat.HotBarSlot) + case *protocol.UseItemTransactionData: + d.checkHotbarSlot(dat.HotBarSlot) + if dat.ActionType != protocol.UseItemActionClickBlock { + return + } + if dat.TriggerType != protocol.TriggerTypePlayerInput && dat.TriggerType != protocol.TriggerTypeSimulationTick { + d.mPlayer.FailDetection(d, nil) + } + if dat.ClientPrediction != protocol.ClientPredictionFailure && dat.ClientPrediction != protocol.ClientPredictionSuccess { + d.mPlayer.FailDetection(d, nil) + } + } +} + +func (d *BadPacketF) checkHotbarSlot(slot int32) { + if slot < 0 || slot >= 9 { + d.mPlayer.FailDetection(d, nil) + } +} diff --git a/player/detection/register.go b/player/detection/register.go index 63ae25fe..a32173c6 100644 --- a/player/detection/register.go +++ b/player/detection/register.go @@ -14,6 +14,8 @@ func Register(p *player.Player) { p.RegisterDetection(New_BadPacketB(p)) p.RegisterDetection(New_BadPacketC(p)) p.RegisterDetection(New_BadPacketD(p)) + p.RegisterDetection(New_BadPacketE(p)) + p.RegisterDetection(New_BadPacketF(p)) // edition faker detections p.RegisterDetection(New_EditionFakerA(p)) From e9100a341396a01b2cbe4b2aec52b13eb02159b1 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Wed, 20 Aug 2025 23:31:17 -0400 Subject: [PATCH 14/29] remove evil variable that probably caused a bunch of issues --- example/spectrum/spectrum.go | 2 +- player/component/movement.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/example/spectrum/spectrum.go b/example/spectrum/spectrum.go index 89a50353..76f82433 100644 --- a/example/spectrum/spectrum.go +++ b/example/spectrum/spectrum.go @@ -71,7 +71,7 @@ func main() { oconfig.Global.Movement.VelocityAcceptanceThreshold = 0.077 oconfig.Global.Movement.PersuasionThreshold = 0.002 - oconfig.Global.Movement.CorrectionThreshold = 0.3 + oconfig.Global.Movement.CorrectionThreshold = 0.003 oconfig.Global.Combat.MaximumAttackAngle = 90 oconfig.Global.Combat.EnableClientEntityTracking = true diff --git a/player/component/movement.go b/player/component/movement.go index f1bb3119..b90042bf 100644 --- a/player/component/movement.go +++ b/player/component/movement.go @@ -690,9 +690,7 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.pressingSneak = pk.InputData.Load(packet.InputFlagSneaking) mc.pressingSprint = pk.InputData.Load(packet.InputFlagSprintDown) - hasForwardKeyPressed := mc.impulse.Y() > 1e-4 - startFlag, stopFlag := pk.InputData.Load(packet.InputFlagStartSprinting), pk.InputData.Load(packet.InputFlagStopSprinting) || !hasForwardKeyPressed - + startFlag, stopFlag := pk.InputData.Load(packet.InputFlagStartSprinting), pk.InputData.Load(packet.InputFlagStopSprinting) isNewVersionPlayer := mc.mPlayer.VersionInRange(player.GameVersion1_21_0, 65536) var needsSpeedAdjusted bool if startFlag && stopFlag /*&& hasForwardKeyPressed*/ { From 7391b4b888fa877e3d30ffc1693d3b05f26cc4c5 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Thu, 21 Aug 2025 09:20:47 -0400 Subject: [PATCH 15/29] exempt player on the tick they disable flight --- player/component/movement.go | 14 +++++++++++++- player/movement.go | 2 ++ player/simulation/movement.go | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/player/component/movement.go b/player/component/movement.go index b90042bf..4a0b95c6 100644 --- a/player/component/movement.go +++ b/player/component/movement.go @@ -123,6 +123,7 @@ type AuthoritativeMovementComponent struct { glideBoostTicks int64 flying, mayFly, trustFlyStatus bool + justDisabledFlight bool allowedInputs int64 hasFirstInput bool @@ -649,6 +650,11 @@ func (mc *AuthoritativeMovementComponent) SetTrustFlyStatus(trust bool) { mc.trustFlyStatus = trust } +// JustDisabledFlight returns true if the movement component just disabled flight. +func (mc *AuthoritativeMovementComponent) JustDisabledFlight() bool { + return mc.justDisabledFlight +} + // Update updates the states of the movement component from the given input. func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { //assert.IsTrue(mc.mPlayer != nil, "parent player is null") @@ -670,6 +676,9 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.flying = true } } else if pk.InputData.Load(packet.InputFlagStopFlying) { + if mc.flying { + mc.justDisabledFlight = true + } mc.flying = false mc.nonAuthoritative.toggledFly = false } @@ -831,7 +840,7 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { if mc.jumpDelay > 0 { mc.jumpDelay-- } - + mc.justDisabledFlight = false } // ServerUpdate updates certain states of the movement component based on a packet sent by the remote server. @@ -886,6 +895,9 @@ func (mc *AuthoritativeMovementComponent) Reset() { mc.vel = mc.nonAuthoritative.vel mc.lastMov = mc.nonAuthoritative.lastMov mc.mov = mc.nonAuthoritative.mov + if mc.flying { + mc.onGround = false + } } // PendingCorrections returns the number of pending corrections the movement component has. diff --git a/player/movement.go b/player/movement.go index 2a847891..9897453c 100644 --- a/player/movement.go +++ b/player/movement.go @@ -224,6 +224,8 @@ type MovementComponent interface { TrustFlyStatus() bool // SetTrustFlyStatus sets whether the movement component can trust the fly status sent by the client. SetTrustFlyStatus(bool) + // JustDisabledFlight returns true if the movement component just disabled flight. + JustDisabledFlight() bool // Update updates the states of the movement component from the given input. Update(input *packet.PlayerAuthInput) diff --git a/player/simulation/movement.go b/player/simulation/movement.go index 06adb456..c8da5988 100644 --- a/player/simulation/movement.go +++ b/player/simulation/movement.go @@ -274,7 +274,7 @@ func simulationIsReliable(p *player.Player, movement player.MovementComponent) b } return (p.GameMode == packet.GameTypeSurvival || p.GameMode == packet.GameTypeAdventure) && - !(movement.Flying() || movement.NoClip() || !p.Alive) + !(movement.Flying() || movement.JustDisabledFlight() || movement.NoClip() || !p.Alive) } func landOnBlock(movement player.MovementComponent, old mgl32.Vec3, blockUnder world.Block) { From cf23c12526dd2a8492cfc576435db40cf91906ff Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Thu, 21 Aug 2025 12:42:28 -0400 Subject: [PATCH 16/29] component/movement.go: init airSpeed --- player/component/movement.go | 1 + 1 file changed, 1 insertion(+) diff --git a/player/component/movement.go b/player/component/movement.go index 4a0b95c6..8dffb651 100644 --- a/player/component/movement.go +++ b/player/component/movement.go @@ -137,6 +137,7 @@ func NewAuthoritativeMovementComponent(p *player.Player) *AuthoritativeMovementC mPlayer: p, nonAuthoritative: &NonAuthoritativeMovement{}, defaultMovementSpeed: 0.1, + airSpeed: 0.02, } } From 9633ae100d37f0ac6db73b0df6db56e1a0021a74 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sun, 24 Aug 2025 09:33:05 -0400 Subject: [PATCH 17/29] fixed consumption when player is not hungry --- player/component/acknowledgement/movement.go | 7 +++++-- player/packet.go | 2 +- player/player.go | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/player/component/acknowledgement/movement.go b/player/component/acknowledgement/movement.go index df5dc3e7..b96d7e12 100644 --- a/player/component/acknowledgement/movement.go +++ b/player/component/acknowledgement/movement.go @@ -119,11 +119,14 @@ func NewUpdateAttributesACK(p *player.Player, attributes []protocol.Attribute) * func (ack *UpdateAttributes) Run() { for _, attribute := range ack.attributes { - if attribute.Name == "minecraft:movement" { + switch attribute.Name { + case "minecraft:movement": ack.mPlayer.Movement().SetMovementSpeed(attribute.Value) ack.mPlayer.Movement().SetDefaultMovementSpeed(attribute.Default) - } else if attribute.Name == "minecraft:health" { + case "minecraft:health": ack.mPlayer.Alive = attribute.Value > 0 + case "minecraft:player.hunger": + ack.mPlayer.IsHungry = attribute.Value < 20 } } } diff --git a/player/packet.go b/player/packet.go index 63059254..3cf93179 100644 --- a/player/packet.go +++ b/player/packet.go @@ -169,7 +169,7 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { inv, _ := p.inventory.WindowFromWindowID(protocol.WindowIDInventory) inv.SetSlot(int(tr.HotBarSlot), held.Grow(-1)) } else if c, ok := held.Item().(item.Consumable); ok { - if p.StartUseConsumableTick == 0 { + if p.StartUseConsumableTick == 0 && (c.AlwaysConsumable() || p.IsHungry) { p.StartUseConsumableTick = p.InputCount p.consumedSlot = int(tr.HotBarSlot) } else { diff --git a/player/player.go b/player/player.go index 4edec3d1..98527bfd 100755 --- a/player/player.go +++ b/player/player.go @@ -89,6 +89,9 @@ type Player struct { // cause desync due to the client's own interpolation. PendingCorrectionACK bool + // IsHungry is a boolean indicating whether the player is hungry. + IsHungry bool + // GameMode is the gamemode of the player. The player is exempt from movement predictions // if they are not in survival or adventure mode. GameMode int32 From 5f792e036af9c0062896e879be08529f927a652e Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sun, 24 Aug 2025 10:31:59 -0400 Subject: [PATCH 18/29] another fix for consumption (buckets) ffs --- player/packet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/packet.go b/player/packet.go index 3cf93179..1e8c5d82 100644 --- a/player/packet.go +++ b/player/packet.go @@ -169,7 +169,7 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { inv, _ := p.inventory.WindowFromWindowID(protocol.WindowIDInventory) inv.SetSlot(int(tr.HotBarSlot), held.Grow(-1)) } else if c, ok := held.Item().(item.Consumable); ok { - if p.StartUseConsumableTick == 0 && (c.AlwaysConsumable() || p.IsHungry) { + if p.StartUseConsumableTick == 0 && c.ConsumeDuration() > 0 && (c.AlwaysConsumable() || p.IsHungry) { p.StartUseConsumableTick = p.InputCount p.consumedSlot = int(tr.HotBarSlot) } else { From 8183fd665160984e260fb62650e199409db77a7d Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sun, 24 Aug 2025 10:43:22 -0400 Subject: [PATCH 19/29] fix unknown block exception for block breaking --- player/world.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/world.go b/player/world.go index 19fe7cae..cb4cd768 100644 --- a/player/world.go +++ b/player/world.go @@ -317,7 +317,7 @@ func (p *Player) getExpectedBlockBreakTime(pos protocol.BlockPos) float32 { } */ b := p.World().Block(df_cube.Pos{int(pos.X()), int(pos.Y()), int(pos.Z())}) - if blockHash, _ := b.Hash(); blockHash == math.MaxUint64 { + if _, blockHash := b.Hash(); blockHash == math.MaxUint64 { // If the block hash is MaxUint64, then the block is unknown to dragonfly. In the future, // we should implement more blocks to avoid this condition allowing clients to break those // blocks at any interval they please. From 9814a1177c4383c51f9f72f787bae09bae212ee8 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sun, 24 Aug 2025 10:44:09 -0400 Subject: [PATCH 20/29] small improvement for handling unknown blocks --- player/world.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/world.go b/player/world.go index cb4cd768..000817ce 100644 --- a/player/world.go +++ b/player/world.go @@ -317,7 +317,7 @@ func (p *Player) getExpectedBlockBreakTime(pos protocol.BlockPos) float32 { } */ b := p.World().Block(df_cube.Pos{int(pos.X()), int(pos.Y()), int(pos.Z())}) - if _, blockHash := b.Hash(); blockHash == math.MaxUint64 { + if hash1, hash2 := b.Hash(); hash1 == 0 && hash2 == math.MaxUint64 { // If the block hash is MaxUint64, then the block is unknown to dragonfly. In the future, // we should implement more blocks to avoid this condition allowing clients to break those // blocks at any interval they please. From 6a876575053f4745070f8b9ac680a65d6055cef3 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sun, 24 Aug 2025 10:46:42 -0400 Subject: [PATCH 21/29] block break exemption for air???? --- player/world.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/player/world.go b/player/world.go index 000817ce..c1319b32 100644 --- a/player/world.go +++ b/player/world.go @@ -325,8 +325,8 @@ func (p *Player) getExpectedBlockBreakTime(pos protocol.BlockPos) float32 { } if _, isAir := b.(block.Air); isAir { - // Is it possible that the server already thinks the block is broken? - return 1_000_000_000 + // Let the player send a break action for air, it won't affect anything in-game. + return 0 } else if utils.BlockName(b) == "minecraft:web" { // Cobwebs are not implemented in Dragonfly, and therefore the break time duration won't be accurate. // Just return 1 and accept when the client does break the cobweb. From 46070432911b4530572f9db50ca3fc0c6ac76fe4 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sun, 24 Aug 2025 15:02:09 -0400 Subject: [PATCH 22/29] try fixing weird bug by making all item stacks unbreakable? --- player/items.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/items.go b/player/items.go index 70a27104..6fa02f4b 100644 --- a/player/items.go +++ b/player/items.go @@ -27,7 +27,7 @@ func (p *Player) ConvertToStack(it protocol.ItemStack) item.Stack { t = nbter.DecodeNBT(it.NBTData).(world.Item) } s := item.NewStack(t, int(it.Count)) - return nbtconv_Item(it.NBTData, &s) + return nbtconv_Item(it.NBTData, &s).AsUnbreakable() } // noinspection ALL From b331bfe35f104c6b9e3a53b4be0bebc5a638cbd1 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sun, 24 Aug 2025 15:04:09 -0400 Subject: [PATCH 23/29] increase interaction distance --- game/minecraft.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/minecraft.go b/game/minecraft.go index 02bb8b54..601bb3cd 100755 --- a/game/minecraft.go +++ b/game/minecraft.go @@ -6,7 +6,7 @@ import ( ) const ( - MaxBlockInteractionDistance float32 = 6.01 + MaxBlockInteractionDistance float32 = 6.6 ) // sinTable ... From d090ceb8baab19f056234fe6dd4bf0ca56cb3775 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sun, 24 Aug 2025 17:27:36 -0400 Subject: [PATCH 24/29] cap client requested chunk radius to last one sent by server --- player/component/world.go | 13 ++++++++++++- player/packet.go | 2 +- player/world.go | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/player/component/world.go b/player/component/world.go index 55c22288..0454112c 100644 --- a/player/component/world.go +++ b/player/component/world.go @@ -22,9 +22,11 @@ import ( type WorldUpdaterComponent struct { mPlayer *player.Player + chunkRadius int32 + serverChunkRadius int32 + breakingBlockPos *protocol.BlockPos prevPlaceRequest *protocol.UseItemTransactionData - chunkRadius int32 initalInteractionAccepted bool } @@ -334,8 +336,17 @@ func (c *WorldUpdaterComponent) ValidateInteraction(pk *packet.InventoryTransact return true } +// SetServerChunkRadius sets the server chunk radius of the world updater component. +func (c *WorldUpdaterComponent) SetServerChunkRadius(radius int32) { + c.serverChunkRadius = radius + c.chunkRadius = radius +} + // SetChunkRadius sets the chunk radius of the world updater component. func (c *WorldUpdaterComponent) SetChunkRadius(radius int32) { + if radius > c.serverChunkRadius && c.serverChunkRadius != 0 { + radius = c.serverChunkRadius + } c.chunkRadius = radius } diff --git a/player/packet.go b/player/packet.go index 1e8c5d82..9f375157 100644 --- a/player/packet.go +++ b/player/packet.go @@ -322,7 +322,7 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { scale, )) case *packet.ChunkRadiusUpdated: - p.worldUpdater.SetChunkRadius(pk.ChunkRadius + 4) + p.worldUpdater.SetServerChunkRadius(pk.ChunkRadius + 4) case *packet.InventorySlot: p.inventory.HandleInventorySlot(pk) case *packet.InventoryContent: diff --git a/player/world.go b/player/world.go index c1319b32..5b6c03c2 100644 --- a/player/world.go +++ b/player/world.go @@ -31,6 +31,8 @@ type WorldUpdaterComponent interface { // ValidateInteraction validates if a player is allowed to perform an action on an interactable block. ValidateInteraction(pk *packet.InventoryTransaction) bool + // SetServerChunkRadius sets the server chunk radius of the world updater component. + SetServerChunkRadius(radius int32) // SetChunkRadius sets the chunk radius of the world updater component. SetChunkRadius(radius int32) // ChunkRadius returns the chunk radius of the world updater component. From fd680bae764ee3704a0314b8d02b99d5b682d872 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Wed, 27 Aug 2025 22:09:37 -0400 Subject: [PATCH 25/29] bad_packet_e: little lenience for android devices on intel CPUs --- player/detection/bad_packet_e.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/detection/bad_packet_e.go b/player/detection/bad_packet_e.go index 0938189c..e78f47b5 100644 --- a/player/detection/bad_packet_e.go +++ b/player/detection/bad_packet_e.go @@ -49,7 +49,7 @@ func (d *BadPacketE) Detect(pk packet.Packet) { } for index := range 2 { - if v := i.MoveVector[index]; v < -1 || v > 1 { + if v := i.MoveVector[index]; v < -1.001 || v > 1.001 { d.mPlayer.FailDetection(d, nil) } } From 9110d87a74d235d90e390677d369e9cd9ab8e53e Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Thu, 28 Aug 2025 10:36:11 -0400 Subject: [PATCH 26/29] bad_packet_f: validate hotbar slot on MobEquipment --- player/detection/bad_packet_f.go | 42 +++++++++++++++++--------------- player/packet.go | 4 ++- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/player/detection/bad_packet_f.go b/player/detection/bad_packet_f.go index 29550bc1..46030e79 100644 --- a/player/detection/bad_packet_f.go +++ b/player/detection/bad_packet_f.go @@ -32,7 +32,7 @@ func (*BadPacketF) SubType() string { } func (*BadPacketF) Description() string { - return "Checks if the player's TriggerType is valid" + return "Checks if the player's inventory actions are valid" } func (*BadPacketF) Punishable() bool { @@ -44,26 +44,28 @@ func (d *BadPacketF) Metadata() *player.DetectionMetadata { } func (d *BadPacketF) Detect(pk packet.Packet) { - tr, ok := pk.(*packet.InventoryTransaction) - if !ok || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) { - return - } - - switch dat := tr.TransactionData.(type) { - case *protocol.ReleaseItemTransactionData: - d.checkHotbarSlot(dat.HotBarSlot) - case *protocol.UseItemOnEntityTransactionData: - d.checkHotbarSlot(dat.HotBarSlot) - case *protocol.UseItemTransactionData: - d.checkHotbarSlot(dat.HotBarSlot) - if dat.ActionType != protocol.UseItemActionClickBlock { - return - } - if dat.TriggerType != protocol.TriggerTypePlayerInput && dat.TriggerType != protocol.TriggerTypeSimulationTick { - d.mPlayer.FailDetection(d, nil) + switch pk := pk.(type) { + case *packet.InventoryTransaction: + switch dat := pk.TransactionData.(type) { + case *protocol.ReleaseItemTransactionData: + d.checkHotbarSlot(dat.HotBarSlot) + case *protocol.UseItemOnEntityTransactionData: + d.checkHotbarSlot(dat.HotBarSlot) + case *protocol.UseItemTransactionData: + d.checkHotbarSlot(dat.HotBarSlot) + if dat.ActionType != protocol.UseItemActionClickBlock || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) { + return + } + if dat.TriggerType != protocol.TriggerTypePlayerInput && dat.TriggerType != protocol.TriggerTypeSimulationTick { + d.mPlayer.FailDetection(d, nil) + } + if dat.ClientPrediction != protocol.ClientPredictionFailure && dat.ClientPrediction != protocol.ClientPredictionSuccess { + d.mPlayer.FailDetection(d, nil) + } } - if dat.ClientPrediction != protocol.ClientPredictionFailure && dat.ClientPrediction != protocol.ClientPredictionSuccess { - d.mPlayer.FailDetection(d, nil) + case *packet.MobEquipment: + if pk.WindowID == protocol.WindowIDInventory { + d.checkHotbarSlot(int32(pk.HotBarSlot)) } } } diff --git a/player/packet.go b/player/packet.go index 9f375157..6f2425f5 100644 --- a/player/packet.go +++ b/player/packet.go @@ -238,7 +238,9 @@ func (p *Player) HandleClientPacket(ctx *context.HandlePacketContext) { } case *packet.MobEquipment: p.LastEquipmentData = pk - p.inventory.SetHeldSlot(int32(pk.HotBarSlot)) + if pk.WindowID == protocol.WindowIDInventory { + p.inventory.SetHeldSlot(int32(pk.HotBarSlot)) + } if p.StartUseConsumableTick != 0 && p.consumedSlot != int(pk.HotBarSlot) { p.StartUseConsumableTick = 0 p.consumedSlot = 0 From 5c6bf5832c02f98363129b3403bf45f59db3c82d Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Fri, 29 Aug 2025 22:38:56 -0400 Subject: [PATCH 27/29] feat: impl configurable latency cutoffs (check oconfig for updates) (#96) --- example/spectrum/spectrum.go | 12 ++++--- player/acks.go | 7 ++++ player/component/acknowledgement/block.go | 19 +++++++++-- player/component/acknowledgement/movement.go | 24 ++++++++++++-- player/component/acks.go | 30 ++++++++++------- player/component/movement.go | 12 ++++++- player/component/world.go | 34 ++++++++++++++++++-- player/opts.go | 1 + player/packet.go | 8 ++--- player/player.go | 1 + 10 files changed, 119 insertions(+), 29 deletions(-) diff --git a/example/spectrum/spectrum.go b/example/spectrum/spectrum.go index 76f82433..27d80070 100644 --- a/example/spectrum/spectrum.go +++ b/example/spectrum/spectrum.go @@ -75,7 +75,11 @@ func main() { oconfig.Global.Combat.MaximumAttackAngle = 90 oconfig.Global.Combat.EnableClientEntityTracking = true - oconfig.Global.Combat.MaxRewind = 6 + + oconfig.Global.Network.GlobalMovementCutoffThreshold = 6 + oconfig.Global.Network.MaxEntityRewind = 6 + oconfig.Global.Network.MaxKnockbackDelay = -1 + oconfig.Global.Network.MaxBlockUpdateDelay = -1 /* packs, err := utils.ResourcePacks("/home/ethaniccc/temp/proxy-packs", "content_keys.json") if err != nil { @@ -157,9 +161,9 @@ func main() { proc.Player().SetCloser(func() { f.Close() }) - /* proc.Player().SetRecoverFunc(func(p *player.Player, err any) { - debug.PrintStack() - }) */ + proc.Player().SetRecoverFunc(func(p *player.Player, err any) { + panic(err) + }) proc.Player().AddPerm(player.PermissionDebug) proc.Player().AddPerm(player.PermissionAlerts) proc.Player().AddPerm(player.PermissionLogs) diff --git a/player/acks.go b/player/acks.go index 359f3b49..05b42a37 100644 --- a/player/acks.go +++ b/player/acks.go @@ -40,6 +40,13 @@ type Acknowledgment interface { Run() } +// TickableAcknowledgment is an interface for acknowledgments that can be ticked whenever the player sends a PlayerAuthInput packet. +// This is primarily used for expiring movement acknowledgments. +type TickableAcknowledgment interface { + // Tick ticks the acknowledgment. + Tick() +} + func (p *Player) SetACKs(c AcknowledgmentComponent) { p.acks = c } diff --git a/player/component/acknowledgement/block.go b/player/component/acknowledgement/block.go index 7c38210c..8fa10528 100644 --- a/player/component/acknowledgement/block.go +++ b/player/component/acknowledgement/block.go @@ -15,11 +15,12 @@ type UpdateBlock struct { b world.Block pos df_cube.Pos - valid bool + expiresIn int64 + valid bool } -func NewUpdateBlockACK(p *player.Player, pos df_cube.Pos, b world.Block) *UpdateBlock { - return &UpdateBlock{mPlayer: p, pos: pos, b: b, valid: true} +func NewUpdateBlockACK(p *player.Player, pos df_cube.Pos, b world.Block, expiresIn int64) *UpdateBlock { + return &UpdateBlock{mPlayer: p, pos: pos, b: b, valid: true, expiresIn: expiresIn} } func (ack *UpdateBlock) Run() { @@ -27,6 +28,18 @@ func (ack *UpdateBlock) Run() { return } ack.mPlayer.World().SetBlock(ack.pos, ack.b, nil) + ack.valid = false +} + +func (ack *UpdateBlock) Tick() { + if !ack.valid { + return + } + ack.expiresIn-- + if ack.expiresIn <= 0 { + ack.mPlayer.Dbg.Notify(player.DebugModeLatency, true, "updateBlock ack for %T at %v lag-compensation expired", ack.b, ack.pos) + ack.Run() + } } func (ack *UpdateBlock) Invalidate() { diff --git a/player/component/acknowledgement/movement.go b/player/component/acknowledgement/movement.go index b96d7e12..edecb58b 100644 --- a/player/component/acknowledgement/movement.go +++ b/player/component/acknowledgement/movement.go @@ -4,6 +4,7 @@ import ( "github.com/df-mc/dragonfly/server/entity/effect" "github.com/go-gl/mathgl/mgl32" "github.com/oomph-ac/oomph/entity" + "github.com/oomph-ac/oomph/game" "github.com/oomph-ac/oomph/player" "github.com/oomph-ac/oomph/utils" "github.com/sandertv/gophertunnel/minecraft/protocol" @@ -176,14 +177,31 @@ func (ack *PlayerUpdateActorData) Run() { type Knockback struct { mPlayer *player.Player knockback mgl32.Vec3 + + expiresIn int64 + ran bool } -func NewKnockbackACK(p *player.Player, knockback mgl32.Vec3) *Knockback { - return &Knockback{mPlayer: p, knockback: knockback} +func NewKnockbackACK(p *player.Player, knockback mgl32.Vec3, expiresIn int64) *Knockback { + return &Knockback{mPlayer: p, knockback: knockback, expiresIn: expiresIn} } func (ack *Knockback) Run() { - ack.mPlayer.Movement().SetKnockback(ack.knockback) + if !ack.ran { + ack.mPlayer.Movement().SetKnockback(ack.knockback) + ack.ran = true + } +} + +func (ack *Knockback) Tick() { + if ack.ran { + return + } + ack.expiresIn-- + if ack.expiresIn <= 0 { + ack.mPlayer.Dbg.Notify(player.DebugModeLatency, true, "knockback ack for %T (%v) lag-compensation expired", ack.knockback, game.RoundVec32(ack.knockback, 4)) + ack.Run() + } } // MovementCorrection is an acknowledgment that is ran whenever the player receives a movement correction from the server. diff --git a/player/component/acks.go b/player/component/acks.go index 1250f585..438edceb 100644 --- a/player/component/acks.go +++ b/player/component/acks.go @@ -1,7 +1,6 @@ package component import ( - "fmt" "math/rand/v2" "github.com/oomph-ac/oomph/game" @@ -12,8 +11,7 @@ import ( ) const ( - ACK_DIVIDER = 1_000 - MAX_ALLOWED_PENDING_ACKS = 1200 // ~ 60 seconds worth of pending ACKs + AckDivider = 1000 ) // ACKComponent is the component of the player that is responsible for sending and handling @@ -34,7 +32,7 @@ func NewACKComponent(p *player.Player) *ACKComponent { mPlayer: p, ticksSinceLastResponse: 0, - pending: make([]*ackBatch, 0, MAX_ALLOWED_PENDING_ACKS), + pending: make([]*ackBatch, 0, p.Opts().Network.MaxACKTimeout*20), } c.Refresh() @@ -55,9 +53,9 @@ func (ackC *ACKComponent) Add(ack player.Acknowledgment) { func (ackC *ACKComponent) Execute(ackID int64) bool { ackC.mPlayer.Dbg.Notify(player.DebugModeACKs, true, "got raw ACK ID %d", ackID) if !ackC.legacyMode { - ackID /= ACK_DIVIDER + ackID /= AckDivider if ackC.mPlayer.ClientDat.DeviceOS != protocol.DeviceOrbis { - ackID /= ACK_DIVIDER + ackID /= AckDivider } } @@ -113,7 +111,7 @@ func (ackC *ACKComponent) Responsive() bool { if len(ackC.pending) == 0 { return true } - return ackC.ticksSinceLastResponse <= MAX_ALLOWED_PENDING_ACKS + return ackC.ticksSinceLastResponse <= int64(ackC.mPlayer.Opts().Network.MaxACKTimeout)*20 } // Legacy returns true if the acknowledgment component is using legacy mode. @@ -130,6 +128,16 @@ func (ackC *ACKComponent) SetLegacy(legacy bool) { func (ackC *ACKComponent) Tick(client bool) { if client { ackC.clientTicked = true + + // Tick all expiring tickable acknowledgments. + for _, batch := range ackC.pending { + for _, ack := range batch.acks { + if tickable, ok := ack.(player.TickableAcknowledgment); ok { + tickable.Tick() + } + } + } + return } @@ -143,7 +151,7 @@ func (ackC *ACKComponent) Tick(client bool) { } // Validate that there are no duplicate timestamps. - knownTimestamps := make(map[int64]struct{}) + /* knownTimestamps := make(map[int64]struct{}, len(ackC.pending)) for _, batch := range ackC.pending { _, exists := knownTimestamps[batch.timestamp] knownTimestamps[batch.timestamp] = struct{}{} @@ -154,7 +162,7 @@ func (ackC *ACKComponent) Tick(client bool) { ackC.mPlayer.Disconnect(game.ErrorInternalDuplicateACK) break } - } + } */ // If the client hasn't sent us a PlayerAuthInput packet, we can assume that their client may be // frozen. In this case, we don't increase the ticksSinceLastResponse counter. @@ -181,7 +189,7 @@ func (ackC *ACKComponent) Flush() { timestamp := ackC.currentBatch.timestamp if ackC.legacyMode && ackC.mPlayer.ClientDat.DeviceOS == protocol.DeviceOrbis { - timestamp /= ACK_DIVIDER + timestamp /= AckDivider } ackC.mPlayer.SendPacketToClient(&packet.NetworkStackLatency{ @@ -211,7 +219,7 @@ func (ackC *ACKComponent) Refresh() { ackC.SetTimestamp(int64(rand.Uint32())) if ackC.legacyMode { // On older versions of the game, timestamps in NetworkStackLatency were sent to the nearest thousand. - ackC.currentBatch.timestamp *= ACK_DIVIDER + ackC.currentBatch.timestamp *= AckDivider } unique := true diff --git a/player/component/movement.go b/player/component/movement.go index 8dffb651..fbcc01ec 100644 --- a/player/component/movement.go +++ b/player/component/movement.go @@ -877,7 +877,17 @@ func (mc *AuthoritativeMovementComponent) ServerUpdate(pk packet.Packet) { case *packet.SetActorData: mc.mPlayer.ACKs().Add(acknowledgement.NewUpdateActorData(mc.mPlayer, pk.EntityMetadata)) case *packet.SetActorMotion: - mc.mPlayer.ACKs().Add(acknowledgement.NewKnockbackACK(mc.mPlayer, pk.Velocity)) + networkOpts := mc.mPlayer.Opts().Network + kbTimeout := int64(networkOpts.MaxKnockbackDelay) + if kbTimeout < 0 { + kbTimeout = 1_000_000_000 + } + kbAck := acknowledgement.NewKnockbackACK(mc.mPlayer, pk.Velocity, kbTimeout) + if cutoff := networkOpts.GlobalMovementCutoffThreshold; cutoff >= 0 && mc.mPlayer.ServerTick-mc.mPlayer.ClientTick >= int64(cutoff) { + kbAck.Run() + } else { + mc.mPlayer.ACKs().Add(kbAck) + } case *packet.UpdateAbilities: mc.mPlayer.ACKs().Add(acknowledgement.NewUpdateAbilitiesACK(mc.mPlayer, pk.AbilityData)) case *packet.UpdateAttributes: diff --git a/player/component/world.go b/player/component/world.go index 0454112c..4c10d74d 100644 --- a/player/component/world.go +++ b/player/component/world.go @@ -75,7 +75,17 @@ func (c *WorldUpdaterComponent) HandleUpdateBlock(pk *packet.UpdateBlock) { // TODO: Add a block policy to allow servers to determine whether block updates should be lag-compensated or if movement should // use the latest world state instantly. - c.mPlayer.ACKs().Add(acknowledgement.NewUpdateBlockACK(c.mPlayer, pos, b)) + networkOpts := c.mPlayer.Opts().Network + timeout := int64(networkOpts.MaxBlockUpdateDelay) + if timeout < 0 { + timeout = 1_000_000_000 + } + blockAck := acknowledgement.NewUpdateBlockACK(c.mPlayer, pos, b, timeout) + if cutoff := networkOpts.GlobalMovementCutoffThreshold; cutoff >= 0 && c.mPlayer.ServerTick-c.mPlayer.ClientTick >= int64(cutoff) { + blockAck.Run() + } else { + c.mPlayer.ACKs().Add(blockAck) + } } // HandleUpdateSubChunkBlocks handles an UpdateSubChunkBlocks packet from the server. @@ -84,6 +94,14 @@ func (c *WorldUpdaterComponent) HandleUpdateSubChunkBlocks(pk *packet.UpdateSubC c.mPlayer.ACKs().Add(acknowledgement.NewPlayerInitalizedACK(c.mPlayer)) } + networkOpts := c.mPlayer.Opts().Network + blockAckTimeout := int64(networkOpts.MaxBlockUpdateDelay) + if blockAckTimeout < 0 { + blockAckTimeout = 1_000_000_000 + } + cutoff := networkOpts.GlobalMovementCutoffThreshold + useLagComp := cutoff >= 0 && c.mPlayer.ServerTick-c.mPlayer.ClientTick >= int64(cutoff) + // TODO: Does the sub-chunk position sent in this packet even matter? for _, entry := range pk.Blocks { pos := df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())} @@ -92,7 +110,12 @@ func (c *WorldUpdaterComponent) HandleUpdateSubChunkBlocks(pk *packet.UpdateSubC c.mPlayer.Log().Warn("unable to find block with runtime ID", "blockRuntimeID", entry.BlockRuntimeID) b = block.Air{} } - c.mPlayer.ACKs().Add(acknowledgement.NewUpdateBlockACK(c.mPlayer, pos, b)) + blockAck := acknowledgement.NewUpdateBlockACK(c.mPlayer, pos, b, blockAckTimeout) + if useLagComp { + blockAck.Run() + } else { + c.mPlayer.ACKs().Add(blockAck) + } } for _, entry := range pk.Extra { pos := df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())} @@ -101,7 +124,12 @@ func (c *WorldUpdaterComponent) HandleUpdateSubChunkBlocks(pk *packet.UpdateSubC c.mPlayer.Log().Warn("unable to find block with runtime ID", "blockRuntimeID", entry.BlockRuntimeID) b = block.Air{} } - c.mPlayer.ACKs().Add(acknowledgement.NewUpdateBlockACK(c.mPlayer, pos, b)) + blockAck := acknowledgement.NewUpdateBlockACK(c.mPlayer, pos, b, blockAckTimeout) + if useLagComp { + blockAck.Run() + } else { + c.mPlayer.ACKs().Add(blockAck) + } } } diff --git a/player/opts.go b/player/opts.go index 2cdf4769..c6df9e54 100644 --- a/player/opts.go +++ b/player/opts.go @@ -5,6 +5,7 @@ import "github.com/oomph-ac/oconfig" type Opts struct { Combat oconfig.CombatOpts Movement oconfig.MovementOpts + Network oconfig.NetworkOpts } func (p *Player) Opts() *Opts { diff --git a/player/packet.go b/player/packet.go index 6f2425f5..929ea734 100644 --- a/player/packet.go +++ b/player/packet.go @@ -282,7 +282,7 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { pk.EntityMetadata, pk.Position, pk.Velocity, - p.Opts().Combat.MaxRewind, + p.Opts().Network.MaxEntityRewind, false, width, height, @@ -293,7 +293,7 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { pk.EntityMetadata, pk.Position, pk.Velocity, - p.Opts().Combat.MaxRewind, + p.Opts().Network.MaxEntityRewind, false, width, height, @@ -306,7 +306,7 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { pk.EntityMetadata, pk.Position, pk.Velocity, - p.Opts().Combat.MaxRewind, + p.Opts().Network.MaxEntityRewind, true, width, height, @@ -317,7 +317,7 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { pk.EntityMetadata, pk.Position, pk.Velocity, - p.Opts().Combat.MaxRewind, + p.Opts().Network.MaxEntityRewind, true, width, height, diff --git a/player/player.go b/player/player.go index 98527bfd..b0dbadd6 100755 --- a/player/player.go +++ b/player/player.go @@ -257,6 +257,7 @@ func New(log *slog.Logger, mState MonitoringState, listener *minecraft.Listener) p.opts = new(Opts) p.opts.Combat = oconfig.Combat() p.opts.Movement = oconfig.Movement() + p.opts.Network = oconfig.Network() p.world = world.New(func(msg string, args ...any) { p.Dbg.Notify(DebugModeChunks, true, msg, args...) From 5b4e3a6244c633fc354cfa9fd96cf465c5b32922 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Thu, 4 Sep 2025 22:25:02 -0400 Subject: [PATCH 28/29] fix positions on still entities (#97) --- entity/entity.go | 8 ++++---- go.mod | 2 +- player/packet.go | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/entity/entity.go b/entity/entity.go index f5fcdbf8..18d880db 100755 --- a/entity/entity.go +++ b/entity/entity.go @@ -39,14 +39,14 @@ type Entity struct { } // New creates and returns a new Entity instance. -func New(entType string, metadata map[uint32]any, pos, vel mgl32.Vec3, historySize int, isPlayer bool, width, height, scale float32) *Entity { +func New(entType string, metadata map[uint32]any, pos mgl32.Vec3, historySize int, isPlayer bool, width, height, scale float32) *Entity { e := &Entity{ Type: entType, Metadata: metadata, Position: pos, PrevPosition: pos, - RecvPosition: pos.Add(vel), + RecvPosition: pos, Width: width, Height: height, @@ -56,10 +56,10 @@ func New(entType string, metadata map[uint32]any, pos, vel mgl32.Vec3, historySi IsPlayer: isPlayer, } - e.InterpolationTicks = EntityMobInterpolationTicks + /* e.InterpolationTicks = EntityMobInterpolationTicks if isPlayer { e.InterpolationTicks = EntityPlayerInterpolationTicks - } + } */ return e } diff --git a/go.mod b/go.mod index 0dce8ad5..0503cfb7 100755 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/oomph-ac/multiversion v0.0.0-20250311011509-e9c78bda67c1 github.com/oomph-ac/oconfig v0.0.0-20250315200330-e36f34d634e5 github.com/sandertv/go-raknet v1.14.3-0.20250305181847-6af3e95113d6 - github.com/sandertv/gophertunnel v1.48.1 + github.com/sandertv/gophertunnel v1.49.0 github.com/zeebo/xxh3 v1.0.2 golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc ) diff --git a/player/packet.go b/player/packet.go index 929ea734..f253d20f 100644 --- a/player/packet.go +++ b/player/packet.go @@ -281,7 +281,6 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { pk.EntityType, pk.EntityMetadata, pk.Position, - pk.Velocity, p.Opts().Network.MaxEntityRewind, false, width, @@ -292,7 +291,6 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { pk.EntityType, pk.EntityMetadata, pk.Position, - pk.Velocity, p.Opts().Network.MaxEntityRewind, false, width, @@ -305,7 +303,6 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { "", pk.EntityMetadata, pk.Position, - pk.Velocity, p.Opts().Network.MaxEntityRewind, true, width, @@ -316,7 +313,6 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { "", pk.EntityMetadata, pk.Position, - pk.Velocity, p.Opts().Network.MaxEntityRewind, true, width, From c4752ade1e18d5f132adce10893317bb8f090803 Mon Sep 17 00:00:00 2001 From: ethaniccc Date: Sat, 20 Sep 2025 22:01:04 -0400 Subject: [PATCH 29/29] scaffold_b.go: improvements --- player/detection/scaffold_b.go | 38 +++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go index c91bfcc3..6a3e5fd4 100644 --- a/player/detection/scaffold_b.go +++ b/player/detection/scaffold_b.go @@ -18,7 +18,9 @@ type ScaffoldB struct { mPlayer *player.Player metadata *player.DetectionMetadata - initialFace cube.Face + initialFace cube.Face + prevSimBlockPos cube.Pos + hasPrevSimBlockPos bool } func New_ScaffoldB(p *player.Player) *ScaffoldB { @@ -74,6 +76,7 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { blockFace := cube.Face(dat.BlockFace) if dat.TriggerType == protocol.TriggerTypePlayerInput { d.initialFace = faceNotSet + d.hasPrevSimBlockPos = false } else if d.initialFace == faceNotSet && blockFace != cube.FaceUp && blockFace != cube.FaceDown { d.initialFace = blockFace } @@ -93,6 +96,9 @@ func (d *ScaffoldB) Detect(pk packet.Packet) { } else { d.mPlayer.PassDetection(d, 0.5) } + if d.initialFace != faceNotSet && (blockFace == cube.FaceUp || blockFace == cube.FaceDown) { + d.initialFace = blockFace + } } func (d *ScaffoldB) isFaceInteractable( @@ -107,6 +113,13 @@ func (d *ScaffoldB) isFaceInteractable( floorPosStart := cube.PosFromVec3(startPos) floorPosEnd := cube.PosFromVec3(endPos) + eyeOffset := game.DefaultPlayerHeightOffset + if d.mPlayer.Movement().Sneaking() { + eyeOffset = game.SneakingPlayerHeightOffset + } + floorEyeStart := int(startPos[1] + eyeOffset) + floorEyeEnd := int(endPos[1] + eyeOffset) + if !isClientInput { interactableFaces[cube.FaceDown] = struct{}{} interactableFaces[cube.FaceUp] = struct{}{} @@ -114,12 +127,31 @@ func (d *ScaffoldB) isFaceInteractable( interactableFaces[d.initialFace] = struct{}{} interactableFaces[d.initialFace.Opposite()] = struct{}{} } + + prevPos := d.prevSimBlockPos + d.prevSimBlockPos = blockPos + d.hasPrevSimBlockPos = true + + if d.hasPrevSimBlockPos { + found := false + for iFace := range interactableFaces { + if prevPos.Side(iFace) == blockPos { + found = true + break + } + } + // Simulation placements must be in a chain and not seperated from each other. + if !found { + d.mPlayer.Log().Debug("scaffold_b (invalid sim placement)", "blockPos", blockPos, "prevSimBlockPos", prevPos, "interactableFaces", interactableFaces) + return false + } + } } else { // Check for the Y-axis faces first. // If floor(eyePos.Y) < blockPos.Y -> the bottom face is interactable. // If floor(eyePos.Y) > blockPos.Y -> the top face is interactable. - isBelowBlock := floorPosStart[1] < blockY || floorPosEnd[1] < blockY - isAboveBlock := floorPosStart[1] > blockY || floorPosEnd[1] > blockY + isBelowBlock := floorEyeStart < blockY || floorEyeEnd < blockY + isAboveBlock := floorEyeStart > blockY || floorEyeEnd > blockY isOnBlock := floorPosStart[1] == blockY+1 || floorPosEnd[1] == blockY+1 if isBelowBlock { interactableFaces[cube.FaceDown] = struct{}{}