package mpsync import ( "context" "net" "path/filepath" "testing" "time" "mc/internal/block" "mc/internal/logx" "mc/internal/netproto" "mc/internal/netserver" "mc/internal/world" "mc/internal/worldgen" ) // syncHandler 测试用服务器处理器:登录 → hub.AddPlayer。 type syncHandler struct { hub *Hub recv chan *netserver.Conn } func (h *syncHandler) OnConnect(c *netserver.Conn) {} func (h *syncHandler) OnHandshake(c *netserver.Conn, hs netproto.Handshake) uint8 { if h.recv != nil { h.recv <- c } h.hub.AddPlayer(c, 8, 8) return 0 } func (h *syncHandler) OnMove(c *netserver.Conn, m netproto.PlayerMove) {} func (h *syncHandler) OnDisconnect(c *netserver.Conn) { h.hub.RemovePlayer(c) } // TestLoginReceivesChunk 登录后客户端收到区块数据帧(多人同步.md §4)。 func TestLoginReceivesChunk(t *testing.T) { reg, err := block.Load(filepath.Join("..", "..", "assets", "config", "blocks.json")) if err != nil { t.Fatalf("加载注册表失败: %v", err) } gen, err := worldgen.New(reg, 7, 0.005, 0.01, 0.1) if err != nil { t.Fatalf("创建生成器失败: %v", err) } w, err := world.New(reg, gen, 4) if err != nil { t.Fatalf("创建世界失败: %v", err) } defer w.Close() deadline := time.Now().Add(15 * time.Second) for time.Now().Before(deadline) { w.Update(8, 64, 8, 8*time.Millisecond) if w.Block(8, 0, 8) != block.Air { break } time.Sleep(2 * time.Millisecond) } hub := NewHub(w, 4) log, _ := logx.New("", logx.LevelDebug) h := &syncHandler{hub: hub, recv: make(chan *netserver.Conn, 1)} srv := netserver.New("127.0.0.1:0", log, h) ctx, cancel := context.WithCancel(context.Background()) defer cancel() if err := srv.Listen(ctx); err != nil { t.Fatalf("监听失败: %v", err) } nc, err := net.DialTimeout("tcp", srv.Addr(), 3*time.Second) if err != nil { t.Fatalf("拨号失败: %v", err) } defer nc.Close() hs := netproto.Handshake{ProtocolVersion: 1, ClientID: "同步测试"} _, _ = nc.Write(netproto.EncodeFrame(netproto.Frame{MsgID: netproto.MsgHandshake, Payload: hs.Encode()})) gotChunk := false buf := make([]byte, 64*1024) _ = nc.SetReadDeadline(time.Now().Add(5 * time.Second)) acc := []byte{} for !gotChunk { n, err := nc.Read(buf) if err != nil { break } acc = append(acc, buf[:n]...) for len(acc) >= 4 { f, rest, err := netproto.DecodeFrame(acc) if err != nil { break } acc = rest if f.MsgID == netproto.MsgChunkData { gotChunk = true } } } if !gotChunk { t.Fatal("登录后未收到区块数据") } } // TestBlockChangeBroadcast 服务器权威写方块 → 客户端收到广播(多人同步.md §5)。 func TestBlockChangeBroadcast(t *testing.T) { reg, err := block.Load(filepath.Join("..", "..", "assets", "config", "blocks.json")) if err != nil { t.Fatalf("加载注册表失败: %v", err) } gen, err := worldgen.New(reg, 8, 0.005, 0.01, 0.1) if err != nil { t.Fatalf("创建生成器失败: %v", err) } w, err := world.New(reg, gen, 2) if err != nil { t.Fatalf("创建世界失败: %v", err) } defer w.Close() hub := NewHub(w, 4) log, _ := logx.New("", logx.LevelDebug) h := &syncHandler{hub: hub, recv: make(chan *netserver.Conn, 1)} srv := netserver.New("127.0.0.1:0", log, h) ctx, cancel := context.WithCancel(context.Background()) defer cancel() if err := srv.Listen(ctx); err != nil { t.Fatalf("监听失败: %v", err) } nc, err := net.DialTimeout("tcp", srv.Addr(), 3*time.Second) if err != nil { t.Fatalf("拨号失败: %v", err) } defer nc.Close() _, _ = nc.Write(netproto.EncodeFrame(netproto.Frame{MsgID: netproto.MsgHandshake, Payload: netproto.Handshake{ProtocolVersion: 1}.Encode()})) // 等待服务器完成登录注册(避免广播早于 AddPlayer 执行,多人同步.md §2 时序) select { case <-h.recv: case <-time.After(3 * time.Second): t.Fatal("未完成登录注册") } torch, _ := reg.ID("torch") go hub.SetBlockWorld(8, 70, 8, block.NewState(torch, 0)) buf := make([]byte, 64*1024) _ = nc.SetReadDeadline(time.Now().Add(5 * time.Second)) acc := []byte{} deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { n, err := nc.Read(buf) if err != nil { break } acc = append(acc, buf[:n]...) for len(acc) >= 4 { f, rest, err := netproto.DecodeFrame(acc) if err != nil { break } acc = rest if f.MsgID != netproto.MsgBlockChange { continue } m, err := netproto.DecodeBlockChange(f.Payload) if err != nil { t.Fatalf("解码失败: %v", err) } if m.X == 8 && m.Y == 70 && m.Z == 8 && m.BlockID == torch { return } } } t.Fatal("未收到方块变更广播") }