Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add sample SQP server for tf2e proto #17

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# OSX Junk
.DS_Store
21 changes: 21 additions & 0 deletions lib/svrsample/protocol/tf2/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package tf2

import (
"bytes"
"encoding/binary"
)

type (
// encoder is a struct which implements proto.WireEncoder
encoder struct{}
)

// WriteString writes a string to the provided buffer.
func (e *encoder) WriteString(resp *bytes.Buffer, s string) error {
return binary.Write(resp, binary.LittleEndian, []byte(s+"\x00"))
}

// Write writes arbitrary data to the provided buffer.
func (e *encoder) Write(resp *bytes.Buffer, v interface{}) error {
return binary.Write(resp, binary.LittleEndian, v)
}
171 changes: 171 additions & 0 deletions lib/svrsample/protocol/tf2/tf2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package tf2

import (
"bytes"
"runtime"

"github.com/multiplay/go-svrquery/lib/svrsample/common"
)

type (
QueryResponder struct {
enc *encoder
extended bool
state common.QueryState
version int8
}

instanceInfo struct {
Retail byte
InstanceType byte
ClientDLLCRC uint32
NetProtocol uint16
RandomServerID uint64
}

instanceInfoV8 struct {
Retail byte
InstanceType byte
ClientDLLCRC uint32
NetProtocol uint16
HealthFlags uint32
RandomServerID uint32
}

performanceInfo struct {
AverageFrameTime float32
MaxFrameTime float32
AverageUserCommandTime float32
MaxUserCommandTime float32
}

matchState struct {
Phase byte
MaxRounds byte
MaxRoundsIMC byte
MaxRoundsMilitia byte
TimeLimitSeconds uint16
TimePassedSeconds uint16
MaxScore uint16
Team byte
}

queryWireFormat struct {
Header int32
ResponseType byte
Version int8
InstanceInfo *instanceInfo
InstanceInfoV8 *instanceInfoV8
BuildName *string
Datacenter *string
GameMode *string
Port uint16
Platform string
PlaylistVersion string
PlaylistNum uint32
PlaylistName string
PlatformNum *byte
NumClients uint8
MaxClients uint8
Map string
PerformanceInfo *performanceInfo
TeamsLeftWithPlayersNum *uint16
MatchState *matchState
EOP uint64
}
)

const (
notApplicable = "n/a"
)

// NewQueryResponder returns creates a new responder capable of responding
// to tf2-formatted queries.
func NewQueryResponder(state common.QueryState, version int8, extended bool) (common.QueryResponder, error) {
q := &QueryResponder{
enc: &encoder{},
extended: extended,
state: state,
version: version,
}
return q, nil
}

// Respond writes a query response to the requester in the tf2 wire protocol.
func (q *QueryResponder) Respond(_ string, _ []byte) ([]byte, error) {
resp := bytes.NewBuffer(nil)
responseFlag := byte(80)
dc := "dc"

if q.extended {
responseFlag = byte(78)
}

f := queryWireFormat{
Header: -1,
ResponseType: responseFlag,
Version: q.version,
Port: q.state.Port,
Platform: runtime.GOOS,
PlaylistVersion: notApplicable,
PlaylistName: notApplicable,
NumClients: uint8(q.state.CurrentPlayers),
MaxClients: uint8(q.state.MaxPlayers),
Map: q.state.Map,
}

if q.version > 1 {
f.BuildName = &q.state.ServerName
f.Datacenter = &dc
f.GameMode = &q.state.GameType
}

if q.version > 2 {
f.MatchState = &matchState{
Team: 255, // Team information omitted
}
}

// Performance Info
if q.version > 4 {
f.PerformanceInfo = &performanceInfo{
AverageFrameTime: 1.2,
MaxFrameTime: 3.4,
AverageUserCommandTime: 5.6,
MaxUserCommandTime: 7.8,
}
}

if q.version > 5 {
i := uint16(0)
f.TeamsLeftWithPlayersNum = &i
}

if q.version > 6 {
i := byte(0)
f.PlatformNum = &i
}

if q.version > 7 {
f.InstanceInfoV8 = &instanceInfoV8{
HealthFlags: uint32(
1<<0 | // Packet Loss In
1<<1 | // Packet Loss Out
1<<2 | // Packet Choked In
1<<3 | // Packet Choked Out
1<<4 | // Slow Server Frames
1<<5 | // Hitching
1<<6, // DOS
),
RandomServerID: 123456,
}
} else if q.version > 1 {
f.InstanceInfo = &instanceInfo{RandomServerID: 123456}
}

if err := common.WireWrite(resp, q.enc, f); err != nil {
return nil, err
}

return resp.Bytes(), nil
}
104 changes: 104 additions & 0 deletions lib/svrsample/protocol/tf2/tf2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package tf2

