diff --git a/README.md b/README.md index f868e70..af9e84c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bbox.go b/bbox.go index 2ccc74d..ccf109d 100644 --- a/bbox.go +++ b/bbox.go @@ -2,9 +2,18 @@ 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 { @@ -12,47 +21,47 @@ func (s *MovementState) SwimPose() bool { } // 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}) } diff --git a/block.go b/block.go index 528418c..d39d846 100644 --- a/block.go +++ b/block.go @@ -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) { diff --git a/block_test.go b/block_test.go new file mode 100644 index 0000000..e830b6d --- /dev/null +++ b/block_test.go @@ -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) + } +} diff --git a/collision.go b/collision.go index c00243c..3765af8 100644 --- a/collision.go +++ b/collision.go @@ -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 @@ -27,7 +27,7 @@ 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 @@ -35,25 +35,25 @@ func doBBClipCollide(stationary, moving cube.BBox, velocity mgl64.Vec3) (result 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 @@ -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. @@ -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 @@ -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() } diff --git a/constants.go b/constants.go index 19b9761..4438d41 100644 --- a/constants.go +++ b/constants.go @@ -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 diff --git a/go.mod b/go.mod index aa8e161..3730844 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,25 @@ module github.com/oomph-ac/bedsim -go 1.25.0 +go 1.26.1 require ( + github.com/chewxy/math32 v1.11.1 github.com/df-mc/dragonfly v0.10.11-0.20260205145355-8d1311b36535 github.com/go-gl/mathgl v1.2.0 - github.com/sandertv/gophertunnel v1.53.1-0.20260205132042-c839e607304f + github.com/sandertv/gophertunnel v1.57.0 ) +replace github.com/df-mc/dragonfly => github.com/hashimthearab/dragonfly v0.0.0-20260721043247-e11e4f6ede86 + require ( - github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 // indirect + github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479 // indirect github.com/df-mc/goleveldb v1.1.9 // indirect - github.com/df-mc/worldupgrader v1.0.20 // indirect + github.com/df-mc/worldupgrader v1.0.21 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/klauspost/compress v1.18.1 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/segmentio/fasthash v1.0.3 // indirect - golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/text v0.38.0 // indirect ) diff --git a/go.sum b/go.sum index 92ea313..3ede9ef 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ -github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 h1:/G0ghZwrhou0Wq21qc1vXXMm/t/aKWkALWwITptKbE0= -github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9/go.mod h1:TOk10ahXejq9wkEaym3KPRNeuR/h5Jx+s8QRWIa2oTM= -github.com/df-mc/dragonfly v0.10.11-0.20260205145355-8d1311b36535 h1:mbKNV+DY50ecEswbzv8qW17kwAxVifPCjBwBd84kyGw= -github.com/df-mc/dragonfly v0.10.11-0.20260205145355-8d1311b36535/go.mod h1:uhz6mAbgiUbkWfIWo88dqNNMJBuuaU5SD7sCjirhmb4= +github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479 h1:UZbbt19ACBOFO+CiDQFjaEoPJkBhj7GNGtIq59WR6Os= +github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479/go.mod h1:TOk10ahXejq9wkEaym3KPRNeuR/h5Jx+s8QRWIa2oTM= +github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8= +github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= github.com/df-mc/goleveldb v1.1.9 h1:ihdosZyy5jkQKrxucTQmN90jq/2lUwQnJZjIYIC/9YU= github.com/df-mc/goleveldb v1.1.9/go.mod h1:+NHCup03Sci5q84APIA21z3iPZCuk6m6ABtg4nANCSk= -github.com/df-mc/worldupgrader v1.0.20 h1:wfJyG3bFeaM/HXy7TCiO4HKVw3Mf3N4gPFmgxMHsKnc= -github.com/df-mc/worldupgrader v1.0.20/go.mod h1:tsSOLTRm9mpG7VHvYpAjjZrkRHWmSbKZAm9bOLNnlDk= +github.com/df-mc/worldupgrader v1.0.21 h1:Qr4/QB8ek7En0vkTuRXYq4FrZM0HHSOXsJOL7Ko4Cjg= +github.com/df-mc/worldupgrader v1.0.21/go.mod h1:tsSOLTRm9mpG7VHvYpAjjZrkRHWmSbKZAm9bOLNnlDk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-gl/mathgl v1.2.0 h1:v2eOj/y1B2afDxF6URV1qCYmo1KW08lAMtTbOn3KXCY= github.com/go-gl/mathgl v1.2.0/go.mod h1:pf9+b5J3LFP7iZ4XXaVzZrCle0Q/vNpB/vDe5+3ulRE= @@ -15,37 +15,39 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashimthearab/dragonfly v0.0.0-20260721043247-e11e4f6ede86 h1:Mk87hJwo3XaqjOKeKkN6+eBkWIZZmKIOzY3/jHoeEWw= +github.com/hashimthearab/dragonfly v0.0.0-20260721043247-e11e4f6ede86/go.mod h1:qZwpBcuVNCqHg8Nj6gec5bG+5LrBYPok1k7FKDQQrng= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/sandertv/gophertunnel v1.53.1-0.20260205132042-c839e607304f h1:D/wN9mwHazsrKb5+NDDX1s9H28R50BQp4TH0GRhPx0I= -github.com/sandertv/gophertunnel v1.53.1-0.20260205132042-c839e607304f/go.mod h1:F8+ZPbzxJ0LqunXEaDjqeyUgHVB0rI5ZU+PHnptXGfI= +github.com/sandertv/gophertunnel v1.57.0 h1:UkgVg1xLCsOSm79rP09WmodGSHgA8M7+l4quL01cIL8= +github.com/sandertv/gophertunnel v1.57.0/go.mod h1:W4VnrX9AIPIVXNDMEIKMIRj1T80EdOgdqXpGbQpyAbE= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= -golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= -golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= +golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/input.go b/input.go index 8d20f53..bf59470 100644 --- a/input.go +++ b/input.go @@ -1,17 +1,17 @@ package bedsim -import "github.com/go-gl/mathgl/mgl64" +import "github.com/go-gl/mathgl/mgl32" // InputState represents a single tick's client input and reported state. type InputState struct { - MoveVector mgl64.Vec2 + MoveVector mgl32.Vec2 - Pitch float64 - Yaw float64 - HeadYaw float64 + Pitch float32 + Yaw float32 + HeadYaw float32 - ClientPos mgl64.Vec3 - ClientVel mgl64.Vec3 + ClientPos mgl32.Vec3 + ClientVel mgl32.Vec3 HorizontalCollision bool VerticalCollision bool diff --git a/interfaces.go b/interfaces.go index 20e5d2e..252183f 100644 --- a/interfaces.go +++ b/interfaces.go @@ -8,8 +8,9 @@ import ( // WorldProvider bridges the world/chunk system for collision and block lookups. type WorldProvider interface { Block(pos cube.Pos) world.Block - BlockCollisions(pos cube.Pos) []cube.BBox - GetNearbyBBoxes(aabb cube.BBox) []cube.BBox + // BlockCollisions returns block-local collision boxes at pos. + BlockCollisions(pos cube.Pos) []cube.BBox32 + GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 IsChunkLoaded(chunkX, chunkZ int32) bool } @@ -23,7 +24,7 @@ type LiquidProvider interface { // custom block data instead of Dragonfly's default block types. type BlockSemanticsProvider interface { BlockName(world.Block) string - BlockFriction(world.Block) float64 + BlockFriction(world.Block) float32 BlockClimbable(world.Block) bool } diff --git a/liquid.go b/liquid.go index 82d5b5c..27e5660 100644 --- a/liquid.go +++ b/liquid.go @@ -1,17 +1,17 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" + "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) -// Liquid movement follows oomph PR #145 at 0bcbb8b. bedsim retains float64, -// provider-based liquid lookup and its legacy impulse clamps. It also requires +// Liquid movement follows oomph PR #145 at 0bcbb8b, with provider-based liquid +// lookup and legacy impulse clamps. It also requires // recent server-observed water contact before trusting the client swim flag. // See README.md for complete compatibility and security notes. @@ -36,12 +36,12 @@ func (k liquidKind) matches(liquid world.Liquid) bool { var liquidFaces = [...]struct { delta cube.Pos - vec mgl64.Vec3 + vec mgl32.Vec3 }{ - {cube.Pos{-1, 0, 0}, mgl64.Vec3{-1, 0, 0}}, - {cube.Pos{1, 0, 0}, mgl64.Vec3{1, 0, 0}}, - {cube.Pos{0, 0, -1}, mgl64.Vec3{0, 0, -1}}, - {cube.Pos{0, 0, 1}, mgl64.Vec3{0, 0, 1}}, + {cube.Pos{-1, 0, 0}, mgl32.Vec3{-1, 0, 0}}, + {cube.Pos{1, 0, 0}, mgl32.Vec3{1, 0, 0}}, + {cube.Pos{0, 0, -1}, mgl32.Vec3{0, 0, -1}}, + {cube.Pos{0, 0, 1}, mgl32.Vec3{0, 0, 1}}, } func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, touchingLiquid bool) { @@ -72,8 +72,8 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, if moveRelativeSpeed == 0 { moveRelativeSpeed = DefaultLavaMovementSpeed } - depthStriderLevel := 0.0 - swimSpeedMultiplier := DefaultSwimSpeedMultiplier + depthStriderLevel := float32(0) + swimSpeedMultiplier := float32(DefaultSwimSpeedMultiplier) if water { moveRelativeSpeed = state.UnderwaterMovementSpeed if moveRelativeSpeed == 0 { @@ -83,7 +83,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, swimSpeedMultiplier = state.SwimSpeedMultiplier } if inventory, ok := s.Inventory.(DepthStriderProvider); ok { - depthStriderLevel = math.Min(math.Max(float64(inventory.DepthStriderLevel()), 0), 3) + depthStriderLevel = math32.Min(math32.Max(float32(inventory.DepthStriderLevel()), 0), 3) if !state.OnGround { depthStriderLevel *= 0.5 } @@ -105,7 +105,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, vel := state.Vel if water { - drag := 0.8 + drag := float32(0.8) if state.Sprinting { drag = 0.9 } @@ -121,7 +121,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, if s.Effects != nil { if amplifier, ok := s.Effects.GetEffect(packet.EffectLevitation); ok { - target := LevitationGravityMultiplier * float64(amplifier+1) + target := LevitationGravityMultiplier * float32(amplifier+1) vel[1] += (target - vel[1]) * 0.2 } else if state.HasGravity { vel[1] -= liquidGravity(state.Swimming, water) @@ -131,7 +131,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, } if state.CollideX || state.CollideZ { - raised := mgl64.Vec3{vel.X(), vel.Y() + 0.6 + initialY - state.Pos.Y(), vel.Z()} + raised := mgl32.Vec3{vel.X(), vel.Y() + 0.6 + initialY - state.Pos.Y(), vel.Z()} raisedBox := state.BoundingBox(s.Options.UseSlideOffset).Translate(raised) hasCollision := hasNearbyBBoxes(s.World, raisedBox) hasLiquid := s.containsAnyLiquid(raisedBox) @@ -144,7 +144,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, state.FallDistance = 0 } -func liquidGravity(swimming, water bool) float64 { +func liquidGravity(swimming, water bool) float32 { if !water { return 0.02 } @@ -158,16 +158,16 @@ func (s *Simulator) updateSwimTravel(state *MovementState) { if !state.Swimming || state.EffectiveJumping { return } - targetY := -MCSin(state.Rotation.X() * math.Pi / 180) - rate := 0.06 + targetY := -MCSin(state.Rotation.X() * math32.Pi / 180) + rate := float32(0.06) if targetY < -0.2 { rate = 0.085 } if targetY > 0 && !state.WantDownSlow { - belowPos := cube.PosFromVec3(state.Pos.Add(mgl64.Vec3{0, DefaultPlayerHeightOffset - 1.1})) + belowPos := posFromVec3(state.Pos.Add(mgl32.Vec3{0, DefaultPlayerHeightOffset - 1.1})) if _, belowAir := s.liquidMovementBlock(belowPos).(block.Air); belowAir { - liquidPos := cube.PosFromVec3(state.Pos.Add(mgl64.Vec3{0, DefaultPlayerHeightOffset - 1.2})) + liquidPos := posFromVec3(state.Pos.Add(mgl32.Vec3{0, DefaultPlayerHeightOffset - 1.2})) if _, liquid := s.liquidAt(liquidPos); !liquid { vel := state.Vel vel[1] = 0 @@ -182,16 +182,16 @@ func (s *Simulator) updateSwimTravel(state *MovementState) { } func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) []cube.Pos { - box := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl64.Vec3{1e-4, 0, 1e-4}) - offset := mgl64.Vec3{0.001, 0.401, 0.001} + box := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl32.Vec3{1e-4, 0, 1e-4}) + offset := mgl32.Vec3{0.001, 0.401, 0.001} if kind == liquidLava { - offset = mgl64.Vec3{0.1, 0.4, 0.1} + offset = mgl32.Vec3{0.1, 0.4, 0.1} } box = shrinkLiquidBox(box, offset) min, max := box.Min(), box.Max() - minX, minY, minZ := int(math.Floor(min.X())), int(math.Floor(min.Y())), int(math.Floor(min.Z())) - maxX, maxY, maxZ := int(math.Floor(max.X()+1)), int(math.Floor(max.Y()+1)), int(math.Floor(max.Z()+1)) + minX, minY, minZ := int(math32.Floor(min.X())), int(math32.Floor(min.Y())), int(math32.Floor(min.Z())) + maxX, maxY, maxZ := int(math32.Floor(max.X()+1)), int(math32.Floor(max.Y()+1)), int(math32.Floor(max.Z()+1)) positions := make([]cube.Pos, 0, 4) for x := minX; x < maxX; x++ { for y := minY; y < maxY; y++ { @@ -203,7 +203,7 @@ func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) } if s.Options.Debugf != nil { height := liquidHeight(liquid) - surface := float64(pos[1]) + height + surface := float32(pos[1]) + height s.debugf( "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, @@ -217,7 +217,7 @@ func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) return positions } -func shrinkLiquidBox(box cube.BBox, offset mgl64.Vec3) cube.BBox { +func shrinkLiquidBox(box cube.BBox32, offset mgl32.Vec3) cube.BBox32 { min, max := box.Min().Add(offset), box.Max().Sub(offset) originalMin, originalMax := box.Min(), box.Max() for axis := range 3 { @@ -226,7 +226,7 @@ func shrinkLiquidBox(box cube.BBox, offset mgl64.Vec3) cube.BBox { min[axis], max[axis] = mid, mid } } - return cube.Box(min.X(), min.Y(), min.Z(), max.X(), max.Y(), max.Z()) + return cube.Box32(min.X(), min.Y(), min.Z(), max.X(), max.Y(), max.Z()) } func (s *Simulator) liquidMovementBlock(pos cube.Pos) world.Block { @@ -238,7 +238,7 @@ func (s *Simulator) liquidMovementBlock(pos cube.Pos) world.Block { // blockCollisions returns the collision boxes at pos, treating an absent world // as empty space so liquid flow never dereferences a nil provider. -func (s *Simulator) blockCollisions(pos cube.Pos) []cube.BBox { +func (s *Simulator) blockCollisions(pos cube.Pos) []cube.BBox32 { if s.World == nil { return nil } @@ -277,17 +277,17 @@ func (s *Simulator) liquidAt(pos cube.Pos) (world.Liquid, bool) { return liquid, ok } -func liquidHeight(liquid world.Liquid) float64 { +func liquidHeight(liquid world.Liquid) float32 { if liquid.LiquidFalling() { return 1 } - return float64(liquid.LiquidDepth()+1) / 9 + return float32(liquid.LiquidDepth()+1) / 9 } -func (s *Simulator) containsAnyLiquid(box cube.BBox) bool { +func (s *Simulator) containsAnyLiquid(box cube.BBox32) bool { min, max := box.Min(), box.Max() - minX, minY, minZ := int(math.Floor(min.X())), int(math.Floor(min.Y())), int(math.Floor(min.Z())) - maxX, maxY, maxZ := int(math.Ceil(max.X())), int(math.Ceil(max.Y())), int(math.Ceil(max.Z())) + minX, minY, minZ := int(math32.Floor(min.X())), int(math32.Floor(min.Y())), int(math32.Floor(min.Z())) + maxX, maxY, maxZ := int(math32.Ceil(max.X())), int(math32.Ceil(max.Y())), int(math32.Ceil(max.Z())) for x := minX; x < maxX; x++ { for z := minZ; z < maxZ; z++ { for y := minY; y < maxY; y++ { @@ -301,7 +301,7 @@ func (s *Simulator) containsAnyLiquid(box cube.BBox) bool { } func (s *Simulator) applyLiquidFlow(state *MovementState, positions []cube.Pos, kind liquidKind) { - flow := mgl64.Vec3{} + flow := mgl32.Vec3{} for _, pos := range positions { liquid, ok := s.liquidAt(pos) if !ok || !kind.matches(liquid) { @@ -310,7 +310,7 @@ func (s *Simulator) applyLiquidFlow(state *MovementState, positions []cube.Pos, flow = flow.Add(s.liquidFlow(pos, liquid)) } if length := flow.Len(); length >= 1e-4 { - strength := 0.014 + strength := float32(0.014) if kind == liquidLava { strength = 0.0035 } @@ -319,15 +319,15 @@ func (s *Simulator) applyLiquidFlow(state *MovementState, positions []cube.Pos, } } -func (s *Simulator) liquidFlow(pos cube.Pos, liquid world.Liquid) mgl64.Vec3 { +func (s *Simulator) liquidFlow(pos cube.Pos, liquid world.Liquid) mgl32.Vec3 { currentDecay := liquidDecay(liquid) - flow := mgl64.Vec3{} + flow := mgl32.Vec3{} for _, face := range liquidFaces { neighbourPos := pos.Add(face.delta) if neighbour, ok := s.liquidAt(neighbourPos); ok { if neighbour.LiquidType() == liquid.LiquidType() { if !s.liquidFlowSideClosed(pos, neighbourPos) && !s.liquidFlowSideClosed(neighbourPos, pos) { - flow = flow.Add(face.vec.Mul(float64(liquidDecay(neighbour) - currentDecay))) + flow = flow.Add(face.vec.Mul(float32(liquidDecay(neighbour) - currentDecay))) } continue } @@ -337,7 +337,7 @@ func (s *Simulator) liquidFlow(pos cube.Pos, liquid world.Liquid) mgl64.Vec3 { } below := neighbourPos.Side(cube.FaceDown) if lower, ok := s.liquidAt(below); ok && lower.LiquidType() == liquid.LiquidType() { - flow = flow.Add(face.vec.Mul(float64(liquidDecay(lower) - currentDecay + 8))) + flow = flow.Add(face.vec.Mul(float32(liquidDecay(lower) - currentDecay + 8))) } } if liquid.LiquidFalling() { @@ -356,7 +356,7 @@ func (s *Simulator) liquidFlow(pos cube.Pos, liquid world.Liquid) mgl64.Vec3 { if length := flow.Len(); length > 1e-4 { return flow.Mul(1 / length) } - return mgl64.Vec3{} + return mgl32.Vec3{} } func (s *Simulator) liquidFlowSideClosed(pos, side cube.Pos) bool { diff --git a/liquid_hardening_test.go b/liquid_hardening_test.go index 008290c..caf3699 100644 --- a/liquid_hardening_test.go +++ b/liquid_hardening_test.go @@ -1,13 +1,13 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" "testing" "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" + "github.com/go-gl/mathgl/mgl32" ) func dryState() *MovementState { @@ -184,7 +184,7 @@ func TestRealWaterContactDoesNotNeedSwimmingFlag(t *testing.T) { state.Swimming = false sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) } // The security window's default is pinned so a regression cannot silently @@ -205,7 +205,7 @@ func TestSwimWaterGraceResetOnTeleport(t *testing.T) { state := dryState() state.Swimming = true state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks - state.TeleportPos = mgl64.Vec3{50, 50, 50} + state.TeleportPos = mgl32.Vec3{50, 50, 50} state.TeleportCompletionTicks = 3 state.TicksSinceTeleport = 0 @@ -245,7 +245,7 @@ func TestLavaWinsOverStaleWaterGrace(t *testing.T) { sim.SimulateState(state) // Lava gravity, not water travel's zero gravity for a swimmer. - assertVec(t, state.Vel, mgl64.Vec3{0, -0.02, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.02, 0}) } // The swim-speed multiplier branch scales acceleration by @@ -253,7 +253,7 @@ func TestLavaWinsOverStaleWaterGrace(t *testing.T) { // against none pins that expression, which the golden cannot reach because it // runs with a multiplier of 1. func TestSwimSpeedMultiplierDepthStriderScaling(t *testing.T) { - run := func(level int) float64 { + run := func(level int) float32 { sim := newLiquidSim(filledColumn(waterSource)) sim.Inventory = depthStriderInventory{level: level} state := submergedState() @@ -261,7 +261,7 @@ func TestSwimSpeedMultiplierDepthStriderScaling(t *testing.T) { state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks state.SwimSpeedMultiplier = 2 state.OnGround = true - state.Impulse = mgl64.Vec2{0, 0.98} + state.Impulse = mgl32.Vec2{0, 0.98} sim.SimulateState(state) return state.Vel.Z() } @@ -272,7 +272,7 @@ func TestSwimSpeedMultiplierDepthStriderScaling(t *testing.T) { } // fraction 0 -> 0.7; fraction 1 -> 1.0. Drag is 0.8 in both cases because // the Depth Strider drag term is gated on multiplier <= 1. - if ratio := full / none; math.Abs(ratio-1/0.7) > 1e-9 { + if ratio := full / none; math32.Abs(ratio-1/0.7) > 1e-6 { t.Fatalf("full/none acceleration ratio = %.17g, want %.17g", ratio, 1/0.7) } } @@ -335,7 +335,7 @@ func TestExplicitLiquidsProviderDetectsWaterlogged(t *testing.T) { t.Fatal("expected waterlogged blocks from the explicit provider") } sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) } // With RequireLiquidLayer set, a simulator that cannot see layer 1 refuses to @@ -344,7 +344,7 @@ func TestRequireLiquidLayerFailsClosed(t *testing.T) { sim := newLiquidSim(newLiquidWorld()) sim.Options.RequireLiquidLayer = true state := submergedState() - state.Vel = mgl64.Vec3{0.5, 0.5, 0.5} + state.Vel = mgl32.Vec3{0.5, 0.5, 0.5} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeUnreliable { @@ -383,12 +383,12 @@ func TestUpstreamImpulseClampingOptIn(t *testing.T) { name string upstream bool input InputState - want float64 + want float32 }{ - {"sneak default", false, InputState{SneakDown: true, MoveVector: mgl64.Vec2{0, 1}}, MaxSneakImpulse * 0.98}, - {"sneak upstream", true, InputState{SneakDown: true, MoveVector: mgl64.Vec2{0, 1}}, 0.98}, - {"consumable default", false, InputState{UsingConsumable: true, MoveVector: mgl64.Vec2{0, 1}}, MaxConsumingImpulse * 0.98}, - {"consumable upstream", true, InputState{UsingConsumable: true, MoveVector: mgl64.Vec2{0, 1}}, 0.98}, + {"sneak default", false, InputState{SneakDown: true, MoveVector: mgl32.Vec2{0, 1}}, MaxSneakImpulse * 0.98}, + {"sneak upstream", true, InputState{SneakDown: true, MoveVector: mgl32.Vec2{0, 1}}, 0.98}, + {"consumable default", false, InputState{UsingConsumable: true, MoveVector: mgl32.Vec2{0, 1}}, MaxConsumingImpulse * 0.98}, + {"consumable upstream", true, InputState{UsingConsumable: true, MoveVector: mgl32.Vec2{0, 1}}, 0.98}, } for _, tc := range cases { @@ -411,7 +411,7 @@ func TestUpstreamImpulseClampingStillBoundsMoveVector(t *testing.T) { sim.Options.UpstreamImpulseClamping = true state := newBaseState() - sim.applyInput(state, InputState{MoveVector: mgl64.Vec2{5, -5}}) + sim.applyInput(state, InputState{MoveVector: mgl32.Vec2{5, -5}}) if !approxEqual(state.Impulse.X(), 0.98) || !approxEqual(state.Impulse.Y(), -0.98) { t.Fatalf("impulse = %v, want the move vector clamped to [-1, 1] then scaled", state.Impulse) } @@ -423,14 +423,14 @@ func TestFlyingIsUnreliableBeforePhysics(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.Flying = true - state.Vel = mgl64.Vec3{0.25, 0.25, 0.25} - state.Client.Vel = mgl64.Vec3{1, 2, 3} + state.Vel = mgl32.Vec3{0.25, 0.25, 0.25} + state.Client.Vel = mgl32.Vec3{1, 2, 3} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeUnreliable { t.Fatalf("outcome = %v, want unreliable", result.Outcome) } - assertVec(t, state.Vel, mgl64.Vec3{1, 2, 3}) + assertVec(t, state.Vel, mgl32.Vec3{1, 2, 3}) } // The liquid gate itself also excludes flying, independently of the reliability @@ -466,7 +466,7 @@ func TestFlowDropWeightIsEight(t *testing.T) { // +X: open with liquid below -> (0 - 0 + 8) = +8 // -X: same-type neighbour -> (1 - 0) = -1 // +Z: same-type neighbour -> (4 - 0) = +4 - want := mgl64.Vec3{7, 0, 4}.Normalize() + want := mgl32.Vec3{7, 0, 4}.Normalize() assertVec(t, flow, want) } @@ -481,14 +481,14 @@ func TestFallingFlowDownwardWeightIsSix(t *testing.T) { flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}) // Horizontal flow normalizes to (-1, 0, 0), then Y -= 6, then normalizes. - want := mgl64.Vec3{-1, -6, 0}.Normalize() + want := mgl32.Vec3{-1, -6, 0}.Normalize() assertVec(t, flow, want) } // A waterlogged stairs block whose solid face points at the neighbour blocks // flow through that face. func TestStairsSolidFaceBlocksFlow(t *testing.T) { - build := func(facing cube.Direction) mgl64.Vec3 { + build := func(facing cube.Direction) mgl32.Vec3 { w := newLayeredLiquidWorld() w.waterlog(cube.Pos{0, 0, 0}, block.Stairs{Facing: facing}, block.Water{Depth: 8}) w.set(cube.Pos{1, 0, 0}, block.Water{Depth: 4}) @@ -540,9 +540,9 @@ func TestSwimHitboxChangesCeilingCollision(t *testing.T) { state := submergedState() // Starts clear of the ceiling in both poses; only the standing hitbox // reaches it after the upward move. - state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Pos = mgl32.Vec3{0.5, 0, 0.5} state.Client.Pos = state.Pos - state.Vel = mgl64.Vec3{0, 0.5, 0} + state.Vel = mgl32.Vec3{0, 0.5, 0} return newLiquidSim(w), state } @@ -568,7 +568,7 @@ func TestSwimHitboxChangesCeilingCollision(t *testing.T) { // map-iteration nondeterminism only probabilistically, so this repeats the same // scenario and compares runs against each other. func TestLiquidSimulationIsRepeatable(t *testing.T) { - run := func() (mgl64.Vec3, mgl64.Vec3) { + run := func() (mgl32.Vec3, mgl32.Vec3) { w := newLiquidWorld(). fill(cube.Pos{-8, 0, -8}, cube.Pos{8, 8, 8}, block.Water{Depth: 8}). set(cube.Pos{1, 0, 0}, block.Water{Depth: 6}). @@ -583,7 +583,7 @@ func TestLiquidSimulationIsRepeatable(t *testing.T) { state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks input := InputState{ Jumping: true, - MoveVector: mgl64.Vec2{0.5, 1}, + MoveVector: mgl32.Vec2{0.5, 1}, Pitch: 25, Yaw: 40, HeadYaw: 40, @@ -629,7 +629,7 @@ func TestLiquidGoldenScenario(t *testing.T) { // is gated on multiplier <= 1, is actually reached. input := InputState{ Jumping: true, - MoveVector: mgl64.Vec2{0.5, 1}, + MoveVector: mgl32.Vec2{0.5, 1}, Pitch: 25, Yaw: 40, HeadYaw: 40, @@ -638,15 +638,15 @@ func TestLiquidGoldenScenario(t *testing.T) { sim.Simulate(state, input) } - wantPos := mgl64.Vec3{-0.012654883672021777, 2.7281474976710665, 3.5954677602500538} - wantVel := mgl64.Vec3{-0.02702143903177032, 0.15437050046578696, 0.1142856536788795} + wantPos := mgl32.Vec3{-0.012654960155487061, 2.922518253326416, 3.5954680442810059} + wantVel := mgl32.Vec3{-0.02702143903177032, 0.15549643337726593, 0.1142856627702713} - const tolerance = 1e-12 + const tolerance = 1e-6 for axis, name := range []string{"X", "Y", "Z"} { - if math.Abs(state.Pos[axis]-wantPos[axis]) > tolerance { + if math32.Abs(state.Pos[axis]-wantPos[axis]) > tolerance { t.Errorf("Pos.%s = %.17g, want %.17g", name, state.Pos[axis], wantPos[axis]) } - if math.Abs(state.Vel[axis]-wantVel[axis]) > tolerance { + if math32.Abs(state.Vel[axis]-wantVel[axis]) > tolerance { t.Errorf("Vel.%s = %.17g, want %.17g", name, state.Vel[axis], wantVel[axis]) } } diff --git a/liquid_test.go b/liquid_test.go index 07cf173..fc86bf5 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -1,13 +1,13 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" "testing" "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" + "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -54,7 +54,7 @@ func (w *liquidWorld) Block(pos cube.Pos) world.Block { return block.Air{} } -func (w *liquidWorld) BlockCollisions(pos cube.Pos) []cube.BBox { +func (w *liquidWorld) BlockCollisions(pos cube.Pos) []cube.BBox32 { b := w.Block(pos) if _, air := b.(block.Air); air { return nil @@ -62,16 +62,18 @@ func (w *liquidWorld) BlockCollisions(pos cube.Pos) []cube.BBox { if _, liquid := b.(world.Liquid); liquid { return nil } - return []cube.BBox{cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())} + return []cube.BBox32{cube.Box32(0, 0, 0, 1, 1, 1)} } -func (w *liquidWorld) GetNearbyBBoxes(aabb cube.BBox) []cube.BBox { +func (w *liquidWorld) GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 { min, max := aabb.Min(), aabb.Max() - var out []cube.BBox - for x := int(math.Floor(min.X())); x <= int(math.Floor(max.X())); x++ { - for y := int(math.Floor(min.Y())); y <= int(math.Floor(max.Y())); y++ { - for z := int(math.Floor(min.Z())); z <= int(math.Floor(max.Z())); z++ { - for _, bb := range w.BlockCollisions(cube.Pos{x, y, z}) { + var out []cube.BBox32 + for x := int(math32.Floor(min.X())); x <= int(math32.Floor(max.X())); x++ { + for y := int(math32.Floor(min.Y())); y <= int(math32.Floor(max.Y())); y++ { + for z := int(math32.Floor(min.Z())); z <= int(math32.Floor(max.Z())); z++ { + pos := cube.Pos{x, y, z} + for _, bb := range w.BlockCollisions(pos) { + bb = bb.Translate(posVec3(pos)) if bb.IntersectsWith(aabb) { out = append(out, bb) } @@ -141,7 +143,7 @@ func newLiquidSim(w WorldProvider) *Simulator { // submergedState returns a state standing inside a liquid column at 0.5/0.5/0.5. func submergedState() *MovementState { state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 0.5, 0.5} + state.Pos = mgl32.Vec3{0.5, 0.5, 0.5} state.Client.Pos = state.Pos return state } @@ -153,11 +155,11 @@ func filledColumn(b world.Block) *liquidWorld { return newLiquidWorld().fill(cube.Pos{-2, 0, -2}, cube.Pos{2, 3, 2}, b) } -func approxEqual(a, b float64) bool { - return math.Abs(a-b) < 1e-9 +func approxEqual(a, b float32) bool { + return math32.Abs(a-b) < 1e-6 } -func assertVec(t *testing.T, got, want mgl64.Vec3) { +func assertVec(t *testing.T, got, want mgl32.Vec3) { t.Helper() if !approxEqual(got.X(), want.X()) || !approxEqual(got.Y(), want.Y()) || !approxEqual(got.Z(), want.Z()) { t.Fatalf("velocity = %v, want %v", got, want) @@ -168,7 +170,7 @@ func assertVec(t *testing.T, got, want mgl64.Vec3) { // client's swim pose. This drives collision, liquid detection and exit probing. func TestSwimmingBoundingBoxUsesWidthAsHeight(t *testing.T) { state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 10, 0.5} + state.Pos = mgl32.Vec3{0.5, 10, 0.5} standing := state.BoundingBox(false) if height := standing.Height(); !approxEqual(height, 1.8) { @@ -191,7 +193,7 @@ func TestSwimmingBoundingBoxUsesWidthAsHeight(t *testing.T) { // open air and fit through gaps a standing player cannot. func TestSwimmingFlagAloneDoesNotShrinkHitbox(t *testing.T) { state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 10, 0.5} + state.Pos = mgl32.Vec3{0.5, 10, 0.5} state.Swimming = true state.SwimWaterGraceTicks = 0 @@ -211,10 +213,10 @@ func TestSwimmingFlagAloneDoesNotShrinkHitbox(t *testing.T) { func TestSpoofedSwimmingCannotFitThroughCeilingGap(t *testing.T) { sim := newLiquidSim(newLiquidWorld().set(cube.Pos{0, 2, 0}, block.Stone{})) state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Pos = mgl32.Vec3{0.5, 0, 0.5} state.Client.Pos = state.Pos state.Swimming = true - state.Vel = mgl64.Vec3{0, 0.5, 0} + state.Vel = mgl32.Vec3{0, 0.5, 0} sim.SimulateState(state) if !state.CollideY { @@ -224,7 +226,7 @@ func TestSpoofedSwimmingCannotFitThroughCeilingGap(t *testing.T) { func TestSwimmingClientBoundingBoxUsesWidthAsHeight(t *testing.T) { state := newBaseState() - state.Client.Pos = mgl64.Vec3{0.5, 10, 0.5} + state.Client.Pos = mgl32.Vec3{0.5, 10, 0.5} if height := state.ClientBoundingBox(false).Height(); !approxEqual(height, 1.8) { t.Fatalf("standing client height = %v, want 1.8", height) @@ -239,8 +241,8 @@ func TestSwimmingClientBoundingBoxUsesWidthAsHeight(t *testing.T) { // The swim hitbox must scale with the entity size, not use a hardcoded 0.6. func TestSwimmingBoundingBoxRespectsScale(t *testing.T) { state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 10, 0.5} - state.Size = mgl64.Vec3{0.6, 1.8, 2} + state.Pos = mgl32.Vec3{0.5, 10, 0.5} + state.Size = mgl32.Vec3{0.6, 1.8, 2} state.Swimming = true state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks @@ -310,7 +312,7 @@ func TestSwimAmountInterpolation(t *testing.T) { for i := 1; i <= 3; i++ { sim.applyInput(state, InputState{}) - if want := float64(i) * 0.1; !approxEqual(state.SwimAmount, want) { + if want := float32(i) * 0.1; !approxEqual(state.SwimAmount, want) { t.Fatalf("tick %d: SwimAmount = %v, want %v", i, state.SwimAmount, want) } } @@ -376,11 +378,11 @@ func TestWaterDragAndGravity(t *testing.T) { state := submergedState() sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) // Second tick: previous velocity is dragged by 0.8, then gravity applies. sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.005*0.8 - 0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.005*0.8 - 0.005, 0}) } // Sprinting in water raises horizontal drag from 0.8 to 0.9. @@ -388,11 +390,11 @@ func TestWaterSprintDrag(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) normal := submergedState() - normal.Vel = mgl64.Vec3{0.5, 0, 0} + normal.Vel = mgl32.Vec3{0.5, 0, 0} sim.SimulateState(normal) sprinting := submergedState() - sprinting.Vel = mgl64.Vec3{0.5, 0, 0} + sprinting.Vel = mgl32.Vec3{0.5, 0, 0} sprinting.Sprinting = true sim.SimulateState(sprinting) @@ -408,21 +410,21 @@ func TestWaterSprintDrag(t *testing.T) { func TestWaterVerticalDragIndependentOfSprint(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() - state.Vel = mgl64.Vec3{0, 0.5, 0} + state.Vel = mgl32.Vec3{0, 0.5, 0} state.Sprinting = true sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, 0.5*0.8 - 0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, 0.5*0.8 - 0.005, 0}) } // Lava uses a flat 0.5 drag on every axis and a heavier 0.02 gravity. func TestLavaDragAndGravity(t *testing.T) { sim := newLiquidSim(filledColumn(lavaSource)) state := submergedState() - state.Vel = mgl64.Vec3{0.4, 0.4, 0.4} + state.Vel = mgl32.Vec3{0.4, 0.4, 0.4} sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0.2, 0.4*0.5 - 0.02, 0.2}) + assertVec(t, state.Vel, mgl32.Vec3{0.2, 0.4*0.5 - 0.02, 0.2}) } // Swimming removes water gravity entirely. @@ -430,7 +432,7 @@ func TestSwimmingCancelsWaterGravity(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.Swimming = true - state.Rotation = mgl64.Vec3{0, 0, 0} + state.Rotation = mgl32.Vec3{0, 0, 0} sim.SimulateState(state) if !approxEqual(state.Vel.Y(), 0) { @@ -445,7 +447,7 @@ func TestNoGravityInLiquid(t *testing.T) { state.HasGravity = false sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{}) + assertVec(t, state.Vel, mgl32.Vec3{}) } // Levitation replaces liquid gravity with a pull toward the levitation target. @@ -456,7 +458,7 @@ func TestLevitationOverridesLiquidGravity(t *testing.T) { sim.SimulateState(state) // target = 0.05 * (0+1); vel += (target - vel) * 0.2 - assertVec(t, state.Vel, mgl64.Vec3{0, 0.05 * 0.2, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, 0.05 * 0.2, 0}) } func TestLevitationAmplifierScales(t *testing.T) { @@ -465,7 +467,7 @@ func TestLevitationAmplifierScales(t *testing.T) { state := submergedState() sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, (LevitationGravityMultiplier * 4) * 0.2, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, (LevitationGravityMultiplier * 4) * 0.2, 0}) } // A nil effects provider must not panic and must fall back to gravity. @@ -475,7 +477,7 @@ func TestNilEffectsProviderFallsBackToGravity(t *testing.T) { state := submergedState() sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) } // Falling into liquid clears accumulated fall distance. @@ -497,7 +499,7 @@ func TestEffectiveJumpingAscendsInWater(t *testing.T) { state.EffectiveJumping = true sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, 0.04*0.8 - 0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, 0.04*0.8 - 0.005, 0}) } // Mid-transition into the swim pose zeroes the ascent instead of applying it. @@ -508,7 +510,7 @@ func TestSwimTransitionZeroesJumpAscent(t *testing.T) { state.SwimAmount = 0.5 sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) } // A fully-transitioned swimmer still ascends normally. @@ -536,7 +538,7 @@ func TestWantDownSinksInWater(t *testing.T) { apply(state) sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.04*0.8 - 0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.04*0.8 - 0.005, 0}) }) } } @@ -548,7 +550,7 @@ func TestWantDownIgnoredInLava(t *testing.T) { state.WantDown = true sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.02, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.02, 0}) } // The descend inputs must not alter the sneak impulse clamp. Upstream dropped @@ -558,10 +560,10 @@ func TestDescendInputsDoNotChangeSneakImpulseClamp(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) sneaking := newBaseState() - sim.applyInput(sneaking, InputState{SneakDown: true, MoveVector: mgl64.Vec2{0, 1}}) + sim.applyInput(sneaking, InputState{SneakDown: true, MoveVector: mgl32.Vec2{0, 1}}) descending := newBaseState() - sim.applyInput(descending, InputState{SneakDown: true, WantDown: true, MoveVector: mgl64.Vec2{0, 1}}) + sim.applyInput(descending, InputState{SneakDown: true, WantDown: true, MoveVector: mgl32.Vec2{0, 1}}) if !approxEqual(descending.Impulse.Y(), sneaking.Impulse.Y()) { t.Fatalf("descending impulse %v must match sneaking impulse %v", @@ -577,11 +579,11 @@ func TestSwimTravelFollowsPitch(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.Swimming = true - state.Rotation = mgl64.Vec3{-90, 0, 0} // looking straight up + state.Rotation = mgl32.Vec3{-90, 0, 0} // looking straight up sim.SimulateState(state) // targetY = -sin(-90deg) = 1; vel += (1 - 0) * 0.06, then drag 0.8. - assertVec(t, state.Vel, mgl64.Vec3{0, 0.06 * 0.8, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, 0.06 * 0.8, 0}) } // A steep downward pitch uses the faster 0.085 interpolation rate. @@ -589,11 +591,11 @@ func TestSwimTravelUsesFasterRateWhenDivingSteeply(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.Swimming = true - state.Rotation = mgl64.Vec3{90, 0, 0} // looking straight down + state.Rotation = mgl32.Vec3{90, 0, 0} // looking straight down sim.SimulateState(state) // targetY = -sin(90deg) = -1, below -0.2 so rate is 0.085. - assertVec(t, state.Vel, mgl64.Vec3{0, -0.085 * 0.8, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.085 * 0.8, 0}) } // Swim travel is suppressed while jumping, letting the jump impulse win. @@ -603,11 +605,11 @@ func TestSwimTravelSkippedWhileJumping(t *testing.T) { state.Swimming = true state.EffectiveJumping = true state.SwimAmount = 1 - state.Rotation = mgl64.Vec3{90, 0, 0} + state.Rotation = mgl32.Vec3{90, 0, 0} sim.SimulateState(state) // Pitch steering skipped, so only the 0.04 jump impulse applies. - assertVec(t, state.Vel, mgl64.Vec3{0, 0.04 * 0.8, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, 0.04 * 0.8, 0}) } // Swimming upward at the surface stops the climb once the head clears the @@ -618,10 +620,10 @@ func TestSwimTravelStopsAtSurface(t *testing.T) { sim := newLiquidSim(w) state := submergedState() // Both head probes (+0.52 and +0.42) clear the liquid surface at y=1. - state.Pos = mgl64.Vec3{0.5, 1.5, 0.5} + state.Pos = mgl32.Vec3{0.5, 1.5, 0.5} state.Swimming = true - state.Rotation = mgl64.Vec3{-90, 0, 0} - state.Vel = mgl64.Vec3{0, 0.5, 0} + state.Rotation = mgl32.Vec3{-90, 0, 0} + state.Vel = mgl32.Vec3{0, 0.5, 0} // The hitbox has just left the water, so water travel is still in its // grace window. state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks @@ -639,8 +641,8 @@ func TestSwimTravelContinuesWhileHeadSubmerged(t *testing.T) { sim := newLiquidSim(w) state := submergedState() state.Swimming = true - state.Rotation = mgl64.Vec3{-90, 0, 0} - state.Vel = mgl64.Vec3{0, 0.5, 0} + state.Rotation = mgl32.Vec3{-90, 0, 0} + state.Vel = mgl32.Vec3{0, 0.5, 0} sim.SimulateState(state) if approxEqual(state.Vel.Y(), 0) { @@ -653,11 +655,11 @@ func TestSwimTravelSurfaceClampSkippedWhenWantDownSlow(t *testing.T) { w := newLiquidWorld().fill(cube.Pos{-2, -4, -2}, cube.Pos{2, 0, 2}, waterSource) sim := newLiquidSim(w) state := submergedState() - state.Pos = mgl64.Vec3{0.5, 1.5, 0.5} + state.Pos = mgl32.Vec3{0.5, 1.5, 0.5} state.Swimming = true - state.Rotation = mgl64.Vec3{-90, 0, 0} + state.Rotation = mgl32.Vec3{-90, 0, 0} state.WantDownSlow = true - state.Vel = mgl64.Vec3{0, 0.5, 0} + state.Vel = mgl32.Vec3{0, 0.5, 0} state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks sim.SimulateState(state) @@ -671,13 +673,13 @@ func TestSwimTravelSurfaceClampSkippedWhenWantDownSlow(t *testing.T) { func TestDepthStriderLowersDragCoefficient(t *testing.T) { base := newLiquidSim(filledColumn(waterSource)) baseState := submergedState() - baseState.Vel = mgl64.Vec3{0.5, 0, 0} + baseState.Vel = mgl32.Vec3{0.5, 0, 0} base.SimulateState(baseState) strider := newLiquidSim(filledColumn(waterSource)) strider.Inventory = depthStriderInventory{level: 3} striderState := submergedState() - striderState.Vel = mgl64.Vec3{0.5, 0, 0} + striderState.Vel = mgl32.Vec3{0.5, 0, 0} striderState.OnGround = true strider.SimulateState(striderState) @@ -699,17 +701,17 @@ func TestDepthStriderLowersDragCoefficient(t *testing.T) { func TestDepthStriderIncreasesAcceleration(t *testing.T) { base := newLiquidSim(filledColumn(waterSource)) baseState := submergedState() - baseState.Impulse = mgl64.Vec2{0, 0.98} + baseState.Impulse = mgl32.Vec2{0, 0.98} base.SimulateState(baseState) strider := newLiquidSim(filledColumn(waterSource)) strider.Inventory = depthStriderInventory{level: 3} striderState := submergedState() - striderState.Impulse = mgl64.Vec2{0, 0.98} + striderState.Impulse = mgl32.Vec2{0, 0.98} striderState.OnGround = true strider.SimulateState(striderState) - if !(math.Abs(striderState.Vel.Z()) > math.Abs(baseState.Vel.Z())) { + if !(math32.Abs(striderState.Vel.Z()) > math32.Abs(baseState.Vel.Z())) { t.Fatalf("depth strider Z = %v must exceed base Z = %v", striderState.Vel.Z(), baseState.Vel.Z()) } @@ -720,12 +722,12 @@ func TestDepthStriderHalvedWhenAirborne(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) sim.Inventory = depthStriderInventory{level: 3} state := submergedState() - state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Vel = mgl32.Vec3{0.5, 0, 0} state.OnGround = false sim.SimulateState(state) // level 1.5 -> fraction 0.5 -> drag = 0.8 + (0.54600006 - 0.8) * 0.5. - want := 0.5 * (0.8 + (0.54600006-0.8)*0.5) + want := float32(0.5 * (0.8 + (0.54600006-0.8)*0.5)) if !approxEqual(state.Vel.X(), want) { t.Fatalf("airborne depth strider X = %v, want %v", state.Vel.X(), want) } @@ -736,7 +738,7 @@ func TestDepthStriderClampedToMaxLevel(t *testing.T) { clamped := newLiquidSim(filledColumn(waterSource)) clamped.Inventory = depthStriderInventory{level: 99} clampedState := submergedState() - clampedState.Vel = mgl64.Vec3{0.5, 0, 0} + clampedState.Vel = mgl32.Vec3{0.5, 0, 0} clampedState.OnGround = true clamped.SimulateState(clampedState) @@ -750,7 +752,7 @@ func TestDepthStriderNegativeLevelIgnored(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) sim.Inventory = depthStriderInventory{level: -5} state := submergedState() - state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Vel = mgl32.Vec3{0.5, 0, 0} sim.SimulateState(state) if !approxEqual(state.Vel.X(), 0.5*0.8) { @@ -763,7 +765,7 @@ func TestDepthStriderIgnoredInLava(t *testing.T) { sim := newLiquidSim(filledColumn(lavaSource)) sim.Inventory = depthStriderInventory{level: 3} state := submergedState() - state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Vel = mgl32.Vec3{0.5, 0, 0} sim.SimulateState(state) if !approxEqual(state.Vel.X(), 0.5*0.5) { @@ -776,7 +778,7 @@ func TestInventoryWithoutDepthStriderProvider(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) sim.Inventory = mockInventory{} state := submergedState() - state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Vel = mgl32.Vec3{0.5, 0, 0} sim.SimulateState(state) if !approxEqual(state.Vel.X(), 0.5*0.8) { @@ -791,24 +793,24 @@ func TestSwimSpeedMultiplierRequiresSwimming(t *testing.T) { boostedState := submergedState() boostedState.Swimming = true boostedState.SwimSpeedMultiplier = 2 - boostedState.Impulse = mgl64.Vec2{0, 0.98} + boostedState.Impulse = mgl32.Vec2{0, 0.98} boosted.SimulateState(boostedState) plain := newLiquidSim(filledColumn(waterSource)) plainState := submergedState() plainState.Swimming = true plainState.SwimSpeedMultiplier = 1 - plainState.Impulse = mgl64.Vec2{0, 0.98} + plainState.Impulse = mgl32.Vec2{0, 0.98} plain.SimulateState(plainState) - if !(math.Abs(boostedState.Vel.Z()) > math.Abs(plainState.Vel.Z())) { + if !(math32.Abs(boostedState.Vel.Z()) > math32.Abs(plainState.Vel.Z())) { t.Fatalf("boosted Z = %v must exceed plain Z = %v", boostedState.Vel.Z(), plainState.Vel.Z()) } notSwimming := newLiquidSim(filledColumn(waterSource)) notSwimmingState := submergedState() notSwimmingState.SwimSpeedMultiplier = 2 - notSwimmingState.Impulse = mgl64.Vec2{0, 0.98} + notSwimmingState.Impulse = mgl32.Vec2{0, 0.98} notSwimming.SimulateState(notSwimmingState) if !approxEqual(notSwimmingState.Vel.Z(), plainState.Vel.Z()) { @@ -848,14 +850,14 @@ func TestZeroSwimSpeedMultiplierTreatedAsDefault(t *testing.T) { state := submergedState() state.Swimming = true state.SwimSpeedMultiplier = 0 - state.Impulse = mgl64.Vec2{0, 0.98} + state.Impulse = mgl32.Vec2{0, 0.98} sim.SimulateState(state) explicit := newLiquidSim(filledColumn(waterSource)) explicitState := submergedState() explicitState.Swimming = true explicitState.SwimSpeedMultiplier = DefaultSwimSpeedMultiplier - explicitState.Impulse = mgl64.Vec2{0, 0.98} + explicitState.Impulse = mgl32.Vec2{0, 0.98} explicit.SimulateState(explicitState) assertVec(t, state.Vel, explicitState.Vel) @@ -866,13 +868,13 @@ func TestZeroMovementSpeedsUseDefaults(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.UnderwaterMovementSpeed = 0 - state.Impulse = mgl64.Vec2{0, 0.98} + state.Impulse = mgl32.Vec2{0, 0.98} sim.SimulateState(state) explicit := newLiquidSim(filledColumn(waterSource)) explicitState := submergedState() explicitState.UnderwaterMovementSpeed = DefaultUnderwaterMovementSpeed - explicitState.Impulse = mgl64.Vec2{0, 0.98} + explicitState.Impulse = mgl32.Vec2{0, 0.98} explicit.SimulateState(explicitState) assertVec(t, state.Vel, explicitState.Vel) @@ -896,7 +898,7 @@ func TestLavaUsesWiderHorizontalMargin(t *testing.T) { sim := newLiquidSim(w) state := submergedState() // Position the player so the box only just reaches into x=1. - state.Pos = mgl64.Vec3{0.75, 0.5, 0.5} + state.Pos = mgl32.Vec3{0.75, 0.5, 0.5} water := sim.touchingLiquidBlocks(state, liquidWater) lava := sim.touchingLiquidBlocks(state, liquidLava) @@ -931,7 +933,7 @@ func TestWaterTakesPriorityOverLava(t *testing.T) { sim.SimulateState(state) // Water gravity (0.005), not lava gravity (0.02). - assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) } // Without a LiquidProvider, liquids are read from WorldProvider.Block. @@ -940,7 +942,7 @@ func TestLiquidFallsBackToBlockProvider(t *testing.T) { state := submergedState() sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) } // A LiquidProvider exposes waterlogged blocks whose main layer is a solid. @@ -956,7 +958,7 @@ func TestLiquidProviderDetectsWaterloggedBlocks(t *testing.T) { t.Fatal("expected waterlogged blocks to register as water") } sim.SimulateState(state) - assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) } // A world with no liquids at all must run normal (non-liquid) physics. @@ -983,13 +985,13 @@ func TestUnloadedChunkCancelsLiquidSimulation(t *testing.T) { w.chunkLoaded = false sim := newLiquidSim(w) state := submergedState() - state.Vel = mgl64.Vec3{0.5, 0.5, 0.5} + state.Vel = mgl32.Vec3{0.5, 0.5, 0.5} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeUnloadedChunk { t.Fatalf("outcome = %v, want unloaded chunk", result.Outcome) } - assertVec(t, state.Vel, mgl64.Vec3{}) + assertVec(t, state.Vel, mgl32.Vec3{}) } // Being inside a liquid is a reliable scenario; v0.1.3 bailed out here. @@ -1059,7 +1061,7 @@ func TestSwimmingPreservesWaterTravelOutsideWater(t *testing.T) { sim := newLiquidSim(newLiquidWorld()) state := submergedState() state.Swimming = true - state.Rotation = mgl64.Vec3{0, 0, 0} + state.Rotation = mgl32.Vec3{0, 0, 0} state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks // Seeded so that falling back to normal physics would be visible as a // gravity pull rather than an indistinguishable zero. @@ -1154,7 +1156,7 @@ func TestUniformLiquidHasNoFlow(t *testing.T) { state := submergedState() sim.applyLiquidFlow(state, sim.touchingLiquidBlocks(state, liquidWater), liquidWater) - assertVec(t, state.Vel, mgl64.Vec3{}) + assertVec(t, state.Vel, mgl32.Vec3{}) } // Falling liquid against a solid neighbour gains a strong downward component. @@ -1255,8 +1257,8 @@ func TestLiquidExitProbeBoostsOverLedge(t *testing.T) { set(cube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) state := submergedState() - state.Vel = mgl64.Vec3{0.5, 0, 0} - state.Impulse = mgl64.Vec2{0, 0.98} + state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Impulse = mgl32.Vec2{0, 0.98} sim.SimulateState(state) if !state.CollideX { @@ -1282,17 +1284,17 @@ func TestLiquidExitProbeBlockedByCollisionAlone(t *testing.T) { w.set(cube.Pos{0, 1, 0}, block.Stone{}) } state := submergedState() - state.Pos = mgl64.Vec3{0.5, 0.4, 0.5} + state.Pos = mgl32.Vec3{0.5, 0.4, 0.5} state.Client.Pos = state.Pos state.Swimming = true state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks - state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Vel = mgl32.Vec3{0.5, 0, 0} return newLiquidSim(w), state } for _, overhang := range []bool{false, true} { sim, state := build(overhang) - raised := state.BoundingBox(false).Translate(mgl64.Vec3{0, 0.6, 0}) + raised := state.BoundingBox(false).Translate(mgl32.Vec3{0, 0.6, 0}) if sim.containsAnyLiquid(raised) { t.Fatalf("overhang=%t: probe box must contain no liquid to isolate the collision term", overhang) } @@ -1323,7 +1325,7 @@ func TestLiquidExitProbeBlockedByLiquidAbove(t *testing.T) { set(cube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) state := submergedState() - state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Vel = mgl32.Vec3{0.5, 0, 0} sim.SimulateState(state) if approxEqual(state.Vel.Y(), 0.3) { @@ -1381,8 +1383,8 @@ func TestClimbUsesEffectiveJumping(t *testing.T) { // Shrinking a box past its own size collapses it to its midpoint instead of // inverting it. func TestShrinkLiquidBoxCollapsesToMidpoint(t *testing.T) { - box := cube.Box(0, 0, 0, 1, 0.2, 1) - shrunk := shrinkLiquidBox(box, mgl64.Vec3{0.001, 0.401, 0.001}) + box := cube.Box32(0, 0, 0, 1, 0.2, 1) + shrunk := shrinkLiquidBox(box, mgl32.Vec3{0.001, 0.401, 0.001}) if !approxEqual(shrunk.Min().Y(), 0.1) || !approxEqual(shrunk.Max().Y(), 0.1) { t.Fatalf("collapsed Y = [%v %v], want [0.1 0.1]", shrunk.Min().Y(), shrunk.Max().Y()) diff --git a/math.go b/math.go index 431c4ed..e8ca031 100644 --- a/math.go +++ b/math.go @@ -1,39 +1,48 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" - "github.com/go-gl/mathgl/mgl64" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl32" ) -var mcSinTable []float64 +var mcSinTable []float32 func init() { - mcSinTable = make([]float64, 65536) + mcSinTable = make([]float32, 65536) for i := range 65536 { - mcSinTable[i] = math.Sin(float64(i) * math.Pi * 2 / 65536) + mcSinTable[i] = math32.Sin(float32(i) * math32.Pi * 2 / 65536) } } // MCSin returns the Minecraft sin of the given angle. -func MCSin(val float64) float64 { +func MCSin(val float32) float32 { return mcSinTable[uint16(val*10430.378)&65535] } // MCCos returns the Minecraft cos of the given angle. -func MCCos(val float64) float64 { +func MCCos(val float32) float32 { return mcSinTable[uint16(val*10430.378+16384.0)&65535] } // ClampFloat clamps the given value to the given range. -func ClampFloat(num, min, max float64) float64 { +func ClampFloat(num, min, max float32) float32 { if num < min { return min } - return math.Min(num, max) + return math32.Min(num, max) } // Vec3HzDistSqr returns the squared horizontal distance in a vector. -func Vec3HzDistSqr(vec3 mgl64.Vec3) float64 { +func Vec3HzDistSqr(vec3 mgl32.Vec3) float32 { return vec3.X()*vec3.X() + vec3.Z()*vec3.Z() } + +func posFromVec3(vec mgl32.Vec3) cube.Pos { + return cube.Pos{int(math32.Floor(vec.X())), int(math32.Floor(vec.Y())), int(math32.Floor(vec.Z()))} +} + +func posVec3(pos cube.Pos) mgl32.Vec3 { + return mgl32.Vec3{float32(pos.X()), float32(pos.Y()), float32(pos.Z())} +} diff --git a/movement.go b/movement.go index 0f48fe8..483c348 100644 --- a/movement.go +++ b/movement.go @@ -2,14 +2,14 @@ package bedsim import ( "github.com/df-mc/dragonfly/server/block/cube" - "github.com/go-gl/mathgl/mgl64" + "github.com/go-gl/mathgl/mgl32" ) // ClientState holds non-authoritative movement data sent by the client. type ClientState struct { - Pos, LastPos mgl64.Vec3 - Vel, LastVel mgl64.Vec3 - Mov, LastMov mgl64.Vec3 + Pos, LastPos mgl32.Vec3 + Vel, LastVel mgl32.Vec3 + Mov, LastMov mgl32.Vec3 HorizontalCollision bool VerticalCollision bool @@ -20,40 +20,40 @@ type ClientState struct { type MovementState struct { Client ClientState - Pos, LastPos mgl64.Vec3 - Vel, LastVel mgl64.Vec3 - Mov, LastMov mgl64.Vec3 + Pos, LastPos mgl32.Vec3 + Vel, LastVel mgl32.Vec3 + Mov, LastMov mgl32.Vec3 - Rotation, LastRotation mgl64.Vec3 + Rotation, LastRotation mgl32.Vec3 - SlideOffset mgl64.Vec2 - Impulse mgl64.Vec2 - Size mgl64.Vec3 + SlideOffset mgl32.Vec2 + Impulse mgl32.Vec2 + Size mgl32.Vec3 SupportingBlockPos *cube.Pos - Gravity float64 - JumpHeight float64 - FallDistance float64 + Gravity float32 + JumpHeight float32 + FallDistance float32 - MovementSpeed float64 - DefaultMovementSpeed float64 - AirSpeed float64 - UnderwaterMovementSpeed float64 - LavaMovementSpeed float64 + MovementSpeed float32 + DefaultMovementSpeed float32 + AirSpeed float32 + UnderwaterMovementSpeed float32 + LavaMovementSpeed float32 // SwimSpeedMultiplier scales swimming acceleration; zero means the default. - SwimSpeedMultiplier float64 + SwimSpeedMultiplier float32 // DolphinBoostTicks is the remaining dolphin-boost duration. DolphinBoostTicks int64 ServerUpdatedSpeed bool - Knockback mgl64.Vec3 + Knockback mgl32.Vec3 TicksSinceKnockback uint64 - PendingTeleportPos mgl64.Vec3 + PendingTeleportPos mgl32.Vec3 PendingTeleports int - TeleportPos mgl64.Vec3 + TeleportPos mgl32.Vec3 TicksSinceTeleport uint64 TeleportCompletionTicks uint64 TeleportIsSmoothed bool @@ -68,7 +68,7 @@ type MovementState struct { JumpDelay uint64 Swimming bool - SwimAmount float64 + SwimAmount float32 // SwimWaterGraceTicks retains recent server-observed water contact. SwimWaterGraceTicks int64 AutoJumpingInWater bool @@ -102,22 +102,22 @@ type MovementState struct { GameMode int32 } -func (s *MovementState) SetPos(newPos mgl64.Vec3) { +func (s *MovementState) SetPos(newPos mgl32.Vec3) { s.LastPos = s.Pos s.Pos = newPos } -func (s *MovementState) SetVel(newVel mgl64.Vec3) { +func (s *MovementState) SetVel(newVel mgl32.Vec3) { s.LastVel = s.Vel s.Vel = newVel } -func (s *MovementState) SetMov(newMov mgl64.Vec3) { +func (s *MovementState) SetMov(newMov mgl32.Vec3) { s.LastMov = s.Mov s.Mov = newMov } -func (s *MovementState) SetRotation(newRot mgl64.Vec3) { +func (s *MovementState) SetRotation(newRot mgl32.Vec3) { s.LastRotation = s.Rotation s.Rotation = newRot } diff --git a/result.go b/result.go index cb0bb4b..029881a 100644 --- a/result.go +++ b/result.go @@ -1,6 +1,6 @@ package bedsim -import "github.com/go-gl/mathgl/mgl64" +import "github.com/go-gl/mathgl/mgl32" // SimulationOutcome describes which path the simulator took for the current tick. type SimulationOutcome uint8 @@ -15,17 +15,17 @@ const ( // SimulationResult captures the outcome of a single simulation tick. type SimulationResult struct { - Position mgl64.Vec3 - Velocity mgl64.Vec3 - Movement mgl64.Vec3 + Position mgl32.Vec3 + Velocity mgl32.Vec3 + Movement mgl32.Vec3 OnGround bool CollideX bool CollideY bool CollideZ bool - PositionDelta mgl64.Vec3 - VelocityDelta mgl64.Vec3 + PositionDelta mgl32.Vec3 + VelocityDelta mgl32.Vec3 NeedsCorrection bool Outcome SimulationOutcome diff --git a/simulation.go b/simulation.go index ad1712e..c076d6e 100644 --- a/simulation.go +++ b/simulation.go @@ -1,13 +1,13 @@ package bedsim import ( + "github.com/chewxy/math32" "iter" - "math" "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" + "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -67,13 +67,13 @@ func (s *Simulator) simulateCore(state *MovementState) SimulationOutcome { s.resetToClient(state) return SimulationOutcomeUnreliable } - if s.World != nil && !s.World.IsChunkLoaded(int32(math.Floor(state.Pos.X()))>>4, int32(math.Floor(state.Pos.Z()))>>4) { - state.SetVel(mgl64.Vec3{}) + if s.World != nil && !s.World.IsChunkLoaded(int32(math32.Floor(state.Pos.X()))>>4, int32(math32.Floor(state.Pos.Z()))>>4) { + state.SetVel(mgl32.Vec3{}) state.SwimWaterGraceTicks = 0 return SimulationOutcomeUnloadedChunk } if state.Immobile || !state.Ready { - state.SetVel(mgl64.Vec3{}) + state.SetVel(mgl32.Vec3{}) // Frozen ticks observe nothing, so the budget must not simply pause // and resume later. state.SwimWaterGraceTicks = 0 @@ -138,7 +138,7 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { state.Client.ToggledFly = false } - state.SetRotation(mgl64.Vec3{input.Pitch, input.HeadYaw, input.Yaw}) + state.SetRotation(mgl32.Vec3{input.Pitch, input.HeadYaw, input.Yaw}) state.PressingSneak = input.Sneaking state.PressingSprint = input.SprintDown @@ -202,7 +202,7 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { state.WantDownSlow = input.WantDownSlow // Preserve bedsim's public impulse clamps unless upstream behavior is opted in. - maxImpulse := 1.0 + maxImpulse := float32(1) if !s.Options.UpstreamImpulseClamping { if input.UsingConsumable { maxImpulse *= MaxConsumingImpulse @@ -211,7 +211,7 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { maxImpulse *= MaxSneakImpulse } } - moveVector := mgl64.Vec2{ + moveVector := mgl32.Vec2{ ClampFloat(input.MoveVector[0], -maxImpulse, maxImpulse), ClampFloat(input.MoveVector[1], -maxImpulse, maxImpulse), } @@ -223,7 +223,7 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { state.JumpHeight = DefaultJumpHeight if s.Effects != nil { if amp, ok := s.Effects.GetEffect(packet.EffectJumpBoost); ok { - state.JumpHeight += float64(amp) * 0.1 + state.JumpHeight += float32(amp) * 0.1 } } @@ -280,7 +280,7 @@ func (s *Simulator) tickState(state *MovementState) { } } state.TicksSinceKnockback++ - if state.TicksSinceTeleport < math.MaxUint64 { + if state.TicksSinceTeleport < math32.MaxUint64 { state.TicksSinceTeleport++ } if state.JumpDelay > 0 { @@ -291,7 +291,7 @@ func (s *Simulator) tickState(state *MovementState) { func (s *Simulator) simulateMovement(state *MovementState) { if state.Vel.LenSqr() < 1e-12 { - state.SetVel(mgl64.Vec3{}) + state.SetVel(mgl32.Vec3{}) } // Bound retained water evidence before collision and travel inspect it. @@ -332,7 +332,7 @@ func (s *Simulator) simulateMovement(state *MovementState) { return } - blockUnder := s.blockAtPos(cube.PosFromVec3(state.Pos.Sub(mgl64.Vec3{0, 0.5}))) + blockUnder := s.blockAtPos(posFromVec3(state.Pos.Sub(mgl32.Vec3{0, 0.5}))) blockFriction := DefaultAirFriction moveRelativeSpeed := state.AirSpeed if state.OnGround { @@ -369,7 +369,7 @@ func (s *Simulator) simulateMovement(state *MovementState) { s.debugf("moveRelative force applied (vel=%v)", state.Vel) s.debugfIf(s.attemptJump(state, &clientJumpPrevented), "jump force applied (sprint=%v): %v", state.Sprinting, state.Vel) - nearClimbable := s.blockClimbable(s.blockAtPos(cube.PosFromVec3(state.Pos))) + nearClimbable := s.blockClimbable(s.blockAtPos(posFromVec3(state.Pos))) if nearClimbable { newVel := state.Vel negClimbSpeed := -ClimbSpeed @@ -408,9 +408,9 @@ func (s *Simulator) simulateMovement(state *MovementState) { if state.SupportingBlockPos != nil { blockUnder = s.blockAtPos(*state.SupportingBlockPos) } else { - blockUnder = s.blockAtPos(cube.PosFromVec3(state.Pos.Sub(mgl64.Vec3{0, 0.2}))) + blockUnder = s.blockAtPos(posFromVec3(state.Pos.Sub(mgl32.Vec3{0, 0.2}))) if _, isAir := blockUnder.(block.Air); isAir { - below := s.blockAtPos(cube.PosFromVec3(state.Pos).Side(cube.FaceDown)) + below := s.blockAtPos(posFromVec3(state.Pos).Side(cube.FaceDown)) if IsWall(below) || IsFence(below) { blockUnder = below } @@ -428,13 +428,13 @@ func (s *Simulator) simulateMovement(state *MovementState) { if inCobweb { s.debugf("post-move cobweb force applied (0 vel)") - state.SetVel(mgl64.Vec3{}) + state.SetVel(mgl32.Vec3{}) } newVel := state.Vel if s.Effects != nil { if amp, ok := s.Effects.GetEffect(packet.EffectLevitation); ok { - levSpeed := LevitationGravityMultiplier * float64(amp) + levSpeed := LevitationGravityMultiplier * float32(amp) newVel[1] += (levSpeed - newVel[1]) * 0.2 } else if state.HasGravity { newVel[1] -= state.Gravity @@ -510,7 +510,7 @@ func (s *Simulator) attemptTeleport(state *MovementState) bool { if !state.TeleportIsSmoothed { state.SetPos(state.TeleportPos) - state.SetVel(mgl64.Vec3{}) + state.SetVel(mgl32.Vec3{}) state.JumpDelay = 0 s.attemptJump(state, nil) return true @@ -518,7 +518,7 @@ func (s *Simulator) attemptTeleport(state *MovementState) bool { posDelta := state.TeleportPos.Sub(state.Pos) if remaining := state.RemainingTeleportTicks() + 1; remaining > 0 { - newPos := state.Pos.Add(posDelta.Mul(1.0 / float64(remaining))) + newPos := state.Pos.Add(posDelta.Mul(1.0 / float32(remaining))) state.SetPos(newPos) state.JumpDelay = 0 return remaining > 1 @@ -527,10 +527,10 @@ func (s *Simulator) attemptTeleport(state *MovementState) bool { } func (s *Simulator) simulateGlide(state *MovementState) { - radians := math.Pi / 180.0 + radians := math32.Pi / 180.0 yaw, pitch := state.Rotation.Z()*radians, state.Rotation.X()*radians - yawCos := MCCos(-yaw - math.Pi) - yawSin := MCSin(-yaw - math.Pi) + yawCos := MCCos(-yaw - math32.Pi) + yawSin := MCSin(-yaw - math32.Pi) pitchCos := MCCos(pitch) pitchSin := MCSin(pitch) @@ -539,7 +539,7 @@ func (s *Simulator) simulateGlide(state *MovementState) { lookZ := yawCos * -pitchCos vel := state.Vel - velHz := math.Sqrt(vel[0]*vel[0] + vel[2]*vel[2]) + velHz := math32.Sqrt(vel[0]*vel[0] + vel[2]*vel[2]) lookHz := pitchCos sqrPitchCos := pitchCos * pitchCos @@ -587,7 +587,7 @@ func (s *Simulator) walkOnBlock(state *MovementState, blockUnder world.Block) { newVel := state.Vel switch s.blockName(blockUnder) { case "minecraft:slime": - yMov := math.Abs(newVel.Y()) + yMov := math32.Abs(newVel.Y()) if yMov < 0.1 && !state.PressingSneak { d1 := 0.4 + yMov*0.2 newVel[0] *= d1 @@ -598,7 +598,7 @@ func (s *Simulator) walkOnBlock(state *MovementState, blockUnder world.Block) { s.debugf("walkOnBlock: oldVel=%v newVel=%v", oldVel, newVel) } -func (s *Simulator) landOnBlock(state *MovementState, old mgl64.Vec3, blockUnder world.Block) { +func (s *Simulator) landOnBlock(state *MovementState, old mgl32.Vec3, blockUnder world.Block) { newVel := state.Vel if old.Y() >= 0 || state.PressingSneak { newVel[1] = 0 @@ -609,18 +609,18 @@ func (s *Simulator) landOnBlock(state *MovementState, old mgl64.Vec3, blockUnder switch s.blockName(blockUnder) { case "minecraft:slime": newVel[1] = SlimeBounceMultiplier * old.Y() - if math.Abs(newVel[1]) < 1e-4 { + if math32.Abs(newVel[1]) < 1e-4 { newVel[1] = 0.0 } case "minecraft:bed": - newVel[1] = math.Min(1.0, BedBounceMultiplier*old.Y()) + newVel[1] = math32.Min(1.0, BedBounceMultiplier*old.Y()) default: newVel[1] = 0 } state.SetVel(newVel) } -func (s *Simulator) setPostCollisionMotion(state *MovementState, oldVel mgl64.Vec3, oldOnGround bool, blockUnder world.Block) { +func (s *Simulator) setPostCollisionMotion(state *MovementState, oldVel mgl32.Vec3, oldOnGround bool, blockUnder world.Block) { if !oldOnGround && state.CollideY { s.landOnBlock(state, oldVel, blockUnder) } else if state.CollideY { @@ -639,7 +639,7 @@ func (s *Simulator) setPostCollisionMotion(state *MovementState, oldVel mgl64.Ve state.SetVel(newVel) } -func updateFallDistance(state *MovementState, oldY float64) { +func updateFallDistance(state *MovementState, oldY float32) { yDelta := state.Pos.Y() - oldY if yDelta < 0 && !state.OnGround { state.FallDistance -= yDelta @@ -651,15 +651,15 @@ func updateFallDistance(state *MovementState, oldY float64) { } } -func moveRelative(state *MovementState, moveRelativeSpeed float64) { +func moveRelative(state *MovementState, moveRelativeSpeed float32) { impulse := state.Impulse force := impulse.Y()*impulse.Y() + impulse.X()*impulse.X() if force >= 1e-4 { - force = moveRelativeSpeed / math.Max(math.Sqrt(force), 1.0) + force = moveRelativeSpeed / math32.Max(math32.Sqrt(force), 1.0) mf, ms := impulse.Y()*force, impulse.X()*force - yaw := state.Rotation.Z() * math.Pi / 180.0 + yaw := state.Rotation.Z() * math32.Pi / 180.0 v2, v3 := MCSin(yaw), MCCos(yaw) newVel := state.Vel @@ -684,7 +684,7 @@ func (s *Simulator) attemptJump(state *MovementState, clientJumpPrevented *bool) } newVel := state.Vel - newVel[1] = math.Max(state.JumpHeight, newVel[1]) + newVel[1] = math32.Max(state.JumpHeight, newVel[1]) state.JumpDelay = JumpDelayTicks if state.Sprinting { @@ -704,7 +704,7 @@ func (s *Simulator) attemptJump(state *MovementState, clientJumpPrevented *bool) return true } -func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl64.Vec3) bool { +func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl32.Vec3) bool { w := s.World if w == nil { return false @@ -713,9 +713,9 @@ func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl64.Vec3) bool collisionBB := state.BoundingBox(useSlideOffset) bbList := w.GetNearbyBBoxes(collisionBB.Extend(jumpVel)) - yVel := mgl64.Vec3{0, jumpVel.Y()} - xVel := mgl64.Vec3{jumpVel.X()} - zVel := mgl64.Vec3{0, 0, jumpVel.Z()} + yVel := mgl32.Vec3{0, jumpVel.Y()} + xVel := mgl32.Vec3{jumpVel.X()} + zVel := mgl32.Vec3{0, 0, jumpVel.Z()} for i := len(bbList) - 1; i >= 0; i-- { yVel = BBClipCollide(bbList[i], collisionBB, yVel, false, nil) @@ -735,9 +735,9 @@ func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl64.Vec3) bool return false } - xVel = mgl64.Vec3{jumpVel.X()} - yVel = mgl64.Vec3{0, jumpVel.Y()} - zVel = mgl64.Vec3{0, 0, jumpVel.Z()} + xVel = mgl32.Vec3{jumpVel.X()} + yVel = mgl32.Vec3{0, jumpVel.Y()} + zVel = mgl32.Vec3{0, 0, jumpVel.Z()} collisionBB = state.BoundingBox(useSlideOffset) for i := len(bbList) - 1; i >= 0; i-- { @@ -770,14 +770,14 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool bbList := w.GetNearbyBBoxes(collisionBB.Extend(currVel)) useOneWayCollisions := state.StuckInCollider - penetration := mgl64.Vec3{} + penetration := mgl32.Vec3{} - yVel := mgl64.Vec3{0, currVel.Y()} + yVel := mgl32.Vec3{0, currVel.Y()} if clientJumpPrevented { yVel[1] = 0 } - xVel := mgl64.Vec3{currVel.X()} - zVel := mgl64.Vec3{0, 0, currVel.Z()} + xVel := mgl32.Vec3{currVel.X()} + zVel := mgl32.Vec3{0, 0, currVel.Z()} for i := len(bbList) - 1; i >= 0; i-- { yVel = BBClipCollide(bbList[i], collisionBB, yVel, useOneWayCollisions, &penetration) @@ -798,7 +798,7 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool s.debugf("(Z) hz-collision non-step=%v /w penetration=%v (oneWay=%v)", zVel, penetration, useOneWayCollisions) collisionVel := yVel.Add(xVel).Add(zVel) - collisionPos := mgl64.Vec3{ + collisionPos := mgl32.Vec3{ (collisionBB.Min().X() + collisionBB.Max().X()) * 0.5, collisionBB.Min().Y(), (collisionBB.Min().Z() + collisionBB.Max().Z()) * 0.5, @@ -815,9 +815,9 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool onGround := state.OnGround || (yCollision && currVel.Y() < 0.0) if onGround && (xCollision || zCollision) { - stepYVel := mgl64.Vec3{0, StepHeight} - stepXVel := mgl64.Vec3{currVel.X()} - stepZVel := mgl64.Vec3{0, 0, currVel.Z()} + stepYVel := mgl32.Vec3{0, StepHeight} + stepXVel := mgl32.Vec3{currVel.X()} + stepZVel := mgl32.Vec3{0, 0, currVel.Z()} stepBB := state.BoundingBox(useSlideOffset) for _, blockBox := range bbList { @@ -855,7 +855,7 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool } else { hasStepCollisions = hasNearbyBBoxes(w, stepBB) } - stepPos := mgl64.Vec3{ + stepPos := mgl32.Vec3{ (stepBB.Min().X() + stepBB.Max().X()) * 0.5, stepBB.Min().Y(), (stepBB.Min().Z() + stepBB.Max().Z()) * 0.5, @@ -889,7 +889,7 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool } } - endPos := mgl64.Vec3{ + endPos := mgl32.Vec3{ (collisionBB.Min().X() + collisionBB.Max().X()) * 0.5, collisionBB.Min().Y(), (collisionBB.Min().Z() + collisionBB.Max().Z()) * 0.5, @@ -902,17 +902,17 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool s.debugf("applying slideOffset, able to subtract endPos.y this frame by %f", state.SlideOffset.Y()) } else { s.debugf("using slide offset, RESETTING slide offset vector") - state.SlideOffset = mgl64.Vec2{} + state.SlideOffset = mgl32.Vec2{} } } state.SetPos(endPos) - yCollision = math.Abs(currVel.Y()-collisionVel.Y()) >= 1e-5 - state.CollideX = math.Abs(currVel.X()-collisionVel.X()) >= 1e-5 + yCollision = math32.Abs(currVel.Y()-collisionVel.Y()) >= 1e-5 + state.CollideX = math32.Abs(currVel.X()-collisionVel.X()) >= 1e-5 state.CollideY = yCollision - state.CollideZ = math.Abs(currVel.Z()-collisionVel.Z()) >= 1e-5 + state.CollideZ = math32.Abs(currVel.Z()-collisionVel.Z()) >= 1e-5 - state.OnGround = (yCollision && currVel.Y() < 0) || (state.OnGround && !yCollision && math.Abs(currVel.Y()) <= 1e-5) + state.OnGround = (yCollision && currVel.Y() < 0) || (state.OnGround && !yCollision && math32.Abs(currVel.Y()) <= 1e-5) checkSupportingBlockPos(state, w, useSlideOffset, currVel) state.SetVel(collisionVel) s.debugf("clientVel=%v clientPos=%v", state.Client.Mov, state.Client.Pos) @@ -936,8 +936,8 @@ func (s *Simulator) avoidEdge(state *MovementState) { return } - edgeBoundry := 0.025 - offset := 0.05 + edgeBoundry := float32(0.025) + offset := float32(0.05) // Cap iterations to avoid excessive work with very large velocities. // should never happen, defensive. const maxIter = 1000 @@ -945,11 +945,11 @@ func (s *Simulator) avoidEdge(state *MovementState) { oldVel := state.Vel newVel := state.Vel useSlideOffset := s.Options.UseSlideOffset - bb := state.BoundingBox(useSlideOffset).GrowVec3(mgl64.Vec3{-edgeBoundry, 0, -edgeBoundry}) + bb := state.BoundingBox(useSlideOffset).GrowVec3(mgl32.Vec3{-edgeBoundry, 0, -edgeBoundry}) xMov, zMov := newVel.X(), newVel.Z() i := 0 - for i = 0; i < maxIter && xMov != 0.0 && !hasNearbyBBoxes(w, bb.Translate(mgl64.Vec3{xMov, -StepHeight * 1.01, 0})); i++ { + for i = 0; i < maxIter && xMov != 0.0 && !hasNearbyBBoxes(w, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, 0})); i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -962,7 +962,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { xMov = 0 } - for i = 0; i < maxIter && zMov != 0.0 && !hasNearbyBBoxes(w, bb.Translate(mgl64.Vec3{0, -StepHeight * 1.01, zMov})); i++ { + for i = 0; i < maxIter && zMov != 0.0 && !hasNearbyBBoxes(w, bb.Translate(mgl32.Vec3{0, -StepHeight * 1.01, zMov})); i++ { if zMov < offset && zMov >= -offset { zMov = 0 } else if zMov > 0 { @@ -975,7 +975,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { zMov = 0 } - for i = 0; i < maxIter && xMov != 0.0 && zMov != 0.0 && !hasNearbyBBoxes(w, bb.Translate(mgl64.Vec3{xMov, -StepHeight * 1.01, zMov})); i++ { + for i = 0; i < maxIter && xMov != 0.0 && zMov != 0.0 && !hasNearbyBBoxes(w, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, zMov})); i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -1020,7 +1020,7 @@ func (s *Simulator) isInsideCobweb(state *MovementState) bool { boxes := s.World.BlockCollisions(pos) for _, box := range boxes { - if bb.IntersectsWith(box.Translate(pos.Vec3())) { + if bb.IntersectsWith(box.Translate(posVec3(pos))) { insideCobweb = true break } @@ -1032,14 +1032,14 @@ func (s *Simulator) isInsideCobweb(state *MovementState) bool { return insideCobweb } -func nearbyBlocks(aabb cube.BBox, w WorldProvider) iter.Seq2[cube.Pos, world.Block] { +func nearbyBlocks(aabb cube.BBox32, w WorldProvider) iter.Seq2[cube.Pos, world.Block] { return func(yield func(cube.Pos, world.Block) bool) { if w == nil { return } min, max := aabb.Min(), aabb.Max() - minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2])) - maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2])) + minX, minY, minZ := int(math32.Floor(min[0])), int(math32.Floor(min[1])), int(math32.Floor(min[2])) + maxX, maxY, maxZ := int(math32.Ceil(max[0])), int(math32.Ceil(max[1])), int(math32.Ceil(max[2])) for y := minY; y <= maxY; y++ { for x := minX; x <= maxX; x++ { @@ -1054,7 +1054,7 @@ func nearbyBlocks(aabb cube.BBox, w WorldProvider) iter.Seq2[cube.Pos, world.Blo } } -func checkSupportingBlockPos(state *MovementState, w WorldProvider, useSlideOffset bool, vel mgl64.Vec3) { +func checkSupportingBlockPos(state *MovementState, w WorldProvider, useSlideOffset bool, vel mgl32.Vec3) { if !state.OnGround { state.SupportingBlockPos = nil return @@ -1062,18 +1062,18 @@ func checkSupportingBlockPos(state *MovementState, w WorldProvider, useSlideOffs decBB := state.BoundingBox(useSlideOffset).ExtendTowards(cube.FaceDown, 1e-3) findSupportingBlock(state, w, decBB) if state.SupportingBlockPos == nil { - decBB = decBB.Translate(mgl64.Vec3{-vel[0], 0, -vel[2]}) + decBB = decBB.Translate(mgl32.Vec3{-vel[0], 0, -vel[2]}) findSupportingBlock(state, w, decBB) } } -func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox) { +func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox32) { if w == nil { return } var blockPos *cube.Pos - minDist := math.MaxFloat64 - 1 - centerPos := cube.PosFromVec3(state.Pos).Vec3().Add(mgl64.Vec3{0.5, 0.5, 0.5}) + minDist := float32(math32.MaxFloat32 - 1) + centerPos := posVec3(posFromVec3(state.Pos)).Add(mgl32.Vec3{0.5, 0.5, 0.5}) for pos := range nearbyBlocks(bb, w) { boxes := w.BlockCollisions(pos) @@ -1082,10 +1082,10 @@ func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox) { } for _, box := range boxes { - if !bb.IntersectsWith(box.Translate(pos.Vec3())) { + if !bb.IntersectsWith(box.Translate(posVec3(pos))) { continue } - dist := pos.Vec3().Sub(centerPos).LenSqr() + dist := posVec3(pos).Sub(centerPos).LenSqr() if dist < minDist { minDist = dist supportPos := pos @@ -1106,10 +1106,10 @@ func (s *Simulator) blockAtPos(pos cube.Pos) world.Block { } type nearbyBBoxProbe interface { - HasNearbyBBoxes(aabb cube.BBox) bool + HasNearbyBBoxes(aabb cube.BBox32) bool } -func hasNearbyBBoxes(w WorldProvider, aabb cube.BBox) bool { +func hasNearbyBBoxes(w WorldProvider, aabb cube.BBox32) bool { if w == nil { return false } diff --git a/simulator.go b/simulator.go index f633d90..757fd61 100644 --- a/simulator.go +++ b/simulator.go @@ -1,7 +1,7 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" "github.com/df-mc/dragonfly/server/world" ) @@ -30,14 +30,14 @@ const ( type SimulationOptions struct { Mode SimulationMode - PositionCorrectionThreshold float64 - VelocityCorrectionThreshold float64 + PositionCorrectionThreshold float32 + VelocityCorrectionThreshold float32 UseSlideOffset bool SprintTiming SprintTiming LimitAllVelocity bool - LimitAllVelocityThreshold float64 + LimitAllVelocityThreshold float32 // IgnoreClientStepTiebreaker, when true, skips the client-alignment // tie-breaker in the step-up collision logic. Pathfinders that drive their @@ -77,7 +77,7 @@ func (DefaultBlockSemantics) BlockName(b world.Block) string { return BlockName(b) } -func (DefaultBlockSemantics) BlockFriction(b world.Block) float64 { +func (DefaultBlockSemantics) BlockFriction(b world.Block) float32 { return BlockFriction(b) } @@ -105,9 +105,9 @@ func (s *Simulator) blockName(b world.Block) string { return BlockName(b) } -func (s *Simulator) blockFriction(b world.Block) float64 { +func (s *Simulator) blockFriction(b world.Block) float32 { if s.BlockSemantics != nil { - if friction := s.BlockSemantics.BlockFriction(b); friction > 0 && !math.IsInf(friction, 1) { + if friction := s.BlockSemantics.BlockFriction(b); friction > 0 && !math32.IsInf(friction, 1) { return friction } } diff --git a/simulator_test.go b/simulator_test.go index 5425a21..19e4c21 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -2,14 +2,14 @@ package bedsim import ( "fmt" - "math" + "github.com/chewxy/math32" "strings" "testing" "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" + "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -19,11 +19,11 @@ func (mockWorld) Block(pos cube.Pos) world.Block { return block.Air{} } -func (mockWorld) BlockCollisions(pos cube.Pos) []cube.BBox { +func (mockWorld) BlockCollisions(pos cube.Pos) []cube.BBox32 { return nil } -func (mockWorld) GetNearbyBBoxes(aabb cube.BBox) []cube.BBox { +func (mockWorld) GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 { return nil } @@ -33,23 +33,49 @@ func (mockWorld) IsChunkLoaded(chunkX, chunkZ int32) bool { type staticWorld struct { chunkLoaded bool - boxes []cube.BBox + boxes []cube.BBox32 +} + +type cobwebWorld struct { + pos cube.Pos +} + +func (w cobwebWorld) Block(pos cube.Pos) world.Block { + if pos == w.pos { + return block.Cobweb{} + } + return block.Air{} +} + +func (w cobwebWorld) BlockCollisions(pos cube.Pos) []cube.BBox32 { + if pos != w.pos { + return nil + } + return []cube.BBox32{cube.Box32(0, 0, 0, 1, 1, 1)} +} + +func (cobwebWorld) GetNearbyBBoxes(cube.BBox32) []cube.BBox32 { + return nil +} + +func (cobwebWorld) IsChunkLoaded(int32, int32) bool { + return true } func (w staticWorld) Block(pos cube.Pos) world.Block { return block.Air{} } -func (w staticWorld) BlockCollisions(pos cube.Pos) []cube.BBox { +func (w staticWorld) BlockCollisions(pos cube.Pos) []cube.BBox32 { return nil } -func (w staticWorld) GetNearbyBBoxes(aabb cube.BBox) []cube.BBox { +func (w staticWorld) GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 { if len(w.boxes) == 0 { return nil } - out := make([]cube.BBox, 0, len(w.boxes)) + out := make([]cube.BBox32, 0, len(w.boxes)) for _, bb := range w.boxes { if aabb.IntersectsWith(bb) { out = append(out, bb) @@ -81,7 +107,7 @@ func (m mockInventory) HasElytra() bool { type overrideBlockSemantics struct { name string - friction float64 + friction float32 climbable bool } @@ -89,7 +115,7 @@ func (s overrideBlockSemantics) BlockName(world.Block) string { return s.name } -func (s overrideBlockSemantics) BlockFriction(world.Block) float64 { +func (s overrideBlockSemantics) BlockFriction(world.Block) float32 { return s.friction } @@ -100,14 +126,14 @@ func (s overrideBlockSemantics) BlockClimbable(world.Block) bool { func newBaseState() *MovementState { return &MovementState{ Client: ClientState{ - Pos: mgl64.Vec3{}, - Vel: mgl64.Vec3{}, - Mov: mgl64.Vec3{}, + Pos: mgl32.Vec3{}, + Vel: mgl32.Vec3{}, + Mov: mgl32.Vec3{}, }, - Pos: mgl64.Vec3{}, - Vel: mgl64.Vec3{}, - Mov: mgl64.Vec3{}, - Size: mgl64.Vec3{0.6, 1.8, 1}, + Pos: mgl32.Vec3{}, + Vel: mgl32.Vec3{}, + Mov: mgl32.Vec3{}, + Size: mgl32.Vec3{0.6, 1.8, 1}, MovementSpeed: 0.1, DefaultMovementSpeed: 0.1, AirSpeed: 0.02, @@ -121,6 +147,17 @@ func newBaseState() *MovementState { } } +func TestInsideCobwebTranslatesBlockLocalCollisionBoxes(t *testing.T) { + pos := cube.Pos{32, 64, -24} + sim := &Simulator{World: cobwebWorld{pos: pos}} + state := newBaseState() + state.Pos = mgl32.Vec3{float32(pos.X()) + 0.5, float32(pos.Y()), float32(pos.Z()) + 0.5} + + if !sim.isInsideCobweb(state) { + t.Fatal("expected collision with block-local cobweb box away from the origin") + } +} + func containsLog(logs []string, needle string) bool { for _, line := range logs { if strings.Contains(line, needle) { @@ -143,9 +180,9 @@ func TestSimulateMoveRelative(t *testing.T) { state := newBaseState() input := InputState{ - MoveVector: mgl64.Vec2{0, 1}, - ClientPos: mgl64.Vec3{}, - ClientVel: mgl64.Vec3{}, + MoveVector: mgl32.Vec2{0, 1}, + ClientPos: mgl32.Vec3{}, + ClientVel: mgl32.Vec3{}, Yaw: 0, Pitch: 0, HeadYaw: 0, @@ -167,7 +204,7 @@ func TestSimulateStateOutcomeTeleport(t *testing.T) { } state := newBaseState() - state.TeleportPos = mgl64.Vec3{12, 63, -4} + state.TeleportPos = mgl32.Vec3{12, 63, -4} state.TicksSinceTeleport = 0 state.TeleportCompletionTicks = 0 state.TeleportIsSmoothed = false @@ -188,8 +225,8 @@ func TestSimulateStateTeleportDoesNotUpdateFallDistance(t *testing.T) { } state := newBaseState() - state.Pos = mgl64.Vec3{0, 70, 0} - state.TeleportPos = mgl64.Vec3{0, 60, 0} + state.Pos = mgl32.Vec3{0, 70, 0} + state.TeleportPos = mgl32.Vec3{0, 60, 0} state.TicksSinceTeleport = 0 state.TeleportCompletionTicks = 0 @@ -210,10 +247,10 @@ func TestSimulateStateOutcomeUnreliable(t *testing.T) { state := newBaseState() state.GameMode = packet.GameTypeCreative - state.Pos = mgl64.Vec3{10, 70, 10} - state.Client.Pos = mgl64.Vec3{3, 64, -1} - state.Vel = mgl64.Vec3{0.3, 0.9, -0.2} - state.Client.Vel = mgl64.Vec3{-0.1, 0, 0.2} + state.Pos = mgl32.Vec3{10, 70, 10} + state.Client.Pos = mgl32.Vec3{3, 64, -1} + state.Vel = mgl32.Vec3{0.3, 0.9, -0.2} + state.Client.Vel = mgl32.Vec3{-0.1, 0, 0.2} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeUnreliable { @@ -236,10 +273,10 @@ func TestSimulateStateNoClipPassesThroughClientState(t *testing.T) { state := newBaseState() state.NoClip = true state.OnGround = true - state.Pos = mgl64.Vec3{10, 70, 10} - state.Client.Pos = mgl64.Vec3{3, 64, -1} - state.Vel = mgl64.Vec3{1, 2, 3} - state.Client.Vel = mgl64.Vec3{0.1, 0.2, 0.3} + state.Pos = mgl32.Vec3{10, 70, 10} + state.Client.Pos = mgl32.Vec3{3, 64, -1} + state.Vel = mgl32.Vec3{1, 2, 3} + state.Client.Vel = mgl32.Vec3{0.1, 0.2, 0.3} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeUnreliable { @@ -258,15 +295,15 @@ func TestSimulateStateNoClipPassesThroughClientState(t *testing.T) { func TestUpdateFallDistanceUsesResolvedGroundState(t *testing.T) { state := newBaseState() - state.Pos = mgl64.Vec3{0, 10, 0} + state.Pos = mgl32.Vec3{0, 10, 0} - state.SetPos(mgl64.Vec3{0, 7, 0}) + state.SetPos(mgl32.Vec3{0, 7, 0}) updateFallDistance(state, 10) if state.FallDistance != 3 { t.Fatalf("expected fall distance to increase after downward move, got %v", state.FallDistance) } - state.SetPos(mgl64.Vec3{0, 8, 0}) + state.SetPos(mgl32.Vec3{0, 8, 0}) updateFallDistance(state, 7) if state.FallDistance != 0 { t.Fatalf("expected upward move to reset fall distance, got %v", state.FallDistance) @@ -274,7 +311,7 @@ func TestUpdateFallDistanceUsesResolvedGroundState(t *testing.T) { state.FallDistance = 4 state.OnGround = true - state.SetPos(mgl64.Vec3{0, 6, 0}) + state.SetPos(mgl32.Vec3{0, 6, 0}) updateFallDistance(state, 8) if state.FallDistance != 0 { t.Fatalf("expected grounded move to clear fall distance, got %v", state.FallDistance) @@ -324,13 +361,13 @@ func TestSimulatorInvalidBlockSemanticsFrictionFallsBackToDefault(t *testing.T) tests := []struct { name string - friction float64 + friction float32 }{ {name: "zero", friction: 0}, {name: "negative", friction: -0.42}, - {name: "nan", friction: math.NaN()}, - {name: "positive infinity", friction: math.Inf(1)}, - {name: "negative infinity", friction: math.Inf(-1)}, + {name: "nan", friction: math32.NaN()}, + {name: "positive infinity", friction: math32.Inf(1)}, + {name: "negative infinity", friction: math32.Inf(-1)}, } for _, tt := range tests { @@ -356,13 +393,13 @@ func TestSimulateStateOutcomeUnloadedChunk(t *testing.T) { } state := newBaseState() - state.Vel = mgl64.Vec3{0.2, 0.1, -0.1} + state.Vel = mgl32.Vec3{0.2, 0.1, -0.1} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeUnloadedChunk { t.Fatalf("expected unloaded chunk outcome, got %v", result.Outcome) } - if state.Vel != (mgl64.Vec3{}) { + if state.Vel != (mgl32.Vec3{}) { t.Fatalf("expected velocity to be cleared, got %v", state.Vel) } } @@ -375,13 +412,13 @@ func TestSimulateStateOutcomeImmobileOrNotReady(t *testing.T) { state := newBaseState() state.Immobile = true - state.Vel = mgl64.Vec3{0.5, -0.3, 0.5} + state.Vel = mgl32.Vec3{0.5, -0.3, 0.5} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeImmobileOrNotReady { t.Fatalf("expected immobile/not-ready outcome, got %v", result.Outcome) } - if state.Vel != (mgl64.Vec3{}) { + if state.Vel != (mgl32.Vec3{}) { t.Fatalf("expected velocity to be cleared, got %v", state.Vel) } } @@ -394,7 +431,7 @@ func TestSimulateStateSkipsGravityWhenDisabled(t *testing.T) { state := newBaseState() state.HasGravity = false - state.Impulse = mgl64.Vec2{0, 0.98} + state.Impulse = mgl32.Vec2{0, 0.98} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeNormal { @@ -415,7 +452,7 @@ func TestSimulateStateInvalidGlideContinuesNormalMovement(t *testing.T) { state := newBaseState() state.Gliding = true state.OnGround = true - state.Impulse = mgl64.Vec2{0, 0.98} + state.Impulse = mgl32.Vec2{0, 0.98} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeNormal { @@ -442,7 +479,7 @@ func TestSimulateStateDebugTraceIncludesCollisionStream(t *testing.T) { } state := newBaseState() - state.Impulse = mgl64.Vec2{0, 0.98} + state.Impulse = mgl32.Vec2{0, 0.98} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeNormal { @@ -470,8 +507,8 @@ func TestSimulateStateDebugTraceJumpBlocked(t *testing.T) { sim := &Simulator{ World: staticWorld{ chunkLoaded: true, - boxes: []cube.BBox{ - cube.Box(0, 2, 1, 1, 3, 2), + boxes: []cube.BBox32{ + cube.Box32(0, 2, 1, 1, 3, 2), }, }, Effects: mockEffects{}, @@ -483,12 +520,12 @@ func TestSimulateStateDebugTraceJumpBlocked(t *testing.T) { } state := newBaseState() - state.Pos = mgl64.Vec3{0, 0, 0.69} + state.Pos = mgl32.Vec3{0, 0, 0.69} state.Client.Pos = state.Pos state.OnGround = true state.Jumping = true state.Sprinting = true - state.Rotation = mgl64.Vec3{0, 0, 0} + state.Rotation = mgl32.Vec3{0, 0, 0} state.JumpHeight = DefaultJumpHeight result := sim.SimulateState(state) @@ -509,13 +546,13 @@ func TestStepUpTiebreaker(t *testing.T) { // Geometry: ground at Y=0, a 0.5-high slab at X=1 (X=1..2, Y=0..0.5). // The player stands on the ground at X≈0.5, walks in +X toward the slab. // The step-up (0.5 blocks) is within StepHeight (0.6). - slabBox := cube.Box(1, 0, -1, 2, 0.5, 2) - groundBox := cube.Box(-1, -1, -1, 1, 0, 2) + slabBox := cube.Box32(1, 0, -1, 2, 0.5, 2) + groundBox := cube.Box32(-1, -1, -1, 1, 0, 2) - startPos := mgl64.Vec3{0.5, 0, 0.5} + startPos := mgl32.Vec3{0.5, 0, 0.5} - runSim := func(ignoreStepTiebreaker bool) (mgl64.Vec3, bool) { - w := staticWorld{chunkLoaded: true, boxes: []cube.BBox{slabBox, groundBox}} + runSim := func(ignoreStepTiebreaker bool) (mgl32.Vec3, bool) { + w := staticWorld{chunkLoaded: true, boxes: []cube.BBox32{slabBox, groundBox}} sim := &Simulator{ World: w, Effects: mockEffects{}, @@ -531,9 +568,9 @@ func TestStepUpTiebreaker(t *testing.T) { state.JumpHeight = DefaultJumpHeight input := InputState{ - MoveVector: mgl64.Vec2{0, 1}, + MoveVector: mgl32.Vec2{0, 1}, ClientPos: startPos, - ClientVel: mgl64.Vec3{}, + ClientVel: mgl32.Vec3{}, Yaw: -90, // face +X HeadYaw: -90, } @@ -564,8 +601,8 @@ func TestStepUpTiebreaker(t *testing.T) { t.Run("blocked step still rejected with flag", func(t *testing.T) { // Place a ceiling directly above the slab so stepping up would cause collision. - ceilingBox := cube.Box(1, 1.3, -1, 2, 2.3, 2) // leaves only 0.8 gap, player is 1.8 tall - w := staticWorld{chunkLoaded: true, boxes: []cube.BBox{slabBox, groundBox, ceilingBox}} + ceilingBox := cube.Box32(1, 1.3, -1, 2, 2.3, 2) // leaves only 0.8 gap, player is 1.8 tall + w := staticWorld{chunkLoaded: true, boxes: []cube.BBox32{slabBox, groundBox, ceilingBox}} sim := &Simulator{ World: w, Effects: mockEffects{}, @@ -581,9 +618,9 @@ func TestStepUpTiebreaker(t *testing.T) { state.JumpHeight = DefaultJumpHeight input := InputState{ - MoveVector: mgl64.Vec2{0, 1}, + MoveVector: mgl32.Vec2{0, 1}, ClientPos: startPos, - ClientVel: mgl64.Vec3{}, + ClientVel: mgl32.Vec3{}, Yaw: -90, HeadYaw: -90, } @@ -610,8 +647,8 @@ func TestResultFromStateCorrectionModes(t *testing.T) { name: "authoritative velocity-only drift", mode: SimulationModeAuthoritative, mutate: func(state *MovementState) { - state.Vel = mgl64.Vec3{0.5, 0, 0} - state.Client.Vel = mgl64.Vec3{} + state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Client.Vel = mgl32.Vec3{} }, wantSet: true, }, @@ -619,8 +656,8 @@ func TestResultFromStateCorrectionModes(t *testing.T) { name: "permissive velocity-only drift", mode: SimulationModePermissive, mutate: func(state *MovementState) { - state.Vel = mgl64.Vec3{0.5, 0, 0} - state.Client.Vel = mgl64.Vec3{} + state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Client.Vel = mgl32.Vec3{} }, wantSet: false, }, @@ -628,8 +665,8 @@ func TestResultFromStateCorrectionModes(t *testing.T) { name: "permissive position drift", mode: SimulationModePermissive, mutate: func(state *MovementState) { - state.Pos = mgl64.Vec3{0.5, 0, 0} - state.Client.Pos = mgl64.Vec3{} + state.Pos = mgl32.Vec3{0.5, 0, 0} + state.Client.Pos = mgl32.Vec3{} }, wantSet: true, }, @@ -637,8 +674,8 @@ func TestResultFromStateCorrectionModes(t *testing.T) { name: "passive position drift", mode: SimulationModePassive, mutate: func(state *MovementState) { - state.Pos = mgl64.Vec3{0.5, 0, 0} - state.Client.Pos = mgl64.Vec3{} + state.Pos = mgl32.Vec3{0.5, 0, 0} + state.Client.Pos = mgl32.Vec3{} }, wantSet: false, },