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/block.go b/server/block/block.go index 044f65a81..e13947979 100644 --- a/server/block/block.go +++ b/server/block/block.go @@ -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) { diff --git a/server/block/button.go b/server/block/button.go new file mode 100644 index 000000000..6230aa24e --- /dev/null +++ b/server/block/button.go @@ -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 +} diff --git a/server/block/button_type.go b/server/block/button_type.go new file mode 100644 index 000000000..5fc59162a --- /dev/null +++ b/server/block/button_type.go @@ -0,0 +1,186 @@ +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 letting arrows press it. +func (b button) Wood() bool { + return b >= 2 && b <= 13 +} + +// 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 { + 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 { + return []ButtonType{ + StoneButton(), + PolishedBlackstoneButton(), + OakButton(), + SpruceButton(), + BirchButton(), + JungleButton(), + AcaciaButton(), + DarkOakButton(), + MangroveButton(), + CherryButton(), + BambooButton(), + CrimsonButton(), + WarpedButton(), + PaleOakButton(), + } +} diff --git a/server/block/hash.go b/server/block/hash.go index 12c33d96d..7de1c0a11 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -29,6 +29,7 @@ const ( hashBookshelf hashBrewingStand hashBricks + hashButton hashCactus hashCake hashCalcite @@ -160,6 +161,7 @@ const ( hashPolishedTuff hashPortal hashPotato + hashPressurePlate hashPrismarine hashPumpkin hashPumpkinSeeds @@ -172,6 +174,7 @@ const ( hashRawGold hashRawIron hashRedstoneBlock + hashRedstoneLamp hashRedstoneOre hashRedstoneTorch hashRedstoneWire @@ -331,6 +334,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) } @@ -855,6 +862,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()) } @@ -903,6 +914,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..f10381e23 --- /dev/null +++ b/server/block/pressure_plate.go @@ -0,0 +1,205 @@ +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" +) + +// 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 +} + +// 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 || !attachmentSupported(tx, pos, cube.FaceUp) { + return false + } + place(tx, pos, p, user, ctx) + return placed(ctx) +} + +// 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.detects(e) || !entityIntersects(e, pressurePlateActivationBox(pos)) { + return + } + 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 := 15 + if p.Type.Weighted() { + power = max(1, p.detectPower(pos, tx)) + } + 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 !attachmentSupported(tx, pos, cube.FaceUp) { + 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 { + effective := pickaxeEffective + if p.Type.Wood() { + effective = axeEffective + } + return newBreakInfo(0.5, alwaysHarvestable, effective, oneOf(PressurePlate{Type: p.Type})) +} + +// SideClosed ... +func (PressurePlate) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false +} + +// FuelInfo ... +func (p PressurePlate) FuelInfo() item.FuelInfo { + if p.Type.Flammable() { + 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(world.ClampRedstonePower(p.Power))} +} + +// 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 + } + if living, ok := e.(pressurePlateLivingEntity); ok { + return !living.Dead() + } + return e.H().Type().EncodeEntity() == "minecraft:armor_stand" +} + +// 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.detects(e) || !entityIntersects(e, box) { + continue + } + if n++; n >= limit { + break + } + } + return n +} + +// 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 + } + if p.entitiesOn(pos, tx, 1) > 0 { + return 15 + } + return 0 +} + +// 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 +} + +// 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 +} + +// pressurePlateActivationBox is the box entities must intersect to press the +// plate at a position. +func pressurePlateActivationBox(pos cube.Pos) cube.BBox { + return cube.Box(0.125, 0, 0.125, 0.875, 0.25, 0.875).Translate(pos.Vec3()) +} + +// 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..8ea869aae --- /dev/null +++ b/server/block/pressure_plate_type.go @@ -0,0 +1,215 @@ +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. +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 { + 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 { + 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 new file mode 100644 index 000000000..23c400c44 --- /dev/null +++ b/server/block/redstone_lamp.go @@ -0,0 +1,70 @@ +package block + +import ( + "math/rand/v2" + + "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 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) { + lit := power > 0 + if lit == r.Lit { + return r, false + } + if !lit { + tx.ScheduleBlockUpdate(pos, r, redstoneTicks(3)) + return r, false + } + r.Lit = true + return r, true +} + +// 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 = false + tx.SetBlock(pos, r, nil) +} + +// 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/redstone_test.go b/server/block/redstone_test.go index 8b5ebbe2f..0a205f1b3 100644 --- a/server/block/redstone_test.go +++ b/server/block/redstone_test.go @@ -17,6 +17,49 @@ 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}.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 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} @@ -982,6 +1025,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/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 4522a98cd..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()) @@ -217,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()) @@ -282,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{}) @@ -384,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{}) @@ -397,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/ent.go b/server/entity/ent.go index 08b40857b..339d6fdc2 100644 --- a/server/entity/ent.go +++ b/server/entity/ent.go @@ -162,6 +162,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 02ebf65d2..f225d9285 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,37 @@ func (m *Movement) Send() { } } +// 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)) + + 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. 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()) + 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) + } +} + // 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 1d9be719b..d5427f7cb 100644 --- a/server/entity/projectile.go +++ b/server/entity/projectile.go @@ -206,6 +206,16 @@ 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()) } + // 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()) + } if lt.conf.SurviveBlockCollision { lt.hitBlockSurviving(e, r, m, tx) return m @@ -333,13 +343,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) @@ -356,7 +368,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/player/player.go b/server/player/player.go index 2049a0834..82e7aff5e 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2631,7 +2631,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) @@ -2978,26 +2980,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)) diff --git a/server/session/world.go b/server/session/world.go index 9e47cbf4b..6bacbbd59 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 }