From b154f9c1caed39601f53c81280cdd488a55b3ee2 Mon Sep 17 00:00:00 2001 From: "Natanael [Root]" Date: Tue, 23 Jun 2026 14:52:39 +0200 Subject: [PATCH] server/block: Implement the snow layer block Adds the snow layer block (minecraft:snow_layer), a thin partial covering of snow that may be stacked up to eight layers high. The block scales its collision box with the number of layers (a single layer is passable), may be placed on top of any block with a solid upward face, and stacks an extra layer when more snow is used on it, never converting into a full snow block. Breaking it drops snowballs (one to four, depending on the number of layers) or, with Silk Touch, the layers themselves. It melts a layer at a time in biomes too warm to sustain snow. Melting from a nearby block light source, gravity and snowlogging (covered_bit) are left for a follow-up and marked where relevant. Fixes #871. Co-Authored-By: Claude Opus 4.8 --- server/block/hash.go | 5 ++ server/block/model/snow.go | 28 +++++++++ server/block/register.go | 2 + server/block/snow_layer.go | 121 +++++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 server/block/model/snow.go create mode 100644 server/block/snow_layer.go diff --git a/server/block/hash.go b/server/block/hash.go index 684d9f010..fc8c999df 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -180,6 +180,7 @@ const ( hashSmoker hashSmoothBasalt hashSnow + hashSnowLayer hashSoulSand hashSoulSoil hashSponge @@ -920,6 +921,10 @@ func (Snow) Hash() (uint64, uint64) { return hashSnow, 0 } +func (s SnowLayer) Hash() (uint64, uint64) { + return hashSnowLayer, uint64(s.Height) +} + func (SoulSand) Hash() (uint64, uint64) { return hashSoulSand, 0 } diff --git a/server/block/model/snow.go b/server/block/model/snow.go new file mode 100644 index 000000000..46340b704 --- /dev/null +++ b/server/block/model/snow.go @@ -0,0 +1,28 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Snow is the model of a snow layer. The height of its bounding box scales with the number of layers the block +// has, ranging from a single passable layer to a near-full block. +type Snow struct { + // Layers is the number of snow layers the block has, ranging from 1 to 8. + Layers int +} + +// BBox returns a flat box whose height scales with the number of layers. A single layer returns no box at all, so +// that entities walk over it freely as they do in vanilla. +func (s Snow) BBox(cube.Pos, world.BlockSource) []cube.BBox { + if s.Layers <= 1 { + return nil + } + return []cube.BBox{cube.Box(0, 0, 0, 1, float64(s.Layers-1)*0.125, 1)} +} + +// FaceSolid returns true only for the upward face of a full eight-layer block, the only state to which other +// blocks may be attached. +func (s Snow) FaceSolid(_ cube.Pos, face cube.Face, _ world.BlockSource) bool { + return s.Layers >= 8 && face == cube.FaceUp +} diff --git a/server/block/register.go b/server/block/register.go index 116c454e6..c6fff44fa 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -108,6 +108,7 @@ func init() { world.RegisterBlock(SmithingTable{}) world.RegisterBlock(SmoothBasalt{}) world.RegisterBlock(Snow{}) + registerAll(allSnowLayers()) world.RegisterBlock(SoulSand{}) world.RegisterBlock(SoulSoil{}) world.RegisterBlock(Sponge{Wet: true}) @@ -382,6 +383,7 @@ func init() { world.RegisterItem(Smoker{}) world.RegisterItem(SmoothBasalt{}) world.RegisterItem(Snow{}) + world.RegisterItem(SnowLayer{}) world.RegisterItem(SoulSand{}) world.RegisterItem(SoulSoil{}) world.RegisterItem(Sponge{Wet: true}) diff --git a/server/block/snow_layer.go b/server/block/snow_layer.go new file mode 100644 index 000000000..12eafdcd0 --- /dev/null +++ b/server/block/snow_layer.go @@ -0,0 +1,121 @@ +package block + +import ( + "math/rand/v2" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +// SnowLayer is a thin, partial covering of snow that may be stacked up to eight layers high. +type SnowLayer struct { + transparent + + // Height is the number of snow layers in addition to the bottom one, ranging from 0 (a single layer) to 7 + // (eight layers, the height of a full block). + Height int +} + +// Model ... +func (s SnowLayer) Model() world.BlockModel { + return model.Snow{Layers: s.Height + 1} +} + +// ReplaceableBy returns true if more snow may still be stacked onto the layer, or, for any other block, only when +// this is a single layer thin enough to be replaced. +func (s SnowLayer) ReplaceableBy(b world.Block) bool { + if _, ok := b.(SnowLayer); ok { + return s.Height < 7 + } + return s.Height == 0 +} + +// UseOnBlock places a new snow layer or, when used on an existing layer, stacks another layer onto it. +func (s SnowLayer) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + if existing, ok := tx.Block(pos).(SnowLayer); ok && existing.Height < 7 { + existing.Height++ + place(tx, pos, existing, user, ctx) + return placed(ctx) + } + + pos, _, used := firstReplaceable(tx, pos, face, s) + if !used { + return false + } + below := pos.Side(cube.FaceDown) + if !tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) { + // Snow can only rest on top of a block with a solid upward face. + return false + } + + place(tx, pos, s, user, ctx) + return placed(ctx) +} + +// NeighbourUpdateTick breaks the snow layer if the block below it no longer supports it. +func (s SnowLayer) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + below := pos.Side(cube.FaceDown) + if !tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) { + breakBlock(s, pos, tx) + } +} + +// RandomTick melts the snow in biomes too warm to sustain it, removing a single layer at a time until it is gone. +func (s SnowLayer) RandomTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + // TODO: also melt when the block light level is 12 or higher, regardless of the biome temperature. The world + // does not currently expose the block light level separately from the sky light, which is required to do this + // without melting snow in daylight. + if tx.Temperature(pos) <= 0.15 { + return + } + if s.Height == 0 { + tx.SetBlock(pos, nil, nil) + return + } + s.Height-- + tx.SetBlock(pos, s, nil) +} + +// BreakInfo ... +func (s SnowLayer) BreakInfo() BreakInfo { + return newBreakInfo(0.1, alwaysHarvestable, shovelEffective, func(t item.Tool, enchantments []item.Enchantment) []item.Stack { + layers := s.Height + 1 + if hasSilkTouch(enchantments) { + if layers >= 8 { + return []item.Stack{item.NewStack(Snow{}, 1)} + } + return []item.Stack{item.NewStack(SnowLayer{}, layers)} + } + switch { + case layers <= 3: + return []item.Stack{item.NewStack(item.Snowball{}, 1)} + case layers <= 5: + return []item.Stack{item.NewStack(item.Snowball{}, 2)} + case layers <= 7: + return []item.Stack{item.NewStack(item.Snowball{}, 3)} + default: + return []item.Stack{item.NewStack(item.Snowball{}, 4)} + } + }) +} + +// EncodeItem ... +func (SnowLayer) EncodeItem() (name string, meta int16) { + return "minecraft:snow_layer", 0 +} + +// EncodeBlock ... +func (s SnowLayer) EncodeBlock() (string, map[string]any) { + return "minecraft:snow_layer", map[string]any{"height": int32(s.Height), "covered_bit": boolByte(false)} +} + +// allSnowLayers ... +func allSnowLayers() (s []world.Block) { + for h := 0; h < 8; h++ { + s = append(s, SnowLayer{Height: h}) + } + return +}