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) }