Files

166 lines
5.2 KiB
Go
Raw Permalink Normal View History

2026-07-09 08:33:57 +08:00
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)
}