From 79aaa1764807b6c30f93d1e5929bb7b0bd983dd4 Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:51:04 -0400 Subject: [PATCH 01/11] feat(simulation): add liquid simulation --- player/component/acknowledgement/block.go | 12 +- player/component/acknowledgement/movement.go | 5 + player/component/movement.go | 121 ++++++++- player/component/world.go | 34 ++- player/movement.go | 21 ++ player/packet.go | 4 + player/simulation/movement.go | 265 ++++++++++++++++++- world/world.go | 47 +++- 8 files changed, 470 insertions(+), 39 deletions(-) diff --git a/player/component/acknowledgement/block.go b/player/component/acknowledgement/block.go index c58fe5cf..23b6a6c3 100644 --- a/player/component/acknowledgement/block.go +++ b/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/player/component/acknowledgement/movement.go b/player/component/acknowledgement/movement.go index 94573481..028ed473 100644 --- a/player/component/acknowledgement/movement.go +++ b/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/player/component/movement.go b/player/component/movement.go index e91a1b04..5a88a12a 100644 --- a/player/component/movement.go +++ b/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,6 +113,9 @@ type AuthoritativeMovementComponent struct { serverSprint, serverSprintApplied bool sneaking, pressingSneak bool + swimming bool + autoJumpingInWater bool + wantDownSlow bool jumping, pressingJump bool jumpDelay uint64 @@ -138,10 +145,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, } } @@ -352,6 +362,26 @@ 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 +} + +// AutoJumpingInWater returns whether the client is automatically swimming upward. +func (mc *AuthoritativeMovementComponent) AutoJumpingInWater() bool { + return mc.autoJumpingInWater +} + +// 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 +497,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 +518,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 +532,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 +608,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 +858,15 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.sneaking = pk.InputData.Load(packet.InputFlagSneakDown) } + if pk.InputData.Load(packet.InputFlagStopSwimming) { + mc.swimming = false + } else if pk.InputData.Load(packet.InputFlagStartSwimming) { + mc.swimming = true + mc.sneaking = false + } + mc.autoJumpingInWater = pk.InputData.Load(packet.InputFlagAutoJumpingInWater) + mc.wantDownSlow = pk.InputData.Load(packet.InputFlagWantDownSlow) + mc.mPlayer.Dbg.Notify( player.DebugModeMovementSim, true, @@ -876,6 +952,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 +980,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 +1118,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 +1168,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,6 +1191,9 @@ func (mc *AuthoritativeMovementComponent) ResetTransferState(pos mgl32.Vec3) { mc.sneaking = false mc.pressingSneak = false + mc.swimming = false + mc.autoJumpingInWater = false + mc.wantDownSlow = false mc.jumping = false mc.pressingJump = false diff --git a/player/component/world.go b/player/component/world.go index e655f575..d16a13ec 100644 --- a/player/component/world.go +++ b/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,10 +75,14 @@ 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 } + if pk.Layer == 1 { + c.batchedExtraBlockUpdates.SetBlock(pos, pk.NewBlockRuntimeID) + return + } c.AddPendingUpdate(pos, pk.NewBlockRuntimeID) } @@ -89,7 +95,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())}, entry.BlockRuntimeID) } for _, entry := range pk.Extra { - c.AddPendingUpdate(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, entry.BlockRuntimeID) + c.batchedExtraBlockUpdates.SetBlock(df_cube.Pos{int(entry.BlockPos.X()), int(entry.BlockPos.Y()), int(entry.BlockPos.Z())}, entry.BlockRuntimeID) } } @@ -391,7 +397,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 +440,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/player/movement.go b/player/movement.go index 8f2f91e9..4b7e5713 100644 --- a/player/movement.go +++ b/player/movement.go @@ -96,6 +96,15 @@ 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) + // AutoJumpingInWater returns whether the client is automatically swimming upward. + AutoJumpingInWater() 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 +188,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/player/packet.go b/player/packet.go index 415095c4..62fce9d5 100644 --- a/player/packet.go +++ b/player/packet.go @@ -432,6 +432,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/player/simulation/movement.go b/player/simulation/movement.go index 68a9a514..105169c8 100644 --- a/player/simulation/movement.go +++ b/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,26 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) movement.SetVel(mgl32.Vec3{}) } + waterBlocks := touchingLiquidBlocks[block.Water](p, movement) + lavaBlocks := touchingLiquidBlocks[block.Lava](p, movement) + if movement.Swimming() && len(waterBlocks) == 0 { + movement.SetSwimming(false) + } + if !movement.Flying() && (len(waterBlocks) != 0 || len(lavaBlocks) != 0) { + p.Dbg.Notify(player.DebugModeMovementSim, attemptKnockback(movement), "knockback applied in liquid: %v", movement.Vel()) + if len(waterBlocks) != 0 { + applyLiquidFlow(p, movement, waterBlocks) + simulateLiquidTravel(p, movement, block.Water{}) + } else { + simulateLiquidTravel(p, movement, block.Lava{}) + } + if movement.Gliding() && movement.OnGround() { + movement.SetGliding(false) + movement.SetGlideBoost(0) + } + return + } + blockUnder := p.World().Block(df_cube.Pos(cube.PosFromVec3(movement.Pos().Sub(mgl32.Vec3{0, 0.5})))) blockFriction := game.DefaultAirFriction moveRelativeSpeed := movement.AirSpeed() @@ -268,12 +289,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 +298,244 @@ 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) { + initialY := movement.Pos().Y() + _, water := liquid.(block.Water) + if water { + updateSwimTravel(p, movement) + } + + if movement.PressingJump() { + newVel := movement.Vel() + below := p.World().Block(df_cube.Pos(cube.PosFromVec3(movement.Pos().Sub(mgl32.Vec3{0, 1.1})))) + if movement.Swimming() { + if _, air := below.(block.Air); air { + newVel[1] = 0 + } else { + newVel[1] += 0.04 + } + } else { + newVel[1] += 0.04 + } + movement.SetVel(newVel) + } + + moveRelativeSpeed := movement.LavaMovementSpeed() + depthStriderLevel := float32(0) + if water { + moveRelativeSpeed = movement.UnderwaterMovementSpeed() + 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 multiplier := movement.SwimSpeedMultiplier(); multiplier > 1 { + moveRelativeSpeed *= (0.7 + depthStriderFraction*0.3) * multiplier + } 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()) + + if movement.XCollision() || movement.ZCollision() { + vel := movement.Vel() + raised := mgl32.Vec3{vel.X(), vel.Y() + 0.6 + initialY - movement.Pos().Y(), vel.Z()} + if !utils.HasNearbyBBoxes(movement.BoundingBox().Translate(raised), p.World()) { + vel[1] = 0.3 + movement.SetVel(vel) + } + } + + vel := movement.Vel() + if water { + drag := float32(0.8) + if movement.Sprinting() { + drag = 0.9 + } + if depthStriderLevel > 0 && movement.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() { + vel[1] -= 0.02 + } + 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.PressingJump() { + 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().Sub(mgl32.Vec3{0, 1.1}))) + _, belowAir := p.World().Block(belowPos).(block.Air) + _, liquidBelow := liquidAt(p, df_cube.Pos(cube.PosFromVec3(movement.Pos().Sub(mgl32.Vec3{0, 1.2})))) + if belowAir && !liquidBelow { + vel := movement.Vel() + vel[1] = 0 + movement.SetVel(vel) + return + } + } + vel := movement.Vel() + vel[1] += (targetY - vel[1]) * rate + movement.SetVel(vel) +} + +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 { + 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 applyLiquidFlow(p *player.Player, movement player.MovementComponent, positions []df_cube.Pos) { + flow := mgl32.Vec3{} + for _, pos := range positions { + liquid, ok := liquidAt(p, pos) + water, waterOK := liquid.(block.Water) + if !ok || !waterOK { + continue + } + flow = flow.Add(liquidFlow(p, pos, water)) + } + if length := flow.Len(); length >= 1e-4 { + vel := movement.Vel().Add(flow.Mul(0.014 / length)) + movement.SetVel(vel) + p.Dbg.Notify(player.DebugModeMovementSim, true, "water flow applied flow=%v vel=%v", flow, vel) + } +} + +func liquidFlow(p *player.Player, pos df_cube.Pos, water block.Water) mgl32.Vec3 { + currentDecay := liquidDecay(water) + 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 { + neighbourWater, same := neighbour.(block.Water) + if same { + if !liquidFlowSideClosed(p, pos, neighbourPos) && !liquidFlowSideClosed(p, neighbourPos, pos) { + flow = flow.Add(face.vec.Mul(float32(liquidDecay(neighbourWater) - 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 lowerWater, same := lower.(block.Water); same { + flow = flow.Add(face.vec.Mul(float32(liquidDecay(lowerWater) - currentDecay + 8))) + } + } + } + if water.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/world/world.go b/world/world.go index de84fa66..91c1d334 100644 --- a/world/world.go +++ b/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,31 @@ 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) + updates[chunkPos] = make(map[df_cube.Pos]world.Block) } c := w.Chunk(chunkPos) @@ -105,8 +119,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 +128,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 +200,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. From e85520bcda348ed324a12d290540a289e996054a Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:56:55 -0400 Subject: [PATCH 02/11] feat(this): polish liquid movement --- player/component/movement.go | 11 ++++++ player/simulation/movement.go | 66 +++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/player/component/movement.go b/player/component/movement.go index 5a88a12a..55015b49 100644 --- a/player/component/movement.go +++ b/player/component/movement.go @@ -914,6 +914,17 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { } mc.impulse = pk.MoveVector.Mul(0.98) + mc.mPlayer.Dbg.Notify( + player.DebugModeMovementSim, + true, + "input swimming=%t jumping=%t pressingJump=%t autoJumpingInWater=%t wantDownSlow=%t jumpCurrentRaw=%t", + mc.swimming, + mc.jumping, + mc.pressingJump, + mc.autoJumpingInWater, + 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. diff --git a/player/simulation/movement.go b/player/simulation/movement.go index 105169c8..c3badcae 100644 --- a/player/simulation/movement.go +++ b/player/simulation/movement.go @@ -64,6 +64,14 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) 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(), + ) if movement.Swimming() && len(waterBlocks) == 0 { movement.SetSwimming(false) } @@ -307,7 +315,7 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l if movement.PressingJump() { newVel := movement.Vel() - below := p.World().Block(df_cube.Pos(cube.PosFromVec3(movement.Pos().Sub(mgl32.Vec3{0, 1.1})))) + below := liquidMovementBlock(p, df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.1})))) if movement.Swimming() { if _, air := below.(block.Air); air { newVel[1] = 0 @@ -344,15 +352,6 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l setPostCollisionMotion(p, oldVel, oldOnGround, block.Air{}) movement.SetMov(movement.Vel()) - if movement.XCollision() || movement.ZCollision() { - vel := movement.Vel() - raised := mgl32.Vec3{vel.X(), vel.Y() + 0.6 + initialY - movement.Pos().Y(), vel.Z()} - if !utils.HasNearbyBBoxes(movement.BoundingBox().Translate(raised), p.World()) { - vel[1] = 0.3 - movement.SetVel(vel) - } - } - vel := movement.Vel() if water { drag := float32(0.8) @@ -372,7 +371,21 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l target := game.LevitationGravityMultiplier * float32(levitation.Amplifier+1) vel[1] += (target - vel[1]) * 0.2 } else if movement.HasGravity() { - vel[1] -= 0.02 + gravity := float32(0.02) + if water { + gravity = 0.005 + if movement.Swimming() { + gravity = 0 + } + } + vel[1] -= gravity + } + if water && (movement.XCollision() || movement.ZCollision() || movement.Client().HorizontalCollision()) { + raised := mgl32.Vec3{vel.X(), vel.Y() + 0.6 + initialY - movement.Pos().Y(), vel.Z()} + raisedBox := movement.BoundingBox().Translate(raised) + if !utils.HasNearbyBBoxes(raisedBox, p.World()) && !containsAnyLiquid(p, raisedBox) { + vel[1] = 0.3 + } } movement.SetVel(vel) movement.SetFallDistance(0) @@ -391,10 +404,8 @@ func updateSwimTravel(p *player.Player, movement player.MovementComponent) { } if targetY > 0 && !movement.WantDownSlow() { - belowPos := df_cube.Pos(cube.PosFromVec3(movement.Pos().Sub(mgl32.Vec3{0, 1.1}))) - _, belowAir := p.World().Block(belowPos).(block.Air) - _, liquidBelow := liquidAt(p, df_cube.Pos(cube.PosFromVec3(movement.Pos().Sub(mgl32.Vec3{0, 1.2})))) - if belowAir && !liquidBelow { + belowPos := df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.1}))) + if _, belowAir := liquidMovementBlock(p, belowPos).(block.Air); belowAir { vel := movement.Vel() vel[1] = 0 movement.SetVel(vel) @@ -406,6 +417,14 @@ func updateSwimTravel(p *player.Player, movement player.MovementComponent) { 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} @@ -454,6 +473,22 @@ func liquidAt(p *player.Player, pos df_cube.Pos) (world.Liquid, bool) { return liquid, ok } +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 y := minY; y < maxY; y++ { + for z := minZ; z < maxZ; z++ { + 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) { flow := mgl32.Vec3{} for _, pos := range positions { @@ -464,6 +499,7 @@ func applyLiquidFlow(p *player.Player, movement player.MovementComponent, positi } flow = flow.Add(liquidFlow(p, pos, water)) } + flow[1] = 0 if length := flow.Len(); length >= 1e-4 { vel := movement.Vel().Add(flow.Mul(0.014 / length)) movement.SetVel(vel) From 63544888b1b0c612c032de64cad0769b83f26541 Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:43:18 -0400 Subject: [PATCH 03/11] feat(movement): improve liquid simulation --- player/component/movement.go | 15 +++++++++++---- player/movement.go | 2 ++ player/simulation/movement.go | 28 ++++++++++------------------ 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/player/component/movement.go b/player/component/movement.go index 55015b49..d0dc0789 100644 --- a/player/component/movement.go +++ b/player/component/movement.go @@ -115,6 +115,7 @@ type AuthoritativeMovementComponent struct { sneaking, pressingSneak bool swimming bool autoJumpingInWater bool + wantDown bool wantDownSlow bool jumping, pressingJump bool @@ -377,6 +378,11 @@ 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 @@ -865,6 +871,7 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.sneaking = false } 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( @@ -881,9 +888,6 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { 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) @@ -917,11 +921,13 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.mPlayer.Dbg.Notify( player.DebugModeMovementSim, true, - "input swimming=%t jumping=%t pressingJump=%t autoJumpingInWater=%t wantDownSlow=%t jumpCurrentRaw=%t", + "input swimming=%t sneaking=%t jumping=%t pressingJump=%t autoJumpingInWater=%t wantDown=%t wantDownSlow=%t jumpCurrentRaw=%t", mc.swimming, + mc.sneaking, mc.jumping, mc.pressingJump, mc.autoJumpingInWater, + mc.wantDown, mc.wantDownSlow, pk.InputData.Load(packet.InputFlagJumpCurrentRaw), ) @@ -1204,6 +1210,7 @@ func (mc *AuthoritativeMovementComponent) ResetTransferState(pos mgl32.Vec3) { mc.pressingSneak = false mc.swimming = false mc.autoJumpingInWater = false + mc.wantDown = false mc.wantDownSlow = false mc.jumping = false diff --git a/player/movement.go b/player/movement.go index 4b7e5713..8a63b8ae 100644 --- a/player/movement.go +++ b/player/movement.go @@ -102,6 +102,8 @@ type MovementComponent interface { SetSwimming(swimming bool) // 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 diff --git a/player/simulation/movement.go b/player/simulation/movement.go index c3badcae..1510a804 100644 --- a/player/simulation/movement.go +++ b/player/simulation/movement.go @@ -101,6 +101,9 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) blockFriction *= utils.BlockFriction(blockUnder) moveRelativeSpeed = mSpeed * (0.16277136 / (blockFriction * blockFriction * blockFriction)) } + if movement.Sneaking() { + moveRelativeSpeed *= game.MaxSneakImpulse + } if movement.Gliding() { _, hasElytra := p.Inventory().Chestplate().Item().(item.Elytra) @@ -310,6 +313,11 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l initialY := movement.Pos().Y() _, water := liquid.(block.Water) if water { + if movement.WantDown() || movement.WantDownSlow() { + vel := movement.Vel() + vel[1] -= 0.04 + movement.SetVel(vel) + } updateSwimTravel(p, movement) } @@ -380,10 +388,10 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l } vel[1] -= gravity } - if water && (movement.XCollision() || movement.ZCollision() || movement.Client().HorizontalCollision()) { + if water && (movement.XCollision() || movement.ZCollision()) { raised := mgl32.Vec3{vel.X(), vel.Y() + 0.6 + initialY - movement.Pos().Y(), vel.Z()} raisedBox := movement.BoundingBox().Translate(raised) - if !utils.HasNearbyBBoxes(raisedBox, p.World()) && !containsAnyLiquid(p, raisedBox) { + if !utils.HasNearbyBBoxes(raisedBox, p.World()) { vel[1] = 0.3 } } @@ -473,22 +481,6 @@ func liquidAt(p *player.Player, pos df_cube.Pos) (world.Liquid, bool) { return liquid, ok } -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 y := minY; y < maxY; y++ { - for z := minZ; z < maxZ; z++ { - 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) { flow := mgl32.Vec3{} for _, pos := range positions { From dca57114f89934d83bdb601687c37e9c4f28e3e5 Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:57:23 -0400 Subject: [PATCH 04/11] feat(this): polish liquid movement --- game/movement.go | 1 - player/component/movement.go | 38 +++++++++++++++++-------- player/movement.go | 4 +++ player/simulation/movement.go | 53 ++++++++++++++++++++++++----------- world/world.go | 2 -- 5 files changed, 67 insertions(+), 31 deletions(-) diff --git a/game/movement.go b/game/movement.go index 0c85301c..9dbe422b 100755 --- a/game/movement.go +++ b/game/movement.go @@ -14,7 +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) diff --git a/player/component/movement.go b/player/component/movement.go index d0dc0789..280859b5 100644 --- a/player/component/movement.go +++ b/player/component/movement.go @@ -114,12 +114,13 @@ type AuthoritativeMovementComponent struct { 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 @@ -338,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 @@ -373,6 +379,11 @@ 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 @@ -870,6 +881,11 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.swimming = true mc.sneaking = false } + if mc.swimming { + 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) @@ -881,18 +897,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 - } - 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 @@ -921,11 +931,13 @@ func (mc *AuthoritativeMovementComponent) Update(pk *packet.PlayerAuthInput) { mc.mPlayer.Dbg.Notify( player.DebugModeMovementSim, true, - "input swimming=%t sneaking=%t jumping=%t pressingJump=%t autoJumpingInWater=%t wantDown=%t wantDownSlow=%t jumpCurrentRaw=%t", + "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, @@ -1209,12 +1221,14 @@ 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/player/movement.go b/player/movement.go index 8a63b8ae..19965c2e 100644 --- a/player/movement.go +++ b/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. @@ -100,6 +102,8 @@ type MovementComponent interface { 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. diff --git a/player/simulation/movement.go b/player/simulation/movement.go index 1510a804..d552c3c6 100644 --- a/player/simulation/movement.go +++ b/player/simulation/movement.go @@ -138,7 +138,7 @@ 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 { @@ -312,6 +312,7 @@ func simulationIsReliable(p *player.Player, movement player.MovementComponent) b func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, liquid world.Liquid) { initialY := movement.Pos().Y() _, water := liquid.(block.Water) + jumping := movement.EffectiveJumping() if water { if movement.WantDown() || movement.WantDownSlow() { vel := movement.Vel() @@ -321,15 +322,13 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l updateSwimTravel(p, movement) } - if movement.PressingJump() { + if jumping { newVel := movement.Vel() below := liquidMovementBlock(p, df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.1})))) - if movement.Swimming() { - if _, air := below.(block.Air); air { - newVel[1] = 0 - } else { - newVel[1] += 0.04 - } + _, belowAir := below.(block.Air) + legacyTransition := p.VersionInRange(-1, player.GameVersion1_21_120) && movement.SwimAmount() > 0 && movement.SwimAmount() < 1 + if movement.Swimming() && belowAir || legacyTransition { + newVel[1] = 0 } else { newVel[1] += 0.04 } @@ -388,10 +387,13 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l } vel[1] -= gravity } - if water && (movement.XCollision() || movement.ZCollision()) { + 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) - if !utils.HasNearbyBBoxes(raisedBox, p.World()) { + 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 } } @@ -401,7 +403,7 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l } func updateSwimTravel(p *player.Player, movement player.MovementComponent) { - if !movement.Swimming() || movement.PressingJump() { + if !movement.Swimming() || movement.EffectiveJumping() { return } pitch := movement.Rotation().X() * math32.Pi / 180 @@ -414,10 +416,13 @@ func updateSwimTravel(p *player.Player, movement player.MovementComponent) { 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 { - vel := movement.Vel() - vel[1] = 0 - movement.SetVel(vel) - return + 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() @@ -481,6 +486,23 @@ func liquidAt(p *player.Player, pos df_cube.Pos) (world.Liquid, bool) { return liquid, ok } +// 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) { flow := mgl32.Vec3{} for _, pos := range positions { @@ -491,7 +513,6 @@ func applyLiquidFlow(p *player.Player, movement player.MovementComponent, positi } flow = flow.Add(liquidFlow(p, pos, water)) } - flow[1] = 0 if length := flow.Len(); length >= 1e-4 { vel := movement.Vel().Add(flow.Mul(0.014 / length)) movement.SetVel(vel) diff --git a/world/world.go b/world/world.go index 91c1d334..a677e1f5 100644 --- a/world/world.go +++ b/world/world.go @@ -110,8 +110,6 @@ func (w *World) BlockLayer(pos df_cube.Pos, layer uint8) world.Block { if b, ok := blockUpdates[df_cube.Pos(blockPos)]; ok { return b } - } else { - updates[chunkPos] = make(map[df_cube.Pos]world.Block) } c := w.Chunk(chunkPos) From a4d1480ea4841ae9d0bbda33037f4b52d3b846e3 Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:23:33 -0400 Subject: [PATCH 05/11] feat(this): refine movement simulation --- game/movement.go | 1 - player/component/movement.go | 3 ++- player/simulation/movement.go | 8 ++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/game/movement.go b/game/movement.go index 9dbe422b..13a3e8d3 100755 --- a/game/movement.go +++ b/game/movement.go @@ -14,7 +14,6 @@ const ( BedBounceMultiplier = float32(-0.66) // This can be validated in Mob::ascendLadder() ClimbSpeed = float32(0.2) - MaxSneakImpulse = float32(0.3) MaxNormalizedImpulse = float32(0.70710678118) // 1/sqrt(2) DefaultPlayerHeightOffset = float32(1.62) diff --git a/player/component/movement.go b/player/component/movement.go index 280859b5..3313be6d 100644 --- a/player/component/movement.go +++ b/player/component/movement.go @@ -875,13 +875,14 @@ 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 mc.swimming { + if wasSwimming { mc.swimAmount = game.ClampFloat(mc.swimAmount+0.1, 0, 1) } else { mc.swimAmount = game.ClampFloat(mc.swimAmount-0.1, 0, 1) diff --git a/player/simulation/movement.go b/player/simulation/movement.go index d552c3c6..5e1ef959 100644 --- a/player/simulation/movement.go +++ b/player/simulation/movement.go @@ -101,10 +101,6 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) blockFriction *= utils.BlockFriction(blockUnder) moveRelativeSpeed = mSpeed * (0.16277136 / (blockFriction * blockFriction * blockFriction)) } - if movement.Sneaking() { - moveRelativeSpeed *= game.MaxSneakImpulse - } - if movement.Gliding() { _, hasElytra := p.Inventory().Chestplate().Item().(item.Elytra) if hasElytra && !movement.OnGround() { @@ -326,8 +322,8 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l newVel := movement.Vel() below := liquidMovementBlock(p, df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.1})))) _, belowAir := below.(block.Air) - legacyTransition := p.VersionInRange(-1, player.GameVersion1_21_120) && movement.SwimAmount() > 0 && movement.SwimAmount() < 1 - if movement.Swimming() && belowAir || legacyTransition { + swimTransition := movement.SwimAmount() > 0 && movement.SwimAmount() < 1 + if movement.Swimming() && belowAir || swimTransition { newVel[1] = 0 } else { newVel[1] += 0.04 From fb62da3c21d932de1a5bfd1c8c2041a3b89228c7 Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:34:08 -0400 Subject: [PATCH 06/11] feat(this): refine liquid swimming --- player/simulation/movement.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/player/simulation/movement.go b/player/simulation/movement.go index 5e1ef959..f3f4cd0b 100644 --- a/player/simulation/movement.go +++ b/player/simulation/movement.go @@ -72,9 +72,6 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) len(lavaBlocks), movement.Swimming(), ) - if movement.Swimming() && len(waterBlocks) == 0 { - movement.SetSwimming(false) - } if !movement.Flying() && (len(waterBlocks) != 0 || len(lavaBlocks) != 0) { p.Dbg.Notify(player.DebugModeMovementSim, attemptKnockback(movement), "knockback applied in liquid: %v", movement.Vel()) if len(waterBlocks) != 0 { @@ -320,10 +317,17 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l if jumping { newVel := movement.Vel() - below := liquidMovementBlock(p, df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.1})))) - _, belowAir := below.(block.Air) + jumpUnsupported := false + if movement.Swimming() { + blockPos := df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.1}))) + if _, air := liquidMovementBlock(p, blockPos).(block.Air); air { + liquidPos := df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.2}))) + _, supported := liquidAt(p, liquidPos) + jumpUnsupported = !supported + } + } swimTransition := movement.SwimAmount() > 0 && movement.SwimAmount() < 1 - if movement.Swimming() && belowAir || swimTransition { + if jumpUnsupported || swimTransition { newVel[1] = 0 } else { newVel[1] += 0.04 From 1e62f09aaf1a5a8bc23f8a0404142a313138f471 Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:43:52 -0400 Subject: [PATCH 07/11] feat(this): align climb debug output --- player/simulation/movement.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player/simulation/movement.go b/player/simulation/movement.go index f3f4cd0b..b63291e0 100644 --- a/player/simulation/movement.go +++ b/player/simulation/movement.go @@ -138,7 +138,7 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) 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) } From 317b324c268942db8e4a34ed9fb4b93a5d884e86 Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:35:47 -0400 Subject: [PATCH 08/11] feat(this): finalize liquid movement --- player/simulation/movement.go | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/player/simulation/movement.go b/player/simulation/movement.go index b63291e0..6e1b2525 100644 --- a/player/simulation/movement.go +++ b/player/simulation/movement.go @@ -75,15 +75,15 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) if !movement.Flying() && (len(waterBlocks) != 0 || len(lavaBlocks) != 0) { p.Dbg.Notify(player.DebugModeMovementSim, attemptKnockback(movement), "knockback applied in liquid: %v", movement.Vel()) if len(waterBlocks) != 0 { + if movement.Gliding() { + movement.SetGliding(false) + movement.SetGlideBoost(0) + } applyLiquidFlow(p, movement, waterBlocks) simulateLiquidTravel(p, movement, block.Water{}) } else { simulateLiquidTravel(p, movement, block.Lava{}) } - if movement.Gliding() && movement.OnGround() { - movement.SetGliding(false) - movement.SetGlideBoost(0) - } return } @@ -317,17 +317,8 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l if jumping { newVel := movement.Vel() - jumpUnsupported := false - if movement.Swimming() { - blockPos := df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.1}))) - if _, air := liquidMovementBlock(p, blockPos).(block.Air); air { - liquidPos := df_cube.Pos(cube.PosFromVec3(movement.Pos().Add(mgl32.Vec3{0, game.DefaultPlayerHeightOffset - 1.2}))) - _, supported := liquidAt(p, liquidPos) - jumpUnsupported = !supported - } - } swimTransition := movement.SwimAmount() > 0 && movement.SwimAmount() < 1 - if jumpUnsupported || swimTransition { + if swimTransition { newVel[1] = 0 } else { newVel[1] += 0.04 @@ -337,8 +328,12 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l 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() { @@ -346,8 +341,8 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l } } depthStriderFraction := depthStriderLevel / float32(enchantment.DepthStrider.MaxLevel()) - if multiplier := movement.SwimSpeedMultiplier(); multiplier > 1 { - moveRelativeSpeed *= (0.7 + depthStriderFraction*0.3) * multiplier + if swimSpeedMultiplier > 1 { + moveRelativeSpeed *= (0.7 + depthStriderFraction*0.3) * swimSpeedMultiplier } else { moveRelativeSpeed += (movement.MovementSpeed() - moveRelativeSpeed) * depthStriderFraction } @@ -365,7 +360,7 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l if movement.Sprinting() { drag = 0.9 } - if depthStriderLevel > 0 && movement.SwimSpeedMultiplier() <= 1 { + if depthStriderLevel > 0 && swimSpeedMultiplier <= 1 { drag += (0.54600006 - drag) * (depthStriderLevel / float32(enchantment.DepthStrider.MaxLevel())) } vel[0] *= drag From 71b2ed67d5ee04e9d8f5149fc77c745f4900ca98 Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:53:05 -0400 Subject: [PATCH 09/11] feat(simulation): poor little lava forgot about it --- player/simulation/movement.go | 58 +++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/player/simulation/movement.go b/player/simulation/movement.go index 6e1b2525..14e285bd 100644 --- a/player/simulation/movement.go +++ b/player/simulation/movement.go @@ -79,9 +79,10 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) movement.SetGliding(false) movement.SetGlideBoost(0) } - applyLiquidFlow(p, movement, waterBlocks) + applyLiquidFlow(p, movement, waterBlocks, block.Water{}) simulateLiquidTravel(p, movement, block.Water{}) } else { + applyLiquidFlow(p, movement, lavaBlocks, block.Lava{}) simulateLiquidTravel(p, movement, block.Lava{}) } return @@ -452,6 +453,22 @@ func touchingLiquidBlocks[T world.Liquid](p *player.Player, movement player.Move 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) } } @@ -481,6 +498,13 @@ func liquidAt(p *player.Player, pos df_cube.Pos) (world.Liquid, bool) { 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() @@ -498,25 +522,28 @@ func containsAnyLiquid(p *player.Player, box cube.BBox) bool { return false } -func applyLiquidFlow(p *player.Player, movement player.MovementComponent, positions []df_cube.Pos) { +func applyLiquidFlow(p *player.Player, movement player.MovementComponent, positions []df_cube.Pos, liquid world.Liquid) { flow := mgl32.Vec3{} for _, pos := range positions { - liquid, ok := liquidAt(p, pos) - water, waterOK := liquid.(block.Water) - if !ok || !waterOK { + current, ok := liquidAt(p, pos) + if !ok || current.LiquidType() != liquid.LiquidType() { continue } - flow = flow.Add(liquidFlow(p, pos, water)) + flow = flow.Add(liquidFlow(p, pos, current)) } if length := flow.Len(); length >= 1e-4 { - vel := movement.Vel().Add(flow.Mul(0.014 / length)) + 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, "water flow applied flow=%v vel=%v", flow, 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, water block.Water) mgl32.Vec3 { - currentDecay := liquidDecay(water) +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 @@ -530,10 +557,9 @@ func liquidFlow(p *player.Player, pos df_cube.Pos, water block.Water) mgl32.Vec3 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 { - neighbourWater, same := neighbour.(block.Water) - if same { + if neighbour.LiquidType() == liquid.LiquidType() { if !liquidFlowSideClosed(p, pos, neighbourPos) && !liquidFlowSideClosed(p, neighbourPos, pos) { - flow = flow.Add(face.vec.Mul(float32(liquidDecay(neighbourWater) - currentDecay))) + flow = flow.Add(face.vec.Mul(float32(liquidDecay(neighbour) - currentDecay))) } continue } @@ -543,12 +569,12 @@ func liquidFlow(p *player.Player, pos df_cube.Pos, water block.Water) mgl32.Vec3 } below := df_cube.Pos{neighbourPos[0], neighbourPos[1] - 1, neighbourPos[2]} if lower, ok := liquidAt(p, below); ok { - if lowerWater, same := lower.(block.Water); same { - flow = flow.Add(face.vec.Mul(float32(liquidDecay(lowerWater) - currentDecay + 8))) + if lower.LiquidType() == liquid.LiquidType() { + flow = flow.Add(face.vec.Mul(float32(liquidDecay(lower) - currentDecay + 8))) } } } - if water.LiquidFalling() { + 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]} From fedcda22e8f7a5ac2cd9419397ed215ee3fc2773 Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:01:40 -0400 Subject: [PATCH 10/11] fix(lint): resolve monorepo lint issues --- anticheat/integration/proxy/proxy.go | 2 +- transferproxy/.golangci.yml | 7 +++++++ transferproxy/proxy.go | 10 ++++++---- transferproxy/state_tracker.go | 5 +++-- 4 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 transferproxy/.golangci.yml 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/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: From 0bcbb8be25593f836a66ee6a4e302d4fb81fd2bb Mon Sep 17 00:00:00 2001 From: NopeNotDark <61342696+NopeNotDark@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:29:40 -0400 Subject: [PATCH 11/11] fix(simulation): preserve water travel while swimming --- anticheat/player/simulation/movement.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/anticheat/player/simulation/movement.go b/anticheat/player/simulation/movement.go index b95f0454..f637b64f 100644 --- a/anticheat/player/simulation/movement.go +++ b/anticheat/player/simulation/movement.go @@ -72,18 +72,19 @@ func SimulatePlayerMovement(p *player.Player, movement player.MovementComponent) len(lavaBlocks), movement.Swimming(), ) - if !movement.Flying() && (len(waterBlocks) != 0 || len(lavaBlocks) != 0) { + 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 len(waterBlocks) != 0 { + if waterTravel { if movement.Gliding() { movement.SetGliding(false) movement.SetGlideBoost(0) } applyLiquidFlow(p, movement, waterBlocks, block.Water{}) - simulateLiquidTravel(p, movement, block.Water{}) + simulateLiquidTravel(p, movement, block.Water{}, len(waterBlocks) != 0) } else { applyLiquidFlow(p, movement, lavaBlocks, block.Lava{}) - simulateLiquidTravel(p, movement, block.Lava{}) + simulateLiquidTravel(p, movement, block.Lava{}, true) } return } @@ -303,7 +304,7 @@ 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) { +func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, liquid world.Liquid, touchingLiquid bool) { initialY := movement.Pos().Y() _, water := liquid.(block.Water) jumping := movement.EffectiveJumping() @@ -319,7 +320,7 @@ func simulateLiquidTravel(p *player.Player, movement player.MovementComponent, l if jumping { newVel := movement.Vel() swimTransition := movement.SwimAmount() > 0 && movement.SwimAmount() < 1 - if swimTransition { + if swimTransition || water && movement.Swimming() && !touchingLiquid { newVel[1] = 0 } else { newVel[1] += 0.04