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
28 changes: 1 addition & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,7 @@ go get github.com/oomph-ac/bedsim

## Setup

Before calling any bedsim function (`BlockName`, `BlockClimbable`, `BlockFriction`, or running a simulation tick), you **must** finalize the Dragonfly block registry used by your world. Without this, block runtime/hash lookups may be incomplete and `BlockName` can cache incorrect mappings permanently.

```go
import "github.com/df-mc/dragonfly/server/world"

var blocks = world.NewBlockRegistry()

func init() {
// Register custom blocks/states before finalizing.
// blocks.RegisterBlock(...)
// blocks.RegisterBlockState(...)

blocks.Finalize()
}
```

```go
conf := server.DefaultConfig()
conf.Blocks = blocks

sessionConf := session.Config{BlockRegistry: blocks}

ch := chunk.New(blocks, world.Overworld.Range())
decoded, err := chunk.NetworkDecode(blocks, payload, subChunkCount, world.Overworld.Range())
```

If you use only vanilla blocks, `world.DefaultBlockRegistry` is still valid after it has been finalized by Dragonfly configuration setup or by an explicit `world.DefaultBlockRegistry.Finalize()` call. If you register custom blocks, do so **before** calling `Finalize`.
BedSim does not manage Dragonfly's block registry lifecycle. `BlockName` obtains the canonical name from the supplied `world.Block` and caches it by the block's raw base and state hashes.

## Usage

Expand Down
27 changes: 18 additions & 9 deletions bbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,66 @@ package bedsim

import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/go-gl/mathgl/mgl64"
"github.com/go-gl/mathgl/mgl32"
)

// BBoxFromDragonfly returns a simulation bounding box rounded to float32 coordinates.
func BBoxFromDragonfly(box cube.BBox) cube.BBox32 {
min, max := box.Min(), box.Max()
return cube.Box32(
float32(min.X()), float32(min.Y()), float32(min.Z()),
float32(max.X()), float32(max.Y()), float32(max.Z()),
)
}

// 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 {
func (s *MovementState) BoundingBox(useSlideOffset bool) cube.BBox32 {
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
yOffset := float32(0)
if useSlideOffset {
yOffset = s.SlideOffset.Y()
}

return cube.Box(
return cube.Box32(
s.Pos[0]-width,
s.Pos[1]+yOffset,
s.Pos[2]-width,
s.Pos[0]+width,
s.Pos[1]+height+yOffset,
s.Pos[2]+width,
).GrowVec3(mgl64.Vec3{-1e-4, 0, -1e-4})
).GrowVec3(mgl32.Vec3{-1e-4, 0, -1e-4})
}

