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..def17dc 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) { + 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) + } + 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,33 @@ func (g *GameServer) ManageBuffer() { } // Find the largest buffer health - var bufferHealth int32 = -1 + var bufferHealth float32 + var activePlayers bool + g.GameDataMutex.Lock() // BufferHealth can be modified by processUDP in a different thread 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 { + activePlayers = true + } + if playerBufferHealth > bufferHealth { + bufferHealth = playerBufferHealth } } } + g.GameDataMutex.Unlock() - // Adjust the buffer size - if bufferHealth != -1 { - if bufferHealth > g.BufferTarget && g.GameData.BufferSize > 0 { + 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) - } 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 +155,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 +174,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 90bfaa2..90473a1 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 { @@ -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 7d79d10..efa2eac 100644 --- a/internal/gameServer/udp.go +++ b/internal/gameServer/udp.go @@ -22,11 +22,10 @@ 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 []uint32 + PendingInput []InputData CountLag []uint32 - PendingPlugin []byte sendBuffer []byte recvBuffer []byte PlayerAlive []bool @@ -47,6 +46,7 @@ const ( DisconnectTimeoutS = 30 NoRegID = 255 InputDataMax int = 60 * 60 // One minute of input data + BufferHealthMax = 10 CS4 = 32 ) @@ -71,7 +71,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 +127,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 { @@ -147,14 +149,11 @@ 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.BufferHealth[sendingPlayerNumber] = int32(g.GameData.recvBuffer[11]) - - g.GameDataMutex.Lock() // PlayerAlive can be modified by ManagePlayers in a different thread + g.GameData.CountLag[sendingPlayerNumber] = g.sendUDPInput(count, addr, playerNumber, spectator != 0, sendingPlayerNumber) + 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() - - g.GameData.CountLag[sendingPlayerNumber] = countLag case CP0Info: if g.GameData.Status&StatusDesync == 0 { viCount := binary.BigEndian.Uint32(g.GameData.recvBuffer[1:]) @@ -213,13 +212,13 @@ 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([]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) diff --git a/internal/lobbyServer/lobby.go b/internal/lobbyServer/lobby.go index f2231bd..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 } @@ -671,7 +674,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)