Chunk generation and sending no longer block the read loop. The read loop pushes a non-blocking recenter request and stays free to handle movement, chat, and keep-alive acks immediately; a per-connection streamer goroutine generates chunks in a worker pool and sends them serially under the write mutex. - network/streamer.go: per-connection streamer. spiralOrder emits chunks centre-outward; parallelSend fans Cache.Frame across a worker pool (Cache.Frame is already goroutine-safe), parallelGenerate warms a one-ring predictive border so movement finds ready chunks; the loaded-set is owned solely by the streamer. - network/play.go: beginPlay launches the streamer and pushes an initial recenter instead of the old blocking streamAround; onPlayerMove now just calls requestRecenter (non-blocking). - network/handler.go: ctx (connection lifetime) + streamer field; the streamer stops when the read loop ends (cancel on serve exit). - network/configuration.go: client view_distance is saved (clamped 2..16) and drives the streamer radius. - Tests: spiral order (centre-first, ring structure) and the non-blocking recenter guarantee the read loop relies on.
84 lines
2.3 KiB
Go
84 lines
2.3 KiB
Go
package network
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestSpiralOrderCenterFirst confirms the centre coordinate is returned first
|
|
// and the ring order expands outward (Chebyshev distance non-decreasing).
|
|
func TestSpiralOrderCenterFirst(t *testing.T) {
|
|
order := spiralOrder(0, 0, 2)
|
|
if order[0] != [2]int32{0, 0} {
|
|
t.Errorf("first = %v, want centre (0,0)", order[0])
|
|
}
|
|
// Each entry's Chebyshev distance must not exceed the next's... actually it
|
|
// only needs to be non-decreasing within ring blocks. Verify the max
|
|
// distance of the first k entries grows as expected by checking the full
|
|
// set contains exactly the (2*2+1)² = 49 distinct coords.
|
|
want := (2*2 + 1) * (2 * 2 + 1)
|
|
if len(order) != want {
|
|
t.Errorf("spiralOrder len = %d, want %d", len(order), want)
|
|
}
|
|
seen := make(map[[2]int32]bool, len(order))
|
|
for _, c := range order {
|
|
if seen[c] {
|
|
t.Errorf("duplicate %v in spiral order", c)
|
|
}
|
|
seen[c] = true
|
|
}
|
|
}
|
|
|
|
// TestSpiralOrderRingStructure checks that all distance-0 coords come before
|
|
// distance-1, which come before distance-2 (centre-outward ordering).
|
|
func TestSpiralOrderRingStructure(t *testing.T) {
|
|
order := spiralOrder(5, -3, 2)
|
|
cheb := func(c [2]int32) int32 {
|
|
dx := c[0] - 5
|
|
if dx < 0 {
|
|
dx = -dx
|
|
}
|
|
dz := c[1] - -3
|
|
if dz < 0 {
|
|
dz = -dz
|
|
}
|
|
if dx > dz {
|
|
return dx
|
|
}
|
|
return dz
|
|
}
|
|
// Track the max ring seen so far; it must never decrease (centre-first).
|
|
var maxRing int32
|
|
for _, c := range order {
|
|
r := cheb(c)
|
|
if r < maxRing {
|
|
t.Errorf("ring %d appeared after ring %d — not centre-first", r, maxRing)
|
|
}
|
|
if r > maxRing {
|
|
maxRing = r
|
|
}
|
|
}
|
|
if maxRing != 2 {
|
|
t.Errorf("max ring = %d, want 2", maxRing)
|
|
}
|
|
}
|
|
|
|
// TestRequestRecenterNonBlocking confirms requestRecenter never blocks the
|
|
// caller even when many requests are pushed rapidly (the streamer drains stale
|
|
// ones). This is the property the read loop relies on to stay responsive.
|
|
func TestRequestRecenterNonBlocking(t *testing.T) {
|
|
s := newStreamer(nil, nil, nil, 4)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for i := 0; i < 1000; i++ {
|
|
s.requestRecenter(int32(i), int32(i))
|
|
}
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
// good: 1000 rapid requests returned without blocking
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("requestRecenter blocked for 2s")
|
|
}
|
|
}
|