295 lines
8.7 KiB
Go
295 lines
8.7 KiB
Go
|
|
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.
|
||
|
|
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 bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// HasLiquidDrops returns whether the redstone torch drops its item when flowing liquid breaks it.
|
||
|
|
func (RedstoneTorch) HasLiquidDrops() bool {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// LightEmissionLevel returns the light level emitted by the redstone torch (7 when lit, 0 when unlit).
|
||
|
|
func (t RedstoneTorch) LightEmissionLevel() uint8 {
|
||
|
|
if t.Lit {
|
||
|
|
return 7
|
||
|
|
}
|
||
|
|
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.
|
||
|
|
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 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if !tx.Block(pos.Side(face.Opposite())).Model().FaceSolid(pos.Side(face.Opposite()), face, tx) {
|
||
|
|
fallbackFace, ok := findTorchPlacementFace(pos, tx)
|
||
|
|
if !ok {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
face = fallbackFace
|
||
|
|
}
|
||
|
|
t.Facing = face.Opposite()
|
||
|
|
t.Lit = true
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// NeighbourUpdateTick is called when a neighbouring block is updated.
|
||
|
|
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)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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)
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
shouldBeLit := t.inputStrength(pos, tx) == 0
|
||
|
|
if shouldBeLit == t.Lit {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
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
|
||
|
|
}
|
||
|
|
|
||
|
|
currentTick := tx.CurrentTick()
|
||
|
|
burnedOut, recoverable := tx.Redstone().TorchBurnoutStatus(pos, currentTick)
|
||
|
|
if !burnedOut {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if !recoverable {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
tx.Redstone().ClearTorchBurnout(pos)
|
||
|
|
|
||
|
|
torch.Lit = torch.inputStrength(pos, tx) == 0
|
||
|
|
tx.SetBlock(pos, torch, nil)
|
||
|
|
updateTorchRedstone(pos, tx)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
inputPos := pos.Side(t.Facing)
|
||
|
|
if source == inputPos {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
for _, face := range cube.Faces() {
|
||
|
|
if source == inputPos.Side(face) {
|
||
|
|
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)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// EncodeItem encodes the redstone torch as an item.
|
||
|
|
func (RedstoneTorch) EncodeItem() (name string, meta int16) {
|
||
|
|
return "minecraft:redstone_torch", 0
|
||
|
|
}
|
||
|
|
|
||
|
|
// EncodeBlock encodes the redstone torch as a block for network transmission.
|
||
|
|
func (t RedstoneTorch) EncodeBlock() (name string, properties map[string]any) {
|
||
|
|
face := "unknown"
|
||
|
|
if t.Facing != unknownFace {
|
||
|
|
face = t.Facing.String()
|
||
|
|
if t.Facing == cube.FaceDown {
|
||
|
|
face = "top"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if t.Lit {
|
||
|
|
return "minecraft:redstone_torch", map[string]any{"torch_facing_direction": face}
|
||
|
|
}
|
||
|
|
return "minecraft:unlit_redstone_torch", map[string]any{"torch_facing_direction": face}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
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
|
||
|
|
}
|
||
|
|
all = append(all, RedstoneTorch{Facing: f, Lit: true})
|
||
|
|
all = append(all, RedstoneTorch{Facing: f})
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|