From d6afa5a17de5f51b3b068170a92e3bdd81b0a6ec Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 3 Aug 2026 23:24:59 -0700 Subject: [PATCH 1/5] feat: centralize movement block semantics --- README.md | 9 +- block.go | 81 +++++++++++++++ block_semantics_test.go | 215 ++++++++++++++++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 12 +-- interfaces.go | 15 +-- liquid_test.go | 7 +- simulation.go | 24 ++--- simulator.go | 38 ++----- simulator_test.go | 55 ++++------ 10 files changed, 365 insertions(+), 94 deletions(-) create mode 100644 block_semantics_test.go diff --git a/README.md b/README.md index af9e84c..bec330a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Implement provider adapters to bridge your world and player systems: ```go sim := bedsim.Simulator{ World: myWorldProvider, // block lookups, collisions, chunk-loaded checks - BlockSemantics: myBlockSemantics, // optional: per-world names, friction, climbability + BlockSemantics: myBlockSemantics, // optional: complete per-world movement semantics Liquids: myLiquidProvider, // second block layer (waterlogged blocks) Effects: myEffectsProvider, // jump boost, levitation, slow falling Inventory: myInventoryProvider, // elytra equipped check @@ -44,8 +44,11 @@ if result.NeedsCorrection { Set `BlockSemantics` when movement behavior must come from a per-world block registry or custom block data instead of bedsim's Dragonfly-backed defaults. -Custom friction values must be finite and positive; invalid values fall back to -Dragonfly defaults. +The adapter implements `BlockMovementSemanticsProvider` and returns the full +`MovementBlockSemantics` bundle: ground friction, climbability, cobweb status, +slime/bed bounce behavior, and whether the block is unsafe for authoritative +simulation. Custom friction values must be finite and positive; invalid values +fall back to Dragonfly defaults. Implement `DepthStriderProvider` on the inventory adapter when Depth Strider should affect water movement. diff --git a/block.go b/block.go index d39d846..b660678 100644 --- a/block.go +++ b/block.go @@ -9,6 +9,11 @@ import ( "github.com/df-mc/dragonfly/server/world" ) +// SoulSandGroundFrictionMultiplier is the native friction adjustment used by +// SoulSandBlock::calcGroundFriction when Soul Speed is not active. Soul sand +// and soul soil share this ground-friction path. +const SoulSandGroundFrictionMultiplier float32 = 1.225000023841858 + type blockNameKey struct { base, state uint64 } @@ -51,6 +56,26 @@ func BlockFriction(b world.Block) float32 { } } +// BlockGroundFriction returns the friction used by ordinary grounded travel. +// It is intentionally separate from BlockFriction: most blocks expose their +// ordinary friction directly, while soul sand and soul soil apply a movement +// specific adjustment in the native travel path. +func BlockGroundFriction(b world.Block) float32 { + friction := BlockFriction(b) + if isSoulGroundBlock(b, BlockName(b)) { + friction *= SoulSandGroundFrictionMultiplier + } + return friction +} + +func isSoulGroundBlock(b world.Block, name string) bool { + switch b.(type) { + case block.SoulSand, block.SoulSoil: + return true + } + return name == "minecraft:soul_sand" || name == "minecraft:soul_soil" +} + // BlockClimbable returns whether the given block is climbable. func BlockClimbable(b world.Block) bool { switch b.(type) { @@ -67,6 +92,62 @@ func BlockClimbable(b world.Block) bool { } } +// BlockCobweb reports whether the block applies the cobweb movement slowdown. +// "web" is the canonical Bedrock identifier; accepting "cobweb" as well +// keeps custom registries and older adapters interoperable. +func BlockCobweb(b world.Block) bool { + switch BlockName(b) { + case "minecraft:web", "minecraft:cobweb": + return true + default: + return false + } +} + +// MovementBounce identifies the vanilla vertical response when an entity +// lands on a block. +type MovementBounce uint8 + +const ( + MovementBounceNone MovementBounce = iota + MovementBounceSlime + MovementBounceBed +) + +// MovementBlockSemantics is the complete set of block properties consumed by +// the movement integrator. A custom registry must return all properties from +// one world-consistent snapshot; zero values mean ordinary block behaviour. +type MovementBlockSemantics struct { + GroundFriction float32 + Climbable bool + Cobweb bool + Bounce MovementBounce + + // Unsupported marks blocks whose collision/contact behaviour is not safe + // for authoritative simulation. Bamboo is the built-in example. + Unsupported bool +} + +// DefaultMovementBlockSemantics resolves the vanilla movement properties for +// a block using the built-in Dragonfly-backed registry. +func DefaultMovementBlockSemantics(b world.Block) MovementBlockSemantics { + semantics := MovementBlockSemantics{ + GroundFriction: BlockGroundFriction(b), + Climbable: BlockClimbable(b), + Cobweb: BlockCobweb(b), + } + + switch BlockName(b) { + case "minecraft:slime": + semantics.Bounce = MovementBounceSlime + case "minecraft:bed": + semantics.Bounce = MovementBounceBed + case "minecraft:bamboo": + semantics.Unsupported = true + } + return semantics +} + // 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. diff --git a/block_semantics_test.go b/block_semantics_test.go new file mode 100644 index 0000000..5ace048 --- /dev/null +++ b/block_semantics_test.go @@ -0,0 +1,215 @@ +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/mgl32" +) + +func TestBlockGroundFrictionSoulBlocks(t *testing.T) { + want := DefaultBlockFriction * SoulSandGroundFrictionMultiplier + + for name, b := range map[string]world.Block{ + "soul sand": block.SoulSand{}, + "soul soil": block.SoulSoil{}, + } { + t.Run(name, func(t *testing.T) { + if got := BlockGroundFriction(b); math.Abs(float64(got-want)) > 1e-6 { + t.Fatalf("ground friction = %.8f, want %.8f", got, want) + } + }) + } +} + +func TestDefaultMovementBlockSemantics(t *testing.T) { + tests := []struct { + name string + block world.Block + climbable bool + cobweb bool + bounce MovementBounce + unsupported bool + groundWant float32 + }{ + { + name: "air", + block: block.Air{}, + groundWant: DefaultBlockFriction, + }, + { + name: "ladder", + block: block.Ladder{}, + climbable: true, + groundWant: DefaultBlockFriction, + }, + { + name: "vines", + block: block.Vines{}, + climbable: true, + groundWant: DefaultBlockFriction, + }, + { + name: "soul soil", + block: block.SoulSoil{}, + groundWant: DefaultBlockFriction * SoulSandGroundFrictionMultiplier, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DefaultMovementBlockSemantics(tt.block) + if math.Abs(float64(got.GroundFriction-tt.groundWant)) > 1e-6 { + t.Fatalf("ground friction = %.8f, want %.8f", got.GroundFriction, tt.groundWant) + } + if got.Climbable != tt.climbable { + t.Fatalf("climbable = %v, want %v", got.Climbable, tt.climbable) + } + if got.Cobweb != tt.cobweb { + t.Fatalf("cobweb = %v, want %v", got.Cobweb, tt.cobweb) + } + if got.Bounce != tt.bounce { + t.Fatalf("bounce = %v, want %v", got.Bounce, tt.bounce) + } + if got.Unsupported != tt.unsupported { + t.Fatalf("unsupported = %v, want %v", got.Unsupported, tt.unsupported) + } + }) + } +} + +func TestDefaultMovementBlockSemanticsSpecialBlocks(t *testing.T) { + for name, want := range map[string]struct { + block world.Block + bounce MovementBounce + unsupported bool + }{ + "slime": {block: semanticsNamedBlock{"minecraft:slime"}, bounce: MovementBounceSlime}, + "bed": {block: semanticsNamedBlock{"minecraft:bed"}, bounce: MovementBounceBed}, + "bamboo": {block: semanticsNamedBlock{"minecraft:bamboo"}, unsupported: true}, + "cobweb": {block: semanticsNamedBlock{"minecraft:web"}}, + } { + t.Run(name, func(t *testing.T) { + got := DefaultMovementBlockSemantics(want.block) + if got.Bounce != want.bounce || got.Unsupported != want.unsupported { + t.Fatalf("semantics = %+v, want bounce=%v unsupported=%v", got, want.bounce, want.unsupported) + } + if name == "cobweb" && !got.Cobweb { + t.Fatalf("expected cobweb semantics") + } + }) + } +} + +// semanticsNamedBlock is enough to exercise name-based semantics without depending on +// a particular Dragonfly block implementation being present in the registry. +type semanticsNamedBlock struct{ name string } + +func (b semanticsNamedBlock) Hash() (uint64, uint64) { return 0, math.MaxUint64 } +func (b semanticsNamedBlock) EncodeBlock() (string, map[string]any) { + return b.name, nil +} +func (b semanticsNamedBlock) Model() world.BlockModel { return block.Air{}.Model() } + +type extendedMovementSemantics struct { + groundFriction float32 + climbable bool + cobweb bool + bounce MovementBounce + unsupported bool +} + +func (s extendedMovementSemantics) BlockMovementSemantics(world.Block) MovementBlockSemantics { + return MovementBlockSemantics{ + GroundFriction: s.groundFriction, + Climbable: s.climbable, + Cobweb: s.cobweb, + Bounce: s.bounce, + Unsupported: s.unsupported, + } +} + +func TestSimulatorCompleteBlockSemanticsProvider(t *testing.T) { + sim := &Simulator{ + BlockSemantics: extendedMovementSemantics{ + groundFriction: 0.37, + climbable: true, + cobweb: true, + bounce: MovementBounceBed, + unsupported: true, + }, + } + + got := sim.blockMovementSemantics(block.Air{}) + if got.GroundFriction != 0.37 || !got.Climbable || !got.Cobweb || + got.Bounce != MovementBounceBed || !got.Unsupported { + t.Fatalf("got incomplete semantic bundle: %+v", got) + } +} + +func TestSimulatorInvalidBlockSemanticsFrictionFallsBack(t *testing.T) { + for name, friction := range map[string]float32{ + "zero": 0, + "negative": -0.42, + "nan": float32(math.NaN()), + "positive infinity": float32(math.Inf(1)), + "negative infinity": float32(math.Inf(-1)), + } { + t.Run(name, func(t *testing.T) { + sim := &Simulator{ + BlockSemantics: extendedMovementSemantics{groundFriction: friction}, + } + got := sim.blockMovementSemantics(block.Air{}).GroundFriction + if got != DefaultBlockFriction { + t.Fatalf("ground friction = %v, want %v", got, DefaultBlockFriction) + } + }) + } +} + +type blockMovementWorld struct { + b world.Block +} + +func (w blockMovementWorld) Block(cube.Pos) world.Block { + return w.b +} + +func (blockMovementWorld) BlockCollisions(cube.Pos) []cube.BBox32 { + return nil +} + +func (blockMovementWorld) GetNearbyBBoxes(cube.BBox32) []cube.BBox32 { + return nil +} + +func (blockMovementWorld) IsChunkLoaded(int32, int32) bool { + return true +} + +func TestSimulateGroundUsesSoulSoilFriction(t *testing.T) { + sim := &Simulator{ + World: blockMovementWorld{b: block.SoulSoil{}}, + Effects: mockEffects{}, + } + + state := newBaseState() + state.Pos = mgl32.Vec3{0, 1, 0} + state.Client.Pos = state.Pos + state.OnGround = true + state.HasGravity = false + state.Impulse = mgl32.Vec2{0, 0.98} + + result := sim.SimulateState(state) + groundFriction := DefaultAirFriction * BlockGroundFriction(block.SoulSoil{}) + moveRelativeSpeed := state.MovementSpeed * + (0.16277136 / (groundFriction * groundFriction * groundFriction)) + wantZ := 0.98 * moveRelativeSpeed * groundFriction + + if math.Abs(float64(result.Velocity.Z()-wantZ)) > 1e-5 { + t.Fatalf("ground velocity Z = %.8f, want %.8f", result.Velocity.Z(), wantZ) + } +} diff --git a/go.mod b/go.mod index b7cf8b4..96ede50 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,9 @@ require ( github.com/chewxy/math32 v1.11.1 github.com/df-mc/dragonfly v0.10.11-0.20260721135200-3e3556bddb5f github.com/go-gl/mathgl v1.2.0 - github.com/sandertv/gophertunnel v1.57.0 + github.com/sandertv/gophertunnel v1.57.2-0.20260720171832-1706d533aa80 ) - require ( github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479 // indirect github.com/df-mc/goleveldb v1.1.9 // indirect diff --git a/go.sum b/go.sum index 3ede9ef..a108a85 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479 h1:UZbbt19ACBOFO+ github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479/go.mod h1:TOk10ahXejq9wkEaym3KPRNeuR/h5Jx+s8QRWIa2oTM= github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8= github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= +github.com/df-mc/dragonfly v0.10.11-0.20260721135200-3e3556bddb5f h1:3fOtzXVIEsci6+Nayy+R/glF053UdQq2sRtHdQdinrs= +github.com/df-mc/dragonfly v0.10.11-0.20260721135200-3e3556bddb5f/go.mod h1:NGnnP9U+QN2NeTfl+Ul2NvVvVbDaRTEgcMS2Gz9dpzA= 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.21 h1:Qr4/QB8ek7En0vkTuRXYq4FrZM0HHSOXsJOL7Ko4Cjg= @@ -15,8 +17,6 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashimthearab/dragonfly v0.0.0-20260721043247-e11e4f6ede86 h1:Mk87hJwo3XaqjOKeKkN6+eBkWIZZmKIOzY3/jHoeEWw= -github.com/hashimthearab/dragonfly v0.0.0-20260721043247-e11e4f6ede86/go.mod h1:qZwpBcuVNCqHg8Nj6gec5bG+5LrBYPok1k7FKDQQrng= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -27,8 +27,8 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/sandertv/gophertunnel v1.57.0 h1:UkgVg1xLCsOSm79rP09WmodGSHgA8M7+l4quL01cIL8= -github.com/sandertv/gophertunnel v1.57.0/go.mod h1:W4VnrX9AIPIVXNDMEIKMIRj1T80EdOgdqXpGbQpyAbE= +github.com/sandertv/gophertunnel v1.57.2-0.20260720171832-1706d533aa80 h1:epvVbD9bZVnPDXFcQE6YAufexj3MhcpBWlEIuGPJDek= +github.com/sandertv/gophertunnel v1.57.2-0.20260720171832-1706d533aa80/go.mod h1:Frjuk3g1EEGRYjzyrEMS4j/hDvTcez2STfuRHkRPlNU= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= @@ -49,5 +49,5 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/interfaces.go b/interfaces.go index 252183f..b8bd761 100644 --- a/interfaces.go +++ b/interfaces.go @@ -19,13 +19,14 @@ type LiquidProvider interface { Liquid(pos cube.Pos) (world.Liquid, bool) } -// BlockSemanticsProvider resolves movement-relevant block behavior. Implement -// this when names, friction, or climbability come from a per-world registry or -// custom block data instead of Dragonfly's default block types. -type BlockSemanticsProvider interface { - BlockName(world.Block) string - BlockFriction(world.Block) float32 - BlockClimbable(world.Block) bool +// BlockMovementSemanticsProvider resolves the complete movement-relevant +// behavior for a block. Implement this when block properties come from a +// per-world registry or custom block data instead of Dragonfly's defaults. +// +// GroundFriction is the post-adjustment value used by grounded travel. A +// non-positive or non-finite value falls back to bedsim's default resolution. +type BlockMovementSemanticsProvider interface { + BlockMovementSemantics(world.Block) MovementBlockSemantics } // DefaultBlockSemantics uses bedsim's built-in Dragonfly-backed block helpers. diff --git a/liquid_test.go b/liquid_test.go index fc86bf5..3ed1afe 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -1355,9 +1355,10 @@ func TestClimbUsesEffectiveJumping(t *testing.T) { t.Run(name, func(t *testing.T) { sim := newLiquidSim(newLiquidWorld()) sim.BlockSemantics = overrideBlockSemantics{ - name: "minecraft:ladder", - friction: DefaultBlockFriction, - climbable: true, + semantics: MovementBlockSemantics{ + GroundFriction: DefaultBlockFriction, + Climbable: true, + }, } state := submergedState() apply(state) diff --git a/simulation.go b/simulation.go index d6427dd..3a6f894 100644 --- a/simulation.go +++ b/simulation.go @@ -337,10 +337,8 @@ func (s *Simulator) simulateMovement(state *MovementState) { moveRelativeSpeed := state.AirSpeed if state.OnGround { mSpeed := state.MovementSpeed - if s.blockName(blockUnder) == "minecraft:soul_sand" { - mSpeed *= 0.543 - } - blockFriction *= s.blockFriction(blockUnder) + blockSemantics := s.blockMovementSemantics(blockUnder) + blockFriction *= blockSemantics.GroundFriction moveRelativeSpeed = mSpeed * (0.16277136 / (blockFriction * blockFriction * blockFriction)) } @@ -364,12 +362,12 @@ func (s *Simulator) simulateMovement(state *MovementState) { var clientJumpPrevented bool s.debugfIf(attemptKnockback(state), "knockback applied: %v", state.Vel) - s.debugf("blockUnder=%s, blockFriction=%v, speed=%v", s.blockName(blockUnder), blockFriction, moveRelativeSpeed) + s.debugf("blockUnder=%s, blockFriction=%v, speed=%v", BlockName(blockUnder), blockFriction, moveRelativeSpeed) 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) - nearClimbable := s.blockClimbable(s.blockAtPos(posFromVec3(state.Pos))) + nearClimbable := s.blockMovementSemantics(s.blockAtPos(posFromVec3(state.Pos))).Climbable if nearClimbable { newVel := state.Vel negClimbSpeed := -ClimbSpeed @@ -460,7 +458,7 @@ func (s *Simulator) simulationIsReliable(state *MovementState) bool { if _, isAir := b.(block.Air); isAir { continue } - if s.blockName(b) == "minecraft:bamboo" { + if s.blockMovementSemantics(b).Unsupported { isReliable = false break } @@ -585,8 +583,8 @@ func (s *Simulator) walkOnBlock(state *MovementState, blockUnder world.Block) { oldVel := state.Vel newVel := state.Vel - switch s.blockName(blockUnder) { - case "minecraft:slime": + switch s.blockMovementSemantics(blockUnder).Bounce { + case MovementBounceSlime: yMov := math32.Abs(newVel.Y()) if yMov < 0.1 && !state.PressingSneak { d1 := 0.4 + yMov*0.2 @@ -606,13 +604,13 @@ func (s *Simulator) landOnBlock(state *MovementState, old mgl32.Vec3, blockUnder return } - switch s.blockName(blockUnder) { - case "minecraft:slime": + switch s.blockMovementSemantics(blockUnder).Bounce { + case MovementBounceSlime: newVel[1] = SlimeBounceMultiplier * old.Y() if math32.Abs(newVel[1]) < 1e-4 { newVel[1] = 0.0 } - case "minecraft:bed": + case MovementBounceBed: newVel[1] = math32.Min(1.0, BedBounceMultiplier*old.Y()) default: newVel[1] = 0 @@ -1014,7 +1012,7 @@ func (s *Simulator) isInsideCobweb(state *MovementState) bool { if _, isAir := b.(block.Air); isAir { continue } - if s.blockName(b) != "minecraft:web" { + if !s.blockMovementSemantics(b).Cobweb { continue } diff --git a/simulator.go b/simulator.go index 757fd61..e4c59a8 100644 --- a/simulator.go +++ b/simulator.go @@ -64,7 +64,7 @@ type Simulator struct { World WorldProvider // BlockSemantics optionally resolves movement-specific block behavior from // the same world snapshot as World. Nil uses DefaultBlockSemantics. - BlockSemantics BlockSemanticsProvider + BlockSemantics BlockMovementSemanticsProvider // Liquids exposes second-layer liquids. World is used when it implements // LiquidProvider; otherwise waterlogged blocks are invisible. Liquids LiquidProvider @@ -73,16 +73,8 @@ type Simulator struct { Options SimulationOptions } -func (DefaultBlockSemantics) BlockName(b world.Block) string { - return BlockName(b) -} - -func (DefaultBlockSemantics) BlockFriction(b world.Block) float32 { - return BlockFriction(b) -} - -func (DefaultBlockSemantics) BlockClimbable(b world.Block) bool { - return BlockClimbable(b) +func (DefaultBlockSemantics) BlockMovementSemantics(b world.Block) MovementBlockSemantics { + return DefaultMovementBlockSemantics(b) } // swimWaterGraceTicks resolves the configured grace window: zero means the @@ -98,25 +90,17 @@ func (s *Simulator) swimWaterGraceTicks() int64 { } } -func (s *Simulator) blockName(b world.Block) string { - if s.BlockSemantics != nil { - return s.BlockSemantics.BlockName(b) - } - return BlockName(b) +func validGroundFriction(friction float32) bool { + return friction > 0 && !math32.IsInf(friction, 1) } -func (s *Simulator) blockFriction(b world.Block) float32 { +func (s *Simulator) blockMovementSemantics(b world.Block) MovementBlockSemantics { if s.BlockSemantics != nil { - if friction := s.BlockSemantics.BlockFriction(b); friction > 0 && !math32.IsInf(friction, 1) { - return friction + semantics := s.BlockSemantics.BlockMovementSemantics(b) + if !validGroundFriction(semantics.GroundFriction) { + semantics.GroundFriction = DefaultMovementBlockSemantics(b).GroundFriction } + return semantics } - return BlockFriction(b) -} - -func (s *Simulator) blockClimbable(b world.Block) bool { - if s.BlockSemantics != nil { - return s.BlockSemantics.BlockClimbable(b) - } - return BlockClimbable(b) + return DefaultMovementBlockSemantics(b) } diff --git a/simulator_test.go b/simulator_test.go index 0392347..7aeb54b 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -114,21 +114,11 @@ func (m mockInventory) HasElytra() bool { } type overrideBlockSemantics struct { - name string - friction float32 - climbable bool + semantics MovementBlockSemantics } -func (s overrideBlockSemantics) BlockName(world.Block) string { - return s.name -} - -func (s overrideBlockSemantics) BlockFriction(world.Block) float32 { - return s.friction -} - -func (s overrideBlockSemantics) BlockClimbable(world.Block) bool { - return s.climbable +func (s overrideBlockSemantics) BlockMovementSemantics(world.Block) MovementBlockSemantics { + return s.semantics } func newBaseState() *MovementState { @@ -341,36 +331,36 @@ func TestSimulatorBlockSemanticsOverridesDefaults(t *testing.T) { sim := &Simulator{ World: mockWorld{}, BlockSemantics: overrideBlockSemantics{ - name: "minecraft:custom_floor", - friction: 0.42, - climbable: true, + semantics: MovementBlockSemantics{ + GroundFriction: 0.42, + Climbable: true, + Cobweb: true, + Bounce: MovementBounceBed, + }, }, } b := block.Air{} - if got := sim.blockName(b); got != "minecraft:custom_floor" { - t.Fatalf("expected semantic block name, got %q", got) + got := sim.blockMovementSemantics(b) + if got.GroundFriction != 0.42 { + t.Fatalf("expected semantic block friction, got %v", got.GroundFriction) } - if got := sim.blockFriction(b); got != 0.42 { - t.Fatalf("expected semantic block friction, got %v", got) - } - if !sim.blockClimbable(b) { + if !got.Climbable { t.Fatalf("expected semantic climbable value") } + if !got.Cobweb || got.Bounce != MovementBounceBed { + t.Fatalf("expected complete semantic bundle, got %+v", got) + } } func TestSimulatorDefaultBlockSemanticsFallback(t *testing.T) { b := block.Air{} sim := &Simulator{World: mockWorld{}} - if got := sim.blockName(b); got != BlockName(b) { - t.Fatalf("expected default block name, got %q", got) - } - if got := sim.blockFriction(b); got != BlockFriction(b) { - t.Fatalf("expected default block friction, got %v", got) - } - if got := sim.blockClimbable(b); got != BlockClimbable(b) { - t.Fatalf("expected default climbable value, got %v", got) + got := sim.blockMovementSemantics(b) + want := DefaultMovementBlockSemantics(b) + if got != want { + t.Fatalf("expected default movement semantics %+v, got %+v", want, got) } } @@ -394,11 +384,10 @@ func TestSimulatorInvalidBlockSemanticsFrictionFallsBackToDefault(t *testing.T) sim := &Simulator{ World: mockWorld{}, BlockSemantics: overrideBlockSemantics{ - name: "minecraft:custom_floor", - friction: tt.friction, + semantics: MovementBlockSemantics{GroundFriction: tt.friction}, }, } - if got := sim.blockFriction(b); got != want { + if got := sim.blockMovementSemantics(b).GroundFriction; got != want { t.Fatalf("expected invalid semantic friction to fall back to %v, got %v", want, got) } }) From 39d6551a0944931dcd2cb536c7abeb1c60d41476 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 4 Aug 2026 09:57:45 -0700 Subject: [PATCH 2/5] refactor: move block semantics into owners --- README.md | 10 +++-- block.go | 92 +++++++---------------------------------- block/bounce.go | 36 ++++++++++++++++ block/climbable.go | 27 ++++++++++++ block/contact.go | 14 +++++++ block/friction.go | 20 +++++++++ block/ground.go | 35 ++++++++++++++++ block/semantics.go | 85 +++++++++++++++++++++++++++++++++++++ block_semantics_test.go | 39 +++++++++-------- simulation.go | 15 ------- 10 files changed, 257 insertions(+), 116 deletions(-) create mode 100644 block/bounce.go create mode 100644 block/climbable.go create mode 100644 block/contact.go create mode 100644 block/friction.go create mode 100644 block/ground.go create mode 100644 block/semantics.go diff --git a/README.md b/README.md index bec330a..5917d60 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,13 @@ Set `BlockSemantics` when movement behavior must come from a per-world block registry or custom block data instead of bedsim's Dragonfly-backed defaults. The adapter implements `BlockMovementSemanticsProvider` and returns the full `MovementBlockSemantics` bundle: ground friction, climbability, cobweb status, -slime/bed bounce behavior, and whether the block is unsafe for authoritative -simulation. Custom friction values must be finite and positive; invalid values -fall back to Dragonfly defaults. +and slime/bed bounce behavior. The built-in owners for these semantics live in +the `github.com/oomph-ac/bedsim/block` package. Custom friction values must be +finite and positive; invalid values fall back to Dragonfly defaults. + +BedSim's semantics package does not mutate Dragonfly's registry. Applications +own registry setup and must register any additional block implementations +before finalizing their registry. Implement `DepthStriderProvider` on the inventory adapter when Depth Strider should affect water movement. diff --git a/block.go b/block.go index b660678..f4d2b65 100644 --- a/block.go +++ b/block.go @@ -7,12 +7,12 @@ import ( "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" + movementblock "github.com/oomph-ac/bedsim/block" ) -// SoulSandGroundFrictionMultiplier is the native friction adjustment used by -// SoulSandBlock::calcGroundFriction when Soul Speed is not active. Soul sand -// and soul soil share this ground-friction path. -const SoulSandGroundFrictionMultiplier float32 = 1.225000023841858 +// SoulSandGroundFrictionMultiplier is retained as a root-package alias for +// the owner constant in bedsim/block. +const SoulSandGroundFrictionMultiplier = movementblock.SoulGroundFrictionMultiplier type blockNameKey struct { base, state uint64 @@ -40,20 +40,7 @@ func BlockName(b world.Block) string { // BlockFriction returns the friction of the block. func BlockFriction(b world.Block) float32 { - if f, ok := b.(block.Frictional); ok { - return float32(f.Friction()) - } - - switch BlockName(b) { - case "minecraft:slime": - return 0.8 - case "minecraft:ice", "minecraft:packed_ice": - return 0.98 - case "minecraft:blue_ice": - return 0.99 - default: - return 0.6 - } + return movementblock.Friction(b, BlockName(b)) } // BlockGroundFriction returns the friction used by ordinary grounded travel. @@ -61,91 +48,40 @@ func BlockFriction(b world.Block) float32 { // ordinary friction directly, while soul sand and soul soil apply a movement // specific adjustment in the native travel path. func BlockGroundFriction(b world.Block) float32 { - friction := BlockFriction(b) - if isSoulGroundBlock(b, BlockName(b)) { - friction *= SoulSandGroundFrictionMultiplier - } - return friction -} - -func isSoulGroundBlock(b world.Block, name string) bool { - switch b.(type) { - case block.SoulSand, block.SoulSoil: - return true - } - return name == "minecraft:soul_sand" || name == "minecraft:soul_soil" + return DefaultMovementBlockSemantics(b).GroundFriction } // BlockClimbable returns whether the given block is climbable. func BlockClimbable(b world.Block) bool { - switch b.(type) { - case block.Ladder: - return true - } - - switch BlockName(b) { - case "minecraft:vine", "minecraft:cave_vines", "minecraft:cave_vines_body_with_berries", "minecraft:cave_vines_head_with_berries", - "minecraft:twisting_vines", "minecraft:weeping_vines": - return true - default: - return false - } + return DefaultMovementBlockSemantics(b).Climbable } // BlockCobweb reports whether the block applies the cobweb movement slowdown. // "web" is the canonical Bedrock identifier; accepting "cobweb" as well // keeps custom registries and older adapters interoperable. func BlockCobweb(b world.Block) bool { - switch BlockName(b) { - case "minecraft:web", "minecraft:cobweb": - return true - default: - return false - } + return DefaultMovementBlockSemantics(b).Cobweb } // MovementBounce identifies the vanilla vertical response when an entity // lands on a block. -type MovementBounce uint8 +type MovementBounce = movementblock.Bounce const ( - MovementBounceNone MovementBounce = iota - MovementBounceSlime - MovementBounceBed + MovementBounceNone = movementblock.BounceNone + MovementBounceSlime = movementblock.BounceSlime + MovementBounceBed = movementblock.BounceBed ) // MovementBlockSemantics is the complete set of block properties consumed by // the movement integrator. A custom registry must return all properties from // one world-consistent snapshot; zero values mean ordinary block behaviour. -type MovementBlockSemantics struct { - GroundFriction float32 - Climbable bool - Cobweb bool - Bounce MovementBounce - - // Unsupported marks blocks whose collision/contact behaviour is not safe - // for authoritative simulation. Bamboo is the built-in example. - Unsupported bool -} +type MovementBlockSemantics = movementblock.MovementSemantics // DefaultMovementBlockSemantics resolves the vanilla movement properties for // a block using the built-in Dragonfly-backed registry. func DefaultMovementBlockSemantics(b world.Block) MovementBlockSemantics { - semantics := MovementBlockSemantics{ - GroundFriction: BlockGroundFriction(b), - Climbable: BlockClimbable(b), - Cobweb: BlockCobweb(b), - } - - switch BlockName(b) { - case "minecraft:slime": - semantics.Bounce = MovementBounceSlime - case "minecraft:bed": - semantics.Bounce = MovementBounceBed - case "minecraft:bamboo": - semantics.Unsupported = true - } - return semantics + return movementblock.Resolve(b, BlockName(b), BlockFriction(b)) } // BlockSupportHeight returns the effective standing surface height for a ground diff --git a/block/bounce.go b/block/bounce.go new file mode 100644 index 0000000..5ff44ea --- /dev/null +++ b/block/bounce.go @@ -0,0 +1,36 @@ +package block + +import ( + dfblock "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/world" +) + +// Slime owns slime's bounce response. +type Slime struct{} + +func (Slime) Matches(b world.Block, name string) bool { + if _, ok := b.(dfblock.Slime); ok { + return true + } + return name == "minecraft:slime" +} + +func (Slime) Apply(s *MovementSemantics) { + s.Bounce = BounceSlime +} + +func (Slime) Friction() (float32, bool) { return 0.8, true } + +// Bed owns the bed bounce response. +type Bed struct{} + +func (Bed) Matches(b world.Block, name string) bool { + if _, ok := b.(dfblock.Bed); ok { + return true + } + return name == "minecraft:bed" +} + +func (Bed) Apply(s *MovementSemantics) { + s.Bounce = BounceBed +} diff --git a/block/climbable.go b/block/climbable.go new file mode 100644 index 0000000..20a1b3d --- /dev/null +++ b/block/climbable.go @@ -0,0 +1,27 @@ +package block + +import ( + dfblock "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/world" +) + +// ClimbableBlock owns the ladder and vine movement predicate. +type ClimbableBlock struct{} + +func (ClimbableBlock) Matches(b world.Block, name string) bool { + if _, ok := b.(dfblock.Ladder); ok { + return true + } + + switch name { + case "minecraft:vine", "minecraft:cave_vines", "minecraft:cave_vines_body_with_berries", "minecraft:cave_vines_head_with_berries", + "minecraft:twisting_vines", "minecraft:weeping_vines": + return true + default: + return false + } +} + +func (ClimbableBlock) Apply(s *MovementSemantics) { + s.Climbable = true +} diff --git a/block/contact.go b/block/contact.go new file mode 100644 index 0000000..5542b0a --- /dev/null +++ b/block/contact.go @@ -0,0 +1,14 @@ +package block + +import "github.com/df-mc/dragonfly/server/world" + +// Cobweb owns the cobweb contact slowdown. +type Cobweb struct{} + +func (Cobweb) Matches(_ world.Block, name string) bool { + return name == "minecraft:web" || name == "minecraft:cobweb" +} + +func (Cobweb) Apply(s *MovementSemantics) { + s.Cobweb = true +} diff --git a/block/friction.go b/block/friction.go new file mode 100644 index 0000000..8ef01d9 --- /dev/null +++ b/block/friction.go @@ -0,0 +1,20 @@ +package block + +import "github.com/df-mc/dragonfly/server/world" + +// frictionBlock supplies a named fallback for blocks whose concrete +// implementation is not available to the caller. +type frictionBlock struct { + name string + friction float32 +} + +func (b frictionBlock) Matches(_ world.Block, name string) bool { + return name == b.name +} + +func (frictionBlock) Apply(*MovementSemantics) {} + +func (b frictionBlock) Friction() (float32, bool) { + return b.friction, true +} diff --git a/block/ground.go b/block/ground.go new file mode 100644 index 0000000..71c82c6 --- /dev/null +++ b/block/ground.go @@ -0,0 +1,35 @@ +package block + +import ( + dfblock "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/world" +) + +// SoulSand owns the ground-friction adjustment shared by Soul Sand and its +// Soul Soil counterpart in vanilla movement. +type SoulSand struct{} + +func (SoulSand) Matches(b world.Block, name string) bool { + if _, ok := b.(dfblock.SoulSand); ok { + return true + } + return name == "minecraft:soul_sand" +} + +func (SoulSand) Apply(s *MovementSemantics) { + s.GroundFriction *= SoulGroundFrictionMultiplier +} + +// SoulSoil owns the same ground-friction adjustment as Soul Sand. +type SoulSoil struct{} + +func (SoulSoil) Matches(b world.Block, name string) bool { + if _, ok := b.(dfblock.SoulSoil); ok { + return true + } + return name == "minecraft:soul_soil" +} + +func (SoulSoil) Apply(s *MovementSemantics) { + s.GroundFriction *= SoulGroundFrictionMultiplier +} diff --git a/block/semantics.go b/block/semantics.go new file mode 100644 index 0000000..16ab0f6 --- /dev/null +++ b/block/semantics.go @@ -0,0 +1,85 @@ +// Package block owns BedSim's built-in movement semantics for blocks. +// +// It deliberately does not register blocks in Dragonfly's world registry. +// Registry ownership belongs to the application because registration affects +// runtime IDs and must happen before the registry is finalized. +package block + +import ( + dfblock "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/world" +) + +// SoulGroundFrictionMultiplier is the native ground-friction adjustment used +// by SoulSandBlock::calcGroundFriction when Soul Speed is not active. +const SoulGroundFrictionMultiplier float32 = 1.225000023841858 + +// Bounce identifies the vanilla vertical response when an entity lands on a +// block. +type Bounce uint8 + +const ( + BounceNone Bounce = iota + BounceSlime + BounceBed +) + +// MovementSemantics is the movement behavior owned by one resolved block. +// GroundFriction is supplied by Resolve so frictional blocks and block-owned +// adjustments are composed in one place. +type MovementSemantics struct { + GroundFriction float32 + Climbable bool + Cobweb bool + Bounce Bounce +} + +// Owner contributes movement behavior for one family of blocks. +type Owner interface { + Matches(world.Block, string) bool + Apply(*MovementSemantics) +} + +var owners = [...]Owner{ + SoulSand{}, + SoulSoil{}, + ClimbableBlock{}, + Cobweb{}, + Slime{}, + Bed{}, + frictionBlock{name: "minecraft:ice", friction: 0.98}, + frictionBlock{name: "minecraft:packed_ice", friction: 0.98}, + frictionBlock{name: "minecraft:blue_ice", friction: 0.99}, +} + +// Resolve returns the built-in movement semantics for b. name should be the +// caller's cached canonical block name; it lets custom block implementations +// participate without pretending to be Dragonfly concrete types. +func Resolve(b world.Block, name string, groundFriction float32) MovementSemantics { + semantics := MovementSemantics{GroundFriction: groundFriction} + for _, owner := range owners { + if owner.Matches(b, name) { + owner.Apply(&semantics) + } + } + return semantics +} + +// Friction returns the ordinary block friction before block-specific ground +// adjustments are applied. +func Friction(b world.Block, name string) float32 { + if f, ok := b.(dfblock.Frictional); ok { + return float32(f.Friction()) + } + + for _, owner := range owners { + frictionOwner, ok := owner.(interface{ Friction() (float32, bool) }) + if !ok || !owner.Matches(b, name) { + continue + } + if friction, ok := frictionOwner.Friction(); ok { + return friction + } + } + return 0.6 +} diff --git a/block_semantics_test.go b/block_semantics_test.go index 5ace048..8edba95 100644 --- a/block_semantics_test.go +++ b/block_semantics_test.go @@ -27,13 +27,12 @@ func TestBlockGroundFrictionSoulBlocks(t *testing.T) { func TestDefaultMovementBlockSemantics(t *testing.T) { tests := []struct { - name string - block world.Block - climbable bool - cobweb bool - bounce MovementBounce - unsupported bool - groundWant float32 + name string + block world.Block + climbable bool + cobweb bool + bounce MovementBounce + groundWant float32 }{ { name: "air", @@ -74,28 +73,24 @@ func TestDefaultMovementBlockSemantics(t *testing.T) { if got.Bounce != tt.bounce { t.Fatalf("bounce = %v, want %v", got.Bounce, tt.bounce) } - if got.Unsupported != tt.unsupported { - t.Fatalf("unsupported = %v, want %v", got.Unsupported, tt.unsupported) - } }) } } func TestDefaultMovementBlockSemanticsSpecialBlocks(t *testing.T) { for name, want := range map[string]struct { - block world.Block - bounce MovementBounce - unsupported bool + block world.Block + bounce MovementBounce }{ "slime": {block: semanticsNamedBlock{"minecraft:slime"}, bounce: MovementBounceSlime}, "bed": {block: semanticsNamedBlock{"minecraft:bed"}, bounce: MovementBounceBed}, - "bamboo": {block: semanticsNamedBlock{"minecraft:bamboo"}, unsupported: true}, + "bamboo": {block: semanticsNamedBlock{"minecraft:bamboo"}}, "cobweb": {block: semanticsNamedBlock{"minecraft:web"}}, } { t.Run(name, func(t *testing.T) { got := DefaultMovementBlockSemantics(want.block) - if got.Bounce != want.bounce || got.Unsupported != want.unsupported { - t.Fatalf("semantics = %+v, want bounce=%v unsupported=%v", got, want.bounce, want.unsupported) + if got.Bounce != want.bounce { + t.Fatalf("semantics = %+v, want bounce=%v", got, want.bounce) } if name == "cobweb" && !got.Cobweb { t.Fatalf("expected cobweb semantics") @@ -119,7 +114,6 @@ type extendedMovementSemantics struct { climbable bool cobweb bool bounce MovementBounce - unsupported bool } func (s extendedMovementSemantics) BlockMovementSemantics(world.Block) MovementBlockSemantics { @@ -128,7 +122,6 @@ func (s extendedMovementSemantics) BlockMovementSemantics(world.Block) MovementB Climbable: s.climbable, Cobweb: s.cobweb, Bounce: s.bounce, - Unsupported: s.unsupported, } } @@ -139,17 +132,23 @@ func TestSimulatorCompleteBlockSemanticsProvider(t *testing.T) { climbable: true, cobweb: true, bounce: MovementBounceBed, - unsupported: true, }, } got := sim.blockMovementSemantics(block.Air{}) if got.GroundFriction != 0.37 || !got.Climbable || !got.Cobweb || - got.Bounce != MovementBounceBed || !got.Unsupported { + got.Bounce != MovementBounceBed { t.Fatalf("got incomplete semantic bundle: %+v", got) } } +func TestBambooDoesNotInvalidateSimulation(t *testing.T) { + sim := &Simulator{World: blockMovementWorld{b: block.Bamboo{}}} + if !sim.simulationIsReliable(newBaseState()) { + t.Fatal("bamboo should use ordinary collision simulation") + } +} + func TestSimulatorInvalidBlockSemanticsFrictionFallsBack(t *testing.T) { for name, friction := range map[string]float32{ "zero": 0, diff --git a/simulation.go b/simulation.go index 3a6f894..6a2f007 100644 --- a/simulation.go +++ b/simulation.go @@ -452,21 +452,6 @@ func (s *Simulator) simulationIsReliable(state *MovementState) bool { return true } - stateBB := state.BoundingBox(s.Options.UseSlideOffset) - isReliable := true - for _, b := range nearbyBlocks(stateBB.Grow(1), s.World) { - if _, isAir := b.(block.Air); isAir { - continue - } - if s.blockMovementSemantics(b).Unsupported { - isReliable = false - break - } - } - if !isReliable { - return false - } - if state.GameMode != packet.GameTypeSurvival && state.GameMode != packet.GameTypeAdventure { return false } From 84505784dca9835c1050c4aa24f4c54e2b73e9f8 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 4 Aug 2026 16:51:22 -0700 Subject: [PATCH 3/5] refactor: remove semantics aliases --- README.md | 8 +++---- block.go | 51 ----------------------------------------- block/bounce.go | 2 -- block/climbable.go | 1 - block/contact.go | 1 - block/friction.go | 2 -- block/ground.go | 3 --- block/semantics.go | 29 ++++++++--------------- block_semantics_test.go | 31 +++++++++++++------------ interfaces.go | 11 ++++----- liquid_test.go | 3 ++- simulation.go | 7 +++--- simulator.go | 11 +++++---- simulator_test.go | 17 +++++++------- 14 files changed, 54 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 5917d60..385fadd 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,10 @@ if result.NeedsCorrection { Set `BlockSemantics` when movement behavior must come from a per-world block registry or custom block data instead of bedsim's Dragonfly-backed defaults. The adapter implements `BlockMovementSemanticsProvider` and returns the full -`MovementBlockSemantics` bundle: ground friction, climbability, cobweb status, -and slime/bed bounce behavior. The built-in owners for these semantics live in -the `github.com/oomph-ac/bedsim/block` package. Custom friction values must be -finite and positive; invalid values fall back to Dragonfly defaults. +`block.MovementSemantics` bundle: ground friction, climbability, cobweb status, +and slime/bed bounce behavior. Built-in owners live in the +`github.com/oomph-ac/bedsim/block` package. Custom friction values must be +finite and positive; invalid values fall back to the built-in resolver. BedSim's semantics package does not mutate Dragonfly's registry. Applications own registry setup and must register any additional block implementations diff --git a/block.go b/block.go index f4d2b65..b348fc2 100644 --- a/block.go +++ b/block.go @@ -7,13 +7,8 @@ import ( "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" - movementblock "github.com/oomph-ac/bedsim/block" ) -// SoulSandGroundFrictionMultiplier is retained as a root-package alias for -// the owner constant in bedsim/block. -const SoulSandGroundFrictionMultiplier = movementblock.SoulGroundFrictionMultiplier - type blockNameKey struct { base, state uint64 } @@ -38,52 +33,6 @@ func BlockName(b world.Block) string { return stored.(string) } -// BlockFriction returns the friction of the block. -func BlockFriction(b world.Block) float32 { - return movementblock.Friction(b, BlockName(b)) -} - -// BlockGroundFriction returns the friction used by ordinary grounded travel. -// It is intentionally separate from BlockFriction: most blocks expose their -// ordinary friction directly, while soul sand and soul soil apply a movement -// specific adjustment in the native travel path. -func BlockGroundFriction(b world.Block) float32 { - return DefaultMovementBlockSemantics(b).GroundFriction -} - -// BlockClimbable returns whether the given block is climbable. -func BlockClimbable(b world.Block) bool { - return DefaultMovementBlockSemantics(b).Climbable -} - -// BlockCobweb reports whether the block applies the cobweb movement slowdown. -// "web" is the canonical Bedrock identifier; accepting "cobweb" as well -// keeps custom registries and older adapters interoperable. -func BlockCobweb(b world.Block) bool { - return DefaultMovementBlockSemantics(b).Cobweb -} - -// MovementBounce identifies the vanilla vertical response when an entity -// lands on a block. -type MovementBounce = movementblock.Bounce - -const ( - MovementBounceNone = movementblock.BounceNone - MovementBounceSlime = movementblock.BounceSlime - MovementBounceBed = movementblock.BounceBed -) - -// MovementBlockSemantics is the complete set of block properties consumed by -// the movement integrator. A custom registry must return all properties from -// one world-consistent snapshot; zero values mean ordinary block behaviour. -type MovementBlockSemantics = movementblock.MovementSemantics - -// DefaultMovementBlockSemantics resolves the vanilla movement properties for -// a block using the built-in Dragonfly-backed registry. -func DefaultMovementBlockSemantics(b world.Block) MovementBlockSemantics { - return movementblock.Resolve(b, BlockName(b), BlockFriction(b)) -} - // 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. diff --git a/block/bounce.go b/block/bounce.go index 5ff44ea..d228ae5 100644 --- a/block/bounce.go +++ b/block/bounce.go @@ -5,7 +5,6 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -// Slime owns slime's bounce response. type Slime struct{} func (Slime) Matches(b world.Block, name string) bool { @@ -21,7 +20,6 @@ func (Slime) Apply(s *MovementSemantics) { func (Slime) Friction() (float32, bool) { return 0.8, true } -// Bed owns the bed bounce response. type Bed struct{} func (Bed) Matches(b world.Block, name string) bool { diff --git a/block/climbable.go b/block/climbable.go index 20a1b3d..298ebfc 100644 --- a/block/climbable.go +++ b/block/climbable.go @@ -5,7 +5,6 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -// ClimbableBlock owns the ladder and vine movement predicate. type ClimbableBlock struct{} func (ClimbableBlock) Matches(b world.Block, name string) bool { diff --git a/block/contact.go b/block/contact.go index 5542b0a..3538e3f 100644 --- a/block/contact.go +++ b/block/contact.go @@ -2,7 +2,6 @@ package block import "github.com/df-mc/dragonfly/server/world" -// Cobweb owns the cobweb contact slowdown. type Cobweb struct{} func (Cobweb) Matches(_ world.Block, name string) bool { diff --git a/block/friction.go b/block/friction.go index 8ef01d9..9e5a250 100644 --- a/block/friction.go +++ b/block/friction.go @@ -2,8 +2,6 @@ package block import "github.com/df-mc/dragonfly/server/world" -// frictionBlock supplies a named fallback for blocks whose concrete -// implementation is not available to the caller. type frictionBlock struct { name string friction float32 diff --git a/block/ground.go b/block/ground.go index 71c82c6..94d2439 100644 --- a/block/ground.go +++ b/block/ground.go @@ -5,8 +5,6 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -// SoulSand owns the ground-friction adjustment shared by Soul Sand and its -// Soul Soil counterpart in vanilla movement. type SoulSand struct{} func (SoulSand) Matches(b world.Block, name string) bool { @@ -20,7 +18,6 @@ func (SoulSand) Apply(s *MovementSemantics) { s.GroundFriction *= SoulGroundFrictionMultiplier } -// SoulSoil owns the same ground-friction adjustment as Soul Sand. type SoulSoil struct{} func (SoulSoil) Matches(b world.Block, name string) bool { diff --git a/block/semantics.go b/block/semantics.go index 16ab0f6..118f295 100644 --- a/block/semantics.go +++ b/block/semantics.go @@ -1,8 +1,4 @@ -// Package block owns BedSim's built-in movement semantics for blocks. -// -// It deliberately does not register blocks in Dragonfly's world registry. -// Registry ownership belongs to the application because registration affects -// runtime IDs and must happen before the registry is finalized. +// Package block provides BedSim's built-in movement semantics. package block import ( @@ -10,12 +6,10 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -// SoulGroundFrictionMultiplier is the native ground-friction adjustment used -// by SoulSandBlock::calcGroundFriction when Soul Speed is not active. +// SoulGroundFrictionMultiplier is the vanilla soul-ground adjustment. const SoulGroundFrictionMultiplier float32 = 1.225000023841858 -// Bounce identifies the vanilla vertical response when an entity lands on a -// block. +// Bounce identifies a block's landing response. type Bounce uint8 const ( @@ -24,9 +18,7 @@ const ( BounceBed ) -// MovementSemantics is the movement behavior owned by one resolved block. -// GroundFriction is supplied by Resolve so frictional blocks and block-owned -// adjustments are composed in one place. +// MovementSemantics is the movement behavior resolved for a block. type MovementSemantics struct { GroundFriction float32 Climbable bool @@ -34,7 +26,7 @@ type MovementSemantics struct { Bounce Bounce } -// Owner contributes movement behavior for one family of blocks. +// Owner contributes movement behavior for a block family. type Owner interface { Matches(world.Block, string) bool Apply(*MovementSemantics) @@ -52,11 +44,9 @@ var owners = [...]Owner{ frictionBlock{name: "minecraft:blue_ice", friction: 0.99}, } -// Resolve returns the built-in movement semantics for b. name should be the -// caller's cached canonical block name; it lets custom block implementations -// participate without pretending to be Dragonfly concrete types. -func Resolve(b world.Block, name string, groundFriction float32) MovementSemantics { - semantics := MovementSemantics{GroundFriction: groundFriction} +// Resolve returns built-in movement semantics for b. +func Resolve(b world.Block, name string) MovementSemantics { + semantics := MovementSemantics{GroundFriction: Friction(b, name)} for _, owner := range owners { if owner.Matches(b, name) { owner.Apply(&semantics) @@ -65,8 +55,7 @@ func Resolve(b world.Block, name string, groundFriction float32) MovementSemanti return semantics } -// Friction returns the ordinary block friction before block-specific ground -// adjustments are applied. +// Friction returns a block's ordinary friction. func Friction(b world.Block, name string) float32 { if f, ok := b.(dfblock.Frictional); ok { return float32(f.Friction()) diff --git a/block_semantics_test.go b/block_semantics_test.go index 8edba95..5c2134d 100644 --- a/block_semantics_test.go +++ b/block_semantics_test.go @@ -8,17 +8,18 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" + movementblock "github.com/oomph-ac/bedsim/block" ) func TestBlockGroundFrictionSoulBlocks(t *testing.T) { - want := DefaultBlockFriction * SoulSandGroundFrictionMultiplier + want := DefaultBlockFriction * movementblock.SoulGroundFrictionMultiplier for name, b := range map[string]world.Block{ "soul sand": block.SoulSand{}, "soul soil": block.SoulSoil{}, } { t.Run(name, func(t *testing.T) { - if got := BlockGroundFriction(b); math.Abs(float64(got-want)) > 1e-6 { + if got := movementblock.Resolve(b, BlockName(b)).GroundFriction; math.Abs(float64(got-want)) > 1e-6 { t.Fatalf("ground friction = %.8f, want %.8f", got, want) } }) @@ -31,7 +32,7 @@ func TestDefaultMovementBlockSemantics(t *testing.T) { block world.Block climbable bool cobweb bool - bounce MovementBounce + bounce movementblock.Bounce groundWant float32 }{ { @@ -54,13 +55,13 @@ func TestDefaultMovementBlockSemantics(t *testing.T) { { name: "soul soil", block: block.SoulSoil{}, - groundWant: DefaultBlockFriction * SoulSandGroundFrictionMultiplier, + groundWant: DefaultBlockFriction * movementblock.SoulGroundFrictionMultiplier, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := DefaultMovementBlockSemantics(tt.block) + got := movementblock.Resolve(tt.block, BlockName(tt.block)) if math.Abs(float64(got.GroundFriction-tt.groundWant)) > 1e-6 { t.Fatalf("ground friction = %.8f, want %.8f", got.GroundFriction, tt.groundWant) } @@ -80,15 +81,15 @@ func TestDefaultMovementBlockSemantics(t *testing.T) { func TestDefaultMovementBlockSemanticsSpecialBlocks(t *testing.T) { for name, want := range map[string]struct { block world.Block - bounce MovementBounce + bounce movementblock.Bounce }{ - "slime": {block: semanticsNamedBlock{"minecraft:slime"}, bounce: MovementBounceSlime}, - "bed": {block: semanticsNamedBlock{"minecraft:bed"}, bounce: MovementBounceBed}, + "slime": {block: semanticsNamedBlock{"minecraft:slime"}, bounce: movementblock.BounceSlime}, + "bed": {block: semanticsNamedBlock{"minecraft:bed"}, bounce: movementblock.BounceBed}, "bamboo": {block: semanticsNamedBlock{"minecraft:bamboo"}}, "cobweb": {block: semanticsNamedBlock{"minecraft:web"}}, } { t.Run(name, func(t *testing.T) { - got := DefaultMovementBlockSemantics(want.block) + got := movementblock.Resolve(want.block, BlockName(want.block)) if got.Bounce != want.bounce { t.Fatalf("semantics = %+v, want bounce=%v", got, want.bounce) } @@ -113,11 +114,11 @@ type extendedMovementSemantics struct { groundFriction float32 climbable bool cobweb bool - bounce MovementBounce + bounce movementblock.Bounce } -func (s extendedMovementSemantics) BlockMovementSemantics(world.Block) MovementBlockSemantics { - return MovementBlockSemantics{ +func (s extendedMovementSemantics) BlockMovementSemantics(world.Block) movementblock.MovementSemantics { + return movementblock.MovementSemantics{ GroundFriction: s.groundFriction, Climbable: s.climbable, Cobweb: s.cobweb, @@ -131,13 +132,13 @@ func TestSimulatorCompleteBlockSemanticsProvider(t *testing.T) { groundFriction: 0.37, climbable: true, cobweb: true, - bounce: MovementBounceBed, + bounce: movementblock.BounceBed, }, } got := sim.blockMovementSemantics(block.Air{}) if got.GroundFriction != 0.37 || !got.Climbable || !got.Cobweb || - got.Bounce != MovementBounceBed { + got.Bounce != movementblock.BounceBed { t.Fatalf("got incomplete semantic bundle: %+v", got) } } @@ -203,7 +204,7 @@ func TestSimulateGroundUsesSoulSoilFriction(t *testing.T) { state.Impulse = mgl32.Vec2{0, 0.98} result := sim.SimulateState(state) - groundFriction := DefaultAirFriction * BlockGroundFriction(block.SoulSoil{}) + groundFriction := DefaultAirFriction * movementblock.Resolve(block.SoulSoil{}, BlockName(block.SoulSoil{})).GroundFriction moveRelativeSpeed := state.MovementSpeed * (0.16277136 / (groundFriction * groundFriction * groundFriction)) wantZ := 0.98 * moveRelativeSpeed * groundFriction diff --git a/interfaces.go b/interfaces.go index b8bd761..b545d7e 100644 --- a/interfaces.go +++ b/interfaces.go @@ -3,6 +3,7 @@ package bedsim import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" + "github.com/oomph-ac/bedsim/block" ) // WorldProvider bridges the world/chunk system for collision and block lookups. @@ -19,14 +20,10 @@ type LiquidProvider interface { Liquid(pos cube.Pos) (world.Liquid, bool) } -// BlockMovementSemanticsProvider resolves the complete movement-relevant -// behavior for a block. Implement this when block properties come from a -// per-world registry or custom block data instead of Dragonfly's defaults. -// -// GroundFriction is the post-adjustment value used by grounded travel. A -// non-positive or non-finite value falls back to bedsim's default resolution. +// BlockMovementSemanticsProvider resolves movement behavior from a custom +// world registry or block data. type BlockMovementSemanticsProvider interface { - BlockMovementSemantics(world.Block) MovementBlockSemantics + BlockMovementSemantics(world.Block) block.MovementSemantics } // DefaultBlockSemantics uses bedsim's built-in Dragonfly-backed block helpers. diff --git a/liquid_test.go b/liquid_test.go index 3ed1afe..3b75166 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -8,6 +8,7 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" + movementblock "github.com/oomph-ac/bedsim/block" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -1355,7 +1356,7 @@ func TestClimbUsesEffectiveJumping(t *testing.T) { t.Run(name, func(t *testing.T) { sim := newLiquidSim(newLiquidWorld()) sim.BlockSemantics = overrideBlockSemantics{ - semantics: MovementBlockSemantics{ + semantics: movementblock.MovementSemantics{ GroundFriction: DefaultBlockFriction, Climbable: true, }, diff --git a/simulation.go b/simulation.go index 6a2f007..3c08d27 100644 --- a/simulation.go +++ b/simulation.go @@ -8,6 +8,7 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" + movementblock "github.com/oomph-ac/bedsim/block" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -569,7 +570,7 @@ func (s *Simulator) walkOnBlock(state *MovementState, blockUnder world.Block) { oldVel := state.Vel newVel := state.Vel switch s.blockMovementSemantics(blockUnder).Bounce { - case MovementBounceSlime: + case movementblock.BounceSlime: yMov := math32.Abs(newVel.Y()) if yMov < 0.1 && !state.PressingSneak { d1 := 0.4 + yMov*0.2 @@ -590,12 +591,12 @@ func (s *Simulator) landOnBlock(state *MovementState, old mgl32.Vec3, blockUnder } switch s.blockMovementSemantics(blockUnder).Bounce { - case MovementBounceSlime: + case movementblock.BounceSlime: newVel[1] = SlimeBounceMultiplier * old.Y() if math32.Abs(newVel[1]) < 1e-4 { newVel[1] = 0.0 } - case MovementBounceBed: + case movementblock.BounceBed: newVel[1] = math32.Min(1.0, BedBounceMultiplier*old.Y()) default: newVel[1] = 0 diff --git a/simulator.go b/simulator.go index e4c59a8..e3d41d8 100644 --- a/simulator.go +++ b/simulator.go @@ -4,6 +4,7 @@ import ( "github.com/chewxy/math32" "github.com/df-mc/dragonfly/server/world" + "github.com/oomph-ac/bedsim/block" ) // SimulationMode defines how strict the simulator should be with client corrections. @@ -73,8 +74,8 @@ type Simulator struct { Options SimulationOptions } -func (DefaultBlockSemantics) BlockMovementSemantics(b world.Block) MovementBlockSemantics { - return DefaultMovementBlockSemantics(b) +func (DefaultBlockSemantics) BlockMovementSemantics(b world.Block) block.MovementSemantics { + return block.Resolve(b, BlockName(b)) } // swimWaterGraceTicks resolves the configured grace window: zero means the @@ -94,13 +95,13 @@ func validGroundFriction(friction float32) bool { return friction > 0 && !math32.IsInf(friction, 1) } -func (s *Simulator) blockMovementSemantics(b world.Block) MovementBlockSemantics { +func (s *Simulator) blockMovementSemantics(b world.Block) block.MovementSemantics { if s.BlockSemantics != nil { semantics := s.BlockSemantics.BlockMovementSemantics(b) if !validGroundFriction(semantics.GroundFriction) { - semantics.GroundFriction = DefaultMovementBlockSemantics(b).GroundFriction + semantics.GroundFriction = block.Resolve(b, BlockName(b)).GroundFriction } return semantics } - return DefaultMovementBlockSemantics(b) + return block.Resolve(b, BlockName(b)) } diff --git a/simulator_test.go b/simulator_test.go index 7aeb54b..6f100b3 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -10,6 +10,7 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" + movementblock "github.com/oomph-ac/bedsim/block" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -114,10 +115,10 @@ func (m mockInventory) HasElytra() bool { } type overrideBlockSemantics struct { - semantics MovementBlockSemantics + semantics movementblock.MovementSemantics } -func (s overrideBlockSemantics) BlockMovementSemantics(world.Block) MovementBlockSemantics { +func (s overrideBlockSemantics) BlockMovementSemantics(world.Block) movementblock.MovementSemantics { return s.semantics } @@ -331,11 +332,11 @@ func TestSimulatorBlockSemanticsOverridesDefaults(t *testing.T) { sim := &Simulator{ World: mockWorld{}, BlockSemantics: overrideBlockSemantics{ - semantics: MovementBlockSemantics{ + semantics: movementblock.MovementSemantics{ GroundFriction: 0.42, Climbable: true, Cobweb: true, - Bounce: MovementBounceBed, + Bounce: movementblock.BounceBed, }, }, } @@ -348,7 +349,7 @@ func TestSimulatorBlockSemanticsOverridesDefaults(t *testing.T) { if !got.Climbable { t.Fatalf("expected semantic climbable value") } - if !got.Cobweb || got.Bounce != MovementBounceBed { + if !got.Cobweb || got.Bounce != movementblock.BounceBed { t.Fatalf("expected complete semantic bundle, got %+v", got) } } @@ -358,7 +359,7 @@ func TestSimulatorDefaultBlockSemanticsFallback(t *testing.T) { sim := &Simulator{World: mockWorld{}} got := sim.blockMovementSemantics(b) - want := DefaultMovementBlockSemantics(b) + want := movementblock.Resolve(b, BlockName(b)) if got != want { t.Fatalf("expected default movement semantics %+v, got %+v", want, got) } @@ -366,7 +367,7 @@ func TestSimulatorDefaultBlockSemanticsFallback(t *testing.T) { func TestSimulatorInvalidBlockSemanticsFrictionFallsBackToDefault(t *testing.T) { b := block.Air{} - want := BlockFriction(b) + want := movementblock.Friction(b, BlockName(b)) tests := []struct { name string @@ -384,7 +385,7 @@ func TestSimulatorInvalidBlockSemanticsFrictionFallsBackToDefault(t *testing.T) sim := &Simulator{ World: mockWorld{}, BlockSemantics: overrideBlockSemantics{ - semantics: MovementBlockSemantics{GroundFriction: tt.friction}, + semantics: movementblock.MovementSemantics{GroundFriction: tt.friction}, }, } if got := sim.blockMovementSemantics(b).GroundFriction; got != want { From d648af2924d69825cdc670fcbe19387ff66775cd Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Thu, 6 Aug 2026 15:52:19 -0400 Subject: [PATCH 4/5] fix: preserve native block movement semantics --- README.md | 11 ++-- block/climbable.go | 2 +- block/ground.go | 15 +---- block/semantics.go | 24 ++++--- block_semantics_test.go | 142 ++++++++++++++++++++++------------------ simulation.go | 8 +-- simulator.go | 18 ++++- simulator_test.go | 18 +++++ 8 files changed, 143 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 385fadd..7a76e9f 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,13 @@ if result.NeedsCorrection { Set `BlockSemantics` when movement behavior must come from a per-world block registry or custom block data instead of bedsim's Dragonfly-backed defaults. The adapter implements `BlockMovementSemanticsProvider` and returns the full -`block.MovementSemantics` bundle: ground friction, climbability, cobweb status, -and slime/bed bounce behavior. Built-in owners live in the -`github.com/oomph-ac/bedsim/block` package. Custom friction values must be -finite and positive; invalid values fall back to the built-in resolver. +`block.MovementSemantics` bundle: ground friction, any acceleration-only +friction multiplier, climbability, cobweb status, and slime/bed bounce +behavior. Built-in owners live in the +`github.com/oomph-ac/bedsim/block` package. Custom ground friction must be +finite and positive; an invalid value falls back to the built-in resolver. An +invalid acceleration multiplier likewise falls back to the built-in block +semantics. BedSim's semantics package does not mutate Dragonfly's registry. Applications own registry setup and must register any additional block implementations diff --git a/block/climbable.go b/block/climbable.go index 298ebfc..64a5341 100644 --- a/block/climbable.go +++ b/block/climbable.go @@ -13,7 +13,7 @@ func (ClimbableBlock) Matches(b world.Block, name string) bool { } switch name { - case "minecraft:vine", "minecraft:cave_vines", "minecraft:cave_vines_body_with_berries", "minecraft:cave_vines_head_with_berries", + case "minecraft:ladder", "minecraft:vine", "minecraft:cave_vines", "minecraft:cave_vines_body_with_berries", "minecraft:cave_vines_head_with_berries", "minecraft:twisting_vines", "minecraft:weeping_vines": return true default: diff --git a/block/ground.go b/block/ground.go index 94d2439..441a40d 100644 --- a/block/ground.go +++ b/block/ground.go @@ -15,18 +15,5 @@ func (SoulSand) Matches(b world.Block, name string) bool { } func (SoulSand) Apply(s *MovementSemantics) { - s.GroundFriction *= SoulGroundFrictionMultiplier -} - -type SoulSoil struct{} - -func (SoulSoil) Matches(b world.Block, name string) bool { - if _, ok := b.(dfblock.SoulSoil); ok { - return true - } - return name == "minecraft:soul_soil" -} - -func (SoulSoil) Apply(s *MovementSemantics) { - s.GroundFriction *= SoulGroundFrictionMultiplier + s.GroundAccelerationFrictionMultiplier *= SoulSandAccelerationFrictionMultiplier } diff --git a/block/semantics.go b/block/semantics.go index 118f295..c0b8f38 100644 --- a/block/semantics.go +++ b/block/semantics.go @@ -6,8 +6,11 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -// SoulGroundFrictionMultiplier is the vanilla soul-ground adjustment. -const SoulGroundFrictionMultiplier float32 = 1.225000023841858 +// SoulSandAccelerationFrictionMultiplier is the native adjustment used when +// calculating grounded acceleration on soul sand. Ordinary drag still uses the +// block's unadjusted friction. This exact native value replaces the legacy +// 0.543 speed approximation. +const SoulSandAccelerationFrictionMultiplier float32 = 1.225000023841858 // Bounce identifies a block's landing response. type Bounce uint8 @@ -20,10 +23,11 @@ const ( // MovementSemantics is the movement behavior resolved for a block. type MovementSemantics struct { - GroundFriction float32 - Climbable bool - Cobweb bool - Bounce Bounce + GroundFriction float32 + GroundAccelerationFrictionMultiplier float32 + Climbable bool + Cobweb bool + Bounce Bounce } // Owner contributes movement behavior for a block family. @@ -34,19 +38,21 @@ type Owner interface { var owners = [...]Owner{ SoulSand{}, - SoulSoil{}, ClimbableBlock{}, Cobweb{}, Slime{}, Bed{}, frictionBlock{name: "minecraft:ice", friction: 0.98}, frictionBlock{name: "minecraft:packed_ice", friction: 0.98}, - frictionBlock{name: "minecraft:blue_ice", friction: 0.99}, + frictionBlock{name: "minecraft:blue_ice", friction: 0.989}, } // Resolve returns built-in movement semantics for b. func Resolve(b world.Block, name string) MovementSemantics { - semantics := MovementSemantics{GroundFriction: Friction(b, name)} + semantics := MovementSemantics{ + GroundFriction: Friction(b, name), + GroundAccelerationFrictionMultiplier: 1, + } for _, owner := range owners { if owner.Matches(b, name) { owner.Apply(&semantics) diff --git a/block_semantics_test.go b/block_semantics_test.go index 5c2134d..6ede0ce 100644 --- a/block_semantics_test.go +++ b/block_semantics_test.go @@ -11,16 +11,21 @@ import ( movementblock "github.com/oomph-ac/bedsim/block" ) -func TestBlockGroundFrictionSoulBlocks(t *testing.T) { - want := DefaultBlockFriction * movementblock.SoulGroundFrictionMultiplier - - for name, b := range map[string]world.Block{ - "soul sand": block.SoulSand{}, - "soul soil": block.SoulSoil{}, +func TestSoulBlocksKeepOrdinaryGroundFriction(t *testing.T) { + for name, tt := range map[string]struct { + block world.Block + accelScale float32 + }{ + "soul sand": {block: block.SoulSand{}, accelScale: movementblock.SoulSandAccelerationFrictionMultiplier}, + "soul soil": {block: block.SoulSoil{}, accelScale: 1}, } { t.Run(name, func(t *testing.T) { - if got := movementblock.Resolve(b, BlockName(b)).GroundFriction; math.Abs(float64(got-want)) > 1e-6 { - t.Fatalf("ground friction = %.8f, want %.8f", got, want) + got := movementblock.Resolve(tt.block, BlockName(tt.block)) + if got.GroundFriction != DefaultBlockFriction { + t.Fatalf("ground friction = %.8f, want %.8f", got.GroundFriction, DefaultBlockFriction) + } + if got.GroundAccelerationFrictionMultiplier != tt.accelScale { + t.Fatalf("ground acceleration friction multiplier = %.8f, want %.8f", got.GroundAccelerationFrictionMultiplier, tt.accelScale) } }) } @@ -46,6 +51,12 @@ func TestDefaultMovementBlockSemantics(t *testing.T) { climbable: true, groundWant: DefaultBlockFriction, }, + { + name: "registry-backed ladder", + block: semanticsNamedBlock{"minecraft:ladder"}, + climbable: true, + groundWant: DefaultBlockFriction, + }, { name: "vines", block: block.Vines{}, @@ -55,7 +66,7 @@ func TestDefaultMovementBlockSemantics(t *testing.T) { { name: "soul soil", block: block.SoulSoil{}, - groundWant: DefaultBlockFriction * movementblock.SoulGroundFrictionMultiplier, + groundWant: DefaultBlockFriction, }, } @@ -78,6 +89,19 @@ func TestDefaultMovementBlockSemantics(t *testing.T) { } } +func TestBlueIceFrictionMatchesAcrossBlockRepresentations(t *testing.T) { + for name, b := range map[string]world.Block{ + "dragonfly": block.BlueIce{}, + "registry-backed": semanticsNamedBlock{"minecraft:blue_ice"}, + } { + t.Run(name, func(t *testing.T) { + if got := movementblock.Resolve(b, BlockName(b)).GroundFriction; got != 0.989 { + t.Fatalf("blue ice friction = %.8f, want 0.989", got) + } + }) + } +} + func TestDefaultMovementBlockSemanticsSpecialBlocks(t *testing.T) { for name, want := range map[string]struct { block world.Block @@ -111,33 +135,36 @@ func (b semanticsNamedBlock) EncodeBlock() (string, map[string]any) { func (b semanticsNamedBlock) Model() world.BlockModel { return block.Air{}.Model() } type extendedMovementSemantics struct { - groundFriction float32 - climbable bool - cobweb bool - bounce movementblock.Bounce + groundFriction float32 + groundAccelerationFrictionMultiplier float32 + climbable bool + cobweb bool + bounce movementblock.Bounce } func (s extendedMovementSemantics) BlockMovementSemantics(world.Block) movementblock.MovementSemantics { return movementblock.MovementSemantics{ - GroundFriction: s.groundFriction, - Climbable: s.climbable, - Cobweb: s.cobweb, - Bounce: s.bounce, + GroundFriction: s.groundFriction, + GroundAccelerationFrictionMultiplier: s.groundAccelerationFrictionMultiplier, + Climbable: s.climbable, + Cobweb: s.cobweb, + Bounce: s.bounce, } } func TestSimulatorCompleteBlockSemanticsProvider(t *testing.T) { sim := &Simulator{ BlockSemantics: extendedMovementSemantics{ - groundFriction: 0.37, - climbable: true, - cobweb: true, - bounce: movementblock.BounceBed, + groundFriction: 0.37, + groundAccelerationFrictionMultiplier: 1.25, + climbable: true, + cobweb: true, + bounce: movementblock.BounceBed, }, } got := sim.blockMovementSemantics(block.Air{}) - if got.GroundFriction != 0.37 || !got.Climbable || !got.Cobweb || + if got.GroundFriction != 0.37 || got.GroundAccelerationFrictionMultiplier != 1.25 || !got.Climbable || !got.Cobweb || got.Bounce != movementblock.BounceBed { t.Fatalf("got incomplete semantic bundle: %+v", got) } @@ -150,26 +177,6 @@ func TestBambooDoesNotInvalidateSimulation(t *testing.T) { } } -func TestSimulatorInvalidBlockSemanticsFrictionFallsBack(t *testing.T) { - for name, friction := range map[string]float32{ - "zero": 0, - "negative": -0.42, - "nan": float32(math.NaN()), - "positive infinity": float32(math.Inf(1)), - "negative infinity": float32(math.Inf(-1)), - } { - t.Run(name, func(t *testing.T) { - sim := &Simulator{ - BlockSemantics: extendedMovementSemantics{groundFriction: friction}, - } - got := sim.blockMovementSemantics(block.Air{}).GroundFriction - if got != DefaultBlockFriction { - t.Fatalf("ground friction = %v, want %v", got, DefaultBlockFriction) - } - }) - } -} - type blockMovementWorld struct { b world.Block } @@ -190,26 +197,37 @@ func (blockMovementWorld) IsChunkLoaded(int32, int32) bool { return true } -func TestSimulateGroundUsesSoulSoilFriction(t *testing.T) { - sim := &Simulator{ - World: blockMovementWorld{b: block.SoulSoil{}}, - Effects: mockEffects{}, - } +func TestSimulateSoulGroundSeparatesAccelerationFromDrag(t *testing.T) { + for name, tt := range map[string]struct { + block world.Block + accelerationFrictionFactor float32 + }{ + "soul sand": {block: block.SoulSand{}, accelerationFrictionFactor: 1.225000023841858}, + "soul soil": {block: block.SoulSoil{}, accelerationFrictionFactor: 1}, + } { + t.Run(name, func(t *testing.T) { + sim := &Simulator{ + World: blockMovementWorld{b: tt.block}, + Effects: mockEffects{}, + } - state := newBaseState() - state.Pos = mgl32.Vec3{0, 1, 0} - state.Client.Pos = state.Pos - state.OnGround = true - state.HasGravity = false - state.Impulse = mgl32.Vec2{0, 0.98} - - result := sim.SimulateState(state) - groundFriction := DefaultAirFriction * movementblock.Resolve(block.SoulSoil{}, BlockName(block.SoulSoil{})).GroundFriction - moveRelativeSpeed := state.MovementSpeed * - (0.16277136 / (groundFriction * groundFriction * groundFriction)) - wantZ := 0.98 * moveRelativeSpeed * groundFriction - - if math.Abs(float64(result.Velocity.Z()-wantZ)) > 1e-5 { - t.Fatalf("ground velocity Z = %.8f, want %.8f", result.Velocity.Z(), wantZ) + state := newBaseState() + state.Pos = mgl32.Vec3{0, 1, 0} + state.Client.Pos = state.Pos + state.OnGround = true + state.HasGravity = false + state.Impulse = mgl32.Vec2{0, 0.98} + + result := sim.SimulateState(state) + groundFriction := DefaultAirFriction * DefaultBlockFriction + accelerationFriction := groundFriction * tt.accelerationFrictionFactor + moveRelativeSpeed := state.MovementSpeed * + (0.16277136 / (accelerationFriction * accelerationFriction * accelerationFriction)) + wantZ := 0.98 * moveRelativeSpeed * groundFriction + + if math.Abs(float64(result.Velocity.Z()-wantZ)) > 1e-5 { + t.Fatalf("ground velocity Z = %.8f, want %.8f", result.Velocity.Z(), wantZ) + } + }) } } diff --git a/simulation.go b/simulation.go index 3c08d27..7595772 100644 --- a/simulation.go +++ b/simulation.go @@ -340,7 +340,8 @@ func (s *Simulator) simulateMovement(state *MovementState) { mSpeed := state.MovementSpeed blockSemantics := s.blockMovementSemantics(blockUnder) blockFriction *= blockSemantics.GroundFriction - moveRelativeSpeed = mSpeed * (0.16277136 / (blockFriction * blockFriction * blockFriction)) + accelerationFriction := blockFriction * blockSemantics.GroundAccelerationFrictionMultiplier + moveRelativeSpeed = mSpeed * (0.16277136 / (accelerationFriction * accelerationFriction * accelerationFriction)) } if state.Gliding { @@ -998,11 +999,10 @@ func (s *Simulator) isInsideCobweb(state *MovementState) bool { if _, isAir := b.(block.Air); isAir { continue } - if !s.blockMovementSemantics(b).Cobweb { + if !bb.IntersectsWith(cube.Box32(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))) { continue } - - if bb.IntersectsWith(cube.Box32(0, 0, 0, 1, 1, 1).Translate(posVec3(pos))) { + if s.blockMovementSemantics(b).Cobweb { insideCobweb = true } if insideCobweb { diff --git a/simulator.go b/simulator.go index e3d41d8..d531b43 100644 --- a/simulator.go +++ b/simulator.go @@ -95,11 +95,27 @@ func validGroundFriction(friction float32) bool { return friction > 0 && !math32.IsInf(friction, 1) } +func validGroundAccelerationFrictionMultiplier(multiplier float32) bool { + return multiplier > 0 && !math32.IsInf(multiplier, 1) +} + func (s *Simulator) blockMovementSemantics(b world.Block) block.MovementSemantics { if s.BlockSemantics != nil { semantics := s.BlockSemantics.BlockMovementSemantics(b) + var fallback block.MovementSemantics + resolvedFallback := false + resolveFallback := func() block.MovementSemantics { + if !resolvedFallback { + fallback = block.Resolve(b, BlockName(b)) + resolvedFallback = true + } + return fallback + } if !validGroundFriction(semantics.GroundFriction) { - semantics.GroundFriction = block.Resolve(b, BlockName(b)).GroundFriction + semantics.GroundFriction = resolveFallback().GroundFriction + } + if !validGroundAccelerationFrictionMultiplier(semantics.GroundAccelerationFrictionMultiplier) { + semantics.GroundAccelerationFrictionMultiplier = resolveFallback().GroundAccelerationFrictionMultiplier } return semantics } diff --git a/simulator_test.go b/simulator_test.go index 6f100b3..08ee041 100644 --- a/simulator_test.go +++ b/simulator_test.go @@ -395,6 +395,24 @@ func TestSimulatorInvalidBlockSemanticsFrictionFallsBackToDefault(t *testing.T) } } +func TestSimulatorInvalidAccelerationMultiplierFallsBackToBuiltIn(t *testing.T) { + b := block.SoulSand{} + want := movementblock.Resolve(b, BlockName(b)).GroundAccelerationFrictionMultiplier + sim := &Simulator{ + World: mockWorld{}, + BlockSemantics: overrideBlockSemantics{ + semantics: movementblock.MovementSemantics{ + GroundFriction: DefaultBlockFriction, + GroundAccelerationFrictionMultiplier: 0, + }, + }, + } + + if got := sim.blockMovementSemantics(b).GroundAccelerationFrictionMultiplier; got != want { + t.Fatalf("expected invalid acceleration multiplier to fall back to %v, got %v", want, got) + } +} + func TestSimulateStateOutcomeUnloadedChunk(t *testing.T) { sim := &Simulator{ World: staticWorld{chunkLoaded: false}, From e2e9cd1a2787b00baf499141abbe8e81d71cb336 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Thu, 6 Aug 2026 16:19:32 -0400 Subject: [PATCH 5/5] refactor: simplify block semantics resolution --- README.md | 2 +- block/bounce.go | 18 +++++++------ block/climbable.go | 6 ++--- block/contact.go | 6 ++--- block/friction.go | 9 ++++--- block/ground.go | 6 ++--- block/semantics.go | 60 ++++++++++++++++++++--------------------- block/semantics_test.go | 49 +++++++++++++++++++++++++++++++++ interfaces.go | 7 +++-- 9 files changed, 109 insertions(+), 54 deletions(-) create mode 100644 block/semantics_test.go diff --git a/README.md b/README.md index 7a76e9f..609f9d4 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ registry or custom block data instead of bedsim's Dragonfly-backed defaults. The adapter implements `BlockMovementSemanticsProvider` and returns the full `block.MovementSemantics` bundle: ground friction, any acceleration-only friction multiplier, climbability, cobweb status, and slime/bed bounce -behavior. Built-in owners live in the +behavior. Built-in rules live in the `github.com/oomph-ac/bedsim/block` package. Custom ground friction must be finite and positive; an invalid value falls back to the built-in resolver. An invalid acceleration multiplier likewise falls back to the built-in block diff --git a/block/bounce.go b/block/bounce.go index d228ae5..6d68f6f 100644 --- a/block/bounce.go +++ b/block/bounce.go @@ -5,30 +5,32 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -type Slime struct{} +type slime struct{} -func (Slime) Matches(b world.Block, name string) bool { +func (slime) Matches(b world.Block, name string) bool { if _, ok := b.(dfblock.Slime); ok { return true } return name == "minecraft:slime" } -func (Slime) Apply(s *MovementSemantics) { +func (slime) Apply(s *resolution) { + if !s.groundFrictionSet { + s.GroundFriction = 0.8 + s.groundFrictionSet = true + } s.Bounce = BounceSlime } -func (Slime) Friction() (float32, bool) { return 0.8, true } - -type Bed struct{} +type bed struct{} -func (Bed) Matches(b world.Block, name string) bool { +func (bed) Matches(b world.Block, name string) bool { if _, ok := b.(dfblock.Bed); ok { return true } return name == "minecraft:bed" } -func (Bed) Apply(s *MovementSemantics) { +func (bed) Apply(s *resolution) { s.Bounce = BounceBed } diff --git a/block/climbable.go b/block/climbable.go index 64a5341..5bb0f9b 100644 --- a/block/climbable.go +++ b/block/climbable.go @@ -5,9 +5,9 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -type ClimbableBlock struct{} +type climbableBlock struct{} -func (ClimbableBlock) Matches(b world.Block, name string) bool { +func (climbableBlock) Matches(b world.Block, name string) bool { if _, ok := b.(dfblock.Ladder); ok { return true } @@ -21,6 +21,6 @@ func (ClimbableBlock) Matches(b world.Block, name string) bool { } } -func (ClimbableBlock) Apply(s *MovementSemantics) { +func (climbableBlock) Apply(s *resolution) { s.Climbable = true } diff --git a/block/contact.go b/block/contact.go index 3538e3f..6eb6d96 100644 --- a/block/contact.go +++ b/block/contact.go @@ -2,12 +2,12 @@ package block import "github.com/df-mc/dragonfly/server/world" -type Cobweb struct{} +type cobweb struct{} -func (Cobweb) Matches(_ world.Block, name string) bool { +func (cobweb) Matches(_ world.Block, name string) bool { return name == "minecraft:web" || name == "minecraft:cobweb" } -func (Cobweb) Apply(s *MovementSemantics) { +func (cobweb) Apply(s *resolution) { s.Cobweb = true } diff --git a/block/friction.go b/block/friction.go index 9e5a250..395fa33 100644 --- a/block/friction.go +++ b/block/friction.go @@ -11,8 +11,9 @@ func (b frictionBlock) Matches(_ world.Block, name string) bool { return name == b.name } -func (frictionBlock) Apply(*MovementSemantics) {} - -func (b frictionBlock) Friction() (float32, bool) { - return b.friction, true +func (b frictionBlock) Apply(s *resolution) { + if !s.groundFrictionSet { + s.GroundFriction = b.friction + s.groundFrictionSet = true + } } diff --git a/block/ground.go b/block/ground.go index 441a40d..06657bf 100644 --- a/block/ground.go +++ b/block/ground.go @@ -5,15 +5,15 @@ import ( "github.com/df-mc/dragonfly/server/world" ) -type SoulSand struct{} +type soulSand struct{} -func (SoulSand) Matches(b world.Block, name string) bool { +func (soulSand) Matches(b world.Block, name string) bool { if _, ok := b.(dfblock.SoulSand); ok { return true } return name == "minecraft:soul_sand" } -func (SoulSand) Apply(s *MovementSemantics) { +func (soulSand) Apply(s *resolution) { s.GroundAccelerationFrictionMultiplier *= SoulSandAccelerationFrictionMultiplier } diff --git a/block/semantics.go b/block/semantics.go index c0b8f38..575f86c 100644 --- a/block/semantics.go +++ b/block/semantics.go @@ -30,18 +30,23 @@ type MovementSemantics struct { Bounce Bounce } -// Owner contributes movement behavior for a block family. -type Owner interface { +// rule contributes movement behavior for a block family. +type rule interface { Matches(world.Block, string) bool - Apply(*MovementSemantics) + Apply(*resolution) } -var owners = [...]Owner{ - SoulSand{}, - ClimbableBlock{}, - Cobweb{}, - Slime{}, - Bed{}, +type resolution struct { + MovementSemantics + groundFrictionSet bool +} + +var rules = [...]rule{ + soulSand{}, + climbableBlock{}, + cobweb{}, + slime{}, + bed{}, frictionBlock{name: "minecraft:ice", friction: 0.98}, frictionBlock{name: "minecraft:packed_ice", friction: 0.98}, frictionBlock{name: "minecraft:blue_ice", friction: 0.989}, @@ -49,32 +54,27 @@ var owners = [...]Owner{ // Resolve returns built-in movement semantics for b. func Resolve(b world.Block, name string) MovementSemantics { - semantics := MovementSemantics{ - GroundFriction: Friction(b, name), - GroundAccelerationFrictionMultiplier: 1, + result := resolution{ + MovementSemantics: MovementSemantics{ + GroundAccelerationFrictionMultiplier: 1, + }, + } + if f, ok := b.(dfblock.Frictional); ok { + result.GroundFriction = float32(f.Friction()) + result.groundFrictionSet = true } - for _, owner := range owners { - if owner.Matches(b, name) { - owner.Apply(&semantics) + for _, rule := range rules { + if rule.Matches(b, name) { + rule.Apply(&result) } } - return semantics + if !result.groundFrictionSet { + result.GroundFriction = 0.6 + } + return result.MovementSemantics } // Friction returns a block's ordinary friction. func Friction(b world.Block, name string) float32 { - if f, ok := b.(dfblock.Frictional); ok { - return float32(f.Friction()) - } - - for _, owner := range owners { - frictionOwner, ok := owner.(interface{ Friction() (float32, bool) }) - if !ok || !owner.Matches(b, name) { - continue - } - if friction, ok := frictionOwner.Friction(); ok { - return friction - } - } - return 0.6 + return Resolve(b, name).GroundFriction } diff --git a/block/semantics_test.go b/block/semantics_test.go new file mode 100644 index 0000000..de9983c --- /dev/null +++ b/block/semantics_test.go @@ -0,0 +1,49 @@ +package block + +import ( + "testing" + + "github.com/df-mc/dragonfly/server/world" +) + +type countingRule struct { + matches int +} + +func (r *countingRule) Matches(world.Block, string) bool { + r.matches++ + return true +} + +func (*countingRule) Apply(s *resolution) { + s.GroundFriction = 0.7 + s.groundFrictionSet = true +} + +func TestResolveVisitsEachRuleOnce(t *testing.T) { + original := rules[0] + rule := &countingRule{} + rules[0] = rule + t.Cleanup(func() { rules[0] = original }) + + Resolve(namedBlock{"minecraft:test"}, "minecraft:test") + if rule.matches != 1 { + t.Fatalf("rule matches called %d times, want 1", rule.matches) + } +} + +type namedBlock struct { + name string +} + +func (b namedBlock) EncodeBlock() (string, map[string]any) { + return b.name, nil +} + +func (namedBlock) Hash() (uint64, uint64) { + return 0, 0 +} + +func (namedBlock) Model() world.BlockModel { + return nil +} diff --git a/interfaces.go b/interfaces.go index b545d7e..417a561 100644 --- a/interfaces.go +++ b/interfaces.go @@ -20,8 +20,11 @@ type LiquidProvider interface { Liquid(pos cube.Pos) (world.Liquid, bool) } -// BlockMovementSemanticsProvider resolves movement behavior from a custom -// world registry or block data. +// BlockMovementSemanticsProvider resolves the complete movement behavior for a +// block from a custom world registry or block data. GroundFriction and +// GroundAccelerationFrictionMultiplier must be finite and positive; invalid +// values fall back to BedSim's built-in semantics. Boolean and bounce values are +// used as returned, including their zero values. type BlockMovementSemanticsProvider interface { BlockMovementSemantics(world.Block) block.MovementSemantics }