diff --git a/README.md b/README.md index af9e84c..609f9d4 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,18 @@ 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 +`block.MovementSemantics` bundle: ground friction, any acceleration-only +friction multiplier, climbability, cobweb status, and slime/bed bounce +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 +semantics. + +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 d39d846..b348fc2 100644 --- a/block.go +++ b/block.go @@ -33,40 +33,6 @@ func BlockName(b world.Block) string { return stored.(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 - } -} - -// 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 - } -} - // 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 new file mode 100644 index 0000000..6d68f6f --- /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" +) + +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 *resolution) { + if !s.groundFrictionSet { + s.GroundFriction = 0.8 + s.groundFrictionSet = true + } + s.Bounce = BounceSlime +} + +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 *resolution) { + s.Bounce = BounceBed +} diff --git a/block/climbable.go b/block/climbable.go new file mode 100644 index 0000000..5bb0f9b --- /dev/null +++ b/block/climbable.go @@ -0,0 +1,26 @@ +package block + +import ( + dfblock "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/world" +) + +type climbableBlock struct{} + +func (climbableBlock) Matches(b world.Block, name string) bool { + if _, ok := b.(dfblock.Ladder); ok { + return true + } + + switch name { + 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: + return false + } +} + +func (climbableBlock) Apply(s *resolution) { + s.Climbable = true +} diff --git a/block/contact.go b/block/contact.go new file mode 100644 index 0000000..6eb6d96 --- /dev/null +++ b/block/contact.go @@ -0,0 +1,13 @@ +package block + +import "github.com/df-mc/dragonfly/server/world" + +type cobweb struct{} + +func (cobweb) Matches(_ world.Block, name string) bool { + return name == "minecraft:web" || name == "minecraft:cobweb" +} + +func (cobweb) Apply(s *resolution) { + s.Cobweb = true +} diff --git a/block/friction.go b/block/friction.go new file mode 100644 index 0000000..395fa33 --- /dev/null +++ b/block/friction.go @@ -0,0 +1,19 @@ +package block + +import "github.com/df-mc/dragonfly/server/world" + +type frictionBlock struct { + name string + friction float32 +} + +func (b frictionBlock) Matches(_ world.Block, name string) bool { + return name == b.name +} + +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 new file mode 100644 index 0000000..06657bf --- /dev/null +++ b/block/ground.go @@ -0,0 +1,19 @@ +package block + +import ( + dfblock "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/world" +) + +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 *resolution) { + s.GroundAccelerationFrictionMultiplier *= SoulSandAccelerationFrictionMultiplier +} diff --git a/block/semantics.go b/block/semantics.go new file mode 100644 index 0000000..575f86c --- /dev/null +++ b/block/semantics.go @@ -0,0 +1,80 @@ +// Package block provides BedSim's built-in movement semantics. +package block + +import ( + dfblock "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/world" +) + +// 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 + +const ( + BounceNone Bounce = iota + BounceSlime + BounceBed +) + +// MovementSemantics is the movement behavior resolved for a block. +type MovementSemantics struct { + GroundFriction float32 + GroundAccelerationFrictionMultiplier float32 + Climbable bool + Cobweb bool + Bounce Bounce +} + +// rule contributes movement behavior for a block family. +type rule interface { + Matches(world.Block, string) bool + Apply(*resolution) +} + +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}, +} + +// Resolve returns built-in movement semantics for b. +func Resolve(b world.Block, name string) MovementSemantics { + result := resolution{ + MovementSemantics: MovementSemantics{ + GroundAccelerationFrictionMultiplier: 1, + }, + } + if f, ok := b.(dfblock.Frictional); ok { + result.GroundFriction = float32(f.Friction()) + result.groundFrictionSet = true + } + for _, rule := range rules { + if rule.Matches(b, name) { + rule.Apply(&result) + } + } + 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 { + 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/block_semantics_test.go b/block_semantics_test.go new file mode 100644 index 0000000..6ede0ce --- /dev/null +++ b/block_semantics_test.go @@ -0,0 +1,233 @@ +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" + movementblock "github.com/oomph-ac/bedsim/block" +) + +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) { + 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) + } + }) + } +} + +func TestDefaultMovementBlockSemantics(t *testing.T) { + tests := []struct { + name string + block world.Block + climbable bool + cobweb bool + bounce movementblock.Bounce + groundWant float32 + }{ + { + name: "air", + block: block.Air{}, + groundWant: DefaultBlockFriction, + }, + { + name: "ladder", + block: block.Ladder{}, + climbable: true, + groundWant: DefaultBlockFriction, + }, + { + name: "registry-backed ladder", + block: semanticsNamedBlock{"minecraft:ladder"}, + climbable: true, + groundWant: DefaultBlockFriction, + }, + { + name: "vines", + block: block.Vines{}, + climbable: true, + groundWant: DefaultBlockFriction, + }, + { + name: "soul soil", + block: block.SoulSoil{}, + groundWant: DefaultBlockFriction, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + 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) + } + 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) + } + }) + } +} + +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 + bounce movementblock.Bounce + }{ + "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 := movementblock.Resolve(want.block, BlockName(want.block)) + 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") + } + }) + } +} + +// 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 + groundAccelerationFrictionMultiplier float32 + climbable bool + cobweb bool + bounce movementblock.Bounce +} + +func (s extendedMovementSemantics) BlockMovementSemantics(world.Block) movementblock.MovementSemantics { + return movementblock.MovementSemantics{ + 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, + groundAccelerationFrictionMultiplier: 1.25, + climbable: true, + cobweb: true, + bounce: movementblock.BounceBed, + }, + } + + got := sim.blockMovementSemantics(block.Air{}) + 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) + } +} + +func TestBambooDoesNotInvalidateSimulation(t *testing.T) { + sim := &Simulator{World: blockMovementWorld{b: block.Bamboo{}}} + if !sim.simulationIsReliable(newBaseState()) { + t.Fatal("bamboo should use ordinary collision simulation") + } +} + +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 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 * 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/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..417a561 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,13 +20,13 @@ 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 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 } // DefaultBlockSemantics uses bedsim's built-in Dragonfly-backed block helpers. diff --git a/liquid_test.go b/liquid_test.go index fc86bf5..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,9 +1356,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: movementblock.MovementSemantics{ + GroundFriction: DefaultBlockFriction, + Climbable: true, + }, } state := submergedState() apply(state) diff --git a/simulation.go b/simulation.go index d6427dd..7595772 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" ) @@ -337,11 +338,10 @@ 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) - moveRelativeSpeed = mSpeed * (0.16277136 / (blockFriction * blockFriction * blockFriction)) + blockSemantics := s.blockMovementSemantics(blockUnder) + blockFriction *= blockSemantics.GroundFriction + accelerationFriction := blockFriction * blockSemantics.GroundAccelerationFrictionMultiplier + moveRelativeSpeed = mSpeed * (0.16277136 / (accelerationFriction * accelerationFriction * accelerationFriction)) } if state.Gliding { @@ -364,12 +364,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 @@ -454,21 +454,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.blockName(b) == "minecraft:bamboo" { - isReliable = false - break - } - } - if !isReliable { - return false - } - if state.GameMode != packet.GameTypeSurvival && state.GameMode != packet.GameTypeAdventure { return false } @@ -585,8 +570,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 movementblock.BounceSlime: yMov := math32.Abs(newVel.Y()) if yMov < 0.1 && !state.PressingSneak { d1 := 0.4 + yMov*0.2 @@ -606,13 +591,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 movementblock.BounceSlime: newVel[1] = SlimeBounceMultiplier * old.Y() if math32.Abs(newVel[1]) < 1e-4 { newVel[1] = 0.0 } - case "minecraft:bed": + case movementblock.BounceBed: newVel[1] = math32.Min(1.0, BedBounceMultiplier*old.Y()) default: newVel[1] = 0 @@ -1014,11 +999,10 @@ func (s *Simulator) isInsideCobweb(state *MovementState) bool { if _, isAir := b.(block.Air); isAir { continue } - if s.blockName(b) != "minecraft:web" { + 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 757fd61..d531b43 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. @@ -64,7 +65,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 +74,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) block.MovementSemantics { + return block.Resolve(b, BlockName(b)) } // swimWaterGraceTicks resolves the configured grace window: zero means the @@ -98,25 +91,33 @@ 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 { - if s.BlockSemantics != nil { - if friction := s.BlockSemantics.BlockFriction(b); friction > 0 && !math32.IsInf(friction, 1) { - return friction - } - } - return BlockFriction(b) +func validGroundAccelerationFrictionMultiplier(multiplier float32) bool { + return multiplier > 0 && !math32.IsInf(multiplier, 1) } -func (s *Simulator) blockClimbable(b world.Block) bool { +func (s *Simulator) blockMovementSemantics(b world.Block) block.MovementSemantics { if s.BlockSemantics != nil { - return s.BlockSemantics.BlockClimbable(b) + 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 = resolveFallback().GroundFriction + } + if !validGroundAccelerationFrictionMultiplier(semantics.GroundAccelerationFrictionMultiplier) { + semantics.GroundAccelerationFrictionMultiplier = resolveFallback().GroundAccelerationFrictionMultiplier + } + return semantics } - return BlockClimbable(b) + return block.Resolve(b, BlockName(b)) } diff --git a/simulator_test.go b/simulator_test.go index 0392347..08ee041 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,21 +115,11 @@ func (m mockInventory) HasElytra() bool { } type overrideBlockSemantics struct { - name string - friction float32 - climbable bool + semantics movementblock.MovementSemantics } -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) movementblock.MovementSemantics { + return s.semantics } func newBaseState() *MovementState { @@ -341,42 +332,42 @@ func TestSimulatorBlockSemanticsOverridesDefaults(t *testing.T) { sim := &Simulator{ World: mockWorld{}, BlockSemantics: overrideBlockSemantics{ - name: "minecraft:custom_floor", - friction: 0.42, - climbable: true, + semantics: movementblock.MovementSemantics{ + GroundFriction: 0.42, + Climbable: true, + Cobweb: true, + Bounce: movementblock.BounceBed, + }, }, } 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 != movementblock.BounceBed { + 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 := movementblock.Resolve(b, BlockName(b)) + if got != want { + t.Fatalf("expected default movement semantics %+v, got %+v", want, got) } } func TestSimulatorInvalidBlockSemanticsFrictionFallsBackToDefault(t *testing.T) { b := block.Air{} - want := BlockFriction(b) + want := movementblock.Friction(b, BlockName(b)) tests := []struct { name string @@ -394,17 +385,34 @@ func TestSimulatorInvalidBlockSemanticsFrictionFallsBackToDefault(t *testing.T) sim := &Simulator{ World: mockWorld{}, BlockSemantics: overrideBlockSemantics{ - name: "minecraft:custom_floor", - friction: tt.friction, + semantics: movementblock.MovementSemantics{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) } }) } } +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},