From ec93573cbab9630bc3ad916af8a398b127075999 Mon Sep 17 00:00:00 2001 From: steven carpenter Date: Mon, 30 Jun 2025 16:39:46 -0400 Subject: [PATCH] vendored https://github.com/WoozyMasta/a2s/blob/master/pkg/a2s/a2s_players.go --- .direnv/flake-profile | 1 + .direnv/flake-profile-1-link | 1 + .envrc | 1 + internal/a2s/bread/helpers.go | 37 ++++++ internal/a2s/bread/reader.go | 222 ++++++++++++++++++++++++++++++++++ internal/a2s/players.go | 57 +++++++++ main.go | 1 + 7 files changed, 320 insertions(+) create mode 120000 .direnv/flake-profile create mode 120000 .direnv/flake-profile-1-link create mode 100644 .envrc create mode 100644 internal/a2s/bread/helpers.go create mode 100644 internal/a2s/bread/reader.go create mode 100644 internal/a2s/players.go diff --git a/.direnv/flake-profile b/.direnv/flake-profile new file mode 120000 index 0000000..0c05709 --- /dev/null +++ b/.direnv/flake-profile @@ -0,0 +1 @@ +flake-profile-1-link \ No newline at end of file diff --git a/.direnv/flake-profile-1-link b/.direnv/flake-profile-1-link new file mode 120000 index 0000000..c57c7b6 --- /dev/null +++ b/.direnv/flake-profile-1-link @@ -0,0 +1 @@ +/nix/store/73lbr3h20af5k4bmdz8wd5wi0vyv29d6-nix-shell-env \ No newline at end of file diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/internal/a2s/bread/helpers.go b/internal/a2s/bread/helpers.go new file mode 100644 index 0000000..7b20614 --- /dev/null +++ b/internal/a2s/bread/helpers.go @@ -0,0 +1,37 @@ +package bread + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" +) + +var ( + ErrUnderflow = errors.New("buffer underflow: not enough data to read") // Error if buffer underflow + ErrNumber = errors.New("failed to read number from buffer") // Error with read number + ErrBool = errors.New("unsupported boolean byte in buffer") // Error with read boolean + ErrString = errors.New("length of the string from the buffer is less than expected") // Error with read string +) + +// Helper for check bytes in buffer not underflow +func checkLength(buf *bytes.Buffer, size int) error { + if buf.Len() < size { + return fmt.Errorf("%w: got %d of expected %d bytes", ErrUnderflow, buf.Len(), size) + } + + return nil +} + +// Helper for read number in LittleEndian +func readNumber(buf *bytes.Buffer, data any, size int) error { + if buf.Len() < size { + return fmt.Errorf("%w: got %d of expected %d bytes", ErrUnderflow, buf.Len(), size) + } + + if err := binary.Read(buf, binary.LittleEndian, data); err != nil { + return fmt.Errorf("%w: %w", ErrNumber, err) + } + + return nil +} diff --git a/internal/a2s/bread/reader.go b/internal/a2s/bread/reader.go new file mode 100644 index 0000000..efee553 --- /dev/null +++ b/internal/a2s/bread/reader.go @@ -0,0 +1,222 @@ +// Package bread is a set of helper functions for reading bytes into different types from a buffer. +package bread + +import ( + "bytes" + "fmt" + "math" + "time" +) + +// Read bytes buffer and return byte +func Byte(buf *bytes.Buffer) (byte, error) { + if err := checkLength(buf, 1); err != nil { + return 0, err + } + + return buf.ReadByte() +} + +// Read bytes buffer and return boolean, where 1 is true, 0 is false, other error +func Bool(buf *bytes.Buffer) (bool, error) { + value, err := Byte(buf) + if err != nil { + return false, err + } + + switch value { + case 1: + return true, nil + case 0: + return false, nil + default: + return false, fmt.Errorf("%w: 0x%X", ErrBool, value) + } +} + +// Read bytes buffer and return int8 +func Int8(buf *bytes.Buffer) (int8, error) { + value, err := Byte(buf) + if err != nil { + return 0, err + } + + return int8(value), nil +} + +// Read bytes buffer and return int16 +func Int16(buf *bytes.Buffer) (int16, error) { + var value int16 + if err := readNumber(buf, &value, 2); err != nil { + return 0, err + } + + return value, nil +} + +// Read bytes buffer and return int32 +func Int32(buf *bytes.Buffer) (int32, error) { + var value int32 + if err := readNumber(buf, &value, 4); err != nil { + return 0, err + } + + return value, nil +} + +// Read bytes buffer and return int64 +func Int64(buf *bytes.Buffer) (int64, error) { + var value int64 + if err := readNumber(buf, &value, 8); err != nil { + return 0, err + } + + return value, nil +} + +// Read bytes buffer and return uint16 +func Uint16(buf *bytes.Buffer) (uint16, error) { + var value uint16 + if err := readNumber(buf, &value, 2); err != nil { + return 0, err + } + + return value, nil +} + +// Read bytes buffer and return uint32 +func Uint32(buf *bytes.Buffer) (uint32, error) { + var value uint32 + if err := readNumber(buf, &value, 4); err != nil { + return 0, err + } + + return value, nil +} + +// Read bytes buffer and return uint64 +func Uint64(buf *bytes.Buffer) (uint64, error) { + var value uint64 + if err := readNumber(buf, &value, 8); err != nil { + return 0, err + } + + return value, nil +} + +// Read bytes buffer and return float32 +func Float32(buf *bytes.Buffer) (float32, error) { + var value float32 + if err := readNumber(buf, &value, 4); err != nil { + return 0, err + } + + return value, nil +} + +// Read bytes buffer and return float64 +func Float64(buf *bytes.Buffer) (float64, error) { + var value float64 + if err := readNumber(buf, &value, 8); err != nil { + return 0, err + } + + return value, nil +} + +// Read bytes buffer to first 0x00 delimiter and return as string +func String(buf *bytes.Buffer) (string, error) { + value, err := buf.ReadBytes(0x00) + if err != nil { + return "", err + } + + return string(value[:len(value)-1]), nil +} + +// Read bytes buffer by size count and return as string +func StringLen(buf *bytes.Buffer, size int) (string, error) { + if err := checkLength(buf, size); err != nil { + return "", err + } + + value := make([]byte, size) + n, err := buf.Read(value) + if err != nil { + return "", err + } + + if n != size { + return "", fmt.Errorf("%w: got '%s' with %d length but expected %d", ErrString, value, n, size) + } + + return string(value), nil +} + +// Read bytes buffer as float32 and return as time.Duration +func Duration32(buf *bytes.Buffer) (time.Duration, error) { + f, err := Float32(buf) + if err != nil { + return 0, err + } + + seconds := int64(f) + nanoseconds := int64(math.Round(float64(f-float32(seconds)) * 1e9)) + + return time.Duration(seconds)*time.Second + time.Duration(nanoseconds)*time.Nanosecond, nil +} + +// Read bytes buffer as float64 and return as time.Duration +func Duration64(buf *bytes.Buffer) (time.Duration, error) { + f, err := Float64(buf) + if err != nil { + return 0, err + } + + seconds := int64(f) + nanoseconds := int64(math.Round((f - float64(seconds)) * 1e9)) + + return time.Duration(seconds)*time.Second + time.Duration(nanoseconds)*time.Nanosecond, nil +} + +// Read bytes buffer to first 0x00 delimiter +func BytesPage(buf *bytes.Buffer) ([]byte, error) { + value, err := buf.ReadBytes(0x00) + if err != nil { + return nil, err + } + n := len(value) - 1 + + return value[:n], nil +} + +// Replace Escape sequence to Escape value +// +// {0x01, 0x01} -> 0x01 +// {0x01, 0x02} -> 0x00 +// {0x01, 0x03} -> 0xFF +func EscapeSequences(data []byte) []byte { + var buf bytes.Buffer + + for i := 0; i < len(data); i++ { + if data[i] == 0x01 && i+1 < len(data) { + switch data[i+1] { + case 0x01: + buf.WriteByte(0x01) + i++ + case 0x02: + buf.WriteByte(0x00) + i++ + case 0x03: + buf.WriteByte(0xFF) + i++ + default: + buf.WriteByte(data[i]) + } + } else { + buf.WriteByte(data[i]) + } + } + + return buf.Bytes() +} diff --git a/internal/a2s/players.go b/internal/a2s/players.go new file mode 100644 index 0000000..6d6e54b --- /dev/null +++ b/internal/a2s/players.go @@ -0,0 +1,57 @@ +package a2s + +import ( + "bytes" + "fmt" + "time" + + "reforgerds-updater/internal/bread" +) + +// https://developer.valvesoftware.com/wiki/Server_queries#Response_Format_2 +type Player struct { + Name string `json:"name,omitempty"` + Duration time.Duration `json:"duration,omitempty"` + Score uint32 `json:"score,omitempty"` + Index byte `json:"index,omitempty"` +} + +// Get A2S_PLAYER +func (c *Client) GetPlayers() (*[]Player, error) { + data, _, _, err := c.Get(PlayerRequest) + if err != nil { + return nil, err + } + + buf := bytes.NewBuffer(data) + count, err := bread.Byte(buf) + if err != nil { + return nil, fmt.Errorf("%w count: %w", ErrPlayerRead, err) + } + + players := []Player{} + + for i := 0; i < int(count); i++ { + player := Player{} + + if player.Index, err = bread.Byte(buf); err != nil { + return nil, fmt.Errorf("%w index: %w", ErrPlayerRead, err) + } + + if player.Name, err = bread.String(buf); err != nil { + return nil, fmt.Errorf("%w name: %w", ErrPlayerRead, err) + } + + if player.Score, err = bread.Uint32(buf); err != nil { + return nil, fmt.Errorf("%w score: %w", ErrPlayerRead, err) + } + + if player.Duration, err = bread.Duration32(buf); err != nil { + return nil, fmt.Errorf("%w duration: %w", ErrPlayerRead, err) + } + + players = append(players, player) + } + + return &players, nil +} diff --git a/main.go b/main.go index 919f5dd..77065c8 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "strings" + "git.skdevstudios.com/specCon18/reforgerds-updater/internal/a2s" ) type Update struct {