// ClientBoundingBox returns the bounding box translated to the client's position.
func (s *MovementState) ClientBoundingBox(useSlideOffset bool) cube.BBox {
func (s *MovementState) ClientBoundingBox(useSlideOffset bool) cube.BBox32 {
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
yOffset := float32(0)
if useSlideOffset {
yOffset = s.SlideOffset.Y()
}

return cube.Box(
return cube.Box32(
s.Client.Pos[0]-width,
s.Client.Pos[1]+yOffset,
s.Client.Pos[2]-width,
s.Client.Pos[0]+width,
s.Client.Pos[1]+height+yOffset,
s.Client.Pos[2]+width,
).GrowVec3(mgl64.Vec3{-1e-4, 0, -1e-4})
).GrowVec3(mgl32.Vec3{-1e-4, 0, -1e-4})
}
41 changes: 19 additions & 22 deletions block.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,34 @@ import (
"github.com/df-mc/dragonfly/server/world"
)

var (
blockNameMapping map[uint64]string
blockNameMappingOnce sync.Once
)

func initBlockNameMapping() {
blockNameMapping = make(map[uint64]string, len(world.Blocks()))
for _, b := range world.Blocks() {
x, y := b.Hash()
if x == 0 && y == math.MaxUint64 {
continue
}
name, _ := b.EncodeBlock()
blockNameMapping[world.BlockHash(b)] = name
}
type blockNameKey struct {
base, state uint64
}

var blockNameCache sync.Map

// BlockName returns the canonical name of a block.
func BlockName(b world.Block) string {
blockNameMappingOnce.Do(initBlockNameMapping)
if n, ok := blockNameMapping[world.BlockHash(b)]; ok {
return n
base, state := b.Hash()
if base == 0 && state == math.MaxUint64 {
name, _ := b.EncodeBlock()
return name
}
n, _ := b.EncodeBlock()
return n

key := blockNameKey{base: base, state: state}
if name, ok := blockNameCache.Load(key); ok {
return name.(string)
}

name, _ := b.EncodeBlock()
stored, _ := blockNameCache.LoadOrStore(key, name)
return stored.(string)
}

// BlockFriction returns the friction of the block.
func BlockFriction(b world.Block) float64 {
func BlockFriction(b world.Block) float32 {
if f, ok := b.(block.Frictional); ok {
return f.Friction()
return float32(f.Friction())
}

switch BlockName(b) {
Expand Down
64 changes: 64 additions & 0 deletions block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package bedsim

import (
"math"
"testing"

"github.com/df-mc/dragonfly/server/world"
)

type namedBlock struct {
name string
base, state uint64
encodeCalls *int
}

func (b namedBlock) EncodeBlock() (string, map[string]any) {
*b.encodeCalls++
return b.name, nil
}

func (b namedBlock) Hash() (uint64, uint64) {
return b.base, b.state
}

func (namedBlock) Model() world.BlockModel {
return nil
}

func TestBlockNameCachesRawHashPair(t *testing.T) {
var calls int
b := namedBlock{name: "test:cached", base: 0xf32ca, state: 7, encodeCalls: &calls}

if got := BlockName(b); got != b.name {
t.Fatalf("first BlockName() = %q, want %q", got, b.name)
}
if got := BlockName(b); got != b.name {
t.Fatalf("second BlockName() = %q, want %q", got, b.name)
}
if calls != 1 {
t.Fatalf("EncodeBlock() called %d times, want 1", calls)
}
}

func TestBlockNameDoesNotCacheUnknownHash(t *testing.T) {
var calls int
b := namedBlock{name: "test:unknown", state: math.MaxUint64, encodeCalls: &calls}

BlockName(b)
BlockName(b)
if calls != 2 {
t.Fatalf("EncodeBlock() called %d times, want 2", calls)
}
}

func TestBlockNameCachesMaxStateWithKnownBase(t *testing.T) {
var calls int
b := namedBlock{name: "test:max_state", base: 1, state: math.MaxUint64, encodeCalls: &calls}

BlockName(b)
BlockName(b)
if calls != 1 {
t.Fatalf("EncodeBlock() called %d times, want 1", calls)
}
}
38 changes: 19 additions & 19 deletions collision.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package bedsim

import (
"math"
"github.com/chewxy/math32"

"github.com/df-mc/dragonfly/server/block/cube"
"github.com/go-gl/mathgl/mgl64"
"github.com/go-gl/mathgl/mgl32"
)

type clipCollideResult struct {
depenetratingAxis int
penetration float64
clippedVelocity mgl64.Vec3
depenetratingVelocity mgl64.Vec3
penetration float32
clippedVelocity mgl32.Vec3
depenetratingVelocity mgl32.Vec3
}

// BBClipCollide clips or depenetrates a moving bounding box against a stationary one.
func BBClipCollide(this, c cube.BBox, vel mgl64.Vec3, oneWay bool, penetration *mgl64.Vec3) mgl64.Vec3 {
func BBClipCollide(this, c cube.BBox32, vel mgl32.Vec3, oneWay bool, penetration *mgl32.Vec3) mgl32.Vec3 {
result := doBBClipCollide(this, c, vel)
if penetration != nil && penetration[result.depenetratingAxis] < result.penetration {
penetration[result.depenetratingAxis] = result.penetration
Expand All @@ -27,33 +27,33 @@ func BBClipCollide(this, c cube.BBox, vel mgl64.Vec3, oneWay bool, penetration *
return result.depenetratingVelocity
}

func doBBClipCollide(stationary, moving cube.BBox, velocity mgl64.Vec3) (result clipCollideResult) {
func doBBClipCollide(stationary, moving cube.BBox32, velocity mgl32.Vec3) (result clipCollideResult) {
result.clippedVelocity = velocity
result.depenetratingVelocity = velocity

if BBHasZeroVolume(stationary) {
return
}

axisPenetrations := [3]float64{}
axisPenetrationsSigned := [3]float64{}
normalDirs := [3]float64{}
axisPenetrations := [3]float32{}
axisPenetrationsSigned := [3]float32{}
normalDirs := [3]float32{}
separatingAxes, separatingAxis := 0, 0
resultPenetration := math.MaxFloat64 - 1
resultPenetration := float32(math32.MaxFloat32 - 1)

for i := range 3 {
minPenetration := moving.Max()[i] - stationary.Min()[i]
maxPenetration := stationary.Max()[i] - moving.Min()[i]

if math.Abs(minPenetration) <= 1e-7 {
if math32.Abs(minPenetration) <= 1e-7 {
minPenetration = 0
}
if math.Abs(maxPenetration) <= 1e-7 {
if math32.Abs(maxPenetration) <= 1e-7 {
maxPenetration = 0
}

minPositive := math.Max(0, minPenetration)
maxPositive := math.Max(0, maxPenetration)
minPositive := math32.Max(0, minPenetration)
maxPositive := math32.Max(0, maxPenetration)

if minPositive == 0 {
axisPenetrations[i] = 0
Expand All @@ -80,7 +80,7 @@ func doBBClipCollide(stationary, moving cube.BBox, velocity mgl64.Vec3) (result
if separatingAxes > 1 {
return
}
resultPenetration = math.Min(resultPenetration, axisPenetrations[i])
resultPenetration = math32.Min(resultPenetration, axisPenetrations[i])
}

// No separating axes means a collision.
Expand All @@ -95,9 +95,9 @@ func doBBClipCollide(stationary, moving cube.BBox, velocity mgl64.Vec3) (result

desiredVelocity := axisPenetrations[bestAxis] * normalDirs[bestAxis]
if desiredVelocity > 0 {
result.depenetratingVelocity[bestAxis] = math.Max(desiredVelocity, velocity[bestAxis])
result.depenetratingVelocity[bestAxis] = math32.Max(desiredVelocity, velocity[bestAxis])
} else {
result.depenetratingVelocity[bestAxis] = math.Min(desiredVelocity, velocity[bestAxis])
result.depenetratingVelocity[bestAxis] = math32.Min(desiredVelocity, velocity[bestAxis])
}
result.depenetratingAxis = bestAxis
return
Expand All @@ -115,6 +115,6 @@ func doBBClipCollide(stationary, moving cube.BBox, velocity mgl64.Vec3) (result
}

// BBHasZeroVolume returns true if the bounding box has zero volume.
func BBHasZeroVolume(bb cube.BBox) bool {
func BBHasZeroVolume(bb cube.BBox32) bool {
return bb.Min() == bb.Max()
}
42 changes: 21 additions & 21 deletions constants.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
package bedsim

const (
DefaultJumpHeight = 0.42
DefaultAirFriction = 0.91
DefaultBlockFriction = 0.6
NormalGravityMultiplier = 0.98
LevitationGravityMultiplier = 0.05
NormalGravity = 0.08
SlowFallingGravity = 0.01
StepHeight = 0.6
SlideOffsetMultiplier = 0.4
SlimeBounceMultiplier = -1.0
BedBounceMultiplier = -0.66
DefaultJumpHeight = float32(0.42)
DefaultAirFriction = float32(0.91)
DefaultBlockFriction = float32(0.6)
NormalGravityMultiplier = float32(0.98)
LevitationGravityMultiplier = float32(0.05)
NormalGravity = float32(0.08)
SlowFallingGravity = float32(0.01)
StepHeight = float32(0.6)
SlideOffsetMultiplier = float32(0.4)
SlimeBounceMultiplier = float32(-1)
BedBounceMultiplier = float32(-0.66)
// This can be validated in Mob::ascendLadder().
ClimbSpeed = 0.2
MaxConsumingImpulse = 0.1225
MaxSneakImpulse = 0.3
ClimbSpeed = float32(0.2)
MaxConsumingImpulse = float32(0.1225)
MaxSneakImpulse = float32(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
MaxNormalizedImpulse = float32(0.70710678118) // 1/sqrt(2)
DefaultUnderwaterMovementSpeed = float32(0.02)
DefaultLavaMovementSpeed = float32(0.02)
DefaultSwimSpeedMultiplier = float32(1)

DefaultPlayerHeightOffset = 1.62
SneakingPlayerHeightOffset = 1.27
DefaultPlayerHeightOffset = float32(1.62)
SneakingPlayerHeightOffset = float32(1.27)

// TerminalVelocity is the natural convergence of the gravity formula:
// (v - 0.08) * 0.98 = v → v = -3.92. This is not explicitly clamped;
// it emerges from the per-tick gravity and drag multipliers.
TerminalVelocity = -3.92
TerminalVelocity = float32(-3.92)

JumpDelayTicks = 10
GlideBoostTicks = 20
Expand Down
Loading