From 4a4e327b8aca4c2812c9e6625a1cebd64973484e Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Thu, 7 Aug 2025 13:55:50 +0200 Subject: [PATCH 1/7] remove extra timestamp from log --- internal/lobbyServer/lobby.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/lobbyServer/lobby.go b/internal/lobbyServer/lobby.go index f2231bd..50be5b8 100644 --- a/internal/lobbyServer/lobby.go +++ b/internal/lobbyServer/lobby.go @@ -671,7 +671,7 @@ func (s *LobbyServer) wsHandler(w http.ResponseWriter, r *http.Request) { g.Running = true g.StartTime = time.Now() - g.Logger.Info("starting game", "buffer_target", g.BufferTarget, "time", g.StartTime.Format(time.RFC3339)) + g.Logger.Info("starting game", "buffer_target", g.BufferTarget) g.NumberOfPlayers = len(g.Players) sendMessage.Accept = Accepted go s.watchGameServer(roomName, g) From 129fef9a34ec5c7b4804bc92f535480ded990624 Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:42:37 +0200 Subject: [PATCH 2/7] small cleanup --- internal/gameServer/udp.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/gameServer/udp.go b/internal/gameServer/udp.go index 7d79d10..58ae132 100644 --- a/internal/gameServer/udp.go +++ b/internal/gameServer/udp.go @@ -147,14 +147,12 @@ func (g *GameServer) processUDP(addr *net.UDPAddr) { g.Logger.Error(err, "could not process request", "regID", regID) return } - countLag := g.sendUDPInput(count, addr, playerNumber, spectator != 0, sendingPlayerNumber) + g.GameData.CountLag[sendingPlayerNumber] = g.sendUDPInput(count, addr, playerNumber, spectator != 0, sendingPlayerNumber) g.GameData.BufferHealth[sendingPlayerNumber] = int32(g.GameData.recvBuffer[11]) g.GameDataMutex.Lock() // PlayerAlive can be modified by ManagePlayers in a different thread g.GameData.PlayerAlive[sendingPlayerNumber] = true g.GameDataMutex.Unlock() - - g.GameData.CountLag[sendingPlayerNumber] = countLag case CP0Info: if g.GameData.Status&StatusDesync == 0 { viCount := binary.BigEndian.Uint32(g.GameData.recvBuffer[1:]) From c09ba03c013df4cafd3dd1306232dd782da6fdf6 Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:47:51 +0200 Subject: [PATCH 3/7] combine data structure --- internal/gameServer/tcp.go | 2 +- internal/gameServer/udp.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/gameServer/tcp.go b/internal/gameServer/tcp.go index 90bfaa2..5cd1d7a 100644 --- a/internal/gameServer/tcp.go +++ b/internal/gameServer/tcp.go @@ -298,7 +298,7 @@ func (g *GameServer) processTCP(conn *net.TCPConn) { g.Logger.Info("registered player", "registration", g.Registrations[playerNumber], "number", playerNumber, "bufferLeft", tcpData.Buffer.Len(), "address", conn.RemoteAddr().String()) g.GameDataMutex.Lock() // any player can modify this, which would be in a different thread - g.GameData.PendingPlugin[playerNumber] = plugin + g.GameData.PendingInput[playerNumber] = InputData{0, plugin} g.GameData.PlayerAlive[playerNumber] = true g.GameDataMutex.Unlock() } else { diff --git a/internal/gameServer/udp.go b/internal/gameServer/udp.go index 58ae132..e24540f 100644 --- a/internal/gameServer/udp.go +++ b/internal/gameServer/udp.go @@ -24,9 +24,8 @@ type GameData struct { BufferSize uint32 BufferHealth []int32 Inputs []*lru.Cache[uint32, InputData] - PendingInput []uint32 + PendingInput []InputData CountLag []uint32 - PendingPlugin []byte sendBuffer []byte recvBuffer []byte PlayerAlive []bool @@ -71,7 +70,7 @@ func (g *GameServer) getPlayerNumberByID(regID uint32) (byte, error) { func (g *GameServer) fillInput(playerNumber byte, count uint32) InputData { input, inputExists := g.GameData.Inputs[playerNumber].Get(count) if !inputExists { - input = InputData{keys: g.GameData.PendingInput[playerNumber], plugin: g.GameData.PendingPlugin[playerNumber]} + input = g.GameData.PendingInput[playerNumber] g.GameData.Inputs[playerNumber].Add(count, input) } return input @@ -127,8 +126,10 @@ func (g *GameServer) processUDP(addr *net.UDPAddr) { g.GameData.PlayerAddresses[playerNumber] = addr count := binary.BigEndian.Uint32(g.GameData.recvBuffer[2:]) - g.GameData.PendingInput[playerNumber] = binary.BigEndian.Uint32(g.GameData.recvBuffer[6:]) - g.GameData.PendingPlugin[playerNumber] = g.GameData.recvBuffer[10] + g.GameData.PendingInput[playerNumber] = InputData{ + keys: binary.BigEndian.Uint32(g.GameData.recvBuffer[6:]), + plugin: g.GameData.recvBuffer[10], + } for i := range 4 { if g.GameData.PlayerAddresses[i] != nil { @@ -216,8 +217,7 @@ func (g *GameServer) createUDPServer() error { for i := range 4 { g.GameData.Inputs[i], _ = lru.New[uint32, InputData](InputDataMax) } - g.GameData.PendingInput = make([]uint32, 4) - g.GameData.PendingPlugin = make([]byte, 4) + g.GameData.PendingInput = make([]InputData, 4) g.GameData.SyncValues, _ = lru.New[uint32, []byte](100) // Store up to 100 sync values g.GameData.PlayerAlive = make([]bool, 4) g.GameData.CountLag = make([]uint32, 4) From 64a562ad1d17504f1c1880071b5916bebde31628 Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:07:54 +0200 Subject: [PATCH 4/7] new method for calculating buffer size --- .github/workflows/build.yml | 1 - go.mod | 4 ++-- go.sum | 8 +++---- internal/gameServer/server.go | 41 +++++++++++++++++++++++++---------- internal/gameServer/tcp.go | 2 +- internal/gameServer/udp.go | 8 ++++--- internal/lobbyServer/lobby.go | 7 ++++-- 7 files changed, 46 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b581808..f922a3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,6 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: "go.mod" - cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v8 diff --git a/go.mod b/go.mod index f0ea4da..a01202e 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,12 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/golang-lru/v2 v2.0.7 go.uber.org/zap v1.27.0 - golang.org/x/net v0.42.0 + golang.org/x/net v0.43.0 ) require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.35.0 // indirect ) require ( diff --git a/go.sum b/go.sum index 3f49868..5ee42fa 100644 --- a/go.sum +++ b/go.sum @@ -30,9 +30,9 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/gameServer/server.go b/internal/gameServer/server.go index fb28626..0a56296 100644 --- a/internal/gameServer/server.go +++ b/internal/gameServer/server.go @@ -1,6 +1,7 @@ package gameserver import ( + "fmt" "net" "strings" "sync" @@ -49,7 +50,7 @@ type GameServer struct { Features map[string]string NeedsUpdatePlayers bool NumberOfPlayers int - BufferTarget int32 + BufferTarget uint32 QuitChannel *chan bool } @@ -92,6 +93,19 @@ func (g *GameServer) isConnClosed(err error) bool { return strings.Contains(err.Error(), "use of closed network connection") } +func (g *GameServer) bufferHealthAverage(playerNumber int) (float32, error) { + var bufferHealth float32 + if g.GameData.BufferHealth[playerNumber].Len() > 0 { + for _, k := range g.GameData.BufferHealth[playerNumber].Keys() { + value, _ := g.GameData.BufferHealth[playerNumber].Peek(k) + bufferHealth += float32(value) + } + return bufferHealth / float32(g.GameData.BufferHealth[playerNumber].Len()), nil + } else { + return 0, fmt.Errorf("no buffer health data for player %d", playerNumber) + } +} + func (g *GameServer) ManageBuffer() { for { if !g.Running { @@ -100,27 +114,29 @@ func (g *GameServer) ManageBuffer() { } // Find the largest buffer health - var bufferHealth int32 = -1 + var bufferHealth float32 + var foundPlayer bool for i := range 4 { - if g.GameData.BufferHealth[i] != -1 && g.GameData.CountLag[i] == 0 { - if g.GameData.BufferHealth[i] > bufferHealth { - bufferHealth = g.GameData.BufferHealth[i] + if g.GameData.CountLag[i] == 0 { + playerBufferHealth, err := g.bufferHealthAverage(i) + if err == nil && playerBufferHealth > bufferHealth { + bufferHealth = playerBufferHealth + foundPlayer = true } } } - // Adjust the buffer size - if bufferHealth != -1 { - if bufferHealth > g.BufferTarget && g.GameData.BufferSize > 0 { + if foundPlayer { + if bufferHealth > float32(g.BufferTarget)+0.5 && g.GameData.BufferSize > 0 { g.GameData.BufferSize-- g.Logger.Info("reduced buffer size", "bufferHealth", bufferHealth, "bufferSize", g.GameData.BufferSize) - } else if bufferHealth < g.BufferTarget { + } else if bufferHealth < float32(g.BufferTarget)-0.5 { g.GameData.BufferSize++ g.Logger.Info("increased buffer size", "bufferHealth", bufferHealth, "bufferSize", g.GameData.BufferSize) } } - time.Sleep(time.Second * 3) + time.Sleep(time.Second) } } @@ -135,7 +151,8 @@ func (g *GameServer) ManagePlayers() { _, ok := g.Registrations[i] if ok { if g.GameData.PlayerAlive[i] { - g.Logger.Info("player status", "player", i, "regID", g.Registrations[i].RegID, "bufferSize", g.GameData.BufferSize, "bufferHealth", g.GameData.BufferHealth[i], "countLag", g.GameData.CountLag[i], "address", g.GameData.PlayerAddresses[i]) + playerBufferHealth, _ := g.bufferHealthAverage(int(i)) + g.Logger.Info("player status", "player", i, "regID", g.Registrations[i].RegID, "bufferSize", g.GameData.BufferSize, "bufferHealth", playerBufferHealth, "countLag", g.GameData.CountLag[i], "address", g.GameData.PlayerAddresses[i]) playersActive = true } else { g.Logger.Info("player disconnected UDP", "player", i, "regID", g.Registrations[i].RegID, "address", g.GameData.PlayerAddresses[i]) @@ -153,7 +170,7 @@ func (g *GameServer) ManagePlayers() { g.PlayersMutex.Unlock() } } - g.GameData.BufferHealth[i] = -1 + g.GameData.BufferHealth[i].Purge() } } g.GameData.PlayerAlive[i] = false diff --git a/internal/gameServer/tcp.go b/internal/gameServer/tcp.go index 5cd1d7a..90473a1 100644 --- a/internal/gameServer/tcp.go +++ b/internal/gameServer/tcp.go @@ -353,7 +353,7 @@ func (g *GameServer) processTCP(conn *net.TCPConn) { g.PlayersMutex.Unlock() } } - g.GameData.BufferHealth[i] = -1 + g.GameData.BufferHealth[i].Purge() g.GameDataMutex.Unlock() } } diff --git a/internal/gameServer/udp.go b/internal/gameServer/udp.go index e24540f..d48523d 100644 --- a/internal/gameServer/udp.go +++ b/internal/gameServer/udp.go @@ -22,7 +22,7 @@ type GameData struct { SyncValues *lru.Cache[uint32, []byte] PlayerAddresses []*net.UDPAddr BufferSize uint32 - BufferHealth []int32 + BufferHealth []*lru.Cache[uint32, byte] Inputs []*lru.Cache[uint32, InputData] PendingInput []InputData CountLag []uint32 @@ -46,6 +46,7 @@ const ( DisconnectTimeoutS = 30 NoRegID = 255 InputDataMax int = 60 * 60 // One minute of input data + BufferHealthMax = 10 CS4 = 32 ) @@ -149,7 +150,7 @@ func (g *GameServer) processUDP(addr *net.UDPAddr) { return } g.GameData.CountLag[sendingPlayerNumber] = g.sendUDPInput(count, addr, playerNumber, spectator != 0, sendingPlayerNumber) - g.GameData.BufferHealth[sendingPlayerNumber] = int32(g.GameData.recvBuffer[11]) + g.GameData.BufferHealth[sendingPlayerNumber].Add(count, g.GameData.recvBuffer[11]) g.GameDataMutex.Lock() // PlayerAlive can be modified by ManagePlayers in a different thread g.GameData.PlayerAlive[sendingPlayerNumber] = true @@ -212,10 +213,11 @@ func (g *GameServer) createUDPServer() error { g.GameData.PlayerAddresses = make([]*net.UDPAddr, 4) g.GameData.BufferSize = 3 - g.GameData.BufferHealth = []int32{-1, -1, -1, -1} g.GameData.Inputs = make([]*lru.Cache[uint32, InputData], 4) + g.GameData.BufferHealth = make([]*lru.Cache[uint32, byte], 4) for i := range 4 { g.GameData.Inputs[i], _ = lru.New[uint32, InputData](InputDataMax) + g.GameData.BufferHealth[i], _ = lru.New[uint32, byte](BufferHealthMax) } g.GameData.PendingInput = make([]InputData, 4) g.GameData.SyncValues, _ = lru.New[uint32, []byte](100) // Store up to 100 sync values diff --git a/internal/lobbyServer/lobby.go b/internal/lobbyServer/lobby.go index 50be5b8..60f12c1 100644 --- a/internal/lobbyServer/lobby.go +++ b/internal/lobbyServer/lobby.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "math" "net" @@ -84,7 +85,7 @@ type RoomData struct { RoomName string `json:"room_name"` MD5 string `json:"MD5"` Port int `json:"port"` - BufferTarget int32 `json:"buffer_target,omitempty"` + BufferTarget uint32 `json:"buffer_target,omitempty"` } type SocketMessage struct { @@ -110,7 +111,9 @@ func (s *LobbyServer) sendData(ws *websocket.Conn, message SocketMessage) error err := ws.WriteJSON(message) s.SendMutex.Unlock() if err != nil { - return fmt.Errorf("error sending data: %s", err.Error()) + if !errors.Is(err, websocket.ErrCloseSent) { + return err + } } return nil } From 82bd7e21ecbebbbdaa4a6261a06fe85cba9c2372 Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:58:09 +0200 Subject: [PATCH 5/7] protect bufferhealth with mutex (#139) --- internal/gameServer/server.go | 2 ++ internal/gameServer/udp.go | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/gameServer/server.go b/internal/gameServer/server.go index 0a56296..1c08a1b 100644 --- a/internal/gameServer/server.go +++ b/internal/gameServer/server.go @@ -94,6 +94,8 @@ func (g *GameServer) isConnClosed(err error) bool { } func (g *GameServer) bufferHealthAverage(playerNumber int) (float32, error) { + g.GameDataMutex.Lock() + defer g.GameDataMutex.Unlock() var bufferHealth float32 if g.GameData.BufferHealth[playerNumber].Len() > 0 { for _, k := range g.GameData.BufferHealth[playerNumber].Keys() { diff --git a/internal/gameServer/udp.go b/internal/gameServer/udp.go index d48523d..9f35b8b 100644 --- a/internal/gameServer/udp.go +++ b/internal/gameServer/udp.go @@ -150,9 +150,8 @@ func (g *GameServer) processUDP(addr *net.UDPAddr) { return } g.GameData.CountLag[sendingPlayerNumber] = g.sendUDPInput(count, addr, playerNumber, spectator != 0, sendingPlayerNumber) + g.GameDataMutex.Lock() // PlayerAlive and BufferHealth can be modified by ManagePlayers in a different thread g.GameData.BufferHealth[sendingPlayerNumber].Add(count, g.GameData.recvBuffer[11]) - - g.GameDataMutex.Lock() // PlayerAlive can be modified by ManagePlayers in a different thread g.GameData.PlayerAlive[sendingPlayerNumber] = true g.GameDataMutex.Unlock() case CP0Info: From d389434ca0e49ab1a9de0654315f9ee41c23edbb Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:11:03 +0200 Subject: [PATCH 6/7] fix deadlock --- internal/gameServer/server.go | 4 ++-- internal/gameServer/udp.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/gameServer/server.go b/internal/gameServer/server.go index 1c08a1b..6320b29 100644 --- a/internal/gameServer/server.go +++ b/internal/gameServer/server.go @@ -94,8 +94,6 @@ func (g *GameServer) isConnClosed(err error) bool { } func (g *GameServer) bufferHealthAverage(playerNumber int) (float32, error) { - g.GameDataMutex.Lock() - defer g.GameDataMutex.Unlock() var bufferHealth float32 if g.GameData.BufferHealth[playerNumber].Len() > 0 { for _, k := range g.GameData.BufferHealth[playerNumber].Keys() { @@ -118,6 +116,7 @@ func (g *GameServer) ManageBuffer() { // Find the largest buffer health var bufferHealth float32 var foundPlayer bool + g.GameDataMutex.Lock() // BufferHealth can be modified by processUDP in a different thread for i := range 4 { if g.GameData.CountLag[i] == 0 { playerBufferHealth, err := g.bufferHealthAverage(i) @@ -127,6 +126,7 @@ func (g *GameServer) ManageBuffer() { } } } + g.GameDataMutex.Unlock() if foundPlayer { if bufferHealth > float32(g.BufferTarget)+0.5 && g.GameData.BufferSize > 0 { diff --git a/internal/gameServer/udp.go b/internal/gameServer/udp.go index 9f35b8b..efa2eac 100644 --- a/internal/gameServer/udp.go +++ b/internal/gameServer/udp.go @@ -150,7 +150,7 @@ func (g *GameServer) processUDP(addr *net.UDPAddr) { return } g.GameData.CountLag[sendingPlayerNumber] = g.sendUDPInput(count, addr, playerNumber, spectator != 0, sendingPlayerNumber) - g.GameDataMutex.Lock() // PlayerAlive and BufferHealth can be modified by ManagePlayers in a different thread + g.GameDataMutex.Lock() // PlayerAlive and BufferHealth can be modified in different threads g.GameData.BufferHealth[sendingPlayerNumber].Add(count, g.GameData.recvBuffer[11]) g.GameData.PlayerAlive[sendingPlayerNumber] = true g.GameDataMutex.Unlock() From ed77edacdd0add7d90c5075760227b34180a141e Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:19:29 +0200 Subject: [PATCH 7/7] improve check for active players (#140) --- internal/gameServer/server.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/gameServer/server.go b/internal/gameServer/server.go index 6320b29..def17dc 100644 --- a/internal/gameServer/server.go +++ b/internal/gameServer/server.go @@ -94,8 +94,8 @@ func (g *GameServer) isConnClosed(err error) bool { } func (g *GameServer) bufferHealthAverage(playerNumber int) (float32, error) { - var bufferHealth float32 if g.GameData.BufferHealth[playerNumber].Len() > 0 { + var bufferHealth float32 for _, k := range g.GameData.BufferHealth[playerNumber].Keys() { value, _ := g.GameData.BufferHealth[playerNumber].Peek(k) bufferHealth += float32(value) @@ -115,20 +115,22 @@ func (g *GameServer) ManageBuffer() { // Find the largest buffer health var bufferHealth float32 - var foundPlayer bool + var activePlayers bool g.GameDataMutex.Lock() // BufferHealth can be modified by processUDP in a different thread for i := range 4 { if g.GameData.CountLag[i] == 0 { playerBufferHealth, err := g.bufferHealthAverage(i) - if err == nil && playerBufferHealth > bufferHealth { + if err == nil { + activePlayers = true + } + if playerBufferHealth > bufferHealth { bufferHealth = playerBufferHealth - foundPlayer = true } } } g.GameDataMutex.Unlock() - if foundPlayer { + if activePlayers { if bufferHealth > float32(g.BufferTarget)+0.5 && g.GameData.BufferSize > 0 { g.GameData.BufferSize-- g.Logger.Info("reduced buffer size", "bufferHealth", bufferHealth, "bufferSize", g.GameData.BufferSize)