From a07805fe5a8556a5f982b29aea1e246e54d10212 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 7 Jan 2025 00:54:39 -0800 Subject: [PATCH] implement more redstone components --- server/block/hash.go | 25 ++ server/block/lava.go | 5 + server/block/melon.go | 5 + server/block/model/diode.go | 19 ++ server/block/moving.go | 71 ++++++ server/block/observer.go | 105 ++++++++ server/block/obsidian.go | 5 + server/block/piston.go | 344 +++++++++++++++++++++++++++ server/block/piston_arm_collision.go | 70 ++++++ server/block/piston_resolver.go | 142 +++++++++++ server/block/pumpkin.go | 5 + server/block/redstone.go | 10 + server/block/redstone_repeater.go | 166 +++++++++++++ server/block/redstone_wire.go | 7 +- server/block/register.go | 9 + server/block/sign.go | 5 + server/block/water.go | 5 + server/block/wood_door.go | 5 + server/session/world.go | 4 + server/world/sound/block.go | 6 + 20 files changed, 1012 insertions(+), 1 deletion(-) create mode 100644 server/block/model/diode.go create mode 100644 server/block/moving.go create mode 100644 server/block/observer.go create mode 100644 server/block/piston.go create mode 100644 server/block/piston_arm_collision.go create mode 100644 server/block/piston_resolver.go create mode 100644 server/block/redstone_repeater.go diff --git a/server/block/hash.go b/server/block/hash.go index 43810ad27..2145ecae5 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -117,6 +117,7 @@ const ( hashMelon hashMelonSeeds hashMossCarpet + hashMoving hashMud hashMudBricks hashMuddyMangroveRoots @@ -130,10 +131,13 @@ const ( hashNetherite hashNetherrack hashNote + hashObserver hashObsidian hashPackedIce hashPackedMud hashPinkPetals + hashPiston + hashPistonArmCollision hashPlanks hashPodzol hashPolishedBlackstoneBrick @@ -154,6 +158,7 @@ const ( hashRedstoneBlock hashRedstoneLamp hashRedstoneOre + hashRedstoneRepeater hashRedstoneTorch hashRedstoneWire hashReinforcedDeepslate @@ -659,6 +664,10 @@ func (MossCarpet) Hash() (uint64, uint64) { return hashMossCarpet, 0 } +func (Moving) Hash() (uint64, uint64) { + return hashMoving, 0 +} + func (Mud) Hash() (uint64, uint64) { return hashMud, 0 } @@ -711,6 +720,10 @@ func (Note) Hash() (uint64, uint64) { return hashNote, 0 } +func (o Observer) Hash() (uint64, uint64) { + return hashObserver, uint64(o.Facing) | uint64(boolByte(o.Powered))<<3 +} + func (o Obsidian) Hash() (uint64, uint64) { return hashObsidian, uint64(boolByte(o.Crying)) } @@ -727,6 +740,14 @@ func (p PinkPetals) Hash() (uint64, uint64) { return hashPinkPetals, uint64(p.AdditionalCount) | uint64(p.Facing)<<8 } +func (p Piston) Hash() (uint64, uint64) { + return hashPiston, uint64(p.Facing) | uint64(boolByte(p.Sticky))<<3 +} + +func (c PistonArmCollision) Hash() (uint64, uint64) { + return hashPistonArmCollision, uint64(c.Facing) +} + func (p Planks) Hash() (uint64, uint64) { return hashPlanks, uint64(p.Wood.Uint8()) } @@ -807,6 +828,10 @@ func (r RedstoneOre) Hash() (uint64, uint64) { return hashRedstoneOre, uint64(r.Type.Uint8()) | uint64(boolByte(r.Lit))<<1 } +func (r RedstoneRepeater) Hash() (uint64, uint64) { + return hashRedstoneRepeater, uint64(r.Facing) | uint64(boolByte(r.Powered))<<2 | uint64(r.Delay)<<3 +} + func (t RedstoneTorch) Hash() (uint64, uint64) { return hashRedstoneTorch, uint64(t.Facing) | uint64(boolByte(t.Lit))<<3 } diff --git a/server/block/lava.go b/server/block/lava.go index 239b0ac0e..6134f9b6b 100644 --- a/server/block/lava.go +++ b/server/block/lava.go @@ -87,6 +87,11 @@ func (Lava) LightEmissionLevel() uint8 { return 15 } +// PistonBreakable ... +func (Lava) PistonBreakable() bool { + return true +} + // NeighbourUpdateTick ... func (l Lava) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { if !l.Harden(pos, tx, nil) { diff --git a/server/block/melon.go b/server/block/melon.go index 142d15f9e..5ccc9e77e 100644 --- a/server/block/melon.go +++ b/server/block/melon.go @@ -20,6 +20,11 @@ func (Melon) CompostChance() float64 { return 0.65 } +// PistonBreakable ... +func (Melon) PistonBreakable() bool { + return true +} + // EncodeItem ... func (Melon) EncodeItem() (name string, meta int16) { return "minecraft:melon_block", 0 diff --git a/server/block/model/diode.go b/server/block/model/diode.go new file mode 100644 index 000000000..b2c08a352 --- /dev/null +++ b/server/block/model/diode.go @@ -0,0 +1,19 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Diode is a model used by redstone gates, such as repeaters and comparators. +type Diode struct{} + +// BBox ... +func (Diode) BBox(cube.Pos, world.BlockSource) []cube.BBox { + return []cube.BBox{full.ExtendTowards(cube.FaceDown, 0.875)} +} + +// FaceSolid ... +func (Diode) FaceSolid(cube.Pos, cube.Face, world.BlockSource) bool { + return false +} diff --git a/server/block/moving.go b/server/block/moving.go new file mode 100644 index 000000000..2efb8e40b --- /dev/null +++ b/server/block/moving.go @@ -0,0 +1,71 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/internal/nbtconv" + "github.com/df-mc/dragonfly/server/world" +) + +// Moving ... +type Moving struct { + empty + transparent + + // Moving represents the block that is moving. + Moving world.Block + // Extra represents an extra block that is moving with the main block. + Extra world.Block + // Piston is the position of the piston that is moving the block. + Piston cube.Pos + // Expanding is true if the moving block is expanding, false if it is contracting. + Expanding bool +} + +// EncodeBlock ... +func (Moving) EncodeBlock() (string, map[string]any) { + return "minecraft:moving_block", nil +} + +// PistonImmovable ... +func (Moving) PistonImmovable() bool { + return true +} + +// EncodeNBT ... +func (b Moving) EncodeNBT() map[string]any { + if b.Moving == nil { + b.Moving = Air{} + } + if b.Extra == nil { + b.Extra = Air{} + } + data := map[string]any{ + "id": "MovingBlock", + "expanding": b.Expanding, + "movingBlock": nbtconv.WriteBlock(b.Moving), + "movingBlockExtra": nbtconv.WriteBlock(b.Extra), + "pistonPosX": int32(b.Piston.X()), + "pistonPosY": int32(b.Piston.Y()), + "pistonPosZ": int32(b.Piston.Z()), + } + if nbt, ok := b.Moving.(world.NBTer); ok { + data["movingEntity"] = nbt.EncodeNBT() + } + return data +} + +// DecodeNBT ... +func (b Moving) DecodeNBT(m map[string]any) any { + b.Expanding = nbtconv.Bool(m, "expanding") + b.Moving = nbtconv.Block(m, "movingBlock") + b.Extra = nbtconv.Block(m, "movingBlockExtra") + b.Piston = cube.Pos{ + int(nbtconv.Int32(m, "pistonPosX")), + int(nbtconv.Int32(m, "pistonPosY")), + int(nbtconv.Int32(m, "pistonPosZ")), + } + if nbt, ok := b.Moving.(world.NBTer); ok { + b.Moving = nbt.DecodeNBT(m["movingEntity"].(map[string]any)).(world.Block) + } + return b +} diff --git a/server/block/observer.go b/server/block/observer.go new file mode 100644 index 000000000..f26ff3c04 --- /dev/null +++ b/server/block/observer.go @@ -0,0 +1,105 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" + "math/rand/v2" + "time" +) + +// Observer is a block that emits a redstone signal when the block or fluid it faces experiences a change. +type Observer struct { + solid + + // Facing is the direction the observer is observing. + Facing cube.Face + // Powered is whether the observer is powered or not. + Powered bool +} + +// RedstoneSource ... +func (Observer) RedstoneSource() bool { + return true +} + +// RedstoneBlocking ... +func (Observer) RedstoneBlocking() bool { + return true +} + +// WeakPower ... +func (o Observer) WeakPower(pos cube.Pos, face cube.Face, tx *world.Tx, accountForDust bool) int { + return o.StrongPower(pos, face, tx, accountForDust) +} + +// StrongPower ... +func (o Observer) StrongPower(_ cube.Pos, face cube.Face, _ *world.Tx, _ bool) int { + if !o.Powered || face != o.Facing { + return 0 + } + return 15 +} + +// ScheduledTick ... +func (o Observer) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + o.Powered = !o.Powered + if o.Powered { + tx.ScheduleBlockUpdate(pos, o, time.Millisecond*100) + } + tx.SetBlock(pos, o, nil) + updateDirectionalRedstone(pos, tx, o.Facing.Opposite()) +} + +// NeighbourUpdateTick ... +func (o Observer) NeighbourUpdateTick(pos, changedNeighbour cube.Pos, tx *world.Tx) { + if pos.Side(o.Facing) != changedNeighbour { + return + } + if o.Powered { + return + } + tx.ScheduleBlockUpdate(pos, o, time.Millisecond*100) +} + +// UseOnBlock ... +func (o Observer) 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, o) + if !used { + return false + } + o.Facing = calculateFace(user, pos, false) + if o.Facing.Axis() == cube.Y { + o.Facing = o.Facing.Opposite() + } + + place(tx, pos, o, user, ctx) + return placed(ctx) +} + +// BreakInfo ... +func (o Observer) BreakInfo() BreakInfo { + return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(o)).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { + updateDirectionalRedstone(pos, tx, o.Facing.Opposite()) + }) +} + +// EncodeItem ... +func (o Observer) EncodeItem() (name string, meta int16) { + return "minecraft:observer", 0 +} + +// EncodeBlock ... +func (o Observer) EncodeBlock() (string, map[string]any) { + return "minecraft:observer", map[string]any{"minecraft:facing_direction": o.Facing.String(), "powered_bit": o.Powered} +} + +// allObservers ... +func allObservers() (observers []world.Block) { + for _, f := range cube.Faces() { + observers = append(observers, Observer{Facing: f}) + observers = append(observers, Observer{Facing: f, Powered: true}) + } + return +} diff --git a/server/block/obsidian.go b/server/block/obsidian.go index 5d99bd4a0..0e68d7b81 100644 --- a/server/block/obsidian.go +++ b/server/block/obsidian.go @@ -13,6 +13,11 @@ type Obsidian struct { Crying bool } +// PistonImmovable ... +func (o Obsidian) PistonImmovable() bool { + return !o.Crying +} + // LightEmissionLevel ... func (o Obsidian) LightEmissionLevel() uint8 { if o.Crying { diff --git a/server/block/piston.go b/server/block/piston.go new file mode 100644 index 000000000..0c774d24e --- /dev/null +++ b/server/block/piston.go @@ -0,0 +1,344 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/internal/nbtconv" + "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" + "math/rand/v2" + "time" +) + +// Piston is a block capable of pushing blocks, players, and mobs when given a redstone pulse. +type Piston struct { + solid + transparent + sourceWaterDisplacer + + // Facing represents the direction the piston is facing. + Facing cube.Face + // Sticky is true if the piston is sticky, false if not. + Sticky bool + + // AttachedBlocks ... + AttachedBlocks []cube.Pos + // BreakBlocks ... + BreakBlocks []cube.Pos + + // Progress is how far the block has been moved. It can either be 0.0, 0.5, or 1.0. + Progress float64 + // LastProgress ... + LastProgress float64 + + // State ... + State int + // NewState ... + NewState int +} + +// PistonImmovable represents a block that cannot be moved by a piston. +type PistonImmovable interface { + // PistonImmovable returns whether the block is immovable. + PistonImmovable() bool +} + +// PistonBreakable represents a block that can be broken by a piston. +type PistonBreakable interface { + // PistonBreakable returns whether the block can be broken by a piston. + PistonBreakable() bool +} + +// PistonUpdater represents a block that can be updated through a piston movement. +type PistonUpdater interface { + // PistonUpdate is called when a piston moves the block. + PistonUpdate(pos cube.Pos, tx *world.Tx) +} + +// BreakInfo ... +func (p Piston) BreakInfo() BreakInfo { + return newBreakInfo(1.5, alwaysHarvestable, pickaxeEffective, oneOf(Piston{Sticky: p.Sticky})) +} + +// SideClosed ... +func (Piston) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// EncodeItem ... +func (p Piston) EncodeItem() (name string, meta int16) { + if p.Sticky { + return "minecraft:sticky_piston", 0 + } + return "minecraft:piston", 0 +} + +// EncodeBlock ... +func (p Piston) EncodeBlock() (string, map[string]any) { + if p.Sticky { + return "minecraft:sticky_piston", map[string]any{"facing_direction": int32(p.Facing)} + } + return "minecraft:piston", map[string]any{"facing_direction": int32(p.Facing)} +} + +// UseOnBlock ... +func (p Piston) 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 { + return false + } + p.Facing = calculateFace(user, pos, false) + + place(tx, pos, p, user, ctx) + if placed(ctx) { + p.RedstoneUpdate(pos, tx) + return true + } + return false +} + +// EncodeNBT ... +func (p Piston) EncodeNBT() map[string]any { + attachedBlocks := make([]int32, 0, len(p.AttachedBlocks)*3) + for _, pos := range p.AttachedBlocks { + attachedBlocks = append(attachedBlocks, int32(pos[0]), int32(pos[1]), int32(pos[2])) + } + breakBlocks := make([]int32, 0, len(p.BreakBlocks)*3) + for _, pos := range p.BreakBlocks { + breakBlocks = append(breakBlocks, int32(pos[0]), int32(pos[1]), int32(pos[2])) + } + return map[string]any{ + "AttachedBlocks": attachedBlocks, + "BreakBlocks": breakBlocks, + + "LastProgress": float32(p.LastProgress), + "Progress": float32(p.Progress), + + "NewState": uint8(p.NewState), + "State": uint8(p.State), + + "Sticky": p.Sticky, + + "id": "PistonArm", + } +} + +// DecodeNBT ... +func (p Piston) DecodeNBT(m map[string]any) any { + if attached := nbtconv.Slice(m, "AttachedBlocks"); attached != nil { + p.AttachedBlocks = make([]cube.Pos, 0, len(attached)/3) + for i := 0; i < len(attached); i += 3 { + p.AttachedBlocks = append(p.AttachedBlocks, cube.Pos{ + int(attached[i].(int32)), + int(attached[i+1].(int32)), + int(attached[i+2].(int32)), + }) + } + } + if breakBlocks := nbtconv.Slice(m, "BreakBlocks"); breakBlocks != nil { + p.BreakBlocks = make([]cube.Pos, 0, len(breakBlocks)/3) + for i := 0; i < len(breakBlocks); i += 3 { + p.BreakBlocks = append(p.BreakBlocks, cube.Pos{ + int(breakBlocks[i].(int32)), + int(breakBlocks[i+1].(int32)), + int(breakBlocks[i+2].(int32)), + }) + } + } + p.LastProgress = float64(nbtconv.Float32(m, "LastProgress")) + p.Progress = float64(nbtconv.Float32(m, "Progress")) + p.NewState = int(nbtconv.Uint8(m, "NewState")) + p.State = int(nbtconv.Uint8(m, "State")) + p.Sticky = nbtconv.Bool(m, "Sticky") + return p +} + +// RedstoneUpdate ... +func (p Piston) RedstoneUpdate(pos cube.Pos, tx *world.Tx) { + tx.ScheduleBlockUpdate(pos, p, time.Millisecond*50) +} + +// ScheduledTick ... +func (p Piston) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + if receivedRedstonePower(pos, tx, p.armFace()) { + if !p.push(pos, tx) { + return + } + } else if !p.pull(pos, tx) { + return + } + tx.ScheduleBlockUpdate(pos, p, time.Millisecond*50) +} + +// armFace ... +func (p Piston) armFace() cube.Face { + if p.Facing.Axis() == cube.Y { + return p.Facing + } + return p.Facing.Opposite() +} + +// push ... +func (p Piston) push(pos cube.Pos, tx *world.Tx) bool { + if p.State == 0 { + resolver := pistonResolve(tx, pos, p, true) + if !resolver.success { + return false + } + + for _, breakPos := range resolver.breakPositions { + p.BreakBlocks = append(p.BreakBlocks, breakPos) + if b, ok := tx.Block(breakPos).(Breakable); ok { + tx.SetBlock(breakPos, nil, nil) + for _, drop := range b.BreakInfo().Drops(item.ToolNone{}, nil) { + dropItem(tx, drop, breakPos.Vec3Centre()) + } + } + } + + face := p.armFace() + for _, attachedPos := range resolver.attachedPositions { + side := attachedPos.Side(face) + p.AttachedBlocks = append(p.AttachedBlocks, attachedPos) + + tx.SetBlock(side, Moving{Piston: pos, Moving: tx.Block(attachedPos), Expanding: true}, nil) + tx.SetBlock(attachedPos, nil, nil) + updateAroundRedstone(attachedPos, tx) + } + + p.State = 1 + tx.SetBlock(pos.Side(face), PistonArmCollision{Facing: p.Facing}, nil) + } else if p.State == 1 { + if p.Progress == 1 { + p.State = 2 + } + p.LastProgress = p.Progress + + if p.State == 1 { + p.Progress += 0.5 + if p.Progress == 0.5 { + tx.PlaySound(pos.Vec3Centre(), sound.PistonExtend{}) + } + } + + if p.State == 2 { + face := p.armFace() + for _, attachedPos := range p.AttachedBlocks { + side := attachedPos.Side(face) + moving, ok := tx.Block(side).(Moving) + if !ok { + continue + } + + tx.SetBlock(side, moving.Moving, nil) + if u, ok := moving.Moving.(RedstoneUpdater); ok { + u.RedstoneUpdate(side, tx) + } + if u, ok := moving.Moving.(PistonUpdater); ok { + u.PistonUpdate(side, tx) + } + updateAroundRedstone(side, tx) + } + + p.AttachedBlocks = nil + p.BreakBlocks = nil + } + } else if p.State == 3 { + return p.pull(pos, tx) + } else { + return false + } + + p.NewState = p.State + tx.SetBlock(pos, p, nil) + return true +} + +// pull ... +func (p Piston) pull(pos cube.Pos, tx *world.Tx) bool { + if p.State == 2 { + face := p.armFace() + tx.SetBlock(pos.Side(face), nil, nil) + + resolver := pistonResolve(tx, pos, p, false) + if !resolver.success { + return false + } + + for _, breakPos := range resolver.breakPositions { + p.BreakBlocks = append(p.BreakBlocks, breakPos) + if b, ok := tx.Block(breakPos).(Breakable); ok { + tx.SetBlock(breakPos, nil, nil) + for _, drop := range b.BreakInfo().Drops(item.ToolNone{}, nil) { + dropItem(tx, drop, breakPos.Vec3Centre()) + } + } + } + + face = face.Opposite() + for _, attachedPos := range resolver.attachedPositions { + side := attachedPos.Side(face) + p.AttachedBlocks = append(p.AttachedBlocks, attachedPos) + + tx.SetBlock(side, Moving{Piston: pos, Moving: tx.Block(attachedPos)}, nil) + tx.SetBlock(attachedPos, nil, nil) + updateAroundRedstone(attachedPos, tx) + } + + p.State = 3 + } else if p.State == 3 { + if p.Progress == 0 { + p.State = 0 + } + p.LastProgress = p.Progress + + if p.State == 3 { + p.Progress -= 0.5 + if p.Progress == 0.5 { + tx.PlaySound(pos.Vec3Centre(), sound.PistonRetract{}) + } + } + + if p.State == 0 { + face := p.armFace() + for _, attachedPos := range p.AttachedBlocks { + side := attachedPos.Side(face.Opposite()) + moving, ok := tx.Block(side).(Moving) + if !ok { + continue + } + + tx.SetBlock(side, moving.Moving, nil) + if r, ok := moving.Moving.(RedstoneUpdater); ok { + r.RedstoneUpdate(side, tx) + } + if r, ok := moving.Moving.(PistonUpdater); ok { + r.PistonUpdate(side, tx) + } + updateAroundRedstone(side, tx) + } + + p.AttachedBlocks = nil + p.BreakBlocks = nil + } + } else if p.State == 1 { + return p.push(pos, tx) + } else { + return false + } + + p.NewState = p.State + tx.SetBlock(pos, p, nil) + return true +} + +// allPistons ... +func allPistons() (pistons []world.Block) { + for _, f := range cube.Faces() { + for _, s := range []bool{false, true} { + pistons = append(pistons, Piston{Facing: f, Sticky: s}) + } + } + return +} diff --git a/server/block/piston_arm_collision.go b/server/block/piston_arm_collision.go new file mode 100644 index 000000000..33f6cd481 --- /dev/null +++ b/server/block/piston_arm_collision.go @@ -0,0 +1,70 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) + +// PistonArmCollision is a block that is used when a piston is extended and colliding with a block. +type PistonArmCollision struct { + empty + transparent + sourceWaterDisplacer + + // Facing represents the direction the piston is facing. + Facing cube.Face +} + +// PistonImmovable ... +func (PistonArmCollision) PistonImmovable() bool { + return true +} + +// SideClosed ... +func (PistonArmCollision) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false +} + +// EncodeBlock ... +func (c PistonArmCollision) EncodeBlock() (string, map[string]any) { + return "minecraft:piston_arm_collision", map[string]any{"facing_direction": int32(c.Facing)} +} + +// BreakInfo ... +func (c PistonArmCollision) BreakInfo() BreakInfo { + return newBreakInfo(1.5, alwaysHarvestable, pickaxeEffective, simpleDrops()).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) { + pistonPos := pos.Side(c.pistonFace()) + if p, ok := tx.Block(pistonPos).(Piston); ok { + tx.SetBlock(pistonPos, nil, nil) + if g, ok := u.(interface { + GameMode() world.GameMode + }); !ok || !g.GameMode().CreativeInventory() { + dropItem(tx, item.NewStack(Piston{Sticky: p.Sticky}, 1), pos.Vec3Centre()) + } + } + }) +} + +// NeighbourUpdateTick ... +func (c PistonArmCollision) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if _, ok := tx.Block(pos.Side(c.pistonFace())).(Piston); !ok { + tx.SetBlock(pos, nil, nil) + } +} + +// pistonFace ... +func (c PistonArmCollision) pistonFace() cube.Face { + if c.Facing.Axis() != cube.Y { + return c.Facing + } + return c.Facing.Opposite() +} + +// allPistonArmCollisions ... +func allPistonArmCollisions() (pistonArmCollisions []world.Block) { + for _, f := range cube.Faces() { + pistonArmCollisions = append(pistonArmCollisions, PistonArmCollision{Facing: f}) + } + return +} diff --git a/server/block/piston_resolver.go b/server/block/piston_resolver.go new file mode 100644 index 000000000..a252185cb --- /dev/null +++ b/server/block/piston_resolver.go @@ -0,0 +1,142 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "sort" +) + +// pistonResolver ... +type pistonResolver struct { + tx *world.Tx + pos cube.Pos + + attachedPositions []cube.Pos + breakPositions []cube.Pos + + history map[cube.Pos]struct{} + + success bool +} + +// pistonResolve ... +func pistonResolve(tx *world.Tx, pos cube.Pos, piston Piston, push bool) *pistonResolver { + r := &pistonResolver{ + tx: tx, + pos: pos, + + history: make(map[cube.Pos]struct{}), + } + + face := piston.armFace() + if push { + if r.calculateBlocks(r.pos.Side(face), face, face) { + r.success = true + } + } else { + if piston.Sticky { + r.calculateBlocks(r.pos.Side(face).Side(face), face, face.Opposite()) + } + r.success = true + } + + sort.SliceStable(r.attachedPositions, func(i, j int) bool { + posOne := r.attachedPositions[i] + posTwo := r.attachedPositions[j] + + pushI := 1 + if !push { + pushI = -1 + } + + positive := 1 + if !face.Positive() { + positive = -1 + } + + offset := posOne.Sub(posTwo) + direction := pushI * positive + switch face.Axis() { + case cube.Y: + return offset.Y()*direction > 0 + case cube.Z: + return offset.Z()*direction > 0 + case cube.X: + return offset.X()*direction > 0 + } + panic("should never happen") + }) + return r +} + +// calculateBlocks ... +func (r *pistonResolver) calculateBlocks(pos cube.Pos, face cube.Face, breakFace cube.Face) bool { + if pos.Side(breakFace).OutOfBounds(r.tx.Range()) { + r.breakPositions = nil + r.attachedPositions = nil + return false + } + if _, ok := r.history[pos]; ok { + return true + } + r.history[pos] = struct{}{} + + block := r.tx.Block(pos) + if _, ok := block.(Air); ok { + return true + } + if !r.canMove(pos, block) { + if face == breakFace { + r.breakPositions = nil + r.attachedPositions = nil + return false + } + return true + } + if r.canBreak(block) { + if face == breakFace { + r.breakPositions = append(r.breakPositions, pos) + } + return true + } + + if _, ok := block.(GlazedTerracotta); ok && face != breakFace { + // Glazed terracotta can't be pushed, but can be pulled. + return true + } + + r.attachedPositions = append(r.attachedPositions, pos) + if len(r.attachedPositions) >= 13 { + r.breakPositions = nil + r.attachedPositions = nil + return false + } + + // account for slime. + if !r.calculateBlocks(pos.Side(breakFace), breakFace, breakFace) { + return false + } + + return true +} + +// canMove ... +func (r *pistonResolver) canMove(pos cube.Pos, block world.Block) bool { + if p, ok := block.(Piston); ok { + if r.pos == pos { + return false + } + return p.State == 0 + } + p, ok := block.(PistonImmovable) + return !ok || !p.PistonImmovable() +} + +// canBreak ... +func (r *pistonResolver) canBreak(block world.Block) bool { + if l, ok := block.(LiquidRemovable); ok && l.HasLiquidDrops() { + return true + } + p, ok := block.(PistonBreakable) + return ok && p.PistonBreakable() +} diff --git a/server/block/pumpkin.go b/server/block/pumpkin.go index 51777ef38..e67e98c59 100644 --- a/server/block/pumpkin.go +++ b/server/block/pumpkin.go @@ -73,6 +73,11 @@ func (p Pumpkin) KnockBackResistance() float64 { return 0 } +// PistonBreakable ... +func (Pumpkin) PistonBreakable() bool { + return true +} + // EncodeItem ... func (p Pumpkin) EncodeItem() (name string, meta int16) { if p.Carved { diff --git a/server/block/redstone.go b/server/block/redstone.go index 698682015..4ad1a15ac 100644 --- a/server/block/redstone.go +++ b/server/block/redstone.go @@ -101,6 +101,16 @@ func updateDirectionalRedstone(pos cube.Pos, tx *world.Tx, face cube.Face) { updateAroundRedstone(pos.Side(face), tx, face.Opposite()) } +// updateGateRedstone is used to update redstone gates on each face of the given offset centre position. +func updateGateRedstone(centre cube.Pos, tx *world.Tx, face cube.Face) { + pos := centre.Side(face.Opposite()) + if r, ok := tx.Block(pos).(RedstoneUpdater); ok { + r.RedstoneUpdate(pos, tx) + } + + updateAroundRedstone(pos, tx, face) +} + // receivedRedstonePower returns true if the given position is receiving power from any faces that aren't ignored. func receivedRedstonePower(pos cube.Pos, w *world.Tx, ignoredFaces ...cube.Face) bool { for _, face := range cube.Faces() { diff --git a/server/block/redstone_repeater.go b/server/block/redstone_repeater.go new file mode 100644 index 000000000..50f2f25cb --- /dev/null +++ b/server/block/redstone_repeater.go @@ -0,0 +1,166 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" + "math/rand/v2" + "time" +) + +// RedstoneRepeater is a block used in redstone circuits to "repeat" redstone signals back to full strength, delay +// signals, prevent signals moving backwards, or to "lock" signals in one state. +type RedstoneRepeater struct { + transparent + flowingWaterDisplacer + + // Facing is the direction from the torch to the block. + Facing cube.Direction + // Powered is true if the repeater is powered by a redstone signal. + Powered bool + // Delay represents the delay of the repeater in redstone ticks. It is between the range of one to four. + Delay int +} + +// SideClosed ... +func (RedstoneRepeater) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// Model ... +func (RedstoneRepeater) Model() world.BlockModel { + return model.Diode{} +} + +// BreakInfo ... +func (r RedstoneRepeater) BreakInfo() BreakInfo { + return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(r)).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { + updateGateRedstone(pos, tx, r.Facing.Face()) + }) +} + +// EncodeItem ... +func (RedstoneRepeater) EncodeItem() (name string, meta int16) { + return "minecraft:repeater", 0 +} + +// EncodeBlock ... +func (r RedstoneRepeater) EncodeBlock() (string, map[string]any) { + name := "minecraft:unpowered_repeater" + if r.Powered { + name = "minecraft:powered_repeater" + } + return name, map[string]any{ + "minecraft:cardinal_direction": r.Facing.String(), + "repeater_delay": int32(r.Delay), + } +} + +// UseOnBlock ... +func (r RedstoneRepeater) 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 { + return false + } + if d, ok := tx.Block(pos.Side(cube.FaceDown)).(LightDiffuser); ok && d.LightDiffusionLevel() == 0 { + return false + } + r.Facing = user.Rotation().Direction().Opposite() + + place(tx, pos, r, user, ctx) + if placed(ctx) { + r.RedstoneUpdate(pos, tx) + return true + } + return false +} + +// NeighbourUpdateTick ... +func (r RedstoneRepeater) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if d, ok := tx.Block(pos.Side(cube.FaceDown)).(LightDiffuser); ok && d.LightDiffusionLevel() == 0 { + tx.SetBlock(pos, nil, nil) + dropItem(tx, item.NewStack(r, 1), pos.Vec3Centre()) + } +} + +// Activate ... +func (r RedstoneRepeater) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { + if r.Delay++; r.Delay > 3 { + r.Delay = 0 + } + tx.SetBlock(pos, r, nil) + return true +} + +// RedstoneUpdate ... +func (r RedstoneRepeater) RedstoneUpdate(pos cube.Pos, tx *world.Tx) { + if r.Locked() { + // Ignore this update; the repeater is locked. + return + } + if r.inputStrength(pos, tx) > 0 != r.Powered { + tx.ScheduleBlockUpdate(pos, r, time.Duration(r.Delay+1)*time.Millisecond*100) + } +} + +// ScheduledTick ... +func (r RedstoneRepeater) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + if r.Locked() { + // Ignore this tick; the repeater is locked. + return + } + + r.Powered = !r.Powered + tx.SetBlock(pos, r, nil) + updateGateRedstone(pos, tx, r.Facing.Face().Opposite()) + + if r.Powered && r.inputStrength(pos, tx) <= 0 { + tx.ScheduleBlockUpdate(pos, r, time.Duration(r.Delay+1)*time.Millisecond*100) + } + tx.SetBlock(pos, r, nil) + updateGateRedstone(pos, tx, r.Facing.Face()) +} + +// Locked ... +func (RedstoneRepeater) Locked() bool { + //TODO implement me + return false +} + +// RedstoneSource ... +func (r RedstoneRepeater) RedstoneSource() bool { + return r.Powered +} + +// WeakPower ... +func (r RedstoneRepeater) WeakPower(_ cube.Pos, face cube.Face, _ *world.Tx, _ bool) int { + if r.Powered && face == r.Facing.Face() { + return 15 + } + return 0 +} + +// StrongPower ... +func (r RedstoneRepeater) StrongPower(pos cube.Pos, face cube.Face, tx *world.Tx, accountForDust bool) int { + return r.WeakPower(pos, face, tx, accountForDust) +} + +// inputStrength ... +func (r RedstoneRepeater) inputStrength(pos cube.Pos, tx *world.Tx) int { + face := r.Facing.Face() + return tx.RedstonePower(pos.Side(face), face, true) +} + +// allRedstoneRepeaters ... +func allRedstoneRepeaters() (repeaters []world.Block) { + for _, d := range cube.Directions() { + for _, p := range []bool{false, true} { + for i := 0; i < 4; i++ { + repeaters = append(repeaters, RedstoneRepeater{Facing: d, Delay: i, Powered: p}) + } + } + } + return +} diff --git a/server/block/redstone_wire.go b/server/block/redstone_wire.go index 3cdb9edb0..6b5fa8ed3 100644 --- a/server/block/redstone_wire.go +++ b/server/block/redstone_wire.go @@ -171,7 +171,12 @@ func (RedstoneWire) connectsTo(block world.Block, face cube.Face, allowDirectSou if _, ok := block.(RedstoneWire); ok { return true } - // TODO: Account for other redstone blocks. + if r, ok := block.(RedstoneRepeater); ok { + return r.Facing.Face() == face || r.Facing.Face().Opposite() == face + } + if _, ok := block.(Piston); ok { + return true + } c, ok := block.(world.Conductor) return ok && allowDirectSources && c.RedstoneSource() } diff --git a/server/block/register.go b/server/block/register.go index 6e7a1a677..b4e837e0d 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -62,6 +62,7 @@ func init() { world.RegisterBlock(Lapis{}) world.RegisterBlock(Melon{}) world.RegisterBlock(MossCarpet{}) + world.RegisterBlock(Moving{}) world.RegisterBlock(MudBricks{}) world.RegisterBlock(Mud{}) world.RegisterBlock(NetherBrickFence{}) @@ -186,6 +187,9 @@ func init() { registerAll(allMuddyMangroveRoots()) registerAll(allNetherBricks()) registerAll(allNetherWart()) + registerAll(allObservers()) + registerAll(allPistonArmCollisions()) + registerAll(allPistons()) registerAll(allPinkPetals()) registerAll(allPlanks()) registerAll(allPotato()) @@ -195,6 +199,7 @@ func init() { registerAll(allPumpkins()) registerAll(allPurpurs()) registerAll(allQuartz()) + registerAll(allRedstoneRepeaters()) registerAll(allRedstoneTorches()) registerAll(allRedstoneWires()) registerAll(allSandstones()) @@ -325,10 +330,13 @@ func init() { world.RegisterItem(Netherite{}) world.RegisterItem(Netherrack{}) world.RegisterItem(Note{Pitch: 24}) + world.RegisterItem(Observer{}) world.RegisterItem(Obsidian{Crying: true}) world.RegisterItem(Obsidian{}) world.RegisterItem(PackedIce{}) world.RegisterItem(PackedMud{}) + world.RegisterItem(Piston{}) + world.RegisterItem(Piston{Sticky: true}) world.RegisterItem(PinkPetals{}) world.RegisterItem(Podzol{}) world.RegisterItem(PolishedBlackstoneBrick{Cracked: true}) @@ -351,6 +359,7 @@ func init() { world.RegisterItem(RedstoneOre{}) world.RegisterItem(RedstoneOre{Type: DeepslateOre()}) world.RegisterItem(RedstoneTorch{}) + world.RegisterItem(RedstoneRepeater{}) world.RegisterItem(RedstoneWire{}) world.RegisterItem(ReinforcedDeepslate{}) world.RegisterItem(ResinBricks{Chiseled: true}) diff --git a/server/block/sign.go b/server/block/sign.go index 2bf423bc9..5f6c3faae 100644 --- a/server/block/sign.go +++ b/server/block/sign.go @@ -56,6 +56,11 @@ func (s Sign) MaxCount() int { return 16 } +// PistonBreakable ... +func (Sign) PistonBreakable() bool { + return true +} + // FlammabilityInfo ... func (s Sign) FlammabilityInfo() FlammabilityInfo { return newFlammabilityInfo(0, 0, true) diff --git a/server/block/water.go b/server/block/water.go index cc830d66c..13919457e 100644 --- a/server/block/water.go +++ b/server/block/water.go @@ -62,6 +62,11 @@ func (w Water) WithDepth(depth int, falling bool) world.Liquid { return w } +// PistonBreakable ... +func (Water) PistonBreakable() bool { + return true +} + // LiquidFalling returns Water.Falling. func (w Water) LiquidFalling() bool { return w.Falling diff --git a/server/block/wood_door.go b/server/block/wood_door.go index c8260d3ab..f8e423682 100644 --- a/server/block/wood_door.go +++ b/server/block/wood_door.go @@ -47,6 +47,11 @@ func (d WoodDoor) Model() world.BlockModel { return model.Door{Facing: d.Facing, Open: d.Open, Right: d.Right} } +// PistonBreakable ... +func (WoodDoor) PistonBreakable() bool { + return true +} + // NeighbourUpdateTick ... func (d WoodDoor) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { if d.Top { diff --git a/server/session/world.go b/server/session/world.go index c36300eee..deb5c0b09 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -812,6 +812,10 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) pk.SoundType = packet.SoundEventComposterFillLayer case sound.ComposterReady: pk.SoundType = packet.SoundEventComposterReady + case sound.PistonExtend: + pk.SoundType = packet.SoundEventPistonOut + case sound.PistonRetract: + pk.SoundType = packet.SoundEventPistonIn case sound.DispenseFail: pk.SoundType = packet.SoundEventBlockClickFail case sound.Dispense: diff --git a/server/world/sound/block.go b/server/world/sound/block.go index 8ec4735fb..49f3d55ec 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -191,6 +191,12 @@ type PowerOn struct{ sound } // PowerOff is a sound played when a redstone component is powered off. type PowerOff struct{ sound } +// PistonExtend is a sound played when a piston extends. +type PistonExtend struct{ sound } + +// PistonRetract is a sound played when a piston retracts. +type PistonRetract struct{ sound } + // DispenseFail is a sound played when a dispenser fails to dispense an item. type DispenseFail struct{ sound }