Add A2S query functionality with request and response types

This commit is contained in:
2025-10-04 22:37:52 +08:00
parent 6aef9a2b9a
commit d3cc135a34
7 changed files with 170 additions and 0 deletions

View File

@@ -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 ( @server (
prefix: /api/server/statistics prefix: /api/server/statistics
) )
@@ -273,5 +296,12 @@ service ServerStatistics {
) )
@handler topKillerHandler @handler topKillerHandler
post /top-killer (TopKillerReq) returns (TopKillerResp) 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)
} }

View File

@@ -29,6 +29,7 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // 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 github.com/spaolacci/murmur3 v1.1.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect

View File

@@ -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/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 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 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 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 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= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@@ -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)
}
}
}

View File

@@ -14,6 +14,12 @@ import (
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes( server.AddRoutes(
[]rest.Route{ []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 // Ping the server to check if it's alive
Method: http.MethodGet, Method: http.MethodGet,

View File

@@ -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
}

View File

@@ -3,6 +3,28 @@
package types 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 { type PingReq struct {
} }