lil refactor

This commit is contained in:
ChillerDragon 2024-06-23 13:05:57 +08:00
parent 0a88246a9a
commit a6c11c71a8
21 changed files with 1114 additions and 390 deletions

View file

@ -1,13 +1,55 @@
# go-teeworlds-protocol # go-teeworlds-protocol
## Early and active development! Still undergoing major refactors!
## WARNING! NOT READY TO BE USED YET!
## Apis might change. Packages and repository might be renamed!
A client side network protocol implementation of the game teeworlds. A client side network protocol implementation of the game teeworlds.
## run client ## low level api for power users
The packages **chunk7, messages7, network7, packer, protocol7** Implement the low level 0.7 teeworlds protocol. Use them if you want to build something advanced such as a custom proxy.
## high level api for ease of use
The package **teeworlds7** implements a high level client library. Designed for ease of use.
```go
package main
import (
"fmt"
"os"
"time"
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/teeworlds7"
)
func main() {
client := teeworlds7.Client{Name: "nameless tee"}
// Register your callback for incoming chat messages
// For a full list of all callbacks see: https://github.com/teeworlds-go/go-teeworlds-protocol/tree/master/teeworlds7/user_hooks.go
client.OnChat(func(msg *messages7.SvChat, defaultAction teeworlds7.DefaultAction) {
// the default action prints the chat message to the console
// if this is not called and you don't print it your self the chat will not be visible
defaultAction()
if msg.Message == "!ping" {
// Send reply in chat using the SendChat() action
// For a full list of all actions see: https://github.com/teeworlds-go/go-teeworlds-protocol/tree/master/teeworlds7/user_actions.go
client.SendChat("pong")
}
})
client.Connect("127.0.0.1", 8303)
}
``` ```
go build
./teeworlds 127.0.0.1 8303
``` Example usages:
- [client_verbose](./examples/client_verbose/) a verbose client show casing the easy to use high level api
## tests ## tests

2
examples/client_verbose/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
client_verbose
client_verbose.exe

View file

@ -0,0 +1,7 @@
# client_verbose
```
go build client_verbose.go
./client_verbose
```

View file

@ -0,0 +1,69 @@
package main
// TODO: split this up into multiple examples
// the verbose mode should not have a render loop
// the verbose mode should not implement disconnect
import (
"fmt"
"os"
"time"
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
"github.com/teeworlds-go/go-teeworlds-protocol/teeworlds7"
)
func main() {
client := teeworlds7.Client{
Name: "nameless tee",
}
client.OnAccept(func(msg *messages7.CtrlAccept, defaultAction teeworlds7.DefaultAction) {
// respond with the next message to establish a connection
defaultAction()
fmt.Println("got accept message")
})
// read incoming traffic
// you can also alter packet here before it will be processed by the internal state machine
client.OnPacket(func(packet *protocol7.Packet) {
fmt.Printf("got packet with %d messages\n", len(packet.Messages))
})
// inspect outgoing traffic
// you can also alter packet here before it will be sent to the server
client.OnSend(func(packet *protocol7.Packet) {
fmt.Printf("sending packet with %d messages\n", len(packet.Messages))
})
client.OnChat(func(msg *messages7.SvChat, defaultAction teeworlds7.DefaultAction) {
// the default action prints the chat message to the console
// if this is not called and you don't print it your self the chat will not be visible
defaultAction()
// additional custom chat print
fmt.Printf("%d %s\n", msg.ClientId, msg.Message)
})
// this is matching the default behavior
client.OnDisconnect(func(msg *messages7.CtrlClose, defaultAction teeworlds7.DefaultAction) {
fmt.Printf("disconnected (%s)\n", msg.Reason)
os.Exit(0)
})
// if you do not implement OnError it will throw on error
client.OnError(func(err error) {
fmt.Print(err)
})
go func() {
client.Connect("127.0.0.1", 8303)
}()
for {
// render loop
time.Sleep(100_000_000)
}
}

45
messages7/sv_broadcast.go Normal file
View file

