Implement multiplayer persistence and vanilla lighting
This commit is contained in:
parent
8f7cacf9d9
commit
cae06eb97e
47 changed files with 3784 additions and 465 deletions
321
internal/network/multiplayer_test.go
Normal file
321
internal/network/multiplayer_test.go
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
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
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue