Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

Server-side Minecraft Bedrock movement simulation library for Go.

`bedsim` replicates the Bedrock client's movement physics (collisions, stepping, edge-avoidance, gliding, teleportation) on the server, producing authoritative position and velocity values that can be compared against client-reported state.
`bedsim` replicates the Bedrock client's movement physics (collisions, stepping, edge-avoidance, liquids, gliding, teleportation) on the server, producing authoritative position and velocity values that can be compared against client-reported state.

Original code was written by [ethaniccc](https://github.com/ethaniccc) in [oomph](https://github.com/oomph-ac/oomph) and has been ported over into this library.
The liquid movement physics were ported from [oomph#145](https://github.com/oomph-ac/oomph/pull/145) by [NopeNotDark](https://github.com/NopeNotDark).

## Installation

Expand Down Expand Up @@ -50,12 +51,14 @@ Implement provider adapters to bridge your world and player systems:
sim := bedsim.Simulator{
World: myWorldProvider, // block lookups, collisions, chunk-loaded checks
BlockSemantics: myBlockSemantics, // optional: per-world names, friction, climbability
Liquids: myLiquidProvider, // second block layer (waterlogged blocks)
Effects: myEffectsProvider, // jump boost, levitation, slow falling
Inventory: myInventoryProvider, // elytra equipped check
Options: bedsim.SimulationOptions{
Mode: bedsim.SimulationModeAuthoritative,
PositionCorrectionThreshold: 0.5,
VelocityCorrectionThreshold: 0.5,
RequireLiquidLayer: true,
},
}

Expand All @@ -70,6 +73,102 @@ registry or custom block data instead of bedsim's Dragonfly-backed defaults.
Custom friction values must be finite and positive; invalid values fall back to
Dragonfly defaults.

Implement `DepthStriderProvider` on the inventory adapter when Depth Strider
should affect water movement.

> **Set `Liquids` if you simulate authoritatively.** Bedrock stores waterlogged
> blocks as a liquid in the *second* block layer. Without a `LiquidProvider`,
> bedsim only sees liquids returned by `WorldProvider.Block`, so waterlogged
> blocks look dry and the player is simulated with air physics inside them.
> Check `sim.HasLiquidLayer()` at startup, or set
> `SimulationOptions.RequireLiquidLayer` to make the simulator return
> `SimulationOutcomeUnreliable` rather than silently mis-simulate.
>
> If `Liquids` is nil and `World` itself implements `LiquidProvider`, that is
> used instead. This keeps pre-existing integrations working, but it is
> discovered by type assertion, so a signature typo degrades silently — prefer
> the explicit field.

### Liquid movement

When the player's hitbox touches water or lava (and the player is not flying),
bedsim runs liquid travel instead of the normal ground/air step. Water takes
priority when both are touched, and a player whose client reports the swimming
pose keeps water travel even after leaving the water blocks.

Liquid travel covers: per-liquid acceleration and drag, liquid gravity,
levitation, Depth Strider, the dolphin-boost swim multiplier, pitch-steered
swim travel with a surface clamp, flow from depth gradients including falling
liquid and solid faces, and an exit probe that boosts the player over a ledge.

Callers feed these `InputState` fields from the client's input flags:

| Field | Client input flag |
| --- | --- |
| `StartSwimming` / `StopSwimming` | `StartSwimming` / `StopSwimming` |
| `WantDown` / `WantDownSlow` | `WantDown` / `WantDownSlow` |
| `AutoJumpingInWater` | `AutoJumpingInWater` |
| `AscendBlock` | `AscendBlock` |

`Jumping` remains edge-triggered from `StartJumping` and arms a ground jump.
`EffectiveJumping` is derived from the held jump key, `AutoJumpingInWater`, and
`AscendBlock`, and drives liquid ascent and ladder climbing.

These `MovementState` fields tune liquid physics; each is optional and falls
back to a documented default when left at its zero value:

- `UnderwaterMovementSpeed` (default `0.02`)
- `LavaMovementSpeed` (default `0.02`)
- `SwimSpeedMultiplier` (default `1`; a dolphin boost sets it to `2`)

Set `DolphinBoostTicks` when the client receives a dolphin boost. `Simulate`
counts it down and restores `SwimSpeedMultiplier` to its default on expiry;
callers using `SimulateState` must manage that lifecycle themselves.

While the swim pose is active the bounding box collapses to a width-sized cube,
matching the client's swim pose. This affects collisions and liquid detection,
not just liquid travel. The pose requires both `Swimming` and recent
server-observed water contact — see `MovementState.SwimPose` and the divergence
note below.

#### Divergences from the upstream source

Two behaviors intentionally differ from oomph PR #145.

**Swim water-travel grace (security hardening).** Upstream keys water travel on
`touchingWater || Swimming`, trusting the client's swimming flag because the
surrounding anticheat validates it. A standalone simulator cannot. Left as-is,
a client that latches the flag gets two things it should not: *zero gravity*
forever in open air with no correction raised, and a server-side hitbox
shrunk from 1.8 to 0.6, letting it fit through gaps a standing player cannot.

bedsim gates **both** the water-travel branch and the swim pose on recent
server-observed water contact, bounded by
`SimulationOptions.SwimWaterGraceTicks` (default `DefaultSwimWaterGraceTicks`,
10). The budget refills on every tick the hitbox actually touches water, is
clamped to the configured bound before anything reads it, and is cleared on any
frame that was not simulated — unreliable, unloaded chunk, immobile, or
teleport. A negative value requires real water contact on every tick. Lava the
player is actually standing in takes priority over a retained water grace.

Because the budget is applied at the start of a tick and decremented at the end,
it is constant for the whole tick, so collision, liquid detection and exit
probing always agree on one hitbox. The cost is that entering water adopts the
swim pose one tick later than upstream, erring toward the larger box.

Players genuinely in water are unaffected. Two residual limits are worth
knowing: a client that reaches real water once every `SwimWaterGraceTicks` ticks
sustains water travel at up to a 10:1 duty cycle, so the guard bounds hovering
to the neighbourhood of actual water rather than eliminating it; and the pose
lag above is a deliberate one-tick divergence from upstream. Lower
`SwimWaterGraceTicks` to tighten both.

**Impulse clamps.** Upstream removed `MaxSneakImpulse` and `MaxConsumingImpulse`
in this PR, clamping the move vector to `[-1, 1]` instead. bedsim keeps both by
default, because they are public API affecting all movement and removing them
would be a breaking change outside liquid scope. Set
`SimulationOptions.UpstreamImpulseClamping` to opt into upstream's behavior.

### Simulation modes

- `Simulate` — applies client input, runs physics, advances tick counters, and returns the result. Use this when bedsim owns the full tick lifecycle.
Expand Down
12 changes: 12 additions & 0 deletions bbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ import (
"github.com/go-gl/mathgl/mgl64"
)

// SwimPose reports whether recent server-observed water contact permits the
// client-requested collapsed hitbox.
func (s *MovementState) SwimPose() bool {
return s.Swimming && s.SwimWaterGraceTicks > 0
}

// BoundingBox returns the entity bounding box translated to the current position.
func (s *MovementState) BoundingBox(useSlideOffset bool) cube.BBox {
scale := s.Size[2]
width := (s.Size[0] * 0.5) * scale
height := s.Size[1] * scale
if s.SwimPose() {
height = s.Size[0] * scale
}
yOffset := 0.0
if useSlideOffset {
yOffset = s.SlideOffset.Y()
Expand All @@ -30,6 +39,9 @@ func (s *MovementState) ClientBoundingBox(useSlideOffset bool) cube.BBox {
scale := s.Size[2]
width := (s.Size[0] * 0.5) * scale
height := s.Size[1] * scale
if s.SwimPose() {
height = s.Size[0] * scale
}
yOffset := 0.0
if useSlideOffset {
yOffset = s.SlideOffset.Y()
Expand Down
17 changes: 13 additions & 4 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ const (
SlimeBounceMultiplier = -1.0
BedBounceMultiplier = -0.66
// This can be validated in Mob::ascendLadder().
ClimbSpeed = 0.2
MaxConsumingImpulse = 0.1225
MaxSneakImpulse = 0.3
MaxNormalizedImpulse = 0.70710678118 // 1/sqrt(2)
ClimbSpeed = 0.2
MaxConsumingImpulse = 0.1225
MaxSneakImpulse = 0.3
// Deprecated: MaxNormalizedImpulse is unused by the simulator. The
// diagonal-impulse normalization it was intended for is disabled upstream
// as well. It is retained only for API compatibility.
MaxNormalizedImpulse = 0.70710678118 // 1/sqrt(2)
DefaultUnderwaterMovementSpeed = 0.02
DefaultLavaMovementSpeed = 0.02
DefaultSwimSpeedMultiplier = 1.0

DefaultPlayerHeightOffset = 1.62
SneakingPlayerHeightOffset = 1.27
Expand All @@ -28,4 +34,7 @@ const (

JumpDelayTicks = 10
GlideBoostTicks = 20

// DefaultSwimWaterGraceTicks bounds retained server-observed water contact.
DefaultSwimWaterGraceTicks = 10
)
11 changes: 9 additions & 2 deletions input.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@ type InputState struct {
SneakDown bool
Sneaking bool

StartJumping bool
Jumping bool
StartJumping bool
Jumping bool
AutoJumpingInWater bool
AscendBlock bool

StartSwimming bool
StopSwimming bool
WantDown bool
WantDownSlow bool

StopGliding bool
StartGliding bool
Expand Down
10 changes: 10 additions & 0 deletions interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ type WorldProvider interface {
IsChunkLoaded(chunkX, chunkZ int32) bool
}

// LiquidProvider returns liquids from either block layer at a position.
type LiquidProvider interface {
Liquid(pos cube.Pos) (world.Liquid, bool)
}

// BlockSemanticsProvider resolves movement-relevant block behavior. Implement
// this when names, friction, or climbability come from a per-world registry or
// custom block data instead of Dragonfly's default block types.
Expand All @@ -34,3 +39,8 @@ type EffectsProvider interface {
type InventoryProvider interface {
HasElytra() bool
}

// DepthStriderProvider exposes the equipped Depth Strider level.
type DepthStriderProvider interface {
DepthStriderLevel() int
}
Loading