From bbd3a0e5248ec636672a86d30020469d3f2904ce Mon Sep 17 00:00:00 2001 From: cqdetdev <101936396+cqdetdev@users.noreply.github.com> Date: Sun, 10 May 2026 17:26:58 -0400 Subject: [PATCH 1/4] feat: add redstone core engine and basic sources --- cmd/blockhash/main.go | 12 +- server/block/hash.go | 17 +- server/block/lever.go | 132 ----- server/block/note.go | 23 +- server/block/redstone.go | 459 ++++++---------- server/block/redstone_block.go | 60 --- server/block/redstone_common.go | 11 + server/block/redstone_sources.go | 594 +++++++++++++++++++++ server/block/redstone_sources_test.go | 291 +++++++++++ server/block/redstone_test.go | 139 +++++ server/block/redstone_torch.go | 291 +++-------- server/block/redstone_torch_test.go | 93 ++++ server/block/redstone_wire.go | 273 ---------- server/block/register.go | 31 ++ server/session/world.go | 8 + server/world/block.go | 50 -- server/world/conf.go | 1 + server/world/handler.go | 15 +- server/world/redstone.go | 724 ++++++++++++++++++++++++++ server/world/redstone_bench_test.go | 50 ++ server/world/redstone_test.go | 248 +++++++++ server/world/sound/block.go | 12 + server/world/tick.go | 42 +- server/world/tx.go | 99 ++-- server/world/world.go | 64 ++- 25 files changed, 2614 insertions(+), 1125 deletions(-) delete mode 100644 server/block/lever.go delete mode 100644 server/block/redstone_block.go create mode 100644 server/block/redstone_common.go create mode 100644 server/block/redstone_sources.go create mode 100644 server/block/redstone_sources_test.go create mode 100644 server/block/redstone_test.go create mode 100644 server/block/redstone_torch_test.go delete mode 100644 server/block/redstone_wire.go create mode 100644 server/world/redstone.go create mode 100644 server/world/redstone_bench_test.go create mode 100644 server/world/redstone_test.go diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index 3f86174cd..ab3794f6a 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -252,11 +252,19 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[ case "CoralType", "SkullType": return "uint64(" + s + ".Uint8())", 3 case "AnvilType", "SandstoneType", "PrismarineType", "StoneBricksType", "NetherBricksType", "FroglightType", - "WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType", "CopperType", "OxidationType": + "WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType", "CopperType", "OxidationType", "BellAttachment": return "uint64(" + s + ".Uint8())", 2 + case "CrafterOrientation": + return "uint64(" + s + ".Uint8())", 4 case "OreType", "FireType", "DoubleTallGrassType": return "uint64(" + s + ".Uint8())", 1 - case "Direction", "Axis": + case "Direction": + return "uint64(" + s + ")", 2 + case "Axis": + if _, ok := directives["lever_axis"]; ok { + receiver, _, _ := strings.Cut(s, ".") + return "leverAxisHash(" + receiver + ")", 1 + } return "uint64(" + s + ")", 2 case "Face": return "uint64(" + s + ")", 3 diff --git a/server/block/hash.go b/server/block/hash.go index 684d9f010..11cb75f02 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -25,6 +25,7 @@ const ( hashBookshelf hashBrewingStand hashBricks + hashButton hashCactus hashCake hashCalcite @@ -148,6 +149,7 @@ const ( hashPolishedBlackstoneBrick hashPolishedTuff hashPotato + hashPressurePlate hashPrismarine hashPumpkin hashPumpkinSeeds @@ -160,6 +162,7 @@ const ( hashRawGold hashRawIron hashRedstoneBlock + hashRedstoneLamp hashRedstoneOre hashRedstoneTorch hashRedstoneWire @@ -300,6 +303,10 @@ func (Bricks) Hash() (uint64, uint64) { return hashBricks, 0 } +func (b Button) Hash() (uint64, uint64) { + return hashButton, uint64(b.Type) | uint64(b.Facing)<<8 | uint64(boolByte(b.Pressed))<<11 +} + func (c Cactus) Hash() (uint64, uint64) { return hashCactus, uint64(c.Age) } @@ -665,7 +672,7 @@ func (l Lectern) Hash() (uint64, uint64) { } func (l Lever) Hash() (uint64, uint64) { - return hashLever, uint64(boolByte(l.Powered)) | uint64(l.Facing)<<1 | uint64(l.Direction)<<4 + return hashLever, uint64(l.Facing) | leverAxisHash(l)<<3 | uint64(boolByte(l.Powered))<<4 } func (l Light) Hash() (uint64, uint64) { @@ -792,6 +799,10 @@ func (p Potato) Hash() (uint64, uint64) { return hashPotato, uint64(p.Growth) } +func (p PressurePlate) Hash() (uint64, uint64) { + return hashPressurePlate, uint64(p.Type) | uint64(p.Power)<<8 +} + func (p Prismarine) Hash() (uint64, uint64) { return hashPrismarine, uint64(p.Type.Uint8()) } @@ -840,6 +851,10 @@ func (RedstoneBlock) Hash() (uint64, uint64) { return hashRedstoneBlock, 0 } +func (r RedstoneLamp) Hash() (uint64, uint64) { + return hashRedstoneLamp, uint64(boolByte(r.Lit)) +} + func (r RedstoneOre) Hash() (uint64, uint64) { return hashRedstoneOre, uint64(r.Type.Uint8()) | uint64(boolByte(r.Lit))<<1 } diff --git a/server/block/lever.go b/server/block/lever.go deleted file mode 100644 index 10a2cacbe..000000000 --- a/server/block/lever.go +++ /dev/null @@ -1,132 +0,0 @@ -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. Currently, only North and West directions are supported due to Bedrock Edition limitations. - Direction cube.Direction -} - -// RedstoneSource ... -func (l Lever) RedstoneSource() bool { - return true -} - -// WeakPower ... -func (l Lever) WeakPower(cube.Pos, cube.Face, *world.Tx, bool) int { - if l.Powered { - return 15 - } - return 0 -} - -// StrongPower ... -func (l Lever) StrongPower(_ cube.Pos, face cube.Face, _ *world.Tx, _ bool) int { - if l.Powered && l.Facing == face { - return 15 - } - return 0 -} - -// SideClosed ... -func (l Lever) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { - return false -} - -// NeighbourUpdateTick ... -func (l Lever) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { - supportPos := pos.Side(l.Facing.Opposite()) - if !tx.Block(supportPos).Model().FaceSolid(supportPos, l.Facing, tx) { - breakBlock(l, pos, tx) - } -} - -// UseOnBlock ... -func (l Lever) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { - pos, face, used := firstReplaceable(tx, pos, face, l) - if !used { - return false - } - supportPos := pos.Side(face.Opposite()) - if !tx.Block(supportPos).Model().FaceSolid(supportPos, face, tx) { - return false - } - - l.Powered = 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(tx, pos, l, user, ctx) - return placed(ctx) -} - -// Activate ... -func (l Lever) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { - l.Powered = !l.Powered - tx.SetBlock(pos, l, nil) - if l.Powered { - tx.PlaySound(pos.Vec3Centre(), sound.PowerOn{}) - } else { - tx.PlaySound(pos.Vec3Centre(), sound.PowerOff{}) - } - updateDirectionalRedstone(pos, tx, l.Facing.Opposite()) - return true -} - -// BreakInfo ... -func (l Lever) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, nothingEffective, oneOf(Lever{})).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { - updateDirectionalRedstone(pos, tx, 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/note.go b/server/block/note.go index 719d9cd52..f32ea434a 100644 --- a/server/block/note.go +++ b/server/block/note.go @@ -61,19 +61,17 @@ func (n Note) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ * return true } -// RedstoneUpdate updates the note block's powered state and plays the note when it first becomes powered. -func (n Note) RedstoneUpdate(pos cube.Pos, tx *world.Tx) { - poweredFaces := n.poweredFaces(pos, tx) - powered := len(poweredFaces) > 0 +// RedstonePowerUpdate updates the note block's powered state and plays the note when it first becomes powered. +func (n Note) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, power int) (world.Block, bool) { + powered := power > 0 if powered == n.Powered { - return + return n, false } n.Powered = powered if powered && n.canPlay(pos, tx) { n.playNote(pos, tx) } - tx.SetBlock(pos, n, &world.SetOpts{DisableBlockUpdates: true}) - updateAroundRedstone(pos, tx, poweredFaces...) + return n, true } // canPlay reports whether the block above the note block is air. @@ -82,17 +80,6 @@ func (n Note) canPlay(pos cube.Pos, tx *world.Tx) bool { return ok } -func (n Note) poweredFaces(pos cube.Pos, tx *world.Tx) []cube.Face { - var faces []cube.Face - for _, face := range cube.Faces() { - adjacentPos := pos.Side(face) - if power := tx.RedstonePower(adjacentPos, face, true); power > 0 { - faces = append(faces, face) - } - } - return faces -} - // BreakInfo ... func (n Note) BreakInfo() BreakInfo { return newBreakInfo(0.8, alwaysHarvestable, axeEffective, oneOf(Note{})) diff --git a/server/block/redstone.go b/server/block/redstone.go index 4ba84dd68..5e329581e 100644 --- a/server/block/redstone.go +++ b/server/block/redstone.go @@ -1,380 +1,215 @@ package block import ( - "slices" - "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/block/model" - "github.com/df-mc/dragonfly/server/event" + "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" ) -// 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 +// RedstoneBlock is a solid block that emits maximum redstone power. +type RedstoneBlock struct { + solid } -// wireNode is a data structure to keep track of redstone wires and neighbours that will receive updates. -type wireNode struct { - visited bool +// BreakInfo ... +func (r RedstoneBlock) BreakInfo() BreakInfo { + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(r)).withBlastResistance(30) +} - pos cube.Pos - block world.Block - source cube.Pos +// RedstonePower always returns maximum power. +func (RedstoneBlock) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { + return 15 +} - neighbours []*wireNode - oriented bool +// RedstoneStrongPower always returns maximum strong power. +func (RedstoneBlock) RedstoneStrongPower(cube.Pos, *world.Tx, cube.Face) int { + return 15 +} - xBias int32 - zBias int32 +// EncodeItem ... +func (RedstoneBlock) EncodeItem() (name string, meta int16) { + return "minecraft:redstone_block", 0 +} - layer uint32 +// EncodeBlock ... +func (RedstoneBlock) EncodeBlock() (string, map[string]any) { + return "minecraft:redstone_block", nil } -const ( - wireHeadingNorth = 0 - wireHeadingEast = 1 - wireHeadingSouth = 2 - wireHeadingWest = 3 -) +// RedstoneWire is redstone dust placed in the world. Power is stored as a value from 0 to 15. +type RedstoneWire struct { + empty + transparent + sourceWaterDisplacer -// 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, tx *world.Tx) { - n := &wireNetwork{ - nodeCache: make(map[cube.Pos]*wireNode), - } + // Power is the current signal strength carried by the wire. + Power int +} - root := &wireNode{ - block: tx.Block(pos), - pos: pos, - visited: true, +// UseOnBlock places redstone wire on a replaceable block. +func (r RedstoneWire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(tx, pos, face, r) + if !used || !redstoneWireSupported(tx, pos) { + return false } - n.nodeCache[pos] = root - n.nodes = append(n.nodes, root) + place(tx, pos, r, user, ctx) + return placed(ctx) +} - n.propagateChanges(tx, root, 0) - n.breadthFirstWalk(tx) +// RedstonePower returns the wire's current signal strength. +func (r RedstoneWire) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { + return r.Power } -// updateAroundRedstone updates redstone components around the given center 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, tx *world.Tx, ignoredFaces ...cube.Face) { - // Order matches Java 1.19's RedstoneCircuit traversal. - 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) - updateRedstoneFrom(pos, centre, tx) - updateRedstoneFrom(pos.Side(cube.FaceUp), centre, tx) - updateRedstoneFrom(pos.Side(cube.FaceDown), centre, tx) - updateReceiversAroundPoweredBlock(pos, tx, face.Opposite()) - } +// RedstoneSignalLoss returns the signal loss through a wire segment. +func (RedstoneWire) RedstoneSignalLoss(cube.Pos, *world.Tx, cube.Face, cube.Face) int { + return 1 } -// updateReceiversAroundPoweredBlock updates redstone receivers directly adjacent to an indirectly powered solid block. -// This keeps mechanisms behind the powered block in sync without walking corner positions around the original update. -// For example, this covers the following vertical path: torch -> stone -> note block. -func updateReceiversAroundPoweredBlock(pos cube.Pos, tx *world.Tx, ignoredFaces ...cube.Face) { - if _, ok := tx.Block(pos).Model().(model.Solid); !ok { - return - } - for _, face := range cube.Faces() { - if slices.Contains(ignoredFaces, face) || tx.RedstonePower(pos, face, true) == 0 { +// RedstoneRelayerNeighbours returns all wire positions directly connected to this dust, including dust stepping up or +// down adjacent blocks. +func (RedstoneWire) RedstoneRelayerNeighbours(pos cube.Pos, tx *world.Tx) []cube.Pos { + neighbours := make([]cube.Pos, 0, 12) + for _, face := range cube.HorizontalFaces() { + side := pos.Side(face) + if side.OutOfBounds(tx.Range()) { continue } - updateRedstoneFrom(pos.Side(face), pos, tx) + neighbours = append(neighbours, side) + + above := pos.Side(cube.FaceUp) + if !redstoneWireBlocksConnection(tx, above, cube.FaceDown) && redstoneWireSupported(tx, side.Side(cube.FaceUp)) { + neighbours = append(neighbours, side.Side(cube.FaceUp)) + } + if !redstoneWireBlocksConnection(tx, side, cube.FaceUp) { + down := side.Side(cube.FaceDown) + if !down.OutOfBounds(tx.Range()) { + neighbours = append(neighbours, down) + } + } } + return neighbours } -// updateRedstone dispatches a cancellable redstone update to the block at pos, if it handles redstone updates. -// Prefer updateRedstone over calling RedstoneUpdate directly so HandleRedstoneUpdate gets a chance to observe -// and optionally cancel the update. Direct RedstoneUpdate calls are reserved for internal state initialisation -// (e.g. seeding a freshly placed block) where handler cancellation isn't appropriate. -func updateRedstone(pos cube.Pos, tx *world.Tx) { - updateRedstoneFrom(pos, pos, tx) +// RedstonePowerUpdate updates the wire strength to match its strongest input. +func (r RedstoneWire) RedstonePowerUpdate(_ cube.Pos, _ *world.Tx, power int) (world.Block, bool) { + power = max(0, min(power, 15)) + if r.Power == power { + return r, false + } + r.Power = power + return r, true } -// updateRedstoneFrom dispatches a cancellable redstone update and records the position that caused it. -func updateRedstoneFrom(pos, source cube.Pos, tx *world.Tx) { - r, ok := tx.Block(pos).(world.RedstoneUpdater) - if !ok { - return +// NeighbourUpdateTick breaks unsupported wire. +func (r RedstoneWire) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if !redstoneWireSupported(tx, pos) { + breakBlock(r, pos, tx) } - if redstoneUpdateCancelled(pos, tx) { - return - } - tx.Redstone().WithUpdateSource(source, func() { - r.RedstoneUpdate(pos, tx) - }) } -// redstoneUpdateCancelled checks if the redstone update has been cancelled by the HandleRedstoneUpdate handler. -func redstoneUpdateCancelled(pos cube.Pos, tx *world.Tx) bool { - ctx := event.C(tx) - tx.World().Handler().HandleRedstoneUpdate(ctx, pos) - return ctx.Cancelled() +// HasLiquidDrops ... +func (RedstoneWire) HasLiquidDrops() bool { + return true } -// updateDirectionalRedstone updates redstone components through the given face. This implementation is based off of -// RedstoneCircuit and Java 1.19. -func updateDirectionalRedstone(pos cube.Pos, tx *world.Tx, face cube.Face) { - updateAroundRedstone(pos, tx) - updateAroundRedstone(pos.Side(face), tx, face.Opposite()) +// BreakInfo ... +func (r RedstoneWire) BreakInfo() BreakInfo { + return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(RedstoneWire{})) } -// 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(tx *world.Tx, node *wireNode) { - var neighboursVisited [24]bool - var neighbourNodes [24]*wireNode - for i, offset := range redstoneNeighbourOffsets { - neighbourPos := node.pos.Add(offset) - neighbour, ok := n.nodeCache[neighbourPos] - if !ok { - neighbour = &wireNode{ - pos: neighbourPos, - block: tx.Block(neighbourPos), - } - n.nodeCache[neighbourPos] = neighbour - n.nodes = append(n.nodes, neighbour) - } - neighbourNodes[i] = neighbour - neighboursVisited[i] = 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) +// SideClosed ... +func (RedstoneWire) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false } -// redstoneNeighbourOffsets lists the 24 positions visited around a redstone wire node: the 6 immediate neighbours -// followed by the unique neighbours-of-neighbours, in the order west, east, down, up, north, south. The fixed -// indices here are referenced directly by identifyNeighbours and the redstoneReordering tables below. -var redstoneNeighbourOffsets = [...]cube.Pos{ - // Immediate neighbours, in the order of west, east, down, up, north, and south. - {-1, 0, 0}, - {1, 0, 0}, - {0, -1, 0}, - {0, 1, 0}, - {0, 0, -1}, - {0, 0, 1}, - - // Neighbours of neighbours, in the same order, except that duplicates are omitted. - {-2, 0, 0}, - {-1, -1, 0}, - {-1, 1, 0}, - {-1, 0, -1}, - {-1, 0, 1}, - - {2, 0, 0}, - {1, -1, 0}, - {1, 1, 0}, - {1, 0, -1}, - {1, 0, 1}, - - {0, -2, 0}, - {0, -1, -1}, - {0, -1, 1}, - - {0, 2, 0}, - {0, 1, -1}, - {0, 1, 1}, - - {0, 0, -2}, - {0, 0, 2}, +// EncodeItem ... +func (RedstoneWire) EncodeItem() (name string, meta int16) { + return "minecraft:redstone", 0 } -// redstoneReordering 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 redstoneReordering = [...][24]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}, +// EncodeBlock ... +func (r RedstoneWire) EncodeBlock() (string, map[string]any) { + return "minecraft:redstone_wire", map[string]any{"redstone_signal": int32(max(0, min(r.Power, 15)))} } -// orientNeighbours reorders the neighbours of a node based on the direction that is determined to be forward. -func (n *wireNetwork) orientNeighbours(src [24]*wireNode, dst *wireNode, heading uint32) { - dst.oriented = true - dst.neighbours = make([]*wireNode, 0, 24) - for _, i := range redstoneReordering[heading] { - dst.neighbours = append(dst.neighbours, src[i]) +func allRedstoneWires() (wires []world.Block) { + for i := 0; i <= 15; i++ { + wires = append(wires, RedstoneWire{Power: i}) } + return } -// 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(tx *world.Tx, node *wireNode, layer uint32) { - if !node.oriented { - n.identifyNeighbours(tx, node) - } - - layerOne := layer + 1 - for _, neighbour := range node.neighbours { - if layerOne > neighbour.layer { - neighbour.layer = layerOne - neighbour.source = node.pos - n.updateQueue[1] = append(n.updateQueue[1], neighbour) - } +func redstoneWireSupported(tx *world.Tx, pos cube.Pos) bool { + below := pos.Side(cube.FaceDown) + if below.OutOfBounds(tx.Range()) { + return false } + return tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) +} - layerTwo := layer + 2 - for _, neighbour := range node.neighbours[:4] { - if layerTwo > neighbour.layer { - neighbour.layer = layerTwo - neighbour.source = node.pos - n.updateQueue[2] = append(n.updateQueue[2], neighbour) - } +func redstoneWireBlocksConnection(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + if pos.OutOfBounds(tx.Range()) { + return true } + return tx.Block(pos).Model().FaceSolid(pos, face, tx) } -// 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(tx *world.Tx) { - 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(tx, node, n.currentWalkLayer) - continue - } - updateRedstoneFrom(node.pos, node.source, tx) - } - - n.shiftQueue() - n.currentWalkLayer++ +func redstoneOpenableTransition(open bool, oldPower, newPower int) (bool, bool) { + if newPower > 0 { + return true, !open } - - n.currentWalkLayer = 0 + if oldPower > 0 { + return false, open + } + return open, false } -// 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 -} +// RedstoneLamp is a lamp that lights while powered. +type RedstoneLamp struct { + solid -// updateNode processes a node which has had neighbouring redstone wires that have experienced value changes. -func (n *wireNetwork) updateNode(tx *world.Tx, node *wireNode, layer uint32) { - node.visited = true - if redstoneUpdateCancelled(node.pos, tx) { - return - } + // Lit is true when the lamp is powered and emitting light. + Lit bool +} - newWire, changed := n.calculateCurrentChanges(tx, node) - if !changed { - return +// LightEmissionLevel ... +func (r RedstoneLamp) LightEmissionLevel() uint8 { + if r.Lit { + return 15 } - node.block = newWire - n.propagateChanges(tx, node, layer) + return 0 } -// 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. -// It returns the new redstone wire block and a boolean indicating whether the power level changed. -func (n *wireNetwork) calculateCurrentChanges(tx *world.Tx, node *wireNode) (RedstoneWire, bool) { - wire := node.block.(RedstoneWire) - i := wire.Power - - if !node.oriented { - n.identifyNeighbours(tx, node) +// RedstonePowerUpdate updates the lamp's lit state to match its redstone input. +func (r RedstoneLamp) RedstonePowerUpdate(_ cube.Pos, _ *world.Tx, power int) (world.Block, bool) { + lit := power > 0 + if r.Lit == lit { + return r, false } + r.Lit = lit + return r, true +} - j := calculateRedstoneWirePower(node.pos, tx, func(pos cube.Pos) world.Block { - if cached, ok := n.nodeCache[pos]; ok { - return cached.block - } - return tx.Block(pos) - }) +// BreakInfo ... +func (r RedstoneLamp) BreakInfo() BreakInfo { + return newBreakInfo(0.3, alwaysHarvestable, nothingEffective, oneOf(RedstoneLamp{})) +} - if i == j { - return wire, false - } - wire.Power = j - tx.SetBlock(node.pos, wire, &world.SetOpts{DisableBlockUpdates: true}) - return wire, true +// EncodeItem ... +func (RedstoneLamp) EncodeItem() (name string, meta int16) { + return "minecraft:redstone_lamp", 0 } -// maxRedstoneWirePower returns the greater of strength and the power level of b if it is redstone wire. -func maxRedstoneWirePower(b world.Block, strength int) int { - if wire, ok := b.(RedstoneWire); ok { - return max(wire.Power, strength) +// EncodeBlock ... +func (r RedstoneLamp) EncodeBlock() (string, map[string]any) { + if r.Lit { + return "minecraft:lit_redstone_lamp", nil } - return strength + return "minecraft:redstone_lamp", nil } -// 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, 1: - return wireHeadingNorth - case 2, 5: - return wireHeadingEast - case 3, 4: - return wireHeadingWest - case 6, 7, 8: - return wireHeadingSouth - } - panic("should never happen") +func allRedstoneLamps() (lamps []world.Block) { + return []world.Block{RedstoneLamp{}, RedstoneLamp{Lit: true}} } diff --git a/server/block/redstone_block.go b/server/block/redstone_block.go deleted file mode 100644 index 284122842..000000000 --- a/server/block/redstone_block.go +++ /dev/null @@ -1,60 +0,0 @@ -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" -) - -// RedstoneBlock is a mineral block equivalent to nine redstone dust. -// It acts as a permanently powered redstone power source that can be pushed by pistons. -type RedstoneBlock struct { - solid -} - -// BreakInfo ... -func (r RedstoneBlock) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(r)).withBlastResistance(30).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { - updateAroundRedstone(pos, tx) - }) -} - -// 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, tx *world.Tx, user item.User, ctx *item.UseContext) bool { - pos, _, used := firstReplaceable(tx, pos, face, r) - if !used { - return false - } - place(tx, pos, r, user, ctx) - if placed(ctx) { - updateAroundRedstone(pos, tx) - return true - } - return false -} - -// RedstoneSource ... -func (r RedstoneBlock) RedstoneSource() bool { - return true -} - -// WeakPower ... -func (r RedstoneBlock) WeakPower(_ cube.Pos, _ cube.Face, _ *world.Tx, _ bool) int { - return 15 -} - -// StrongPower ... -func (r RedstoneBlock) StrongPower(_ cube.Pos, _ cube.Face, _ *world.Tx, _ bool) int { - return 0 -} diff --git a/server/block/redstone_common.go b/server/block/redstone_common.go new file mode 100644 index 000000000..91efbb279 --- /dev/null +++ b/server/block/redstone_common.go @@ -0,0 +1,11 @@ +package block + +import "time" + +func redstonePower(power int) int { + return min(max(power, 0), 15) +} + +func redstoneTicks(ticks int) time.Duration { + return time.Duration(max(ticks, 1)) * time.Second / 10 +} diff --git a/server/block/redstone_sources.go b/server/block/redstone_sources.go new file mode 100644 index 000000000..8f8f35522 --- /dev/null +++ b/server/block/redstone_sources.go @@ -0,0 +1,594 @@ +package block + +import ( + "math/rand/v2" + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +const ( + redstoneSourceStone = iota + redstoneSourcePolishedBlackstone + redstoneSourceOak + redstoneSourceSpruce + redstoneSourceBirch + redstoneSourceJungle + redstoneSourceAcacia + redstoneSourceDarkOak + redstoneSourceMangrove + redstoneSourceCherry + redstoneSourceBamboo + redstoneSourceCrimson + redstoneSourceWarped + redstoneSourcePaleOak + redstoneSourceLightWeighted + redstoneSourceHeavyWeighted +) + +// Lever is a switch that emits redstone power while active. +type Lever struct { + empty + transparent + sourceWaterDisplacer + + // Facing is the face the lever is attached to. + Facing cube.Face + // Axis is the horizontal axis used by floor and ceiling levers. + //blockhash:lever_axis + Axis cube.Axis + // Powered is true if the lever is switched on. + Powered bool +} + +// UseOnBlock places a lever attached to the clicked face. +func (l Lever) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, face, used := firstReplaceable(tx, pos, face, l) + if !used || !redstoneAttachmentSupported(tx, pos, face) { + return false + } + l.Facing = face + if user != nil && (face == cube.FaceUp || face == cube.FaceDown) { + l.Axis = user.Rotation().Direction().Face().Axis() + } + place(tx, pos, l, user, ctx) + return placed(ctx) +} + +// Activate toggles the lever. +func (l Lever) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { + l.Powered = !l.Powered + tx.SetBlock(pos, l, nil) + tx.PlaySound(pos.Vec3Centre(), sound.Click{}) + return true +} + +// NeighbourUpdateTick breaks the lever if its supporting block is removed. +func (l Lever) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if !redstoneAttachmentSupported(tx, pos, l.Facing) { + breakBlock(l, pos, tx) + } +} + +// RedstonePower returns maximum power while the lever is active. +func (l Lever) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { + if l.Powered { + return 15 + } + return 0 +} + +// RedstoneStrongPower strongly powers the block the lever is attached to. +func (l Lever) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { + if l.Powered && face == l.Facing.Opposite() { + return 15 + } + return 0 +} + +// BreakInfo ... +func (l Lever) BreakInfo() BreakInfo { + return newBreakInfo(0.5, alwaysHarvestable, nothingEffective, oneOf(l)) +} + +// SideClosed ... +func (Lever) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false +} + +// EncodeItem ... +func (Lever) EncodeItem() (name string, meta int16) { + return "minecraft:lever", 0 +} + +// EncodeBlock ... +func (l Lever) EncodeBlock() (string, map[string]any) { + return "minecraft:lever", map[string]any{ + "lever_direction": leverDirection(l.Facing, l.Axis), + "open_bit": boolByte(l.Powered), + } +} + +func allLevers() (levers []world.Block) { + for _, face := range cube.HorizontalFaces() { + levers = append(levers, Lever{Facing: face}, Lever{Facing: face, Powered: true}) + } + for _, face := range []cube.Face{cube.FaceDown, cube.FaceUp} { + for _, axis := range []cube.Axis{cube.X, cube.Z} { + levers = append(levers, Lever{Facing: face, Axis: axis}, Lever{Facing: face, Axis: axis, Powered: true}) + } + } + return +} + +func leverDirection(face cube.Face, axis cube.Axis) string { + switch face { + case cube.FaceDown: + if axis == cube.Z { + return "down_north_south" + } + return "down_east_west" + case cube.FaceUp: + if axis == cube.Z { + return "up_north_south" + } + return "up_east_west" + case cube.FaceNorth: + return "north" + case cube.FaceSouth: + return "south" + case cube.FaceWest: + return "west" + case cube.FaceEast: + return "east" + default: + return "down_east_west" + } +} + +func leverAxisHash(l Lever) uint64 { + if (l.Facing == cube.FaceDown || l.Facing == cube.FaceUp) && l.Axis == cube.Z { + return 1 + } + return 0 +} + +// Button is a pressable redstone power source. +type Button struct { + empty + transparent + sourceWaterDisplacer + + // Type identifies the button material. + Type int + // Facing is the face the button is attached to. + Facing cube.Face + // Pressed is true while the button emits power. + Pressed bool +} + +// UseOnBlock places a button attached to the clicked face. +func (b Button) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, face, used := firstReplaceable(tx, pos, face, b) + if !used || !redstoneAttachmentSupported(tx, pos, face) { + return false + } + b.Facing = face + place(tx, pos, b, user, ctx) + return placed(ctx) +} + +// Activate presses the button and schedules release. +func (b Button) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { + if b.Pressed { + return true + } + b.Pressed = true + tx.SetBlock(pos, b, nil) + tx.ScheduleBlockUpdate(pos, b, b.pressDuration()) + tx.PlaySound(pos.Vec3Centre(), sound.Click{}) + return true +} + +// NeighbourUpdateTick breaks the button if its supporting block is removed. +func (b Button) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if !redstoneAttachmentSupported(tx, pos, b.Facing) { + breakBlock(b, pos, tx) + } +} + +// ScheduledTick releases a pressed button. +func (b Button) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + if !b.Pressed { + return + } + b.Pressed = false + tx.SetBlock(pos, b, nil) + tx.PlaySound(pos.Vec3Centre(), sound.Click{}) +} + +// RedstonePower returns maximum power while the button is pressed. +func (b Button) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { + if b.Pressed { + return 15 + } + return 0 +} + +// RedstoneStrongPower strongly powers the block the button is attached to. +func (b Button) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { + if b.Pressed && face == b.Facing.Opposite() { + return 15 + } + return 0 +} + +// BreakInfo ... +func (b Button) BreakInfo() BreakInfo { + return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(b)) +} + +// SideClosed ... +func (Button) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false +} + +// EncodeItem ... +func (b Button) EncodeItem() (name string, meta int16) { + return sourceName(b.Type, "button"), 0 +} + +// EncodeBlock ... +func (b Button) EncodeBlock() (string, map[string]any) { + return sourceName(b.Type, "button"), map[string]any{"button_pressed_bit": boolByte(b.Pressed), "facing_direction": int32(b.Facing)} +} + +func (b Button) pressDuration() time.Duration { + if b.Type >= redstoneSourceOak && b.Type <= redstoneSourcePaleOak { + return time.Second * 3 / 2 + } + return time.Second +} + +func allButtons() (buttons []world.Block) { + for _, typ := range redstoneSourceTypes() { + for _, face := range cube.Faces() { + buttons = append(buttons, Button{Type: typ, Facing: face}, Button{Type: typ, Facing: face, Pressed: true}) + } + } + return +} + +// PressurePlate emits redstone power while stepped on. +type PressurePlate struct { + empty + transparent + sourceWaterDisplacer + + // Type identifies the pressure plate material. + Type int + // Power is the current redstone signal emitted by the plate. + Power int +} + +// UseOnBlock places the pressure plate on a solid surface. +func (p PressurePlate) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(tx, pos, face, p) + if !used || !redstoneFloorComponentSupported(tx, pos) { + return false + } + place(tx, pos, p, user, ctx) + return placed(ctx) +} + +// Model ... +func (PressurePlate) Model() world.BlockModel { + return model.Carpet{} +} + +// EntityStepOn powers the plate when an entity stands on it. +func (p PressurePlate) EntityStepOn(pos cube.Pos, tx *world.Tx, e world.Entity) { + power := p.entityPower(e) + if power == 0 { + return + } + if p.weighted() { + power = max(power, p.detectPower(pos, tx)) + } + if p.Power == power { + tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) + return + } + p.Power = power + tx.SetBlock(pos, p, nil) + tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) + tx.PlaySound(pos.Vec3Centre(), sound.PressurePlateClickOn{}) +} + +// NeighbourUpdateTick breaks the pressure plate if its supporting block is removed. +func (p PressurePlate) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if !redstoneFloorComponentSupported(tx, pos) { + breakBlock(p, pos, tx) + } +} + +// ScheduledTick releases the plate if nothing refreshes it. +func (p PressurePlate) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + power := p.detectPower(pos, tx) + if power > 0 { + if p.Power != power { + p.Power = power + tx.SetBlock(pos, p, nil) + } + tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) + return + } + if p.Power == 0 { + return + } + p.Power = 0 + tx.SetBlock(pos, p, nil) + tx.PlaySound(pos.Vec3Centre(), sound.PressurePlateClickOff{}) +} + +// RedstonePower returns the plate's analog power level. +func (p PressurePlate) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { + return p.Power +} + +// RedstoneStrongPower strongly powers the block below the pressure plate. +func (p PressurePlate) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { + if face == cube.FaceDown { + return p.Power + } + return 0 +} + +// BreakInfo ... +func (p PressurePlate) BreakInfo() BreakInfo { + return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(p)) +} + +// SideClosed ... +func (PressurePlate) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false +} + +// EncodeItem ... +func (p PressurePlate) EncodeItem() (name string, meta int16) { + return pressurePlateSourceName(p.Type), 0 +} + +// EncodeBlock ... +func (p PressurePlate) EncodeBlock() (string, map[string]any) { + return pressurePlateSourceName(p.Type), map[string]any{"redstone_signal": int32(max(0, min(p.Power, 15)))} +} + +func (p PressurePlate) stepPower() int { + if p.Type == redstoneSourceLightWeighted || p.Type == redstoneSourceHeavyWeighted { + return 1 + } + return 15 +} + +func (p PressurePlate) entityPower(e world.Entity) int { + if !p.detectsEntity(e) { + return 0 + } + return p.stepPower() +} + +func (p PressurePlate) detectsEntity(e world.Entity) bool { + if p.ignoresEntity(e) { + return false + } + if p.stoneLike() { + return pressurePlateStoneEntity(e) + } + return true +} + +func (p PressurePlate) detectPower(pos cube.Pos, tx *world.Tx) int { + box := pressurePlateActivationBox(pos) + entities := 0 + for e := range tx.EntitiesWithin(box.Grow(1)) { + if p.entityPower(e) == 0 || !pressurePlateEntityIntersects(e, box) { + continue + } + if !p.weighted() { + return 15 + } + entities++ + if entities >= p.weightedMaxEntities() { + return 15 + } + } + if p.weighted() { + return p.weightedPower(entities) + } + return 0 +} + +func (p PressurePlate) stoneLike() bool { + return p.Type == redstoneSourceStone || p.Type == redstoneSourcePolishedBlackstone +} + +func (p PressurePlate) weighted() bool { + return p.Type == redstoneSourceLightWeighted || p.Type == redstoneSourceHeavyWeighted +} + +func (p PressurePlate) weightedPower(entities int) int { + if entities <= 0 { + return 0 + } + if p.Type == redstoneSourceHeavyWeighted { + return min(15, (entities+9)/10) + } + return min(15, entities) +} + +func (p PressurePlate) weightedMaxEntities() int { + if p.Type == redstoneSourceHeavyWeighted { + return 141 + } + return 15 +} + +func (p PressurePlate) releaseDelay() time.Duration { + if p.weighted() { + return time.Second / 2 + } + return time.Second +} + +func (p PressurePlate) ignoresEntity(e world.Entity) bool { + return pressurePlateEntityName(e) == "minecraft:snowball" +} + +type pressurePlateLivingEntity interface { + Health() float64 + Dead() bool +} + +func pressurePlateStoneEntity(e world.Entity) bool { + if living, ok := e.(pressurePlateLivingEntity); ok { + return living.Health() > 0 && !living.Dead() + } + return pressurePlateEntityName(e) == "minecraft:player" || pressurePlateEntityName(e) == "minecraft:armor_stand" +} + +func pressurePlateEntityName(e world.Entity) string { + h := e.H() + if h == nil || h.Type() == nil { + return "" + } + return h.Type().EncodeEntity() +} + +func pressurePlateActivationBox(pos cube.Pos) cube.BBox { + return cube.Box(float64(pos[0]), float64(pos[1]), float64(pos[2]), float64(pos[0]+1), float64(pos[1])+0.25, float64(pos[2]+1)) +} + +func pressurePlateEntityIntersects(e world.Entity, box cube.BBox) bool { + h := e.H() + if h == nil || h.Type() == nil { + return false + } + return h.Type().BBox(e).Translate(e.Position()).IntersectsWith(box) +} + +func allPressurePlates() (plates []world.Block) { + for _, typ := range append(redstoneSourceTypes(), redstoneSourceLightWeighted, redstoneSourceHeavyWeighted) { + for i := 0; i <= 15; i++ { + plates = append(plates, PressurePlate{Type: typ, Power: i}) + } + } + return +} + +func redstoneSourceTypes() []int { + types := []int{ + redstoneSourceStone, + redstoneSourcePolishedBlackstone, + redstoneSourceOak, + redstoneSourceSpruce, + redstoneSourceBirch, + redstoneSourceJungle, + redstoneSourceAcacia, + redstoneSourceDarkOak, + redstoneSourceMangrove, + redstoneSourceCherry, + redstoneSourceBamboo, + redstoneSourceCrimson, + redstoneSourceWarped, + redstoneSourcePaleOak, + } + return types +} + +func pressurePlateSourceName(typ int) string { + if typ == redstoneSourceLightWeighted { + return "minecraft:light_weighted_pressure_plate" + } + if typ == redstoneSourceHeavyWeighted { + return "minecraft:heavy_weighted_pressure_plate" + } + return sourceName(typ, "pressure_plate") +} + +func sourceName(typ int, suffix string) string { + switch typ { + case redstoneSourceStone: + return "minecraft:stone_" + suffix + case redstoneSourcePolishedBlackstone: + return "minecraft:polished_blackstone_" + suffix + case redstoneSourceOak: + if suffix == "button" { + return "minecraft:wooden_button" + } + return "minecraft:wooden_pressure_plate" + case redstoneSourceSpruce: + return "minecraft:spruce_" + suffix + case redstoneSourceBirch: + return "minecraft:birch_" + suffix + case redstoneSourceJungle: + return "minecraft:jungle_" + suffix + case redstoneSourceAcacia: + return "minecraft:acacia_" + suffix + case redstoneSourceDarkOak: + return "minecraft:dark_oak_" + suffix + case redstoneSourceMangrove: + return "minecraft:mangrove_" + suffix + case redstoneSourceCherry: + return "minecraft:cherry_" + suffix + case redstoneSourceBamboo: + return "minecraft:bamboo_" + suffix + case redstoneSourceCrimson: + return "minecraft:crimson_" + suffix + case redstoneSourceWarped: + return "minecraft:warped_" + suffix + case redstoneSourcePaleOak: + return "minecraft:pale_oak_" + suffix + default: + return "minecraft:stone_" + suffix + } +} + +func (b Button) Model() world.BlockModel { + return model.Empty{} +} + +func (p PressurePlate) FuelInfo() item.FuelInfo { + if p.Type >= redstoneSourceOak && p.Type <= redstoneSourcePaleOak { + return newFuelInfo(time.Second * 15) + } + return item.FuelInfo{} +} + +func (b Button) FuelInfo() item.FuelInfo { + if b.Type >= redstoneSourceOak && b.Type <= redstoneSourcePaleOak { + return newFuelInfo(time.Second * 5) + } + return item.FuelInfo{} +} + +func redstoneAttachmentSupported(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + support := pos.Side(face.Opposite()) + if support.OutOfBounds(tx.Range()) { + return false + } + return tx.Block(support).Model().FaceSolid(support, face, tx) +} + +func redstoneFloorComponentSupported(tx *world.Tx, pos cube.Pos) bool { + support := pos.Side(cube.FaceDown) + if support.OutOfBounds(tx.Range()) { + return false + } + return tx.Block(support).Model().FaceSolid(support, cube.FaceUp, tx) +} diff --git a/server/block/redstone_sources_test.go b/server/block/redstone_sources_test.go new file mode 100644 index 000000000..014e0650a --- /dev/null +++ b/server/block/redstone_sources_test.go @@ -0,0 +1,291 @@ +package block + +import ( + "fmt" + "testing" + + "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" +) + +func TestLeverPower(t *testing.T) { + if power := (Lever{}).RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 0 { + t.Fatalf("unpowered lever power = %d, want 0", power) + } + if power := (Lever{Powered: true}).RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 15 { + t.Fatalf("powered lever power = %d, want 15", power) + } +} + +func TestLeverEncodeBlock(t *testing.T) { + tests := []struct { + name string + l Lever + want string + }{ + {name: "wall", l: Lever{Facing: cube.FaceEast}, want: "east"}, + {name: "floor east west", l: Lever{Facing: cube.FaceUp, Axis: cube.X}, want: "up_east_west"}, + {name: "floor north south", l: Lever{Facing: cube.FaceUp, Axis: cube.Z}, want: "up_north_south"}, + {name: "ceiling east west", l: Lever{Facing: cube.FaceDown, Axis: cube.X}, want: "down_east_west"}, + {name: "ceiling north south", l: Lever{Facing: cube.FaceDown, Axis: cube.Z}, want: "down_north_south"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, props := test.l.EncodeBlock() + if direction := props["lever_direction"]; direction != test.want { + t.Fatalf("lever_direction = %v, want %s", direction, test.want) + } + }) + } +} + +func TestRedstoneAttachableSupport(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + if redstoneAttachmentSupported(tx, pos, cube.FaceUp) { + err = fmt.Errorf("lever without support was supported") + return + } + tx.SetBlock(pos.Side(cube.FaceDown), Lever{}, nil) + if redstoneAttachmentSupported(tx, pos, cube.FaceUp) { + err = fmt.Errorf("lever on lever support was supported") + return + } + tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, nil) + if !redstoneAttachmentSupported(tx, pos, cube.FaceUp) { + err = fmt.Errorf("lever on solid support was not supported") + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestRedstoneFloorComponentSupport(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + if redstoneFloorComponentSupported(tx, pos) { + err = fmt.Errorf("floor component without support was supported") + return + } + tx.SetBlock(pos.Side(cube.FaceDown), Button{Facing: cube.FaceUp}, nil) + if redstoneFloorComponentSupported(tx, pos) { + err = fmt.Errorf("floor component on button support was supported") + return + } + tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, nil) + if !redstoneFloorComponentSupported(tx, pos) { + err = fmt.Errorf("floor component on solid support was not supported") + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestButtonPowerAndDuration(t *testing.T) { + stone := Button{Type: redstoneSourceStone} + if power := stone.RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 0 { + t.Fatalf("unpressed button power = %d, want 0", power) + } + stone.Pressed = true + if power := stone.RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 15 { + t.Fatalf("pressed button power = %d, want 15", power) + } + if stone.pressDuration() >= (Button{Type: redstoneSourceOak}).pressDuration() { + t.Fatal("stone button duration should be shorter than wooden button duration") + } +} + +func TestPressurePlatePower(t *testing.T) { + plate := PressurePlate{Type: redstoneSourceStone, Power: 15} + if power := plate.RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 15 { + t.Fatalf("stone pressure plate power = %d, want 15", power) + } + if power := (PressurePlate{Type: redstoneSourceLightWeighted}).stepPower(); power != 1 { + t.Fatalf("weighted pressure plate step power = %d, want first analog level 1", power) + } + if power := (PressurePlate{Type: redstoneSourceLightWeighted}).weightedPower(16); power != 15 { + t.Fatalf("light weighted pressure plate count power = %d, want 15", power) + } + if power := (PressurePlate{Type: redstoneSourceHeavyWeighted}).weightedPower(11); power != 2 { + t.Fatalf("heavy weighted pressure plate count power = %d, want 2", power) + } + if power := (PressurePlate{Type: redstoneSourceHeavyWeighted}).weightedPower(141); power != 15 { + t.Fatalf("heavy weighted pressure plate max power = %d, want 15", power) + } +} + +func TestPressurePlateItemActivation(t *testing.T) { + itemEntity := fakeItemEntity{} + if power := (PressurePlate{Type: redstoneSourceStone}).entityPower(itemEntity); power != 0 { + t.Fatalf("stone pressure plate item power = %d, want 0", power) + } + if power := (PressurePlate{Type: redstoneSourceOak}).entityPower(itemEntity); power != 15 { + t.Fatalf("wood pressure plate item power = %d, want 15", power) + } + if power := (PressurePlate{Type: redstoneSourceLightWeighted}).entityPower(itemEntity); power != 1 { + t.Fatalf("light weighted pressure plate item power = %d, want 1", power) + } + if power := (PressurePlate{Type: redstoneSourceHeavyWeighted}).entityPower(fakeSnowballEntity{}); power != 0 { + t.Fatalf("heavy weighted pressure plate snowball power = %d, want 0", power) + } + if power := (PressurePlate{Type: redstoneSourceStone}).entityPower(fakeLivingEntity{health: 20}); power != 15 { + t.Fatalf("stone pressure plate living entity power = %d, want 15", power) + } + if power := (PressurePlate{Type: redstoneSourceStone}).entityPower(fakeLivingEntity{}); power != 0 { + t.Fatalf("stone pressure plate dead living entity power = %d, want 0", power) + } +} + +func TestPressurePlateDetectsEntityBoundingBoxOnEdge(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + tx.AddEntity(world.EntitySpawnOpts{Position: mgl64.Vec3{1.2, 1.0625, 0.5}}.New(pressurePlateTestEntityType{name: "minecraft:player"}, pressurePlateTestEntityConfig{})) + + if power := (PressurePlate{Type: redstoneSourceStone}).detectPower(pos, tx); power != 15 { + err = fmt.Errorf("edge-overlapping pressure plate power = %d, want 15", power) + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestWeightedPressurePlateCountsWorldEntities(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + for i := 0; i < 11; i++ { + tx.AddEntity(world.EntitySpawnOpts{Position: mgl64.Vec3{0.5 + float64(i%3)*0.01, 1.0625, 0.5}}.New(pressurePlateTestEntityType{name: "minecraft:item"}, pressurePlateTestEntityConfig{})) + } + tx.AddEntity(world.EntitySpawnOpts{Position: mgl64.Vec3{0.5, 1.0625, 0.5}}.New(pressurePlateTestEntityType{name: "minecraft:snowball"}, pressurePlateTestEntityConfig{})) + + if power := (PressurePlate{Type: redstoneSourceLightWeighted}).detectPower(pos, tx); power != 11 { + err = fmt.Errorf("light weighted plate power = %d, want 11", power) + return + } + if power := (PressurePlate{Type: redstoneSourceHeavyWeighted}).detectPower(pos, tx); power != 2 { + err = fmt.Errorf("heavy weighted plate power = %d, want 2", power) + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestRedstoneSourceNames(t *testing.T) { + tests := map[int]string{ + redstoneSourceStone: "minecraft:stone_button", + redstoneSourcePolishedBlackstone: "minecraft:polished_blackstone_button", + redstoneSourceOak: "minecraft:wooden_button", + redstoneSourceMangrove: "minecraft:mangrove_button", + redstoneSourcePaleOak: "minecraft:pale_oak_button", + } + for typ, want := range tests { + if got, _ := (Button{Type: typ}).EncodeItem(); got != want { + t.Fatalf("button type %d encodes to %q, want %q", typ, got, want) + } + } + if got, _ := (PressurePlate{Type: redstoneSourceLightWeighted}).EncodeItem(); got != "minecraft:light_weighted_pressure_plate" { + t.Fatalf("light weighted pressure plate encodes to %q", got) + } + if got, _ := (PressurePlate{Type: redstoneSourceHeavyWeighted}).EncodeItem(); got != "minecraft:heavy_weighted_pressure_plate" { + t.Fatalf("heavy weighted pressure plate encodes to %q", got) + } +} + +type fakeItemEntity struct{} + +func (fakeItemEntity) Close() error { return nil } +func (fakeItemEntity) H() *world.EntityHandle { return nil } +func (fakeItemEntity) Position() mgl64.Vec3 { return mgl64.Vec3{} } +func (fakeItemEntity) Rotation() cube.Rotation { return cube.Rotation{} } +func (fakeItemEntity) Item() item.Stack { return item.Stack{} } + +type fakeSnowballEntity struct{ fakeItemEntity } + +func (fakeSnowballEntity) H() *world.EntityHandle { + return world.EntitySpawnOpts{}.New(pressurePlateTestEntityType{name: "minecraft:snowball"}, pressurePlateTestEntityConfig{}) +} + +type fakeLivingEntity struct { + fakeItemEntity + health float64 +} + +func (e fakeLivingEntity) Health() float64 { return e.health } +func (e fakeLivingEntity) Dead() bool { return e.health <= 0 } + +type pressurePlateTestEntityConfig struct{} + +func (pressurePlateTestEntityConfig) Apply(*world.EntityData) {} + +type pressurePlateTestEntityType struct { + name string +} + +func (pressurePlateTestEntityType) Open(_ *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { + return pressurePlateTestEntity{handle: handle, data: data} +} + +func (t pressurePlateTestEntityType) EncodeEntity() string { + if t.name != "" { + return t.name + } + return "minecraft:test_entity" +} +func (pressurePlateTestEntityType) BBox(world.Entity) cube.BBox { + return cube.Box(-0.3, 0, -0.3, 0.3, 1.8, 0.3) +} +func (pressurePlateTestEntityType) DecodeNBT(map[string]any, *world.EntityData) {} +func (pressurePlateTestEntityType) EncodeNBT(*world.EntityData) map[string]any { return nil } + +type pressurePlateTestEntity struct { + handle *world.EntityHandle + data *world.EntityData +} + +func (e pressurePlateTestEntity) Close() error { return nil } +func (e pressurePlateTestEntity) H() *world.EntityHandle { return e.handle } +func (e pressurePlateTestEntity) Position() mgl64.Vec3 { return e.data.Pos } +func (e pressurePlateTestEntity) Rotation() cube.Rotation { return e.data.Rot } + +func TestRedstoneSourceHashesIncludeMaterial(t *testing.T) { + _, stoneButton := (Button{Type: redstoneSourceStone, Facing: cube.FaceUp}).Hash() + _, oakButton := (Button{Type: redstoneSourceOak, Facing: cube.FaceUp}).Hash() + if stoneButton == oakButton { + t.Fatal("stone and oak buttons produced the same block hash") + } + + _, stonePlate := (PressurePlate{Type: redstoneSourceStone}).Hash() + _, oakPlate := (PressurePlate{Type: redstoneSourceOak}).Hash() + if stonePlate == oakPlate { + t.Fatal("stone and oak pressure plates produced the same block hash") + } +} diff --git a/server/block/redstone_test.go b/server/block/redstone_test.go new file mode 100644 index 000000000..b9991c1ac --- /dev/null +++ b/server/block/redstone_test.go @@ -0,0 +1,139 @@ +package block + +import ( + "fmt" + "testing" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +func TestRedstoneBlockPower(t *testing.T) { + for _, face := range cube.Faces() { + if power := (RedstoneBlock{}).RedstonePower(cube.Pos{}, nil, face); power != 15 { + t.Fatalf("RedstoneBlock power from %v = %d, want 15", face, power) + } + } +} + +func TestRedstoneWirePowerUpdate(t *testing.T) { + wire := RedstoneWire{Power: 3} + after, changed := wire.RedstonePowerUpdate(cube.Pos{}, nil, 12) + if !changed { + t.Fatal("RedstoneWire update did not report a change") + } + if got := after.(RedstoneWire).Power; got != 12 { + t.Fatalf("RedstoneWire power = %d, want 12", got) + } + + after, changed = wire.RedstonePowerUpdate(cube.Pos{}, nil, 24) + if !changed { + t.Fatal("RedstoneWire clamped update did not report a change") + } + if got := after.(RedstoneWire).Power; got != 15 { + t.Fatalf("RedstoneWire clamped power = %d, want 15", got) + } + + _, changed = (RedstoneWire{Power: 15}).RedstonePowerUpdate(cube.Pos{}, nil, 15) + if changed { + t.Fatal("RedstoneWire unchanged power reported a change") + } +} + +func TestRedstoneWireRequiresSolidSupport(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + if redstoneWireSupported(tx, pos) { + err = fmt.Errorf("wire without support was supported") + return + } + tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, nil) + if !redstoneWireSupported(tx, pos) { + err = fmt.Errorf("wire on solid support was not supported") + return + } + tx.SetBlock(pos.Side(cube.FaceDown), RedstoneWire{}, nil) + if redstoneWireSupported(tx, pos) { + err = fmt.Errorf("wire on top of wire was supported") + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestRedstoneWireConnectsUpBlocks(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + up := cube.Pos{1, 2, 0} + tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(pos, RedstoneWire{}, nil) + tx.SetBlock(up.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(up, RedstoneWire{}, nil) + + for _, neighbour := range (RedstoneWire{}).RedstoneRelayerNeighbours(pos, tx) { + if neighbour == up { + return + } + } + err = fmt.Errorf("wire neighbours did not include upward step") + }) + if err != nil { + t.Fatal(err) + } +} + +func TestStrongPowerConductsThroughSolidBlocks(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + conductor := cube.Pos{0, 1, 0} + target := conductor.Side(cube.FaceEast) + tx.SetBlock(conductor, Stone{}, nil) + tx.SetBlock(conductor.Side(cube.FaceWest), RedstoneBlock{}, nil) + + if power := tx.RedstonePower(target); power != 15 { + err = fmt.Errorf("conducted strong power = %d, want 15", power) + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestRedstoneLampPowerUpdate(t *testing.T) { + after, changed := (RedstoneLamp{}).RedstonePowerUpdate(cube.Pos{}, nil, 15) + if !changed { + t.Fatal("RedstoneLamp update did not report a change") + } + if !after.(RedstoneLamp).Lit { + t.Fatal("RedstoneLamp did not become lit") + } + if after.(RedstoneLamp).LightEmissionLevel() != 15 { + t.Fatal("lit RedstoneLamp should emit light level 15") + } + + after, changed = (RedstoneLamp{Lit: true}).RedstonePowerUpdate(cube.Pos{}, nil, 0) + if !changed { + t.Fatal("RedstoneLamp unpower update did not report a change") + } + if after.(RedstoneLamp).Lit { + t.Fatal("RedstoneLamp did not turn off") + } +} diff --git a/server/block/redstone_torch.go b/server/block/redstone_torch.go index 6aef00182..e94571f53 100644 --- a/server/block/redstone_torch.go +++ b/server/block/redstone_torch.go @@ -2,32 +2,36 @@ package block import ( "math/rand/v2" - "time" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" - "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" ) -// RedstoneTorch is a non-solid block that emits light and provides a full-strength redstone signal when lit. +var ( + _ world.RedstonePowerSource = RedstoneTorch{} + _ world.RedstonePowerConsumer = RedstoneTorch{} + _ world.ScheduledTicker = RedstoneTorch{} +) + +// RedstoneTorch is a torch-like inverter that turns off while the block it is attached to is powered. type RedstoneTorch struct { transparent empty // Facing is the direction from the torch to the block it is attached to. Facing cube.Face - // Lit indicates whether the redstone torch is currently lit and emitting power. + // Lit is true if the torch is currently emitting redstone power. Lit bool } -// HasLiquidDrops returns whether the redstone torch drops its item when flowing liquid breaks it. -func (RedstoneTorch) HasLiquidDrops() bool { - return true +// BreakInfo ... +func (t RedstoneTorch) BreakInfo() BreakInfo { + return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(t)) } -// LightEmissionLevel returns the light level emitted by the redstone torch (7 when lit, 0 when unlit). +// LightEmissionLevel ... func (t RedstoneTorch) LightEmissionLevel() uint8 { if t.Lit { return 7 @@ -35,260 +39,135 @@ func (t RedstoneTorch) LightEmissionLevel() uint8 { return 0 } -// BreakInfo returns information about breaking the redstone torch. -func (t RedstoneTorch) BreakInfo() BreakInfo { - return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(t)).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { - tx.Redstone().ClearTorchBurnout(pos) - updateTorchRedstone(pos, tx) - }) -} - -// UseOnBlock handles the placement of a redstone torch on a block surface. +// UseOnBlock places a redstone torch on the clicked block face. func (t RedstoneTorch) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { pos, face, used := firstReplaceable(tx, pos, face, t) - if !used { - return false - } - if face == cube.FaceDown { - return false - } - if _, ok := tx.Block(pos).(world.Liquid); ok { + if !used || face == cube.FaceDown { return false } if !tx.Block(pos.Side(face.Opposite())).Model().FaceSolid(pos.Side(face.Opposite()), face, tx) { - fallbackFace, ok := findTorchPlacementFace(pos, tx) - if !ok { + found := false + for _, i := range []cube.Face{cube.FaceSouth, cube.FaceWest, cube.FaceNorth, cube.FaceEast, cube.FaceDown} { + if tx.Block(pos.Side(i)).Model().FaceSolid(pos.Side(i), i.Opposite(), tx) { + found = true + face = i.Opposite() + break + } + } + if !found { return false } - face = fallbackFace } t.Facing = face.Opposite() - t.Lit = true + t.Lit = !t.attachmentPowered(pos, tx) place(tx, pos, t, user, ctx) if placed(ctx) { - // Initialise the freshly placed torch state before propagating its output. - t.RedstoneUpdate(pos, tx) - updateTorchRedstone(pos, tx) - return true + tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) } - return false + return placed(ctx) } -// NeighbourUpdateTick is called when a neighbouring block is updated. +// NeighbourUpdateTick breaks unsupported torches and otherwise schedules inverse-state refreshes. func (t RedstoneTorch) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { if !tx.Block(pos.Side(t.Facing)).Model().FaceSolid(pos.Side(t.Facing), t.Facing.Opposite(), tx) { - tx.Redstone().ClearTorchBurnout(pos) breakBlock(t, pos, tx) return } - if t.recoverFromBurnout(pos, tx) { - return - } - updateRedstone(pos, tx) + tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) } -// RedstoneUpdate is called when the redstone power state changes nearby. This method ignores burned-out torches and -// schedules state changes for active torches. -func (t RedstoneTorch) RedstoneUpdate(pos cube.Pos, tx *world.Tx) { - currentTick := tx.CurrentTick() - if burnedOut, _ := tx.Redstone().TorchBurnoutStatus(pos, currentTick); burnedOut { - if t.updateSourceTouchesInput(pos, tx) { - t.recoverFromBurnout(pos, tx) - } +// ScheduledTick refreshes the lit state after the torch's one-redstone-tick inversion delay. +func (t RedstoneTorch) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + if tx == nil { return } - - shouldBeLit := t.inputStrength(pos, tx) == 0 - if shouldBeLit == t.Lit { - return + lit := !t.attachmentPowered(pos, tx) + if t.Lit != lit { + t.Lit = lit + tx.SetBlock(pos, t, nil) } - tx.Redstone().MarkTorchSelfTriggeredIfActive(pos) - tx.ScheduleBlockUpdate(pos, t, time.Millisecond*100) } -// recoverFromBurnout relights a burned-out torch after a real neighbouring block update once its rapid-toggle history -// has expired. Redstone propagation alone may visit calculation-only positions, so it must not recover burned-out -// torches that did not receive an actual neighbour update. -func (RedstoneTorch) recoverFromBurnout(pos cube.Pos, tx *world.Tx) bool { - torch, ok := redstoneTorchAt(pos, tx) - if !ok { - return false +// RedstonePower emits power from every side except the attached block while lit. +func (t RedstoneTorch) RedstonePower(_ cube.Pos, _ *world.Tx, face cube.Face) int { + if t.Lit && face != t.Facing { + return 15 } + return 0 +} - currentTick := tx.CurrentTick() - burnedOut, recoverable := tx.Redstone().TorchBurnoutStatus(pos, currentTick) - if !burnedOut { - return false +// RedstoneStrongPower strongly powers the block above the torch while lit. +func (t RedstoneTorch) RedstoneStrongPower(pos cube.Pos, tx *world.Tx, face cube.Face) int { + if face == cube.FaceUp { + return t.RedstonePower(pos, tx, face) } - if !recoverable { - return true - } - tx.Redstone().ClearTorchBurnout(pos) + return 0 +} - torch.Lit = torch.inputStrength(pos, tx) == 0 - tx.SetBlock(pos, torch, nil) - updateTorchRedstone(pos, tx) - return true +// RedstonePowerUpdate schedules the delayed inverse-state refresh. +func (t RedstoneTorch) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, _ int) (world.Block, bool) { + if tx == nil || t.Lit == !t.attachmentPowered(pos, tx) { + return t, false + } + tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) + return t, false } -// updateSourceTouchesInput reports whether the current redstone update came from the block the torch is attached to, -// or from a block directly beside it. Dust on top of the attached block can legitimately recover a burnout loop, while -// disconnected dust visited by the broad wire walk should not. -func (t RedstoneTorch) updateSourceTouchesInput(pos cube.Pos, tx *world.Tx) bool { - source, ok := tx.Redstone().UpdateSource() - if !ok { +func (t RedstoneTorch) attachmentPowered(pos cube.Pos, tx *world.Tx) bool { + if tx == nil { return false } - inputPos := pos.Side(t.Facing) - if source == inputPos { + attached := pos.Side(t.Facing) + if tx.RedstonePower(attached) > 0 { return true } - for _, face := range cube.Faces() { - if source == inputPos.Side(face) { - return true + if source, ok := tx.Block(attached).(world.RedstonePowerSource); ok { + for _, face := range cube.Faces() { + if redstonePower(source.RedstonePower(attached, tx, face)) > 0 { + return true + } } } return false } -// ScheduledTick is called when a scheduled block update occurs. -// This method handles state changes and checks for burnout conditions. -func (RedstoneTorch) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { - torch, ok := redstoneTorchAt(pos, tx) - if !ok { - return - } - - currentTick := tx.CurrentTick() - if burnedOut, _ := tx.Redstone().TorchBurnoutStatus(pos, currentTick); burnedOut { - return - } - - shouldBeLit := torch.inputStrength(pos, tx) == 0 - if shouldBeLit == torch.Lit { - tx.Redstone().PruneTorchBurnout(pos, currentTick) - return - } - - if tx.Redstone().RecordTorchToggle(pos, currentTick) { - torch.burnOut(pos, tx) - return - } - - torch.Lit = !torch.Lit - tx.SetBlock(pos, torch, nil) - updateTorchRedstone(pos, tx) -} - -// burnOut puts the redstone torch into burnout state, turning it off and playing effects. -func (RedstoneTorch) burnOut(pos cube.Pos, tx *world.Tx) { - torch, ok := redstoneTorchAt(pos, tx) - if !ok { - return - } - - tx.Redstone().BurnOutTorch(pos) - torch.Lit = false - tx.PlaySound(pos.Vec3Centre(), sound.Fizz{}) - tx.SetBlock(pos, torch, nil) - updateTorchRedstone(pos, tx) -} - -// redstoneTorchAt returns the current torch at pos. Scheduled redstone updates carry an old block value, so mutation -// paths must reload the live world block before writing torch state back. -func redstoneTorchAt(pos cube.Pos, tx *world.Tx) (RedstoneTorch, bool) { - t, ok := tx.Block(pos).(RedstoneTorch) - if !ok { - tx.Redstone().ClearTorchBurnout(pos) - } - return t, ok -} - -// updateTorchRedstone updates receivers around the torch and behind the block it strongly powers above. -func updateTorchRedstone(pos cube.Pos, tx *world.Tx) { - tx.Redstone().WithActiveTorchUpdate(pos, func() { - updateDirectionalRedstone(pos, tx, cube.FaceUp) - }) +// HasLiquidDrops ... +func (t RedstoneTorch) HasLiquidDrops() bool { + return true } -// EncodeItem encodes the redstone torch as an item. -func (RedstoneTorch) EncodeItem() (name string, meta int16) { +// EncodeItem ... +func (t RedstoneTorch) EncodeItem() (name string, meta int16) { return "minecraft:redstone_torch", 0 } -// EncodeBlock encodes the redstone torch as a block for network transmission. +// 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" - } - } + name = "minecraft:unlit_redstone_torch" if t.Lit { - return "minecraft:redstone_torch", map[string]any{"torch_facing_direction": face} + name = "minecraft:redstone_torch" } - return "minecraft:unlit_redstone_torch", map[string]any{"torch_facing_direction": face} + return name, map[string]any{"torch_facing_direction": torchFacingDirection(t.Facing)} } -// RedstoneSource ... -func (t RedstoneTorch) RedstoneSource() bool { - return true -} - -// WeakPower returns the weak redstone power level provided to adjacent blocks. -func (t RedstoneTorch) WeakPower(_ cube.Pos, face cube.Face, _ *world.Tx, _ bool) int { - if !t.Lit { - return 0 - } - if face.Opposite() == t.Facing { - return 0 - } - return 15 -} - -// StrongPower returns the strong redstone power level provided to the block above the torch. -func (t RedstoneTorch) StrongPower(_ cube.Pos, face cube.Face, _ *world.Tx, _ bool) int { - if t.Lit && face == cube.FaceDown { - return 15 - } - return 0 -} - -// inputStrength returns the redstone power level received by the block the torch is attached to. -func (t RedstoneTorch) inputStrength(pos cube.Pos, tx *world.Tx) int { - return tx.RedstonePower(pos.Side(t.Facing), t.Facing, true) -} - -// redstoneTorchFallbackSides lists the faces to check for placing a redstone torch on a non-solid block. -var redstoneTorchFallbackSides = [...]cube.Face{ - cube.FaceSouth, - cube.FaceWest, - cube.FaceNorth, - cube.FaceEast, - cube.FaceDown, -} - -// findTorchPlacementFace finds a valid face for placing a redstone torch on a non-solid block. -// It returns the face the torch should be placed on and whether it was found. -func findTorchPlacementFace(pos cube.Pos, tx *world.Tx) (cube.Face, bool) { - for _, side := range redstoneTorchFallbackSides { - if tx.Block(pos.Side(side)).Model().FaceSolid(pos.Side(side), side.Opposite(), tx) { - return side.Opposite(), true - } +func torchFacingDirection(face cube.Face) string { + switch face { + case cube.FaceDown: + return "top" + case unknownFace: + return "unknown" + default: + return face.String() } - return 0, false } -// allRedstoneTorches returns all possible redstone torch block states. -func allRedstoneTorches() (all []world.Block) { - for _, f := range append(cube.Faces(), unknownFace) { - if f == cube.FaceUp { - continue +func allRedstoneTorches() (torches []world.Block) { + for _, face := range cube.Faces() { + if face == cube.FaceUp { + face = unknownFace } - all = append(all, RedstoneTorch{Facing: f, Lit: true}) - all = append(all, RedstoneTorch{Facing: f}) + torches = append(torches, RedstoneTorch{Facing: face}, RedstoneTorch{Facing: face, Lit: true}) } return } diff --git a/server/block/redstone_torch_test.go b/server/block/redstone_torch_test.go new file mode 100644 index 000000000..3a4a6effe --- /dev/null +++ b/server/block/redstone_torch_test.go @@ -0,0 +1,93 @@ +package block + +import ( + "fmt" + "testing" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +func TestRedstoneTorchPowerAndEncode(t *testing.T) { + torch := RedstoneTorch{Facing: cube.FaceDown, Lit: true} + if power := torch.RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 15 { + t.Fatalf("lit redstone torch output = %d, want 15", power) + } + if power := torch.RedstonePower(cube.Pos{}, nil, cube.FaceDown); power != 0 { + t.Fatalf("redstone torch attached-face output = %d, want 0", power) + } + if power := (RedstoneTorch{Facing: cube.FaceDown}).RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 0 { + t.Fatalf("unlit redstone torch output = %d, want 0", power) + } + if light := torch.LightEmissionLevel(); light != 7 { + t.Fatalf("lit redstone torch light = %d, want 7", light) + } + + name, props := torch.EncodeBlock() + if name != "minecraft:redstone_torch" { + t.Fatalf("redstone torch block name = %q, want minecraft:redstone_torch", name) + } + if facing := props["torch_facing_direction"]; facing != "top" { + t.Fatalf("torch_facing_direction = %v, want top", facing) + } + if _, ok := world.BlockByName(name, props); !ok { + t.Fatalf("BlockByName(%s, %#v) was not found", name, props) + } + + name, props = (RedstoneTorch{Facing: cube.FaceNorth}).EncodeBlock() + if name != "minecraft:unlit_redstone_torch" { + t.Fatalf("unlit redstone torch block name = %q, want minecraft:unlit_redstone_torch", name) + } + if facing := props["torch_facing_direction"]; facing != "north" { + t.Fatalf("unlit torch_facing_direction = %v, want north", facing) + } + if count := len(allRedstoneTorches()); count != len(cube.Faces())*2 { + t.Fatalf("allRedstoneTorches returned %d states, want %d", count, len(cube.Faces())*2) + } +} + +func TestRedstoneTorchInverseScheduledTick(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + support := cube.Pos{0, 1, 0} + pos := support.Side(cube.FaceUp) + tx.SetBlock(support, RedstoneBlock{}, nil) + torch := RedstoneTorch{Facing: cube.FaceDown, Lit: true} + tx.SetBlock(pos, torch, nil) + torch.ScheduledTick(pos, tx, nil) + after, ok := tx.Block(pos).(RedstoneTorch) + if !ok { + err = fmt.Errorf("redstone torch missing after scheduled tick") + return + } + if after.Lit { + err = fmt.Errorf("redstone torch stayed lit while attached block was powered") + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestRedstoneTorchRegisteredStates(t *testing.T) { + if b, ok := world.BlockByName("minecraft:redstone_torch", map[string]any{ + "torch_facing_direction": "west", + }); !ok { + t.Fatal("lit redstone torch state was not registered") + } else if torch, ok := b.(RedstoneTorch); !ok || !torch.Lit || torch.Facing != cube.FaceWest { + t.Fatalf("registered lit redstone torch = %#v, want west lit RedstoneTorch", b) + } + + if b, ok := world.BlockByName("minecraft:unlit_redstone_torch", map[string]any{ + "torch_facing_direction": "top", + }); !ok { + t.Fatal("unlit redstone torch state was not registered") + } else if torch, ok := b.(RedstoneTorch); !ok || torch.Lit || torch.Facing != cube.FaceDown { + t.Fatalf("registered unlit redstone torch = %#v, want standing unlit RedstoneTorch", b) + } +} diff --git a/server/block/redstone_wire.go b/server/block/redstone_wire.go deleted file mode 100644 index 3c110dcc4..000000000 --- a/server/block/redstone_wire.go +++ /dev/null @@ -1,273 +0,0 @@ -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(RedstoneWire{})).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { - updateStrongRedstone(pos, tx) - }) -} - -// EncodeBlock ... -func (r RedstoneWire) EncodeBlock() (string, map[string]any) { - return "minecraft:redstone_wire", map[string]any{ - "redstone_signal": int32(r.Power), - } -} - -// EncodeItem ... -func (RedstoneWire) EncodeItem() (name string, meta int16) { - return "minecraft:redstone", 0 -} - -// UseOnBlock ... -func (r RedstoneWire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { - pos, _, used := firstReplaceable(tx, pos, face, r) - if !used { - return false - } - belowPos := pos.Side(cube.FaceDown) - if !tx.Block(belowPos).Model().FaceSolid(belowPos, cube.FaceUp, tx) { - return false - } - r.Power = r.calculatePower(pos, tx) - place(tx, pos, r, user, ctx) - if placed(ctx) { - updateStrongRedstone(pos, tx) - return true - } - return false -} - -// NeighbourUpdateTick ... -func (r RedstoneWire) NeighbourUpdateTick(pos, neighbour cube.Pos, tx *world.Tx) { - if pos == neighbour { - // Ignore the self-update sent after this wire's block state changes. - return - } - below := pos.Side(cube.FaceDown) - if !tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) { - breakBlock(r, pos, tx) - return - } - if changed, ok := r.updateFromNeighbour(pos, tx); ok && !changed { - updateStrongRedstone(pos, tx) - } -} - -// RedstoneUpdate ... -func (r RedstoneWire) RedstoneUpdate(pos cube.Pos, tx *world.Tx) { - r.updatePower(pos, tx) -} - -// updateFromNeighbour updates the wire after a neighbour change. changed reports whether the wire's power changed, -// and ok reports whether the update was allowed by the redstone update handler. -func (r RedstoneWire) updateFromNeighbour(pos cube.Pos, tx *world.Tx) (changed bool, ok bool) { - if redstoneUpdateCancelled(pos, tx) { - return false, false - } - return r.updatePower(pos, tx), true -} - -// updatePower recalculates the wire's power and propagates the network when the power changes. -func (r RedstoneWire) updatePower(pos cube.Pos, tx *world.Tx) bool { - if power := r.calculatePower(pos, tx); r.Power != power { - r.Power = power - tx.SetBlock(pos, r, &world.SetOpts{DisableBlockUpdates: true}) - updateStrongRedstone(pos, tx) - return true - } - return false -} - -// RedstoneSource ... -func (RedstoneWire) RedstoneSource() bool { - return false -} - -// WeaklyPowersBlocks returns true because powered redstone dust weakly powers conductive blocks it points into or rests on top of. -func (RedstoneWire) WeaklyPowersBlocks() bool { - return true -} - -// WeakPower returns the power emitted by the wire toward a neighbouring receiver. Dust powers upward, never powers -// downward, and only powers horizontal receivers in connected directions. A powered wire with no horizontal -// connections behaves as an unconnected cross and powers every horizontal side. -func (r RedstoneWire) WeakPower(pos cube.Pos, face cube.Face, tx *world.Tx, accountForDust bool) int { - if !accountForDust { - return 0 - } - if face == cube.FaceUp { - return r.Power - } - if face == cube.FaceDown { - return 0 - } - if !r.hasHorizontalRedstoneConnection(pos, tx) { - return r.Power - } - if r.connection(pos, face.Opposite(), tx) { - return r.Power - } - if r.connection(pos, face, tx) && !r.connection(pos, face.RotateLeft(), tx) && !r.connection(pos, face.RotateRight(), tx) { - return r.Power - } - return 0 -} - -// StrongPower returns 0 because redstone dust weakly powers conductive blocks rather than strongly powering them. -func (RedstoneWire) StrongPower(cube.Pos, cube.Face, *world.Tx, bool) int { - return 0 -} - -// calculatePower returns the highest level of received redstone power at the provided position. -func (r RedstoneWire) calculatePower(pos cube.Pos, tx *world.Tx) int { - return calculateRedstoneWirePower(pos, tx, tx.Block) -} - -// calculateRedstoneWirePower returns the highest level of received redstone power at the provided position. blockAt is -// injected so direct updates and the BFS wire network share the same rules while the BFS path may still read cached -// node state. -func calculateRedstoneWirePower(pos cube.Pos, tx *world.Tx, blockAt func(cube.Pos) world.Block) int { - aboveBlocksVerticalTravel := blocksRedstoneWireVerticalTravel(blockAt(pos.Side(cube.FaceUp))) - var blockPower, wirePower int - for _, side := range cube.Faces() { - neighbourPos := pos.Side(side) - neighbour := blockAt(neighbourPos) - - wirePower = maxRedstoneWirePower(neighbour, wirePower) - blockPower = max(blockPower, tx.RedstonePower(neighbourPos, side, false)) - - if side.Axis() == cube.Y { - // Only check horizontal neighbours from here on. - continue - } - - if canRedstoneWireStepDown(pos, neighbourPos, neighbour, tx) && !aboveBlocksVerticalTravel { - wirePower = maxRedstoneWirePower(blockAt(neighbourPos.Side(cube.FaceUp)), wirePower) - } - if canRedstoneWireStepDown(neighbourPos.Side(cube.FaceDown), neighbourPos, neighbour, tx) && !blocksRedstoneWireVerticalTravel(neighbour) { - wirePower = maxRedstoneWirePower(blockAt(neighbourPos.Side(cube.FaceDown)), wirePower) - } - - if _, neighbourSolid := neighbour.Model().(model.Solid); !neighbourSolid { - wirePower = maxRedstoneWirePower(blockAt(neighbourPos.Side(cube.FaceDown)), wirePower) - } - } - return max(blockPower, wirePower-1) -} - -// hasHorizontalRedstoneConnection checks if the dust connects horizontally to redstone wire or a redstone source. It -// does not include passive receivers such as doors, trapdoors, or note blocks. -func (r RedstoneWire) hasHorizontalRedstoneConnection(pos cube.Pos, tx *world.Tx) bool { - for _, face := range cube.HorizontalFaces() { - if r.connection(pos, face, tx) { - return true - } - } - return false -} - -// connection returns true if the dust shape connects through the given face to another wire or a redstone source. It -// also accounts for valid one-block vertical wire connections. -func (r RedstoneWire) connection(pos cube.Pos, face cube.Face, tx *world.Tx) bool { - sidePos := pos.Side(face) - sideBlock := tx.Block(sidePos) - if r.connectsAbove(pos, sidePos, sideBlock, tx) || r.connectsTo(sideBlock, true) { - return true - } - return r.connectsBelow(sidePos, sideBlock, tx) -} - -// connectsAbove checks if the redstone wire can connect to the block above it. -func (r RedstoneWire) connectsAbove(pos, sidePos cube.Pos, sideBlock world.Block, tx *world.Tx) bool { - if blocksRedstoneWireVerticalTravel(tx.Block(pos.Side(cube.FaceUp))) || !r.canRunOnTop(tx, sidePos, sideBlock) { - return false - } - return r.connectsTo(tx.Block(sidePos.Side(cube.FaceUp)), false) -} - -// connectsBelow checks if the redstone wire can connect to the block below it. -func (r RedstoneWire) connectsBelow(sidePos cube.Pos, sideBlock world.Block, tx *world.Tx) bool { - _, sideSolid := sideBlock.Model().(model.Solid) - return !sideSolid && r.connectsTo(tx.Block(sidePos.Side(cube.FaceDown)), false) -} - -// connectsTo reports whether a block is part of the redstone wire connection graph. Passive redstone receivers are not -// connections; direct source conductors count only when allowDirectSources is true. -func (RedstoneWire) connectsTo(block world.Block, allowDirectSources bool) bool { - if _, ok := block.(RedstoneWire); ok { - return true - } - c, ok := block.(world.Conductor) - return ok && allowDirectSources && c.RedstoneSource() -} - -// canRunOnTop checks whether redstone dust can be placed on top of the block. -func (RedstoneWire) canRunOnTop(tx *world.Tx, pos cube.Pos, block world.Block) bool { - return block.Model().FaceSolid(pos, cube.FaceUp, tx) -} - -// blocksRedstoneWireVerticalTravel checks if the block above redstone wire blocks vertical wire travel. -func blocksRedstoneWireVerticalTravel(block world.Block) bool { - if _, ok := block.Model().(model.Solid); !ok { - return false - } - diffuser, ok := block.(LightDiffuser) - return !ok || diffuser.LightDiffusionLevel() != 0 -} - -// canRedstoneWireStepDown checks if redstone dust can provide power while travelling down around the side block. -func canRedstoneWireStepDown(from, side cube.Pos, block world.Block, tx *world.Tx) bool { - if stepDowner, ok := block.(RedstoneWireStepDowner); ok { - return stepDowner.CanRedstoneWireStepDown(side, from, tx) - } - for _, face := range cube.Faces() { - if !block.Model().FaceSolid(side, face, tx) { - return false - } - } - return true -} - -// TrimMaterial delegates to item.RedstoneWire so the block form stays valid for smithing trim decoding too. -func (RedstoneWire) TrimMaterial() string { - return item.RedstoneWire{}.TrimMaterial() -} - -// MaterialColour delegates to item.RedstoneWire to keep trim metadata defined in one place. -func (RedstoneWire) MaterialColour() string { - return item.RedstoneWire{}.MaterialColour() -} - -// allRedstoneWires returns a list of all redstone dust states. -func allRedstoneWires() (all []world.Block) { - for i := range 16 { - all = append(all, RedstoneWire{Power: i}) - } - return -} diff --git a/server/block/register.go b/server/block/register.go index 116c454e6..fcedc53dd 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -147,6 +147,7 @@ func init() { registerAll(allBlastFurnaces()) registerAll(allBoneBlock()) registerAll(allBrewingStands()) + registerAll(allButtons()) registerAll(allCactus()) registerAll(allCake()) registerAll(allCampfires()) @@ -196,11 +197,13 @@ func init() { registerAll(allPinkPetals()) registerAll(allPlanks()) registerAll(allPotato()) + registerAll(allPressurePlates()) registerAll(allPrismarine()) registerAll(allPumpkinStems()) registerAll(allPumpkins()) registerAll(allPurpurs()) registerAll(allQuartz()) + registerAll(allRedstoneLamps()) registerAll(allRedstoneTorches()) registerAll(allRedstoneWires()) registerAll(allSandstones()) @@ -256,6 +259,7 @@ func init() { world.RegisterItem(Bookshelf{}) world.RegisterItem(BrewingStand{}) world.RegisterItem(Bricks{}) + world.RegisterItem(Button{Type: redstoneSourceStone}) world.RegisterItem(Cactus{}) world.RegisterItem(Cake{}) world.RegisterItem(Calcite{}) @@ -353,6 +357,7 @@ func init() { world.RegisterItem(PolishedBlackstoneBrick{Cracked: true}) world.RegisterItem(PolishedBlackstoneBrick{}) world.RegisterItem(Potato{}) + world.RegisterItem(PressurePlate{Type: redstoneSourceStone}) world.RegisterItem(PumpkinSeeds{}) world.RegisterItem(Pumpkin{Carved: true}) world.RegisterItem(Pumpkin{}) @@ -369,6 +374,7 @@ func init() { world.RegisterItem(RedstoneTorch{}) world.RegisterItem(RedstoneWire{}) world.RegisterItem(ReinforcedDeepslate{}) + world.RegisterItem(RedstoneLamp{}) world.RegisterItem(ResinBricks{Chiseled: true}) world.RegisterItem(ResinBricks{}) world.RegisterItem(Resin{}) @@ -508,6 +514,31 @@ func init() { for _, t := range DeepslateTypes() { world.RegisterItem(Deepslate{Type: t}) } + for _, typ := range []int{ + redstoneSourcePolishedBlackstone, + } { + world.RegisterItem(Button{Type: typ}) + world.RegisterItem(PressurePlate{Type: typ}) + } + world.RegisterItem(PressurePlate{Type: redstoneSourceLightWeighted}) + world.RegisterItem(PressurePlate{Type: redstoneSourceHeavyWeighted}) + for _, typ := range []int{ + redstoneSourceOak, + redstoneSourceSpruce, + redstoneSourceBirch, + redstoneSourceJungle, + redstoneSourceAcacia, + redstoneSourceDarkOak, + redstoneSourceMangrove, + redstoneSourceCherry, + redstoneSourceBamboo, + redstoneSourceCrimson, + redstoneSourceWarped, + redstoneSourcePaleOak, + } { + world.RegisterItem(Button{Type: typ}) + world.RegisterItem(PressurePlate{Type: typ}) + } for _, o := range OxidationTypes() { world.RegisterItem(CopperBars{Oxidation: o}) world.RegisterItem(CopperBars{Oxidation: o, Waxed: true}) diff --git a/server/session/world.go b/server/session/world.go index 1ecdfb728..801c496ca 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -550,6 +550,14 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) Position: vec64To32(pos), }) return + case sound.PistonIn: + pk.SoundType = packet.SoundEventPistonIn + case sound.PistonOut: + pk.SoundType = packet.SoundEventPistonOut + case sound.PressurePlateClickOn: + pk.SoundType = packet.SoundEventPressurePlateClickOn + case sound.PressurePlateClickOff: + pk.SoundType = packet.SoundEventPressurePlateClickOff case sound.SignWaxed: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventWaxOn, diff --git a/server/world/block.go b/server/world/block.go index 165cce560..21882b214 100644 --- a/server/world/block.go +++ b/server/world/block.go @@ -71,56 +71,6 @@ type Liquid interface { LiquidRemoveBlock(pos cube.Pos, tx *Tx, removed Block) } -// Conductor represents a block that can conduct a redstone signal. -type Conductor interface { - Block - // RedstoneSource returns true if the conductor is a signal source. - RedstoneSource() bool - - // WeakPower returns the weak power level emitted by this conductor toward a neighbouring receiver. - // The face argument is relative to the receiving block, not this conductor. - // Weak power can pass through a solid block to power redstone components on the other side, but - // cannot power solid blocks themselves or travel further. - // The accountForDust parameter indicates whether redstone dust should be considered when - // calculating power levels. - WeakPower(pos cube.Pos, face cube.Face, tx *Tx, accountForDust bool) int - - // StrongPower returns the strong power level emitted by this conductor toward a neighbouring - // receiver. The face argument uses the same convention as WeakPower. - // Strong power can be transmitted through solid blocks. When a solid block receives strong power - // through one of its faces, it can provide weak power to adjacent redstone components on all other - // faces. Strong power can also directly power any redstone component. - // The accountForDust parameter indicates whether redstone dust should be considered when - // calculating power levels. - StrongPower(pos cube.Pos, face cube.Face, tx *Tx, accountForDust bool) int -} - -// WeakBlockPowerer represents a conductor whose weak power may weakly power an adjacent conductive block. Weakly -// powered blocks may activate mechanisms and repeaters, but do not power adjacent redstone dust. For example, -// dust pointing into a stone block opens a door on the stone's far side, but a second stretch of dust there stays -// dark. -type WeakBlockPowerer interface { - Conductor - // WeaklyPowersBlocks returns true if this conductor's WeakPower can make an adjacent conductive block weakly powered. - WeaklyPowersBlocks() bool -} - -// RedstonePowerRelayer represents a block with custom behaviour for whether -// neighbouring redstone power may be relayed through it by Tx.RedstonePower. -type RedstonePowerRelayer interface { - Block - // RelaysRedstonePowerThrough reports whether this non-conductor block may - // relay neighbouring redstone power through itself to receivers on its other sides. - RelaysRedstonePowerThrough() bool -} - -// RedstoneUpdater represents a block that reacts to nearby redstone power changes. -type RedstoneUpdater interface { - Block - // RedstoneUpdate is called when a change in redstone signal is computed. - RedstoneUpdate(pos cube.Pos, tx *Tx) -} - // RegisterBlock registers the Block passed in the DefaultBlockRegistry. // // This function exists for backwards compatibility and works well for the common "single server per process" setup, diff --git a/server/world/conf.go b/server/world/conf.go index 94bede71e..b2772f2d7 100644 --- a/server/world/conf.go +++ b/server/world/conf.go @@ -119,6 +119,7 @@ func (conf Config) New() *World { s := conf.Provider.Settings() w := &World{ scheduledUpdates: newScheduledTickQueue(s.CurrentTick), + redstone: newRedstoneEngine(s.CurrentTick), entities: make(map[*EntityHandle]ChunkPos), viewers: make(map[*Loader]Viewer), chunks: make(map[ChunkPos]*Column), diff --git a/server/world/handler.go b/server/world/handler.go index bdd248ae1..de194d4c5 100644 --- a/server/world/handler.go +++ b/server/world/handler.go @@ -8,6 +8,14 @@ import ( type Context = event.Context[*Tx] +// RedstoneHandler may be implemented by world Handlers to receive redstone updates. It is intentionally separate +// from Handler so existing handlers remain source-compatible. +type RedstoneHandler interface { + // HandleRedstoneUpdate handles a redstone update proposed by the World redstone engine. ctx.Cancel() may be + // called to suppress the proposed redstone mutation and any propagation from that mutation. + HandleRedstoneUpdate(ctx *Context, update RedstoneUpdate) +} + // Handler handles events that are called by a world. Implementations of // Handler may be used to listen to specific events such as when an Entity is // added to the world. @@ -63,9 +71,8 @@ type Handler interface { // The affected entities, affected blocks, item drop chance, and whether the // explosion spawns fire may be altered. HandleExplosion(ctx *Context, position mgl64.Vec3, entities *[]Entity, blocks *[]cube.Pos, itemDropChance *float64, spawnFire *bool) - // HandleRedstoneUpdate handles a redstone update at a position. ctx.Cancel() may be called - // to cancel the redstone update. - HandleRedstoneUpdate(ctx *Context, pos cube.Pos) + // HandleRedstoneUpdate handles a redstone update. ctx.Cancel() may be called to cancel the proposed update. + HandleRedstoneUpdate(ctx *Context, update RedstoneUpdate) // HandleClose handles the World being closed. HandleClose may be used as a // moment to finish code running on other goroutines that operates on the // World specifically. HandleClose is called directly before the World stops @@ -81,6 +88,7 @@ var _ Handler = (*NopHandler)(nil) // Users may embed NopHandler to avoid having to implement each method. type NopHandler struct{} +func (NopHandler) HandleRedstoneUpdate(*Context, RedstoneUpdate) {} func (NopHandler) HandleLiquidFlow(*Context, cube.Pos, cube.Pos, Liquid, Block) {} func (NopHandler) HandleLiquidDecay(*Context, cube.Pos, Liquid, Liquid) {} func (NopHandler) HandleLiquidHarden(*Context, cube.Pos, Block, Block, Block) {} @@ -92,5 +100,4 @@ func (NopHandler) HandleLeavesDecay(*Context, cube.Pos) func (NopHandler) HandleEntitySpawn(*Tx, Entity) {} func (NopHandler) HandleEntityDespawn(*Tx, Entity) {} func (NopHandler) HandleExplosion(*Context, mgl64.Vec3, *[]Entity, *[]cube.Pos, *float64, *bool) {} -func (NopHandler) HandleRedstoneUpdate(*Context, cube.Pos) {} func (NopHandler) HandleClose(*Tx) {} diff --git a/server/world/redstone.go b/server/world/redstone.go new file mode 100644 index 000000000..5b08fe1ca --- /dev/null +++ b/server/world/redstone.go @@ -0,0 +1,724 @@ +package world + +import ( + "encoding/binary" + "hash/fnv" + "maps" + "slices" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/event" +) + +// RedstoneUpdateCause describes the world event that caused a redstone update to be evaluated. +type RedstoneUpdateCause uint8 + +const ( + // RedstoneUpdateCauseBlockUpdate means a block or liquid change invalidated nearby redstone. + RedstoneUpdateCauseBlockUpdate RedstoneUpdateCause = iota + // RedstoneUpdateCauseScheduledTick means a scheduled redstone tick invalidated a component. + RedstoneUpdateCauseScheduledTick + // RedstoneUpdateCauseCompilerRebuild means a redstone compiler rebuild invalidated a component. + RedstoneUpdateCauseCompilerRebuild +) + +// RedstoneUpdate represents a redstone state transition proposed by the world redstone engine. Handlers may cancel +// the event to suppress the proposed mutation and any propagation from that mutation. +type RedstoneUpdate struct { + // Pos is the block position that will receive the update. + Pos cube.Pos + // ChangedNeighbour is the neighbouring block that caused the update, if any. + ChangedNeighbour cube.Pos + // Before is the block currently at Pos. + Before Block + // After is the block that will replace Before, if the update is a block-state update. After is nil for updates + // that perform side effects instead of replacing the block. + After Block + // OldPower is the last redstone power observed by the engine at Pos. + OldPower int + // NewPower is the redstone power observed by the engine at Pos for this update. + NewPower int + // CurrentTick is the world tick during which the update was evaluated. + CurrentTick int64 + // NetworkID identifies the compiled dynamic redstone region that produced the update. + NetworkID uint64 + // Cause identifies why the update was evaluated. + Cause RedstoneUpdateCause +} + +// RedstonePowerSource is implemented by blocks that emit redstone power. The face passed is the face of the source +// block that power is being read from. +type RedstonePowerSource interface { + RedstonePower(pos cube.Pos, tx *Tx, face cube.Face) int +} + +// RedstoneStrongPowerSource is implemented by sources that strongly power blocks from specific faces. Strong power may +// pass through solid blocks, unlike weak redstone wire power. +type RedstoneStrongPowerSource interface { + RedstoneStrongPower(pos cube.Pos, tx *Tx, face cube.Face) int +} + +// RedstonePowerRelayer is implemented by redstone wire-like blocks that relay power through a compiled redstone +// network. The returned value is the signal loss when power enters through from and leaves through to. +type RedstonePowerRelayer interface { + RedstoneSignalLoss(pos cube.Pos, tx *Tx, from, to cube.Face) int +} + +// RedstonePowerRelayerNeighbourer may be implemented by relayers with non-adjacent connections, such as redstone +// wire stepping up and down block edges. +type RedstonePowerRelayerNeighbourer interface { + RedstoneRelayerNeighbours(pos cube.Pos, tx *Tx) []cube.Pos +} + +// RedstonePowerConsumer is implemented by blocks whose block state changes when their input power changes. The +// returned block is written to the world if changed is true and the redstone update event is not cancelled. +type RedstonePowerConsumer interface { + RedstonePowerUpdate(pos cube.Pos, tx *Tx, power int) (after Block, changed bool) +} + +// RedstonePowerTransitionConsumer may be implemented by consumers that need the previous and new input power to +// distinguish a real redstone transition from a same-power block update. +type RedstonePowerTransitionConsumer interface { + RedstonePowerTransitionUpdate(pos cube.Pos, tx *Tx, oldPower, newPower int) (after Block, changed bool) +} + +// RedstonePowerSounder may be implemented by consumers that play a sound after an uncancelled redstone-driven state +// change. +type RedstonePowerSounder interface { + RedstonePowerUpdateSound(pos cube.Pos, tx *Tx, before, after Block, oldPower, newPower int) Sound +} + +// RedstonePowerPostUpdater may be implemented by consumers that need to apply side effects after an uncancelled +// redstone state update, such as syncing the other half of a door. +type RedstonePowerPostUpdater interface { + RedstonePowerPostUpdate(pos cube.Pos, tx *Tx, before, after Block, oldPower, newPower int) +} + +// RedstonePowerAction is implemented by blocks that perform a side effect when their input power changes, such as TNT +// priming on a rising edge. The action is run only if the redstone update event is not cancelled. The returned bool +// reports whether a side effect was performed. +type RedstonePowerAction interface { + RedstonePowerAction(pos cube.Pos, tx *Tx, oldPower, newPower int) bool +} + +// RedstoneComparatorReadable is implemented by blocks that expose an analog signal to a comparator. +type RedstoneComparatorReadable interface { + RedstoneComparatorOutput(pos cube.Pos, tx *Tx, face cube.Face) int +} + +type redstoneEngine struct { + currentTick int64 + dirty map[cube.Pos]redstoneDirty + power map[cube.Pos]int + output map[cube.Pos]int + evaluating map[cube.Pos]struct{} +} + +type redstoneDirty struct { + changed cube.Pos + cause RedstoneUpdateCause +} + +type redstoneGraph struct { + id uint64 + nodes []redstoneNode + edges []redstoneEdge +} + +type redstoneNode struct { + pos cube.Pos + power int + source bool + sink bool +} + +type redstoneEdge struct { + from, to int + weight int +} + +func newRedstoneEngine(tick int64) *redstoneEngine { + return &redstoneEngine{ + currentTick: tick, + dirty: make(map[cube.Pos]redstoneDirty), + power: make(map[cube.Pos]int), + output: make(map[cube.Pos]int), + evaluating: make(map[cube.Pos]struct{}), + } +} + +func (e *redstoneEngine) invalidateAround(pos, changed cube.Pos, cause RedstoneUpdateCause, r cube.Range) { + if e == nil || pos.OutOfBounds(r) { + return + } + e.invalidate(pos, changed, cause, r) + pos.Neighbours(func(neighbour cube.Pos) { + e.invalidate(neighbour, changed, cause, r) + }, r) +} + +func (e *redstoneEngine) invalidate(pos, changed cube.Pos, cause RedstoneUpdateCause, r cube.Range) { + if pos.OutOfBounds(r) { + return + } + e.dirty[pos] = redstoneDirty{changed: changed, cause: cause} +} + +func (e *redstoneEngine) removeChunk(chunkPos ChunkPos) { + if e == nil { + return + } + maps.DeleteFunc(e.dirty, func(pos cube.Pos, _ redstoneDirty) bool { + return chunkPosFromBlockPos(pos) == chunkPos + }) + maps.DeleteFunc(e.dirty, func(_ cube.Pos, dirty redstoneDirty) bool { + return chunkPosFromBlockPos(dirty.changed) == chunkPos + }) + maps.DeleteFunc(e.power, func(pos cube.Pos, _ int) bool { + return chunkPosFromBlockPos(pos) == chunkPos + }) + maps.DeleteFunc(e.output, func(pos cube.Pos, _ int) bool { + return chunkPosFromBlockPos(pos) == chunkPos + }) +} + +func (e *redstoneEngine) forget(pos cube.Pos) { + if e == nil { + return + } + delete(e.power, pos) + delete(e.output, pos) + delete(e.evaluating, pos) +} + +func (e *redstoneEngine) tick(tx *Tx, tick int64) { + if e == nil || len(e.dirty) == 0 { + return + } + e.currentTick = tick + dirty := maps.Clone(e.dirty) + clear(e.dirty) + + candidates := slices.Collect(maps.Keys(dirty)) + slices.SortFunc(candidates, compareBlockPos) + + graph := e.compile(tx, candidates) + powers := e.graphPower(tx, graph) + for i, node := range graph.nodes { + d := dirty[node.pos] + if node.sink { + e.update(tx, node.pos, d.changed, d.cause, graph.id, powers[i]) + } + } + for _, node := range graph.nodes { + d := dirty[node.pos] + if node.source { + e.updateSource(tx, node.pos, d.changed, d.cause, graph.id) + } + } +} + +func (e *redstoneEngine) compile(tx *Tx, candidates []cube.Pos) redstoneGraph { + nodes := make([]redstoneNode, 0, len(candidates)) + seen := make(map[cube.Pos]struct{}, len(candidates)*2) + for _, pos := range candidates { + e.compileRegion(tx, pos, seen, &nodes) + pos.Neighbours(func(neighbour cube.Pos) { + if b, ok := tx.World().blockLoaded(neighbour); ok && isRedstoneRelevant(b) { + e.compileRegion(tx, neighbour, seen, &nodes) + } + }, tx.Range()) + } + slices.SortFunc(nodes, func(a, b redstoneNode) int { + return compareBlockPos(a.pos, b.pos) + }) + edges := e.compileEdges(tx, nodes) + return redstoneGraph{id: redstoneGraphID(nodes, edges), nodes: nodes, edges: edges} +} + +func (e *redstoneEngine) compileRegion(tx *Tx, pos cube.Pos, seen map[cube.Pos]struct{}, nodes *[]redstoneNode) { + if _, ok := seen[pos]; ok || pos.OutOfBounds(tx.Range()) { + return + } + queue := []cube.Pos{pos} + for len(queue) != 0 { + p := queue[0] + queue = queue[1:] + if _, ok := seen[p]; ok || p.OutOfBounds(tx.Range()) { + continue + } + seen[p] = struct{}{} + + b, ok := tx.World().blockLoaded(p) + if !ok { + continue + } + source, consumer, action, relayer := classifyRedstoneBlock(b) + if !source && !consumer && !action && !relayer { + continue + } + *nodes = append(*nodes, redstoneNode{ + pos: p, + source: source, + sink: consumer || action, + }) + if !relayer { + continue + } + e.redstoneRelayerNeighbours(tx, p, b, func(neighbour cube.Pos) { + if b, ok := tx.World().blockLoaded(neighbour); ok && isRedstoneRelevant(b) { + queue = append(queue, neighbour) + } + }) + } +} + +func (e *redstoneEngine) update(tx *Tx, pos, changed cube.Pos, cause RedstoneUpdateCause, graphID uint64, newPower int) { + b := tx.Block(pos) + oldPower, newPower := e.power[pos], clampRedstonePower(newPower) + + after, blockChanged := b, false + if consumer, ok := b.(RedstonePowerTransitionConsumer); ok { + after, blockChanged = consumer.RedstonePowerTransitionUpdate(pos, tx, oldPower, newPower) + } else if consumer, ok := b.(RedstonePowerConsumer); ok { + after, blockChanged = consumer.RedstonePowerUpdate(pos, tx, newPower) + } + action, hasAction := b.(RedstonePowerAction) + actionChanged := hasAction && oldPower != newPower + if !blockChanged && !actionChanged { + e.power[pos] = newPower + return + } + + update := RedstoneUpdate{ + Pos: pos, + ChangedNeighbour: changed, + Before: b, + After: after, + OldPower: oldPower, + NewPower: newPower, + CurrentTick: e.currentTick, + NetworkID: graphID, + Cause: cause, + } + ctx := event.C(tx) + if handler, ok := tx.World().Handler().(RedstoneHandler); ok { + handler.HandleRedstoneUpdate(ctx, update) + } + if ctx.Cancelled() { + return + } + if blockChanged { + tx.SetBlock(pos, after, &SetOpts{DisableRedstoneUpdates: true}) + e.invalidateAround(pos, pos, RedstoneUpdateCauseBlockUpdate, tx.Range()) + } + if blockChanged { + if postUpdater, ok := b.(RedstonePowerPostUpdater); ok { + postUpdater.RedstonePowerPostUpdate(pos, tx, b, after, oldPower, newPower) + } + if sounder, ok := b.(RedstonePowerSounder); ok { + if s := sounder.RedstonePowerUpdateSound(pos, tx, b, after, oldPower, newPower); s != nil { + tx.PlaySound(pos.Vec3Centre(), s) + } + } + } + acted := false + if actionChanged { + acted = action.RedstonePowerAction(pos, tx, oldPower, newPower) + } + if blockChanged || actionChanged || acted { + e.power[pos] = newPower + } +} + +func (e *redstoneEngine) updateSource(tx *Tx, pos, changed cube.Pos, cause RedstoneUpdateCause, graphID uint64) { + b := tx.Block(pos) + oldPower, newPower := e.output[pos], e.sourcePower(pos, tx) + if oldPower == newPower { + return + } + update := RedstoneUpdate{ + Pos: pos, + ChangedNeighbour: changed, + Before: b, + OldPower: oldPower, + NewPower: newPower, + CurrentTick: e.currentTick, + NetworkID: graphID, + Cause: cause, + } + ctx := event.C(tx) + if handler, ok := tx.World().Handler().(RedstoneHandler); ok { + handler.HandleRedstoneUpdate(ctx, update) + } + if ctx.Cancelled() { + return + } + e.output[pos] = newPower + e.invalidateAround(pos, pos, RedstoneUpdateCauseBlockUpdate, tx.Range()) +} + +func (e *redstoneEngine) directPower(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.directPowerFrom(pos, tx, face)) + } + return power +} + +func (e *redstoneEngine) directPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + neighbour := pos.Side(face) + if neighbour.OutOfBounds(tx.Range()) { + return 0 + } + b, ok := tx.World().blockLoaded(neighbour) + if !ok { + return 0 + } + if source, ok := b.(RedstonePowerSource); ok { + return clampRedstonePower(e.redstonePower(source, neighbour, tx, face.Opposite())) + } + return 0 +} + +func (e *redstoneEngine) strongPower(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.strongPowerFrom(pos, tx, face)) + } + return power +} + +func (e *redstoneEngine) strongPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + neighbour := pos.Side(face) + if neighbour.OutOfBounds(tx.Range()) { + return 0 + } + b, ok := tx.World().blockLoaded(neighbour) + if !ok { + return 0 + } + if source, ok := b.(RedstoneStrongPowerSource); ok { + return clampRedstonePower(source.RedstoneStrongPower(neighbour, tx, face.Opposite())) + } + return 0 +} + +func (e *redstoneEngine) conductedStrongPower(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.conductedStrongPowerFrom(pos, tx, face)) + } + return power +} + +func (e *redstoneEngine) conductedStrongPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) int { + conductorPos := pos.Side(face) + if conductorPos.OutOfBounds(tx.Range()) { + return 0 + } + conductor, ok := tx.World().blockLoaded(conductorPos) + if !ok || !conductor.Model().FaceSolid(conductorPos, face.Opposite(), tx) { + return 0 + } + power := 0 + for _, sourceFace := range cube.Faces() { + power = max(power, e.strongPowerFrom(conductorPos, tx, sourceFace)) + } + return power +} + +func (e *redstoneEngine) sourcePower(pos cube.Pos, tx *Tx) int { + b, ok := tx.World().blockLoaded(pos) + if !ok { + return 0 + } + source, ok := b.(RedstonePowerSource) + if !ok { + return 0 + } + power := 0 + for _, face := range cube.Faces() { + power = max(power, clampRedstonePower(e.redstonePower(source, pos, tx, face))) + } + return power +} + +func (e *redstoneEngine) graphPower(tx *Tx, graph redstoneGraph) []int { + powers := make([]int, len(graph.nodes)) + if len(graph.nodes) == 0 { + return powers + } + + index := make(map[cube.Pos]int, len(graph.nodes)) + sources := make([]RedstonePowerSource, len(graph.nodes)) + relayers := make([]RedstonePowerRelayer, len(graph.nodes)) + edges := make([][]redstoneEdge, len(graph.nodes)) + for i, node := range graph.nodes { + index[node.pos] = i + if b, ok := tx.World().blockLoaded(node.pos); ok { + sources[i], _ = b.(RedstonePowerSource) + relayers[i], _ = b.(RedstonePowerRelayer) + } + } + for _, edge := range graph.edges { + edges[edge.from] = append(edges[edge.from], edge) + } + + queue := make([]int, 0, len(graph.nodes)) + push := func(i, power int) { + power = clampRedstonePower(power) + if power <= powers[i] { + return + } + powers[i] = power + queue = append(queue, i) + } + + for i, node := range graph.nodes { + push(i, e.conductedStrongPower(node.pos, tx)) + } + + for i, source := range sources { + // Relayers such as redstone wire store their previous output as RedstonePower. + // They must be recomputed from real sources, not used as seeds themselves. + if source == nil || relayers[i] != nil { + continue + } + pos := graph.nodes[i].pos + for _, face := range cube.Faces() { + j, ok := index[pos.Side(face)] + if !ok { + continue + } + power := clampRedstonePower(e.redstonePower(source, pos, tx, face)) + push(j, power) + } + } + + for head := 0; head < len(queue); head++ { + i := queue[head] + if relayers[i] == nil { + continue + } + for _, edge := range edges[i] { + push(edge.to, powers[i]-edge.weight) + } + } + return powers +} + +func (e *redstoneEngine) powerTo(pos cube.Pos, tx *Tx) int { + power := 0 + for _, face := range cube.Faces() { + power = max(power, e.powerFrom(pos, tx, face, false)) + } + power = max(power, e.conductedStrongPower(pos, tx)) + return clampRedstonePower(power) +} + +func (e *redstoneEngine) powerFrom(pos cube.Pos, tx *Tx, face cube.Face, relayerSources bool) int { + power := e.conductedStrongPowerFrom(pos, tx, face) + type step struct { + pos cube.Pos + from cube.Face + loss int + depth int + } + queue := []step{{pos: pos.Side(face), from: face.Opposite(), loss: 0, depth: 0}} + seen := make(map[cube.Pos]int, 16) + for len(queue) != 0 { + s := queue[0] + queue = queue[1:] + if s.pos.OutOfBounds(tx.Range()) || s.loss >= 15 || s.depth >= 15 { + continue + } + if s.pos == pos { + continue + } + if loss, ok := seen[s.pos]; ok && loss <= s.loss { + continue + } + seen[s.pos] = s.loss + + b, ok := tx.World().blockLoaded(s.pos) + if !ok { + continue + } + relayer, isRelayer := b.(RedstonePowerRelayer) + // See graphPower: relayers carry recomputed power through edges, so their + // stored RedstonePower should not count as an independent source here. + if source, ok := b.(RedstonePowerSource); ok && (!isRelayer || relayerSources) { + power = max(power, clampRedstonePower(e.redstonePower(source, s.pos, tx, s.from)-s.loss)) + } + if !isRelayer { + continue + } + for _, next := range e.redstoneRelayerNeighbourPositions(tx, s.pos, b) { + to := redstoneStepFace(s.pos, next) + if to == s.from { + continue + } + loss := s.loss + max(relayer.RedstoneSignalLoss(s.pos, tx, s.from, to), 1) + if loss <= 15 { + queue = append(queue, step{pos: next, from: to.Opposite(), loss: loss, depth: s.depth + 1}) + } + } + } + return clampRedstonePower(power) +} + +func (e *redstoneEngine) redstonePower(source RedstonePowerSource, pos cube.Pos, tx *Tx, face cube.Face) int { + if _, ok := e.evaluating[pos]; ok { + return 0 + } + e.evaluating[pos] = struct{}{} + defer delete(e.evaluating, pos) + return source.RedstonePower(pos, tx, face) +} + +func (e *redstoneEngine) compileEdges(tx *Tx, nodes []redstoneNode) []redstoneEdge { + index := make(map[cube.Pos]int, len(nodes)) + for i, node := range nodes { + index[node.pos] = i + } + edges := make([]redstoneEdge, 0, len(nodes)) + for i, node := range nodes { + b, loaded := tx.World().blockLoaded(node.pos) + if !loaded { + continue + } + relayer, ok := b.(RedstonePowerRelayer) + if !ok { + continue + } + for _, neighbour := range e.redstoneRelayerNeighbourPositions(tx, node.pos, b) { + j, ok := index[neighbour] + if !ok { + continue + } + face := redstoneStepFace(node.pos, neighbour) + edges = append(edges, redstoneEdge{from: i, to: j, weight: max(relayer.RedstoneSignalLoss(node.pos, tx, face.Opposite(), face), 1)}) + } + } + slices.SortFunc(edges, compareRedstoneEdge) + return edges +} + +func (e *redstoneEngine) redstoneRelayerNeighbourPositions(tx *Tx, pos cube.Pos, b Block) []cube.Pos { + if neighbourer, ok := b.(RedstonePowerRelayerNeighbourer); ok { + neighbours := slices.Clone(neighbourer.RedstoneRelayerNeighbours(pos, tx)) + slices.SortFunc(neighbours, compareBlockPos) + return neighbours + } + neighbours := make([]cube.Pos, 0, len(cube.Faces())) + e.redstoneRelayerNeighbours(tx, pos, b, func(neighbour cube.Pos) { + neighbours = append(neighbours, neighbour) + }) + slices.SortFunc(neighbours, compareBlockPos) + return neighbours +} + +func (e *redstoneEngine) redstoneRelayerNeighbours(tx *Tx, pos cube.Pos, _ Block, f func(cube.Pos)) { + for _, face := range cube.Faces() { + neighbour := pos.Side(face) + if !neighbour.OutOfBounds(tx.Range()) { + f(neighbour) + } + } +} + +func redstoneStepFace(from, to cube.Pos) cube.Face { + dx, dy, dz := to[0]-from[0], to[1]-from[1], to[2]-from[2] + switch { + case dx > 0: + return cube.FaceEast + case dx < 0: + return cube.FaceWest + case dz > 0: + return cube.FaceSouth + case dz < 0: + return cube.FaceNorth + case dy > 0: + return cube.FaceUp + case dy < 0: + return cube.FaceDown + default: + return cube.FaceUp + } +} + +func redstoneGraphID(nodes []redstoneNode, edges []redstoneEdge) uint64 { + if len(nodes) == 0 { + return 0 + } + h := fnv.New64a() + var buf [32]byte + for _, node := range nodes { + binary.LittleEndian.PutUint64(buf[0:], uint64(node.pos[0])) + binary.LittleEndian.PutUint64(buf[8:], uint64(node.pos[1])) + binary.LittleEndian.PutUint64(buf[16:], uint64(node.pos[2])) + binary.LittleEndian.PutUint64(buf[24:], uint64(boolInt(node.source)<<1|boolInt(node.sink))) + _, _ = h.Write(buf[:]) + } + for _, edge := range edges { + binary.LittleEndian.PutUint64(buf[0:], uint64(edge.from)) + binary.LittleEndian.PutUint64(buf[8:], uint64(edge.to)) + binary.LittleEndian.PutUint64(buf[16:], uint64(edge.weight)) + _, _ = h.Write(buf[:24]) + } + return h.Sum64() +} + +func classifyRedstoneBlock(b Block) (source, consumer, action, relayer bool) { + _, source = b.(RedstonePowerSource) + _, consumer = b.(RedstonePowerConsumer) + if !consumer { + _, consumer = b.(RedstonePowerTransitionConsumer) + } + _, action = b.(RedstonePowerAction) + _, relayer = b.(RedstonePowerRelayer) + return +} + +func isRedstoneRelevant(b Block) bool { + source, consumer, action, relayer := classifyRedstoneBlock(b) + return source || consumer || action || relayer +} + +func compareBlockPos(a, b cube.Pos) int { + if a[1] != b[1] { + return a[1] - b[1] + } + if a[2] != b[2] { + return a[2] - b[2] + } + return a[0] - b[0] +} + +func compareRedstoneEdge(a, b redstoneEdge) int { + if a.from != b.from { + return a.from - b.from + } + if a.to != b.to { + return a.to - b.to + } + return a.weight - b.weight +} + +func clampRedstonePower(power int) int { + if power < 0 { + return 0 + } + if power > 15 { + return 15 + } + return power +} + +func boolInt(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/server/world/redstone_bench_test.go b/server/world/redstone_bench_test.go new file mode 100644 index 000000000..ae1ae5297 --- /dev/null +++ b/server/world/redstone_bench_test.go @@ -0,0 +1,50 @@ +package world + +import ( + "testing" + + "github.com/df-mc/dragonfly/server/block/cube" +) + +var redstoneGraphIDBenchmarkResult uint64 + +func BenchmarkRedstoneGraphID(b *testing.B) { + nodes := redstoneBenchmarkNodes(256) + edges := redstoneBenchmarkEdges(len(nodes)) + + b.ReportAllocs() + b.SetBytes(int64(len(nodes)*32 + len(edges)*24)) + b.ResetTimer() + + var result uint64 + for i := 0; i < b.N; i++ { + result = redstoneGraphID(nodes, edges) + } + redstoneGraphIDBenchmarkResult = result +} + +func redstoneBenchmarkNodes(n int) []redstoneNode { + nodes := make([]redstoneNode, n) + for i := range nodes { + nodes[i] = redstoneNode{ + pos: cube.Pos{i%16 - 8, i/16 - 8, (i * 7) % 16}, + power: i % 16, + source: i%3 == 0, + sink: i%5 == 0, + } + } + return nodes +} + +func redstoneBenchmarkEdges(nodes int) []redstoneEdge { + edges := make([]redstoneEdge, 0, nodes*2) + for i := 0; i < nodes; i++ { + if i+1 < nodes { + edges = append(edges, redstoneEdge{from: i, to: i + 1, weight: i%3 + 1}) + } + if i+16 < nodes { + edges = append(edges, redstoneEdge{from: i, to: i + 16, weight: i%5 + 1}) + } + } + return edges +} diff --git a/server/world/redstone_test.go b/server/world/redstone_test.go new file mode 100644 index 000000000..ccfd45480 --- /dev/null +++ b/server/world/redstone_test.go @@ -0,0 +1,248 @@ +package world + +import ( + "slices" + "testing" + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" +) + +var _ Handler = minimalRedstoneTestHandler{} + +type minimalRedstoneTestHandler struct{} + +func (minimalRedstoneTestHandler) HandleLiquidFlow(*Context, cube.Pos, cube.Pos, Liquid, Block) {} +func (minimalRedstoneTestHandler) HandleLiquidDecay(*Context, cube.Pos, Liquid, Liquid) {} +func (minimalRedstoneTestHandler) HandleLiquidHarden(*Context, cube.Pos, Block, Block, Block) {} +func (minimalRedstoneTestHandler) HandleSound(*Context, Sound, mgl64.Vec3) {} +func (minimalRedstoneTestHandler) HandleFireSpread(*Context, cube.Pos, cube.Pos) {} +func (minimalRedstoneTestHandler) HandleBlockBurn(*Context, cube.Pos) {} +func (minimalRedstoneTestHandler) HandleCropTrample(*Context, cube.Pos) {} +func (minimalRedstoneTestHandler) HandleLeavesDecay(*Context, cube.Pos) {} +func (minimalRedstoneTestHandler) HandleEntitySpawn(*Tx, Entity) {} +func (minimalRedstoneTestHandler) HandleEntityDespawn(*Tx, Entity) {} +func (minimalRedstoneTestHandler) HandleExplosion(*Context, mgl64.Vec3, *[]Entity, *[]cube.Pos, *float64, *bool) { +} +func (minimalRedstoneTestHandler) HandleRedstoneUpdate(*Context, RedstoneUpdate) {} +func (minimalRedstoneTestHandler) HandleClose(*Tx) {} + +func TestClampRedstonePower(t *testing.T) { + tests := []struct { + name string + power int + want int + }{ + {name: "negative", power: -1, want: 0}, + {name: "zero", power: 0, want: 0}, + {name: "middle", power: 8, want: 8}, + {name: "maximum", power: 15, want: 15}, + {name: "over maximum", power: 16, want: 15}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := clampRedstonePower(test.power); got != test.want { + t.Fatalf("clampRedstonePower(%d) = %d, want %d", test.power, got, test.want) + } + }) + } +} + +func TestCompareBlockPosSortOrder(t *testing.T) { + positions := []cube.Pos{ + {3, 2, 1}, + {2, 1, 2}, + {1, 1, 1}, + {0, 1, 1}, + {0, 0, 9}, + {9, 0, -1}, + } + want := []cube.Pos{ + {9, 0, -1}, + {0, 0, 9}, + {0, 1, 1}, + {1, 1, 1}, + {2, 1, 2}, + {3, 2, 1}, + } + + slices.SortFunc(positions, compareBlockPos) + if !slices.Equal(positions, want) { + t.Fatalf("sorted positions = %v, want %v", positions, want) + } + if got := compareBlockPos(cube.Pos{1, 2, 3}, cube.Pos{1, 2, 3}); got != 0 { + t.Fatalf("compareBlockPos(equal positions) = %d, want 0", got) + } +} + +func TestRedstoneRelayerNeighbourPositionsAreDeterministic(t *testing.T) { + engine := newRedstoneEngine(0) + pos := cube.Pos{0, 64, 0} + got := engine.redstoneRelayerNeighbourPositions(nil, pos, redstoneNeighbourOrderTestBlock{neighbours: []cube.Pos{ + {1, 64, 0}, + {0, 63, 0}, + {0, 64, -1}, + {-1, 64, 0}, + {0, 65, 0}, + {0, 64, 1}, + }}) + want := []cube.Pos{ + {0, 63, 0}, + {0, 64, -1}, + {-1, 64, 0}, + {1, 64, 0}, + {0, 64, 1}, + {0, 65, 0}, + } + if !slices.Equal(got, want) { + t.Fatalf("redstone relayer neighbours = %v, want %v", got, want) + } +} + +func TestRedstoneGraphID(t *testing.T) { + if got := redstoneGraphID(nil, nil); got != 0 { + t.Fatalf("redstoneGraphID(nil) = %d, want 0", got) + } + if got := redstoneGraphID([]redstoneNode{}, []redstoneEdge{{from: 0, to: 1, weight: 1}}); got != 0 { + t.Fatalf("redstoneGraphID(empty) = %d, want 0", got) + } + + nodes := []redstoneNode{ + {pos: cube.Pos{0, 0, 0}, power: 0, source: true}, + {pos: cube.Pos{1, -4, 3}, power: 7, sink: true}, + {pos: cube.Pos{-8, 12, 2}, power: 15, source: true, sink: true}, + } + edges := []redstoneEdge{ + {from: 0, to: 1, weight: 1}, + {from: 1, to: 2, weight: 2}, + } + id := redstoneGraphID(nodes, edges) + if id == 0 { + t.Fatalf("redstoneGraphID(non-empty nodes) = 0, want non-zero") + } + if got := redstoneGraphID(slices.Clone(nodes), slices.Clone(edges)); got != id { + t.Fatalf("redstoneGraphID(cloned nodes) = %d, want %d", got, id) + } + + changedPower := slices.Clone(nodes) + changedPower[1].power++ + if got := redstoneGraphID(changedPower, edges); got != id { + t.Fatalf("redstoneGraphID(nodes with changed power) = %d, want topology ID %d", got, id) + } + + movedNode := slices.Clone(nodes) + movedNode[2].pos[0]++ + if got := redstoneGraphID(movedNode, edges); got == id { + t.Fatalf("redstoneGraphID(nodes with moved position) = %d, want value different from %d", got, id) + } + + changedEdge := slices.Clone(edges) + changedEdge[0].weight++ + if got := redstoneGraphID(nodes, changedEdge); got == id { + t.Fatalf("redstoneGraphID(nodes with changed edge) = %d, want value different from %d", got, id) + } +} + +func TestRedstoneEngineInvalidateAround(t *testing.T) { + var nilEngine *redstoneEngine + nilEngine.invalidateAround(cube.Pos{0, 0, 0}, cube.Pos{0, 0, 0}, RedstoneUpdateCauseBlockUpdate, cube.Range{0, 0}) + + engine := newRedstoneEngine(42) + pos, changed := cube.Pos{8, 0, 8}, cube.Pos{9, 0, 8} + engine.invalidateAround(pos, changed, RedstoneUpdateCauseBlockUpdate, cube.Range{0, 1}) + + want := map[cube.Pos]redstoneDirty{ + {8, 0, 8}: redstoneDirty{changed: changed, cause: RedstoneUpdateCauseBlockUpdate}, + {9, 0, 8}: redstoneDirty{changed: changed, cause: RedstoneUpdateCauseBlockUpdate}, + {7, 0, 8}: redstoneDirty{changed: changed, cause: RedstoneUpdateCauseBlockUpdate}, + {8, 1, 8}: redstoneDirty{changed: changed, cause: RedstoneUpdateCauseBlockUpdate}, + {8, 0, 9}: redstoneDirty{changed: changed, cause: RedstoneUpdateCauseBlockUpdate}, + {8, 0, 7}: redstoneDirty{changed: changed, cause: RedstoneUpdateCauseBlockUpdate}, + } + if len(engine.dirty) != len(want) { + t.Fatalf("dirty positions = %v, want %v", engine.dirty, want) + } + for pos, dirty := range want { + if got, ok := engine.dirty[pos]; !ok || got != dirty { + t.Fatalf("dirty[%v] = %v, %t; want %v, true", pos, got, ok, dirty) + } + } + + engine.invalidateAround(cube.Pos{0, -1, 0}, changed, RedstoneUpdateCauseScheduledTick, cube.Range{0, 1}) + if len(engine.dirty) != len(want) { + t.Fatalf("out-of-bounds invalidation changed dirty positions to %v, want %v", engine.dirty, want) + } +} + +func TestScheduledTickQueueReschedulesEarlierTick(t *testing.T) { + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + + queue.schedule(DefaultBlockRegistry, pos, b, time.Second) + queue.schedule(DefaultBlockRegistry, pos, b, time.Second/10) + queue.schedule(DefaultBlockRegistry, pos, b, time.Second*2) + + index := scheduledTickIndex{pos: pos, hash: BlockHash(b)} + if got, want := queue.scheduledTicks[index], int64(102); got != want { + t.Fatalf("scheduled tick = %d, want %d", got, want) + } + ticks := queue.fromChunk(chunkPosFromBlockPos(pos)) + if len(ticks) != 1 { + t.Fatalf("active ticks = %v, want one tick", ticks) + } + if got, want := ticks[0].t, int64(102); got != want { + t.Fatalf("fromChunk tick = %d, want %d", got, want) + } +} + +func TestScheduledTickQueueRemoveChunkClearsSchedule(t *testing.T) { + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + + queue.schedule(DefaultBlockRegistry, pos, b, time.Second) + queue.removeChunk(chunkPosFromBlockPos(pos)) + + if len(queue.ticks) != 0 { + t.Fatalf("ticks after removeChunk = %v, want empty", queue.ticks) + } + if len(queue.scheduledTicks) != 0 { + t.Fatalf("scheduled ticks after removeChunk = %v, want empty", queue.scheduledTicks) + } +} + +func TestScheduledTickQueueCanRescheduleWhileCurrentTickIsDue(t *testing.T) { + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + index := scheduledTickIndex{pos: pos, hash: BlockHash(b)} + queue.scheduledTicks[index] = 100 + + queue.schedule(DefaultBlockRegistry, pos, b, time.Second/2) + if got, want := queue.scheduledTicks[index], int64(110); got != want { + t.Fatalf("rescheduled tick = %d, want %d", got, want) + } +} + +type scheduledTickTestBlock struct{} + +func (scheduledTickTestBlock) EncodeBlock() (string, map[string]any) { + return "test:scheduled_tick", nil +} +func (scheduledTickTestBlock) Hash() (uint64, uint64) { return 1 << 40, 0 } +func (scheduledTickTestBlock) Model() BlockModel { return nil } + +type redstoneNeighbourOrderTestBlock struct { + neighbours []cube.Pos +} + +func (b redstoneNeighbourOrderTestBlock) RedstoneRelayerNeighbours(cube.Pos, *Tx) []cube.Pos { + return slices.Clone(b.neighbours) +} +func (redstoneNeighbourOrderTestBlock) EncodeBlock() (string, map[string]any) { + return "test:redstone_neighbour_order", nil +} +func (redstoneNeighbourOrderTestBlock) Hash() (uint64, uint64) { return 1 << 41, 0 } +func (redstoneNeighbourOrderTestBlock) Model() BlockModel { return nil } diff --git a/server/world/sound/block.go b/server/world/sound/block.go index fc62f59ab..15aebeae3 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -120,6 +120,18 @@ type DoorCrash struct{ sound } // Click is a clicking sound. type Click struct{ sound } +// PistonIn is played when a piston retracts. +type PistonIn struct{ sound } + +// PistonOut is played when a piston extends. +type PistonOut struct{ sound } + +// PressurePlateClickOn is played when a pressure plate starts emitting power. +type PressurePlateClickOn struct{ sound } + +// PressurePlateClickOff is played when a pressure plate stops emitting power. +type PressurePlateClickOff struct{ sound } + // Ignite is a sound played when using a flint & steel. type Ignite struct{ sound } diff --git a/server/world/tick.go b/server/world/tick.go index 1088a1156..67b89b2cc 100644 --- a/server/world/tick.go +++ b/server/world/tick.go @@ -92,6 +92,7 @@ func (t ticker) tick(tx *Tx) { w.scheduledUpdates.tick(tx, tick) t.tickBlocksRandomly(tx, loaders, tick) t.performNeighbourUpdates(tx) + w.redstone.tick(tx, tick) } // performNeighbourUpdates performs all block updates that came as a result of a neighbouring block being changed. @@ -268,9 +269,9 @@ func (g *randUint4) uint4(r *rand.Rand) uint8 { // scheduledTickQueue implements a queue for scheduled block updates. Scheduled // block updates are both position and block type specific. type scheduledTickQueue struct { - ticks []scheduledTick - furthestTicks map[scheduledTickIndex]int64 - currentTick int64 + ticks []scheduledTick + scheduledTicks map[scheduledTickIndex]int64 + currentTick int64 } type scheduledTick struct { @@ -287,7 +288,7 @@ type scheduledTickIndex struct { // newScheduledTickQueue creates a queue for scheduled block ticks. func newScheduledTickQueue(tick int64) *scheduledTickQueue { - return &scheduledTickQueue{furthestTicks: make(map[scheduledTickIndex]int64), currentTick: tick} + return &scheduledTickQueue{scheduledTicks: make(map[scheduledTickIndex]int64), currentTick: tick} } // tick processes scheduled ticks, calling ScheduledTicker.ScheduledTick for any @@ -301,6 +302,10 @@ func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) { if t.t > tick { continue } + index := scheduledTickIndex{pos: t.pos, hash: t.bhash} + if scheduledTick, ok := queue.scheduledTicks[index]; !ok || scheduledTick != t.t { + continue + } b := tx.Block(t.pos) if ticker, ok := b.(ScheduledTicker); ok && w.conf.Blocks.BlockHash(b) == t.bhash { ticker.ScheduledTick(t.pos, tx, w.r) @@ -315,25 +320,21 @@ func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) { queue.ticks = slices.DeleteFunc(queue.ticks, func(t scheduledTick) bool { return t.t <= tick }) - maps.DeleteFunc(queue.furthestTicks, func(index scheduledTickIndex, t int64) bool { + maps.DeleteFunc(queue.scheduledTicks, func(index scheduledTickIndex, t int64) bool { return t <= tick }) } // schedule schedules a block update at the position passed for the block type -// passed after a specific delay. A block update is only scheduled if no block -// update with the same position and block type is already scheduled at a later -// time than the newly scheduled update. +// passed after a specific delay. A block update replaces an existing update for +// the same position and block type if it would occur sooner. func (queue *scheduledTickQueue) schedule(br BlockRegistry, pos cube.Pos, b Block, delay time.Duration) { resTick := queue.currentTick + int64(max(delay/(time.Second/20), 1)) index := scheduledTickIndex{pos: pos, hash: br.BlockHash(b)} - if t, ok := queue.furthestTicks[index]; ok && t >= resTick { - // Already have a tick scheduled for this position that will occur after - // the delay passed. Block updates can only be scheduled if they are - // after any currently scheduled updates. + if t, ok := queue.scheduledTicks[index]; ok && t <= resTick && t > queue.currentTick { return } - queue.furthestTicks[index] = resTick + queue.scheduledTicks[index] = resTick queue.ticks = append(queue.ticks, scheduledTick{pos: pos, t: resTick, b: b, bhash: index.hash}) } @@ -341,7 +342,8 @@ func (queue *scheduledTickQueue) schedule(br BlockRegistry, pos cube.Pos, b Bloc func (queue *scheduledTickQueue) fromChunk(pos ChunkPos) []scheduledTick { m := make([]scheduledTick, 0, 8) for _, t := range queue.ticks { - if pos == chunkPosFromBlockPos(t.pos) { + index := scheduledTickIndex{pos: t.pos, hash: t.bhash} + if pos == chunkPosFromBlockPos(t.pos) && queue.scheduledTicks[index] == t.t { m = append(m, t) } } @@ -353,6 +355,9 @@ func (queue *scheduledTickQueue) removeChunk(pos ChunkPos) { queue.ticks = slices.DeleteFunc(queue.ticks, func(tick scheduledTick) bool { return chunkPosFromBlockPos(tick.pos) == pos }) + maps.DeleteFunc(queue.scheduledTicks, func(index scheduledTickIndex, _ int64) bool { + return chunkPosFromBlockPos(index.pos) == pos + }) } // add adds a slice of scheduled ticks to the queue. It assumes no duplicate @@ -361,11 +366,10 @@ func (queue *scheduledTickQueue) add(ticks []scheduledTick) { queue.ticks = append(queue.ticks, ticks...) for _, t := range ticks { index := scheduledTickIndex{pos: t.pos, hash: t.bhash} - if existing, ok := queue.furthestTicks[index]; ok { - // Make sure we find the furthest tick for each of the ticks added. - // Some ticks may have the same block and position, in which case we - // need to set the furthest tick. - queue.furthestTicks[index] = max(existing, t.t) + if existing, ok := queue.scheduledTicks[index]; ok { + queue.scheduledTicks[index] = min(existing, t.t) + } else { + queue.scheduledTicks[index] = t.t } } } diff --git a/server/world/tx.go b/server/world/tx.go index 61655893a..1f037c521 100644 --- a/server/world/tx.go +++ b/server/world/tx.go @@ -8,7 +8,6 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/player/chat" - "github.com/df-mc/dragonfly/server/world/redstone" "github.com/go-gl/mathgl/mgl64" ) @@ -51,6 +50,12 @@ func (tx *Tx) Block(pos cube.Pos) Block { return tx.World().block(pos) } +// BlockLoaded returns the block at the position passed if the chunk containing it is already loaded. It returns false +// without loading or generating the chunk when the block is unavailable. +func (tx *Tx) BlockLoaded(pos cube.Pos) (Block, bool) { + return tx.World().blockLoaded(pos) +} + // Liquid attempts to return a Liquid block at the position passed. This // Liquid may be in the foreground or in any other layer. If found, the Liquid // is returned. If not, the bool returned is false. @@ -58,6 +63,12 @@ func (tx *Tx) Liquid(pos cube.Pos) (Liquid, bool) { return tx.World().liquid(pos) } +// LiquidLoaded attempts to return a Liquid block at the position passed if the chunk containing it is already loaded. +// It returns false without loading or generating the chunk when the liquid is unavailable. +func (tx *Tx) LiquidLoaded(pos cube.Pos) (Liquid, bool) { + return tx.World().liquidLoaded(pos) +} + // SetLiquid sets a Liquid at a specific position in the World. Unlike // SetBlock, SetLiquid will not necessarily overwrite any existing blocks. It // will instead be in the same position as a block currently there, unless @@ -89,6 +100,49 @@ func (tx *Tx) ScheduleBlockUpdate(pos cube.Pos, b Block, delay time.Duration) { tx.World().scheduleBlockUpdate(pos, b, delay) } +// ScheduleRedstoneUpdate schedules a redstone re-evaluation around the position passed in the next redstone phase. +func (tx *Tx) ScheduleRedstoneUpdate(pos cube.Pos) { + tx.World().redstone.invalidateAround(pos, pos, RedstoneUpdateCauseScheduledTick, tx.Range()) +} + +// RedstonePower returns the strongest redstone power currently applied to the position passed. +func (tx *Tx) RedstonePower(pos cube.Pos) int { + return tx.World().redstone.powerTo(pos, tx) +} + +// RedstoneDirectPower returns the strongest direct redstone power currently applied to the position passed, excluding +// power conducted through solid blocks. +func (tx *Tx) RedstoneDirectPower(pos cube.Pos) int { + return tx.World().redstone.directPower(pos, tx) +} + +// RedstoneStrongPower returns the strongest strong redstone power currently applied to the position passed. +func (tx *Tx) RedstoneStrongPower(pos cube.Pos) int { + return tx.World().redstone.strongPower(pos, tx) +} + +// RedstonePowerFrom returns the strongest redstone power reaching pos from the side passed. +func (tx *Tx) RedstonePowerFrom(pos cube.Pos, face cube.Face) int { + return tx.World().redstone.powerFrom(pos, tx, face, false) +} + +// RedstoneDirectPowerFrom returns the strongest direct redstone power reaching pos from the side passed. +func (tx *Tx) RedstoneDirectPowerFrom(pos cube.Pos, face cube.Face) int { + return tx.World().redstone.directPowerFrom(pos, tx, face) +} + +// RedstoneStrongPowerFrom returns the strongest strong redstone power reaching pos from the side passed. +func (tx *Tx) RedstoneStrongPowerFrom(pos cube.Pos, face cube.Face) int { + return tx.World().redstone.strongPowerFrom(pos, tx, face) +} + +// RedstoneStoredPowerFrom returns the strongest redstone power reaching pos from the side passed, allowing stored +// relayer output to contribute. This is used by ticked analog components such as comparators that sample the previous +// wire state for feedback loops. +func (tx *Tx) RedstoneStoredPowerFrom(pos cube.Pos, face cube.Face) int { + return tx.World().redstone.powerFrom(pos, tx, face, true) +} + // HighestLightBlocker gets the Y value of the highest fully light blocking // block at the x and z values passed in the World. func (tx *Tx) HighestLightBlocker(x, z int) int { @@ -278,44 +332,6 @@ func (tx *Tx) BroadcastSleepingReminder(sleeper Sleeper) { } } -// RedstonePower returns the redstone power emitted by the block at pos toward a neighbouring receiver. -// The face argument is relative to the receiving block. -func (tx *Tx) RedstonePower(pos cube.Pos, face cube.Face, accountForDust bool) (power int) { - b := tx.Block(pos) - if c, ok := b.(Conductor); ok { - return c.WeakPower(pos, face, tx, accountForDust) - } - // The wiki states that in the future some blocks may be transparent but still relay redstone. - // If a block implements RedstonePowerRelayer, it should always be prioritised over lightDiffuser. - if r, ok := b.(RedstonePowerRelayer); ok { - if !r.RelaysRedstonePowerThrough() { - return 0 - } - } else if d, ok := b.(lightDiffuser); ok && d.LightDiffusionLevel() != 15 { - return 0 - } - for _, f := range cube.Faces() { - if !b.Model().FaceSolid(pos, f, tx) { - return 0 - } - } - for _, f := range cube.Faces() { - c, ok := tx.Block(pos.Side(f)).(Conductor) - if !ok { - continue - } - sourcePos := pos.Side(f) - power = max(power, c.StrongPower(sourcePos, f, tx, accountForDust)) - if !accountForDust { - continue - } - if weakBlockPowerer, ok := c.(WeakBlockPowerer); ok && weakBlockPowerer.WeaklyPowersBlocks() { - power = max(power, c.WeakPower(sourcePos, f, tx, accountForDust)) - } - } - return power -} - // World returns the World of the Tx. It panics if the transaction was already // marked complete. func (tx *Tx) World() *World { @@ -333,11 +349,6 @@ func (tx *Tx) CurrentTick() int64 { return w.set.CurrentTick } -// Redstone returns the transient redstone runtime state owned by the transaction's world. -func (tx *Tx) Redstone() *redstone.State { - return &tx.World().redstone -} - // close finishes the Tx, causing any following call on the Tx to panic. func (tx *Tx) close() { tx.closed = true diff --git a/server/world/world.go b/server/world/world.go index 82290e434..08d886bad 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -16,7 +16,6 @@ import ( "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/internal/sliceutil" "github.com/df-mc/dragonfly/server/world/chunk" - "github.com/df-mc/dragonfly/server/world/redstone" "github.com/df-mc/goleveldb/leveldb" "github.com/go-gl/mathgl/mgl64" "github.com/google/uuid" @@ -65,10 +64,9 @@ type World struct { // tick value passed, the block update will be performed and the entry will // be removed from the map. scheduledUpdates *scheduledTickQueue + redstone *redstoneEngine neighbourUpdates []neighbourUpdate - redstone redstone.State - viewerMu sync.Mutex viewers map[*Loader]Viewer } @@ -160,6 +158,18 @@ func (w *World) block(pos cube.Pos) Block { return w.blockInChunk(w.chunk(chunkPosFromBlockPos(pos)), pos) } +// blockLoaded reads a block from a position only if its chunk is already loaded. +func (w *World) blockLoaded(pos cube.Pos) (Block, bool) { + if pos.OutOfBounds(w.ra) { + return w.conf.Blocks.Air(), false + } + c, ok := w.chunks[chunkPosFromBlockPos(pos)] + if !ok { + return w.conf.Blocks.Air(), false + } + return w.blockInChunk(c, pos), true +} + // blockInChunk reads a block from a chunk at the position passed. The block // is assumed to be within the chunk passed. func (w *World) blockInChunk(c *Column, pos cube.Pos) Block { @@ -241,6 +251,10 @@ type SetOpts struct { // performance is very important, or where it is known no liquid can be // present anyway. DisableLiquidDisplacement bool + // DisableRedstoneUpdates makes SetBlock not invalidate the redstone engine + // around the changed block. This is used by the redstone engine while + // applying its own block-state updates to avoid duplicate same-tick work. + DisableRedstoneUpdates bool } // setBlock writes a block to the position passed. If a chunk is not yet loaded @@ -268,12 +282,19 @@ func (w *World) setBlock(pos cube.Pos, b Block, opts *SetOpts) { x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2]) c := w.chunk(chunkPosFromBlockPos(pos)) + oldRID := c.Block(x, y, z, 0) + oldBlock := w.conf.Blocks.BlockByRuntimeIDOrAir(oldRID) + if w.conf.Blocks.NBTBlock(oldRID) { + if blockEntity, ok := c.BlockEntities[pos]; ok { + oldBlock = blockEntity + } + } rid := w.conf.Blocks.BlockRuntimeID(b) var before uint32 if rid != w.conf.Blocks.AirRuntimeID() && !opts.DisableLiquidDisplacement { - before = c.Block(x, y, z, 0) + before = oldRID } c.modified = true @@ -317,6 +338,10 @@ func (w *World) setBlock(pos cube.Pos, b Block, opts *SetOpts) { } } + if isRedstoneRelevant(oldBlock) || isRedstoneRelevant(b) { + w.redstone.forget(pos) + } + for _, viewer := range viewers { viewer.ViewBlockUpdate(pos, b, 0) } @@ -324,6 +349,9 @@ func (w *World) setBlock(pos cube.Pos, b Block, opts *SetOpts) { if !opts.DisableBlockUpdates { w.doBlockUpdatesAround(pos) } + if !opts.DisableRedstoneUpdates { + w.redstone.invalidateAround(pos, pos, RedstoneUpdateCauseBlockUpdate, w.Range()) + } } // setBiome sets the Biome at the position passed. If a chunk is not yet loaded @@ -456,6 +484,31 @@ func (w *World) liquid(pos cube.Pos) (Liquid, bool) { return liq, ok } +// liquidLoaded attempts to return a Liquid block at the position passed only if the chunk containing the position is +// already loaded. It returns false without loading or generating the chunk when the liquid is unavailable. +func (w *World) liquidLoaded(pos cube.Pos) (Liquid, bool) { + if pos.OutOfBounds(w.Range()) { + return nil, false + } + c, ok := w.chunks[chunkPosFromBlockPos(pos)] + if !ok { + return nil, false + } + x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2]) + for _, layer := range []uint8{0, 1} { + id := c.Block(x, y, z, layer) + b, ok := BlockByRuntimeID(id) + if !ok { + w.conf.Log.Error("liquidLoaded: no block with runtime ID", "ID", id) + return nil, false + } + if liq, ok := b.(Liquid); ok { + return liq, true + } + } + return nil, false +} + // setLiquid sets a Liquid at a specific position in the World. Unlike // setBlock, setLiquid will not necessarily overwrite any existing blocks. It // will instead be in the same position as a block currently there, unless @@ -472,6 +525,7 @@ func (w *World) setLiquid(pos cube.Pos, b Liquid) { if b == nil { w.removeLiquids(c, pos) w.doBlockUpdatesAround(pos) + w.redstone.invalidateAround(pos, pos, RedstoneUpdateCauseBlockUpdate, w.Range()) return } x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2]) @@ -495,6 +549,7 @@ func (w *World) setLiquid(pos cube.Pos, b Liquid) { c.modified = true w.doBlockUpdatesAround(pos) + w.redstone.invalidateAround(pos, pos, RedstoneUpdateCauseBlockUpdate, w.Range()) } // removeLiquids removes any liquid blocks that may be present at a specific @@ -1055,6 +1110,7 @@ func (w *World) saveChunk(_ *Tx, pos ChunkPos, c *Column) { func (w *World) closeChunk(tx *Tx, pos ChunkPos, c *Column) { w.saveChunk(tx, pos, c) w.scheduledUpdates.removeChunk(pos) + w.redstone.removeChunk(pos) // Note: We close c.Entities here because some entities may remove // themselves from the world in their Close method, which can lead to // unexpected conditions. From e84205ee74b9ec0a90284fa944c06678cdc789cc Mon Sep 17 00:00:00 2001 From: cqdetdev <101936396+cqdetdev@users.noreply.github.com> Date: Sun, 10 May 2026 19:38:54 -0400 Subject: [PATCH 2/4] fix: prepare redstone core for review --- server/block/redstone.go | 31 ++--- server/block/redstone_test.go | 114 +++++++++++++++++- server/world/handler.go | 8 +- server/world/redstone.go | 154 ++++++++++++++++++------ server/world/redstone/state.go | 165 -------------------------- server/world/redstone_test.go | 211 +++++++++++++++++++++++++++++++-- server/world/tick.go | 34 +++--- server/world/world.go | 2 +- server/world/world_test.go | 60 ++++++++++ 9 files changed, 526 insertions(+), 253 deletions(-) delete mode 100644 server/world/redstone/state.go create mode 100644 server/world/world_test.go diff --git a/server/block/redstone.go b/server/block/redstone.go index 5e329581e..68f12aaa2 100644 --- a/server/block/redstone.go +++ b/server/block/redstone.go @@ -22,9 +22,10 @@ func (RedstoneBlock) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { return 15 } -// RedstoneStrongPower always returns maximum strong power. +// RedstoneStrongPower returns no strong power. Redstone blocks power adjacent components directly, but do not power +// adjacent opaque blocks. func (RedstoneBlock) RedstoneStrongPower(cube.Pos, *world.Tx, cube.Face) int { - return 15 + return 0 } // EncodeItem ... @@ -79,10 +80,10 @@ func (RedstoneWire) RedstoneRelayerNeighbours(pos cube.Pos, tx *world.Tx) []cube neighbours = append(neighbours, side) above := pos.Side(cube.FaceUp) - if !redstoneWireBlocksConnection(tx, above, cube.FaceDown) && redstoneWireSupported(tx, side.Side(cube.FaceUp)) { + if !redstoneWireBlocksConnectionLoaded(tx, above, cube.FaceDown) && redstoneWireSupportedLoaded(tx, side.Side(cube.FaceUp)) { neighbours = append(neighbours, side.Side(cube.FaceUp)) } - if !redstoneWireBlocksConnection(tx, side, cube.FaceUp) { + if !redstoneWireBlocksConnectionLoaded(tx, side, cube.FaceUp) { down := side.Side(cube.FaceDown) if !down.OutOfBounds(tx.Range()) { neighbours = append(neighbours, down) @@ -149,21 +150,21 @@ func redstoneWireSupported(tx *world.Tx, pos cube.Pos) bool { return tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) } -func redstoneWireBlocksConnection(tx *world.Tx, pos cube.Pos, face cube.Face) bool { - if pos.OutOfBounds(tx.Range()) { - return true +func redstoneWireSupportedLoaded(tx *world.Tx, pos cube.Pos) bool { + below := pos.Side(cube.FaceDown) + if below.OutOfBounds(tx.Range()) { + return false } - return tx.Block(pos).Model().FaceSolid(pos, face, tx) + b, ok := tx.BlockLoaded(below) + return ok && b.Model().FaceSolid(below, cube.FaceUp, tx) } -func redstoneOpenableTransition(open bool, oldPower, newPower int) (bool, bool) { - if newPower > 0 { - return true, !open - } - if oldPower > 0 { - return false, open +func redstoneWireBlocksConnectionLoaded(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + if pos.OutOfBounds(tx.Range()) { + return true } - return open, false + b, ok := tx.BlockLoaded(pos) + return ok && b.Model().FaceSolid(pos, face, tx) } // RedstoneLamp is a lamp that lights while powered. diff --git a/server/block/redstone_test.go b/server/block/redstone_test.go index b9991c1ac..97d367ef1 100644 --- a/server/block/redstone_test.go +++ b/server/block/redstone_test.go @@ -13,6 +13,35 @@ func TestRedstoneBlockPower(t *testing.T) { if power := (RedstoneBlock{}).RedstonePower(cube.Pos{}, nil, face); power != 15 { t.Fatalf("RedstoneBlock power from %v = %d, want 15", face, power) } + if power := (RedstoneBlock{}).RedstoneStrongPower(cube.Pos{}, nil, face); power != 0 { + t.Fatalf("RedstoneBlock strong power from %v = %d, want 0", face, power) + } + } + + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + source := cube.Pos{0, 1, 0} + target := source.Side(cube.FaceEast) + tx.SetBlock(source, RedstoneBlock{}, nil) + if power := tx.RedstonePower(target); power != 15 { + err = fmt.Errorf("RedstoneBlock weak power = %d, want 15", power) + return + } + if power := tx.RedstoneDirectPower(target); power != 15 { + err = fmt.Errorf("RedstoneBlock direct power = %d, want 15", power) + return + } + if power := tx.RedstoneStrongPower(target); power != 0 { + err = fmt.Errorf("RedstoneBlock strong power = %d, want 0", power) + } + }) + if err != nil { + t.Fatal(err) } } @@ -95,6 +124,33 @@ func TestRedstoneWireConnectsUpBlocks(t *testing.T) { } } +func TestRedstoneWireRelayerNeighboursDoNotLoadAdjacentChunks(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{15, 1, 0} + unloadedNeighbour := pos.Side(cube.FaceEast) + tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(pos, RedstoneWire{}, nil) + + if _, ok := tx.BlockLoaded(unloadedNeighbour); ok { + err = fmt.Errorf("adjacent chunk was loaded before relayer neighbour lookup") + return + } + _ = (RedstoneWire{}).RedstoneRelayerNeighbours(pos, tx) + if _, ok := tx.BlockLoaded(unloadedNeighbour); ok { + err = fmt.Errorf("relayer neighbour lookup loaded adjacent chunk") + } + }) + if err != nil { + t.Fatal(err) + } +} + func TestStrongPowerConductsThroughSolidBlocks(t *testing.T) { w := world.New() defer func() { @@ -103,10 +159,11 @@ func TestStrongPowerConductsThroughSolidBlocks(t *testing.T) { var err error <-w.Exec(func(tx *world.Tx) { - conductor := cube.Pos{0, 1, 0} + source := cube.Pos{0, 1, 0} + conductor := source.Side(cube.FaceEast) target := conductor.Side(cube.FaceEast) + tx.SetBlock(source, Lever{Facing: cube.FaceWest, Powered: true}, nil) tx.SetBlock(conductor, Stone{}, nil) - tx.SetBlock(conductor.Side(cube.FaceWest), RedstoneBlock{}, nil) if power := tx.RedstonePower(target); power != 15 { err = fmt.Errorf("conducted strong power = %d, want 15", power) @@ -117,6 +174,59 @@ func TestStrongPowerConductsThroughSolidBlocks(t *testing.T) { } } +func TestStrongPowerDoesNotConductThroughTransparentFullBlocks(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + source := cube.Pos{0, 1, 0} + conductor := source.Side(cube.FaceEast) + target := conductor.Side(cube.FaceEast) + tx.SetBlock(source, Lever{Facing: cube.FaceWest, Powered: true}, nil) + tx.SetBlock(conductor, Glass{}, nil) + + if power := tx.RedstonePower(target); power != 0 { + err = fmt.Errorf("power conducted through glass = %d, want 0", power) + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestRedstoneBlockDoesNotPowerLampThroughSolidBlock(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + source := cube.Pos{0, 1, 0} + conductor := source.Side(cube.FaceEast) + lampPos := conductor.Side(cube.FaceEast) + tx.SetBlock(source, RedstoneBlock{}, nil) + tx.SetBlock(conductor, Stone{}, nil) + tx.SetBlock(lampPos, RedstoneLamp{}, nil) + + power := tx.RedstonePower(lampPos) + if power != 0 { + err = fmt.Errorf("lamp power through opaque block = %d, want 0", power) + return + } + after, changed := (RedstoneLamp{}).RedstonePowerUpdate(lampPos, tx, power) + if changed || after.(RedstoneLamp).Lit { + err = fmt.Errorf("lamp lit from redstone block through opaque block") + } + }) + if err != nil { + t.Fatal(err) + } +} + func TestRedstoneLampPowerUpdate(t *testing.T) { after, changed := (RedstoneLamp{}).RedstonePowerUpdate(cube.Pos{}, nil, 15) if !changed { diff --git a/server/world/handler.go b/server/world/handler.go index de194d4c5..80a8f4c68 100644 --- a/server/world/handler.go +++ b/server/world/handler.go @@ -8,8 +8,8 @@ import ( type Context = event.Context[*Tx] -// RedstoneHandler may be implemented by world Handlers to receive redstone updates. It is intentionally separate -// from Handler so existing handlers remain source-compatible. +// RedstoneHandler may be implemented by world Handlers to receive detailed redstone updates. It is intentionally +// separate from Handler so existing handlers remain source-compatible. type RedstoneHandler interface { // HandleRedstoneUpdate handles a redstone update proposed by the World redstone engine. ctx.Cancel() may be // called to suppress the proposed redstone mutation and any propagation from that mutation. @@ -71,8 +71,6 @@ type Handler interface { // The affected entities, affected blocks, item drop chance, and whether the // explosion spawns fire may be altered. HandleExplosion(ctx *Context, position mgl64.Vec3, entities *[]Entity, blocks *[]cube.Pos, itemDropChance *float64, spawnFire *bool) - // HandleRedstoneUpdate handles a redstone update. ctx.Cancel() may be called to cancel the proposed update. - HandleRedstoneUpdate(ctx *Context, update RedstoneUpdate) // HandleClose handles the World being closed. HandleClose may be used as a // moment to finish code running on other goroutines that operates on the // World specifically. HandleClose is called directly before the World stops @@ -88,7 +86,7 @@ var _ Handler = (*NopHandler)(nil) // Users may embed NopHandler to avoid having to implement each method. type NopHandler struct{} -func (NopHandler) HandleRedstoneUpdate(*Context, RedstoneUpdate) {} +func (NopHandler) HandleRedstoneUpdate(*Context, cube.Pos) {} func (NopHandler) HandleLiquidFlow(*Context, cube.Pos, cube.Pos, Liquid, Block) {} func (NopHandler) HandleLiquidDecay(*Context, cube.Pos, Liquid, Liquid) {} func (NopHandler) HandleLiquidHarden(*Context, cube.Pos, Block, Block, Block) {} diff --git a/server/world/redstone.go b/server/world/redstone.go index 5b08fe1ca..ba1fda5b0 100644 --- a/server/world/redstone.go +++ b/server/world/redstone.go @@ -107,11 +107,12 @@ type RedstoneComparatorReadable interface { } type redstoneEngine struct { - currentTick int64 - dirty map[cube.Pos]redstoneDirty - power map[cube.Pos]int - output map[cube.Pos]int - evaluating map[cube.Pos]struct{} + currentTick int64 + dirty map[cube.Pos]redstoneDirty + power map[cube.Pos]int + output map[cube.Pos]int + evaluating map[cube.Pos]struct{} + suppressedSources map[cube.Pos]int } type redstoneDirty struct { @@ -203,6 +204,13 @@ func (e *redstoneEngine) tick(tx *Tx, tick int64) { slices.SortFunc(candidates, compareBlockPos) graph := e.compile(tx, candidates) + cancelledSources, checkedSources := e.updateGraphSources(tx, graph, dirty) + previousSuppressed := e.suppressedSources + e.suppressedSources = cancelledSources + defer func() { + e.suppressedSources = previousSuppressed + }() + powers := e.graphPower(tx, graph) for i, node := range graph.nodes { d := dirty[node.pos] @@ -211,6 +219,9 @@ func (e *redstoneEngine) tick(tx *Tx, tick int64) { } } for _, node := range graph.nodes { + if _, ok := checkedSources[node.pos]; ok { + continue + } d := dirty[node.pos] if node.source { e.updateSource(tx, node.pos, d.changed, d.cause, graph.id) @@ -277,36 +288,50 @@ func (e *redstoneEngine) update(tx *Tx, pos, changed cube.Pos, cause RedstoneUpd b := tx.Block(pos) oldPower, newPower := e.power[pos], clampRedstonePower(newPower) + action, hasAction := b.(RedstonePowerAction) + actionChanged := hasAction && oldPower != newPower + if oldPower != newPower { + update := RedstoneUpdate{ + Pos: pos, + ChangedNeighbour: changed, + Before: b, + OldPower: oldPower, + NewPower: newPower, + CurrentTick: e.currentTick, + NetworkID: graphID, + Cause: cause, + } + if !e.redstoneUpdateAllowed(tx, update) { + return + } + } + after, blockChanged := b, false if consumer, ok := b.(RedstonePowerTransitionConsumer); ok { after, blockChanged = consumer.RedstonePowerTransitionUpdate(pos, tx, oldPower, newPower) } else if consumer, ok := b.(RedstonePowerConsumer); ok { after, blockChanged = consumer.RedstonePowerUpdate(pos, tx, newPower) } - action, hasAction := b.(RedstonePowerAction) - actionChanged := hasAction && oldPower != newPower if !blockChanged && !actionChanged { e.power[pos] = newPower return } - update := RedstoneUpdate{ - Pos: pos, - ChangedNeighbour: changed, - Before: b, - After: after, - OldPower: oldPower, - NewPower: newPower, - CurrentTick: e.currentTick, - NetworkID: graphID, - Cause: cause, - } - ctx := event.C(tx) - if handler, ok := tx.World().Handler().(RedstoneHandler); ok { - handler.HandleRedstoneUpdate(ctx, update) - } - if ctx.Cancelled() { - return + if oldPower == newPower { + update := RedstoneUpdate{ + Pos: pos, + ChangedNeighbour: changed, + Before: b, + After: after, + OldPower: oldPower, + NewPower: newPower, + CurrentTick: e.currentTick, + NetworkID: graphID, + Cause: cause, + } + if !e.redstoneUpdateAllowed(tx, update) { + return + } } if blockChanged { tx.SetBlock(pos, after, &SetOpts{DisableRedstoneUpdates: true}) @@ -331,11 +356,41 @@ func (e *redstoneEngine) update(tx *Tx, pos, changed cube.Pos, cause RedstoneUpd } } -func (e *redstoneEngine) updateSource(tx *Tx, pos, changed cube.Pos, cause RedstoneUpdateCause, graphID uint64) { +func (e *redstoneEngine) updateGraphSources(tx *Tx, graph redstoneGraph, dirty map[cube.Pos]redstoneDirty) (map[cube.Pos]int, map[cube.Pos]struct{}) { + var cancelled map[cube.Pos]int + var checked map[cube.Pos]struct{} + for _, node := range graph.nodes { + if !node.source { + continue + } + b, ok := tx.World().blockLoaded(node.pos) + if !ok { + continue + } + if _, ok := b.(RedstonePowerRelayer); ok { + continue + } + if checked == nil { + checked = make(map[cube.Pos]struct{}) + } + checked[node.pos] = struct{}{} + + d := dirty[node.pos] + if !e.updateSource(tx, node.pos, d.changed, d.cause, graph.id) { + if cancelled == nil { + cancelled = make(map[cube.Pos]int) + } + cancelled[node.pos] = e.output[node.pos] + } + } + return cancelled, checked +} + +func (e *redstoneEngine) updateSource(tx *Tx, pos, changed cube.Pos, cause RedstoneUpdateCause, graphID uint64) bool { b := tx.Block(pos) oldPower, newPower := e.output[pos], e.sourcePower(pos, tx) if oldPower == newPower { - return + return true } update := RedstoneUpdate{ Pos: pos, @@ -347,15 +402,12 @@ func (e *redstoneEngine) updateSource(tx *Tx, pos, changed cube.Pos, cause Redst NetworkID: graphID, Cause: cause, } - ctx := event.C(tx) - if handler, ok := tx.World().Handler().(RedstoneHandler); ok { - handler.HandleRedstoneUpdate(ctx, update) - } - if ctx.Cancelled() { - return + if !e.redstoneUpdateAllowed(tx, update) { + return false } e.output[pos] = newPower e.invalidateAround(pos, pos, RedstoneUpdateCauseBlockUpdate, tx.Range()) + return true } func (e *redstoneEngine) directPower(pos cube.Pos, tx *Tx) int { @@ -399,6 +451,9 @@ func (e *redstoneEngine) strongPowerFrom(pos cube.Pos, tx *Tx, face cube.Face) i return 0 } if source, ok := b.(RedstoneStrongPowerSource); ok { + if power, ok := e.suppressedSources[neighbour]; ok { + return clampRedstonePower(power) + } return clampRedstonePower(source.RedstoneStrongPower(neighbour, tx, face.Opposite())) } return 0 @@ -418,7 +473,7 @@ func (e *redstoneEngine) conductedStrongPowerFrom(pos cube.Pos, tx *Tx, face cub return 0 } conductor, ok := tx.World().blockLoaded(conductorPos) - if !ok || !conductor.Model().FaceSolid(conductorPos, face.Opposite(), tx) { + if !ok || !redstoneStrongPowerConductor(conductorPos, conductor, tx, face.Opposite()) { return 0 } power := 0 @@ -569,6 +624,9 @@ func (e *redstoneEngine) powerFrom(pos cube.Pos, tx *Tx, face cube.Face, relayer } func (e *redstoneEngine) redstonePower(source RedstonePowerSource, pos cube.Pos, tx *Tx, face cube.Face) int { + if power, ok := e.suppressedSources[pos]; ok { + return clampRedstonePower(power) + } if _, ok := e.evaluating[pos]; ok { return 0 } @@ -577,6 +635,36 @@ func (e *redstoneEngine) redstonePower(source RedstonePowerSource, pos cube.Pos, return source.RedstonePower(pos, tx, face) } +type legacyRedstoneHandler interface { + HandleRedstoneUpdate(ctx *Context, pos cube.Pos) +} + +func (e *redstoneEngine) redstoneUpdateAllowed(tx *Tx, update RedstoneUpdate) bool { + ctx := event.C(tx) + handler := tx.World().Handler() + if legacy, ok := handler.(legacyRedstoneHandler); ok { + legacy.HandleRedstoneUpdate(ctx, update.Pos) + } + if rich, ok := handler.(RedstoneHandler); ok { + rich.HandleRedstoneUpdate(ctx, update) + } + return !ctx.Cancelled() +} + +type redstoneLightDiffuser interface { + LightDiffusionLevel() uint8 +} + +func redstoneStrongPowerConductor(pos cube.Pos, b Block, tx *Tx, face cube.Face) bool { + if !b.Model().FaceSolid(pos, face, tx) { + return false + } + if diffuser, ok := b.(redstoneLightDiffuser); ok && diffuser.LightDiffusionLevel() == 0 { + return false + } + return true +} + func (e *redstoneEngine) compileEdges(tx *Tx, nodes []redstoneNode) []redstoneEdge { index := make(map[cube.Pos]int, len(nodes)) for i, node := range nodes { diff --git a/server/world/redstone/state.go b/server/world/redstone/state.go deleted file mode 100644 index 359038b23..000000000 --- a/server/world/redstone/state.go +++ /dev/null @@ -1,165 +0,0 @@ -package redstone - -import ( - "slices" - - "github.com/df-mc/dragonfly/server/block/cube" -) - -const ( - // torchBurnoutThreshold is the maximum number of state changes allowed before burnout occurs. - torchBurnoutThreshold = 8 - // torchBurnoutWindowTicks is the time window during which state changes are counted. - torchBurnoutWindowTicks = 60 -) - -// State holds transient redstone runtime state owned by a world. -type State struct { - torchBurnout map[cube.Pos]torchBurnout - activeTorchUpdates map[cube.Pos]int - updateSources []cube.Pos -} - -// torchBurnout holds the burnout state and state change history for a redstone torch. -type torchBurnout struct { - expirationTicks []int64 - burnedOut bool - // selfTriggered is set when the next scheduled toggle was caused by this torch's own outgoing propagation. - selfTriggered bool -} - -// TorchBurnoutStatus returns the current transient burnout state for a redstone torch. Expired state-change -// history is pruned before the state is returned. -func (s *State) TorchBurnoutStatus(pos cube.Pos, currentTick int64) (burnedOut, recoverable bool) { - data, ok := s.pruneTorchBurnoutData(pos, currentTick) - if !ok { - return false, false - } - return data.burnedOut, len(data.expirationTicks) < torchBurnoutThreshold -} - -// PruneTorchBurnout removes idle redstone torch burnout state once all tracked state changes have expired. -func (s *State) PruneTorchBurnout(pos cube.Pos, currentTick int64) { - data, ok := s.torchBurnout[pos] - if !ok { - return - } - remaining := data.removeExpired(currentTick) - data.selfTriggered = false - if remaining == 0 && !data.burnedOut { - s.ClearTorchBurnout(pos) - return - } - s.torchBurnout[pos] = data -} - -// RecordTorchToggle records a self-triggered redstone torch toggle and reports whether it should burn out. -func (s *State) RecordTorchToggle(pos cube.Pos, currentTick int64) (burnsOut bool) { - data, ok := s.torchBurnout[pos] - if !ok { - return false - } - data.removeExpired(currentTick) - if data.selfTriggered { - data.expirationTicks = append(data.expirationTicks, currentTick+torchBurnoutWindowTicks) - } - data.selfTriggered = false - if len(data.expirationTicks) == 0 && !data.burnedOut { - s.ClearTorchBurnout(pos) - return false - } - s.torchBurnout[pos] = data - return len(data.expirationTicks) >= torchBurnoutThreshold -} - -// BurnOutTorch marks a redstone torch as burned out until a later redstone update recovers it. -func (s *State) BurnOutTorch(pos cube.Pos) { - data := s.torchBurnoutData(pos) - data.burnedOut = true - data.selfTriggered = false - s.torchBurnout[pos] = data -} - -// ClearTorchBurnout removes transient burnout state for a redstone torch. -func (s *State) ClearTorchBurnout(pos cube.Pos) { - delete(s.torchBurnout, pos) -} - -// WithActiveTorchUpdate marks redstone propagation as originating from the redstone torch at pos for the duration of fn. -func (s *State) WithActiveTorchUpdate(pos cube.Pos, fn func()) { - if s.activeTorchUpdates == nil { - s.activeTorchUpdates = make(map[cube.Pos]int) - } - s.activeTorchUpdates[pos]++ - defer func() { - if s.activeTorchUpdates[pos] <= 1 { - delete(s.activeTorchUpdates, pos) - return - } - s.activeTorchUpdates[pos]-- - }() - fn() -} - -// WithUpdateSource marks redstone propagation as originating from pos for the duration of fn. -func (s *State) WithUpdateSource(pos cube.Pos, fn func()) { - s.updateSources = append(s.updateSources, pos) - defer func() { - s.updateSources = s.updateSources[:len(s.updateSources)-1] - }() - fn() -} - -// UpdateSource returns the position that caused the current redstone update. -func (s *State) UpdateSource() (cube.Pos, bool) { - if len(s.updateSources) == 0 { - return cube.Pos{}, false - } - return s.updateSources[len(s.updateSources)-1], true -} - -// MarkTorchSelfTriggeredIfActive marks the next scheduled tick for the redstone torch at pos as self-triggered if the -// current redstone propagation originated from that torch. -func (s *State) MarkTorchSelfTriggeredIfActive(pos cube.Pos) { - if s.activeTorchUpdates[pos] == 0 { - return - } - data := s.torchBurnoutData(pos) - data.selfTriggered = true - s.torchBurnout[pos] = data -} - -// torchBurnoutData retrieves or creates burnout tracking data for the given torch position. -func (s *State) torchBurnoutData(pos cube.Pos) torchBurnout { - if s.torchBurnout == nil { - s.torchBurnout = make(map[cube.Pos]torchBurnout) - } - data, ok := s.torchBurnout[pos] - if !ok { - data.expirationTicks = make([]int64, 0, torchBurnoutThreshold+1) - } - return data -} - -// pruneTorchBurnoutData removes expired burnout history and returns the remaining torch data if it is still relevant. -func (s *State) pruneTorchBurnoutData(pos cube.Pos, currentTick int64) (torchBurnout, bool) { - data, ok := s.torchBurnout[pos] - if !ok { - return torchBurnout{}, false - } - data.removeExpired(currentTick) - if len(data.expirationTicks) == 0 && !data.burnedOut && !data.selfTriggered { - s.ClearTorchBurnout(pos) - return torchBurnout{}, false - } - s.torchBurnout[pos] = data - return data, true -} - -// removeExpired removes expired state change entries and returns the number of entries that remain. -func (data *torchBurnout) removeExpired(currentTick int64) int { - data.expirationTicks = slices.DeleteFunc(data.expirationTicks, func(t int64) bool { - return t < currentTick - }) - return len(data.expirationTicks) -} diff --git a/server/world/redstone_test.go b/server/world/redstone_test.go index ccfd45480..84c7e52bf 100644 --- a/server/world/redstone_test.go +++ b/server/world/redstone_test.go @@ -25,8 +25,7 @@ func (minimalRedstoneTestHandler) HandleEntitySpawn(*Tx, Entity) func (minimalRedstoneTestHandler) HandleEntityDespawn(*Tx, Entity) {} func (minimalRedstoneTestHandler) HandleExplosion(*Context, mgl64.Vec3, *[]Entity, *[]cube.Pos, *float64, *bool) { } -func (minimalRedstoneTestHandler) HandleRedstoneUpdate(*Context, RedstoneUpdate) {} -func (minimalRedstoneTestHandler) HandleClose(*Tx) {} +func (minimalRedstoneTestHandler) HandleClose(*Tx) {} func TestClampRedstonePower(t *testing.T) { tests := []struct { @@ -175,18 +174,111 @@ func TestRedstoneEngineInvalidateAround(t *testing.T) { } } -func TestScheduledTickQueueReschedulesEarlierTick(t *testing.T) { +func TestRedstoneCancelledSourceDoesNotPropagate(t *testing.T) { + sourcePos, sinkPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + w.Handle(&redstoneCancellationHandler{cancel: map[cube.Pos]struct{}{sourcePos: {}}}) + var sinkPowered bool + var sourceOutput int + <-w.Exec(func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.SetBlock(sinkPos, redstoneCancellationConsumer{}, nil) + tx.World().redstone.tick(tx, 1) + + sinkPowered = tx.Block(sinkPos).(redstoneCancellationConsumer).Powered + sourceOutput = tx.World().redstone.output[sourcePos] + }) + if sinkPowered { + t.Fatalf("sink powered after cancelling source update") + } + if sourceOutput != 0 { + t.Fatalf("stored source output = %d, want 0", sourceOutput) + } +} + +func TestRedstoneCancelledConsumerDoesNotUpdate(t *testing.T) { + sourcePos, sinkPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + consumerUpdates := 0 + redstoneCancellationConsumerUpdates = &consumerUpdates + t.Cleanup(func() { + redstoneCancellationConsumerUpdates = nil + }) + w.Handle(&redstoneCancellationHandler{cancel: map[cube.Pos]struct{}{sinkPos: {}}}) + var sinkPowered bool + <-w.Exec(func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.SetBlock(sinkPos, redstoneCancellationConsumer{}, nil) + tx.World().redstone.tick(tx, 1) + + sinkPowered = tx.Block(sinkPos).(redstoneCancellationConsumer).Powered + }) + if consumerUpdates != 0 { + t.Fatalf("consumer updates = %d, want 0", consumerUpdates) + } + if sinkPowered { + t.Fatalf("sink powered after cancelling consumer update") + } +} + +func TestRedstoneCancelledActionDoesNotRun(t *testing.T) { + sourcePos, actionPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + actions := 0 + redstoneCancellationActions = &actions + t.Cleanup(func() { + redstoneCancellationActions = nil + }) + w.Handle(&redstoneCancellationHandler{cancel: map[cube.Pos]struct{}{actionPos: {}}}) + <-w.Exec(func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.SetBlock(actionPos, redstoneCancellationAction{}, nil) + tx.World().redstone.tick(tx, 1) + }) + if actions != 0 { + t.Fatalf("actions = %d, want 0", actions) + } +} + +func TestScheduledTickQueueKeepsLaterTickForSameBlock(t *testing.T) { + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + + queue.schedule(DefaultBlockRegistry, pos, b, time.Second/20) + queue.schedule(DefaultBlockRegistry, pos, b, time.Second/10) + + index := scheduledTickIndex{pos: pos, hash: DefaultBlockRegistry.BlockHash(b)} + if got, want := queue.furthestTicks[index], int64(102); got != want { + t.Fatalf("furthest tick = %d, want %d", got, want) + } + ticks := queue.fromChunk(chunkPosFromBlockPos(pos)) + if len(ticks) != 2 { + t.Fatalf("active ticks = %v, want two ticks", ticks) + } + got, want := []int64{ticks[0].t, ticks[1].t}, []int64{101, 102} + if !slices.Equal(got, want) { + t.Fatalf("fromChunk ticks = %v, want %v", got, want) + } +} + +func TestScheduledTickQueueIgnoresEarlierTickBehindLaterTick(t *testing.T) { queue := newScheduledTickQueue(100) pos := cube.Pos{8, 64, 8} b := scheduledTickTestBlock{} - queue.schedule(DefaultBlockRegistry, pos, b, time.Second) queue.schedule(DefaultBlockRegistry, pos, b, time.Second/10) - queue.schedule(DefaultBlockRegistry, pos, b, time.Second*2) + queue.schedule(DefaultBlockRegistry, pos, b, time.Second/20) - index := scheduledTickIndex{pos: pos, hash: BlockHash(b)} - if got, want := queue.scheduledTicks[index], int64(102); got != want { - t.Fatalf("scheduled tick = %d, want %d", got, want) + index := scheduledTickIndex{pos: pos, hash: DefaultBlockRegistry.BlockHash(b)} + if got, want := queue.furthestTicks[index], int64(102); got != want { + t.Fatalf("furthest tick = %d, want %d", got, want) } ticks := queue.fromChunk(chunkPosFromBlockPos(pos)) if len(ticks) != 1 { @@ -208,8 +300,8 @@ func TestScheduledTickQueueRemoveChunkClearsSchedule(t *testing.T) { if len(queue.ticks) != 0 { t.Fatalf("ticks after removeChunk = %v, want empty", queue.ticks) } - if len(queue.scheduledTicks) != 0 { - t.Fatalf("scheduled ticks after removeChunk = %v, want empty", queue.scheduledTicks) + if len(queue.furthestTicks) != 0 { + t.Fatalf("furthest ticks after removeChunk = %v, want empty", queue.furthestTicks) } } @@ -217,11 +309,11 @@ func TestScheduledTickQueueCanRescheduleWhileCurrentTickIsDue(t *testing.T) { queue := newScheduledTickQueue(100) pos := cube.Pos{8, 64, 8} b := scheduledTickTestBlock{} - index := scheduledTickIndex{pos: pos, hash: BlockHash(b)} - queue.scheduledTicks[index] = 100 + index := scheduledTickIndex{pos: pos, hash: DefaultBlockRegistry.BlockHash(b)} + queue.furthestTicks[index] = 100 queue.schedule(DefaultBlockRegistry, pos, b, time.Second/2) - if got, want := queue.scheduledTicks[index], int64(110); got != want { + if got, want := queue.furthestTicks[index], int64(110); got != want { t.Fatalf("rescheduled tick = %d, want %d", got, want) } } @@ -246,3 +338,96 @@ func (redstoneNeighbourOrderTestBlock) EncodeBlock() (string, map[string]any) { } func (redstoneNeighbourOrderTestBlock) Hash() (uint64, uint64) { return 1 << 41, 0 } func (redstoneNeighbourOrderTestBlock) Model() BlockModel { return nil } + +type redstoneCancellationHandler struct { + NopHandler + cancel map[cube.Pos]struct{} +} + +func (h *redstoneCancellationHandler) HandleRedstoneUpdate(ctx *Context, update RedstoneUpdate) { + if _, ok := h.cancel[update.Pos]; ok { + ctx.Cancel() + } +} + +var ( + redstoneCancellationConsumerUpdates *int + redstoneCancellationActions *int +) + +func redstoneCancellationTestRegistry() BlockRegistry { + registry := NewBlockRegistry() + for _, power := range []int32{0, 15} { + registry.RegisterBlockState(BlockState{Name: "test:redstone_source", Properties: map[string]any{"power": power}}) + registry.RegisterBlock(redstoneCancellationSource{Power: int(power)}) + } + for _, powered := range []bool{false, true} { + registry.RegisterBlockState(BlockState{Name: "test:redstone_consumer", Properties: map[string]any{"powered": powered}}) + registry.RegisterBlock(redstoneCancellationConsumer{Powered: powered}) + } + registry.RegisterBlockState(BlockState{Name: "test:redstone_action", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneCancellationAction{}) + return registry +} + +type redstoneCancellationSource struct { + Power int +} + +func (b redstoneCancellationSource) RedstonePower(cube.Pos, *Tx, cube.Face) int { + return b.Power +} +func (b redstoneCancellationSource) EncodeBlock() (string, map[string]any) { + return "test:redstone_source", map[string]any{"power": int32(b.Power)} +} +func (b redstoneCancellationSource) Hash() (uint64, uint64) { + return 1 << 42, uint64(b.Power) +} +func (redstoneCancellationSource) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneCancellationConsumer struct { + Powered bool +} + +func (b redstoneCancellationConsumer) RedstonePowerUpdate(_ cube.Pos, _ *Tx, power int) (Block, bool) { + if redstoneCancellationConsumerUpdates != nil { + (*redstoneCancellationConsumerUpdates)++ + } + powered := power > 0 + if b.Powered == powered { + return b, false + } + b.Powered = powered + return b, true +} +func (b redstoneCancellationConsumer) EncodeBlock() (string, map[string]any) { + return "test:redstone_consumer", map[string]any{"powered": b.Powered} +} +func (b redstoneCancellationConsumer) Hash() (uint64, uint64) { + if b.Powered { + return 1 << 43, 1 + } + return 1 << 43, 0 +} +func (redstoneCancellationConsumer) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneCancellationAction struct{} + +func (redstoneCancellationAction) RedstonePowerAction(cube.Pos, *Tx, int, int) bool { + if redstoneCancellationActions != nil { + (*redstoneCancellationActions)++ + } + return true +} +func (redstoneCancellationAction) EncodeBlock() (string, map[string]any) { + return "test:redstone_action", nil +} +func (redstoneCancellationAction) Hash() (uint64, uint64) { return 1 << 44, 0 } +func (redstoneCancellationAction) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneCancellationModel struct{} + +func (redstoneCancellationModel) BBox(cube.Pos, BlockSource) []cube.BBox { return nil } +func (redstoneCancellationModel) FaceSolid(cube.Pos, cube.Face, BlockSource) bool { + return false +} diff --git a/server/world/tick.go b/server/world/tick.go index 67b89b2cc..88a8afe63 100644 --- a/server/world/tick.go +++ b/server/world/tick.go @@ -269,9 +269,9 @@ func (g *randUint4) uint4(r *rand.Rand) uint8 { // scheduledTickQueue implements a queue for scheduled block updates. Scheduled // block updates are both position and block type specific. type scheduledTickQueue struct { - ticks []scheduledTick - scheduledTicks map[scheduledTickIndex]int64 - currentTick int64 + ticks []scheduledTick + furthestTicks map[scheduledTickIndex]int64 + currentTick int64 } type scheduledTick struct { @@ -288,7 +288,7 @@ type scheduledTickIndex struct { // newScheduledTickQueue creates a queue for scheduled block ticks. func newScheduledTickQueue(tick int64) *scheduledTickQueue { - return &scheduledTickQueue{scheduledTicks: make(map[scheduledTickIndex]int64), currentTick: tick} + return &scheduledTickQueue{furthestTicks: make(map[scheduledTickIndex]int64), currentTick: tick} } // tick processes scheduled ticks, calling ScheduledTicker.ScheduledTick for any @@ -302,10 +302,6 @@ func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) { if t.t > tick { continue } - index := scheduledTickIndex{pos: t.pos, hash: t.bhash} - if scheduledTick, ok := queue.scheduledTicks[index]; !ok || scheduledTick != t.t { - continue - } b := tx.Block(t.pos) if ticker, ok := b.(ScheduledTicker); ok && w.conf.Blocks.BlockHash(b) == t.bhash { ticker.ScheduledTick(t.pos, tx, w.r) @@ -320,21 +316,22 @@ func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) { queue.ticks = slices.DeleteFunc(queue.ticks, func(t scheduledTick) bool { return t.t <= tick }) - maps.DeleteFunc(queue.scheduledTicks, func(index scheduledTickIndex, t int64) bool { + maps.DeleteFunc(queue.furthestTicks, func(index scheduledTickIndex, t int64) bool { return t <= tick }) } // schedule schedules a block update at the position passed for the block type -// passed after a specific delay. A block update replaces an existing update for -// the same position and block type if it would occur sooner. +// passed after a specific delay. A block update is only scheduled if no block +// update with the same position and block type is already scheduled at a later +// time than the newly scheduled update. func (queue *scheduledTickQueue) schedule(br BlockRegistry, pos cube.Pos, b Block, delay time.Duration) { resTick := queue.currentTick + int64(max(delay/(time.Second/20), 1)) index := scheduledTickIndex{pos: pos, hash: br.BlockHash(b)} - if t, ok := queue.scheduledTicks[index]; ok && t <= resTick && t > queue.currentTick { + if t, ok := queue.furthestTicks[index]; ok && t >= resTick && t > queue.currentTick { return } - queue.scheduledTicks[index] = resTick + queue.furthestTicks[index] = resTick queue.ticks = append(queue.ticks, scheduledTick{pos: pos, t: resTick, b: b, bhash: index.hash}) } @@ -342,8 +339,7 @@ func (queue *scheduledTickQueue) schedule(br BlockRegistry, pos cube.Pos, b Bloc func (queue *scheduledTickQueue) fromChunk(pos ChunkPos) []scheduledTick { m := make([]scheduledTick, 0, 8) for _, t := range queue.ticks { - index := scheduledTickIndex{pos: t.pos, hash: t.bhash} - if pos == chunkPosFromBlockPos(t.pos) && queue.scheduledTicks[index] == t.t { + if pos == chunkPosFromBlockPos(t.pos) { m = append(m, t) } } @@ -355,7 +351,7 @@ func (queue *scheduledTickQueue) removeChunk(pos ChunkPos) { queue.ticks = slices.DeleteFunc(queue.ticks, func(tick scheduledTick) bool { return chunkPosFromBlockPos(tick.pos) == pos }) - maps.DeleteFunc(queue.scheduledTicks, func(index scheduledTickIndex, _ int64) bool { + maps.DeleteFunc(queue.furthestTicks, func(index scheduledTickIndex, _ int64) bool { return chunkPosFromBlockPos(index.pos) == pos }) } @@ -366,10 +362,10 @@ func (queue *scheduledTickQueue) add(ticks []scheduledTick) { queue.ticks = append(queue.ticks, ticks...) for _, t := range ticks { index := scheduledTickIndex{pos: t.pos, hash: t.bhash} - if existing, ok := queue.scheduledTicks[index]; ok { - queue.scheduledTicks[index] = min(existing, t.t) + if existing, ok := queue.furthestTicks[index]; ok { + queue.furthestTicks[index] = max(existing, t.t) } else { - queue.scheduledTicks[index] = t.t + queue.furthestTicks[index] = t.t } } } diff --git a/server/world/world.go b/server/world/world.go index 08d886bad..b04d479c6 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -497,7 +497,7 @@ func (w *World) liquidLoaded(pos cube.Pos) (Liquid, bool) { x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2]) for _, layer := range []uint8{0, 1} { id := c.Block(x, y, z, layer) - b, ok := BlockByRuntimeID(id) + b, ok := w.conf.Blocks.BlockByRuntimeID(id) if !ok { w.conf.Log.Error("liquidLoaded: no block with runtime ID", "ID", id) return nil, false diff --git a/server/world/world_test.go b/server/world/world_test.go new file mode 100644 index 000000000..cdddac6ae --- /dev/null +++ b/server/world/world_test.go @@ -0,0 +1,60 @@ +package world + +import ( + "testing" + + "github.com/df-mc/dragonfly/server/block/cube" +) + +func TestLiquidLoadedUsesWorldBlockRegistry(t *testing.T) { + br := NewBlockRegistry() + liquid := customLiquidTestBlock{} + br.RegisterBlockState(BlockState{Name: "test:liquid", Properties: map[string]any{}}) + br.RegisterBlock(liquid) + + w := Config{Blocks: br}.New() + defer func() { + if err := w.Close(); err != nil { + t.Fatalf("close world: %v", err) + } + }() + + pos := cube.Pos{0, 64, 0} + var ( + got Liquid + ok bool + ) + <-w.Exec(func(tx *Tx) { + c := tx.World().chunk(chunkPosFromBlockPos(pos)) + c.SetBlock(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0, tx.World().conf.Blocks.BlockRuntimeID(liquid)) + c.modified = true + + got, ok = tx.LiquidLoaded(pos) + }) + if !ok { + t.Fatal("LiquidLoaded returned ok=false, want true") + } + if got != liquid { + t.Fatalf("LiquidLoaded returned %#v, want %#v", got, liquid) + } +} + +type customLiquidTestBlock struct{} + +func (customLiquidTestBlock) EncodeBlock() (string, map[string]any) { + return "test:liquid", nil +} +func (customLiquidTestBlock) Hash() (uint64, uint64) { return 1 << 45, 0 } +func (customLiquidTestBlock) Model() BlockModel { return redstoneCancellationModel{} } +func (customLiquidTestBlock) LiquidDepth() int { return 0 } +func (customLiquidTestBlock) SpreadDecay() int { return 1 } +func (customLiquidTestBlock) WithDepth(int, bool) Liquid { + return customLiquidTestBlock{} +} +func (customLiquidTestBlock) LiquidFalling() bool { return false } +func (customLiquidTestBlock) BlastResistance() float64 { return 100 } +func (customLiquidTestBlock) LiquidType() string { return "test" } +func (customLiquidTestBlock) Harden(cube.Pos, *Tx, *cube.Pos) bool { + return false +} +func (customLiquidTestBlock) LiquidRemoveBlock(cube.Pos, *Tx, Block) {} From 72496eae9adf76464e062a378948accccd30c7a7 Mon Sep 17 00:00:00 2001 From: cqdetdev <101936396+cqdetdev@users.noreply.github.com> Date: Sun, 10 May 2026 20:43:19 -0400 Subject: [PATCH 3/4] fix: tighten redstone core parity --- server/block/redstone.go | 274 ++++++++++++++++++++++++-- server/block/redstone_sources.go | 6 +- server/block/redstone_sources_test.go | 24 +++ server/block/redstone_test.go | 135 +++++++++++++ server/world/redstone.go | 29 ++- server/world/redstone_test.go | 186 +++++++++++++++++ server/world/tick.go | 4 + 7 files changed, 635 insertions(+), 23 deletions(-) diff --git a/server/block/redstone.go b/server/block/redstone.go index 68f12aaa2..c873b5248 100644 --- a/server/block/redstone.go +++ b/server/block/redstone.go @@ -1,6 +1,8 @@ package block import ( + "math/rand/v2" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" @@ -58,8 +60,11 @@ func (r RedstoneWire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx return placed(ctx) } -// RedstonePower returns the wire's current signal strength. -func (r RedstoneWire) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { +// RedstonePower returns the wire's current signal strength from connected faces. +func (r RedstoneWire) RedstonePower(pos cube.Pos, tx *world.Tx, face cube.Face) int { + if tx != nil && redstoneWireFaceHorizontal(face) && !redstoneWirePowersHorizontalFace(pos, tx, face) { + return 0 + } return r.Power } @@ -72,22 +77,22 @@ func (RedstoneWire) RedstoneSignalLoss(cube.Pos, *world.Tx, cube.Face, cube.Face // down adjacent blocks. func (RedstoneWire) RedstoneRelayerNeighbours(pos cube.Pos, tx *world.Tx) []cube.Pos { neighbours := make([]cube.Pos, 0, 12) + faces := redstoneWirePoweredHorizontalFaces(pos, tx) for _, face := range cube.HorizontalFaces() { + if !faces[face] { + continue + } side := pos.Side(face) if side.OutOfBounds(tx.Range()) { continue } - neighbours = append(neighbours, side) - - above := pos.Side(cube.FaceUp) - if !redstoneWireBlocksConnectionLoaded(tx, above, cube.FaceDown) && redstoneWireSupportedLoaded(tx, side.Side(cube.FaceUp)) { - neighbours = append(neighbours, side.Side(cube.FaceUp)) + positions := redstoneWireHorizontalConnectionPositions(pos, tx, face) + if len(positions) != 0 { + neighbours = append(neighbours, positions...) + continue } - if !redstoneWireBlocksConnectionLoaded(tx, side, cube.FaceUp) { - down := side.Side(cube.FaceDown) - if !down.OutOfBounds(tx.Range()) { - neighbours = append(neighbours, down) - } + if redstoneWireRelevantLoaded(tx, side) { + neighbours = append(neighbours, side) } } return neighbours @@ -167,6 +172,136 @@ func redstoneWireBlocksConnectionLoaded(tx *world.Tx, pos cube.Pos, face cube.Fa return ok && b.Model().FaceSolid(pos, face, tx) } +func redstoneWirePowersHorizontalFace(pos cube.Pos, tx *world.Tx, face cube.Face) bool { + return redstoneWirePoweredHorizontalFaces(pos, tx)[face] +} + +func redstoneWirePoweredHorizontalFaces(pos cube.Pos, tx *world.Tx) map[cube.Face]bool { + connections := make(map[cube.Face]bool, len(cube.HorizontalFaces())) + for _, face := range cube.HorizontalFaces() { + if len(redstoneWireHorizontalConnectionPositions(pos, tx, face)) != 0 { + connections[face] = true + } + } + switch len(connections) { + case 0: + for _, face := range cube.HorizontalFaces() { + connections[face] = true + } + case 1: + for face := range connections { + connections[face.Opposite()] = true + } + } + return connections +} + +func redstoneWireHorizontalConnectionPositions(pos cube.Pos, tx *world.Tx, face cube.Face) []cube.Pos { + side := pos.Side(face) + if side.OutOfBounds(tx.Range()) { + return nil + } + positions := make([]cube.Pos, 0, 3) + if redstoneWireDirectConnectionLoaded(tx, side, face.Opposite()) { + positions = append(positions, side) + } + + above := pos.Side(cube.FaceUp) + sideAbove := side.Side(cube.FaceUp) + if !redstoneWireBlocksConnectionLoaded(tx, above, cube.FaceDown) && redstoneWireAtLoaded(tx, sideAbove) && redstoneWireSupportedLoaded(tx, sideAbove) { + positions = append(positions, sideAbove) + } + if !redstoneWireBlocksConnectionLoaded(tx, side, cube.FaceUp) { + down := side.Side(cube.FaceDown) + if !down.OutOfBounds(tx.Range()) && redstoneWireAtLoaded(tx, down) { + positions = append(positions, down) + } + } + return positions +} + +func redstoneWireDirectConnectionLoaded(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + b, ok := tx.BlockLoaded(pos) + if !ok { + return false + } + if _, ok := b.(RedstoneWire); ok { + return true + } + if _, ok := b.(world.RedstonePowerSource); ok { + return true + } + if _, ok := b.(world.RedstoneStrongPowerSource); ok { + return true + } + if _, ok := b.(world.RedstonePowerRelayer); ok { + return true + } + return redstoneWireNonSolidComponent(pos, b, tx, face) +} + +func redstoneWireNonSolidComponent(pos cube.Pos, b world.Block, tx *world.Tx, face cube.Face) bool { + model := b.Model() + if model == nil || model.FaceSolid(pos, face, tx) { + return false + } + if _, ok := b.(world.RedstonePowerConsumer); ok { + return true + } + if _, ok := b.(world.RedstonePowerTransitionConsumer); ok { + return true + } + if _, ok := b.(world.RedstonePowerAction); ok { + return true + } + return false +} + +func redstoneWireAtLoaded(tx *world.Tx, pos cube.Pos) bool { + b, ok := tx.BlockLoaded(pos) + if !ok { + return false + } + _, ok = b.(RedstoneWire) + return ok +} + +func redstoneWireRelevantLoaded(tx *world.Tx, pos cube.Pos) bool { + b, ok := tx.BlockLoaded(pos) + return ok && redstoneWireRelevant(b) +} + +func redstoneWireRelevant(b world.Block) bool { + if _, ok := b.(world.RedstonePowerSource); ok { + return true + } + if _, ok := b.(world.RedstoneStrongPowerSource); ok { + return true + } + if _, ok := b.(world.RedstonePowerRelayer); ok { + return true + } + if _, ok := b.(world.RedstonePowerConsumer); ok { + return true + } + if _, ok := b.(world.RedstonePowerTransitionConsumer); ok { + return true + } + if _, ok := b.(world.RedstonePowerAction); ok { + return true + } + return false +} + +func redstoneWireFaceHorizontal(face cube.Face) bool { + switch face { + case cube.FaceNorth, cube.FaceSouth, cube.FaceWest, cube.FaceEast: + return true + default: + return false + } +} + // RedstoneLamp is a lamp that lights while powered. type RedstoneLamp struct { solid @@ -183,16 +318,121 @@ func (r RedstoneLamp) LightEmissionLevel() uint8 { return 0 } -// RedstonePowerUpdate updates the lamp's lit state to match its redstone input. -func (r RedstoneLamp) RedstonePowerUpdate(_ cube.Pos, _ *world.Tx, power int) (world.Block, bool) { - lit := power > 0 - if r.Lit == lit { +// RedstonePowerUpdate lights the lamp immediately and schedules delayed turn-off when power is removed. +func (r RedstoneLamp) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, power int) (world.Block, bool) { + if redstoneLampPower(pos, tx, power) > 0 { + if r.Lit { + return r, false + } + r.Lit = true + return r, true + } + if !r.Lit { + return r, false + } + if tx != nil { + tx.ScheduleBlockUpdate(pos, r, redstoneTicks(2)) return r, false } - r.Lit = lit + r.Lit = false return r, true } +// ScheduledTick turns the lamp off after its delay if it was not repowered. +func (r RedstoneLamp) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + if tx == nil || !r.Lit || redstoneLampPower(pos, tx, tx.RedstonePower(pos)) > 0 { + return + } + r.Lit = false + tx.SetBlock(pos, r, nil) +} + +func redstoneLampPower(pos cube.Pos, tx *world.Tx, power int) int { + if tx == nil { + return redstonePower(power) + } + return max(redstoneLampGraphPower(pos, tx, power), tx.RedstoneDirectPower(pos), redstoneLampConductedStrongPower(pos, tx)) +} + +func redstoneLampGraphPower(pos cube.Pos, tx *world.Tx, power int) int { + power = redstonePower(power) + if power == 0 { + return 0 + } + for _, face := range cube.Faces() { + neighbour := pos.Side(face) + if neighbour.OutOfBounds(tx.Range()) { + continue + } + b, ok := tx.BlockLoaded(neighbour) + if !ok { + continue + } + if _, ok := b.(world.RedstonePowerRelayer); !ok { + continue + } + if redstoneLampRelayerConnectsTo(neighbour, pos, tx, b) { + return power + } + } + return 0 +} + +func redstoneLampRelayerConnectsTo(relayerPos, target cube.Pos, tx *world.Tx, b world.Block) bool { + neighbourer, ok := b.(world.RedstonePowerRelayerNeighbourer) + if !ok { + return true + } + for _, neighbour := range neighbourer.RedstoneRelayerNeighbours(relayerPos, tx) { + if neighbour == target { + return true + } + } + return false +} + +func redstoneLampConductedStrongPower(pos cube.Pos, tx *world.Tx) int { + power := 0 + for _, face := range cube.Faces() { + conductorPos := pos.Side(face) + if conductorPos.OutOfBounds(tx.Range()) { + continue + } + conductor, ok := tx.BlockLoaded(conductorPos) + if !ok || !redstoneLampStrongPowerConductor(conductorPos, conductor, tx, face.Opposite()) { + continue + } + power = max(power, tx.RedstoneStrongPower(conductorPos)) + } + return redstonePower(power) +} + +func redstoneLampStrongPowerConductor(pos cube.Pos, b world.Block, tx *world.Tx, face cube.Face) bool { + if !b.Model().FaceSolid(pos, face, tx) { + return false + } + if redstoneLampExplicitNonConductor(b) { + return false + } + if diffuser, ok := b.(redstoneLampLightDiffuser); ok && diffuser.LightDiffusionLevel() == 0 { + return false + } + return true +} + +func redstoneLampExplicitNonConductor(b world.Block) bool { + name, _ := b.EncodeBlock() + switch name { + case "minecraft:redstone_block", "minecraft:piston", "minecraft:sticky_piston", "minecraft:piston_arm", "minecraft:observer": + return true + } + return false +} + +type redstoneLampLightDiffuser interface { + LightDiffusionLevel() uint8 +} + // BreakInfo ... func (r RedstoneLamp) BreakInfo() BreakInfo { return newBreakInfo(0.3, alwaysHarvestable, nothingEffective, oneOf(RedstoneLamp{})) diff --git a/server/block/redstone_sources.go b/server/block/redstone_sources.go index 8f8f35522..f39146d29 100644 --- a/server/block/redstone_sources.go +++ b/server/block/redstone_sources.go @@ -440,9 +440,6 @@ func (p PressurePlate) weightedMaxEntities() int { } func (p PressurePlate) releaseDelay() time.Duration { - if p.weighted() { - return time.Second / 2 - } return time.Second } @@ -471,7 +468,8 @@ func pressurePlateEntityName(e world.Entity) string { } func pressurePlateActivationBox(pos cube.Pos) cube.BBox { - return cube.Box(float64(pos[0]), float64(pos[1]), float64(pos[2]), float64(pos[0]+1), float64(pos[1])+0.25, float64(pos[2]+1)) + const inset = 1.0 / 16.0 + return cube.Box(float64(pos[0])+inset, float64(pos[1]), float64(pos[2])+inset, float64(pos[0]+1)-inset, float64(pos[1])+0.25, float64(pos[2]+1)-inset) } func pressurePlateEntityIntersects(e world.Entity, box cube.BBox) bool { diff --git a/server/block/redstone_sources_test.go b/server/block/redstone_sources_test.go index 014e0650a..2e6057f41 100644 --- a/server/block/redstone_sources_test.go +++ b/server/block/redstone_sources_test.go @@ -3,6 +3,7 @@ package block import ( "fmt" "testing" + "time" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/item" @@ -128,6 +129,9 @@ func TestPressurePlatePower(t *testing.T) { if power := (PressurePlate{Type: redstoneSourceHeavyWeighted}).weightedPower(141); power != 15 { t.Fatalf("heavy weighted pressure plate max power = %d, want 15", power) } + if delay := (PressurePlate{Type: redstoneSourceLightWeighted}).releaseDelay(); delay != time.Second { + t.Fatalf("weighted pressure plate release delay = %v, want %v", delay, time.Second) + } } func TestPressurePlateItemActivation(t *testing.T) { @@ -172,6 +176,26 @@ func TestPressurePlateDetectsEntityBoundingBoxOnEdge(t *testing.T) { } } +func TestPressurePlateActivationBoxIsInset(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + tx.AddEntity(world.EntitySpawnOpts{Position: mgl64.Vec3{-0.25, 1.0625, 0.5}}.New(pressurePlateTestEntityType{name: "minecraft:player"}, pressurePlateTestEntityConfig{})) + + if power := (PressurePlate{Type: redstoneSourceStone}).detectPower(pos, tx); power != 0 { + err = fmt.Errorf("rim-overlapping pressure plate power = %d, want 0", power) + } + }) + if err != nil { + t.Fatal(err) + } +} + func TestWeightedPressurePlateCountsWorldEntities(t *testing.T) { w := world.New() defer func() { diff --git a/server/block/redstone_test.go b/server/block/redstone_test.go index 97d367ef1..56220ce63 100644 --- a/server/block/redstone_test.go +++ b/server/block/redstone_test.go @@ -124,6 +124,42 @@ func TestRedstoneWireConnectsUpBlocks(t *testing.T) { } } +func TestRedstoneWireRelayerNeighboursFollowShape(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + source := pos.Side(cube.FaceWest) + endLamp := pos.Side(cube.FaceEast) + sideLamp := pos.Side(cube.FaceNorth) + tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(source, RedstoneBlock{}, nil) + tx.SetBlock(pos, RedstoneWire{}, nil) + tx.SetBlock(endLamp, RedstoneLamp{}, nil) + tx.SetBlock(sideLamp, RedstoneLamp{}, nil) + + neighbours := (RedstoneWire{}).RedstoneRelayerNeighbours(pos, tx) + if !redstoneNeighbourTestContains(neighbours, source) { + err = fmt.Errorf("wire neighbours %v did not include source connection %v", neighbours, source) + return + } + if !redstoneNeighbourTestContains(neighbours, endLamp) { + err = fmt.Errorf("wire neighbours %v did not include line-end lamp %v", neighbours, endLamp) + return + } + if redstoneNeighbourTestContains(neighbours, sideLamp) { + err = fmt.Errorf("wire neighbours %v included side lamp %v", neighbours, sideLamp) + } + }) + if err != nil { + t.Fatal(err) + } +} + func TestRedstoneWireRelayerNeighboursDoNotLoadAdjacentChunks(t *testing.T) { w := world.New() defer func() { @@ -151,6 +187,38 @@ func TestRedstoneWireRelayerNeighboursDoNotLoadAdjacentChunks(t *testing.T) { } } +func TestRedstoneWirePowersEndLampButNotSideLamp(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + wire := cube.Pos{0, 1, 0} + endLamp := wire.Side(cube.FaceEast) + sideLamp := wire.Side(cube.FaceNorth) + tx.SetBlock(wire.Side(cube.FaceDown), Stone{}, nil) + tx.SetBlock(wire.Side(cube.FaceWest), RedstoneBlock{}, nil) + tx.SetBlock(wire, RedstoneWire{Power: 1}, nil) + tx.SetBlock(endLamp, RedstoneLamp{}, nil) + tx.SetBlock(sideLamp, RedstoneLamp{}, nil) + + after, changed := (RedstoneLamp{}).RedstonePowerUpdate(endLamp, tx, tx.RedstonePower(endLamp)) + if !changed || !after.(RedstoneLamp).Lit { + err = fmt.Errorf("strength-1 line dust did not light end lamp: changed=%t after=%#v", changed, after) + return + } + after, changed = (RedstoneLamp{}).RedstonePowerUpdate(sideLamp, tx, tx.RedstonePower(sideLamp)) + if changed || after.(RedstoneLamp).Lit { + err = fmt.Errorf("line dust powered side lamp: changed=%t after=%#v", changed, after) + } + }) + if err != nil { + t.Fatal(err) + } +} + func TestStrongPowerConductsThroughSolidBlocks(t *testing.T) { w := world.New() defer func() { @@ -247,3 +315,70 @@ func TestRedstoneLampPowerUpdate(t *testing.T) { t.Fatal("RedstoneLamp did not turn off") } } + +func TestRedstoneLampDelayedTurnOff(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + lamp := RedstoneLamp{Lit: true} + tx.SetBlock(pos, lamp, nil) + + after, changed := lamp.RedstonePowerUpdate(pos, tx, 0) + if changed { + err = fmt.Errorf("RedstoneLamp reported immediate off change: %#v", after) + return + } + if !tx.Block(pos).(RedstoneLamp).Lit { + err = fmt.Errorf("RedstoneLamp turned off before scheduled tick") + return + } + tx.Block(pos).(RedstoneLamp).ScheduledTick(pos, tx, nil) + if tx.Block(pos).(RedstoneLamp).Lit { + err = fmt.Errorf("RedstoneLamp stayed lit after delayed off tick") + } + }) + if err != nil { + t.Fatal(err) + } +} + +func TestRedstoneLampRepowerCancelsTurnOff(t *testing.T) { + w := world.New() + defer func() { + _ = w.Close() + }() + + var err error + <-w.Exec(func(tx *world.Tx) { + pos := cube.Pos{0, 1, 0} + lamp := RedstoneLamp{Lit: true} + tx.SetBlock(pos, lamp, nil) + _, changed := lamp.RedstonePowerUpdate(pos, tx, 0) + if changed { + err = fmt.Errorf("RedstoneLamp reported immediate off change") + return + } + tx.SetBlock(pos.Side(cube.FaceWest), RedstoneBlock{}, nil) + tx.Block(pos).(RedstoneLamp).ScheduledTick(pos, tx, nil) + if !tx.Block(pos).(RedstoneLamp).Lit { + err = fmt.Errorf("RedstoneLamp turned off after being repowered") + } + }) + if err != nil { + t.Fatal(err) + } +} + +func redstoneNeighbourTestContains(neighbours []cube.Pos, pos cube.Pos) bool { + for _, neighbour := range neighbours { + if neighbour == pos { + return true + } + } + return false +} diff --git a/server/world/redstone.go b/server/world/redstone.go index ba1fda5b0..24f55f395 100644 --- a/server/world/redstone.go +++ b/server/world/redstone.go @@ -614,7 +614,14 @@ func (e *redstoneEngine) powerFrom(pos cube.Pos, tx *Tx, face cube.Face, relayer if to == s.from { continue } - loss := s.loss + max(relayer.RedstoneSignalLoss(s.pos, tx, s.from, to), 1) + nextBlock, ok := tx.World().blockLoaded(next) + if !ok { + continue + } + loss := s.loss + if _, nextRelayer := nextBlock.(RedstonePowerRelayer); nextRelayer { + loss += max(relayer.RedstoneSignalLoss(s.pos, tx, s.from, to), 1) + } if loss <= 15 { queue = append(queue, step{pos: next, from: to.Opposite(), loss: loss, depth: s.depth + 1}) } @@ -659,12 +666,24 @@ func redstoneStrongPowerConductor(pos cube.Pos, b Block, tx *Tx, face cube.Face) if !b.Model().FaceSolid(pos, face, tx) { return false } + if redstoneExplicitNonConductor(b) { + return false + } if diffuser, ok := b.(redstoneLightDiffuser); ok && diffuser.LightDiffusionLevel() == 0 { return false } return true } +func redstoneExplicitNonConductor(b Block) bool { + name, _ := b.EncodeBlock() + switch name { + case "minecraft:redstone_block", "minecraft:piston", "minecraft:sticky_piston", "minecraft:piston_arm", "minecraft:observer": + return true + } + return false +} + func (e *redstoneEngine) compileEdges(tx *Tx, nodes []redstoneNode) []redstoneEdge { index := make(map[cube.Pos]int, len(nodes)) for i, node := range nodes { @@ -686,7 +705,13 @@ func (e *redstoneEngine) compileEdges(tx *Tx, nodes []redstoneNode) []redstoneEd continue } face := redstoneStepFace(node.pos, neighbour) - edges = append(edges, redstoneEdge{from: i, to: j, weight: max(relayer.RedstoneSignalLoss(node.pos, tx, face.Opposite(), face), 1)}) + weight := 0 + if neighbourBlock, ok := tx.World().blockLoaded(neighbour); ok { + if _, neighbourRelayer := neighbourBlock.(RedstonePowerRelayer); neighbourRelayer { + weight = max(relayer.RedstoneSignalLoss(node.pos, tx, face.Opposite(), face), 1) + } + } + edges = append(edges, redstoneEdge{from: i, to: j, weight: weight}) } } slices.SortFunc(edges, compareRedstoneEdge) diff --git a/server/world/redstone_test.go b/server/world/redstone_test.go index 84c7e52bf..d9d94f2e2 100644 --- a/server/world/redstone_test.go +++ b/server/world/redstone_test.go @@ -1,6 +1,7 @@ package world import ( + "math/rand/v2" "slices" "testing" "time" @@ -143,6 +144,24 @@ func TestRedstoneGraphID(t *testing.T) { } } +func TestRedstoneStrongPowerConductorExcludesExplicitNonConductors(t *testing.T) { + pos := cube.Pos{0, 64, 0} + if !redstoneStrongPowerConductor(pos, redstoneNamedSolidBlock{name: "minecraft:stone"}, nil, cube.FaceWest) { + t.Fatal("stone was not treated as a strong-power conductor") + } + for _, name := range []string{ + "minecraft:redstone_block", + "minecraft:piston", + "minecraft:sticky_piston", + "minecraft:piston_arm", + "minecraft:observer", + } { + if redstoneStrongPowerConductor(pos, redstoneNamedSolidBlock{name: name}, nil, cube.FaceWest) { + t.Fatalf("%s was treated as a strong-power conductor", name) + } + } +} + func TestRedstoneEngineInvalidateAround(t *testing.T) { var nilEngine *redstoneEngine nilEngine.invalidateAround(cube.Pos{0, 0, 0}, cube.Pos{0, 0, 0}, RedstoneUpdateCauseBlockUpdate, cube.Range{0, 0}) @@ -246,6 +265,31 @@ func TestRedstoneCancelledActionDoesNotRun(t *testing.T) { } } +func TestRedstoneRelayerToSinkDoesNotLosePower(t *testing.T) { + sourcePos, relayerPos, sinkPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0}, cube.Pos{2, 64, 0} + w := Config{Blocks: redstoneSignalLossTestRegistry()}.New() + defer w.Close() + + var directPower, sinkPower int + <-w.Exec(func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneLossSource{Power: 15}, nil) + tx.SetBlock(relayerPos, redstoneLossRelayer{}, nil) + tx.SetBlock(sinkPos, redstoneLossConsumer{}, nil) + + directPower = tx.RedstonePower(sinkPos) + tx.World().redstone.tick(tx, 1) + if sink, ok := tx.Block(sinkPos).(redstoneLossConsumer); ok { + sinkPower = sink.Power + } + }) + if directPower != 15 { + t.Fatalf("powerFrom through relayer into sink = %d, want 15", directPower) + } + if sinkPower != 15 { + t.Fatalf("graph power through relayer into sink = %d, want 15", sinkPower) + } +} + func TestScheduledTickQueueKeepsLaterTickForSameBlock(t *testing.T) { queue := newScheduledTickQueue(100) pos := cube.Pos{8, 64, 8} @@ -318,14 +362,81 @@ func TestScheduledTickQueueCanRescheduleWhileCurrentTickIsDue(t *testing.T) { } } +func TestScheduledTickQueueSkipsStaleDueTickBehindLaterTick(t *testing.T) { + registry := scheduledTickTestRegistry() + w := Config{Blocks: registry}.New() + defer w.Close() + + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + index := scheduledTickIndex{pos: pos, hash: registry.BlockHash(b)} + + ticks := 0 + scheduledTickTestBlockTicks = &ticks + t.Cleanup(func() { + scheduledTickTestBlockTicks = nil + }) + + var ( + ticksAfterFirst, ticksAfterSecond int + activeAfterFirst, activeAfterSecond []scheduledTick + furthestAfterFirst int64 + hasFurthestAfterFirst bool + ) + <-w.Exec(func(tx *Tx) { + tx.SetBlock(pos, b, nil) + queue.schedule(registry, pos, b, time.Second/20) + queue.schedule(registry, pos, b, time.Second/10) + + queue.tick(tx, 101) + ticksAfterFirst = ticks + activeAfterFirst = queue.fromChunk(chunkPosFromBlockPos(pos)) + furthestAfterFirst, hasFurthestAfterFirst = queue.furthestTicks[index] + + queue.tick(tx, 102) + ticksAfterSecond = ticks + activeAfterSecond = queue.fromChunk(chunkPosFromBlockPos(pos)) + }) + if ticksAfterFirst != 0 { + t.Fatalf("stale due tick executed %d time(s), want 0", ticksAfterFirst) + } + if !hasFurthestAfterFirst || furthestAfterFirst != 102 { + t.Fatalf("furthest tick after stale tick = %d, %t; want 102, true", furthestAfterFirst, hasFurthestAfterFirst) + } + if len(activeAfterFirst) != 1 || activeAfterFirst[0].t != 102 { + t.Fatalf("active ticks after stale tick = %v, want only tick 102", activeAfterFirst) + } + if ticksAfterSecond != 1 { + t.Fatalf("later scheduled tick executed %d time(s), want 1", ticksAfterSecond) + } + if len(activeAfterSecond) != 0 { + t.Fatalf("active ticks after later tick = %v, want empty", activeAfterSecond) + } +} + type scheduledTickTestBlock struct{} +var scheduledTickTestBlockTicks *int + +func (scheduledTickTestBlock) ScheduledTick(cube.Pos, *Tx, *rand.Rand) { + if scheduledTickTestBlockTicks != nil { + (*scheduledTickTestBlockTicks)++ + } +} func (scheduledTickTestBlock) EncodeBlock() (string, map[string]any) { return "test:scheduled_tick", nil } func (scheduledTickTestBlock) Hash() (uint64, uint64) { return 1 << 40, 0 } func (scheduledTickTestBlock) Model() BlockModel { return nil } +func scheduledTickTestRegistry() BlockRegistry { + registry := NewBlockRegistry() + registry.RegisterBlockState(BlockState{Name: "test:scheduled_tick", Properties: map[string]any{}}) + registry.RegisterBlock(scheduledTickTestBlock{}) + return registry +} + type redstoneNeighbourOrderTestBlock struct { neighbours []cube.Pos } @@ -431,3 +542,78 @@ func (redstoneCancellationModel) BBox(cube.Pos, BlockSource) []cube.BBox { retur func (redstoneCancellationModel) FaceSolid(cube.Pos, cube.Face, BlockSource) bool { return false } + +func redstoneSignalLossTestRegistry() BlockRegistry { + registry := NewBlockRegistry() + registry.RegisterBlockState(BlockState{Name: "test:redstone_loss_source", Properties: map[string]any{"power": int32(15)}}) + registry.RegisterBlock(redstoneLossSource{Power: 15}) + registry.RegisterBlockState(BlockState{Name: "test:redstone_loss_relayer", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneLossRelayer{}) + for power := int32(0); power <= 15; power++ { + registry.RegisterBlockState(BlockState{Name: "test:redstone_loss_consumer", Properties: map[string]any{"power": power}}) + registry.RegisterBlock(redstoneLossConsumer{Power: int(power)}) + } + return registry +} + +type redstoneLossSource struct { + Power int +} + +func (b redstoneLossSource) RedstonePower(cube.Pos, *Tx, cube.Face) int { + return b.Power +} +func (b redstoneLossSource) EncodeBlock() (string, map[string]any) { + return "test:redstone_loss_source", map[string]any{"power": int32(b.Power)} +} +func (b redstoneLossSource) Hash() (uint64, uint64) { + return 1 << 45, uint64(b.Power) +} +func (redstoneLossSource) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneLossRelayer struct{} + +func (redstoneLossRelayer) RedstoneSignalLoss(cube.Pos, *Tx, cube.Face, cube.Face) int { + return 1 +} +func (redstoneLossRelayer) EncodeBlock() (string, map[string]any) { + return "test:redstone_loss_relayer", nil +} +func (redstoneLossRelayer) Hash() (uint64, uint64) { return 1 << 46, 0 } +func (redstoneLossRelayer) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneLossConsumer struct { + Power int +} + +func (b redstoneLossConsumer) RedstonePowerUpdate(_ cube.Pos, _ *Tx, power int) (Block, bool) { + if b.Power == power { + return b, false + } + b.Power = power + return b, true +} +func (b redstoneLossConsumer) EncodeBlock() (string, map[string]any) { + return "test:redstone_loss_consumer", map[string]any{"power": int32(b.Power)} +} +func (b redstoneLossConsumer) Hash() (uint64, uint64) { + return 1 << 47, uint64(b.Power) +} +func (redstoneLossConsumer) Model() BlockModel { return redstoneCancellationModel{} } + +type redstoneNamedSolidBlock struct { + name string +} + +func (b redstoneNamedSolidBlock) EncodeBlock() (string, map[string]any) { + return b.name, nil +} +func (redstoneNamedSolidBlock) Hash() (uint64, uint64) { return 1 << 48, 0 } +func (redstoneNamedSolidBlock) Model() BlockModel { return redstoneSolidModel{} } + +type redstoneSolidModel struct{} + +func (redstoneSolidModel) BBox(cube.Pos, BlockSource) []cube.BBox { return nil } +func (redstoneSolidModel) FaceSolid(cube.Pos, cube.Face, BlockSource) bool { + return true +} diff --git a/server/world/tick.go b/server/world/tick.go index 88a8afe63..1c7f52e6b 100644 --- a/server/world/tick.go +++ b/server/world/tick.go @@ -302,6 +302,10 @@ func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) { if t.t > tick { continue } + index := scheduledTickIndex{pos: t.pos, hash: t.bhash} + if furthest, ok := queue.furthestTicks[index]; ok && furthest > t.t { + continue + } b := tx.Block(t.pos) if ticker, ok := b.(ScheduledTicker); ok && w.conf.Blocks.BlockHash(b) == t.bhash { ticker.ScheduledTick(t.pos, tx, w.r) From 2a409acd747479b0986306de7d2685c73ca0ff83 Mon Sep 17 00:00:00 2001 From: cqdetdev <101936396+cqdetdev@users.noreply.github.com> Date: Sun, 10 May 2026 21:55:37 -0400 Subject: [PATCH 4/4] refactor: narrow redstone core scope --- cmd/blockhash/main.go | 12 +- server/block/hash.go | 17 +- server/block/lever.go | 126 ++++++ server/block/redstone.go | 453 +------------------- server/block/redstone_block.go | 52 +++ server/block/redstone_common.go | 11 - server/block/redstone_sources.go | 592 -------------------------- server/block/redstone_sources_test.go | 315 -------------- server/block/redstone_test.go | 230 +++------- server/block/redstone_torch.go | 65 ++- server/block/redstone_wire.go | 286 +++++++++++++ server/block/register.go | 31 -- server/session/world.go | 8 - server/world/redstone.go | 55 +-- server/world/redstone_test.go | 170 ++++++-- server/world/redstone_torch.go | 93 ++++ server/world/sound/block.go | 12 - server/world/tick.go | 24 +- server/world/tx.go | 26 ++ server/world/world.go | 17 +- server/world/world_test.go | 53 +++ 21 files changed, 968 insertions(+), 1680 deletions(-) create mode 100644 server/block/lever.go create mode 100644 server/block/redstone_block.go delete mode 100644 server/block/redstone_common.go delete mode 100644 server/block/redstone_sources.go delete mode 100644 server/block/redstone_sources_test.go create mode 100644 server/block/redstone_wire.go create mode 100644 server/world/redstone_torch.go diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index ab3794f6a..3f86174cd 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -252,19 +252,11 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[ case "CoralType", "SkullType": return "uint64(" + s + ".Uint8())", 3 case "AnvilType", "SandstoneType", "PrismarineType", "StoneBricksType", "NetherBricksType", "FroglightType", - "WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType", "CopperType", "OxidationType", "BellAttachment": + "WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType", "CopperType", "OxidationType": return "uint64(" + s + ".Uint8())", 2 - case "CrafterOrientation": - return "uint64(" + s + ".Uint8())", 4 case "OreType", "FireType", "DoubleTallGrassType": return "uint64(" + s + ".Uint8())", 1 - case "Direction": - return "uint64(" + s + ")", 2 - case "Axis": - if _, ok := directives["lever_axis"]; ok { - receiver, _, _ := strings.Cut(s, ".") - return "leverAxisHash(" + receiver + ")", 1 - } + case "Direction", "Axis": return "uint64(" + s + ")", 2 case "Face": return "uint64(" + s + ")", 3 diff --git a/server/block/hash.go b/server/block/hash.go index 11cb75f02..684d9f010 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -25,7 +25,6 @@ const ( hashBookshelf hashBrewingStand hashBricks - hashButton hashCactus hashCake hashCalcite @@ -149,7 +148,6 @@ const ( hashPolishedBlackstoneBrick hashPolishedTuff hashPotato - hashPressurePlate hashPrismarine hashPumpkin hashPumpkinSeeds @@ -162,7 +160,6 @@ const ( hashRawGold hashRawIron hashRedstoneBlock - hashRedstoneLamp hashRedstoneOre hashRedstoneTorch hashRedstoneWire @@ -303,10 +300,6 @@ func (Bricks) Hash() (uint64, uint64) { return hashBricks, 0 } -func (b Button) Hash() (uint64, uint64) { - return hashButton, uint64(b.Type) | uint64(b.Facing)<<8 | uint64(boolByte(b.Pressed))<<11 -} - func (c Cactus) Hash() (uint64, uint64) { return hashCactus, uint64(c.Age) } @@ -672,7 +665,7 @@ func (l Lectern) Hash() (uint64, uint64) { } func (l Lever) Hash() (uint64, uint64) { - return hashLever, uint64(l.Facing) | leverAxisHash(l)<<3 | uint64(boolByte(l.Powered))<<4 + return hashLever, uint64(boolByte(l.Powered)) | uint64(l.Facing)<<1 | uint64(l.Direction)<<4 } func (l Light) Hash() (uint64, uint64) { @@ -799,10 +792,6 @@ func (p Potato) Hash() (uint64, uint64) { return hashPotato, uint64(p.Growth) } -func (p PressurePlate) Hash() (uint64, uint64) { - return hashPressurePlate, uint64(p.Type) | uint64(p.Power)<<8 -} - func (p Prismarine) Hash() (uint64, uint64) { return hashPrismarine, uint64(p.Type.Uint8()) } @@ -851,10 +840,6 @@ func (RedstoneBlock) Hash() (uint64, uint64) { return hashRedstoneBlock, 0 } -func (r RedstoneLamp) Hash() (uint64, uint64) { - return hashRedstoneLamp, uint64(boolByte(r.Lit)) -} - func (r RedstoneOre) Hash() (uint64, uint64) { return hashRedstoneOre, uint64(r.Type.Uint8()) | uint64(boolByte(r.Lit))<<1 } diff --git a/server/block/lever.go b/server/block/lever.go new file mode 100644 index 000000000..5a1e395c3 --- /dev/null +++ b/server/block/lever.go @@ -0,0 +1,126 @@ +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. Currently, only North and West directions are supported due to Bedrock Edition limitations. + Direction cube.Direction +} + +// RedstonePower returns maximum power while the lever is active. +func (l Lever) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { + if l.Powered { + return 15 + } + return 0 +} + +// RedstoneStrongPower strongly powers the block the lever is attached to. +func (l Lever) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { + if l.Powered && l.Facing.Opposite() == face { + return 15 + } + return 0 +} + +// SideClosed ... +func (l Lever) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false +} + +// NeighbourUpdateTick ... +func (l Lever) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + supportPos := pos.Side(l.Facing.Opposite()) + if !tx.Block(supportPos).Model().FaceSolid(supportPos, l.Facing, tx) { + breakBlock(l, pos, tx) + } +} + +// UseOnBlock ... +func (l Lever) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, face, used := firstReplaceable(tx, pos, face, l) + if !used { + return false + } + supportPos := pos.Side(face.Opposite()) + if !tx.Block(supportPos).Model().FaceSolid(supportPos, face, tx) { + return false + } + + l.Powered = 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(tx, pos, l, user, ctx) + return placed(ctx) +} + +// Activate ... +func (l Lever) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { + l.Powered = !l.Powered + tx.SetBlock(pos, l, nil) + if l.Powered { + tx.PlaySound(pos.Vec3Centre(), sound.PowerOn{}) + } else { + tx.PlaySound(pos.Vec3Centre(), sound.PowerOff{}) + } + return true +} + +// BreakInfo ... +func (l Lever) BreakInfo() BreakInfo { + return newBreakInfo(0.5, alwaysHarvestable, nothingEffective, oneOf(Lever{})).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { + tx.ScheduleRedstoneUpdate(pos) + }) +} + +// 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/redstone.go b/server/block/redstone.go index c873b5248..c8ad9bd68 100644 --- a/server/block/redstone.go +++ b/server/block/redstone.go @@ -1,456 +1,13 @@ package block import ( - "math/rand/v2" - - "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" + "time" ) -// RedstoneBlock is a solid block that emits maximum redstone power. -type RedstoneBlock struct { - solid -} - -// BreakInfo ... -func (r RedstoneBlock) BreakInfo() BreakInfo { - return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(r)).withBlastResistance(30) -} - -// RedstonePower always returns maximum power. -func (RedstoneBlock) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { - return 15 -} - -// RedstoneStrongPower returns no strong power. Redstone blocks power adjacent components directly, but do not power -// adjacent opaque blocks. -func (RedstoneBlock) RedstoneStrongPower(cube.Pos, *world.Tx, cube.Face) int { - return 0 -} - -// EncodeItem ... -func (RedstoneBlock) EncodeItem() (name string, meta int16) { - return "minecraft:redstone_block", 0 -} - -// EncodeBlock ... -func (RedstoneBlock) EncodeBlock() (string, map[string]any) { - return "minecraft:redstone_block", nil -} - -// RedstoneWire is redstone dust placed in the world. Power is stored as a value from 0 to 15. -type RedstoneWire struct { - empty - transparent - sourceWaterDisplacer - - // Power is the current signal strength carried by the wire. - Power int -} - -// UseOnBlock places redstone wire on a replaceable block. -func (r RedstoneWire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { - pos, _, used := firstReplaceable(tx, pos, face, r) - if !used || !redstoneWireSupported(tx, pos) { - return false - } - place(tx, pos, r, user, ctx) - return placed(ctx) -} - -// RedstonePower returns the wire's current signal strength from connected faces. -func (r RedstoneWire) RedstonePower(pos cube.Pos, tx *world.Tx, face cube.Face) int { - if tx != nil && redstoneWireFaceHorizontal(face) && !redstoneWirePowersHorizontalFace(pos, tx, face) { - return 0 - } - return r.Power -} - -// RedstoneSignalLoss returns the signal loss through a wire segment. -func (RedstoneWire) RedstoneSignalLoss(cube.Pos, *world.Tx, cube.Face, cube.Face) int { - return 1 -} - -// RedstoneRelayerNeighbours returns all wire positions directly connected to this dust, including dust stepping up or -// down adjacent blocks. -func (RedstoneWire) RedstoneRelayerNeighbours(pos cube.Pos, tx *world.Tx) []cube.Pos { - neighbours := make([]cube.Pos, 0, 12) - faces := redstoneWirePoweredHorizontalFaces(pos, tx) - for _, face := range cube.HorizontalFaces() { - if !faces[face] { - continue - } - side := pos.Side(face) - if side.OutOfBounds(tx.Range()) { - continue - } - positions := redstoneWireHorizontalConnectionPositions(pos, tx, face) - if len(positions) != 0 { - neighbours = append(neighbours, positions...) - continue - } - if redstoneWireRelevantLoaded(tx, side) { - neighbours = append(neighbours, side) - } - } - return neighbours -} - -// RedstonePowerUpdate updates the wire strength to match its strongest input. -func (r RedstoneWire) RedstonePowerUpdate(_ cube.Pos, _ *world.Tx, power int) (world.Block, bool) { - power = max(0, min(power, 15)) - if r.Power == power { - return r, false - } - r.Power = power - return r, true -} - -// NeighbourUpdateTick breaks unsupported wire. -func (r RedstoneWire) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { - if !redstoneWireSupported(tx, pos) { - breakBlock(r, pos, tx) - } -} - -// HasLiquidDrops ... -func (RedstoneWire) HasLiquidDrops() bool { - return true -} - -// BreakInfo ... -func (r RedstoneWire) BreakInfo() BreakInfo { - return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(RedstoneWire{})) -} - -// SideClosed ... -func (RedstoneWire) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { - return false -} - -// 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(max(0, min(r.Power, 15)))} -} - -func allRedstoneWires() (wires []world.Block) { - for i := 0; i <= 15; i++ { - wires = append(wires, RedstoneWire{Power: i}) - } - return -} - -func redstoneWireSupported(tx *world.Tx, pos cube.Pos) bool { - below := pos.Side(cube.FaceDown) - if below.OutOfBounds(tx.Range()) { - return false - } - return tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) -} - -func redstoneWireSupportedLoaded(tx *world.Tx, pos cube.Pos) bool { - below := pos.Side(cube.FaceDown) - if below.OutOfBounds(tx.Range()) { - return false - } - b, ok := tx.BlockLoaded(below) - return ok && b.Model().FaceSolid(below, cube.FaceUp, tx) -} - -func redstoneWireBlocksConnectionLoaded(tx *world.Tx, pos cube.Pos, face cube.Face) bool { - if pos.OutOfBounds(tx.Range()) { - return true - } - b, ok := tx.BlockLoaded(pos) - return ok && b.Model().FaceSolid(pos, face, tx) -} - -func redstoneWirePowersHorizontalFace(pos cube.Pos, tx *world.Tx, face cube.Face) bool { - return redstoneWirePoweredHorizontalFaces(pos, tx)[face] -} - -func redstoneWirePoweredHorizontalFaces(pos cube.Pos, tx *world.Tx) map[cube.Face]bool { - connections := make(map[cube.Face]bool, len(cube.HorizontalFaces())) - for _, face := range cube.HorizontalFaces() { - if len(redstoneWireHorizontalConnectionPositions(pos, tx, face)) != 0 { - connections[face] = true - } - } - switch len(connections) { - case 0: - for _, face := range cube.HorizontalFaces() { - connections[face] = true - } - case 1: - for face := range connections { - connections[face.Opposite()] = true - } - } - return connections -} - -func redstoneWireHorizontalConnectionPositions(pos cube.Pos, tx *world.Tx, face cube.Face) []cube.Pos { - side := pos.Side(face) - if side.OutOfBounds(tx.Range()) { - return nil - } - positions := make([]cube.Pos, 0, 3) - if redstoneWireDirectConnectionLoaded(tx, side, face.Opposite()) { - positions = append(positions, side) - } - - above := pos.Side(cube.FaceUp) - sideAbove := side.Side(cube.FaceUp) - if !redstoneWireBlocksConnectionLoaded(tx, above, cube.FaceDown) && redstoneWireAtLoaded(tx, sideAbove) && redstoneWireSupportedLoaded(tx, sideAbove) { - positions = append(positions, sideAbove) - } - if !redstoneWireBlocksConnectionLoaded(tx, side, cube.FaceUp) { - down := side.Side(cube.FaceDown) - if !down.OutOfBounds(tx.Range()) && redstoneWireAtLoaded(tx, down) { - positions = append(positions, down) - } - } - return positions -} - -func redstoneWireDirectConnectionLoaded(tx *world.Tx, pos cube.Pos, face cube.Face) bool { - b, ok := tx.BlockLoaded(pos) - if !ok { - return false - } - if _, ok := b.(RedstoneWire); ok { - return true - } - if _, ok := b.(world.RedstonePowerSource); ok { - return true - } - if _, ok := b.(world.RedstoneStrongPowerSource); ok { - return true - } - if _, ok := b.(world.RedstonePowerRelayer); ok { - return true - } - return redstoneWireNonSolidComponent(pos, b, tx, face) -} - -func redstoneWireNonSolidComponent(pos cube.Pos, b world.Block, tx *world.Tx, face cube.Face) bool { - model := b.Model() - if model == nil || model.FaceSolid(pos, face, tx) { - return false - } - if _, ok := b.(world.RedstonePowerConsumer); ok { - return true - } - if _, ok := b.(world.RedstonePowerTransitionConsumer); ok { - return true - } - if _, ok := b.(world.RedstonePowerAction); ok { - return true - } - return false -} - -func redstoneWireAtLoaded(tx *world.Tx, pos cube.Pos) bool { - b, ok := tx.BlockLoaded(pos) - if !ok { - return false - } - _, ok = b.(RedstoneWire) - return ok -} - -func redstoneWireRelevantLoaded(tx *world.Tx, pos cube.Pos) bool { - b, ok := tx.BlockLoaded(pos) - return ok && redstoneWireRelevant(b) -} - -func redstoneWireRelevant(b world.Block) bool { - if _, ok := b.(world.RedstonePowerSource); ok { - return true - } - if _, ok := b.(world.RedstoneStrongPowerSource); ok { - return true - } - if _, ok := b.(world.RedstonePowerRelayer); ok { - return true - } - if _, ok := b.(world.RedstonePowerConsumer); ok { - return true - } - if _, ok := b.(world.RedstonePowerTransitionConsumer); ok { - return true - } - if _, ok := b.(world.RedstonePowerAction); ok { - return true - } - return false -} - -func redstoneWireFaceHorizontal(face cube.Face) bool { - switch face { - case cube.FaceNorth, cube.FaceSouth, cube.FaceWest, cube.FaceEast: - return true - default: - return false - } -} - -// RedstoneLamp is a lamp that lights while powered. -type RedstoneLamp struct { - solid - - // Lit is true when the lamp is powered and emitting light. - Lit bool -} - -// LightEmissionLevel ... -func (r RedstoneLamp) LightEmissionLevel() uint8 { - if r.Lit { - return 15 - } - return 0 -} - -// RedstonePowerUpdate lights the lamp immediately and schedules delayed turn-off when power is removed. -func (r RedstoneLamp) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, power int) (world.Block, bool) { - if redstoneLampPower(pos, tx, power) > 0 { - if r.Lit { - return r, false - } - r.Lit = true - return r, true - } - if !r.Lit { - return r, false - } - if tx != nil { - tx.ScheduleBlockUpdate(pos, r, redstoneTicks(2)) - return r, false - } - r.Lit = false - return r, true -} - -// ScheduledTick turns the lamp off after its delay if it was not repowered. -func (r RedstoneLamp) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { - if tx == nil || !r.Lit || redstoneLampPower(pos, tx, tx.RedstonePower(pos)) > 0 { - return - } - r.Lit = false - tx.SetBlock(pos, r, nil) -} - -func redstoneLampPower(pos cube.Pos, tx *world.Tx, power int) int { - if tx == nil { - return redstonePower(power) - } - return max(redstoneLampGraphPower(pos, tx, power), tx.RedstoneDirectPower(pos), redstoneLampConductedStrongPower(pos, tx)) -} - -func redstoneLampGraphPower(pos cube.Pos, tx *world.Tx, power int) int { - power = redstonePower(power) - if power == 0 { - return 0 - } - for _, face := range cube.Faces() { - neighbour := pos.Side(face) - if neighbour.OutOfBounds(tx.Range()) { - continue - } - b, ok := tx.BlockLoaded(neighbour) - if !ok { - continue - } - if _, ok := b.(world.RedstonePowerRelayer); !ok { - continue - } - if redstoneLampRelayerConnectsTo(neighbour, pos, tx, b) { - return power - } - } - return 0 -} - -func redstoneLampRelayerConnectsTo(relayerPos, target cube.Pos, tx *world.Tx, b world.Block) bool { - neighbourer, ok := b.(world.RedstonePowerRelayerNeighbourer) - if !ok { - return true - } - for _, neighbour := range neighbourer.RedstoneRelayerNeighbours(relayerPos, tx) { - if neighbour == target { - return true - } - } - return false -} - -func redstoneLampConductedStrongPower(pos cube.Pos, tx *world.Tx) int { - power := 0 - for _, face := range cube.Faces() { - conductorPos := pos.Side(face) - if conductorPos.OutOfBounds(tx.Range()) { - continue - } - conductor, ok := tx.BlockLoaded(conductorPos) - if !ok || !redstoneLampStrongPowerConductor(conductorPos, conductor, tx, face.Opposite()) { - continue - } - power = max(power, tx.RedstoneStrongPower(conductorPos)) - } - return redstonePower(power) -} - -func redstoneLampStrongPowerConductor(pos cube.Pos, b world.Block, tx *world.Tx, face cube.Face) bool { - if !b.Model().FaceSolid(pos, face, tx) { - return false - } - if redstoneLampExplicitNonConductor(b) { - return false - } - if diffuser, ok := b.(redstoneLampLightDiffuser); ok && diffuser.LightDiffusionLevel() == 0 { - return false - } - return true -} - -func redstoneLampExplicitNonConductor(b world.Block) bool { - name, _ := b.EncodeBlock() - switch name { - case "minecraft:redstone_block", "minecraft:piston", "minecraft:sticky_piston", "minecraft:piston_arm", "minecraft:observer": - return true - } - return false -} - -type redstoneLampLightDiffuser interface { - LightDiffusionLevel() uint8 -} - -// BreakInfo ... -func (r RedstoneLamp) BreakInfo() BreakInfo { - return newBreakInfo(0.3, alwaysHarvestable, nothingEffective, oneOf(RedstoneLamp{})) -} - -// EncodeItem ... -func (RedstoneLamp) EncodeItem() (name string, meta int16) { - return "minecraft:redstone_lamp", 0 -} - -// EncodeBlock ... -func (r RedstoneLamp) EncodeBlock() (string, map[string]any) { - if r.Lit { - return "minecraft:lit_redstone_lamp", nil - } - return "minecraft:redstone_lamp", nil +func redstonePower(power int) int { + return min(max(power, 0), 15) } -func allRedstoneLamps() (lamps []world.Block) { - return []world.Block{RedstoneLamp{}, RedstoneLamp{Lit: true}} +func redstoneTicks(ticks int) time.Duration { + return time.Duration(max(ticks, 1)) * time.Second / 10 } diff --git a/server/block/redstone_block.go b/server/block/redstone_block.go new file mode 100644 index 000000000..9a0f4ef98 --- /dev/null +++ b/server/block/redstone_block.go @@ -0,0 +1,52 @@ +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" +) + +// RedstoneBlock is a mineral block equivalent to nine redstone dust. +// It acts as a permanently powered redstone power source that can be pushed by pistons. +type RedstoneBlock struct { + solid +} + +// BreakInfo ... +func (r RedstoneBlock) BreakInfo() BreakInfo { + return newBreakInfo(5, pickaxeHarvestable, pickaxeEffective, oneOf(r)).withBlastResistance(30).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { + tx.ScheduleRedstoneUpdate(pos) + }) +} + +// 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, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(tx, pos, face, r) + if !used { + return false + } + place(tx, pos, r, user, ctx) + return placed(ctx) +} + +// RedstonePower always returns maximum power. +func (RedstoneBlock) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { + return 15 +} + +// RedstoneStrongPower returns no strong power. Redstone blocks power adjacent components directly, but do not power +// adjacent opaque blocks. +func (RedstoneBlock) RedstoneStrongPower(cube.Pos, *world.Tx, cube.Face) int { + return 0 +} diff --git a/server/block/redstone_common.go b/server/block/redstone_common.go deleted file mode 100644 index 91efbb279..000000000 --- a/server/block/redstone_common.go +++ /dev/null @@ -1,11 +0,0 @@ -package block - -import "time" - -func redstonePower(power int) int { - return min(max(power, 0), 15) -} - -func redstoneTicks(ticks int) time.Duration { - return time.Duration(max(ticks, 1)) * time.Second / 10 -} diff --git a/server/block/redstone_sources.go b/server/block/redstone_sources.go deleted file mode 100644 index f39146d29..000000000 --- a/server/block/redstone_sources.go +++ /dev/null @@ -1,592 +0,0 @@ -package block - -import ( - "math/rand/v2" - "time" - - "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/block/model" - "github.com/df-mc/dragonfly/server/item" - "github.com/df-mc/dragonfly/server/world" - "github.com/df-mc/dragonfly/server/world/sound" - "github.com/go-gl/mathgl/mgl64" -) - -const ( - redstoneSourceStone = iota - redstoneSourcePolishedBlackstone - redstoneSourceOak - redstoneSourceSpruce - redstoneSourceBirch - redstoneSourceJungle - redstoneSourceAcacia - redstoneSourceDarkOak - redstoneSourceMangrove - redstoneSourceCherry - redstoneSourceBamboo - redstoneSourceCrimson - redstoneSourceWarped - redstoneSourcePaleOak - redstoneSourceLightWeighted - redstoneSourceHeavyWeighted -) - -// Lever is a switch that emits redstone power while active. -type Lever struct { - empty - transparent - sourceWaterDisplacer - - // Facing is the face the lever is attached to. - Facing cube.Face - // Axis is the horizontal axis used by floor and ceiling levers. - //blockhash:lever_axis - Axis cube.Axis - // Powered is true if the lever is switched on. - Powered bool -} - -// UseOnBlock places a lever attached to the clicked face. -func (l Lever) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { - pos, face, used := firstReplaceable(tx, pos, face, l) - if !used || !redstoneAttachmentSupported(tx, pos, face) { - return false - } - l.Facing = face - if user != nil && (face == cube.FaceUp || face == cube.FaceDown) { - l.Axis = user.Rotation().Direction().Face().Axis() - } - place(tx, pos, l, user, ctx) - return placed(ctx) -} - -// Activate toggles the lever. -func (l Lever) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { - l.Powered = !l.Powered - tx.SetBlock(pos, l, nil) - tx.PlaySound(pos.Vec3Centre(), sound.Click{}) - return true -} - -// NeighbourUpdateTick breaks the lever if its supporting block is removed. -func (l Lever) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { - if !redstoneAttachmentSupported(tx, pos, l.Facing) { - breakBlock(l, pos, tx) - } -} - -// RedstonePower returns maximum power while the lever is active. -func (l Lever) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { - if l.Powered { - return 15 - } - return 0 -} - -// RedstoneStrongPower strongly powers the block the lever is attached to. -func (l Lever) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { - if l.Powered && face == l.Facing.Opposite() { - return 15 - } - return 0 -} - -// BreakInfo ... -func (l Lever) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, nothingEffective, oneOf(l)) -} - -// SideClosed ... -func (Lever) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { - return false -} - -// EncodeItem ... -func (Lever) EncodeItem() (name string, meta int16) { - return "minecraft:lever", 0 -} - -// EncodeBlock ... -func (l Lever) EncodeBlock() (string, map[string]any) { - return "minecraft:lever", map[string]any{ - "lever_direction": leverDirection(l.Facing, l.Axis), - "open_bit": boolByte(l.Powered), - } -} - -func allLevers() (levers []world.Block) { - for _, face := range cube.HorizontalFaces() { - levers = append(levers, Lever{Facing: face}, Lever{Facing: face, Powered: true}) - } - for _, face := range []cube.Face{cube.FaceDown, cube.FaceUp} { - for _, axis := range []cube.Axis{cube.X, cube.Z} { - levers = append(levers, Lever{Facing: face, Axis: axis}, Lever{Facing: face, Axis: axis, Powered: true}) - } - } - return -} - -func leverDirection(face cube.Face, axis cube.Axis) string { - switch face { - case cube.FaceDown: - if axis == cube.Z { - return "down_north_south" - } - return "down_east_west" - case cube.FaceUp: - if axis == cube.Z { - return "up_north_south" - } - return "up_east_west" - case cube.FaceNorth: - return "north" - case cube.FaceSouth: - return "south" - case cube.FaceWest: - return "west" - case cube.FaceEast: - return "east" - default: - return "down_east_west" - } -} - -func leverAxisHash(l Lever) uint64 { - if (l.Facing == cube.FaceDown || l.Facing == cube.FaceUp) && l.Axis == cube.Z { - return 1 - } - return 0 -} - -// Button is a pressable redstone power source. -type Button struct { - empty - transparent - sourceWaterDisplacer - - // Type identifies the button material. - Type int - // Facing is the face the button is attached to. - Facing cube.Face - // Pressed is true while the button emits power. - Pressed bool -} - -// UseOnBlock places a button attached to the clicked face. -func (b Button) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { - pos, face, used := firstReplaceable(tx, pos, face, b) - if !used || !redstoneAttachmentSupported(tx, pos, face) { - return false - } - b.Facing = face - place(tx, pos, b, user, ctx) - return placed(ctx) -} - -// Activate presses the button and schedules release. -func (b Button) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, _ *item.UseContext) bool { - if b.Pressed { - return true - } - b.Pressed = true - tx.SetBlock(pos, b, nil) - tx.ScheduleBlockUpdate(pos, b, b.pressDuration()) - tx.PlaySound(pos.Vec3Centre(), sound.Click{}) - return true -} - -// NeighbourUpdateTick breaks the button if its supporting block is removed. -func (b Button) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { - if !redstoneAttachmentSupported(tx, pos, b.Facing) { - breakBlock(b, pos, tx) - } -} - -// ScheduledTick releases a pressed button. -func (b Button) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { - if !b.Pressed { - return - } - b.Pressed = false - tx.SetBlock(pos, b, nil) - tx.PlaySound(pos.Vec3Centre(), sound.Click{}) -} - -// RedstonePower returns maximum power while the button is pressed. -func (b Button) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { - if b.Pressed { - return 15 - } - return 0 -} - -// RedstoneStrongPower strongly powers the block the button is attached to. -func (b Button) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { - if b.Pressed && face == b.Facing.Opposite() { - return 15 - } - return 0 -} - -// BreakInfo ... -func (b Button) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(b)) -} - -// SideClosed ... -func (Button) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { - return false -} - -// EncodeItem ... -func (b Button) EncodeItem() (name string, meta int16) { - return sourceName(b.Type, "button"), 0 -} - -// EncodeBlock ... -func (b Button) EncodeBlock() (string, map[string]any) { - return sourceName(b.Type, "button"), map[string]any{"button_pressed_bit": boolByte(b.Pressed), "facing_direction": int32(b.Facing)} -} - -func (b Button) pressDuration() time.Duration { - if b.Type >= redstoneSourceOak && b.Type <= redstoneSourcePaleOak { - return time.Second * 3 / 2 - } - return time.Second -} - -func allButtons() (buttons []world.Block) { - for _, typ := range redstoneSourceTypes() { - for _, face := range cube.Faces() { - buttons = append(buttons, Button{Type: typ, Facing: face}, Button{Type: typ, Facing: face, Pressed: true}) - } - } - return -} - -// PressurePlate emits redstone power while stepped on. -type PressurePlate struct { - empty - transparent - sourceWaterDisplacer - - // Type identifies the pressure plate material. - Type int - // Power is the current redstone signal emitted by the plate. - Power int -} - -// UseOnBlock places the pressure plate on a solid surface. -func (p PressurePlate) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { - pos, _, used := firstReplaceable(tx, pos, face, p) - if !used || !redstoneFloorComponentSupported(tx, pos) { - return false - } - place(tx, pos, p, user, ctx) - return placed(ctx) -} - -// Model ... -func (PressurePlate) Model() world.BlockModel { - return model.Carpet{} -} - -// EntityStepOn powers the plate when an entity stands on it. -func (p PressurePlate) EntityStepOn(pos cube.Pos, tx *world.Tx, e world.Entity) { - power := p.entityPower(e) - if power == 0 { - return - } - if p.weighted() { - power = max(power, p.detectPower(pos, tx)) - } - if p.Power == power { - tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) - return - } - p.Power = power - tx.SetBlock(pos, p, nil) - tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) - tx.PlaySound(pos.Vec3Centre(), sound.PressurePlateClickOn{}) -} - -// NeighbourUpdateTick breaks the pressure plate if its supporting block is removed. -func (p PressurePlate) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { - if !redstoneFloorComponentSupported(tx, pos) { - breakBlock(p, pos, tx) - } -} - -// ScheduledTick releases the plate if nothing refreshes it. -func (p PressurePlate) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { - power := p.detectPower(pos, tx) - if power > 0 { - if p.Power != power { - p.Power = power - tx.SetBlock(pos, p, nil) - } - tx.ScheduleBlockUpdate(pos, p, p.releaseDelay()) - return - } - if p.Power == 0 { - return - } - p.Power = 0 - tx.SetBlock(pos, p, nil) - tx.PlaySound(pos.Vec3Centre(), sound.PressurePlateClickOff{}) -} - -// RedstonePower returns the plate's analog power level. -func (p PressurePlate) RedstonePower(cube.Pos, *world.Tx, cube.Face) int { - return p.Power -} - -// RedstoneStrongPower strongly powers the block below the pressure plate. -func (p PressurePlate) RedstoneStrongPower(_ cube.Pos, _ *world.Tx, face cube.Face) int { - if face == cube.FaceDown { - return p.Power - } - return 0 -} - -// BreakInfo ... -func (p PressurePlate) BreakInfo() BreakInfo { - return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, oneOf(p)) -} - -// SideClosed ... -func (PressurePlate) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { - return false -} - -// EncodeItem ... -func (p PressurePlate) EncodeItem() (name string, meta int16) { - return pressurePlateSourceName(p.Type), 0 -} - -// EncodeBlock ... -func (p PressurePlate) EncodeBlock() (string, map[string]any) { - return pressurePlateSourceName(p.Type), map[string]any{"redstone_signal": int32(max(0, min(p.Power, 15)))} -} - -func (p PressurePlate) stepPower() int { - if p.Type == redstoneSourceLightWeighted || p.Type == redstoneSourceHeavyWeighted { - return 1 - } - return 15 -} - -func (p PressurePlate) entityPower(e world.Entity) int { - if !p.detectsEntity(e) { - return 0 - } - return p.stepPower() -} - -func (p PressurePlate) detectsEntity(e world.Entity) bool { - if p.ignoresEntity(e) { - return false - } - if p.stoneLike() { - return pressurePlateStoneEntity(e) - } - return true -} - -func (p PressurePlate) detectPower(pos cube.Pos, tx *world.Tx) int { - box := pressurePlateActivationBox(pos) - entities := 0 - for e := range tx.EntitiesWithin(box.Grow(1)) { - if p.entityPower(e) == 0 || !pressurePlateEntityIntersects(e, box) { - continue - } - if !p.weighted() { - return 15 - } - entities++ - if entities >= p.weightedMaxEntities() { - return 15 - } - } - if p.weighted() { - return p.weightedPower(entities) - } - return 0 -} - -func (p PressurePlate) stoneLike() bool { - return p.Type == redstoneSourceStone || p.Type == redstoneSourcePolishedBlackstone -} - -func (p PressurePlate) weighted() bool { - return p.Type == redstoneSourceLightWeighted || p.Type == redstoneSourceHeavyWeighted -} - -func (p PressurePlate) weightedPower(entities int) int { - if entities <= 0 { - return 0 - } - if p.Type == redstoneSourceHeavyWeighted { - return min(15, (entities+9)/10) - } - return min(15, entities) -} - -func (p PressurePlate) weightedMaxEntities() int { - if p.Type == redstoneSourceHeavyWeighted { - return 141 - } - return 15 -} - -func (p PressurePlate) releaseDelay() time.Duration { - return time.Second -} - -func (p PressurePlate) ignoresEntity(e world.Entity) bool { - return pressurePlateEntityName(e) == "minecraft:snowball" -} - -type pressurePlateLivingEntity interface { - Health() float64 - Dead() bool -} - -func pressurePlateStoneEntity(e world.Entity) bool { - if living, ok := e.(pressurePlateLivingEntity); ok { - return living.Health() > 0 && !living.Dead() - } - return pressurePlateEntityName(e) == "minecraft:player" || pressurePlateEntityName(e) == "minecraft:armor_stand" -} - -func pressurePlateEntityName(e world.Entity) string { - h := e.H() - if h == nil || h.Type() == nil { - return "" - } - return h.Type().EncodeEntity() -} - -func pressurePlateActivationBox(pos cube.Pos) cube.BBox { - const inset = 1.0 / 16.0 - return cube.Box(float64(pos[0])+inset, float64(pos[1]), float64(pos[2])+inset, float64(pos[0]+1)-inset, float64(pos[1])+0.25, float64(pos[2]+1)-inset) -} - -func pressurePlateEntityIntersects(e world.Entity, box cube.BBox) bool { - h := e.H() - if h == nil || h.Type() == nil { - return false - } - return h.Type().BBox(e).Translate(e.Position()).IntersectsWith(box) -} - -func allPressurePlates() (plates []world.Block) { - for _, typ := range append(redstoneSourceTypes(), redstoneSourceLightWeighted, redstoneSourceHeavyWeighted) { - for i := 0; i <= 15; i++ { - plates = append(plates, PressurePlate{Type: typ, Power: i}) - } - } - return -} - -func redstoneSourceTypes() []int { - types := []int{ - redstoneSourceStone, - redstoneSourcePolishedBlackstone, - redstoneSourceOak, - redstoneSourceSpruce, - redstoneSourceBirch, - redstoneSourceJungle, - redstoneSourceAcacia, - redstoneSourceDarkOak, - redstoneSourceMangrove, - redstoneSourceCherry, - redstoneSourceBamboo, - redstoneSourceCrimson, - redstoneSourceWarped, - redstoneSourcePaleOak, - } - return types -} - -func pressurePlateSourceName(typ int) string { - if typ == redstoneSourceLightWeighted { - return "minecraft:light_weighted_pressure_plate" - } - if typ == redstoneSourceHeavyWeighted { - return "minecraft:heavy_weighted_pressure_plate" - } - return sourceName(typ, "pressure_plate") -} - -func sourceName(typ int, suffix string) string { - switch typ { - case redstoneSourceStone: - return "minecraft:stone_" + suffix - case redstoneSourcePolishedBlackstone: - return "minecraft:polished_blackstone_" + suffix - case redstoneSourceOak: - if suffix == "button" { - return "minecraft:wooden_button" - } - return "minecraft:wooden_pressure_plate" - case redstoneSourceSpruce: - return "minecraft:spruce_" + suffix - case redstoneSourceBirch: - return "minecraft:birch_" + suffix - case redstoneSourceJungle: - return "minecraft:jungle_" + suffix - case redstoneSourceAcacia: - return "minecraft:acacia_" + suffix - case redstoneSourceDarkOak: - return "minecraft:dark_oak_" + suffix - case redstoneSourceMangrove: - return "minecraft:mangrove_" + suffix - case redstoneSourceCherry: - return "minecraft:cherry_" + suffix - case redstoneSourceBamboo: - return "minecraft:bamboo_" + suffix - case redstoneSourceCrimson: - return "minecraft:crimson_" + suffix - case redstoneSourceWarped: - return "minecraft:warped_" + suffix - case redstoneSourcePaleOak: - return "minecraft:pale_oak_" + suffix - default: - return "minecraft:stone_" + suffix - } -} - -func (b Button) Model() world.BlockModel { - return model.Empty{} -} - -func (p PressurePlate) FuelInfo() item.FuelInfo { - if p.Type >= redstoneSourceOak && p.Type <= redstoneSourcePaleOak { - return newFuelInfo(time.Second * 15) - } - return item.FuelInfo{} -} - -func (b Button) FuelInfo() item.FuelInfo { - if b.Type >= redstoneSourceOak && b.Type <= redstoneSourcePaleOak { - return newFuelInfo(time.Second * 5) - } - return item.FuelInfo{} -} - -func redstoneAttachmentSupported(tx *world.Tx, pos cube.Pos, face cube.Face) bool { - support := pos.Side(face.Opposite()) - if support.OutOfBounds(tx.Range()) { - return false - } - return tx.Block(support).Model().FaceSolid(support, face, tx) -} - -func redstoneFloorComponentSupported(tx *world.Tx, pos cube.Pos) bool { - support := pos.Side(cube.FaceDown) - if support.OutOfBounds(tx.Range()) { - return false - } - return tx.Block(support).Model().FaceSolid(support, cube.FaceUp, tx) -} diff --git a/server/block/redstone_sources_test.go b/server/block/redstone_sources_test.go deleted file mode 100644 index 2e6057f41..000000000 --- a/server/block/redstone_sources_test.go +++ /dev/null @@ -1,315 +0,0 @@ -package block - -import ( - "fmt" - "testing" - "time" - - "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/item" - "github.com/df-mc/dragonfly/server/world" - "github.com/go-gl/mathgl/mgl64" -) - -func TestLeverPower(t *testing.T) { - if power := (Lever{}).RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 0 { - t.Fatalf("unpowered lever power = %d, want 0", power) - } - if power := (Lever{Powered: true}).RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 15 { - t.Fatalf("powered lever power = %d, want 15", power) - } -} - -func TestLeverEncodeBlock(t *testing.T) { - tests := []struct { - name string - l Lever - want string - }{ - {name: "wall", l: Lever{Facing: cube.FaceEast}, want: "east"}, - {name: "floor east west", l: Lever{Facing: cube.FaceUp, Axis: cube.X}, want: "up_east_west"}, - {name: "floor north south", l: Lever{Facing: cube.FaceUp, Axis: cube.Z}, want: "up_north_south"}, - {name: "ceiling east west", l: Lever{Facing: cube.FaceDown, Axis: cube.X}, want: "down_east_west"}, - {name: "ceiling north south", l: Lever{Facing: cube.FaceDown, Axis: cube.Z}, want: "down_north_south"}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - _, props := test.l.EncodeBlock() - if direction := props["lever_direction"]; direction != test.want { - t.Fatalf("lever_direction = %v, want %s", direction, test.want) - } - }) - } -} - -func TestRedstoneAttachableSupport(t *testing.T) { - w := world.New() - defer func() { - _ = w.Close() - }() - - var err error - <-w.Exec(func(tx *world.Tx) { - pos := cube.Pos{0, 1, 0} - if redstoneAttachmentSupported(tx, pos, cube.FaceUp) { - err = fmt.Errorf("lever without support was supported") - return - } - tx.SetBlock(pos.Side(cube.FaceDown), Lever{}, nil) - if redstoneAttachmentSupported(tx, pos, cube.FaceUp) { - err = fmt.Errorf("lever on lever support was supported") - return - } - tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, nil) - if !redstoneAttachmentSupported(tx, pos, cube.FaceUp) { - err = fmt.Errorf("lever on solid support was not supported") - } - }) - if err != nil { - t.Fatal(err) - } -} - -func TestRedstoneFloorComponentSupport(t *testing.T) { - w := world.New() - defer func() { - _ = w.Close() - }() - - var err error - <-w.Exec(func(tx *world.Tx) { - pos := cube.Pos{0, 1, 0} - if redstoneFloorComponentSupported(tx, pos) { - err = fmt.Errorf("floor component without support was supported") - return - } - tx.SetBlock(pos.Side(cube.FaceDown), Button{Facing: cube.FaceUp}, nil) - if redstoneFloorComponentSupported(tx, pos) { - err = fmt.Errorf("floor component on button support was supported") - return - } - tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, nil) - if !redstoneFloorComponentSupported(tx, pos) { - err = fmt.Errorf("floor component on solid support was not supported") - } - }) - if err != nil { - t.Fatal(err) - } -} - -func TestButtonPowerAndDuration(t *testing.T) { - stone := Button{Type: redstoneSourceStone} - if power := stone.RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 0 { - t.Fatalf("unpressed button power = %d, want 0", power) - } - stone.Pressed = true - if power := stone.RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 15 { - t.Fatalf("pressed button power = %d, want 15", power) - } - if stone.pressDuration() >= (Button{Type: redstoneSourceOak}).pressDuration() { - t.Fatal("stone button duration should be shorter than wooden button duration") - } -} - -func TestPressurePlatePower(t *testing.T) { - plate := PressurePlate{Type: redstoneSourceStone, Power: 15} - if power := plate.RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 15 { - t.Fatalf("stone pressure plate power = %d, want 15", power) - } - if power := (PressurePlate{Type: redstoneSourceLightWeighted}).stepPower(); power != 1 { - t.Fatalf("weighted pressure plate step power = %d, want first analog level 1", power) - } - if power := (PressurePlate{Type: redstoneSourceLightWeighted}).weightedPower(16); power != 15 { - t.Fatalf("light weighted pressure plate count power = %d, want 15", power) - } - if power := (PressurePlate{Type: redstoneSourceHeavyWeighted}).weightedPower(11); power != 2 { - t.Fatalf("heavy weighted pressure plate count power = %d, want 2", power) - } - if power := (PressurePlate{Type: redstoneSourceHeavyWeighted}).weightedPower(141); power != 15 { - t.Fatalf("heavy weighted pressure plate max power = %d, want 15", power) - } - if delay := (PressurePlate{Type: redstoneSourceLightWeighted}).releaseDelay(); delay != time.Second { - t.Fatalf("weighted pressure plate release delay = %v, want %v", delay, time.Second) - } -} - -func TestPressurePlateItemActivation(t *testing.T) { - itemEntity := fakeItemEntity{} - if power := (PressurePlate{Type: redstoneSourceStone}).entityPower(itemEntity); power != 0 { - t.Fatalf("stone pressure plate item power = %d, want 0", power) - } - if power := (PressurePlate{Type: redstoneSourceOak}).entityPower(itemEntity); power != 15 { - t.Fatalf("wood pressure plate item power = %d, want 15", power) - } - if power := (PressurePlate{Type: redstoneSourceLightWeighted}).entityPower(itemEntity); power != 1 { - t.Fatalf("light weighted pressure plate item power = %d, want 1", power) - } - if power := (PressurePlate{Type: redstoneSourceHeavyWeighted}).entityPower(fakeSnowballEntity{}); power != 0 { - t.Fatalf("heavy weighted pressure plate snowball power = %d, want 0", power) - } - if power := (PressurePlate{Type: redstoneSourceStone}).entityPower(fakeLivingEntity{health: 20}); power != 15 { - t.Fatalf("stone pressure plate living entity power = %d, want 15", power) - } - if power := (PressurePlate{Type: redstoneSourceStone}).entityPower(fakeLivingEntity{}); power != 0 { - t.Fatalf("stone pressure plate dead living entity power = %d, want 0", power) - } -} - -func TestPressurePlateDetectsEntityBoundingBoxOnEdge(t *testing.T) { - w := world.New() - defer func() { - _ = w.Close() - }() - - var err error - <-w.Exec(func(tx *world.Tx) { - pos := cube.Pos{0, 1, 0} - tx.AddEntity(world.EntitySpawnOpts{Position: mgl64.Vec3{1.2, 1.0625, 0.5}}.New(pressurePlateTestEntityType{name: "minecraft:player"}, pressurePlateTestEntityConfig{})) - - if power := (PressurePlate{Type: redstoneSourceStone}).detectPower(pos, tx); power != 15 { - err = fmt.Errorf("edge-overlapping pressure plate power = %d, want 15", power) - } - }) - if err != nil { - t.Fatal(err) - } -} - -func TestPressurePlateActivationBoxIsInset(t *testing.T) { - w := world.New() - defer func() { - _ = w.Close() - }() - - var err error - <-w.Exec(func(tx *world.Tx) { - pos := cube.Pos{0, 1, 0} - tx.AddEntity(world.EntitySpawnOpts{Position: mgl64.Vec3{-0.25, 1.0625, 0.5}}.New(pressurePlateTestEntityType{name: "minecraft:player"}, pressurePlateTestEntityConfig{})) - - if power := (PressurePlate{Type: redstoneSourceStone}).detectPower(pos, tx); power != 0 { - err = fmt.Errorf("rim-overlapping pressure plate power = %d, want 0", power) - } - }) - if err != nil { - t.Fatal(err) - } -} - -func TestWeightedPressurePlateCountsWorldEntities(t *testing.T) { - w := world.New() - defer func() { - _ = w.Close() - }() - - var err error - <-w.Exec(func(tx *world.Tx) { - pos := cube.Pos{0, 1, 0} - for i := 0; i < 11; i++ { - tx.AddEntity(world.EntitySpawnOpts{Position: mgl64.Vec3{0.5 + float64(i%3)*0.01, 1.0625, 0.5}}.New(pressurePlateTestEntityType{name: "minecraft:item"}, pressurePlateTestEntityConfig{})) - } - tx.AddEntity(world.EntitySpawnOpts{Position: mgl64.Vec3{0.5, 1.0625, 0.5}}.New(pressurePlateTestEntityType{name: "minecraft:snowball"}, pressurePlateTestEntityConfig{})) - - if power := (PressurePlate{Type: redstoneSourceLightWeighted}).detectPower(pos, tx); power != 11 { - err = fmt.Errorf("light weighted plate power = %d, want 11", power) - return - } - if power := (PressurePlate{Type: redstoneSourceHeavyWeighted}).detectPower(pos, tx); power != 2 { - err = fmt.Errorf("heavy weighted plate power = %d, want 2", power) - } - }) - if err != nil { - t.Fatal(err) - } -} - -func TestRedstoneSourceNames(t *testing.T) { - tests := map[int]string{ - redstoneSourceStone: "minecraft:stone_button", - redstoneSourcePolishedBlackstone: "minecraft:polished_blackstone_button", - redstoneSourceOak: "minecraft:wooden_button", - redstoneSourceMangrove: "minecraft:mangrove_button", - redstoneSourcePaleOak: "minecraft:pale_oak_button", - } - for typ, want := range tests { - if got, _ := (Button{Type: typ}).EncodeItem(); got != want { - t.Fatalf("button type %d encodes to %q, want %q", typ, got, want) - } - } - if got, _ := (PressurePlate{Type: redstoneSourceLightWeighted}).EncodeItem(); got != "minecraft:light_weighted_pressure_plate" { - t.Fatalf("light weighted pressure plate encodes to %q", got) - } - if got, _ := (PressurePlate{Type: redstoneSourceHeavyWeighted}).EncodeItem(); got != "minecraft:heavy_weighted_pressure_plate" { - t.Fatalf("heavy weighted pressure plate encodes to %q", got) - } -} - -type fakeItemEntity struct{} - -func (fakeItemEntity) Close() error { return nil } -func (fakeItemEntity) H() *world.EntityHandle { return nil } -func (fakeItemEntity) Position() mgl64.Vec3 { return mgl64.Vec3{} } -func (fakeItemEntity) Rotation() cube.Rotation { return cube.Rotation{} } -func (fakeItemEntity) Item() item.Stack { return item.Stack{} } - -type fakeSnowballEntity struct{ fakeItemEntity } - -func (fakeSnowballEntity) H() *world.EntityHandle { - return world.EntitySpawnOpts{}.New(pressurePlateTestEntityType{name: "minecraft:snowball"}, pressurePlateTestEntityConfig{}) -} - -type fakeLivingEntity struct { - fakeItemEntity - health float64 -} - -func (e fakeLivingEntity) Health() float64 { return e.health } -func (e fakeLivingEntity) Dead() bool { return e.health <= 0 } - -type pressurePlateTestEntityConfig struct{} - -func (pressurePlateTestEntityConfig) Apply(*world.EntityData) {} - -type pressurePlateTestEntityType struct { - name string -} - -func (pressurePlateTestEntityType) Open(_ *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { - return pressurePlateTestEntity{handle: handle, data: data} -} - -func (t pressurePlateTestEntityType) EncodeEntity() string { - if t.name != "" { - return t.name - } - return "minecraft:test_entity" -} -func (pressurePlateTestEntityType) BBox(world.Entity) cube.BBox { - return cube.Box(-0.3, 0, -0.3, 0.3, 1.8, 0.3) -} -func (pressurePlateTestEntityType) DecodeNBT(map[string]any, *world.EntityData) {} -func (pressurePlateTestEntityType) EncodeNBT(*world.EntityData) map[string]any { return nil } - -type pressurePlateTestEntity struct { - handle *world.EntityHandle - data *world.EntityData -} - -func (e pressurePlateTestEntity) Close() error { return nil } -func (e pressurePlateTestEntity) H() *world.EntityHandle { return e.handle } -func (e pressurePlateTestEntity) Position() mgl64.Vec3 { return e.data.Pos } -func (e pressurePlateTestEntity) Rotation() cube.Rotation { return e.data.Rot } - -func TestRedstoneSourceHashesIncludeMaterial(t *testing.T) { - _, stoneButton := (Button{Type: redstoneSourceStone, Facing: cube.FaceUp}).Hash() - _, oakButton := (Button{Type: redstoneSourceOak, Facing: cube.FaceUp}).Hash() - if stoneButton == oakButton { - t.Fatal("stone and oak buttons produced the same block hash") - } - - _, stonePlate := (PressurePlate{Type: redstoneSourceStone}).Hash() - _, oakPlate := (PressurePlate{Type: redstoneSourceOak}).Hash() - if stonePlate == oakPlate { - t.Fatal("stone and oak pressure plates produced the same block hash") - } -} diff --git a/server/block/redstone_test.go b/server/block/redstone_test.go index 56220ce63..7ac44d525 100644 --- a/server/block/redstone_test.go +++ b/server/block/redstone_test.go @@ -45,6 +45,43 @@ func TestRedstoneBlockPower(t *testing.T) { } } +func TestLeverPower(t *testing.T) { + if power := (Lever{}).RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 0 { + t.Fatalf("unpowered lever power = %d, want 0", power) + } + if power := (Lever{Powered: true}).RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 15 { + t.Fatalf("powered lever power = %d, want 15", power) + } + if power := (Lever{Facing: cube.FaceWest, Powered: true}).RedstoneStrongPower(cube.Pos{}, nil, cube.FaceEast); power != 15 { + t.Fatalf("attached-face lever strong power = %d, want 15", power) + } + if power := (Lever{Facing: cube.FaceWest, Powered: true}).RedstoneStrongPower(cube.Pos{}, nil, cube.FaceWest); power != 0 { + t.Fatalf("opposite-face lever strong power = %d, want 0", power) + } +} + +func TestLeverEncodeBlock(t *testing.T) { + tests := []struct { + name string + l Lever + want string + }{ + {name: "wall", l: Lever{Facing: cube.FaceEast}, want: "east"}, + {name: "floor east west", l: Lever{Facing: cube.FaceUp, Direction: cube.West}, want: "up_east_west"}, + {name: "floor north south", l: Lever{Facing: cube.FaceUp, Direction: cube.North}, want: "up_north_south"}, + {name: "ceiling east west", l: Lever{Facing: cube.FaceDown, Direction: cube.West}, want: "down_east_west"}, + {name: "ceiling north south", l: Lever{Facing: cube.FaceDown, Direction: cube.North}, want: "down_north_south"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, props := test.l.EncodeBlock() + if direction := props["lever_direction"]; direction != test.want { + t.Fatalf("lever_direction = %v, want %s", direction, test.want) + } + }) + } +} + func TestRedstoneWirePowerUpdate(t *testing.T) { wire := RedstoneWire{Power: 3} after, changed := wire.RedstonePowerUpdate(cube.Pos{}, nil, 12) @@ -69,6 +106,16 @@ func TestRedstoneWirePowerUpdate(t *testing.T) { } } +func TestRedstoneWireDoesNotPowerDown(t *testing.T) { + wire := RedstoneWire{Power: 15} + if power := wire.RedstonePower(cube.Pos{}, nil, cube.FaceDown); power != 0 { + t.Fatalf("wire downward power = %d, want 0", power) + } + if power := wire.RedstonePower(cube.Pos{}, nil, cube.FaceUp); power != 15 { + t.Fatalf("wire upward power = %d, want 15", power) + } +} + func TestRedstoneWireRequiresSolidSupport(t *testing.T) { w := world.New() defer func() { @@ -124,42 +171,6 @@ func TestRedstoneWireConnectsUpBlocks(t *testing.T) { } } -func TestRedstoneWireRelayerNeighboursFollowShape(t *testing.T) { - w := world.New() - defer func() { - _ = w.Close() - }() - - var err error - <-w.Exec(func(tx *world.Tx) { - pos := cube.Pos{0, 1, 0} - source := pos.Side(cube.FaceWest) - endLamp := pos.Side(cube.FaceEast) - sideLamp := pos.Side(cube.FaceNorth) - tx.SetBlock(pos.Side(cube.FaceDown), Stone{}, nil) - tx.SetBlock(source, RedstoneBlock{}, nil) - tx.SetBlock(pos, RedstoneWire{}, nil) - tx.SetBlock(endLamp, RedstoneLamp{}, nil) - tx.SetBlock(sideLamp, RedstoneLamp{}, nil) - - neighbours := (RedstoneWire{}).RedstoneRelayerNeighbours(pos, tx) - if !redstoneNeighbourTestContains(neighbours, source) { - err = fmt.Errorf("wire neighbours %v did not include source connection %v", neighbours, source) - return - } - if !redstoneNeighbourTestContains(neighbours, endLamp) { - err = fmt.Errorf("wire neighbours %v did not include line-end lamp %v", neighbours, endLamp) - return - } - if redstoneNeighbourTestContains(neighbours, sideLamp) { - err = fmt.Errorf("wire neighbours %v included side lamp %v", neighbours, sideLamp) - } - }) - if err != nil { - t.Fatal(err) - } -} - func TestRedstoneWireRelayerNeighboursDoNotLoadAdjacentChunks(t *testing.T) { w := world.New() defer func() { @@ -187,38 +198,6 @@ func TestRedstoneWireRelayerNeighboursDoNotLoadAdjacentChunks(t *testing.T) { } } -func TestRedstoneWirePowersEndLampButNotSideLamp(t *testing.T) { - w := world.New() - defer func() { - _ = w.Close() - }() - - var err error - <-w.Exec(func(tx *world.Tx) { - wire := cube.Pos{0, 1, 0} - endLamp := wire.Side(cube.FaceEast) - sideLamp := wire.Side(cube.FaceNorth) - tx.SetBlock(wire.Side(cube.FaceDown), Stone{}, nil) - tx.SetBlock(wire.Side(cube.FaceWest), RedstoneBlock{}, nil) - tx.SetBlock(wire, RedstoneWire{Power: 1}, nil) - tx.SetBlock(endLamp, RedstoneLamp{}, nil) - tx.SetBlock(sideLamp, RedstoneLamp{}, nil) - - after, changed := (RedstoneLamp{}).RedstonePowerUpdate(endLamp, tx, tx.RedstonePower(endLamp)) - if !changed || !after.(RedstoneLamp).Lit { - err = fmt.Errorf("strength-1 line dust did not light end lamp: changed=%t after=%#v", changed, after) - return - } - after, changed = (RedstoneLamp{}).RedstonePowerUpdate(sideLamp, tx, tx.RedstonePower(sideLamp)) - if changed || after.(RedstoneLamp).Lit { - err = fmt.Errorf("line dust powered side lamp: changed=%t after=%#v", changed, after) - } - }) - if err != nil { - t.Fatal(err) - } -} - func TestStrongPowerConductsThroughSolidBlocks(t *testing.T) { w := world.New() defer func() { @@ -265,58 +244,7 @@ func TestStrongPowerDoesNotConductThroughTransparentFullBlocks(t *testing.T) { } } -func TestRedstoneBlockDoesNotPowerLampThroughSolidBlock(t *testing.T) { - w := world.New() - defer func() { - _ = w.Close() - }() - - var err error - <-w.Exec(func(tx *world.Tx) { - source := cube.Pos{0, 1, 0} - conductor := source.Side(cube.FaceEast) - lampPos := conductor.Side(cube.FaceEast) - tx.SetBlock(source, RedstoneBlock{}, nil) - tx.SetBlock(conductor, Stone{}, nil) - tx.SetBlock(lampPos, RedstoneLamp{}, nil) - - power := tx.RedstonePower(lampPos) - if power != 0 { - err = fmt.Errorf("lamp power through opaque block = %d, want 0", power) - return - } - after, changed := (RedstoneLamp{}).RedstonePowerUpdate(lampPos, tx, power) - if changed || after.(RedstoneLamp).Lit { - err = fmt.Errorf("lamp lit from redstone block through opaque block") - } - }) - if err != nil { - t.Fatal(err) - } -} - -func TestRedstoneLampPowerUpdate(t *testing.T) { - after, changed := (RedstoneLamp{}).RedstonePowerUpdate(cube.Pos{}, nil, 15) - if !changed { - t.Fatal("RedstoneLamp update did not report a change") - } - if !after.(RedstoneLamp).Lit { - t.Fatal("RedstoneLamp did not become lit") - } - if after.(RedstoneLamp).LightEmissionLevel() != 15 { - t.Fatal("lit RedstoneLamp should emit light level 15") - } - - after, changed = (RedstoneLamp{Lit: true}).RedstonePowerUpdate(cube.Pos{}, nil, 0) - if !changed { - t.Fatal("RedstoneLamp unpower update did not report a change") - } - if after.(RedstoneLamp).Lit { - t.Fatal("RedstoneLamp did not turn off") - } -} - -func TestRedstoneLampDelayedTurnOff(t *testing.T) { +func TestRedstoneTorchBurnout(t *testing.T) { w := world.New() defer func() { _ = w.Close() @@ -325,60 +253,32 @@ func TestRedstoneLampDelayedTurnOff(t *testing.T) { var err error <-w.Exec(func(tx *world.Tx) { pos := cube.Pos{0, 1, 0} - lamp := RedstoneLamp{Lit: true} - tx.SetBlock(pos, lamp, nil) + tx.SetBlock(pos.Side(cube.FaceDown), RedstoneBlock{}, nil) + torch := RedstoneTorch{Facing: cube.FaceDown, Lit: true} + tx.SetBlock(pos, torch, nil) - after, changed := lamp.RedstonePowerUpdate(pos, tx, 0) - if changed { - err = fmt.Errorf("RedstoneLamp reported immediate off change: %#v", after) - return + for i := 0; i < 7; i++ { + if tx.RecordRedstoneTorchToggle(pos) { + err = fmt.Errorf("torch burned out after %d toggles, want below threshold", i+1) + return + } } - if !tx.Block(pos).(RedstoneLamp).Lit { - err = fmt.Errorf("RedstoneLamp turned off before scheduled tick") + torch.ScheduledTick(pos, tx, nil) + after, ok := tx.Block(pos).(RedstoneTorch) + if !ok { + err = fmt.Errorf("redstone torch missing after burnout tick") return } - tx.Block(pos).(RedstoneLamp).ScheduledTick(pos, tx, nil) - if tx.Block(pos).(RedstoneLamp).Lit { - err = fmt.Errorf("RedstoneLamp stayed lit after delayed off tick") - } - }) - if err != nil { - t.Fatal(err) - } -} - -func TestRedstoneLampRepowerCancelsTurnOff(t *testing.T) { - w := world.New() - defer func() { - _ = w.Close() - }() - - var err error - <-w.Exec(func(tx *world.Tx) { - pos := cube.Pos{0, 1, 0} - lamp := RedstoneLamp{Lit: true} - tx.SetBlock(pos, lamp, nil) - _, changed := lamp.RedstonePowerUpdate(pos, tx, 0) - if changed { - err = fmt.Errorf("RedstoneLamp reported immediate off change") + if after.Lit { + err = fmt.Errorf("redstone torch stayed lit after burnout") return } - tx.SetBlock(pos.Side(cube.FaceWest), RedstoneBlock{}, nil) - tx.Block(pos).(RedstoneLamp).ScheduledTick(pos, tx, nil) - if !tx.Block(pos).(RedstoneLamp).Lit { - err = fmt.Errorf("RedstoneLamp turned off after being repowered") + burnedOut, recoverable := tx.RedstoneTorchBurnoutStatus(pos) + if !burnedOut || recoverable { + err = fmt.Errorf("burnout status = burnedOut %t recoverable %t, want true false", burnedOut, recoverable) } }) if err != nil { t.Fatal(err) } } - -func redstoneNeighbourTestContains(neighbours []cube.Pos, pos cube.Pos) bool { - for _, neighbour := range neighbours { - if neighbour == pos { - return true - } - } - return false -} diff --git a/server/block/redstone_torch.go b/server/block/redstone_torch.go index e94571f53..a7bf4f317 100644 --- a/server/block/redstone_torch.go +++ b/server/block/redstone_torch.go @@ -6,6 +6,7 @@ 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" ) @@ -28,7 +29,10 @@ type RedstoneTorch struct { // BreakInfo ... func (t RedstoneTorch) BreakInfo() BreakInfo { - return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(t)) + return newBreakInfo(0, alwaysHarvestable, nothingEffective, oneOf(t)).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { + tx.ClearRedstoneTorchBurnout(pos) + tx.ScheduleRedstoneUpdate(pos) + }) } // LightEmissionLevel ... @@ -45,6 +49,9 @@ func (t RedstoneTorch) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx if !used || face == cube.FaceDown { return false } + if _, ok := tx.Block(pos).(world.Liquid); ok { + return false + } if !tx.Block(pos.Side(face.Opposite())).Model().FaceSolid(pos.Side(face.Opposite()), face, tx) { found := false for _, i := range []cube.Face{cube.FaceSouth, cube.FaceWest, cube.FaceNorth, cube.FaceEast, cube.FaceDown} { @@ -59,11 +66,13 @@ func (t RedstoneTorch) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx } } t.Facing = face.Opposite() - t.Lit = !t.attachmentPowered(pos, tx) + t.Lit = true place(tx, pos, t, user, ctx) if placed(ctx) { - tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) + if t.attachmentPowered(pos, tx) { + tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) + } } return placed(ctx) } @@ -71,9 +80,13 @@ func (t RedstoneTorch) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx // NeighbourUpdateTick breaks unsupported torches and otherwise schedules inverse-state refreshes. func (t RedstoneTorch) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { if !tx.Block(pos.Side(t.Facing)).Model().FaceSolid(pos.Side(t.Facing), t.Facing.Opposite(), tx) { + tx.ClearRedstoneTorchBurnout(pos) breakBlock(t, pos, tx) return } + if t.recoverFromBurnout(pos, tx) { + return + } tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) } @@ -82,11 +95,26 @@ func (t RedstoneTorch) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { if tx == nil { return } + torch, ok := tx.Block(pos).(RedstoneTorch) + if !ok { + tx.ClearRedstoneTorchBurnout(pos) + return + } + if burnedOut, _ := tx.RedstoneTorchBurnoutStatus(pos); burnedOut { + return + } + t = torch lit := !t.attachmentPowered(pos, tx) if t.Lit != lit { + if tx.RecordRedstoneTorchToggle(pos) { + t.burnOut(pos, tx) + return + } t.Lit = lit tx.SetBlock(pos, t, nil) + return } + tx.PruneRedstoneTorchBurnout(pos) } // RedstonePower emits power from every side except the attached block while lit. @@ -110,10 +138,41 @@ func (t RedstoneTorch) RedstonePowerUpdate(pos cube.Pos, tx *world.Tx, _ int) (w if tx == nil || t.Lit == !t.attachmentPowered(pos, tx) { return t, false } + if t.recoverFromBurnout(pos, tx) { + return t, false + } tx.ScheduleBlockUpdate(pos, t, redstoneTicks(1)) return t, false } +func (t RedstoneTorch) recoverFromBurnout(pos cube.Pos, tx *world.Tx) bool { + burnedOut, recoverable := tx.RedstoneTorchBurnoutStatus(pos) + if !burnedOut { + return false + } + if !recoverable { + return true + } + tx.ClearRedstoneTorchBurnout(pos) + live, ok := tx.Block(pos).(RedstoneTorch) + if !ok { + return true + } + lit := !live.attachmentPowered(pos, tx) + if live.Lit != lit { + live.Lit = lit + tx.SetBlock(pos, live, nil) + } + return true +} + +func (t RedstoneTorch) burnOut(pos cube.Pos, tx *world.Tx) { + tx.BurnOutRedstoneTorch(pos) + t.Lit = false + tx.PlaySound(pos.Vec3Centre(), sound.Fizz{}) + tx.SetBlock(pos, t, nil) +} + func (t RedstoneTorch) attachmentPowered(pos cube.Pos, tx *world.Tx) bool { if tx == nil { return false diff --git a/server/block/redstone_wire.go b/server/block/redstone_wire.go new file mode 100644 index 000000000..c9511bcb6 --- /dev/null +++ b/server/block/redstone_wire.go @@ -0,0 +1,286 @@ +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" +) + +// 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(RedstoneWire{})).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) { + tx.ScheduleRedstoneUpdate(pos) + }) +} + +// EncodeBlock ... +func (r RedstoneWire) EncodeBlock() (string, map[string]any) { + return "minecraft:redstone_wire", map[string]any{ + "redstone_signal": int32(redstonePower(r.Power)), + } +} + +// EncodeItem ... +func (RedstoneWire) EncodeItem() (name string, meta int16) { + return "minecraft:redstone", 0 +} + +// UseOnBlock ... +func (r RedstoneWire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(tx, pos, face, r) + if !used || !redstoneWireSupported(tx, pos) { + return false + } + r.Power = tx.RedstonePower(pos) + place(tx, pos, r, user, ctx) + return placed(ctx) +} + +// NeighbourUpdateTick ... +func (r RedstoneWire) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if !redstoneWireSupported(tx, pos) { + breakBlock(r, pos, tx) + } +} + +// RedstonePower returns the wire's current signal strength from connected faces. +func (r RedstoneWire) RedstonePower(pos cube.Pos, tx *world.Tx, face cube.Face) int { + if face == cube.FaceDown { + return 0 + } + if tx != nil && redstoneWireFaceHorizontal(face) && !redstoneWirePowersHorizontalFace(pos, tx, face) { + return 0 + } + return redstonePower(r.Power) +} + +// RedstoneSignalLoss returns the signal loss through a wire segment. +func (RedstoneWire) RedstoneSignalLoss(cube.Pos, *world.Tx, cube.Face, cube.Face) int { + return 1 +} + +// RedstoneRelayerNeighbours returns all wire positions directly connected to this dust, including dust stepping up or +// down adjacent blocks. +func (RedstoneWire) RedstoneRelayerNeighbours(pos cube.Pos, tx *world.Tx) []cube.Pos { + neighbours := make([]cube.Pos, 0, 12) + faces := redstoneWirePoweredHorizontalFaces(pos, tx) + for _, face := range cube.HorizontalFaces() { + if !faces[face] { + continue + } + side := pos.Side(face) + if side.OutOfBounds(tx.Range()) { + continue + } + positions := redstoneWireHorizontalConnectionPositions(pos, tx, face) + if len(positions) != 0 { + neighbours = append(neighbours, positions...) + continue + } + if redstoneWireRelevantLoaded(tx, side) { + neighbours = append(neighbours, side) + } + } + return neighbours +} + +// RedstonePowerUpdate updates the wire strength to match its strongest input. +func (r RedstoneWire) RedstonePowerUpdate(_ cube.Pos, _ *world.Tx, power int) (world.Block, bool) { + power = redstonePower(power) + if r.Power == power { + return r, false + } + r.Power = power + return r, true +} + +func redstoneWireSupported(tx *world.Tx, pos cube.Pos) bool { + below := pos.Side(cube.FaceDown) + if below.OutOfBounds(tx.Range()) { + return false + } + return tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) +} + +func redstoneWireSupportedLoaded(tx *world.Tx, pos cube.Pos) bool { + below := pos.Side(cube.FaceDown) + if below.OutOfBounds(tx.Range()) { + return false + } + b, ok := tx.BlockLoaded(below) + return ok && b.Model().FaceSolid(below, cube.FaceUp, tx) +} + +func redstoneWireBlocksConnectionLoaded(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + if pos.OutOfBounds(tx.Range()) { + return true + } + b, ok := tx.BlockLoaded(pos) + return ok && b.Model().FaceSolid(pos, face, tx) +} + +func redstoneWirePowersHorizontalFace(pos cube.Pos, tx *world.Tx, face cube.Face) bool { + return redstoneWirePoweredHorizontalFaces(pos, tx)[face] +} + +func redstoneWirePoweredHorizontalFaces(pos cube.Pos, tx *world.Tx) map[cube.Face]bool { + connections := make(map[cube.Face]bool, len(cube.HorizontalFaces())) + for _, face := range cube.HorizontalFaces() { + if len(redstoneWireHorizontalConnectionPositions(pos, tx, face)) != 0 { + connections[face] = true + } + } + switch len(connections) { + case 0: + for _, face := range cube.HorizontalFaces() { + connections[face] = true + } + case 1: + for face := range connections { + connections[face.Opposite()] = true + } + } + return connections +} + +func redstoneWireHorizontalConnectionPositions(pos cube.Pos, tx *world.Tx, face cube.Face) []cube.Pos { + side := pos.Side(face) + if side.OutOfBounds(tx.Range()) { + return nil + } + positions := make([]cube.Pos, 0, 3) + if redstoneWireDirectConnectionLoaded(tx, side, face.Opposite()) { + positions = append(positions, side) + } + + above := pos.Side(cube.FaceUp) + sideAbove := side.Side(cube.FaceUp) + if !redstoneWireBlocksConnectionLoaded(tx, above, cube.FaceDown) && redstoneWireAtLoaded(tx, sideAbove) && redstoneWireSupportedLoaded(tx, sideAbove) { + positions = append(positions, sideAbove) + } + if !redstoneWireBlocksConnectionLoaded(tx, side, cube.FaceUp) { + down := side.Side(cube.FaceDown) + if !down.OutOfBounds(tx.Range()) && redstoneWireAtLoaded(tx, down) { + positions = append(positions, down) + } + } + return positions +} + +func redstoneWireDirectConnectionLoaded(tx *world.Tx, pos cube.Pos, face cube.Face) bool { + b, ok := tx.BlockLoaded(pos) + if !ok { + return false + } + if _, ok := b.(RedstoneWire); ok { + return true + } + if _, ok := b.(world.RedstonePowerSource); ok { + return true + } + if _, ok := b.(world.RedstoneStrongPowerSource); ok { + return true + } + if _, ok := b.(world.RedstonePowerRelayer); ok { + return true + } + return redstoneWireNonSolidComponent(pos, b, tx, face) +} + +func redstoneWireNonSolidComponent(pos cube.Pos, b world.Block, tx *world.Tx, face cube.Face) bool { + model := b.Model() + if model == nil || model.FaceSolid(pos, face, tx) { + return false + } + if _, ok := b.(world.RedstonePowerConsumer); ok { + return true + } + if _, ok := b.(world.RedstonePowerTransitionConsumer); ok { + return true + } + if _, ok := b.(world.RedstonePowerAction); ok { + return true + } + return false +} + +func redstoneWireAtLoaded(tx *world.Tx, pos cube.Pos) bool { + b, ok := tx.BlockLoaded(pos) + if !ok { + return false + } + _, ok = b.(RedstoneWire) + return ok +} + +func redstoneWireRelevantLoaded(tx *world.Tx, pos cube.Pos) bool { + b, ok := tx.BlockLoaded(pos) + return ok && redstoneWireRelevant(b) +} + +func redstoneWireRelevant(b world.Block) bool { + if _, ok := b.(world.RedstonePowerSource); ok { + return true + } + if _, ok := b.(world.RedstoneStrongPowerSource); ok { + return true + } + if _, ok := b.(world.RedstonePowerRelayer); ok { + return true + } + if _, ok := b.(world.RedstonePowerConsumer); ok { + return true + } + if _, ok := b.(world.RedstonePowerTransitionConsumer); ok { + return true + } + if _, ok := b.(world.RedstonePowerAction); ok { + return true + } + return false +} + +func redstoneWireFaceHorizontal(face cube.Face) bool { + switch face { + case cube.FaceNorth, cube.FaceSouth, cube.FaceWest, cube.FaceEast: + return true + default: + return false + } +} + +// TrimMaterial delegates to item.RedstoneWire so the block form stays valid for smithing trim decoding too. +func (RedstoneWire) TrimMaterial() string { + return item.RedstoneWire{}.TrimMaterial() +} + +// MaterialColour delegates to item.RedstoneWire to keep trim metadata defined in one place. +func (RedstoneWire) MaterialColour() string { + return item.RedstoneWire{}.MaterialColour() +} + +// allRedstoneWires returns a list of all redstone dust states. +func allRedstoneWires() (all []world.Block) { + for i := range 16 { + all = append(all, RedstoneWire{Power: i}) + } + return +} diff --git a/server/block/register.go b/server/block/register.go index fcedc53dd..116c454e6 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -147,7 +147,6 @@ func init() { registerAll(allBlastFurnaces()) registerAll(allBoneBlock()) registerAll(allBrewingStands()) - registerAll(allButtons()) registerAll(allCactus()) registerAll(allCake()) registerAll(allCampfires()) @@ -197,13 +196,11 @@ func init() { registerAll(allPinkPetals()) registerAll(allPlanks()) registerAll(allPotato()) - registerAll(allPressurePlates()) registerAll(allPrismarine()) registerAll(allPumpkinStems()) registerAll(allPumpkins()) registerAll(allPurpurs()) registerAll(allQuartz()) - registerAll(allRedstoneLamps()) registerAll(allRedstoneTorches()) registerAll(allRedstoneWires()) registerAll(allSandstones()) @@ -259,7 +256,6 @@ func init() { world.RegisterItem(Bookshelf{}) world.RegisterItem(BrewingStand{}) world.RegisterItem(Bricks{}) - world.RegisterItem(Button{Type: redstoneSourceStone}) world.RegisterItem(Cactus{}) world.RegisterItem(Cake{}) world.RegisterItem(Calcite{}) @@ -357,7 +353,6 @@ func init() { world.RegisterItem(PolishedBlackstoneBrick{Cracked: true}) world.RegisterItem(PolishedBlackstoneBrick{}) world.RegisterItem(Potato{}) - world.RegisterItem(PressurePlate{Type: redstoneSourceStone}) world.RegisterItem(PumpkinSeeds{}) world.RegisterItem(Pumpkin{Carved: true}) world.RegisterItem(Pumpkin{}) @@ -374,7 +369,6 @@ func init() { world.RegisterItem(RedstoneTorch{}) world.RegisterItem(RedstoneWire{}) world.RegisterItem(ReinforcedDeepslate{}) - world.RegisterItem(RedstoneLamp{}) world.RegisterItem(ResinBricks{Chiseled: true}) world.RegisterItem(ResinBricks{}) world.RegisterItem(Resin{}) @@ -514,31 +508,6 @@ func init() { for _, t := range DeepslateTypes() { world.RegisterItem(Deepslate{Type: t}) } - for _, typ := range []int{ - redstoneSourcePolishedBlackstone, - } { - world.RegisterItem(Button{Type: typ}) - world.RegisterItem(PressurePlate{Type: typ}) - } - world.RegisterItem(PressurePlate{Type: redstoneSourceLightWeighted}) - world.RegisterItem(PressurePlate{Type: redstoneSourceHeavyWeighted}) - for _, typ := range []int{ - redstoneSourceOak, - redstoneSourceSpruce, - redstoneSourceBirch, - redstoneSourceJungle, - redstoneSourceAcacia, - redstoneSourceDarkOak, - redstoneSourceMangrove, - redstoneSourceCherry, - redstoneSourceBamboo, - redstoneSourceCrimson, - redstoneSourceWarped, - redstoneSourcePaleOak, - } { - world.RegisterItem(Button{Type: typ}) - world.RegisterItem(PressurePlate{Type: typ}) - } for _, o := range OxidationTypes() { world.RegisterItem(CopperBars{Oxidation: o}) world.RegisterItem(CopperBars{Oxidation: o, Waxed: true}) diff --git a/server/session/world.go b/server/session/world.go index 801c496ca..1ecdfb728 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -550,14 +550,6 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) Position: vec64To32(pos), }) return - case sound.PistonIn: - pk.SoundType = packet.SoundEventPistonIn - case sound.PistonOut: - pk.SoundType = packet.SoundEventPistonOut - case sound.PressurePlateClickOn: - pk.SoundType = packet.SoundEventPressurePlateClickOn - case sound.PressurePlateClickOff: - pk.SoundType = packet.SoundEventPressurePlateClickOff case sound.SignWaxed: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventWaxOn, diff --git a/server/world/redstone.go b/server/world/redstone.go index 24f55f395..31ca73970 100644 --- a/server/world/redstone.go +++ b/server/world/redstone.go @@ -18,8 +18,6 @@ const ( RedstoneUpdateCauseBlockUpdate RedstoneUpdateCause = iota // RedstoneUpdateCauseScheduledTick means a scheduled redstone tick invalidated a component. RedstoneUpdateCauseScheduledTick - // RedstoneUpdateCauseCompilerRebuild means a redstone compiler rebuild invalidated a component. - RedstoneUpdateCauseCompilerRebuild ) // RedstoneUpdate represents a redstone state transition proposed by the world redstone engine. Handlers may cancel @@ -101,11 +99,6 @@ type RedstonePowerAction interface { RedstonePowerAction(pos cube.Pos, tx *Tx, oldPower, newPower int) bool } -// RedstoneComparatorReadable is implemented by blocks that expose an analog signal to a comparator. -type RedstoneComparatorReadable interface { - RedstoneComparatorOutput(pos cube.Pos, tx *Tx, face cube.Face) int -} - type redstoneEngine struct { currentTick int64 dirty map[cube.Pos]redstoneDirty @@ -113,6 +106,7 @@ type redstoneEngine struct { output map[cube.Pos]int evaluating map[cube.Pos]struct{} suppressedSources map[cube.Pos]int + torchBurnout map[cube.Pos]redstoneTorchBurnout } type redstoneDirty struct { @@ -181,6 +175,9 @@ func (e *redstoneEngine) removeChunk(chunkPos ChunkPos) { maps.DeleteFunc(e.output, func(pos cube.Pos, _ int) bool { return chunkPosFromBlockPos(pos) == chunkPos }) + maps.DeleteFunc(e.torchBurnout, func(pos cube.Pos, _ redstoneTorchBurnout) bool { + return chunkPosFromBlockPos(pos) == chunkPos + }) } func (e *redstoneEngine) forget(pos cube.Pos) { @@ -276,11 +273,11 @@ func (e *redstoneEngine) compileRegion(tx *Tx, pos cube.Pos, seen map[cube.Pos]s if !relayer { continue } - e.redstoneRelayerNeighbours(tx, p, b, func(neighbour cube.Pos) { + for _, neighbour := range e.redstoneRelayerNeighbourPositions(tx, p, b) { if b, ok := tx.World().blockLoaded(neighbour); ok && isRedstoneRelevant(b) { queue = append(queue, neighbour) } - }) + } } } @@ -290,39 +287,23 @@ func (e *redstoneEngine) update(tx *Tx, pos, changed cube.Pos, cause RedstoneUpd action, hasAction := b.(RedstonePowerAction) actionChanged := hasAction && oldPower != newPower - if oldPower != newPower { - update := RedstoneUpdate{ - Pos: pos, - ChangedNeighbour: changed, - Before: b, - OldPower: oldPower, - NewPower: newPower, - CurrentTick: e.currentTick, - NetworkID: graphID, - Cause: cause, - } - if !e.redstoneUpdateAllowed(tx, update) { - return - } - } - after, blockChanged := b, false if consumer, ok := b.(RedstonePowerTransitionConsumer); ok { after, blockChanged = consumer.RedstonePowerTransitionUpdate(pos, tx, oldPower, newPower) } else if consumer, ok := b.(RedstonePowerConsumer); ok { after, blockChanged = consumer.RedstonePowerUpdate(pos, tx, newPower) } - if !blockChanged && !actionChanged { - e.power[pos] = newPower - return - } - if oldPower == newPower { + if oldPower != newPower || blockChanged || actionChanged { + var updateAfter Block + if blockChanged { + updateAfter = after + } update := RedstoneUpdate{ Pos: pos, ChangedNeighbour: changed, Before: b, - After: after, + After: updateAfter, OldPower: oldPower, NewPower: newPower, CurrentTick: e.currentTick, @@ -333,6 +314,12 @@ func (e *redstoneEngine) update(tx *Tx, pos, changed cube.Pos, cause RedstoneUpd return } } + + if !blockChanged && !actionChanged { + e.power[pos] = newPower + return + } + if blockChanged { tx.SetBlock(pos, after, &SetOpts{DisableRedstoneUpdates: true}) e.invalidateAround(pos, pos, RedstoneUpdateCauseBlockUpdate, tx.Range()) @@ -677,11 +664,7 @@ func redstoneStrongPowerConductor(pos cube.Pos, b Block, tx *Tx, face cube.Face) func redstoneExplicitNonConductor(b Block) bool { name, _ := b.EncodeBlock() - switch name { - case "minecraft:redstone_block", "minecraft:piston", "minecraft:sticky_piston", "minecraft:piston_arm", "minecraft:observer": - return true - } - return false + return name == "minecraft:redstone_block" } func (e *redstoneEngine) compileEdges(tx *Tx, nodes []redstoneNode) []redstoneEdge { diff --git a/server/world/redstone_test.go b/server/world/redstone_test.go index d9d94f2e2..d9688aea0 100644 --- a/server/world/redstone_test.go +++ b/server/world/redstone_test.go @@ -100,6 +100,23 @@ func TestRedstoneRelayerNeighbourPositionsAreDeterministic(t *testing.T) { } } +func TestRedstoneCompileRegionIncludesCustomRelayerNeighbours(t *testing.T) { + pos, step := cube.Pos{0, 64, 0}, cube.Pos{1, 65, 0} + w := Config{Blocks: redstoneCustomRelayerTestRegistry()}.New() + defer w.Close() + + var nodes []redstoneNode + <-w.Exec(func(tx *Tx) { + tx.SetBlock(pos, redstoneStepRelayer{}, nil) + tx.SetBlock(step, redstoneStepRelayer{}, nil) + + tx.World().redstone.compileRegion(tx, pos, make(map[cube.Pos]struct{}), &nodes) + }) + if !redstoneNodeTestContains(nodes, step) { + t.Fatalf("compiled region nodes = %v, want custom neighbour %v included", nodes, step) + } +} + func TestRedstoneGraphID(t *testing.T) { if got := redstoneGraphID(nil, nil); got != 0 { t.Fatalf("redstoneGraphID(nil) = %d, want 0", got) @@ -144,22 +161,40 @@ func TestRedstoneGraphID(t *testing.T) { } } -func TestRedstoneStrongPowerConductorExcludesExplicitNonConductors(t *testing.T) { +func TestRedstoneStrongPowerConductorExcludesRedstoneBlock(t *testing.T) { pos := cube.Pos{0, 64, 0} if !redstoneStrongPowerConductor(pos, redstoneNamedSolidBlock{name: "minecraft:stone"}, nil, cube.FaceWest) { t.Fatal("stone was not treated as a strong-power conductor") } - for _, name := range []string{ - "minecraft:redstone_block", - "minecraft:piston", - "minecraft:sticky_piston", - "minecraft:piston_arm", - "minecraft:observer", - } { - if redstoneStrongPowerConductor(pos, redstoneNamedSolidBlock{name: name}, nil, cube.FaceWest) { - t.Fatalf("%s was treated as a strong-power conductor", name) + if redstoneStrongPowerConductor(pos, redstoneNamedSolidBlock{name: "minecraft:redstone_block"}, nil, cube.FaceWest) { + t.Fatal("redstone block was treated as a strong-power conductor") + } +} + +func TestRedstoneTorchBurnoutState(t *testing.T) { + engine := newRedstoneEngine(0) + pos := cube.Pos{0, 64, 0} + for i := range redstoneTorchBurnoutThreshold - 1 { + if engine.recordRedstoneTorchToggle(pos, int64(i)) { + t.Fatalf("torch burned out after %d toggles, want below threshold", i+1) } } + if !engine.recordRedstoneTorchToggle(pos, redstoneTorchBurnoutThreshold-1) { + t.Fatal("torch did not burn out at threshold") + } + burnedOut, recoverable := engine.redstoneTorchBurnoutStatus(pos, redstoneTorchBurnoutThreshold) + if !burnedOut || recoverable { + t.Fatalf("burnout status = burnedOut %t recoverable %t, want true false", burnedOut, recoverable) + } + burnedOut, recoverable = engine.redstoneTorchBurnoutStatus(pos, redstoneTorchBurnoutWindowTicks+redstoneTorchBurnoutThreshold) + if !burnedOut || !recoverable { + t.Fatalf("expired burnout status = burnedOut %t recoverable %t, want true true", burnedOut, recoverable) + } + engine.clearRedstoneTorchBurnout(pos) + burnedOut, recoverable = engine.redstoneTorchBurnoutStatus(pos, redstoneTorchBurnoutWindowTicks+redstoneTorchBurnoutThreshold) + if burnedOut || recoverable { + t.Fatalf("cleared burnout status = burnedOut %t recoverable %t, want false false", burnedOut, recoverable) + } } func TestRedstoneEngineInvalidateAround(t *testing.T) { @@ -222,11 +257,6 @@ func TestRedstoneCancelledConsumerDoesNotUpdate(t *testing.T) { w := Config{Blocks: redstoneCancellationTestRegistry()}.New() defer w.Close() - consumerUpdates := 0 - redstoneCancellationConsumerUpdates = &consumerUpdates - t.Cleanup(func() { - redstoneCancellationConsumerUpdates = nil - }) w.Handle(&redstoneCancellationHandler{cancel: map[cube.Pos]struct{}{sinkPos: {}}}) var sinkPowered bool <-w.Exec(func(tx *Tx) { @@ -236,9 +266,6 @@ func TestRedstoneCancelledConsumerDoesNotUpdate(t *testing.T) { sinkPowered = tx.Block(sinkPos).(redstoneCancellationConsumer).Powered }) - if consumerUpdates != 0 { - t.Fatalf("consumer updates = %d, want 0", consumerUpdates) - } if sinkPowered { t.Fatalf("sink powered after cancelling consumer update") } @@ -290,7 +317,36 @@ func TestRedstoneRelayerToSinkDoesNotLosePower(t *testing.T) { } } -func TestScheduledTickQueueKeepsLaterTickForSameBlock(t *testing.T) { +func TestRedstoneUpdateIncludesAfterForConsumerStateChange(t *testing.T) { + sourcePos, sinkPos := cube.Pos{0, 64, 0}, cube.Pos{1, 64, 0} + w := Config{Blocks: redstoneCancellationTestRegistry()}.New() + defer w.Close() + + handler := &redstoneRecordingHandler{} + w.Handle(handler) + <-w.Exec(func(tx *Tx) { + tx.SetBlock(sourcePos, redstoneCancellationSource{Power: 15}, nil) + tx.SetBlock(sinkPos, redstoneCancellationConsumer{}, nil) + tx.World().redstone.tick(tx, 1) + }) + + for _, update := range handler.updates { + if update.Pos != sinkPos { + continue + } + after, ok := update.After.(redstoneCancellationConsumer) + if !ok { + t.Fatalf("consumer update After = %#v, want redstoneCancellationConsumer", update.After) + } + if !after.Powered { + t.Fatalf("consumer update After.Powered = false, want true") + } + return + } + t.Fatalf("no redstone update recorded for sink %v; updates=%v", sinkPos, handler.updates) +} + +func TestScheduledTickQueueKeepsLatestTickForSameBlock(t *testing.T) { queue := newScheduledTickQueue(100) pos := cube.Pos{8, 64, 8} b := scheduledTickTestBlock{} @@ -303,12 +359,28 @@ func TestScheduledTickQueueKeepsLaterTickForSameBlock(t *testing.T) { t.Fatalf("furthest tick = %d, want %d", got, want) } ticks := queue.fromChunk(chunkPosFromBlockPos(pos)) - if len(ticks) != 2 { - t.Fatalf("active ticks = %v, want two ticks", ticks) + if len(ticks) != 1 { + t.Fatalf("active ticks = %v, want one latest tick", ticks) } - got, want := []int64{ticks[0].t, ticks[1].t}, []int64{101, 102} - if !slices.Equal(got, want) { - t.Fatalf("fromChunk ticks = %v, want %v", got, want) + if got, want := ticks[0].t, int64(102); got != want { + t.Fatalf("fromChunk tick = %d, want %d", got, want) + } +} + +func TestScheduledTickQueueFromChunkOmitsSupersededTicks(t *testing.T) { + queue := newScheduledTickQueue(100) + pos := cube.Pos{8, 64, 8} + b := scheduledTickTestBlock{} + hash := DefaultBlockRegistry.BlockHash(b) + queue.ticks = []scheduledTick{ + {pos: pos, t: 101, b: b, bhash: hash}, + {pos: pos, t: 102, b: b, bhash: hash}, + } + queue.furthestTicks[scheduledTickIndex{pos: pos, hash: hash}] = 102 + + ticks := queue.fromChunk(chunkPosFromBlockPos(pos)) + if len(ticks) != 1 || ticks[0].t != 102 { + t.Fatalf("fromChunk active ticks = %v, want only tick 102", ticks) } } @@ -450,6 +522,39 @@ func (redstoneNeighbourOrderTestBlock) EncodeBlock() (string, map[string]any) { func (redstoneNeighbourOrderTestBlock) Hash() (uint64, uint64) { return 1 << 41, 0 } func (redstoneNeighbourOrderTestBlock) Model() BlockModel { return nil } +type redstoneStepRelayer struct{} + +func (redstoneStepRelayer) RedstoneRelayerNeighbours(pos cube.Pos, _ *Tx) []cube.Pos { + return []cube.Pos{ + {pos[0] + 1, pos[1] + 1, pos[2]}, + {pos[0] - 1, pos[1] - 1, pos[2]}, + } +} +func (redstoneStepRelayer) RedstoneSignalLoss(cube.Pos, *Tx, cube.Face, cube.Face) int { + return 1 +} +func (redstoneStepRelayer) EncodeBlock() (string, map[string]any) { + return "test:redstone_step_relayer", nil +} +func (redstoneStepRelayer) Hash() (uint64, uint64) { return 1 << 42, 0 } +func (redstoneStepRelayer) Model() BlockModel { return redstoneCancellationModel{} } + +func redstoneCustomRelayerTestRegistry() BlockRegistry { + registry := NewBlockRegistry() + registry.RegisterBlockState(BlockState{Name: "test:redstone_step_relayer", Properties: map[string]any{}}) + registry.RegisterBlock(redstoneStepRelayer{}) + return registry +} + +func redstoneNodeTestContains(nodes []redstoneNode, pos cube.Pos) bool { + for _, node := range nodes { + if node.pos == pos { + return true + } + } + return false +} + type redstoneCancellationHandler struct { NopHandler cancel map[cube.Pos]struct{} @@ -461,6 +566,15 @@ func (h *redstoneCancellationHandler) HandleRedstoneUpdate(ctx *Context, update } } +type redstoneRecordingHandler struct { + NopHandler + updates []RedstoneUpdate +} + +func (h *redstoneRecordingHandler) HandleRedstoneUpdate(_ *Context, update RedstoneUpdate) { + h.updates = append(h.updates, update) +} + var ( redstoneCancellationConsumerUpdates *int redstoneCancellationActions *int @@ -492,7 +606,7 @@ func (b redstoneCancellationSource) EncodeBlock() (string, map[string]any) { return "test:redstone_source", map[string]any{"power": int32(b.Power)} } func (b redstoneCancellationSource) Hash() (uint64, uint64) { - return 1 << 42, uint64(b.Power) + return 1 << 43, uint64(b.Power) } func (redstoneCancellationSource) Model() BlockModel { return redstoneCancellationModel{} } @@ -516,9 +630,9 @@ func (b redstoneCancellationConsumer) EncodeBlock() (string, map[string]any) { } func (b redstoneCancellationConsumer) Hash() (uint64, uint64) { if b.Powered { - return 1 << 43, 1 + return 1 << 44, 1 } - return 1 << 43, 0 + return 1 << 44, 0 } func (redstoneCancellationConsumer) Model() BlockModel { return redstoneCancellationModel{} } @@ -533,7 +647,7 @@ func (redstoneCancellationAction) RedstonePowerAction(cube.Pos, *Tx, int, int) b func (redstoneCancellationAction) EncodeBlock() (string, map[string]any) { return "test:redstone_action", nil } -func (redstoneCancellationAction) Hash() (uint64, uint64) { return 1 << 44, 0 } +func (redstoneCancellationAction) Hash() (uint64, uint64) { return 1 << 45, 0 } func (redstoneCancellationAction) Model() BlockModel { return redstoneCancellationModel{} } type redstoneCancellationModel struct{} diff --git a/server/world/redstone_torch.go b/server/world/redstone_torch.go new file mode 100644 index 000000000..4ff6f2be6 --- /dev/null +++ b/server/world/redstone_torch.go @@ -0,0 +1,93 @@ +package world + +import ( + "slices" + + "github.com/df-mc/dragonfly/server/block/cube" +) + +const ( + // redstoneTorchBurnoutThreshold is the maximum number of torch state changes allowed before burnout occurs. + redstoneTorchBurnoutThreshold = 8 + // redstoneTorchBurnoutWindowTicks is the window during which torch state changes are counted. + redstoneTorchBurnoutWindowTicks = 60 +) + +type redstoneTorchBurnout struct { + expirationTicks []int64 + burnedOut bool +} + +func (e *redstoneEngine) redstoneTorchBurnoutStatus(pos cube.Pos, currentTick int64) (burnedOut, recoverable bool) { + data, ok := e.pruneRedstoneTorchBurnoutData(pos, currentTick) + if !ok { + return false, false + } + return data.burnedOut, len(data.expirationTicks) < redstoneTorchBurnoutThreshold +} + +func (e *redstoneEngine) pruneRedstoneTorchBurnout(pos cube.Pos, currentTick int64) { + data, ok := e.torchBurnout[pos] + if !ok { + return + } + data.removeExpired(currentTick) + if len(data.expirationTicks) == 0 && !data.burnedOut { + e.clearRedstoneTorchBurnout(pos) + return + } + e.torchBurnout[pos] = data +} + +func (e *redstoneEngine) recordRedstoneTorchToggle(pos cube.Pos, currentTick int64) (burnsOut bool) { + data := e.redstoneTorchBurnoutData(pos) + data.removeExpired(currentTick) + data.expirationTicks = append(data.expirationTicks, currentTick+redstoneTorchBurnoutWindowTicks) + if len(data.expirationTicks) >= redstoneTorchBurnoutThreshold { + data.burnedOut = true + burnsOut = true + } + e.torchBurnout[pos] = data + return burnsOut +} + +func (e *redstoneEngine) burnOutRedstoneTorch(pos cube.Pos) { + data := e.redstoneTorchBurnoutData(pos) + data.burnedOut = true + e.torchBurnout[pos] = data +} + +func (e *redstoneEngine) clearRedstoneTorchBurnout(pos cube.Pos) { + delete(e.torchBurnout, pos) +} + +func (e *redstoneEngine) redstoneTorchBurnoutData(pos cube.Pos) redstoneTorchBurnout { + if e.torchBurnout == nil { + e.torchBurnout = make(map[cube.Pos]redstoneTorchBurnout) + } + data, ok := e.torchBurnout[pos] + if !ok { + data.expirationTicks = make([]int64, 0, redstoneTorchBurnoutThreshold+1) + } + return data +} + +func (e *redstoneEngine) pruneRedstoneTorchBurnoutData(pos cube.Pos, currentTick int64) (redstoneTorchBurnout, bool) { + data, ok := e.torchBurnout[pos] + if !ok { + return redstoneTorchBurnout{}, false + } + data.removeExpired(currentTick) + if len(data.expirationTicks) == 0 && !data.burnedOut { + e.clearRedstoneTorchBurnout(pos) + return redstoneTorchBurnout{}, false + } + e.torchBurnout[pos] = data + return data, true +} + +func (data *redstoneTorchBurnout) removeExpired(currentTick int64) { + data.expirationTicks = slices.DeleteFunc(data.expirationTicks, func(t int64) bool { + return t < currentTick + }) +} diff --git a/server/world/sound/block.go b/server/world/sound/block.go index 15aebeae3..fc62f59ab 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -120,18 +120,6 @@ type DoorCrash struct{ sound } // Click is a clicking sound. type Click struct{ sound } -// PistonIn is played when a piston retracts. -type PistonIn struct{ sound } - -// PistonOut is played when a piston extends. -type PistonOut struct{ sound } - -// PressurePlateClickOn is played when a pressure plate starts emitting power. -type PressurePlateClickOn struct{ sound } - -// PressurePlateClickOff is played when a pressure plate stops emitting power. -type PressurePlateClickOff struct{ sound } - // Ignite is a sound played when using a flint & steel. type Ignite struct{ sound } diff --git a/server/world/tick.go b/server/world/tick.go index 1c7f52e6b..e9a2fa37b 100644 --- a/server/world/tick.go +++ b/server/world/tick.go @@ -272,6 +272,7 @@ type scheduledTickQueue struct { ticks []scheduledTick furthestTicks map[scheduledTickIndex]int64 currentTick int64 + ticking bool } type scheduledTick struct { @@ -296,14 +297,14 @@ func newScheduledTickQueue(tick int64) *scheduledTickQueue { // queue. func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) { queue.currentTick = tick + queue.ticking = true w := tx.World() for _, t := range queue.ticks { if t.t > tick { continue } - index := scheduledTickIndex{pos: t.pos, hash: t.bhash} - if furthest, ok := queue.furthestTicks[index]; ok && furthest > t.t { + if !queue.active(t) { continue } b := tx.Block(t.pos) @@ -315,10 +316,11 @@ func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) { } } } + queue.ticking = false // Clear scheduled ticks that were processed from the queue. queue.ticks = slices.DeleteFunc(queue.ticks, func(t scheduledTick) bool { - return t.t <= tick + return t.t <= tick || !queue.active(t) }) maps.DeleteFunc(queue.furthestTicks, func(index scheduledTickIndex, t int64) bool { return t <= tick @@ -336,6 +338,11 @@ func (queue *scheduledTickQueue) schedule(br BlockRegistry, pos cube.Pos, b Bloc return } queue.furthestTicks[index] = resTick + if !queue.ticking { + queue.ticks = slices.DeleteFunc(queue.ticks, func(t scheduledTick) bool { + return t.pos == pos && t.bhash == index.hash + }) + } queue.ticks = append(queue.ticks, scheduledTick{pos: pos, t: resTick, b: b, bhash: index.hash}) } @@ -343,7 +350,7 @@ func (queue *scheduledTickQueue) schedule(br BlockRegistry, pos cube.Pos, b Bloc func (queue *scheduledTickQueue) fromChunk(pos ChunkPos) []scheduledTick { m := make([]scheduledTick, 0, 8) for _, t := range queue.ticks { - if pos == chunkPosFromBlockPos(t.pos) { + if pos == chunkPosFromBlockPos(t.pos) && queue.active(t) { m = append(m, t) } } @@ -372,4 +379,13 @@ func (queue *scheduledTickQueue) add(ticks []scheduledTick) { queue.furthestTicks[index] = t.t } } + queue.ticks = slices.DeleteFunc(queue.ticks, func(t scheduledTick) bool { + return !queue.active(t) + }) +} + +func (queue *scheduledTickQueue) active(t scheduledTick) bool { + index := scheduledTickIndex{pos: t.pos, hash: t.bhash} + furthest, ok := queue.furthestTicks[index] + return ok && furthest == t.t } diff --git a/server/world/tx.go b/server/world/tx.go index 1f037c521..7595f6697 100644 --- a/server/world/tx.go +++ b/server/world/tx.go @@ -143,6 +143,32 @@ func (tx *Tx) RedstoneStoredPowerFrom(pos cube.Pos, face cube.Face) int { return tx.World().redstone.powerFrom(pos, tx, face, true) } +// RedstoneTorchBurnoutStatus reports whether the torch at pos is burned out and whether it may recover after its +// recent toggle history expires. +func (tx *Tx) RedstoneTorchBurnoutStatus(pos cube.Pos) (burnedOut, recoverable bool) { + return tx.World().redstone.redstoneTorchBurnoutStatus(pos, tx.CurrentTick()) +} + +// PruneRedstoneTorchBurnout removes expired burnout history for the torch at pos. +func (tx *Tx) PruneRedstoneTorchBurnout(pos cube.Pos) { + tx.World().redstone.pruneRedstoneTorchBurnout(pos, tx.CurrentTick()) +} + +// RecordRedstoneTorchToggle records a redstone torch state transition and reports whether it should burn out. +func (tx *Tx) RecordRedstoneTorchToggle(pos cube.Pos) bool { + return tx.World().redstone.recordRedstoneTorchToggle(pos, tx.CurrentTick()) +} + +// BurnOutRedstoneTorch marks the torch at pos as burned out. +func (tx *Tx) BurnOutRedstoneTorch(pos cube.Pos) { + tx.World().redstone.burnOutRedstoneTorch(pos) +} + +// ClearRedstoneTorchBurnout removes transient burnout history for the torch at pos. +func (tx *Tx) ClearRedstoneTorchBurnout(pos cube.Pos) { + tx.World().redstone.clearRedstoneTorchBurnout(pos) +} + // HighestLightBlocker gets the Y value of the highest fully light blocking // block at the x and z values passed in the World. func (tx *Tx) HighestLightBlocker(x, z int) int { diff --git a/server/world/world.go b/server/world/world.go index b04d479c6..5767b2e1d 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -167,7 +167,7 @@ func (w *World) blockLoaded(pos cube.Pos) (Block, bool) { if !ok { return w.conf.Blocks.Air(), false } - return w.blockInChunk(c, pos), true + return w.blockInChunkLoaded(c, pos), true } // blockInChunk reads a block from a chunk at the position passed. The block @@ -195,6 +195,21 @@ func (w *World) blockInChunk(c *Column, pos cube.Pos) Block { return w.conf.Blocks.BlockByRuntimeIDOrAir(rid) } +// blockInChunkLoaded reads a block from an already-loaded chunk without repairing missing block entity data. It is +// used by loaded-only queries, which must not mutate chunks or broadcast viewer updates. +func (w *World) blockInChunkLoaded(c *Column, pos cube.Pos) Block { + if pos.OutOfBounds(w.ra) { + return w.conf.Blocks.Air() + } + rid := c.Block(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0) + if w.conf.Blocks.NBTBlock(rid) { + if b, ok := c.BlockEntities[pos]; ok { + return b + } + } + return w.conf.Blocks.BlockByRuntimeIDOrAir(rid) +} + // biome reads the Biome at the position passed. If a chunk is not yet loaded // at that position, the chunk is loaded, or generated if it could not be found // in the world save, and the Biome returned. diff --git a/server/world/world_test.go b/server/world/world_test.go index cdddac6ae..c05c53b70 100644 --- a/server/world/world_test.go +++ b/server/world/world_test.go @@ -39,6 +39,43 @@ func TestLiquidLoadedUsesWorldBlockRegistry(t *testing.T) { } } +func TestBlockLoadedDoesNotCreateMissingNBTBlockEntity(t *testing.T) { + br := NewBlockRegistry() + block := customNBTTestBlock{} + br.RegisterBlockState(BlockState{Name: "test:nbt_block", Properties: map[string]any{}}) + br.RegisterBlock(block) + + w := Config{Blocks: br}.New() + defer func() { + if err := w.Close(); err != nil { + t.Fatalf("close world: %v", err) + } + }() + + pos := cube.Pos{0, 64, 0} + var ( + got Block + ok, created bool + ) + <-w.Exec(func(tx *Tx) { + c := tx.World().chunk(chunkPosFromBlockPos(pos)) + c.SetBlock(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0, tx.World().conf.Blocks.BlockRuntimeID(block)) + c.modified = true + + got, ok = tx.BlockLoaded(pos) + _, created = c.BlockEntities[pos] + }) + if !ok { + t.Fatal("BlockLoaded returned ok=false, want true") + } + if got != block { + t.Fatalf("BlockLoaded returned %#v, want %#v", got, block) + } + if created { + t.Fatal("BlockLoaded created missing block entity data") + } +} + type customLiquidTestBlock struct{} func (customLiquidTestBlock) EncodeBlock() (string, map[string]any) { @@ -58,3 +95,19 @@ func (customLiquidTestBlock) Harden(cube.Pos, *Tx, *cube.Pos) bool { return false } func (customLiquidTestBlock) LiquidRemoveBlock(cube.Pos, *Tx, Block) {} + +type customNBTTestBlock struct { + Decoded bool +} + +func (customNBTTestBlock) EncodeBlock() (string, map[string]any) { + return "test:nbt_block", nil +} +func (customNBTTestBlock) Hash() (uint64, uint64) { return 1 << 46, 0 } +func (customNBTTestBlock) Model() BlockModel { return redstoneCancellationModel{} } +func (customNBTTestBlock) EncodeNBT() map[string]any { + return nil +} +func (customNBTTestBlock) DecodeNBT(map[string]any) any { + return customNBTTestBlock{Decoded: true} +}