import (
"bytes"
"runtime"
"testing"

"github.com/multiplay/go-svrquery/lib/svrquery/protocol"
"github.com/multiplay/go-svrquery/lib/svrquery/protocol/titanfall"
"github.com/multiplay/go-svrquery/lib/svrsample/common"
"github.com/stretchr/testify/require"
)

type (
// queryerFromBytes is a queryer which responds with data in the provided
// byte buffer.
queryerFromBytes struct {
*bytes.Buffer
}
)

func (q queryerFromBytes) Close() error {
return nil
}

func (q queryerFromBytes) Key() string {
return ""
}

func (q queryerFromBytes) Address() string {
return ""
}

func Test_Respond(t *testing.T) {
q, err := NewQueryResponder(common.QueryState{
CurrentPlayers: 1,
MaxPlayers: 2,
ServerName: "my server",
GameType: "game type",
Map: "my map",
Port: 8080,
}, 1, false)
require.NoError(t, err)
require.NotNil(t, q)

resp, err := q.Respond("", nil)
require.NoError(t, err)
require.Equal(t, []byte{0xff, 0xff, 0xff, 0xff, 0x50, 0x1, 0x90, 0x1f, 0x64, 0x61, 0x72, 0x77, 0x69, 0x6e, 0x0, 0x6e, 0x2f, 0x61, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6e, 0x2f, 0x61, 0x0, 0x1, 0x2, 0x6d, 0x79, 0x20, 0x6d, 0x61, 0x70, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, resp)
}

func Test_Respond_tf2e_v8(t *testing.T) {
q, err := NewQueryResponder(common.QueryState{
CurrentPlayers: 1,
MaxPlayers: 2,
ServerName: "my server",
GameType: "game type",
Map: "my map",
Port: 8080,
}, 8, true)
require.NoError(t, err)
require.NotNil(t, q)

data, err := q.Respond("", nil)
require.NoError(t, err)

p, err := protocol.Get("tf2e-v8")
require.NoError(t, err)

client := p(&queryerFromBytes{
bytes.NewBuffer(data),
})
resp, err := client.Query()
require.NoError(t, err)
require.Equal(t, &titanfall.Info{
Header: titanfall.Header{
Prefix: -1,
Command: 78,
Version: 8,
},
InstanceInfoV8: titanfall.InstanceInfoV8{
HealthFlags: 127,
RandomServerID: 123456,
},
BuildName: "my server",
Datacenter: "multiplay-dc",
GameMode: "game type",
BasicInfo: titanfall.BasicInfo{
Port: 8080,
Platform: runtime.GOOS,
PlaylistName: "n/a",
PlaylistVersion: "n/a",
NumClients: 1,
MaxClients: 2,
Map: "my map",
PlatformPlayers: map[string]uint8{},
},
PerformanceInfo: titanfall.PerformanceInfo{
AverageFrameTime: 1.2,
MaxFrameTime: 3.4,
AverageUserCommandTime: 5.6,
MaxUserCommandTime: 7.8,
},
}, resp)
}
10 changes: 9 additions & 1 deletion lib/svrsample/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"fmt"

"github.com/multiplay/go-svrquery/lib/svrsample/common"

"github.com/multiplay/go-svrquery/lib/svrsample/protocol/sqp"
"github.com/multiplay/go-svrquery/lib/svrsample/protocol/tf2"
)

var (
Expand All @@ -19,6 +19,14 @@ func GetResponder(proto string, state common.QueryState) (common.QueryResponder,
switch proto {
case "sqp":
return sqp.NewQueryResponder(state)
case "tf2":
return tf2.NewQueryResponder(state, 1, false)
case "tf2e":
return tf2.NewQueryResponder(state, 3, true)
case "tf2e-v7":
return tf2.NewQueryResponder(state, 7, true)
case "tf2e-v8":
return tf2.NewQueryResponder(state, 8, true)
}
return nil, fmt.Errorf("%w: %s", ErrProtoNotFound, proto)
}