From 6ad5a87236f7b3bb19ff6dc6970884d30fe1dd3c Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 00:06:38 -0400 Subject: [PATCH 1/8] feat: convert simulation to native float32 math --- bbox.go | 22 +++- block.go | 8 +- collision.go | 38 +++--- constants.go | 42 +++--- go.mod | 2 + go.sum | 4 + input.go | 14 +- interfaces.go | 11 +- liquid.go | 103 +++++++-------- liquid_hardening_test.go | 136 +++++++++---------- liquid_test.go | 275 ++++++++++++++++++++------------------- math.go | 29 +++-- movement.go | 60 ++++----- native_float32_test.go | 25 ++++ result.go | 12 +- simulation.go | 153 +++++++++++----------- simulator.go | 14 +- simulator_test.go | 123 ++++++++--------- 18 files changed, 563 insertions(+), 508 deletions(-) create mode 100644 native_float32_test.go diff --git a/bbox.go b/bbox.go index 2ccc74d..73f989b 100644 --- a/bbox.go +++ b/bbox.go @@ -1,10 +1,20 @@ package bedsim import ( - "github.com/df-mc/dragonfly/server/block/cube" - "github.com/go-gl/mathgl/mgl64" + dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/ethaniccc/float32-cube/cube" + "github.com/go-gl/mathgl/mgl32" ) +// BBoxFromDragonfly returns a simulation bounding box rounded to float32 coordinates. +func BBoxFromDragonfly(box dfcube.BBox) cube.BBox { + min, max := box.Min(), box.Max() + return cube.Box( + 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 { @@ -19,7 +29,7 @@ func (s *MovementState) BoundingBox(useSlideOffset bool) cube.BBox { if s.SwimPose() { height = s.Size[0] * scale } - yOffset := 0.0 + yOffset := float32(0) if useSlideOffset { yOffset = s.SlideOffset.Y() } @@ -31,7 +41,7 @@ func (s *MovementState) BoundingBox(useSlideOffset bool) cube.BBox { 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. @@ -42,7 +52,7 @@ func (s *MovementState) ClientBoundingBox(useSlideOffset bool) cube.BBox { if s.SwimPose() { height = s.Size[0] * scale } - yOffset := 0.0 + yOffset := float32(0) if useSlideOffset { yOffset = s.SlideOffset.Y() } @@ -54,5 +64,5 @@ func (s *MovementState) ClientBoundingBox(useSlideOffset bool) cube.BBox { 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..0aeb5cf 100644 --- a/block.go +++ b/block.go @@ -5,7 +5,7 @@ import ( "sync" "github.com/df-mc/dragonfly/server/block" - "github.com/df-mc/dragonfly/server/block/cube" + dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" ) @@ -37,9 +37,9 @@ func BlockName(b world.Block) 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) { @@ -73,7 +73,7 @@ func BlockClimbable(b world.Block) bool { // BlockSupportHeight returns the effective standing surface height for a ground // block by sampling its collision boxes at the block centre (0.5, 0.5). // This handles slabs, stairs, and any other sub-block geometry correctly. -func BlockSupportHeight(b world.Block, pos cube.Pos, src world.BlockSource) float32 { +func BlockSupportHeight(b world.Block, pos dfcube.Pos, src world.BlockSource) float32 { boxes := b.Model().BBox(pos, src) maxY := float32(-1) for _, box := range boxes { diff --git a/collision.go b/collision.go index c00243c..e09e979 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/ethaniccc/float32-cube/cube" + "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.BBox, 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.BBox, 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 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..df14850 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/oomph-ac/bedsim go 1.25.0 require ( + github.com/chewxy/math32 v1.11.1 github.com/df-mc/dragonfly v0.10.11-0.20260205145355-8d1311b36535 + github.com/ethaniccc/float32-cube v0.0.0-20250511224129-7af1f8c4ee12 github.com/go-gl/mathgl v1.2.0 github.com/sandertv/gophertunnel v1.53.1-0.20260205132042-c839e607304f ) diff --git a/go.sum b/go.sum index 92ea313..3090985 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,15 @@ 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/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8= +github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= 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/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/ethaniccc/float32-cube v0.0.0-20250511224129-7af1f8c4ee12 h1:o8NDdPPBeF7y//XYIRvzrXPB08/Lblt/ceu1+3vS1hM= +github.com/ethaniccc/float32-cube v0.0.0-20250511224129-7af1f8c4ee12/go.mod h1:xBh0GYHZ5yHg3YvvUGriGLSAlm7YW2S9SRULstdLZLk= 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= 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..e19c9b8 100644 --- a/interfaces.go +++ b/interfaces.go @@ -1,21 +1,22 @@ package bedsim import ( - "github.com/df-mc/dragonfly/server/block/cube" + dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" + "github.com/ethaniccc/float32-cube/cube" ) // 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 + Block(pos dfcube.Pos) world.Block + BlockCollisions(pos dfcube.Pos) []cube.BBox GetNearbyBBoxes(aabb cube.BBox) []cube.BBox IsChunkLoaded(chunkX, chunkZ int32) bool } // LiquidProvider returns liquids from either block layer at a position. type LiquidProvider interface { - Liquid(pos cube.Pos) (world.Liquid, bool) + Liquid(pos dfcube.Pos) (world.Liquid, bool) } // BlockSemanticsProvider resolves movement-relevant block behavior. Implement @@ -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..e8ccd99 100644 --- a/liquid.go +++ b/liquid.go @@ -1,17 +1,18 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" "github.com/df-mc/dragonfly/server/block" - "github.com/df-mc/dragonfly/server/block/cube" + dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" + "github.com/ethaniccc/float32-cube/cube" + "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. @@ -35,13 +36,13 @@ func (k liquidKind) matches(liquid world.Liquid) bool { } var liquidFaces = [...]struct { - delta cube.Pos - vec mgl64.Vec3 + delta dfcube.Pos + 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}}, + {dfcube.Pos{-1, 0, 0}, mgl32.Vec3{-1, 0, 0}}, + {dfcube.Pos{1, 0, 0}, mgl32.Vec3{1, 0, 0}}, + {dfcube.Pos{0, 0, -1}, mgl32.Vec3{0, 0, -1}}, + {dfcube.Pos{0, 0, 1}, mgl32.Vec3{0, 0, 1}}, } func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, touchingLiquid bool) { @@ -72,8 +73,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 +84,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 +106,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 +122,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 +132,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 +145,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 +159,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 @@ -181,29 +182,29 @@ func (s *Simulator) updateSwimTravel(state *MovementState) { state.SetVel(vel) } -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} +func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) []dfcube.Pos { + 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)) - positions := make([]cube.Pos, 0, 4) + 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([]dfcube.Pos, 0, 4) for x := minX; x < maxX; x++ { for y := minY; y < maxY; y++ { for z := minZ; z < maxZ; z++ { - pos := cube.Pos{x, y, z} + pos := dfcube.Pos{x, y, z} liquid, ok := s.liquidAt(pos) if !ok || !kind.matches(liquid) { continue } 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 +218,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.BBox, offset mgl32.Vec3) cube.BBox { min, max := box.Min().Add(offset), box.Max().Sub(offset) originalMin, originalMax := box.Min(), box.Max() for axis := range 3 { @@ -229,7 +230,7 @@ func shrinkLiquidBox(box cube.BBox, offset mgl64.Vec3) cube.BBox { return cube.Box(min.X(), min.Y(), min.Z(), max.X(), max.Y(), max.Z()) } -func (s *Simulator) liquidMovementBlock(pos cube.Pos) world.Block { +func (s *Simulator) liquidMovementBlock(pos dfcube.Pos) world.Block { if liquid, ok := s.liquidAt(pos); ok { return liquid } @@ -238,7 +239,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 dfcube.Pos) []cube.BBox { if s.World == nil { return nil } @@ -267,7 +268,7 @@ func (s *Simulator) HasLiquidLayer() bool { return ok } -func (s *Simulator) liquidAt(pos cube.Pos) (world.Liquid, bool) { +func (s *Simulator) liquidAt(pos dfcube.Pos) (world.Liquid, bool) { if provider, ok := s.liquidLayer(); ok { if liquid, found := provider.Liquid(pos); found { return liquid, true @@ -277,21 +278,21 @@ 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 { 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++ { - if _, ok := s.liquidAt(cube.Pos{x, y, z}); ok { + if _, ok := s.liquidAt(dfcube.Pos{x, y, z}); ok { return true } } @@ -300,8 +301,8 @@ func (s *Simulator) containsAnyLiquid(box cube.BBox) bool { return false } -func (s *Simulator) applyLiquidFlow(state *MovementState, positions []cube.Pos, kind liquidKind) { - flow := mgl64.Vec3{} +func (s *Simulator) applyLiquidFlow(state *MovementState, positions []dfcube.Pos, kind liquidKind) { + flow := mgl32.Vec3{} for _, pos := range positions { liquid, ok := s.liquidAt(pos) if !ok || !kind.matches(liquid) { @@ -310,7 +311,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 +320,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 dfcube.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 } @@ -335,15 +336,15 @@ func (s *Simulator) liquidFlow(pos cube.Pos, liquid world.Liquid) mgl64.Vec3 { if len(s.blockCollisions(neighbourPos)) != 0 { continue } - below := neighbourPos.Side(cube.FaceDown) + below := neighbourPos.Side(dfcube.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() { for _, face := range liquidFaces { neighbourPos := pos.Add(face.delta) - aboveNeighbour := neighbourPos.Side(cube.FaceUp) + aboveNeighbour := neighbourPos.Side(dfcube.FaceUp) if len(s.blockCollisions(neighbourPos)) != 0 || len(s.blockCollisions(aboveNeighbour)) != 0 { if length := flow.Len(); length > 1e-4 { flow = flow.Mul(1 / length) @@ -356,10 +357,10 @@ 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 { +func (s *Simulator) liquidFlowSideClosed(pos, side dfcube.Pos) bool { stairs, ok := s.blockAtPos(pos).(block.Stairs) return ok && stairs.Model().FaceSolid(pos, pos.Face(side), s.World) } diff --git a/liquid_hardening_test.go b/liquid_hardening_test.go index 008290c..d80b233 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" + dfcube "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,16 +272,16 @@ 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) } } type explicitLiquids struct { - layer map[cube.Pos]world.Liquid + layer map[dfcube.Pos]world.Liquid } -func (p explicitLiquids) Liquid(pos cube.Pos) (world.Liquid, bool) { +func (p explicitLiquids) Liquid(pos dfcube.Pos) (world.Liquid, bool) { liquid, ok := p.layer[pos] return liquid, ok } @@ -292,7 +292,7 @@ func TestHasLiquidLayerReportsExplicitProvider(t *testing.T) { t.Fatal("a plain world must not report liquid layer support") } - sim.Liquids = explicitLiquids{layer: map[cube.Pos]world.Liquid{}} + sim.Liquids = explicitLiquids{layer: map[dfcube.Pos]world.Liquid{}} if !sim.HasLiquidLayer() { t.Fatal("an explicit Liquids provider must report support") } @@ -310,10 +310,10 @@ func TestHasLiquidLayerAcceptsWorldProvider(t *testing.T) { // The explicit field wins over the world assertion when both are present. func TestExplicitLiquidsFieldTakesPrecedence(t *testing.T) { w := newLayeredLiquidWorld() - w.waterlog(cube.Pos{0, 0, 0}, block.Air{}, waterSource) + w.waterlog(dfcube.Pos{0, 0, 0}, block.Air{}, waterSource) sim := newLiquidSim(w) - sim.Liquids = explicitLiquids{layer: map[cube.Pos]world.Liquid{}} + sim.Liquids = explicitLiquids{layer: map[dfcube.Pos]world.Liquid{}} state := submergedState() if got := len(sim.touchingLiquidBlocks(state, liquidWater)); got != 0 { @@ -323,9 +323,9 @@ func TestExplicitLiquidsFieldTakesPrecedence(t *testing.T) { // Liquids supplied through the explicit field are detected normally. func TestExplicitLiquidsProviderDetectsWaterlogged(t *testing.T) { - layer := map[cube.Pos]world.Liquid{} + layer := map[dfcube.Pos]world.Liquid{} for y := range 4 { - layer[cube.Pos{0, y, 0}] = waterSource + layer[dfcube.Pos{0, y, 0}] = waterSource } sim := newLiquidSim(newLiquidWorld()) sim.Liquids = explicitLiquids{layer: layer} @@ -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 @@ -455,18 +455,18 @@ func TestLiquidGateExcludesFlying(t *testing.T) { // weight observable through the normalized result. func TestFlowDropWeightIsEight(t *testing.T) { w := newLiquidWorld(). - set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(cube.Pos{-1, 0, 0}, block.Water{Depth: 7}). - set(cube.Pos{0, 0, 1}, block.Water{Depth: 4}). - set(cube.Pos{0, 0, -1}, block.Water{Depth: 8}). - set(cube.Pos{1, -1, 0}, block.Water{Depth: 8}) + set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(dfcube.Pos{-1, 0, 0}, block.Water{Depth: 7}). + set(dfcube.Pos{0, 0, 1}, block.Water{Depth: 4}). + set(dfcube.Pos{0, 0, -1}, block.Water{Depth: 8}). + set(dfcube.Pos{1, -1, 0}, block.Water{Depth: 8}) sim := newLiquidSim(w) - flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) + flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) // +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) } @@ -474,35 +474,35 @@ func TestFlowDropWeightIsEight(t *testing.T) { // 6 against the unit-normalized horizontal flow. func TestFallingFlowDownwardWeightIsSix(t *testing.T) { w := newLiquidWorld(). - set(cube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}). - set(cube.Pos{-1, 0, 0}, block.Water{Depth: 4}). - set(cube.Pos{1, 0, 0}, block.Stone{}) + set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}). + set(dfcube.Pos{-1, 0, 0}, block.Water{Depth: 4}). + set(dfcube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) - flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}) + flow := sim.liquidFlow(dfcube.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 dfcube.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}) + w.waterlog(dfcube.Pos{0, 0, 0}, block.Stairs{Facing: facing}, block.Water{Depth: 8}) + w.set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 4}) sim := newLiquidSim(w) - return sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) + return sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) } // Facing east: the stairs' full side faces the +X neighbour and closes it. - if flow := build(cube.East); !approxEqual(flow.X(), 0) { + if flow := build(dfcube.East); !approxEqual(flow.X(), 0) { t.Fatalf("east-facing stairs: flow X = %v, want 0", flow.X()) } // Facing west: the +X side is open, so flow proceeds toward the shallower // neighbour. - if flow := build(cube.West); !(flow.X() > 0) { + if flow := build(dfcube.West); !(flow.X() > 0) { t.Fatalf("west-facing stairs: flow X = %v, want positive", flow.X()) } } @@ -522,7 +522,7 @@ func TestNilWorldIsSafe(t *testing.T) { if sim.containsAnyLiquid(state.BoundingBox(false)) { t.Fatal("no world must contain no liquid") } - if flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}); flow.Len() != 0 { + if flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}); flow.Len() != 0 { t.Fatalf("flow = %v, want zero with no world", flow) } if sim.HasLiquidLayer() { @@ -535,14 +535,14 @@ func TestNilWorldIsSafe(t *testing.T) { func TestSwimHitboxChangesCeilingCollision(t *testing.T) { newCeilingSim := func() (*Simulator, *MovementState) { w := newLiquidWorld(). - fill(cube.Pos{-1, 0, -1}, cube.Pos{1, 1, 1}, waterSource). - set(cube.Pos{0, 2, 0}, block.Stone{}) + fill(dfcube.Pos{-1, 0, -1}, dfcube.Pos{1, 1, 1}, waterSource). + set(dfcube.Pos{0, 2, 0}, block.Stone{}) 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,13 +568,13 @@ 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}). - set(cube.Pos{0, 0, 1}, block.Water{Depth: 4}). - set(cube.Pos{-1, 1, 0}, block.Water{Depth: 8, Falling: true}). - set(cube.Pos{2, 0, 2}, block.Stone{}) + fill(dfcube.Pos{-8, 0, -8}, dfcube.Pos{8, 8, 8}, block.Water{Depth: 8}). + set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 6}). + set(dfcube.Pos{0, 0, 1}, block.Water{Depth: 4}). + set(dfcube.Pos{-1, 1, 0}, block.Water{Depth: 8, Falling: true}). + set(dfcube.Pos{2, 0, 2}, block.Stone{}) sim := newLiquidSim(w) sim.Inventory = depthStriderInventory{level: 2} state := submergedState() @@ -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, @@ -609,11 +609,11 @@ func TestLiquidGoldenScenario(t *testing.T) { // Deep enough that the player stays submerged for the whole run, so the // golden measures liquid physics rather than a surface transition. 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}). - set(cube.Pos{0, 0, 1}, block.Water{Depth: 4}). - set(cube.Pos{-1, 1, 0}, block.Water{Depth: 8, Falling: true}). - set(cube.Pos{2, 0, 2}, block.Stone{}) + fill(dfcube.Pos{-8, 0, -8}, dfcube.Pos{8, 8, 8}, block.Water{Depth: 8}). + set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 6}). + set(dfcube.Pos{0, 0, 1}, block.Water{Depth: 4}). + set(dfcube.Pos{-1, 1, 0}, block.Water{Depth: 8, Falling: true}). + set(dfcube.Pos{2, 0, 2}, block.Stone{}) sim := newLiquidSim(w) sim.Inventory = depthStriderInventory{level: 2} @@ -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..facf3fc 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -1,13 +1,14 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" "testing" "github.com/df-mc/dragonfly/server/block" - "github.com/df-mc/dragonfly/server/block/cube" + dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" + "github.com/ethaniccc/float32-cube/cube" + "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -22,39 +23,39 @@ var ( // deliberately does not implement LiquidProvider, so simulations against it // exercise the fallback path through WorldProvider.Block. type liquidWorld struct { - blocks map[cube.Pos]world.Block + blocks map[dfcube.Pos]world.Block chunkLoaded bool } func newLiquidWorld() *liquidWorld { - return &liquidWorld{blocks: map[cube.Pos]world.Block{}, chunkLoaded: true} + return &liquidWorld{blocks: map[dfcube.Pos]world.Block{}, chunkLoaded: true} } -func (w *liquidWorld) set(pos cube.Pos, b world.Block) *liquidWorld { +func (w *liquidWorld) set(pos dfcube.Pos, b world.Block) *liquidWorld { w.blocks[pos] = b return w } // fill places b in the inclusive cuboid between min and max. -func (w *liquidWorld) fill(min, max cube.Pos, b world.Block) *liquidWorld { +func (w *liquidWorld) fill(min, max dfcube.Pos, b world.Block) *liquidWorld { for x := min[0]; x <= max[0]; x++ { for y := min[1]; y <= max[1]; y++ { for z := min[2]; z <= max[2]; z++ { - w.blocks[cube.Pos{x, y, z}] = b + w.blocks[dfcube.Pos{x, y, z}] = b } } } return w } -func (w *liquidWorld) Block(pos cube.Pos) world.Block { +func (w *liquidWorld) Block(pos dfcube.Pos) world.Block { if b, ok := w.blocks[pos]; ok { return b } return block.Air{} } -func (w *liquidWorld) BlockCollisions(pos cube.Pos) []cube.BBox { +func (w *liquidWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { b := w.Block(pos) if _, air := b.(block.Air); air { return nil @@ -62,16 +63,16 @@ 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.BBox{cube.Box(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))} } func (w *liquidWorld) GetNearbyBBoxes(aabb cube.BBox) []cube.BBox { 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}) { + 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++ { + for _, bb := range w.BlockCollisions(dfcube.Pos{x, y, z}) { if bb.IntersectsWith(aabb) { out = append(out, bb) } @@ -90,20 +91,20 @@ func (w *liquidWorld) IsChunkLoaded(chunkX, chunkZ int32) bool { // second block layer (waterlogged blocks) as well as the main layer. type layeredLiquidWorld struct { *liquidWorld - layer map[cube.Pos]world.Liquid + layer map[dfcube.Pos]world.Liquid } func newLayeredLiquidWorld() *layeredLiquidWorld { - return &layeredLiquidWorld{liquidWorld: newLiquidWorld(), layer: map[cube.Pos]world.Liquid{}} + return &layeredLiquidWorld{liquidWorld: newLiquidWorld(), layer: map[dfcube.Pos]world.Liquid{}} } -func (w *layeredLiquidWorld) waterlog(pos cube.Pos, b world.Block, liquid world.Liquid) *layeredLiquidWorld { +func (w *layeredLiquidWorld) waterlog(pos dfcube.Pos, b world.Block, liquid world.Liquid) *layeredLiquidWorld { w.blocks[pos] = b w.layer[pos] = liquid return w } -func (w *layeredLiquidWorld) Liquid(pos cube.Pos) (world.Liquid, bool) { +func (w *layeredLiquidWorld) Liquid(pos dfcube.Pos) (world.Liquid, bool) { if liquid, ok := w.layer[pos]; ok { return liquid, true } @@ -141,7 +142,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 } @@ -150,14 +151,14 @@ func submergedState() *MovementState { // with the given liquid from y=0 to y=3, so the player is fully submerged and // the liquid gradient is uniform (no flow). func filledColumn(b world.Block) *liquidWorld { - return newLiquidWorld().fill(cube.Pos{-2, 0, -2}, cube.Pos{2, 3, 2}, b) + return newLiquidWorld().fill(dfcube.Pos{-2, 0, -2}, dfcube.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 +169,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 +192,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 @@ -209,12 +210,12 @@ func TestSwimmingFlagAloneDoesNotShrinkHitbox(t *testing.T) { // A spoofed swimming flag with no water anywhere must not let the player pass // through a gap that only the collapsed swim hitbox fits. func TestSpoofedSwimmingCannotFitThroughCeilingGap(t *testing.T) { - sim := newLiquidSim(newLiquidWorld().set(cube.Pos{0, 2, 0}, block.Stone{})) + sim := newLiquidSim(newLiquidWorld().set(dfcube.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 +225,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 +240,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 +311,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 +377,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 +389,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 +409,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 +431,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 +446,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 +457,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 +466,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 +476,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 +498,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 +509,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 +537,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 +549,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 +559,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 +578,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 +590,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,25 +604,25 @@ 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 // liquid, preventing the player from swimming out into open air. func TestSwimTravelStopsAtSurface(t *testing.T) { // Liquid only below the player's head-check probes. - w := newLiquidWorld().fill(cube.Pos{-2, -4, -2}, cube.Pos{2, 0, 2}, waterSource) + w := newLiquidWorld().fill(dfcube.Pos{-2, -4, -2}, dfcube.Pos{2, 0, 2}, waterSource) 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 @@ -635,12 +636,12 @@ func TestSwimTravelStopsAtSurface(t *testing.T) { // While the head is still submerged the climb continues normally. func TestSwimTravelContinuesWhileHeadSubmerged(t *testing.T) { - w := newLiquidWorld().fill(cube.Pos{-2, -4, -2}, cube.Pos{2, 0, 2}, waterSource) + w := newLiquidWorld().fill(dfcube.Pos{-2, -4, -2}, dfcube.Pos{2, 0, 2}, waterSource) 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) { @@ -650,14 +651,14 @@ func TestSwimTravelContinuesWhileHeadSubmerged(t *testing.T) { // WantDownSlow suppresses the surface clamp so the player can hover. func TestSwimTravelSurfaceClampSkippedWhenWantDownSlow(t *testing.T) { - w := newLiquidWorld().fill(cube.Pos{-2, -4, -2}, cube.Pos{2, 0, 2}, waterSource) + w := newLiquidWorld().fill(dfcube.Pos{-2, -4, -2}, dfcube.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 +672,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 +700,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 +721,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 +737,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 +751,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 +764,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 +777,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 +792,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 +849,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 +867,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) @@ -881,7 +882,7 @@ func TestZeroMovementSpeedsUseDefaults(t *testing.T) { // Water is detected through a shallow vertical offset, so a player standing on // top of a water block is still considered to be in water. func TestWaterDetectedAtFeet(t *testing.T) { - sim := newLiquidSim(newLiquidWorld().set(cube.Pos{0, 0, 0}, waterSource)) + sim := newLiquidSim(newLiquidWorld().set(dfcube.Pos{0, 0, 0}, waterSource)) state := submergedState() if got := len(sim.touchingLiquidBlocks(state, liquidWater)); got != 1 { @@ -892,11 +893,11 @@ func TestWaterDetectedAtFeet(t *testing.T) { // Lava uses a wider horizontal shrink than water, so a player at the very edge // of a lava block touches water but not lava. func TestLavaUsesWiderHorizontalMargin(t *testing.T) { - w := newLiquidWorld().set(cube.Pos{0, 0, 0}, waterSource).set(cube.Pos{1, 0, 0}, lavaSource) + w := newLiquidWorld().set(dfcube.Pos{0, 0, 0}, waterSource).set(dfcube.Pos{1, 0, 0}, lavaSource) 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) @@ -924,14 +925,14 @@ func TestLiquidTypeFiltering(t *testing.T) { // Water travel takes priority when a player touches both liquids. func TestWaterTakesPriorityOverLava(t *testing.T) { w := newLiquidWorld(). - fill(cube.Pos{-2, 0, -2}, cube.Pos{2, 3, 2}, waterSource). - set(cube.Pos{0, 0, 0}, lavaSource) + fill(dfcube.Pos{-2, 0, -2}, dfcube.Pos{2, 3, 2}, waterSource). + set(dfcube.Pos{0, 0, 0}, lavaSource) sim := newLiquidSim(w) state := submergedState() 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,14 +941,14 @@ 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. func TestLiquidProviderDetectsWaterloggedBlocks(t *testing.T) { w := newLayeredLiquidWorld() for y := range 4 { - w.waterlog(cube.Pos{0, y, 0}, block.Air{}, waterSource) + w.waterlog(dfcube.Pos{0, y, 0}, block.Air{}, waterSource) } sim := newLiquidSim(w) state := submergedState() @@ -956,7 +957,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 +984,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 +1060,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. @@ -1106,11 +1107,11 @@ func TestSwimmingOutsideWaterSuppressesJump(t *testing.T) { // Flowing water pushes the player toward the lower-depth neighbour. func TestLiquidFlowPushesTowardLowerDepth(t *testing.T) { w := newLiquidWorld(). - set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(cube.Pos{1, 0, 0}, block.Water{Depth: 7}). - set(cube.Pos{-1, 0, 0}, block.Water{Depth: 8}). - set(cube.Pos{0, 0, 1}, block.Water{Depth: 8}). - set(cube.Pos{0, 0, -1}, block.Water{Depth: 8}) + set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 7}). + set(dfcube.Pos{-1, 0, 0}, block.Water{Depth: 8}). + set(dfcube.Pos{0, 0, 1}, block.Water{Depth: 8}). + set(dfcube.Pos{0, 0, -1}, block.Water{Depth: 8}) sim := newLiquidSim(w) state := submergedState() @@ -1123,8 +1124,8 @@ func TestLiquidFlowPushesTowardLowerDepth(t *testing.T) { // Water flow strength is 0.014 per tick along the normalized flow vector. func TestWaterFlowStrength(t *testing.T) { w := newLiquidWorld(). - set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(cube.Pos{1, 0, 0}, block.Water{Depth: 7}) + set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 7}) sim := newLiquidSim(w) state := submergedState() @@ -1137,8 +1138,8 @@ func TestWaterFlowStrength(t *testing.T) { // Lava flow is much weaker than water flow. func TestLavaFlowStrength(t *testing.T) { w := newLiquidWorld(). - set(cube.Pos{0, 0, 0}, block.Lava{Depth: 8}). - set(cube.Pos{1, 0, 0}, block.Lava{Depth: 7}) + set(dfcube.Pos{0, 0, 0}, block.Lava{Depth: 8}). + set(dfcube.Pos{1, 0, 0}, block.Lava{Depth: 7}) sim := newLiquidSim(w) state := submergedState() @@ -1154,17 +1155,17 @@ 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. func TestFallingLiquidFlowsDownwardAlongSolids(t *testing.T) { w := newLiquidWorld(). - set(cube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}). - set(cube.Pos{1, 0, 0}, block.Stone{}) + set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}). + set(dfcube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) - flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}) + flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}) if !(flow.Y() < 0) { t.Fatalf("falling liquid flow Y = %v, want negative", flow.Y()) } @@ -1173,11 +1174,11 @@ func TestFallingLiquidFlowsDownwardAlongSolids(t *testing.T) { // Non-falling liquid never gains the downward push. func TestNonFallingLiquidHasNoDownwardFlow(t *testing.T) { w := newLiquidWorld(). - set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(cube.Pos{1, 0, 0}, block.Stone{}) + set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(dfcube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) - flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) + flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) if flow.Y() < 0 { t.Fatalf("non-falling liquid flow Y = %v, want no downward push", flow.Y()) } @@ -1186,12 +1187,12 @@ func TestNonFallingLiquidHasNoDownwardFlow(t *testing.T) { // A solid neighbour blocks flow in that direction rather than contributing. func TestSolidNeighbourBlocksFlow(t *testing.T) { w := newLiquidWorld(). - set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(cube.Pos{1, 0, 0}, block.Stone{}). - set(cube.Pos{1, -1, 0}, block.Water{Depth: 8}) + set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(dfcube.Pos{1, 0, 0}, block.Stone{}). + set(dfcube.Pos{1, -1, 0}, block.Water{Depth: 8}) sim := newLiquidSim(w) - flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) + flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) if !approxEqual(flow.X(), 0) { t.Fatalf("flow X = %v, want 0 through a solid neighbour", flow.X()) } @@ -1200,11 +1201,11 @@ func TestSolidNeighbourBlocksFlow(t *testing.T) { // An open neighbour with liquid below pulls the flow into the drop. func TestFlowFallsIntoOpenDrop(t *testing.T) { w := newLiquidWorld(). - set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(cube.Pos{1, -1, 0}, block.Water{Depth: 8}) + set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(dfcube.Pos{1, -1, 0}, block.Water{Depth: 8}) sim := newLiquidSim(w) - flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) + flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) if !(flow.X() > 0) { t.Fatalf("flow X = %v, want a positive pull into the drop", flow.X()) } @@ -1251,12 +1252,12 @@ func TestFallingLiquidDecayAndHeight(t *testing.T) { // hop out of the liquid. func TestLiquidExitProbeBoostsOverLedge(t *testing.T) { w := newLiquidWorld(). - fill(cube.Pos{-1, 0, -1}, cube.Pos{0, 0, 1}, waterSource). - set(cube.Pos{1, 0, 0}, block.Stone{}) + fill(dfcube.Pos{-1, 0, -1}, dfcube.Pos{0, 0, 1}, waterSource). + set(dfcube.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 { @@ -1276,23 +1277,23 @@ func TestLiquidExitProbeBoostsOverLedge(t *testing.T) { func TestLiquidExitProbeBlockedByCollisionAlone(t *testing.T) { build := func(overhang bool) (*Simulator, *MovementState) { w := newLiquidWorld(). - fill(cube.Pos{-1, 0, -1}, cube.Pos{0, 0, 1}, waterSource). - set(cube.Pos{1, 0, 0}, block.Stone{}) + fill(dfcube.Pos{-1, 0, -1}, dfcube.Pos{0, 0, 1}, waterSource). + set(dfcube.Pos{1, 0, 0}, block.Stone{}) if overhang { - w.set(cube.Pos{0, 1, 0}, block.Stone{}) + w.set(dfcube.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) } @@ -1319,11 +1320,11 @@ func TestLiquidExitProbeBlockedByCollisionAlone(t *testing.T) { // submerged rather than at the surface. func TestLiquidExitProbeBlockedByLiquidAbove(t *testing.T) { w := newLiquidWorld(). - fill(cube.Pos{-1, 0, -1}, cube.Pos{0, 4, 1}, waterSource). - set(cube.Pos{1, 0, 0}, block.Stone{}) + fill(dfcube.Pos{-1, 0, -1}, dfcube.Pos{0, 4, 1}, waterSource). + set(dfcube.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) { @@ -1382,7 +1383,7 @@ func TestClimbUsesEffectiveJumping(t *testing.T) { // 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}) + 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..a0928b2 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" + dfcube "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) dfcube.Pos { + return dfcube.Pos{int(math32.Floor(vec.X())), int(math32.Floor(vec.Y())), int(math32.Floor(vec.Z()))} +} + +func posVec3(pos dfcube.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..0e3279d 100644 --- a/movement.go +++ b/movement.go @@ -1,15 +1,15 @@ package bedsim import ( - "github.com/df-mc/dragonfly/server/block/cube" - "github.com/go-gl/mathgl/mgl64" + dfcube "github.com/df-mc/dragonfly/server/block/cube" + "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 + SupportingBlockPos *dfcube.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/native_float32_test.go b/native_float32_test.go new file mode 100644 index 0000000..176769d --- /dev/null +++ b/native_float32_test.go @@ -0,0 +1,25 @@ +package bedsim + +import ( + "testing" + + dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/ethaniccc/float32-cube/cube" + "github.com/go-gl/mathgl/mgl32" +) + +func TestNativeFloat32Surface(t *testing.T) { + var sin func(float32) float32 = MCSin + state := MovementState{Pos: mgl32.Vec3{1, 2, 3}} + if got := sin(state.Pos.X()); got != MCSin(1) { + t.Fatalf("MCSin(%v) = %v, want %v", state.Pos.X(), got, MCSin(1)) + } +} + +func TestBBoxFromDragonflyRoundsAtProviderBoundary(t *testing.T) { + got := BBoxFromDragonfly(dfcube.Box(0.1, 0.2, 0.3, 0.9, 1.8, 0.7)) + want := cube.Box(float32(0.1), float32(0.2), float32(0.3), float32(0.9), float32(1.8), float32(0.7)) + if got != want { + t.Fatalf("BBoxFromDragonfly() = %v, want %v", got, want) + } +} 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..77c7c49 100644 --- a/simulation.go +++ b/simulation.go @@ -1,13 +1,14 @@ package bedsim import ( + "github.com/chewxy/math32" "iter" - "math" "github.com/df-mc/dragonfly/server/block" - "github.com/df-mc/dragonfly/server/block/cube" + dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" + "github.com/ethaniccc/float32-cube/cube" + "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -67,13 +68,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 +139,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 +203,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 +212,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 +224,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 +281,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 +292,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 +333,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 +370,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 +409,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(dfcube.FaceDown)) if IsWall(below) || IsFence(below) { blockUnder = below } @@ -428,13 +429,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 +511,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 +519,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 +528,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 +540,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 +588,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 +599,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 +610,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 +640,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 +652,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 +685,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 +705,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 +714,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 +736,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 +771,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 +799,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 +816,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 +856,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 +890,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 +903,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 +937,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 +946,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 +963,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 +976,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 +1021,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,19 +1033,19 @@ func (s *Simulator) isInsideCobweb(state *MovementState) bool { return insideCobweb } -func nearbyBlocks(aabb cube.BBox, w WorldProvider) iter.Seq2[cube.Pos, world.Block] { - return func(yield func(cube.Pos, world.Block) bool) { +func nearbyBlocks(aabb cube.BBox, w WorldProvider) iter.Seq2[dfcube.Pos, world.Block] { + return func(yield func(dfcube.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++ { for z := minZ; z <= maxZ; z++ { - pos := cube.Pos{x, y, z} + pos := dfcube.Pos{x, y, z} if !yield(pos, w.Block(pos)) { return } @@ -1054,7 +1055,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,7 +1063,7 @@ 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) } } @@ -1071,9 +1072,9 @@ func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox) { 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}) + var blockPos *dfcube.Pos + 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 +1083,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 @@ -1098,7 +1099,7 @@ func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox) { state.SupportingBlockPos = blockPos } -func (s *Simulator) blockAtPos(pos cube.Pos) world.Block { +func (s *Simulator) blockAtPos(pos dfcube.Pos) world.Block { if s.World == nil { return block.Air{} } 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..3b73772 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -2,24 +2,25 @@ 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" + dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" + "github.com/ethaniccc/float32-cube/cube" + "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) type mockWorld struct{} -func (mockWorld) Block(pos cube.Pos) world.Block { +func (mockWorld) Block(pos dfcube.Pos) world.Block { return block.Air{} } -func (mockWorld) BlockCollisions(pos cube.Pos) []cube.BBox { +func (mockWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { return nil } @@ -36,11 +37,11 @@ type staticWorld struct { boxes []cube.BBox } -func (w staticWorld) Block(pos cube.Pos) world.Block { +func (w staticWorld) Block(pos dfcube.Pos) world.Block { return block.Air{} } -func (w staticWorld) BlockCollisions(pos cube.Pos) []cube.BBox { +func (w staticWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { return nil } @@ -81,7 +82,7 @@ func (m mockInventory) HasElytra() bool { type overrideBlockSemantics struct { name string - friction float64 + friction float32 climbable bool } @@ -89,7 +90,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 +101,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, @@ -143,9 +144,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 +168,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 +189,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 +211,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 +237,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 +259,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 +275,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 +325,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 +357,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 +376,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 +395,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 +416,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 +443,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 { @@ -483,12 +484,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) @@ -512,9 +513,9 @@ func TestStepUpTiebreaker(t *testing.T) { slabBox := cube.Box(1, 0, -1, 2, 0.5, 2) groundBox := cube.Box(-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) { + runSim := func(ignoreStepTiebreaker bool) (mgl32.Vec3, bool) { w := staticWorld{chunkLoaded: true, boxes: []cube.BBox{slabBox, groundBox}} sim := &Simulator{ World: w, @@ -531,9 +532,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, } @@ -581,9 +582,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 +611,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 +620,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 +629,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 +638,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, }, From 9b3c99b0a31556a05e483de1746053bb882dc16f Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 00:38:40 -0400 Subject: [PATCH 2/8] refactor: use dragonfly float32 bounding boxes --- bbox.go | 13 ++++++------- block.go | 1 + collision.go | 8 ++++---- go.mod | 19 ++++++++++--------- go.sum | 42 ++++++++++++++++++++---------------------- interfaces.go | 5 ++--- liquid.go | 9 ++++----- liquid_test.go | 11 +++++------ native_float32_test.go | 3 +-- simulation.go | 11 +++++------ simulator_test.go | 27 +++++++++++++-------------- 11 files changed, 71 insertions(+), 78 deletions(-) diff --git a/bbox.go b/bbox.go index 73f989b..fc53c66 100644 --- a/bbox.go +++ b/bbox.go @@ -2,14 +2,13 @@ package bedsim import ( dfcube "github.com/df-mc/dragonfly/server/block/cube" - "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" ) // BBoxFromDragonfly returns a simulation bounding box rounded to float32 coordinates. -func BBoxFromDragonfly(box dfcube.BBox) cube.BBox { +func BBoxFromDragonfly(box dfcube.BBox) dfcube.BBox32 { min, max := box.Min(), box.Max() - return cube.Box( + return dfcube.Box32( float32(min.X()), float32(min.Y()), float32(min.Z()), float32(max.X()), float32(max.Y()), float32(max.Z()), ) @@ -22,7 +21,7 @@ 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) dfcube.BBox32 { scale := s.Size[2] width := (s.Size[0] * 0.5) * scale height := s.Size[1] * scale @@ -34,7 +33,7 @@ func (s *MovementState) BoundingBox(useSlideOffset bool) cube.BBox { yOffset = s.SlideOffset.Y() } - return cube.Box( + return dfcube.Box32( s.Pos[0]-width, s.Pos[1]+yOffset, s.Pos[2]-width, @@ -45,7 +44,7 @@ func (s *MovementState) BoundingBox(useSlideOffset bool) cube.BBox { } // 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) dfcube.BBox32 { scale := s.Size[2] width := (s.Size[0] * 0.5) * scale height := s.Size[1] * scale @@ -57,7 +56,7 @@ func (s *MovementState) ClientBoundingBox(useSlideOffset bool) cube.BBox { yOffset = s.SlideOffset.Y() } - return cube.Box( + return dfcube.Box32( s.Client.Pos[0]-width, s.Client.Pos[1]+yOffset, s.Client.Pos[2]-width, diff --git a/block.go b/block.go index 0aeb5cf..f35b5cc 100644 --- a/block.go +++ b/block.go @@ -15,6 +15,7 @@ var ( ) func initBlockNameMapping() { + world.DefaultBlockRegistry.Finalize() blockNameMapping = make(map[uint64]string, len(world.Blocks())) for _, b := range world.Blocks() { x, y := b.Hash() diff --git a/collision.go b/collision.go index e09e979..5645ce2 100644 --- a/collision.go +++ b/collision.go @@ -3,7 +3,7 @@ package bedsim import ( "github.com/chewxy/math32" - "github.com/ethaniccc/float32-cube/cube" + dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/go-gl/mathgl/mgl32" ) @@ -15,7 +15,7 @@ type clipCollideResult struct { } // BBClipCollide clips or depenetrates a moving bounding box against a stationary one. -func BBClipCollide(this, c cube.BBox, vel mgl32.Vec3, oneWay bool, penetration *mgl32.Vec3) mgl32.Vec3 { +func BBClipCollide(this, c dfcube.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 mgl32.Vec3, oneWay bool, penetration * return result.depenetratingVelocity } -func doBBClipCollide(stationary, moving cube.BBox, velocity mgl32.Vec3) (result clipCollideResult) { +func doBBClipCollide(stationary, moving dfcube.BBox32, velocity mgl32.Vec3) (result clipCollideResult) { result.clippedVelocity = velocity result.depenetratingVelocity = velocity @@ -115,6 +115,6 @@ func doBBClipCollide(stationary, moving cube.BBox, velocity mgl32.Vec3) (result } // BBHasZeroVolume returns true if the bounding box has zero volume. -func BBHasZeroVolume(bb cube.BBox) bool { +func BBHasZeroVolume(bb dfcube.BBox32) bool { return bb.Min() == bb.Max() } diff --git a/go.mod b/go.mod index df14850..3730844 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +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/ethaniccc/float32-cube v0.0.0-20250511224129-7af1f8c4ee12 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 3090985..3ede9ef 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +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/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/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/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/ethaniccc/float32-cube v0.0.0-20250511224129-7af1f8c4ee12 h1:o8NDdPPBeF7y//XYIRvzrXPB08/Lblt/ceu1+3vS1hM= -github.com/ethaniccc/float32-cube v0.0.0-20250511224129-7af1f8c4ee12/go.mod h1:xBh0GYHZ5yHg3YvvUGriGLSAlm7YW2S9SRULstdLZLk= +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= @@ -19,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/interfaces.go b/interfaces.go index e19c9b8..25aca3d 100644 --- a/interfaces.go +++ b/interfaces.go @@ -3,14 +3,13 @@ package bedsim import ( dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/ethaniccc/float32-cube/cube" ) // WorldProvider bridges the world/chunk system for collision and block lookups. type WorldProvider interface { Block(pos dfcube.Pos) world.Block - BlockCollisions(pos dfcube.Pos) []cube.BBox - GetNearbyBBoxes(aabb cube.BBox) []cube.BBox + BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 + GetNearbyBBoxes(aabb dfcube.BBox32) []dfcube.BBox32 IsChunkLoaded(chunkX, chunkZ int32) bool } diff --git a/liquid.go b/liquid.go index e8ccd99..6290b0b 100644 --- a/liquid.go +++ b/liquid.go @@ -6,7 +6,6 @@ import ( "github.com/df-mc/dragonfly/server/block" dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -218,7 +217,7 @@ func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) return positions } -func shrinkLiquidBox(box cube.BBox, offset mgl32.Vec3) cube.BBox { +func shrinkLiquidBox(box dfcube.BBox32, offset mgl32.Vec3) dfcube.BBox32 { min, max := box.Min().Add(offset), box.Max().Sub(offset) originalMin, originalMax := box.Min(), box.Max() for axis := range 3 { @@ -227,7 +226,7 @@ func shrinkLiquidBox(box cube.BBox, offset mgl32.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 dfcube.Box32(min.X(), min.Y(), min.Z(), max.X(), max.Y(), max.Z()) } func (s *Simulator) liquidMovementBlock(pos dfcube.Pos) world.Block { @@ -239,7 +238,7 @@ func (s *Simulator) liquidMovementBlock(pos dfcube.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 dfcube.Pos) []cube.BBox { +func (s *Simulator) blockCollisions(pos dfcube.Pos) []dfcube.BBox32 { if s.World == nil { return nil } @@ -285,7 +284,7 @@ func liquidHeight(liquid world.Liquid) float32 { return float32(liquid.LiquidDepth()+1) / 9 } -func (s *Simulator) containsAnyLiquid(box cube.BBox) bool { +func (s *Simulator) containsAnyLiquid(box dfcube.BBox32) bool { min, max := box.Min(), box.Max() 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())) diff --git a/liquid_test.go b/liquid_test.go index facf3fc..109e52c 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -7,7 +7,6 @@ import ( "github.com/df-mc/dragonfly/server/block" dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -55,7 +54,7 @@ func (w *liquidWorld) Block(pos dfcube.Pos) world.Block { return block.Air{} } -func (w *liquidWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { +func (w *liquidWorld) BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 { b := w.Block(pos) if _, air := b.(block.Air); air { return nil @@ -63,12 +62,12 @@ func (w *liquidWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { if _, liquid := b.(world.Liquid); liquid { return nil } - return []cube.BBox{cube.Box(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))} + return []dfcube.BBox32{dfcube.Box32(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))} } -func (w *liquidWorld) GetNearbyBBoxes(aabb cube.BBox) []cube.BBox { +func (w *liquidWorld) GetNearbyBBoxes(aabb dfcube.BBox32) []dfcube.BBox32 { min, max := aabb.Min(), aabb.Max() - var out []cube.BBox + var out []dfcube.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++ { @@ -1382,7 +1381,7 @@ 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) + box := dfcube.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) { diff --git a/native_float32_test.go b/native_float32_test.go index 176769d..b7e8833 100644 --- a/native_float32_test.go +++ b/native_float32_test.go @@ -4,7 +4,6 @@ import ( "testing" dfcube "github.com/df-mc/dragonfly/server/block/cube" - "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" ) @@ -18,7 +17,7 @@ func TestNativeFloat32Surface(t *testing.T) { func TestBBoxFromDragonflyRoundsAtProviderBoundary(t *testing.T) { got := BBoxFromDragonfly(dfcube.Box(0.1, 0.2, 0.3, 0.9, 1.8, 0.7)) - want := cube.Box(float32(0.1), float32(0.2), float32(0.3), float32(0.9), float32(1.8), float32(0.7)) + want := dfcube.Box32(float32(0.1), float32(0.2), float32(0.3), float32(0.9), float32(1.8), float32(0.7)) if got != want { t.Fatalf("BBoxFromDragonfly() = %v, want %v", got, want) } diff --git a/simulation.go b/simulation.go index 77c7c49..28e8ef1 100644 --- a/simulation.go +++ b/simulation.go @@ -7,7 +7,6 @@ import ( "github.com/df-mc/dragonfly/server/block" dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -1033,7 +1032,7 @@ func (s *Simulator) isInsideCobweb(state *MovementState) bool { return insideCobweb } -func nearbyBlocks(aabb cube.BBox, w WorldProvider) iter.Seq2[dfcube.Pos, world.Block] { +func nearbyBlocks(aabb dfcube.BBox32, w WorldProvider) iter.Seq2[dfcube.Pos, world.Block] { return func(yield func(dfcube.Pos, world.Block) bool) { if w == nil { return @@ -1060,7 +1059,7 @@ func checkSupportingBlockPos(state *MovementState, w WorldProvider, useSlideOffs state.SupportingBlockPos = nil return } - decBB := state.BoundingBox(useSlideOffset).ExtendTowards(cube.FaceDown, 1e-3) + decBB := state.BoundingBox(useSlideOffset).ExtendTowards(dfcube.FaceDown, 1e-3) findSupportingBlock(state, w, decBB) if state.SupportingBlockPos == nil { decBB = decBB.Translate(mgl32.Vec3{-vel[0], 0, -vel[2]}) @@ -1068,7 +1067,7 @@ func checkSupportingBlockPos(state *MovementState, w WorldProvider, useSlideOffs } } -func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox) { +func findSupportingBlock(state *MovementState, w WorldProvider, bb dfcube.BBox32) { if w == nil { return } @@ -1107,10 +1106,10 @@ func (s *Simulator) blockAtPos(pos dfcube.Pos) world.Block { } type nearbyBBoxProbe interface { - HasNearbyBBoxes(aabb cube.BBox) bool + HasNearbyBBoxes(aabb dfcube.BBox32) bool } -func hasNearbyBBoxes(w WorldProvider, aabb cube.BBox) bool { +func hasNearbyBBoxes(w WorldProvider, aabb dfcube.BBox32) bool { if w == nil { return false } diff --git a/simulator_test.go b/simulator_test.go index 3b73772..155a9c2 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -9,7 +9,6 @@ import ( "github.com/df-mc/dragonfly/server/block" dfcube "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - "github.com/ethaniccc/float32-cube/cube" "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -20,11 +19,11 @@ func (mockWorld) Block(pos dfcube.Pos) world.Block { return block.Air{} } -func (mockWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { +func (mockWorld) BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 { return nil } -func (mockWorld) GetNearbyBBoxes(aabb cube.BBox) []cube.BBox { +func (mockWorld) GetNearbyBBoxes(aabb dfcube.BBox32) []dfcube.BBox32 { return nil } @@ -34,23 +33,23 @@ func (mockWorld) IsChunkLoaded(chunkX, chunkZ int32) bool { type staticWorld struct { chunkLoaded bool - boxes []cube.BBox + boxes []dfcube.BBox32 } func (w staticWorld) Block(pos dfcube.Pos) world.Block { return block.Air{} } -func (w staticWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { +func (w staticWorld) BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 { return nil } -func (w staticWorld) GetNearbyBBoxes(aabb cube.BBox) []cube.BBox { +func (w staticWorld) GetNearbyBBoxes(aabb dfcube.BBox32) []dfcube.BBox32 { if len(w.boxes) == 0 { return nil } - out := make([]cube.BBox, 0, len(w.boxes)) + out := make([]dfcube.BBox32, 0, len(w.boxes)) for _, bb := range w.boxes { if aabb.IntersectsWith(bb) { out = append(out, bb) @@ -471,8 +470,8 @@ func TestSimulateStateDebugTraceJumpBlocked(t *testing.T) { sim := &Simulator{ World: staticWorld{ chunkLoaded: true, - boxes: []cube.BBox{ - cube.Box(0, 2, 1, 1, 3, 2), + boxes: []dfcube.BBox32{ + dfcube.Box32(0, 2, 1, 1, 3, 2), }, }, Effects: mockEffects{}, @@ -510,13 +509,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 := dfcube.Box32(1, 0, -1, 2, 0.5, 2) + groundBox := dfcube.Box32(-1, -1, -1, 1, 0, 2) startPos := mgl32.Vec3{0.5, 0, 0.5} runSim := func(ignoreStepTiebreaker bool) (mgl32.Vec3, bool) { - w := staticWorld{chunkLoaded: true, boxes: []cube.BBox{slabBox, groundBox}} + w := staticWorld{chunkLoaded: true, boxes: []dfcube.BBox32{slabBox, groundBox}} sim := &Simulator{ World: w, Effects: mockEffects{}, @@ -565,8 +564,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 := dfcube.Box32(1, 1.3, -1, 2, 2.3, 2) // leaves only 0.8 gap, player is 1.8 tall + w := staticWorld{chunkLoaded: true, boxes: []dfcube.BBox32{slabBox, groundBox, ceilingBox}} sim := &Simulator{ World: w, Effects: mockEffects{}, From f5ffaacab256c5e58a50aa39e12736a8bcf315be Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 01:00:26 -0400 Subject: [PATCH 3/8] test: cover block-local cobweb collisions --- interfaces.go | 1 + simulator_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/interfaces.go b/interfaces.go index 25aca3d..918a508 100644 --- a/interfaces.go +++ b/interfaces.go @@ -8,6 +8,7 @@ import ( // WorldProvider bridges the world/chunk system for collision and block lookups. type WorldProvider interface { Block(pos dfcube.Pos) world.Block + // BlockCollisions returns block-local collision boxes at pos. BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 GetNearbyBBoxes(aabb dfcube.BBox32) []dfcube.BBox32 IsChunkLoaded(chunkX, chunkZ int32) bool diff --git a/simulator_test.go b/simulator_test.go index 155a9c2..f99f44a 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -36,6 +36,32 @@ type staticWorld struct { boxes []dfcube.BBox32 } +type cobwebWorld struct { + pos dfcube.Pos +} + +func (w cobwebWorld) Block(pos dfcube.Pos) world.Block { + if pos == w.pos { + return block.Cobweb{} + } + return block.Air{} +} + +func (w cobwebWorld) BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 { + if pos != w.pos { + return nil + } + return []dfcube.BBox32{dfcube.Box32(0, 0, 0, 1, 1, 1)} +} + +func (cobwebWorld) GetNearbyBBoxes(dfcube.BBox32) []dfcube.BBox32 { + return nil +} + +func (cobwebWorld) IsChunkLoaded(int32, int32) bool { + return true +} + func (w staticWorld) Block(pos dfcube.Pos) world.Block { return block.Air{} } @@ -121,6 +147,17 @@ func newBaseState() *MovementState { } } +func TestInsideCobwebTranslatesBlockLocalCollisionBoxes(t *testing.T) { + pos := dfcube.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) { From f185c7af53750dc76ae95f8d0a6d0ac9444199a2 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 01:07:10 -0400 Subject: [PATCH 4/8] refactor: simplify dragonfly cube imports --- bbox.go | 14 ++--- block.go | 4 +- collision.go | 8 +-- interfaces.go | 10 ++-- liquid.go | 42 +++++++-------- liquid_hardening_test.go | 74 +++++++++++++------------- liquid_test.go | 112 +++++++++++++++++++-------------------- math.go | 8 +-- movement.go | 4 +- native_float32_test.go | 6 +-- simulation.go | 22 ++++---- simulator_test.go | 44 +++++++-------- 12 files changed, 174 insertions(+), 174 deletions(-) diff --git a/bbox.go b/bbox.go index fc53c66..ccf109d 100644 --- a/bbox.go +++ b/bbox.go @@ -1,14 +1,14 @@ package bedsim import ( - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/go-gl/mathgl/mgl32" ) // BBoxFromDragonfly returns a simulation bounding box rounded to float32 coordinates. -func BBoxFromDragonfly(box dfcube.BBox) dfcube.BBox32 { +func BBoxFromDragonfly(box cube.BBox) cube.BBox32 { min, max := box.Min(), box.Max() - return dfcube.Box32( + return cube.Box32( float32(min.X()), float32(min.Y()), float32(min.Z()), float32(max.X()), float32(max.Y()), float32(max.Z()), ) @@ -21,7 +21,7 @@ func (s *MovementState) SwimPose() bool { } // BoundingBox returns the entity bounding box translated to the current position. -func (s *MovementState) BoundingBox(useSlideOffset bool) dfcube.BBox32 { +func (s *MovementState) BoundingBox(useSlideOffset bool) cube.BBox32 { scale := s.Size[2] width := (s.Size[0] * 0.5) * scale height := s.Size[1] * scale @@ -33,7 +33,7 @@ func (s *MovementState) BoundingBox(useSlideOffset bool) dfcube.BBox32 { yOffset = s.SlideOffset.Y() } - return dfcube.Box32( + return cube.Box32( s.Pos[0]-width, s.Pos[1]+yOffset, s.Pos[2]-width, @@ -44,7 +44,7 @@ func (s *MovementState) BoundingBox(useSlideOffset bool) dfcube.BBox32 { } // ClientBoundingBox returns the bounding box translated to the client's position. -func (s *MovementState) ClientBoundingBox(useSlideOffset bool) dfcube.BBox32 { +func (s *MovementState) ClientBoundingBox(useSlideOffset bool) cube.BBox32 { scale := s.Size[2] width := (s.Size[0] * 0.5) * scale height := s.Size[1] * scale @@ -56,7 +56,7 @@ func (s *MovementState) ClientBoundingBox(useSlideOffset bool) dfcube.BBox32 { yOffset = s.SlideOffset.Y() } - return dfcube.Box32( + return cube.Box32( s.Client.Pos[0]-width, s.Client.Pos[1]+yOffset, s.Client.Pos[2]-width, diff --git a/block.go b/block.go index f35b5cc..e1a8a70 100644 --- a/block.go +++ b/block.go @@ -5,7 +5,7 @@ import ( "sync" "github.com/df-mc/dragonfly/server/block" - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" ) @@ -74,7 +74,7 @@ func BlockClimbable(b world.Block) bool { // BlockSupportHeight returns the effective standing surface height for a ground // block by sampling its collision boxes at the block centre (0.5, 0.5). // This handles slabs, stairs, and any other sub-block geometry correctly. -func BlockSupportHeight(b world.Block, pos dfcube.Pos, src world.BlockSource) float32 { +func BlockSupportHeight(b world.Block, pos cube.Pos, src world.BlockSource) float32 { boxes := b.Model().BBox(pos, src) maxY := float32(-1) for _, box := range boxes { diff --git a/collision.go b/collision.go index 5645ce2..3765af8 100644 --- a/collision.go +++ b/collision.go @@ -3,7 +3,7 @@ package bedsim import ( "github.com/chewxy/math32" - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/go-gl/mathgl/mgl32" ) @@ -15,7 +15,7 @@ type clipCollideResult struct { } // BBClipCollide clips or depenetrates a moving bounding box against a stationary one. -func BBClipCollide(this, c dfcube.BBox32, vel mgl32.Vec3, oneWay bool, penetration *mgl32.Vec3) mgl32.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 dfcube.BBox32, vel mgl32.Vec3, oneWay bool, penetrati return result.depenetratingVelocity } -func doBBClipCollide(stationary, moving dfcube.BBox32, velocity mgl32.Vec3) (result clipCollideResult) { +func doBBClipCollide(stationary, moving cube.BBox32, velocity mgl32.Vec3) (result clipCollideResult) { result.clippedVelocity = velocity result.depenetratingVelocity = velocity @@ -115,6 +115,6 @@ func doBBClipCollide(stationary, moving dfcube.BBox32, velocity mgl32.Vec3) (res } // BBHasZeroVolume returns true if the bounding box has zero volume. -func BBHasZeroVolume(bb dfcube.BBox32) bool { +func BBHasZeroVolume(bb cube.BBox32) bool { return bb.Min() == bb.Max() } diff --git a/interfaces.go b/interfaces.go index 918a508..252183f 100644 --- a/interfaces.go +++ b/interfaces.go @@ -1,22 +1,22 @@ package bedsim import ( - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" ) // WorldProvider bridges the world/chunk system for collision and block lookups. type WorldProvider interface { - Block(pos dfcube.Pos) world.Block + Block(pos cube.Pos) world.Block // BlockCollisions returns block-local collision boxes at pos. - BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 - GetNearbyBBoxes(aabb dfcube.BBox32) []dfcube.BBox32 + BlockCollisions(pos cube.Pos) []cube.BBox32 + GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 IsChunkLoaded(chunkX, chunkZ int32) bool } // LiquidProvider returns liquids from either block layer at a position. type LiquidProvider interface { - Liquid(pos dfcube.Pos) (world.Liquid, bool) + Liquid(pos cube.Pos) (world.Liquid, bool) } // BlockSemanticsProvider resolves movement-relevant block behavior. Implement diff --git a/liquid.go b/liquid.go index 6290b0b..27e5660 100644 --- a/liquid.go +++ b/liquid.go @@ -4,7 +4,7 @@ import ( "github.com/chewxy/math32" "github.com/df-mc/dragonfly/server/block" - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" @@ -35,13 +35,13 @@ func (k liquidKind) matches(liquid world.Liquid) bool { } var liquidFaces = [...]struct { - delta dfcube.Pos + delta cube.Pos vec mgl32.Vec3 }{ - {dfcube.Pos{-1, 0, 0}, mgl32.Vec3{-1, 0, 0}}, - {dfcube.Pos{1, 0, 0}, mgl32.Vec3{1, 0, 0}}, - {dfcube.Pos{0, 0, -1}, mgl32.Vec3{0, 0, -1}}, - {dfcube.Pos{0, 0, 1}, mgl32.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) { @@ -181,7 +181,7 @@ func (s *Simulator) updateSwimTravel(state *MovementState) { state.SetVel(vel) } -func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) []dfcube.Pos { +func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) []cube.Pos { 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 { @@ -192,11 +192,11 @@ func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) min, max := box.Min(), box.Max() 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([]dfcube.Pos, 0, 4) + positions := make([]cube.Pos, 0, 4) for x := minX; x < maxX; x++ { for y := minY; y < maxY; y++ { for z := minZ; z < maxZ; z++ { - pos := dfcube.Pos{x, y, z} + pos := cube.Pos{x, y, z} liquid, ok := s.liquidAt(pos) if !ok || !kind.matches(liquid) { continue @@ -217,7 +217,7 @@ func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) return positions } -func shrinkLiquidBox(box dfcube.BBox32, offset mgl32.Vec3) dfcube.BBox32 { +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,10 +226,10 @@ func shrinkLiquidBox(box dfcube.BBox32, offset mgl32.Vec3) dfcube.BBox32 { min[axis], max[axis] = mid, mid } } - return dfcube.Box32(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 dfcube.Pos) world.Block { +func (s *Simulator) liquidMovementBlock(pos cube.Pos) world.Block { if liquid, ok := s.liquidAt(pos); ok { return liquid } @@ -238,7 +238,7 @@ func (s *Simulator) liquidMovementBlock(pos dfcube.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 dfcube.Pos) []dfcube.BBox32 { +func (s *Simulator) blockCollisions(pos cube.Pos) []cube.BBox32 { if s.World == nil { return nil } @@ -267,7 +267,7 @@ func (s *Simulator) HasLiquidLayer() bool { return ok } -func (s *Simulator) liquidAt(pos dfcube.Pos) (world.Liquid, bool) { +func (s *Simulator) liquidAt(pos cube.Pos) (world.Liquid, bool) { if provider, ok := s.liquidLayer(); ok { if liquid, found := provider.Liquid(pos); found { return liquid, true @@ -284,14 +284,14 @@ func liquidHeight(liquid world.Liquid) float32 { return float32(liquid.LiquidDepth()+1) / 9 } -func (s *Simulator) containsAnyLiquid(box dfcube.BBox32) bool { +func (s *Simulator) containsAnyLiquid(box cube.BBox32) bool { min, max := box.Min(), box.Max() 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++ { - if _, ok := s.liquidAt(dfcube.Pos{x, y, z}); ok { + if _, ok := s.liquidAt(cube.Pos{x, y, z}); ok { return true } } @@ -300,7 +300,7 @@ func (s *Simulator) containsAnyLiquid(box dfcube.BBox32) bool { return false } -func (s *Simulator) applyLiquidFlow(state *MovementState, positions []dfcube.Pos, kind liquidKind) { +func (s *Simulator) applyLiquidFlow(state *MovementState, positions []cube.Pos, kind liquidKind) { flow := mgl32.Vec3{} for _, pos := range positions { liquid, ok := s.liquidAt(pos) @@ -319,7 +319,7 @@ func (s *Simulator) applyLiquidFlow(state *MovementState, positions []dfcube.Pos } } -func (s *Simulator) liquidFlow(pos dfcube.Pos, liquid world.Liquid) mgl32.Vec3 { +func (s *Simulator) liquidFlow(pos cube.Pos, liquid world.Liquid) mgl32.Vec3 { currentDecay := liquidDecay(liquid) flow := mgl32.Vec3{} for _, face := range liquidFaces { @@ -335,7 +335,7 @@ func (s *Simulator) liquidFlow(pos dfcube.Pos, liquid world.Liquid) mgl32.Vec3 { if len(s.blockCollisions(neighbourPos)) != 0 { continue } - below := neighbourPos.Side(dfcube.FaceDown) + below := neighbourPos.Side(cube.FaceDown) if lower, ok := s.liquidAt(below); ok && lower.LiquidType() == liquid.LiquidType() { flow = flow.Add(face.vec.Mul(float32(liquidDecay(lower) - currentDecay + 8))) } @@ -343,7 +343,7 @@ func (s *Simulator) liquidFlow(pos dfcube.Pos, liquid world.Liquid) mgl32.Vec3 { if liquid.LiquidFalling() { for _, face := range liquidFaces { neighbourPos := pos.Add(face.delta) - aboveNeighbour := neighbourPos.Side(dfcube.FaceUp) + aboveNeighbour := neighbourPos.Side(cube.FaceUp) if len(s.blockCollisions(neighbourPos)) != 0 || len(s.blockCollisions(aboveNeighbour)) != 0 { if length := flow.Len(); length > 1e-4 { flow = flow.Mul(1 / length) @@ -359,7 +359,7 @@ func (s *Simulator) liquidFlow(pos dfcube.Pos, liquid world.Liquid) mgl32.Vec3 { return mgl32.Vec3{} } -func (s *Simulator) liquidFlowSideClosed(pos, side dfcube.Pos) bool { +func (s *Simulator) liquidFlowSideClosed(pos, side cube.Pos) bool { stairs, ok := s.blockAtPos(pos).(block.Stairs) return ok && stairs.Model().FaceSolid(pos, pos.Face(side), s.World) } diff --git a/liquid_hardening_test.go b/liquid_hardening_test.go index d80b233..caf3699 100644 --- a/liquid_hardening_test.go +++ b/liquid_hardening_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/df-mc/dragonfly/server/block" - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" ) @@ -278,10 +278,10 @@ func TestSwimSpeedMultiplierDepthStriderScaling(t *testing.T) { } type explicitLiquids struct { - layer map[dfcube.Pos]world.Liquid + layer map[cube.Pos]world.Liquid } -func (p explicitLiquids) Liquid(pos dfcube.Pos) (world.Liquid, bool) { +func (p explicitLiquids) Liquid(pos cube.Pos) (world.Liquid, bool) { liquid, ok := p.layer[pos] return liquid, ok } @@ -292,7 +292,7 @@ func TestHasLiquidLayerReportsExplicitProvider(t *testing.T) { t.Fatal("a plain world must not report liquid layer support") } - sim.Liquids = explicitLiquids{layer: map[dfcube.Pos]world.Liquid{}} + sim.Liquids = explicitLiquids{layer: map[cube.Pos]world.Liquid{}} if !sim.HasLiquidLayer() { t.Fatal("an explicit Liquids provider must report support") } @@ -310,10 +310,10 @@ func TestHasLiquidLayerAcceptsWorldProvider(t *testing.T) { // The explicit field wins over the world assertion when both are present. func TestExplicitLiquidsFieldTakesPrecedence(t *testing.T) { w := newLayeredLiquidWorld() - w.waterlog(dfcube.Pos{0, 0, 0}, block.Air{}, waterSource) + w.waterlog(cube.Pos{0, 0, 0}, block.Air{}, waterSource) sim := newLiquidSim(w) - sim.Liquids = explicitLiquids{layer: map[dfcube.Pos]world.Liquid{}} + sim.Liquids = explicitLiquids{layer: map[cube.Pos]world.Liquid{}} state := submergedState() if got := len(sim.touchingLiquidBlocks(state, liquidWater)); got != 0 { @@ -323,9 +323,9 @@ func TestExplicitLiquidsFieldTakesPrecedence(t *testing.T) { // Liquids supplied through the explicit field are detected normally. func TestExplicitLiquidsProviderDetectsWaterlogged(t *testing.T) { - layer := map[dfcube.Pos]world.Liquid{} + layer := map[cube.Pos]world.Liquid{} for y := range 4 { - layer[dfcube.Pos{0, y, 0}] = waterSource + layer[cube.Pos{0, y, 0}] = waterSource } sim := newLiquidSim(newLiquidWorld()) sim.Liquids = explicitLiquids{layer: layer} @@ -455,14 +455,14 @@ func TestLiquidGateExcludesFlying(t *testing.T) { // weight observable through the normalized result. func TestFlowDropWeightIsEight(t *testing.T) { w := newLiquidWorld(). - set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(dfcube.Pos{-1, 0, 0}, block.Water{Depth: 7}). - set(dfcube.Pos{0, 0, 1}, block.Water{Depth: 4}). - set(dfcube.Pos{0, 0, -1}, block.Water{Depth: 8}). - set(dfcube.Pos{1, -1, 0}, block.Water{Depth: 8}) + set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(cube.Pos{-1, 0, 0}, block.Water{Depth: 7}). + set(cube.Pos{0, 0, 1}, block.Water{Depth: 4}). + set(cube.Pos{0, 0, -1}, block.Water{Depth: 8}). + set(cube.Pos{1, -1, 0}, block.Water{Depth: 8}) sim := newLiquidSim(w) - flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) + flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) // +X: open with liquid below -> (0 - 0 + 8) = +8 // -X: same-type neighbour -> (1 - 0) = -1 // +Z: same-type neighbour -> (4 - 0) = +4 @@ -474,12 +474,12 @@ func TestFlowDropWeightIsEight(t *testing.T) { // 6 against the unit-normalized horizontal flow. func TestFallingFlowDownwardWeightIsSix(t *testing.T) { w := newLiquidWorld(). - set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}). - set(dfcube.Pos{-1, 0, 0}, block.Water{Depth: 4}). - set(dfcube.Pos{1, 0, 0}, block.Stone{}) + set(cube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}). + set(cube.Pos{-1, 0, 0}, block.Water{Depth: 4}). + set(cube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) - flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}) + 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 := mgl32.Vec3{-1, -6, 0}.Normalize() assertVec(t, flow, want) @@ -488,21 +488,21 @@ func TestFallingFlowDownwardWeightIsSix(t *testing.T) { // A waterlogged stairs block whose solid face points at the neighbour blocks // flow through that face. func TestStairsSolidFaceBlocksFlow(t *testing.T) { - build := func(facing dfcube.Direction) mgl32.Vec3 { + build := func(facing cube.Direction) mgl32.Vec3 { w := newLayeredLiquidWorld() - w.waterlog(dfcube.Pos{0, 0, 0}, block.Stairs{Facing: facing}, block.Water{Depth: 8}) - w.set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 4}) + 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}) sim := newLiquidSim(w) - return sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) + return sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) } // Facing east: the stairs' full side faces the +X neighbour and closes it. - if flow := build(dfcube.East); !approxEqual(flow.X(), 0) { + if flow := build(cube.East); !approxEqual(flow.X(), 0) { t.Fatalf("east-facing stairs: flow X = %v, want 0", flow.X()) } // Facing west: the +X side is open, so flow proceeds toward the shallower // neighbour. - if flow := build(dfcube.West); !(flow.X() > 0) { + if flow := build(cube.West); !(flow.X() > 0) { t.Fatalf("west-facing stairs: flow X = %v, want positive", flow.X()) } } @@ -522,7 +522,7 @@ func TestNilWorldIsSafe(t *testing.T) { if sim.containsAnyLiquid(state.BoundingBox(false)) { t.Fatal("no world must contain no liquid") } - if flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}); flow.Len() != 0 { + if flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}); flow.Len() != 0 { t.Fatalf("flow = %v, want zero with no world", flow) } if sim.HasLiquidLayer() { @@ -535,8 +535,8 @@ func TestNilWorldIsSafe(t *testing.T) { func TestSwimHitboxChangesCeilingCollision(t *testing.T) { newCeilingSim := func() (*Simulator, *MovementState) { w := newLiquidWorld(). - fill(dfcube.Pos{-1, 0, -1}, dfcube.Pos{1, 1, 1}, waterSource). - set(dfcube.Pos{0, 2, 0}, block.Stone{}) + fill(cube.Pos{-1, 0, -1}, cube.Pos{1, 1, 1}, waterSource). + set(cube.Pos{0, 2, 0}, block.Stone{}) state := submergedState() // Starts clear of the ceiling in both poses; only the standing hitbox // reaches it after the upward move. @@ -570,11 +570,11 @@ func TestSwimHitboxChangesCeilingCollision(t *testing.T) { func TestLiquidSimulationIsRepeatable(t *testing.T) { run := func() (mgl32.Vec3, mgl32.Vec3) { w := newLiquidWorld(). - fill(dfcube.Pos{-8, 0, -8}, dfcube.Pos{8, 8, 8}, block.Water{Depth: 8}). - set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 6}). - set(dfcube.Pos{0, 0, 1}, block.Water{Depth: 4}). - set(dfcube.Pos{-1, 1, 0}, block.Water{Depth: 8, Falling: true}). - set(dfcube.Pos{2, 0, 2}, block.Stone{}) + 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}). + set(cube.Pos{0, 0, 1}, block.Water{Depth: 4}). + set(cube.Pos{-1, 1, 0}, block.Water{Depth: 8, Falling: true}). + set(cube.Pos{2, 0, 2}, block.Stone{}) sim := newLiquidSim(w) sim.Inventory = depthStriderInventory{level: 2} state := submergedState() @@ -609,11 +609,11 @@ func TestLiquidGoldenScenario(t *testing.T) { // Deep enough that the player stays submerged for the whole run, so the // golden measures liquid physics rather than a surface transition. w := newLiquidWorld(). - fill(dfcube.Pos{-8, 0, -8}, dfcube.Pos{8, 8, 8}, block.Water{Depth: 8}). - set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 6}). - set(dfcube.Pos{0, 0, 1}, block.Water{Depth: 4}). - set(dfcube.Pos{-1, 1, 0}, block.Water{Depth: 8, Falling: true}). - set(dfcube.Pos{2, 0, 2}, block.Stone{}) + 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}). + set(cube.Pos{0, 0, 1}, block.Water{Depth: 4}). + set(cube.Pos{-1, 1, 0}, block.Water{Depth: 8, Falling: true}). + set(cube.Pos{2, 0, 2}, block.Stone{}) sim := newLiquidSim(w) sim.Inventory = depthStriderInventory{level: 2} diff --git a/liquid_test.go b/liquid_test.go index 109e52c..fb437db 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/df-mc/dragonfly/server/block" - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" @@ -22,39 +22,39 @@ var ( // deliberately does not implement LiquidProvider, so simulations against it // exercise the fallback path through WorldProvider.Block. type liquidWorld struct { - blocks map[dfcube.Pos]world.Block + blocks map[cube.Pos]world.Block chunkLoaded bool } func newLiquidWorld() *liquidWorld { - return &liquidWorld{blocks: map[dfcube.Pos]world.Block{}, chunkLoaded: true} + return &liquidWorld{blocks: map[cube.Pos]world.Block{}, chunkLoaded: true} } -func (w *liquidWorld) set(pos dfcube.Pos, b world.Block) *liquidWorld { +func (w *liquidWorld) set(pos cube.Pos, b world.Block) *liquidWorld { w.blocks[pos] = b return w } // fill places b in the inclusive cuboid between min and max. -func (w *liquidWorld) fill(min, max dfcube.Pos, b world.Block) *liquidWorld { +func (w *liquidWorld) fill(min, max cube.Pos, b world.Block) *liquidWorld { for x := min[0]; x <= max[0]; x++ { for y := min[1]; y <= max[1]; y++ { for z := min[2]; z <= max[2]; z++ { - w.blocks[dfcube.Pos{x, y, z}] = b + w.blocks[cube.Pos{x, y, z}] = b } } } return w } -func (w *liquidWorld) Block(pos dfcube.Pos) world.Block { +func (w *liquidWorld) Block(pos cube.Pos) world.Block { if b, ok := w.blocks[pos]; ok { return b } return block.Air{} } -func (w *liquidWorld) BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 { +func (w *liquidWorld) BlockCollisions(pos cube.Pos) []cube.BBox32 { b := w.Block(pos) if _, air := b.(block.Air); air { return nil @@ -62,16 +62,16 @@ func (w *liquidWorld) BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 { if _, liquid := b.(world.Liquid); liquid { return nil } - return []dfcube.BBox32{dfcube.Box32(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))} + return []cube.BBox32{cube.Box32(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))} } -func (w *liquidWorld) GetNearbyBBoxes(aabb dfcube.BBox32) []dfcube.BBox32 { +func (w *liquidWorld) GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 { min, max := aabb.Min(), aabb.Max() - var out []dfcube.BBox32 + 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++ { - for _, bb := range w.BlockCollisions(dfcube.Pos{x, y, z}) { + for _, bb := range w.BlockCollisions(cube.Pos{x, y, z}) { if bb.IntersectsWith(aabb) { out = append(out, bb) } @@ -90,20 +90,20 @@ func (w *liquidWorld) IsChunkLoaded(chunkX, chunkZ int32) bool { // second block layer (waterlogged blocks) as well as the main layer. type layeredLiquidWorld struct { *liquidWorld - layer map[dfcube.Pos]world.Liquid + layer map[cube.Pos]world.Liquid } func newLayeredLiquidWorld() *layeredLiquidWorld { - return &layeredLiquidWorld{liquidWorld: newLiquidWorld(), layer: map[dfcube.Pos]world.Liquid{}} + return &layeredLiquidWorld{liquidWorld: newLiquidWorld(), layer: map[cube.Pos]world.Liquid{}} } -func (w *layeredLiquidWorld) waterlog(pos dfcube.Pos, b world.Block, liquid world.Liquid) *layeredLiquidWorld { +func (w *layeredLiquidWorld) waterlog(pos cube.Pos, b world.Block, liquid world.Liquid) *layeredLiquidWorld { w.blocks[pos] = b w.layer[pos] = liquid return w } -func (w *layeredLiquidWorld) Liquid(pos dfcube.Pos) (world.Liquid, bool) { +func (w *layeredLiquidWorld) Liquid(pos cube.Pos) (world.Liquid, bool) { if liquid, ok := w.layer[pos]; ok { return liquid, true } @@ -150,7 +150,7 @@ func submergedState() *MovementState { // with the given liquid from y=0 to y=3, so the player is fully submerged and // the liquid gradient is uniform (no flow). func filledColumn(b world.Block) *liquidWorld { - return newLiquidWorld().fill(dfcube.Pos{-2, 0, -2}, dfcube.Pos{2, 3, 2}, b) + return newLiquidWorld().fill(cube.Pos{-2, 0, -2}, cube.Pos{2, 3, 2}, b) } func approxEqual(a, b float32) bool { @@ -209,7 +209,7 @@ func TestSwimmingFlagAloneDoesNotShrinkHitbox(t *testing.T) { // A spoofed swimming flag with no water anywhere must not let the player pass // through a gap that only the collapsed swim hitbox fits. func TestSpoofedSwimmingCannotFitThroughCeilingGap(t *testing.T) { - sim := newLiquidSim(newLiquidWorld().set(dfcube.Pos{0, 2, 0}, block.Stone{})) + sim := newLiquidSim(newLiquidWorld().set(cube.Pos{0, 2, 0}, block.Stone{})) state := newBaseState() state.Pos = mgl32.Vec3{0.5, 0, 0.5} state.Client.Pos = state.Pos @@ -614,7 +614,7 @@ func TestSwimTravelSkippedWhileJumping(t *testing.T) { // liquid, preventing the player from swimming out into open air. func TestSwimTravelStopsAtSurface(t *testing.T) { // Liquid only below the player's head-check probes. - w := newLiquidWorld().fill(dfcube.Pos{-2, -4, -2}, dfcube.Pos{2, 0, 2}, waterSource) + w := newLiquidWorld().fill(cube.Pos{-2, -4, -2}, cube.Pos{2, 0, 2}, waterSource) sim := newLiquidSim(w) state := submergedState() // Both head probes (+0.52 and +0.42) clear the liquid surface at y=1. @@ -635,7 +635,7 @@ func TestSwimTravelStopsAtSurface(t *testing.T) { // While the head is still submerged the climb continues normally. func TestSwimTravelContinuesWhileHeadSubmerged(t *testing.T) { - w := newLiquidWorld().fill(dfcube.Pos{-2, -4, -2}, dfcube.Pos{2, 0, 2}, waterSource) + w := newLiquidWorld().fill(cube.Pos{-2, -4, -2}, cube.Pos{2, 0, 2}, waterSource) sim := newLiquidSim(w) state := submergedState() state.Swimming = true @@ -650,7 +650,7 @@ func TestSwimTravelContinuesWhileHeadSubmerged(t *testing.T) { // WantDownSlow suppresses the surface clamp so the player can hover. func TestSwimTravelSurfaceClampSkippedWhenWantDownSlow(t *testing.T) { - w := newLiquidWorld().fill(dfcube.Pos{-2, -4, -2}, dfcube.Pos{2, 0, 2}, waterSource) + w := newLiquidWorld().fill(cube.Pos{-2, -4, -2}, cube.Pos{2, 0, 2}, waterSource) sim := newLiquidSim(w) state := submergedState() state.Pos = mgl32.Vec3{0.5, 1.5, 0.5} @@ -881,7 +881,7 @@ func TestZeroMovementSpeedsUseDefaults(t *testing.T) { // Water is detected through a shallow vertical offset, so a player standing on // top of a water block is still considered to be in water. func TestWaterDetectedAtFeet(t *testing.T) { - sim := newLiquidSim(newLiquidWorld().set(dfcube.Pos{0, 0, 0}, waterSource)) + sim := newLiquidSim(newLiquidWorld().set(cube.Pos{0, 0, 0}, waterSource)) state := submergedState() if got := len(sim.touchingLiquidBlocks(state, liquidWater)); got != 1 { @@ -892,7 +892,7 @@ func TestWaterDetectedAtFeet(t *testing.T) { // Lava uses a wider horizontal shrink than water, so a player at the very edge // of a lava block touches water but not lava. func TestLavaUsesWiderHorizontalMargin(t *testing.T) { - w := newLiquidWorld().set(dfcube.Pos{0, 0, 0}, waterSource).set(dfcube.Pos{1, 0, 0}, lavaSource) + w := newLiquidWorld().set(cube.Pos{0, 0, 0}, waterSource).set(cube.Pos{1, 0, 0}, lavaSource) sim := newLiquidSim(w) state := submergedState() // Position the player so the box only just reaches into x=1. @@ -924,8 +924,8 @@ func TestLiquidTypeFiltering(t *testing.T) { // Water travel takes priority when a player touches both liquids. func TestWaterTakesPriorityOverLava(t *testing.T) { w := newLiquidWorld(). - fill(dfcube.Pos{-2, 0, -2}, dfcube.Pos{2, 3, 2}, waterSource). - set(dfcube.Pos{0, 0, 0}, lavaSource) + fill(cube.Pos{-2, 0, -2}, cube.Pos{2, 3, 2}, waterSource). + set(cube.Pos{0, 0, 0}, lavaSource) sim := newLiquidSim(w) state := submergedState() @@ -947,7 +947,7 @@ func TestLiquidFallsBackToBlockProvider(t *testing.T) { func TestLiquidProviderDetectsWaterloggedBlocks(t *testing.T) { w := newLayeredLiquidWorld() for y := range 4 { - w.waterlog(dfcube.Pos{0, y, 0}, block.Air{}, waterSource) + w.waterlog(cube.Pos{0, y, 0}, block.Air{}, waterSource) } sim := newLiquidSim(w) state := submergedState() @@ -1106,11 +1106,11 @@ func TestSwimmingOutsideWaterSuppressesJump(t *testing.T) { // Flowing water pushes the player toward the lower-depth neighbour. func TestLiquidFlowPushesTowardLowerDepth(t *testing.T) { w := newLiquidWorld(). - set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 7}). - set(dfcube.Pos{-1, 0, 0}, block.Water{Depth: 8}). - set(dfcube.Pos{0, 0, 1}, block.Water{Depth: 8}). - set(dfcube.Pos{0, 0, -1}, block.Water{Depth: 8}) + set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(cube.Pos{1, 0, 0}, block.Water{Depth: 7}). + set(cube.Pos{-1, 0, 0}, block.Water{Depth: 8}). + set(cube.Pos{0, 0, 1}, block.Water{Depth: 8}). + set(cube.Pos{0, 0, -1}, block.Water{Depth: 8}) sim := newLiquidSim(w) state := submergedState() @@ -1123,8 +1123,8 @@ func TestLiquidFlowPushesTowardLowerDepth(t *testing.T) { // Water flow strength is 0.014 per tick along the normalized flow vector. func TestWaterFlowStrength(t *testing.T) { w := newLiquidWorld(). - set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(dfcube.Pos{1, 0, 0}, block.Water{Depth: 7}) + set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(cube.Pos{1, 0, 0}, block.Water{Depth: 7}) sim := newLiquidSim(w) state := submergedState() @@ -1137,8 +1137,8 @@ func TestWaterFlowStrength(t *testing.T) { // Lava flow is much weaker than water flow. func TestLavaFlowStrength(t *testing.T) { w := newLiquidWorld(). - set(dfcube.Pos{0, 0, 0}, block.Lava{Depth: 8}). - set(dfcube.Pos{1, 0, 0}, block.Lava{Depth: 7}) + set(cube.Pos{0, 0, 0}, block.Lava{Depth: 8}). + set(cube.Pos{1, 0, 0}, block.Lava{Depth: 7}) sim := newLiquidSim(w) state := submergedState() @@ -1160,11 +1160,11 @@ func TestUniformLiquidHasNoFlow(t *testing.T) { // Falling liquid against a solid neighbour gains a strong downward component. func TestFallingLiquidFlowsDownwardAlongSolids(t *testing.T) { w := newLiquidWorld(). - set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}). - set(dfcube.Pos{1, 0, 0}, block.Stone{}) + set(cube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}). + set(cube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) - flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}) + flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8, Falling: true}) if !(flow.Y() < 0) { t.Fatalf("falling liquid flow Y = %v, want negative", flow.Y()) } @@ -1173,11 +1173,11 @@ func TestFallingLiquidFlowsDownwardAlongSolids(t *testing.T) { // Non-falling liquid never gains the downward push. func TestNonFallingLiquidHasNoDownwardFlow(t *testing.T) { w := newLiquidWorld(). - set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(dfcube.Pos{1, 0, 0}, block.Stone{}) + set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(cube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) - flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) + flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) if flow.Y() < 0 { t.Fatalf("non-falling liquid flow Y = %v, want no downward push", flow.Y()) } @@ -1186,12 +1186,12 @@ func TestNonFallingLiquidHasNoDownwardFlow(t *testing.T) { // A solid neighbour blocks flow in that direction rather than contributing. func TestSolidNeighbourBlocksFlow(t *testing.T) { w := newLiquidWorld(). - set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(dfcube.Pos{1, 0, 0}, block.Stone{}). - set(dfcube.Pos{1, -1, 0}, block.Water{Depth: 8}) + set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(cube.Pos{1, 0, 0}, block.Stone{}). + set(cube.Pos{1, -1, 0}, block.Water{Depth: 8}) sim := newLiquidSim(w) - flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) + flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) if !approxEqual(flow.X(), 0) { t.Fatalf("flow X = %v, want 0 through a solid neighbour", flow.X()) } @@ -1200,11 +1200,11 @@ func TestSolidNeighbourBlocksFlow(t *testing.T) { // An open neighbour with liquid below pulls the flow into the drop. func TestFlowFallsIntoOpenDrop(t *testing.T) { w := newLiquidWorld(). - set(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}). - set(dfcube.Pos{1, -1, 0}, block.Water{Depth: 8}) + set(cube.Pos{0, 0, 0}, block.Water{Depth: 8}). + set(cube.Pos{1, -1, 0}, block.Water{Depth: 8}) sim := newLiquidSim(w) - flow := sim.liquidFlow(dfcube.Pos{0, 0, 0}, block.Water{Depth: 8}) + flow := sim.liquidFlow(cube.Pos{0, 0, 0}, block.Water{Depth: 8}) if !(flow.X() > 0) { t.Fatalf("flow X = %v, want a positive pull into the drop", flow.X()) } @@ -1251,8 +1251,8 @@ func TestFallingLiquidDecayAndHeight(t *testing.T) { // hop out of the liquid. func TestLiquidExitProbeBoostsOverLedge(t *testing.T) { w := newLiquidWorld(). - fill(dfcube.Pos{-1, 0, -1}, dfcube.Pos{0, 0, 1}, waterSource). - set(dfcube.Pos{1, 0, 0}, block.Stone{}) + fill(cube.Pos{-1, 0, -1}, cube.Pos{0, 0, 1}, waterSource). + set(cube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) state := submergedState() state.Vel = mgl32.Vec3{0.5, 0, 0} @@ -1276,10 +1276,10 @@ func TestLiquidExitProbeBoostsOverLedge(t *testing.T) { func TestLiquidExitProbeBlockedByCollisionAlone(t *testing.T) { build := func(overhang bool) (*Simulator, *MovementState) { w := newLiquidWorld(). - fill(dfcube.Pos{-1, 0, -1}, dfcube.Pos{0, 0, 1}, waterSource). - set(dfcube.Pos{1, 0, 0}, block.Stone{}) + fill(cube.Pos{-1, 0, -1}, cube.Pos{0, 0, 1}, waterSource). + set(cube.Pos{1, 0, 0}, block.Stone{}) if overhang { - w.set(dfcube.Pos{0, 1, 0}, block.Stone{}) + w.set(cube.Pos{0, 1, 0}, block.Stone{}) } state := submergedState() state.Pos = mgl32.Vec3{0.5, 0.4, 0.5} @@ -1319,8 +1319,8 @@ func TestLiquidExitProbeBlockedByCollisionAlone(t *testing.T) { // submerged rather than at the surface. func TestLiquidExitProbeBlockedByLiquidAbove(t *testing.T) { w := newLiquidWorld(). - fill(dfcube.Pos{-1, 0, -1}, dfcube.Pos{0, 4, 1}, waterSource). - set(dfcube.Pos{1, 0, 0}, block.Stone{}) + fill(cube.Pos{-1, 0, -1}, cube.Pos{0, 4, 1}, waterSource). + set(cube.Pos{1, 0, 0}, block.Stone{}) sim := newLiquidSim(w) state := submergedState() state.Vel = mgl32.Vec3{0.5, 0, 0} @@ -1381,7 +1381,7 @@ 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 := dfcube.Box32(0, 0, 0, 1, 0.2, 1) + 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) { diff --git a/math.go b/math.go index a0928b2..e8ca031 100644 --- a/math.go +++ b/math.go @@ -3,7 +3,7 @@ package bedsim import ( "github.com/chewxy/math32" - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/go-gl/mathgl/mgl32" ) @@ -39,10 +39,10 @@ func Vec3HzDistSqr(vec3 mgl32.Vec3) float32 { return vec3.X()*vec3.X() + vec3.Z()*vec3.Z() } -func posFromVec3(vec mgl32.Vec3) dfcube.Pos { - return dfcube.Pos{int(math32.Floor(vec.X())), int(math32.Floor(vec.Y())), int(math32.Floor(vec.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 dfcube.Pos) mgl32.Vec3 { +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 0e3279d..483c348 100644 --- a/movement.go +++ b/movement.go @@ -1,7 +1,7 @@ package bedsim import ( - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/go-gl/mathgl/mgl32" ) @@ -30,7 +30,7 @@ type MovementState struct { Impulse mgl32.Vec2 Size mgl32.Vec3 - SupportingBlockPos *dfcube.Pos + SupportingBlockPos *cube.Pos Gravity float32 JumpHeight float32 diff --git a/native_float32_test.go b/native_float32_test.go index b7e8833..3ef5e50 100644 --- a/native_float32_test.go +++ b/native_float32_test.go @@ -3,7 +3,7 @@ package bedsim import ( "testing" - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/go-gl/mathgl/mgl32" ) @@ -16,8 +16,8 @@ func TestNativeFloat32Surface(t *testing.T) { } func TestBBoxFromDragonflyRoundsAtProviderBoundary(t *testing.T) { - got := BBoxFromDragonfly(dfcube.Box(0.1, 0.2, 0.3, 0.9, 1.8, 0.7)) - want := dfcube.Box32(float32(0.1), float32(0.2), float32(0.3), float32(0.9), float32(1.8), float32(0.7)) + got := BBoxFromDragonfly(cube.Box(0.1, 0.2, 0.3, 0.9, 1.8, 0.7)) + want := cube.Box32(float32(0.1), float32(0.2), float32(0.3), float32(0.9), float32(1.8), float32(0.7)) if got != want { t.Fatalf("BBoxFromDragonfly() = %v, want %v", got, want) } diff --git a/simulation.go b/simulation.go index 28e8ef1..c076d6e 100644 --- a/simulation.go +++ b/simulation.go @@ -5,7 +5,7 @@ import ( "iter" "github.com/df-mc/dragonfly/server/block" - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" @@ -410,7 +410,7 @@ func (s *Simulator) simulateMovement(state *MovementState) { } else { blockUnder = s.blockAtPos(posFromVec3(state.Pos.Sub(mgl32.Vec3{0, 0.2}))) if _, isAir := blockUnder.(block.Air); isAir { - below := s.blockAtPos(posFromVec3(state.Pos).Side(dfcube.FaceDown)) + below := s.blockAtPos(posFromVec3(state.Pos).Side(cube.FaceDown)) if IsWall(below) || IsFence(below) { blockUnder = below } @@ -1032,8 +1032,8 @@ func (s *Simulator) isInsideCobweb(state *MovementState) bool { return insideCobweb } -func nearbyBlocks(aabb dfcube.BBox32, w WorldProvider) iter.Seq2[dfcube.Pos, world.Block] { - return func(yield func(dfcube.Pos, world.Block) bool) { +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 } @@ -1044,7 +1044,7 @@ func nearbyBlocks(aabb dfcube.BBox32, w WorldProvider) iter.Seq2[dfcube.Pos, wor for y := minY; y <= maxY; y++ { for x := minX; x <= maxX; x++ { for z := minZ; z <= maxZ; z++ { - pos := dfcube.Pos{x, y, z} + pos := cube.Pos{x, y, z} if !yield(pos, w.Block(pos)) { return } @@ -1059,7 +1059,7 @@ func checkSupportingBlockPos(state *MovementState, w WorldProvider, useSlideOffs state.SupportingBlockPos = nil return } - decBB := state.BoundingBox(useSlideOffset).ExtendTowards(dfcube.FaceDown, 1e-3) + decBB := state.BoundingBox(useSlideOffset).ExtendTowards(cube.FaceDown, 1e-3) findSupportingBlock(state, w, decBB) if state.SupportingBlockPos == nil { decBB = decBB.Translate(mgl32.Vec3{-vel[0], 0, -vel[2]}) @@ -1067,11 +1067,11 @@ func checkSupportingBlockPos(state *MovementState, w WorldProvider, useSlideOffs } } -func findSupportingBlock(state *MovementState, w WorldProvider, bb dfcube.BBox32) { +func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox32) { if w == nil { return } - var blockPos *dfcube.Pos + var blockPos *cube.Pos minDist := float32(math32.MaxFloat32 - 1) centerPos := posVec3(posFromVec3(state.Pos)).Add(mgl32.Vec3{0.5, 0.5, 0.5}) @@ -1098,7 +1098,7 @@ func findSupportingBlock(state *MovementState, w WorldProvider, bb dfcube.BBox32 state.SupportingBlockPos = blockPos } -func (s *Simulator) blockAtPos(pos dfcube.Pos) world.Block { +func (s *Simulator) blockAtPos(pos cube.Pos) world.Block { if s.World == nil { return block.Air{} } @@ -1106,10 +1106,10 @@ func (s *Simulator) blockAtPos(pos dfcube.Pos) world.Block { } type nearbyBBoxProbe interface { - HasNearbyBBoxes(aabb dfcube.BBox32) bool + HasNearbyBBoxes(aabb cube.BBox32) bool } -func hasNearbyBBoxes(w WorldProvider, aabb dfcube.BBox32) bool { +func hasNearbyBBoxes(w WorldProvider, aabb cube.BBox32) bool { if w == nil { return false } diff --git a/simulator_test.go b/simulator_test.go index f99f44a..19e4c21 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/df-mc/dragonfly/server/block" - dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" @@ -15,15 +15,15 @@ import ( type mockWorld struct{} -func (mockWorld) Block(pos dfcube.Pos) world.Block { +func (mockWorld) Block(pos cube.Pos) world.Block { return block.Air{} } -func (mockWorld) BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 { +func (mockWorld) BlockCollisions(pos cube.Pos) []cube.BBox32 { return nil } -func (mockWorld) GetNearbyBBoxes(aabb dfcube.BBox32) []dfcube.BBox32 { +func (mockWorld) GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 { return nil } @@ -33,28 +33,28 @@ func (mockWorld) IsChunkLoaded(chunkX, chunkZ int32) bool { type staticWorld struct { chunkLoaded bool - boxes []dfcube.BBox32 + boxes []cube.BBox32 } type cobwebWorld struct { - pos dfcube.Pos + pos cube.Pos } -func (w cobwebWorld) Block(pos dfcube.Pos) world.Block { +func (w cobwebWorld) Block(pos cube.Pos) world.Block { if pos == w.pos { return block.Cobweb{} } return block.Air{} } -func (w cobwebWorld) BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 { +func (w cobwebWorld) BlockCollisions(pos cube.Pos) []cube.BBox32 { if pos != w.pos { return nil } - return []dfcube.BBox32{dfcube.Box32(0, 0, 0, 1, 1, 1)} + return []cube.BBox32{cube.Box32(0, 0, 0, 1, 1, 1)} } -func (cobwebWorld) GetNearbyBBoxes(dfcube.BBox32) []dfcube.BBox32 { +func (cobwebWorld) GetNearbyBBoxes(cube.BBox32) []cube.BBox32 { return nil } @@ -62,20 +62,20 @@ func (cobwebWorld) IsChunkLoaded(int32, int32) bool { return true } -func (w staticWorld) Block(pos dfcube.Pos) world.Block { +func (w staticWorld) Block(pos cube.Pos) world.Block { return block.Air{} } -func (w staticWorld) BlockCollisions(pos dfcube.Pos) []dfcube.BBox32 { +func (w staticWorld) BlockCollisions(pos cube.Pos) []cube.BBox32 { return nil } -func (w staticWorld) GetNearbyBBoxes(aabb dfcube.BBox32) []dfcube.BBox32 { +func (w staticWorld) GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 { if len(w.boxes) == 0 { return nil } - out := make([]dfcube.BBox32, 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) @@ -148,7 +148,7 @@ func newBaseState() *MovementState { } func TestInsideCobwebTranslatesBlockLocalCollisionBoxes(t *testing.T) { - pos := dfcube.Pos{32, 64, -24} + 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} @@ -507,8 +507,8 @@ func TestSimulateStateDebugTraceJumpBlocked(t *testing.T) { sim := &Simulator{ World: staticWorld{ chunkLoaded: true, - boxes: []dfcube.BBox32{ - dfcube.Box32(0, 2, 1, 1, 3, 2), + boxes: []cube.BBox32{ + cube.Box32(0, 2, 1, 1, 3, 2), }, }, Effects: mockEffects{}, @@ -546,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 := dfcube.Box32(1, 0, -1, 2, 0.5, 2) - groundBox := dfcube.Box32(-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 := mgl32.Vec3{0.5, 0, 0.5} runSim := func(ignoreStepTiebreaker bool) (mgl32.Vec3, bool) { - w := staticWorld{chunkLoaded: true, boxes: []dfcube.BBox32{slabBox, groundBox}} + w := staticWorld{chunkLoaded: true, boxes: []cube.BBox32{slabBox, groundBox}} sim := &Simulator{ World: w, Effects: mockEffects{}, @@ -601,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 := dfcube.Box32(1, 1.3, -1, 2, 2.3, 2) // leaves only 0.8 gap, player is 1.8 tall - w := staticWorld{chunkLoaded: true, boxes: []dfcube.BBox32{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{}, From 78e19eb4d91b1fb7bddfcfa873f3479fb7e32f50 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 01:14:00 -0400 Subject: [PATCH 5/8] test: keep collision fixture boxes block-local --- liquid_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/liquid_test.go b/liquid_test.go index fb437db..fc86bf5 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -62,7 +62,7 @@ func (w *liquidWorld) BlockCollisions(pos cube.Pos) []cube.BBox32 { if _, liquid := b.(world.Liquid); liquid { return nil } - return []cube.BBox32{cube.Box32(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))} + return []cube.BBox32{cube.Box32(0, 0, 0, 1, 1, 1)} } func (w *liquidWorld) GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 { @@ -71,7 +71,9 @@ func (w *liquidWorld) GetNearbyBBoxes(aabb cube.BBox32) []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++ { - for _, bb := range w.BlockCollisions(cube.Pos{x, y, 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) } From 0da47dbec2dc8e00fa05dbcc5a03c29ce1e7a4f2 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 01:23:25 -0400 Subject: [PATCH 6/8] test: remove native float32 surface checks --- native_float32_test.go | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 native_float32_test.go diff --git a/native_float32_test.go b/native_float32_test.go deleted file mode 100644 index 3ef5e50..0000000 --- a/native_float32_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package bedsim - -import ( - "testing" - - "github.com/df-mc/dragonfly/server/block/cube" - "github.com/go-gl/mathgl/mgl32" -) - -func TestNativeFloat32Surface(t *testing.T) { - var sin func(float32) float32 = MCSin - state := MovementState{Pos: mgl32.Vec3{1, 2, 3}} - if got := sin(state.Pos.X()); got != MCSin(1) { - t.Fatalf("MCSin(%v) = %v, want %v", state.Pos.X(), got, MCSin(1)) - } -} - -func TestBBoxFromDragonflyRoundsAtProviderBoundary(t *testing.T) { - got := BBoxFromDragonfly(cube.Box(0.1, 0.2, 0.3, 0.9, 1.8, 0.7)) - want := cube.Box32(float32(0.1), float32(0.2), float32(0.3), float32(0.9), float32(1.8), float32(0.7)) - if got != want { - t.Fatalf("BBoxFromDragonfly() = %v, want %v", got, want) - } -} From 510d09f4476e1a298848ef186d554d11fb04a49f Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 01:26:34 -0400 Subject: [PATCH 7/8] fix: avoid finalizing dragonfly block registry --- README.md | 28 +-------------------------- block.go | 38 +++++++++++++++++------------------- block_test.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 48 deletions(-) create mode 100644 block_test.go 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/block.go b/block.go index e1a8a70..f6b9159 100644 --- a/block.go +++ b/block.go @@ -9,32 +9,28 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -var ( - blockNameMapping map[uint64]string - blockNameMappingOnce sync.Once -) - -func initBlockNameMapping() { - world.DefaultBlockRegistry.Finalize() - 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 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. diff --git a/block_test.go b/block_test.go new file mode 100644 index 0000000..a077749 --- /dev/null +++ b/block_test.go @@ -0,0 +1,53 @@ +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) + } +} From cba7acf0676167f326d97baf51079a16d8e20e0a Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 01:28:01 -0400 Subject: [PATCH 8/8] test: preserve block name sentinel semantics --- block.go | 2 +- block_test.go | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/block.go b/block.go index f6b9159..d39d846 100644 --- a/block.go +++ b/block.go @@ -18,7 +18,7 @@ var blockNameCache sync.Map // BlockName returns the canonical name of a block. func BlockName(b world.Block) string { base, state := b.Hash() - if state == math.MaxUint64 { + if base == 0 && state == math.MaxUint64 { name, _ := b.EncodeBlock() return name } diff --git a/block_test.go b/block_test.go index a077749..e830b6d 100644 --- a/block_test.go +++ b/block_test.go @@ -51,3 +51,14 @@ func TestBlockNameDoesNotCacheUnknownHash(t *testing.T) { 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) + } +}