diff --git a/README.md b/README.md index 537ee80..91842d4 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,55 @@ # 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. -## 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 diff --git a/examples/client_verbose/.gitignore b/examples/client_verbose/.gitignore new file mode 100644 index 0000000..52d84d6 --- /dev/null +++ b/examples/client_verbose/.gitignore @@ -0,0 +1,2 @@ +client_verbose +client_verbose.exe diff --git a/examples/client_verbose/README.md b/examples/client_verbose/README.md new file mode 100644 index 0000000..7314815 --- /dev/null +++ b/examples/client_verbose/README.md @@ -0,0 +1,7 @@ +# client_verbose + +``` +go build client_verbose.go +./client_verbose +``` + diff --git a/examples/client_verbose/client_verbose.go b/examples/client_verbose/client_verbose.go new file mode 100644 index 0000000..1376319 --- /dev/null +++ b/examples/client_verbose/client_verbose.go @@ -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) + } +} diff --git a/messages7/sv_broadcast.go b/messages7/sv_broadcast.go new file mode 100644 index 0000000..b8e828d --- /dev/null +++ b/messages7/sv_broadcast.go @@ -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 +} diff --git a/messages7/sv_chat.go b/messages7/sv_chat.go index 94a9f66..50430dc 100644 --- a/messages7/sv_chat.go +++ b/messages7/sv_chat.go @@ -11,7 +11,7 @@ import ( type SvChat struct { ChunkHeader *chunk7.ChunkHeader - Mode int + Mode network7.ChatMode ClientId int TargetId int Message string @@ -35,7 +35,7 @@ func (msg SvChat) Vital() bool { func (msg SvChat) Pack() []byte { return slices.Concat( - packer.PackInt(msg.Mode), + packer.PackInt(int(msg.Mode)), packer.PackInt(msg.ClientId), packer.PackInt(msg.TargetId), packer.PackStr(msg.Message), @@ -43,7 +43,7 @@ func (msg SvChat) Pack() []byte { } func (msg *SvChat) Unpack(u *packer.Unpacker) { - msg.Mode = u.GetInt() + msg.Mode = network7.ChatMode(u.GetInt()) msg.ClientId = u.GetInt() msg.TargetId = u.GetInt() msg.Message = u.GetString() diff --git a/messages7/sv_team.go b/messages7/sv_team.go new file mode 100644 index 0000000..1e1fbc0 --- /dev/null +++ b/messages7/sv_team.go @@ -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 +} diff --git a/network7/network7.go b/network7/network7.go index a85d1d0..e2ca2ad 100644 --- a/network7/network7.go +++ b/network7/network7.go @@ -5,6 +5,14 @@ const ( NetVersion = "0.7 802f1be60a05665f" ClientVersion = 0x0705 + ChatAll ChatMode = 1 + ChatTeam ChatMode = 2 + ChatWhisper ChatMode = 3 + + TeamSpectators GameTeam = -1 + TeamRed GameTeam = 0 + TeamBlue GameTeam = 1 + MsgCtrlKeepAlive = 0x00 MsgCtrlConnect = 0x01 MsgCtrlAccept = 0x02 @@ -42,11 +50,45 @@ const ( MsgSysMaplistEntryAdd = 29 MsgSysMaplistEntryRem = 30 - MsgGameSvMotd = 1 - MsgGameSvChat = 3 - MsgGameReadyToEnter = 8 - MsgGameSvClientInfo = 18 - MsgGameClStartInfo = 27 + MsgGameSvMotd = 1 + MsgGameSvBroadcast = 2 + MsgGameSvChat = 3 + MsgGameSvTeam = 4 + MsgGameSvKillMsg = 5 + MsgGameSvTuneParams = 6 + MsgGameSvExtraProjectile = 7 + MsgGameReadyToEnter = 8 + MsgGameWeaponPickup = 9 + MsgGameEmoticon = 10 + MsgGameSvVoteClearoptions = 11 + MsgGameSvVoteOptionlistadd = 12 + MsgGameSvVotePptionadd = 13 + MsgGameSvVoteOptionremove = 14 + MsgGameSvVoteSet = 15 + MsgGameSvVoteStatus = 16 + MsgGameSvServerSettings = 17 + MsgGameSvClientInfo = 18 + MsgGameSvGameInfo = 19 + MsgGameSvClientDrop = 20 + MsgGameSvGameMsg = 21 + MsgGameDeClientEnter = 22 + MsgGameDeClientLeave = 23 + MsgGameClSay = 24 + MsgGameClSetTeam = 25 + MsgGameClSetSpectatorMode = 26 + 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 TypeNet MsgType = 2 @@ -61,5 +103,7 @@ const ( NumWeapons Weapon = 6 ) +type ChatMode int +type GameTeam int type MsgType int type Weapon int diff --git a/protocol7/connection.go b/protocol7/connection.go deleted file mode 100644 index f90c5b6..0000000 --- a/protocol7/connection.go +++ /dev/null @@ -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 -} diff --git a/protocol7/packet.go b/protocol7/packet.go index 733eabe..749de77 100644 --- a/protocol7/packet.go +++ b/protocol7/packet.go @@ -39,7 +39,7 @@ type Packet struct { 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 { return msg.Pack() } @@ -305,7 +305,7 @@ func (packet *Packet) Unpack(data []byte) error { return nil } -func (packet *Packet) Pack(connection *Connection) []byte { +func (packet *Packet) Pack(connection *Session) []byte { payload := []byte{} control := false diff --git a/protocol7/packet_test.go b/protocol7/packet_test.go index 7857a33..7a99df8 100644 --- a/protocol7/packet_test.go +++ b/protocol7/packet_test.go @@ -134,7 +134,7 @@ func TestRepackUnknownMessages(t *testing.T) { 0x00, } - conn := Connection{} + conn := Session{} packet := Packet{} packet.Unpack(dump) @@ -162,7 +162,7 @@ func TestPackUpdateChunkHeaders(t *testing.T) { // When packing the chunk header will be set automatically // Based on the current context - conn := &Connection{Sequence: 1} + conn := &Session{Sequence: 1} packet.Pack(conn) { diff --git a/protocol7/session.go b/protocol7/session.go new file mode 100644 index 0000000..38a7b01 --- /dev/null +++ b/protocol7/session.go @@ -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, + } +} diff --git a/teeworlds.go b/teeworlds.go deleted file mode 100644 index 149797d..0000000 --- a/teeworlds.go +++ /dev/null @@ -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 - } - } -} diff --git a/teeworlds7/callbacks.go b/teeworlds7/callbacks.go new file mode 100644 index 0000000..cb971ae --- /dev/null +++ b/teeworlds7/callbacks.go @@ -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) +} diff --git a/teeworlds7/client.go b/teeworlds7/client.go new file mode 100644 index 0000000..80a8ec6 --- /dev/null +++ b/teeworlds7/client.go @@ -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) +} diff --git a/teeworlds7/game.go b/teeworlds7/game.go new file mode 100644 index 0000000..cb01979 --- /dev/null +++ b/teeworlds7/game.go @@ -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 +} diff --git a/teeworlds7/networking.go b/teeworlds7/networking.go new file mode 100644 index 0000000..96f763c --- /dev/null +++ b/teeworlds7/networking.go @@ -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 + } + } +} diff --git a/teeworlds7/packet.go b/teeworlds7/packet.go new file mode 100644 index 0000000..9c3e76d --- /dev/null +++ b/teeworlds7/packet.go @@ -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 +} diff --git a/teeworlds7/system.go b/teeworlds7/system.go new file mode 100644 index 0000000..277a6fa --- /dev/null +++ b/teeworlds7/system.go @@ -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 +} diff --git a/teeworlds7/user_actions.go b/teeworlds7/user_actions.go new file mode 100644 index 0000000..46effc4 --- /dev/null +++ b/teeworlds7/user_actions.go @@ -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, + }, + ) +} diff --git a/teeworlds7/user_hooks.go b/teeworlds7/user_hooks.go new file mode 100644 index 0000000..8176eb6 --- /dev/null +++ b/teeworlds7/user_hooks.go @@ -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 +}