From 6402a53f6108e6eb352ee0f0ce4d579404e09539 Mon Sep 17 00:00:00 2001 From: cqdetdev <101936396+cqdetdev@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:39:49 -0400 Subject: [PATCH 1/9] server/block: Add buttons, pressure plates and redstone lamp Port the redstone power sources narrowed out of the world-owned engine review (#1250) onto the merged engine: buttons of every material with press scheduling, stone/wooden pressure plates, light and heavy weighted plates with analog 1-15 output based on entity count, and the redstone lamp driven through RedstonePowerConsumer. Pressure plates react to entities through EntityStepOn and scheduled release ticks, ignoring snowballs and, for stone-like plates, non-living entities, matching vanilla behaviour. Materials are modelled as ButtonType and PressurePlateType following the established *_type.go value type convention. Adds the pressure plate click sounds and their level sound event mappings. --- cmd/blockhash/main.go | 2 +- server/block/button.go | 136 +++++++++++++++ server/block/button_type.go | 169 ++++++++++++++++++ server/block/hash.go | 15 ++ server/block/pressure_plate.go | 255 ++++++++++++++++++++++++++++ server/block/pressure_plate_type.go | 196 +++++++++++++++++++++ server/block/redstone.go | 26 +++ server/block/redstone_lamp.go | 54 ++++++ server/block/register.go | 10 ++ server/session/world.go | 4 + server/world/sound/block.go | 6 + 11 files changed, 872 insertions(+), 1 deletion(-) create mode 100644 server/block/button.go create mode 100644 server/block/button_type.go create mode 100644 server/block/pressure_plate.go create mode 100644 server/block/pressure_plate_type.go create mode 100644 server/block/redstone.go create mode 100644 server/block/redstone_lamp.go diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index 5fdbe8d24..c3c442079 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -248,7 +248,7 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[ return "uint64(" + s + ".Uint8())", 5 case "GrindstoneAttachment": return "uint64(" + s + ".Uint8())", 2 - case "WoodType", "LeavesType", "FlowerType", "DoubleFlowerType", "Colour": + case "WoodType", "LeavesType", "FlowerType", "DoubleFlowerType", "Colour", "ButtonType", "PressurePlateType": // Assuming these were all based on metadata, it should be safe to assume a bit size of 4 for this. return "uint64(" + s + ".Uint8())", 4 case "CoralType", "SkullType": diff --git a/server/block/button.go b/server/block/button.go new file mode 100644 index 000000000..52c31514e --- /dev/null +++ b/server/block/button.go @@ -0,0 +1,136 @@ +package block + +import ( + "math/rand/v2" + "time" + + "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/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +// Button is a non-solid block that emits redstone power for a short duration +// when pressed. +type Button struct { + empty + transparent + sourceWaterDisplacer + + // Type is the material the button is made of. + Type ButtonType + // Facing is the face of the block that the button is attached to. + Facing cube.Face + // Pressed is true while the button emits power. + Pressed bool +} + +// Model ... +func (Button) Model() world.BlockModel { + return model.Empty{} +} + +// UseOnBlock places the button attached to the clicked face. +func (b Button) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, face, used := firstReplaceable(tx, pos, face, b) + if !used || !redstoneAttachmentSupported(tx, pos, face) { + return false + } + b.Facing = face + place(tx, pos, b, user, ctx) + return placed(ctx) +} + +// Activate presses the button and schedules its release. +func (b Button) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { + if b.Pressed { + return true + } + b.Pressed = true + tx.SetBlock(pos, b, nil) + tx.ScheduleBlockUpdate(pos, b, b.pressDuration()) + tx.PlaySound(pos.Vec3Centre(), sound.Click{}) + return true +} + +// NeighbourUpdateTick breaks the button if its supporting block is removed. +func (b Button) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if !redstoneAttachmentSupported(tx, pos, b.Facing) { + breakBlock(b, pos, tx) + } +} + +// ScheduledTick releases a pressed button. +func (b Button) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + if !b.Pressed { + return + } + b.Pressed = false + tx.SetBlock(pos, b, nil) + tx.PlaySound(pos.Vec3Centre(), sound.Click{}) +} + +// RedstonePower returns maximum power while the button is pressed. +func (b Button) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { + if b.Pressed { + return 15 + } + return 0 +} + +// RedstoneStrongPower strongly powers the block the button is attached to. +func (b Button) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { + if b.Pressed && face == b.Facing.Opposite() { + return 15 + } + return 0 +} + +// BreakInfo ... +func (b Button) BreakInfo() BreakInfo { + return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(b)) +} + +// SideClosed ... +func (Button) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false +} + +// FuelInfo ... +func (b Button) FuelInfo() item.FuelInfo { + if b.Type.Wood() { + return newFuelInfo(time.Second * 5) + } + return item.FuelInfo{} +} + +// EncodeItem ... +func (b Button) EncodeItem() (name string, meta int16) { + return "minecraft:" + b.Type.String(), 0 +} + +// EncodeBlock ... +func (b Button) EncodeBlock() (string, map[string]any) { + return "minecraft:" + b.Type.String(), map[string]any{"button_pressed_bit": boolByte(b.Pressed), "facing_direction": int32(b.Facing)} +} + +// pressDuration returns how long the button stays pressed: 1.5 seconds for +// wooden buttons and 1 second for stone-like buttons. +func (b Button) pressDuration() time.Duration { + if b.Type.Wood() { + return time.Second * 3 / 2 + } + return time.Second +} + +// allButtons ... +func allButtons() (buttons []world.Block) { + for _, t := range ButtonTypes() { + for _, face := range cube.Faces() { + buttons = append(buttons, Button{Type: t, Facing: face}, Button{Type: t, Facing: face, Pressed: true}) + } + } + return +} diff --git a/server/block/button_type.go b/server/block/button_type.go new file mode 100644 index 000000000..d782df8d8 --- /dev/null +++ b/server/block/button_type.go @@ -0,0 +1,169 @@ +package block + +// ButtonType represents the material a button is made of. +type ButtonType struct { + button +} + +type button uint8 + +// StoneButton returns the stone button variant. +func StoneButton() ButtonType { + return ButtonType{0} +} + +// PolishedBlackstoneButton returns the polished blackstone button variant. +func PolishedBlackstoneButton() ButtonType { + return ButtonType{1} +} + +// OakButton returns the oak button variant. +func OakButton() ButtonType { + return ButtonType{2} +} + +// SpruceButton returns the spruce button variant. +func SpruceButton() ButtonType { + return ButtonType{3} +} + +// BirchButton returns the birch button variant. +func BirchButton() ButtonType { + return ButtonType{4} +} + +// JungleButton returns the jungle button variant. +func JungleButton() ButtonType { + return ButtonType{5} +} + +// AcaciaButton returns the acacia button variant. +func AcaciaButton() ButtonType { + return ButtonType{6} +} + +// DarkOakButton returns the dark oak button variant. +func DarkOakButton() ButtonType { + return ButtonType{7} +} + +// MangroveButton returns the mangrove button variant. +func MangroveButton() ButtonType { + return ButtonType{8} +} + +// CherryButton returns the cherry button variant. +func CherryButton() ButtonType { + return ButtonType{9} +} + +// BambooButton returns the bamboo button variant. +func BambooButton() ButtonType { + return ButtonType{10} +} + +// CrimsonButton returns the crimson button variant. +func CrimsonButton() ButtonType { + return ButtonType{11} +} + +// WarpedButton returns the warped button variant. +func WarpedButton() ButtonType { + return ButtonType{12} +} + +// PaleOakButton returns the pale oak button variant. +func PaleOakButton() ButtonType { + return ButtonType{13} +} + +// Uint8 returns the button type as a uint8. +func (b button) Uint8() uint8 { + return uint8(b) +} + +// Wood reports whether the button is made of wood, giving it a longer press +// duration and making it usable as furnace fuel. +func (b button) Wood() bool { + return b >= 2 +} + +// Name ... +func (b button) Name() string { + switch b { + case 0: + return "Stone Button" + case 1: + return "Polished Blackstone Button" + case 2: + return "Oak Button" + case 3: + return "Spruce Button" + case 4: + return "Birch Button" + case 5: + return "Jungle Button" + case 6: + return "Acacia Button" + case 7: + return "Dark Oak Button" + case 8: + return "Mangrove Button" + case 9: + return "Cherry Button" + case 10: + return "Bamboo Button" + case 11: + return "Crimson Button" + case 12: + return "Warped Button" + case 13: + return "Pale Oak Button" + } + panic("unknown button type") +} + +// String ... +func (b button) String() string { + switch b { + case 0: + return "stone_button" + case 1: + return "polished_blackstone_button" + case 2: + // Oak buttons use the legacy wooden identifier. + return "wooden_button" + case 3: + return "spruce_button" + case 4: + return "birch_button" + case 5: + return "jungle_button" + case 6: + return "acacia_button" + case 7: + return "dark_oak_button" + case 8: + return "mangrove_button" + case 9: + return "cherry_button" + case 10: + return "bamboo_button" + case 11: + return "crimson_button" + case 12: + return "warped_button" + case 13: + return "pale_oak_button" + } + panic("unknown button type") +} + +// ButtonTypes ... +func ButtonTypes() []ButtonType { + types := make([]ButtonType, 14) + for i := range types { + types[i] = ButtonType{button(i)} + } + return types +} diff --git a/server/block/hash.go b/server/block/hash.go index 088c8c52c..7cf52e8da 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -29,6 +29,7 @@ const ( hashBookshelf hashBrewingStand hashBricks + hashButton hashCactus hashCake hashCalcite @@ -156,6 +157,7 @@ const ( hashPolishedTuff hashPortal hashPotato + hashPressurePlate hashPrismarine hashPumpkin hashPumpkinSeeds @@ -168,6 +170,7 @@ const ( hashRawGold hashRawIron hashRedstoneBlock + hashRedstoneLamp hashRedstoneOre hashRedstoneTorch hashRedstoneWire @@ -325,6 +328,10 @@ func (Bricks) Hash() (uint64, uint64) { return hashBricks, 0 } +func (b Button) Hash() (uint64, uint64) { + return hashButton, uint64(b.Type.Uint8()) | uint64(b.Facing)<<4 | uint64(boolByte(b.Pressed))<<7 +} + func (c Cactus) Hash() (uint64, uint64) { return hashCactus, uint64(c.Age) } @@ -833,6 +840,10 @@ func (p Potato) Hash() (uint64, uint64) { return hashPotato, uint64(p.Growth) } +func (p PressurePlate) Hash() (uint64, uint64) { + return hashPressurePlate, uint64(p.Type.Uint8()) | uint64(p.Power)<<4 +} + func (p Prismarine) Hash() (uint64, uint64) { return hashPrismarine, uint64(p.Type.Uint8()) } @@ -881,6 +892,10 @@ func (RedstoneBlock) Hash() (uint64, uint64) { return hashRedstoneBlock, 0 } +func (r RedstoneLamp) Hash() (uint64, uint64) { + return hashRedstoneLamp, uint64(boolByte(r.Lit)) +} + func (r RedstoneOre) Hash() (uint64, uint64) { return hashRedstoneOre, uint64(r.Type.Uint8()) | uint64(boolByte(r.Lit))<<1 } diff --git a/server/block/pressure_plate.go b/server/block/pressure_plate.go new file mode 100644 index 000000000..53aef40ec --- /dev/null +++ b/server/block/pressure_plate.go @@ -0,0 +1,255 @@ +package block + +import ( + "math/rand/v2" + "time" + + "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/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +// PressurePlate is a non-solid block that emits redstone power while entities +// stand on it. Weighted variants emit an analog power level based on the +// number of entities on the plate. +type PressurePlate struct { + empty + transparent + sourceWaterDisplacer + + // Type is the material the pressure plate is made of. + Type PressurePlateType + // Power is the current redstone signal emitted by the plate. + Power int +} + +// Model ... +func (PressurePlate) Model() world.BlockModel { + return model.Carpet{} +} + +// UseOnBlock places the pressure plate on a solid surface. +func (p PressurePlate) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(tx, pos, face, p) + if !used || !redstoneFloorComponentSupported(tx, pos) { + return false + } + place(tx, pos, p, user, ctx) + return placed(ctx) +} + +// EntityStepOn powers the plate when an entity stands on it. +func (p PressurePlate) EntityStepOn(pos cube.Pos, tx *world.Tx, e world.Entity) { + power := p.entityPower(e) + if power == 0 { + return + } + if p.Type.Weighted() { + power = max(power, p.detectPower(pos, tx)) + } + if p.Power == power { + tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) + return + } + p.Power = power + tx.SetBlock(pos, p, nil) + tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) + tx.PlaySound(pos.Vec3Centre(), sound.PressurePlateClickOn{}) +} + +// NeighbourUpdateTick breaks the pressure plate if its supporting block is removed. +func (p PressurePlate) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if !redstoneFloorComponentSupported(tx, pos) { + breakBlock(p, pos, tx) + } +} + +// ScheduledTick releases the plate if no entity keeps it pressed. +func (p PressurePlate) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + power := p.detectPower(pos, tx) + if power > 0 { + if p.Power != power { + p.Power = power + tx.SetBlock(pos, p, nil) + } + tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) + return + } + if p.Power == 0 { + return + } + p.Power = 0 + tx.SetBlock(pos, p, nil) + tx.PlaySound(pos.Vec3Centre(), sound.PressurePlateClickOff{}) +} + +// RedstonePower returns the plate's analog power level. +func (p PressurePlate) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { + return p.Power +} + +// RedstoneStrongPower strongly powers the block below the pressure plate. +func (p PressurePlate) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { + if face == cube.FaceDown { + return p.Power + } + return 0 +} + +// BreakInfo ... +func (p PressurePlate) BreakInfo() BreakInfo { + return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(p)) +} + +// SideClosed ... +func (PressurePlate) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false +} + +// FuelInfo ... +func (p PressurePlate) FuelInfo() item.FuelInfo { + if p.Type.Wood() { + return newFuelInfo(time.Second * 15) + } + return item.FuelInfo{} +} + +// EncodeItem ... +func (p PressurePlate) EncodeItem() (name string, meta int16) { + return "minecraft:" + p.Type.String(), 0 +} + +// EncodeBlock ... +func (p PressurePlate) EncodeBlock() (string, map[string]any) { + return "minecraft:" + p.Type.String(), map[string]any{"redstone_signal": int32(max(0, min(p.Power, 15)))} +} + +// stepPower is the power a single detected entity contributes: the first +// analog level for weighted plates and full power otherwise. +func (p PressurePlate) stepPower() int { + if p.Type.Weighted() { + return 1 + } + return 15 +} + +func (p PressurePlate) entityPower(e world.Entity) int { + if !p.detectsEntity(e) { + return 0 + } + return p.stepPower() +} + +// detectsEntity reports whether an entity activates the plate. Stone-like +// plates only react to living entities, players and armour stands; snowballs +// never activate a plate. +func (p PressurePlate) detectsEntity(e world.Entity) bool { + if pressurePlateEntityName(e) == "minecraft:snowball" { + return false + } + if !p.Type.Wood() && !p.Type.Weighted() { + return pressurePlateStoneEntity(e) + } + return true +} + +// detectPower scans the entities intersecting the plate's activation box and +// returns the power level they produce. +func (p PressurePlate) detectPower(pos cube.Pos, tx *world.Tx) int { + box := pressurePlateActivationBox(pos) + entities := 0 + for e := range tx.EntitiesWithin(box.Grow(1)) { + if p.entityPower(e) == 0 || !pressurePlateEntityIntersects(e, box) { + continue + } + if !p.Type.Weighted() { + return 15 + } + entities++ + if entities >= p.weightedMaxEntities() { + return 15 + } + } + if p.Type.Weighted() { + return p.weightedPower(entities) + } + return 0 +} + +// weightedPower converts an entity count to the analog power of a weighted +// plate: one level per entity for light plates and per ten entities, rounded +// up, for heavy plates. +func (p PressurePlate) weightedPower(entities int) int { + if entities <= 0 { + return 0 + } + if p.Type == HeavyWeightedPressurePlate() { + return min(15, (entities+9)/10) + } + return min(15, entities) +} + +// weightedMaxEntities is the entity count at which a weighted plate reaches +// full power, so scanning may stop early. +func (p PressurePlate) weightedMaxEntities() int { + if p.Type == HeavyWeightedPressurePlate() { + return 141 + } + return 15 +} + +// releaseDelay is the delay before the plate re-checks its entities: 0.5 +// seconds for weighted plates and 1 second otherwise. +func (p PressurePlate) releaseDelay() time.Duration { + if p.Type.Weighted() { + return time.Second / 2 + } + return time.Second +} + +type pressurePlateLivingEntity interface { + Health() float64 + Dead() bool +} + +func pressurePlateStoneEntity(e world.Entity) bool { + if living, ok := e.(pressurePlateLivingEntity); ok { + return living.Health() > 0 && !living.Dead() + } + return pressurePlateEntityName(e) == "minecraft:player" || pressurePlateEntityName(e) == "minecraft:armor_stand" +} + +func pressurePlateEntityName(e world.Entity) string { + h := e.H() + if h == nil || h.Type() == nil { + return "" + } + return h.Type().EncodeEntity() +} + +// pressurePlateActivationBox is the box entities must intersect to press the +// plate at a position. +func pressurePlateActivationBox(pos cube.Pos) cube.BBox { + return cube.Box(float64(pos[0]), float64(pos[1]), float64(pos[2]), float64(pos[0]+1), float64(pos[1])+0.25, float64(pos[2]+1)) +} + +func pressurePlateEntityIntersects(e world.Entity, box cube.BBox) bool { + h := e.H() + if h == nil || h.Type() == nil { + return false + } + return h.Type().BBox(e).Translate(e.Position()).IntersectsWith(box) +} + +// allPressurePlates ... +func allPressurePlates() (plates []world.Block) { + for _, t := range PressurePlateTypes() { + for power := 0; power <= 15; power++ { + plates = append(plates, PressurePlate{Type: t, Power: power}) + } + } + return +} diff --git a/server/block/pressure_plate_type.go b/server/block/pressure_plate_type.go new file mode 100644 index 000000000..f5f7a4b52 --- /dev/null +++ b/server/block/pressure_plate_type.go @@ -0,0 +1,196 @@ +package block + +// PressurePlateType represents the material a pressure plate is made of, +// including the weighted gold and iron variants. +type PressurePlateType struct { + pressurePlate +} + +type pressurePlate uint8 + +// StonePressurePlate returns the stone pressure plate variant. +func StonePressurePlate() PressurePlateType { + return PressurePlateType{0} +} + +// PolishedBlackstonePressurePlate returns the polished blackstone pressure plate variant. +func PolishedBlackstonePressurePlate() PressurePlateType { + return PressurePlateType{1} +} + +// OakPressurePlate returns the oak pressure plate variant. +func OakPressurePlate() PressurePlateType { + return PressurePlateType{2} +} + +// SprucePressurePlate returns the spruce pressure plate variant. +func SprucePressurePlate() PressurePlateType { + return PressurePlateType{3} +} + +// BirchPressurePlate returns the birch pressure plate variant. +func BirchPressurePlate() PressurePlateType { + return PressurePlateType{4} +} + +// JunglePressurePlate returns the jungle pressure plate variant. +func JunglePressurePlate() PressurePlateType { + return PressurePlateType{5} +} + +// AcaciaPressurePlate returns the acacia pressure plate variant. +func AcaciaPressurePlate() PressurePlateType { + return PressurePlateType{6} +} + +// DarkOakPressurePlate returns the dark oak pressure plate variant. +func DarkOakPressurePlate() PressurePlateType { + return PressurePlateType{7} +} + +// MangrovePressurePlate returns the mangrove pressure plate variant. +func MangrovePressurePlate() PressurePlateType { + return PressurePlateType{8} +} + +// CherryPressurePlate returns the cherry pressure plate variant. +func CherryPressurePlate() PressurePlateType { + return PressurePlateType{9} +} + +// BambooPressurePlate returns the bamboo pressure plate variant. +func BambooPressurePlate() PressurePlateType { + return PressurePlateType{10} +} + +// CrimsonPressurePlate returns the crimson pressure plate variant. +func CrimsonPressurePlate() PressurePlateType { + return PressurePlateType{11} +} + +// WarpedPressurePlate returns the warped pressure plate variant. +func WarpedPressurePlate() PressurePlateType { + return PressurePlateType{12} +} + +// PaleOakPressurePlate returns the pale oak pressure plate variant. +func PaleOakPressurePlate() PressurePlateType { + return PressurePlateType{13} +} + +// LightWeightedPressurePlate returns the light weighted (gold) pressure plate +// variant, which emits one power level per entity on it. +func LightWeightedPressurePlate() PressurePlateType { + return PressurePlateType{14} +} + +// HeavyWeightedPressurePlate returns the heavy weighted (iron) pressure plate +// variant, which emits one power level per ten entities on it. +func HeavyWeightedPressurePlate() PressurePlateType { + return PressurePlateType{15} +} + +// Uint8 returns the pressure plate type as a uint8. +func (p pressurePlate) Uint8() uint8 { + return uint8(p) +} + +// Wood reports whether the pressure plate is made of wood, making it react to +// every entity and usable as furnace fuel. +func (p pressurePlate) Wood() bool { + return p >= 2 && p <= 13 +} + +// Weighted reports whether the pressure plate emits an analog power level +// based on the number of entities on it. +func (p pressurePlate) Weighted() bool { + return p == 14 || p == 15 +} + +// Name ... +func (p pressurePlate) Name() string { + switch p { + case 0: + return "Stone Pressure Plate" + case 1: + return "Polished Blackstone Pressure Plate" + case 2: + return "Oak Pressure Plate" + case 3: + return "Spruce Pressure Plate" + case 4: + return "Birch Pressure Plate" + case 5: + return "Jungle Pressure Plate" + case 6: + return "Acacia Pressure Plate" + case 7: + return "Dark Oak Pressure Plate" + case 8: + return "Mangrove Pressure Plate" + case 9: + return "Cherry Pressure Plate" + case 10: + return "Bamboo Pressure Plate" + case 11: + return "Crimson Pressure Plate" + case 12: + return "Warped Pressure Plate" + case 13: + return "Pale Oak Pressure Plate" + case 14: + return "Light Weighted Pressure Plate" + case 15: + return "Heavy Weighted Pressure Plate" + } + panic("unknown pressure plate type") +} + +// String ... +func (p pressurePlate) String() string { + switch p { + case 0: + return "stone_pressure_plate" + case 1: + return "polished_blackstone_pressure_plate" + case 2: + // Oak pressure plates use the legacy wooden identifier. + return "wooden_pressure_plate" + case 3: + return "spruce_pressure_plate" + case 4: + return "birch_pressure_plate" + case 5: + return "jungle_pressure_plate" + case 6: + return "acacia_pressure_plate" + case 7: + return "dark_oak_pressure_plate" + case 8: + return "mangrove_pressure_plate" + case 9: + return "cherry_pressure_plate" + case 10: + return "bamboo_pressure_plate" + case 11: + return "crimson_pressure_plate" + case 12: + return "warped_pressure_plate" + case 13: + return "pale_oak_pressure_plate" + case 14: + return "light_weighted_pressure_plate" + case 15: + return "heavy_weighted_pressure_plate" + } + panic("unknown pressure plate type") +} + +// PressurePlateTypes ... +func PressurePlateTypes() []PressurePlateType { + types := make([]PressurePlateType, 16) + for i := range types { + types[i] = PressurePlateType{pressurePlate(i)} + } + return types +} diff --git a/server/block/redstone.go b/server/block/redstone.go new file mode 100644 index 000000000..66d0c5894 --- /dev/null +++ b/server/block/redstone.go @@ -0,0 +1,26 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// redstoneAttachmentSupported reports whether a redstone component may attach +// to the block behind the face passed. +func redstoneAttachmentSupported(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + support := pos.Side(face.Opposite()) + if support.OutOfBounds(tx.Range()) { + return false + } + return tx.Block(support).Model().FaceSolid(support, face, tx) +} + +// redstoneFloorComponentSupported reports whether a floor-mounted redstone +// component is supported by the block below it. +func redstoneFloorComponentSupported(tx *world.Tx, pos cube.Pos) bool { + support := pos.Side(cube.FaceDown) + if support.OutOfBounds(tx.Range()) { + return false + } + return tx.Block(support).Model().FaceSolid(support, cube.FaceUp, tx) +} diff --git a/server/block/redstone_lamp.go b/server/block/redstone_lamp.go new file mode 100644 index 000000000..af170d797 --- /dev/null +++ b/server/block/redstone_lamp.go @@ -0,0 +1,54 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// RedstoneLamp is a lamp that lights while powered. +type RedstoneLamp struct { + solid + + // Lit is true when the lamp is powered and emitting light. + Lit bool +} + +// LightEmissionLevel ... +func (r RedstoneLamp) LightEmissionLevel() uint8 { + if r.Lit { + return 15 + } + return 0 +} + +// RedstonePowerUpdate updates the lamp's lit state to match its redstone input. +func (r RedstoneLamp) RedstonePowerUpdate(_ cube.Pos, _ *world.Tx, power int) (world.Block, bool) { + lit := power > 0 + if r.Lit == lit { + return r, false + } + r.Lit = lit + return r, true +} + +// BreakInfo ... +func (r RedstoneLamp) BreakInfo() BreakInfo { + return newBreakInfo(0.3, alwaysHarvestable, nothingEffective, oneOf(RedstoneLamp{})) +} + +// EncodeItem ... +func (RedstoneLamp) EncodeItem() (name string, meta int16) { + return "minecraft:redstone_lamp", 0 +} + +// EncodeBlock ... +func (r RedstoneLamp) EncodeBlock() (string, map[string]any) { + if r.Lit { + return "minecraft:lit_redstone_lamp", nil + } + return "minecraft:redstone_lamp", nil +} + +func allRedstoneLamps() []world.Block { + return []world.Block{RedstoneLamp{}, RedstoneLamp{Lit: true}} +} diff --git a/server/block/register.go b/server/block/register.go index 4376990dd..a7b33897d 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -195,9 +195,12 @@ func init() { registerAll(allLadders()) registerAll(allLanterns()) registerAll(allLava()) + registerAll(allButtons()) registerAll(allLeaves()) registerAll(allLecterns()) registerAll(allLevers()) + registerAll(allPressurePlates()) + registerAll(allRedstoneLamps()) registerAll(allLight()) registerAll(allLitPumpkins()) registerAll(allLogs()) @@ -343,6 +346,13 @@ func init() { world.RegisterItem(Lapis{}) world.RegisterItem(Lectern{}) world.RegisterItem(Lever{}) + world.RegisterItem(RedstoneLamp{}) + for _, t := range ButtonTypes() { + world.RegisterItem(Button{Type: t}) + } + for _, t := range PressurePlateTypes() { + world.RegisterItem(PressurePlate{Type: t}) + } world.RegisterItem(LilyPad{}) world.RegisterItem(Magma{}) world.RegisterItem(LitPumpkin{}) diff --git a/server/session/world.go b/server/session/world.go index f8ba2c123..0d6a3cb67 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -561,6 +561,10 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) pk.SoundType = packet.SoundEventExplode case sound.Thunder: pk.SoundType, pk.EntityType = packet.SoundEventThunder, "minecraft:lightning_bolt" + case sound.PressurePlateClickOn: + pk.SoundType = packet.SoundEventPressurePlateClickOn + case sound.PressurePlateClickOff: + pk.SoundType = packet.SoundEventPressurePlateClickOff case sound.Click: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundClick, diff --git a/server/world/sound/block.go b/server/world/sound/block.go index 02cfad5bb..0b8bae3e9 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -126,6 +126,12 @@ type DoorCrash struct{ sound } // Click is a clicking sound. type Click struct{ sound } +// PressurePlateClickOn is played when a pressure plate starts emitting power. +type PressurePlateClickOn struct{ sound } + +// PressurePlateClickOff is played when a pressure plate stops emitting power. +type PressurePlateClickOff struct{ sound } + // Ignite is a sound played when using a flint & steel. type Ignite struct{ sound } From 3cdf93db881726efdb14d7bb011f7308bfea19ab Mon Sep 17 00:00:00 2001 From: cqdetdev <101936396+cqdetdev@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:16:55 -0400 Subject: [PATCH 2/9] server/block: Address redstone power source review Non-player entities now trigger EntityStepper blocks: entity movement checks the block stood on after each tick like players do, letting mobs, items and projectiles activate pressure plates. Wooden buttons are pressed by arrows. Projectiles notify the block of the cell they come to rest in, since blocks without a collision box are passed through by the trace, and a wooden button stays pressed while an arrow rests inside it. Redstone lamps now turn off after two redstone ticks instead of immediately, keeping them lit through short pulses. Buttons and pressure plates drop their unpowered forms so a replaced component cannot start permanently powered, and crimson and warped variants are no longer usable as furnace fuel, matching WoodType.Flammable. --- server/block/button.go | 39 +++++++++++++++++++++++++---- server/block/button_type.go | 8 +++++- server/block/pressure_plate.go | 4 +-- server/block/pressure_plate_type.go | 8 +++++- server/block/redstone_lamp.go | 31 +++++++++++++++++------ server/entity/ent.go | 1 + server/entity/movement.go | 22 ++++++++++++++++ server/entity/projectile.go | 8 ++++++ 8 files changed, 105 insertions(+), 16 deletions(-) diff --git a/server/block/button.go b/server/block/button.go index 52c31514e..1aeb22871 100644 --- a/server/block/button.go +++ b/server/block/button.go @@ -45,14 +45,27 @@ func (b Button) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world // Activate presses the button and schedules its release. func (b Button) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { + b.press(pos, tx) + return true +} + +// ProjectileHit presses wooden buttons hit by an arrow. +func (b Button) ProjectileHit(pos cube.Pos, tx *world.Tx, e world.Entity, _ cube.Face) { + if !b.Type.Wood() || e.H().Type().EncodeEntity() != "minecraft:arrow" { + return + } + b.press(pos, tx) +} + +// press activates an unpressed button and schedules its release. +func (b Button) press(pos cube.Pos, tx *world.Tx) { if b.Pressed { - return true + return } b.Pressed = true tx.SetBlock(pos, b, nil) tx.ScheduleBlockUpdate(pos, b, b.pressDuration()) tx.PlaySound(pos.Vec3Centre(), sound.Click{}) - return true } // NeighbourUpdateTick breaks the button if its supporting block is removed. @@ -62,16 +75,32 @@ func (b Button) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { } } -// ScheduledTick releases a pressed button. +// ScheduledTick releases a pressed button, unless an arrow rests inside a +// wooden button, keeping it pressed. func (b Button) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { if !b.Pressed { return } + if b.Type.Wood() && arrowWithin(pos, tx) { + tx.ScheduleBlockUpdate(pos, b, b.pressDuration()) + return + } b.Pressed = false tx.SetBlock(pos, b, nil) tx.PlaySound(pos.Vec3Centre(), sound.Click{}) } +// arrowWithin reports whether an arrow intersects the block space at pos. +func arrowWithin(pos cube.Pos, tx *world.Tx) bool { + box := cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3()) + for e := range tx.EntitiesWithin(box) { + if e.H().Type().EncodeEntity() == "minecraft:arrow" { + return true + } + } + return false +} + // RedstonePower returns maximum power while the button is pressed. func (b Button) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { if b.Pressed { @@ -90,7 +119,7 @@ func (b Button) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int // BreakInfo ... func (b Button) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(b)) + return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(Button{Type: b.Type})) } // SideClosed ... @@ -100,7 +129,7 @@ func (Button) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { // FuelInfo ... func (b Button) FuelInfo() item.FuelInfo { - if b.Type.Wood() { + if b.Type.Flammable() { return newFuelInfo(time.Second * 5) } return item.FuelInfo{} diff --git a/server/block/button_type.go b/server/block/button_type.go index d782df8d8..c7cb647a9 100644 --- a/server/block/button_type.go +++ b/server/block/button_type.go @@ -83,11 +83,17 @@ func (b button) Uint8() uint8 { } // Wood reports whether the button is made of wood, giving it a longer press -// duration and making it usable as furnace fuel. +// duration and letting arrows press it. func (b button) Wood() bool { return b >= 2 } +// Flammable reports whether the button can burn, making it usable as furnace +// fuel. Crimson and warped buttons are wooden but do not burn. +func (b button) Flammable() bool { + return b.Wood() && b != CrimsonButton().button && b != WarpedButton().button +} + // Name ... func (b button) Name() string { switch b { diff --git a/server/block/pressure_plate.go b/server/block/pressure_plate.go index 53aef40ec..3ef6a5016 100644 --- a/server/block/pressure_plate.go +++ b/server/block/pressure_plate.go @@ -101,7 +101,7 @@ func (p PressurePlate) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Fa // BreakInfo ... func (p PressurePlate) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(p)) + return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(PressurePlate{Type: p.Type})) } // SideClosed ... @@ -111,7 +111,7 @@ func (PressurePlate) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { // FuelInfo ... func (p PressurePlate) FuelInfo() item.FuelInfo { - if p.Type.Wood() { + if p.Type.Flammable() { return newFuelInfo(time.Second * 15) } return item.FuelInfo{} diff --git a/server/block/pressure_plate_type.go b/server/block/pressure_plate_type.go index f5f7a4b52..4a84e3016 100644 --- a/server/block/pressure_plate_type.go +++ b/server/block/pressure_plate_type.go @@ -96,11 +96,17 @@ func (p pressurePlate) Uint8() uint8 { } // Wood reports whether the pressure plate is made of wood, making it react to -// every entity and usable as furnace fuel. +// every entity. func (p pressurePlate) Wood() bool { return p >= 2 && p <= 13 } +// Flammable reports whether the pressure plate can burn, making it usable as +// furnace fuel. Crimson and warped plates are wooden but do not burn. +func (p pressurePlate) Flammable() bool { + return p.Wood() && p != CrimsonPressurePlate().pressurePlate && p != WarpedPressurePlate().pressurePlate +} + // Weighted reports whether the pressure plate emits an analog power level // based on the number of entities on it. func (p pressurePlate) Weighted() bool { diff --git a/server/block/redstone_lamp.go b/server/block/redstone_lamp.go index af170d797..0e4676fbb 100644 --- a/server/block/redstone_lamp.go +++ b/server/block/redstone_lamp.go @@ -1,6 +1,8 @@ package block import ( + "math/rand/v2" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" ) @@ -21,14 +23,29 @@ func (r RedstoneLamp) LightEmissionLevel() uint8 { return 0 } -// RedstonePowerUpdate updates the lamp's lit state to match its redstone input. -func (r RedstoneLamp) RedstonePowerUpdate(_ cube.Pos, _ *world.Tx, power int) (world.Block, bool) { - lit := power > 0 - if r.Lit == lit { - return r, false +// RedstonePowerUpdate lights the lamp as soon as it is powered. Turning off +// is delayed by two redstone ticks, keeping the lamp lit through short pulses. +func (r RedstoneLamp) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, power int) (world.Block, bool) { + if power > 0 { + if r.Lit { + return r, false + } + r.Lit = true + return r, true + } + if r.Lit { + tx.ScheduleBlockUpdate(pos, r, redstoneTicks(2)) + } + return r, false +} + +// ScheduledTick turns the lamp off if it is still unpowered. +func (r RedstoneLamp) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + if !r.Lit || tx.RedstonePower(pos) > 0 { + return } - r.Lit = lit - return r, true + r.Lit = false + tx.SetBlock(pos, r, nil) } // BreakInfo ... diff --git a/server/entity/ent.go b/server/entity/ent.go index 4d5f1a8e9..9d8fdcf16 100644 --- a/server/entity/ent.go +++ b/server/entity/ent.go @@ -147,6 +147,7 @@ func (e *Ent) Tick(tx *world.Tx, current int64) { } if m != nil { m.Send() + m.checkSteppers(tx) } if e.checkPortalInsiders() && e.finishPendingPortalTravel(tx) { return diff --git a/server/entity/movement.go b/server/entity/movement.go index ff7b83764..f4f7e2d37 100644 --- a/server/entity/movement.go +++ b/server/entity/movement.go @@ -1,6 +1,7 @@ package entity import ( + "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" @@ -43,6 +44,27 @@ func (m *Movement) Send() { } } +// checkSteppers calls EntityStepOn on the block the entity stands on after +// the movement, mirroring the behaviour of players for other entities. +func (m *Movement) checkSteppers(tx *world.Tx) { + if !m.onGround { + return + } + box := m.e.H().Type().BBox(m.e).Translate(m.pos).Grow(-0.0001) + low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) + y := int(math.Floor(box.Min()[1] - 0.0001)) + + for x := low[0]; x <= high[0]; x++ { + for z := low[2]; z <= high[2]; z++ { + pos := cube.Pos{x, y, z} + if stepper, ok := tx.Block(pos).(block.EntityStepper); ok { + stepper.EntityStepOn(pos, tx, m.e) + return + } + } + } +} + // Position returns the position as a result of the Movement as an mgl64.Vec3. func (m *Movement) Position() mgl64.Vec3 { return m.pos diff --git a/server/entity/projectile.go b/server/entity/projectile.go index 03d54d7bc..14b2656ce 100644 --- a/server/entity/projectile.go +++ b/server/entity/projectile.go @@ -206,6 +206,14 @@ func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { if h, ok := tx.Block(bpos).(block.ProjectileHitter); ok { h.ProjectileHit(bpos, tx, e, r.Face()) } + // Blocks without a collision box, such as buttons, are passed through + // by the trace, so the block of the cell the projectile comes to rest + // in is notified as well. + if rest := bpos.Side(r.Face()); rest != bpos { + if h, ok := tx.Block(rest).(block.ProjectileHitter); ok { + h.ProjectileHit(rest, tx, e, r.Face()) + } + } if lt.conf.SurviveBlockCollision { lt.hitBlockSurviving(e, r, m, tx) return m From 0aaddd1243b888fd5b420565ee3874bbe87853c9 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 14 Jul 2026 12:51:58 -0400 Subject: [PATCH 3/9] server: Fix projectile redstone interactions --- server/block/button.go | 5 ++-- server/block/redstone_test.go | 39 +++++++++++++++++++++++++++ server/entity/movement.go | 20 ++++++++------ server/entity/projectile.go | 12 +++++---- server/entity/projectile_test.go | 45 ++++++++++++++++++++++++++++++++ server/player/player.go | 24 +++-------------- 6 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 server/entity/projectile_test.go diff --git a/server/block/button.go b/server/block/button.go index 1aeb22871..eb0de4504 100644 --- a/server/block/button.go +++ b/server/block/button.go @@ -93,8 +93,9 @@ func (b Button) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { // arrowWithin reports whether an arrow intersects the block space at pos. func arrowWithin(pos cube.Pos, tx *world.Tx) bool { box := cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3()) - for e := range tx.EntitiesWithin(box) { - if e.H().Type().EncodeEntity() == "minecraft:arrow" { + for e := range tx.EntitiesWithin(box.Grow(1)) { + if e.H().Type().EncodeEntity() == "minecraft:arrow" && + e.H().Type().BBox(e).Translate(e.Position()).IntersectsWith(box) { return true } } diff --git a/server/block/redstone_test.go b/server/block/redstone_test.go index 8b5ebbe2f..7b914ffb5 100644 --- a/server/block/redstone_test.go +++ b/server/block/redstone_test.go @@ -17,6 +17,26 @@ func runWorld(w *world.World, f func(*world.Tx)) { w.Do(f).Wait(context.Background()) } +func TestWoodenButtonRemainsPressedWithArrowOnBoundary(t *testing.T) { + w := world.Config{Synchronous: true, Entities: redstoneArrowTestEntityRegistry()}.New() + defer w.Close() + + pos := cube.Pos{0, 64, 0} + var pressed bool + runWorld(w, func(tx *world.Tx) { + button := Button{Type: OakButton(), Facing: cube.FaceWest, Pressed: true} + tx.SetBlock(pos, button, nil) + tx.AddEntityAt(world.EntitySpawnOpts{}.New(redstoneArrowTestEntityType{}, redstoneTNTTestEntityConfig{}), mgl64.Vec3{1, 64.5, 0.5}) + + button.ScheduledTick(pos, tx, nil) + pressed = tx.Block(pos).(Button).Pressed + }) + + if !pressed { + t.Fatal("wooden button released while an arrow bounding box intersected its boundary") + } +} + func TestRedstoneWirePowersBlockBelowButNotAbove(t *testing.T) { wire := RedstoneWire{Power: 15} pos := cube.Pos{0, 64, 0} @@ -963,6 +983,10 @@ func redstoneBreakDropTestEntityRegistry() world.EntityRegistry { }.New([]world.EntityType{redstoneTNTTestEntityType{}}) } +func redstoneArrowTestEntityRegistry() world.EntityRegistry { + return world.EntityRegistryConfig{}.New([]world.EntityType{redstoneArrowTestEntityType{}}) +} + type redstoneTNTTestEntityConfig struct{} func (redstoneTNTTestEntityConfig) Apply(*world.EntityData) {} @@ -982,6 +1006,21 @@ func (redstoneTNTTestEntityType) EncodeNBT(*world.EntityData) map[string]any { return nil } +type redstoneArrowTestEntityType struct{} + +func (redstoneArrowTestEntityType) Open(_ *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { + return redstoneTNTTestEntity{handle: handle, data: data} +} + +func (redstoneArrowTestEntityType) EncodeEntity() string { return "minecraft:arrow" } +func (redstoneArrowTestEntityType) BBox(world.Entity) cube.BBox { + return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125) +} +func (redstoneArrowTestEntityType) DecodeNBT(map[string]any, *world.EntityData) {} +func (redstoneArrowTestEntityType) EncodeNBT(*world.EntityData) map[string]any { + return nil +} + type redstoneTNTTestEntity struct { handle *world.EntityHandle data *world.EntityData diff --git a/server/entity/movement.go b/server/entity/movement.go index f4f7e2d37..122042be9 100644 --- a/server/entity/movement.go +++ b/server/entity/movement.go @@ -44,13 +44,9 @@ func (m *Movement) Send() { } } -// checkSteppers calls EntityStepOn on the block the entity stands on after -// the movement, mirroring the behaviour of players for other entities. -func (m *Movement) checkSteppers(tx *world.Tx) { - if !m.onGround { - return - } - box := m.e.H().Type().BBox(m.e).Translate(m.pos).Grow(-0.0001) +// StepOnBlock calls EntityStepOn on the block beneath an entity at pos. +func StepOnBlock(tx *world.Tx, e world.Entity, pos mgl64.Vec3) { + box := e.H().Type().BBox(e).Translate(pos).Grow(-0.0001) low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) y := int(math.Floor(box.Min()[1] - 0.0001)) @@ -58,13 +54,21 @@ func (m *Movement) checkSteppers(tx *world.Tx) { for z := low[2]; z <= high[2]; z++ { pos := cube.Pos{x, y, z} if stepper, ok := tx.Block(pos).(block.EntityStepper); ok { - stepper.EntityStepOn(pos, tx, m.e) + stepper.EntityStepOn(pos, tx, e) return } } } } +// checkSteppers calls EntityStepOn on the block the entity stands on after +// the movement, mirroring the behaviour of players for other entities. +func (m *Movement) checkSteppers(tx *world.Tx) { + if m.onGround { + StepOnBlock(tx, m.e, m.pos) + } +} + // Position returns the position as a result of the Movement as an mgl64.Vec3. func (m *Movement) Position() mgl64.Vec3 { return m.pos diff --git a/server/entity/projectile.go b/server/entity/projectile.go index 14b2656ce..45601d2e0 100644 --- a/server/entity/projectile.go +++ b/server/entity/projectile.go @@ -338,13 +338,15 @@ func (lt *ProjectileBehaviour) tickMovement(e *Ent, tx *world.Tx) (*Movement, tr } var ( - end = pos.Add(vel) - hit trace.Result - ok bool + end = pos.Add(vel) + hit trace.Result + onGround bool + ok bool ) if !mgl64.FloatEqual(end.Sub(pos).LenSqr(), 0) { if hit, ok = trace.Perform(pos, end, tx, e.H().Type().BBox(e).Grow(1.0), lt.ignores(e)); ok { - if _, ok := hit.(trace.BlockResult); ok { + if r, ok := hit.(trace.BlockResult); ok { + onGround = r.Face() == cube.FaceUp // Undo the gravity because the velocity as a result of gravity // at the point of collision should be 0. vel[1] = (vel[1] + lt.mc.Gravity) / (1 - lt.mc.Drag) @@ -361,7 +363,7 @@ func (lt *ProjectileBehaviour) tickMovement(e *Ent, tx *world.Tx) (*Movement, tr end = hit.Position() } } - return &Movement{v: viewers, e: e, pos: end, vel: vel, dpos: end.Sub(pos), dvel: vel.Sub(velBefore), rot: rot}, hit + return &Movement{v: viewers, e: e, pos: end, vel: vel, dpos: end.Sub(pos), dvel: vel.Sub(velBefore), rot: rot, onGround: onGround}, hit } // ignores returns a function to ignore entities in trace.Perform that are diff --git a/server/entity/projectile_test.go b/server/entity/projectile_test.go new file mode 100644 index 000000000..484e81731 --- /dev/null +++ b/server/entity/projectile_test.go @@ -0,0 +1,45 @@ +package entity + +import ( + "testing" + + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +func TestProjectileActivatesPressurePlateOnTopFaceCollision(t *testing.T) { + for _, test := range []struct { + name string + plateType block.PressurePlateType + want int + }{ + {name: "wooden", plateType: block.OakPressurePlate(), want: 15}, + {name: "weighted", plateType: block.LightWeightedPressurePlate(), want: 1}, + } { + t.Run(test.name, func(t *testing.T) { + w := world.Config{Synchronous: true, Entities: DefaultRegistry}.New() + defer w.Close() + + platePos := cube.Pos{0, 64, 0} + mustDo(t, w, func(tx *world.Tx) { + tx.SetBlock(platePos.Side(cube.FaceDown), block.Stone{}, nil) + tx.SetBlock(platePos, block.PressurePlate{Type: test.plateType}, nil) + + conf := arrowConf + handle := world.EntitySpawnOpts{ + Position: mgl64.Vec3{0.5, 65, 0.5}, + Velocity: mgl64.Vec3{0, -1, 0}, + }.New(ArrowType, conf) + arrow := tx.AddEntity(handle).(*Ent) + arrow.Tick(tx, 0) + + plate := tx.Block(platePos).(block.PressurePlate) + if plate.Power != test.want { + t.Fatalf("pressure plate power after arrow collision = %d, want %d", plate.Power, test.want) + } + }) + }) + } +} diff --git a/server/player/player.go b/server/player/player.go index 71953a306..9dea3805f 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2596,7 +2596,9 @@ func (p *Player) Tick(tx *world.Tx, current int64) { p.checkBlockCollisions(p.data.Vel) p.onGround = p.checkOnGround(mgl64.Vec3{}) - p.checkEntitySteppers() + if p.OnGround() { + entity.StepOnBlock(p.tx, p, p.Position()) + } p.effects.Tick(p, p.tx) @@ -2933,26 +2935,6 @@ func (p *Player) checkEntityInsiders(entityBBox cube.BBox) { } } -// checkEntitySteppers checks if the player is standing on any EntityStepper blocks. -func (p *Player) checkEntitySteppers() { - if !p.OnGround() { - return - } - box := Type.BBox(p).Translate(p.Position()).Grow(-0.0001) - low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) - y := int(math.Floor(box.Min()[1] - 0.0001)) - - for x := low[0]; x <= high[0]; x++ { - for z := low[2]; z <= high[2]; z++ { - pos := cube.Pos{x, y, z} - if stepper, ok := p.tx.Block(pos).(block.EntityStepper); ok { - stepper.EntityStepOn(pos, p.tx, p) - return - } - } - } -} - // checkOnGround checks if the player is currently considered to be on the ground. func (p *Player) checkOnGround(deltaPos mgl64.Vec3) bool { box := Type.BBox(p).Translate(p.Position()).Extend(mgl64.Vec3{0, -0.05}).Extend(deltaPos.Mul(-1.0)) From 9dd314c3e27b97af05fd7afbddf8fdcee9e7fd08 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Fri, 17 Jul 2026 23:39:57 -0400 Subject: [PATCH 4/9] server: Fix redstone power source parity issues Redstone lamps now turn off after three redstone ticks (six game ticks) to match Bedrock; the previous two-tick delay was the Java value. Wooden buttons and pressure plates are now axe-effective, while stone, polished blackstone and weighted variants stay pickaxe-effective. Pressure plates recompute their power on the rising edge only and let the scheduled tick keep the weighted level current, matching the vanilla periodic re-check and avoiding a per-entity rescan each tick. The click sound now plays only when a plate first activates. Snowballs are no longer excluded from activating wooden and weighted plates, which react to any entity in vanilla. --- server/block/button.go | 6 +++++- server/block/pressure_plate.go | 27 +++++++++++++++------------ server/block/redstone_lamp.go | 4 ++-- server/entity/projectile.go | 7 +++---- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/server/block/button.go b/server/block/button.go index eb0de4504..2809c4938 100644 --- a/server/block/button.go +++ b/server/block/button.go @@ -120,7 +120,11 @@ func (b Button) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int // BreakInfo ... func (b Button) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(Button{Type: b.Type})) + effective := pickaxeEffective + if b.Type.Wood() { + effective = axeEffective + } + return newBreakInfo(0.5, alwaysHarvestable, effective, oneOf(Button{Type: b.Type})) } // SideClosed ... diff --git a/server/block/pressure_plate.go b/server/block/pressure_plate.go index 3ef6a5016..294049364 100644 --- a/server/block/pressure_plate.go +++ b/server/block/pressure_plate.go @@ -43,17 +43,19 @@ func (p PressurePlate) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx // EntityStepOn powers the plate when an entity stands on it. func (p PressurePlate) EntityStepOn(pos cube.Pos, tx *world.Tx, e world.Entity) { - power := p.entityPower(e) - if power == 0 { + if p.entityPower(e) == 0 { return } - if p.Type.Weighted() { - power = max(power, p.detectPower(pos, tx)) - } - if p.Power == power { + if p.Power > 0 { + // The plate is already active. Its scheduled tick keeps the (weighted) + // level current, so a stepping entity only needs to defer the release. tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) return } + power := p.stepPower() + if p.Type.Weighted() { + power = max(power, p.detectPower(pos, tx)) + } p.Power = power tx.SetBlock(pos, p, nil) tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) @@ -101,7 +103,11 @@ func (p PressurePlate) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Fa // BreakInfo ... func (p PressurePlate) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(PressurePlate{Type: p.Type})) + effective := pickaxeEffective + if p.Type.Wood() { + effective = axeEffective + } + return newBreakInfo(0.5, alwaysHarvestable, effective, oneOf(PressurePlate{Type: p.Type})) } // SideClosed ... @@ -144,12 +150,9 @@ func (p PressurePlate) entityPower(e world.Entity) int { } // detectsEntity reports whether an entity activates the plate. Stone-like -// plates only react to living entities, players and armour stands; snowballs -// never activate a plate. +// plates only react to living entities, players and armour stands; wooden and +// weighted plates react to any entity. func (p PressurePlate) detectsEntity(e world.Entity) bool { - if pressurePlateEntityName(e) == "minecraft:snowball" { - return false - } if !p.Type.Wood() && !p.Type.Weighted() { return pressurePlateStoneEntity(e) } diff --git a/server/block/redstone_lamp.go b/server/block/redstone_lamp.go index 0e4676fbb..12664e086 100644 --- a/server/block/redstone_lamp.go +++ b/server/block/redstone_lamp.go @@ -24,7 +24,7 @@ func (r RedstoneLamp) LightEmissionLevel() uint8 { } // RedstonePowerUpdate lights the lamp as soon as it is powered. Turning off -// is delayed by two redstone ticks, keeping the lamp lit through short pulses. +// is delayed by three redstone ticks, keeping the lamp lit through short pulses. func (r RedstoneLamp) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, power int) (world.Block, bool) { if power > 0 { if r.Lit { @@ -34,7 +34,7 @@ func (r RedstoneLamp) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, power int) return r, true } if r.Lit { - tx.ScheduleBlockUpdate(pos, r, redstoneTicks(2)) + tx.ScheduleBlockUpdate(pos, r, redstoneTicks(3)) } return r, false } diff --git a/server/entity/projectile.go b/server/entity/projectile.go index 45601d2e0..c00806b22 100644 --- a/server/entity/projectile.go +++ b/server/entity/projectile.go @@ -209,10 +209,9 @@ func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { // Blocks without a collision box, such as buttons, are passed through // by the trace, so the block of the cell the projectile comes to rest // in is notified as well. - if rest := bpos.Side(r.Face()); rest != bpos { - if h, ok := tx.Block(rest).(block.ProjectileHitter); ok { - h.ProjectileHit(rest, tx, e, r.Face()) - } + rest := bpos.Side(r.Face()) + if h, ok := tx.Block(rest).(block.ProjectileHitter); ok { + h.ProjectileHit(rest, tx, e, r.Face()) } if lt.conf.SurviveBlockCollision { lt.hitBlockSurviving(e, r, m, tx) From 721a0fb0e501d6f73f4e209d91d77b6446a10746 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Sun, 19 Jul 2026 21:30:01 -0400 Subject: [PATCH 5/9] server: Fix redstone source vanilla parity --- server/block/button.go | 52 ++++++++++++++++++++++++++++------ server/block/pressure_plate.go | 10 +++---- server/entity/movement.go | 11 +++++-- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/server/block/button.go b/server/block/button.go index 2809c4938..f86b08f0d 100644 --- a/server/block/button.go +++ b/server/block/button.go @@ -51,7 +51,7 @@ func (b Button) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ // ProjectileHit presses wooden buttons hit by an arrow. func (b Button) ProjectileHit(pos cube.Pos, tx *world.Tx, e world.Entity, _ cube.Face) { - if !b.Type.Wood() || e.H().Type().EncodeEntity() != "minecraft:arrow" { + if !b.Type.Wood() || e.H().Type().EncodeEntity() != "minecraft:arrow" || !buttonArrowIntersects(b, pos, e) { return } b.press(pos, tx) @@ -81,7 +81,7 @@ func (b Button) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { if !b.Pressed { return } - if b.Type.Wood() && arrowWithin(pos, tx) { + if b.Type.Wood() && arrowWithin(b, pos, tx) { tx.ScheduleBlockUpdate(pos, b, b.pressDuration()) return } @@ -90,18 +90,52 @@ func (b Button) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { tx.PlaySound(pos.Vec3Centre(), sound.Click{}) } -// arrowWithin reports whether an arrow intersects the block space at pos. -func arrowWithin(pos cube.Pos, tx *world.Tx) bool { - box := cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3()) +// arrowWithin reports whether an arrow intersects the button at pos. +func arrowWithin(b Button, pos cube.Pos, tx *world.Tx) bool { + box := buttonBox(b).Translate(pos.Vec3()) for e := range tx.EntitiesWithin(box.Grow(1)) { - if e.H().Type().EncodeEntity() == "minecraft:arrow" && - e.H().Type().BBox(e).Translate(e.Position()).IntersectsWith(box) { + if e.H().Type().EncodeEntity() == "minecraft:arrow" && buttonArrowIntersects(b, pos, e) { return true } } return false } +func buttonArrowIntersects(b Button, pos cube.Pos, e world.Entity) bool { + return e.H().Type().BBox(e).Translate(e.Position()).IntersectsWith(buttonBox(b).Translate(pos.Vec3())) +} + +// buttonBox returns the projectile-sensitive shape of a button. Buttons have +// no physical collision box, but projectiles must touch their visible shape. +func buttonBox(b Button) cube.BBox { + const ( + minLong = 5.0 / 16 + maxLong = 11.0 / 16 + minShort = 6.0 / 16 + maxShort = 10.0 / 16 + ) + depth := 2.0 / 16 + if b.Pressed { + depth = 1.0 / 16 + } + switch b.Facing { + case cube.FaceDown: + return cube.Box(minLong, 1-depth, minShort, maxLong, 1, maxShort) + case cube.FaceUp: + return cube.Box(minLong, 0, minShort, maxLong, depth, maxShort) + case cube.FaceNorth: + return cube.Box(minLong, minShort, 1-depth, maxLong, maxShort, 1) + case cube.FaceSouth: + return cube.Box(minLong, minShort, 0, maxLong, maxShort, depth) + case cube.FaceWest: + return cube.Box(1-depth, minShort, minLong, 1, maxShort, maxLong) + case cube.FaceEast: + return cube.Box(0, minShort, minLong, depth, maxShort, maxLong) + default: + panic("invalid button face") + } +} + // RedstonePower returns maximum power while the button is pressed. func (b Button) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { if b.Pressed { @@ -121,10 +155,12 @@ func (b Button) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int // BreakInfo ... func (b Button) BreakInfo() BreakInfo { effective := pickaxeEffective + harvestable := pickaxeHarvestable if b.Type.Wood() { effective = axeEffective + harvestable = alwaysHarvestable } - return newBreakInfo(0.5, alwaysHarvestable, effective, oneOf(Button{Type: b.Type})) + return newBreakInfo(0.5, harvestable, effective, oneOf(Button{Type: b.Type})) } // SideClosed ... diff --git a/server/block/pressure_plate.go b/server/block/pressure_plate.go index 294049364..3f6c31880 100644 --- a/server/block/pressure_plate.go +++ b/server/block/pressure_plate.go @@ -28,7 +28,7 @@ type PressurePlate struct { // Model ... func (PressurePlate) Model() world.BlockModel { - return model.Carpet{} + return model.Empty{} } // UseOnBlock places the pressure plate on a solid surface. @@ -41,9 +41,9 @@ func (p PressurePlate) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx return placed(ctx) } -// EntityStepOn powers the plate when an entity stands on it. -func (p PressurePlate) EntityStepOn(pos cube.Pos, tx *world.Tx, e world.Entity) { - if p.entityPower(e) == 0 { +// EntityInside powers the plate when an entity enters its activation area. +func (p PressurePlate) EntityInside(pos cube.Pos, tx *world.Tx, e world.Entity) { + if p.entityPower(e) == 0 || !pressurePlateEntityIntersects(e, pressurePlateActivationBox(pos)) { return } if p.Power > 0 { @@ -236,7 +236,7 @@ func pressurePlateEntityName(e world.Entity) string { // pressurePlateActivationBox is the box entities must intersect to press the // plate at a position. func pressurePlateActivationBox(pos cube.Pos) cube.BBox { - return cube.Box(float64(pos[0]), float64(pos[1]), float64(pos[2]), float64(pos[0]+1), float64(pos[1])+0.25, float64(pos[2]+1)) + return cube.Box(float64(pos[0])+0.125, float64(pos[1]), float64(pos[2])+0.125, float64(pos[0])+0.875, float64(pos[1])+0.25, float64(pos[2])+0.875) } func pressurePlateEntityIntersects(e world.Entity, box cube.BBox) bool { diff --git a/server/entity/movement.go b/server/entity/movement.go index 122042be9..c1df017f1 100644 --- a/server/entity/movement.go +++ b/server/entity/movement.go @@ -61,9 +61,16 @@ func StepOnBlock(tx *world.Tx, e world.Entity, pos mgl64.Vec3) { } } -// checkSteppers calls EntityStepOn on the block the entity stands on after -// the movement, mirroring the behaviour of players for other entities. +// checkSteppers handles pressure plates intersecting the entity and the block +// it stands on after movement, mirroring player behaviour. func (m *Movement) checkSteppers(tx *world.Tx) { + box := m.e.H().Type().BBox(m.e).Translate(m.pos).Grow(-0.0001) + low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) + for pos := range cube.Range3D(low, high) { + if plate, ok := tx.Block(pos).(block.PressurePlate); ok { + plate.EntityInside(pos, tx, m.e) + } + } if m.onGround { StepOnBlock(tx, m.e, m.pos) } From 86a03f764ef64279029d5a277cba56813cb1ec20 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 20 Jul 2026 01:07:21 -0400 Subject: [PATCH 6/9] server/entity: Remove projectile pressure plate test --- server/entity/projectile_test.go | 45 -------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 server/entity/projectile_test.go diff --git a/server/entity/projectile_test.go b/server/entity/projectile_test.go deleted file mode 100644 index 484e81731..000000000 --- a/server/entity/projectile_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package entity - -import ( - "testing" - - "github.com/df-mc/dragonfly/server/block" - "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" -) - -func TestProjectileActivatesPressurePlateOnTopFaceCollision(t *testing.T) { - for _, test := range []struct { - name string - plateType block.PressurePlateType - want int - }{ - {name: "wooden", plateType: block.OakPressurePlate(), want: 15}, - {name: "weighted", plateType: block.LightWeightedPressurePlate(), want: 1}, - } { - t.Run(test.name, func(t *testing.T) { - w := world.Config{Synchronous: true, Entities: DefaultRegistry}.New() - defer w.Close() - - platePos := cube.Pos{0, 64, 0} - mustDo(t, w, func(tx *world.Tx) { - tx.SetBlock(platePos.Side(cube.FaceDown), block.Stone{}, nil) - tx.SetBlock(platePos, block.PressurePlate{Type: test.plateType}, nil) - - conf := arrowConf - handle := world.EntitySpawnOpts{ - Position: mgl64.Vec3{0.5, 65, 0.5}, - Velocity: mgl64.Vec3{0, -1, 0}, - }.New(ArrowType, conf) - arrow := tx.AddEntity(handle).(*Ent) - arrow.Tick(tx, 0) - - plate := tx.Block(platePos).(block.PressurePlate) - if plate.Power != test.want { - t.Fatalf("pressure plate power after arrow collision = %d, want %d", plate.Power, test.want) - } - }) - }) - } -} From 093ce679594fac700af6bc5646e1d50a0e218efd Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 20 Jul 2026 01:10:36 -0400 Subject: [PATCH 7/9] server/block: Share attachment support checks --- server/block/block.go | 10 ++++++++++ server/block/button.go | 4 ++-- server/block/pressure_plate.go | 4 ++-- server/block/redstone.go | 26 -------------------------- 4 files changed, 14 insertions(+), 30 deletions(-) delete mode 100644 server/block/redstone.go diff --git a/server/block/block.go b/server/block/block.go index 044f65a81..7f1915422 100644 --- a/server/block/block.go +++ b/server/block/block.go @@ -178,6 +178,16 @@ func firstReplaceable(tx *world.Tx, pos cube.Pos, face cube.Face, with world.Blo return pos, face, false } +// attachmentSupported reports whether the block at pos may attach to the +// adjacent block through face. +func attachmentSupported(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + support := pos.Side(face.Opposite()) + if support.OutOfBounds(tx.Range()) { + return false + } + return tx.Block(support).Model().FaceSolid(support, face, tx) +} + // place places the block passed at the position passed. If the user implements the block.Placer interface, it // will use its PlaceBlock method. If not, the block is placed without interaction from the user. func place(tx *world.Tx, pos cube.Pos, b world.Block, user item.User, ctx *item.UseContext) { diff --git a/server/block/button.go b/server/block/button.go index f86b08f0d..de761af26 100644 --- a/server/block/button.go +++ b/server/block/button.go @@ -35,7 +35,7 @@ func (Button) Model() world.BlockModel { // UseOnBlock places the button attached to the clicked face. func (b Button) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, face, used := firstReplaceable(tx, pos, face, b) - if !used || !redstoneAttachmentSupported(tx, pos, face) { + if !used || !attachmentSupported(tx, pos, face) { return false } b.Facing = face @@ -70,7 +70,7 @@ func (b Button) press(pos cube.Pos, tx *world.Tx) { // NeighbourUpdateTick breaks the button if its supporting block is removed. func (b Button) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { - if !redstoneAttachmentSupported(tx, pos, b.Facing) { + if !attachmentSupported(tx, pos, b.Facing) { breakBlock(b, pos, tx) } } diff --git a/server/block/pressure_plate.go b/server/block/pressure_plate.go index 3f6c31880..730e9d3fc 100644 --- a/server/block/pressure_plate.go +++ b/server/block/pressure_plate.go @@ -34,7 +34,7 @@ func (PressurePlate) Model() world.BlockModel { // UseOnBlock places the pressure plate on a solid surface. func (p PressurePlate) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, _, used := firstReplaceable(tx, pos, face, p) - if !used || !redstoneFloorComponentSupported(tx, pos) { + if !used || !attachmentSupported(tx, pos, cube.FaceUp) { return false } place(tx, pos, p, user, ctx) @@ -64,7 +64,7 @@ func (p PressurePlate) EntityInside(pos cube.Pos, tx *world.Tx, e world.Entity) // NeighbourUpdateTick breaks the pressure plate if its supporting block is removed. func (p PressurePlate) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { - if !redstoneFloorComponentSupported(tx, pos) { + if !attachmentSupported(tx, pos, cube.FaceUp) { breakBlock(p, pos, tx) } } diff --git a/server/block/redstone.go b/server/block/redstone.go deleted file mode 100644 index 66d0c5894..000000000 --- a/server/block/redstone.go +++ /dev/null @@ -1,26 +0,0 @@ -package block - -import ( - "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/world" -) - -// redstoneAttachmentSupported reports whether a redstone component may attach -// to the block behind the face passed. -func redstoneAttachmentSupported(tx *world.Tx, pos cube.Pos, face cube.Face) bool { - support := pos.Side(face.Opposite()) - if support.OutOfBounds(tx.Range()) { - return false - } - return tx.Block(support).Model().FaceSolid(support, face, tx) -} - -// redstoneFloorComponentSupported reports whether a floor-mounted redstone -// component is supported by the block below it. -func redstoneFloorComponentSupported(tx *world.Tx, pos cube.Pos) bool { - support := pos.Side(cube.FaceDown) - if support.OutOfBounds(tx.Range()) { - return false - } - return tx.Block(support).Model().FaceSolid(support, cube.FaceUp, tx) -} From d2d62348585a78614f2ee7fe412f09216fe5a591 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 01:06:49 -0400 Subject: [PATCH 8/9] server: simplify redstone source implementation --- server/block/block.go | 6 ++ server/block/button.go | 58 +++++-------- server/block/button_type.go | 8 +- server/block/pressure_plate.go | 127 ++++++++-------------------- server/block/pressure_plate_type.go | 6 +- server/block/redstone_lamp.go | 15 ++-- server/block/redstone_test.go | 29 +++++-- server/block/redstone_wire.go | 13 +-- server/block/register.go | 20 ++--- server/entity/movement.go | 15 ++-- server/entity/projectile.go | 9 +- 11 files changed, 123 insertions(+), 183 deletions(-) diff --git a/server/block/block.go b/server/block/block.go index 7f1915422..e13947979 100644 --- a/server/block/block.go +++ b/server/block/block.go @@ -188,6 +188,12 @@ func attachmentSupported(tx *world.Tx, pos cube.Pos, face cube.Face) bool { return tx.Block(support).Model().FaceSolid(support, face, tx) } +// entityIntersects reports whether the bounding box of the entity passed +// overlaps the box passed. +func entityIntersects(e world.Entity, box cube.BBox) bool { + return e.H().Type().BBox(e).Translate(e.Position()).IntersectsWith(box) +} + // place places the block passed at the position passed. If the user implements the block.Placer interface, it // will use its PlaceBlock method. If not, the block is placed without interaction from the user. func place(tx *world.Tx, pos cube.Pos, b world.Block, user item.User, ctx *item.UseContext) { diff --git a/server/block/button.go b/server/block/button.go index de761af26..6230aa24e 100644 --- a/server/block/button.go +++ b/server/block/button.go @@ -5,7 +5,6 @@ import ( "time" "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/df-mc/dragonfly/server/world/sound" @@ -27,11 +26,6 @@ type Button struct { Pressed bool } -// Model ... -func (Button) Model() world.BlockModel { - return model.Empty{} -} - // UseOnBlock places the button attached to the clicked face. func (b Button) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, face, used := firstReplaceable(tx, pos, face, b) @@ -51,10 +45,9 @@ func (b Button) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ // ProjectileHit presses wooden buttons hit by an arrow. func (b Button) ProjectileHit(pos cube.Pos, tx *world.Tx, e world.Entity, _ cube.Face) { - if !b.Type.Wood() || e.H().Type().EncodeEntity() != "minecraft:arrow" || !buttonArrowIntersects(b, pos, e) { - return + if b.Type.Wood() && b.arrowIntersects(e, buttonBox(b).Translate(pos.Vec3())) { + b.press(pos, tx) } - b.press(pos, tx) } // press activates an unpressed button and schedules its release. @@ -81,7 +74,7 @@ func (b Button) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { if !b.Pressed { return } - if b.Type.Wood() && arrowWithin(b, pos, tx) { + if b.Type.Wood() && b.arrowWithin(pos, tx) { tx.ScheduleBlockUpdate(pos, b, b.pressDuration()) return } @@ -91,49 +84,40 @@ func (b Button) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { } // arrowWithin reports whether an arrow intersects the button at pos. -func arrowWithin(b Button, pos cube.Pos, tx *world.Tx) bool { +func (b Button) arrowWithin(pos cube.Pos, tx *world.Tx) bool { box := buttonBox(b).Translate(pos.Vec3()) for e := range tx.EntitiesWithin(box.Grow(1)) { - if e.H().Type().EncodeEntity() == "minecraft:arrow" && buttonArrowIntersects(b, pos, e) { + if b.arrowIntersects(e, box) { return true } } return false } -func buttonArrowIntersects(b Button, pos cube.Pos, e world.Entity) bool { - return e.H().Type().BBox(e).Translate(e.Position()).IntersectsWith(buttonBox(b).Translate(pos.Vec3())) +// arrowIntersects reports whether an entity is an arrow overlapping the box passed. +func (Button) arrowIntersects(e world.Entity, box cube.BBox) bool { + return e.H().Type().EncodeEntity() == "minecraft:arrow" && entityIntersects(e, box) } -// buttonBox returns the projectile-sensitive shape of a button. Buttons have -// no physical collision box, but projectiles must touch their visible shape. +// buttonBox returns the projectile-sensitive shape of a button: a 6x4 pane +// centred on the face it is attached to, protruding out of it. Buttons have no +// physical collision box, but projectiles must touch their visible shape. func buttonBox(b Button) cube.BBox { - const ( - minLong = 5.0 / 16 - maxLong = 11.0 / 16 - minShort = 6.0 / 16 - maxShort = 10.0 / 16 - ) depth := 2.0 / 16 if b.Pressed { depth = 1.0 / 16 } - switch b.Facing { - case cube.FaceDown: - return cube.Box(minLong, 1-depth, minShort, maxLong, 1, maxShort) - case cube.FaceUp: - return cube.Box(minLong, 0, minShort, maxLong, depth, maxShort) - case cube.FaceNorth: - return cube.Box(minLong, minShort, 1-depth, maxLong, maxShort, 1) - case cube.FaceSouth: - return cube.Box(minLong, minShort, 0, maxLong, maxShort, depth) - case cube.FaceWest: - return cube.Box(1-depth, minShort, minLong, 1, maxShort, maxLong) - case cube.FaceEast: - return cube.Box(0, minShort, minLong, depth, maxShort, maxLong) - default: - panic("invalid button face") + long, short := cube.X, cube.Z + switch b.Facing.Axis() { + case cube.X: + long, short = cube.Z, cube.Y + case cube.Z: + short = cube.Y } + return cube.Box(0.5, 0.5, 0.5, 0.5, 0.5, 0.5). + Stretch(long, 3.0/16).Stretch(short, 2.0/16). + TranslateTowards(b.Facing.Opposite(), 0.5). + ExtendTowards(b.Facing, depth) } // RedstonePower returns maximum power while the button is pressed. diff --git a/server/block/button_type.go b/server/block/button_type.go index c7cb647a9..b3f45520d 100644 --- a/server/block/button_type.go +++ b/server/block/button_type.go @@ -85,7 +85,7 @@ func (b button) Uint8() uint8 { // Wood reports whether the button is made of wood, giving it a longer press // duration and letting arrows press it. func (b button) Wood() bool { - return b >= 2 + return b >= 2 && b <= 13 } // Flammable reports whether the button can burn, making it usable as furnace @@ -167,9 +167,5 @@ func (b button) String() string { // ButtonTypes ... func ButtonTypes() []ButtonType { - types := make([]ButtonType, 14) - for i := range types { - types[i] = ButtonType{button(i)} - } - return types + return []ButtonType{StoneButton(), PolishedBlackstoneButton(), OakButton(), SpruceButton(), BirchButton(), JungleButton(), AcaciaButton(), DarkOakButton(), MangroveButton(), CherryButton(), BambooButton(), CrimsonButton(), WarpedButton(), PaleOakButton()} } diff --git a/server/block/pressure_plate.go b/server/block/pressure_plate.go index 730e9d3fc..f10381e23 100644 --- a/server/block/pressure_plate.go +++ b/server/block/pressure_plate.go @@ -5,7 +5,6 @@ import ( "time" "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/df-mc/dragonfly/server/world/sound" @@ -26,11 +25,6 @@ type PressurePlate struct { Power int } -// Model ... -func (PressurePlate) Model() world.BlockModel { - return model.Empty{} -} - // UseOnBlock places the pressure plate on a solid surface. func (p PressurePlate) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, _, used := firstReplaceable(tx, pos, face, p) @@ -43,7 +37,7 @@ func (p PressurePlate) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx // EntityInside powers the plate when an entity enters its activation area. func (p PressurePlate) EntityInside(pos cube.Pos, tx *world.Tx, e world.Entity) { - if p.entityPower(e) == 0 || !pressurePlateEntityIntersects(e, pressurePlateActivationBox(pos)) { + if !p.detects(e) || !entityIntersects(e, pressurePlateActivationBox(pos)) { return } if p.Power > 0 { @@ -52,9 +46,9 @@ func (p PressurePlate) EntityInside(pos cube.Pos, tx *world.Tx, e world.Entity) tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) return } - power := p.stepPower() + power := 15 if p.Type.Weighted() { - power = max(power, p.detectPower(pos, tx)) + power = max(1, p.detectPower(pos, tx)) } p.Power = power tx.SetBlock(pos, p, nil) @@ -130,78 +124,51 @@ func (p PressurePlate) EncodeItem() (name string, meta int16) { // EncodeBlock ... func (p PressurePlate) EncodeBlock() (string, map[string]any) { - return "minecraft:" + p.Type.String(), map[string]any{"redstone_signal": int32(max(0, min(p.Power, 15)))} -} - -// stepPower is the power a single detected entity contributes: the first -// analog level for weighted plates and full power otherwise. -func (p PressurePlate) stepPower() int { - if p.Type.Weighted() { - return 1 - } - return 15 + return "minecraft:" + p.Type.String(), map[string]any{"redstone_signal": int32(world.ClampRedstonePower(p.Power))} } -func (p PressurePlate) entityPower(e world.Entity) int { - if !p.detectsEntity(e) { - return 0 +// detects reports whether an entity activates the plate. Stone-like plates only +// react to living entities and armour stands; wooden and weighted plates react +// to any entity. +func (p PressurePlate) detects(e world.Entity) bool { + if p.Type.Wood() || p.Type.Weighted() { + return true } - return p.stepPower() -} - -// detectsEntity reports whether an entity activates the plate. Stone-like -// plates only react to living entities, players and armour stands; wooden and -// weighted plates react to any entity. -func (p PressurePlate) detectsEntity(e world.Entity) bool { - if !p.Type.Wood() && !p.Type.Weighted() { - return pressurePlateStoneEntity(e) + if living, ok := e.(pressurePlateLivingEntity); ok { + return !living.Dead() } - return true + return e.H().Type().EncodeEntity() == "minecraft:armor_stand" } -// detectPower scans the entities intersecting the plate's activation box and -// returns the power level they produce. -func (p PressurePlate) detectPower(pos cube.Pos, tx *world.Tx) int { - box := pressurePlateActivationBox(pos) - entities := 0 +// entitiesOn counts the entities intersecting the plate's activation box, +// stopping early once limit is reached. +func (p PressurePlate) entitiesOn(pos cube.Pos, tx *world.Tx, limit int) int { + box, n := pressurePlateActivationBox(pos), 0 for e := range tx.EntitiesWithin(box.Grow(1)) { - if p.entityPower(e) == 0 || !pressurePlateEntityIntersects(e, box) { + if !p.detects(e) || !entityIntersects(e, box) { continue } - if !p.Type.Weighted() { - return 15 - } - entities++ - if entities >= p.weightedMaxEntities() { - return 15 + if n++; n >= limit { + break } } - if p.Type.Weighted() { - return p.weightedPower(entities) - } - return 0 + return n } -// weightedPower converts an entity count to the analog power of a weighted -// plate: one level per entity for light plates and per ten entities, rounded -// up, for heavy plates. -func (p PressurePlate) weightedPower(entities int) int { - if entities <= 0 { - return 0 - } - if p.Type == HeavyWeightedPressurePlate() { - return min(15, (entities+9)/10) +// detectPower returns the power level the entities on the plate produce. +// Weighted plates emit one level per entity, or per ten entities rounded up for +// the heavy variant; every other plate emits full power for any entity at all. +func (p PressurePlate) detectPower(pos cube.Pos, tx *world.Tx) int { + switch p.Type { + case LightWeightedPressurePlate(): + return p.entitiesOn(pos, tx, 15) + case HeavyWeightedPressurePlate(): + return (p.entitiesOn(pos, tx, 150) + 9) / 10 } - return min(15, entities) -} - -// weightedMaxEntities is the entity count at which a weighted plate reaches -// full power, so scanning may stop early. -func (p PressurePlate) weightedMaxEntities() int { - if p.Type == HeavyWeightedPressurePlate() { - return 141 + if p.entitiesOn(pos, tx, 1) > 0 { + return 15 } - return 15 + return 0 } // releaseDelay is the delay before the plate re-checks its entities: 0.5 @@ -213,38 +180,18 @@ func (p PressurePlate) releaseDelay() time.Duration { return time.Second } +// pressurePlateLivingEntity is implemented by entities that can die. Health is +// part of the interface so that only entities with a full health state match, +// even though Dead alone decides whether the plate reacts. type pressurePlateLivingEntity interface { Health() float64 Dead() bool } -func pressurePlateStoneEntity(e world.Entity) bool { - if living, ok := e.(pressurePlateLivingEntity); ok { - return living.Health() > 0 && !living.Dead() - } - return pressurePlateEntityName(e) == "minecraft:player" || pressurePlateEntityName(e) == "minecraft:armor_stand" -} - -func pressurePlateEntityName(e world.Entity) string { - h := e.H() - if h == nil || h.Type() == nil { - return "" - } - return h.Type().EncodeEntity() -} - // pressurePlateActivationBox is the box entities must intersect to press the // plate at a position. func pressurePlateActivationBox(pos cube.Pos) cube.BBox { - return cube.Box(float64(pos[0])+0.125, float64(pos[1]), float64(pos[2])+0.125, float64(pos[0])+0.875, float64(pos[1])+0.25, float64(pos[2])+0.875) -} - -func pressurePlateEntityIntersects(e world.Entity, box cube.BBox) bool { - h := e.H() - if h == nil || h.Type() == nil { - return false - } - return h.Type().BBox(e).Translate(e.Position()).IntersectsWith(box) + return cube.Box(0.125, 0, 0.125, 0.875, 0.25, 0.875).Translate(pos.Vec3()) } // allPressurePlates ... diff --git a/server/block/pressure_plate_type.go b/server/block/pressure_plate_type.go index 4a84e3016..08f240427 100644 --- a/server/block/pressure_plate_type.go +++ b/server/block/pressure_plate_type.go @@ -194,9 +194,5 @@ func (p pressurePlate) String() string { // PressurePlateTypes ... func PressurePlateTypes() []PressurePlateType { - types := make([]PressurePlateType, 16) - for i := range types { - types[i] = PressurePlateType{pressurePlate(i)} - } - return types + return []PressurePlateType{StonePressurePlate(), PolishedBlackstonePressurePlate(), OakPressurePlate(), SprucePressurePlate(), BirchPressurePlate(), JunglePressurePlate(), AcaciaPressurePlate(), DarkOakPressurePlate(), MangrovePressurePlate(), CherryPressurePlate(), BambooPressurePlate(), CrimsonPressurePlate(), WarpedPressurePlate(), PaleOakPressurePlate(), LightWeightedPressurePlate(), HeavyWeightedPressurePlate()} } diff --git a/server/block/redstone_lamp.go b/server/block/redstone_lamp.go index 12664e086..23c400c44 100644 --- a/server/block/redstone_lamp.go +++ b/server/block/redstone_lamp.go @@ -26,17 +26,16 @@ func (r RedstoneLamp) LightEmissionLevel() uint8 { // RedstonePowerUpdate lights the lamp as soon as it is powered. Turning off // is delayed by three redstone ticks, keeping the lamp lit through short pulses. func (r RedstoneLamp) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, power int) (world.Block, bool) { - if power > 0 { - if r.Lit { - return r, false - } - r.Lit = true - return r, true + lit := power > 0 + if lit == r.Lit { + return r, false } - if r.Lit { + if !lit { tx.ScheduleBlockUpdate(pos, r, redstoneTicks(3)) + return r, false } - return r, false + r.Lit = true + return r, true } // ScheduledTick turns the lamp off if it is still unpowered. diff --git a/server/block/redstone_test.go b/server/block/redstone_test.go index 7b914ffb5..0a205f1b3 100644 --- a/server/block/redstone_test.go +++ b/server/block/redstone_test.go @@ -18,7 +18,7 @@ func runWorld(w *world.World, f func(*world.Tx)) { } func TestWoodenButtonRemainsPressedWithArrowOnBoundary(t *testing.T) { - w := world.Config{Synchronous: true, Entities: redstoneArrowTestEntityRegistry()}.New() + w := world.Config{Synchronous: true}.New() defer w.Close() pos := cube.Pos{0, 64, 0} @@ -37,6 +37,29 @@ func TestWoodenButtonRemainsPressedWithArrowOnBoundary(t *testing.T) { } } +func TestWeightedPressurePlateCountsEntities(t *testing.T) { + for _, count := range []int{0, 1, 3} { + w := world.Config{Synchronous: true}.New() + + pos := cube.Pos{0, 64, 0} + var power int + runWorld(w, func(tx *world.Tx) { + plate := PressurePlate{Type: LightWeightedPressurePlate()} + tx.SetBlock(pos, plate, nil) + for i := range count { + tx.AddEntityAt(world.EntitySpawnOpts{}.New(redstoneArrowTestEntityType{}, redstoneTNTTestEntityConfig{}), + mgl64.Vec3{0.5, 64.05, 0.5 + float64(i)*0.01}) + } + power = plate.detectPower(pos, tx) + }) + _ = w.Close() + + if power != count { + t.Fatalf("light weighted plate with %v entities: want power %v, got %v", count, count, power) + } + } +} + func TestRedstoneWirePowersBlockBelowButNotAbove(t *testing.T) { wire := RedstoneWire{Power: 15} pos := cube.Pos{0, 64, 0} @@ -983,10 +1006,6 @@ func redstoneBreakDropTestEntityRegistry() world.EntityRegistry { }.New([]world.EntityType{redstoneTNTTestEntityType{}}) } -func redstoneArrowTestEntityRegistry() world.EntityRegistry { - return world.EntityRegistryConfig{}.New([]world.EntityType{redstoneArrowTestEntityType{}}) -} - type redstoneTNTTestEntityConfig struct{} func (redstoneTNTTestEntityConfig) Apply(*world.EntityData) {} diff --git a/server/block/redstone_wire.go b/server/block/redstone_wire.go index 9a9337ff3..4298cde1d 100644 --- a/server/block/redstone_wire.go +++ b/server/block/redstone_wire.go @@ -23,7 +23,7 @@ type RedstoneWire struct { func (r RedstoneWire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, _, used := firstReplaceable(tx, pos, face, r) - if !used || !redstoneWireSupported(tx, pos) { + if !used || !attachmentSupported(tx, pos, cube.FaceUp) { return false } place(tx, pos, r, user, ctx) @@ -84,7 +84,7 @@ func (r RedstoneWire) RedstonePowerUpdate(_ cube.Pos, _ *world.Tx, power int) (w } func (r RedstoneWire) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { - if !redstoneWireSupported(tx, pos) { + if !attachmentSupported(tx, pos, cube.FaceUp) { breakBlock(r, pos, tx) } } @@ -131,15 +131,6 @@ func redstoneTicks(ticks int) time.Duration { return time.Duration(max(ticks, 1)) * time.Second / 10 } -// redstoneWireSupported reports whether redstone wire can stay placed at pos. -func redstoneWireSupported(tx *world.Tx, pos cube.Pos) bool { - below := pos.Side(cube.FaceDown) - if below.OutOfBounds(tx.Range()) { - return false - } - return tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) -} - // redstoneWireSupportedLoaded checks support without loading neighbouring chunks. func redstoneWireSupportedLoaded(tx *world.Tx, pos cube.Pos) bool { below := pos.Side(cube.FaceDown) diff --git a/server/block/register.go b/server/block/register.go index d7ee03455..4bafd5868 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -167,6 +167,7 @@ func init() { registerAll(allBlastFurnaces()) registerAll(allBoneBlock()) registerAll(allBrewingStands()) + registerAll(allButtons()) registerAll(allCactus()) registerAll(allCake()) registerAll(allCampfires()) @@ -203,12 +204,9 @@ func init() { registerAll(allLadders()) registerAll(allLanterns()) registerAll(allLava()) - registerAll(allButtons()) registerAll(allLeaves()) registerAll(allLecterns()) registerAll(allLevers()) - registerAll(allPressurePlates()) - registerAll(allRedstoneLamps()) registerAll(allLight()) registerAll(allLitPumpkins()) registerAll(allLogs()) @@ -220,11 +218,13 @@ func init() { registerAll(allPinkPetals()) registerAll(allPlanks()) registerAll(allPotato()) + registerAll(allPressurePlates()) registerAll(allPrismarine()) registerAll(allPumpkinStems()) registerAll(allPumpkins()) registerAll(allPurpurs()) registerAll(allQuartz()) + registerAll(allRedstoneLamps()) registerAll(allRedstoneTorches()) registerAll(allRedstoneWires()) registerAll(allSandstones()) @@ -285,6 +285,9 @@ func init() { world.RegisterItem(Bookshelf{}) world.RegisterItem(BrewingStand{}) world.RegisterItem(Bricks{}) + for _, t := range ButtonTypes() { + world.RegisterItem(Button{Type: t}) + } world.RegisterItem(Cactus{}) world.RegisterItem(Cake{}) world.RegisterItem(Calcite{}) @@ -358,13 +361,6 @@ func init() { world.RegisterItem(Lapis{}) world.RegisterItem(Lectern{}) world.RegisterItem(Lever{}) - world.RegisterItem(RedstoneLamp{}) - for _, t := range ButtonTypes() { - world.RegisterItem(Button{Type: t}) - } - for _, t := range PressurePlateTypes() { - world.RegisterItem(PressurePlate{Type: t}) - } world.RegisterItem(LilyPad{}) world.RegisterItem(Magma{}) world.RegisterItem(LitPumpkin{}) @@ -394,6 +390,9 @@ func init() { world.RegisterItem(PolishedBlackstoneBrick{Cracked: true}) world.RegisterItem(PolishedBlackstoneBrick{}) world.RegisterItem(Potato{}) + for _, t := range PressurePlateTypes() { + world.RegisterItem(PressurePlate{Type: t}) + } world.RegisterItem(PumpkinSeeds{}) world.RegisterItem(Pumpkin{Carved: true}) world.RegisterItem(Pumpkin{}) @@ -407,6 +406,7 @@ func init() { world.RegisterItem(RawGold{}) world.RegisterItem(RawIron{}) world.RegisterItem(RedstoneBlock{}) + world.RegisterItem(RedstoneLamp{}) world.RegisterItem(RedstoneTorch{}) world.RegisterItem(RedstoneWire{}) world.RegisterItem(ReinforcedDeepslate{}) diff --git a/server/entity/movement.go b/server/entity/movement.go index c1df017f1..0d223cdcf 100644 --- a/server/entity/movement.go +++ b/server/entity/movement.go @@ -50,19 +50,18 @@ func StepOnBlock(tx *world.Tx, e world.Entity, pos mgl64.Vec3) { low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) y := int(math.Floor(box.Min()[1] - 0.0001)) - for x := low[0]; x <= high[0]; x++ { - for z := low[2]; z <= high[2]; z++ { - pos := cube.Pos{x, y, z} - if stepper, ok := tx.Block(pos).(block.EntityStepper); ok { - stepper.EntityStepOn(pos, tx, e) - return - } + for pos := range cube.Range3D(cube.Pos{low[0], y, low[2]}, cube.Pos{high[0], y, high[2]}) { + if stepper, ok := tx.Block(pos).(block.EntityStepper); ok { + stepper.EntityStepOn(pos, tx, e) + return } } } // checkSteppers handles pressure plates intersecting the entity and the block -// it stands on after movement, mirroring player behaviour. +// it stands on after movement, mirroring player behaviour. Only pressure plates +// are dispatched here: every other block.EntityInsider is deliberately left to +// entity physics, as documented on Ent.checkPortalInsiders. func (m *Movement) checkSteppers(tx *world.Tx) { box := m.e.H().Type().BBox(m.e).Translate(m.pos).Grow(-0.0001) low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) diff --git a/server/entity/projectile.go b/server/entity/projectile.go index c00806b22..f98f80986 100644 --- a/server/entity/projectile.go +++ b/server/entity/projectile.go @@ -206,9 +206,12 @@ func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { if h, ok := tx.Block(bpos).(block.ProjectileHitter); ok { h.ProjectileHit(bpos, tx, e, r.Face()) } - // Blocks without a collision box, such as buttons, are passed through - // by the trace, so the block of the cell the projectile comes to rest - // in is notified as well. + // TODO: trace.BlockIntercept returns early for blocks whose model has + // no BBox (model.Empty), so the trace passes straight through buttons + // and the like. Until the trace layer can report pass-through hits, the + // block of the cell the projectile comes to rest in is notified as well. + // Implementations must therefore verify the projectile really touches + // them, as Button.ProjectileHit does. rest := bpos.Side(r.Face()) if h, ok := tx.Block(rest).(block.ProjectileHitter); ok { h.ProjectileHit(rest, tx, e, r.Face()) From fd240009f605fb3fc961b3df207caa0f45fa0ce1 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 21 Jul 2026 01:31:56 -0400 Subject: [PATCH 9/9] server/block: format redstone type lists --- server/block/button_type.go | 17 ++++++++++++++++- server/block/pressure_plate_type.go | 19 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/server/block/button_type.go b/server/block/button_type.go index b3f45520d..5fc59162a 100644 --- a/server/block/button_type.go +++ b/server/block/button_type.go @@ -167,5 +167,20 @@ func (b button) String() string { // ButtonTypes ... func ButtonTypes() []ButtonType { - return []ButtonType{StoneButton(), PolishedBlackstoneButton(), OakButton(), SpruceButton(), BirchButton(), JungleButton(), AcaciaButton(), DarkOakButton(), MangroveButton(), CherryButton(), BambooButton(), CrimsonButton(), WarpedButton(), PaleOakButton()} + return []ButtonType{ + StoneButton(), + PolishedBlackstoneButton(), + OakButton(), + SpruceButton(), + BirchButton(), + JungleButton(), + AcaciaButton(), + DarkOakButton(), + MangroveButton(), + CherryButton(), + BambooButton(), + CrimsonButton(), + WarpedButton(), + PaleOakButton(), + } } diff --git a/server/block/pressure_plate_type.go b/server/block/pressure_plate_type.go index 08f240427..8ea869aae 100644 --- a/server/block/pressure_plate_type.go +++ b/server/block/pressure_plate_type.go @@ -194,5 +194,22 @@ func (p pressurePlate) String() string { // PressurePlateTypes ... func PressurePlateTypes() []PressurePlateType { - return []PressurePlateType{StonePressurePlate(), PolishedBlackstonePressurePlate(), OakPressurePlate(), SprucePressurePlate(), BirchPressurePlate(), JunglePressurePlate(), AcaciaPressurePlate(), DarkOakPressurePlate(), MangrovePressurePlate(), CherryPressurePlate(), BambooPressurePlate(), CrimsonPressurePlate(), WarpedPressurePlate(), PaleOakPressurePlate(), LightWeightedPressurePlate(), HeavyWeightedPressurePlate()} + return []PressurePlateType{ + StonePressurePlate(), + PolishedBlackstonePressurePlate(), + OakPressurePlate(), + SprucePressurePlate(), + BirchPressurePlate(), + JunglePressurePlate(), + AcaciaPressurePlate(), + DarkOakPressurePlate(), + MangrovePressurePlate(), + CherryPressurePlate(), + BambooPressurePlate(), + CrimsonPressurePlate(), + WarpedPressurePlate(), + PaleOakPressurePlate(), + LightWeightedPressurePlate(), + HeavyWeightedPressurePlate(), + } }