Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7f4573b
scaffold_b.go: impl (open src since it just enforces vanilla)
ethaniccc Jul 25, 2025
0372434
detection/scaffold_b.go: fixes :)
ethaniccc Jul 26, 2025
f4f1c39
detection/scaffold_b.go: ffs?!
ethaniccc Jul 26, 2025
6bc9142
scaffold_b.go: actually FIX TS godbless
ethaniccc Jul 27, 2025
425e073
scaffold_b.go: little leniency for ts because MCBE client weird
ethaniccc Jul 27, 2025
a543bde
update
ethaniccc Aug 16, 2025
c934e1a
Merge branch 'master' into feat/scaffold-dtc
ethaniccc Aug 16, 2025
3f79329
detection/scaffold_b.go: make more strict
ethaniccc Aug 16, 2025
f2239f3
detection/scaffold_b.go: remove debug
ethaniccc Aug 16, 2025
d89dbf4
detection/scaffold_b.go: only run if client is holding block and if c…
ethaniccc Aug 19, 2025
114d101
we already check the client prediction here ffs
ethaniccc Aug 19, 2025
3a75afa
detection/scaffold_b.go: get rid of faceNotInit and don't use eyePos
ethaniccc Aug 20, 2025
e5cf9e2
Merge branch 'master' into feat/scaffold-dtc
ethaniccc Aug 20, 2025
1dd7734
add CPS limits (check oconfig)
ethaniccc Aug 20, 2025
ef604dd
re-introduce no-slowdown & fast-eat mitigations
ethaniccc Aug 20, 2025
f3a2a79
added two badpackets detections
ethaniccc Aug 20, 2025
e9100a3
remove evil variable that probably caused a bunch of issues
ethaniccc Aug 21, 2025
7391b4b
exempt player on the tick they disable flight
ethaniccc Aug 21, 2025
cf23c12
component/movement.go: init airSpeed
ethaniccc Aug 21, 2025
9633ae1
fixed consumption when player is not hungry
ethaniccc Aug 24, 2025
5f792e0
another fix for consumption (buckets) ffs
ethaniccc Aug 24, 2025
8183fd6
fix unknown block exception for block breaking
ethaniccc Aug 24, 2025
9814a11
small improvement for handling unknown blocks
ethaniccc Aug 24, 2025
6a87657
block break exemption for air????
ethaniccc Aug 24, 2025
4607043
try fixing weird bug by making all item stacks unbreakable?
ethaniccc Aug 24, 2025
b331bfe
increase interaction distance
ethaniccc Aug 24, 2025
d090ceb
cap client requested chunk radius to last one sent by server
ethaniccc Aug 24, 2025
fd680ba
bad_packet_e: little lenience for android devices on intel CPUs
ethaniccc Aug 28, 2025
9110d87
bad_packet_f: validate hotbar slot on MobEquipment
ethaniccc Aug 28, 2025
5c6bf58
feat: impl configurable latency cutoffs (check oconfig for updates) (…
ethaniccc Aug 30, 2025
5b4e3a6
fix positions on still entities (#97)
ethaniccc Sep 5, 2025
dfbb0fe
Merge branch 'master' into feat/scaffold-dtc
ethaniccc Sep 19, 2025
44b8f87
Merge branch 'master' into feat/scaffold-dtc
ethaniccc Sep 19, 2025
219e522
Merge branch 'master' into feat/scaffold-dtc
ethaniccc Sep 19, 2025
1d73604
Merge branch 'master' into feat/scaffold-dtc
ethaniccc Sep 19, 2025
c4752ad
scaffold_b.go: improvements
ethaniccc Sep 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions game/aabb.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,21 +118,21 @@ 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
} else if t > 1 {
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.
Expand Down Expand Up @@ -185,17 +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 hit2 {
return point2, true
}

possibleBB := cube.Box(point1.X(), point1.Y(), point1.Z(), point2.X(), point2.Y(), point2.Z())
return ClosestPointToBBox(origin, possibleBB), true
}
Expand Down
20 changes: 20 additions & 0 deletions game/math.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ 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
}

// 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 {
Expand Down
2 changes: 1 addition & 1 deletion player/component/world.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,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.
Expand Down
6 changes: 6 additions & 0 deletions player/detection/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ 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_ScaffoldC(p))

//p.RegisterDetection(New_NukerA(p))

// killaura detections
Expand Down
206 changes: 206 additions & 0 deletions player/detection/scaffold_b.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
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"
"github.com/oomph-ac/oomph/player"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)

var (
faceNotSet cube.Face = -1
)

type ScaffoldB struct {
mPlayer *player.Player
metadata *player.DetectionMetadata

initialFace cube.Face
prevSimBlockPos cube.Pos
hasPrevSimBlockPos bool
}

func New_ScaffoldB(p *player.Player) *ScaffoldB {
return &ScaffoldB{
mPlayer: p,
metadata: &player.DetectionMetadata{
FailBuffer: 1.01,
MaxBuffer: 1.5,

MaxViolations: 10,
},
initialFace: faceNotSet,
}
}

func (d *ScaffoldB) Type() string {
return TypeScaffold
}

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 || !d.mPlayer.VersionInRange(player.GameVersion1_21_20, protocol.CurrentProtocol) {
return
}
inHand := d.mPlayer.Inventory().Holding()
if _, isBlock := inHand.Item().(world.Block); !isBlock {
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
d.hasPrevSimBlockPos = false
} else if d.initialFace == faceNotSet && blockFace != cube.FaceUp && blockFace != cube.FaceDown {
d.initialFace = blockFace
}
if dat.ClientPrediction == protocol.ClientPredictionFailure {
return
}

blockPos := cube.Pos{int(dat.BlockPosition[0]), int(dat.BlockPosition[1]), int(dat.BlockPosition[2])}
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)
}
if d.initialFace != faceNotSet && (blockFace == cube.FaceUp || blockFace == cube.FaceDown) {
d.initialFace = blockFace
}
}

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]
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{}{}
if d.initialFace != faceNotSet {
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 := floorEyeStart < blockY || floorEyeEnd < blockY
isAboveBlock := floorEyeStart > blockY || floorEyeEnd > blockY
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 == 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 == 0 || endZDelta == 0 {
interactableFaces[cube.FaceNorth] = struct{}{}
interactableFaces[cube.FaceSouth] = struct{}{}
}
}
//fmt.Println(isOnBlock, floorPosStart[1], floorPosEnd[1], blockY)
}

// 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 floorPosStart[0] < blockX || floorPosEnd[0] < blockX {
interactableFaces[cube.FaceWest] = struct{}{}
}
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 floorPosStart[2] < blockZ || floorPosEnd[2] < blockZ {
interactableFaces[cube.FaceNorth] = struct{}{}
}
if floorPosStart[2] > blockZ || floorPosEnd[2] > blockZ {
interactableFaces[cube.FaceSouth] = struct{}{}
}
}

_, interactable := interactableFaces[targetFace]
if !interactable {
d.mPlayer.Log().Debug("scaffold_b", "blockPos", blockPos, "startPos", startPos, "endPos", endPos, "isClientInput", isClientInput, "targetFace", targetFace, "interactableFaces", interactableFaces)
}
return interactable
}