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"
"sync"
"github.com/google/uuid"
"github.com/Tnze/go-mc/net"
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.
// We are using type system to force programmers to update packets.
type Packet758 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 {
Err error
ID int32
@ -46,23 +26,6 @@ func (s WritePacketError) Unwrap() error {
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 {
queue *list.List
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
import (
"context"
"crypto/rsa"
_ "embed"
"sync"
"time"
"github.com/google/uuid"
"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.
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"
"context"
"errors"
"github.com/Tnze/go-mc/chat"
"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
@ -16,59 +14,49 @@ const keepAliveInterval = time.Second * 15
// keepAliveWaitInterval represents how long does the player expired
const keepAliveWaitInterval = time.Second * 30
type ClientDelay struct {
Delay time.Duration
type KeepAliveClient interface {
SendKeepAlive(id int64)
SendDisconnect(reason chat.Message)
}
type KeepAlive struct {
join chan *Client
quit chan *Client
tick chan *Client
join chan KeepAliveClient
quit chan KeepAliveClient
tick chan KeepAliveClient
pingList *list.List
waitList *list.List
listIndex map[*Client]*list.Element
listIndex map[KeepAliveClient]*list.Element
listTimer *time.Timer
waitTimer *time.Timer
// 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.
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{
join: make(chan *Client),
quit: make(chan *Client),
tick: make(chan *Client),
join: make(chan KeepAliveClient),
quit: make(chan KeepAliveClient),
tick: make(chan KeepAliveClient),
pingList: list.New(),
waitList: list.New(),
listIndex: make(map[*Client]*list.Element),
listIndex: make(map[KeepAliveClient]*list.Element),
listTimer: time.NewTimer(keepAliveInterval),
waitTimer: time.NewTimer(keepAliveWaitInterval),
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)
}
// Init implement Component for KeepAlive
func (k *KeepAlive) Init(g *Game) {
g.AddHandler(&PacketHandler{
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
},
})
}
func (k *KeepAlive) ClientJoin(client KeepAliveClient) { k.join <- client }
func (k *KeepAlive) ClientTick(client KeepAliveClient) { k.tick <- client }
func (k *KeepAlive) ClientLeft(client KeepAliveClient) { k.quit <- client }
// Run implement Component for KeepAlive
func (k *KeepAlive) Run(ctx context.Context) {
@ -76,35 +64,29 @@ func (k *KeepAlive) Run(ctx context.Context) {
select {
case <-ctx.Done():
return
case p := <-k.join:
k.pushPlayer(p)
case p := <-k.quit:
k.removePlayer(p)
case c := <-k.join:
k.pushPlayer(c)
case c := <-k.quit:
k.removePlayer(c)
case now := <-k.listTimer.C:
k.pingPlayer(now)
case <-k.waitTimer.C:
k.kickPlayer()
case p := <-k.tick:
k.tickPlayer(p)
case c := <-k.tick:
k.tickPlayer(c)
}
}
}
// ClientJoin implement Component for KeepAlive
func (k *KeepAlive) ClientJoin(client *Client, _ *Player) { k.join <- client }
// 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) pushPlayer(c KeepAliveClient) {
k.listIndex[c] = k.pingList.PushBack(
keepAliveItem{player: c, t: time.Now()},
)
}
func (k *KeepAlive) removePlayer(p *Client) {
elem := k.listIndex[p]
delete(k.listIndex, p)
func (k *KeepAlive) removePlayer(c KeepAliveClient) {
elem := k.listIndex[c]
delete(k.listIndex, c)
if elem.Prev() == nil {
// At present, it is difficult to distinguish
// which linked list the player is in,
@ -118,26 +100,23 @@ func (k *KeepAlive) removePlayer(p *Client) {
func (k *KeepAlive) pingPlayer(now time.Time) {
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.
p.WritePacket(Packet758(pk.Marshal(
packetid.ClientboundKeepAlive,
pk.Long(k.keepAliveID),
)))
c.SendKeepAlive(k.keepAliveID)
k.keepAliveID++
// Clientbound KeepAlive packet is sent, move the player to waiting list.
k.listIndex[p] = k.waitList.PushBack(
keepAliveItem{player: p, t: now},
k.listIndex[c] = k.waitList.PushBack(
keepAliveItem{player: c, t: now},
)
}
// Wait for next earliest player
keepAliveSetTimer(k.pingList, k.listTimer, keepAliveInterval)
}
func (k *KeepAlive) tickPlayer(p *Client) {
elem, ok := k.listIndex[p]
func (k *KeepAlive) tickPlayer(c KeepAliveClient) {
elem, ok := k.listIndex[c]
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
}
if elem.Prev() == nil {
@ -150,19 +129,19 @@ func (k *KeepAlive) tickPlayer(p *Client) {
now := time.Now()
delay := now.Sub(k.waitList.Remove(elem).(keepAliveItem).t)
for _, f := range k.updatePlayerDelay {
f(p, delay)
f(c, delay)
}
// move the player to ping list
k.listIndex[p] = k.pingList.PushBack(
keepAliveItem{player: p, t: now},
k.listIndex[c] = k.pingList.PushBack(
keepAliveItem{player: c, t: now},
)
}
func (k *KeepAlive) kickPlayer() {
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)
player.PutErr(errors.New("keepalive: client did not response"))
c.SendDisconnect(chat.TranslateMsg("disconnect.timeout"))
}
keepAliveSetTimer(k.waitList, k.waitTimer, keepAliveWaitInterval)
}
@ -180,6 +159,6 @@ func keepAliveSetTimer(l *list.List, timer *time.Timer, interval time.Duration)
}
type keepAliveItem struct {
player *Client
player KeepAliveClient
t time.Time
}

View File

@ -3,7 +3,6 @@ package server
import (
"encoding/base64"
"encoding/json"
"errors"
"image"
"image/png"
"strings"
@ -95,38 +94,35 @@ func (s *Server) listResp() ([]byte, error) {
// PingInfo implement ListPingHandler.
type PingInfo struct {
name string
protocol int
*PlayerList
name string
protocol int
description chat.Message
favicon string
}
// 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
if icon != nil {
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
var sb strings.Builder
sb.WriteString("data:image/png;base64,")
w := base64.NewEncoder(base64.StdEncoding, &sb)
err = png.Encode(w, icon)
if err != nil {
return nil, err
if err := png.Encode(w, icon); err != nil {
panic(err)
}
err = w.Close()
if err != nil {
return nil, err
if err := w.Close(); err != nil {
panic(err)
}
favIcon = sb.String()
}
p = &PingInfo{
name: name,
protocol: protocol,
PlayerList: list,
description: motd,
favicon: favIcon,
}

View File

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