package network import ( "bufio" "bytes" "io" "log/slog" "net" "sync" "testing" "time" "regionio/internal/protocol" "regionio/internal/server" "regionio/internal/world" ) type recordingConn struct { mu sync.Mutex buf bytes.Buffer } func (c *recordingConn) Read([]byte) (int, error) { return 0, io.EOF } func (c *recordingConn) Close() error { return nil } func (c *recordingConn) LocalAddr() net.Addr { return testAddr("local") } func (c *recordingConn) RemoteAddr() net.Addr { return testAddr("remote") } func (c *recordingConn) SetDeadline(time.Time) error { return nil } func (c *recordingConn) SetReadDeadline(time.Time) error { return nil } func (c *recordingConn) SetWriteDeadline(time.Time) error { return nil } func (c *recordingConn) Write(p []byte) (int, error) { c.mu.Lock() defer c.mu.Unlock() return c.buf.Write(p) } func (c *recordingConn) take(t *testing.T) []protocol.Packet { t.Helper() c.mu.Lock() raw := append([]byte(nil), c.buf.Bytes()...) c.buf.Reset() c.mu.Unlock() reader := bytes.NewReader(raw) br := bufio.NewReader(reader) var packets []protocol.Packet for br.Buffered() > 0 || reader.Len() > 0 { pkt, err := protocol.ReadPacket(br, -1) if err != nil { t.Fatalf("decode recorded packet: %v", err) } packets = append(packets, pkt) } return packets } func (c *recordingConn) countPacketID(id int32) int { c.mu.Lock() raw := append([]byte(nil), c.buf.Bytes()...) c.mu.Unlock() reader := bytes.NewReader(raw) br := bufio.NewReader(reader) count := 0 for br.Buffered() > 0 || reader.Len() > 0 { packet, err := protocol.ReadPacket(br, -1) if err != nil { return count } if packet.ID == id { count++ } } return count } type testAddr string func (a testAddr) Network() string { return "test" } func (a testAddr) String() string { return string(a) } func TestIntegrationTwoPlayersShareBlockAndChatUpdates(t *testing.T) { cfg := server.DefaultConfig() cfg.WorldDir = "" cfg.MaxPlayers = 4 cache := world.NewCache(-1, world.GenerateFlat) srv, err := server.NewWithCache(cfg, cache) if err != nil { t.Fatal(err) } raw1, raw2 := &recordingConn{}, &recordingConn{} h1 := &handler{conn: NewConn(raw1), srv: srv, log: slog.Default()} h2 := &handler{conn: NewConn(raw2), srv: srv, log: slog.Default()} h1.conn.Profile = server.Profile{Name: "Alice", UUID: server.OfflineUUID("Alice")} h2.conn.Profile = server.Profile{Name: "Bob", UUID: server.OfflineUUID("Bob")} h1.session, err = srv.RegisterPlayer(h1.conn.Profile, h1.conn.Send) if err != nil { t.Fatal(err) } defer srv.UnregisterPlayer(h1.session) h2.session, err = srv.RegisterPlayer(h2.conn.Profile, h2.conn.Send) if err != nil { t.Fatal(err) } defer srv.UnregisterPlayer(h2.session) action := protocol.NewWriter(24) action.VarInt(0).Position(2, world.FlatSurfaceY, 3).Byte(1).VarInt(17) if err := h1.handlePlayerAction(protocol.Packet{ID: protocol.PlayPlayerAction, Data: action.Bytes()}); err != nil { t.Fatal(err) } if got := cache.GetBlock(2, world.FlatSurfaceY, 3); got != world.StateAir { t.Fatalf("shared block state = %d, want air", got) } assertPacketIDs(t, raw1.take(t), protocol.PlayBlockUpdate, protocol.PlayLightUpdate, protocol.PlayBlockChangedAck) assertPacketIDs(t, raw2.take(t), protocol.PlayBlockUpdate, protocol.PlayLightUpdate) chat := protocol.NewWriter(16).String("hello") if err := h1.handleChat(protocol.Packet{ID: protocol.PlayChatMessage, Data: chat.Bytes()}); err != nil { t.Fatal(err) } assertPacketIDs(t, raw1.take(t), protocol.PlaySystemChat) assertPacketIDs(t, raw2.take(t), protocol.PlaySystemChat) } func TestBoundaryEditBroadcastsEveryChangedLightChunk(t *testing.T) { cfg := server.DefaultConfig() cfg.WorldDir = "" cache := world.NewCache(-1, func(cx, cz int32) *world.Chunk { return world.NewChunk(cx, cz, world.BiomePlains) }) if _, err := cache.FrameErr(0, 0); err != nil { t.Fatal(err) } if _, err := cache.FrameErr(1, 0); err != nil { t.Fatal(err) } srv, err := server.NewWithCache(cfg, cache) if err != nil { t.Fatal(err) } recorder := &recordingConn{} h := &handler{conn: NewConn(recorder), srv: srv, log: slog.Default()} h.conn.Profile = server.Profile{Name: "Alice", UUID: server.OfflineUUID("Alice")} h.session, err = srv.RegisterPlayer(h.conn.Profile, h.conn.Send) if err != nil { t.Fatal(err) } defer srv.UnregisterPlayer(h.session) srv.SetPlayerTransform(h.session, 15, 100, 8, 0, 0, true) srv.SetPlayerViewDistance(h.session, 2) valid, lightChunks := cache.SetBlockWithLight(15, 100, 8, world.StateGlowstone) if !valid || len(lightChunks) != 2 { t.Fatalf("boundary edit valid=%v light chunks=%v; want two", valid, lightChunks) } h.broadcastBlockUpdate(15, 100, 8, world.StateGlowstone, lightChunks) assertPacketIDs(t, recorder.take(t), protocol.PlayBlockUpdate, protocol.PlayLightUpdate, protocol.PlayLightUpdate) } func TestIntegrationFourClientsVisibilityMovementLeaveAndLight(t *testing.T) { cfg := server.DefaultConfig() cfg.WorldDir = "" cfg.MaxPlayers = 4 cache := world.NewCache(-1, world.GenerateFlat) srv, err := server.NewWithCache(cfg, cache) if err != nil { t.Fatal(err) } names := []string{"Alice", "Bob", "Carol", "Dave"} positions := [][3]float64{{0, 80, 0}, {16, 80, 0}, {160, 80, 0}, {176, 80, 0}} handlers := make([]*handler, len(names)) recorders := make([]*recordingConn, len(names)) for i, name := range names { recorders[i] = &recordingConn{} h := &handler{conn: NewConn(recorders[i]), srv: srv, log: slog.Default(), viewDistance: 2} h.conn.Profile = server.Profile{Name: name, UUID: server.OfflineUUID(name)} h.session, err = srv.RegisterPlayer(h.conn.Profile, h.conn.Send) if err != nil { t.Fatalf("register %s: %v", name, err) } srv.SetPlayerTransform(h.session, positions[i][0], positions[i][1], positions[i][2], 0, 0, true) srv.SetPlayerViewDistance(h.session, h.viewDistance) handlers[i] = h } defer func() { for _, h := range handlers { srv.UnregisterPlayer(h.session) } }() mobID := srv.Entities().Add(&world.Entity{ TypeID: 100, TypeName: "minecraft:pig", X: 8, Y: 80, Z: 8, }) for _, h := range handlers { if err := h.syncVisibleEntities(); err != nil { t.Fatal(err) } } for i, recorder := range recorders { packets := recorder.take(t) if got := countPackets(packets, protocol.PlayPlayerInfoUpdate); got != 4 { t.Fatalf("client %s player-info count = %d, want 4", names[i], got) } wantAdds := 1 if i < 2 { wantAdds = 2 // nearby player plus the pig } if got := countPackets(packets, protocol.PlayAddEntity); got != wantAdds { t.Fatalf("client %s add-entity count = %d, want %d", names[i], got, wantAdds) } } move := protocol.NewWriter(40) move.Float64(32).Float64(80).Float64(0).Float32(90).Float32(15).Byte(1) if err := handlers[1].handlePlay(protocol.Packet{ID: protocol.PlayMovePosRot, Data: move.Bytes()}); err != nil { t.Fatal(err) } if err := handlers[0].syncVisibleEntities(); err != nil { t.Fatal(err) } packets := recorders[0].take(t) if got := countPackets(packets, protocol.PlayTeleportEntity); got != 1 { t.Fatalf("nearby movement teleports = %d, want 1", got) } assertTeleportTransform(t, firstPacket(t, packets, protocol.PlayTeleportEntity), handlers[1].session.EntityID, 32, 80, 0, 90, 15, true) move = protocol.NewWriter(32) move.Float64(64).Float64(80).Float64(0).Byte(1) if err := handlers[1].handlePlay(protocol.Packet{ID: protocol.PlayMovePos, Data: move.Bytes()}); err != nil { t.Fatal(err) } if err := handlers[0].syncVisibleEntities(); err != nil { t.Fatal(err) } assertPacketIDs(t, recorders[0].take(t), protocol.PlayRemoveEntities) move = protocol.NewWriter(32) move.Float64(16).Float64(80).Float64(0).Byte(1) if err := handlers[1].handlePlay(protocol.Packet{ID: protocol.PlayMovePos, Data: move.Bytes()}); err != nil { t.Fatal(err) } if err := handlers[0].syncVisibleEntities(); err != nil { t.Fatal(err) } assertPacketIDs(t, recorders[0].take(t), protocol.PlayAddEntity) srv.UnregisterPlayer(handlers[1].session) if err := handlers[0].syncVisibleEntities(); err != nil { t.Fatal(err) } assertPacketIDs(t, recorders[0].take(t), protocol.PlayRemoveEntities, protocol.PlayPlayerInfoRemove) action := protocol.NewWriter(24) action.VarInt(0).Position(2, world.FlatSurfaceY, 3).Byte(1).VarInt(91) if err := handlers[0].handlePlayerAction(protocol.Packet{ID: protocol.PlayPlayerAction, Data: action.Bytes()}); err != nil { t.Fatal(err) } assertPacketIDs(t, recorders[0].take(t), protocol.PlayBlockUpdate, protocol.PlayLightUpdate, protocol.PlayBlockChangedAck) if packets := recorders[2].take(t); len(packets) != 0 { t.Fatalf("far client Carol received %d block/light packets", len(packets)) } if packets := recorders[3].take(t); len(packets) != 0 { t.Fatalf("far client Dave received %d block/light packets", len(packets)) } srv.Entities().Remove(mobID) } func countPackets(packets []protocol.Packet, id int32) int { count := 0 for _, packet := range packets { if packet.ID == id { count++ } } return count } func firstPacket(t *testing.T, packets []protocol.Packet, id int32) protocol.Packet { t.Helper() for _, packet := range packets { if packet.ID == id { return packet } } t.Fatalf("packet %#x not found", id) return protocol.Packet{} } func assertTeleportTransform(t *testing.T, packet protocol.Packet, entityID int32, x, y, z float64, yaw, pitch float32, onGround bool) { t.Helper() r := packet.Body() if got, err := r.VarInt(); err != nil || got != entityID { t.Fatalf("teleport entity = %d, %v; want %d", got, err, entityID) } for _, field := range []struct { name string want float64 }{{"x", x}, {"y", y}, {"z", z}} { got, err := r.Float64() if err != nil || got != field.want { t.Fatalf("teleport %s = %v, %v; want %v", field.name, got, err, field.want) } } for i := 0; i < 3; i++ { if got, err := r.Float64(); err != nil || got != 0 { t.Fatalf("teleport velocity[%d] = %v, %v; want 0", i, got, err) } } if got, err := r.Float32(); err != nil || got != yaw { t.Fatalf("teleport yaw = %v, %v; want %v", got, err, yaw) } if got, err := r.Float32(); err != nil || got != pitch { t.Fatalf("teleport pitch = %v, %v; want %v", got, err, pitch) } if _, err := r.Int32(); err != nil { t.Fatal(err) } if got, err := r.Bool(); err != nil || got != onGround { t.Fatalf("teleport onGround = %v, %v; want %v", got, err, onGround) } } func assertPacketIDs(t *testing.T, packets []protocol.Packet, want ...int32) { t.Helper() if len(packets) != len(want) { ids := make([]int32, len(packets)) for i := range packets { ids[i] = packets[i].ID } t.Fatalf("packet IDs = %v (count %d), want %v (count %d)", ids, len(packets), want, len(want)) } for i := range want { if packets[i].ID != want[i] { t.Fatalf("packet[%d] id = %#x, want %#x", i, packets[i].ID, want[i]) } } }