diff --git a/server/gameplay.go b/server/gameplay.go index c4905ef..ee66757 100644 --- a/server/gameplay.go +++ b/server/gameplay.go @@ -2,13 +2,13 @@ package server import ( _ "embed" - "github.com/Tnze/go-mc/chat" - "github.com/Tnze/go-mc/nbt" "sync/atomic" "github.com/google/uuid" + "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/data/packetid" + "github.com/Tnze/go-mc/nbt" "github.com/Tnze/go-mc/net" pk "github.com/Tnze/go-mc/net/packet" ) @@ -34,10 +34,7 @@ var dimensionCodecSNBT string var dimensionSNBT string func (g *Game) AcceptPlayer(name string, id uuid.UUID, protocol int32, conn *net.Conn) { - remove := g.PlayerList.TryInsert(PlayerSample{ - Name: name, - ID: id, - }) + remove := g.PlayerList.TryInsert(PlayerSample{Name: name, ID: id}) if remove == nil { _ = conn.WritePacket(pk.Marshal( packetid.ClientboundDisconnect, diff --git a/server/keepalive.go b/server/keepalive.go new file mode 100644 index 0000000..ffab787 --- /dev/null +++ b/server/keepalive.go @@ -0,0 +1,166 @@ +package server + +import ( + "container/heap" + "errors" + "github.com/Tnze/go-mc/data/packetid" + pk "github.com/Tnze/go-mc/net/packet" + "sync" + "time" +) + +// keepAliveInterval represents the interval when the server sends keep alive +const keepAliveInterval = time.Second * 15 + +// keepAliveWaitInterval represents how long does the player expired +const keepAliveWaitInterval = time.Second * 30 + +type KeepAlive struct { + list keepAliveHeap + listLock sync.Mutex + + waitList keepAliveHeap + waitItem keepAliveItem + waitLock sync.Mutex + waitTimer *time.Timer + + onPlayerExpire func(p *Player) +} + +func NewKeepAlive() (k KeepAlive) { + heap.Init(&k.list) + heap.Init(&k.waitList) + return +} + +func (k *KeepAlive) AddPlayer(p *Player) { + k.listLock.Lock() + defer k.listLock.Unlock() + + now := time.Now() + heap.Push(&k.list, keepAliveItem{ + player: p, + lastTick: now, + }) +} + +func (k *KeepAlive) RemovePlayer(p *Player) { + k.listLock.Lock() + defer k.listLock.Unlock() + + for i, v := range k.list { + if v.player.UUID == p.UUID { + heap.Remove(&k.list, i) + return + } + } + panic("KeepAlive: Fail to remove player: " + p.UUID.String() + ": not found") +} + +func (k *KeepAlive) Run() { + listTimer := time.NewTimer(keepAliveInterval) + k.waitTimer = time.NewTimer(keepAliveWaitInterval) + var listItem keepAliveItem + // 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. + var keepAliveID int64 + + for { + select { + case now := <-listTimer.C: + if listItem.player == nil { + listItem = getEarliest(&k.list, &k.listLock, listTimer, keepAliveInterval) + break + } + + // Send Clientbound KeepAlive packet. + listItem.player.WritePacket(Packet757(pk.Marshal( + packetid.ClientboundKeepAlive, + pk.Long(keepAliveID), + ))) + keepAliveID++ + + // Clientbound KeepAlive packet is sent, add the player to waiting list. + k.waitLock.Lock() + heap.Push(&k.waitList, keepAliveItem{ + player: listItem.player, + lastTick: now, + }) + k.waitLock.Unlock() + + // Wait for next earliest player + listItem = getEarliest(&k.list, &k.listLock, listTimer, keepAliveInterval) + + case <-k.waitTimer.C: + if k.waitItem.player == nil { + k.waitItem = getEarliest(&k.waitList, &k.waitLock, k.waitTimer, keepAliveWaitInterval) + break + } + k.onPlayerExpire(k.waitItem.player) + k.waitItem = getEarliest(&k.waitList, &k.waitLock, k.waitTimer, keepAliveWaitInterval) + } + } +} + +func (k *KeepAlive) TickPlayer(player *Player, packet Packet757) error { + var KeepAliveID pk.Long + if err := pk.Packet(packet).Scan(&KeepAliveID); err != nil { + return err + } + k.waitLock.Lock() + defer k.waitLock.Unlock() + + if k.waitItem.player.UUID == player.UUID { + if k.waitTimer.Stop() { + k.waitItem = getEarliest(&k.waitList, &k.waitLock, k.waitTimer, keepAliveWaitInterval) + } else { + <-k.waitTimer.C + return errors.New("keepalive: player " + player.UUID.String() + " is already expired") + } + } else { + for i, v := range k.waitList { + if v.player.UUID == player.UUID { + heap.Remove(&k.waitList, i) + goto Success + } + } + return errors.New("keepalive: player " + player.UUID.String() + " not found") + } +Success: + heap.Push(&k.list, keepAliveItem{ + player: player, + lastTick: time.Now(), + }) + return nil +} + +func getEarliest(list *keepAliveHeap, listLock *sync.Mutex, timer *time.Timer, interval time.Duration) (item keepAliveItem) { + listLock.Lock() + defer listLock.Unlock() + + if list.Len() == 0 { + timer.Reset(interval) + } else { + item = heap.Pop(list).(keepAliveItem) + timer.Reset(time.Until(item.lastTick.Add(interval))) + } + return +} + +type keepAliveItem struct { + player *Player + lastTick time.Time +} + +type keepAliveHeap []keepAliveItem + +func (k *keepAliveHeap) Len() int { return len(*k) } +func (k *keepAliveHeap) Less(i, j int) bool { return (*k)[i].lastTick.Before((*k)[j].lastTick) } +func (k *keepAliveHeap) Swap(i, j int) { (*k)[i], (*k)[j] = (*k)[j], (*k)[i] } +func (k *keepAliveHeap) Push(x interface{}) { *k = append(*k, x.(keepAliveItem)) } +func (k *keepAliveHeap) Pop() (x interface{}) { + i := len(*k) - 1 + x = (*k)[i] + *k = (*k)[:i] + return +} diff --git a/server/player.go b/server/player.go index 3d1ac5c..617a332 100644 --- a/server/player.go +++ b/server/player.go @@ -3,10 +3,15 @@ package server import ( "github.com/Tnze/go-mc/net" pk "github.com/Tnze/go-mc/net/packet" + "github.com/google/uuid" + "sync" ) type Player struct { *net.Conn + writeLock sync.Mutex + + uuid.UUID EntityID int32 Gamemode byte handlers map[int32][]packetHandlerFunc @@ -18,6 +23,8 @@ type Packet757 pk.Packet // WritePacket to player client. The type of parameter will update per version. func (p *Player) WritePacket(packet Packet757) error { + p.writeLock.Lock() + defer p.writeLock.Unlock() return p.Conn.WritePacket(pk.Packet(packet)) } @@ -26,7 +33,7 @@ type PacketHandler struct { F packetHandlerFunc } -type packetHandlerFunc func(player *Player, packet Packet757) error +type packetHandlerFunc func(packet Packet757) error func (p *Player) Add(ph PacketHandler) { if p.handlers == nil {