diff --git a/internal/redstone/redstone.go b/internal/redstone/redstone.go new file mode 100644 index 00000000..ff8087a6 --- /dev/null +++ b/internal/redstone/redstone.go @@ -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) + } + } + } +} diff --git a/internal/redstone/redstone_test.go b/internal/redstone/redstone_test.go new file mode 100644 index 00000000..b6ad87e7 --- /dev/null +++ b/internal/redstone/redstone_test.go @@ -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) + } +} diff --git a/tools/zig.zip b/tools/zig.zip index 28d0af71..46497855 100644 Binary files a/tools/zig.zip and b/tools/zig.zip differ