Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/blockhash/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
16 changes: 16 additions & 0 deletions server/block/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,22 @@ 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)
}

// 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) {
Expand Down
190 changes: 190 additions & 0 deletions server/block/button.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package block

import (
"math/rand/v2"
"time"

"github.com/df-mc/dragonfly/server/block/cube"
"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
}

// 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 || !attachmentSupported(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 {
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() && b.arrowIntersects(e, buttonBox(b).Translate(pos.Vec3())) {
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
}
b.Pressed = true
tx.SetBlock(pos, b, nil)
tx.ScheduleBlockUpdate(pos, b, b.pressDuration())
tx.PlaySound(pos.Vec3Centre(), sound.Click{})
}

// NeighbourUpdateTick breaks the button if its supporting block is removed.
func (b Button) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) {
if !attachmentSupported(tx, pos, b.Facing) {
breakBlock(b, pos, tx)
}
}

// 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() && b.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 button at pos.
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 b.arrowIntersects(e, box) {
return true
}
}
return false
}

// 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: 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 {
depth := 2.0 / 16
if b.Pressed {
depth = 1.0 / 16
}
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.
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 {
effective := pickaxeEffective
harvestable := pickaxeHarvestable
if b.Type.Wood() {
effective = axeEffective
harvestable = alwaysHarvestable
}
return newBreakInfo(0.5, harvestable, effective, oneOf(Button{Type: b.Type}))
}

// SideClosed ...
func (Button) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool {
return false
}

// FuelInfo ...
func (b Button) FuelInfo() item.FuelInfo {
if b.Type.Flammable() {
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
}
Loading