diff --git a/examples/frameworkServer/main.go b/examples/frameworkServer/main.go index 6e77f60..1239f13 100644 --- a/examples/frameworkServer/main.go +++ b/examples/frameworkServer/main.go @@ -1,10 +1,8 @@ package main import ( - "container/list" _ "embed" "log" - "sync" "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/data/packetid" @@ -16,98 +14,46 @@ import ( ) type MyServer struct { - PlayerList *list.List - PlayerListLock sync.Mutex - server.MojangLoginHandler - Settings struct { - Name string - MaxPlayer int - MOTD chat.Message - } + *server.PlayerList } -func main() { - var ms MyServer - ms.PlayerList = list.New() - ms.MojangLoginHandler.OnlineMode = true - ms.MojangLoginHandler.Threshold = 256 +const MaxPlayer = 20 - ms.Settings.Name = "MyServer" - ms.Settings.MaxPlayer = 1 - ms.Settings.MOTD = chat.Message{Text: "A Minecraft Server ", Extra: []chat.Message{{Text: "Powered by go-mc", Color: "yellow"}}} +func main() { + motd := &chat.Message{Text: "A Minecraft Server ", Extra: []chat.Message{{Text: "Powered by go-mc", Color: "yellow"}}} + ms := MyServer{ + PlayerList: server.NewPlayerList("MyServer", server.ProtocolVersion, MaxPlayer, motd), + } s := server.Server{ - ListPingHandler: &ms, - LoginHandler: &ms, - GamePlay: &ms, + ListPingHandler: ms.PlayerList, + LoginHandler: &server.MojangLoginHandler{ + OnlineMode: true, + Threshold: 256, + }, + GamePlay: &ms, } if err := s.Listen(":25565"); err != nil { log.Fatalf("Listen error: %v", err) } } -func (m *MyServer) Name() string { - return m.Settings.Name -} - -func (m *MyServer) Protocol() int { - return server.ProtocolVersion -} - -func (m *MyServer) MaxPlayer() int { - return m.Settings.MaxPlayer -} - -func (m *MyServer) OnlinePlayer() int { - m.PlayerListLock.Lock() - defer m.PlayerListLock.Unlock() - return m.PlayerList.Len() -} - -func (m *MyServer) PlayerSamples() (sample []server.PlayerSample) { - m.PlayerListLock.Lock() - defer m.PlayerListLock.Unlock() - // get first 10 players - sample = make([]server.PlayerSample, 0, 10) - v := m.PlayerList.Front() - for i := 0; i < 10; i++ { - if v == nil { - break - } - sample = append(sample, v.Value.(server.PlayerSample)) - v = v.Next() - } - return -} - -func (m *MyServer) Description() chat.Message { - return m.Settings.MOTD -} - func (m *MyServer) AcceptPlayer(name string, id uuid.UUID, protocol int32, conn *net.Conn) { // Add player into PlayerList - m.PlayerListLock.Lock() - if m.PlayerList.Len() >= m.Settings.MaxPlayer { + remove := m.TryInsert(server.PlayerSample{ + Name: name, + ID: id, + }) + if remove == nil { err := conn.WritePacket(pk.Marshal(packetid.KickDisconnect, chat.TranslateMsg("multiplayer.disconnect.server_full"), )) if err != nil { log.Printf("Write packet fail: %v", err) } - m.PlayerListLock.Unlock() return } - elem := m.PlayerList.PushBack(server.PlayerSample{ - Name: name, - ID: id, - }) - m.PlayerListLock.Unlock() - defer func() { - m.PlayerListLock.Lock() - defer m.PlayerListLock.Unlock() - // remove player in PlayerList - m.PlayerList.Remove(elem) - }() + defer remove() if err := m.joinGame(conn); err != nil { log.Printf("Write packet fail: %v", err) @@ -148,7 +94,7 @@ func (m *MyServer) joinGame(conn *net.Conn) error { pk.NBT(nbt.StringifiedMessage(dimensionSNBT)), // Dimension pk.Identifier("world"), // World Name pk.Long(0), // Hashed Seed - pk.VarInt(m.Settings.MaxPlayer), // Max Players + pk.VarInt(MaxPlayer), // Max Players pk.VarInt(15), // View Distance pk.VarInt(15), // Simulation Distance pk.Boolean(false), // Reduced Debug Info diff --git a/server/ping.go b/server/ping.go index f153620..f5bb669 100644 --- a/server/ping.go +++ b/server/ping.go @@ -16,7 +16,7 @@ type ListPingHandler interface { MaxPlayer() int OnlinePlayer() int PlayerSamples() []PlayerSample - Description() chat.Message + Description() *chat.Message } type PlayerSample struct { @@ -60,8 +60,8 @@ func (s *Server) listResp() ([]byte, error) { Online int `json:"online"` Sample []PlayerSample `json:"sample"` } `json:"players"` - Description chat.Message `json:"description"` - FavIcon string `json:"favicon,omitempty"` + Description *chat.Message `json:"description"` + FavIcon string `json:"favicon,omitempty"` } list.Version.Name = s.ListPingHandler.Name() diff --git a/server/playerlist.go b/server/playerlist.go new file mode 100644 index 0000000..913b9fe --- /dev/null +++ b/server/playerlist.go @@ -0,0 +1,90 @@ +package server + +import ( + "container/list" + "sync" + + "github.com/Tnze/go-mc/chat" +) + +// PlayerList is an implement of ListPingHandler based on linked-list. +// This struct should not be copied after used. +type PlayerList struct { + name string + protocol int + maxPlayer int + description *chat.Message + players *list.List + // Only the linked-list is protected by this Mutex. + // Because others field never change after created. + playersLock sync.Mutex +} + +// NewPlayerList create a PlayerList which implement ListPingHandler. +func NewPlayerList(name string, protocol, maxPlayers int, motd *chat.Message) *PlayerList { + return &PlayerList{ + name: name, + protocol: protocol, + maxPlayer: maxPlayers, + description: motd, + players: list.New(), + } +} + +// TryInsert trying to insert player into PlayerList. +// Return nil if the server is full (length of list larger than maxPlayers), +// otherwise return a function which is used to remove the player from PlayerList +func (p *PlayerList) TryInsert(player PlayerSample) (remove func()) { + p.playersLock.Lock() + defer p.playersLock.Unlock() + + if p.players.Len() >= p.maxPlayer { + return nil + } + + elem := p.players.PushBack(player) + return func() { + p.playersLock.Lock() + p.players.Remove(elem) + p.playersLock.Unlock() + } +} + +func (p *PlayerList) Name() string { + return p.name +} + +func (p *PlayerList) Protocol() int { + return p.protocol +} + +func (p *PlayerList) MaxPlayer() int { + return p.maxPlayer +} + +func (p *PlayerList) OnlinePlayer() int { + p.playersLock.Lock() + defer p.playersLock.Unlock() + return p.players.Len() +} + +func (p *PlayerList) PlayerSamples() (sample []PlayerSample) { + p.playersLock.Lock() + defer p.playersLock.Unlock() + // Up to 10 players can be returned + length := p.players.Len() + if length > 10 { + length = 10 + } + sample = make([]PlayerSample, length) + v := p.players.Front() + for i := 0; i < length; i++ { + sample[i] = v.Value.(PlayerSample) + v = v.Next() + } + return +} + +func (p *PlayerList) Description() *chat.Message { + return p.description +} diff --git a/server/server.go b/server/server.go index a544e48..7c7bc9b 100644 --- a/server/server.go +++ b/server/server.go @@ -1,3 +1,25 @@ +// Package server provide a minecraft server framework. +// You can build the server you want by combining the various functional modules provided here. +// An example can be found in examples/frameworkServer. +// +// A server is roughly divided into two parts: +// +// +----------------------------------------------+ +// | Go-MC Server Framework | +// +-----------------------+----------------------+ +// | Gate | GamePlay | +// +--------------+--------+--------+-------------+ +// | LoginHandler | ListPingHandler | Coming Soon | +// +--------------+-----------------+-------------+ +// +// Gate, which is used to respond to the client login request, provide login verification, +// respond to the List Ping Request and providing the online players' information. +// +// Gameplay, which is used to handle all things after a player successfully logs in +// (that is, after the LoginSuccess package is sent), +// and is responsible for functions including player status, chunk management, keep alive, chat, etc. +// +// The implement of Gameplay will provide later. package server import "github.com/Tnze/go-mc/net"