diff --git a/chunk/chunk.go b/chunk/chunk.go index 83b7cb8..c7230d2 100644 --- a/chunk/chunk.go +++ b/chunk/chunk.go @@ -10,6 +10,17 @@ type ChunkFlags struct { Resend bool } +func (flags *ChunkFlags) ToInt() int { + v := 0 + if flags.Resend { + v |= chunkFlagResend + } + if flags.Vital { + v |= chunkFlagVital + } + return v +} + type ChunkHeader struct { Flags ChunkFlags Size int @@ -23,6 +34,21 @@ type Chunk struct { Data []byte } +func (header *ChunkHeader) Pack() []byte { + len := 2 + if header.Flags.Vital { + len = 3 + } + data := make([]byte, len) + data[0] = (byte(header.Flags.ToInt()&0x03) << 6) | ((byte(header.Size) >> 6) & 0x3f) + data[1] = (byte(header.Size) & 0x3f) + if header.Flags.Vital { + data[1] |= (byte(header.Seq) >> 2) & 0xc0 + data[2] = byte(header.Seq) & 0xff + } + return data +} + func (header *ChunkHeader) Unpack(data []byte) { flagBits := (data[0] >> 6) & 0x03 header.Flags.Vital = (flagBits & chunkFlagVital) != 0 diff --git a/messages/cl_start_info.go b/messages/cl_start_info.go new file mode 100644 index 0000000..f4f191e --- /dev/null +++ b/messages/cl_start_info.go @@ -0,0 +1,81 @@ +package message + +import ( + "slices" + + "github.com/teeworlds-go/teeworlds/packer" +) + +type ClStartInfo struct { + Name string + Clan string + Country int + Body string + Marking string + Decoration string + Hands string + Feet string + Eyes string + CustomColorBody bool + CustomColorMarking bool + CustomColorDecoration bool + CustomColorHands bool + CustomColorFeet bool + CustomColorEyes bool + ColorBody int + ColorMarking int + ColorDecoration int + ColorHands int + ColorFeet int + ColorEyes int +} + +func (info *ClStartInfo) Pack() []byte { + return slices.Concat( + packer.PackStr(info.Name), + packer.PackStr(info.Clan), + packer.PackInt(info.Country), + packer.PackStr(info.Body), + packer.PackStr(info.Marking), + packer.PackStr(info.Decoration), + packer.PackStr(info.Hands), + packer.PackStr(info.Feet), + packer.PackStr(info.Eyes), + packer.PackBool(info.CustomColorBody), + packer.PackBool(info.CustomColorMarking), + packer.PackBool(info.CustomColorDecoration), + packer.PackBool(info.CustomColorHands), + packer.PackBool(info.CustomColorFeet), + packer.PackBool(info.CustomColorEyes), + packer.PackInt(info.ColorBody), + packer.PackInt(info.ColorMarking), + packer.PackInt(info.ColorDecoration), + packer.PackInt(info.ColorHands), + packer.PackInt(info.ColorFeet), + packer.PackInt(info.ColorEyes), + ) +} + +func (info *ClStartInfo) Unpack(u *packer.Unpacker) { + info.Name = u.GetString() + info.Clan = u.GetString() + info.Country = u.GetInt() + info.Body = u.GetString() + info.Marking = u.GetString() + info.Decoration = u.GetString() + info.Hands = u.GetString() + info.Feet = u.GetString() + info.Eyes = u.GetString() + info.CustomColorBody = u.GetInt() != 0 + info.CustomColorMarking = u.GetInt() != 0 + info.CustomColorDecoration = u.GetInt() != 0 + info.CustomColorHands = u.GetInt() != 0 + info.CustomColorFeet = u.GetInt() != 0 + info.CustomColorEyes = u.GetInt() != 0 + info.ColorBody = u.GetInt() + info.ColorMarking = u.GetInt() + info.ColorDecoration = u.GetInt() + info.ColorHands = u.GetInt() + info.ColorFeet = u.GetInt() + info.ColorEyes = u.GetInt() +} diff --git a/messages/cl_start_info_test.go b/messages/cl_start_info_test.go new file mode 100644 index 0000000..ab89faa --- /dev/null +++ b/messages/cl_start_info_test.go @@ -0,0 +1,124 @@ +package message + +import ( + "reflect" + "testing" + + "github.com/teeworlds-go/teeworlds/packer" +) + +func TestPackStartInfo(t *testing.T) { + want := []byte{ + 0x67, 0x6f, 0x70, 0x68, 0x65, 0x72, 0x00, + 0x00, 0x40, 0x67, 0x72, 0x65, 0x65, 0x6e, 0x73, 0x77, 0x61, 0x72, + 0x64, 0x00, 0x64, 0x75, 0x6f, 0x64, 0x6f, 0x6e, 0x6e, 0x79, 0x00, + 0x00, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x00, 0x73, + 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x00, 0x73, 0x74, 0x61, + 0x6e, 0x64, 0x61, 0x72, 0x64, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x80, 0xfc, 0xaf, 0x05, 0xeb, 0x83, 0xd0, 0x0a, 0x80, 0xfe, + 0x07, 0x80, 0xfe, 0x07, 0x80, 0xfe, 0x07, 0x80, 0xfe, 0x07, + } + + info := ClStartInfo{ + Name: "gopher", + Clan: "", + Country: -1, + Body: "greensward", + Marking: "duodonny", + Decoration: "", + Hands: "standard", + Feet: "standard", + Eyes: "standard", + CustomColorBody: true, + CustomColorMarking: true, + CustomColorDecoration: false, + CustomColorHands: false, + CustomColorFeet: false, + CustomColorEyes: false, + ColorBody: 5635840, + ColorMarking: -11141356, + ColorDecoration: 65408, + ColorHands: 65408, + ColorFeet: 65408, + ColorEyes: 65408, + } + + got := info.Pack() + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, wanted %v", got, want) + } +} + +func TestUnpackStartInfo(t *testing.T) { + u := packer.Unpacker{} + u.Reset([]byte{ + 0x67, 0x6f, 0x70, 0x68, 0x65, 0x72, 0x00, + 0x00, 0x40, 0x67, 0x72, 0x65, 0x65, 0x6e, 0x73, 0x77, 0x61, 0x72, + 0x64, 0x00, 0x64, 0x75, 0x6f, 0x64, 0x6f, 0x6e, 0x6e, 0x79, 0x00, + 0x00, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x00, 0x73, + 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x00, 0x73, 0x74, 0x61, + 0x6e, 0x64, 0x61, 0x72, 0x64, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x80, 0xfc, 0xaf, 0x05, 0xeb, 0x83, 0xd0, 0x0a, 0x80, 0xfe, + 0x07, 0x80, 0xfe, 0x07, 0x80, 0xfe, 0x07, 0x80, 0xfe, 0x07, + }) + + info := ClStartInfo{} + info.Unpack(&u) + + { + got := info.Eyes + want := "standard" + if got != want { + t.Errorf("got %v, wanted %v", got, want) + } + + got = info.Decoration + want = "" + if got != want { + t.Errorf("got %v, wanted %v", got, want) + } + + got = info.Marking + want = "duodonny" + if got != want { + t.Errorf("got %v, wanted %v", got, want) + } + } + + { + got := info.ColorDecoration + want := 65408 + if got != want { + t.Errorf("got %v, wanted %v", got, want) + } + } + + wantedInfo := ClStartInfo{ + Name: "gopher", + Clan: "", + Country: -1, + Body: "greensward", + Marking: "duodonny", + Decoration: "", + Hands: "standard", + Feet: "standard", + Eyes: "standard", + CustomColorBody: true, + CustomColorMarking: true, + CustomColorDecoration: false, + CustomColorHands: false, + CustomColorFeet: false, + CustomColorEyes: false, + ColorBody: 5635840, + ColorMarking: -11141356, + ColorDecoration: 65408, + ColorHands: 65408, + ColorFeet: 65408, + ColorEyes: 65408, + } + + if !reflect.DeepEqual(info, wantedInfo) { + t.Errorf("got %v, wanted %v", info, wantedInfo) + } +} diff --git a/messages/game.go b/messages/sv_client_info.go similarity index 100% rename from messages/game.go rename to messages/sv_client_info.go diff --git a/messages/game_test.go b/messages/sv_client_info_test.go similarity index 100% rename from messages/game_test.go rename to messages/sv_client_info_test.go diff --git a/packer/packer.go b/packer/packer.go index 0734dfd..852d8fa 100644 --- a/packer/packer.go +++ b/packer/packer.go @@ -111,6 +111,20 @@ func (u *Unpacker) GetInt() int { return res } +func PackStr(str string) []byte { + return slices.Concat( + []byte(str), + []byte{0x00}, + ) +} + +func PackBool(b bool) []byte { + if b { + return []byte{0x01} + } + return []byte{0x00} +} + func PackInt(num int) []byte { res := []byte{0x00} idx := 0 diff --git a/packer/packer_test.go b/packer/packer_test.go index 306855b..32656ca 100644 --- a/packer/packer_test.go +++ b/packer/packer_test.go @@ -7,6 +7,24 @@ import ( // pack +func TestPackEmptyString(t *testing.T) { + got := PackStr("") + want := []byte{0x00} + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, wanted %v", got, want) + } +} + +func TestPackSimpleString(t *testing.T) { + got := PackStr("foo") + want := []byte{'f', 'o', 'o', 0x00} + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, wanted %v", got, want) + } +} + func TestPackSmallPositiveInts(t *testing.T) { got := PackInt(1) want := []byte{0x01} diff --git a/teeworlds.go b/teeworlds.go index 1bcc081..80339c2 100644 --- a/teeworlds.go +++ b/teeworlds.go @@ -35,6 +35,7 @@ const ( msgGameSvChat = 3 msgGameReadyToEnter = 8 msgGameSvClientInfo = 18 + msgGameClStartInfo = 27 ) func ctrlToken(myToken []byte) []byte { @@ -80,12 +81,19 @@ type TeeworldsClient struct { serverToken [4]byte conn net.Conn + // 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 (client TeeworldsClient) sendCtrlMsg(data []byte) { +func (client *TeeworldsClient) sendCtrlMsg(data []byte) { header := packet.PacketHeader{ Flags: packet.PacketFlags{ Connless: false, @@ -102,17 +110,51 @@ func (client TeeworldsClient) sendCtrlMsg(data []byte) { client.conn.Write(packet) } -func (client TeeworldsClient) sendKeepAlive() { +func (client *TeeworldsClient) sendKeepAlive() { client.sendCtrlMsg([]byte{msgCtrlKeepAlive}) } -func (client TeeworldsClient) sendReady() { +func (client *TeeworldsClient) sendReady() { ready := []byte{0x40, 0x01, 0x02, 0x25} + client.Sequence++ client.sendPacket(ready, 1) } -func (client TeeworldsClient) sendPacket(payload []byte, numChunks int) { +type ChunkArgs struct { + MsgId int + System bool + Flags chunk.ChunkFlags + Payload []byte +} + +func (client *TeeworldsClient) packChunk(c ChunkArgs) []byte { + c.MsgId <<= 1 + if c.System { + c.MsgId |= 1 + } + + client.Sequence++ + msgAndSys := packer.PackInt(c.MsgId) + + chunkHeader := chunk.ChunkHeader{ + Flags: c.Flags, + Size: len(msgAndSys) + len(c.Payload), + Seq: client.Sequence, + } + + data := slices.Concat( + chunkHeader.Pack(), + msgAndSys, + c.Payload, + ) + + fmt.Printf("packed chunk: %x\n", data) + + return data +} + +func (client *TeeworldsClient) sendPacket(payload []byte, numChunks int) { header := packet.PacketHeader{ Flags: packet.PacketFlags{ Connless: false, @@ -129,29 +171,53 @@ func (client TeeworldsClient) sendPacket(payload []byte, numChunks int) { client.conn.Write(packet) } -func (client TeeworldsClient) sendInfo() { +func (client *TeeworldsClient) sendInfo() { info := []byte{0x40, 0x28, 0x01, 0x03, 0x30, 0x2E, 0x37, 0x20, 0x38, 0x30, 0x32, 0x66, 0x31, 0x62, 0x65, 0x36, 0x30, 0x61, 0x30, 0x35, 0x36, 0x36, 0x35, 0x66, 0x00, 0x6D, 0x79, 0x5F, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6F, 0x72, 0x64, 0x5F, 0x31, 0x32, 0x33, 0x00, 0x85, 0x1C, 0x00} + client.Sequence++ client.sendPacket(info, 1) } -func (client TeeworldsClient) sendStartInfo() { - info := []byte{ - 0x41, 0x14, 0x03, 0x36, 0x67, 0x6f, 0x70, 0x68, 0x65, 0x72, 0x00, 0x00, 0x40, 0x67, 0x72, 0x65, - 0x65, 0x6e, 0x73, 0x77, 0x61, 0x72, 0x64, 0x00, 0x64, 0x75, 0x6f, 0x64, 0x6f, 0x6e, 0x6e, 0x79, - 0x00, 0x00, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x00, 0x73, 0x74, 0x61, 0x6e, 0x64, - 0x61, 0x72, 0x64, 0x00, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x00, 0x01, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x80, 0xfc, 0xaf, 0x05, 0xeb, 0x83, 0xd0, 0x0a, 0x80, 0xfe, 0x07, 0x80, 0xfe, - 0x07, 0x80, 0xfe, 0x07, 0x80, 0xfe, 0x07, +func (client *TeeworldsClient) sendStartInfo() { + info := message.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, } - client.sendPacket(info, 1) + payload := client.packChunk(ChunkArgs{ + MsgId: msgGameClStartInfo, + Flags: chunk.ChunkFlags{ + Vital: true, + }, + Payload: info.Pack(), + }) + + client.sendPacket(payload, 1) } -func (client TeeworldsClient) sendEnterGame() { +func (client *TeeworldsClient) sendEnterGame() { enter := []byte{ 0x40, 0x01, 0x04, 0x27, } diff --git a/teeworlds_test.go b/teeworlds_test.go deleted file mode 100644 index a501db9..0000000 --- a/teeworlds_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "testing" -) - -func TestIsCtrlMsg(t *testing.T) { - data := []byte{0x04, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x04} - - got := isCtrlMsg(data) - want := true - - if got != want { - t.Errorf("got %t, wanted %t", got, want) - } -}