diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index 8244acfdf..b32248bcf 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -235,6 +235,10 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[ return "uint64(" + s + ")", 8 case "Block": return "world.BlockHash(" + s + ")", 32 + case "ButtonType": + return "uint64(" + s + ".Uint8())", 6 + case "PressurePlateType": + return "uint64(" + s + ".Uint8())", 8 case "Attachment": if _, ok := directives["facing_only"]; ok { log.Println("Found directive: 'facing_only'") diff --git a/server/block/block.go b/server/block/block.go index 5a1234376..447d954bc 100644 --- a/server/block/block.go +++ b/server/block/block.go @@ -101,6 +101,9 @@ type Permutable interface { Permutations() []customblock.Permutation } +// unknownFace is a face that is used for certain block items. This should not be exposed in the API. +var unknownFace = cube.Face(len(cube.Faces())) + func calculateFace(user item.User, placePos cube.Pos) cube.Face { userPos := user.Position() pos := cube.PosFromVec3(userPos) diff --git a/server/block/button.go b/server/block/button.go new file mode 100644 index 000000000..90dfd5c5e --- /dev/null +++ b/server/block/button.go @@ -0,0 +1,153 @@ +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/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" + "math/rand" + "time" +) + +// TODO: Activate on projectile hit + +// Button is a non-solid block that can provide temporary redstone power. +type Button struct { + empty + transparent + sourceWaterDisplacer + + // Type is the type of the button. + Type ButtonType + // Facing is the face of the block that the button is on. + Facing cube.Face + // Pressed is whether the button is pressed or not. + Pressed bool +} + +// FuelInfo ... +func (b Button) FuelInfo() item.FuelInfo { + if b.Type == StoneButton() || b.Type == PolishedBlackstoneButton() { + return item.FuelInfo{} + } + return newFuelInfo(time.Second * 5) +} + +// Source ... +func (b Button) Source() bool { + return true +} + +// WeakPower ... +func (b Button) WeakPower(cube.Pos, cube.Face, *world.World, bool) int { + if b.Pressed { + return 15 + } + return 0 +} + +// StrongPower ... +func (b Button) StrongPower(_ cube.Pos, face cube.Face, _ *world.World, _ bool) int { + if b.Pressed && b.Facing == face { + return 15 + } + return 0 +} + +// ScheduledTick ... +func (b Button) ScheduledTick(pos cube.Pos, w *world.World, r *rand.Rand) { + if !b.Pressed { + return + } + b.Pressed = false + w.SetBlock(pos, b, nil) + w.PlaySound(pos.Vec3Centre(), sound.PowerOff{}) + updateDirectionalRedstone(pos, w, b.Facing.Opposite()) +} + +// NeighbourUpdateTick ... +func (b Button) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { + if !w.Block(pos.Side(b.Facing.Opposite())).Model().FaceSolid(pos.Side(b.Facing.Opposite()), b.Facing, w) { + w.SetBlock(pos, nil, nil) + dropItem(w, item.NewStack(b, 1), pos.Vec3Centre()) + updateDirectionalRedstone(pos, w, b.Facing.Opposite()) + } +} + +// Activate ... +func (b Button) Activate(pos cube.Pos, _ cube.Face, w *world.World, _ item.User, _ *item.UseContext) bool { + return b.Click(pos, w) +} + +// Click ... +func (b Button) Click(pos cube.Pos, w *world.World) bool { + if b.Pressed { + return true + } + b.Pressed = true + w.SetBlock(pos, b, nil) + w.PlaySound(pos.Vec3Centre(), sound.PowerOn{}) + updateDirectionalRedstone(pos, w, b.Facing.Opposite()) + + delay := time.Millisecond * 1500 + if b.Type == StoneButton() || b.Type == PolishedBlackstoneButton() { + delay = time.Millisecond * 1000 + } + w.ScheduleBlockUpdate(pos, delay) + return true +} + +// UseOnBlock ... +func (b Button) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) bool { + pos, face, used := firstReplaceable(w, pos, face, b) + if !used { + return false + } + if !w.Block(pos.Side(face.Opposite())).Model().FaceSolid(pos.Side(face.Opposite()), face, w) { + return false + } + + b.Facing = face + place(w, pos, b, user, ctx) + return placed(ctx) +} + +// BreakInfo ... +func (b Button) BreakInfo() BreakInfo { + harvestTool := alwaysHarvestable + effectiveTool := axeEffective + if b.Type == StoneButton() || b.Type == PolishedBlackstoneButton() { + harvestTool = pickaxeHarvestable + effectiveTool = pickaxeEffective + } + return newBreakInfo(0.5, harvestTool, effectiveTool, oneOf(b)).withBreakHandler(func(pos cube.Pos, w *world.World, _ item.User) { + updateDirectionalRedstone(pos, w, b.Facing.Opposite()) + }) +} + +// SideClosed ... +func (b Button) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// EncodeItem ... +func (b Button) EncodeItem() (name string, meta int16) { + return "minecraft:" + b.Type.String() + "_button", 0 +} + +// EncodeBlock ... +func (b Button) EncodeBlock() (string, map[string]any) { + return "minecraft:" + b.Type.String() + "_button", map[string]any{"facing_direction": int32(b.Facing), "button_pressed_bit": b.Pressed} +} + +// allButtons ... +func allButtons() (buttons []world.Block) { + for _, w := range ButtonTypes() { + for _, f := range cube.Faces() { + buttons = append(buttons, Button{Type: w, Facing: f}) + buttons = append(buttons, Button{Type: w, Facing: f, Pressed: true}) + } + } + return +} diff --git a/server/block/button_type.go b/server/block/button_type.go new file mode 100644 index 000000000..05771a699 --- /dev/null +++ b/server/block/button_type.go @@ -0,0 +1,69 @@ +package block + +// ButtonType represents a type of button. +type ButtonType struct { + button + + // Wood is the type of wood of the button. + wood WoodType +} + +type button uint8 + +// WoodButton returns the wood button type. +func WoodButton(w WoodType) ButtonType { + return ButtonType{0, w} +} + +// StoneButton returns the stone button type. +func StoneButton() ButtonType { + return ButtonType{button: 1} +} + +// PolishedBlackstoneButton returns the polished blackstone button type. +func PolishedBlackstoneButton() ButtonType { + return ButtonType{button: 2} +} + +// Uint8 ... +func (b ButtonType) Uint8() uint8 { + return b.wood.Uint8() | uint8(b.button)<<4 +} + +// Name ... +func (b ButtonType) Name() string { + switch b.button { + case 0: + return b.wood.Name() + " Button" + case 1: + return "Stone Button" + case 2: + return "Polished Blackstone Button" + } + panic("unknown button type") +} + +// String ... +func (b ButtonType) String() string { + switch b.button { + case 0: + if b.wood == OakWood() { + return "wooden" + } + return b.wood.String() + case 1: + return "stone" + case 2: + return "polished_blackstone" + } + panic("unknown button type") +} + +// ButtonTypes ... +func ButtonTypes() []ButtonType { + types := []ButtonType{StoneButton(), PolishedBlackstoneButton()} + for _, w := range WoodTypes() { + types = append(types, WoodButton(w)) + } + return types +} diff --git a/server/block/hash.go b/server/block/hash.go index 098786c88..540c88b18 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -24,6 +24,7 @@ const ( hashBookshelf hashBrewingStand hashBricks + hashButton hashCactus hashCake hashCalcite @@ -93,7 +94,9 @@ const ( hashInvisibleBedrock hashIron hashIronBars + hashIronDoor hashIronOre + hashIronTrapDoor hashItemFrame hashJukebox hashKelp @@ -104,6 +107,7 @@ const ( hashLava hashLeaves hashLectern + hashLever hashLight hashLitPumpkin hashLog @@ -132,6 +136,7 @@ const ( hashPolishedBlackstoneBrick hashPolishedTuff hashPotato + hashPressurePlate hashPrismarine hashPumpkin hashPumpkinSeeds @@ -143,6 +148,11 @@ const ( hashRawCopper hashRawGold hashRawIron + hashRedstoneBlock + hashRedstoneLamp + hashRedstoneOre + hashRedstoneTorch + hashRedstoneWire hashReinforcedDeepslate hashSand hashSandstone @@ -169,6 +179,7 @@ const ( hashStonecutter hashSugarCane hashTNT + hashTarget hashTerracotta hashTorch hashTuff @@ -270,6 +281,10 @@ func (Bricks) Hash() (uint64, uint64) { return hashBricks, 0 } +func (b Button) Hash() (uint64, uint64) { + return hashButton, uint64(b.Type.Uint8()) | uint64(b.Facing)<<6 | uint64(boolByte(b.Pressed))<<9 +} + func (c Cactus) Hash() (uint64, uint64) { return hashCactus, uint64(c.Age) } @@ -546,10 +561,18 @@ func (IronBars) Hash() (uint64, uint64) { return hashIronBars, 0 } +func (d IronDoor) Hash() (uint64, uint64) { + return hashIronDoor, uint64(d.Facing) | uint64(boolByte(d.Open))<<2 | uint64(boolByte(d.Top))<<3 | uint64(boolByte(d.Right))<<4 +} + func (i IronOre) Hash() (uint64, uint64) { return hashIronOre, uint64(i.Type.Uint8()) } +func (t IronTrapDoor) Hash() (uint64, uint64) { + return hashIronTrapDoor, uint64(t.Facing) | uint64(boolByte(t.Open))<<2 | uint64(boolByte(t.Top))<<3 +} + func (i ItemFrame) Hash() (uint64, uint64) { return hashItemFrame, uint64(i.Facing) | uint64(boolByte(i.Glowing))<<3 } @@ -590,6 +613,10 @@ func (l Lectern) Hash() (uint64, uint64) { return hashLectern, uint64(l.Facing) } +func (l Lever) Hash() (uint64, uint64) { + return hashLever, uint64(boolByte(l.Powered)) | uint64(l.Facing)<<1 | uint64(l.Direction)<<4 +} + func (l Light) Hash() (uint64, uint64) { return hashLight, uint64(l.Level) } @@ -702,6 +729,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)<<8 +} + func (p Prismarine) Hash() (uint64, uint64) { return hashPrismarine, uint64(p.Type.Uint8()) } @@ -746,6 +777,26 @@ func (RawIron) Hash() (uint64, uint64) { return hashRawIron, 0 } +func (RedstoneBlock) Hash() (uint64, uint64) { + return hashRedstoneBlock, 0 +} + +func (l RedstoneLamp) Hash() (uint64, uint64) { + return hashRedstoneLamp, uint64(boolByte(l.Lit)) +} + +func (c RedstoneOre) Hash() (uint64, uint64) { + return hashRedstoneOre, uint64(c.Type.Uint8()) +} + +func (t RedstoneTorch) Hash() (uint64, uint64) { + return hashRedstoneTorch, uint64(t.Facing) | uint64(boolByte(t.Lit))<<3 +} + +func (r RedstoneWire) Hash() (uint64, uint64) { + return hashRedstoneWire, uint64(r.Power) +} + func (ReinforcedDeepslate) Hash() (uint64, uint64) { return hashReinforcedDeepslate, 0 } @@ -850,6 +901,10 @@ func (TNT) Hash() (uint64, uint64) { return hashTNT, 0 } +func (Target) Hash() (uint64, uint64) { + return hashTarget, 0 +} + func (Terracotta) Hash() (uint64, uint64) { return hashTerracotta, 0 } diff --git a/server/block/hopper.go b/server/block/hopper.go index c62a0e31e..53925a09c 100644 --- a/server/block/hopper.go +++ b/server/block/hopper.go @@ -128,6 +128,17 @@ func (h Hopper) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world. return placed(ctx) } +// RedstoneUpdate ... +func (h Hopper) RedstoneUpdate(pos cube.Pos, w *world.World) { + powered := receivedRedstonePower(pos, w) + if powered == h.Powered { + return + } + + h.Powered = powered + w.SetBlock(pos, h, nil) +} + // Tick ... func (h Hopper) Tick(currentTick int64, pos cube.Pos, w *world.World) { h.TransferCooldown-- diff --git a/server/block/iron_door.go b/server/block/iron_door.go new file mode 100644 index 000000000..778da7f60 --- /dev/null +++ b/server/block/iron_door.go @@ -0,0 +1,157 @@ +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/df-mc/dragonfly/server/world/particle" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" + "math/rand" + "time" +) + +// IronDoor is a variant of the door made of iron that can only be opened using redstone. +type IronDoor struct { + transparent + bass + sourceWaterDisplacer + + // Facing is the direction the door is facing. + Facing cube.Direction + // Open is whether the door is open. + Open bool + // Top is whether the block is the top or bottom half of a door + Top bool + // Right is whether the door hinge is on the right side + Right bool +} + +// Model ... +func (d IronDoor) Model() world.BlockModel { + return model.Door{Facing: d.Facing, Open: d.Open, Right: d.Right} +} + +// NeighbourUpdateTick ... +func (d IronDoor) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { + if d.Top { + if _, ok := w.Block(pos.Side(cube.FaceDown)).(IronDoor); !ok { + w.SetBlock(pos, nil, nil) + w.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: d}) + } + return + } + if solid := w.Block(pos.Side(cube.FaceDown)).Model().FaceSolid(pos.Side(cube.FaceDown), cube.FaceUp, w); !solid { + w.SetBlock(pos, nil, nil) + w.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: d}) + } else if _, ok := w.Block(pos.Side(cube.FaceUp)).(IronDoor); !ok { + w.SetBlock(pos, nil, nil) + w.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: d}) + } +} + +// UseOnBlock handles the directional placing of doors +func (d IronDoor) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) bool { + if face != cube.FaceUp { + // Doors can only be placed when clicking the top face. + return false + } + below := pos + pos = pos.Side(cube.FaceUp) + if !replaceableWith(w, pos, d) || !replaceableWith(w, pos.Side(cube.FaceUp), d) { + return false + } + if !w.Block(below).Model().FaceSolid(below, cube.FaceUp, w) { + return false + } + d.Facing = user.Rotation().Direction() + left := w.Block(pos.Side(d.Facing.RotateLeft().Face())) + right := w.Block(pos.Side(d.Facing.RotateRight().Face())) + if _, ok := left.(IronDoor); ok { + d.Right = true + } + // The side the door hinge is on can be affected by the blocks to the left and right of the door. In particular, + // opaque blocks on the right side of the door with transparent blocks on the left side result in a right sided + // door hinge. + if diffuser, ok := right.(LightDiffuser); !ok || diffuser.LightDiffusionLevel() != 0 { + if diffuser, ok := left.(LightDiffuser); ok && diffuser.LightDiffusionLevel() == 0 { + d.Right = true + } + } + + ctx.IgnoreBBox = true + place(w, pos, d, user, ctx) + place(w, pos.Side(cube.FaceUp), IronDoor{Facing: d.Facing, Top: true, Right: d.Right}, user, ctx) + ctx.SubtractFromCount(1) + return placed(ctx) +} + +// BreakInfo ... +func (d IronDoor) BreakInfo() BreakInfo { + return newBreakInfo(3, alwaysHarvestable, axeEffective, oneOf(d)) +} + +// SideClosed ... +func (d IronDoor) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// RedstoneUpdate ... +func (d IronDoor) RedstoneUpdate(pos cube.Pos, w *world.World) { + if d.Open == receivedRedstonePower(pos, w) { + return + } + if !d.Open { + d.Open = true + w.PlaySound(pos.Vec3Centre(), sound.DoorOpen{Block: d}) + w.SetBlock(pos, d, &world.SetOpts{DisableBlockUpdates: true}) + } else { + w.ScheduleBlockUpdate(pos, time.Millisecond*50) + } +} + +// ScheduledTick ... +func (d IronDoor) ScheduledTick(pos cube.Pos, w *world.World, _ *rand.Rand) { + if receivedRedstonePower(pos, w) { + return + } + d.Open = false + w.PlaySound(pos.Vec3Centre(), sound.DoorClose{Block: d}) + w.SetBlock(pos, d, &world.SetOpts{DisableBlockUpdates: true}) +} + +// EncodeItem ... +func (d IronDoor) EncodeItem() (name string, meta int16) { + return "minecraft:iron_door", 0 +} + +// EncodeBlock ... +func (d IronDoor) EncodeBlock() (name string, properties map[string]any) { + direction := 3 + switch d.Facing { + case cube.South: + direction = 1 + case cube.West: + direction = 2 + case cube.East: + direction = 0 + } + + return "minecraft:iron_door", map[string]any{"direction": int32(direction), "door_hinge_bit": d.Right, "open_bit": d.Open, "upper_block_bit": d.Top} +} + +// allIronDoors returns a list of all door types +func allIronDoors() (doors []world.Block) { + for i := cube.Direction(0); i <= 3; i++ { + doors = append(doors, IronDoor{Facing: i, Open: false, Top: false, Right: false}) + doors = append(doors, IronDoor{Facing: i, Open: false, Top: true, Right: false}) + doors = append(doors, IronDoor{Facing: i, Open: true, Top: true, Right: false}) + doors = append(doors, IronDoor{Facing: i, Open: true, Top: false, Right: false}) + doors = append(doors, IronDoor{Facing: i, Open: false, Top: false, Right: true}) + doors = append(doors, IronDoor{Facing: i, Open: false, Top: true, Right: true}) + doors = append(doors, IronDoor{Facing: i, Open: true, Top: true, Right: true}) + doors = append(doors, IronDoor{Facing: i, Open: true, Top: false, Right: true}) + } + return +} diff --git a/server/block/iron_trapdoor.go b/server/block/iron_trapdoor.go new file mode 100644 index 000000000..bc2b687a9 --- /dev/null +++ b/server/block/iron_trapdoor.go @@ -0,0 +1,101 @@ +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/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" + "math" + "math/rand" + "time" +) + +// IronTrapDoor is a solid, transparent block that can be used as an openable 1×1 barrier. +type IronTrapDoor struct { + transparent + bass + sourceWaterDisplacer + + // Facing is the direction the trapdoor is facing. + Facing cube.Direction + // Open is whether the trapdoor is open. + Open bool + // Top is whether the trapdoor occupies the top or bottom part of a block. + Top bool +} + +// Model ... +func (t IronTrapDoor) Model() world.BlockModel { + return model.Trapdoor{Facing: t.Facing, Top: t.Top, Open: t.Open} +} + +// UseOnBlock handles the directional placing of trapdoors and makes sure they are properly placed upside down +// when needed. +func (t IronTrapDoor) UseOnBlock(pos cube.Pos, face cube.Face, clickPos mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) bool { + pos, face, used := firstReplaceable(w, pos, face, t) + if !used { + return false + } + t.Facing = user.Rotation().Direction().Opposite() + t.Top = (clickPos.Y() > 0.5 && face != cube.FaceUp) || face == cube.FaceDown + + place(w, pos, t, user, ctx) + return placed(ctx) +} + +// BreakInfo ... +func (t IronTrapDoor) BreakInfo() BreakInfo { + return newBreakInfo(5, alwaysHarvestable, axeEffective, oneOf(t)) +} + +// SideClosed ... +func (t IronTrapDoor) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// RedstoneUpdate ... +func (t IronTrapDoor) RedstoneUpdate(pos cube.Pos, w *world.World) { + if t.Open == receivedRedstonePower(pos, w) { + return + } + if !t.Open { + t.Open = true + w.PlaySound(pos.Vec3Centre(), sound.TrapdoorOpen{Block: t}) + w.SetBlock(pos, t, &world.SetOpts{DisableBlockUpdates: true}) + } else { + w.ScheduleBlockUpdate(pos, time.Millisecond*50) + } +} + +// ScheduledTick ... +func (t IronTrapDoor) ScheduledTick(pos cube.Pos, w *world.World, _ *rand.Rand) { + if receivedRedstonePower(pos, w) { + return + } + t.Open = false + w.PlaySound(pos.Vec3Centre(), sound.TrapdoorClose{Block: t}) + w.SetBlock(pos, t, &world.SetOpts{DisableBlockUpdates: true}) +} + +// EncodeItem ... +func (t IronTrapDoor) EncodeItem() (name string, meta int16) { + return "minecraft:iron_trapdoor", 0 +} + +// EncodeBlock ... +func (t IronTrapDoor) EncodeBlock() (name string, properties map[string]any) { + return "minecraft:iron_trapdoor", map[string]any{"direction": int32(math.Abs(float64(t.Facing) - 3)), "open_bit": t.Open, "upside_down_bit": t.Top} +} + +// allIronTrapdoors returns a list of all trapdoor types +func allIronTrapdoors() (trapdoors []world.Block) { + for i := cube.Direction(0); i <= 3; i++ { + trapdoors = append(trapdoors, IronTrapDoor{Facing: i, Open: false, Top: false}) + trapdoors = append(trapdoors, IronTrapDoor{Facing: i, Open: false, Top: true}) + trapdoors = append(trapdoors, IronTrapDoor{Facing: i, Open: true, Top: true}) + trapdoors = append(trapdoors, IronTrapDoor{Facing: i, Open: true, Top: false}) + } + return +} diff --git a/server/block/lever.go b/server/block/lever.go new file mode 100644 index 000000000..1d6d5a68f --- /dev/null +++ b/server/block/lever.go @@ -0,0 +1,133 @@ +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/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +// Lever is a non-solid block that can provide switchable redstone power. +type Lever struct { + empty + transparent + flowingWaterDisplacer + + // Powered is if the lever is switched on. + Powered bool + // Facing is the face of the block that the lever is attached to. + Facing cube.Face + // Direction is the direction the lever is pointing. This is only used for levers that are attached on up or down + // faces. + // TODO: Better handle lever direction on up or down faces—using a `cube.Axis` results in a default `Lever` with an + // axis `Y` and a face `Down` which does not map to an existing block state. + Direction cube.Direction +} + +// Source ... +func (l Lever) Source() bool { + return true +} + +// WeakPower ... +func (l Lever) WeakPower(cube.Pos, cube.Face, *world.World, bool) int { + if l.Powered { + return 15 + } + return 0 +} + +// StrongPower ... +func (l Lever) StrongPower(_ cube.Pos, face cube.Face, _ *world.World, _ bool) int { + if l.Powered && l.Facing == face { + return 15 + } + return 0 +} + +// SideClosed ... +func (l Lever) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// NeighbourUpdateTick ... +func (l Lever) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { + if !w.Block(pos.Side(l.Facing.Opposite())).Model().FaceSolid(pos.Side(l.Facing.Opposite()), l.Facing, w) { + w.SetBlock(pos, nil, nil) + dropItem(w, item.NewStack(l, 1), pos.Vec3Centre()) + updateDirectionalRedstone(pos, w, l.Facing.Opposite()) + } +} + +// UseOnBlock ... +func (l Lever) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) bool { + pos, face, used := firstReplaceable(w, pos, face, l) + if !used { + return false + } + if !w.Block(pos.Side(face.Opposite())).Model().FaceSolid(pos.Side(face.Opposite()), face, w) { + return false + } + + l.Facing = face + l.Direction = cube.North + if face.Axis() == cube.Y && user.Rotation().Direction().Face().Axis() == cube.X { + l.Direction = cube.West + } + place(w, pos, l, user, ctx) + return placed(ctx) +} + +// Activate ... +func (l Lever) Activate(pos cube.Pos, _ cube.Face, w *world.World, _ item.User, _ *item.UseContext) bool { + l.Powered = !l.Powered + w.SetBlock(pos, l, nil) + if l.Powered { + w.PlaySound(pos.Vec3Centre(), sound.PowerOn{}) + } else { + w.PlaySound(pos.Vec3Centre(), sound.PowerOff{}) + } + updateDirectionalRedstone(pos, w, l.Facing.Opposite()) + return true +} + +// BreakInfo ... +func (l Lever) BreakInfo() BreakInfo { + return newBreakInfo(0.5, alwaysHarvestable, nothingEffective, oneOf(l)).withBreakHandler(func(pos cube.Pos, w *world.World, _ item.User) { + updateDirectionalRedstone(pos, w, l.Facing.Opposite()) + }) +} + +// EncodeItem ... +func (l Lever) EncodeItem() (name string, meta int16) { + return "minecraft:lever", 0 +} + +// EncodeBlock ... +func (l Lever) EncodeBlock() (string, map[string]any) { + direction := l.Facing.String() + if l.Facing == cube.FaceDown || l.Facing == cube.FaceUp { + axis := "east_west" + if l.Direction == cube.North { + axis = "north_south" + } + direction += "_" + axis + } + return "minecraft:lever", map[string]any{"open_bit": l.Powered, "lever_direction": direction} +} + +// allLevers ... +func allLevers() (all []world.Block) { + f := func(facing cube.Face, direction cube.Direction) { + all = append(all, Lever{Facing: facing, Direction: direction}) + all = append(all, Lever{Facing: facing, Direction: direction, Powered: true}) + } + for _, facing := range cube.Faces() { + f(facing, cube.North) + if facing == cube.FaceDown || facing == cube.FaceUp { + f(facing, cube.West) + } + } + return +} diff --git a/server/block/pressure_plate.go b/server/block/pressure_plate.go new file mode 100644 index 000000000..e759fba28 --- /dev/null +++ b/server/block/pressure_plate.go @@ -0,0 +1,146 @@ +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/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" + "math/rand" + "time" +) + +// PressurePlate is a non-solid block that produces a redstone signal when stood on by an entity +type PressurePlate struct { + sourceWaterDisplacer + transparent + empty + + // Type is the type of the pressure plate. + Type PressurePlateType + // Power specifies the redstone power level currently being produced by the pressure plate. + Power int +} + +// BreakInfo ... +func (p PressurePlate) BreakInfo() BreakInfo { + harvestTool := alwaysHarvestable + effectiveTool := axeEffective + if p.Type == StonePressurePlate() || p.Type == PolishedBlackstonePressurePlate() || p.Type == HeavyWeightedPressurePlate() || p.Type == LightWeightedPressurePlate() { + harvestTool = pickaxeHarvestable + effectiveTool = pickaxeEffective + } + return newBreakInfo(0.5, harvestTool, effectiveTool, oneOf(p)).withBreakHandler(func(pos cube.Pos, w *world.World, _ item.User) { + updateAroundRedstone(pos, w) + }) +} + +// Source ... +func (p PressurePlate) Source() bool { + return true +} + +// WeakPower ... +func (p PressurePlate) WeakPower(cube.Pos, cube.Face, *world.World, bool) int { + return p.Power +} + +// StrongPower ... +func (p PressurePlate) StrongPower(pos cube.Pos, face cube.Face, w *world.World, redstone bool) int { + return p.Power +} + +func (p PressurePlate) EntityInside(pos cube.Pos, w *world.World, e world.Entity) { + var power int + entityCount := len(w.EntitiesWithin(cube.Box( + float64(pos.X()), float64(pos.Y()), float64(pos.Z()), + float64(pos.X()+1), float64(pos.Y()+1), float64(pos.Z()+1), + ), nil)) + + switch p.Type { + case StonePressurePlate(), PolishedBlackstonePressurePlate(): + //TODO: add a check if its a living entity currently not possible due to import cycle + power = 15 + case HeavyWeightedPressurePlate(): + power = min(entityCount, 15) + case LightWeightedPressurePlate(): + power = min((entityCount+9)/10, 15) + default: + power = 15 + } + + if power > 0 && power != p.Power { + p.Power = power + w.PlaySound(pos.Vec3Centre(), sound.PowerOn{}) + w.SetBlock(pos, p, &world.SetOpts{DisableBlockUpdates: false}) + updateAroundRedstone(pos, w) + } + + w.ScheduleBlockUpdate(pos, time.Millisecond*50) +} + +// ScheduledTick ... +func (p PressurePlate) ScheduledTick(pos cube.Pos, w *world.World, r *rand.Rand) { + if p.Power == 0 { + return + } + + entityCount := len(w.EntitiesWithin(cube.Box(float64(pos.X()), float64(pos.Y()), float64(pos.Z()), float64(pos.X()+1), float64(pos.Y()+1), float64(pos.Z()+1)), nil)) + if entityCount != 0 { + return + } + + p.Power = 0 + w.SetBlock(pos, p, &world.SetOpts{DisableBlockUpdates: false}) + w.PlaySound(pos.Vec3Centre(), sound.PowerOff{}) + updateAroundRedstone(pos, w) +} + +// NeighbourUpdateTick ... +func (p PressurePlate) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { + if d, ok := w.Block(pos.Side(cube.FaceDown)).(LightDiffuser); ok && d.LightDiffusionLevel() == 0 { + w.SetBlock(pos, nil, nil) + dropItem(w, item.NewStack(p, 1), pos.Vec3Centre()) + } +} + +// UseOnBlock ... +func (p PressurePlate) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(w, pos, face, p) + if !used { + return false + } + + belowPos := pos.Side(cube.FaceDown) + if !w.Block(belowPos).Model().FaceSolid(belowPos, cube.FaceUp, w) { + return false + } + + place(w, pos, p, user, ctx) + return placed(ctx) +} + +// SideClosed ... +func (p PressurePlate) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// EncodeItem ... +func (p PressurePlate) EncodeItem() (name string, meta int16) { + return "minecraft:" + p.Type.String() + "_pressure_plate", 0 +} + +// EncodeBlock ... +func (p PressurePlate) EncodeBlock() (string, map[string]any) { + return "minecraft:" + p.Type.String() + "_pressure_plate", map[string]any{"redstone_signal": int32(p.Power)} +} + +// allPressurePlates ... +func allPressurePlates() (pressureplates []world.Block) { + for _, w := range PressurePlateTypes() { + for i := 0; i <= 15; i++ { + pressureplates = append(pressureplates, PressurePlate{Type: w, Power: i}) + } + } + return +} diff --git a/server/block/pressure_plate_type.go b/server/block/pressure_plate_type.go new file mode 100644 index 000000000..8482748b2 --- /dev/null +++ b/server/block/pressure_plate_type.go @@ -0,0 +1,87 @@ +package block + +// PressurePlateType represents a type of pressure plate. +type PressurePlateType struct { + pressureplate + + // Wood is the type of wood of the pressure plate. + wood WoodType +} + +type pressureplate uint8 + +// WoodPressurePlate returns the wood button type. +func WoodPressurePlate(w WoodType) PressurePlateType { + return PressurePlateType{0, w} +} + +// StonePressurePlate returns the stone pressure plate type. +func StonePressurePlate() PressurePlateType { + return PressurePlateType{pressureplate: 1} +} + +// PolishedBlackstonePressurePlate returns the polished blackstone pressure plate type. +func PolishedBlackstonePressurePlate() PressurePlateType { + return PressurePlateType{pressureplate: 2} +} + +// HeavyWeightedPressurePlate returns the heavy weighted pressure plate type. +func HeavyWeightedPressurePlate() PressurePlateType { + return PressurePlateType{pressureplate: 3} +} + +// LightWeightedPressurePlate returns the light weighted pressure plate type. +func LightWeightedPressurePlate() PressurePlateType { + return PressurePlateType{pressureplate: 4} +} + +// Uint8 ... +func (p PressurePlateType) Uint8() uint8 { + return p.wood.Uint8() | uint8(p.pressureplate)<<4 +} + +// Name ... +func (p PressurePlateType) Name() string { + switch p.pressureplate { + case 0: + return p.wood.Name() + " Pressure Plate" + case 1: + return "Stone Pressure Plate" + case 2: + return "Polished Blackstone Pressure Plate" + case 3: + return "Heavy Weighted Pressure Plate" + case 4: + return "Light Weighted Pressure Plate" + } + panic("unknown pressure plate type") +} + +// String ... +func (p PressurePlateType) String() string { + switch p.pressureplate { + case 0: + if p.wood == OakWood() { + return "wooden" + } + return p.wood.String() + case 1: + return "stone" + case 2: + return "polished_blackstone" + case 3: + return "heavy_weighted" + case 4: + return "light_weighted" + } + panic("unknown pressure plate type") +} + +// PressurePlateTypes ... +func PressurePlateTypes() []PressurePlateType { + types := []PressurePlateType{StonePressurePlate(), PolishedBlackstonePressurePlate(), HeavyWeightedPressurePlate(), LightWeightedPressurePlate()} + for _, w := range WoodTypes() { + types = append(types, WoodPressurePlate(w)) + } + return types +} diff --git a/server/block/redstone.go b/server/block/redstone.go new file mode 100644 index 000000000..154b45212 --- /dev/null +++ b/server/block/redstone.go @@ -0,0 +1,394 @@ +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/world" + "slices" +) + +// RedstoneUpdater represents a block that can be updated through a change in redstone signal. +type RedstoneUpdater interface { + // RedstoneUpdate is called when a change in redstone signal is computed. + RedstoneUpdate(pos cube.Pos, w *world.World) +} + +// RedstoneBlocking represents a block that blocks redstone signals. +type RedstoneBlocking interface { + // RedstoneBlocking returns true if the block blocks redstone signals. + RedstoneBlocking() bool +} + +// wireNetwork implements a minimally-invasive bolt-on accelerator that performs a breadth-first search through redstone +// wires in order to more efficiently and compute new redstone wire power levels and determine the order in which other +// blocks should be updated. This implementation is heavily based off of RedstoneWireTurbo and MCHPRS. +type wireNetwork struct { + nodes []*wireNode + nodeCache map[cube.Pos]*wireNode + updateQueue [3][]*wireNode + currentWalkLayer uint32 +} + +// wireNode is a data structure to keep track of redstone wires and neighbours that will receive updates. +type wireNode struct { + visited bool + + pos cube.Pos + block world.Block + + neighbours []*wireNode + oriented bool + + xBias int32 + zBias int32 + + layer uint32 +} + +const ( + wireHeadingNorth = 0 + wireHeadingEast = 1 + wireHeadingSouth = 2 + wireHeadingWest = 3 +) + +// updateStrongRedstone sets off the breadth-first walk through all redstone wires connected to the initial position +// triggered. This is the main entry point for the redstone update algorithm. +func updateStrongRedstone(pos cube.Pos, w *world.World) { + n := &wireNetwork{ + nodeCache: make(map[cube.Pos]*wireNode), + updateQueue: [3][]*wireNode{}, + } + + root := &wireNode{ + block: w.Block(pos), + pos: pos, + visited: true, + } + n.nodeCache[pos] = root + n.nodes = append(n.nodes, root) + + n.propagateChanges(w, root, 0) + n.breadthFirstWalk(w) +} + +// updateAroundRedstone updates redstone components around the given centre position. It will also ignore any faces +// provided within the ignoredFaces parameter. This implementation is based off of RedstoneCircuit and Java 1.19. +func updateAroundRedstone(centre cube.Pos, w *world.World, ignoredFaces ...cube.Face) { + for _, face := range []cube.Face{ + cube.FaceWest, + cube.FaceEast, + cube.FaceDown, + cube.FaceUp, + cube.FaceNorth, + cube.FaceSouth, + } { + if slices.Contains(ignoredFaces, face) { + continue + } + + pos := centre.Side(face) + if r, ok := w.Block(pos).(RedstoneUpdater); ok { + r.RedstoneUpdate(pos, w) + } + } +} + +// updateDirectionalRedstone updates redstone components through the given face. This implementation is based off of +// RedstoneCircuit and Java 1.19. +func updateDirectionalRedstone(pos cube.Pos, w *world.World, face cube.Face) { + updateAroundRedstone(pos, w) + updateAroundRedstone(pos.Side(face), w, face.Opposite()) +} + +// receivedRedstonePower returns true if the given position is receiving power from any faces that aren't ignored. +func receivedRedstonePower(pos cube.Pos, w *world.World, ignoredFaces ...cube.Face) bool { + for _, face := range cube.Faces() { + if slices.Contains(ignoredFaces, face) { + continue + } + if w.RedstonePower(pos.Side(face), face, true) > 0 { + return true + } + } + return false +} + +// identifyNeighbours identifies the neighbouring positions of a given node, determines their types, and links them into +// the graph. After that, based on what nodes in the graph have been visited, the neighbours are reordered left-to-right +// relative to the direction of information flow. +func (n *wireNetwork) identifyNeighbours(w *world.World, node *wireNode) { + neighbours := computeRedstoneNeighbours(node.pos) + neighboursVisited := make([]bool, 0, 24) + neighbourNodes := make([]*wireNode, 0, 24) + for _, neighbourPos := range neighbours[:24] { + neighbour, ok := n.nodeCache[neighbourPos] + if !ok { + neighbour = &wireNode{ + pos: neighbourPos, + block: w.Block(neighbourPos), + } + n.nodeCache[neighbourPos] = neighbour + n.nodes = append(n.nodes, neighbour) + } + + neighbourNodes = append(neighbourNodes, neighbour) + neighboursVisited = append(neighboursVisited, neighbour.visited) + } + + fromWest := neighboursVisited[0] || neighboursVisited[7] || neighboursVisited[8] + fromEast := neighboursVisited[1] || neighboursVisited[12] || neighboursVisited[13] + fromNorth := neighboursVisited[4] || neighboursVisited[17] || neighboursVisited[20] + fromSouth := neighboursVisited[5] || neighboursVisited[18] || neighboursVisited[21] + + var cX, cZ int32 + if fromWest { + cX++ + } + if fromEast { + cX-- + } + if fromNorth { + cZ++ + } + if fromSouth { + cZ-- + } + + var heading uint32 + if cX == 0 && cZ == 0 { + heading = computeRedstoneHeading(node.xBias, node.zBias) + for _, neighbourNode := range neighbourNodes { + neighbourNode.xBias = node.xBias + neighbourNode.zBias = node.zBias + } + } else { + if cX != 0 && cZ != 0 { + if node.xBias != 0 { + cZ = 0 + } + if node.zBias != 0 { + cX = 0 + } + } + heading = computeRedstoneHeading(cX, cZ) + for _, neighbourNode := range neighbourNodes { + neighbourNode.xBias = cX + neighbourNode.zBias = cZ + } + } + + n.orientNeighbours(&neighbourNodes, node, heading) +} + +// reordering contains lookup tables that completely remap neighbour positions into a left-to-right ordering, based on +// the cardinal direction that is determined to be forward. +var reordering = [][]uint32{ + {2, 3, 16, 19, 0, 4, 1, 5, 7, 8, 17, 20, 12, 13, 18, 21, 6, 9, 22, 14, 11, 10, 23, 15}, + {2, 3, 16, 19, 4, 1, 5, 0, 17, 20, 12, 13, 18, 21, 7, 8, 22, 14, 11, 15, 23, 9, 6, 10}, + {2, 3, 16, 19, 1, 5, 0, 4, 12, 13, 18, 21, 7, 8, 17, 20, 11, 15, 23, 10, 6, 14, 22, 9}, + {2, 3, 16, 19, 5, 0, 4, 1, 18, 21, 7, 8, 17, 20, 12, 13, 23, 10, 6, 9, 22, 15, 11, 14}, +} + +// orientNeighbours reorders the neighbours of a node based on the direction that is determined to be forward. +func (n *wireNetwork) orientNeighbours(src *[]*wireNode, dst *wireNode, heading uint32) { + dst.oriented = true + dst.neighbours = make([]*wireNode, 0, 24) + for _, i := range reordering[heading] { + dst.neighbours = append(dst.neighbours, (*src)[i]) + } +} + +// propagateChanges propagates changes for any redstone wire in layer N, informing the neighbours to recompute their +// states in layers N + 1 and N + 2. +func (n *wireNetwork) propagateChanges(w *world.World, node *wireNode, layer uint32) { + if !node.oriented { + n.identifyNeighbours(w, node) + } + + layerOne := layer + 1 + for _, neighbour := range node.neighbours[:24] { + if layerOne > neighbour.layer { + neighbour.layer = layerOne + n.updateQueue[1] = append(n.updateQueue[1], neighbour) + } + } + + layerTwo := layer + 2 + for _, neighbour := range node.neighbours[:4] { + if layerTwo > neighbour.layer { + neighbour.layer = layerTwo + n.updateQueue[2] = append(n.updateQueue[2], neighbour) + } + } +} + +// breadthFirstWalk performs a breadth-first (layer by layer) traversal through redstone wires, propagating value +// changes to neighbours in the order that they are visited. +func (n *wireNetwork) breadthFirstWalk(w *world.World) { + n.shiftQueue() + n.currentWalkLayer = 1 + + for len(n.updateQueue[0]) > 0 || len(n.updateQueue[1]) > 0 { + for _, node := range n.updateQueue[0] { + if _, ok := node.block.(RedstoneWire); ok { + n.updateNode(w, node, n.currentWalkLayer) + continue + } + if t, ok := node.block.(RedstoneUpdater); ok { + t.RedstoneUpdate(node.pos, w) + } + } + + n.shiftQueue() + n.currentWalkLayer++ + } + + n.currentWalkLayer = 0 +} + +// shiftQueue shifts the update queue, moving all nodes from the current layer to the next layer. The last queue is then +// simply invalidated. +func (n *wireNetwork) shiftQueue() { + n.updateQueue[0] = n.updateQueue[1] + n.updateQueue[1] = n.updateQueue[2] + n.updateQueue[2] = nil +} + +// updateNode processes a node which has had neighbouring redstone wires that have experienced value changes. +func (n *wireNetwork) updateNode(w *world.World, node *wireNode, layer uint32) { + node.visited = true + + oldWire := node.block.(RedstoneWire) + newWire := n.calculateCurrentChanges(w, node) + if oldWire.Power != newWire.Power { + node.block = newWire + + n.propagateChanges(w, node, layer) + } +} + +var ( + rsNeighbours = [...]uint32{4, 5, 6, 7} + rsNeighboursUp = [...]uint32{9, 11, 13, 15} + rsNeighboursDn = [...]uint32{8, 10, 12, 14} +) + +// calculateCurrentChanges computes redstone wire power levels from neighboring blocks. Modifications cut the number of +// power level changes by about 45% from vanilla, and also synergies well with the breadth-first search implementation. +func (n *wireNetwork) calculateCurrentChanges(w *world.World, node *wireNode) RedstoneWire { + wire := node.block.(RedstoneWire) + i := wire.Power + + var blockPower int + if !node.oriented { + n.identifyNeighbours(w, node) + } + + var wirePower int + for _, face := range cube.Faces() { + wirePower = max(wirePower, w.RedstonePower(node.pos.Side(face), face, false)) + } + + if wirePower < 15 { + centerUp := node.neighbours[1].block + _, centerUpSolid := centerUp.Model().(model.Solid) + for m := 0; m < 4; m++ { + neighbour := node.neighbours[rsNeighbours[m]].block + _, neighbourSolid := neighbour.Model().(model.Solid) + + blockPower = n.maxCurrentStrength(neighbour, blockPower) + if !neighbourSolid { + neighbourDown := node.neighbours[rsNeighboursDn[m]].block + blockPower = n.maxCurrentStrength(neighbourDown, blockPower) + } else if d, ok := neighbour.(LightDiffuser); (!ok || d.LightDiffusionLevel() > 0) && !centerUpSolid { + neighbourUp := node.neighbours[rsNeighboursUp[m]].block + blockPower = n.maxCurrentStrength(neighbourUp, blockPower) + } + } + } + + j := blockPower - 1 + if wirePower > j { + j = wirePower + } + + if i != j { + wire.Power = j + w.SetBlock(node.pos, wire, &world.SetOpts{DisableBlockUpdates: true}) + } + return wire +} + +// maxCurrentStrength computes a redstone wire's power level based on a cached state. +func (n *wireNetwork) maxCurrentStrength(neighbour world.Block, strength int) int { + if wire, ok := neighbour.(RedstoneWire); ok { + return max(wire.Power, strength) + } + return strength +} + +// computeRedstoneNeighbours computes the neighbours of a redstone wire node, ignoring neighbours that don't necessarily +// need to be updated, but are in vanilla. +func computeRedstoneNeighbours(pos cube.Pos) []cube.Pos { + return []cube.Pos{ + // Immediate neighbours, in the order of west, east, down, up, north, and finally south. + pos.Side(cube.FaceWest), + pos.Side(cube.FaceEast), + pos.Side(cube.FaceDown), + pos.Side(cube.FaceUp), + pos.Side(cube.FaceNorth), + pos.Side(cube.FaceSouth), + + // Neighbours of neighbours, in the same order, except that duplicates are not included. + pos.Side(cube.FaceWest).Side(cube.FaceWest), + pos.Side(cube.FaceWest).Side(cube.FaceDown), + pos.Side(cube.FaceWest).Side(cube.FaceUp), + pos.Side(cube.FaceWest).Side(cube.FaceNorth), + pos.Side(cube.FaceWest).Side(cube.FaceSouth), + + pos.Side(cube.FaceEast).Side(cube.FaceEast), + pos.Side(cube.FaceEast).Side(cube.FaceDown), + pos.Side(cube.FaceEast).Side(cube.FaceUp), + pos.Side(cube.FaceEast).Side(cube.FaceNorth), + pos.Side(cube.FaceEast).Side(cube.FaceSouth), + + pos.Side(cube.FaceDown).Side(cube.FaceDown), + pos.Side(cube.FaceDown).Side(cube.FaceNorth), + pos.Side(cube.FaceDown).Side(cube.FaceSouth), + + pos.Side(cube.FaceUp).Side(cube.FaceUp), + pos.Side(cube.FaceUp).Side(cube.FaceNorth), + pos.Side(cube.FaceUp).Side(cube.FaceSouth), + + pos.Side(cube.FaceNorth).Side(cube.FaceNorth), + pos.Side(cube.FaceSouth).Side(cube.FaceSouth), + } +} + +// computeRedstoneHeading computes the cardinal direction that is "forward" given which redstone wires have been visited +// and which have not around the position currently being processed. +func computeRedstoneHeading(rX, rZ int32) uint32 { + code := (rX + 1) + 3*(rZ+1) + switch code { + case 0: + return wireHeadingNorth + case 1: + return wireHeadingNorth + case 2: + return wireHeadingEast + case 3: + return wireHeadingWest + case 4: + return wireHeadingWest + case 5: + return wireHeadingEast + case 6: + return wireHeadingSouth + case 7: + return wireHeadingSouth + case 8: + return wireHeadingSouth + } + panic("should never happen") +} diff --git a/server/block/redstone_block.go b/server/block/redstone_block.go new file mode 100644 index 000000000..37443cee3 --- /dev/null +++ b/server/block/redstone_block.go @@ -0,0 +1,58 @@ +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" +) + +type RedstoneBlock struct { + solid +} + +// BreakInfo ... +func (r RedstoneBlock) BreakInfo() BreakInfo { + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(r)).withBreakHandler(func(pos cube.Pos, w *world.World, _ item.User) { + updateAroundRedstone(pos, w) + }) +} + +// EncodeItem ... +func (r RedstoneBlock) EncodeItem() (name string, meta int16) { + return "minecraft:redstone_block", 0 +} + +// EncodeBlock ... +func (r RedstoneBlock) EncodeBlock() (string, map[string]any) { + return "minecraft:redstone_block", nil +} + +// UseOnBlock ... +func (r RedstoneBlock) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(w, pos, face, r) + if !used { + return false + } + place(w, pos, r, user, ctx) + if placed(ctx) { + updateAroundRedstone(pos, w) + return true + } + return false +} + +// Source ... +func (r RedstoneBlock) Source() bool { + return true +} + +// WeakPower ... +func (r RedstoneBlock) WeakPower(cube.Pos, cube.Face, *world.World, bool) int { + return 15 +} + +// StrongPower ... +func (r RedstoneBlock) StrongPower(cube.Pos, cube.Face, *world.World, bool) int { + return 0 +} diff --git a/server/block/redstone_lamp.go b/server/block/redstone_lamp.go new file mode 100644 index 000000000..320833141 --- /dev/null +++ b/server/block/redstone_lamp.go @@ -0,0 +1,77 @@ +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" + "time" +) + +// RedstoneLamp is a block that produces light when activated with a redstone signal. +type RedstoneLamp struct { + solid + + // Lit is if the redstone lamp is lit and emitting light. + Lit bool +} + +// BreakInfo ... +func (l RedstoneLamp) BreakInfo() BreakInfo { + return newBreakInfo(0.3, alwaysHarvestable, nothingEffective, oneOf(l)) +} + +// LightEmissionLevel ... +func (l RedstoneLamp) LightEmissionLevel() uint8 { + if l.Lit { + return 15 + } + return 0 +} + +// EncodeItem ... +func (l RedstoneLamp) EncodeItem() (name string, meta int16) { + return "minecraft:redstone_lamp", 0 +} + +// EncodeBlock ... +func (l RedstoneLamp) EncodeBlock() (string, map[string]any) { + if l.Lit { + return "minecraft:lit_redstone_lamp", nil + } + return "minecraft:redstone_lamp", nil +} + +// UseOnBlock ... +func (l RedstoneLamp) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) (used bool) { + pos, _, used = firstReplaceable(w, pos, face, l) + if !used { + return + } + l.Lit = receivedRedstonePower(pos, w) + place(w, pos, l, user, ctx) + return placed(ctx) +} + +// RedstoneUpdate ... +func (l RedstoneLamp) RedstoneUpdate(pos cube.Pos, w *world.World) { + if l.Lit == receivedRedstonePower(pos, w) { + return + } + if !l.Lit { + l.Lit = true + w.SetBlock(pos, l, &world.SetOpts{DisableBlockUpdates: true}) + } else { + w.ScheduleBlockUpdate(pos, time.Millisecond*50) + } +} + +// ScheduledTick ... +func (l RedstoneLamp) ScheduledTick(pos cube.Pos, w *world.World, _ *rand.Rand) { + if receivedRedstonePower(pos, w) { + return + } + l.Lit = false + w.SetBlock(pos, l, &world.SetOpts{DisableBlockUpdates: true}) +} diff --git a/server/block/redstone_ore.go b/server/block/redstone_ore.go new file mode 100644 index 000000000..c6333dfb7 --- /dev/null +++ b/server/block/redstone_ore.go @@ -0,0 +1,39 @@ +package block + +import "github.com/df-mc/dragonfly/server/item" + +// RedstoneOre is a common ore. +type RedstoneOre struct { + solid + bassDrum + + // Type is the type of coal ore. + Type OreType +} + +// BreakInfo ... +func (c RedstoneOre) BreakInfo() BreakInfo { + i := newBreakInfo(c.Type.Hardness(), func(t item.Tool) bool { + return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierIron.HarvestLevel + }, pickaxeEffective, silkTouchOneOf(RedstoneWire{}, c)).withXPDropRange(1, 5) + if c.Type == DeepslateOre() { + i = i.withBlastResistance(9) + } + return i +} + +// SmeltInfo ... +func (RedstoneOre) SmeltInfo() item.SmeltInfo { + return newOreSmeltInfo(item.NewStack(RedstoneWire{}, 1), 0.7) +} + +// EncodeItem ... +func (c RedstoneOre) EncodeItem() (name string, meta int16) { + return "minecraft:" + c.Type.Prefix() + "redstone_ore", 0 +} + +// EncodeBlock ... +func (c RedstoneOre) EncodeBlock() (string, map[string]any) { + return "minecraft:" + c.Type.Prefix() + "redstone_ore", nil + +} diff --git a/server/block/redstone_torch.go b/server/block/redstone_torch.go new file mode 100644 index 000000000..c36a97387 --- /dev/null +++ b/server/block/redstone_torch.go @@ -0,0 +1,173 @@ +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" + "time" +) + +// RedstoneTorch is a non-solid blocks that emits little light and also a full-strength redstone signal when lit. +// TODO: Redstone torches should burn out when used too recently and excessively. +type RedstoneTorch struct { + transparent + empty + + // Facing is the direction from the torch to the block. + Facing cube.Face + // Lit is if the redstone torch is lit and emitting power. + Lit bool +} + +// HasLiquidDrops ... +func (RedstoneTorch) HasLiquidDrops() bool { + return true +} + +// LightEmissionLevel ... +func (t RedstoneTorch) LightEmissionLevel() uint8 { + if t.Lit { + return 7 + } + return 0 +} + +// BreakInfo ... +func (t RedstoneTorch) BreakInfo() BreakInfo { + return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(t)).withBreakHandler(func(pos cube.Pos, w *world.World, _ item.User) { + updateDirectionalRedstone(pos, w, t.Facing.Opposite()) + }) +} + +// UseOnBlock ... +func (t RedstoneTorch) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) bool { + pos, face, used := firstReplaceable(w, pos, face, t) + if !used { + return false + } + if face == cube.FaceDown { + return false + } + if _, ok := w.Block(pos).(world.Liquid); ok { + return false + } + if !w.Block(pos.Side(face.Opposite())).Model().FaceSolid(pos.Side(face.Opposite()), face, w) { + found := false + for _, i := range []cube.Face{cube.FaceSouth, cube.FaceWest, cube.FaceNorth, cube.FaceEast, cube.FaceDown} { + if w.Block(pos.Side(i)).Model().FaceSolid(pos.Side(i), i.Opposite(), w) { + found = true + face = i.Opposite() + break + } + } + if !found { + return false + } + } + t.Facing = face.Opposite() + t.Lit = true + + place(w, pos, t, user, ctx) + if placed(ctx) { + t.RedstoneUpdate(pos, w) + updateDirectionalRedstone(pos, w, t.Facing.Opposite()) + return true + } + return false +} + +// NeighbourUpdateTick ... +func (t RedstoneTorch) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { + if !w.Block(pos.Side(t.Facing)).Model().FaceSolid(pos.Side(t.Facing), t.Facing.Opposite(), w) { + w.SetBlock(pos, nil, nil) + dropItem(w, item.NewStack(t, 1), pos.Vec3Centre()) + updateDirectionalRedstone(pos, w, t.Facing.Opposite()) + } +} + +// RedstoneUpdate ... +func (t RedstoneTorch) RedstoneUpdate(pos cube.Pos, w *world.World) { + if t.inputStrength(pos, w) > 0 != t.Lit { + return + } + w.ScheduleBlockUpdate(pos, time.Millisecond*100) +} + +// ScheduledTick ... +func (t RedstoneTorch) ScheduledTick(pos cube.Pos, w *world.World, _ *rand.Rand) { + if t.inputStrength(pos, w) > 0 != t.Lit { + return + } + t.Lit = !t.Lit + w.SetBlock(pos, t, nil) + updateDirectionalRedstone(pos, w, t.Facing.Opposite()) +} + +// EncodeItem ... +func (RedstoneTorch) EncodeItem() (name string, meta int16) { + return "minecraft:redstone_torch", 0 +} + +// EncodeBlock ... +func (t RedstoneTorch) EncodeBlock() (name string, properties map[string]any) { + face := "unknown" + if t.Facing != unknownFace { + face = t.Facing.String() + if t.Facing == cube.FaceDown { + face = "top" + } + } + if t.Lit { + return "minecraft:redstone_torch", map[string]any{"torch_facing_direction": face} + } + return "minecraft:unlit_redstone_torch", map[string]any{"torch_facing_direction": face} +} + +// Source ... +func (t RedstoneTorch) Source() bool { + return t.Lit +} + +// WeakPower ... +func (t RedstoneTorch) WeakPower(_ cube.Pos, face cube.Face, _ *world.World, _ bool) int { + if !t.Lit { + return 0 + } + if face == cube.FaceDown { + if t.Facing.Opposite() != cube.FaceDown { + return 15 + } + return 0 + } + if face != t.Facing.Opposite() { + return 15 + } + return 0 +} + +// StrongPower ... +func (t RedstoneTorch) StrongPower(_ cube.Pos, face cube.Face, _ *world.World, _ bool) int { + if t.Lit && face == cube.FaceDown { + return 15 + } + return 0 +} + +// inputStrength ... +func (t RedstoneTorch) inputStrength(pos cube.Pos, w *world.World) int { + return w.RedstonePower(pos.Side(t.Facing), t.Facing, true) +} + +// allRedstoneTorches ... +func allRedstoneTorches() (all []world.Block) { + for _, f := range append(cube.Faces(), unknownFace) { + if f == cube.FaceUp { + continue + } + all = append(all, RedstoneTorch{Facing: f, Lit: true}) + all = append(all, RedstoneTorch{Facing: f}) + } + return +} diff --git a/server/block/redstone_wire.go b/server/block/redstone_wire.go new file mode 100644 index 000000000..9d895db7a --- /dev/null +++ b/server/block/redstone_wire.go @@ -0,0 +1,190 @@ +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" +) + +// RedstoneWire is a block that is used to transfer a charge between objects. Charged objects can be used to open doors +// or activate certain items. This block is the placed form of redstone which can be found by mining Redstone Ore with +// an Iron Pickaxe or better. Deactivated redstone wire will appear dark red, but activated redstone wire will appear +// bright red with a sparkling particle effect. +type RedstoneWire struct { + empty + transparent + + // Power is the current power level of the redstone wire. It ranges from 0 to 15. + Power int +} + +// HasLiquidDrops ... +func (RedstoneWire) HasLiquidDrops() bool { + return true +} + +// BreakInfo ... +func (r RedstoneWire) BreakInfo() BreakInfo { + return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(r)).withBreakHandler(func(pos cube.Pos, w *world.World, _ item.User) { + updateStrongRedstone(pos, w) + }) +} + +// EncodeItem ... +func (RedstoneWire) EncodeItem() (name string, meta int16) { + return "minecraft:redstone", 0 +} + +// EncodeBlock ... +func (r RedstoneWire) EncodeBlock() (string, map[string]any) { + return "minecraft:redstone_wire", map[string]any{ + "redstone_signal": int32(r.Power), + } +} + +// UseOnBlock ... +func (r RedstoneWire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) (used bool) { + pos, _, used = firstReplaceable(w, pos, face, r) + if !used { + return + } + if _, ok := w.Liquid(pos); ok { + return false + } + belowPos := pos.Side(cube.FaceDown) + if !w.Block(belowPos).Model().FaceSolid(belowPos, cube.FaceUp, w) { + return + } + r.Power = r.calculatePower(pos, w) + place(w, pos, r, user, ctx) + if placed(ctx) { + updateStrongRedstone(pos, w) + return true + } + return false +} + +// NeighbourUpdateTick ... +func (r RedstoneWire) NeighbourUpdateTick(pos, neighbour cube.Pos, w *world.World) { + if pos == neighbour { + // Ignore neighbour updates on ourself. + return + } + if _, ok := w.Block(pos.Side(cube.FaceDown)).(Air); ok { + w.SetBlock(pos, nil, nil) + dropItem(w, item.NewStack(r, 1), pos.Vec3Centre()) + return + } + r.RedstoneUpdate(pos, w) +} + +// RedstoneUpdate ... +func (r RedstoneWire) RedstoneUpdate(pos cube.Pos, w *world.World) { + if power := r.calculatePower(pos, w); r.Power != power { + r.Power = power + w.SetBlock(pos, r, &world.SetOpts{DisableBlockUpdates: true}) + updateStrongRedstone(pos, w) + } +} + +// Source ... +func (RedstoneWire) Source() bool { + return false +} + +// WeakPower ... +func (r RedstoneWire) WeakPower(pos cube.Pos, face cube.Face, w *world.World, accountForDust bool) int { + if !accountForDust { + return 0 + } + if face == cube.FaceUp { + return r.Power + } + if face == cube.FaceDown { + return 0 + } + if r.connection(pos, face.Opposite(), w) { + return r.Power + } + if r.connection(pos, face, w) && !r.connection(pos, face.RotateLeft(), w) && !r.connection(pos, face.RotateRight(), w) { + return r.Power + } + return 0 +} + +// StrongPower ... +func (r RedstoneWire) StrongPower(pos cube.Pos, face cube.Face, w *world.World, accountForDust bool) int { + return r.WeakPower(pos, face, w, accountForDust) +} + +// calculatePower returns the highest level of received redstone power at the provided position. +func (r RedstoneWire) calculatePower(pos cube.Pos, w *world.World) int { + aboveBlock := w.Block(pos.Side(cube.FaceUp)) + _, aboveSolid := aboveBlock.Model().(model.Solid) + + var blockPower, wirePower int + for _, side := range cube.Faces() { + neighbourPos := pos.Side(side) + neighbour := w.Block(neighbourPos) + + wirePower = r.maxCurrentStrength(wirePower, neighbourPos, w) + blockPower = max(blockPower, w.RedstonePower(neighbourPos, side, false)) + + if side.Axis() == cube.Y { + // Only check horizontal neighbours from here on. + continue + } + + if d, ok := neighbour.(LightDiffuser); (!ok || d.LightDiffusionLevel() > 0) && !aboveSolid { + wirePower = r.maxCurrentStrength(wirePower, neighbourPos.Side(cube.FaceUp), w) + } + if _, neighbourSolid := neighbour.Model().(model.Solid); !neighbourSolid { + wirePower = r.maxCurrentStrength(wirePower, neighbourPos.Side(cube.FaceDown), w) + } + } + return max(blockPower, wirePower-1) +} + +// maxCurrentStrength ... +func (RedstoneWire) maxCurrentStrength(power int, pos cube.Pos, w *world.World) int { + if wire, ok := w.Block(pos).(RedstoneWire); ok { + return max(wire.Power, power) + } + return power +} + +// connection returns true if the dust connects to the given face. +func (r RedstoneWire) connection(pos cube.Pos, face cube.Face, w *world.World) bool { + sidePos := pos.Side(face) + sideBlock := w.Block(sidePos) + if _, solidAbove := w.Block(pos.Side(cube.FaceUp)).Model().(model.Solid); !solidAbove && r.canRunOnTop(w, sidePos, sideBlock) && r.connectsTo(w.Block(sidePos.Side(cube.FaceUp)), face, false) { + return true + } + _, sideSolid := sideBlock.Model().(model.Solid) + return r.connectsTo(sideBlock, face, true) || !sideSolid && r.connectsTo(w.Block(sidePos.Side(cube.FaceDown)), face, false) +} + +// connectsTo ... +func (RedstoneWire) connectsTo(block world.Block, face cube.Face, allowDirectSources bool) bool { + if _, ok := block.(RedstoneWire); ok { + return true + } + // TODO: Account for other redstone blocks. + c, ok := block.(world.Conductor) + return ok && allowDirectSources && c.Source() +} + +// canRunOnTop ... +func (RedstoneWire) canRunOnTop(w *world.World, pos cube.Pos, block world.Block) bool { + return block.Model().FaceSolid(pos, cube.FaceUp, w) +} + +// allRedstoneWires returns a list of all redstone dust states. +func allRedstoneWires() (all []world.Block) { + for i := 0; i < 16; i++ { + all = append(all, RedstoneWire{Power: i}) + } + return +} diff --git a/server/block/register.go b/server/block/register.go index 1ce675a6c..5a41ed6c8 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -84,6 +84,9 @@ func init() { world.RegisterBlock(RawCopper{}) world.RegisterBlock(RawGold{}) world.RegisterBlock(RawIron{}) + world.RegisterBlock(RedstoneBlock{}) + world.RegisterBlock(RedstoneLamp{Lit: true}) + world.RegisterBlock(RedstoneLamp{}) world.RegisterBlock(ReinforcedDeepslate{}) world.RegisterBlock(Sand{Red: true}) world.RegisterBlock(Sand{}) @@ -98,6 +101,7 @@ func init() { world.RegisterBlock(SporeBlossom{}) world.RegisterBlock(Stone{Smooth: true}) world.RegisterBlock(Stone{}) + world.RegisterBlock(Target{}) world.RegisterBlock(TNT{}) world.RegisterBlock(Terracotta{}) world.RegisterBlock(Tuff{}) @@ -116,6 +120,7 @@ func init() { world.RegisterBlock(GoldOre{Type: ore}) world.RegisterBlock(IronOre{Type: ore}) world.RegisterBlock(LapisOre{Type: ore}) + world.RegisterBlock(RedstoneOre{Type: ore}) } registerAll(allAnvils()) @@ -127,6 +132,7 @@ func init() { registerAll(allBlastFurnaces()) registerAll(allBoneBlock()) registerAll(allBrewingStands()) + registerAll(allButtons()) registerAll(allCactus()) registerAll(allCake()) registerAll(allCampfires()) @@ -156,6 +162,8 @@ func init() { registerAll(allGrindstones()) registerAll(allHayBales()) registerAll(allHoppers()) + registerAll(allIronDoors()) + registerAll(allIronTrapdoors()) registerAll(allItemFrames()) registerAll(allKelp()) registerAll(allLadders()) @@ -163,6 +171,7 @@ func init() { registerAll(allLava()) registerAll(allLeaves()) registerAll(allLecterns()) + registerAll(allLevers()) registerAll(allLight()) registerAll(allLitPumpkins()) registerAll(allLogs()) @@ -173,11 +182,14 @@ func init() { registerAll(allNetherWart()) registerAll(allPlanks()) registerAll(allPotato()) + registerAll(allPressurePlates()) registerAll(allPrismarine()) registerAll(allPumpkinStems()) registerAll(allPumpkins()) registerAll(allPurpurs()) registerAll(allQuartz()) + registerAll(allRedstoneTorches()) + registerAll(allRedstoneWires()) registerAll(allSandstones()) registerAll(allSeaPickles()) registerAll(allSigns()) @@ -277,11 +289,14 @@ func init() { world.RegisterItem(Iron{}) world.RegisterItem(ItemFrame{Glowing: true}) world.RegisterItem(ItemFrame{}) + world.RegisterItem(IronDoor{}) + world.RegisterItem(IronTrapDoor{}) world.RegisterItem(Jukebox{}) world.RegisterItem(Kelp{}) world.RegisterItem(Ladder{}) world.RegisterItem(Lapis{}) world.RegisterItem(Lectern{}) + world.RegisterItem(Lever{}) world.RegisterItem(LitPumpkin{}) world.RegisterItem(Loom{}) world.RegisterItem(MelonSeeds{}) @@ -320,6 +335,10 @@ func init() { world.RegisterItem(RawCopper{}) world.RegisterItem(RawGold{}) world.RegisterItem(RawIron{}) + world.RegisterItem(RedstoneBlock{}) + world.RegisterItem(RedstoneLamp{}) + world.RegisterItem(RedstoneTorch{}) + world.RegisterItem(RedstoneWire{}) world.RegisterItem(ReinforcedDeepslate{}) world.RegisterItem(Sand{Red: true}) world.RegisterItem(Sand{}) @@ -338,6 +357,7 @@ func init() { world.RegisterItem(Stone{Smooth: true}) world.RegisterItem(Stone{}) world.RegisterItem(SugarCane{}) + world.RegisterItem(Target{}) world.RegisterItem(TNT{}) world.RegisterItem(Terracotta{}) world.RegisterItem(Tuff{}) @@ -372,6 +392,12 @@ func init() { for _, t := range AnvilTypes() { world.RegisterItem(Anvil{Type: t}) } + for _, t := range ButtonTypes() { + world.RegisterItem(Button{Type: t}) + } + for _, t := range PressurePlateTypes() { + world.RegisterItem(PressurePlate{Type: t}) + } for _, c := range item.Colours() { world.RegisterItem(Banner{Colour: c}) world.RegisterItem(Carpet{Colour: c}) @@ -406,6 +432,7 @@ func init() { world.RegisterItem(GoldOre{Type: ore}) world.RegisterItem(IronOre{Type: ore}) world.RegisterItem(LapisOre{Type: ore}) + world.RegisterItem(RedstoneOre{Type: ore}) } for _, f := range FireTypes() { world.RegisterItem(Lantern{Type: f}) diff --git a/server/block/target.go b/server/block/target.go new file mode 100644 index 000000000..761b35171 --- /dev/null +++ b/server/block/target.go @@ -0,0 +1,100 @@ +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" + "math/rand" + "time" +) + +// Target is a block that provides redstone power based on how close a projectile hits its center. +type Target struct { + solid + + // Power is the redstone power level emitted by the target block, ranging from 0 to 15. + Power int +} + +// BreakInfo ... +func (t Target) BreakInfo() BreakInfo { + return newBreakInfo(0.5, alwaysHarvestable, hoeEffective, oneOf(t)).withBreakHandler(func(pos cube.Pos, w *world.World, _ item.User) { + updateAroundRedstone(pos, w) + }) +} + +// Source ... +func (t Target) Source() bool { + return true +} + +// WeakPower ... +func (t Target) WeakPower(cube.Pos, cube.Face, *world.World, bool) int { + return t.Power +} + +// StrongPower ... +func (t Target) StrongPower(_ cube.Pos, _ cube.Face, _ *world.World, _ bool) int { + return t.Power +} + +// HitByProjectile handles when a projectile hits the target block. +func (t Target) HitByProjectile(pos mgl64.Vec3, blockPos cube.Pos, w *world.World, delay time.Duration) { + center := blockPos.Vec3Centre() + distance := pos.Sub(center).Len() + + maxDistance := math.Sqrt(0.75) + normalizedDistance := math.Min(distance/maxDistance, 1.0) + + var rawPower float64 + if normalizedDistance <= 0.58 { + rawPower = 15 + } else if normalizedDistance > 0.9 { + rawPower = 0 + } else { + rawPower = 15 * (1 - math.Pow(normalizedDistance, 4.5)) + } + + t.Power = int(math.Max(math.Round(rawPower), 0)) + w.SetBlock(blockPos, t, &world.SetOpts{DisableBlockUpdates: false}) + w.PlaySound(blockPos.Vec3Centre(), sound.PowerOn{}) + + w.ScheduleBlockUpdate(blockPos, delay) +} + +// ScheduledTick ... +func (t Target) ScheduledTick(pos cube.Pos, w *world.World, _ *rand.Rand) { + if t.Power > 0 { + t.Power = 0 + w.SetBlock(pos, t, nil) + w.PlaySound(pos.Vec3Centre(), sound.PowerOff{}) + } +} + +// DecodeNBT ... +func (t Target) DecodeNBT(data map[string]any) any { + t.Power = int(nbtconv.Int32(data, "Power")) + return t +} + +// EncodeNBT ... +func (t Target) EncodeNBT() map[string]any { + m := map[string]any{ + "Power": int32(t.Power), + } + return m +} + +// EncodeItem ... +func (t Target) EncodeItem() (name string, meta int16) { + return "minecraft:target", 0 +} + +// EncodeBlock ... +func (t Target) EncodeBlock() (name string, properties map[string]interface{}) { + return "minecraft:target", nil +} diff --git a/server/block/tnt.go b/server/block/tnt.go index ff22c4f77..af4030bd3 100644 --- a/server/block/tnt.go +++ b/server/block/tnt.go @@ -17,6 +17,18 @@ type TNT struct { igniter world.Entity } +// NeighbourUpdateTick ... +func (t TNT) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { + t.RedstoneUpdate(pos, w) +} + +// RedstoneUpdate ... +func (t TNT) RedstoneUpdate(pos cube.Pos, w *world.World) { + if receivedRedstonePower(pos, w) { + t.Ignite(pos, w, t.igniter) + } +} + // Activate ... func (t TNT) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, ctx *item.UseContext) bool { held, _ := u.HeldItems() diff --git a/server/block/wood_door.go b/server/block/wood_door.go index 0a595b760..80f418ad5 100644 --- a/server/block/wood_door.go +++ b/server/block/wood_door.go @@ -8,6 +8,7 @@ import ( "github.com/df-mc/dragonfly/server/world/particle" "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" + "math/rand" "time" ) @@ -133,6 +134,30 @@ func (d WoodDoor) SideClosed(cube.Pos, cube.Pos, *world.World) bool { return false } +// RedstoneUpdate ... +func (d WoodDoor) RedstoneUpdate(pos cube.Pos, w *world.World) { + if d.Open == receivedRedstonePower(pos, w) { + return + } + if !d.Open { + d.Open = true + w.PlaySound(pos.Vec3Centre(), sound.DoorOpen{Block: d}) + w.SetBlock(pos, d, &world.SetOpts{DisableBlockUpdates: true}) + } else { + w.ScheduleBlockUpdate(pos, time.Millisecond*50) + } +} + +// ScheduledTick ... +func (d WoodDoor) ScheduledTick(pos cube.Pos, w *world.World, _ *rand.Rand) { + if receivedRedstonePower(pos, w) { + return + } + d.Open = false + w.PlaySound(pos.Vec3Centre(), sound.DoorClose{Block: d}) + w.SetBlock(pos, d, &world.SetOpts{DisableBlockUpdates: true}) +} + // EncodeItem ... func (d WoodDoor) EncodeItem() (name string, meta int16) { if d.Wood == OakWood() { diff --git a/server/block/wood_fence_gate.go b/server/block/wood_fence_gate.go index 8f25d4402..1c616091c 100644 --- a/server/block/wood_fence_gate.go +++ b/server/block/wood_fence_gate.go @@ -7,6 +7,7 @@ import ( "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" + "math/rand" "time" ) @@ -94,6 +95,30 @@ func (f WoodFenceGate) SideClosed(cube.Pos, cube.Pos, *world.World) bool { return false } +// RedstoneUpdate ... +func (f WoodFenceGate) RedstoneUpdate(pos cube.Pos, w *world.World) { + if f.Open == receivedRedstonePower(pos, w) { + return + } + if !f.Open { + f.Open = true + w.PlaySound(pos.Vec3Centre(), sound.FenceGateOpen{Block: f}) + w.SetBlock(pos, f, nil) + } else { + w.ScheduleBlockUpdate(pos, time.Millisecond*50) + } +} + +// ScheduledTick ... +func (f WoodFenceGate) ScheduledTick(pos cube.Pos, w *world.World, _ *rand.Rand) { + if receivedRedstonePower(pos, w) { + return + } + f.Open = false + w.PlaySound(pos.Vec3Centre(), sound.FenceGateClose{Block: f}) + w.SetBlock(pos, f, &world.SetOpts{DisableBlockUpdates: true}) +} + // EncodeItem ... func (f WoodFenceGate) EncodeItem() (name string, meta int16) { if f.Wood == OakWood() { diff --git a/server/block/wood_trapdoor.go b/server/block/wood_trapdoor.go index 1eb7fec84..17a66f8ee 100644 --- a/server/block/wood_trapdoor.go +++ b/server/block/wood_trapdoor.go @@ -8,6 +8,7 @@ import ( "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" "math" + "math/rand" "time" ) @@ -82,6 +83,30 @@ func (t WoodTrapdoor) SideClosed(cube.Pos, cube.Pos, *world.World) bool { return false } +// RedstoneUpdate ... +func (t WoodTrapdoor) RedstoneUpdate(pos cube.Pos, w *world.World) { + if t.Open == receivedRedstonePower(pos, w) { + return + } + if !t.Open { + t.Open = true + w.PlaySound(pos.Vec3Centre(), sound.TrapdoorOpen{Block: t}) + w.SetBlock(pos, t, &world.SetOpts{DisableBlockUpdates: true}) + } else { + w.ScheduleBlockUpdate(pos, time.Millisecond*50) + } +} + +// ScheduledTick ... +func (t WoodTrapdoor) ScheduledTick(pos cube.Pos, w *world.World, _ *rand.Rand) { + if receivedRedstonePower(pos, w) { + return + } + t.Open = false + w.PlaySound(pos.Vec3Centre(), sound.TrapdoorClose{Block: t}) + w.SetBlock(pos, t, &world.SetOpts{DisableBlockUpdates: true}) +} + // EncodeItem ... func (t WoodTrapdoor) EncodeItem() (name string, meta int16) { if t.Wood == OakWood() { diff --git a/server/entity/projectile.go b/server/entity/projectile.go index 8a5e520c4..58e815870 100644 --- a/server/entity/projectile.go +++ b/server/entity/projectile.go @@ -172,6 +172,23 @@ func (lt *ProjectileBehaviour) Tick(e *Ent) *Movement { if t, ok := w.Block(bpos).(block.TNT); ok && e.OnFireDuration() > 0 { t.Ignite(bpos, w, e) } + if b, ok := w.Block(bpos).(block.Button); ok { + if b.Type != block.StoneButton() || b.Type != block.PolishedBlackstoneButton() { + b.Click(bpos, w) + } + } + if t, ok := w.Block(bpos).(block.Target); ok { + if _, ok := e.Type().(EnderPearlType); !ok { + delay := time.Millisecond * 400 + + //TODO: account for trident when added. + if _, ok := e.Type().(ArrowType); ok { + delay = time.Millisecond * 1000 + } + + t.HitByProjectile(r.Position(), r.BlockPosition(), w, delay) + } + } if lt.conf.SurviveBlockCollision { lt.hitBlockSurviving(e, r, m) return m diff --git a/server/world/block.go b/server/world/block.go index cfec471ea..d97108418 100644 --- a/server/world/block.go +++ b/server/world/block.go @@ -75,6 +75,17 @@ type Liquid interface { Harden(pos cube.Pos, w *World, flownIntoBy *cube.Pos) bool } +// Conductor represents a block that can conduct a redstone signal. +type Conductor interface { + Block + // Source returns true if the conductor is a signal source. + Source() bool + // WeakPower returns the power from a partial source and has limited usage. + WeakPower(pos cube.Pos, face cube.Face, w *World, accountForDust bool) int + // StrongPower returns the power from a full source and can be passed to any redstone component. + StrongPower(pos cube.Pos, face cube.Face, w *World, accountForDust bool) int +} + // hashes holds a list of runtime IDs indexed by the hash of the Block that implements the blocks pointed to by those // runtime IDs. It is used to look up a block's runtime ID quickly. var ( @@ -296,6 +307,11 @@ type lightDiffuser interface { LightDiffusionLevel() uint8 } +// redstoneBlocking is identical to a block.RedstoneBlocking. +type redstoneBlocking interface { + RedstoneBlocking() bool +} + // replaceableBlock represents a block that may be replaced by another block automatically. An example is // grass, which may be replaced by clicking it with another block. type replaceableBlock interface { diff --git a/server/world/sound/block.go b/server/world/sound/block.go index d9a35c73d..cdb1b9558 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -185,6 +185,12 @@ type ComposterReady struct{ sound } // PotionBrewed is a sound played when a potion is brewed. type PotionBrewed struct{ sound } +// PowerOn is a sound played when a redstone component is powered on. +type PowerOn struct{ sound } + +// PowerOff is a sound played when a redstone component is powered off. +type PowerOff struct{ sound } + // LecternBookPlace is a sound played when a book is placed in a lectern. type LecternBookPlace struct{ sound } diff --git a/server/world/tick.go b/server/world/tick.go index b2b163aae..dd4b91149 100644 --- a/server/world/tick.go +++ b/server/world/tick.go @@ -87,6 +87,13 @@ func (t ticker) tickScheduledBlocks(tick int64) { t.w.updateMu.Unlock() for _, pos := range positions { + // We need to incrementally delete each scheduled update from the map, as each block update itself might attempt + // to schedule another block update, which could conflict with the current update selection. + // TODO: Definitely shouldn't lock like this. I need coffee. And sleep. + t.w.updateMu.Lock() + delete(t.w.scheduledUpdates, pos) + t.w.updateMu.Unlock() + if ticker, ok := t.w.Block(pos).(ScheduledTicker); ok { ticker.ScheduledTick(pos, t.w, t.w.r) } diff --git a/server/world/world.go b/server/world/world.go index fce62e3a7..55977a867 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -950,7 +950,7 @@ func (w *World) ScheduleBlockUpdate(pos cube.Pos, delay time.Duration) { t := w.set.CurrentTick w.set.Unlock() - w.scheduledUpdates[pos] = t + delay.Nanoseconds()/int64(time.Second/20) + w.scheduledUpdates[pos] = t + (delay.Milliseconds() / 50) } // doBlockUpdatesAround schedules block updates directly around and on the position passed. @@ -1018,6 +1018,33 @@ func (w *World) PortalDestination(dim Dimension) *World { return w } +// RedstonePower returns the level of redstone power being emitted from a position to the provided face. +func (w *World) RedstonePower(pos cube.Pos, face cube.Face, accountForDust bool) (power int) { + b := w.Block(pos) + if c, ok := b.(Conductor); ok { + power = c.WeakPower(pos, face, w, accountForDust) + } + if b, ok := b.(redstoneBlocking); ok && b.RedstoneBlocking() { + return power + } + if d, ok := b.(lightDiffuser); ok && d.LightDiffusionLevel() == 0 { + return power + } + for _, f := range cube.Faces() { + if !b.Model().FaceSolid(pos, f, w) { + return power + } + } + for _, f := range cube.Faces() { + c, ok := w.Block(pos.Side(f)).(Conductor) + if !ok { + continue + } + power = max(power, c.StrongPower(pos.Side(f), f, w, accountForDust)) + } + return power +} + // Save saves the World to the provider. func (w *World) Save() { w.conf.Log.Debug("Saving chunks in memory to disk...")