diff --git a/anticheat/game/movement.go b/anticheat/game/movement.go index 0c85301c..13a3e8d3 100755 --- a/anticheat/game/movement.go +++ b/anticheat/game/movement.go @@ -14,8 +14,6 @@ const ( BedBounceMultiplier = float32(-0.66) // This can be validated in Mob::ascendLadder() ClimbSpeed = float32(0.2) - MaxConsumingImpulse = float32(0.1225) - MaxSneakImpulse = float32(0.3) MaxNormalizedImpulse = float32(0.70710678118) // 1/sqrt(2) DefaultPlayerHeightOffset = float32(1.62) diff --git a/anticheat/integration/proxy/proxy.go b/anticheat/integration/proxy/proxy.go index 17f423ba..d0adf2b4 100644 --- a/anticheat/integration/proxy/proxy.go +++ b/anticheat/integration/proxy/proxy.go @@ -6,11 +6,11 @@ import ( "log/slog" "time" - proxycore "github.com/oomph-ac/oomph/transferproxy" "github.com/oomph-ac/oomph/anticheat/player" "github.com/oomph-ac/oomph/anticheat/player/component" playercontext "github.com/oomph-ac/oomph/anticheat/player/context" "github.com/oomph-ac/oomph/anticheat/player/detection" + proxycore "github.com/oomph-ac/oomph/transferproxy" "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) diff --git a/anticheat/player/component/acknowledgement/block.go b/anticheat/player/component/acknowledgement/block.go index 5e088257..c28b7327 100644 --- a/anticheat/player/component/acknowledgement/block.go +++ b/anticheat/player/component/acknowledgement/block.go @@ -10,6 +10,7 @@ import ( type UpdateBlockBatch struct { mPlayer *player.Player updates map[df_cube.Pos]uint32 + layer uint8 expiresIn int64 valid bool @@ -19,6 +20,11 @@ func NewUpdateBlockBatchACK(p *player.Player) *UpdateBlockBatch { return &UpdateBlockBatch{mPlayer: p, updates: make(map[df_cube.Pos]uint32), valid: true} } +// NewLayerUpdateBlockBatchACK creates a block update batch for a non-primary Bedrock storage layer. +func NewLayerUpdateBlockBatchACK(p *player.Player, layer uint8) *UpdateBlockBatch { + return &UpdateBlockBatch{mPlayer: p, updates: make(map[df_cube.Pos]uint32), layer: layer, valid: true} +} + func (ack *UpdateBlockBatch) Blocks() map[df_cube.Pos]uint32 { return ack.updates } @@ -50,8 +56,10 @@ func (ack *UpdateBlockBatch) Run() { ack.mPlayer.Log().Warn("unable to find block with runtime ID", "blockRuntimeID", bRuntimeID) b = block.Air{} } - ack.mPlayer.World().SetBlock(pos, b, nil) - ack.mPlayer.WorldUpdater().RemovePendingUpdate(pos, bRuntimeID) + ack.mPlayer.World().SetBlockLayer(pos, b, ack.layer) + if ack.layer == 0 { + ack.mPlayer.WorldUpdater().RemovePendingUpdate(pos, bRuntimeID) + } } ack.updates = nil } diff --git a/anticheat/player/component/acknowledgement/movement.go b/anticheat/player/component/acknowledgement/movement.go index 843ed507..10ff60b5 100644 --- a/anticheat/player/component/acknowledgement/movement.go +++ b/anticheat/player/component/acknowledgement/movement.go @@ -124,6 +124,10 @@ func (ack *UpdateAttributes) Run() { case "minecraft:movement": ack.mPlayer.Movement().SetMovementSpeed(attribute.Value) ack.mPlayer.Movement().SetDefaultMovementSpeed(attribute.Default) + case "minecraft:underwater_movement": + ack.mPlayer.Movement().SetUnderwaterMovementSpeed(attribute.Value) + case "minecraft:lava_movement": + ack.mPlayer.Movement().SetLavaMovementSpeed(attribute.Value) case "minecraft:health": ack.mPlayer.Alive = attribute.Value > 0 case "minecraft:player.hunger": @@ -165,6 +169,7 @@ func (ack *PlayerUpdateActorData) Run() { ack.mPlayer.Movement().SetImmobile(utils.HasDataFlag(entity.DataFlagImmobile, flags)) ack.mPlayer.Movement().SetServerSprint(utils.HasDataFlag(entity.DataFlagSprinting, flags)) ack.mPlayer.Movement().SetHasGravity(utils.HasDataFlag(entity.DataFlagAffectedByGravity, flags)) + ack.mPlayer.Movement().SetSwimming(utils.HasDataFlag(entity.DataFlagSwimming, flags)) if !utils.HasDataFlag(entity.DataFlagAction, flags) && ack.mPlayer.StartUseConsumableTick != 0 { ack.mPlayer.StartUseConsumableTick = 0 diff --git a/anticheat/player/component/movement.go b/anticheat/player/component/movement.go index 5c9bb959..56c9d8b0 100644 --- a/anticheat/player/component/movement.go +++ b/anticheat/player/component/movement.go @@ -89,10 +89,14 @@ type AuthoritativeMovementComponent struct { jumpHeight float32 fallDistance float32 - movementSpeed float32 - defaultMovementSpeed float32 - airSpeed float32 - serverUpdatedSpeed bool + movementSpeed float32 + defaultMovementSpeed float32 + airSpeed float32 + underwaterMovementSpeed float32 + lavaMovementSpeed float32 + swimSpeedMultiplier float32 + dolphinBoostTicks int64 + serverUpdatedSpeed bool knockback mgl32.Vec3 ticksSinceKb uint64 @@ -109,9 +113,14 @@ type AuthoritativeMovementComponent struct { serverSprint, serverSprintApplied bool sneaking, pressingSneak bool + swimming bool + swimAmount float32 + autoJumpingInWater bool + wantDown bool + wantDownSlow bool - jumping, pressingJump bool - jumpDelay uint64 + jumping, pressingJump, effectiveJumping bool + jumpDelay uint64 collideX, collideY, collideZ bool onGround bool @@ -138,10 +147,13 @@ type AuthoritativeMovementComponent struct { func NewAuthoritativeMovementComponent(p *player.Player) *AuthoritativeMovementComponent { return &AuthoritativeMovementComponent{ - mPlayer: p, - nonAuthoritative: &NonAuthoritativeMovement{}, - defaultMovementSpeed: 0.1, - airSpeed: 0.02, + mPlayer: p, + nonAuthoritative: &NonAuthoritativeMovement{}, + defaultMovementSpeed: 0.1, + airSpeed: 0.02, + underwaterMovementSpeed: 0.02, + lavaMovementSpeed: 0.02, + swimSpeedMultiplier: 1, } } @@ -327,6 +339,11 @@ func (mc *AuthoritativeMovementComponent) PressingJump() bool { return mc.pressingJump } +// EffectiveJumping returns the jumping state produced by continuous jump, automatic liquid jump, or block ascent input. +func (mc *AuthoritativeMovementComponent) EffectiveJumping() bool { + return mc.effectiveJumping +} + // JumpDelay returns the number of ticks until the movement component can make another jump. func (mc *AuthoritativeMovementComponent) JumpDelay() uint64 { return mc.jumpDelay @@ -352,6 +369,36 @@ func (mc *AuthoritativeMovementComponent) SetPressingSneak(pressing bool) { mc.pressingSneak = pressing } +// Swimming returns whether the client is using the swimming movement pose. +func (mc *AuthoritativeMovementComponent) Swimming() bool { + return mc.swimming +} + +// SetSwimming sets whether the movement component is using the swimming movement pose. +func (mc *AuthoritativeMovementComponent) SetSwimming(swimming bool) { + mc.swimming = swimming +} + +// SwimAmount returns the client swim-pose interpolation amount. +func (mc *AuthoritativeMovementComponent) SwimAmount() float32 { + return mc.swimAmount +} + +// AutoJumpingInWater returns whether the client is automatically swimming upward. +func (mc *AuthoritativeMovementComponent) AutoJumpingInWater() bool { + return mc.autoJumpingInWater +} + +// WantDown returns whether the client requested downward movement. +func (mc *AuthoritativeMovementComponent) WantDown() bool { + return mc.wantDown +} + +// WantDownSlow returns whether the client requested the slow downward swim input. +func (mc *AuthoritativeMovementComponent) WantDownSlow() bool { + return mc.wantDownSlow +} + // PenetratedLastFrame returns true if the movement component had penetrated through a block in // the previous simulation frame. func (mc *AuthoritativeMovementComponent) PenetratedLastFrame() bool { @@ -467,6 +514,9 @@ func (mc *AuthoritativeMovementComponent) BoundingBox() cube.BBox { scale := mc.size[2] width := (mc.size[0] * 0.5) * scale height := mc.size[1] * scale + if mc.swimming { + height = mc.size[0] * scale + } var yOffset float32 if mc.mPlayer.VersionInRange(-1, player.GameVersion1_20_60) { yOffset = mc.slideOffset.Y() @@ -485,6 +535,10 @@ func (mc *AuthoritativeMovementComponent) BoundingBox() cube.BBox { // ClientBoundingBox returns the bounding box of the movement component translated to the client's position. func (mc *AuthoritativeMovementComponent) ClientBoundingBox() cube.BBox { width := mc.size[0] / 2 + height := mc.size[1] + if mc.swimming { + height = mc.size[0] + } var yOffset float32 if mc.mPlayer.VersionInRange(-1, player.GameVersion1_20_60) { yOffset = mc.slideOffset.Y() @@ -495,7 +549,7 @@ func (mc *AuthoritativeMovementComponent) ClientBoundingBox() cube.BBox { mc.nonAuthoritative.pos[1]+yOffset, mc.nonAuthoritative.pos[2]-width, mc.nonAuthoritative.pos[0]+width, - mc.nonAuthoritative.pos[1]+mc.size[1]+yOffset, + mc.nonAuthoritative.pos[1]+height+yOffset, mc.nonAuthoritative.pos[2]+width, ).GrowVec3(mgl32.Vec3{-1e-4, 0, -1e-4}) } @@ -571,6 +625,36 @@ func (mc *AuthoritativeMovementComponent) SetAirSpeed(newSpeed float32) { mc.airSpeed = newSpeed } +// UnderwaterMovementSpeed returns the base relative acceleration in water. +func (mc *AuthoritativeMovementComponent) UnderwaterMovementSpeed() float32 { + return mc.underwaterMovementSpeed +} + +// SetUnderwaterMovementSpeed sets the base relative acceleration in water. +func (mc *AuthoritativeMovementComponent) SetUnderwaterMovementSpeed(speed float32) { + mc.underwaterMovementSpeed = speed +} + +// LavaMovementSpeed returns the base relative acceleration in lava. +func (mc *AuthoritativeMovementComponent) LavaMovementSpeed() float32 { + return mc.lavaMovementSpeed +} + +// SetLavaMovementSpeed sets the base relative acceleration in lava. +func (mc *AuthoritativeMovementComponent) SetLavaMovementSpeed(speed float32) { + mc.lavaMovementSpeed = speed +} + +// SwimSpeedMultiplier returns the transient water-speed multiplier. +func (mc *AuthoritativeMovementComponent) SwimSpeedMultiplier() float32 { + return mc.swimSpeedMultiplier +} + +// SetSwimSpeedMultiplier sets the transient water-speed multiplier. +func (mc *AuthoritativeMovementComponent) SetSwimSpeedMultiplier(multiplier float32) { + mc.swimSpeedMultiplier = multiplier +} + // XCollision returns true if the movement component is collided with a block // on the x-axis. func (mc *AuthoritativeMovementComponent) XCollision() bool { @@ -791,6 +875,22 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.sneaking = pk.InputData.Load(packet.InputFlagSneakDown) } + wasSwimming := mc.swimming + if pk.InputData.Load(packet.InputFlagStopSwimming) { + mc.swimming = false + } else if pk.InputData.Load(packet.InputFlagStartSwimming) { + mc.swimming = true + mc.sneaking = false + } + if wasSwimming { + mc.swimAmount = game.ClampFloat(mc.swimAmount+0.1, 0, 1) + } else { + mc.swimAmount = game.ClampFloat(mc.swimAmount-0.1, 0, 1) + } + mc.autoJumpingInWater = pk.InputData.Load(packet.InputFlagAutoJumpingInWater) + mc.wantDown = pk.InputData.Load(packet.InputFlagWantDown) + mc.wantDownSlow = pk.InputData.Load(packet.InputFlagWantDownSlow) + mc.mPlayer.Dbg.Notify( player.DebugModeMovementSim, true, @@ -798,21 +898,12 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { pk.MoveVector, ) - maxImpulse := float32(1.0) - /* if pk.MoveVector[0] != 0 && pk.MoveVector[1] != 0 { - maxImpulse = game.MaxNormalizedImpulse - } */ - if mc.mPlayer.StartUseConsumableTick != 0 { - maxImpulse *= game.MaxConsumingImpulse - } - if mc.sneaking { - maxImpulse *= game.MaxSneakImpulse - } - pk.MoveVector[0] = game.ClampFloat(pk.MoveVector[0], -maxImpulse, maxImpulse) - pk.MoveVector[1] = game.ClampFloat(pk.MoveVector[1], -maxImpulse, maxImpulse) + pk.MoveVector[0] = game.ClampFloat(pk.MoveVector[0], -1, 1) + pk.MoveVector[1] = game.ClampFloat(pk.MoveVector[1], -1, 1) mc.jumping = pk.InputData.Load(packet.InputFlagStartJumping) mc.pressingJump = pk.InputData.Load(packet.InputFlagJumping) + mc.effectiveJumping = mc.pressingJump || mc.autoJumpingInWater || pk.InputData.Load(packet.InputFlagAscendBlock) mc.jumpHeight = game.DefaultJumpHeight if jumpBoost, ok := mc.mPlayer.Effects().Get(packet.EffectJumpBoost); ok { mc.jumpHeight += float32(jumpBoost.Amplifier) * 0.1 @@ -838,6 +929,21 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { } mc.impulse = pk.MoveVector.Mul(0.98) + mc.mPlayer.Dbg.Notify( + player.DebugModeMovementSim, + true, + "input swimming=%t swimAmount=%.1f sneaking=%t jumping=%t pressingJump=%t effectiveJumping=%t autoJumpingInWater=%t wantDown=%t wantDownSlow=%t jumpCurrentRaw=%t", + mc.swimming, + mc.swimAmount, + mc.sneaking, + mc.jumping, + mc.pressingJump, + mc.effectiveJumping, + mc.autoJumpingInWater, + mc.wantDown, + mc.wantDownSlow, + pk.InputData.Load(packet.InputFlagJumpCurrentRaw), + ) simulation.SimulatePlayerMovement(mc.mPlayer, mc) // On older versions, there seems to be a delay before the sprinting status is actually applied. @@ -876,6 +982,13 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { } mc.glideBoostTicks-- + if mc.dolphinBoostTicks > 0 { + mc.dolphinBoostTicks-- + if mc.dolphinBoostTicks <= 0 { + mc.dolphinBoostTicks = 0 + mc.swimSpeedMultiplier = 1 + } + } mc.ticksSinceKb++ mc.ticksSinceTeleport++ if mc.jumpDelay > 0 { @@ -897,6 +1010,14 @@ func (mc *AuthoritativeMovementComponent) ServerUpdate(pk packet.Packet) { pk.Operation, )) } + case *packet.MovementEffect: + if pk.EntityRuntimeID == mc.mPlayer.RuntimeId && pk.Type == packet.MovementEffectTypeDolphinBoost { + mc.dolphinBoostTicks = max(0, int64(pk.Duration)) + mc.swimSpeedMultiplier = 1 + if mc.dolphinBoostTicks > 0 { + mc.swimSpeedMultiplier = 2 + } + } case *packet.MoveActorAbsolute: if utils.HasFlag(uint64(pk.Flags), packet.MoveFlagTeleport) { mc.SetPendingTeleportPos(pk.Position) @@ -1027,6 +1148,11 @@ func (mc *AuthoritativeMovementComponent) Sync() { } else { flags = utils.RemoveFlag(flags, entity.DataFlagImmobile) } + if mc.swimming { + flags = utils.AddFlag(flags, entity.DataFlagSwimming) + } else { + flags = utils.RemoveFlag(flags, entity.DataFlagSwimming) + } actorData.EntityMetadata[entity.DataKeyFlags] = flags } mc.mPlayer.SendPacketToClient(actorData) @@ -1072,6 +1198,10 @@ func (mc *AuthoritativeMovementComponent) ResetTransferState(pos mgl32.Vec3) { mc.movementSpeed = mc.defaultMovementSpeed mc.airSpeed = 0.02 + mc.underwaterMovementSpeed = 0.02 + mc.lavaMovementSpeed = 0.02 + mc.swimSpeedMultiplier = 1 + mc.dolphinBoostTicks = 0 mc.serverUpdatedSpeed = false mc.knockback = mgl32.Vec3{} @@ -1091,9 +1221,15 @@ func (mc *AuthoritativeMovementComponent) ResetTransferState(pos mgl32.Vec3) { mc.sneaking = false mc.pressingSneak = false + mc.swimming = false + mc.swimAmount = 0 + mc.autoJumpingInWater = false + mc.wantDown = false + mc.wantDownSlow = false mc.jumping = false mc.pressingJump = false + mc.effectiveJumping = false mc.jumpDelay = 0 mc.collideX = false diff --git a/anticheat/player/component/world.go b/anticheat/player/component/world.go index 9ade3eee..c5ddccc6 100644 --- a/anticheat/player/component/world.go +++ b/anticheat/player/component/world.go @@ -25,9 +25,10 @@ type WorldUpdaterComponent struct { chunkRadius int32 serverChunkRadius int32 - clientPlacedBlocks map[df_cube.Pos]*chainedBlockPlacement - pendingBlockUpdates map[df_cube.Pos]uint32 - batchedBlockUpdates *acknowledgement.UpdateBlockBatch + clientPlacedBlocks map[df_cube.Pos]*chainedBlockPlacement + pendingBlockUpdates map[df_cube.Pos]uint32 + batchedBlockUpdates *acknowledgement.UpdateBlockBatch + batchedExtraBlockUpdates *acknowledgement.UpdateBlockBatch breakingBlockPos *protocol.BlockPos prevPlaceRequest *protocol.UseItemTransactionData @@ -40,9 +41,10 @@ func NewWorldUpdaterComponent(p *player.Player) *WorldUpdaterComponent { mPlayer: p, chunkRadius: 1_000_000_000, - clientPlacedBlocks: make(map[df_cube.Pos]*chainedBlockPlacement), - pendingBlockUpdates: make(map[df_cube.Pos]uint32), - batchedBlockUpdates: acknowledgement.NewUpdateBlockBatchACK(p), + clientPlacedBlocks: make(map[df_cube.Pos]*chainedBlockPlacement), + pendingBlockUpdates: make(map[df_cube.Pos]uint32), + batchedBlockUpdates: acknowledgement.NewUpdateBlockBatchACK(p), + batchedExtraBlockUpdates: acknowledgement.NewLayerUpdateBlockBatchACK(p, 1), initalInteractionAccepted: true, } @@ -73,11 +75,16 @@ func (c *WorldUpdaterComponent) HandleLevelChunk(pk *packet.LevelChunk) { // HandleUpdateBlock handles an UpdateBlock packet from the server. func (c *WorldUpdaterComponent) HandleUpdateBlock(pk *packet.UpdateBlock) { pos := df_cube.Pos{int(pk.Position.X()), int(pk.Position.Y()), int(pk.Position.Z())} - if pk.Layer != 0 { + if pk.Layer > 1 { c.mPlayer.Log().Debug("unsupported layer update block", "layer", pk.Layer, "block", pk.NewBlockRuntimeID, "pos", pos) return } - c.AddPendingUpdate(pos, c.mPlayer.DecodeBlockRuntimeID(pk.NewBlockRuntimeID)) + blockRuntimeID := c.mPlayer.DecodeBlockRuntimeID(pk.NewBlockRuntimeID) + if pk.Layer == 1 { + c.batchedExtraBlockUpdates.SetBlock(pos, blockRuntimeID) + return + } + c.AddPendingUpdate(pos, blockRuntimeID) } // HandleUpdateSubChunkBlocks handles an UpdateSubChunkBlocks packet from the server. @@ -89,7 +96,7 @@ func (c *WorldUpdaterComponent) HandleUpdateSubChunkBlocks(pk *packet.UpdateSubC c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.DecodeBlockRuntimeID(entry.BlockRuntimeID)) } for _, entry := range pk.Extra { - c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.DecodeBlockRuntimeID(entry.BlockRuntimeID)) + c.batchedExtraBlockUpdates.SetBlock(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, c.mPlayer.DecodeBlockRuntimeID(entry.BlockRuntimeID)) } } @@ -391,7 +398,7 @@ func (c *WorldUpdaterComponent) RemovePendingUpdate(pos df_cube.Pos, blockRuntim } func (c *WorldUpdaterComponent) Flush() { - if !c.batchedBlockUpdates.HasUpdates() { + if !c.batchedBlockUpdates.HasUpdates() && !c.batchedExtraBlockUpdates.HasUpdates() { return } @@ -434,12 +441,20 @@ func (c *WorldUpdaterComponent) Flush() { } c.batchedBlockUpdates.SetExpiry(blockAckTimeout) + c.batchedExtraBlockUpdates.SetExpiry(blockAckTimeout) if noLagComp { c.batchedBlockUpdates.Run() + c.batchedExtraBlockUpdates.Run() } else { - c.mPlayer.ACKs().Add(c.batchedBlockUpdates) + if c.batchedBlockUpdates.HasUpdates() { + c.mPlayer.ACKs().Add(c.batchedBlockUpdates) + } + if c.batchedExtraBlockUpdates.HasUpdates() { + c.mPlayer.ACKs().Add(c.batchedExtraBlockUpdates) + } } c.batchedBlockUpdates = acknowledgement.NewUpdateBlockBatchACK(c.mPlayer) + c.batchedExtraBlockUpdates = acknowledgement.NewLayerUpdateBlockBatchACK(c.mPlayer, 1) } func (c *WorldUpdaterComponent) Tick() { diff --git a/anticheat/player/movement.go b/anticheat/player/movement.go index 0792f729..559e8b5d 100644 --- a/anticheat/player/movement.go +++ b/anticheat/player/movement.go @@ -84,6 +84,8 @@ type MovementComponent interface { Jumping() bool // PressingJump returns true if the movement component is holding down the key bound to the jump action. PressingJump() bool + // EffectiveJumping returns the jumping state produced by continuous jump, automatic liquid jump, or block ascent input. + EffectiveJumping() bool // JumpDelay returns the number of ticks until the movement component can make another jump. JumpDelay() uint64 // SetJumpDelay sets the number of ticks until the movement component can make another jump. @@ -96,6 +98,19 @@ type MovementComponent interface { // SetPressingSneak sets if the movement component is holding down the key bound o the sneak action. SetPressingSneak(pressing bool) + // Swimming returns whether the client is using the swimming movement pose. + Swimming() bool + // SetSwimming sets whether the movement component is using the swimming movement pose. + SetSwimming(swimming bool) + // SwimAmount returns the client swim-pose interpolation amount. + SwimAmount() float32 + // AutoJumpingInWater returns whether the client is automatically swimming upward. + AutoJumpingInWater() bool + // WantDown returns whether the client requested downward movement. + WantDown() bool + // WantDownSlow returns whether the client requested the slow downward swim input. + WantDownSlow() bool + // PenetratedLastFrame returns true if the movement component had penetrated through a block in // the previous simulation frame. PenetratedLastFrame() bool @@ -179,6 +194,18 @@ type MovementComponent interface { AirSpeed() float32 // SetAirSpeed sets the movement speed of the movement component while off ground. SetAirSpeed(airSpeed float32) + // UnderwaterMovementSpeed returns the base relative acceleration in water. + UnderwaterMovementSpeed() float32 + // SetUnderwaterMovementSpeed sets the base relative acceleration in water. + SetUnderwaterMovementSpeed(speed float32) + // LavaMovementSpeed returns the base relative acceleration in lava. + LavaMovementSpeed() float32 + // SetLavaMovementSpeed sets the base relative acceleration in lava. + SetLavaMovementSpeed(speed float32) + // SwimSpeedMultiplier returns the transient water-speed multiplier applied by movement effects such as a dolphin boost. + SwimSpeedMultiplier() float32 + // SetSwimSpeedMultiplier sets the transient water-speed multiplier. + SetSwimSpeedMultiplier(multiplier float32) // JumpHeight returns the jump height of the movement component. JumpHeight() float32 diff --git a/anticheat/player/packet.go b/anticheat/player/packet.go index a54c93f2..c7f60e9a 100644 --- a/anticheat/player/packet.go +++ b/anticheat/player/packet.go @@ -414,6 +414,10 @@ func (p *Player) HandleServerPacket(ctx *context.HandlePacketContext) { pk.Tick = 0 ctx.SetModified() p.Movement().ServerUpdate(pk) + case *packet.MovementEffect: + if pk.EntityRuntimeID == p.RuntimeId { + p.Movement().ServerUpdate(pk) + } case *packet.MoveActorAbsolute: if pk.EntityRuntimeID != p.RuntimeId { p.entTracker.HandleMoveActorAbsolute(pk) diff --git a/anticheat/player/simulation/movement.go b/anticheat/player/simulation/movement.go index 67b1da10..f637b64f 100644 --- a/anticheat/player/simulation/movement.go +++ b/anticheat/player/simulation/movement.go @@ -7,6 +7,7 @@ import ( "github.com/df-mc/dragonfly/server/block" df_cube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/enchantment" "github.com/df-mc/dragonfly/server/world" "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" @@ -61,6 +62,33 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) movement.SetVel(mgl32.Vec3{}) } + waterBlocks := touchingLiquidBlocks[block.Water](p, movement) + lavaBlocks := touchingLiquidBlocks[block.Lava](p, movement) + p.Dbg.Notify( + player.DebugModeMovementSim, + true, + "liquid contact water=%d lava=%d swimming=%t", + len(waterBlocks), + len(lavaBlocks), + movement.Swimming(), + ) + waterTravel := len(waterBlocks) != 0 || movement.Swimming() + if !movement.Flying() && (waterTravel || len(lavaBlocks) != 0) { + p.Dbg.Notify(player.DebugModeMovementSim, attemptKnockback(movement), "knockback applied in liquid: %v", movement.Vel()) + if waterTravel { + if movement.Gliding() { + movement.SetGliding(false) + movement.SetGlideBoost(0) + } + applyLiquidFlow(p, movement, waterBlocks, block.Water{}) + simulateLiquidTravel(p, movement, block.Water{}, len(waterBlocks) != 0) + } else { + applyLiquidFlow(p, movement, lavaBlocks, block.Lava{}) + simulateLiquidTravel(p, movement, block.Lava{}, true) + } + return + } + blockUnder := p.World().Block(df_cube.Pos(cube.PosFromVec3(movement.Pos().Sub(mgl32.Vec3{0, 0.5})))) blockFriction := game.DefaultAirFriction moveRelativeSpeed := movement.AirSpeed() @@ -72,7 +100,6 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) blockFriction *= utils.BlockFriction(blockUnder) moveRelativeSpeed = mSpeed * (0.16277136 / (blockFriction * blockFriction * blockFriction)) } - if movement.Gliding() { _, hasElytra := p.Inventory().Chestplate().Item().(item.Elytra) if hasElytra && !movement.OnGround() { @@ -106,14 +133,14 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) if newVel[1] < negClimbSpeed { newVel[1] = negClimbSpeed } - if movement.PressingJump() || movement.XCollision() || movement.ZCollision() { + if movement.EffectiveJumping() || movement.XCollision() || movement.ZCollision() { newVel[1] = game.ClimbSpeed } if movement.Sneaking() && newVel[1] < 0 { newVel[1] = 0 } - p.Dbg.Notify(player.DebugModeMovementSim, true, "added climb velocity: %v (collided=%v pressingJump=%v)", newVel, movement.XCollision() || movement.ZCollision(), movement.PressingJump()) + p.Dbg.Notify(player.DebugModeMovementSim, true, "added climb velocity: %v (collided=%v effectiveJumping=%v)", newVel, movement.XCollision() || movement.ZCollision(), movement.EffectiveJumping()) movement.SetVel(newVel) } @@ -268,12 +295,6 @@ func simulationIsReliable(p *player.Player, movement player.MovementComponent) b stateBB := movement.BoundingBox() for result := range utils.NearbyBlocks(stateBB.Grow(1), false, true, p.World()) { - if _, isLiquid := result.Block.(world.Liquid); isLiquid { - blockBB := cube.Box(0, 0, 0, 1, 1, 1).Translate(result.Position.Vec3()) - if stateBB.IntersectsWith(blockBB) { - return false - } - } if utils.BlockName(result.Block) == "minecraft:bamboo" { return false } @@ -283,6 +304,309 @@ func simulationIsReliable(p *player.Player, movement player.MovementComponent) b !(movement.Flying() || movement.JustDisabledFlight() || movement.NoClip() || !p.Alive) } +func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, liquid world.Liquid, touchingLiquid bool) { + initialY := movement.Pos().Y() + _, water := liquid.(block.Water) + jumping := movement.EffectiveJumping() + if water { + if movement.WantDown() || movement.WantDownSlow() { + vel := movement.Vel() + vel[1] -= 0.04 + movement.SetVel(vel) + } + updateSwimTravel(p, movement) + } + + if jumping { + newVel := movement.Vel() + swimTransition := movement.SwimAmount() > 0 && movement.SwimAmount() < 1 + if swimTransition || water && movement.Swimming() && !touchingLiquid { + newVel[1] = 0 + } else { + newVel[1] += 0.04 + } + movement.SetVel(newVel) + } + + moveRelativeSpeed := movement.LavaMovementSpeed() + depthStriderLevel := float32(0) + swimSpeedMultiplier := float32(1) + if water { + moveRelativeSpeed = movement.UnderwaterMovementSpeed() + if movement.Swimming() { + swimSpeedMultiplier = movement.SwimSpeedMultiplier() + } + if enchant, ok := p.Inventory().Boots().Enchantment(enchantment.DepthStrider); ok { + depthStriderLevel = math32.Min(float32(enchant.Level()), float32(enchantment.DepthStrider.MaxLevel())) + if !movement.OnGround() { + depthStriderLevel *= 0.5 + } + } + depthStriderFraction := depthStriderLevel / float32(enchantment.DepthStrider.MaxLevel()) + if swimSpeedMultiplier > 1 { + moveRelativeSpeed *= (0.7 + depthStriderFraction*0.3) * swimSpeedMultiplier + } else { + moveRelativeSpeed += (movement.MovementSpeed() - moveRelativeSpeed) * depthStriderFraction + } + } + moveRelative(movement, moveRelativeSpeed) + oldVel := movement.Vel() + oldOnGround := movement.OnGround() + tryCollisions(p, p.World(), p.Dbg, p.VersionInRange(-1, player.GameVersion1_20_60), false) + setPostCollisionMotion(p, oldVel, oldOnGround, block.Air{}) + movement.SetMov(movement.Vel()) + + vel := movement.Vel() + if water { + drag := float32(0.8) + if movement.Sprinting() { + drag = 0.9 + } + if depthStriderLevel > 0 && swimSpeedMultiplier <= 1 { + drag += (0.54600006 - drag) * (depthStriderLevel / float32(enchantment.DepthStrider.MaxLevel())) + } + vel[0] *= drag + vel[1] *= 0.8 + vel[2] *= drag + } else { + vel = vel.Mul(0.5) + } + if levitation, ok := p.Effects().Get(packet.EffectLevitation); ok { + target := game.LevitationGravityMultiplier * float32(levitation.Amplifier+1) + vel[1] += (target - vel[1]) * 0.2 + } else if movement.HasGravity() { + gravity := float32(0.02) + if water { + gravity = 0.005 + if movement.Swimming() { + gravity = 0 + } + } + vel[1] -= gravity + } + if movement.XCollision() || movement.ZCollision() { + raised := mgl32.Vec3{vel.X(), vel.Y() + 0.6 + initialY - movement.Pos().Y(), vel.Z()} + raisedBox := movement.BoundingBox().Translate(raised) + hasCollision := utils.HasNearbyBBoxes(raisedBox, p.World()) + hasLiquid := containsAnyLiquid(p, raisedBox) + p.Dbg.Notify(player.DebugModeMovementSim, true, "liquid exit probe collision=%t liquid=%t box=%v", hasCollision, hasLiquid, raisedBox) + if !hasCollision && !hasLiquid { + vel[1] = 0.3 + } + } + movement.SetVel(vel) + movement.SetFallDistance(0) + p.Dbg.Notify(player.DebugModeMovementSim, true, "endOfLiquidFrame type=%T vel=%v pos=%v", liquid, vel, movement.Pos()) +} + +func updateSwimTravel(p *player.Player, movement player.MovementComponent) { + if !movement.Swimming() || movement.EffectiveJumping() { + return + } + pitch := movement.Rotation().X() * math32.Pi / 180 + targetY := -game.MCSin(pitch) + rate := float32(0.06) + if targetY < -0.2 { + rate = 0.085 + } + + if targetY > 0 && !movement.WantDownSlow() { + belowPos := df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.1}))) + if _, belowAir := liquidMovementBlock(p, belowPos).(block.Air); belowAir { + liquidPos := df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.2}))) + if _, liquid := liquidAt(p, liquidPos); !liquid { + vel := movement.Vel() + vel[1] = 0 + movement.SetVel(vel) + return + } + } + } + vel := movement.Vel() + vel[1] += (targetY - vel[1]) * rate + movement.SetVel(vel) +} + +func liquidMovementBlock(p *player.Player, pos df_cube.Pos) world.Block { + extra := p.World().BlockLayer(pos, 1) + if _, air := extra.(block.Air); !air { + return extra + } + return p.World().Block(pos) +} + +func touchingLiquidBlocks[T world.Liquid](p *player.Player, movement player.MovementComponent) []df_cube.Pos { + box := movement.BoundingBox().GrowVec3(mgl32.Vec3{1e-4, 0, 1e-4}) + offset := mgl32.Vec3{0.001, 0.401, 0.001} + var target T + if _, lava := any(target).(block.Lava); lava { + offset = mgl32.Vec3{0.1, 0.4, 0.1} + } + box = shrinkBox(box, offset) + + min, max := box.Min(), box.Max() + minX, minY, minZ := int(math.Floor(float64(min.X()))), int(math.Floor(float64(min.Y()))), int(math.Floor(float64(min.Z()))) + maxX, maxY, maxZ := int(math.Floor(float64(max.X()+1))), int(math.Floor(float64(max.Y()+1))), int(math.Floor(float64(max.Z()+1))) + positions := make([]df_cube.Pos, 0, 4) + for x := minX; x < maxX; x++ { + for y := minY; y < maxY; y++ { + for z := minZ; z < maxZ; z++ { + pos := df_cube.Pos{x, y, z} + if liquid, ok := liquidAt(p, pos); ok { + if _, matches := liquid.(T); matches { + height := liquidHeight(liquid) + surface := float32(pos[1]) + height + p.Dbg.Notify( + player.DebugModeMovementSim, + true, + "liquid block type=%s pos=%v depth=%d falling=%t height=%.6f surface=%.6f boxY=[%.6f %.6f] immersion=%.6f", + liquid.LiquidType(), + pos, + liquid.LiquidDepth(), + liquid.LiquidFalling(), + height, + surface, + box.Min().Y(), + box.Max().Y(), + surface-box.Min().Y(), + ) + positions = append(positions, pos) + } + } + } + } + } + return positions +} + +func shrinkBox(box cube.BBox, offset mgl32.Vec3) cube.BBox { + min, max := box.Min().Add(offset), box.Max().Sub(offset) + originalMin, originalMax := box.Min(), box.Max() + for axis := 0; axis < 3; axis++ { + if min[axis] > max[axis] { + mid := (originalMin[axis] + originalMax[axis]) * 0.5 + min[axis], max[axis] = mid, mid + } + } + return cube.Box(min.X(), min.Y(), min.Z(), max.X(), max.Y(), max.Z()) +} + +func liquidAt(p *player.Player, pos df_cube.Pos) (world.Liquid, bool) { + if liquid, ok := p.World().BlockLayer(pos, 1).(world.Liquid); ok { + return liquid, true + } + liquid, ok := p.World().Block(pos).(world.Liquid) + return liquid, ok +} + +func liquidHeight(liquid world.Liquid) float32 { + if liquid.LiquidFalling() { + return 1 + } + return float32(liquid.LiquidDepth()+1) / 9 +} + +// containsAnyLiquid returns whether the box covers a block containing liquid. +func containsAnyLiquid(p *player.Player, box cube.BBox) bool { + min, max := box.Min(), box.Max() + minX, minY, minZ := int(math.Floor(float64(min.X()))), int(math.Floor(float64(min.Y()))), int(math.Floor(float64(min.Z()))) + maxX, maxY, maxZ := int(math.Ceil(float64(max.X()))), int(math.Ceil(float64(max.Y()))), int(math.Ceil(float64(max.Z()))) + for x := minX; x < maxX; x++ { + for z := minZ; z < maxZ; z++ { + for y := minY; y < maxY; y++ { + if _, ok := liquidAt(p, df_cube.Pos{x, y, z}); ok { + return true + } + } + } + } + return false +} + +func applyLiquidFlow(p *player.Player, movement player.MovementComponent, positions []df_cube.Pos, liquid world.Liquid) { + flow := mgl32.Vec3{} + for _, pos := range positions { + current, ok := liquidAt(p, pos) + if !ok || current.LiquidType() != liquid.LiquidType() { + continue + } + flow = flow.Add(liquidFlow(p, pos, current)) + } + if length := flow.Len(); length >= 1e-4 { + strength := float32(0.014) + if liquid.LiquidType() == "lava" { + strength = 0.0035 + } + vel := movement.Vel().Add(flow.Mul(strength / length)) + movement.SetVel(vel) + p.Dbg.Notify(player.DebugModeMovementSim, true, "%s flow applied strength=%.6f flow=%v vel=%v", liquid.LiquidType(), strength, flow, vel) + } +} + +func liquidFlow(p *player.Player, pos df_cube.Pos, liquid world.Liquid) mgl32.Vec3 { + currentDecay := liquidDecay(liquid) + flow := mgl32.Vec3{} + faces := []struct { + delta df_cube.Pos + vec mgl32.Vec3 + }{ + {df_cube.Pos{-1, 0, 0}, mgl32.Vec3{-1, 0, 0}}, + {df_cube.Pos{1, 0, 0}, mgl32.Vec3{1, 0, 0}}, + {df_cube.Pos{0, 0, -1}, mgl32.Vec3{0, 0, -1}}, + {df_cube.Pos{0, 0, 1}, mgl32.Vec3{0, 0, 1}}, + } + for _, face := range faces { + neighbourPos := df_cube.Pos{pos[0] + face.delta[0], pos[1], pos[2] + face.delta[2]} + if neighbour, ok := liquidAt(p, neighbourPos); ok { + if neighbour.LiquidType() == liquid.LiquidType() { + if !liquidFlowSideClosed(p, pos, neighbourPos) && !liquidFlowSideClosed(p, neighbourPos, pos) { + flow = flow.Add(face.vec.Mul(float32(liquidDecay(neighbour) - currentDecay))) + } + continue + } + } + if len(utils.BlockCollisions(p.World().Block(neighbourPos), cube.Pos(neighbourPos), p.World())) != 0 { + continue + } + below := df_cube.Pos{neighbourPos[0], neighbourPos[1] - 1, neighbourPos[2]} + if lower, ok := liquidAt(p, below); ok { + if lower.LiquidType() == liquid.LiquidType() { + flow = flow.Add(face.vec.Mul(float32(liquidDecay(lower) - currentDecay + 8))) + } + } + } + if liquid.LiquidFalling() { + for _, face := range faces { + neighbourPos := df_cube.Pos{pos[0] + face.delta[0], pos[1], pos[2] + face.delta[2]} + aboveNeighbour := df_cube.Pos{neighbourPos[0], neighbourPos[1] + 1, neighbourPos[2]} + if len(utils.BlockCollisions(p.World().Block(neighbourPos), cube.Pos(neighbourPos), p.World())) != 0 || + len(utils.BlockCollisions(p.World().Block(aboveNeighbour), cube.Pos(aboveNeighbour), p.World())) != 0 { + if length := flow.Len(); length > 1e-4 { + flow = flow.Mul(1 / length) + } + flow[1] -= 6 + break + } + } + } + if length := flow.Len(); length > 1e-4 { + return flow.Mul(1 / length) + } + return mgl32.Vec3{} +} + +func liquidFlowSideClosed(p *player.Player, pos, side df_cube.Pos) bool { + stairs, ok := p.World().Block(pos).(block.Stairs) + return ok && stairs.Model().FaceSolid(pos, pos.Face(side), p.World()) +} + +func liquidDecay(liquid world.Liquid) int { + if liquid.LiquidFalling() { + return 0 + } + return 8 - liquid.LiquidDepth() +} + func landOnBlock(movement player.MovementComponent, old mgl32.Vec3, blockUnder world.Block) { newVel := movement.Vel() if old.Y() >= 0 || movement.PressingSneak() { diff --git a/anticheat/world/world.go b/anticheat/world/world.go index 8ef4a31d..2d27d742 100644 --- a/anticheat/world/world.go +++ b/anticheat/world/world.go @@ -27,8 +27,9 @@ type World struct { chunks map[protocol.ChunkPos]ChunkInfo subChunks map[protocol.ChunkPos][]xxh3.Uint128 - exemptedChunks map[protocol.ChunkPos]struct{} - blockUpdates map[protocol.ChunkPos]map[df_cube.Pos]world.Block + exemptedChunks map[protocol.ChunkPos]struct{} + blockUpdates map[protocol.ChunkPos]map[df_cube.Pos]world.Block + extraBlockUpdates map[protocol.ChunkPos]map[df_cube.Pos]world.Block debugFn func(string, ...any) @@ -40,8 +41,9 @@ func New(debugFn func(string, ...any)) *World { chunks: make(map[protocol.ChunkPos]ChunkInfo), subChunks: make(map[protocol.ChunkPos][]xxh3.Uint128), - exemptedChunks: make(map[protocol.ChunkPos]struct{}), - blockUpdates: make(map[protocol.ChunkPos]map[df_cube.Pos]world.Block), + exemptedChunks: make(map[protocol.ChunkPos]struct{}), + blockUpdates: make(map[protocol.ChunkPos]map[df_cube.Pos]world.Block), + extraBlockUpdates: make(map[protocol.ChunkPos]map[df_cube.Pos]world.Block), debugFn: debugFn, } @@ -85,19 +87,29 @@ func (w *World) Chunk(pos protocol.ChunkPos) *chunk.Chunk { // Block returns the block at the position passed. func (w *World) Block(pos df_cube.Pos) world.Block { + return w.BlockLayer(pos, 0) +} + +// BlockLayer returns the block at the position and storage layer passed. +func (w *World) BlockLayer(pos df_cube.Pos, layer uint8) world.Block { + if layer > 1 { + return block.Air{} + } blockPos := cube.Pos(pos) if blockPos.OutOfBounds(cube.Range(world.Overworld.Range())) { return block.Air{} } chunkPos := protocol.ChunkPos{int32(blockPos[0]) >> 4, int32(blockPos[2]) >> 4} - blockUpdates, found := w.blockUpdates[chunkPos] + updates := w.blockUpdates + if layer == 1 { + updates = w.extraBlockUpdates + } + blockUpdates, found := updates[chunkPos] if found { if b, ok := blockUpdates[df_cube.Pos(blockPos)]; ok { return b } - } else { - w.blockUpdates[chunkPos] = make(map[df_cube.Pos]world.Block) } c := w.Chunk(chunkPos) @@ -105,8 +117,7 @@ func (w *World) Block(pos df_cube.Pos) world.Block { return block.Air{} } - // TODO: Implement and account for multi-layer blocks. - rid := c.Block(uint8(blockPos[0]), int16(blockPos[1]), uint8(blockPos[2]), 0) + rid := c.Block(uint8(blockPos[0]), int16(blockPos[1]), uint8(blockPos[2]), layer) if b, ok := world.BlockByRuntimeID(rid); ok { return b } @@ -115,14 +126,23 @@ func (w *World) Block(pos df_cube.Pos) world.Block { // SetBlock sets the block at the position passed. func (w *World) SetBlock(pos df_cube.Pos, b world.Block, _ *world.SetOpts) { - if cube.Pos(pos).OutOfBounds(cube.Range(world.Overworld.Range())) { + w.SetBlockLayer(pos, b, 0) +} + +// SetBlockLayer updates a block in the selected Bedrock storage layer. +func (w *World) SetBlockLayer(pos df_cube.Pos, b world.Block, layer uint8) { + if layer > 1 || cube.Pos(pos).OutOfBounds(cube.Range(world.Overworld.Range())) { return } chunkPos := protocol.ChunkPos{int32(pos[0]) >> 4, int32(pos[2]) >> 4} - if w.blockUpdates[chunkPos] == nil { - w.blockUpdates[chunkPos] = make(map[df_cube.Pos]world.Block) + updates := w.blockUpdates + if layer == 1 { + updates = w.extraBlockUpdates + } + if updates[chunkPos] == nil { + updates[chunkPos] = make(map[df_cube.Pos]world.Block) } - w.blockUpdates[chunkPos][pos] = b + updates[chunkPos][pos] = b } // CleanChunks cleans up the chunks in respect to the given chunk radius and chunk position. @@ -178,6 +198,7 @@ func (w *World) removeChunk(info ChunkInfo, chunkPos protocol.ChunkPos) { delete(w.subChunks, chunkPos) delete(w.chunks, chunkPos) delete(w.blockUpdates, chunkPos) + delete(w.extraBlockUpdates, chunkPos) } // chunkInRange returns true if the chunk position is within the given radius of the chunk position. diff --git a/transferproxy/.golangci.yml b/transferproxy/.golangci.yml new file mode 100644 index 00000000..5eddf00c --- /dev/null +++ b/transferproxy/.golangci.yml @@ -0,0 +1,7 @@ +version: "2" + +linters: + exclusions: + rules: + - path: proxy_test\.go + linters: [errcheck] diff --git a/transferproxy/proxy.go b/transferproxy/proxy.go index bcbe0ed2..59d97528 100644 --- a/transferproxy/proxy.go +++ b/transferproxy/proxy.go @@ -572,16 +572,18 @@ func (s *session) rewriteClientPacket(pk packet.Packet) { pk.EntityRuntimeID = to } case *packet.Interact: - if pk.TargetEntityRuntimeID == math.MaxInt64 { + switch pk.TargetEntityRuntimeID { + case math.MaxInt64: pk.TargetEntityRuntimeID = from - } else if pk.TargetEntityRuntimeID == from { + case from: pk.TargetEntityRuntimeID = to } case *packet.InventoryTransaction: if tx, ok := pk.TransactionData.(*protocol.UseItemOnEntityTransactionData); ok { - if tx.TargetEntityRuntimeID == math.MaxInt64 { + switch tx.TargetEntityRuntimeID { + case math.MaxInt64: tx.TargetEntityRuntimeID = from - } else if tx.TargetEntityRuntimeID == from { + case from: tx.TargetEntityRuntimeID = to } } diff --git a/transferproxy/state_tracker.go b/transferproxy/state_tracker.go index 450b4e4d..14cad413 100644 --- a/transferproxy/state_tracker.go +++ b/transferproxy/state_tracker.go @@ -49,9 +49,10 @@ func (t *backendStateTracker) handle(pk packet.Packet, clientRuntimeID uint64) { if pk.EntityRuntimeID != clientRuntimeID { break } - if pk.Operation == packet.MobEffectAdd || pk.Operation == packet.MobEffectModify { + switch pk.Operation { + case packet.MobEffectAdd, packet.MobEffectModify: t.effects[pk.EffectType] = struct{}{} - } else if pk.Operation == packet.MobEffectRemove { + case packet.MobEffectRemove: delete(t.effects, pk.EffectType) } case *packet.PlayerList: