feat(redstone): 方块更新队列(延迟+迭代上限)与红石信号传播(含测试)

This commit is contained in:
NianGao Dev
2026-08-15 23:07:36 +08:00
parent 58aa9f5e02
commit 00397e7b13
3 changed files with 213 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
// Package redstone 方块更新队列与红石信号传播(场景/红石与方块更新.md §2–§3)。
//
// 设计要点:
// - 延迟更新队列(0/1/2/4 tick)+ 每 tick 迭代上限(防无限递归);
// - 红石信号 0–15,仅沿线缆传播(每格衰减 1),与方块光完全分离。
package redstone
import (
"sync"
)
// 更新类型(红石与方块更新.md §2)。
const (
KindNeighbor = 0 // 邻居变化
KindScheduled = 1 // 定时刻(延迟更新)
KindBlockEntity = 2 // 方块实体 tick
)
// 迭代上限(红石与方块更新.md §2:每 tick 20000,防级联风暴)。
const DefaultIterLimit = 20000
// Update 一个方块更新事件。
type Update struct {
X, Y, Z int32
Kind uint8
Delay int // 剩余延迟 tick
}
// Queue 延迟更新队列(有序消费)。
type Queue struct {
mu sync.Mutex
events []Update
iterCap int
dropped int // 超限丢弃计数(告警用)
}
// NewQueue 创建队列。
func NewQueue() *Queue { return &Queue{iterCap: DefaultIterLimit} }
// Push 入队。
func (q *Queue) Push(u Update) {
q.mu.Lock()
q.events = append(q.events, u)
q.mu.Unlock()
}
// Pending 待处理事件数。
func (q *Queue) Pending() int {
q.mu.Lock()
defer q.mu.Unlock()
return len(q.events)
}
// Dropped 返回被丢弃的事件数(迭代上限触发次数)。
func (q *Queue) Dropped() int {
q.mu.Lock()
defer q.mu.Unlock()
return q.dropped
}
// Tick 每 tick 消费队列:延迟递减,到期的事件交给 handle 处理。
// 处理数量受 iterCap 限制;超限事件丢弃并计数(红石与方块更新.md §6 踩坑)。
func (q *Queue) Tick(handle func(u Update)) {
q.mu.Lock()
kept := q.events[:0]
var due []Update
for _, u := range q.events {
if u.Delay > 0 {
u.Delay--
kept = append(kept, u)
continue
}
due = append(due, u)
}
q.events = kept
q.mu.Unlock()
processed := 0
for _, u := range due {
if processed >= q.iterCap {
q.mu.Lock()
q.dropped++
q.mu.Unlock()
continue
}
handle(u)
processed++
}
}
// 红石信号(红石与方块更新.md §3:0–15,与方块光分离)。
// Src 信号源。
type Src struct {
X, Y, Z int32
Level uint8 // 0–15
}
// SpreadWire 红石信号沿线缆传播:每格衰减 1,仅经过 isWire 判定的线缆格。
// set 写信号值;返回值无(传播结果由调用方通过 set 收集)。
// 与光照 Propagate 的关键区别:红石只走线缆、不穿过所有方块(红石与方块更新.md §6 踩坑)。
func SpreadWire(sources []Src, isWire func(x, y, z int32) bool, set func(x, y, z int32, v uint8)) {
type pos struct{ x, y, z int32 }
q := make([]pos, 0, 64)
power := make(map[pos]uint8, 64)
for _, s := range sources {
p := pos{s.X, s.Y, s.Z}
power[p] = s.Level
set(p.x, p.y, p.z, s.Level)
q = append(q, p)
}
dirs := [6][3]int32{{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}}
for len(q) > 0 {
p := q[0]
q = q[1:]
lv := power[p]
if lv <= 1 {
continue
}
for _, d := range dirs {
n := pos{p.x + d[0], p.y + d[1], p.z + d[2]}
if !isWire(n.x, n.y, n.z) {
continue
}
if old, ok := power[n]; !ok || old < lv-1 {
power[n] = lv - 1
set(n.x, n.y, n.z, lv-1)
q = append(q, n)
}
}
}
}

View File

@@ -0,0 +1,81 @@
package redstone
import "testing"
// TestQueueDelay 延迟队列:Delay 到期才处理(红石与方块更新.md §2)。
func TestQueueDelay(t *testing.T) {
q := NewQueue()
q.Push(Update{X: 1, Delay: 0})
q.Push(Update{X: 2, Delay: 2})
var got []int32
for tick := 0; tick < 3; tick++ {
q.Tick(func(u Update) { got = append(got, u.X) })
}
if len(got) != 2 || got[0] != 1 || got[1] != 2 {
t.Fatalf("延迟处理顺序异常: %v", got)
}
if q.Pending() != 0 {
t.Fatalf("队列应清空,剩 %d", q.Pending())
}
}
// TestQueueIterCap 迭代上限:超限丢弃并计数(红石与方块更新.md §6 防级联风暴)。
func TestQueueIterCap(t *testing.T) {
q := NewQueue()
q.iterCap = 10
for i := 0; i < 30; i++ {
q.Push(Update{X: int32(i)})
}
handled := 0
q.Tick(func(u Update) { handled++ })
if handled != 10 {
t.Fatalf("处理数期望 10,实际 %d", handled)
}
if q.Dropped() != 20 {
t.Fatalf("丢弃数期望 20,实际 %d", q.Dropped())
}
}
// TestSpreadWire 红石信号沿线缆传播:每格衰减 1(红石与方块更新.md §3)。
func TestSpreadWire(t *testing.T) {
// 连续线缆:y=64,x∈[0..5]
isWire := func(x, y, z int32) bool {
return y == 64 && z == 0 && x >= 0 && x <= 5
}
power := map[[3]int32]uint8{}
set := func(x, y, z int32, v uint8) { power[[3]int32{x, y, z}] = v }
SpreadWire([]Src{{X: 0, Y: 64, Z: 0, Level: 15}}, isWire, set)
if got := power[[3]int32{1, 64, 0}]; got != 14 {
t.Fatalf("x=1 信号期望 14,实际 %d", got)
}
if got := power[[3]int32{5, 64, 0}]; got != 10 {
t.Fatalf("x=5 信号期望 10(15-5),实际 %d", got)
}
}
// TestSpreadWireGap 线缆断点中断信号(x=2 非线缆 → x≥3 无信号)。
func TestSpreadWireGap(t *testing.T) {
isWire := func(x, y, z int32) bool {
return y == 64 && z == 0 && x >= 0 && x <= 5 && x != 2
}
power := map[[3]int32]uint8{}
set := func(x, y, z int32, v uint8) { power[[3]int32{x, y, z}] = v }
SpreadWire([]Src{{X: 0, Y: 64, Z: 0, Level: 15}}, isWire, set)
if _, ok := power[[3]int32{3, 64, 0}]; ok {
t.Fatal("断点后不应有信号")
}
}
// TestSpreadWireMultiSource 多源取最大(火把式叠加)。
func TestSpreadWireMultiSource(t *testing.T) {
isWire := func(x, y, z int32) bool { return y == 64 && z == 0 && x >= 0 && x <= 3 }
power := map[[3]int32]uint8{}
set := func(x, y, z int32, v uint8) { power[[3]int32{x, y, z}] = v }
SpreadWire([]Src{{X: 0, Y: 64, Z: 0, Level: 15}, {X: 3, Y: 64, Z: 0, Level: 15}}, isWire, set)
// x=1:距两端各 1 格 → 14;x=2:距两端各 1 格 → 14
if got := power[[3]int32{2, 64, 0}]; got != 14 {
t.Fatalf("x=2 信号期望 14,实际 %d", got)
}
}

Binary file not shown.