@ -0,0 +1,45 @@
package messages7
import (
"github.com/teeworlds-go/go-teeworlds-protocol/chunk7"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type SvBroadcast struct {
ChunkHeader *chunk7.ChunkHeader
Message string
}
func (msg SvBroadcast) MsgId() int {
return network7.MsgGameSvBroadcast
}
func (msg SvBroadcast) MsgType() network7.MsgType {
return network7.TypeNet
}
func (msg SvBroadcast) System() bool {
return false
}
func (msg SvBroadcast) Vital() bool {
return true
}
func (msg SvBroadcast) Pack() []byte {
return packer.PackStr(msg.Message)
}
func (msg *SvBroadcast) Unpack(u *packer.Unpacker) {
msg.Message = u.GetString()
}
func (msg *SvBroadcast) Header() *chunk7.ChunkHeader {
return msg.ChunkHeader
}
func (msg *SvBroadcast) SetHeader(header *chunk7.ChunkHeader) {
msg.ChunkHeader = header
}

View file

@ -11,7 +11,7 @@ import (
type SvChat struct { type SvChat struct {
ChunkHeader *chunk7.ChunkHeader ChunkHeader *chunk7.ChunkHeader
Mode int Mode network7.ChatMode
ClientId int ClientId int
TargetId int TargetId int
Message string Message string
@ -35,7 +35,7 @@ func (msg SvChat) Vital() bool {
func (msg SvChat) Pack() []byte { func (msg SvChat) Pack() []byte {
return slices.Concat( return slices.Concat(
packer.PackInt(msg.Mode), packer.PackInt(int(msg.Mode)),
packer.PackInt(msg.ClientId), packer.PackInt(msg.ClientId),
packer.PackInt(msg.TargetId), packer.PackInt(msg.TargetId),
packer.PackStr(msg.Message), packer.PackStr(msg.Message),
@ -43,7 +43,7 @@ func (msg SvChat) Pack() []byte {
} }
func (msg *SvChat) Unpack(u *packer.Unpacker) { func (msg *SvChat) Unpack(u *packer.Unpacker) {
msg.Mode = u.GetInt() msg.Mode = network7.ChatMode(u.GetInt())
msg.ClientId = u.GetInt() msg.ClientId = u.GetInt()
msg.TargetId = u.GetInt() msg.TargetId = u.GetInt()
msg.Message = u.GetString() msg.Message = u.GetString()

55
messages7/sv_team.go Normal file
View file

@ -0,0 +1,55 @@
package messages7
import (
"slices"
"github.com/teeworlds-go/go-teeworlds-protocol/chunk7"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type SvTeam struct {
ChunkHeader *chunk7.ChunkHeader
ClientId int
Silent bool
CooldownTick int
}
func (msg SvTeam) MsgId() int {
return network7.MsgGameSvTeam
}
func (msg SvTeam) MsgType() network7.MsgType {
return network7.TypeNet
}
func (msg SvTeam) System() bool {
return false
}
func (msg SvTeam) Vital() bool {
return true
}
func (msg SvTeam) Pack() []byte {
return slices.Concat(
packer.PackInt(msg.ClientId),
packer.PackBool(msg.Silent),
packer.PackInt(msg.CooldownTick),
)
}
func (msg *SvTeam) Unpack(u *packer.Unpacker) {
msg.ClientId = u.GetInt()
msg.Silent = u.GetInt() != 0
msg.CooldownTick = u.GetInt()
}
func (msg *SvTeam) Header() *chunk7.ChunkHeader {
return msg.ChunkHeader
}
func (msg *SvTeam) SetHeader(header *chunk7.ChunkHeader) {
msg.ChunkHeader = header
}

View file

@ -5,6 +5,14 @@ const (
NetVersion = "0.7 802f1be60a05665f" NetVersion = "0.7 802f1be60a05665f"
ClientVersion = 0x0705 ClientVersion = 0x0705
ChatAll ChatMode = 1
ChatTeam ChatMode = 2
ChatWhisper ChatMode = 3
TeamSpectators GameTeam = -1
TeamRed GameTeam = 0
TeamBlue GameTeam = 1
MsgCtrlKeepAlive = 0x00 MsgCtrlKeepAlive = 0x00
MsgCtrlConnect = 0x01 MsgCtrlConnect = 0x01
MsgCtrlAccept = 0x02 MsgCtrlAccept = 0x02
@ -43,10 +51,44 @@ const (
MsgSysMaplistEntryRem = 30 MsgSysMaplistEntryRem = 30
MsgGameSvMotd = 1 MsgGameSvMotd = 1
MsgGameSvBroadcast = 2
MsgGameSvChat = 3 MsgGameSvChat = 3
MsgGameSvTeam = 4
MsgGameSvKillMsg = 5
MsgGameSvTuneParams = 6
MsgGameSvExtraProjectile = 7
MsgGameReadyToEnter = 8 MsgGameReadyToEnter = 8
MsgGameWeaponPickup = 9
MsgGameEmoticon = 10
MsgGameSvVoteClearoptions = 11
MsgGameSvVoteOptionlistadd = 12
MsgGameSvVotePptionadd = 13
MsgGameSvVoteOptionremove = 14
MsgGameSvVoteSet = 15
MsgGameSvVoteStatus = 16
MsgGameSvServerSettings = 17
MsgGameSvClientInfo = 18 MsgGameSvClientInfo = 18
MsgGameSvGameInfo = 19
MsgGameSvClientDrop = 20
MsgGameSvGameMsg = 21
MsgGameDeClientEnter = 22
MsgGameDeClientLeave = 23
MsgGameClSay = 24
MsgGameClSetTeam = 25
MsgGameClSetSpectatorMode = 26
MsgGameClStartInfo = 27 MsgGameClStartInfo = 27
MsgGameClKill = 28
MsgGameClReadyChange = 29
MsgGameClEmoticon = 30
MsgGameClVote = 31
MsgGameClCallVote = 32
MsgGameSvSkinChange = 33
MsgGameClSkinChange = 34
MsgGameSvRaceFinish = 35
MsgGameSvCheckpoint = 36
MsgGameSvCommandInfo = 37
MsgGameSvCommandInfoRemove = 38
MsgGameClCommand = 39
TypeControl MsgType = 1 TypeControl MsgType = 1
TypeNet MsgType = 2 TypeNet MsgType = 2
@ -61,5 +103,7 @@ const (
NumWeapons Weapon = 6 NumWeapons Weapon = 6
) )
type ChatMode int
type GameTeam int
type MsgType int type MsgType int
type Weapon int type Weapon int

View file

@ -1,246 +0,0 @@
package protocol7
import (
"bytes"
"fmt"
"os"
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
)
type Player struct {
Info messages7.SvClientInfo
}
type Connection struct {
ClientToken [4]byte
ServerToken [4]byte
// The amount of vital chunks received
Ack int
// The amount of vital chunks sent
Sequence int
// The amount of vital chunks acknowledged by the peer
PeerAck int
Players []Player
}
func (connection *Connection) BuildResponse() *Packet {
return &Packet{
Header: PacketHeader{
Flags: PacketFlags{
Connless: false,
Compression: false,
Resend: false,
Control: false,
},
Ack: connection.Ack,
NumChunks: 0, // will be set in Packet.Pack()
Token: connection.ServerToken,
},
}
}
func (connection *Connection) CtrlToken() *Packet {
response := connection.BuildResponse()
response.Header.Flags.Control = true
response.Messages = append(
response.Messages,
&messages7.CtrlToken{
Token: connection.ClientToken,
},
)
return response
}
func (client *Connection) MsgStartInfo() *messages7.ClStartInfo {
return &messages7.ClStartInfo{
Name: "gopher",
Clan: "",
Country: 0,
Body: "greensward",
Marking: "duodonny",
Decoration: "",
Hands: "standard",
Feet: "standard",
Eyes: "standard",
CustomColorBody: false,
CustomColorMarking: false,
CustomColorDecoration: false,
CustomColorHands: false,
CustomColorFeet: false,
CustomColorEyes: false,
ColorBody: 0,
ColorMarking: 0,
ColorDecoration: 0,
ColorHands: 0,
ColorFeet: 0,
ColorEyes: 0,
}
}
func byteSliceToString(s []byte) string {
n := bytes.IndexByte(s, 0)
if n >= 0 {
s = s[:n]
}
return string(s)
}
func (connection *Connection) printUnknownMessage(msg messages7.NetMessage, msgType string) {
fmt.Printf("%s message id=%d\n", msgType, msg.MsgId())
if msg.Header() == nil {
fmt.Println(" header: nil")
} else {
fmt.Printf(" header: %x\n", msg.Header().Pack())
}
fmt.Printf(" payload: %x\n", msg.Pack())
if msg.Header() != nil {
fmt.Printf(" full msg: %x%x\n", msg.Header().Pack(), msg.Pack())
}
}
func (connection *Connection) OnSystemMsg(msg messages7.NetMessage, response *Packet) bool {
// TODO: is this shadow nasty?
switch msg := msg.(type) {
case *messages7.MapChange:
fmt.Println("got map change")
response.Messages = append(response.Messages, &messages7.Ready{})
case *messages7.MapData:
fmt.Printf("got map chunk %x\n", msg.Data)
case *messages7.ServerInfo:
fmt.Printf("connected to server with name '%s'\n", msg.Name)
case *messages7.ConReady:
fmt.Println("got ready")
response.Messages = append(response.Messages, connection.MsgStartInfo())
case *messages7.Snap:
// fmt.Printf("got snap tick=%d\n", msg.GameTick)
response.Messages = append(response.Messages, &messages7.CtrlKeepAlive{})
case *messages7.SnapSingle:
// fmt.Printf("got snap single tick=%d\n", msg.GameTick)
response.Messages = append(response.Messages, &messages7.CtrlKeepAlive{})
case *messages7.SnapEmpty:
// fmt.Printf("got snap empty tick=%d\n", msg.GameTick)
response.Messages = append(response.Messages, &messages7.CtrlKeepAlive{})
case *messages7.InputTiming:
// fmt.Printf("timing time left=%d\n", msg.TimeLeft)
case *messages7.RconAuthOn:
fmt.Println("you are now authenticated in rcon")
case *messages7.RconAuthOff:
fmt.Println("you are no longer authenticated in rcon")
case *messages7.RconLine:
fmt.Printf("[rcon] %s\n", msg.Line)
case *messages7.RconCmdAdd:
// fmt.Printf("got rcon cmd=%s %s %s\n", msg.Name, msg.Params, msg.Help)
case *messages7.RconCmdRem:
// fmt.Printf("removed cmd=%s\n", msg.Name)
case *messages7.Unknown:
// TODO: msg id of unknown messages should not be -1
fmt.Println("TODO: why is the msg id -1???")
connection.printUnknownMessage(msg, "unknown system")
default:
connection.printUnknownMessage(msg, "unhandled system")
return false
}
return true
}
func (client *Connection) OnChatMessage(msg *messages7.SvChat) {
if msg.ClientId < 0 || msg.ClientId > network7.MaxClients {
fmt.Printf("[chat] *** %s\n", msg.Message)
return
}
name := client.Players[msg.ClientId].Info.Name
fmt.Printf("[chat] <%s> %s\n", name, msg.Message)
}
func (connection *Connection) OnGameMsg(msg messages7.NetMessage, response *Packet) bool {
// TODO: is this shadow nasty?
switch msg := msg.(type) {
case *messages7.ReadyToEnter:
fmt.Println("got ready to enter")
response.Messages = append(response.Messages, &messages7.EnterGame{})
case *messages7.SvMotd:
if msg.Message != "" {
fmt.Printf("[motd] %s\n", msg.Message)
}
case *messages7.SvChat:
connection.OnChatMessage(msg)
case *messages7.SvClientInfo:
connection.Players[msg.ClientId].Info = *msg
fmt.Printf("got client info id=%d name=%s\n", msg.ClientId, msg.Name)
case *messages7.Unknown:
connection.printUnknownMessage(msg, "unknown game")
default:
connection.printUnknownMessage(msg, "unhandled game")
return false
}
return true
}
func (connection *Connection) OnMessage(msg messages7.NetMessage, response *Packet) bool {
if msg.Header() == nil {
// this is probably an unknown message
fmt.Printf("warning ignoring msgId=%d because header is nil\n", msg.MsgId())
return false
}
if msg.Header().Flags.Vital {
connection.Ack++
}
if msg.System() {
return connection.OnSystemMsg(msg, response)
}
return connection.OnGameMsg(msg, response)
}
// Takes a full teeworlds packet as argument
// And returns the response packet from the clients perspective
func (connection *Connection) OnPacket(packet *Packet) *Packet {
response := connection.BuildResponse()
if packet.Header.Flags.Control {
msg := packet.Messages[0]
fmt.Printf("got ctrl msg %d\n", msg.MsgId())
// TODO: is this shadow nasty?
switch msg := msg.(type) {
case *messages7.CtrlToken:
fmt.Printf("got server token %x\n", msg.Token)
connection.ServerToken = msg.Token
response.Header.Token = msg.Token
response.Messages = append(
response.Messages,
&messages7.CtrlConnect{
Token: connection.ClientToken,
},
)
case *messages7.CtrlAccept:
fmt.Println("got accept")
response.Messages = append(
response.Messages,
&messages7.Info{
Version: network7.NetVersion,
Password: "",
ClientVersion: network7.ClientVersion,
},
)
case *messages7.CtrlClose:
fmt.Printf("disconnected (%s)\n", msg.Reason)
os.Exit(0)
default:
fmt.Printf("unknown control message: %d\n", msg.MsgId())
}
return response
}
for _, msg := range packet.Messages {
connection.OnMessage(msg, response)
}
return response
}

View file

@ -39,7 +39,7 @@ type Packet struct {
Messages []messages7.NetMessage Messages []messages7.NetMessage
} }
func PackChunk(msg messages7.NetMessage, connection *Connection) []byte { func PackChunk(msg messages7.NetMessage, connection *Session) []byte {
if _, ok := msg.(*messages7.Unknown); ok { if _, ok := msg.(*messages7.Unknown); ok {
return msg.Pack() return msg.Pack()
} }
@ -305,7 +305,7 @@ func (packet *Packet) Unpack(data []byte) error {
return nil return nil
} }
func (packet *Packet) Pack(connection *Connection) []byte { func (packet *Packet) Pack(connection *Session) []byte {
payload := []byte{} payload := []byte{}
control := false control := false

View file

@ -134,7 +134,7 @@ func TestRepackUnknownMessages(t *testing.T) {
0x00, 0x00,
} }
conn := Connection{} conn := Session{}
packet := Packet{} packet := Packet{}
packet.Unpack(dump) packet.Unpack(dump)
@ -162,7 +162,7 @@ func TestPackUpdateChunkHeaders(t *testing.T) {
// When packing the chunk header will be set automatically // When packing the chunk header will be set automatically
// Based on the current context // Based on the current context
conn := &Connection{Sequence: 1} conn := &Session{Sequence: 1}
packet.Pack(conn) packet.Pack(conn)
{ {

72
protocol7/session.go Normal file
View file

@ -0,0 +1,72 @@
package protocol7
import "github.com/teeworlds-go/go-teeworlds-protocol/messages7"
// teeworlds connection state
type Session struct {
ClientToken [4]byte
ServerToken [4]byte
// The amount of vital chunks received
Ack int
// The amount of vital chunks sent
Sequence int
// The amount of vital chunks acknowledged by the peer
PeerAck int
}
func (connection *Session) BuildResponse() *Packet {
return &Packet{
Header: PacketHeader{
Flags: PacketFlags{
Connless: false,
Compression: false,
Resend: false,
Control: false,
},
Ack: connection.Ack,
NumChunks: 0, // will be set in Packet.Pack()
Token: connection.ServerToken,
},
}
}
func (connection *Session) CtrlToken() *Packet {
response := connection.BuildResponse()
response.Header.Flags.Control = true
response.Messages = append(
response.Messages,
&messages7.CtrlToken{
Token: connection.ClientToken,
},
)
return response
}
func (client *Session) MsgStartInfo() *messages7.ClStartInfo {
return &messages7.ClStartInfo{
Name: "gopher",
Clan: "",
Country: 0,
Body: "greensward",
Marking: "duodonny",
Decoration: "",
Hands: "standard",
Feet: "standard",
Eyes: "standard",
CustomColorBody: false,
CustomColorMarking: false,
CustomColorDecoration: false,
CustomColorHands: false,
CustomColorFeet: false,
CustomColorEyes: false,
ColorBody: 0,
ColorMarking: 0,
ColorDecoration: 0,
ColorHands: 0,
ColorFeet: 0,
ColorEyes: 0,
}
}

View file

@ -1,128 +0,0 @@
package main
import (
"bufio"
"fmt"
"net"
"os"
"strconv"
"time"
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
const (
maxPacksize = 1400
)
func getConnection(serverIp string, serverPort int) (net.Conn, error) {
conn, err := net.Dial("udp", fmt.Sprintf("%s:%d", serverIp, serverPort))
if err != nil {
fmt.Printf("Some error %v", err)
}
return conn, err
}
func readNetwork(ch chan<- []byte, conn net.Conn) {
buf := make([]byte, maxPacksize)
for {
len, err := bufio.NewReader(conn).Read(buf)
packet := make([]byte, len)
copy(packet[:], buf[:])
if err == nil {
ch <- packet
} else {
fmt.Printf("Some error %v\n", err)
break
}
}
conn.Close()
}
func main() {
ch := make(chan []byte, maxPacksize)
serverIp := "127.0.0.1"
serverPort := 8303
if len(os.Args) > 1 {
if os.Args[1][0] == '-' {
fmt.Println("usage: ./teeworlds [serverIp] [serverPort]")
os.Exit(1)
}
serverIp = os.Args[1]
}
if len(os.Args) > 2 {
var err error
serverPort, err = strconv.Atoi(os.Args[2])
if err != nil {
panic(err)
}
}
conn, err := getConnection(serverIp, serverPort)
if err != nil {
fmt.Printf("error connecting %v\n", err)
os.Exit(1)
}
client := &protocol7.Connection{
ClientToken: [4]byte{0x01, 0x02, 0x03, 0x04},
ServerToken: [4]byte{0xff, 0xff, 0xff, 0xff},
Ack: 0,
Players: make([]protocol7.Player, network7.MaxClients),
}
go readNetwork(ch, conn)
tokenPacket := client.CtrlToken()
conn.Write(tokenPacket.Pack(client))
for {
time.Sleep(10_000_000)
select {
case msg := <-ch:
packet := &protocol7.Packet{}
err := packet.Unpack(msg)
if err != nil {
panic(err)
}
response := client.OnPacket(packet)
if response != nil {
// example of inspecting incoming trafic
for _, msg := range packet.Messages {
switch msg := msg.(type) {
case *messages7.SvChat:
// inspect incoming traffic
fmt.Printf("got incoming chat msg: %s\n", msg.Message)
default:
}
}
// example of modifying outgoing traffic
for i, msg := range response.Messages {
switch msg := msg.(type) {
case *messages7.SvChat:
// inspect outgoing traffic
fmt.Printf("got outgoing chat msg: %s\n", msg.Message)
// change outgoing traffic
msg.Message += " (edited by go)"
packet.Messages[i] = msg
default:
}
}
if len(response.Messages) > 0 || response.Header.Flags.Resend {
conn.Write(response.Pack(client))
}
}
default:
// do nothing
}
}
}

99
teeworlds7/callbacks.go Normal file
View file

@ -0,0 +1,99 @@
package teeworlds7
import (
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
// Processes the incoming packet
// It might print to the console
// It might send a response packet
type DefaultAction func()
// TODO: this should be a map but the type checker broke me
//
// // key is the network7.MessageId
// UserMsgCallbacks map[int]UserMsgCallback
type UserMsgCallbacks struct {
PacketIn func(*protocol7.Packet)
PacketOut func(*protocol7.Packet)
MsgUnknown func(*messages7.Unknown, DefaultAction)
InternalError func(error)
CtrlKeepAlive func(*messages7.CtrlKeepAlive, DefaultAction)
CtrlConnect func(*messages7.CtrlConnect, DefaultAction)
CtrlAccept func(*messages7.CtrlAccept, DefaultAction)
CtrlToken func(*messages7.CtrlToken, DefaultAction)
CtrlClose func(*messages7.CtrlClose, DefaultAction)
SysInfo func(*messages7.Info, DefaultAction)
SysMapChange func(*messages7.MapChange, DefaultAction)
SysMapData func(*messages7.MapData, DefaultAction)
SysServerInfo func(*messages7.ServerInfo, DefaultAction)
SysConReady func(*messages7.ConReady, DefaultAction)
SysSnap func(*messages7.Snap, DefaultAction)
SysSnapEmpty func(*messages7.SnapEmpty, DefaultAction)
SysSnapSingle func(*messages7.SnapSingle, DefaultAction)
SysSnapSmall func(*messages7.SnapSmall, DefaultAction)
SysInputTiming func(*messages7.InputTiming, DefaultAction)
SysRconAuthOn func(*messages7.RconAuthOn, DefaultAction)
SysRconAuthOff func(*messages7.RconAuthOff, DefaultAction)
SysRconLine func(*messages7.RconLine, DefaultAction)
SysRconCmdAdd func(*messages7.RconCmdAdd, DefaultAction)
SysRconCmdRem func(*messages7.RconCmdRem, DefaultAction)
SysAuthChallenge func(*messages7.AuthChallenge, DefaultAction)
SysAuthResult func(*messages7.AuthResult, DefaultAction)
SysReady func(*messages7.Ready, DefaultAction)
SysEnterGame func(*messages7.EnterGame, DefaultAction)
SysInput func(*messages7.Input, DefaultAction)
SysRconCmd func(*messages7.RconCmd, DefaultAction)
SysRconAuth func(*messages7.RconAuth, DefaultAction)
SysRequestMapData func(*messages7.RequestMapData, DefaultAction)
SysAuthStart func(*messages7.AuthStart, DefaultAction)
SysAuthResponse func(*messages7.AuthResponse, DefaultAction)
SysPing func(*messages7.Ping, DefaultAction)
SysPingReply func(*messages7.PingReply, DefaultAction)
SysError func(*messages7.Error, DefaultAction)
SysMaplistEntryAdd func(*messages7.MaplistEntryAdd, DefaultAction)
SysMaplistEntryRem func(*messages7.MaplistEntryRem, DefaultAction)
GameSvMotd func(*messages7.SvMotd, DefaultAction)
GameSvBroadcast func(*messages7.SvBroadcast, DefaultAction)
GameSvChat func(*messages7.SvChat, DefaultAction)
GameSvTeam func(*messages7.SvTeam, DefaultAction)
// GameSvKillMsg func(*messages7.SvKillMsg, DefaultAction)
// GameSvTuneParams func(*messages7.SvTuneParams, DefaultAction)
// GameSvExtraProjectile func(*messages7.SvExtraProjectile, DefaultAction)
GameReadyToEnter func(*messages7.ReadyToEnter, DefaultAction)
// GameWeaponPickup func(*messages7.WeaponPickup, DefaultAction)
// GameEmoticon func(*messages7.Emoticon, DefaultAction)
// GameSvVoteClearoptions func(*messages7.SvVoteClearoptions, DefaultAction)
// GameSvVoteOptionlistadd func(*messages7.SvVoteOptionlistadd, DefaultAction)
// GameSvVotePptionadd func(*messages7.SvVotePptionadd, DefaultAction)
// GameSvVoteOptionremove func(*messages7.SvVoteOptionremove, DefaultAction)
// GameSvVoteSet func(*messages7.SvVoteSet, DefaultAction)
// GameSvVoteStatus func(*messages7.SvVoteStatus, DefaultAction)
// GameSvServerSettings func(*messages7.SvServerSettings, DefaultAction)
GameSvClientInfo func(*messages7.SvClientInfo, DefaultAction)
// GameSvGameInfo func(*messages7.SvGameInfo, DefaultAction)
// GameSvClientDrop func(*messages7.SvClientDrop, DefaultAction)
// GameSvGameMsg func(*messages7.SvGameMsg, DefaultAction)
// GameDeClientEnter func(*messages7.DeClientEnter, DefaultAction)
// GameDeClientLeave func(*messages7.DeClientLeave, DefaultAction)
// GameClSay func(*messages7.ClSay, DefaultAction)
// GameClSetTeam func(*messages7.ClSetTeam, DefaultAction)
// GameClSetSpectatorMode func(*messages7.ClSetSpectatorMode, DefaultAction)
GameClStartInfo func(*messages7.ClStartInfo, DefaultAction)
// GameClKill func(*messages7.ClKill, DefaultAction)
// GameClReadyChange func(*messages7.ClReadyChange, DefaultAction)
// GameClEmoticon func(*messages7.ClEmoticon, DefaultAction)
// GameClVote func(*messages7.ClVote, DefaultAction)
// GameClCallVote func(*messages7.ClCallVote, DefaultAction)
// GameSvSkinChange func(*messages7.SvSkinChange, DefaultAction)
// GameClSkinChange func(*messages7.ClSkinChange, DefaultAction)
// GameSvRaceFinish func(*messages7.SvRaceFinish, DefaultAction)
// GameSvCheckpoint func(*messages7.SvCheckpoint, DefaultAction)
// GameSvCommandInfo func(*messages7.SvCommandInfo, DefaultAction)
// GameSvCommandInfoRemove func(*messages7.SvCommandInfoRemove, DefaultAction)
// GameClCommand func(*messages7.ClCommand, DefaultAction)
}

47
teeworlds7/client.go Normal file
View file

@ -0,0 +1,47 @@
package teeworlds7
import (
"log"
"net"
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
type Player struct {
Info messages7.SvClientInfo
}
type Game struct {
Players []Player
}
type Client struct {
Name string
Clan string
Country int
// chunks to be sent on next packet send
// use client.SendMessage() to put your chunks here
QueuedMessages []messages7.NetMessage
// hooks from the user
Callbacks UserMsgCallbacks
// udp connection
Conn net.Conn
// teeworlds session
Session protocol7.Session
// teeworlds game state
Game Game
}
func (client *Client) throwError(err error) {
if client.Callbacks.InternalError != nil {
client.Callbacks.InternalError(err)
return
}
log.Fatal(err)
}

83
teeworlds7/game.go Normal file
View file

@ -0,0 +1,83 @@
package teeworlds7
import (
"fmt"
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
func (client *Client) processGame(netMsg messages7.NetMessage, response *protocol7.Packet) bool {
switch msg := netMsg.(type) {
case *messages7.SvMotd:
defaultAction := func() {
if msg.Message != "" {
fmt.Printf("[motd] %s\n", msg.Message)
}
}
if client.Callbacks.GameSvMotd == nil {
defaultAction()
} else {
client.Callbacks.GameSvMotd(msg, defaultAction)
}
case *messages7.SvBroadcast:
defaultAction := func() {
fmt.Printf("[broadcast] %s\n", msg.Message)
}
if client.Callbacks.GameSvBroadcast == nil {
defaultAction()
} else {
client.Callbacks.GameSvBroadcast(msg, defaultAction)
}
case *messages7.SvChat:
defaultAction := func() {
if msg.ClientId < 0 || msg.ClientId > network7.MaxClients {
fmt.Printf("[chat] *** %s\n", msg.Message)
return
}
name := client.Game.Players[msg.ClientId].Info.Name
fmt.Printf("[chat] <%s> %s\n", name, msg.Message)
}
if client.Callbacks.GameSvChat == nil {
defaultAction()
} else {
client.Callbacks.GameSvChat(msg, defaultAction)
}
case *messages7.SvClientInfo:
defaultAction := func() {
client.Game.Players[msg.ClientId].Info = *msg
fmt.Printf("got client info id=%d name=%s\n", msg.ClientId, msg.Name)
}
if client.Callbacks.GameSvClientInfo == nil {
defaultAction()
} else {
client.Callbacks.GameSvClientInfo(msg, defaultAction)
}
case *messages7.ReadyToEnter:
defaultAction := func() {
fmt.Println("got ready to enter")
response.Messages = append(response.Messages, &messages7.EnterGame{})
}
if client.Callbacks.GameReadyToEnter == nil {
defaultAction()
} else {
client.Callbacks.GameReadyToEnter(msg, defaultAction)
}
case *messages7.Unknown:
defaultAction := func() {
// TODO: msg id of unknown messages should not be -1
fmt.Println("TODO: why is the msg id -1???")
printUnknownMessage(msg, "unknown game")
}
if client.Callbacks.MsgUnknown == nil {
defaultAction()
} else {
client.Callbacks.MsgUnknown(msg, defaultAction)
}
default:
printUnknownMessage(netMsg, "unprocessed game")
return false
}
return true
}

84
teeworlds7/networking.go Normal file
View file

@ -0,0 +1,84 @@
package teeworlds7
import (
"bufio"
"fmt"
"net"
"time"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
const (
maxPacksize = 1400
)
func getConnection(serverIp string, serverPort int) (net.Conn, error) {
conn, err := net.Dial("udp", fmt.Sprintf("%s:%d", serverIp, serverPort))
if err != nil {
fmt.Printf("Some error %v", err)
}
return conn, err
}
func readNetwork(ch chan<- []byte, conn net.Conn) {
buf := make([]byte, maxPacksize)
for {
len, err := bufio.NewReader(conn).Read(buf)
packet := make([]byte, len)
copy(packet[:], buf[:])
if err == nil {
ch <- packet
} else {
fmt.Printf("Some error %v\n", err)
break
}
}
conn.Close()
}
func (client *Client) Connect(serverIp string, serverPort int) {
ch := make(chan []byte, maxPacksize)
conn, err := getConnection(serverIp, serverPort)
if err != nil {
fmt.Printf("error connecting %v\n", err)
return
}
client.Session = protocol7.Session{
ClientToken: [4]byte{0x01, 0x02, 0x03, 0x04},
ServerToken: [4]byte{0xff, 0xff, 0xff, 0xff},
Ack: 0,
}
client.Game.Players = make([]Player, network7.MaxClients)
client.Conn = conn
go readNetwork(ch, conn)
client.SendPacket(client.Session.CtrlToken())
// TODO: do we really need a non blocking network read?
// if not remove the channel, the sleep and the select statement
// if yes also offer an OnTick callback to the user, and also do keepalives and resends
for {
time.Sleep(10_000_000)
select {
case msg := <-ch:
packet := &protocol7.Packet{}
err := packet.Unpack(msg)
if err != nil {
client.throwError(err)
}
err = client.processPacket(packet)
if err != nil {
client.throwError(err)
}
default:
// do nothing
}
}
}

143
teeworlds7/packet.go Normal file
View file

@ -0,0 +1,143 @@
package teeworlds7
import (
"fmt"
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
func printUnknownMessage(msg messages7.NetMessage, msgType string) {
fmt.Printf("%s message id=%d\n", msgType, msg.MsgId())
if msg.Header() == nil {
fmt.Println(" header: nil")
} else {
fmt.Printf(" header: %x\n", msg.Header().Pack())
}
fmt.Printf(" payload: %x\n", msg.Pack())
if msg.Header() != nil {
fmt.Printf(" full msg: %x%x\n", msg.Header().Pack(), msg.Pack())
}
}
func (client *Client) processMessage(msg messages7.NetMessage, response *protocol7.Packet) bool {
if msg.Header() == nil {
// this is probably an unknown message
fmt.Printf("warning ignoring msgId=%d because header is nil\n", msg.MsgId())
return false
}
if msg.Header().Flags.Vital {
client.Session.Ack++
}
if msg.System() {
return client.processSystem(msg, response)
}
return client.processGame(msg, response)
}
func (client *Client) processPacket(packet *protocol7.Packet) error {
if client.Callbacks.PacketIn != nil {
client.Callbacks.PacketIn(packet)
}
response := client.Session.BuildResponse()
if packet.Header.Flags.Control {
if len(packet.Messages) != 1 {
return fmt.Errorf("got control packet with %d messages.\n", len(packet.Messages))
}
msg := packet.Messages[0]
// TODO: is this shadow nasty?
switch msg := msg.(type) {
case *messages7.CtrlKeepAlive:
defaultAction := func() {
fmt.Println("got keep alive")
}
if client.Callbacks.CtrlKeepAlive == nil {
defaultAction()
} else {
client.Callbacks.CtrlKeepAlive(msg, defaultAction)
}
case *messages7.CtrlConnect:
defaultAction := func() {
fmt.Println("we got connect as a client. this should never happen lol.")
fmt.Println("who is tryint to connect to us? We are not a server!")
}
if client.Callbacks.CtrlConnect == nil {
defaultAction()
} else {
client.Callbacks.CtrlConnect(msg, defaultAction)
}
case *messages7.CtrlAccept:
defaultAction := func() {
fmt.Println("got accept")
response.Messages = append(
response.Messages,
&messages7.Info{
Version: network7.NetVersion,
Password: "",
ClientVersion: network7.ClientVersion,
},
)
client.SendPacket(response)
}
if client.Callbacks.CtrlAccept == nil {
defaultAction()
} else {
client.Callbacks.CtrlAccept(msg, defaultAction)
}
case *messages7.CtrlClose:
defaultAction := func() {
fmt.Printf("disconnected (%s)\n", msg.Reason)
}
if client.Callbacks.CtrlClose == nil {
defaultAction()
} else {
client.Callbacks.CtrlClose(msg, defaultAction)
}
case *messages7.CtrlToken:
defaultAction := func() {
fmt.Printf("got server token %x\n", msg.Token)
client.Session.ServerToken = msg.Token
response.Header.Token = msg.Token
response.Messages = append(
response.Messages,
&messages7.CtrlConnect{
Token: client.Session.ClientToken,
},
)
client.SendPacket(response)
}
if client.Callbacks.CtrlToken == nil {
defaultAction()
} else {
client.Callbacks.CtrlToken(msg, defaultAction)
}
case *messages7.Unknown:
defaultAction := func() {
printUnknownMessage(msg, "unknown control")
}
if client.Callbacks.MsgUnknown == nil {
defaultAction()
} else {
client.Callbacks.MsgUnknown(msg, defaultAction)
}
return fmt.Errorf("unknown control message: %d\n", msg.MsgId())
default:
return fmt.Errorf("unprocessed control message: %d\n", msg.MsgId())
}
return nil
}
for _, msg := range packet.Messages {
client.processMessage(msg, response)
}
if len(response.Messages) > 0 || response.Header.Flags.Resend {
client.SendPacket(response)
}
return nil
}

152
teeworlds7/system.go Normal file
View file

@ -0,0 +1,152 @@
package teeworlds7
import (
"fmt"
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
func (client *Client) processSystem(netMsg messages7.NetMessage, response *protocol7.Packet) bool {
switch msg := netMsg.(type) {
case *messages7.MapChange:
defaultAction := func() {
fmt.Println("got map change")
response.Messages = append(response.Messages, &messages7.Ready{})
}
if client.Callbacks.SysMapChange == nil {
defaultAction()
} else {
client.Callbacks.SysMapChange(msg, defaultAction)
}
case *messages7.MapData:
defaultAction := func() {
fmt.Printf("got map chunk %x\n", msg.Data)
}
if client.Callbacks.SysMapData == nil {
defaultAction()
} else {
client.Callbacks.SysMapData(msg, defaultAction)
}
case *messages7.ServerInfo:
defaultAction := func() {
fmt.Printf("connected to server with name '%s'\n", msg.Name)
}
if client.Callbacks.SysServerInfo == nil {
defaultAction()
} else {
client.Callbacks.SysServerInfo(msg, defaultAction)
}
case *messages7.ConReady:
defaultAction := func() {
fmt.Println("connected, sending info")
info := &messages7.ClStartInfo{
Name: client.Name,
Clan: client.Clan,
Country: client.Country,
Body: "greensward",
Marking: "duodonny",
Decoration: "",
Hands: "standard",
Feet: "standard",
Eyes: "standard",
CustomColorBody: false,
CustomColorMarking: false,
CustomColorDecoration: false,
CustomColorHands: false,
CustomColorFeet: false,
CustomColorEyes: false,
ColorBody: 0,
ColorMarking: 0,
ColorDecoration: 0,
ColorHands: 0,
ColorFeet: 0,
ColorEyes: 0,
}
response.Messages = append(response.Messages, info)
}
if client.Callbacks.SysConReady == nil {
defaultAction()
} else {
client.Callbacks.SysConReady(msg, defaultAction)
}
case *messages7.Snap:
// fmt.Printf("got snap tick=%d\n", msg.GameTick)
response.Messages = append(response.Messages, &messages7.CtrlKeepAlive{})
case *messages7.SnapSingle:
// fmt.Printf("got snap single tick=%d\n", msg.GameTick)
response.Messages = append(response.Messages, &messages7.CtrlKeepAlive{})
case *messages7.SnapEmpty:
// fmt.Printf("got snap empty tick=%d\n", msg.GameTick)
response.Messages = append(response.Messages, &messages7.CtrlKeepAlive{})
case *messages7.InputTiming:
defaultAction := func() {
fmt.Printf("timing time left=%d\n", msg.TimeLeft)
}
if client.Callbacks.SysInputTiming == nil {
defaultAction()
} else {
client.Callbacks.SysInputTiming(msg, defaultAction)
}
case *messages7.RconAuthOn:
defaultAction := func() {
fmt.Println("you are now authenticated in rcon")
}
if client.Callbacks.SysRconAuthOn == nil {
defaultAction()
} else {
client.Callbacks.SysRconAuthOn(msg, defaultAction)
}
case *messages7.RconAuthOff:
defaultAction := func() {
fmt.Println("you are no longer authenticated in rcon")
}
if client.Callbacks.SysRconAuthOff == nil {
defaultAction()
} else {
client.Callbacks.SysRconAuthOff(msg, defaultAction)
}
case *messages7.RconLine:
defaultAction := func() {
fmt.Printf("[rcon] %s\n", msg.Line)
}
if client.Callbacks.SysRconLine == nil {
defaultAction()
} else {
client.Callbacks.SysRconLine(msg, defaultAction)
}
case *messages7.RconCmdAdd:
defaultAction := func() {
fmt.Printf("got rcon cmd=%s %s %s\n", msg.Name, msg.Params, msg.Help)
}
if client.Callbacks.SysRconCmdAdd == nil {
defaultAction()
} else {
client.Callbacks.SysRconCmdAdd(msg, defaultAction)
}
case *messages7.RconCmdRem:
defaultAction := func() {
fmt.Printf("removed cmd=%s\n", msg.Name)
}
if client.Callbacks.SysRconCmdRem == nil {
defaultAction()
} else {
client.Callbacks.SysRconCmdRem(msg, defaultAction)
}
case *messages7.Unknown:
defaultAction := func() {
// TODO: msg id of unknown messages should not be -1
fmt.Println("TODO: why is the msg id -1???")
printUnknownMessage(msg, "unknown system")
}
if client.Callbacks.MsgUnknown == nil {
defaultAction()
} else {
client.Callbacks.MsgUnknown(msg, defaultAction)
}
default:
printUnknownMessage(netMsg, "unprocessed system")
return false
}
return true
}

View file

@ -0,0 +1,70 @@
package teeworlds7
import (
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
// ----------------------------
// low level access for experts
// ----------------------------
func (client *Client) SendPacket(packet *protocol7.Packet) {
// TODO: append queued messages to packet messages here
if client.Callbacks.PacketOut != nil {
client.Callbacks.PacketOut(packet)
}
client.Conn.Write(packet.Pack(&client.Session))
}
// WARNING! this is does not send chat messages
// this sends a network chunk and is for expert users
//
// if you want to send a chat message use SendChat()
func (client *Client) SendMessage(msg messages7.NetMessage) {
// TODO: set vital header and stuff
client.QueuedMessages = append(client.QueuedMessages, msg)
}
// ----------------------------
// high level actions
// ----------------------------
// see also SendWhisper()
// see also SendChatTeam()
func (client *Client) SendChat(msg string) {
client.SendMessage(
&messages7.SvChat{
Mode: network7.ChatAll,
Message: msg,
TargetId: -1,
},
)
}
// see also SendWhisper()
// see also SendChat()
func (client *Client) SendChatTeam(msg string) {
client.SendMessage(
&messages7.SvChat{
Mode: network7.ChatTeam,
Message: msg,
TargetId: -1,
},
)
}
// see also SendChat()
// see also SendChatTeam()
func (client *Client) SendWhisper(targetId int, msg string) {
client.SendMessage(
&messages7.SvChat{
Mode: network7.ChatWhisper,
Message: msg,
TargetId: targetId,
},
)
}

84
teeworlds7/user_hooks.go Normal file
View file

@ -0,0 +1,84 @@
package teeworlds7
import (
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
// --------------------------------
// special cases
// --------------------------------
// if not implemented by the user the application might throw and exit
func (client *Client) OnError(callback func(err error)) {
client.Callbacks.InternalError = callback
}
// inspect outgoing traffic
// and alter it before it gets sent to the server
func (client *Client) OnSend(callback func(packet *protocol7.Packet)) {
client.Callbacks.PacketOut = callback
}
// read incoming traffic
// and alter it before it hits the internal state machine
func (client *Client) OnPacket(callback func(packet *protocol7.Packet)) {
client.Callbacks.PacketIn = callback
}
func (client *Client) OnUnknown(callback func(msg *messages7.Unknown, defaultAction DefaultAction)) {
client.Callbacks.MsgUnknown = callback
}
// --------------------------------
// control messages
// --------------------------------
func (client *Client) OnKeepAlive(callback func(msg *messages7.CtrlKeepAlive, defaultAction DefaultAction)) {
client.Callbacks.CtrlKeepAlive = callback
}
// This is just misleading. It should never be called. This message is only received by the server.
// func (client *Client) OnCtrlConnect(callback func(msg *messages7.CtrlConnect, defaultAction DefaultAction)) {
// client.Callbacks.CtrlConnect = callback
// }
func (client *Client) OnAccept(callback func(msg *messages7.CtrlAccept, defaultAction DefaultAction)) {
client.Callbacks.CtrlAccept = callback
}
func (client *Client) OnDisconnect(callback func(msg *messages7.CtrlClose, defaultAction DefaultAction)) {
client.Callbacks.CtrlClose = callback
}
func (client *Client) OnToken(callback func(msg *messages7.CtrlToken, defaultAction DefaultAction)) {
client.Callbacks.CtrlToken = callback
}
// --------------------------------
// game messages
// --------------------------------
func (client *Client) OnMotd(callback func(msg *messages7.SvMotd, defaultAction DefaultAction)) {
client.Callbacks.GameSvMotd = callback
}
func (client *Client) OnBroadcast(callback func(msg *messages7.SvBroadcast, defaultAction DefaultAction)) {
client.Callbacks.GameSvBroadcast = callback
}
func (client *Client) OnChat(callback func(msg *messages7.SvChat, defaultAction DefaultAction)) {
client.Callbacks.GameSvChat = callback
}
func (client *Client) OnTeam(callback func(msg *messages7.SvTeam, defaultAction DefaultAction)) {
client.Callbacks.GameSvTeam = callback
}
// --------------------------------
// system messages
// --------------------------------
func (client *Client) OnMapChange(callback func(msg *messages7.MapChange, defaultAction DefaultAction)) {
client.Callbacks.SysMapChange = callback
}