Compare commits
No commits in common. "775131aa49e71dc08af5e221dd91eb3fe91a73b2" and "4fe975eb075e8879093e722e4747807806c3c2ac" have entirely different histories.
775131aa49
...
4fe975eb07
5 changed files with 11 additions and 278 deletions
5
go.mod
5
go.mod
|
|
@ -1,3 +1,2 @@
|
||||||
module git.skdevstudios.com/specCon18/reforgerds-updater
|
module git.skdevstudios.com/specCon18/reforgerds-updater
|
||||||
|
go 1.16
|
||||||
go 1.18
|
|
||||||
|
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
package a2s
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Errors
|
|
||||||
var (
|
|
||||||
ErrPlayerRead = errors.New("failed to read player data")
|
|
||||||
ErrMultiPacketInvalid = errors.New("invalid multi-packet ID mismatch")
|
|
||||||
ErrMultiPacketMismatch = errors.New("multi-packet assembly failed")
|
|
||||||
errBzip2 = errors.New("bzip2 compressed response not supported")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Flag type for request/response types
|
|
||||||
type Flag byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
PlayerRequest Flag = 0x55
|
|
||||||
ChallengeResponse Flag = 0x41
|
|
||||||
|
|
||||||
DefaultBufferSize = 1400
|
|
||||||
DefaultDeadlineTimeout = 5
|
|
||||||
singlePacket uint32 = 0xFFFFFFFF
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client handles connection and options
|
|
||||||
type Client struct {
|
|
||||||
Conn *net.UDPConn
|
|
||||||
Address *net.UDPAddr
|
|
||||||
Timeout time.Duration
|
|
||||||
BufferSize uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new Client and dials the connection
|
|
||||||
func New(ip string, port int) (*Client, error) {
|
|
||||||
return NewWithAddr(&net.UDPAddr{IP: net.ParseIP(ip), Port: port})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWithString(addr string) (*Client, error) {
|
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewWithAddr(udpAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWithAddr(addr *net.UDPAddr) (*Client, error) {
|
|
||||||
client := &Client{
|
|
||||||
Address: addr,
|
|
||||||
Timeout: DefaultDeadlineTimeout * time.Second,
|
|
||||||
BufferSize: DefaultBufferSize,
|
|
||||||
}
|
|
||||||
return client, client.Dial()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Dial() error {
|
|
||||||
conn, err := net.DialUDP("udp", nil, c.Address)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Conn = conn
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
return c.Conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SetBufferSize(size uint16) {
|
|
||||||
c.BufferSize = size
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SetDeadlineTimeout(seconds int) {
|
|
||||||
c.Timeout = time.Duration(seconds) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Get(requestType Flag) ([]byte, Flag, time.Duration, error) {
|
|
||||||
resp, duration, err := c.request(requestType, singlePacket)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, err
|
|
||||||
}
|
|
||||||
flag := Flag(resp[4])
|
|
||||||
|
|
||||||
if flag == ChallengeResponse {
|
|
||||||
challenge := binary.BigEndian.Uint32(resp[5:9])
|
|
||||||
resp, _, err = c.request(requestType, challenge)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, err
|
|
||||||
}
|
|
||||||
flag = Flag(resp[4])
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateResponseType(requestType, flag); err != nil {
|
|
||||||
return resp[5:], flag, duration, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp[5:], flag, duration, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) request(requestType Flag, challenge uint32) ([]byte, time.Duration, error) {
|
|
||||||
req, err := createHeader(requestType, challenge)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
if _, err := c.Conn.Write(req); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if err := c.Conn.SetReadDeadline(time.Now().Add(c.Timeout)); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := make([]byte, c.BufferSize)
|
|
||||||
n, err := c.Conn.Read(resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
duration := time.Since(start)
|
|
||||||
|
|
||||||
multi, err := isMultiPacket(resp)
|
|
||||||
if err != nil {
|
|
||||||
return resp, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !multi {
|
|
||||||
return resp[:n], duration, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
packetID := binary.LittleEndian.Uint32(resp[4:8])
|
|
||||||
packetCount := int(resp[8] & 0x0F)
|
|
||||||
currentPacket := int(resp[9] & 0x0F)
|
|
||||||
|
|
||||||
if (packetID & 0x80000000) != 0 {
|
|
||||||
return nil, 0, errBzip2
|
|
||||||
}
|
|
||||||
|
|
||||||
packets := make(map[int][]byte)
|
|
||||||
packets[currentPacket] = resp[12:n]
|
|
||||||
|
|
||||||
for len(packets) < packetCount {
|
|
||||||
buf := make([]byte, c.BufferSize)
|
|
||||||
n, err := c.Conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if binary.LittleEndian.Uint32(buf[4:8]) != packetID {
|
|
||||||
return nil, 0, ErrMultiPacketInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPacket = int(buf[9] & 0x0F)
|
|
||||||
if _, exists := packets[currentPacket]; !exists {
|
|
||||||
packets[currentPacket] = buf[12:n]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var assembledResp []byte
|
|
||||||
for i := 0; i < packetCount; i++ {
|
|
||||||
data, exists := packets[i]
|
|
||||||
if !exists {
|
|
||||||
return nil, 0, ErrMultiPacketMismatch
|
|
||||||
}
|
|
||||||
assembledResp = append(assembledResp, data...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return assembledResp, duration, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
|
|
||||||
func createHeader(requestType Flag, challenge uint32) ([]byte, error) {
|
|
||||||
header := []byte{0xFF, 0xFF, 0xFF, 0xFF, byte(requestType)}
|
|
||||||
if challenge != singlePacket {
|
|
||||||
challengeBytes := make([]byte, 4)
|
|
||||||
binary.BigEndian.PutUint32(challengeBytes, challenge)
|
|
||||||
header = append(header, challengeBytes...)
|
|
||||||
}
|
|
||||||
return header, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateResponseType(request, response Flag) error {
|
|
||||||
if request == PlayerRequest && response != 0x44 {
|
|
||||||
return fmt.Errorf("unexpected player response flag: 0x%X", response)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isMultiPacket(buf []byte) (bool, error) {
|
|
||||||
if len(buf) < 4 {
|
|
||||||
return false, errors.New("packet too short")
|
|
||||||
}
|
|
||||||
header := binary.LittleEndian.Uint32(buf[:4])
|
|
||||||
return header == 0xFFFFFFFE, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.skdevstudios.com/specCon18/reforgerds-updater/internal/a2s/bread"
|
"reforgerds-updater/internal/bread"
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://developer.valvesoftware.com/wiki/Server_queries#Response_Format_2
|
// https://developer.valvesoftware.com/wiki/Server_queries#Response_Format_2
|
||||||
|
|
|
||||||
64
main.go
64
main.go
|
|
@ -8,8 +8,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.skdevstudios.com/specCon18/reforgerds-updater/internal/a2s"
|
"git.skdevstudios.com/specCon18/reforgerds-updater/internal/a2s"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -20,13 +18,9 @@ type Update struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
updateURL = "http://127.0.0.1:3000/updates"
|
updateURL = "http://127.0.0.1:3000/updates"
|
||||||
stateFilePath = "latest_version.txt"
|
stateFilePath = "latest_version.txt"
|
||||||
|
|
||||||
serverIP = "127.0.0.1"
|
|
||||||
serverPort = 17777
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
resp, err := http.Get(updateURL)
|
resp, err := http.Get(updateURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -65,38 +59,19 @@ func main() {
|
||||||
if versionCompare(latest, prevVersion) > 0 {
|
if versionCompare(latest, prevVersion) > 0 {
|
||||||
fmt.Printf("New version found! %s > %s\n", latest, prevVersion)
|
fmt.Printf("New version found! %s > %s\n", latest, prevVersion)
|
||||||
|
|
||||||
// Always update the state file
|
// Run steamcmd with reforger_update script
|
||||||
err := os.WriteFile(stateFilePath, []byte(latest), 0644)
|
fmt.Println("Running update command...")
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to write version file: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for online players
|
|
||||||
players, err := fetchPlayers(serverIP, serverPort)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error checking players: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(players) > 0 {
|
|
||||||
fmt.Printf("Players are currently online (%d):\n", len(players))
|
|
||||||
for _, p := range players {
|
|
||||||
fmt.Printf("- %-16s | Score: %d | Time: %s\n", p.Name, p.Score, formatDuration(p.Duration))
|
|
||||||
}
|
|
||||||
fmt.Println("Skipping update while players are online.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// No players — run steamcmd
|
|
||||||
fmt.Println("No players online. Running update command...")
|
|
||||||
cmd := exec.Command("./steamcmd.sh", "+runscript", "reforger_update")
|
cmd := exec.Command("./steamcmd.sh", "+runscript", "reforger_update")
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
err = cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Update command failed: %v\n", err)
|
fmt.Printf("Update command failed: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the new latest version
|
||||||
|
_ = os.WriteFile(stateFilePath, []byte(latest), 0644)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("No new version. Latest seen: %s\n", prevVersion)
|
fmt.Printf("No new version. Latest seen: %s\n", prevVersion)
|
||||||
}
|
}
|
||||||
|
|
@ -130,26 +105,3 @@ func versionCompare(a, b string) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchPlayers(ip string, port int) ([]a2s.Player, error) {
|
|
||||||
client, err := a2s.New(ip, port)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create client: %w", err)
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
client.SetBufferSize(2048)
|
|
||||||
client.SetDeadlineTimeout(3)
|
|
||||||
|
|
||||||
players, err := client.GetPlayers()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get players: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return *players, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatDuration(d time.Duration) string {
|
|
||||||
minutes := int(d.Minutes())
|
|
||||||
seconds := int(d.Seconds()) % 60
|
|
||||||
return fmt.Sprintf("%02d:%02d", minutes, seconds)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
16
scratch
16
scratch
|
|
@ -1,16 +0,0 @@
|
||||||
client, err := a2s.New("127.0.0.1", 27016)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client.SetBufferSize(2048)
|
|
||||||
client.SetDeadlineTimeout(3)
|
|
||||||
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
players,err := client.GetPlayers()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%+v\n", players)
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue