diff --git a/game/aabb.go b/game/aabb.go index dfc510c6..738f3816 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. @@ -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 } diff --git a/game/math.go b/game/math.go index b6898cef..6a757b4c 100755 --- a/game/math.go +++ b/game/math.go @@ -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 { diff --git a/player/component/world.go b/player/component/world.go index 57d4e112..ecd032a2 100644 --- a/player/component/world.go +++ b/player/component/world.go @@ -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. diff --git a/player/detection/register.go b/player/detection/register.go index 8d4f01c6..2d3073c3 100644 --- a/player/detection/register.go +++ b/player/detection/register.go @@ -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 diff --git a/player/detection/scaffold_b.go b/player/detection/scaffold_b.go new file mode 100644 index 00000000..6a3e5fd4 --- /dev/null +++ b/player/detection/scaffold_b.go @@ -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 +}