From d3cc135a3496b41e14cea1c55d00d913fe2bc951 Mon Sep 17 00:00:00 2001 From: cialloo Date: Sat, 4 Oct 2025 22:37:52 +0800 Subject: [PATCH] Add A2S query functionality with request and response types --- api/ServerStatistics.api | 30 ++++++++++ src/go.mod | 1 + src/go.sum | 2 + src/internal/handler/a2squeryhandler.go | 29 +++++++++ src/internal/handler/routes.go | 6 ++ src/internal/logic/a2squerylogic.go | 80 +++++++++++++++++++++++++ src/internal/types/types.go | 22 +++++++ 7 files changed, 170 insertions(+) create mode 100644 src/internal/handler/a2squeryhandler.go create mode 100644 src/internal/logic/a2squerylogic.go diff --git a/api/ServerStatistics.api b/api/ServerStatistics.api index d7eee7d..b36baa4 100644 --- a/api/ServerStatistics.api +++ b/api/ServerStatistics.api @@ -179,6 +179,29 @@ type ( } ) +type ( + A2SQueryReq { + ServerIP string `json:"serverIP"` // Server IP address + ServerPort int `json:"serverPort"` // Server port + Timeout int `json:"timeout,example=3000,default=3000"` // Timeout in milliseconds (default: 3000ms) + } + A2SQueryResp { + ServerName string `json:"serverName"` // Server name + MapName string `json:"mapName"` // Current map name + GameDirectory string `json:"gameDirectory"` // Game directory + GameDescription string `json:"gameDescription"` // Game description + AppID int `json:"appID"` // Steam App ID + PlayerCount int `json:"playerCount"` // Current player count + MaxPlayers int `json:"maxPlayers"` // Maximum player count + BotCount int `json:"botCount"` // Bot count + ServerType string `json:"serverType"` // Server type (e.g., "d" for dedicated) + Environment string `json:"environment"` // Environment (e.g., "w" for Windows, "l" for Linux) + Visibility int `json:"visibility"` // Visibility (0 for public, 1 for private) + VAC int `json:"vac"` // VAC status (0 for unsecured, 1 for secured) + Version string `json:"version"` // Server version + } +) + @server ( prefix: /api/server/statistics ) @@ -273,5 +296,12 @@ service ServerStatistics { ) @handler topKillerHandler post /top-killer (TopKillerReq) returns (TopKillerResp) + + @doc ( + summary: "Perform an A2S query to get real-time server information" + description: "Perform an A2S query to get real-time server information" + ) + @handler a2sQueryHandler + post /a2s-query (A2SQueryReq) returns (A2SQueryResp) } diff --git a/src/go.mod b/src/go.mod index a60164d..395e2de 100644 --- a/src/go.mod +++ b/src/go.mod @@ -29,6 +29,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/rumblefrog/go-a2s v1.0.2 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect diff --git a/src/go.sum b/src/go.sum index 00d6652..b78da10 100644 --- a/src/go.sum +++ b/src/go.sum @@ -63,6 +63,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rumblefrog/go-a2s v1.0.2 h1:rT/QP/B+h2R9/3PEfmOkWPdHnEKExskOMPTTkeX+vuA= +github.com/rumblefrog/go-a2s v1.0.2/go.mod h1:6nq//LMUMa3ElowQ7eH8atnDbQG+nVMFsaMFzSo8p/M= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/src/internal/handler/a2squeryhandler.go b/src/internal/handler/a2squeryhandler.go new file mode 100644 index 0000000..dd1bd5b --- /dev/null +++ b/src/internal/handler/a2squeryhandler.go @@ -0,0 +1,29 @@ +package handler + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "src/internal/logic" + "src/internal/svc" + "src/internal/types" +) + +// Perform an A2S query to get real-time server information +func a2sQueryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.A2SQueryReq + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := logic.NewA2sQueryLogic(r.Context(), svcCtx) + resp, err := l.A2sQuery(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/src/internal/handler/routes.go b/src/internal/handler/routes.go index 710270a..9085cb4 100644 --- a/src/internal/handler/routes.go +++ b/src/internal/handler/routes.go @@ -14,6 +14,12 @@ import ( func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { server.AddRoutes( []rest.Route{ + { + // Perform an A2S query to get real-time server information + Method: http.MethodPost, + Path: "/a2s-query", + Handler: a2sQueryHandler(serverCtx), + }, { // Ping the server to check if it's alive Method: http.MethodGet, diff --git a/src/internal/logic/a2squerylogic.go b/src/internal/logic/a2squerylogic.go new file mode 100644 index 0000000..2c53aca --- /dev/null +++ b/src/internal/logic/a2squerylogic.go @@ -0,0 +1,80 @@ +package logic + +import ( + "context" + "fmt" + "time" + + "src/internal/svc" + "src/internal/types" + + "github.com/rumblefrog/go-a2s" + "github.com/zeromicro/go-zero/core/logx" +) + +type A2sQueryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Perform an A2S query to get real-time server information +func NewA2sQueryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *A2sQueryLogic { + return &A2sQueryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *A2sQueryLogic) A2sQuery(req *types.A2SQueryReq) (resp *types.A2SQueryResp, err error) { + // Set default timeout if not provided + timeout := time.Duration(req.Timeout) * time.Millisecond + if req.Timeout <= 0 { + timeout = 3 * time.Second + } + + // Create A2S client with timeout option + client, err := a2s.NewClient(fmt.Sprintf("%s:%d", req.ServerIP, req.ServerPort), a2s.TimeoutOption(timeout)) + if err != nil { + l.Logger.Errorf("Failed to create A2S client: %v", err) + return nil, err + } + defer client.Close() + + // Query server info + info, err := client.QueryInfo() + if err != nil { + l.Logger.Errorf("Failed to query server info: %v", err) + return nil, err + } + + // Convert boolean values to int + visibility := 0 + if info.Visibility { + visibility = 1 + } + vac := 0 + if info.VAC { + vac = 1 + } + + // Map response to our types + resp = &types.A2SQueryResp{ + ServerName: info.Name, + MapName: info.Map, + GameDirectory: info.Folder, + GameDescription: info.Game, + AppID: int(info.ID), + PlayerCount: int(info.Players), + MaxPlayers: int(info.MaxPlayers), + BotCount: int(info.Bots), + ServerType: info.ServerType.String(), + Environment: info.ServerOS.String(), + Visibility: visibility, + VAC: vac, + Version: info.Version, + } + + return resp, nil +} diff --git a/src/internal/types/types.go b/src/internal/types/types.go index e770070..d0d0065 100644 --- a/src/internal/types/types.go +++ b/src/internal/types/types.go @@ -3,6 +3,28 @@ package types +type A2SQueryReq struct { + ServerIP string `json:"serverIP"` // Server IP address + ServerPort int `json:"serverPort"` // Server port + Timeout int `json:"timeout,example=3000,default=3000"` // Timeout in milliseconds (default: 3000ms) +} + +type A2SQueryResp struct { + ServerName string `json:"serverName"` // Server name + MapName string `json:"mapName"` // Current map name + GameDirectory string `json:"gameDirectory"` // Game directory + GameDescription string `json:"gameDescription"` // Game description + AppID int `json:"appID"` // Steam App ID + PlayerCount int `json:"playerCount"` // Current player count + MaxPlayers int `json:"maxPlayers"` // Maximum player count + BotCount int `json:"botCount"` // Bot count + ServerType string `json:"serverType"` // Server type (e.g., "d" for dedicated) + Environment string `json:"environment"` // Environment (e.g., "w" for Windows, "l" for Linux) + Visibility int `json:"visibility"` // Visibility (0 for public, 1 for private) + VAC int `json:"vac"` // VAC status (0 for unsecured, 1 for secured) + Version string `json:"version"` // Server version +} + type PingReq struct { }