From 456eacbf1681d4187ba41effd1330b27ca8f6d2c Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 20 Jul 2026 20:30:49 -0400 Subject: [PATCH 1/9] feat: expand movement simulation parity --- README.md | 27 ++++- block_effects.go | 80 ++++++++++++++ block_effects_test.go | 170 ++++++++++++++++++++++++++++ bubble.go | 85 ++++++++++++++ bubble_test.go | 73 ++++++++++++ constants.go | 8 +- dynamic_collision_test.go | 103 +++++++++++++++++ input.go | 9 ++ interfaces.go | 33 ++++++ liquid.go | 17 ++- movement.go | 32 ++++++ movement_environment_test.go | 51 +++++++++ parity_test.go | 118 ++++++++++++++++++++ player_features_test.go | 113 +++++++++++++++++++ simulation.go | 207 ++++++++++++++++++++++++++++------- simulator.go | 14 +++ simulator_test.go | 2 +- 17 files changed, 1094 insertions(+), 48 deletions(-) create mode 100644 block_effects.go create mode 100644 block_effects_test.go create mode 100644 bubble.go create mode 100644 bubble_test.go create mode 100644 dynamic_collision_test.go create mode 100644 movement_environment_test.go create mode 100644 parity_test.go create mode 100644 player_features_test.go diff --git a/README.md b/README.md index f868e70..1641718 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Server-side Minecraft Bedrock movement simulation library for Go. -`bedsim` replicates the Bedrock client's movement physics (collisions, stepping, edge-avoidance, liquids, gliding, teleportation) on the server, producing authoritative position and velocity values that can be compared against client-reported state. +`bedsim` replicates the Bedrock client's movement physics on the server, producing authoritative position and velocity values that can be compared against client-reported state. It covers collisions, stepping, edge avoidance, liquids and currents, swimming, bubble columns, Riptide, crawling, gliding, movement enchantments, movement-sensitive blocks, and teleportation. Original code was written by [ethaniccc](https://github.com/ethaniccc) in [oomph](https://github.com/oomph-ac/oomph) and has been ported over into this library. The liquid movement physics were ported from [oomph#145](https://github.com/oomph-ac/oomph/pull/145) by [NopeNotDark](https://github.com/NopeNotDark). @@ -54,6 +54,7 @@ sim := bedsim.Simulator{ Liquids: myLiquidProvider, // second block layer (waterlogged blocks) Effects: myEffectsProvider, // jump boost, levitation, slow falling Inventory: myInventoryProvider, // elytra equipped check + Equipment: myEquipmentProvider, // movement enchantments and leather boots Options: bedsim.SimulationOptions{ Mode: bedsim.SimulationModeAuthoritative, PositionCorrectionThreshold: 0.5, @@ -89,6 +90,30 @@ should affect water movement. > discovered by type assertion, so a signature typo degrades silently — prefer > the explicit field. +### Optional movement capabilities + +`WorldProvider` is the only required world interface. A world may additionally +implement `BubbleColumnProvider` for upward/downward columns and +`MovementCollisionProvider` for player-dependent collision shapes such as +scaffolding and powder snow. Dynamic collision resolution receives sneak and +descend intent plus leather-boots state. + +`MovementEquipmentProvider` supplies Depth Strider, Soul Speed, Swift Sneak, +Riptide, and leather-boots checks. The legacy `DepthStriderProvider` inventory +extension remains supported when `Equipment` is nil. `EffectsProvider` also +controls Weaving-aware cobweb movement. + +Pose changes update `MovementState.Size`. Set `StandingHeight`, +`SneakingHeight`, or `CrawlingHeight` when using non-vanilla dimensions; zero +values preserve the current standing height and use vanilla crouch/crawl +heights. + +Movement-sensitive block behavior includes honey blocks, sweet berry bushes, +powder snow, scaffolding, cobwebs (including Weaving), soul sand with Soul +Speed, slime blocks, beds, climbables, fences/walls, and per-block friction. +Dynamic collision behavior still depends on the world adapter returning the +correct shapes for the current block state. + ### Liquid movement When the player's hitbox touches water or lava (and the player is not flying), diff --git a/block_effects.go b/block_effects.go new file mode 100644 index 0000000..f82c905 --- /dev/null +++ b/block_effects.go @@ -0,0 +1,80 @@ +package bedsim + +import ( + "math" + + "github.com/df-mc/dragonfly/server/block/cube" +) + +func applyInsideBlockMovement(state *MovementState, blockName string, weaving bool) { + velocity := state.Vel + switch blockName { + case "minecraft:honey_block": + velocity[0] *= 0.4 + velocity[1] = max(-0.12, velocity[1]) + velocity[2] *= 0.4 + case "minecraft:sweet_berry_bush": + velocity[0] *= 0.8 + velocity[1] *= 0.75 + velocity[2] *= 0.8 + case "minecraft:powder_snow": + velocity[0] *= 0.9 + velocity[1] *= 1.5 + velocity[2] *= 0.9 + case "minecraft:web", "minecraft:cobweb": + xz, y := 0.25, 0.05 + if weaving { + xz, y = 0.5, 0.25 + } + velocity[0] *= xz + velocity[1] *= y + velocity[2] *= xz + } + state.SetVel(velocity) +} + +func applyAscendableMovement(state *MovementState, blockName string, leatherBoots bool) { + velocity := state.Vel + switch blockName { + case "minecraft:scaffolding": + if state.PressingDescend { + velocity[1] = -0.15 + } else if state.PressingAscend { + velocity[1] = 0.15 + } + case "minecraft:powder_snow": + if state.PressingDescend { + velocity[1] = -0.15 + } else if state.PressingAscend && leatherBoots { + velocity[1] = 0.2 + } + } + state.SetVel(velocity) +} + +func (s *Simulator) applyInsideBlockEffects(state *MovementState) { + if s.World == nil { + return + } + bb := state.BoundingBox(s.Options.UseSlideOffset) + min, maxPoint := bb.Min(), bb.Max() + for x := int(math.Floor(min.X())); x < int(math.Ceil(maxPoint.X())); x++ { + for y := int(math.Floor(min.Y())); y < int(math.Ceil(maxPoint.Y())); y++ { + for z := int(math.Floor(min.Z())); z < int(math.Ceil(maxPoint.Z())); z++ { + pos := cube.Pos{x, y, z} + if !bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())) { + continue + } + b := s.World.Block(pos) + if s.blockAir(b) { + continue + } + name := s.blockName(b) + if name == "minecraft:web" || name == "minecraft:cobweb" { + continue + } + applyInsideBlockMovement(state, name, false) + } + } + } +} diff --git a/block_effects_test.go b/block_effects_test.go new file mode 100644 index 0000000..412305b --- /dev/null +++ b/block_effects_test.go @@ -0,0 +1,170 @@ +package bedsim + +import ( + "math" + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +type namedBlock struct { + block.Air + name string +} + +func (b namedBlock) EncodeBlock() (string, map[string]any) { return b.name, nil } + +type encodedBlockSemantics struct{} + +func (encodedBlockSemantics) BlockName(b world.Block) string { + name, _ := b.EncodeBlock() + return name +} +func (encodedBlockSemantics) BlockFriction(world.Block) float64 { return DefaultBlockFriction } +func (encodedBlockSemantics) BlockClimbable(world.Block) bool { return false } + +func TestInsideBlockMovementMultipliers(t *testing.T) { + tests := []struct { + name string + blockName string + weaving bool + want mgl64.Vec3 + }{ + {name: "honey", blockName: "minecraft:honey_block", want: mgl64.Vec3{0.4, -0.12, 0.4}}, + {name: "sweet berry bush", blockName: "minecraft:sweet_berry_bush", want: mgl64.Vec3{0.8, -0.75, 0.8}}, + {name: "powder snow", blockName: "minecraft:powder_snow", want: mgl64.Vec3{0.9, -1.5, 0.9}}, + {name: "cobweb", blockName: "minecraft:web", want: mgl64.Vec3{0.25, -0.05, 0.25}}, + {name: "cobweb with weaving", blockName: "minecraft:web", weaving: true, want: mgl64.Vec3{0.5, -0.25, 0.5}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := newBaseState() + state.Vel = mgl64.Vec3{1, -1, 1} + + applyInsideBlockMovement(state, tt.blockName, tt.weaving) + + if state.Vel != tt.want { + t.Fatalf("expected velocity %v, got %v", tt.want, state.Vel) + } + }) + } +} + +func TestHoneyBlockReducesJumpPower(t *testing.T) { + sim := &Simulator{ + World: mockWorld{}, + BlockSemantics: overrideBlockSemantics{name: "minecraft:honey_block"}, + } + state := newBaseState() + state.OnGround = true + state.Jumping = true + state.JumpHeight = DefaultJumpHeight + + if !sim.attemptJump(state, nil) { + t.Fatal("expected jump to be applied") + } + if want := DefaultJumpHeight * 0.6; math.Abs(state.Vel.Y()-want) > 1e-12 { + t.Fatalf("expected honey jump velocity %v, got %v", want, state.Vel.Y()) + } +} + +func TestScaffoldingAscendAndDescendSpeeds(t *testing.T) { + state := newBaseState() + state.PressingAscend = true + applyAscendableMovement(state, "minecraft:scaffolding", false) + if state.Vel.Y() != 0.15 { + t.Fatalf("expected scaffolding ascend velocity 0.15, got %v", state.Vel.Y()) + } + + state.PressingAscend = false + state.PressingDescend = true + applyAscendableMovement(state, "minecraft:scaffolding", false) + if state.Vel.Y() != -0.15 { + t.Fatalf("expected scaffolding descend velocity -0.15, got %v", state.Vel.Y()) + } +} + +func TestPowderSnowTraversalRequiresLeatherBoots(t *testing.T) { + state := newBaseState() + state.PressingAscend = true + applyAscendableMovement(state, "minecraft:powder_snow", false) + if state.Vel.Y() != 0 { + t.Fatalf("expected no powder-snow ascent without leather boots, got %v", state.Vel.Y()) + } + + applyAscendableMovement(state, "minecraft:powder_snow", true) + if state.Vel.Y() != 0.2 { + t.Fatalf("expected leather-boots powder-snow ascent 0.2, got %v", state.Vel.Y()) + } +} + +func TestHoneyWalkSlowdownMatchesSlime(t *testing.T) { + sim := &Simulator{BlockSemantics: overrideBlockSemantics{name: "minecraft:honey_block"}} + state := newBaseState() + state.OnGround = true + state.Vel = mgl64.Vec3{1, 0.05, 1} + + sim.walkOnBlock(state, block.Air{}) + + if want := 0.41; math.Abs(state.Vel.X()-want) > 1e-12 || math.Abs(state.Vel.Z()-want) > 1e-12 { + t.Fatalf("expected honey walk slowdown %v, got %v", want, state.Vel) + } +} + +func TestSimulationAppliesInsideBlockMovementEffect(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{ + {0, 0, 0}: namedBlock{name: "minecraft:honey_block"}, + }} + sim := &Simulator{World: w, BlockSemantics: encodedBlockSemantics{}} + state := newBaseState() + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Vel = mgl64.Vec3{0.1, 0, 0} + state.HasGravity = false + + sim.SimulateState(state) + + if want := 0.1 * DefaultAirFriction * 0.4; math.Abs(state.Vel.X()-want) > 1e-12 { + t.Fatalf("expected integrated honey slowdown %v, got %v", want, state.Vel.X()) + } +} + +func TestSimulationAppliesScaffoldingTraversal(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{ + {0, 0, 0}: namedBlock{name: "minecraft:scaffolding"}, + }} + sim := &Simulator{World: w, BlockSemantics: encodedBlockSemantics{}} + state := newBaseState() + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.HasGravity = false + + sim.Simulate(state, InputState{AscendBlock: true}) + + if state.Vel.Y() != 0.15 { + t.Fatalf("expected integrated scaffolding ascent 0.15, got %v", state.Vel.Y()) + } +} + +func TestSimulationDetectsNonSolidCobwebAndAppliesWeaving(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{ + {0, 0, 0}: namedBlock{name: "minecraft:web"}, + }} + sim := &Simulator{ + World: w, + BlockSemantics: encodedBlockSemantics{}, + Effects: fixedEffects{EffectWeaving: 0}, + } + state := newBaseState() + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Vel = mgl64.Vec3{0.1, 0, 0} + state.HasGravity = false + + result := sim.SimulateState(state) + + if want := 0.05; math.Abs(result.Movement.X()-want) > 1e-12 { + t.Fatalf("expected Weaving cobweb movement %v, got %v", want, result.Movement.X()) + } +} diff --git a/bubble.go b/bubble.go new file mode 100644 index 0000000..bb7fa67 --- /dev/null +++ b/bubble.go @@ -0,0 +1,85 @@ +package bedsim + +import ( + "math" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" +) + +// BubbleColumnDirection is the direction a bubble column accelerates entities. +type BubbleColumnDirection uint8 + +const ( + BubbleColumnUp BubbleColumnDirection = iota + BubbleColumnDown +) + +// BubbleColumnProvider exposes bubble-column direction separately from liquid +// state so worlds without a concrete bubble-column block type can participate. +type BubbleColumnProvider interface { + BubbleColumn(pos cube.Pos) (BubbleColumnDirection, bool) +} + +func applyBubbleColumn(state *MovementState, direction BubbleColumnDirection, surface bool) { + velocity := state.Vel + switch direction { + case BubbleColumnDown: + cap := -0.3 + if surface { + cap = -0.9 + } + velocity[1] = math.Max(cap, velocity[1]-0.03) + default: + change, cap := 0.06, 0.7 + if surface { + change, cap = 0.1, 1.8 + } + velocity[1] = math.Min(cap, velocity[1]+change) + } + state.SetVel(velocity) +} + +func (s *Simulator) applyBubbleColumns(state *MovementState) { + provider, ok := s.World.(BubbleColumnProvider) + if !ok { + return + } + bb := state.BoundingBox(s.Options.UseSlideOffset) + min, max := bb.Min(), bb.Max() + for x := int(math.Floor(min.X())); x < int(math.Ceil(max.X())); x++ { + for y := int(math.Floor(min.Y())); y < int(math.Ceil(max.Y())); y++ { + for z := int(math.Floor(min.Z())); z < int(math.Ceil(max.Z())); z++ { + pos := cube.Pos{x, y, z} + direction, found := provider.BubbleColumn(pos) + if !found { + continue + } + above := pos.Side(cube.FaceUp) + _, liquidAbove := s.liquidAt(above) + applyBubbleColumn(state, direction, !liquidAbove && s.blockAir(s.blockAtPos(above))) + } + } + } +} + +func (s *Simulator) attemptRiptide(state *MovementState, touchingLiquid bool) bool { + if s.Equipment == nil || state.RiptideTicks > 0 || !touchingLiquid { + return false + } + level := s.Equipment.EnchantmentLevel(EnchantmentRiptide) + if level <= 0 || !state.StartingSpinAttack { + return false + } + force := 1.5 + 0.75*float64(level-1) + pitch := state.Rotation.X() * math.Pi / 180 + yaw := state.Rotation.Z() * math.Pi / 180 + direction := mgl64.Vec3{-MCSin(yaw) * MCCos(pitch), -MCSin(pitch), MCCos(yaw) * MCCos(pitch)} + if length := direction.Len(); length > 0 { + direction = direction.Mul(force / length) + } + state.SetVel(state.Vel.Add(direction)) + state.RiptideTicks = 20 + state.StartingSpinAttack = false + return true +} diff --git a/bubble_test.go b/bubble_test.go new file mode 100644 index 0000000..9e4ed95 --- /dev/null +++ b/bubble_test.go @@ -0,0 +1,73 @@ +package bedsim + +import ( + "math" + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +func TestBubbleColumnUsesBoarImpulsesAndCaps(t *testing.T) { + tests := []struct { + name string + direction BubbleColumnDirection + surface bool + initial float64 + want float64 + }{ + {name: "submerged up", direction: BubbleColumnUp, initial: 0, want: 0.06}, + {name: "submerged up cap", direction: BubbleColumnUp, initial: 0.69, want: 0.70}, + {name: "surface up", direction: BubbleColumnUp, surface: true, initial: 0, want: 0.10}, + {name: "surface up cap", direction: BubbleColumnUp, surface: true, initial: 1.79, want: 1.80}, + {name: "submerged down", direction: BubbleColumnDown, initial: 0, want: -0.03}, + {name: "submerged down cap", direction: BubbleColumnDown, initial: -0.29, want: -0.30}, + {name: "surface down", direction: BubbleColumnDown, surface: true, initial: 0, want: -0.03}, + {name: "surface down cap", direction: BubbleColumnDown, surface: true, initial: -0.89, want: -0.90}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := newBaseState() + state.Vel[1] = tt.initial + applyBubbleColumn(state, tt.direction, tt.surface) + if math.Abs(state.Vel.Y()-tt.want) > 1e-12 { + t.Fatalf("expected y velocity %v, got %v", tt.want, state.Vel.Y()) + } + }) + } +} + +func TestBubbleColumnSurfaceAcceptsRegistryBackedAir(t *testing.T) { + w := environmentWorld{ + bubbles: map[cube.Pos]BubbleColumnDirection{{0, 0, 0}: BubbleColumnUp}, + blocks: map[cube.Pos]world.Block{{0, 1, 0}: namedBlock{name: "minecraft:air"}}, + } + state := newBaseState() + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + + (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).applyBubbleColumns(state) + + if state.Vel.Y() != 0.1 { + t.Fatalf("expected surface bubble impulse above registry-backed air, got %v", state.Vel.Y()) + } +} + +func TestRiptideLaunchesInWaterAndStartsSpinAttack(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} + sim := &Simulator{World: w, Equipment: fixedEquipment{EnchantmentRiptide: 2}} + state := newBaseState() + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Gravity = NormalGravity + + sim.Simulate(state, InputState{StartSpinAttack: true}) + + if want := 1.8; math.Abs(state.Vel.Z()-want) > 1e-9 { + t.Fatalf("expected riptide velocity %v, got %v", want, state.Vel.Z()) + } + if state.RiptideTicks != 19 { + t.Fatalf("expected 19 riptide ticks after the launch tick, got %d", state.RiptideTicks) + } +} diff --git a/constants.go b/constants.go index 19b9761..28657ec 100644 --- a/constants.go +++ b/constants.go @@ -8,10 +8,10 @@ const ( LevitationGravityMultiplier = 0.05 NormalGravity = 0.08 SlowFallingGravity = 0.01 - StepHeight = 0.6 + StepHeight = 0.5625 SlideOffsetMultiplier = 0.4 SlimeBounceMultiplier = -1.0 - BedBounceMultiplier = -0.66 + BedBounceMultiplier = -0.75 // This can be validated in Mob::ascendLadder(). ClimbSpeed = 0.2 MaxConsumingImpulse = 0.1225 @@ -37,4 +37,8 @@ const ( // DefaultSwimWaterGraceTicks bounds retained server-observed water contact. DefaultSwimWaterGraceTicks = 10 + + // EffectWeaving is the Bedrock effect ID for Weaving. Gophertunnel's + // current named effect constants predate the trial effects. + EffectWeaving int32 = 33 ) diff --git a/dynamic_collision_test.go b/dynamic_collision_test.go new file mode 100644 index 0000000..dd217e3 --- /dev/null +++ b/dynamic_collision_test.go @@ -0,0 +1,103 @@ +package bedsim + +import ( + "math" + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +type dynamicCollisionWorld struct { + environmentWorld + lastContext MovementCollisionContext +} + +func (w *dynamicCollisionWorld) GetMovementBBoxes(_ cube.BBox, context MovementCollisionContext) []cube.BBox { + w.lastContext = context + if context.LeatherBoots && !context.Descending && !context.WantDown { + return []cube.BBox{cube.Box(0, 0, 0, 1, 1, 1)} + } + return nil +} + +func TestMovementCollisionProviderReceivesPlayerDependentContext(t *testing.T) { + w := &dynamicCollisionWorld{} + sim := &Simulator{World: w, Equipment: leatherEquipment{}} + state := newBaseState() + state.Sneaking = true + state.PressingDescend = false + + boxes := sim.nearbyBBoxes(state, state.BoundingBox(false)) + + if len(boxes) != 1 { + t.Fatalf("expected dynamic powder-snow collision, got %d boxes", len(boxes)) + } + if !w.lastContext.LeatherBoots || !w.lastContext.Sneaking { + t.Fatalf("expected equipment and sneak context, got %+v", w.lastContext) + } +} + +type leatherEquipment struct{} + +func (leatherEquipment) EnchantmentLevel(MovementEnchantment) int { return 0 } +func (leatherEquipment) WearingLeatherBoots() bool { return true } + +func TestCannotUnsneakUnderLowCeiling(t *testing.T) { + sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox{ + cube.Box(-1, 1.5, -1, 1, 2, 1), + }}} + state := newBaseState() + state.Sneaking = true + state.Size[1] = 1.5 + + sim.applyInput(state, InputState{StopSneaking: true}) + + if !state.Sneaking || state.Size.Y() != 1.5 { + t.Fatalf("expected forced sneak pose under ceiling, got sneaking=%v size=%v", state.Sneaking, state.Size) + } +} + +func TestCannotStopCrawlingUnderLowCeiling(t *testing.T) { + sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox{ + cube.Box(-1, 0.7, -1, 1, 2, 1), + }}} + state := newBaseState() + state.Crawling = true + state.Size[1] = 0.6 + + sim.applyInput(state, InputState{StopCrawling: true}) + + if !state.Crawling || state.Size.Y() != 0.6 { + t.Fatalf("expected forced crawl pose under ceiling, got crawling=%v size=%v", state.Crawling, state.Size) + } +} + +func TestPoseRestoresCustomStandingHeight(t *testing.T) { + state := newBaseState() + state.Size[1] = 2 + sim := &Simulator{} + + sim.applyInput(state, InputState{StartSneaking: true}) + sim.applyInput(state, InputState{StopSneaking: true}) + + if state.Size.Y() != 2 { + t.Fatalf("expected custom standing height 2 to be restored, got %v", state.Size.Y()) + } +} + +func TestSneakingInWaterDescends(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} + sim := &Simulator{World: w} + state := newBaseState() + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Gravity = NormalGravity + + sim.Simulate(state, InputState{SneakDown: true, Sneaking: true}) + + if want := -0.037; math.Abs(state.Vel.Y()-want) > 1e-12 { + t.Fatalf("expected water descent velocity %v, got %v", want, state.Vel.Y()) + } +} diff --git a/input.go b/input.go index 8d20f53..3eb438f 100644 --- a/input.go +++ b/input.go @@ -37,9 +37,18 @@ type InputState struct { StopSwimming bool WantDown bool WantDownSlow bool + StartCrawling bool + StopCrawling bool + DescendBlock bool StopGliding bool StartGliding bool UsingConsumable bool + UsingItem bool + UsingSpear bool + InventoryAction bool + + StartSpinAttack bool + StopSpinAttack bool } diff --git a/interfaces.go b/interfaces.go index 20e5d2e..6e157ce 100644 --- a/interfaces.go +++ b/interfaces.go @@ -18,6 +18,22 @@ type LiquidProvider interface { Liquid(pos cube.Pos) (world.Liquid, bool) } +// MovementCollisionContext contains player-dependent state needed by dynamic +// collision shapes such as scaffolding and powder snow. +type MovementCollisionContext struct { + Position [3]float64 + Sneaking bool + Descending bool + WantDown bool + LeatherBoots bool +} + +// MovementCollisionProvider optionally resolves collision boxes whose shape +// depends on current player input or equipment. +type MovementCollisionProvider interface { + GetMovementBBoxes(aabb cube.BBox, context MovementCollisionContext) []cube.BBox +} + // BlockSemanticsProvider resolves movement-relevant block behavior. Implement // this when names, friction, or climbability come from a per-world registry or // custom block data instead of Dragonfly's default block types. @@ -44,3 +60,20 @@ type InventoryProvider interface { type DepthStriderProvider interface { DepthStriderLevel() int } + +// MovementEnchantment identifies enchantments that directly affect movement. +type MovementEnchantment uint8 + +const ( + EnchantmentDepthStrider MovementEnchantment = iota + EnchantmentSoulSpeed + EnchantmentSwiftSneak + EnchantmentRiptide +) + +// MovementEquipmentProvider exposes equipment and enchantments whose effects +// are part of client movement physics. +type MovementEquipmentProvider interface { + EnchantmentLevel(enchantment MovementEnchantment) int + WearingLeatherBoots() bool +} diff --git a/liquid.go b/liquid.go index 82d5b5c..46e2269 100644 --- a/liquid.go +++ b/liquid.go @@ -50,7 +50,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, // Captured before updateSwimTravel, matching the upstream ordering. jumping := state.EffectiveJumping if water { - if state.WantDown || state.WantDownSlow { + if state.WantDown || state.WantDownSlow || state.PressingDescend { vel := state.Vel vel[1] -= 0.04 state.SetVel(vel) @@ -82,11 +82,13 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, if state.Swimming && state.SwimSpeedMultiplier != 0 { swimSpeedMultiplier = state.SwimSpeedMultiplier } - if inventory, ok := s.Inventory.(DepthStriderProvider); ok { + if s.Equipment != nil { + depthStriderLevel = math.Min(math.Max(float64(s.Equipment.EnchantmentLevel(EnchantmentDepthStrider)), 0), 3) + } else if inventory, ok := s.Inventory.(DepthStriderProvider); ok { depthStriderLevel = math.Min(math.Max(float64(inventory.DepthStriderLevel()), 0), 3) - if !state.OnGround { - depthStriderLevel *= 0.5 - } + } + if !state.OnGround { + depthStriderLevel *= 0.5 } depthStriderFraction := depthStriderLevel / 3 if swimSpeedMultiplier > 1 { @@ -133,14 +135,17 @@ 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()} raisedBox := state.BoundingBox(s.Options.UseSlideOffset).Translate(raised) - hasCollision := hasNearbyBBoxes(s.World, raisedBox) + hasCollision := len(s.nearbyBBoxes(state, raisedBox)) > 0 hasLiquid := s.containsAnyLiquid(raisedBox) s.debugf("liquid exit probe collision=%t liquid=%t box=%v", hasCollision, hasLiquid, raisedBox) if !hasCollision && !hasLiquid { vel[1] = 0.3 } + state.RiptideTicks = 0 } state.SetVel(vel) + s.applyBubbleColumns(state) + s.applyInsideBlockEffects(state) state.FallDistance = 0 } diff --git a/movement.go b/movement.go index 0f48fe8..42a36d8 100644 --- a/movement.go +++ b/movement.go @@ -29,6 +29,12 @@ type MovementState struct { SlideOffset mgl64.Vec2 Impulse mgl64.Vec2 Size mgl64.Vec3 + // StandingHeight, SneakingHeight, and CrawlingHeight preserve custom entity + // dimensions across pose transitions. Zero values use the current standing + // height and vanilla player pose heights respectively. + StandingHeight float64 + SneakingHeight float64 + CrawlingHeight float64 SupportingBlockPos *cube.Pos @@ -62,6 +68,8 @@ type MovementState struct { ServerSprint, ServerSprintApplied bool Sneaking, PressingSneak bool + PressingAscend bool + PressingDescend bool Jumping, PressingJump bool EffectiveJumping bool @@ -86,6 +94,14 @@ type MovementState struct { GlideBoostTicks int64 HasGravity bool + // SlowFalling reports whether the slow-falling effect is active. Its lower + // gravity only applies while descending. + SlowFalling bool + + Crawling bool + TicksSinceCanSlowdown int + RiptideTicks int + StartingSpinAttack bool Flying, MayFly, TrustFlyStatus bool JustDisabledFlight bool @@ -102,6 +118,22 @@ type MovementState struct { GameMode int32 } +func (s *MovementState) ensurePoseHeights() { + if s.StandingHeight <= 0 { + if !s.Sneaking && !s.Crawling && s.Size.Y() > 0 { + s.StandingHeight = s.Size.Y() + } else { + s.StandingHeight = 1.8 + } + } + if s.SneakingHeight <= 0 { + s.SneakingHeight = 1.5 + } + if s.CrawlingHeight <= 0 { + s.CrawlingHeight = 0.6 + } +} + func (s *MovementState) SetPos(newPos mgl64.Vec3) { s.LastPos = s.Pos s.Pos = newPos diff --git a/movement_environment_test.go b/movement_environment_test.go new file mode 100644 index 0000000..252e235 --- /dev/null +++ b/movement_environment_test.go @@ -0,0 +1,51 @@ +package bedsim + +import ( + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +type environmentWorld struct { + bubbles map[cube.Pos]BubbleColumnDirection + solids map[cube.Pos]bool + blocks map[cube.Pos]world.Block +} + +func (w environmentWorld) Block(pos cube.Pos) world.Block { + if b, ok := w.blocks[pos]; ok { + return b + } + if w.solids[pos] { + return block.Stone{} + } + return block.Air{} +} + +func (w environmentWorld) BlockCollisions(pos cube.Pos) []cube.BBox { + if w.solids[pos] { + return []cube.BBox{cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())} + } + return nil +} + +func (w environmentWorld) GetNearbyBBoxes(cube.BBox) []cube.BBox { return nil } +func (w environmentWorld) IsChunkLoaded(int32, int32) bool { return true } + +func (w environmentWorld) Liquid(pos cube.Pos) (world.Liquid, bool) { + liquid, ok := w.Block(pos).(world.Liquid) + return liquid, ok +} + +func (w environmentWorld) BubbleColumn(pos cube.Pos) (BubbleColumnDirection, bool) { + direction, ok := w.bubbles[pos] + return direction, ok +} + +type fixedEquipment map[MovementEnchantment]int + +func (e fixedEquipment) EnchantmentLevel(enchantment MovementEnchantment) int { + return e[enchantment] +} + +func (fixedEquipment) WearingLeatherBoots() bool { return false } diff --git a/parity_test.go b/parity_test.go new file mode 100644 index 0000000..7666c31 --- /dev/null +++ b/parity_test.go @@ -0,0 +1,118 @@ +package bedsim + +import ( + "math" + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type fixedEffects map[int32]int32 + +func (e fixedEffects) GetEffect(effectID int32) (int32, bool) { + amplifier, ok := e[effectID] + return amplifier, ok +} + +func TestJumpBoostUsesZeroBasedEffectAmplifier(t *testing.T) { + sim := &Simulator{Effects: fixedEffects{packet.EffectJumpBoost: 0}} + state := newBaseState() + + sim.applyInput(state, InputState{}) + + if want := 0.52; math.Abs(state.JumpHeight-want) > 1e-12 { + t.Fatalf("expected jump boost I height %v, got %v", want, state.JumpHeight) + } +} + +func TestLevitationUsesZeroBasedEffectAmplifier(t *testing.T) { + sim := &Simulator{World: mockWorld{}, Effects: fixedEffects{packet.EffectLevitation: 0}} + state := newBaseState() + state.HasGravity = false + + sim.SimulateState(state) + + if want := 0.01; math.Abs(state.Vel.Y()-want) > 1e-12 { + t.Fatalf("expected levitation I velocity %v, got %v", want, state.Vel.Y()) + } +} + +func TestSlowFallingOnlyChangesGravityWhileDescending(t *testing.T) { + sim := &Simulator{World: mockWorld{}, Effects: fixedEffects{packet.EffectSlowFalling: 0}} + state := newBaseState() + state.Vel = mgl64.Vec3{0, 0.2} + state.Gravity = NormalGravity + state.SlowFalling = true + + sim.SimulateState(state) + + if want := (0.2 - NormalGravity) * NormalGravityMultiplier; math.Abs(state.Vel.Y()-want) > 1e-12 { + t.Fatalf("expected normal gravity while ascending, want %v, got %v", want, state.Vel.Y()) + } +} + +func TestBedrockStepHeight(t *testing.T) { + if want := 0.5625; StepHeight != want { + t.Fatalf("expected Bedrock step height %v, got %v", want, StepHeight) + } +} + +func TestBedBounceUsesBedrockRestitutionAndCap(t *testing.T) { + sim := &Simulator{BlockSemantics: overrideBlockSemantics{name: "minecraft:bed"}} + state := newBaseState() + state.Vel = mgl64.Vec3{0, -2} + + sim.landOnBlock(state, state.Vel, block.Air{}) + + if want := 0.75; state.Vel.Y() != want { + t.Fatalf("expected bed bounce %v, got %v", want, state.Vel.Y()) + } +} + +func TestTinyVelocityIsNotDiscardedPrematurely(t *testing.T) { + sim := &Simulator{World: mockWorld{}} + state := newBaseState() + state.HasGravity = false + state.Vel = mgl64.Vec3{1e-7, 0, 0} + + sim.SimulateState(state) + + if state.Vel.X() == 0 { + t.Fatal("expected Bedrock-scale tiny velocity to remain non-zero") + } +} + +func TestSlowFallingChangesGlideGravity(t *testing.T) { + sim := &Simulator{World: mockWorld{}, Inventory: mockInventory{hasElytra: true}} + state := newBaseState() + state.Gliding = true + state.Gravity = NormalGravity + state.SlowFalling = true + state.Vel[1] = -0.01 + + sim.SimulateState(state) + + if want := -0.011025; math.Abs(state.Vel.Y()-want) > 1e-9 { + t.Fatalf("expected slow-falling glide velocity %v, got %v", want, state.Vel.Y()) + } +} + +func TestSneakEdgeProtectionWhileSlightlyAboveGround(t *testing.T) { + sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox{ + cube.Box(-1, -1, -1, 0, 0, 1), + }}} + state := newBaseState() + state.Sneaking = true + state.OnGround = false + state.FallDistance = 0.1 + state.Vel = mgl64.Vec3{0.5, 0, 0} + + sim.avoidEdge(state) + + if state.Vel.X() >= 0.5 { + t.Fatalf("expected edge protection above nearby ground, got velocity %v", state.Vel) + } +} diff --git a/player_features_test.go b/player_features_test.go new file mode 100644 index 0000000..55e38e3 --- /dev/null +++ b/player_features_test.go @@ -0,0 +1,113 @@ +package bedsim + +import ( + "math" + "testing" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +func TestSoulSpeedSkipsSoulSandSlowdown(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{ + {0, 0, 0}: namedBlock{name: "minecraft:soul_sand"}, + }} + base := newBaseState() + base.Pos = mgl64.Vec3{0.5, 1, 0.5} + base.OnGround = true + base.HasGravity = false + + without := *base + (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).Simulate(&without, InputState{MoveVector: mgl64.Vec2{0, 1}}) + + with := *base + (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}, Equipment: fixedEquipment{EnchantmentSoulSpeed: 1}}).Simulate(&with, InputState{MoveVector: mgl64.Vec2{0, 1}}) + + if with.Vel.Z() <= without.Vel.Z() { + t.Fatalf("expected Soul Speed to bypass soul-sand slowdown: with=%v without=%v", with.Vel.Z(), without.Vel.Z()) + } +} + +func TestSwiftSneakAppliesAfterTwoSlowdownTicks(t *testing.T) { + sim := &Simulator{Equipment: fixedEquipment{EnchantmentSwiftSneak: 3}} + state := newBaseState() + input := InputState{SneakDown: true, MoveVector: mgl64.Vec2{0, 1}} + + sim.applyInput(state, input) + if want := 0.3 * 0.98; math.Abs(state.Impulse.Y()-want) > 1e-12 { + t.Fatalf("expected first-tick sneak impulse %v, got %v", want, state.Impulse.Y()) + } + sim.applyInput(state, input) + sim.applyInput(state, input) + if want := 0.75 * 0.98; math.Abs(state.Impulse.Y()-want) > 1e-12 { + t.Fatalf("expected Swift Sneak impulse %v after two ticks, got %v", want, state.Impulse.Y()) + } +} + +func TestItemUseAndInventoryActionInputRules(t *testing.T) { + tests := []struct { + name string + input InputState + want float64 + }{ + {name: "using item", input: InputState{UsingItem: true, MoveVector: mgl64.Vec2{0, 1}}, want: MaxConsumingImpulse * 0.98}, + {name: "using spear", input: InputState{UsingItem: true, UsingSpear: true, MoveVector: mgl64.Vec2{0, 1}}, want: 0.98}, + {name: "inventory action", input: InputState{InventoryAction: true, MoveVector: mgl64.Vec2{0, 1}}, want: 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := newBaseState() + (&Simulator{}).applyInput(state, tt.input) + if math.Abs(state.Impulse.Y()-tt.want) > 1e-12 { + t.Fatalf("expected impulse %v, got %v", tt.want, state.Impulse.Y()) + } + }) + } +} + +func TestCrawlingUpdatesPoseAndSlowdown(t *testing.T) { + state := newBaseState() + (&Simulator{}).applyInput(state, InputState{StartCrawling: true, MoveVector: mgl64.Vec2{0, 1}}) + + if !state.Crawling || state.Size.Y() != 0.6 { + t.Fatalf("expected crawling pose, got crawling=%v size=%v", state.Crawling, state.Size) + } + if want := 0.3 * 0.98; math.Abs(state.Impulse.Y()-want) > 1e-12 { + t.Fatalf("expected crawling slowdown %v, got %v", want, state.Impulse.Y()) + } +} + +func TestLevitationStopsGliding(t *testing.T) { + sim := &Simulator{ + World: mockWorld{}, + Inventory: mockInventory{hasElytra: true}, + Effects: fixedEffects{packet.EffectLevitation: 0}, + } + state := newBaseState() + state.Gliding = true + state.HasGravity = true + + sim.SimulateState(state) + + if state.Gliding { + t.Fatal("expected levitation to stop gliding") + } + if state.Vel.Y() <= 0 { + t.Fatalf("expected levitation velocity after glide stops, got %v", state.Vel) + } +} + +func TestStoppingGlideDoesNotCancelActiveBoost(t *testing.T) { + state := newBaseState() + state.Gliding = true + state.GlideBoostTicks = 10 + + (&Simulator{}).applyInput(state, InputState{StopGliding: true}) + + if state.GlideBoostTicks != 10 { + t.Fatalf("expected glide boost to keep ticking independently, got %d", state.GlideBoostTicks) + } +} diff --git a/simulation.go b/simulation.go index ad1712e..7a2d0f1 100644 --- a/simulation.go +++ b/simulation.go @@ -115,6 +115,7 @@ func (s *Simulator) resultFromState(state *MovementState, outcome SimulationOutc } func (s *Simulator) applyInput(state *MovementState, input InputState) { + state.ensurePoseHeights() state.Client.HorizontalCollision = input.HorizontalCollision state.Client.VerticalCollision = input.VerticalCollision @@ -142,6 +143,8 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { state.PressingSneak = input.Sneaking state.PressingSprint = input.SprintDown + state.PressingAscend = input.AscendBlock || input.Jumping + state.PressingDescend = input.DescendBlock || input.Sneaking startFlag, stopFlag := input.StartSprinting, input.StopSprinting needsSpeedAdjusted := false @@ -179,10 +182,40 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { if input.StartSneaking { state.Sneaking = true + if !state.Crawling { + state.Size[1] = state.SneakingHeight + } } else if input.StopSneaking { - state.Sneaking = false + if state.Crawling { + state.Sneaking = false + } else if s.canFitHeight(state, state.StandingHeight) { + state.Sneaking = false + state.Size[1] = state.StandingHeight + } else { + state.Sneaking = true + state.Size[1] = state.SneakingHeight + } } else { state.Sneaking = input.SneakDown + if state.Sneaking && !state.Crawling { + state.Size[1] = state.SneakingHeight + } + } + if input.StartCrawling { + state.Crawling = true + state.Size[1] = state.CrawlingHeight + } else if input.StopCrawling { + targetHeight := state.StandingHeight + if state.Sneaking { + targetHeight = state.SneakingHeight + } + if s.canFitHeight(state, targetHeight) { + state.Crawling = false + state.Size[1] = targetHeight + } else { + state.Crawling = true + state.Size[1] = state.CrawlingHeight + } } wasSwimming := state.Swimming @@ -204,17 +237,27 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { // Preserve bedsim's public impulse clamps unless upstream behavior is opted in. maxImpulse := 1.0 if !s.Options.UpstreamImpulseClamping { - if input.UsingConsumable { + if input.UsingConsumable || (input.UsingItem && !input.UsingSpear) { maxImpulse *= MaxConsumingImpulse } - if state.Sneaking { - maxImpulse *= MaxSneakImpulse + if state.Sneaking || state.Crawling || state.Gliding { + state.TicksSinceCanSlowdown++ + sneakMultiplier := MaxSneakImpulse + if state.TicksSinceCanSlowdown > 2 && s.Equipment != nil { + sneakMultiplier += 0.15 * float64(s.Equipment.EnchantmentLevel(EnchantmentSwiftSneak)) + } + maxImpulse *= ClampFloat(sneakMultiplier, 0, 1) + } else { + state.TicksSinceCanSlowdown = 0 } } moveVector := mgl64.Vec2{ ClampFloat(input.MoveVector[0], -maxImpulse, maxImpulse), ClampFloat(input.MoveVector[1], -maxImpulse, maxImpulse), } + if input.InventoryAction { + moveVector = mgl64.Vec2{} + } // Ground jumps are edge-triggered; liquid and ladder ascent may be held. state.Jumping = input.StartJumping @@ -223,7 +266,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 += float64(amp+1) * 0.1 } } @@ -231,19 +274,25 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { state.JumpDelay = 0 } state.Gravity = NormalGravity + state.SlowFalling = false if s.Effects != nil { if _, ok := s.Effects.GetEffect(packet.EffectSlowFalling); ok { - state.Gravity = SlowFallingGravity + state.SlowFalling = true } } if input.StopGliding { state.Gliding = false - state.GlideBoostTicks = 0 } else if input.StartGliding { state.Gliding = true } + state.StartingSpinAttack = input.StartSpinAttack + if input.StopSpinAttack && state.RiptideTicks > 0 { + state.RiptideTicks = 0 + state.SetVel(state.Vel.Mul(-0.2)) + } + state.Impulse = moveVector.Mul(0.98) } @@ -286,13 +335,20 @@ func (s *Simulator) tickState(state *MovementState) { if state.JumpDelay > 0 { state.JumpDelay-- } + if state.RiptideTicks > 0 { + state.RiptideTicks-- + } state.JustDisabledFlight = false } func (s *Simulator) simulateMovement(state *MovementState) { - if state.Vel.LenSqr() < 1e-12 { - state.SetVel(mgl64.Vec3{}) + vel := state.Vel + for axis := range 3 { + if math.Abs(vel[axis]) < 1e-8 { + vel[axis] = 0 + } } + state.SetVel(vel) // Bound retained water evidence before collision and travel inspect it. grace := s.swimWaterGraceTicks() @@ -302,6 +358,9 @@ func (s *Simulator) simulateMovement(state *MovementState) { waterBlocks := s.touchingLiquidBlocks(state, liquidWater) lavaBlocks := s.touchingLiquidBlocks(state, liquidLava) + if s.attemptRiptide(state, len(waterBlocks) != 0 || len(lavaBlocks) != 0) { + s.debugf("riptide launch applied: %v", state.Vel) + } inWater := len(waterBlocks) != 0 defer func() { @@ -337,13 +396,18 @@ func (s *Simulator) simulateMovement(state *MovementState) { moveRelativeSpeed := state.AirSpeed if state.OnGround { mSpeed := state.MovementSpeed - if s.blockName(blockUnder) == "minecraft:soul_sand" { + if s.blockName(blockUnder) == "minecraft:soul_sand" && (s.Equipment == nil || s.Equipment.EnchantmentLevel(EnchantmentSoulSpeed) == 0) { mSpeed *= 0.543 } blockFriction *= s.blockFriction(blockUnder) moveRelativeSpeed = mSpeed * (0.16277136 / (blockFriction * blockFriction * blockFriction)) } + if state.Gliding && s.Effects != nil { + if _, levitating := s.Effects.GetEffect(packet.EffectLevitation); levitating { + state.Gliding = false + } + } if state.Gliding { hasElytra := s.Inventory != nil && s.Inventory.HasElytra() if hasElytra && !state.OnGround { @@ -368,6 +432,9 @@ func (s *Simulator) simulateMovement(state *MovementState) { moveRelative(state, moveRelativeSpeed) 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) + insideName := s.blockName(s.blockAtPos(cube.PosFromVec3(state.Pos))) + leatherBoots := s.Equipment != nil && s.Equipment.WearingLeatherBoots() + applyAscendableMovement(state, insideName, leatherBoots) nearClimbable := s.blockClimbable(s.blockAtPos(cube.PosFromVec3(state.Pos))) if nearClimbable { @@ -390,9 +457,15 @@ func (s *Simulator) simulateMovement(state *MovementState) { if inCobweb { newVel := state.Vel - newVel[0] *= 0.25 - newVel[1] *= 0.05 - newVel[2] *= 0.25 + xz, y := 0.25, 0.05 + if s.Effects != nil { + if _, weaving := s.Effects.GetEffect(EffectWeaving); weaving { + xz, y = 0.5, 0.25 + } + } + newVel[0] *= xz + newVel[1] *= y + newVel[2] *= xz state.SetVel(newVel) s.debugf("cobweb force applied (vel=%v)", newVel) } @@ -409,7 +482,7 @@ func (s *Simulator) simulateMovement(state *MovementState) { blockUnder = s.blockAtPos(*state.SupportingBlockPos) } else { blockUnder = s.blockAtPos(cube.PosFromVec3(state.Pos.Sub(mgl64.Vec3{0, 0.2}))) - if _, isAir := blockUnder.(block.Air); isAir { + if s.blockAir(blockUnder) { below := s.blockAtPos(cube.PosFromVec3(state.Pos).Side(cube.FaceDown)) if IsWall(below) || IsFence(below) { blockUnder = below @@ -434,19 +507,20 @@ func (s *Simulator) simulateMovement(state *MovementState) { newVel := state.Vel if s.Effects != nil { if amp, ok := s.Effects.GetEffect(packet.EffectLevitation); ok { - levSpeed := LevitationGravityMultiplier * float64(amp) + levSpeed := LevitationGravityMultiplier * float64(amp+1) newVel[1] += (levSpeed - newVel[1]) * 0.2 } else if state.HasGravity { - newVel[1] -= state.Gravity + newVel[1] -= effectiveGravity(state, newVel) newVel[1] *= NormalGravityMultiplier } } else if state.HasGravity { - newVel[1] -= state.Gravity + newVel[1] -= effectiveGravity(state, newVel) newVel[1] *= NormalGravityMultiplier } newVel[0] *= blockFriction newVel[2] *= blockFriction state.SetVel(newVel) + s.applyInsideBlockEffects(state) } func (s *Simulator) simulationIsReliable(state *MovementState) bool { @@ -457,7 +531,7 @@ func (s *Simulator) simulationIsReliable(state *MovementState) bool { stateBB := state.BoundingBox(s.Options.UseSlideOffset) isReliable := true for _, b := range nearbyBlocks(stateBB.Grow(1), s.World) { - if _, isAir := b.(block.Air); isAir { + if s.blockAir(b) { continue } if s.blockName(b) == "minecraft:bamboo" { @@ -543,7 +617,8 @@ func (s *Simulator) simulateGlide(state *MovementState) { lookHz := pitchCos sqrPitchCos := pitchCos * pitchCos - vel[1] += -0.08 + sqrPitchCos*0.06 + gravity := effectiveGravity(state, vel) + vel[1] += -gravity + sqrPitchCos*(gravity*0.75) if vel[1] < 0 && lookHz > 0 { yAccel := vel[1] * -0.1 * sqrPitchCos vel[1] += yAccel @@ -586,7 +661,7 @@ func (s *Simulator) walkOnBlock(state *MovementState, blockUnder world.Block) { oldVel := state.Vel newVel := state.Vel switch s.blockName(blockUnder) { - case "minecraft:slime": + case "minecraft:slime", "minecraft:honey_block": yMov := math.Abs(newVel.Y()) if yMov < 0.1 && !state.PressingSneak { d1 := 0.4 + yMov*0.2 @@ -613,13 +688,20 @@ func (s *Simulator) landOnBlock(state *MovementState, old mgl64.Vec3, blockUnder newVel[1] = 0.0 } case "minecraft:bed": - newVel[1] = math.Min(1.0, BedBounceMultiplier*old.Y()) + newVel[1] = math.Min(0.75, BedBounceMultiplier*old.Y()) default: newVel[1] = 0 } state.SetVel(newVel) } +func effectiveGravity(state *MovementState, velocity mgl64.Vec3) float64 { + if state.SlowFalling && velocity.Y() < 0 { + return SlowFallingGravity + } + return state.Gravity +} + func (s *Simulator) setPostCollisionMotion(state *MovementState, oldVel mgl64.Vec3, oldOnGround bool, blockUnder world.Block) { if !oldOnGround && state.CollideY { s.landOnBlock(state, oldVel, blockUnder) @@ -684,7 +766,13 @@ func (s *Simulator) attemptJump(state *MovementState, clientJumpPrevented *bool) } newVel := state.Vel - newVel[1] = math.Max(state.JumpHeight, newVel[1]) + jumpHeight := state.JumpHeight + inBlock := s.blockAtPos(cube.PosFromVec3(state.Pos)) + below := s.blockAtPos(cube.PosFromVec3(state.Pos.Sub(mgl64.Vec3{0, 0.1}))) + if s.blockName(inBlock) == "minecraft:honey_block" || s.blockName(below) == "minecraft:honey_block" { + jumpHeight *= 0.6 + } + newVel[1] = math.Max(jumpHeight, newVel[1]) state.JumpDelay = JumpDelayTicks if state.Sprinting { @@ -711,7 +799,7 @@ func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl64.Vec3) bool } useSlideOffset := s.Options.UseSlideOffset collisionBB := state.BoundingBox(useSlideOffset) - bbList := w.GetNearbyBBoxes(collisionBB.Extend(jumpVel)) + bbList := s.nearbyBBoxes(state, collisionBB.Extend(jumpVel)) yVel := mgl64.Vec3{0, jumpVel.Y()} xVel := mgl64.Vec3{jumpVel.X()} @@ -767,7 +855,7 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool var completedStep bool collisionBB := state.BoundingBox(useSlideOffset) currVel := state.Vel - bbList := w.GetNearbyBBoxes(collisionBB.Extend(currVel)) + bbList := s.nearbyBBoxes(state, collisionBB.Extend(currVel)) useOneWayCollisions := state.StuckInCollider penetration := mgl64.Vec3{} @@ -850,10 +938,10 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool newBBListCount := 0 hasStepCollisions := false if s.Options.Debugf != nil { - newBBListCount = len(w.GetNearbyBBoxes(stepBB)) + newBBListCount = len(s.nearbyBBoxes(state, stepBB)) hasStepCollisions = newBBListCount > 0 } else { - hasStepCollisions = hasNearbyBBoxes(w, stepBB) + hasStepCollisions = len(s.nearbyBBoxes(state, stepBB)) > 0 } stepPos := mgl64.Vec3{ (stepBB.Min().X() + stepBB.Max().X()) * 0.5, @@ -926,7 +1014,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { if w == nil { return } - if !state.Sneaking || !state.OnGround || state.Vel.Y() > 0 { + if !state.Sneaking || !s.isAboveGround(state) || state.Vel.Y() > 0 { s.debugf( "avoidEdge: conditions not met (sneaking=%v onGround=%v yVel=%v)", state.Sneaking, @@ -949,7 +1037,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { 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 && len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{xMov, -StepHeight * 1.01, 0}))) == 0; i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -962,7 +1050,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 && len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{0, -StepHeight * 1.01, zMov}))) == 0; i++ { if zMov < offset && zMov >= -offset { zMov = 0 } else if zMov > 0 { @@ -975,7 +1063,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 && len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{xMov, -StepHeight * 1.01, zMov}))) == 0; i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -1003,6 +1091,18 @@ func (s *Simulator) avoidEdge(state *MovementState) { s.debugf("(avoidEdge): oldVel=%v newVel=%v", oldVel, newVel) } +func (s *Simulator) isAboveGround(state *MovementState) bool { + if state.OnGround { + return true + } + if state.FallDistance >= 0.6 || s.World == nil { + return false + } + distance := 0.6 - state.FallDistance + bb := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl64.Vec3{-0.025, 0, -0.025}) + return len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{0, -distance}))) > 0 +} + func (s *Simulator) isInsideCobweb(state *MovementState) bool { if s.World == nil { return false @@ -1011,19 +1111,16 @@ func (s *Simulator) isInsideCobweb(state *MovementState) bool { bb := state.BoundingBox(s.Options.UseSlideOffset) insideCobweb := false for pos, b := range nearbyBlocks(bb.Grow(1), s.World) { - if _, isAir := b.(block.Air); isAir { + if s.blockAir(b) { continue } - if s.blockName(b) != "minecraft:web" { + name := s.blockName(b) + if name != "minecraft:web" && name != "minecraft:cobweb" { continue } - boxes := s.World.BlockCollisions(pos) - for _, box := range boxes { - if bb.IntersectsWith(box.Translate(pos.Vec3())) { - insideCobweb = true - break - } + if bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())) { + insideCobweb = true } if insideCobweb { break @@ -1105,6 +1202,40 @@ func (s *Simulator) blockAtPos(pos cube.Pos) world.Block { return s.World.Block(pos) } +func (s *Simulator) nearbyBBoxes(state *MovementState, aabb cube.BBox) []cube.BBox { + if s.World == nil { + return nil + } + if provider, ok := s.World.(MovementCollisionProvider); ok { + leatherBoots := s.Equipment != nil && s.Equipment.WearingLeatherBoots() + return provider.GetMovementBBoxes(aabb, MovementCollisionContext{ + Position: [3]float64(state.Pos), + Sneaking: state.Sneaking, + Descending: state.PressingDescend, + WantDown: state.WantDown, + LeatherBoots: leatherBoots, + }) + } + return s.World.GetNearbyBBoxes(aabb) +} + +func (s *Simulator) canStand(state *MovementState) bool { + state.ensurePoseHeights() + return s.canFitHeight(state, state.StandingHeight) +} + +func (s *Simulator) canFitHeight(state *MovementState, height float64) bool { + if s.World == nil { + return true + } + standing := *state + standing.Size[1] = height + standing.Sneaking = false + standing.PressingDescend = false + standing.WantDown = false + return len(s.nearbyBBoxes(&standing, standing.BoundingBox(s.Options.UseSlideOffset))) == 0 +} + type nearbyBBoxProbe interface { HasNearbyBBoxes(aabb cube.BBox) bool } diff --git a/simulator.go b/simulator.go index f633d90..2049df4 100644 --- a/simulator.go +++ b/simulator.go @@ -3,6 +3,7 @@ package bedsim import ( "math" + "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/world" ) @@ -70,6 +71,7 @@ type Simulator struct { Liquids LiquidProvider Effects EffectsProvider Inventory InventoryProvider + Equipment MovementEquipmentProvider Options SimulationOptions } @@ -120,3 +122,15 @@ func (s *Simulator) blockClimbable(b world.Block) bool { } return BlockClimbable(b) } + +func (s *Simulator) blockAir(b world.Block) bool { + if _, ok := b.(block.Air); ok { + return true + } + switch s.blockName(b) { + case "minecraft:air", "minecraft:cave_air", "minecraft:void_air": + return true + default: + return false + } +} diff --git a/simulator_test.go b/simulator_test.go index 5425a21..53acb8e 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -508,7 +508,7 @@ func TestSimulateStateDebugTraceJumpBlocked(t *testing.T) { 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). + // The step-up (0.5 blocks) is within StepHeight (0.5625). slabBox := cube.Box(1, 0, -1, 2, 0.5, 2) groundBox := cube.Box(-1, -1, -1, 1, 0, 2) From f6d99e6aed28985bf57ac6b4cb364db2646f78a3 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 20 Jul 2026 20:51:03 -0400 Subject: [PATCH 2/9] fix: preserve legacy depth strider fallback --- README.md | 4 ++-- liquid.go | 7 +++++-- liquid_test.go | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1641718..e47f6f8 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,8 @@ descend intent plus leather-boots state. `MovementEquipmentProvider` supplies Depth Strider, Soul Speed, Swift Sneak, Riptide, and leather-boots checks. The legacy `DepthStriderProvider` inventory -extension remains supported when `Equipment` is nil. `EffectsProvider` also -controls Weaving-aware cobweb movement. +extension remains a fallback when the equipment provider reports no Depth +Strider level. `EffectsProvider` also controls Weaving-aware cobweb movement. Pose changes update `MovementState.Size`. Set `StandingHeight`, `SneakingHeight`, or `CrawlingHeight` when using non-vanilla dimensions; zero diff --git a/liquid.go b/liquid.go index 46e2269..92ccf2b 100644 --- a/liquid.go +++ b/liquid.go @@ -84,8 +84,11 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, } if s.Equipment != nil { depthStriderLevel = math.Min(math.Max(float64(s.Equipment.EnchantmentLevel(EnchantmentDepthStrider)), 0), 3) - } else if inventory, ok := s.Inventory.(DepthStriderProvider); ok { - depthStriderLevel = math.Min(math.Max(float64(inventory.DepthStriderLevel()), 0), 3) + } + if depthStriderLevel == 0 { + if inventory, ok := s.Inventory.(DepthStriderProvider); ok { + depthStriderLevel = math.Min(math.Max(float64(inventory.DepthStriderLevel()), 0), 3) + } } if !state.OnGround { depthStriderLevel *= 0.5 diff --git a/liquid_test.go b/liquid_test.go index 07cf173..de49d54 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -784,6 +784,21 @@ func TestInventoryWithoutDepthStriderProvider(t *testing.T) { } } +func TestZeroEquipmentDepthStriderFallsBackToLegacyInventory(t *testing.T) { + sim := newLiquidSim(filledColumn(waterSource)) + sim.Inventory = depthStriderInventory{level: 3} + sim.Equipment = fixedEquipment{} + state := submergedState() + state.Vel = mgl64.Vec3{0.5, 0, 0} + state.OnGround = true + + sim.SimulateState(state) + + if !approxEqual(state.Vel.X(), 0.5*0.54600006) { + t.Fatalf("legacy depth strider X = %v, want level-3 behavior", state.Vel.X()) + } +} + // A dolphin boost raises the swim speed multiplier, which only takes effect // while actually swimming. func TestSwimSpeedMultiplierRequiresSwimming(t *testing.T) { From 2c01d35de52af83e48f59fff2342c06f2a570e27 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 20 Jul 2026 21:07:08 -0400 Subject: [PATCH 3/9] fix: keep movement semantics Bedrock-native --- README.md | 4 +-- bedrock_semantics_test.go | 66 +++++++++++++++++++++++++++++++++++++++ block_effects.go | 14 ++------- block_effects_test.go | 9 ++---- dynamic_collision_test.go | 4 +-- movement.go | 2 +- player_features_test.go | 5 ++- simulation.go | 35 ++++++++++----------- simulator.go | 7 +---- 9 files changed, 98 insertions(+), 48 deletions(-) create mode 100644 bedrock_semantics_test.go diff --git a/README.md b/README.md index e47f6f8..8229af8 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ descend intent plus leather-boots state. `MovementEquipmentProvider` supplies Depth Strider, Soul Speed, Swift Sneak, Riptide, and leather-boots checks. The legacy `DepthStriderProvider` inventory extension remains a fallback when the equipment provider reports no Depth -Strider level. `EffectsProvider` also controls Weaving-aware cobweb movement. +Strider level. `EffectsProvider` also controls Weaving-aware web movement. Pose changes update `MovementState.Size`. Set `StandingHeight`, `SneakingHeight`, or `CrawlingHeight` when using non-vanilla dimensions; zero @@ -109,7 +109,7 @@ values preserve the current standing height and use vanilla crouch/crawl heights. Movement-sensitive block behavior includes honey blocks, sweet berry bushes, -powder snow, scaffolding, cobwebs (including Weaving), soul sand with Soul +powder snow, scaffolding, webs (including Weaving), soul sand with Soul Speed, slime blocks, beds, climbables, fences/walls, and per-block friction. Dynamic collision behavior still depends on the world adapter returning the correct shapes for the current block state. diff --git a/bedrock_semantics_test.go b/bedrock_semantics_test.go new file mode 100644 index 0000000..9fa9552 --- /dev/null +++ b/bedrock_semantics_test.go @@ -0,0 +1,66 @@ +package bedsim + +import ( + "math" + "testing" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +func TestBlockAirRecognisesOnlyBedrockAirIdentifier(t *testing.T) { + sim := &Simulator{BlockSemantics: encodedBlockSemantics{}} + tests := []struct { + name string + want bool + }{ + {name: "minecraft:air", want: true}, + {name: "minecraft:cave_air", want: false}, + {name: "minecraft:void_air", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := sim.blockAir(namedBlock{name: tt.name}); got != tt.want { + t.Fatalf("blockAir(%q) = %v, want %v", tt.name, got, tt.want) + } + }) + } +} + +func TestJavaWebIdentifierHasNoBedrockMovementEffect(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{ + {0, 0, 0}: namedBlock{name: "minecraft:cobweb"}, + }} + state := newBaseState() + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Vel = mgl64.Vec3{0.1, 0, 0} + state.HasGravity = false + + result := (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).SimulateState(state) + + if want := 0.1; math.Abs(result.Movement.X()-want) > 1e-12 { + t.Fatalf("Java web identifier changed Bedrock movement: got %v, want %v", result.Movement.X(), want) + } +} + +func TestDefaultSneakingHeightMatchesDragonflyBedrockPlayer(t *testing.T) { + state := newBaseState() + + (&Simulator{}).applyInput(state, InputState{StartSneaking: true}) + + if state.Size.Y() != 1.49 { + t.Fatalf("sneaking height = %v, want 1.49", state.Size.Y()) + } +} + +func TestCrawlingCannotStartInOpenAir(t *testing.T) { + state := newBaseState() + + (&Simulator{World: environmentWorld{}}).applyInput(state, InputState{StartCrawling: true}) + + if state.Crawling || state.Size.Y() != 1.8 { + t.Fatalf("open-air crawl was accepted: crawling=%v size=%v", state.Crawling, state.Size) + } +} diff --git a/block_effects.go b/block_effects.go index f82c905..d8807c6 100644 --- a/block_effects.go +++ b/block_effects.go @@ -6,7 +6,7 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" ) -func applyInsideBlockMovement(state *MovementState, blockName string, weaving bool) { +func applyInsideBlockMovement(state *MovementState, blockName string) { velocity := state.Vel switch blockName { case "minecraft:honey_block": @@ -21,14 +21,6 @@ func applyInsideBlockMovement(state *MovementState, blockName string, weaving bo velocity[0] *= 0.9 velocity[1] *= 1.5 velocity[2] *= 0.9 - case "minecraft:web", "minecraft:cobweb": - xz, y := 0.25, 0.05 - if weaving { - xz, y = 0.5, 0.25 - } - velocity[0] *= xz - velocity[1] *= y - velocity[2] *= xz } state.SetVel(velocity) } @@ -70,10 +62,10 @@ func (s *Simulator) applyInsideBlockEffects(state *MovementState) { continue } name := s.blockName(b) - if name == "minecraft:web" || name == "minecraft:cobweb" { + if name == "minecraft:web" { continue } - applyInsideBlockMovement(state, name, false) + applyInsideBlockMovement(state, name) } } } diff --git a/block_effects_test.go b/block_effects_test.go index 412305b..286fcb4 100644 --- a/block_effects_test.go +++ b/block_effects_test.go @@ -30,14 +30,11 @@ func TestInsideBlockMovementMultipliers(t *testing.T) { tests := []struct { name string blockName string - weaving bool want mgl64.Vec3 }{ {name: "honey", blockName: "minecraft:honey_block", want: mgl64.Vec3{0.4, -0.12, 0.4}}, {name: "sweet berry bush", blockName: "minecraft:sweet_berry_bush", want: mgl64.Vec3{0.8, -0.75, 0.8}}, {name: "powder snow", blockName: "minecraft:powder_snow", want: mgl64.Vec3{0.9, -1.5, 0.9}}, - {name: "cobweb", blockName: "minecraft:web", want: mgl64.Vec3{0.25, -0.05, 0.25}}, - {name: "cobweb with weaving", blockName: "minecraft:web", weaving: true, want: mgl64.Vec3{0.5, -0.25, 0.5}}, } for _, tt := range tests { @@ -45,7 +42,7 @@ func TestInsideBlockMovementMultipliers(t *testing.T) { state := newBaseState() state.Vel = mgl64.Vec3{1, -1, 1} - applyInsideBlockMovement(state, tt.blockName, tt.weaving) + applyInsideBlockMovement(state, tt.blockName) if state.Vel != tt.want { t.Fatalf("expected velocity %v, got %v", tt.want, state.Vel) @@ -148,7 +145,7 @@ func TestSimulationAppliesScaffoldingTraversal(t *testing.T) { } } -func TestSimulationDetectsNonSolidCobwebAndAppliesWeaving(t *testing.T) { +func TestSimulationDetectsNonSolidWebAndAppliesWeaving(t *testing.T) { w := environmentWorld{blocks: map[cube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:web"}, }} @@ -165,6 +162,6 @@ func TestSimulationDetectsNonSolidCobwebAndAppliesWeaving(t *testing.T) { result := sim.SimulateState(state) if want := 0.05; math.Abs(result.Movement.X()-want) > 1e-12 { - t.Fatalf("expected Weaving cobweb movement %v, got %v", want, result.Movement.X()) + t.Fatalf("expected Weaving web movement %v, got %v", want, result.Movement.X()) } } diff --git a/dynamic_collision_test.go b/dynamic_collision_test.go index dd217e3..5f8b1a8 100644 --- a/dynamic_collision_test.go +++ b/dynamic_collision_test.go @@ -51,11 +51,11 @@ func TestCannotUnsneakUnderLowCeiling(t *testing.T) { }}} state := newBaseState() state.Sneaking = true - state.Size[1] = 1.5 + state.Size[1] = 1.49 sim.applyInput(state, InputState{StopSneaking: true}) - if !state.Sneaking || state.Size.Y() != 1.5 { + if !state.Sneaking || state.Size.Y() != 1.49 { t.Fatalf("expected forced sneak pose under ceiling, got sneaking=%v size=%v", state.Sneaking, state.Size) } } diff --git a/movement.go b/movement.go index 42a36d8..8948c11 100644 --- a/movement.go +++ b/movement.go @@ -127,7 +127,7 @@ func (s *MovementState) ensurePoseHeights() { } } if s.SneakingHeight <= 0 { - s.SneakingHeight = 1.5 + s.SneakingHeight = 1.49 } if s.CrawlingHeight <= 0 { s.CrawlingHeight = 0.6 diff --git a/player_features_test.go b/player_features_test.go index 55e38e3..54e7edb 100644 --- a/player_features_test.go +++ b/player_features_test.go @@ -70,7 +70,10 @@ func TestItemUseAndInventoryActionInputRules(t *testing.T) { func TestCrawlingUpdatesPoseAndSlowdown(t *testing.T) { state := newBaseState() - (&Simulator{}).applyInput(state, InputState{StartCrawling: true, MoveVector: mgl64.Vec2{0, 1}}) + sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox{ + cube.Box(-1, 0.7, -1, 1, 2, 1), + }}} + sim.applyInput(state, InputState{StartCrawling: true, MoveVector: mgl64.Vec2{0, 1}}) if !state.Crawling || state.Size.Y() != 0.6 { t.Fatalf("expected crawling pose, got crawling=%v size=%v", state.Crawling, state.Size) diff --git a/simulation.go b/simulation.go index 7a2d0f1..a21a71a 100644 --- a/simulation.go +++ b/simulation.go @@ -202,8 +202,11 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { } } if input.StartCrawling { - state.Crawling = true - state.Size[1] = state.CrawlingHeight + if !s.canFitHeight(state, state.StandingHeight) { + state.Crawling = true + state.Sneaking = false + state.Size[1] = state.CrawlingHeight + } } else if input.StopCrawling { targetHeight := state.StandingHeight if state.Sneaking { @@ -453,9 +456,9 @@ func (s *Simulator) simulateMovement(state *MovementState) { s.debugf("added climb velocity: %v (collided=%v effectiveJumping=%v)", newVel, state.CollideX || state.CollideZ, state.EffectiveJumping) } - inCobweb := s.isInsideCobweb(state) + inWeb := s.isInsideWeb(state) - if inCobweb { + if inWeb { newVel := state.Vel xz, y := 0.25, 0.05 if s.Effects != nil { @@ -467,7 +470,7 @@ func (s *Simulator) simulateMovement(state *MovementState) { newVel[1] *= y newVel[2] *= xz state.SetVel(newVel) - s.debugf("cobweb force applied (vel=%v)", newVel) + s.debugf("web force applied (vel=%v)", newVel) } s.avoidEdge(state) @@ -499,8 +502,8 @@ func (s *Simulator) simulateMovement(state *MovementState) { state.SetMov(state.Vel) s.setPostCollisionMotion(state, oldVel, oldOnGround, blockUnder) - if inCobweb { - s.debugf("post-move cobweb force applied (0 vel)") + if inWeb { + s.debugf("post-move web force applied (0 vel)") state.SetVel(mgl64.Vec3{}) } @@ -1103,30 +1106,29 @@ func (s *Simulator) isAboveGround(state *MovementState) bool { return len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{0, -distance}))) > 0 } -func (s *Simulator) isInsideCobweb(state *MovementState) bool { +func (s *Simulator) isInsideWeb(state *MovementState) bool { if s.World == nil { return false } bb := state.BoundingBox(s.Options.UseSlideOffset) - insideCobweb := false + insideWeb := false for pos, b := range nearbyBlocks(bb.Grow(1), s.World) { if s.blockAir(b) { continue } - name := s.blockName(b) - if name != "minecraft:web" && name != "minecraft:cobweb" { + if s.blockName(b) != "minecraft:web" { continue } if bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())) { - insideCobweb = true + insideWeb = true } - if insideCobweb { + if insideWeb { break } } - return insideCobweb + return insideWeb } func nearbyBlocks(aabb cube.BBox, w WorldProvider) iter.Seq2[cube.Pos, world.Block] { @@ -1219,11 +1221,6 @@ func (s *Simulator) nearbyBBoxes(state *MovementState, aabb cube.BBox) []cube.BB return s.World.GetNearbyBBoxes(aabb) } -func (s *Simulator) canStand(state *MovementState) bool { - state.ensurePoseHeights() - return s.canFitHeight(state, state.StandingHeight) -} - func (s *Simulator) canFitHeight(state *MovementState, height float64) bool { if s.World == nil { return true diff --git a/simulator.go b/simulator.go index 2049df4..e5ca3ed 100644 --- a/simulator.go +++ b/simulator.go @@ -127,10 +127,5 @@ func (s *Simulator) blockAir(b world.Block) bool { if _, ok := b.(block.Air); ok { return true } - switch s.blockName(b) { - case "minecraft:air", "minecraft:cave_air", "minecraft:void_air": - return true - default: - return false - } + return s.blockName(b) == "minecraft:air" } From 10c3d991a3434d2f8cd579b448a414e94693ef94 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 20 Jul 2026 23:58:34 -0400 Subject: [PATCH 4/9] feat: convert simulation to native float32 math --- bbox.go | 22 ++- bedrock_semantics_test.go | 14 +- block.go | 8 +- block_effects.go | 15 +- block_effects_test.go | 44 +++--- bubble.go | 34 ++--- bubble_test.go | 24 +-- collision.go | 38 ++--- constants.go | 42 +++--- dynamic_collision_test.go | 13 +- go.mod | 2 + go.sum | 4 + input.go | 14 +- interfaces.go | 13 +- liquid.go | 105 ++++++------- liquid_hardening_test.go | 136 ++++++++--------- liquid_test.go | 277 ++++++++++++++++++----------------- math.go | 29 ++-- movement.go | 66 ++++----- movement_environment_test.go | 19 +-- native_float32_test.go | 25 ++++ parity_test.go | 32 ++-- player_features_test.go | 35 ++--- result.go | 12 +- simulation.go | 183 ++++++++++++----------- simulator.go | 14 +- simulator_test.go | 123 ++++++++-------- 27 files changed, 699 insertions(+), 644 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/bedrock_semantics_test.go b/bedrock_semantics_test.go index 9fa9552..7c66005 100644 --- a/bedrock_semantics_test.go +++ b/bedrock_semantics_test.go @@ -1,12 +1,12 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" "testing" - "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 TestBlockAirRecognisesOnlyBedrockAirIdentifier(t *testing.T) { @@ -30,17 +30,17 @@ func TestBlockAirRecognisesOnlyBedrockAirIdentifier(t *testing.T) { } func TestJavaWebIdentifierHasNoBedrockMovementEffect(t *testing.T) { - w := environmentWorld{blocks: map[cube.Pos]world.Block{ + w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:cobweb"}, }} state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 0, 0.5} - state.Vel = mgl64.Vec3{0.1, 0, 0} + state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Vel = mgl32.Vec3{0.1, 0, 0} state.HasGravity = false result := (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).SimulateState(state) - if want := 0.1; math.Abs(result.Movement.X()-want) > 1e-12 { + if want := float32(0.1); math32.Abs(result.Movement.X()-want) > 1e-6 { t.Fatalf("Java web identifier changed Bedrock movement: got %v, want %v", result.Movement.X(), want) } } 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/block_effects.go b/block_effects.go index d8807c6..ff56e63 100644 --- a/block_effects.go +++ b/block_effects.go @@ -1,9 +1,10 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" - "github.com/df-mc/dragonfly/server/block/cube" + dfcube "github.com/df-mc/dragonfly/server/block/cube" + "github.com/ethaniccc/float32-cube/cube" ) func applyInsideBlockMovement(state *MovementState, blockName string) { @@ -50,11 +51,11 @@ func (s *Simulator) applyInsideBlockEffects(state *MovementState) { } bb := state.BoundingBox(s.Options.UseSlideOffset) min, maxPoint := bb.Min(), bb.Max() - for x := int(math.Floor(min.X())); x < int(math.Ceil(maxPoint.X())); x++ { - for y := int(math.Floor(min.Y())); y < int(math.Ceil(maxPoint.Y())); y++ { - for z := int(math.Floor(min.Z())); z < int(math.Ceil(maxPoint.Z())); z++ { - pos := cube.Pos{x, y, z} - if !bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())) { + for x := int(math32.Floor(min.X())); x < int(math32.Ceil(maxPoint.X())); x++ { + for y := int(math32.Floor(min.Y())); y < int(math32.Ceil(maxPoint.Y())); y++ { + for z := int(math32.Floor(min.Z())); z < int(math32.Ceil(maxPoint.Z())); z++ { + pos := dfcube.Pos{x, y, z} + if !bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))) { continue } b := s.World.Block(pos) diff --git a/block_effects_test.go b/block_effects_test.go index 286fcb4..76011e2 100644 --- a/block_effects_test.go +++ b/block_effects_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" ) type namedBlock struct { @@ -23,24 +23,24 @@ func (encodedBlockSemantics) BlockName(b world.Block) string { name, _ := b.EncodeBlock() return name } -func (encodedBlockSemantics) BlockFriction(world.Block) float64 { return DefaultBlockFriction } +func (encodedBlockSemantics) BlockFriction(world.Block) float32 { return DefaultBlockFriction } func (encodedBlockSemantics) BlockClimbable(world.Block) bool { return false } func TestInsideBlockMovementMultipliers(t *testing.T) { tests := []struct { name string blockName string - want mgl64.Vec3 + want mgl32.Vec3 }{ - {name: "honey", blockName: "minecraft:honey_block", want: mgl64.Vec3{0.4, -0.12, 0.4}}, - {name: "sweet berry bush", blockName: "minecraft:sweet_berry_bush", want: mgl64.Vec3{0.8, -0.75, 0.8}}, - {name: "powder snow", blockName: "minecraft:powder_snow", want: mgl64.Vec3{0.9, -1.5, 0.9}}, + {name: "honey", blockName: "minecraft:honey_block", want: mgl32.Vec3{0.4, -0.12, 0.4}}, + {name: "sweet berry bush", blockName: "minecraft:sweet_berry_bush", want: mgl32.Vec3{0.8, -0.75, 0.8}}, + {name: "powder snow", blockName: "minecraft:powder_snow", want: mgl32.Vec3{0.9, -1.5, 0.9}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { state := newBaseState() - state.Vel = mgl64.Vec3{1, -1, 1} + state.Vel = mgl32.Vec3{1, -1, 1} applyInsideBlockMovement(state, tt.blockName) @@ -64,7 +64,7 @@ func TestHoneyBlockReducesJumpPower(t *testing.T) { if !sim.attemptJump(state, nil) { t.Fatal("expected jump to be applied") } - if want := DefaultJumpHeight * 0.6; math.Abs(state.Vel.Y()-want) > 1e-12 { + if want := float32(DefaultJumpHeight * 0.6); math32.Abs(state.Vel.Y()-want) > 1e-6 { t.Fatalf("expected honey jump velocity %v, got %v", want, state.Vel.Y()) } } @@ -103,39 +103,39 @@ func TestHoneyWalkSlowdownMatchesSlime(t *testing.T) { sim := &Simulator{BlockSemantics: overrideBlockSemantics{name: "minecraft:honey_block"}} state := newBaseState() state.OnGround = true - state.Vel = mgl64.Vec3{1, 0.05, 1} + state.Vel = mgl32.Vec3{1, 0.05, 1} sim.walkOnBlock(state, block.Air{}) - if want := 0.41; math.Abs(state.Vel.X()-want) > 1e-12 || math.Abs(state.Vel.Z()-want) > 1e-12 { + if want := float32(0.41); math32.Abs(state.Vel.X()-want) > 1e-6 || math32.Abs(state.Vel.Z()-want) > 1e-6 { t.Fatalf("expected honey walk slowdown %v, got %v", want, state.Vel) } } func TestSimulationAppliesInsideBlockMovementEffect(t *testing.T) { - w := environmentWorld{blocks: map[cube.Pos]world.Block{ + w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:honey_block"}, }} sim := &Simulator{World: w, BlockSemantics: encodedBlockSemantics{}} state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 0, 0.5} - state.Vel = mgl64.Vec3{0.1, 0, 0} + state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Vel = mgl32.Vec3{0.1, 0, 0} state.HasGravity = false sim.SimulateState(state) - if want := 0.1 * DefaultAirFriction * 0.4; math.Abs(state.Vel.X()-want) > 1e-12 { + if want := float32(0.1 * DefaultAirFriction * 0.4); math32.Abs(state.Vel.X()-want) > 1e-6 { t.Fatalf("expected integrated honey slowdown %v, got %v", want, state.Vel.X()) } } func TestSimulationAppliesScaffoldingTraversal(t *testing.T) { - w := environmentWorld{blocks: map[cube.Pos]world.Block{ + w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:scaffolding"}, }} sim := &Simulator{World: w, BlockSemantics: encodedBlockSemantics{}} state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Pos = mgl32.Vec3{0.5, 0, 0.5} state.HasGravity = false sim.Simulate(state, InputState{AscendBlock: true}) @@ -146,7 +146,7 @@ func TestSimulationAppliesScaffoldingTraversal(t *testing.T) { } func TestSimulationDetectsNonSolidWebAndAppliesWeaving(t *testing.T) { - w := environmentWorld{blocks: map[cube.Pos]world.Block{ + w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:web"}, }} sim := &Simulator{ @@ -155,13 +155,13 @@ func TestSimulationDetectsNonSolidWebAndAppliesWeaving(t *testing.T) { Effects: fixedEffects{EffectWeaving: 0}, } state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 0, 0.5} - state.Vel = mgl64.Vec3{0.1, 0, 0} + state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Vel = mgl32.Vec3{0.1, 0, 0} state.HasGravity = false result := sim.SimulateState(state) - if want := 0.05; math.Abs(result.Movement.X()-want) > 1e-12 { + if want := float32(0.05); math32.Abs(result.Movement.X()-want) > 1e-6 { t.Fatalf("expected Weaving web movement %v, got %v", want, result.Movement.X()) } } diff --git a/bubble.go b/bubble.go index bb7fa67..e5fe1bf 100644 --- a/bubble.go +++ b/bubble.go @@ -1,10 +1,10 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" - "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" ) // BubbleColumnDirection is the direction a bubble column accelerates entities. @@ -18,24 +18,24 @@ const ( // BubbleColumnProvider exposes bubble-column direction separately from liquid // state so worlds without a concrete bubble-column block type can participate. type BubbleColumnProvider interface { - BubbleColumn(pos cube.Pos) (BubbleColumnDirection, bool) + BubbleColumn(pos dfcube.Pos) (BubbleColumnDirection, bool) } func applyBubbleColumn(state *MovementState, direction BubbleColumnDirection, surface bool) { velocity := state.Vel switch direction { case BubbleColumnDown: - cap := -0.3 + cap := float32(-0.3) if surface { cap = -0.9 } - velocity[1] = math.Max(cap, velocity[1]-0.03) + velocity[1] = math32.Max(cap, velocity[1]-0.03) default: - change, cap := 0.06, 0.7 + change, cap := float32(0.06), float32(0.7) if surface { change, cap = 0.1, 1.8 } - velocity[1] = math.Min(cap, velocity[1]+change) + velocity[1] = math32.Min(cap, velocity[1]+change) } state.SetVel(velocity) } @@ -47,15 +47,15 @@ func (s *Simulator) applyBubbleColumns(state *MovementState) { } bb := state.BoundingBox(s.Options.UseSlideOffset) min, max := bb.Min(), bb.Max() - for x := int(math.Floor(min.X())); x < int(math.Ceil(max.X())); x++ { - for y := int(math.Floor(min.Y())); y < int(math.Ceil(max.Y())); y++ { - for z := int(math.Floor(min.Z())); z < int(math.Ceil(max.Z())); z++ { - pos := cube.Pos{x, y, z} + for x := int(math32.Floor(min.X())); x < int(math32.Ceil(max.X())); x++ { + for y := int(math32.Floor(min.Y())); y < int(math32.Ceil(max.Y())); y++ { + for z := int(math32.Floor(min.Z())); z < int(math32.Ceil(max.Z())); z++ { + pos := dfcube.Pos{x, y, z} direction, found := provider.BubbleColumn(pos) if !found { continue } - above := pos.Side(cube.FaceUp) + above := pos.Side(dfcube.FaceUp) _, liquidAbove := s.liquidAt(above) applyBubbleColumn(state, direction, !liquidAbove && s.blockAir(s.blockAtPos(above))) } @@ -71,10 +71,10 @@ func (s *Simulator) attemptRiptide(state *MovementState, touchingLiquid bool) bo if level <= 0 || !state.StartingSpinAttack { return false } - force := 1.5 + 0.75*float64(level-1) - pitch := state.Rotation.X() * math.Pi / 180 - yaw := state.Rotation.Z() * math.Pi / 180 - direction := mgl64.Vec3{-MCSin(yaw) * MCCos(pitch), -MCSin(pitch), MCCos(yaw) * MCCos(pitch)} + force := 1.5 + 0.75*float32(level-1) + pitch := state.Rotation.X() * math32.Pi / 180 + yaw := state.Rotation.Z() * math32.Pi / 180 + direction := mgl32.Vec3{-MCSin(yaw) * MCCos(pitch), -MCSin(pitch), MCCos(yaw) * MCCos(pitch)} if length := direction.Len(); length > 0 { direction = direction.Mul(force / length) } diff --git a/bubble_test.go b/bubble_test.go index 9e4ed95..54b0af4 100644 --- a/bubble_test.go +++ b/bubble_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 TestBubbleColumnUsesBoarImpulsesAndCaps(t *testing.T) { @@ -15,8 +15,8 @@ func TestBubbleColumnUsesBoarImpulsesAndCaps(t *testing.T) { name string direction BubbleColumnDirection surface bool - initial float64 - want float64 + initial float32 + want float32 }{ {name: "submerged up", direction: BubbleColumnUp, initial: 0, want: 0.06}, {name: "submerged up cap", direction: BubbleColumnUp, initial: 0.69, want: 0.70}, @@ -33,7 +33,7 @@ func TestBubbleColumnUsesBoarImpulsesAndCaps(t *testing.T) { state := newBaseState() state.Vel[1] = tt.initial applyBubbleColumn(state, tt.direction, tt.surface) - if math.Abs(state.Vel.Y()-tt.want) > 1e-12 { + if math32.Abs(state.Vel.Y()-tt.want) > 1e-6 { t.Fatalf("expected y velocity %v, got %v", tt.want, state.Vel.Y()) } }) @@ -42,11 +42,11 @@ func TestBubbleColumnUsesBoarImpulsesAndCaps(t *testing.T) { func TestBubbleColumnSurfaceAcceptsRegistryBackedAir(t *testing.T) { w := environmentWorld{ - bubbles: map[cube.Pos]BubbleColumnDirection{{0, 0, 0}: BubbleColumnUp}, - blocks: map[cube.Pos]world.Block{{0, 1, 0}: namedBlock{name: "minecraft:air"}}, + bubbles: map[dfcube.Pos]BubbleColumnDirection{{0, 0, 0}: BubbleColumnUp}, + blocks: map[dfcube.Pos]world.Block{{0, 1, 0}: namedBlock{name: "minecraft:air"}}, } state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Pos = mgl32.Vec3{0.5, 0, 0.5} (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).applyBubbleColumns(state) @@ -56,15 +56,15 @@ func TestBubbleColumnSurfaceAcceptsRegistryBackedAir(t *testing.T) { } func TestRiptideLaunchesInWaterAndStartsSpinAttack(t *testing.T) { - w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} + w := environmentWorld{blocks: map[dfcube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} sim := &Simulator{World: w, Equipment: fixedEquipment{EnchantmentRiptide: 2}} state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Pos = mgl32.Vec3{0.5, 0, 0.5} state.Gravity = NormalGravity sim.Simulate(state, InputState{StartSpinAttack: true}) - if want := 1.8; math.Abs(state.Vel.Z()-want) > 1e-9 { + if want := float32(1.8); math32.Abs(state.Vel.Z()-want) > 1e-6 { t.Fatalf("expected riptide velocity %v, got %v", want, state.Vel.Z()) } if state.RiptideTicks != 19 { 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 28657ec..b68880c 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.5625 - SlideOffsetMultiplier = 0.4 - SlimeBounceMultiplier = -1.0 - BedBounceMultiplier = -0.75 + 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/dynamic_collision_test.go b/dynamic_collision_test.go index 5f8b1a8..f410114 100644 --- a/dynamic_collision_test.go +++ b/dynamic_collision_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" ) type dynamicCollisionWorld struct { @@ -89,15 +90,15 @@ func TestPoseRestoresCustomStandingHeight(t *testing.T) { } func TestSneakingInWaterDescends(t *testing.T) { - w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} + w := environmentWorld{blocks: map[dfcube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} sim := &Simulator{World: w} state := newBaseState() - state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Pos = mgl32.Vec3{0.5, 0, 0.5} state.Gravity = NormalGravity sim.Simulate(state, InputState{SneakDown: true, Sneaking: true}) - if want := -0.037; math.Abs(state.Vel.Y()-want) > 1e-12 { + if want := float32(-0.037); math32.Abs(state.Vel.Y()-want) > 1e-6 { t.Fatalf("expected water descent velocity %v, got %v", want, state.Vel.Y()) } } 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 3eb438f..e9e65ef 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 6e157ce..23ef71a 100644 --- a/interfaces.go +++ b/interfaces.go @@ -1,27 +1,28 @@ 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) } // MovementCollisionContext contains player-dependent state needed by dynamic // collision shapes such as scaffolding and powder snow. type MovementCollisionContext struct { - Position [3]float64 + Position [3]float32 Sneaking bool Descending bool WantDown bool @@ -39,7 +40,7 @@ type MovementCollisionProvider 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 92ccf2b..d339afd 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,11 +84,11 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, swimSpeedMultiplier = state.SwimSpeedMultiplier } if s.Equipment != nil { - depthStriderLevel = math.Min(math.Max(float64(s.Equipment.EnchantmentLevel(EnchantmentDepthStrider)), 0), 3) + depthStriderLevel = math32.Min(math32.Max(float32(s.Equipment.EnchantmentLevel(EnchantmentDepthStrider)), 0), 3) } if depthStriderLevel == 0 { 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 { @@ -110,7 +111,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 } @@ -126,7 +127,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) @@ -136,7 +137,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 := len(s.nearbyBBoxes(state, raisedBox)) > 0 hasLiquid := s.containsAnyLiquid(raisedBox) @@ -152,7 +153,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 } @@ -166,16 +167,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 @@ -189,29 +190,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, @@ -225,7 +226,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 { @@ -237,7 +238,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 } @@ -246,7 +247,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 } @@ -275,7 +276,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 @@ -285,21 +286,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 } } @@ -308,8 +309,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) { @@ -318,7 +319,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 } @@ -327,15 +328,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 } @@ -343,15 +344,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) @@ -364,10 +365,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 de49d54..a6623e5 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(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) { @@ -789,7 +790,7 @@ func TestZeroEquipmentDepthStriderFallsBackToLegacyInventory(t *testing.T) { sim.Inventory = depthStriderInventory{level: 3} sim.Equipment = fixedEquipment{} state := submergedState() - state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Vel = mgl32.Vec3{0.5, 0, 0} state.OnGround = true sim.SimulateState(state) @@ -806,24 +807,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()) { @@ -863,14 +864,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) @@ -881,13 +882,13 @@ func TestZeroMovementSpeedsUseDefaults(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.UnderwaterMovementSpeed = 0 - state.Impulse = mgl64.Vec2{0, 0.98} + state.Impulse = mgl32.Vec2{0, 0.98} sim.SimulateState(state) explicit := newLiquidSim(filledColumn(waterSource)) explicitState := submergedState() explicitState.UnderwaterMovementSpeed = DefaultUnderwaterMovementSpeed - explicitState.Impulse = mgl64.Vec2{0, 0.98} + explicitState.Impulse = mgl32.Vec2{0, 0.98} explicit.SimulateState(explicitState) assertVec(t, state.Vel, explicitState.Vel) @@ -896,7 +897,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 { @@ -907,11 +908,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) @@ -939,14 +940,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. @@ -955,14 +956,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() @@ -971,7 +972,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. @@ -998,13 +999,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. @@ -1074,7 +1075,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. @@ -1121,11 +1122,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() @@ -1138,8 +1139,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() @@ -1152,8 +1153,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() @@ -1169,17 +1170,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()) } @@ -1188,11 +1189,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()) } @@ -1201,12 +1202,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()) } @@ -1215,11 +1216,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()) } @@ -1266,12 +1267,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 { @@ -1291,23 +1292,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) } @@ -1334,11 +1335,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) { @@ -1397,7 +1398,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 8948c11..eb39cfe 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,46 +20,46 @@ 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 // StandingHeight, SneakingHeight, and CrawlingHeight preserve custom entity // dimensions across pose transitions. Zero values use the current standing // height and vanilla player pose heights respectively. - StandingHeight float64 - SneakingHeight float64 - CrawlingHeight float64 + StandingHeight float32 + SneakingHeight float32 + CrawlingHeight float32 - 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 @@ -76,7 +76,7 @@ type MovementState struct { JumpDelay uint64 Swimming bool - SwimAmount float64 + SwimAmount float32 // SwimWaterGraceTicks retains recent server-observed water contact. SwimWaterGraceTicks int64 AutoJumpingInWater bool @@ -134,22 +134,22 @@ func (s *MovementState) ensurePoseHeights() { } } -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/movement_environment_test.go b/movement_environment_test.go index 252e235..0cbaaf2 100644 --- a/movement_environment_test.go +++ b/movement_environment_test.go @@ -2,17 +2,18 @@ package bedsim import ( "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/ethaniccc/float32-cube/cube" ) type environmentWorld struct { - bubbles map[cube.Pos]BubbleColumnDirection - solids map[cube.Pos]bool - blocks map[cube.Pos]world.Block + bubbles map[dfcube.Pos]BubbleColumnDirection + solids map[dfcube.Pos]bool + blocks map[dfcube.Pos]world.Block } -func (w environmentWorld) Block(pos cube.Pos) world.Block { +func (w environmentWorld) Block(pos dfcube.Pos) world.Block { if b, ok := w.blocks[pos]; ok { return b } @@ -22,9 +23,9 @@ func (w environmentWorld) Block(pos cube.Pos) world.Block { return block.Air{} } -func (w environmentWorld) BlockCollisions(pos cube.Pos) []cube.BBox { +func (w environmentWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { if w.solids[pos] { - 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))} } return nil } @@ -32,12 +33,12 @@ func (w environmentWorld) BlockCollisions(pos cube.Pos) []cube.BBox { func (w environmentWorld) GetNearbyBBoxes(cube.BBox) []cube.BBox { return nil } func (w environmentWorld) IsChunkLoaded(int32, int32) bool { return true } -func (w environmentWorld) Liquid(pos cube.Pos) (world.Liquid, bool) { +func (w environmentWorld) Liquid(pos dfcube.Pos) (world.Liquid, bool) { liquid, ok := w.Block(pos).(world.Liquid) return liquid, ok } -func (w environmentWorld) BubbleColumn(pos cube.Pos) (BubbleColumnDirection, bool) { +func (w environmentWorld) BubbleColumn(pos dfcube.Pos) (BubbleColumnDirection, bool) { direction, ok := w.bubbles[pos] return direction, ok } 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/parity_test.go b/parity_test.go index 7666c31..14ddebf 100644 --- a/parity_test.go +++ b/parity_test.go @@ -1,12 +1,12 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" "testing" "github.com/df-mc/dragonfly/server/block" - "github.com/df-mc/dragonfly/server/block/cube" - "github.com/go-gl/mathgl/mgl64" + "github.com/ethaniccc/float32-cube/cube" + "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -23,7 +23,7 @@ func TestJumpBoostUsesZeroBasedEffectAmplifier(t *testing.T) { sim.applyInput(state, InputState{}) - if want := 0.52; math.Abs(state.JumpHeight-want) > 1e-12 { + if want := float32(0.52); math32.Abs(state.JumpHeight-want) > 1e-6 { t.Fatalf("expected jump boost I height %v, got %v", want, state.JumpHeight) } } @@ -35,7 +35,7 @@ func TestLevitationUsesZeroBasedEffectAmplifier(t *testing.T) { sim.SimulateState(state) - if want := 0.01; math.Abs(state.Vel.Y()-want) > 1e-12 { + if want := float32(0.01); math32.Abs(state.Vel.Y()-want) > 1e-6 { t.Fatalf("expected levitation I velocity %v, got %v", want, state.Vel.Y()) } } @@ -43,19 +43,19 @@ func TestLevitationUsesZeroBasedEffectAmplifier(t *testing.T) { func TestSlowFallingOnlyChangesGravityWhileDescending(t *testing.T) { sim := &Simulator{World: mockWorld{}, Effects: fixedEffects{packet.EffectSlowFalling: 0}} state := newBaseState() - state.Vel = mgl64.Vec3{0, 0.2} + state.Vel = mgl32.Vec3{0, 0.2} state.Gravity = NormalGravity state.SlowFalling = true sim.SimulateState(state) - if want := (0.2 - NormalGravity) * NormalGravityMultiplier; math.Abs(state.Vel.Y()-want) > 1e-12 { + if want := float32((0.2 - NormalGravity) * NormalGravityMultiplier); math32.Abs(state.Vel.Y()-want) > 1e-6 { t.Fatalf("expected normal gravity while ascending, want %v, got %v", want, state.Vel.Y()) } } func TestBedrockStepHeight(t *testing.T) { - if want := 0.5625; StepHeight != want { + if want := float32(0.6); StepHeight != want { t.Fatalf("expected Bedrock step height %v, got %v", want, StepHeight) } } @@ -63,25 +63,25 @@ func TestBedrockStepHeight(t *testing.T) { func TestBedBounceUsesBedrockRestitutionAndCap(t *testing.T) { sim := &Simulator{BlockSemantics: overrideBlockSemantics{name: "minecraft:bed"}} state := newBaseState() - state.Vel = mgl64.Vec3{0, -2} + state.Vel = mgl32.Vec3{0, -2} sim.landOnBlock(state, state.Vel, block.Air{}) - if want := 0.75; state.Vel.Y() != want { + if want := float32(0.75); state.Vel.Y() != want { t.Fatalf("expected bed bounce %v, got %v", want, state.Vel.Y()) } } -func TestTinyVelocityIsNotDiscardedPrematurely(t *testing.T) { +func TestTinyVelocityUsesOriginalSquaredThreshold(t *testing.T) { sim := &Simulator{World: mockWorld{}} state := newBaseState() state.HasGravity = false - state.Vel = mgl64.Vec3{1e-7, 0, 0} + state.Vel = mgl32.Vec3{1e-7, 0, 0} sim.SimulateState(state) - if state.Vel.X() == 0 { - t.Fatal("expected Bedrock-scale tiny velocity to remain non-zero") + if state.Vel != (mgl32.Vec3{}) { + t.Fatalf("expected tiny velocity to be zeroed, got %v", state.Vel) } } @@ -95,7 +95,7 @@ func TestSlowFallingChangesGlideGravity(t *testing.T) { sim.SimulateState(state) - if want := -0.011025; math.Abs(state.Vel.Y()-want) > 1e-9 { + if want := float32(-0.011025); math32.Abs(state.Vel.Y()-want) > 1e-6 { t.Fatalf("expected slow-falling glide velocity %v, got %v", want, state.Vel.Y()) } } @@ -108,7 +108,7 @@ func TestSneakEdgeProtectionWhileSlightlyAboveGround(t *testing.T) { state.Sneaking = true state.OnGround = false state.FallDistance = 0.1 - state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Vel = mgl32.Vec3{0.5, 0, 0} sim.avoidEdge(state) diff --git a/player_features_test.go b/player_features_test.go index 54e7edb..28b438b 100644 --- a/player_features_test.go +++ b/player_features_test.go @@ -1,29 +1,30 @@ package bedsim import ( - "math" + "github.com/chewxy/math32" "testing" - "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" ) func TestSoulSpeedSkipsSoulSandSlowdown(t *testing.T) { - w := environmentWorld{blocks: map[cube.Pos]world.Block{ + w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:soul_sand"}, }} base := newBaseState() - base.Pos = mgl64.Vec3{0.5, 1, 0.5} + base.Pos = mgl32.Vec3{0.5, 1, 0.5} base.OnGround = true base.HasGravity = false without := *base - (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).Simulate(&without, InputState{MoveVector: mgl64.Vec2{0, 1}}) + (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).Simulate(&without, InputState{MoveVector: mgl32.Vec2{0, 1}}) with := *base - (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}, Equipment: fixedEquipment{EnchantmentSoulSpeed: 1}}).Simulate(&with, InputState{MoveVector: mgl64.Vec2{0, 1}}) + (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}, Equipment: fixedEquipment{EnchantmentSoulSpeed: 1}}).Simulate(&with, InputState{MoveVector: mgl32.Vec2{0, 1}}) if with.Vel.Z() <= without.Vel.Z() { t.Fatalf("expected Soul Speed to bypass soul-sand slowdown: with=%v without=%v", with.Vel.Z(), without.Vel.Z()) @@ -33,15 +34,15 @@ func TestSoulSpeedSkipsSoulSandSlowdown(t *testing.T) { func TestSwiftSneakAppliesAfterTwoSlowdownTicks(t *testing.T) { sim := &Simulator{Equipment: fixedEquipment{EnchantmentSwiftSneak: 3}} state := newBaseState() - input := InputState{SneakDown: true, MoveVector: mgl64.Vec2{0, 1}} + input := InputState{SneakDown: true, MoveVector: mgl32.Vec2{0, 1}} sim.applyInput(state, input) - if want := 0.3 * 0.98; math.Abs(state.Impulse.Y()-want) > 1e-12 { + if want := float32(0.3 * 0.98); math32.Abs(state.Impulse.Y()-want) > 1e-6 { t.Fatalf("expected first-tick sneak impulse %v, got %v", want, state.Impulse.Y()) } sim.applyInput(state, input) sim.applyInput(state, input) - if want := 0.75 * 0.98; math.Abs(state.Impulse.Y()-want) > 1e-12 { + if want := float32(0.75 * 0.98); math32.Abs(state.Impulse.Y()-want) > 1e-6 { t.Fatalf("expected Swift Sneak impulse %v after two ticks, got %v", want, state.Impulse.Y()) } } @@ -50,18 +51,18 @@ func TestItemUseAndInventoryActionInputRules(t *testing.T) { tests := []struct { name string input InputState - want float64 + want float32 }{ - {name: "using item", input: InputState{UsingItem: true, MoveVector: mgl64.Vec2{0, 1}}, want: MaxConsumingImpulse * 0.98}, - {name: "using spear", input: InputState{UsingItem: true, UsingSpear: true, MoveVector: mgl64.Vec2{0, 1}}, want: 0.98}, - {name: "inventory action", input: InputState{InventoryAction: true, MoveVector: mgl64.Vec2{0, 1}}, want: 0}, + {name: "using item", input: InputState{UsingItem: true, MoveVector: mgl32.Vec2{0, 1}}, want: MaxConsumingImpulse * 0.98}, + {name: "using spear", input: InputState{UsingItem: true, UsingSpear: true, MoveVector: mgl32.Vec2{0, 1}}, want: 0.98}, + {name: "inventory action", input: InputState{InventoryAction: true, MoveVector: mgl32.Vec2{0, 1}}, want: 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { state := newBaseState() (&Simulator{}).applyInput(state, tt.input) - if math.Abs(state.Impulse.Y()-tt.want) > 1e-12 { + if math32.Abs(state.Impulse.Y()-tt.want) > 1e-6 { t.Fatalf("expected impulse %v, got %v", tt.want, state.Impulse.Y()) } }) @@ -73,12 +74,12 @@ func TestCrawlingUpdatesPoseAndSlowdown(t *testing.T) { sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox{ cube.Box(-1, 0.7, -1, 1, 2, 1), }}} - sim.applyInput(state, InputState{StartCrawling: true, MoveVector: mgl64.Vec2{0, 1}}) + sim.applyInput(state, InputState{StartCrawling: true, MoveVector: mgl32.Vec2{0, 1}}) if !state.Crawling || state.Size.Y() != 0.6 { t.Fatalf("expected crawling pose, got crawling=%v size=%v", state.Crawling, state.Size) } - if want := 0.3 * 0.98; math.Abs(state.Impulse.Y()-want) > 1e-12 { + if want := float32(0.3 * 0.98); math32.Abs(state.Impulse.Y()-want) > 1e-6 { t.Fatalf("expected crawling slowdown %v, got %v", want, state.Impulse.Y()) } } 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 a21a71a..5f1a213 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 @@ -139,7 +140,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 @@ -238,7 +239,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 || (input.UsingItem && !input.UsingSpear) { maxImpulse *= MaxConsumingImpulse @@ -247,19 +248,19 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { state.TicksSinceCanSlowdown++ sneakMultiplier := MaxSneakImpulse if state.TicksSinceCanSlowdown > 2 && s.Equipment != nil { - sneakMultiplier += 0.15 * float64(s.Equipment.EnchantmentLevel(EnchantmentSwiftSneak)) + sneakMultiplier += 0.15 * float32(s.Equipment.EnchantmentLevel(EnchantmentSwiftSneak)) } maxImpulse *= ClampFloat(sneakMultiplier, 0, 1) } else { state.TicksSinceCanSlowdown = 0 } } - moveVector := mgl64.Vec2{ + moveVector := mgl32.Vec2{ ClampFloat(input.MoveVector[0], -maxImpulse, maxImpulse), ClampFloat(input.MoveVector[1], -maxImpulse, maxImpulse), } if input.InventoryAction { - moveVector = mgl64.Vec2{} + moveVector = mgl32.Vec2{} } // Ground jumps are edge-triggered; liquid and ladder ascent may be held. @@ -269,7 +270,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+1) * 0.1 + state.JumpHeight += float32(amp+1) * 0.1 } } @@ -332,7 +333,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 { @@ -345,13 +346,9 @@ func (s *Simulator) tickState(state *MovementState) { } func (s *Simulator) simulateMovement(state *MovementState) { - vel := state.Vel - for axis := range 3 { - if math.Abs(vel[axis]) < 1e-8 { - vel[axis] = 0 - } + if state.Vel.LenSqr() < 1e-12 { + state.SetVel(mgl32.Vec3{}) } - state.SetVel(vel) // Bound retained water evidence before collision and travel inspect it. grace := s.swimWaterGraceTicks() @@ -394,8 +391,8 @@ func (s *Simulator) simulateMovement(state *MovementState) { return } - blockUnder := s.blockAtPos(cube.PosFromVec3(state.Pos.Sub(mgl64.Vec3{0, 0.5}))) - blockFriction := DefaultAirFriction + blockUnder := s.blockAtPos(posFromVec3(state.Pos.Sub(mgl32.Vec3{0, 0.5}))) + blockFriction := float32(DefaultAirFriction) moveRelativeSpeed := state.AirSpeed if state.OnGround { mSpeed := state.MovementSpeed @@ -435,11 +432,11 @@ func (s *Simulator) simulateMovement(state *MovementState) { moveRelative(state, moveRelativeSpeed) 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) - insideName := s.blockName(s.blockAtPos(cube.PosFromVec3(state.Pos))) + insideName := s.blockName(s.blockAtPos(posFromVec3(state.Pos))) leatherBoots := s.Equipment != nil && s.Equipment.WearingLeatherBoots() applyAscendableMovement(state, insideName, leatherBoots) - nearClimbable := s.blockClimbable(s.blockAtPos(cube.PosFromVec3(state.Pos))) + nearClimbable := s.blockClimbable(s.blockAtPos(posFromVec3(state.Pos))) if nearClimbable { newVel := state.Vel negClimbSpeed := -ClimbSpeed @@ -460,7 +457,7 @@ func (s *Simulator) simulateMovement(state *MovementState) { if inWeb { newVel := state.Vel - xz, y := 0.25, 0.05 + xz, y := float32(0.25), float32(0.05) if s.Effects != nil { if _, weaving := s.Effects.GetEffect(EffectWeaving); weaving { xz, y = 0.5, 0.25 @@ -484,9 +481,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 s.blockAir(blockUnder) { - 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 } @@ -504,13 +501,13 @@ func (s *Simulator) simulateMovement(state *MovementState) { if inWeb { s.debugf("post-move web 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+1) + levSpeed := LevitationGravityMultiplier * float32(amp+1) newVel[1] += (levSpeed - newVel[1]) * 0.2 } else if state.HasGravity { newVel[1] -= effectiveGravity(state, newVel) @@ -587,7 +584,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 @@ -595,7 +592,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 @@ -604,10 +601,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) @@ -616,7 +613,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 @@ -665,7 +662,7 @@ func (s *Simulator) walkOnBlock(state *MovementState, blockUnder world.Block) { newVel := state.Vel switch s.blockName(blockUnder) { case "minecraft:slime", "minecraft:honey_block": - 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 @@ -676,7 +673,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 @@ -687,25 +684,25 @@ 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(0.75, BedBounceMultiplier*old.Y()) + newVel[1] = math32.Min(0.75, BedBounceMultiplier*old.Y()) default: newVel[1] = 0 } state.SetVel(newVel) } -func effectiveGravity(state *MovementState, velocity mgl64.Vec3) float64 { +func effectiveGravity(state *MovementState, velocity mgl32.Vec3) float32 { if state.SlowFalling && velocity.Y() < 0 { return SlowFallingGravity } return state.Gravity } -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 { @@ -724,7 +721,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 @@ -736,15 +733,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 @@ -770,12 +767,12 @@ func (s *Simulator) attemptJump(state *MovementState, clientJumpPrevented *bool) newVel := state.Vel jumpHeight := state.JumpHeight - inBlock := s.blockAtPos(cube.PosFromVec3(state.Pos)) - below := s.blockAtPos(cube.PosFromVec3(state.Pos.Sub(mgl64.Vec3{0, 0.1}))) + inBlock := s.blockAtPos(posFromVec3(state.Pos)) + below := s.blockAtPos(posFromVec3(state.Pos.Sub(mgl32.Vec3{0, 0.1}))) if s.blockName(inBlock) == "minecraft:honey_block" || s.blockName(below) == "minecraft:honey_block" { jumpHeight *= 0.6 } - newVel[1] = math.Max(jumpHeight, newVel[1]) + newVel[1] = math32.Max(jumpHeight, newVel[1]) state.JumpDelay = JumpDelayTicks if state.Sprinting { @@ -795,7 +792,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 @@ -804,9 +801,9 @@ func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl64.Vec3) bool collisionBB := state.BoundingBox(useSlideOffset) bbList := s.nearbyBBoxes(state, 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) @@ -826,9 +823,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-- { @@ -861,14 +858,14 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool bbList := s.nearbyBBoxes(state, 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) @@ -889,7 +886,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, @@ -906,9 +903,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 { @@ -946,7 +943,7 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool } else { hasStepCollisions = len(s.nearbyBBoxes(state, stepBB)) > 0 } - 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, @@ -980,7 +977,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, @@ -993,17 +990,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) @@ -1027,8 +1024,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 @@ -1036,11 +1033,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 && len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{xMov, -StepHeight * 1.01, 0}))) == 0; i++ { + for i = 0; i < maxIter && xMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, 0}))) == 0; i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -1053,7 +1050,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { xMov = 0 } - for i = 0; i < maxIter && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{0, -StepHeight * 1.01, zMov}))) == 0; i++ { + for i = 0; i < maxIter && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{0, -StepHeight * 1.01, zMov}))) == 0; i++ { if zMov < offset && zMov >= -offset { zMov = 0 } else if zMov > 0 { @@ -1066,7 +1063,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { zMov = 0 } - for i = 0; i < maxIter && xMov != 0.0 && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{xMov, -StepHeight * 1.01, zMov}))) == 0; i++ { + for i = 0; i < maxIter && xMov != 0.0 && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, zMov}))) == 0; i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -1102,8 +1099,8 @@ func (s *Simulator) isAboveGround(state *MovementState) bool { return false } distance := 0.6 - state.FallDistance - bb := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl64.Vec3{-0.025, 0, -0.025}) - return len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{0, -distance}))) > 0 + bb := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl32.Vec3{-0.025, 0, -0.025}) + return len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{0, -distance}))) > 0 } func (s *Simulator) isInsideWeb(state *MovementState) bool { @@ -1121,7 +1118,7 @@ func (s *Simulator) isInsideWeb(state *MovementState) bool { continue } - if bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())) { + if bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))) { insideWeb = true } if insideWeb { @@ -1131,19 +1128,19 @@ func (s *Simulator) isInsideWeb(state *MovementState) bool { return insideWeb } -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 } @@ -1153,7 +1150,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 @@ -1161,7 +1158,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) } } @@ -1170,9 +1167,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) @@ -1181,10 +1178,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 @@ -1197,7 +1194,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{} } @@ -1211,7 +1208,7 @@ func (s *Simulator) nearbyBBoxes(state *MovementState, aabb cube.BBox) []cube.BB if provider, ok := s.World.(MovementCollisionProvider); ok { leatherBoots := s.Equipment != nil && s.Equipment.WearingLeatherBoots() return provider.GetMovementBBoxes(aabb, MovementCollisionContext{ - Position: [3]float64(state.Pos), + Position: [3]float32(state.Pos), Sneaking: state.Sneaking, Descending: state.PressingDescend, WantDown: state.WantDown, @@ -1221,7 +1218,7 @@ func (s *Simulator) nearbyBBoxes(state *MovementState, aabb cube.BBox) []cube.BB return s.World.GetNearbyBBoxes(aabb) } -func (s *Simulator) canFitHeight(state *MovementState, height float64) bool { +func (s *Simulator) canFitHeight(state *MovementState, height float32) bool { if s.World == nil { return true } diff --git a/simulator.go b/simulator.go index e5ca3ed..1e2b36e 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/block" "github.com/df-mc/dragonfly/server/world" @@ -31,14 +31,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 @@ -79,7 +79,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) } @@ -107,9 +107,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 53acb8e..06eec46 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 20f1d6311c5af4a1784da213d70bb1c3abaa49f6 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 00:02:16 -0400 Subject: [PATCH 5/9] Revert "feat: convert simulation to native float32 math" This reverts commit 10c3d991a3434d2f8cd579b448a414e94693ef94. --- bbox.go | 22 +-- bedrock_semantics_test.go | 14 +- block.go | 8 +- block_effects.go | 15 +- block_effects_test.go | 44 +++--- bubble.go | 34 ++--- bubble_test.go | 24 +-- collision.go | 38 ++--- constants.go | 42 +++--- dynamic_collision_test.go | 13 +- go.mod | 2 - go.sum | 4 - input.go | 14 +- interfaces.go | 13 +- liquid.go | 105 +++++++------ liquid_hardening_test.go | 136 ++++++++--------- liquid_test.go | 277 +++++++++++++++++------------------ math.go | 29 ++-- movement.go | 66 ++++----- movement_environment_test.go | 19 ++- native_float32_test.go | 25 ---- parity_test.go | 32 ++-- player_features_test.go | 35 +++-- result.go | 12 +- simulation.go | 183 +++++++++++------------ simulator.go | 14 +- simulator_test.go | 123 ++++++++-------- 27 files changed, 644 insertions(+), 699 deletions(-) delete mode 100644 native_float32_test.go diff --git a/bbox.go b/bbox.go index 73f989b..2ccc74d 100644 --- a/bbox.go +++ b/bbox.go @@ -1,20 +1,10 @@ package bedsim import ( - dfcube "github.com/df-mc/dragonfly/server/block/cube" - "github.com/ethaniccc/float32-cube/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" ) -// 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 { @@ -29,7 +19,7 @@ func (s *MovementState) BoundingBox(useSlideOffset bool) cube.BBox { if s.SwimPose() { height = s.Size[0] * scale } - yOffset := float32(0) + yOffset := 0.0 if useSlideOffset { yOffset = s.SlideOffset.Y() } @@ -41,7 +31,7 @@ func (s *MovementState) BoundingBox(useSlideOffset bool) cube.BBox { s.Pos[0]+width, s.Pos[1]+height+yOffset, s.Pos[2]+width, - ).GrowVec3(mgl32.Vec3{-1e-4, 0, -1e-4}) + ).GrowVec3(mgl64.Vec3{-1e-4, 0, -1e-4}) } // ClientBoundingBox returns the bounding box translated to the client's position. @@ -52,7 +42,7 @@ func (s *MovementState) ClientBoundingBox(useSlideOffset bool) cube.BBox { if s.SwimPose() { height = s.Size[0] * scale } - yOffset := float32(0) + yOffset := 0.0 if useSlideOffset { yOffset = s.SlideOffset.Y() } @@ -64,5 +54,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(mgl32.Vec3{-1e-4, 0, -1e-4}) + ).GrowVec3(mgl64.Vec3{-1e-4, 0, -1e-4}) } diff --git a/bedrock_semantics_test.go b/bedrock_semantics_test.go index 7c66005..9fa9552 100644 --- a/bedrock_semantics_test.go +++ b/bedrock_semantics_test.go @@ -1,12 +1,12 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "testing" - 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/go-gl/mathgl/mgl64" ) func TestBlockAirRecognisesOnlyBedrockAirIdentifier(t *testing.T) { @@ -30,17 +30,17 @@ func TestBlockAirRecognisesOnlyBedrockAirIdentifier(t *testing.T) { } func TestJavaWebIdentifierHasNoBedrockMovementEffect(t *testing.T) { - w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ + w := environmentWorld{blocks: map[cube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:cobweb"}, }} state := newBaseState() - state.Pos = mgl32.Vec3{0.5, 0, 0.5} - state.Vel = mgl32.Vec3{0.1, 0, 0} + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Vel = mgl64.Vec3{0.1, 0, 0} state.HasGravity = false result := (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).SimulateState(state) - if want := float32(0.1); math32.Abs(result.Movement.X()-want) > 1e-6 { + if want := 0.1; math.Abs(result.Movement.X()-want) > 1e-12 { t.Fatalf("Java web identifier changed Bedrock movement: got %v, want %v", result.Movement.X(), want) } } diff --git a/block.go b/block.go index 0aeb5cf..528418c 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" ) @@ -37,9 +37,9 @@ func BlockName(b world.Block) string { } // BlockFriction returns the friction of the block. -func BlockFriction(b world.Block) float32 { +func BlockFriction(b world.Block) float64 { if f, ok := b.(block.Frictional); ok { - return float32(f.Friction()) + return 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 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/block_effects.go b/block_effects.go index ff56e63..d8807c6 100644 --- a/block_effects.go +++ b/block_effects.go @@ -1,10 +1,9 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" - dfcube "github.com/df-mc/dragonfly/server/block/cube" - "github.com/ethaniccc/float32-cube/cube" + "github.com/df-mc/dragonfly/server/block/cube" ) func applyInsideBlockMovement(state *MovementState, blockName string) { @@ -51,11 +50,11 @@ func (s *Simulator) applyInsideBlockEffects(state *MovementState) { } bb := state.BoundingBox(s.Options.UseSlideOffset) min, maxPoint := bb.Min(), bb.Max() - for x := int(math32.Floor(min.X())); x < int(math32.Ceil(maxPoint.X())); x++ { - for y := int(math32.Floor(min.Y())); y < int(math32.Ceil(maxPoint.Y())); y++ { - for z := int(math32.Floor(min.Z())); z < int(math32.Ceil(maxPoint.Z())); z++ { - pos := dfcube.Pos{x, y, z} - if !bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))) { + for x := int(math.Floor(min.X())); x < int(math.Ceil(maxPoint.X())); x++ { + for y := int(math.Floor(min.Y())); y < int(math.Ceil(maxPoint.Y())); y++ { + for z := int(math.Floor(min.Z())); z < int(math.Ceil(maxPoint.Z())); z++ { + pos := cube.Pos{x, y, z} + if !bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())) { continue } b := s.World.Block(pos) diff --git a/block_effects_test.go b/block_effects_test.go index 76011e2..286fcb4 100644 --- a/block_effects_test.go +++ b/block_effects_test.go @@ -1,13 +1,13 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "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/go-gl/mathgl/mgl64" ) type namedBlock struct { @@ -23,24 +23,24 @@ func (encodedBlockSemantics) BlockName(b world.Block) string { name, _ := b.EncodeBlock() return name } -func (encodedBlockSemantics) BlockFriction(world.Block) float32 { return DefaultBlockFriction } +func (encodedBlockSemantics) BlockFriction(world.Block) float64 { return DefaultBlockFriction } func (encodedBlockSemantics) BlockClimbable(world.Block) bool { return false } func TestInsideBlockMovementMultipliers(t *testing.T) { tests := []struct { name string blockName string - want mgl32.Vec3 + want mgl64.Vec3 }{ - {name: "honey", blockName: "minecraft:honey_block", want: mgl32.Vec3{0.4, -0.12, 0.4}}, - {name: "sweet berry bush", blockName: "minecraft:sweet_berry_bush", want: mgl32.Vec3{0.8, -0.75, 0.8}}, - {name: "powder snow", blockName: "minecraft:powder_snow", want: mgl32.Vec3{0.9, -1.5, 0.9}}, + {name: "honey", blockName: "minecraft:honey_block", want: mgl64.Vec3{0.4, -0.12, 0.4}}, + {name: "sweet berry bush", blockName: "minecraft:sweet_berry_bush", want: mgl64.Vec3{0.8, -0.75, 0.8}}, + {name: "powder snow", blockName: "minecraft:powder_snow", want: mgl64.Vec3{0.9, -1.5, 0.9}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { state := newBaseState() - state.Vel = mgl32.Vec3{1, -1, 1} + state.Vel = mgl64.Vec3{1, -1, 1} applyInsideBlockMovement(state, tt.blockName) @@ -64,7 +64,7 @@ func TestHoneyBlockReducesJumpPower(t *testing.T) { if !sim.attemptJump(state, nil) { t.Fatal("expected jump to be applied") } - if want := float32(DefaultJumpHeight * 0.6); math32.Abs(state.Vel.Y()-want) > 1e-6 { + if want := DefaultJumpHeight * 0.6; math.Abs(state.Vel.Y()-want) > 1e-12 { t.Fatalf("expected honey jump velocity %v, got %v", want, state.Vel.Y()) } } @@ -103,39 +103,39 @@ func TestHoneyWalkSlowdownMatchesSlime(t *testing.T) { sim := &Simulator{BlockSemantics: overrideBlockSemantics{name: "minecraft:honey_block"}} state := newBaseState() state.OnGround = true - state.Vel = mgl32.Vec3{1, 0.05, 1} + state.Vel = mgl64.Vec3{1, 0.05, 1} sim.walkOnBlock(state, block.Air{}) - if want := float32(0.41); math32.Abs(state.Vel.X()-want) > 1e-6 || math32.Abs(state.Vel.Z()-want) > 1e-6 { + if want := 0.41; math.Abs(state.Vel.X()-want) > 1e-12 || math.Abs(state.Vel.Z()-want) > 1e-12 { t.Fatalf("expected honey walk slowdown %v, got %v", want, state.Vel) } } func TestSimulationAppliesInsideBlockMovementEffect(t *testing.T) { - w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ + w := environmentWorld{blocks: map[cube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:honey_block"}, }} sim := &Simulator{World: w, BlockSemantics: encodedBlockSemantics{}} state := newBaseState() - state.Pos = mgl32.Vec3{0.5, 0, 0.5} - state.Vel = mgl32.Vec3{0.1, 0, 0} + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Vel = mgl64.Vec3{0.1, 0, 0} state.HasGravity = false sim.SimulateState(state) - if want := float32(0.1 * DefaultAirFriction * 0.4); math32.Abs(state.Vel.X()-want) > 1e-6 { + if want := 0.1 * DefaultAirFriction * 0.4; math.Abs(state.Vel.X()-want) > 1e-12 { t.Fatalf("expected integrated honey slowdown %v, got %v", want, state.Vel.X()) } } func TestSimulationAppliesScaffoldingTraversal(t *testing.T) { - w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ + w := environmentWorld{blocks: map[cube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:scaffolding"}, }} sim := &Simulator{World: w, BlockSemantics: encodedBlockSemantics{}} state := newBaseState() - state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Pos = mgl64.Vec3{0.5, 0, 0.5} state.HasGravity = false sim.Simulate(state, InputState{AscendBlock: true}) @@ -146,7 +146,7 @@ func TestSimulationAppliesScaffoldingTraversal(t *testing.T) { } func TestSimulationDetectsNonSolidWebAndAppliesWeaving(t *testing.T) { - w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ + w := environmentWorld{blocks: map[cube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:web"}, }} sim := &Simulator{ @@ -155,13 +155,13 @@ func TestSimulationDetectsNonSolidWebAndAppliesWeaving(t *testing.T) { Effects: fixedEffects{EffectWeaving: 0}, } state := newBaseState() - state.Pos = mgl32.Vec3{0.5, 0, 0.5} - state.Vel = mgl32.Vec3{0.1, 0, 0} + state.Pos = mgl64.Vec3{0.5, 0, 0.5} + state.Vel = mgl64.Vec3{0.1, 0, 0} state.HasGravity = false result := sim.SimulateState(state) - if want := float32(0.05); math32.Abs(result.Movement.X()-want) > 1e-6 { + if want := 0.05; math.Abs(result.Movement.X()-want) > 1e-12 { t.Fatalf("expected Weaving web movement %v, got %v", want, result.Movement.X()) } } diff --git a/bubble.go b/bubble.go index e5fe1bf..bb7fa67 100644 --- a/bubble.go +++ b/bubble.go @@ -1,10 +1,10 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" - dfcube "github.com/df-mc/dragonfly/server/block/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" ) // BubbleColumnDirection is the direction a bubble column accelerates entities. @@ -18,24 +18,24 @@ const ( // BubbleColumnProvider exposes bubble-column direction separately from liquid // state so worlds without a concrete bubble-column block type can participate. type BubbleColumnProvider interface { - BubbleColumn(pos dfcube.Pos) (BubbleColumnDirection, bool) + BubbleColumn(pos cube.Pos) (BubbleColumnDirection, bool) } func applyBubbleColumn(state *MovementState, direction BubbleColumnDirection, surface bool) { velocity := state.Vel switch direction { case BubbleColumnDown: - cap := float32(-0.3) + cap := -0.3 if surface { cap = -0.9 } - velocity[1] = math32.Max(cap, velocity[1]-0.03) + velocity[1] = math.Max(cap, velocity[1]-0.03) default: - change, cap := float32(0.06), float32(0.7) + change, cap := 0.06, 0.7 if surface { change, cap = 0.1, 1.8 } - velocity[1] = math32.Min(cap, velocity[1]+change) + velocity[1] = math.Min(cap, velocity[1]+change) } state.SetVel(velocity) } @@ -47,15 +47,15 @@ func (s *Simulator) applyBubbleColumns(state *MovementState) { } bb := state.BoundingBox(s.Options.UseSlideOffset) min, max := bb.Min(), bb.Max() - for x := int(math32.Floor(min.X())); x < int(math32.Ceil(max.X())); x++ { - for y := int(math32.Floor(min.Y())); y < int(math32.Ceil(max.Y())); y++ { - for z := int(math32.Floor(min.Z())); z < int(math32.Ceil(max.Z())); z++ { - pos := dfcube.Pos{x, y, z} + for x := int(math.Floor(min.X())); x < int(math.Ceil(max.X())); x++ { + for y := int(math.Floor(min.Y())); y < int(math.Ceil(max.Y())); y++ { + for z := int(math.Floor(min.Z())); z < int(math.Ceil(max.Z())); z++ { + pos := cube.Pos{x, y, z} direction, found := provider.BubbleColumn(pos) if !found { continue } - above := pos.Side(dfcube.FaceUp) + above := pos.Side(cube.FaceUp) _, liquidAbove := s.liquidAt(above) applyBubbleColumn(state, direction, !liquidAbove && s.blockAir(s.blockAtPos(above))) } @@ -71,10 +71,10 @@ func (s *Simulator) attemptRiptide(state *MovementState, touchingLiquid bool) bo if level <= 0 || !state.StartingSpinAttack { return false } - force := 1.5 + 0.75*float32(level-1) - pitch := state.Rotation.X() * math32.Pi / 180 - yaw := state.Rotation.Z() * math32.Pi / 180 - direction := mgl32.Vec3{-MCSin(yaw) * MCCos(pitch), -MCSin(pitch), MCCos(yaw) * MCCos(pitch)} + force := 1.5 + 0.75*float64(level-1) + pitch := state.Rotation.X() * math.Pi / 180 + yaw := state.Rotation.Z() * math.Pi / 180 + direction := mgl64.Vec3{-MCSin(yaw) * MCCos(pitch), -MCSin(pitch), MCCos(yaw) * MCCos(pitch)} if length := direction.Len(); length > 0 { direction = direction.Mul(force / length) } diff --git a/bubble_test.go b/bubble_test.go index 54b0af4..9e4ed95 100644 --- a/bubble_test.go +++ b/bubble_test.go @@ -1,13 +1,13 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "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/go-gl/mathgl/mgl64" ) func TestBubbleColumnUsesBoarImpulsesAndCaps(t *testing.T) { @@ -15,8 +15,8 @@ func TestBubbleColumnUsesBoarImpulsesAndCaps(t *testing.T) { name string direction BubbleColumnDirection surface bool - initial float32 - want float32 + initial float64 + want float64 }{ {name: "submerged up", direction: BubbleColumnUp, initial: 0, want: 0.06}, {name: "submerged up cap", direction: BubbleColumnUp, initial: 0.69, want: 0.70}, @@ -33,7 +33,7 @@ func TestBubbleColumnUsesBoarImpulsesAndCaps(t *testing.T) { state := newBaseState() state.Vel[1] = tt.initial applyBubbleColumn(state, tt.direction, tt.surface) - if math32.Abs(state.Vel.Y()-tt.want) > 1e-6 { + if math.Abs(state.Vel.Y()-tt.want) > 1e-12 { t.Fatalf("expected y velocity %v, got %v", tt.want, state.Vel.Y()) } }) @@ -42,11 +42,11 @@ func TestBubbleColumnUsesBoarImpulsesAndCaps(t *testing.T) { func TestBubbleColumnSurfaceAcceptsRegistryBackedAir(t *testing.T) { w := environmentWorld{ - bubbles: map[dfcube.Pos]BubbleColumnDirection{{0, 0, 0}: BubbleColumnUp}, - blocks: map[dfcube.Pos]world.Block{{0, 1, 0}: namedBlock{name: "minecraft:air"}}, + bubbles: map[cube.Pos]BubbleColumnDirection{{0, 0, 0}: BubbleColumnUp}, + blocks: map[cube.Pos]world.Block{{0, 1, 0}: namedBlock{name: "minecraft:air"}}, } state := newBaseState() - state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Pos = mgl64.Vec3{0.5, 0, 0.5} (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).applyBubbleColumns(state) @@ -56,15 +56,15 @@ func TestBubbleColumnSurfaceAcceptsRegistryBackedAir(t *testing.T) { } func TestRiptideLaunchesInWaterAndStartsSpinAttack(t *testing.T) { - w := environmentWorld{blocks: map[dfcube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} + w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} sim := &Simulator{World: w, Equipment: fixedEquipment{EnchantmentRiptide: 2}} state := newBaseState() - state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Pos = mgl64.Vec3{0.5, 0, 0.5} state.Gravity = NormalGravity sim.Simulate(state, InputState{StartSpinAttack: true}) - if want := float32(1.8); math32.Abs(state.Vel.Z()-want) > 1e-6 { + if want := 1.8; math.Abs(state.Vel.Z()-want) > 1e-9 { t.Fatalf("expected riptide velocity %v, got %v", want, state.Vel.Z()) } if state.RiptideTicks != 19 { diff --git a/collision.go b/collision.go index e09e979..c00243c 100644 --- a/collision.go +++ b/collision.go @@ -1,21 +1,21 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" - "github.com/ethaniccc/float32-cube/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" ) type clipCollideResult struct { depenetratingAxis int - penetration float32 - clippedVelocity mgl32.Vec3 - depenetratingVelocity mgl32.Vec3 + penetration float64 + clippedVelocity mgl64.Vec3 + depenetratingVelocity mgl64.Vec3 } // 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 cube.BBox, vel mgl64.Vec3, oneWay bool, penetration *mgl64.Vec3) mgl64.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 cube.BBox, velocity mgl64.Vec3) (result clipCollideResult) { result.clippedVelocity = velocity result.depenetratingVelocity = velocity @@ -35,25 +35,25 @@ func doBBClipCollide(stationary, moving cube.BBox, velocity mgl32.Vec3) (result return } - axisPenetrations := [3]float32{} - axisPenetrationsSigned := [3]float32{} - normalDirs := [3]float32{} + axisPenetrations := [3]float64{} + axisPenetrationsSigned := [3]float64{} + normalDirs := [3]float64{} separatingAxes, separatingAxis := 0, 0 - resultPenetration := float32(math32.MaxFloat32 - 1) + resultPenetration := math.MaxFloat64 - 1 for i := range 3 { minPenetration := moving.Max()[i] - stationary.Min()[i] maxPenetration := stationary.Max()[i] - moving.Min()[i] - if math32.Abs(minPenetration) <= 1e-7 { + if math.Abs(minPenetration) <= 1e-7 { minPenetration = 0 } - if math32.Abs(maxPenetration) <= 1e-7 { + if math.Abs(maxPenetration) <= 1e-7 { maxPenetration = 0 } - minPositive := math32.Max(0, minPenetration) - maxPositive := math32.Max(0, maxPenetration) + minPositive := math.Max(0, minPenetration) + maxPositive := math.Max(0, maxPenetration) if minPositive == 0 { axisPenetrations[i] = 0 @@ -80,7 +80,7 @@ func doBBClipCollide(stationary, moving cube.BBox, velocity mgl32.Vec3) (result if separatingAxes > 1 { return } - resultPenetration = math32.Min(resultPenetration, axisPenetrations[i]) + resultPenetration = math.Min(resultPenetration, axisPenetrations[i]) } // No separating axes means a collision. @@ -95,9 +95,9 @@ func doBBClipCollide(stationary, moving cube.BBox, velocity mgl32.Vec3) (result desiredVelocity := axisPenetrations[bestAxis] * normalDirs[bestAxis] if desiredVelocity > 0 { - result.depenetratingVelocity[bestAxis] = math32.Max(desiredVelocity, velocity[bestAxis]) + result.depenetratingVelocity[bestAxis] = math.Max(desiredVelocity, velocity[bestAxis]) } else { - result.depenetratingVelocity[bestAxis] = math32.Min(desiredVelocity, velocity[bestAxis]) + result.depenetratingVelocity[bestAxis] = math.Min(desiredVelocity, velocity[bestAxis]) } result.depenetratingAxis = bestAxis return diff --git a/constants.go b/constants.go index b68880c..28657ec 100644 --- a/constants.go +++ b/constants.go @@ -1,36 +1,36 @@ package bedsim const ( - 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) + DefaultJumpHeight = 0.42 + DefaultAirFriction = 0.91 + DefaultBlockFriction = 0.6 + NormalGravityMultiplier = 0.98 + LevitationGravityMultiplier = 0.05 + NormalGravity = 0.08 + SlowFallingGravity = 0.01 + StepHeight = 0.5625 + SlideOffsetMultiplier = 0.4 + SlimeBounceMultiplier = -1.0 + BedBounceMultiplier = -0.75 // This can be validated in Mob::ascendLadder(). - ClimbSpeed = float32(0.2) - MaxConsumingImpulse = float32(0.1225) - MaxSneakImpulse = float32(0.3) + ClimbSpeed = 0.2 + MaxConsumingImpulse = 0.1225 + MaxSneakImpulse = 0.3 // Deprecated: MaxNormalizedImpulse is unused by the simulator. The // diagonal-impulse normalization it was intended for is disabled upstream // as well. It is retained only for API compatibility. - MaxNormalizedImpulse = float32(0.70710678118) // 1/sqrt(2) - DefaultUnderwaterMovementSpeed = float32(0.02) - DefaultLavaMovementSpeed = float32(0.02) - DefaultSwimSpeedMultiplier = float32(1) + MaxNormalizedImpulse = 0.70710678118 // 1/sqrt(2) + DefaultUnderwaterMovementSpeed = 0.02 + DefaultLavaMovementSpeed = 0.02 + DefaultSwimSpeedMultiplier = 1.0 - DefaultPlayerHeightOffset = float32(1.62) - SneakingPlayerHeightOffset = float32(1.27) + DefaultPlayerHeightOffset = 1.62 + SneakingPlayerHeightOffset = 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 = float32(-3.92) + TerminalVelocity = -3.92 JumpDelayTicks = 10 GlideBoostTicks = 20 diff --git a/dynamic_collision_test.go b/dynamic_collision_test.go index f410114..5f8b1a8 100644 --- a/dynamic_collision_test.go +++ b/dynamic_collision_test.go @@ -1,14 +1,13 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "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/ethaniccc/float32-cube/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/go-gl/mathgl/mgl64" ) type dynamicCollisionWorld struct { @@ -90,15 +89,15 @@ func TestPoseRestoresCustomStandingHeight(t *testing.T) { } func TestSneakingInWaterDescends(t *testing.T) { - w := environmentWorld{blocks: map[dfcube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} + w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} sim := &Simulator{World: w} state := newBaseState() - state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Pos = mgl64.Vec3{0.5, 0, 0.5} state.Gravity = NormalGravity sim.Simulate(state, InputState{SneakDown: true, Sneaking: true}) - if want := float32(-0.037); math32.Abs(state.Vel.Y()-want) > 1e-6 { + if want := -0.037; math.Abs(state.Vel.Y()-want) > 1e-12 { t.Fatalf("expected water descent velocity %v, got %v", want, state.Vel.Y()) } } diff --git a/go.mod b/go.mod index df14850..aa8e161 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,7 @@ 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 3090985..92ea313 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/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 e9e65ef..3eb438f 100644 --- a/input.go +++ b/input.go @@ -1,17 +1,17 @@ package bedsim -import "github.com/go-gl/mathgl/mgl32" +import "github.com/go-gl/mathgl/mgl64" // InputState represents a single tick's client input and reported state. type InputState struct { - MoveVector mgl32.Vec2 + MoveVector mgl64.Vec2 - Pitch float32 - Yaw float32 - HeadYaw float32 + Pitch float64 + Yaw float64 + HeadYaw float64 - ClientPos mgl32.Vec3 - ClientVel mgl32.Vec3 + ClientPos mgl64.Vec3 + ClientVel mgl64.Vec3 HorizontalCollision bool VerticalCollision bool diff --git a/interfaces.go b/interfaces.go index 23ef71a..6e157ce 100644 --- a/interfaces.go +++ b/interfaces.go @@ -1,28 +1,27 @@ 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" - "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 + Block(pos cube.Pos) world.Block + BlockCollisions(pos cube.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 dfcube.Pos) (world.Liquid, bool) + Liquid(pos cube.Pos) (world.Liquid, bool) } // MovementCollisionContext contains player-dependent state needed by dynamic // collision shapes such as scaffolding and powder snow. type MovementCollisionContext struct { - Position [3]float32 + Position [3]float64 Sneaking bool Descending bool WantDown bool @@ -40,7 +39,7 @@ type MovementCollisionProvider interface { // custom block data instead of Dragonfly's default block types. type BlockSemanticsProvider interface { BlockName(world.Block) string - BlockFriction(world.Block) float32 + BlockFriction(world.Block) float64 BlockClimbable(world.Block) bool } diff --git a/liquid.go b/liquid.go index d339afd..92ccf2b 100644 --- a/liquid.go +++ b/liquid.go @@ -1,18 +1,17 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "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/ethaniccc/float32-cube/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) -// Liquid movement follows oomph PR #145 at 0bcbb8b, with provider-based liquid -// lookup and legacy impulse clamps. It also requires +// Liquid movement follows oomph PR #145 at 0bcbb8b. bedsim retains float64, +// provider-based liquid lookup and its legacy impulse clamps. It also requires // recent server-observed water contact before trusting the client swim flag. // See README.md for complete compatibility and security notes. @@ -36,13 +35,13 @@ func (k liquidKind) matches(liquid world.Liquid) bool { } var liquidFaces = [...]struct { - delta dfcube.Pos - vec mgl32.Vec3 + delta cube.Pos + vec mgl64.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}, 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}}, } func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, touchingLiquid bool) { @@ -73,8 +72,8 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, if moveRelativeSpeed == 0 { moveRelativeSpeed = DefaultLavaMovementSpeed } - depthStriderLevel := float32(0) - swimSpeedMultiplier := float32(DefaultSwimSpeedMultiplier) + depthStriderLevel := 0.0 + swimSpeedMultiplier := DefaultSwimSpeedMultiplier if water { moveRelativeSpeed = state.UnderwaterMovementSpeed if moveRelativeSpeed == 0 { @@ -84,11 +83,11 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, swimSpeedMultiplier = state.SwimSpeedMultiplier } if s.Equipment != nil { - depthStriderLevel = math32.Min(math32.Max(float32(s.Equipment.EnchantmentLevel(EnchantmentDepthStrider)), 0), 3) + depthStriderLevel = math.Min(math.Max(float64(s.Equipment.EnchantmentLevel(EnchantmentDepthStrider)), 0), 3) } if depthStriderLevel == 0 { if inventory, ok := s.Inventory.(DepthStriderProvider); ok { - depthStriderLevel = math32.Min(math32.Max(float32(inventory.DepthStriderLevel()), 0), 3) + depthStriderLevel = math.Min(math.Max(float64(inventory.DepthStriderLevel()), 0), 3) } } if !state.OnGround { @@ -111,7 +110,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, vel := state.Vel if water { - drag := float32(0.8) + drag := 0.8 if state.Sprinting { drag = 0.9 } @@ -127,7 +126,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, if s.Effects != nil { if amplifier, ok := s.Effects.GetEffect(packet.EffectLevitation); ok { - target := LevitationGravityMultiplier * float32(amplifier+1) + target := LevitationGravityMultiplier * float64(amplifier+1) vel[1] += (target - vel[1]) * 0.2 } else if state.HasGravity { vel[1] -= liquidGravity(state.Swimming, water) @@ -137,7 +136,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, } if state.CollideX || state.CollideZ { - raised := mgl32.Vec3{vel.X(), vel.Y() + 0.6 + initialY - state.Pos.Y(), vel.Z()} + raised := mgl64.Vec3{vel.X(), vel.Y() + 0.6 + initialY - state.Pos.Y(), vel.Z()} raisedBox := state.BoundingBox(s.Options.UseSlideOffset).Translate(raised) hasCollision := len(s.nearbyBBoxes(state, raisedBox)) > 0 hasLiquid := s.containsAnyLiquid(raisedBox) @@ -153,7 +152,7 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, state.FallDistance = 0 } -func liquidGravity(swimming, water bool) float32 { +func liquidGravity(swimming, water bool) float64 { if !water { return 0.02 } @@ -167,16 +166,16 @@ func (s *Simulator) updateSwimTravel(state *MovementState) { if !state.Swimming || state.EffectiveJumping { return } - targetY := -MCSin(state.Rotation.X() * math32.Pi / 180) - rate := float32(0.06) + targetY := -MCSin(state.Rotation.X() * math.Pi / 180) + rate := 0.06 if targetY < -0.2 { rate = 0.085 } if targetY > 0 && !state.WantDownSlow { - belowPos := posFromVec3(state.Pos.Add(mgl32.Vec3{0, DefaultPlayerHeightOffset - 1.1})) + belowPos := cube.PosFromVec3(state.Pos.Add(mgl64.Vec3{0, DefaultPlayerHeightOffset - 1.1})) if _, belowAir := s.liquidMovementBlock(belowPos).(block.Air); belowAir { - liquidPos := posFromVec3(state.Pos.Add(mgl32.Vec3{0, DefaultPlayerHeightOffset - 1.2})) + liquidPos := cube.PosFromVec3(state.Pos.Add(mgl64.Vec3{0, DefaultPlayerHeightOffset - 1.2})) if _, liquid := s.liquidAt(liquidPos); !liquid { vel := state.Vel vel[1] = 0 @@ -190,29 +189,29 @@ func (s *Simulator) updateSwimTravel(state *MovementState) { state.SetVel(vel) } -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} +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} if kind == liquidLava { - offset = mgl32.Vec3{0.1, 0.4, 0.1} + offset = mgl64.Vec3{0.1, 0.4, 0.1} } box = shrinkLiquidBox(box, offset) 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) + 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) 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 } if s.Options.Debugf != nil { height := liquidHeight(liquid) - surface := float32(pos[1]) + height + surface := float64(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, @@ -226,7 +225,7 @@ func (s *Simulator) touchingLiquidBlocks(state *MovementState, kind liquidKind) return positions } -func shrinkLiquidBox(box cube.BBox, offset mgl32.Vec3) cube.BBox { +func shrinkLiquidBox(box cube.BBox, offset mgl64.Vec3) cube.BBox { min, max := box.Min().Add(offset), box.Max().Sub(offset) originalMin, originalMax := box.Min(), box.Max() for axis := range 3 { @@ -238,7 +237,7 @@ func shrinkLiquidBox(box cube.BBox, offset mgl32.Vec3) cube.BBox { return cube.Box(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 } @@ -247,7 +246,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 cube.Pos) []cube.BBox { if s.World == nil { return nil } @@ -276,7 +275,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 @@ -286,21 +285,21 @@ func (s *Simulator) liquidAt(pos dfcube.Pos) (world.Liquid, bool) { return liquid, ok } -func liquidHeight(liquid world.Liquid) float32 { +func liquidHeight(liquid world.Liquid) float64 { if liquid.LiquidFalling() { return 1 } - return float32(liquid.LiquidDepth()+1) / 9 + return float64(liquid.LiquidDepth()+1) / 9 } func (s *Simulator) containsAnyLiquid(box cube.BBox) 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())) + 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())) 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 } } @@ -309,8 +308,8 @@ func (s *Simulator) containsAnyLiquid(box cube.BBox) bool { return false } -func (s *Simulator) applyLiquidFlow(state *MovementState, positions []dfcube.Pos, kind liquidKind) { - flow := mgl32.Vec3{} +func (s *Simulator) applyLiquidFlow(state *MovementState, positions []cube.Pos, kind liquidKind) { + flow := mgl64.Vec3{} for _, pos := range positions { liquid, ok := s.liquidAt(pos) if !ok || !kind.matches(liquid) { @@ -319,7 +318,7 @@ func (s *Simulator) applyLiquidFlow(state *MovementState, positions []dfcube.Pos flow = flow.Add(s.liquidFlow(pos, liquid)) } if length := flow.Len(); length >= 1e-4 { - strength := float32(0.014) + strength := 0.014 if kind == liquidLava { strength = 0.0035 } @@ -328,15 +327,15 @@ 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) mgl64.Vec3 { currentDecay := liquidDecay(liquid) - flow := mgl32.Vec3{} + flow := mgl64.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(float32(liquidDecay(neighbour) - currentDecay))) + flow = flow.Add(face.vec.Mul(float64(liquidDecay(neighbour) - currentDecay))) } continue } @@ -344,15 +343,15 @@ 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))) + flow = flow.Add(face.vec.Mul(float64(liquidDecay(lower) - currentDecay + 8))) } } 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) @@ -365,10 +364,10 @@ func (s *Simulator) liquidFlow(pos dfcube.Pos, liquid world.Liquid) mgl32.Vec3 { if length := flow.Len(); length > 1e-4 { return flow.Mul(1 / length) } - return mgl32.Vec3{} + return mgl64.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..008290c 100644 --- a/liquid_hardening_test.go +++ b/liquid_hardening_test.go @@ -1,13 +1,13 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "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/go-gl/mathgl/mgl64" ) func dryState() *MovementState { @@ -184,7 +184,7 @@ func TestRealWaterContactDoesNotNeedSwimmingFlag(t *testing.T) { state.Swimming = false sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl64.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 = mgl32.Vec3{50, 50, 50} + state.TeleportPos = mgl64.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, mgl32.Vec3{0, -0.02, 0}) + assertVec(t, state.Vel, mgl64.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) float32 { + run := func(level int) float64 { 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 = mgl32.Vec2{0, 0.98} + state.Impulse = mgl64.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; math32.Abs(ratio-1/0.7) > 1e-6 { + if ratio := full / none; math.Abs(ratio-1/0.7) > 1e-9 { t.Fatalf("full/none acceleration ratio = %.17g, want %.17g", ratio, 1/0.7) } } 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} @@ -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, mgl32.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl64.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 = mgl32.Vec3{0.5, 0.5, 0.5} + state.Vel = mgl64.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 float32 + want float64 }{ - {"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}, + {"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}, } for _, tc := range cases { @@ -411,7 +411,7 @@ func TestUpstreamImpulseClampingStillBoundsMoveVector(t *testing.T) { sim.Options.UpstreamImpulseClamping = true state := newBaseState() - sim.applyInput(state, InputState{MoveVector: mgl32.Vec2{5, -5}}) + sim.applyInput(state, InputState{MoveVector: mgl64.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 = mgl32.Vec3{0.25, 0.25, 0.25} - state.Client.Vel = mgl32.Vec3{1, 2, 3} + state.Vel = mgl64.Vec3{0.25, 0.25, 0.25} + state.Client.Vel = mgl64.Vec3{1, 2, 3} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeUnreliable { t.Fatalf("outcome = %v, want unreliable", result.Outcome) } - assertVec(t, state.Vel, mgl32.Vec3{1, 2, 3}) + assertVec(t, state.Vel, mgl64.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(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 - want := mgl32.Vec3{7, 0, 4}.Normalize() + want := mgl64.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(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() + want := mgl64.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 dfcube.Direction) mgl32.Vec3 { + build := func(facing cube.Direction) mgl64.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,14 +535,14 @@ 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. - state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Pos = mgl64.Vec3{0.5, 0, 0.5} state.Client.Pos = state.Pos - state.Vel = mgl32.Vec3{0, 0.5, 0} + state.Vel = mgl64.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() (mgl32.Vec3, mgl32.Vec3) { + run := func() (mgl64.Vec3, mgl64.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() @@ -583,7 +583,7 @@ func TestLiquidSimulationIsRepeatable(t *testing.T) { state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks input := InputState{ Jumping: true, - MoveVector: mgl32.Vec2{0.5, 1}, + MoveVector: mgl64.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(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} @@ -629,7 +629,7 @@ func TestLiquidGoldenScenario(t *testing.T) { // is gated on multiplier <= 1, is actually reached. input := InputState{ Jumping: true, - MoveVector: mgl32.Vec2{0.5, 1}, + MoveVector: mgl64.Vec2{0.5, 1}, Pitch: 25, Yaw: 40, HeadYaw: 40, @@ -638,15 +638,15 @@ func TestLiquidGoldenScenario(t *testing.T) { sim.Simulate(state, input) } - wantPos := mgl32.Vec3{-0.012654960155487061, 2.922518253326416, 3.5954680442810059} - wantVel := mgl32.Vec3{-0.02702143903177032, 0.15549643337726593, 0.1142856627702713} + wantPos := mgl64.Vec3{-0.012654883672021777, 2.7281474976710665, 3.5954677602500538} + wantVel := mgl64.Vec3{-0.02702143903177032, 0.15437050046578696, 0.1142856536788795} - const tolerance = 1e-6 + const tolerance = 1e-12 for axis, name := range []string{"X", "Y", "Z"} { - if math32.Abs(state.Pos[axis]-wantPos[axis]) > tolerance { + if math.Abs(state.Pos[axis]-wantPos[axis]) > tolerance { t.Errorf("Pos.%s = %.17g, want %.17g", name, state.Pos[axis], wantPos[axis]) } - if math32.Abs(state.Vel[axis]-wantVel[axis]) > tolerance { + if math.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 a6623e5..de49d54 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -1,14 +1,13 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "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/ethaniccc/float32-cube/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -23,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) []cube.BBox { +func (w *liquidWorld) BlockCollisions(pos cube.Pos) []cube.BBox { b := w.Block(pos) if _, air := b.(block.Air); air { return nil @@ -63,16 +62,16 @@ 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 []cube.BBox{cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())} } func (w *liquidWorld) GetNearbyBBoxes(aabb cube.BBox) []cube.BBox { min, max := aabb.Min(), aabb.Max() var out []cube.BBox - 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 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}) { if bb.IntersectsWith(aabb) { out = append(out, bb) } @@ -91,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 } @@ -142,7 +141,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 = mgl32.Vec3{0.5, 0.5, 0.5} + state.Pos = mgl64.Vec3{0.5, 0.5, 0.5} state.Client.Pos = state.Pos return state } @@ -151,14 +150,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(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 { - return math32.Abs(a-b) < 1e-6 +func approxEqual(a, b float64) bool { + return math.Abs(a-b) < 1e-9 } -func assertVec(t *testing.T, got, want mgl32.Vec3) { +func assertVec(t *testing.T, got, want mgl64.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) @@ -169,7 +168,7 @@ func assertVec(t *testing.T, got, want mgl32.Vec3) { // client's swim pose. This drives collision, liquid detection and exit probing. func TestSwimmingBoundingBoxUsesWidthAsHeight(t *testing.T) { state := newBaseState() - state.Pos = mgl32.Vec3{0.5, 10, 0.5} + state.Pos = mgl64.Vec3{0.5, 10, 0.5} standing := state.BoundingBox(false) if height := standing.Height(); !approxEqual(height, 1.8) { @@ -192,7 +191,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 = mgl32.Vec3{0.5, 10, 0.5} + state.Pos = mgl64.Vec3{0.5, 10, 0.5} state.Swimming = true state.SwimWaterGraceTicks = 0 @@ -210,12 +209,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(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.Pos = mgl64.Vec3{0.5, 0, 0.5} state.Client.Pos = state.Pos state.Swimming = true - state.Vel = mgl32.Vec3{0, 0.5, 0} + state.Vel = mgl64.Vec3{0, 0.5, 0} sim.SimulateState(state) if !state.CollideY { @@ -225,7 +224,7 @@ func TestSpoofedSwimmingCannotFitThroughCeilingGap(t *testing.T) { func TestSwimmingClientBoundingBoxUsesWidthAsHeight(t *testing.T) { state := newBaseState() - state.Client.Pos = mgl32.Vec3{0.5, 10, 0.5} + state.Client.Pos = mgl64.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) @@ -240,8 +239,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 = mgl32.Vec3{0.5, 10, 0.5} - state.Size = mgl32.Vec3{0.6, 1.8, 2} + state.Pos = mgl64.Vec3{0.5, 10, 0.5} + state.Size = mgl64.Vec3{0.6, 1.8, 2} state.Swimming = true state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks @@ -311,7 +310,7 @@ func TestSwimAmountInterpolation(t *testing.T) { for i := 1; i <= 3; i++ { sim.applyInput(state, InputState{}) - if want := float32(float32(i) * 0.1); !approxEqual(state.SwimAmount, want) { + if want := float64(i) * 0.1; !approxEqual(state.SwimAmount, want) { t.Fatalf("tick %d: SwimAmount = %v, want %v", i, state.SwimAmount, want) } } @@ -377,11 +376,11 @@ func TestWaterDragAndGravity(t *testing.T) { state := submergedState() sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) // Second tick: previous velocity is dragged by 0.8, then gravity applies. sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, -0.005*0.8 - 0.005, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, -0.005*0.8 - 0.005, 0}) } // Sprinting in water raises horizontal drag from 0.8 to 0.9. @@ -389,11 +388,11 @@ func TestWaterSprintDrag(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) normal := submergedState() - normal.Vel = mgl32.Vec3{0.5, 0, 0} + normal.Vel = mgl64.Vec3{0.5, 0, 0} sim.SimulateState(normal) sprinting := submergedState() - sprinting.Vel = mgl32.Vec3{0.5, 0, 0} + sprinting.Vel = mgl64.Vec3{0.5, 0, 0} sprinting.Sprinting = true sim.SimulateState(sprinting) @@ -409,21 +408,21 @@ func TestWaterSprintDrag(t *testing.T) { func TestWaterVerticalDragIndependentOfSprint(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() - state.Vel = mgl32.Vec3{0, 0.5, 0} + state.Vel = mgl64.Vec3{0, 0.5, 0} state.Sprinting = true sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, 0.5*0.8 - 0.005, 0}) + assertVec(t, state.Vel, mgl64.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 = mgl32.Vec3{0.4, 0.4, 0.4} + state.Vel = mgl64.Vec3{0.4, 0.4, 0.4} sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0.2, 0.4*0.5 - 0.02, 0.2}) + assertVec(t, state.Vel, mgl64.Vec3{0.2, 0.4*0.5 - 0.02, 0.2}) } // Swimming removes water gravity entirely. @@ -431,7 +430,7 @@ func TestSwimmingCancelsWaterGravity(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.Swimming = true - state.Rotation = mgl32.Vec3{0, 0, 0} + state.Rotation = mgl64.Vec3{0, 0, 0} sim.SimulateState(state) if !approxEqual(state.Vel.Y(), 0) { @@ -446,7 +445,7 @@ func TestNoGravityInLiquid(t *testing.T) { state.HasGravity = false sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{}) + assertVec(t, state.Vel, mgl64.Vec3{}) } // Levitation replaces liquid gravity with a pull toward the levitation target. @@ -457,7 +456,7 @@ func TestLevitationOverridesLiquidGravity(t *testing.T) { sim.SimulateState(state) // target = 0.05 * (0+1); vel += (target - vel) * 0.2 - assertVec(t, state.Vel, mgl32.Vec3{0, 0.05 * 0.2, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, 0.05 * 0.2, 0}) } func TestLevitationAmplifierScales(t *testing.T) { @@ -466,7 +465,7 @@ func TestLevitationAmplifierScales(t *testing.T) { state := submergedState() sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, (LevitationGravityMultiplier * 4) * 0.2, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, (LevitationGravityMultiplier * 4) * 0.2, 0}) } // A nil effects provider must not panic and must fall back to gravity. @@ -476,7 +475,7 @@ func TestNilEffectsProviderFallsBackToGravity(t *testing.T) { state := submergedState() sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) } // Falling into liquid clears accumulated fall distance. @@ -498,7 +497,7 @@ func TestEffectiveJumpingAscendsInWater(t *testing.T) { state.EffectiveJumping = true sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, 0.04*0.8 - 0.005, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, 0.04*0.8 - 0.005, 0}) } // Mid-transition into the swim pose zeroes the ascent instead of applying it. @@ -509,7 +508,7 @@ func TestSwimTransitionZeroesJumpAscent(t *testing.T) { state.SwimAmount = 0.5 sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) } // A fully-transitioned swimmer still ascends normally. @@ -537,7 +536,7 @@ func TestWantDownSinksInWater(t *testing.T) { apply(state) sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, -0.04*0.8 - 0.005, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, -0.04*0.8 - 0.005, 0}) }) } } @@ -549,7 +548,7 @@ func TestWantDownIgnoredInLava(t *testing.T) { state.WantDown = true sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, -0.02, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, -0.02, 0}) } // The descend inputs must not alter the sneak impulse clamp. Upstream dropped @@ -559,10 +558,10 @@ func TestDescendInputsDoNotChangeSneakImpulseClamp(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) sneaking := newBaseState() - sim.applyInput(sneaking, InputState{SneakDown: true, MoveVector: mgl32.Vec2{0, 1}}) + sim.applyInput(sneaking, InputState{SneakDown: true, MoveVector: mgl64.Vec2{0, 1}}) descending := newBaseState() - sim.applyInput(descending, InputState{SneakDown: true, WantDown: true, MoveVector: mgl32.Vec2{0, 1}}) + sim.applyInput(descending, InputState{SneakDown: true, WantDown: true, MoveVector: mgl64.Vec2{0, 1}}) if !approxEqual(descending.Impulse.Y(), sneaking.Impulse.Y()) { t.Fatalf("descending impulse %v must match sneaking impulse %v", @@ -578,11 +577,11 @@ func TestSwimTravelFollowsPitch(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.Swimming = true - state.Rotation = mgl32.Vec3{-90, 0, 0} // looking straight up + state.Rotation = mgl64.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, mgl32.Vec3{0, 0.06 * 0.8, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, 0.06 * 0.8, 0}) } // A steep downward pitch uses the faster 0.085 interpolation rate. @@ -590,11 +589,11 @@ func TestSwimTravelUsesFasterRateWhenDivingSteeply(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.Swimming = true - state.Rotation = mgl32.Vec3{90, 0, 0} // looking straight down + state.Rotation = mgl64.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, mgl32.Vec3{0, -0.085 * 0.8, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, -0.085 * 0.8, 0}) } // Swim travel is suppressed while jumping, letting the jump impulse win. @@ -604,25 +603,25 @@ func TestSwimTravelSkippedWhileJumping(t *testing.T) { state.Swimming = true state.EffectiveJumping = true state.SwimAmount = 1 - state.Rotation = mgl32.Vec3{90, 0, 0} + state.Rotation = mgl64.Vec3{90, 0, 0} sim.SimulateState(state) // Pitch steering skipped, so only the 0.04 jump impulse applies. - assertVec(t, state.Vel, mgl32.Vec3{0, 0.04 * 0.8, 0}) + assertVec(t, state.Vel, mgl64.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(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. - state.Pos = mgl32.Vec3{0.5, 1.5, 0.5} + state.Pos = mgl64.Vec3{0.5, 1.5, 0.5} state.Swimming = true - state.Rotation = mgl32.Vec3{-90, 0, 0} - state.Vel = mgl32.Vec3{0, 0.5, 0} + state.Rotation = mgl64.Vec3{-90, 0, 0} + state.Vel = mgl64.Vec3{0, 0.5, 0} // The hitbox has just left the water, so water travel is still in its // grace window. state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks @@ -636,12 +635,12 @@ 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 - state.Rotation = mgl32.Vec3{-90, 0, 0} - state.Vel = mgl32.Vec3{0, 0.5, 0} + state.Rotation = mgl64.Vec3{-90, 0, 0} + state.Vel = mgl64.Vec3{0, 0.5, 0} sim.SimulateState(state) if approxEqual(state.Vel.Y(), 0) { @@ -651,14 +650,14 @@ 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} + state.Pos = mgl64.Vec3{0.5, 1.5, 0.5} state.Swimming = true - state.Rotation = mgl32.Vec3{-90, 0, 0} + state.Rotation = mgl64.Vec3{-90, 0, 0} state.WantDownSlow = true - state.Vel = mgl32.Vec3{0, 0.5, 0} + state.Vel = mgl64.Vec3{0, 0.5, 0} state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks sim.SimulateState(state) @@ -672,13 +671,13 @@ func TestSwimTravelSurfaceClampSkippedWhenWantDownSlow(t *testing.T) { func TestDepthStriderLowersDragCoefficient(t *testing.T) { base := newLiquidSim(filledColumn(waterSource)) baseState := submergedState() - baseState.Vel = mgl32.Vec3{0.5, 0, 0} + baseState.Vel = mgl64.Vec3{0.5, 0, 0} base.SimulateState(baseState) strider := newLiquidSim(filledColumn(waterSource)) strider.Inventory = depthStriderInventory{level: 3} striderState := submergedState() - striderState.Vel = mgl32.Vec3{0.5, 0, 0} + striderState.Vel = mgl64.Vec3{0.5, 0, 0} striderState.OnGround = true strider.SimulateState(striderState) @@ -700,17 +699,17 @@ func TestDepthStriderLowersDragCoefficient(t *testing.T) { func TestDepthStriderIncreasesAcceleration(t *testing.T) { base := newLiquidSim(filledColumn(waterSource)) baseState := submergedState() - baseState.Impulse = mgl32.Vec2{0, 0.98} + baseState.Impulse = mgl64.Vec2{0, 0.98} base.SimulateState(baseState) strider := newLiquidSim(filledColumn(waterSource)) strider.Inventory = depthStriderInventory{level: 3} striderState := submergedState() - striderState.Impulse = mgl32.Vec2{0, 0.98} + striderState.Impulse = mgl64.Vec2{0, 0.98} striderState.OnGround = true strider.SimulateState(striderState) - if !(math32.Abs(striderState.Vel.Z()) > math32.Abs(baseState.Vel.Z())) { + if !(math.Abs(striderState.Vel.Z()) > math.Abs(baseState.Vel.Z())) { t.Fatalf("depth strider Z = %v must exceed base Z = %v", striderState.Vel.Z(), baseState.Vel.Z()) } @@ -721,12 +720,12 @@ func TestDepthStriderHalvedWhenAirborne(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) sim.Inventory = depthStriderInventory{level: 3} state := submergedState() - state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Vel = mgl64.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 := float32(0.5 * (0.8 + (0.54600006-0.8)*0.5)) + want := 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) } @@ -737,7 +736,7 @@ func TestDepthStriderClampedToMaxLevel(t *testing.T) { clamped := newLiquidSim(filledColumn(waterSource)) clamped.Inventory = depthStriderInventory{level: 99} clampedState := submergedState() - clampedState.Vel = mgl32.Vec3{0.5, 0, 0} + clampedState.Vel = mgl64.Vec3{0.5, 0, 0} clampedState.OnGround = true clamped.SimulateState(clampedState) @@ -751,7 +750,7 @@ func TestDepthStriderNegativeLevelIgnored(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) sim.Inventory = depthStriderInventory{level: -5} state := submergedState() - state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Vel = mgl64.Vec3{0.5, 0, 0} sim.SimulateState(state) if !approxEqual(state.Vel.X(), 0.5*0.8) { @@ -764,7 +763,7 @@ func TestDepthStriderIgnoredInLava(t *testing.T) { sim := newLiquidSim(filledColumn(lavaSource)) sim.Inventory = depthStriderInventory{level: 3} state := submergedState() - state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Vel = mgl64.Vec3{0.5, 0, 0} sim.SimulateState(state) if !approxEqual(state.Vel.X(), 0.5*0.5) { @@ -777,7 +776,7 @@ func TestInventoryWithoutDepthStriderProvider(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) sim.Inventory = mockInventory{} state := submergedState() - state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Vel = mgl64.Vec3{0.5, 0, 0} sim.SimulateState(state) if !approxEqual(state.Vel.X(), 0.5*0.8) { @@ -790,7 +789,7 @@ func TestZeroEquipmentDepthStriderFallsBackToLegacyInventory(t *testing.T) { sim.Inventory = depthStriderInventory{level: 3} sim.Equipment = fixedEquipment{} state := submergedState() - state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Vel = mgl64.Vec3{0.5, 0, 0} state.OnGround = true sim.SimulateState(state) @@ -807,24 +806,24 @@ func TestSwimSpeedMultiplierRequiresSwimming(t *testing.T) { boostedState := submergedState() boostedState.Swimming = true boostedState.SwimSpeedMultiplier = 2 - boostedState.Impulse = mgl32.Vec2{0, 0.98} + boostedState.Impulse = mgl64.Vec2{0, 0.98} boosted.SimulateState(boostedState) plain := newLiquidSim(filledColumn(waterSource)) plainState := submergedState() plainState.Swimming = true plainState.SwimSpeedMultiplier = 1 - plainState.Impulse = mgl32.Vec2{0, 0.98} + plainState.Impulse = mgl64.Vec2{0, 0.98} plain.SimulateState(plainState) - if !(math32.Abs(boostedState.Vel.Z()) > math32.Abs(plainState.Vel.Z())) { + if !(math.Abs(boostedState.Vel.Z()) > math.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 = mgl32.Vec2{0, 0.98} + notSwimmingState.Impulse = mgl64.Vec2{0, 0.98} notSwimming.SimulateState(notSwimmingState) if !approxEqual(notSwimmingState.Vel.Z(), plainState.Vel.Z()) { @@ -864,14 +863,14 @@ func TestZeroSwimSpeedMultiplierTreatedAsDefault(t *testing.T) { state := submergedState() state.Swimming = true state.SwimSpeedMultiplier = 0 - state.Impulse = mgl32.Vec2{0, 0.98} + state.Impulse = mgl64.Vec2{0, 0.98} sim.SimulateState(state) explicit := newLiquidSim(filledColumn(waterSource)) explicitState := submergedState() explicitState.Swimming = true explicitState.SwimSpeedMultiplier = DefaultSwimSpeedMultiplier - explicitState.Impulse = mgl32.Vec2{0, 0.98} + explicitState.Impulse = mgl64.Vec2{0, 0.98} explicit.SimulateState(explicitState) assertVec(t, state.Vel, explicitState.Vel) @@ -882,13 +881,13 @@ func TestZeroMovementSpeedsUseDefaults(t *testing.T) { sim := newLiquidSim(filledColumn(waterSource)) state := submergedState() state.UnderwaterMovementSpeed = 0 - state.Impulse = mgl32.Vec2{0, 0.98} + state.Impulse = mgl64.Vec2{0, 0.98} sim.SimulateState(state) explicit := newLiquidSim(filledColumn(waterSource)) explicitState := submergedState() explicitState.UnderwaterMovementSpeed = DefaultUnderwaterMovementSpeed - explicitState.Impulse = mgl32.Vec2{0, 0.98} + explicitState.Impulse = mgl64.Vec2{0, 0.98} explicit.SimulateState(explicitState) assertVec(t, state.Vel, explicitState.Vel) @@ -897,7 +896,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 { @@ -908,11 +907,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(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. - state.Pos = mgl32.Vec3{0.75, 0.5, 0.5} + state.Pos = mgl64.Vec3{0.75, 0.5, 0.5} water := sim.touchingLiquidBlocks(state, liquidWater) lava := sim.touchingLiquidBlocks(state, liquidLava) @@ -940,14 +939,14 @@ 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() sim.SimulateState(state) // Water gravity (0.005), not lava gravity (0.02). - assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) } // Without a LiquidProvider, liquids are read from WorldProvider.Block. @@ -956,14 +955,14 @@ func TestLiquidFallsBackToBlockProvider(t *testing.T) { state := submergedState() sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl64.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(dfcube.Pos{0, y, 0}, block.Air{}, waterSource) + w.waterlog(cube.Pos{0, y, 0}, block.Air{}, waterSource) } sim := newLiquidSim(w) state := submergedState() @@ -972,7 +971,7 @@ func TestLiquidProviderDetectsWaterloggedBlocks(t *testing.T) { t.Fatal("expected waterlogged blocks to register as water") } sim.SimulateState(state) - assertVec(t, state.Vel, mgl32.Vec3{0, -0.005, 0}) + assertVec(t, state.Vel, mgl64.Vec3{0, -0.005, 0}) } // A world with no liquids at all must run normal (non-liquid) physics. @@ -999,13 +998,13 @@ func TestUnloadedChunkCancelsLiquidSimulation(t *testing.T) { w.chunkLoaded = false sim := newLiquidSim(w) state := submergedState() - state.Vel = mgl32.Vec3{0.5, 0.5, 0.5} + state.Vel = mgl64.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, mgl32.Vec3{}) + assertVec(t, state.Vel, mgl64.Vec3{}) } // Being inside a liquid is a reliable scenario; v0.1.3 bailed out here. @@ -1075,7 +1074,7 @@ func TestSwimmingPreservesWaterTravelOutsideWater(t *testing.T) { sim := newLiquidSim(newLiquidWorld()) state := submergedState() state.Swimming = true - state.Rotation = mgl32.Vec3{0, 0, 0} + state.Rotation = mgl64.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. @@ -1122,11 +1121,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() @@ -1139,8 +1138,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() @@ -1153,8 +1152,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() @@ -1170,17 +1169,17 @@ func TestUniformLiquidHasNoFlow(t *testing.T) { state := submergedState() sim.applyLiquidFlow(state, sim.touchingLiquidBlocks(state, liquidWater), liquidWater) - assertVec(t, state.Vel, mgl32.Vec3{}) + assertVec(t, state.Vel, mgl64.Vec3{}) } // 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()) } @@ -1189,11 +1188,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()) } @@ -1202,12 +1201,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()) } @@ -1216,11 +1215,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()) } @@ -1267,12 +1266,12 @@ 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} - state.Impulse = mgl32.Vec2{0, 0.98} + state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Impulse = mgl64.Vec2{0, 0.98} sim.SimulateState(state) if !state.CollideX { @@ -1292,23 +1291,23 @@ 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} + state.Pos = mgl64.Vec3{0.5, 0.4, 0.5} state.Client.Pos = state.Pos state.Swimming = true state.SwimWaterGraceTicks = DefaultSwimWaterGraceTicks - state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Vel = mgl64.Vec3{0.5, 0, 0} return newLiquidSim(w), state } for _, overhang := range []bool{false, true} { sim, state := build(overhang) - raised := state.BoundingBox(false).Translate(mgl32.Vec3{0, 0.6, 0}) + raised := state.BoundingBox(false).Translate(mgl64.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) } @@ -1335,11 +1334,11 @@ 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} + state.Vel = mgl64.Vec3{0.5, 0, 0} sim.SimulateState(state) if approxEqual(state.Vel.Y(), 0.3) { @@ -1398,7 +1397,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, mgl32.Vec3{0.001, 0.401, 0.001}) + shrunk := shrinkLiquidBox(box, mgl64.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 a0928b2..431c4ed 100644 --- a/math.go +++ b/math.go @@ -1,48 +1,39 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" - dfcube "github.com/df-mc/dragonfly/server/block/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/go-gl/mathgl/mgl64" ) -var mcSinTable []float32 +var mcSinTable []float64 func init() { - mcSinTable = make([]float32, 65536) + mcSinTable = make([]float64, 65536) for i := range 65536 { - mcSinTable[i] = math32.Sin(float32(i) * math32.Pi * 2 / 65536) + mcSinTable[i] = math.Sin(float64(i) * math.Pi * 2 / 65536) } } // MCSin returns the Minecraft sin of the given angle. -func MCSin(val float32) float32 { +func MCSin(val float64) float64 { return mcSinTable[uint16(val*10430.378)&65535] } // MCCos returns the Minecraft cos of the given angle. -func MCCos(val float32) float32 { +func MCCos(val float64) float64 { return mcSinTable[uint16(val*10430.378+16384.0)&65535] } // ClampFloat clamps the given value to the given range. -func ClampFloat(num, min, max float32) float32 { +func ClampFloat(num, min, max float64) float64 { if num < min { return min } - return math32.Min(num, max) + return math.Min(num, max) } // Vec3HzDistSqr returns the squared horizontal distance in a vector. -func Vec3HzDistSqr(vec3 mgl32.Vec3) float32 { +func Vec3HzDistSqr(vec3 mgl64.Vec3) float64 { 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 eb39cfe..8948c11 100644 --- a/movement.go +++ b/movement.go @@ -1,15 +1,15 @@ package bedsim import ( - dfcube "github.com/df-mc/dragonfly/server/block/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" ) // ClientState holds non-authoritative movement data sent by the client. type ClientState struct { - Pos, LastPos mgl32.Vec3 - Vel, LastVel mgl32.Vec3 - Mov, LastMov mgl32.Vec3 + Pos, LastPos mgl64.Vec3 + Vel, LastVel mgl64.Vec3 + Mov, LastMov mgl64.Vec3 HorizontalCollision bool VerticalCollision bool @@ -20,46 +20,46 @@ type ClientState struct { type MovementState struct { Client ClientState - Pos, LastPos mgl32.Vec3 - Vel, LastVel mgl32.Vec3 - Mov, LastMov mgl32.Vec3 + Pos, LastPos mgl64.Vec3 + Vel, LastVel mgl64.Vec3 + Mov, LastMov mgl64.Vec3 - Rotation, LastRotation mgl32.Vec3 + Rotation, LastRotation mgl64.Vec3 - SlideOffset mgl32.Vec2 - Impulse mgl32.Vec2 - Size mgl32.Vec3 + SlideOffset mgl64.Vec2 + Impulse mgl64.Vec2 + Size mgl64.Vec3 // StandingHeight, SneakingHeight, and CrawlingHeight preserve custom entity // dimensions across pose transitions. Zero values use the current standing // height and vanilla player pose heights respectively. - StandingHeight float32 - SneakingHeight float32 - CrawlingHeight float32 + StandingHeight float64 + SneakingHeight float64 + CrawlingHeight float64 - SupportingBlockPos *dfcube.Pos + SupportingBlockPos *cube.Pos - Gravity float32 - JumpHeight float32 - FallDistance float32 + Gravity float64 + JumpHeight float64 + FallDistance float64 - MovementSpeed float32 - DefaultMovementSpeed float32 - AirSpeed float32 - UnderwaterMovementSpeed float32 - LavaMovementSpeed float32 + MovementSpeed float64 + DefaultMovementSpeed float64 + AirSpeed float64 + UnderwaterMovementSpeed float64 + LavaMovementSpeed float64 // SwimSpeedMultiplier scales swimming acceleration; zero means the default. - SwimSpeedMultiplier float32 + SwimSpeedMultiplier float64 // DolphinBoostTicks is the remaining dolphin-boost duration. DolphinBoostTicks int64 ServerUpdatedSpeed bool - Knockback mgl32.Vec3 + Knockback mgl64.Vec3 TicksSinceKnockback uint64 - PendingTeleportPos mgl32.Vec3 + PendingTeleportPos mgl64.Vec3 PendingTeleports int - TeleportPos mgl32.Vec3 + TeleportPos mgl64.Vec3 TicksSinceTeleport uint64 TeleportCompletionTicks uint64 TeleportIsSmoothed bool @@ -76,7 +76,7 @@ type MovementState struct { JumpDelay uint64 Swimming bool - SwimAmount float32 + SwimAmount float64 // SwimWaterGraceTicks retains recent server-observed water contact. SwimWaterGraceTicks int64 AutoJumpingInWater bool @@ -134,22 +134,22 @@ func (s *MovementState) ensurePoseHeights() { } } -func (s *MovementState) SetPos(newPos mgl32.Vec3) { +func (s *MovementState) SetPos(newPos mgl64.Vec3) { s.LastPos = s.Pos s.Pos = newPos } -func (s *MovementState) SetVel(newVel mgl32.Vec3) { +func (s *MovementState) SetVel(newVel mgl64.Vec3) { s.LastVel = s.Vel s.Vel = newVel } -func (s *MovementState) SetMov(newMov mgl32.Vec3) { +func (s *MovementState) SetMov(newMov mgl64.Vec3) { s.LastMov = s.Mov s.Mov = newMov } -func (s *MovementState) SetRotation(newRot mgl32.Vec3) { +func (s *MovementState) SetRotation(newRot mgl64.Vec3) { s.LastRotation = s.Rotation s.Rotation = newRot } diff --git a/movement_environment_test.go b/movement_environment_test.go index 0cbaaf2..252e235 100644 --- a/movement_environment_test.go +++ b/movement_environment_test.go @@ -2,18 +2,17 @@ package bedsim import ( "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/ethaniccc/float32-cube/cube" ) type environmentWorld struct { - bubbles map[dfcube.Pos]BubbleColumnDirection - solids map[dfcube.Pos]bool - blocks map[dfcube.Pos]world.Block + bubbles map[cube.Pos]BubbleColumnDirection + solids map[cube.Pos]bool + blocks map[cube.Pos]world.Block } -func (w environmentWorld) Block(pos dfcube.Pos) world.Block { +func (w environmentWorld) Block(pos cube.Pos) world.Block { if b, ok := w.blocks[pos]; ok { return b } @@ -23,9 +22,9 @@ func (w environmentWorld) Block(pos dfcube.Pos) world.Block { return block.Air{} } -func (w environmentWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { +func (w environmentWorld) BlockCollisions(pos cube.Pos) []cube.BBox { if w.solids[pos] { - return []cube.BBox{cube.Box(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))} + return []cube.BBox{cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())} } return nil } @@ -33,12 +32,12 @@ func (w environmentWorld) BlockCollisions(pos dfcube.Pos) []cube.BBox { func (w environmentWorld) GetNearbyBBoxes(cube.BBox) []cube.BBox { return nil } func (w environmentWorld) IsChunkLoaded(int32, int32) bool { return true } -func (w environmentWorld) Liquid(pos dfcube.Pos) (world.Liquid, bool) { +func (w environmentWorld) Liquid(pos cube.Pos) (world.Liquid, bool) { liquid, ok := w.Block(pos).(world.Liquid) return liquid, ok } -func (w environmentWorld) BubbleColumn(pos dfcube.Pos) (BubbleColumnDirection, bool) { +func (w environmentWorld) BubbleColumn(pos cube.Pos) (BubbleColumnDirection, bool) { direction, ok := w.bubbles[pos] return direction, ok } diff --git a/native_float32_test.go b/native_float32_test.go deleted file mode 100644 index 176769d..0000000 --- a/native_float32_test.go +++ /dev/null @@ -1,25 +0,0 @@ -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/parity_test.go b/parity_test.go index 14ddebf..7666c31 100644 --- a/parity_test.go +++ b/parity_test.go @@ -1,12 +1,12 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "testing" "github.com/df-mc/dragonfly/server/block" - "github.com/ethaniccc/float32-cube/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -23,7 +23,7 @@ func TestJumpBoostUsesZeroBasedEffectAmplifier(t *testing.T) { sim.applyInput(state, InputState{}) - if want := float32(0.52); math32.Abs(state.JumpHeight-want) > 1e-6 { + if want := 0.52; math.Abs(state.JumpHeight-want) > 1e-12 { t.Fatalf("expected jump boost I height %v, got %v", want, state.JumpHeight) } } @@ -35,7 +35,7 @@ func TestLevitationUsesZeroBasedEffectAmplifier(t *testing.T) { sim.SimulateState(state) - if want := float32(0.01); math32.Abs(state.Vel.Y()-want) > 1e-6 { + if want := 0.01; math.Abs(state.Vel.Y()-want) > 1e-12 { t.Fatalf("expected levitation I velocity %v, got %v", want, state.Vel.Y()) } } @@ -43,19 +43,19 @@ func TestLevitationUsesZeroBasedEffectAmplifier(t *testing.T) { func TestSlowFallingOnlyChangesGravityWhileDescending(t *testing.T) { sim := &Simulator{World: mockWorld{}, Effects: fixedEffects{packet.EffectSlowFalling: 0}} state := newBaseState() - state.Vel = mgl32.Vec3{0, 0.2} + state.Vel = mgl64.Vec3{0, 0.2} state.Gravity = NormalGravity state.SlowFalling = true sim.SimulateState(state) - if want := float32((0.2 - NormalGravity) * NormalGravityMultiplier); math32.Abs(state.Vel.Y()-want) > 1e-6 { + if want := (0.2 - NormalGravity) * NormalGravityMultiplier; math.Abs(state.Vel.Y()-want) > 1e-12 { t.Fatalf("expected normal gravity while ascending, want %v, got %v", want, state.Vel.Y()) } } func TestBedrockStepHeight(t *testing.T) { - if want := float32(0.6); StepHeight != want { + if want := 0.5625; StepHeight != want { t.Fatalf("expected Bedrock step height %v, got %v", want, StepHeight) } } @@ -63,25 +63,25 @@ func TestBedrockStepHeight(t *testing.T) { func TestBedBounceUsesBedrockRestitutionAndCap(t *testing.T) { sim := &Simulator{BlockSemantics: overrideBlockSemantics{name: "minecraft:bed"}} state := newBaseState() - state.Vel = mgl32.Vec3{0, -2} + state.Vel = mgl64.Vec3{0, -2} sim.landOnBlock(state, state.Vel, block.Air{}) - if want := float32(0.75); state.Vel.Y() != want { + if want := 0.75; state.Vel.Y() != want { t.Fatalf("expected bed bounce %v, got %v", want, state.Vel.Y()) } } -func TestTinyVelocityUsesOriginalSquaredThreshold(t *testing.T) { +func TestTinyVelocityIsNotDiscardedPrematurely(t *testing.T) { sim := &Simulator{World: mockWorld{}} state := newBaseState() state.HasGravity = false - state.Vel = mgl32.Vec3{1e-7, 0, 0} + state.Vel = mgl64.Vec3{1e-7, 0, 0} sim.SimulateState(state) - if state.Vel != (mgl32.Vec3{}) { - t.Fatalf("expected tiny velocity to be zeroed, got %v", state.Vel) + if state.Vel.X() == 0 { + t.Fatal("expected Bedrock-scale tiny velocity to remain non-zero") } } @@ -95,7 +95,7 @@ func TestSlowFallingChangesGlideGravity(t *testing.T) { sim.SimulateState(state) - if want := float32(-0.011025); math32.Abs(state.Vel.Y()-want) > 1e-6 { + if want := -0.011025; math.Abs(state.Vel.Y()-want) > 1e-9 { t.Fatalf("expected slow-falling glide velocity %v, got %v", want, state.Vel.Y()) } } @@ -108,7 +108,7 @@ func TestSneakEdgeProtectionWhileSlightlyAboveGround(t *testing.T) { state.Sneaking = true state.OnGround = false state.FallDistance = 0.1 - state.Vel = mgl32.Vec3{0.5, 0, 0} + state.Vel = mgl64.Vec3{0.5, 0, 0} sim.avoidEdge(state) diff --git a/player_features_test.go b/player_features_test.go index 28b438b..54e7edb 100644 --- a/player_features_test.go +++ b/player_features_test.go @@ -1,30 +1,29 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "testing" - 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/ethaniccc/float32-cube/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) func TestSoulSpeedSkipsSoulSandSlowdown(t *testing.T) { - w := environmentWorld{blocks: map[dfcube.Pos]world.Block{ + w := environmentWorld{blocks: map[cube.Pos]world.Block{ {0, 0, 0}: namedBlock{name: "minecraft:soul_sand"}, }} base := newBaseState() - base.Pos = mgl32.Vec3{0.5, 1, 0.5} + base.Pos = mgl64.Vec3{0.5, 1, 0.5} base.OnGround = true base.HasGravity = false without := *base - (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).Simulate(&without, InputState{MoveVector: mgl32.Vec2{0, 1}}) + (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}}).Simulate(&without, InputState{MoveVector: mgl64.Vec2{0, 1}}) with := *base - (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}, Equipment: fixedEquipment{EnchantmentSoulSpeed: 1}}).Simulate(&with, InputState{MoveVector: mgl32.Vec2{0, 1}}) + (&Simulator{World: w, BlockSemantics: encodedBlockSemantics{}, Equipment: fixedEquipment{EnchantmentSoulSpeed: 1}}).Simulate(&with, InputState{MoveVector: mgl64.Vec2{0, 1}}) if with.Vel.Z() <= without.Vel.Z() { t.Fatalf("expected Soul Speed to bypass soul-sand slowdown: with=%v without=%v", with.Vel.Z(), without.Vel.Z()) @@ -34,15 +33,15 @@ func TestSoulSpeedSkipsSoulSandSlowdown(t *testing.T) { func TestSwiftSneakAppliesAfterTwoSlowdownTicks(t *testing.T) { sim := &Simulator{Equipment: fixedEquipment{EnchantmentSwiftSneak: 3}} state := newBaseState() - input := InputState{SneakDown: true, MoveVector: mgl32.Vec2{0, 1}} + input := InputState{SneakDown: true, MoveVector: mgl64.Vec2{0, 1}} sim.applyInput(state, input) - if want := float32(0.3 * 0.98); math32.Abs(state.Impulse.Y()-want) > 1e-6 { + if want := 0.3 * 0.98; math.Abs(state.Impulse.Y()-want) > 1e-12 { t.Fatalf("expected first-tick sneak impulse %v, got %v", want, state.Impulse.Y()) } sim.applyInput(state, input) sim.applyInput(state, input) - if want := float32(0.75 * 0.98); math32.Abs(state.Impulse.Y()-want) > 1e-6 { + if want := 0.75 * 0.98; math.Abs(state.Impulse.Y()-want) > 1e-12 { t.Fatalf("expected Swift Sneak impulse %v after two ticks, got %v", want, state.Impulse.Y()) } } @@ -51,18 +50,18 @@ func TestItemUseAndInventoryActionInputRules(t *testing.T) { tests := []struct { name string input InputState - want float32 + want float64 }{ - {name: "using item", input: InputState{UsingItem: true, MoveVector: mgl32.Vec2{0, 1}}, want: MaxConsumingImpulse * 0.98}, - {name: "using spear", input: InputState{UsingItem: true, UsingSpear: true, MoveVector: mgl32.Vec2{0, 1}}, want: 0.98}, - {name: "inventory action", input: InputState{InventoryAction: true, MoveVector: mgl32.Vec2{0, 1}}, want: 0}, + {name: "using item", input: InputState{UsingItem: true, MoveVector: mgl64.Vec2{0, 1}}, want: MaxConsumingImpulse * 0.98}, + {name: "using spear", input: InputState{UsingItem: true, UsingSpear: true, MoveVector: mgl64.Vec2{0, 1}}, want: 0.98}, + {name: "inventory action", input: InputState{InventoryAction: true, MoveVector: mgl64.Vec2{0, 1}}, want: 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { state := newBaseState() (&Simulator{}).applyInput(state, tt.input) - if math32.Abs(state.Impulse.Y()-tt.want) > 1e-6 { + if math.Abs(state.Impulse.Y()-tt.want) > 1e-12 { t.Fatalf("expected impulse %v, got %v", tt.want, state.Impulse.Y()) } }) @@ -74,12 +73,12 @@ func TestCrawlingUpdatesPoseAndSlowdown(t *testing.T) { sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox{ cube.Box(-1, 0.7, -1, 1, 2, 1), }}} - sim.applyInput(state, InputState{StartCrawling: true, MoveVector: mgl32.Vec2{0, 1}}) + sim.applyInput(state, InputState{StartCrawling: true, MoveVector: mgl64.Vec2{0, 1}}) if !state.Crawling || state.Size.Y() != 0.6 { t.Fatalf("expected crawling pose, got crawling=%v size=%v", state.Crawling, state.Size) } - if want := float32(0.3 * 0.98); math32.Abs(state.Impulse.Y()-want) > 1e-6 { + if want := 0.3 * 0.98; math.Abs(state.Impulse.Y()-want) > 1e-12 { t.Fatalf("expected crawling slowdown %v, got %v", want, state.Impulse.Y()) } } diff --git a/result.go b/result.go index 029881a..cb0bb4b 100644 --- a/result.go +++ b/result.go @@ -1,6 +1,6 @@ package bedsim -import "github.com/go-gl/mathgl/mgl32" +import "github.com/go-gl/mathgl/mgl64" // 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 mgl32.Vec3 - Velocity mgl32.Vec3 - Movement mgl32.Vec3 + Position mgl64.Vec3 + Velocity mgl64.Vec3 + Movement mgl64.Vec3 OnGround bool CollideX bool CollideY bool CollideZ bool - PositionDelta mgl32.Vec3 - VelocityDelta mgl32.Vec3 + PositionDelta mgl64.Vec3 + VelocityDelta mgl64.Vec3 NeedsCorrection bool Outcome SimulationOutcome diff --git a/simulation.go b/simulation.go index 5f1a213..a21a71a 100644 --- a/simulation.go +++ b/simulation.go @@ -1,14 +1,13 @@ package bedsim import ( - "github.com/chewxy/math32" "iter" + "math" "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/ethaniccc/float32-cube/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -68,13 +67,13 @@ func (s *Simulator) simulateCore(state *MovementState) SimulationOutcome { s.resetToClient(state) return SimulationOutcomeUnreliable } - 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{}) + 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{}) state.SwimWaterGraceTicks = 0 return SimulationOutcomeUnloadedChunk } if state.Immobile || !state.Ready { - state.SetVel(mgl32.Vec3{}) + state.SetVel(mgl64.Vec3{}) // Frozen ticks observe nothing, so the budget must not simply pause // and resume later. state.SwimWaterGraceTicks = 0 @@ -140,7 +139,7 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { state.Client.ToggledFly = false } - state.SetRotation(mgl32.Vec3{input.Pitch, input.HeadYaw, input.Yaw}) + state.SetRotation(mgl64.Vec3{input.Pitch, input.HeadYaw, input.Yaw}) state.PressingSneak = input.Sneaking state.PressingSprint = input.SprintDown @@ -239,7 +238,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 := float32(1) + maxImpulse := 1.0 if !s.Options.UpstreamImpulseClamping { if input.UsingConsumable || (input.UsingItem && !input.UsingSpear) { maxImpulse *= MaxConsumingImpulse @@ -248,19 +247,19 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { state.TicksSinceCanSlowdown++ sneakMultiplier := MaxSneakImpulse if state.TicksSinceCanSlowdown > 2 && s.Equipment != nil { - sneakMultiplier += 0.15 * float32(s.Equipment.EnchantmentLevel(EnchantmentSwiftSneak)) + sneakMultiplier += 0.15 * float64(s.Equipment.EnchantmentLevel(EnchantmentSwiftSneak)) } maxImpulse *= ClampFloat(sneakMultiplier, 0, 1) } else { state.TicksSinceCanSlowdown = 0 } } - moveVector := mgl32.Vec2{ + moveVector := mgl64.Vec2{ ClampFloat(input.MoveVector[0], -maxImpulse, maxImpulse), ClampFloat(input.MoveVector[1], -maxImpulse, maxImpulse), } if input.InventoryAction { - moveVector = mgl32.Vec2{} + moveVector = mgl64.Vec2{} } // Ground jumps are edge-triggered; liquid and ladder ascent may be held. @@ -270,7 +269,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 += float32(amp+1) * 0.1 + state.JumpHeight += float64(amp+1) * 0.1 } } @@ -333,7 +332,7 @@ func (s *Simulator) tickState(state *MovementState) { } } state.TicksSinceKnockback++ - if state.TicksSinceTeleport < math32.MaxUint64 { + if state.TicksSinceTeleport < math.MaxUint64 { state.TicksSinceTeleport++ } if state.JumpDelay > 0 { @@ -346,9 +345,13 @@ func (s *Simulator) tickState(state *MovementState) { } func (s *Simulator) simulateMovement(state *MovementState) { - if state.Vel.LenSqr() < 1e-12 { - state.SetVel(mgl32.Vec3{}) + vel := state.Vel + for axis := range 3 { + if math.Abs(vel[axis]) < 1e-8 { + vel[axis] = 0 + } } + state.SetVel(vel) // Bound retained water evidence before collision and travel inspect it. grace := s.swimWaterGraceTicks() @@ -391,8 +394,8 @@ func (s *Simulator) simulateMovement(state *MovementState) { return } - blockUnder := s.blockAtPos(posFromVec3(state.Pos.Sub(mgl32.Vec3{0, 0.5}))) - blockFriction := float32(DefaultAirFriction) + blockUnder := s.blockAtPos(cube.PosFromVec3(state.Pos.Sub(mgl64.Vec3{0, 0.5}))) + blockFriction := DefaultAirFriction moveRelativeSpeed := state.AirSpeed if state.OnGround { mSpeed := state.MovementSpeed @@ -432,11 +435,11 @@ func (s *Simulator) simulateMovement(state *MovementState) { moveRelative(state, moveRelativeSpeed) 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) - insideName := s.blockName(s.blockAtPos(posFromVec3(state.Pos))) + insideName := s.blockName(s.blockAtPos(cube.PosFromVec3(state.Pos))) leatherBoots := s.Equipment != nil && s.Equipment.WearingLeatherBoots() applyAscendableMovement(state, insideName, leatherBoots) - nearClimbable := s.blockClimbable(s.blockAtPos(posFromVec3(state.Pos))) + nearClimbable := s.blockClimbable(s.blockAtPos(cube.PosFromVec3(state.Pos))) if nearClimbable { newVel := state.Vel negClimbSpeed := -ClimbSpeed @@ -457,7 +460,7 @@ func (s *Simulator) simulateMovement(state *MovementState) { if inWeb { newVel := state.Vel - xz, y := float32(0.25), float32(0.05) + xz, y := 0.25, 0.05 if s.Effects != nil { if _, weaving := s.Effects.GetEffect(EffectWeaving); weaving { xz, y = 0.5, 0.25 @@ -481,9 +484,9 @@ func (s *Simulator) simulateMovement(state *MovementState) { if state.SupportingBlockPos != nil { blockUnder = s.blockAtPos(*state.SupportingBlockPos) } else { - blockUnder = s.blockAtPos(posFromVec3(state.Pos.Sub(mgl32.Vec3{0, 0.2}))) + blockUnder = s.blockAtPos(cube.PosFromVec3(state.Pos.Sub(mgl64.Vec3{0, 0.2}))) if s.blockAir(blockUnder) { - below := s.blockAtPos(posFromVec3(state.Pos).Side(dfcube.FaceDown)) + below := s.blockAtPos(cube.PosFromVec3(state.Pos).Side(cube.FaceDown)) if IsWall(below) || IsFence(below) { blockUnder = below } @@ -501,13 +504,13 @@ func (s *Simulator) simulateMovement(state *MovementState) { if inWeb { s.debugf("post-move web force applied (0 vel)") - state.SetVel(mgl32.Vec3{}) + state.SetVel(mgl64.Vec3{}) } newVel := state.Vel if s.Effects != nil { if amp, ok := s.Effects.GetEffect(packet.EffectLevitation); ok { - levSpeed := LevitationGravityMultiplier * float32(amp+1) + levSpeed := LevitationGravityMultiplier * float64(amp+1) newVel[1] += (levSpeed - newVel[1]) * 0.2 } else if state.HasGravity { newVel[1] -= effectiveGravity(state, newVel) @@ -584,7 +587,7 @@ func (s *Simulator) attemptTeleport(state *MovementState) bool { if !state.TeleportIsSmoothed { state.SetPos(state.TeleportPos) - state.SetVel(mgl32.Vec3{}) + state.SetVel(mgl64.Vec3{}) state.JumpDelay = 0 s.attemptJump(state, nil) return true @@ -592,7 +595,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 / float32(remaining))) + newPos := state.Pos.Add(posDelta.Mul(1.0 / float64(remaining))) state.SetPos(newPos) state.JumpDelay = 0 return remaining > 1 @@ -601,10 +604,10 @@ func (s *Simulator) attemptTeleport(state *MovementState) bool { } func (s *Simulator) simulateGlide(state *MovementState) { - radians := math32.Pi / 180.0 + radians := math.Pi / 180.0 yaw, pitch := state.Rotation.Z()*radians, state.Rotation.X()*radians - yawCos := MCCos(-yaw - math32.Pi) - yawSin := MCSin(-yaw - math32.Pi) + yawCos := MCCos(-yaw - math.Pi) + yawSin := MCSin(-yaw - math.Pi) pitchCos := MCCos(pitch) pitchSin := MCSin(pitch) @@ -613,7 +616,7 @@ func (s *Simulator) simulateGlide(state *MovementState) { lookZ := yawCos * -pitchCos vel := state.Vel - velHz := math32.Sqrt(vel[0]*vel[0] + vel[2]*vel[2]) + velHz := math.Sqrt(vel[0]*vel[0] + vel[2]*vel[2]) lookHz := pitchCos sqrPitchCos := pitchCos * pitchCos @@ -662,7 +665,7 @@ func (s *Simulator) walkOnBlock(state *MovementState, blockUnder world.Block) { newVel := state.Vel switch s.blockName(blockUnder) { case "minecraft:slime", "minecraft:honey_block": - yMov := math32.Abs(newVel.Y()) + yMov := math.Abs(newVel.Y()) if yMov < 0.1 && !state.PressingSneak { d1 := 0.4 + yMov*0.2 newVel[0] *= d1 @@ -673,7 +676,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 mgl32.Vec3, blockUnder world.Block) { +func (s *Simulator) landOnBlock(state *MovementState, old mgl64.Vec3, blockUnder world.Block) { newVel := state.Vel if old.Y() >= 0 || state.PressingSneak { newVel[1] = 0 @@ -684,25 +687,25 @@ func (s *Simulator) landOnBlock(state *MovementState, old mgl32.Vec3, blockUnder switch s.blockName(blockUnder) { case "minecraft:slime": newVel[1] = SlimeBounceMultiplier * old.Y() - if math32.Abs(newVel[1]) < 1e-4 { + if math.Abs(newVel[1]) < 1e-4 { newVel[1] = 0.0 } case "minecraft:bed": - newVel[1] = math32.Min(0.75, BedBounceMultiplier*old.Y()) + newVel[1] = math.Min(0.75, BedBounceMultiplier*old.Y()) default: newVel[1] = 0 } state.SetVel(newVel) } -func effectiveGravity(state *MovementState, velocity mgl32.Vec3) float32 { +func effectiveGravity(state *MovementState, velocity mgl64.Vec3) float64 { if state.SlowFalling && velocity.Y() < 0 { return SlowFallingGravity } return state.Gravity } -func (s *Simulator) setPostCollisionMotion(state *MovementState, oldVel mgl32.Vec3, oldOnGround bool, blockUnder world.Block) { +func (s *Simulator) setPostCollisionMotion(state *MovementState, oldVel mgl64.Vec3, oldOnGround bool, blockUnder world.Block) { if !oldOnGround && state.CollideY { s.landOnBlock(state, oldVel, blockUnder) } else if state.CollideY { @@ -721,7 +724,7 @@ func (s *Simulator) setPostCollisionMotion(state *MovementState, oldVel mgl32.Ve state.SetVel(newVel) } -func updateFallDistance(state *MovementState, oldY float32) { +func updateFallDistance(state *MovementState, oldY float64) { yDelta := state.Pos.Y() - oldY if yDelta < 0 && !state.OnGround { state.FallDistance -= yDelta @@ -733,15 +736,15 @@ func updateFallDistance(state *MovementState, oldY float32) { } } -func moveRelative(state *MovementState, moveRelativeSpeed float32) { +func moveRelative(state *MovementState, moveRelativeSpeed float64) { impulse := state.Impulse force := impulse.Y()*impulse.Y() + impulse.X()*impulse.X() if force >= 1e-4 { - force = moveRelativeSpeed / math32.Max(math32.Sqrt(force), 1.0) + force = moveRelativeSpeed / math.Max(math.Sqrt(force), 1.0) mf, ms := impulse.Y()*force, impulse.X()*force - yaw := state.Rotation.Z() * math32.Pi / 180.0 + yaw := state.Rotation.Z() * math.Pi / 180.0 v2, v3 := MCSin(yaw), MCCos(yaw) newVel := state.Vel @@ -767,12 +770,12 @@ func (s *Simulator) attemptJump(state *MovementState, clientJumpPrevented *bool) newVel := state.Vel jumpHeight := state.JumpHeight - inBlock := s.blockAtPos(posFromVec3(state.Pos)) - below := s.blockAtPos(posFromVec3(state.Pos.Sub(mgl32.Vec3{0, 0.1}))) + inBlock := s.blockAtPos(cube.PosFromVec3(state.Pos)) + below := s.blockAtPos(cube.PosFromVec3(state.Pos.Sub(mgl64.Vec3{0, 0.1}))) if s.blockName(inBlock) == "minecraft:honey_block" || s.blockName(below) == "minecraft:honey_block" { jumpHeight *= 0.6 } - newVel[1] = math32.Max(jumpHeight, newVel[1]) + newVel[1] = math.Max(jumpHeight, newVel[1]) state.JumpDelay = JumpDelayTicks if state.Sprinting { @@ -792,7 +795,7 @@ func (s *Simulator) attemptJump(state *MovementState, clientJumpPrevented *bool) return true } -func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl32.Vec3) bool { +func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl64.Vec3) bool { w := s.World if w == nil { return false @@ -801,9 +804,9 @@ func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl32.Vec3) bool collisionBB := state.BoundingBox(useSlideOffset) bbList := s.nearbyBBoxes(state, collisionBB.Extend(jumpVel)) - yVel := mgl32.Vec3{0, jumpVel.Y()} - xVel := mgl32.Vec3{jumpVel.X()} - zVel := mgl32.Vec3{0, 0, jumpVel.Z()} + yVel := mgl64.Vec3{0, jumpVel.Y()} + xVel := mgl64.Vec3{jumpVel.X()} + zVel := mgl64.Vec3{0, 0, jumpVel.Z()} for i := len(bbList) - 1; i >= 0; i-- { yVel = BBClipCollide(bbList[i], collisionBB, yVel, false, nil) @@ -823,9 +826,9 @@ func (s *Simulator) isJumpBlocked(state *MovementState, jumpVel mgl32.Vec3) bool return false } - xVel = mgl32.Vec3{jumpVel.X()} - yVel = mgl32.Vec3{0, jumpVel.Y()} - zVel = mgl32.Vec3{0, 0, jumpVel.Z()} + xVel = mgl64.Vec3{jumpVel.X()} + yVel = mgl64.Vec3{0, jumpVel.Y()} + zVel = mgl64.Vec3{0, 0, jumpVel.Z()} collisionBB = state.BoundingBox(useSlideOffset) for i := len(bbList) - 1; i >= 0; i-- { @@ -858,14 +861,14 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool bbList := s.nearbyBBoxes(state, collisionBB.Extend(currVel)) useOneWayCollisions := state.StuckInCollider - penetration := mgl32.Vec3{} + penetration := mgl64.Vec3{} - yVel := mgl32.Vec3{0, currVel.Y()} + yVel := mgl64.Vec3{0, currVel.Y()} if clientJumpPrevented { yVel[1] = 0 } - xVel := mgl32.Vec3{currVel.X()} - zVel := mgl32.Vec3{0, 0, currVel.Z()} + xVel := mgl64.Vec3{currVel.X()} + zVel := mgl64.Vec3{0, 0, currVel.Z()} for i := len(bbList) - 1; i >= 0; i-- { yVel = BBClipCollide(bbList[i], collisionBB, yVel, useOneWayCollisions, &penetration) @@ -886,7 +889,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 := mgl32.Vec3{ + collisionPos := mgl64.Vec3{ (collisionBB.Min().X() + collisionBB.Max().X()) * 0.5, collisionBB.Min().Y(), (collisionBB.Min().Z() + collisionBB.Max().Z()) * 0.5, @@ -903,9 +906,9 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool onGround := state.OnGround || (yCollision && currVel.Y() < 0.0) if onGround && (xCollision || zCollision) { - stepYVel := mgl32.Vec3{0, StepHeight} - stepXVel := mgl32.Vec3{currVel.X()} - stepZVel := mgl32.Vec3{0, 0, currVel.Z()} + stepYVel := mgl64.Vec3{0, StepHeight} + stepXVel := mgl64.Vec3{currVel.X()} + stepZVel := mgl64.Vec3{0, 0, currVel.Z()} stepBB := state.BoundingBox(useSlideOffset) for _, blockBox := range bbList { @@ -943,7 +946,7 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool } else { hasStepCollisions = len(s.nearbyBBoxes(state, stepBB)) > 0 } - stepPos := mgl32.Vec3{ + stepPos := mgl64.Vec3{ (stepBB.Min().X() + stepBB.Max().X()) * 0.5, stepBB.Min().Y(), (stepBB.Min().Z() + stepBB.Max().Z()) * 0.5, @@ -977,7 +980,7 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool } } - endPos := mgl32.Vec3{ + endPos := mgl64.Vec3{ (collisionBB.Min().X() + collisionBB.Max().X()) * 0.5, collisionBB.Min().Y(), (collisionBB.Min().Z() + collisionBB.Max().Z()) * 0.5, @@ -990,17 +993,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 = mgl32.Vec2{} + state.SlideOffset = mgl64.Vec2{} } } state.SetPos(endPos) - yCollision = math32.Abs(currVel.Y()-collisionVel.Y()) >= 1e-5 - state.CollideX = math32.Abs(currVel.X()-collisionVel.X()) >= 1e-5 + yCollision = math.Abs(currVel.Y()-collisionVel.Y()) >= 1e-5 + state.CollideX = math.Abs(currVel.X()-collisionVel.X()) >= 1e-5 state.CollideY = yCollision - state.CollideZ = math32.Abs(currVel.Z()-collisionVel.Z()) >= 1e-5 + state.CollideZ = math.Abs(currVel.Z()-collisionVel.Z()) >= 1e-5 - state.OnGround = (yCollision && currVel.Y() < 0) || (state.OnGround && !yCollision && math32.Abs(currVel.Y()) <= 1e-5) + state.OnGround = (yCollision && currVel.Y() < 0) || (state.OnGround && !yCollision && math.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) @@ -1024,8 +1027,8 @@ func (s *Simulator) avoidEdge(state *MovementState) { return } - edgeBoundry := float32(0.025) - offset := float32(0.05) + edgeBoundry := 0.025 + offset := 0.05 // Cap iterations to avoid excessive work with very large velocities. // should never happen, defensive. const maxIter = 1000 @@ -1033,11 +1036,11 @@ func (s *Simulator) avoidEdge(state *MovementState) { oldVel := state.Vel newVel := state.Vel useSlideOffset := s.Options.UseSlideOffset - bb := state.BoundingBox(useSlideOffset).GrowVec3(mgl32.Vec3{-edgeBoundry, 0, -edgeBoundry}) + bb := state.BoundingBox(useSlideOffset).GrowVec3(mgl64.Vec3{-edgeBoundry, 0, -edgeBoundry}) xMov, zMov := newVel.X(), newVel.Z() i := 0 - for i = 0; i < maxIter && xMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, 0}))) == 0; i++ { + for i = 0; i < maxIter && xMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{xMov, -StepHeight * 1.01, 0}))) == 0; i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -1050,7 +1053,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { xMov = 0 } - for i = 0; i < maxIter && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{0, -StepHeight * 1.01, zMov}))) == 0; i++ { + for i = 0; i < maxIter && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{0, -StepHeight * 1.01, zMov}))) == 0; i++ { if zMov < offset && zMov >= -offset { zMov = 0 } else if zMov > 0 { @@ -1063,7 +1066,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { zMov = 0 } - for i = 0; i < maxIter && xMov != 0.0 && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, zMov}))) == 0; i++ { + for i = 0; i < maxIter && xMov != 0.0 && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{xMov, -StepHeight * 1.01, zMov}))) == 0; i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -1099,8 +1102,8 @@ func (s *Simulator) isAboveGround(state *MovementState) bool { return false } distance := 0.6 - state.FallDistance - bb := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl32.Vec3{-0.025, 0, -0.025}) - return len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{0, -distance}))) > 0 + bb := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl64.Vec3{-0.025, 0, -0.025}) + return len(s.nearbyBBoxes(state, bb.Translate(mgl64.Vec3{0, -distance}))) > 0 } func (s *Simulator) isInsideWeb(state *MovementState) bool { @@ -1118,7 +1121,7 @@ func (s *Simulator) isInsideWeb(state *MovementState) bool { continue } - if bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))) { + if bb.IntersectsWith(cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3())) { insideWeb = true } if insideWeb { @@ -1128,19 +1131,19 @@ func (s *Simulator) isInsideWeb(state *MovementState) bool { return insideWeb } -func nearbyBlocks(aabb cube.BBox, w WorldProvider) iter.Seq2[dfcube.Pos, world.Block] { - return func(yield func(dfcube.Pos, world.Block) bool) { +func nearbyBlocks(aabb cube.BBox, w WorldProvider) iter.Seq2[cube.Pos, world.Block] { + return func(yield func(cube.Pos, world.Block) bool) { if w == nil { return } min, max := aabb.Min(), aabb.Max() - minX, minY, minZ := int(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])) + 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])) 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 } @@ -1150,7 +1153,7 @@ func nearbyBlocks(aabb cube.BBox, w WorldProvider) iter.Seq2[dfcube.Pos, world.B } } -func checkSupportingBlockPos(state *MovementState, w WorldProvider, useSlideOffset bool, vel mgl32.Vec3) { +func checkSupportingBlockPos(state *MovementState, w WorldProvider, useSlideOffset bool, vel mgl64.Vec3) { if !state.OnGround { state.SupportingBlockPos = nil return @@ -1158,7 +1161,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(mgl32.Vec3{-vel[0], 0, -vel[2]}) + decBB = decBB.Translate(mgl64.Vec3{-vel[0], 0, -vel[2]}) findSupportingBlock(state, w, decBB) } } @@ -1167,9 +1170,9 @@ func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox) { if w == nil { return } - var blockPos *dfcube.Pos - minDist := float32(math32.MaxFloat32 - 1) - centerPos := posVec3(posFromVec3(state.Pos)).Add(mgl32.Vec3{0.5, 0.5, 0.5}) + var blockPos *cube.Pos + minDist := math.MaxFloat64 - 1 + centerPos := cube.PosFromVec3(state.Pos).Vec3().Add(mgl64.Vec3{0.5, 0.5, 0.5}) for pos := range nearbyBlocks(bb, w) { boxes := w.BlockCollisions(pos) @@ -1178,10 +1181,10 @@ func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox) { } for _, box := range boxes { - if !bb.IntersectsWith(box.Translate(posVec3(pos))) { + if !bb.IntersectsWith(box.Translate(pos.Vec3())) { continue } - dist := posVec3(pos).Sub(centerPos).LenSqr() + dist := pos.Vec3().Sub(centerPos).LenSqr() if dist < minDist { minDist = dist supportPos := pos @@ -1194,7 +1197,7 @@ func findSupportingBlock(state *MovementState, w WorldProvider, bb cube.BBox) { 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{} } @@ -1208,7 +1211,7 @@ func (s *Simulator) nearbyBBoxes(state *MovementState, aabb cube.BBox) []cube.BB if provider, ok := s.World.(MovementCollisionProvider); ok { leatherBoots := s.Equipment != nil && s.Equipment.WearingLeatherBoots() return provider.GetMovementBBoxes(aabb, MovementCollisionContext{ - Position: [3]float32(state.Pos), + Position: [3]float64(state.Pos), Sneaking: state.Sneaking, Descending: state.PressingDescend, WantDown: state.WantDown, @@ -1218,7 +1221,7 @@ func (s *Simulator) nearbyBBoxes(state *MovementState, aabb cube.BBox) []cube.BB return s.World.GetNearbyBBoxes(aabb) } -func (s *Simulator) canFitHeight(state *MovementState, height float32) bool { +func (s *Simulator) canFitHeight(state *MovementState, height float64) bool { if s.World == nil { return true } diff --git a/simulator.go b/simulator.go index 1e2b36e..e5ca3ed 100644 --- a/simulator.go +++ b/simulator.go @@ -1,7 +1,7 @@ package bedsim import ( - "github.com/chewxy/math32" + "math" "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/world" @@ -31,14 +31,14 @@ const ( type SimulationOptions struct { Mode SimulationMode - PositionCorrectionThreshold float32 - VelocityCorrectionThreshold float32 + PositionCorrectionThreshold float64 + VelocityCorrectionThreshold float64 UseSlideOffset bool SprintTiming SprintTiming LimitAllVelocity bool - LimitAllVelocityThreshold float32 + LimitAllVelocityThreshold float64 // IgnoreClientStepTiebreaker, when true, skips the client-alignment // tie-breaker in the step-up collision logic. Pathfinders that drive their @@ -79,7 +79,7 @@ func (DefaultBlockSemantics) BlockName(b world.Block) string { return BlockName(b) } -func (DefaultBlockSemantics) BlockFriction(b world.Block) float32 { +func (DefaultBlockSemantics) BlockFriction(b world.Block) float64 { return BlockFriction(b) } @@ -107,9 +107,9 @@ func (s *Simulator) blockName(b world.Block) string { return BlockName(b) } -func (s *Simulator) blockFriction(b world.Block) float32 { +func (s *Simulator) blockFriction(b world.Block) float64 { if s.BlockSemantics != nil { - if friction := s.BlockSemantics.BlockFriction(b); friction > 0 && !math32.IsInf(friction, 1) { + if friction := s.BlockSemantics.BlockFriction(b); friction > 0 && !math.IsInf(friction, 1) { return friction } } diff --git a/simulator_test.go b/simulator_test.go index 06eec46..53acb8e 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -2,25 +2,24 @@ package bedsim import ( "fmt" - "github.com/chewxy/math32" + "math" "strings" "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/ethaniccc/float32-cube/cube" - "github.com/go-gl/mathgl/mgl32" + "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) 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) []cube.BBox { +func (mockWorld) BlockCollisions(pos cube.Pos) []cube.BBox { return nil } @@ -37,11 +36,11 @@ type staticWorld struct { boxes []cube.BBox } -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) []cube.BBox { +func (w staticWorld) BlockCollisions(pos cube.Pos) []cube.BBox { return nil } @@ -82,7 +81,7 @@ func (m mockInventory) HasElytra() bool { type overrideBlockSemantics struct { name string - friction float32 + friction float64 climbable bool } @@ -90,7 +89,7 @@ func (s overrideBlockSemantics) BlockName(world.Block) string { return s.name } -func (s overrideBlockSemantics) BlockFriction(world.Block) float32 { +func (s overrideBlockSemantics) BlockFriction(world.Block) float64 { return s.friction } @@ -101,14 +100,14 @@ func (s overrideBlockSemantics) BlockClimbable(world.Block) bool { func newBaseState() *MovementState { return &MovementState{ Client: ClientState{ - Pos: mgl32.Vec3{}, - Vel: mgl32.Vec3{}, - Mov: mgl32.Vec3{}, + Pos: mgl64.Vec3{}, + Vel: mgl64.Vec3{}, + Mov: mgl64.Vec3{}, }, - Pos: mgl32.Vec3{}, - Vel: mgl32.Vec3{}, - Mov: mgl32.Vec3{}, - Size: mgl32.Vec3{0.6, 1.8, 1}, + Pos: mgl64.Vec3{}, + Vel: mgl64.Vec3{}, + Mov: mgl64.Vec3{}, + Size: mgl64.Vec3{0.6, 1.8, 1}, MovementSpeed: 0.1, DefaultMovementSpeed: 0.1, AirSpeed: 0.02, @@ -144,9 +143,9 @@ func TestSimulateMoveRelative(t *testing.T) { state := newBaseState() input := InputState{ - MoveVector: mgl32.Vec2{0, 1}, - ClientPos: mgl32.Vec3{}, - ClientVel: mgl32.Vec3{}, + MoveVector: mgl64.Vec2{0, 1}, + ClientPos: mgl64.Vec3{}, + ClientVel: mgl64.Vec3{}, Yaw: 0, Pitch: 0, HeadYaw: 0, @@ -168,7 +167,7 @@ func TestSimulateStateOutcomeTeleport(t *testing.T) { } state := newBaseState() - state.TeleportPos = mgl32.Vec3{12, 63, -4} + state.TeleportPos = mgl64.Vec3{12, 63, -4} state.TicksSinceTeleport = 0 state.TeleportCompletionTicks = 0 state.TeleportIsSmoothed = false @@ -189,8 +188,8 @@ func TestSimulateStateTeleportDoesNotUpdateFallDistance(t *testing.T) { } state := newBaseState() - state.Pos = mgl32.Vec3{0, 70, 0} - state.TeleportPos = mgl32.Vec3{0, 60, 0} + state.Pos = mgl64.Vec3{0, 70, 0} + state.TeleportPos = mgl64.Vec3{0, 60, 0} state.TicksSinceTeleport = 0 state.TeleportCompletionTicks = 0 @@ -211,10 +210,10 @@ func TestSimulateStateOutcomeUnreliable(t *testing.T) { state := newBaseState() state.GameMode = packet.GameTypeCreative - 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} + 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} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeUnreliable { @@ -237,10 +236,10 @@ func TestSimulateStateNoClipPassesThroughClientState(t *testing.T) { state := newBaseState() state.NoClip = true state.OnGround = true - 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} + 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} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeUnreliable { @@ -259,15 +258,15 @@ func TestSimulateStateNoClipPassesThroughClientState(t *testing.T) { func TestUpdateFallDistanceUsesResolvedGroundState(t *testing.T) { state := newBaseState() - state.Pos = mgl32.Vec3{0, 10, 0} + state.Pos = mgl64.Vec3{0, 10, 0} - state.SetPos(mgl32.Vec3{0, 7, 0}) + state.SetPos(mgl64.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(mgl32.Vec3{0, 8, 0}) + state.SetPos(mgl64.Vec3{0, 8, 0}) updateFallDistance(state, 7) if state.FallDistance != 0 { t.Fatalf("expected upward move to reset fall distance, got %v", state.FallDistance) @@ -275,7 +274,7 @@ func TestUpdateFallDistanceUsesResolvedGroundState(t *testing.T) { state.FallDistance = 4 state.OnGround = true - state.SetPos(mgl32.Vec3{0, 6, 0}) + state.SetPos(mgl64.Vec3{0, 6, 0}) updateFallDistance(state, 8) if state.FallDistance != 0 { t.Fatalf("expected grounded move to clear fall distance, got %v", state.FallDistance) @@ -325,13 +324,13 @@ func TestSimulatorInvalidBlockSemanticsFrictionFallsBackToDefault(t *testing.T) tests := []struct { name string - friction float32 + friction float64 }{ {name: "zero", friction: 0}, {name: "negative", friction: -0.42}, - {name: "nan", friction: math32.NaN()}, - {name: "positive infinity", friction: math32.Inf(1)}, - {name: "negative infinity", friction: math32.Inf(-1)}, + {name: "nan", friction: math.NaN()}, + {name: "positive infinity", friction: math.Inf(1)}, + {name: "negative infinity", friction: math.Inf(-1)}, } for _, tt := range tests { @@ -357,13 +356,13 @@ func TestSimulateStateOutcomeUnloadedChunk(t *testing.T) { } state := newBaseState() - state.Vel = mgl32.Vec3{0.2, 0.1, -0.1} + state.Vel = mgl64.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 != (mgl32.Vec3{}) { + if state.Vel != (mgl64.Vec3{}) { t.Fatalf("expected velocity to be cleared, got %v", state.Vel) } } @@ -376,13 +375,13 @@ func TestSimulateStateOutcomeImmobileOrNotReady(t *testing.T) { state := newBaseState() state.Immobile = true - state.Vel = mgl32.Vec3{0.5, -0.3, 0.5} + state.Vel = mgl64.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 != (mgl32.Vec3{}) { + if state.Vel != (mgl64.Vec3{}) { t.Fatalf("expected velocity to be cleared, got %v", state.Vel) } } @@ -395,7 +394,7 @@ func TestSimulateStateSkipsGravityWhenDisabled(t *testing.T) { state := newBaseState() state.HasGravity = false - state.Impulse = mgl32.Vec2{0, 0.98} + state.Impulse = mgl64.Vec2{0, 0.98} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeNormal { @@ -416,7 +415,7 @@ func TestSimulateStateInvalidGlideContinuesNormalMovement(t *testing.T) { state := newBaseState() state.Gliding = true state.OnGround = true - state.Impulse = mgl32.Vec2{0, 0.98} + state.Impulse = mgl64.Vec2{0, 0.98} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeNormal { @@ -443,7 +442,7 @@ func TestSimulateStateDebugTraceIncludesCollisionStream(t *testing.T) { } state := newBaseState() - state.Impulse = mgl32.Vec2{0, 0.98} + state.Impulse = mgl64.Vec2{0, 0.98} result := sim.SimulateState(state) if result.Outcome != SimulationOutcomeNormal { @@ -484,12 +483,12 @@ func TestSimulateStateDebugTraceJumpBlocked(t *testing.T) { } state := newBaseState() - state.Pos = mgl32.Vec3{0, 0, 0.69} + state.Pos = mgl64.Vec3{0, 0, 0.69} state.Client.Pos = state.Pos state.OnGround = true state.Jumping = true state.Sprinting = true - state.Rotation = mgl32.Vec3{0, 0, 0} + state.Rotation = mgl64.Vec3{0, 0, 0} state.JumpHeight = DefaultJumpHeight result := sim.SimulateState(state) @@ -513,9 +512,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 := mgl32.Vec3{0.5, 0, 0.5} + startPos := mgl64.Vec3{0.5, 0, 0.5} - runSim := func(ignoreStepTiebreaker bool) (mgl32.Vec3, bool) { + runSim := func(ignoreStepTiebreaker bool) (mgl64.Vec3, bool) { w := staticWorld{chunkLoaded: true, boxes: []cube.BBox{slabBox, groundBox}} sim := &Simulator{ World: w, @@ -532,9 +531,9 @@ func TestStepUpTiebreaker(t *testing.T) { state.JumpHeight = DefaultJumpHeight input := InputState{ - MoveVector: mgl32.Vec2{0, 1}, + MoveVector: mgl64.Vec2{0, 1}, ClientPos: startPos, - ClientVel: mgl32.Vec3{}, + ClientVel: mgl64.Vec3{}, Yaw: -90, // face +X HeadYaw: -90, } @@ -582,9 +581,9 @@ func TestStepUpTiebreaker(t *testing.T) { state.JumpHeight = DefaultJumpHeight input := InputState{ - MoveVector: mgl32.Vec2{0, 1}, + MoveVector: mgl64.Vec2{0, 1}, ClientPos: startPos, - ClientVel: mgl32.Vec3{}, + ClientVel: mgl64.Vec3{}, Yaw: -90, HeadYaw: -90, } @@ -611,8 +610,8 @@ func TestResultFromStateCorrectionModes(t *testing.T) { name: "authoritative velocity-only drift", mode: SimulationModeAuthoritative, mutate: func(state *MovementState) { - state.Vel = mgl32.Vec3{0.5, 0, 0} - state.Client.Vel = mgl32.Vec3{} + state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Client.Vel = mgl64.Vec3{} }, wantSet: true, }, @@ -620,8 +619,8 @@ func TestResultFromStateCorrectionModes(t *testing.T) { name: "permissive velocity-only drift", mode: SimulationModePermissive, mutate: func(state *MovementState) { - state.Vel = mgl32.Vec3{0.5, 0, 0} - state.Client.Vel = mgl32.Vec3{} + state.Vel = mgl64.Vec3{0.5, 0, 0} + state.Client.Vel = mgl64.Vec3{} }, wantSet: false, }, @@ -629,8 +628,8 @@ func TestResultFromStateCorrectionModes(t *testing.T) { name: "permissive position drift", mode: SimulationModePermissive, mutate: func(state *MovementState) { - state.Pos = mgl32.Vec3{0.5, 0, 0} - state.Client.Pos = mgl32.Vec3{} + state.Pos = mgl64.Vec3{0.5, 0, 0} + state.Client.Pos = mgl64.Vec3{} }, wantSet: true, }, @@ -638,8 +637,8 @@ func TestResultFromStateCorrectionModes(t *testing.T) { name: "passive position drift", mode: SimulationModePassive, mutate: func(state *MovementState) { - state.Pos = mgl32.Vec3{0.5, 0, 0} - state.Client.Pos = mgl32.Vec3{} + state.Pos = mgl64.Vec3{0.5, 0, 0} + state.Client.Pos = mgl64.Vec3{} }, wantSet: false, }, From 27d27387da138ffa47909991c7e07644c6ccdc0c Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Thu, 6 Aug 2026 17:23:31 -0400 Subject: [PATCH 6/9] Fix pose transitions and riptide conditions --- README.md | 7 ++ block/environment.go | 2 + block/semantics.go | 4 +- block_effects.go | 72 ++++++++++++++++---- block_effects_test.go | 136 ++++++++++++++++++++++++-------------- block_semantics_test.go | 7 +- bubble.go | 12 +++- bubble_test.go | 94 ++++++++++++++++++++++++++ dynamic_collision_test.go | 68 +++++++++++++++++++ liquid.go | 14 +++- liquid_test.go | 7 ++ movement.go | 12 ++++ simulation.go | 99 ++++++++++++++++++++++----- simulator_test.go | 33 +++++++++ 14 files changed, 476 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 0864f5d..5476557 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,13 @@ Riptide, and leather-boots checks. The legacy `DepthStriderProvider` inventory extension remains a fallback when the equipment provider reports no Depth Strider level. `EffectsProvider` also controls Weaving-aware web movement. +Riptide input flags are not trusted on their own. Set `MovementState.RiptideReady` +for the simulation tick only after validating a charged Riptide-trident release. +Set `MovementState.RiptideCollision` after a server-observed entity collision to +authorize the corresponding stop/reversal; ordinary client stop flags are ignored. +Set `MovementState.RiptideInRain` from trusted weather exposure when rain should +permit launch without direct water contact. + Pose changes update `MovementState.Size`. Set `StandingHeight`, `SneakingHeight`, or `CrawlingHeight` when using non-vanilla dimensions; zero values preserve the current standing height and use vanilla crouch/crawl diff --git a/block/environment.go b/block/environment.go index 0362270..e22cf33 100644 --- a/block/environment.go +++ b/block/environment.go @@ -6,6 +6,7 @@ type environmentRule struct { name string inside InsideMovement traversal Traversal + honey bool } func (r environmentRule) Matches(_ world.Block, name string) bool { @@ -15,4 +16,5 @@ func (r environmentRule) Matches(_ world.Block, name string) bool { func (r environmentRule) Apply(s *resolution) { s.InsideMovement = r.inside s.Traversal = r.traversal + s.Honey = r.honey } diff --git a/block/semantics.go b/block/semantics.go index de3f166..09fd3e7 100644 --- a/block/semantics.go +++ b/block/semantics.go @@ -27,7 +27,6 @@ type InsideMovement uint8 const ( InsideMovementNone InsideMovement = iota - InsideMovementHoney InsideMovementSweetBerryBush InsideMovementPowderSnow ) @@ -47,6 +46,7 @@ type MovementSemantics struct { GroundAccelerationFrictionMultiplier float32 Climbable bool Cobweb bool + Honey bool Bounce Bounce InsideMovement InsideMovement Traversal Traversal @@ -70,7 +70,7 @@ var rules = [...]rule{ cobweb{}, slime{}, bed{}, - environmentRule{name: "minecraft:honey_block", inside: InsideMovementHoney}, + environmentRule{name: "minecraft:honey_block", honey: true}, environmentRule{name: "minecraft:sweet_berry_bush", inside: InsideMovementSweetBerryBush}, environmentRule{name: "minecraft:powder_snow", inside: InsideMovementPowderSnow, traversal: TraversalPowderSnow}, environmentRule{name: "minecraft:scaffolding", traversal: TraversalScaffolding}, diff --git a/block_effects.go b/block_effects.go index 96f85b1..8e1692d 100644 --- a/block_effects.go +++ b/block_effects.go @@ -4,26 +4,43 @@ import ( "github.com/chewxy/math32" "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl32" movementblock "github.com/oomph-ac/bedsim/block" ) func applyInsideBlockMovement(state *MovementState, movement movementblock.InsideMovement) { - velocity := state.Vel switch movement { - case movementblock.InsideMovementHoney: - velocity[0] *= 0.4 - velocity[1] = max(-0.12, velocity[1]) - velocity[2] *= 0.4 case movementblock.InsideMovementSweetBerryBush: - velocity[0] *= 0.8 - velocity[1] *= 0.75 - velocity[2] *= 0.8 + queueStuckSpeedMultiplier(state, mgl32.Vec3{0.8, 0.75, 0.8}) case movementblock.InsideMovementPowderSnow: - velocity[0] *= 0.9 - velocity[1] *= 1.5 - velocity[2] *= 0.9 + queueStuckSpeedMultiplier(state, mgl32.Vec3{0.9, 1.5, 0.9}) } - state.SetVel(velocity) +} + +func queueStuckSpeedMultiplier(state *MovementState, multiplier mgl32.Vec3) { + queued := state.StuckSpeedMultiplier + if queued.LenSqr() <= 1e-7 { + state.StuckSpeedMultiplier = multiplier + return + } + for axis := range 3 { + queued[axis] = min(queued[axis], multiplier[axis]) + } + state.StuckSpeedMultiplier = queued +} + +func applyStuckSpeedMultiplier(state *MovementState) bool { + multiplier := state.StuckSpeedMultiplier + if state.NoClip || multiplier.LenSqr() <= 1e-7 { + return false + } + state.SetVel(mgl32.Vec3{ + state.Vel.X() * multiplier.X(), + state.Vel.Y() * multiplier.Y(), + state.Vel.Z() * multiplier.Z(), + }) + state.StuckSpeedMultiplier = mgl32.Vec3{} + return true } func applyAscendableMovement(state *MovementState, traversal movementblock.Traversal, leatherBoots bool) { @@ -59,12 +76,39 @@ func (s *Simulator) applyInsideBlockEffects(state *MovementState) { continue } b := s.World.Block(pos) - semantics := s.blockMovementSemantics(b) - if semantics.Cobweb { + if s.blockAir(b) { continue } + semantics := s.blockMovementSemantics(b) applyInsideBlockMovement(state, semantics.InsideMovement) } } } + s.applyHoneyWallSlide(state) +} + +func (s *Simulator) applyHoneyWallSlide(state *MovementState) { + if !state.CollideX && !state.CollideZ { + return + } + bb := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl32.Vec3{1e-3, 0, 1e-3}) + min, maxPoint := bb.Min(), bb.Max() + for x := int(math32.Floor(min.X())); x < int(math32.Ceil(maxPoint.X())); x++ { + for y := int(math32.Floor(min.Y())); y < int(math32.Ceil(maxPoint.Y())); y++ { + for z := int(math32.Floor(min.Z())); z < int(math32.Ceil(maxPoint.Z())); z++ { + pos := cube.Pos{x, y, z} + if !bb.IntersectsWith(cube.Box32(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))) { + continue + } + if s.blockMovementSemantics(s.World.Block(pos)).Honey { + velocity := state.Vel + velocity[0] *= 0.4 + velocity[1] = max(-0.12, velocity[1]) + velocity[2] *= 0.4 + state.SetVel(velocity) + return + } + } + } + } } diff --git a/block_effects_test.go b/block_effects_test.go index fceb3bf..ec6a865 100644 --- a/block_effects_test.go +++ b/block_effects_test.go @@ -1,9 +1,10 @@ package bedsim import ( - "github.com/chewxy/math32" "testing" + "github.com/chewxy/math32" + "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" @@ -16,40 +17,73 @@ func (encodedBlockSemantics) BlockMovementSemantics(b world.Block) movementblock return movementblock.Resolve(b, BlockName(b)) } -type namedOverrideSemantics struct { - name string - semantics movementblock.MovementSemantics +type honeyWallWorld struct { + staticWorld + pos cube.Pos } -func (s namedOverrideSemantics) BlockMovementSemantics(b world.Block) movementblock.MovementSemantics { - if BlockName(b) == s.name { - return s.semantics +func (w honeyWallWorld) Block(pos cube.Pos) world.Block { + if pos == w.pos { + return semanticsNamedBlock{name: "minecraft:honey_block"} } - return movementblock.Resolve(b, BlockName(b)) + return block.Air{} } -func TestInsideBlockMovementMultipliers(t *testing.T) { - tests := []struct { - name string - movement movementblock.InsideMovement - want mgl32.Vec3 - }{ - {name: "honey", movement: movementblock.InsideMovementHoney, want: mgl32.Vec3{0.4, -0.12, 0.4}}, - {name: "sweet berry bush", movement: movementblock.InsideMovementSweetBerryBush, want: mgl32.Vec3{0.8, -0.75, 0.8}}, - {name: "powder snow", movement: movementblock.InsideMovementPowderSnow, want: mgl32.Vec3{0.9, -1.5, 0.9}}, +func TestStuckMovementMultiplierKeepsStrongestOverlappingEffect(t *testing.T) { + state := newBaseState() + state.Vel = mgl32.Vec3{1, -1, 1} + + applyInsideBlockMovement(state, movementblock.InsideMovementPowderSnow) + applyInsideBlockMovement(state, movementblock.InsideMovementSweetBerryBush) + applyInsideBlockMovement(state, movementblock.InsideMovementSweetBerryBush) + + if want := (mgl32.Vec3{0.8, 0.75, 0.8}); state.StuckSpeedMultiplier != want { + t.Fatalf("expected strongest multiplier %v without compounding, got %v", want, state.StuckSpeedMultiplier) + } + if want := (mgl32.Vec3{1, -1, 1}); state.Vel != want { + t.Fatalf("inside-block scan changed persistent velocity: got %v, want %v", state.Vel, want) + } +} + +func TestStuckMovementMultiplierAppliesOnceAndClearsVelocity(t *testing.T) { + sim := &Simulator{World: mockWorld{}} + state := newBaseState() + state.HasGravity = false + state.Vel = mgl32.Vec3{1, -1, 1} + state.StuckSpeedMultiplier = mgl32.Vec3{0.8, 0.75, 0.8} + + result := sim.SimulateState(state) + + if want := (mgl32.Vec3{0.8, -0.75, 0.8}); result.Movement != want { + t.Fatalf("expected one scaled displacement %v, got %v", want, result.Movement) + } + if state.Vel != (mgl32.Vec3{}) { + t.Fatalf("expected persistent velocity to clear after stuck movement, got %v", state.Vel) + } + if state.StuckSpeedMultiplier != (mgl32.Vec3{}) { + t.Fatalf("expected pending multiplier to clear, got %v", state.StuckSpeedMultiplier) } +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - state := newBaseState() - state.Vel = mgl32.Vec3{1, -1, 1} +func TestStuckMovementDoesNotBounce(t *testing.T) { + sim := &Simulator{ + World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ + cube.Box32(-1, 0, -1, 1, 1, 1), + }}, + BlockSemantics: overrideBlockSemantics{semantics: movementblock.MovementSemantics{ + Bounce: movementblock.BounceSlime, + }}, + } + state := newBaseState() + state.Pos = mgl32.Vec3{0, 1, 0} + state.Vel = mgl32.Vec3{0, -1, 0} + state.Gravity = NormalGravity + state.StuckSpeedMultiplier = mgl32.Vec3{0.8, 0.75, 0.8} - applyInsideBlockMovement(state, tt.movement) + sim.SimulateState(state) - if state.Vel != tt.want { - t.Fatalf("expected velocity %v, got %v", tt.want, state.Vel) - } - }) + if state.Vel.Y() > 0 { + t.Fatalf("expected stuck movement to suppress bounce, got y velocity %v", state.Vel.Y()) } } @@ -72,6 +106,32 @@ func TestHoneyBlockReducesJumpPower(t *testing.T) { } } +func TestHoneyWallSlideAppliesOnSolidSideContact(t *testing.T) { + w := honeyWallWorld{ + staticWorld: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ + cube.Box32(1, -1, 0, 2, 2, 1), + }}, + pos: cube.Pos{1, 0, 0}, + } + sim := &Simulator{World: w, BlockSemantics: encodedBlockSemantics{}} + state := newBaseState() + state.Pos = mgl32.Vec3{0.7, 0, 0.5} + state.Vel = mgl32.Vec3{1, -0.2, 0.1} + state.HasGravity = false + + sim.SimulateState(state) + + if !state.CollideX { + t.Fatal("expected horizontal collision with honey wall") + } + if state.Vel.Y() != -0.12 { + t.Fatalf("expected honey slide downward cap -0.12, got %v", state.Vel.Y()) + } + if want := float32(0.1 * DefaultAirFriction * 0.4); math32.Abs(state.Vel.Z()-want) > 1e-6 { + t.Fatalf("expected honey slide lateral slowdown %v, got %v", want, state.Vel.Z()) + } +} + func TestScaffoldingAscendAndDescendSpeeds(t *testing.T) { state := newBaseState() state.PressingAscend = true @@ -115,30 +175,6 @@ func TestHoneyWalkSlowdownMatchesSlime(t *testing.T) { } } -func TestSimulationAppliesInsideBlockMovementEffect(t *testing.T) { - w := environmentWorld{blocks: map[cube.Pos]world.Block{ - {0, 0, 0}: semanticsNamedBlock{name: "custom:sticky"}, - }} - sim := &Simulator{ - World: w, - BlockSemantics: namedOverrideSemantics{name: "custom:sticky", semantics: movementblock.MovementSemantics{ - GroundFriction: DefaultBlockFriction, - GroundAccelerationFrictionMultiplier: 1, - InsideMovement: movementblock.InsideMovementHoney, - }}, - } - state := newBaseState() - state.Pos = mgl32.Vec3{0.5, 0, 0.5} - state.Vel = mgl32.Vec3{0.1, 0, 0} - state.HasGravity = false - - sim.SimulateState(state) - - if want := float32(0.1 * DefaultAirFriction * 0.4); math32.Abs(state.Vel.X()-want) > 1e-6 { - t.Fatalf("expected integrated honey slowdown %v, got %v", want, state.Vel.X()) - } -} - func TestSimulationAppliesScaffoldingTraversal(t *testing.T) { w := environmentWorld{blocks: map[cube.Pos]world.Block{ {0, 0, 0}: semanticsNamedBlock{name: "minecraft:scaffolding"}, diff --git a/block_semantics_test.go b/block_semantics_test.go index f01e8b2..6f1f30c 100644 --- a/block_semantics_test.go +++ b/block_semantics_test.go @@ -133,8 +133,9 @@ func TestEnvironmentMovementSemantics(t *testing.T) { name string inside movementblock.InsideMovement traversal movementblock.Traversal + honey bool }{ - {name: "minecraft:honey_block", inside: movementblock.InsideMovementHoney}, + {name: "minecraft:honey_block", honey: true}, {name: "minecraft:sweet_berry_bush", inside: movementblock.InsideMovementSweetBerryBush}, {name: "minecraft:powder_snow", inside: movementblock.InsideMovementPowderSnow, traversal: movementblock.TraversalPowderSnow}, {name: "minecraft:scaffolding", traversal: movementblock.TraversalScaffolding}, @@ -143,8 +144,8 @@ func TestEnvironmentMovementSemantics(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := movementblock.Resolve(semanticsNamedBlock{tt.name}, tt.name) - if got.InsideMovement != tt.inside || got.Traversal != tt.traversal { - t.Fatalf("semantics = %+v, want inside=%v traversal=%v", got, tt.inside, tt.traversal) + if got.InsideMovement != tt.inside || got.Traversal != tt.traversal || got.Honey != tt.honey { + t.Fatalf("semantics = %+v, want inside=%v traversal=%v honey=%v", got, tt.inside, tt.traversal, tt.honey) } }) } diff --git a/bubble.go b/bubble.go index a7fd76a..beb2e33 100644 --- a/bubble.go +++ b/bubble.go @@ -64,8 +64,8 @@ func (s *Simulator) applyBubbleColumns(state *MovementState) { } } -func (s *Simulator) attemptRiptide(state *MovementState, touchingLiquid bool) bool { - if s.Equipment == nil || state.RiptideTicks > 0 || !touchingLiquid { +func (s *Simulator) attemptRiptide(state *MovementState, touchingWater bool) bool { + if s.Equipment == nil || state.RiptideTicks > 0 || !state.RiptideReady || (!touchingWater && !state.RiptideInRain) { return false } level := s.Equipment.EnchantmentLevel(EnchantmentRiptide) @@ -81,6 +81,14 @@ func (s *Simulator) attemptRiptide(state *MovementState, touchingLiquid bool) bo } state.SetVel(state.Vel.Add(direction)) state.RiptideTicks = 20 + state.RiptideCollision = false state.StartingSpinAttack = false return true } + +func stopRiptideOnBlockCollision(state *MovementState) { + if state.RiptideTicks > 0 && (state.CollideX || state.CollideZ) { + state.RiptideTicks = 0 + state.RiptideCollision = false + } +} diff --git a/bubble_test.go b/bubble_test.go index 657da93..8c11eb9 100644 --- a/bubble_test.go +++ b/bubble_test.go @@ -76,6 +76,7 @@ func TestRiptideLaunchesInWaterAndStartsSpinAttack(t *testing.T) { state := newBaseState() state.Pos = mgl32.Vec3{0.5, 0, 0.5} state.Gravity = NormalGravity + state.RiptideReady = true sim.Simulate(state, InputState{StartSpinAttack: true}) @@ -86,3 +87,96 @@ func TestRiptideLaunchesInWaterAndStartsSpinAttack(t *testing.T) { t.Fatalf("expected 19 riptide ticks after the launch tick, got %d", state.RiptideTicks) } } + +func TestRiptideDoesNotLaunchInLava(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Lava{Still: true, Depth: 8}}} + sim := &Simulator{World: w, Equipment: fixedEquipment{EnchantmentRiptide: 2}} + state := newBaseState() + state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Gravity = NormalGravity + state.RiptideReady = true + + sim.Simulate(state, InputState{StartSpinAttack: true}) + + if state.RiptideTicks != 0 { + t.Fatalf("expected lava not to start riptide, got %d ticks", state.RiptideTicks) + } +} + +func TestRiptideDoesNotLaunchWhileFlying(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} + sim := &Simulator{World: w, Equipment: fixedEquipment{EnchantmentRiptide: 2}} + state := newBaseState() + state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Flying = true + state.Gravity = NormalGravity + state.RiptideReady = true + + sim.Simulate(state, InputState{StartSpinAttack: true}) + + if state.RiptideTicks != 0 { + t.Fatalf("expected flying not to start riptide, got %d ticks", state.RiptideTicks) + } +} + +func TestRiptideRequiresValidatedTridentRelease(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} + sim := &Simulator{World: w, Equipment: fixedEquipment{EnchantmentRiptide: 2}} + state := newBaseState() + state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Gravity = NormalGravity + + sim.Simulate(state, InputState{StartSpinAttack: true}) + + if state.RiptideTicks != 0 { + t.Fatalf("expected unvalidated start flag not to launch, got %d ticks", state.RiptideTicks) + } +} + +func TestRiptideLaunchesInRainWithoutBlockWater(t *testing.T) { + sim := &Simulator{World: mockWorld{}, Equipment: fixedEquipment{EnchantmentRiptide: 2}} + state := newBaseState() + state.Gravity = NormalGravity + state.RiptideReady = true + state.RiptideInRain = true + + sim.Simulate(state, InputState{StartSpinAttack: true}) + + if state.RiptideTicks != 19 { + t.Fatalf("expected rain-authorized riptide launch, got %d ticks", state.RiptideTicks) + } +} + +func TestRiptideStopsOnNormalMovementWallCollision(t *testing.T) { + sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ + cube.Box32(0.5, -1, -1, 1.5, 2, 1), + }}} + state := newBaseState() + state.Vel = mgl32.Vec3{1, 0, 0} + state.RiptideTicks = 10 + state.HasGravity = false + + sim.SimulateState(state) + + if state.RiptideTicks != 0 { + t.Fatalf("expected wall collision to stop riptide, got %d ticks", state.RiptideTicks) + } +} + +func TestRiptideStopRequiresValidatedEntityCollision(t *testing.T) { + sim := &Simulator{} + state := newBaseState() + state.RiptideTicks = 10 + state.Vel = mgl32.Vec3{1, 0, 0} + + sim.applyInput(state, InputState{StopSpinAttack: true}) + if state.RiptideTicks != 10 || state.Vel.X() != 1 { + t.Fatalf("spoofed stop changed riptide: ticks=%d vel=%v", state.RiptideTicks, state.Vel) + } + + state.RiptideCollision = true + sim.applyInput(state, InputState{StopSpinAttack: true}) + if state.RiptideTicks != 0 || state.Vel.X() != -0.2 { + t.Fatalf("validated stop was not applied: ticks=%d vel=%v", state.RiptideTicks, state.Vel) + } +} diff --git a/dynamic_collision_test.go b/dynamic_collision_test.go index b033f0e..e2ae9e4 100644 --- a/dynamic_collision_test.go +++ b/dynamic_collision_test.go @@ -45,6 +45,33 @@ type leatherEquipment struct{} func (leatherEquipment) EnchantmentLevel(MovementEnchantment) int { return 0 } func (leatherEquipment) WearingLeatherBoots() bool { return true } +type unloadedCollisionWorld struct { + staticWorld + nearbyCalls int +} + +func (w *unloadedCollisionWorld) GetNearbyBBoxes(aabb cube.BBox32) []cube.BBox32 { + w.nearbyCalls++ + return w.staticWorld.GetNearbyBBoxes(aabb) +} + +func TestPoseTransitionsDoNotReadCollisionsFromUnloadedChunks(t *testing.T) { + w := &unloadedCollisionWorld{staticWorld: staticWorld{chunkLoaded: false}} + sim := &Simulator{World: w} + state := newBaseState() + state.Sneaking = true + state.Size[1] = 1.49 + + result := sim.Simulate(state, InputState{StopSneaking: true}) + + if result.Outcome != SimulationOutcomeUnloadedChunk { + t.Fatalf("expected unloaded outcome, got %v", result.Outcome) + } + if w.nearbyCalls != 0 { + t.Fatalf("expected no collision queries in an unloaded chunk, got %d", w.nearbyCalls) + } +} + func TestCannotUnsneakUnderLowCeiling(t *testing.T) { sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ cube.Box32(-1, 1.5, -1, 1, 2, 1), @@ -54,12 +81,38 @@ func TestCannotUnsneakUnderLowCeiling(t *testing.T) { state.Size[1] = 1.49 sim.applyInput(state, InputState{StopSneaking: true}) + sim.applyInput(state, InputState{}) if !state.Sneaking || state.Size.Y() != 1.49 { t.Fatalf("expected forced sneak pose under ceiling, got sneaking=%v size=%v", state.Sneaking, state.Size) } } +func TestReleasingSneakDownRestoresStandingPose(t *testing.T) { + sim := &Simulator{World: staticWorld{chunkLoaded: true}} + state := newBaseState() + + sim.applyInput(state, InputState{SneakDown: true}) + sim.applyInput(state, InputState{}) + + if state.Sneaking || state.Size.Y() != state.StandingHeight { + t.Fatalf("expected standing pose after releasing sneak, got sneaking=%v size=%v", state.Sneaking, state.Size) + } +} + +func TestCanFitHeightUsesRequestedHeightWhileSwimming(t *testing.T) { + sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ + cube.Box32(-1, 1, -1, 1, 2, 1), + }}} + state := newBaseState() + state.Swimming = true + state.SwimWaterGraceTicks = 1 + + if sim.canFitHeight(state, 1.8) { + t.Fatal("expected standing-height fit check to collide despite active swim pose") + } +} + func TestCannotStopCrawlingUnderLowCeiling(t *testing.T) { sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ cube.Box32(-1, 0.7, -1, 1, 2, 1), @@ -75,6 +128,21 @@ func TestCannotStopCrawlingUnderLowCeiling(t *testing.T) { } } +func TestStopCrawlingWhileSneakingUsesCrouchHeight(t *testing.T) { + sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ + cube.Box32(-1, 1.6, -1, 1, 2, 1), + }}} + state := newBaseState() + state.Crawling = true + state.Size[1] = 0.6 + + sim.applyInput(state, InputState{StopCrawling: true, SneakDown: true}) + + if state.Crawling || !state.Sneaking || state.Size.Y() != 1.49 { + t.Fatalf("expected crouch pose under ceiling, got crawling=%v sneaking=%v size=%v", state.Crawling, state.Sneaking, state.Size) + } +} + func TestPoseRestoresCustomStandingHeight(t *testing.T) { state := newBaseState() state.Size[1] = 2 diff --git a/liquid.go b/liquid.go index 7b0de2f..78749de 100644 --- a/liquid.go +++ b/liquid.go @@ -102,11 +102,20 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, } moveRelative(state, moveRelativeSpeed) + stuckMovement := applyStuckSpeedMultiplier(state) oldVel := state.Vel oldOnGround := state.OnGround s.tryCollisions(state, false) + stopRiptideOnBlockCollision(state) + if stuckMovement { + state.SetMov(state.Vel) + state.SetVel(mgl32.Vec3{}) + oldVel = mgl32.Vec3{} + } s.setPostCollisionMotion(state, oldVel, oldOnGround, block.Air{}) - state.SetMov(state.Vel) + if !stuckMovement { + state.SetMov(state.Vel) + } vel := state.Vel if water { @@ -138,13 +147,12 @@ func (s *Simulator) simulateLiquidTravel(state *MovementState, kind liquidKind, if state.CollideX || state.CollideZ { raised := mgl32.Vec3{vel.X(), vel.Y() + 0.6 + initialY - state.Pos.Y(), vel.Z()} raisedBox := state.BoundingBox(s.Options.UseSlideOffset).Translate(raised) - hasCollision := len(s.nearbyBBoxes(state, raisedBox)) > 0 + hasCollision := s.hasNearbyBBoxes(state, raisedBox) hasLiquid := s.containsAnyLiquid(raisedBox) s.debugf("liquid exit probe collision=%t liquid=%t box=%v", hasCollision, hasLiquid, raisedBox) if !hasCollision && !hasLiquid { vel[1] = 0.3 } - state.RiptideTicks = 0 } state.SetVel(vel) s.applyBubbleColumns(state) diff --git a/liquid_test.go b/liquid_test.go index 5180d7a..635d5eb 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -355,10 +355,17 @@ func TestSwimAmountClampedToUnitRange(t *testing.T) { func TestStartSwimmingClearsSneaking(t *testing.T) { sim := newLiquidSim(newLiquidWorld()) state := newBaseState() + state.Crawling = true sim.applyInput(state, InputState{SneakDown: true, StartSneaking: true, StartSwimming: true}) if state.Sneaking { t.Fatal("StartSwimming must clear Sneaking") } + if state.Size.Y() != state.StandingHeight { + t.Fatalf("StartSwimming must clear the crouched height, got %v", state.Size.Y()) + } + if state.Crawling { + t.Fatal("StartSwimming must clear Crawling") + } } // StopSwimming wins when both flags arrive in the same tick. diff --git a/movement.go b/movement.go index eff5467..779ffa6 100644 --- a/movement.go +++ b/movement.go @@ -29,6 +29,10 @@ type MovementState struct { SlideOffset mgl32.Vec2 Impulse mgl32.Vec2 Size mgl32.Vec3 + // StuckSpeedMultiplier is the strongest queued berry-bush or powder-snow + // multiplier. It applies to one displacement, then clears along with + // persistent velocity. + StuckSpeedMultiplier mgl32.Vec3 // StandingHeight, SneakingHeight, and CrawlingHeight preserve custom entity // dimensions across pose transitions. Zero values use the current standing // height and vanilla player pose heights respectively. @@ -102,6 +106,14 @@ type MovementState struct { TicksSinceCanSlowdown int RiptideTicks int StartingSpinAttack bool + // RiptideReady is a one-tick trusted latch set after validating a charged + // Riptide trident release. RiptideCollision is set after a server-observed + // entity collision and authorizes the matching stop/reversal. + RiptideReady bool + RiptideCollision bool + // RiptideInRain is server-observed weather exposure that permits a + // validated Riptide release without direct block-water contact. + RiptideInRain bool Flying, MayFly, TrustFlyStatus bool JustDisabledFlight bool diff --git a/simulation.go b/simulation.go index 0d88871..f62a3cc 100644 --- a/simulation.go +++ b/simulation.go @@ -51,11 +51,15 @@ func (s *Simulator) debugfIf(cond bool, format string, args ...any) { } func (s *Simulator) simulateCore(state *MovementState) SimulationOutcome { + defer func() { + state.RiptideReady = false + }() teleported := s.attemptTeleport(state) if teleported { // A teleport relocates the player without observing the destination, // so any retained water contact from the origin is void. state.SwimWaterGraceTicks = 0 + state.StuckSpeedMultiplier = mgl32.Vec3{} return SimulationOutcomeTeleport } @@ -72,6 +76,7 @@ func (s *Simulator) simulateCore(state *MovementState) SimulationOutcome { 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 + state.StuckSpeedMultiplier = mgl32.Vec3{} return SimulationOutcomeUnloadedChunk } if state.Immobile || !state.Ready { @@ -79,6 +84,7 @@ func (s *Simulator) simulateCore(state *MovementState) SimulationOutcome { // Frozen ticks observe nothing, so the budget must not simply pause // and resume later. state.SwimWaterGraceTicks = 0 + state.StuckSpeedMultiplier = mgl32.Vec3{} return SimulationOutcomeImmobileOrNotReady } @@ -118,6 +124,7 @@ func (s *Simulator) resultFromState(state *MovementState, outcome SimulationOutc func (s *Simulator) applyInput(state *MovementState, input InputState) { state.ensurePoseHeights() + poseCollisionsAvailable := s.poseCollisionsAvailable(state) state.Client.HorizontalCollision = input.HorizontalCollision state.Client.VerticalCollision = input.VerticalCollision @@ -182,6 +189,10 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { } } + wantSneak := input.SneakDown || input.StartSneaking + if input.StopSneaking { + wantSneak = false + } if input.StartSneaking { state.Sneaking = true if !state.Crawling { @@ -190,7 +201,7 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { } else if input.StopSneaking { if state.Crawling { state.Sneaking = false - } else if s.canFitHeight(state, state.StandingHeight) { + } else if poseCollisionsAvailable && s.canFitHeight(state, state.StandingHeight) { state.Sneaking = false state.Size[1] = state.StandingHeight } else { @@ -198,24 +209,32 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { state.Size[1] = state.SneakingHeight } } else { - state.Sneaking = input.SneakDown - if state.Sneaking && !state.Crawling { + if state.Crawling { + state.Sneaking = false + } else if input.SneakDown { + state.Sneaking = true state.Size[1] = state.SneakingHeight + } else if state.Sneaking && (!poseCollisionsAvailable || !s.canFitHeight(state, state.StandingHeight)) { + state.Size[1] = state.SneakingHeight + } else { + state.Sneaking = false + state.Size[1] = state.StandingHeight } } if input.StartCrawling { - if !s.canFitHeight(state, state.StandingHeight) { + if poseCollisionsAvailable && !s.canFitHeight(state, state.StandingHeight) { state.Crawling = true state.Sneaking = false state.Size[1] = state.CrawlingHeight } } else if input.StopCrawling { targetHeight := state.StandingHeight - if state.Sneaking { + if wantSneak { targetHeight = state.SneakingHeight } - if s.canFitHeight(state, targetHeight) { + if poseCollisionsAvailable && s.canFitHeight(state, targetHeight) { state.Crawling = false + state.Sneaking = wantSneak state.Size[1] = targetHeight } else { state.Crawling = true @@ -229,6 +248,8 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { } else if input.StartSwimming { state.Swimming = true state.Sneaking = false + state.Crawling = false + state.Size[1] = state.StandingHeight } if wasSwimming { state.SwimAmount = ClampFloat(state.SwimAmount+0.1, 0, 1) @@ -293,8 +314,9 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { } state.StartingSpinAttack = input.StartSpinAttack - if input.StopSpinAttack && state.RiptideTicks > 0 { + if input.StopSpinAttack && state.RiptideTicks > 0 && state.RiptideCollision { state.RiptideTicks = 0 + state.RiptideCollision = false state.SetVel(state.Vel.Mul(-0.2)) } @@ -342,6 +364,9 @@ func (s *Simulator) tickState(state *MovementState) { } if state.RiptideTicks > 0 { state.RiptideTicks-- + if state.RiptideTicks == 0 { + state.RiptideCollision = false + } } state.JustDisabledFlight = false } @@ -363,11 +388,11 @@ func (s *Simulator) simulateMovement(state *MovementState) { waterBlocks := s.touchingLiquidBlocks(state, liquidWater) lavaBlocks := s.touchingLiquidBlocks(state, liquidLava) - if s.attemptRiptide(state, len(waterBlocks) != 0 || len(lavaBlocks) != 0) { + inWater := len(waterBlocks) != 0 + if !state.Flying && s.attemptRiptide(state, inWater) { s.debugf("riptide launch applied: %v", state.Vel) } - inWater := len(waterBlocks) != 0 defer func() { if inWater { state.SwimWaterGraceTicks = grace @@ -421,12 +446,18 @@ func (s *Simulator) simulateMovement(state *MovementState) { if hasElytra && !state.OnGround { state.OnGround = false s.simulateGlide(state) + stuckMovement := applyStuckSpeedMultiplier(state) oldVel := state.Vel oldY := state.Pos.Y() s.tryCollisions(state, false) + stopRiptideOnBlockCollision(state) updateFallDistance(state, oldY) s.debugf("(glide) oldVel=%v, collisions=%v diff=%v", oldVel, state.Vel, state.Vel.Sub(state.Client.Vel)) state.SetMov(state.Vel) + if stuckMovement { + state.SetVel(mgl32.Vec3{}) + } + s.applyInsideBlockEffects(state) return } @@ -478,12 +509,14 @@ func (s *Simulator) simulateMovement(state *MovementState) { s.debugf("web force applied (vel=%v)", newVel) } + stuckMovement := applyStuckSpeedMultiplier(state) s.avoidEdge(state) oldVel := state.Vel oldOnGround := state.OnGround oldY := state.Pos.Y() s.tryCollisions(state, clientJumpPrevented) + stopRiptideOnBlockCollision(state) updateFallDistance(state, oldY) if state.SupportingBlockPos != nil { @@ -505,6 +538,10 @@ func (s *Simulator) simulateMovement(state *MovementState) { } state.SetMov(state.Vel) + if stuckMovement { + state.SetVel(mgl32.Vec3{}) + oldVel = mgl32.Vec3{} + } s.setPostCollisionMotion(state, oldVel, oldOnGround, blockUnder) if inCobweb { @@ -549,6 +586,7 @@ func (s *Simulator) resetToClient(state *MovementState) { // A frame we did not simulate proves nothing about water contact, so the // retained evidence is dropped rather than carried across the gap. state.SwimWaterGraceTicks = 0 + state.StuckSpeedMultiplier = mgl32.Vec3{} state.LastPos = state.Client.LastPos state.Pos = state.Client.Pos state.LastVel = state.Client.LastVel @@ -654,7 +692,7 @@ func (s *Simulator) walkOnBlock(state *MovementState, blockUnder world.Block) { oldVel := state.Vel newVel := state.Vel semantics := s.blockMovementSemantics(blockUnder) - if semantics.Bounce == movementblock.BounceSlime || semantics.InsideMovement == movementblock.InsideMovementHoney { + if semantics.Bounce == movementblock.BounceSlime || semantics.Honey { yMov := math32.Abs(newVel.Y()) if yMov < 0.1 && !state.PressingSneak { d1 := 0.4 + yMov*0.2 @@ -762,8 +800,7 @@ func (s *Simulator) attemptJump(state *MovementState, clientJumpPrevented *bool) jumpHeight := state.JumpHeight inBlock := s.blockAtPos(posFromVec3(state.Pos)) below := s.blockAtPos(posFromVec3(state.Pos.Sub(mgl32.Vec3{0, 0.1}))) - if s.blockMovementSemantics(inBlock).InsideMovement == movementblock.InsideMovementHoney || - s.blockMovementSemantics(below).InsideMovement == movementblock.InsideMovementHoney { + if s.blockMovementSemantics(inBlock).Honey || s.blockMovementSemantics(below).Honey { jumpHeight *= 0.6 } newVel[1] = math32.Max(jumpHeight, newVel[1]) @@ -935,7 +972,7 @@ func (s *Simulator) tryCollisions(state *MovementState, clientJumpPrevented bool newBBListCount = len(s.nearbyBBoxes(state, stepBB)) hasStepCollisions = newBBListCount > 0 } else { - hasStepCollisions = len(s.nearbyBBoxes(state, stepBB)) > 0 + hasStepCollisions = s.hasNearbyBBoxes(state, stepBB) } stepPos := mgl32.Vec3{ (stepBB.Min().X() + stepBB.Max().X()) * 0.5, @@ -1031,7 +1068,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { xMov, zMov := newVel.X(), newVel.Z() i := 0 - for i = 0; i < maxIter && xMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, 0}))) == 0; i++ { + for i = 0; i < maxIter && xMov != 0.0 && !s.hasNearbyBBoxes(state, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, 0})); i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -1044,7 +1081,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { xMov = 0 } - for i = 0; i < maxIter && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{0, -StepHeight * 1.01, zMov}))) == 0; i++ { + for i = 0; i < maxIter && zMov != 0.0 && !s.hasNearbyBBoxes(state, bb.Translate(mgl32.Vec3{0, -StepHeight * 1.01, zMov})); i++ { if zMov < offset && zMov >= -offset { zMov = 0 } else if zMov > 0 { @@ -1057,7 +1094,7 @@ func (s *Simulator) avoidEdge(state *MovementState) { zMov = 0 } - for i = 0; i < maxIter && xMov != 0.0 && zMov != 0.0 && len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, zMov}))) == 0; i++ { + for i = 0; i < maxIter && xMov != 0.0 && zMov != 0.0 && !s.hasNearbyBBoxes(state, bb.Translate(mgl32.Vec3{xMov, -StepHeight * 1.01, zMov})); i++ { if xMov < offset && xMov >= -offset { xMov = 0 } else if xMov > 0 { @@ -1094,7 +1131,7 @@ func (s *Simulator) isAboveGround(state *MovementState) bool { } distance := 0.6 - state.FallDistance bb := state.BoundingBox(s.Options.UseSlideOffset).GrowVec3(mgl32.Vec3{-0.025, 0, -0.025}) - return len(s.nearbyBBoxes(state, bb.Translate(mgl32.Vec3{0, -distance}))) > 0 + return s.hasNearbyBBoxes(state, bb.Translate(mgl32.Vec3{0, -distance})) } func (s *Simulator) isInsideCobweb(state *MovementState) bool { @@ -1211,6 +1248,23 @@ func (s *Simulator) nearbyBBoxes(state *MovementState, aabb cube.BBox32) []cube. return s.World.GetNearbyBBoxes(aabb) } +type nearbyBBoxProbe interface { + HasNearbyBBoxes(aabb cube.BBox32) bool +} + +func (s *Simulator) hasNearbyBBoxes(state *MovementState, aabb cube.BBox32) bool { + if s.World == nil { + return false + } + if _, dynamic := s.World.(MovementCollisionProvider); dynamic { + return len(s.nearbyBBoxes(state, aabb)) > 0 + } + if probe, ok := s.World.(nearbyBBoxProbe); ok { + return probe.HasNearbyBBoxes(aabb) + } + return len(s.World.GetNearbyBBoxes(aabb)) > 0 +} + func (s *Simulator) canFitHeight(state *MovementState, height float32) bool { if s.World == nil { return true @@ -1218,7 +1272,18 @@ func (s *Simulator) canFitHeight(state *MovementState, height float32) bool { standing := *state standing.Size[1] = height standing.Sneaking = false + standing.Swimming = false + standing.SwimWaterGraceTicks = 0 standing.PressingDescend = false standing.WantDown = false return len(s.nearbyBBoxes(&standing, standing.BoundingBox(s.Options.UseSlideOffset))) == 0 } + +func (s *Simulator) poseCollisionsAvailable(state *MovementState) bool { + if s.World == nil { + return true + } + chunkX := int32(math32.Floor(state.Pos.X())) >> 4 + chunkZ := int32(math32.Floor(state.Pos.Z())) >> 4 + return s.World.IsChunkLoaded(chunkX, chunkZ) +} diff --git a/simulator_test.go b/simulator_test.go index afe6541..c27e148 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -450,6 +450,39 @@ func TestSimulateStateOutcomeImmobileOrNotReady(t *testing.T) { } } +func TestEarlyExitClearsQueuedStuckMovement(t *testing.T) { + tests := []struct { + name string + setup func(*MovementState) + world WorldProvider + }{ + {name: "unloaded chunk", world: staticWorld{chunkLoaded: false}}, + {name: "immobile", world: mockWorld{}, setup: func(state *MovementState) { state.Immobile = true }}, + {name: "unreliable", world: mockWorld{}, setup: func(state *MovementState) { state.GameMode = packet.GameTypeCreative }}, + {name: "teleport", world: mockWorld{}, setup: func(state *MovementState) { + state.TeleportCompletionTicks = 1 + state.PendingTeleports = 1 + state.TeleportPos = mgl32.Vec3{10, 20, 30} + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + state := newBaseState() + state.StuckSpeedMultiplier = mgl32.Vec3{0.8, 0.75, 0.8} + if tt.setup != nil { + tt.setup(state) + } + + (&Simulator{World: tt.world}).SimulateState(state) + + if state.StuckSpeedMultiplier != (mgl32.Vec3{}) { + t.Fatalf("expected queued stuck movement to clear, got %v", state.StuckSpeedMultiplier) + } + }) + } +} + func TestSimulateStateSkipsGravityWhenDisabled(t *testing.T) { sim := &Simulator{ World: mockWorld{}, From f8c644136818fd73f1ecc2965cfbe58f1cadaa6e Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Thu, 6 Aug 2026 18:36:31 -0400 Subject: [PATCH 7/9] Fix queued block effect handling --- block_effects.go | 6 +++++- block_effects_test.go | 13 +++++++++++++ bubble.go | 1 - bubble_test.go | 20 +++++++++++++------- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/block_effects.go b/block_effects.go index 8e1692d..daae343 100644 --- a/block_effects.go +++ b/block_effects.go @@ -31,7 +31,11 @@ func queueStuckSpeedMultiplier(state *MovementState, multiplier mgl32.Vec3) { func applyStuckSpeedMultiplier(state *MovementState) bool { multiplier := state.StuckSpeedMultiplier - if state.NoClip || multiplier.LenSqr() <= 1e-7 { + if multiplier.LenSqr() <= 1e-7 { + return false + } + if state.NoClip { + state.StuckSpeedMultiplier = mgl32.Vec3{} return false } state.SetVel(mgl32.Vec3{ diff --git a/block_effects_test.go b/block_effects_test.go index ec6a865..79c4ccf 100644 --- a/block_effects_test.go +++ b/block_effects_test.go @@ -65,6 +65,19 @@ func TestStuckMovementMultiplierAppliesOnceAndClearsVelocity(t *testing.T) { } } +func TestNoClipDiscardsQueuedStuckMovement(t *testing.T) { + state := newBaseState() + state.NoClip = true + state.StuckSpeedMultiplier = mgl32.Vec3{0.8, 0.75, 0.8} + + if applyStuckSpeedMultiplier(state) { + t.Fatal("expected no-clip movement not to consume a multiplier") + } + if state.StuckSpeedMultiplier != (mgl32.Vec3{}) { + t.Fatalf("expected no-clip movement to discard the queued effect, got %v", state.StuckSpeedMultiplier) + } +} + func TestStuckMovementDoesNotBounce(t *testing.T) { sim := &Simulator{ World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ diff --git a/bubble.go b/bubble.go index beb2e33..12f9280 100644 --- a/bubble.go +++ b/bubble.go @@ -58,7 +58,6 @@ func (s *Simulator) applyBubbleColumns(state *MovementState) { above := pos.Side(cube.FaceUp) _, liquidAbove := s.liquidAt(above) applyBubbleColumn(state, direction, !liquidAbove && s.blockAir(s.blockAtPos(above))) - return } } } diff --git a/bubble_test.go b/bubble_test.go index 8c11eb9..b057f22 100644 --- a/bubble_test.go +++ b/bubble_test.go @@ -55,18 +55,24 @@ func TestBubbleColumnSurfaceAcceptsRegistryBackedAir(t *testing.T) { } } -func TestBubbleColumnAppliesOnceAcrossMultipleCells(t *testing.T) { - w := environmentWorld{bubbles: map[cube.Pos]BubbleColumnDirection{ - {0, 0, 0}: BubbleColumnUp, - {0, 1, 0}: BubbleColumnUp, - }} +func TestBubbleColumnAppliesForEachOccupiedCell(t *testing.T) { + w := environmentWorld{ + bubbles: map[cube.Pos]BubbleColumnDirection{ + {0, 0, 0}: BubbleColumnUp, + {0, 1, 0}: BubbleColumnUp, + }, + blocks: map[cube.Pos]world.Block{ + {0, 0, 0}: block.Water{Still: true, Depth: 8}, + {0, 1, 0}: block.Water{Still: true, Depth: 8}, + }, + } state := newBaseState() state.Pos = mgl32.Vec3{0.5, 0, 0.5} (&Simulator{World: w}).applyBubbleColumns(state) - if want := float32(0.1); math32.Abs(state.Vel.Y()-want) > 1e-6 { - t.Fatalf("bubble-column velocity = %v, want one impulse %v", state.Vel.Y(), want) + if want := float32(0.16); math32.Abs(state.Vel.Y()-want) > 1e-6 { + t.Fatalf("bubble-column velocity = %v, want per-cell impulses totaling %v", state.Vel.Y(), want) } } From bcfb917c201015e1273f9a133a31c7736beacf11 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Thu, 6 Aug 2026 18:44:58 -0400 Subject: [PATCH 8/9] Preserve fitting pose across swim transitions --- dynamic_collision_test.go | 30 ++++++++++++++++++++++++++++++ simulation.go | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/dynamic_collision_test.go b/dynamic_collision_test.go index e2ae9e4..7955ed2 100644 --- a/dynamic_collision_test.go +++ b/dynamic_collision_test.go @@ -143,6 +143,36 @@ func TestStopCrawlingWhileSneakingUsesCrouchHeight(t *testing.T) { } } +func TestStopSwimmingFallsBackToCrawlUnderLowCeiling(t *testing.T) { + sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ + cube.Box32(-1, 0.7, -1, 1, 2, 1), + }}} + state := newBaseState() + state.Swimming = true + state.SwimWaterGraceTicks = 1 + + sim.applyInput(state, InputState{StopSwimming: true}) + + if state.Swimming || !state.Crawling || state.Size.Y() != 0.6 { + t.Fatalf("expected crawl fallback after swimming, got swimming=%v crawling=%v size=%v", state.Swimming, state.Crawling, state.Size) + } +} + +func TestStartSwimmingWithoutSwimPosePreservesFittingCrawl(t *testing.T) { + sim := &Simulator{World: staticWorld{chunkLoaded: true, boxes: []cube.BBox32{ + cube.Box32(-1, 0.7, -1, 1, 2, 1), + }}} + state := newBaseState() + state.Crawling = true + state.Size[1] = 0.6 + + sim.applyInput(state, InputState{StartSwimming: true}) + + if !state.Swimming || !state.Crawling || state.Size.Y() != 0.6 { + t.Fatalf("expected crawl pose until swim collapse is observed, got swimming=%v crawling=%v size=%v", state.Swimming, state.Crawling, state.Size) + } +} + func TestPoseRestoresCustomStandingHeight(t *testing.T) { state := newBaseState() state.Size[1] = 2 diff --git a/simulation.go b/simulation.go index f62a3cc..df1ce6a 100644 --- a/simulation.go +++ b/simulation.go @@ -245,11 +245,12 @@ func (s *Simulator) applyInput(state *MovementState, input InputState) { wasSwimming := state.Swimming if input.StopSwimming { state.Swimming = false + s.restorePoseAfterSwimming(state, poseCollisionsAvailable) } else if input.StartSwimming { state.Swimming = true - state.Sneaking = false - state.Crawling = false - state.Size[1] = state.StandingHeight + if state.SwimPose() || poseCollisionsAvailable && s.canFitHeight(state, state.StandingHeight) { + setSwimmingPoseFlags(state) + } } if wasSwimming { state.SwimAmount = ClampFloat(state.SwimAmount+0.1, 0, 1) @@ -389,6 +390,10 @@ func (s *Simulator) simulateMovement(state *MovementState) { waterBlocks := s.touchingLiquidBlocks(state, liquidWater) lavaBlocks := s.touchingLiquidBlocks(state, liquidLava) inWater := len(waterBlocks) != 0 + if inWater && state.Swimming { + state.SwimWaterGraceTicks = grace + setSwimmingPoseFlags(state) + } if !state.Flying && s.attemptRiptide(state, inWater) { s.debugf("riptide launch applied: %v", state.Vel) } @@ -1287,3 +1292,27 @@ func (s *Simulator) poseCollisionsAvailable(state *MovementState) bool { chunkZ := int32(math32.Floor(state.Pos.Z())) >> 4 return s.World.IsChunkLoaded(chunkX, chunkZ) } + +func setSwimmingPoseFlags(state *MovementState) { + state.Sneaking = false + state.Crawling = false + state.Size[1] = state.StandingHeight +} + +func (s *Simulator) restorePoseAfterSwimming(state *MovementState, collisionsAvailable bool) { + if collisionsAvailable && s.canFitHeight(state, state.StandingHeight) { + state.Sneaking = false + state.Crawling = false + state.Size[1] = state.StandingHeight + return + } + if collisionsAvailable && s.canFitHeight(state, state.SneakingHeight) { + state.Sneaking = true + state.Crawling = false + state.Size[1] = state.SneakingHeight + return + } + state.Sneaking = false + state.Crawling = true + state.Size[1] = state.CrawlingHeight +} From dff5fc53bcb3952d8035fc616cbc62505887d392 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Thu, 6 Aug 2026 18:57:43 -0400 Subject: [PATCH 9/9] Initialize pose heights for state-only simulation --- liquid_test.go | 16 ++++++++++++++++ simulation.go | 1 + 2 files changed, 17 insertions(+) diff --git a/liquid_test.go b/liquid_test.go index 635d5eb..62f1d60 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -368,6 +368,22 @@ func TestStartSwimmingClearsSneaking(t *testing.T) { } } +func TestSimulateStateInitializesPoseHeightBeforeSwimming(t *testing.T) { + w := environmentWorld{blocks: map[cube.Pos]world.Block{{0, 0, 0}: block.Water{Still: true, Depth: 8}}} + state := newBaseState() + state.Pos = mgl32.Vec3{0.5, 0, 0.5} + state.Swimming = true + state.StandingHeight = 0 + state.SneakingHeight = 0 + state.CrawlingHeight = 0 + + (&Simulator{World: w}).SimulateState(state) + + if state.StandingHeight != 1.8 || state.Size.Y() != 1.8 { + t.Fatalf("expected initialized standing pose, got standing=%v size=%v", state.StandingHeight, state.Size) + } +} + // StopSwimming wins when both flags arrive in the same tick. func TestStopSwimmingTakesPriority(t *testing.T) { sim := newLiquidSim(newLiquidWorld()) diff --git a/simulation.go b/simulation.go index df1ce6a..8ad3341 100644 --- a/simulation.go +++ b/simulation.go @@ -51,6 +51,7 @@ func (s *Simulator) debugfIf(cond bool, format string, args ...any) { } func (s *Simulator) simulateCore(state *MovementState) SimulationOutcome { + state.ensurePoseHeights() defer func() { state.RiptideReady = false }()