adjust PlayerList and KeepAlive in go-mc/server

This commit is contained in:
Tnze
2022-06-21 01:12:55 +08:00
parent d94993f34f
commit 59caded2ef
6 changed files with 70 additions and 263 deletions

View File

@ -5,34 +5,14 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/google/uuid"
"github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
) )
type Client struct {
*net.Conn
Protocol int32
packetQueue *PacketQueue
errChan chan error
}
type Player struct {
uuid.UUID
Name string
}
// Packet758 is a packet in protocol 757. // Packet758 is a packet in protocol 757.
// We are using type system to force programmers to update packets. // We are using type system to force programmers to update packets.
type Packet758 pk.Packet type Packet758 pk.Packet
type Packet757 pk.Packet type Packet757 pk.Packet
// WritePacket to player client. The type of parameter will update per version.
func (c *Client) WritePacket(packet Packet758) {
c.packetQueue.Push(pk.Packet(packet))
}
type WritePacketError struct { type WritePacketError struct {
Err error Err error
ID int32 ID int32
@ -46,23 +26,6 @@ func (s WritePacketError) Unwrap() error {
return s.Err return s.Err
} }
func (c *Client) PutErr(err error) {
select {
case c.errChan <- err:
default:
// previous error exist, ignore this.
}
}
func (c *Client) GetErr() error {
select {
case err := <-c.errChan:
return err
default:
return nil
}
}
type PacketQueue struct { type PacketQueue struct {
queue *list.List queue *list.List
closed bool closed bool

View File

@ -1,56 +0,0 @@
package server
import (
"github.com/Tnze/go-mc/data/packetid"
"github.com/Tnze/go-mc/level"
pk "github.com/Tnze/go-mc/net/packet"
)
type Level interface {
Init(g *Game)
Info() LevelInfo
PlayerJoin(p *Client)
PlayerQuit(p *Client)
}
type LevelInfo struct {
Name string
HashedSeed uint64
}
type SimpleDim struct {
numOfSection int
columns map[level.ChunkPos]*level.Chunk
}
func (s *SimpleDim) Init(*Game) {}
func NewSimpleDim(secs int) *SimpleDim {
return &SimpleDim{
numOfSection: secs,
columns: make(map[level.ChunkPos]*level.Chunk),
}
}
func (s *SimpleDim) LoadChunk(pos level.ChunkPos, c *level.Chunk) {
s.columns[pos] = c
}
func (s *SimpleDim) Info() LevelInfo {
return LevelInfo{
Name: "minecraft:overworld",
HashedSeed: 1234567,
}
}
func (s *SimpleDim) PlayerJoin(p *Client) {
for pos, column := range s.columns {
packet := pk.Marshal(
packetid.ClientboundLevelChunkWithLight,
pos, column,
)
p.WritePacket(Packet758(packet))
}
}
func (s *SimpleDim) PlayerQuit(*Client) {}

View File

@ -1,12 +1,8 @@
package server package server
import ( import (
"context"
"crypto/rsa" "crypto/rsa"
_ "embed" _ "embed"
"sync"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/Tnze/go-mc/net" "github.com/Tnze/go-mc/net"
@ -19,59 +15,3 @@ type GamePlay interface {
// You don't need to close the connection, but to keep not returning while the player is playing. // You don't need to close the connection, but to keep not returning while the player is playing.
AcceptPlayer(name string, id uuid.UUID, profilePubKey *rsa.PublicKey, protocol int32, conn *net.Conn) AcceptPlayer(name string, id uuid.UUID, profilePubKey *rsa.PublicKey, protocol int32, conn *net.Conn)
} }
type Game struct {
WorldLocker sync.Mutex
handlers map[int32][]*PacketHandler
components []Component
}
func (g *Game) AcceptPlayer(name string, id uuid.UUID, profilePubKey *rsa.PublicKey, protocol int32, conn *net.Conn) {
conn.Close()
}
type PacketHandler struct {
ID int32
F packetHandlerFunc
}
type packetHandlerFunc func(client *Client, player *Player, packet Packet758) error
type Component interface {
Init(g *Game)
Run(ctx context.Context)
ClientJoin(c *Client, p *Player)
ClientLeft(c *Client, p *Player, reason error)
}
func NewGame(components ...Component) *Game {
g := &Game{
handlers: make(map[int32][]*PacketHandler),
components: components,
}
for _, c := range components {
c.Init(g)
}
return g
}
func (g *Game) AddHandler(ph *PacketHandler) {
g.handlers[ph.ID] = append(g.handlers[ph.ID], ph)
}
func (g *Game) Run(ctx context.Context) {
for _, c := range g.components {
go c.Run(ctx)
}
ticker := time.NewTicker(time.Second / 20)
for {
select {
case <-ticker.C:
g.WorldLocker.Lock()
//g.Dispatcher.Run(g.World)
g.WorldLocker.Unlock()
case <-ctx.Done():
return
}
}
}

View File

@ -4,10 +4,8 @@ import (
"container/list" "container/list"
"context" "context"
"errors" "errors"
"github.com/Tnze/go-mc/chat"
"time" "time"
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
) )
// keepAliveInterval represents the interval when the server sends keep alive // keepAliveInterval represents the interval when the server sends keep alive
@ -16,59 +14,49 @@ const keepAliveInterval = time.Second * 15
// keepAliveWaitInterval represents how long does the player expired // keepAliveWaitInterval represents how long does the player expired
const keepAliveWaitInterval = time.Second * 30 const keepAliveWaitInterval = time.Second * 30
type ClientDelay struct { type KeepAliveClient interface {
Delay time.Duration SendKeepAlive(id int64)
SendDisconnect(reason chat.Message)
} }
type KeepAlive struct { type KeepAlive struct {
join chan *Client join chan KeepAliveClient
quit chan *Client quit chan KeepAliveClient
tick chan *Client tick chan KeepAliveClient
pingList *list.List pingList *list.List
waitList *list.List waitList *list.List
listIndex map[*Client]*list.Element listIndex map[KeepAliveClient]*list.Element
listTimer *time.Timer listTimer *time.Timer
waitTimer *time.Timer waitTimer *time.Timer
// The Notchian server uses a system-dependent time in milliseconds to generate the keep alive ID value. // The Notchian server uses a system-dependent time in milliseconds to generate the keep alive ID value.
// We don't do that here for security reason. // We don't do that here for security reason.
keepAliveID int64 keepAliveID int64
updatePlayerDelay []func(p *Client, delay time.Duration) updatePlayerDelay []func(p KeepAliveClient, delay time.Duration)
} }
func NewKeepAlive() (k *KeepAlive) { func NewKeepAlive[C KeepAliveClient]() (k *KeepAlive) {
return &KeepAlive{ return &KeepAlive{
join: make(chan *Client), join: make(chan KeepAliveClient),
quit: make(chan *Client), quit: make(chan KeepAliveClient),
tick: make(chan *Client), tick: make(chan KeepAliveClient),
pingList: list.New(), pingList: list.New(),
waitList: list.New(), waitList: list.New(),
listIndex: make(map[*Client]*list.Element), listIndex: make(map[KeepAliveClient]*list.Element),
listTimer: time.NewTimer(keepAliveInterval), listTimer: time.NewTimer(keepAliveInterval),
waitTimer: time.NewTimer(keepAliveWaitInterval), waitTimer: time.NewTimer(keepAliveWaitInterval),
keepAliveID: 0, keepAliveID: 0,
} }
} }
func (k *KeepAlive) AddPlayerDelayUpdateHandler(f func(p *Client, delay time.Duration)) { func (k *KeepAlive) AddPlayerDelayUpdateHandler(f func(c KeepAliveClient, delay time.Duration)) {
k.updatePlayerDelay = append(k.updatePlayerDelay, f) k.updatePlayerDelay = append(k.updatePlayerDelay, f)
} }
// Init implement Component for KeepAlive func (k *KeepAlive) ClientJoin(client KeepAliveClient) { k.join <- client }
func (k *KeepAlive) Init(g *Game) { func (k *KeepAlive) ClientTick(client KeepAliveClient) { k.tick <- client }
g.AddHandler(&PacketHandler{ func (k *KeepAlive) ClientLeft(client KeepAliveClient) { k.quit <- client }
ID: packetid.ServerboundKeepAlive,
F: func(client *Client, player *Player, packet Packet758) error {
var KeepAliveID pk.Long
if err := pk.Packet(packet).Scan(&KeepAliveID); err != nil {
return err
}
k.tick <- client
return nil
},
})
}
// Run implement Component for KeepAlive // Run implement Component for KeepAlive
func (k *KeepAlive) Run(ctx context.Context) { func (k *KeepAlive) Run(ctx context.Context) {
@ -76,35 +64,29 @@ func (k *KeepAlive) Run(ctx context.Context) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case p := <-k.join: case c := <-k.join:
k.pushPlayer(p) k.pushPlayer(c)
case p := <-k.quit: case c := <-k.quit:
k.removePlayer(p) k.removePlayer(c)
case now := <-k.listTimer.C: case now := <-k.listTimer.C:
k.pingPlayer(now) k.pingPlayer(now)
case <-k.waitTimer.C: case <-k.waitTimer.C:
k.kickPlayer() k.kickPlayer()
case p := <-k.tick: case c := <-k.tick:
k.tickPlayer(p) k.tickPlayer(c)
} }
} }
} }
// ClientJoin implement Component for KeepAlive func (k KeepAlive) pushPlayer(c KeepAliveClient) {
func (k *KeepAlive) ClientJoin(client *Client, _ *Player) { k.join <- client } k.listIndex[c] = k.pingList.PushBack(
keepAliveItem{player: c, t: time.Now()},
// ClientLeft implement Component for KeepAlive
func (k *KeepAlive) ClientLeft(client *Client, _ *Player, _ error) { k.quit <- client }
func (k KeepAlive) pushPlayer(p *Client) {
k.listIndex[p] = k.pingList.PushBack(
keepAliveItem{player: p, t: time.Now()},
) )
} }
func (k *KeepAlive) removePlayer(p *Client) { func (k *KeepAlive) removePlayer(c KeepAliveClient) {
elem := k.listIndex[p] elem := k.listIndex[c]
delete(k.listIndex, p) delete(k.listIndex, c)
if elem.Prev() == nil { if elem.Prev() == nil {
// At present, it is difficult to distinguish // At present, it is difficult to distinguish
// which linked list the player is in, // which linked list the player is in,
@ -118,26 +100,23 @@ func (k *KeepAlive) removePlayer(p *Client) {
func (k *KeepAlive) pingPlayer(now time.Time) { func (k *KeepAlive) pingPlayer(now time.Time) {
if elem := k.pingList.Front(); elem != nil { if elem := k.pingList.Front(); elem != nil {
p := k.pingList.Remove(elem).(keepAliveItem).player c := k.pingList.Remove(elem).(keepAliveItem).player
// Send Clientbound KeepAlive packet. // Send Clientbound KeepAlive packet.
p.WritePacket(Packet758(pk.Marshal( c.SendKeepAlive(k.keepAliveID)
packetid.ClientboundKeepAlive,
pk.Long(k.keepAliveID),
)))
k.keepAliveID++ k.keepAliveID++
// Clientbound KeepAlive packet is sent, move the player to waiting list. // Clientbound KeepAlive packet is sent, move the player to waiting list.
k.listIndex[p] = k.waitList.PushBack( k.listIndex[c] = k.waitList.PushBack(
keepAliveItem{player: p, t: now}, keepAliveItem{player: c, t: now},
) )
} }
// Wait for next earliest player // Wait for next earliest player
keepAliveSetTimer(k.pingList, k.listTimer, keepAliveInterval) keepAliveSetTimer(k.pingList, k.listTimer, keepAliveInterval)
} }
func (k *KeepAlive) tickPlayer(p *Client) { func (k *KeepAlive) tickPlayer(c KeepAliveClient) {
elem, ok := k.listIndex[p] elem, ok := k.listIndex[c]
if !ok { if !ok {
p.PutErr(errors.New("keepalive: fail to tick player: client not found")) panic(errors.New("keepalive: fail to tick player: client not found"))
return return
} }
if elem.Prev() == nil { if elem.Prev() == nil {
@ -150,19 +129,19 @@ func (k *KeepAlive) tickPlayer(p *Client) {
now := time.Now() now := time.Now()
delay := now.Sub(k.waitList.Remove(elem).(keepAliveItem).t) delay := now.Sub(k.waitList.Remove(elem).(keepAliveItem).t)
for _, f := range k.updatePlayerDelay { for _, f := range k.updatePlayerDelay {
f(p, delay) f(c, delay)
} }
// move the player to ping list // move the player to ping list
k.listIndex[p] = k.pingList.PushBack( k.listIndex[c] = k.pingList.PushBack(
keepAliveItem{player: p, t: now}, keepAliveItem{player: c, t: now},
) )
} }
func (k *KeepAlive) kickPlayer() { func (k *KeepAlive) kickPlayer() {
if elem := k.waitList.Front(); elem != nil { if elem := k.waitList.Front(); elem != nil {
player := k.waitList.Remove(elem).(keepAliveItem).player c := k.waitList.Remove(elem).(keepAliveItem).player
k.waitList.Remove(elem) k.waitList.Remove(elem)
player.PutErr(errors.New("keepalive: client did not response")) c.SendDisconnect(chat.TranslateMsg("disconnect.timeout"))
} }
keepAliveSetTimer(k.waitList, k.waitTimer, keepAliveWaitInterval) keepAliveSetTimer(k.waitList, k.waitTimer, keepAliveWaitInterval)
} }
@ -180,6 +159,6 @@ func keepAliveSetTimer(l *list.List, timer *time.Timer, interval time.Duration)
} }
type keepAliveItem struct { type keepAliveItem struct {
player *Client player KeepAliveClient
t time.Time t time.Time
} }

View File

@ -3,7 +3,6 @@ package server
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"image" "image"
"image/png" "image/png"
"strings" "strings"
@ -95,38 +94,35 @@ func (s *Server) listResp() ([]byte, error) {
// PingInfo implement ListPingHandler. // PingInfo implement ListPingHandler.
type PingInfo struct { type PingInfo struct {
name string name string
protocol int protocol int
*PlayerList
description chat.Message description chat.Message
favicon string favicon string
} }
// NewPingInfo crate a new PingInfo, the icon can be nil. // NewPingInfo crate a new PingInfo, the icon can be nil.
func NewPingInfo(list *PlayerList, name string, protocol int, motd chat.Message, icon image.Image) (p *PingInfo, err error) { // Panic if icon's size is not 64x64.
func NewPingInfo(name string, protocol int, motd chat.Message, icon image.Image) (p *PingInfo) {
var favIcon string var favIcon string
if icon != nil { if icon != nil {
if !icon.Bounds().Size().Eq(image.Point{X: 64, Y: 64}) { if !icon.Bounds().Size().Eq(image.Point{X: 64, Y: 64}) {
return nil, errors.New("icon size is not 64x64") panic("icon size is not 64x64")
} }
// Encode icon into string "data:image/png;base64,......" format // Encode icon into string "data:image/png;base64,......" format
var sb strings.Builder var sb strings.Builder
sb.WriteString("data:image/png;base64,") sb.WriteString("data:image/png;base64,")
w := base64.NewEncoder(base64.StdEncoding, &sb) w := base64.NewEncoder(base64.StdEncoding, &sb)
err = png.Encode(w, icon) if err := png.Encode(w, icon); err != nil {
if err != nil { panic(err)
return nil, err
} }
err = w.Close() if err := w.Close(); err != nil {
if err != nil { panic(err)
return nil, err
} }
favIcon = sb.String() favIcon = sb.String()
} }
p = &PingInfo{ p = &PingInfo{
name: name, name: name,
protocol: protocol, protocol: protocol,
PlayerList: list,
description: motd, description: motd,
favicon: favIcon, favicon: favIcon,
} }

View File

@ -1,22 +1,22 @@
package server package server
import ( import (
"context"
"errors"
"sync" "sync"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet"
) )
type PlayerListClient interface {
SendDisconnect(reason chat.Message)
}
// PlayerList is a player list based on linked-list. // PlayerList is a player list based on linked-list.
// This struct should not be copied after used. // This struct should not be copied after used.
type PlayerList struct { type PlayerList struct {
maxPlayer int maxPlayer int
clients map[uuid.UUID]*Player players map[PlayerListClient]PlayerSample
// Only the field players is protected by this Mutex. // Only the field players is protected by this Mutex.
// Because others field never change after created. // Because others field never change after created.
playersLock sync.Mutex playersLock sync.Mutex
@ -26,45 +26,33 @@ type PlayerList struct {
func NewPlayerList(maxPlayers int) *PlayerList { func NewPlayerList(maxPlayers int) *PlayerList {
return &PlayerList{ return &PlayerList{
maxPlayer: maxPlayers, maxPlayer: maxPlayers,
clients: make(map[uuid.UUID]*Player), players: make(map[PlayerListClient]PlayerSample),
} }
} }
// Init implement Component for PlayerList
func (p *PlayerList) Init(*Game) {}
// Run implement Component for PlayerList
func (p *PlayerList) Run(context.Context) {}
// ClientJoin implement Component for PlayerList // ClientJoin implement Component for PlayerList
func (p *PlayerList) ClientJoin(client *Client, player *Player) { func (p *PlayerList) ClientJoin(client PlayerListClient, player PlayerSample) {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() defer p.playersLock.Unlock()
if len(p.clients) >= p.maxPlayer { if len(p.players) >= p.maxPlayer {
client.WritePacket(Packet758(pk.Marshal( client.SendDisconnect(chat.TranslateMsg("multiplayer.disconnect.server_full"))
packetid.ClientboundDisconnect,
chat.TranslateMsg("multiplayer.disconnect.server_full"),
)))
client.PutErr(errors.New("playerlist: server full"))
return return
} }
p.clients[player.UUID] = player p.players[client] = player
} }
// ClientLeft implement Component for PlayerList func (p *PlayerList) ClientLeft(client PlayerListClient) {
func (p *PlayerList) ClientLeft(_ *Client, player *Player, _ error) {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() defer p.playersLock.Unlock()
delete(p.clients, player.UUID) delete(p.players, client)
} }
// CheckPlayer implement LoginChecker for PlayerList func (p *PlayerList) CheckPlayer(string, uuid.UUID, int32) (ok bool, reason chat.Message) {
func (p *PlayerList) CheckPlayer(name string, id uuid.UUID, protocol int32) (ok bool, reason chat.Message) {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() defer p.playersLock.Unlock()
if len(p.clients) >= p.maxPlayer { if len(p.players) >= p.maxPlayer {
return false, chat.TranslateMsg("multiplayer.disconnect.server_full") return false, chat.TranslateMsg("multiplayer.disconnect.server_full")
} }
return true, chat.Message{} return true, chat.Message{}
@ -77,24 +65,21 @@ func (p *PlayerList) MaxPlayer() int {
func (p *PlayerList) OnlinePlayer() int { func (p *PlayerList) OnlinePlayer() int {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() defer p.playersLock.Unlock()
return len(p.clients) return len(p.players)
} }
func (p *PlayerList) PlayerSamples() (sample []PlayerSample) { func (p *PlayerList) PlayerSamples() (sample []PlayerSample) {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() defer p.playersLock.Unlock()
// Up to 10 players can be returned // Up to 10 players can be returned
length := len(p.clients) length := len(p.players)
if length > 10 { if length > 10 {
length = 10 length = 10
} }
sample = make([]PlayerSample, length) sample = make([]PlayerSample, length)
var i int var i int
for _, v := range p.clients { for _, player := range p.players {
sample[i] = PlayerSample{ sample[i] = player
Name: v.Name,
ID: v.UUID,
}
i++ i++
if i >= length { if i >= length {
break break