diff options
167 files changed, 9934 insertions, 0 deletions
diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..20eb7d1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Oscar Linderholm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4292485 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# quake + +Building blocks for interacting with Quake clients and servers. diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..d9fed98 --- /dev/null +++ b/client/client.go @@ -0,0 +1,13 @@ +package client + +import ( + "github.com/osm/quake/packet" + "github.com/osm/quake/packet/command" +) + +type Client interface { + Connect(addrPort string) error + Enqueue([]command.Command) + HandleFunc(func(packet.Packet) []command.Command) + Quit() +} diff --git a/client/quake/client.go b/client/quake/client.go new file mode 100644 index 0000000..7b48e6c --- /dev/null +++ b/client/quake/client.go @@ -0,0 +1,107 @@ +package quake + +import ( + "errors" + "log" + "net" + "os" + "sync" + "time" + + "github.com/osm/quake/client" + "github.com/osm/quake/common/context" + "github.com/osm/quake/common/rand" + "github.com/osm/quake/common/sequencer" + "github.com/osm/quake/packet" + "github.com/osm/quake/packet/command" + "github.com/osm/quake/packet/command/stringcmd" + "github.com/osm/quake/protocol" +) + +var ( + ErrNoName = errors.New("no name supplied") + ErrNoTeam = errors.New("no team supplied") +) + +const connectReadDeadline = time.Duration(13) + +type Client struct { + conn *net.UDPConn + logger *log.Logger + ctx *context.Context + cmdsMu sync.Mutex + cmds []command.Command + seq *sequencer.Sequencer + handlers []func(packet.Packet) []command.Command + + addrPort string + qPort uint16 + + serverCount int32 + readDeadline time.Duration + + clientVersion string + isSpectator bool + mapName string + name string + ping int16 + team string + bottomColor byte + topColor byte + + fteEnabled bool + fteExtensions uint32 + fte2Enabled bool + fte2Extensions uint32 + mvdEnabled bool + mvdExtensions uint32 + zQuakeEnabled bool + zQuakeExtensions uint32 +} + +func New(name, team string, opts ...Option) (client.Client, error) { + if name == "" { + return nil, ErrNoName + } + + if team == "" { + return nil, ErrNoTeam + } + + c := &Client{ + ctx: context.New(context.WithProtocolVersion(protocol.VersionQW)), + logger: log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime), + seq: sequencer.New(), + readDeadline: connectReadDeadline, + qPort: rand.Uint16(), + name: name, + ping: 999, + team: team, + } + + for _, opt := range opts { + opt(c) + } + + c.seq.SetPing(c.ping) + + return c, nil +} + +func (c *Client) Enqueue(cmds []command.Command) { + c.cmdsMu.Lock() + c.cmds = append(c.cmds, cmds...) + c.cmdsMu.Unlock() +} + +func (c *Client) HandleFunc(h func(h packet.Packet) []command.Command) { + c.handlers = append(c.handlers, h) +} + +func (c *Client) Quit() { + c.cmds = append(c.cmds, &stringcmd.Command{String: "drop"}) + + if c.seq.GetState() == sequencer.Connected { + time.Sleep(time.Duration(c.ping) * time.Millisecond) + } +} diff --git a/client/quake/commands.go b/client/quake/commands.go new file mode 100644 index 0000000..a416753 --- /dev/null +++ b/client/quake/commands.go @@ -0,0 +1,237 @@ +package quake + +import ( + "fmt" + "strings" + "time" + + "github.com/osm/quake/common/args" + "github.com/osm/quake/common/infostring" + "github.com/osm/quake/common/sequencer" + "github.com/osm/quake/packet/command" + "github.com/osm/quake/packet/command/connect" + "github.com/osm/quake/packet/command/download" + "github.com/osm/quake/packet/command/ip" + "github.com/osm/quake/packet/command/modellist" + "github.com/osm/quake/packet/command/s2cchallenge" + "github.com/osm/quake/packet/command/s2cconnection" + "github.com/osm/quake/packet/command/serverdata" + "github.com/osm/quake/packet/command/soundlist" + "github.com/osm/quake/packet/command/stringcmd" + "github.com/osm/quake/packet/command/stufftext" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/fte" + "github.com/osm/quake/protocol/mvd" +) + +func (c *Client) handleServerCommand(cmd command.Command) []command.Command { + switch cmd := cmd.(type) { + case *s2cchallenge.Command: + return c.handleChallenge(cmd) + case *s2cconnection.Command: + c.seq.SetState(sequencer.Connecting) + return c.handleNew() + case *stufftext.Command: + return c.handleStufftext(cmd) + case *soundlist.Command: + return c.handleSoundList(cmd) + case *modellist.Command: + return c.handleModelList(cmd) + case *serverdata.Command: + c.serverCount = cmd.ServerCount + } + + return nil +} + +func (c *Client) handleChallenge(cmd *s2cchallenge.Command) []command.Command { + userInfo := infostring.New( + infostring.WithKeyValue("name", c.name), + infostring.WithKeyValue("team", c.team), + infostring.WithKeyValue("topcolor", fmt.Sprintf("%d", c.topColor)), + infostring.WithKeyValue("bottomcolor", fmt.Sprintf("%d", c.bottomColor)), + ) + + if c.addrPort != "" { + userInfo.Info = append(userInfo.Info, infostring.Info{ + Key: "prx", + Value: c.addrPort, + }) + } + + if c.clientVersion != "" { + userInfo.Info = append(userInfo.Info, infostring.Info{ + Key: "*client", + Value: c.clientVersion, + }) + } + + if c.isSpectator { + userInfo.Info = append(userInfo.Info, infostring.Info{ + Key: "spectator", + Value: "1", + }) + } + + if c.zQuakeEnabled { + userInfo.Info = append(userInfo.Info, infostring.Info{ + Key: "*z_ext", + Value: fmt.Sprintf("%d", c.zQuakeExtensions), + }) + } + + var extensions []*protocol.Extension + for _, ext := range cmd.Extensions { + if c.fteEnabled && ext.Version == fte.ProtocolVersion { + extensions = append(extensions, ext) + } else if c.fte2Enabled && ext.Version == fte.ProtocolVersion2 { + extensions = append(extensions, ext) + } else if c.mvdEnabled && ext.Version == mvd.ProtocolVersion { + extensions = append(extensions, ext) + } + } + + return []command.Command{ + &connect.Command{ + Command: "connect", + Version: fmt.Sprintf("%v", c.ctx.GetProtocolVersion()), + QPort: c.qPort, + ChallengeID: cmd.ChallengeID, + UserInfo: userInfo, + Extensions: extensions, + }, + } +} + +func (c *Client) handleNew() []command.Command { + return []command.Command{&stringcmd.Command{String: "new"}} +} + +func (c *Client) handleStufftext(cmd *stufftext.Command) []command.Command { + var cmds []command.Command + + for _, a := range args.Parse(cmd.String) { + switch a.Cmd { + case "reconnect": + c.seq.SetState(sequencer.Connecting) + cmds = append(cmds, c.handleNew()...) + case "cmd": + cmds = append(cmds, c.handleStufftextCmds(a.Args)...) + case "packet": + cmds = append(cmds, &ip.Command{String: a.Args[1][1 : len(a.Args[1])-1]}) + case "fullserverinfo": + cmds = append(cmds, c.handleFullServerInfo(a.Args[0])...) + case "skins": + c.readDeadline = time.Duration(c.ping) + c.seq.SetState(sequencer.Connected) + cmds = append(cmds, &stringcmd.Command{ + String: fmt.Sprintf("begin %d", c.serverCount), + }) + } + } + + return cmds +} + +func (c *Client) handleStufftextCmds(args []string) []command.Command { + switch args[0] { + case "ack", "prespawn", "spawn": + return []command.Command{&stringcmd.Command{String: strings.Join(args, " ")}} + case "pext": + return c.handleProtocolExtensions() + case "new": + return c.handleNew() + } + + return nil +} + +func (c *Client) handleProtocolExtensions() []command.Command { + var fteVersion uint32 + var fteExtensions uint32 + var fte2Version uint32 + var fte2Extensions uint32 + var mvdVersion uint32 + var mvdExtensions uint32 + + if c.fteEnabled { + fteVersion = fte.ProtocolVersion + fteExtensions = c.fteExtensions + } + if c.fte2Enabled { + fte2Version = fte.ProtocolVersion2 + fte2Extensions = c.fte2Extensions + } + if c.mvdEnabled { + mvdVersion = mvd.ProtocolVersion + mvdExtensions = c.mvdExtensions + } + + return []command.Command{ + &stringcmd.Command{String: fmt.Sprintf("pext 0x%x 0x%x 0x%x 0x%x 0x%x 0x%x", + fteVersion, + fteExtensions, + fte2Version, + fte2Extensions, + mvdVersion, + mvdExtensions, + )}, + } +} + +func (c *Client) handleFullServerInfo(args string) []command.Command { + inf := infostring.Parse(args) + c.mapName = inf.Get("map") + + return []command.Command{ + &stringcmd.Command{String: fmt.Sprintf("soundlist %d 0", c.serverCount)}, + } +} + +func (c *Client) handleSoundList(cmd *soundlist.Command) []command.Command { + nextCmd := "soundlist" + if cmd.Index == 0 { + nextCmd = "modellist" + } + + return []command.Command{ + &stringcmd.Command{ + String: fmt.Sprintf("%s %d %d", nextCmd, c.serverCount, cmd.Index), + }, + } +} + +func (c *Client) handleModelList(cmd *modellist.Command) []command.Command { + if cmd.Index == 0 { + return c.handlePrespawn() + } + + return []command.Command{ + &stringcmd.Command{ + String: fmt.Sprintf("modellist %d %d", c.serverCount, cmd.Index), + }, + } +} + +func (c *Client) handlePrespawn() []command.Command { + hash, ok := protocol.MapChecksum[c.mapName] + if !ok { + c.logger.Printf("missing map checksum for %s, requesting download", c.mapName) + + // Send download command for the missing map + return []command.Command{ + &download.Command{ + FTEProtocolExtension: c.fteExtensions, + Size16: -1, // -1 signals the server to start sending the file + Name: c.mapName, // map filename + }, + } + } + + // If hash exists, continue normally + return []command.Command{ + &stringcmd.Command{String: fmt.Sprintf("setinfo pmodel %d", protocol.PlayerModel)}, + &stringcmd.Command{String: fmt.Sprintf("setinfo emodel %d", protocol.EyeModel)}, + &stringcmd.Command{String: fmt.Sprintf("prespawn %v 0 %d", c.serverCount, hash)}, + } +} diff --git a/client/quake/move.go b/client/quake/move.go new file mode 100644 index 0000000..600c1fd --- /dev/null +++ b/client/quake/move.go @@ -0,0 +1,30 @@ +package quake + +import ( + "github.com/osm/quake/packet/command/deltausercommand" + "github.com/osm/quake/packet/command/move" + "github.com/osm/quake/protocol" +) + +func (c *Client) getMove(seq uint32) *move.Command { + mov := move.Command{ + Null: &deltausercommand.Command{ + ProtocolVersion: c.ctx.GetProtocolVersion(), + Bits: protocol.CMForward | protocol.CMSide | protocol.CMUp | protocol.CMButtons, + CMForward16: 0, + CMSide16: 0, + CMUp16: 0, + CMButtons: 0, + CMMsec: 13, // ~72hz + }, + Old: &deltausercommand.Command{ + ProtocolVersion: c.ctx.GetProtocolVersion(), + }, + New: &deltausercommand.Command{ + ProtocolVersion: c.ctx.GetProtocolVersion(), + }, + } + + mov.Checksum = mov.GetChecksum(seq - 1) + return &mov +} diff --git a/client/quake/net.go b/client/quake/net.go new file mode 100644 index 0000000..16c9016 --- /dev/null +++ b/client/quake/net.go @@ -0,0 +1,144 @@ +package quake + +import ( + "errors" + "net" + "syscall" + "time" + + "github.com/osm/quake/common/sequencer" + "github.com/osm/quake/packet" + "github.com/osm/quake/packet/clc" + "github.com/osm/quake/packet/command" + "github.com/osm/quake/packet/command/connect" + "github.com/osm/quake/packet/command/getchallenge" + "github.com/osm/quake/packet/command/ip" + "github.com/osm/quake/packet/svc" +) + +func (c *Client) Connect(addrPort string) error { + addr, err := net.ResolveUDPAddr("udp", addrPort) + if err != nil { + return err + } + + conn, err := net.DialUDP("udp", nil, addr) + if err != nil { + return err + } + c.conn = conn + + c.sendChallenge() + + buf := make([]byte, 1024*64) + for { + + var isReadTimeout bool + var incomingSeq uint32 + var incomingAck uint32 + var packet packet.Packet + var cmds []command.Command + + if err := c.conn.SetReadDeadline( + time.Now().Add(time.Millisecond * c.readDeadline), + ); err != nil { + c.logger.Printf("unable to set read deadline, %v\n", err) + } + + n, _, err := c.conn.ReadFromUDP(buf) + if err != nil { + if errors.Is(err, syscall.ECONNREFUSED) { + c.logger.Printf("lost connection - reconnecting in 5 seqonds") + time.Sleep(time.Second * 5) + c.sendChallenge() + continue + } + + if err, ok := err.(net.Error); ok && err.Timeout() { + isReadTimeout = true + goto process + } + + c.logger.Printf("unexpected read error, %v", err) + continue + } + + packet, err = svc.Parse(c.ctx, buf[:n]) + if err != nil { + c.logger.Printf("error when parsing server data, %v", err) + continue + } + + switch p := packet.(type) { + case *svc.Connectionless: + cmds = []command.Command{p.Command} + case *svc.GameData: + cmds = p.Commands + incomingSeq = p.Seq + incomingAck = p.Ack + + } + + for _, h := range c.handlers { + cmds = append(cmds, h(packet)...) + } + process: + c.processCommands(incomingSeq, incomingAck, cmds, isReadTimeout) + } +} + +func (c *Client) processCommands( + incomingSeq, incomingAck uint32, + serverCmds []command.Command, + isReadTmeout bool, +) { + var cmds []command.Command + + for _, serverCmd := range serverCmds { + for _, cmd := range c.handleServerCommand(serverCmd) { + switch cmd := cmd.(type) { + case *connect.Command, *ip.Command: + if _, err := c.conn.Write( + (&clc.Connectionless{Command: cmd}).Bytes(), + ); err != nil { + c.logger.Printf("unable to send connectionless command, %v\n", err) + } + default: + cmds = append(cmds, cmd) + } + } + } + + if c.seq.GetState() <= sequencer.Handshake { + return + } + + c.cmdsMu.Lock() + outSeq, outAck, outCmds, err := c.seq.Process(incomingSeq, incomingAck, append(c.cmds, cmds...)) + c.cmds = []command.Command{} + c.cmdsMu.Unlock() + + if err == sequencer.ErrRateLimit { + return + } + + if _, err := c.conn.Write((&clc.GameData{ + Seq: outSeq, + Ack: outAck, + QPort: c.qPort, + Commands: append(outCmds, c.getMove(outSeq)), + }).Bytes()); err != nil { + c.logger.Printf("unable to send game data, %v\n", err) + } +} + +func (c *Client) sendChallenge() { + c.seq.Reset() + c.seq.SetState(sequencer.Handshake) + c.readDeadline = connectReadDeadline + if _, err := c.conn.Write( + (&clc.Connectionless{Command: &getchallenge.Command{}}).Bytes(), + ); err != nil { + c.logger.Printf("unable to send challenge, %v\n", err) + } +} diff --git a/client/quake/option.go b/client/quake/option.go new file mode 100644 index 0000000..9c94fde --- /dev/null +++ b/client/quake/option.go @@ -0,0 +1,87 @@ +package quake + +import "log" + +type Option func(*Client) + +func WithName(name string) Option { + return func(c *Client) { + c.name = name + } +} + +func WithTeam(team string) Option { + return func(c *Client) { + c.team = team + } +} + +func WithSpectator(isSpectator bool) Option { + return func(c *Client) { + c.isSpectator = isSpectator + } +} + +func WithClientVersion(clientVersion string) Option { + return func(c *Client) { + c.clientVersion = clientVersion + } +} + +func WithServerAddr(addrPort string) Option { + return func(c *Client) { + c.addrPort = addrPort + } +} + +func WithPing(ping int16) Option { + return func(c *Client) { + c.ping = ping + } +} + +func WithBottomColor(color byte) Option { + return func(c *Client) { + c.bottomColor = color + } +} + +func WithTopColor(color byte) Option { + return func(c *Client) { + c.topColor = color + } +} + +func WithFTEExtensions(extensions uint32) Option { + return func(c *Client) { + c.fteEnabled = true + c.fteExtensions = extensions + } +} + +func WithFTE2Extensions(extensions uint32) Option { + return func(c *Client) { + c.fte2Enabled = true + c.fte2Extensions = extensions + } +} + +func WithMVDExtensions(extensions uint32) Option { + return func(c *Client) { + c.mvdEnabled = true + c.mvdExtensions = extensions + } +} + +func WithZQuakeExtensions(extensions uint32) Option { + return func(c *Client) { + c.zQuakeEnabled = true + c.zQuakeExtensions = extensions + } +} + +func WithLogger(logger *log.Logger) Option { + return func(c *Client) { + c.logger = logger + } +} diff --git a/common/args/args.go b/common/args/args.go new file mode 100644 index 0000000..28c3b5f --- /dev/null +++ b/common/args/args.go @@ -0,0 +1,79 @@ +package args + +import ( + "strings" + + "github.com/osm/quake/common/buffer" +) + +type Arg struct { + Cmd string + Args []string +} + +func Parse(input string) []Arg { + var arg string + var args []string + var ret []Arg + var inQuotes bool + + for _, c := range input { + switch c { + case '\n', ';': + if inQuotes { + arg += string(c) + continue + } + + if arg != "" { + args = append(args, strings.TrimSpace(arg)) + } + + ret = append(ret, Arg{Cmd: strings.TrimSpace(args[0]), Args: args[1:]}) + arg = "" + args = []string{} + case ' ': + if inQuotes { + arg += string(c) + continue + } + + if arg != "" { + args = append(args, strings.TrimSpace(arg)) + arg = "" + } + case '"': + inQuotes = !inQuotes + arg += string(c) + default: + arg += string(c) + } + } + + if arg != "" { + args = append(args, strings.TrimSpace(arg)) + } + + if len(args) > 0 { + ret = append(ret, Arg{Cmd: strings.TrimSpace(args[0]), Args: args[1:]}) + } + + return ret +} + +func (s Arg) Bytes() []byte { + buf := buffer.New() + + buf.PutBytes([]byte(s.Cmd)) + + if len(s.Args) > 0 { + buf.PutByte(byte(' ')) + buf.PutBytes([]byte(strings.Join(s.Args, " "))) + } + + return buf.Bytes() +} + +func (s Arg) String() string { + return string(s.Bytes()) +} diff --git a/common/args/args_test.go b/common/args/args_test.go new file mode 100644 index 0000000..4f1967c --- /dev/null +++ b/common/args/args_test.go @@ -0,0 +1,83 @@ +package args + +import ( + "encoding/base64" + "reflect" + "testing" +) + +type argsTest struct { + name string + input string + expected []Arg +} + +var argsTests = []argsTest{ + { + name: "foo bar baz", + input: "Zm9vIGJhciBiYXo=", + expected: []Arg{ + { + Cmd: "foo", + Args: []string{"bar", "baz"}, + }, + }, + }, + { + name: "foo bar baz\n", + input: "Zm9vIGJhciBiYXoK", + expected: []Arg{ + { + Cmd: "foo", + Args: []string{"bar", "baz"}, + }, + }, + }, + { + name: "foo \"bar baz\"", + input: "Zm9vICJiYXIgYmF6Ig==", + expected: []Arg{ + { + Cmd: "foo", + Args: []string{"\"bar baz\""}, + }, + }, + }, + { + name: "foo \"bar baz\";foo bar baz\nfoo bar", + input: "Zm9vICJiYXIgYmF6Ijtmb28gYmFyIGJhegpmb28gYmFy", + expected: []Arg{ + { + Cmd: "foo", + Args: []string{"\"bar baz\""}, + }, + { + Cmd: "foo", + Args: []string{"bar", "baz"}, + }, + { + Cmd: "foo", + Args: []string{"bar"}, + }, + }, + }, +} + +func TestArgs(t *testing.T) { + for _, at := range argsTests { + t.Run(at.name, func(t *testing.T) { + input, err := base64.StdEncoding.DecodeString(at.input) + if err != nil { + t.Errorf("unable to decode input: %#v", err) + } + + args := Parse(string(input)) + + if !reflect.DeepEqual(at.expected, args) { + t.Errorf("parsed args output didn't match") + t.Logf("output: %#v", args) + t.Logf("expected: %#v", at.expected) + } + }) + } +} diff --git a/common/ascii/ascii.go b/common/ascii/ascii.go new file mode 100644 index 0000000..30a5b17 --- /dev/null +++ b/common/ascii/ascii.go @@ -0,0 +1,75 @@ +package ascii + +import ( + "strings" +) + +func Parse(input string) string { + var str strings.Builder + + for i := 0; i < len(input); i++ { + c := input[i] + + if c > 0 && c < 5 { + str.WriteByte('#') + } else if c == 5 { + str.WriteByte('.') + } else if c > 5 && c < 10 { + str.WriteByte('#') + } else if c == 10 { + str.WriteByte(10) + } else if c == 11 { + str.WriteByte('#') + } else if c > 11 && c < 14 { + str.WriteByte(' ') + } else if c > 14 && c < 16 { + str.WriteByte('.') + } else if c == 16 { + str.WriteByte('[') + } else if c == 17 { + str.WriteByte(']') + } else if c > 17 && c < 28 { + str.WriteByte(c + 30) + } else if c >= 28 && c < 32 { + str.WriteByte('.') + } else if c == 32 { + str.WriteByte(' ') + } else if c > 32 && c < 127 { + str.WriteByte(c) + } else if c > 127 && c < 129 { + str.WriteByte('<') + } else if c == 129 { + str.WriteByte('=') + } else if c == 130 { + str.WriteByte('>') + } else if c > 130 && c < 133 { + str.WriteByte('#') + } else if c == 133 { + str.WriteByte('.') + } else if c > 133 && c < 141 { + str.WriteByte('#') + } else if c > 141 && c < 144 { + str.WriteByte('.') + } else if c == 144 { + str.WriteByte('[') + } else if c == 145 { + str.WriteByte(']') + } else if c > 145 && c < 156 { + str.WriteByte(c - 98) + } else if c == 156 { + str.WriteByte('.') + } else if c == 157 { + str.WriteByte('<') + } else if c == 158 { + str.WriteByte('=') + } else if c == 159 { + str.WriteByte('>') + } else if c == 160 { + str.WriteByte(' ') + } else { + str.WriteByte(c - 128) + } + } + + return str.String() +} diff --git a/common/buffer/buffer.go b/common/buffer/buffer.go new file mode 100644 index 0000000..afc5f7f --- /dev/null +++ b/common/buffer/buffer.go @@ -0,0 +1,34 @@ +package buffer + +import ( + "errors" +) + +var ErrBadRead = errors.New("bad read") + +type Buffer struct { + buf []byte + off int +} + +func New(opts ...Option) *Buffer { + b := &Buffer{} + + for _, opt := range opts { + opt(b) + } + + return b +} + +func (b *Buffer) Len() int { + return len(b.buf) +} + +func (b *Buffer) Off() int { + return b.off +} + +func (b *Buffer) Bytes() []byte { + return b.buf +} diff --git a/common/buffer/get.go b/common/buffer/get.go new file mode 100644 index 0000000..4f27b70 --- /dev/null +++ b/common/buffer/get.go @@ -0,0 +1,189 @@ +package buffer + +import ( + "encoding/binary" + "math" + "strings" +) + +func (b *Buffer) ReadByte() (byte, error) { + if b.off+1 > len(b.buf) { + return 0, ErrBadRead + } + + r := b.buf[b.off] + b.off += 1 + return r, nil +} + +func (b *Buffer) GetBytes(n int) ([]byte, error) { + if b.off+n > len(b.buf) { + return nil, ErrBadRead + } + + r := b.buf[b.off : b.off+n] + b.off += n + return r, nil +} + +func (b *Buffer) GetUint8() (uint8, error) { + if b.off+1 > len(b.buf) { + return 0, ErrBadRead + } + + r := b.buf[b.off] + b.off++ + return r, nil +} + +func (b *Buffer) GetInt8() (int8, error) { + r, err := b.GetUint8() + if err != nil { + return 0, err + } + + return int8(r), nil +} + +func (b *Buffer) GetInt16() (int16, error) { + r, err := b.GetUint16() + if err != nil { + return 0, err + } + + return int16(r), nil +} + +func (b *Buffer) GetUint16() (uint16, error) { + if b.off+2 > len(b.buf) { + return 0, ErrBadRead + } + + r := binary.LittleEndian.Uint16(b.buf[b.off : b.off+2]) + b.off += 2 + return r, nil +} + +func (b *Buffer) GetInt32() (int32, error) { + r, err := b.GetUint32() + if err != nil { + return 0, err + } + + return int32(r), nil +} + +func (b *Buffer) GetUint32() (uint32, error) { + if b.off+4 > len(b.buf) { + return 0, ErrBadRead + } + + r := binary.LittleEndian.Uint32(b.buf[b.off : b.off+4]) + b.off += 4 + return r, nil +} + +func (b *Buffer) GetFloat32() (float32, error) { + r, err := b.GetUint32() + if err != nil { + return 0, err + } + + return math.Float32frombits(r), nil +} + +func (b *Buffer) GetAngle8() (float32, error) { + r, err := b.ReadByte() + if err != nil { + return 0, err + } + + return float32(int8(r)) * (360.0 / 256), nil +} + +func (b *Buffer) GetAngle16() (float32, error) { + r, err := b.GetUint16() + if err != nil { + return 0, err + } + + return float32(r) * (360.0 / 65536), nil +} + +func (b *Buffer) GetCoord16() (float32, error) { + r, err := b.GetInt16() + if err != nil { + return 0, err + } + + return float32(r) / 8.0, nil +} + +func (b *Buffer) GetCoord32() (float32, error) { + return b.GetFloat32() +} + +func (b *Buffer) GetString() (string, error) { + var str strings.Builder + + for b.off < b.Len() { + r, err := b.ReadByte() + if err != nil { + return "", err + } + + if r == 0xff { + continue + } + + if r == 0x00 { + break + } + + str.WriteByte(r) + } + + return str.String(), nil +} + +func (b *Buffer) PeekInt32() (int32, error) { + if b.off+4 > len(b.buf) { + return 0, ErrBadRead + } + + return int32(binary.LittleEndian.Uint32(b.buf[b.off : b.off+4])), nil +} + +func (b *Buffer) PeekBytes(n int) ([]byte, error) { + if b.off+n > len(b.buf) { + return nil, ErrBadRead + } + + return b.buf[b.off : b.off+n], nil +} + +func (b *Buffer) PeekBytesAt(off, size int) ([]byte, error) { + if off+size > len(b.buf) { + return nil, ErrBadRead + } + + return b.buf[off : off+size], nil +} + +func (b *Buffer) Skip(n int) error { + if b.off+n > len(b.buf) { + return ErrBadRead + } + + b.off += n + return nil +} + +func (b *Buffer) Seek(n int) error { + if n > len(b.buf) { + return ErrBadRead + } + + b.off = n + return nil +} diff --git a/common/buffer/get_test.go b/common/buffer/get_test.go new file mode 100644 index 0000000..a4cd8a2 --- /dev/null +++ b/common/buffer/get_test.go @@ -0,0 +1,107 @@ +package buffer + +import ( + "math" + "reflect" + "testing" +) + +type test[T any] struct { + name string + input []byte + expected T +} + +var byteTests = []test[byte]{ + {"byte test 0", []byte{0x00}, byte(0)}, + {"byte test 255", []byte{0xff}, byte(255)}, +} + +var int16Tests = []test[int16]{ + {"int16 test 0", []byte{0x00, 0x00}, int16(0)}, + {"int16 test -32768", []byte{0x00, 0x80}, math.MinInt16}, + {"int16 test 32767", []byte{0xff, 0x7f}, math.MaxInt16}, +} + +var uint16Tests = []test[uint16]{ + {"uint16 test 0", []byte{0x00, 0x00}, uint16(0)}, + {"uint16 test 65535", []byte{0xff, 0xff}, math.MaxUint16}, +} + +var int32Tests = []test[int32]{ + {"int32 test 0", []byte{0x00, 0x00, 0x00, 0x00}, int32(0)}, + {"int32 test -2147483648", []byte{0x00, 0x00, 0x00, 0x80}, math.MinInt32}, + {"int32 test 2147483647", []byte{0xff, 0xff, 0xff, 0x7f}, math.MaxInt32}, +} + +var uint32Tests = []test[uint32]{ + {"uint32 test 0", []byte{0x00, 0x00, 0x00, 0x00}, uint32(0)}, + {"uint32 test 4294967295", []byte{0xff, 0xff, 0xff, 0xff}, uint32(math.MaxUint32)}, +} + +var float32Tests = []test[float32]{ + {"float32 test 0", []byte{0x00, 0x00, 0x00, 0x00}, float32(0)}, + {"float32 test 1e-45", []byte{0x01, 0x00, 0x00, 0x00}, math.SmallestNonzeroFloat32}, + {"float32 test 3.4028235e+38", []byte{0xff, 0xff, 0x7f, 0x7f}, math.MaxFloat32}, +} + +var angle8Tests = []test[float32]{ + {"angle8 test 0", []byte{0x00}, float32(0)}, + {"angle8 test 90", []byte{0x40}, float32(90)}, + {"angle8 test -90", []byte{0xc0}, float32(-90)}, +} + +var angle16Tests = []test[float32]{ + {"angle16 test 0", []byte{0x00, 0x00}, float32(0)}, + {"angle16 test 90", []byte{0x00, 0x40}, float32(90)}, + {"angle16 test 180", []byte{0x00, 0x80}, float32(180)}, +} + +var coord16Tests = []test[float32]{ + {"coord16 test 0", []byte{0x00, 0x00}, float32(0)}, + {"coord16 test 4095", []byte{0xf8, 0x7f}, float32(4095)}, + {"coord16 test -4096", []byte{0x00, 0x80}, float32(-4096)}, +} + +var coord32Tests = []test[float32]{ + {"coord32 test 0", []byte{0x00, 0x00, 0x00, 0x00}, float32(0)}, + {"coord32 test 90", []byte{0x00, 0x00, 0xb4, 0x42}, float32(90)}, + {"coord32 test -90", []byte{0x00, 0x00, 0xb4, 0xc2}, float32(-90)}, +} + +var stringTests = []test[string]{ + {"string test foo bar baz", []byte{0x66, 0x6f, 0x6f, 0x20, 0x62, 0x61, 0x72, 0x00}, "foo bar"}, + {"string test foo bar baz", []byte{0x66, 0x6f, 0x6f, 0x0a, 0x00}, "foo\n"}, +} + +func runTest[T any](t *testing.T, tests []test[T], f func(*Buffer) (T, error)) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := f(New(WithData(tt.input))) + + if err != nil { + t.Errorf("%s: should not return an error", tt.name) + } + + if !reflect.DeepEqual(val, tt.expected) { + t.Errorf("%s: invalid value returned", tt.name) + t.Logf("actual: %#v", val) + t.Logf("expected: %#v", tt.expected) + } + }) + } +} + +func TestBuffer(t *testing.T) { + runTest(t, byteTests, func(buf *Buffer) (byte, error) { return buf.ReadByte() }) + runTest(t, int16Tests, func(buf *Buffer) (int16, error) { return buf.GetInt16() }) + runTest(t, uint16Tests, func(buf *Buffer) (uint16, error) { return buf.GetUint16() }) + runTest(t, int32Tests, func(buf *Buffer) (int32, error) { return buf.GetInt32() }) + runTest(t, uint32Tests, func(buf *Buffer) (uint32, error) { return buf.GetUint32() }) + runTest(t, float32Tests, func(buf *Buffer) (float32, error) { return buf.GetFloat32() }) + runTest(t, angle8Tests, func(buf *Buffer) (float32, error) { return buf.GetAngle8() }) + runTest(t, angle16Tests, func(buf *Buffer) (float32, error) { return buf.GetAngle16() }) + runTest(t, coord16Tests, func(buf *Buffer) (float32, error) { return buf.GetCoord16() }) + runTest(t, coord32Tests, func(buf *Buffer) (float32, error) { return buf.GetCoord32() }) + runTest(t, stringTests, func(buf *Buffer) (string, error) { return buf.GetString() }) +} diff --git a/common/buffer/option.go b/common/buffer/option.go new file mode 100644 index 0000000..2e802e6 --- /dev/null +++ b/common/buffer/option.go @@ -0,0 +1,9 @@ +package buffer + +type Option func(*Buffer) + +func WithData(data []byte) Option { + return func(b *Buffer) { + b.buf = data + } +} diff --git a/common/buffer/put.go b/common/buffer/put.go new file mode 100644 index 0000000..8a5064e --- /dev/null +++ b/common/buffer/put.go @@ -0,0 +1,87 @@ +package buffer + +import ( + "encoding/binary" + "math" +) + +func (b *Buffer) PutByte(v byte) { + b.off += 1 + b.buf = append(b.buf, v) +} + +func (b *Buffer) PutBytes(v []byte) { + b.off += len(v) + b.buf = append(b.buf, v...) +} + +func (b *Buffer) PutInt8(v int8) { + b.PutUint8(uint8(v)) +} + +func (b *Buffer) PutUint8(v uint8) { + b.off++ + b.buf = append(b.buf, v) +} + +func (b *Buffer) PutInt16(v int16) { + b.PutUint16(uint16(v)) +} + +func (b *Buffer) PutUint16(v uint16) { + b.off += 2 + + tmp := make([]byte, 2) + binary.LittleEndian.PutUint16(tmp, v) + b.buf = append(b.buf, tmp...) +} + +func (b *Buffer) PutInt32(v int32) { + b.PutUint32(uint32(v)) +} + +func (b *Buffer) PutUint32(v uint32) { + b.off += 4 + + tmp := make([]byte, 4) + binary.LittleEndian.PutUint32(tmp, v) + b.buf = append(b.buf, tmp...) +} + +func (b *Buffer) PutFloat32(v float32) { + b.off += 4 + + tmp := make([]byte, 4) + binary.LittleEndian.PutUint32(tmp, math.Float32bits(v)) + b.buf = append(b.buf, tmp...) +} + +func (b *Buffer) PutString(v string) { + b.off += len(v) + 1 + + for i := 0; i < len(v); i++ { + b.PutByte(byte(v[i])) + } + + b.PutByte(0) +} + +func (b *Buffer) PutCoord16(v float32) { + b.PutUint16(uint16(v * 8.0)) +} + +func (b *Buffer) PutCoord32(v float32) { + b.PutFloat32(v) +} + +func (b *Buffer) PutAngle8(v float32) { + b.PutByte(byte(v / (360.0 / 256))) +} + +func (b *Buffer) PutAngle16(v float32) { + b.PutUint16(uint16(v / (360.0 / 65536))) +} + +func (b *Buffer) PutAngle32(v float32) { + b.PutFloat32(v) +} diff --git a/common/context/context.go b/common/context/context.go new file mode 100644 index 0000000..6aec81d --- /dev/null +++ b/common/context/context.go @@ -0,0 +1,236 @@ +package context + +import ( + "sync" + + "github.com/osm/quake/protocol" +) + +type Context struct { + angleSizeMu sync.Mutex + angleSize uint8 + + coordSizeMU sync.Mutex + coordSize uint8 + + protocolVersioncolVersionMu sync.Mutex + protocolVersion uint32 + + isMVDEnabledMu sync.Mutex + isMVDEnabled bool + + mvdProtocolExtensionMu sync.Mutex + mvdProtocolExtension uint32 + + isFTEEnabledMu sync.Mutex + isFTEEnabled bool + + fteProtocolExtensionMu sync.Mutex + fteProtocolExtension uint32 + + isFTE2EnabledMu sync.Mutex + isFTE2Enabled bool + + fte2ProtocolExtensionMu sync.Mutex + fte2ProtocolExtension uint32 + + isZQuakeEnabledMu sync.Mutex + isZQuakeEnabled bool + + zQuakeProtocolExtensionMu sync.Mutex + zQuakeProtocolExtension uint32 + + isDemMu sync.Mutex + isDem bool + + isQWDMu sync.Mutex + isQWD bool + + isMVDMu sync.Mutex + isMVD bool +} + +func New(opts ...Option) *Context { + ctx := &Context{ + angleSize: 1, + coordSize: 2, + } + + for _, opt := range opts { + opt(ctx) + } + + return ctx +} + +func (ctx *Context) GetAngleSize() uint8 { + ctx.angleSizeMu.Lock() + defer ctx.angleSizeMu.Unlock() + return ctx.angleSize +} + +func (ctx *Context) SetAngleSize(v uint8) { + ctx.angleSizeMu.Lock() + defer ctx.angleSizeMu.Unlock() + ctx.angleSize = v +} + +func (ctx *Context) GetCoordSize() uint8 { + ctx.coordSizeMU.Lock() + defer ctx.coordSizeMU.Unlock() + return ctx.coordSize +} + +func (ctx *Context) SetCoordSize(v uint8) { + ctx.coordSizeMU.Lock() + defer ctx.coordSizeMU.Unlock() + ctx.coordSize = v +} + +func (ctx *Context) GetProtocolVersion() uint32 { + ctx.protocolVersioncolVersionMu.Lock() + defer ctx.protocolVersioncolVersionMu.Unlock() + return ctx.protocolVersion +} + +func (ctx *Context) SetProtocolVersion(v uint32) { + ctx.protocolVersioncolVersionMu.Lock() + defer ctx.protocolVersioncolVersionMu.Unlock() + ctx.protocolVersion = v +} + +func (ctx *Context) GetIsMVDEnabled() bool { + ctx.isMVDEnabledMu.Lock() + defer ctx.isMVDEnabledMu.Unlock() + return ctx.isMVDEnabled +} + +func (ctx *Context) SetIsMVDEnabled(v bool) { + ctx.isMVDEnabledMu.Lock() + defer ctx.isMVDEnabledMu.Unlock() + ctx.isMVDEnabled = v +} + +func (ctx *Context) GetMVDProtocolExtension() uint32 { + ctx.mvdProtocolExtensionMu.Lock() + defer ctx.mvdProtocolExtensionMu.Unlock() + return ctx.mvdProtocolExtension +} + +func (ctx *Context) SetMVDProtocolExtension(v uint32) { + ctx.mvdProtocolExtensionMu.Lock() + defer ctx.mvdProtocolExtensionMu.Unlock() + ctx.mvdProtocolExtension = v +} + +func (ctx *Context) GetIsFTEEnabled() bool { + ctx.isFTEEnabledMu.Lock() + defer ctx.isFTEEnabledMu.Unlock() + return ctx.isFTEEnabled +} + +func (ctx *Context) SetIsFTEEnabled(v bool) { + ctx.isFTEEnabledMu.Lock() + defer ctx.isFTEEnabledMu.Unlock() + ctx.isFTEEnabled = v +} + +func (ctx *Context) GetFTEProtocolExtension() uint32 { + ctx.fteProtocolExtensionMu.Lock() + defer ctx.fteProtocolExtensionMu.Unlock() + return ctx.fteProtocolExtension +} + +func (ctx *Context) SetFTEProtocolExtension(v uint32) { + ctx.fteProtocolExtensionMu.Lock() + defer ctx.fteProtocolExtensionMu.Unlock() + ctx.fteProtocolExtension = v +} + +func (ctx *Context) GetIsFTE2Enabled() bool { + ctx.isFTE2EnabledMu.Lock() + defer ctx.isFTE2EnabledMu.Unlock() + return ctx.isFTE2Enabled +} + +func (ctx *Context) SetIsFTE2Enabled(v bool) { + ctx.isFTE2EnabledMu.Lock() + defer ctx.isFTE2EnabledMu.Unlock() + ctx.isFTE2Enabled = v +} + +func (ctx *Context) GetFTE2ProtocolExtension() uint32 { + ctx.fte2ProtocolExtensionMu.Lock() + defer ctx.fte2ProtocolExtensionMu.Unlock() + return ctx.fte2ProtocolExtension +} + +func (ctx *Context) SetFTE2ProtocolExtension(v uint32) { + ctx.fte2ProtocolExtensionMu.Lock() + defer ctx.fte2ProtocolExtensionMu.Unlock() + ctx.fte2ProtocolExtension = v +} + +func (ctx *Context) GetIsZQuakeEnabled() bool { + ctx.isZQuakeEnabledMu.Lock() + defer ctx.isZQuakeEnabledMu.Unlock() + return ctx.isZQuakeEnabled +} + +func (ctx *Context) SetIsZQuakeEnabled(v bool) { + ctx.isZQuakeEnabledMu.Lock() + defer ctx.isZQuakeEnabledMu.Unlock() + ctx.isZQuakeEnabled = v +} + +func (ctx *Context) GetZQuakeProtocolExtension() uint32 { + ctx.zQuakeProtocolExtensionMu.Lock() + defer ctx.zQuakeProtocolExtensionMu.Unlock() + return ctx.zQuakeProtocolExtension +} + +func (ctx *Context) SetZQuakeProtocolExtension(v uint32) { + ctx.zQuakeProtocolExtensionMu.Lock() + defer ctx.zQuakeProtocolExtensionMu.Unlock() + ctx.zQuakeProtocolExtension = v +} + +func (ctx *Context) GetIsDem() bool { + ctx.isDemMu.Lock() + defer ctx.isDemMu.Unlock() + return ctx.isDem +} + +func (ctx *Context) SetIsDem(v bool) { + ctx.isDemMu.Lock() + defer ctx.isDemMu.Unlock() + ctx.isDem = v +} + +func (ctx *Context) GetIsQWD() bool { + ctx.isQWDMu.Lock() + defer ctx.isQWDMu.Unlock() + return ctx.isQWD +} + +func (ctx *Context) SetIsQWD(v bool) { + ctx.isQWDMu.Lock() + defer ctx.isQWDMu.Unlock() + ctx.isQWD = v +} + +func (ctx *Context) GetIsMVD() bool { + ctx.isMVDMu.Lock() + defer ctx.isMVDMu.Unlock() + return ctx.isMVD +} + +func (ctx *Context) SetIsMVD(v bool) { + ctx.isMVDMu.Lock() + defer ctx.isMVDMu.Unlock() + ctx.isMVD = v +} + +func (ctx *Context) GetIsNQ() bool { + return ctx.GetIsDem() || ctx.GetProtocolVersion() == protocol.VersionNQ +} diff --git a/common/context/option.go b/common/context/option.go new file mode 100644 index 0000000..7c40cd4 --- /dev/null +++ b/common/context/option.go @@ -0,0 +1,27 @@ +package context + +type Option func(*Context) + +func WithIsDem(isDem bool) Option { + return func(ctx *Context) { + ctx.isDem = isDem + } +} + +func WithIsQWD(isQWD bool) Option { + return func(ctx *Context) { + ctx.isQWD = isQWD + } +} + +func WithIsMVD(isMVD bool) Option { + return func(ctx *Context) { + ctx.isMVD = isMVD + } +} + +func WithProtocolVersion(protocolVersion uint32) Option { + return func(ctx *Context) { + ctx.protocolVersion = protocolVersion + } +} diff --git a/common/crc/crc.go b/common/crc/crc.go new file mode 100644 index 0000000..fcd3ffd --- /dev/null +++ b/common/crc/crc.go @@ -0,0 +1,106 @@ +package crc + +var crcTable = []uint16{ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, +} + +var checksumTable = [1024]byte{ + 0x78, 0xd2, 0x94, 0xe3, 0x41, 0xec, 0xd6, 0xd5, 0xcb, 0xfc, 0xdb, 0x8a, + 0x4b, 0xcc, 0x85, 0x01, 0x23, 0xd2, 0xe5, 0xf2, 0x29, 0xa7, 0x45, 0x94, + 0x4a, 0x62, 0xe3, 0xa5, 0x6f, 0x3f, 0xe1, 0x7a, 0x64, 0xed, 0x5c, 0x99, + 0x29, 0x87, 0xa8, 0x78, 0x59, 0x0d, 0xaa, 0x0f, 0x25, 0x0a, 0x5c, 0x58, + 0xfb, 0x00, 0xa7, 0xa8, 0x8a, 0x1d, 0x86, 0x80, 0xc5, 0x1f, 0xd2, 0x28, + 0x69, 0x71, 0x58, 0xc3, 0x51, 0x90, 0xe1, 0xf8, 0x6a, 0xf3, 0x8f, 0xb0, + 0x68, 0xdf, 0x95, 0x40, 0x5c, 0xe4, 0x24, 0x6b, 0x29, 0x19, 0x71, 0x3f, + 0x42, 0x63, 0x6c, 0x48, 0xe7, 0xad, 0xa8, 0x4b, 0x91, 0x8f, 0x42, 0x36, + 0x34, 0xe7, 0x32, 0x55, 0x59, 0x2d, 0x36, 0x38, 0x38, 0x59, 0x9b, 0x08, + 0x16, 0x4d, 0x8d, 0xf8, 0x0a, 0xa4, 0x52, 0x01, 0xbb, 0x52, 0xa9, 0xfd, + 0x40, 0x18, 0x97, 0x37, 0xff, 0xc9, 0x82, 0x27, 0xb2, 0x64, 0x60, 0xce, + 0x00, 0xd9, 0x04, 0xf0, 0x9e, 0x99, 0xbd, 0xce, 0x8f, 0x90, 0x4a, 0xdd, + 0xe1, 0xec, 0x19, 0x14, 0xb1, 0xfb, 0xca, 0x1e, 0x98, 0x0f, 0xd4, 0xcb, + 0x80, 0xd6, 0x05, 0x63, 0xfd, 0xa0, 0x74, 0xa6, 0x86, 0xf6, 0x19, 0x98, + 0x76, 0x27, 0x68, 0xf7, 0xe9, 0x09, 0x9a, 0xf2, 0x2e, 0x42, 0xe1, 0xbe, + 0x64, 0x48, 0x2a, 0x74, 0x30, 0xbb, 0x07, 0xcc, 0x1f, 0xd4, 0x91, 0x9d, + 0xac, 0x55, 0x53, 0x25, 0xb9, 0x64, 0xf7, 0x58, 0x4c, 0x34, 0x16, 0xbc, + 0xf6, 0x12, 0x2b, 0x65, 0x68, 0x25, 0x2e, 0x29, 0x1f, 0xbb, 0xb9, 0xee, + 0x6d, 0x0c, 0x8e, 0xbb, 0xd2, 0x5f, 0x1d, 0x8f, 0xc1, 0x39, 0xf9, 0x8d, + 0xc0, 0x39, 0x75, 0xcf, 0x25, 0x17, 0xbe, 0x96, 0xaf, 0x98, 0x9f, 0x5f, + 0x65, 0x15, 0xc4, 0x62, 0xf8, 0x55, 0xfc, 0xab, 0x54, 0xcf, 0xdc, 0x14, + 0x06, 0xc8, 0xfc, 0x42, 0xd3, 0xf0, 0xad, 0x10, 0x08, 0xcd, 0xd4, 0x11, + 0xbb, 0xca, 0x67, 0xc6, 0x48, 0x5f, 0x9d, 0x59, 0xe3, 0xe8, 0x53, 0x67, + 0x27, 0x2d, 0x34, 0x9e, 0x9e, 0x24, 0x29, 0xdb, 0x69, 0x99, 0x86, 0xf9, + 0x20, 0xb5, 0xbb, 0x5b, 0xb0, 0xf9, 0xc3, 0x67, 0xad, 0x1c, 0x9c, 0xf7, + 0xcc, 0xef, 0xce, 0x69, 0xe0, 0x26, 0x8f, 0x79, 0xbd, 0xca, 0x10, 0x17, + 0xda, 0xa9, 0x88, 0x57, 0x9b, 0x15, 0x24, 0xba, 0x84, 0xd0, 0xeb, 0x4d, + 0x14, 0xf5, 0xfc, 0xe6, 0x51, 0x6c, 0x6f, 0x64, 0x6b, 0x73, 0xec, 0x85, + 0xf1, 0x6f, 0xe1, 0x67, 0x25, 0x10, 0x77, 0x32, 0x9e, 0x85, 0x6e, 0x69, + 0xb1, 0x83, 0x00, 0xe4, 0x13, 0xa4, 0x45, 0x34, 0x3b, 0x40, 0xff, 0x41, + 0x82, 0x89, 0x79, 0x57, 0xfd, 0xd2, 0x8e, 0xe8, 0xfc, 0x1d, 0x19, 0x21, + 0x12, 0x00, 0xd7, 0x66, 0xe5, 0xc7, 0x10, 0x1d, 0xcb, 0x75, 0xe8, 0xfa, + 0xb6, 0xee, 0x7b, 0x2f, 0x1a, 0x25, 0x24, 0xb9, 0x9f, 0x1d, 0x78, 0xfb, + 0x84, 0xd0, 0x17, 0x05, 0x71, 0xb3, 0xc8, 0x18, 0xff, 0x62, 0xee, 0xed, + 0x53, 0xab, 0x78, 0xd3, 0x65, 0x2d, 0xbb, 0xc7, 0xc1, 0xe7, 0x70, 0xa2, + 0x43, 0x2c, 0x7c, 0xc7, 0x16, 0x04, 0xd2, 0x45, 0xd5, 0x6b, 0x6c, 0x7a, + 0x5e, 0xa1, 0x50, 0x2e, 0x31, 0x5b, 0xcc, 0xe8, 0x65, 0x8b, 0x16, 0x85, + 0xbf, 0x82, 0x83, 0xfb, 0xde, 0x9f, 0x36, 0x48, 0x32, 0x79, 0xd6, 0x9b, + 0xfb, 0x52, 0x45, 0xbf, 0x43, 0xf7, 0x0b, 0x0b, 0x19, 0x19, 0x31, 0xc3, + 0x85, 0xec, 0x1d, 0x8c, 0x20, 0xf0, 0x3a, 0xfa, 0x80, 0x4d, 0x2c, 0x7d, + 0xac, 0x60, 0x09, 0xc0, 0x40, 0xee, 0xb9, 0xeb, 0x13, 0x5b, 0xe8, 0x2b, + 0xb1, 0x20, 0xf0, 0xce, 0x4c, 0xbd, 0xc6, 0x04, 0x86, 0x70, 0xc6, 0x33, + 0xc3, 0x15, 0x0f, 0x65, 0x19, 0xfd, 0xc2, 0xd3, +} + +func Byte(base []byte, seq int) byte { + var buf [60 + 4]byte + + l := len(base) + if l > 60 { + l = 60 + } + copy(buf[:], base[:l]) + + i := (uint32(seq) % uint32(len(checksumTable)-4)) + buf[l+0] = byte(seq&0xff) ^ checksumTable[i+0] + buf[l+1] = checksumTable[i+1] + buf[l+2] = byte((seq>>8)&0xff) ^ checksumTable[i+2] + buf[l+3] = checksumTable[i+3] + + var crc uint16 = 0xffff + for _, b := range buf[:l+4] { + crc = (crc << 8) ^ crcTable[(crc>>8)^uint16(b)] + + } + + return byte(crc & 0xff) +} diff --git a/common/crc/crc_test.go b/common/crc/crc_test.go new file mode 100644 index 0000000..74e77cd --- /dev/null +++ b/common/crc/crc_test.go @@ -0,0 +1,48 @@ +package crc + +import ( + "testing" +) + +type crcTest struct { + name string + input []byte + seq int + expected byte +} + +var crcTests = []crcTest{ + { + name: "sequence 0", + input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + seq: 0, + expected: 0x2f, + }, + { + name: "sequence 1", + input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + seq: 1, + expected: 0x2d, + }, + { + name: "sequence 2", + input: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + seq: 2, + expected: 0x37, + }, +} + +func TestCRCs(t *testing.T) { + for _, ct := range crcTests { + t.Run(ct.name, func(t *testing.T) { + crc := Byte(ct.input, ct.seq) + + if crc != ct.expected { + t.Errorf("crc byte didn't match expected") + t.Logf("output: %#v", crc) + t.Logf("expected: %#v", ct.expected) + } + + }) + } +} diff --git a/common/death/death.go b/common/death/death.go new file mode 100644 index 0000000..c71f862 --- /dev/null +++ b/common/death/death.go @@ -0,0 +1,132 @@ +package death + +import ( + "fmt" + "sort" + "strings" +) + +type Weapon string + +const ( + Axe Weapon = "axe" + Discharge Weapon = "discharge" + Drown Weapon = "drown" + Fall Weapon = "fall" + GrenadeLauncher Weapon = "gl" + Lava Weapon = "lava" + LightningGun Weapon = "lg" + Nailgun Weapon = "ng" + NoWeapon Weapon = "no weapon" + QLightningGun Weapon = "quad lg" + QRocketLauncher Weapon = "quad rl" + QShotgun Weapon = "quad sg" + QSuperNailgun Weapon = "quad sng" + QSuperShotgun Weapon = "quad ssg" + RocketLauncher Weapon = "rl" + Shotgun Weapon = "sg" + Slime Weapon = "slime" + Squish Weapon = "squish" + Stomp Weapon = "stomp" + SuperNailgun Weapon = "sng" + SuperShotgun Weapon = "ssg" + TeamKill Weapon = "tk" + Telefrag Weapon = "telefrag" + Trap Weapon = "trap" +) + +type Type uint8 + +const ( + Unknown Type = iota + Death + Suicide + Kill + TeamKillUnknownKiller + TeamKillUnknownVictim +) + +func (t Type) String() string { + names := []string{ + "unknown", + "death", + "suicide", + "kill", + "teamkill", + "teamkill", + } + + if t < 0 || int(t) >= len(names) { + return "type(?)" + } + + return names[t] +} + +type Obituary struct { + Type Type + Killer string + Victim string + Weapon Weapon +} + +func (o Obituary) String() string { + hasKiller := o.Killer != "" + hasVictim := o.Victim != "" + + switch { + case hasKiller && hasVictim: + return fmt.Sprintf("%s %s %s %s", o.Type, o.Killer, o.Weapon, o.Victim) + case hasKiller: + return fmt.Sprintf("%s %s %s", o.Type, o.Killer, o.Weapon) + default: + return fmt.Sprintf("%s %s %s", o.Type, o.Victim, o.Weapon) + } +} + +type Parser struct { + templates []template +} + +func init() { + sort.SliceStable(templates, func(i, j int) bool { + return templates[i].priority > templates[j].priority + }) +} + +func Parse(input string) (*Obituary, bool) { + s := strings.TrimSpace(input) + + for _, t := range templates { + x, y, ok := t.parser.parse(s) + if !ok { + continue + } + + ob := &Obituary{Weapon: t.weapon, Type: toObituaryType(t.typ)} + + switch t.typ { + case PlayerDeath: + ob.Victim = x + case PlayerSuicide: + ob.Victim = x + case XFraggedByY: + ob.Victim = x + ob.Killer = y + case XFragsY: + ob.Killer = x + ob.Victim = y + case XTeamKilled: + ob.Victim = x + case XTeamKills: + ob.Killer = x + default: + ob.Victim = x + ob.Killer = y + } + + return ob, true + } + + return nil, false +} diff --git a/common/death/parser.go b/common/death/parser.go new file mode 100644 index 0000000..e86cffb --- /dev/null +++ b/common/death/parser.go @@ -0,0 +1,55 @@ +package death + +import "strings" + +type parser interface { + parse(msg string) (string, string, bool) +} + +type suffixParser struct { + suffix string +} + +func (m suffixParser) parse(msg string) (x, y string, ok bool) { + if !strings.HasSuffix(msg, m.suffix) { + return "", "", false + } + + x = strings.TrimSpace(strings.TrimSuffix(msg, m.suffix)) + if x == "" { + return "", "", false + } + + return x, "", true +} + +type infixParser struct { + sep string + suffix string +} + +func (m infixParser) parse(msg string) (x, y string, ok bool) { + before, after, found := strings.Cut(msg, m.sep) + if !found { + return "", "", false + } + + x = strings.TrimSpace(before) + if x == "" { + return "", "", false + } + + if m.suffix != "" { + if !strings.HasSuffix(after, m.suffix) { + return "", "", false + } + after = strings.TrimSuffix(after, m.suffix) + } + + y = strings.TrimSpace(after) + if y == "" { + return "", "", false + } + + return x, y, true +} diff --git a/common/death/template.go b/common/death/template.go new file mode 100644 index 0000000..3f77c5b --- /dev/null +++ b/common/death/template.go @@ -0,0 +1,155 @@ +package death + +import "strings" + +type templateType uint8 + +const ( + PlayerDeath templateType = iota + PlayerSuicide + XFraggedByY + XFragsY + XTeamKilled + XTeamKills +) + +func toObituaryType(t templateType) Type { + switch t { + case PlayerDeath: + return Death + case PlayerSuicide: + return Suicide + case XFraggedByY, XFragsY: + return Kill + case XTeamKilled: + return TeamKillUnknownKiller + case XTeamKills: + return TeamKillUnknownVictim + default: + return Unknown + } +} + +type template struct { + parser parser + parts []string + priority int + typ templateType + weapon Weapon +} + +func newTemplate(typ templateType, weapon Weapon, parts ...string) template { + prio := 0 + for _, pr := range parts { + prio += len(pr) + } + + var pa parser + switch typ { + case XFraggedByY, XFragsY: + pa = infixParser{ + sep: parts[0], + suffix: strings.Join(parts[1:], ""), + } + default: + pa = suffixParser{ + suffix: strings.Join(parts, ""), + } + } + + return template{ + parser: pa, + parts: parts, + priority: prio, + typ: typ, + weapon: weapon, + } +} + +var templates = []template{ + newTemplate(PlayerDeath, Drown, " sleeps with the fishes"), + newTemplate(PlayerDeath, Drown, " sucks it down"), + newTemplate(PlayerDeath, Fall, " cratered"), + newTemplate(PlayerDeath, Fall, " fell to her death"), + newTemplate(PlayerDeath, Fall, " fell to his death"), + newTemplate(PlayerDeath, Lava, " burst into flames"), + newTemplate(PlayerDeath, Lava, " turned into hot slag"), + newTemplate(PlayerDeath, Lava, " visits the Volcano God"), + newTemplate(PlayerDeath, NoWeapon, " died"), + newTemplate(PlayerDeath, NoWeapon, " tried to leave"), + newTemplate(PlayerDeath, Slime, " can't exist on slime alone"), + newTemplate(PlayerDeath, Slime, " gulped a load of slime"), + newTemplate(PlayerDeath, Squish, " was squished"), + newTemplate(PlayerDeath, Trap, " ate a lavaball"), + newTemplate(PlayerDeath, Trap, " blew up"), + newTemplate(PlayerDeath, Trap, " was spiked"), + newTemplate(PlayerDeath, Trap, " was zapped"), + newTemplate(PlayerSuicide, Discharge, " discharges into the lava"), + newTemplate(PlayerSuicide, Discharge, " discharges into the slime"), + newTemplate(PlayerSuicide, Discharge, " discharges into the water"), + newTemplate(PlayerSuicide, Discharge, " electrocutes herself"), + newTemplate(PlayerSuicide, Discharge, " electrocutes himself"), + newTemplate(PlayerSuicide, Discharge, " heats up the water"), + newTemplate(PlayerSuicide, Discharge, " railcutes herself"), + newTemplate(PlayerSuicide, Discharge, " railcutes himself"), + newTemplate(PlayerSuicide, GrenadeLauncher, " tries to put the pin back in"), + newTemplate(PlayerSuicide, NoWeapon, " suicides"), + newTemplate(PlayerSuicide, RocketLauncher, " becomes bored with life"), + newTemplate(PlayerSuicide, RocketLauncher, " discovers blast radius"), + newTemplate(XFraggedByY, "Axe", " was ax-murdered by "), + newTemplate(XFraggedByY, "Axe", " was axed to pieces by "), + newTemplate(XFraggedByY, QShotgun, " was lead poisoned by "), + newTemplate(XFraggedByY, QSuperNailgun, " was straw-cuttered by "), + newTemplate(XFraggedByY, Discharge, " accepts ", "' discharge"), + newTemplate(XFraggedByY, Discharge, " accepts ", "'s discharge"), + newTemplate(XFraggedByY, Discharge, " drains ", "' batteries"), + newTemplate(XFraggedByY, Discharge, " drains ", "'s batteries"), + newTemplate(XFraggedByY, GrenadeLauncher, " eats ", "' pineapple"), + newTemplate(XFraggedByY, GrenadeLauncher, " eats ", "'s pineapple"), + newTemplate(XFraggedByY, GrenadeLauncher, " was gibbed by ", "' grenade"), + newTemplate(XFraggedByY, GrenadeLauncher, " was gibbed by ", "'s grenade"), + newTemplate(XFraggedByY, LightningGun, " accepts ", "' shaft"), + newTemplate(XFraggedByY, LightningGun, " accepts ", "'s shaft"), + newTemplate(XFraggedByY, Nailgun, " was body pierced by "), + newTemplate(XFraggedByY, Nailgun, " was nailed by "), + newTemplate(XFraggedByY, QLightningGun, " gets a natural disaster from "), + newTemplate(XFraggedByY, QRocketLauncher, " was brutalized by ", "' quad rocket"), + newTemplate(XFraggedByY, QRocketLauncher, " was brutalized by ", "'s quad rocket"), + newTemplate(XFraggedByY, QRocketLauncher, " was smeared by ", "' quad rocket"), + newTemplate(XFraggedByY, QRocketLauncher, " was smeared by ", "'s quad rocket"), + newTemplate(XFraggedByY, QSuperShotgun, " ate 8 loads of ", "' buckshot"), + newTemplate(XFraggedByY, QSuperShotgun, " ate 8 loads of ", "'s buckshot"), + newTemplate(XFraggedByY, RocketLauncher, " rides ", "' rocket"), + newTemplate(XFraggedByY, RocketLauncher, " rides ", "'s rocket"), + newTemplate(XFraggedByY, RocketLauncher, " was gibbed by ", "' rocket"), + newTemplate(XFraggedByY, RocketLauncher, " was gibbed by ", "'s rocket"), + newTemplate(XFraggedByY, Shotgun, " chewed on ", "' boomstick"), + newTemplate(XFraggedByY, Shotgun, " chewed on ", "'s boomstick"), + newTemplate(XFraggedByY, Stomp, " softens ", "' fall"), + newTemplate(XFraggedByY, Stomp, " softens ", "'s fall"), + newTemplate(XFraggedByY, Stomp, " tried to catch "), + newTemplate(XFraggedByY, Stomp, " was crushed by "), + newTemplate(XFraggedByY, Stomp, " was jumped by "), + newTemplate(XFraggedByY, Stomp, " was literally stomped into particles by "), + newTemplate(XFraggedByY, SuperNailgun, " was perforated by "), + newTemplate(XFraggedByY, SuperNailgun, " was punctured by "), + newTemplate(XFraggedByY, SuperNailgun, " was ventilated by "), + newTemplate(XFraggedByY, SuperShotgun, " ate 2 loads of ", "' buckshot"), + newTemplate(XFraggedByY, SuperShotgun, " ate 2 loads of ", "'s buckshot"), + newTemplate(XFraggedByY, Telefrag, " was telefragged by "), + newTemplate(XFragsY, QRocketLauncher, " rips ", " a new one"), + newTemplate(XFragsY, Squish, " squishes "), + newTemplate(XFragsY, Stomp, " stomps "), + newTemplate(XTeamKilled, Stomp, " was crushed by her teammate"), + newTemplate(XTeamKilled, Stomp, " was crushed by his teammate"), + newTemplate(XTeamKilled, Stomp, " was jumped by her teammate"), + newTemplate(XTeamKilled, Stomp, " was jumped by his teammate"), + newTemplate(XTeamKilled, Telefrag, " was telefragged by her teammate"), + newTemplate(XTeamKilled, Telefrag, " was telefragged by his teammate"), + newTemplate(XTeamKills, Squish, " squished a teammate"), + newTemplate(XTeamKills, TeamKill, " checks her glasses"), + newTemplate(XTeamKills, TeamKill, " checks his glasses"), + newTemplate(XTeamKills, TeamKill, " gets a frag for the other team"), + newTemplate(XTeamKills, TeamKill, " loses another friend"), + newTemplate(XTeamKills, TeamKill, " mows down a teammate"), +} diff --git a/common/infostring/infostring.go b/common/infostring/infostring.go new file mode 100644 index 0000000..114058a --- /dev/null +++ b/common/infostring/infostring.go @@ -0,0 +1,72 @@ +package infostring + +import ( + "strings" + + "github.com/osm/quake/common/buffer" +) + +type InfoString struct { + Info []Info +} + +type Info struct { + Key string + Value string +} + +func New(opts ...Option) *InfoString { + var infoString InfoString + + for _, opt := range opts { + opt(&infoString) + } + + return &infoString +} + +func (is *InfoString) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(byte('"')) + + for i := 0; i < len(is.Info); i++ { + buf.PutBytes([]byte("\\" + is.Info[i].Key)) + buf.PutBytes([]byte("\\" + is.Info[i].Value)) + } + + buf.PutByte(byte('"')) + + return buf.Bytes() +} + +func Parse(input string) *InfoString { + var ret InfoString + + trimmed := strings.Trim(input, "\"") + parts := strings.Split(trimmed, "\\") + + for i := 1; i < len(parts)-1; i += 2 { + ret.Info = append(ret.Info, Info{parts[i], parts[i+1]}) + } + + return &ret +} + +func (is *InfoString) Get(key string) string { + for i := 0; i < len(is.Info); i++ { + if is.Info[i].Key == key { + return is.Info[i].Value + } + } + + return "" +} + +func (is *InfoString) Set(key, value string) { + for i := 0; i < len(is.Info); i++ { + if is.Info[i].Key == key { + is.Info[i].Value = value + } + } +} diff --git a/common/infostring/infostring_test.go b/common/infostring/infostring_test.go new file mode 100644 index 0000000..977090d --- /dev/null +++ b/common/infostring/infostring_test.go @@ -0,0 +1,49 @@ +package infostring + +import ( + "reflect" + "testing" +) + +type infoStringTest struct { + name string + input string + expected InfoString +} + +var infoStringTests = []infoStringTest{ + { + name: "foo", + input: "\"\\FOO\\foo\\BAR\\bar\\BAZ\\baz\"", + expected: InfoString{ + Info: []Info{ + Info{Key: "FOO", Value: "foo"}, + Info{Key: "BAR", Value: "bar"}, + Info{Key: "BAZ", Value: "baz"}, + }, + }, + }, + { + name: "\\foo\\with spaces", + input: "\"\\foo\\with spaces\"", + expected: InfoString{ + Info: []Info{ + Info{Key: "foo", Value: "with spaces"}, + }, + }, + }, +} + +func TestInfoString(t *testing.T) { + for _, is := range infoStringTests { + t.Run(is.name, func(t *testing.T) { + infoString := Parse(is.input) + + if !reflect.DeepEqual(is.expected.Bytes(), infoString.Bytes()) { + t.Errorf("parsed infostring output didn't match") + t.Logf("output: %#v\n", infoString) + t.Logf("expected: %#v\n", is.expected) + } + }) + } +} diff --git a/common/infostring/option.go b/common/infostring/option.go new file mode 100644 index 0000000..f64fefa --- /dev/null +++ b/common/infostring/option.go @@ -0,0 +1,9 @@ +package infostring + +type Option func(*InfoString) + +func WithKeyValue(key, value string) Option { + return func(is *InfoString) { + is.Info = append(is.Info, Info{key, value}) + } +} diff --git a/common/loc/loc.go b/common/loc/loc.go new file mode 100644 index 0000000..c82c816 --- /dev/null +++ b/common/loc/loc.go @@ -0,0 +1,82 @@ +package loc + +import ( + "fmt" + "strings" +) + +type Locations struct { + locations []Location +} + +type Location struct { + Coord [3]float32 + Name string +} + +func Parse(data []byte) (*Locations, error) { + var locs []Location + + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if len(line) == 0 { + continue + } + + var x, y, z float64 + var name string + _, err := fmt.Sscanf(line, "%f %f %f %s", &x, &y, &z, &name) + if err != nil { + return nil, fmt.Errorf("error parsing line '%v'", err) + } + + locs = append(locs, Location{ + Coord: [3]float32{ + float32(x / 8.0), + float32(y / 8.0), + float32(z / 8.0), + }, + Name: trim(name), + }) + } + + return &Locations{locs}, nil +} + +func trim(input string) string { + m := map[string]string{ + "$loc_name_ga": "ga", + "$loc_name_mh": "mh", + "$loc_name_pent": "pent", + "$loc_name_quad": "quad", + "$loc_name_ra": "ra", + "$loc_name_ring": "ring", + "$loc_name_separator": "-", + "$loc_name_ya": "ya", + } + + for x, y := range m { + input = strings.Replace(input, x, y, -1) + } + + return input +} + +func (l *Locations) Get(coord [3]float32) *Location { + var loc *Location + var min float32 + + for _, n := range l.locations { + x := coord[0] - n.Coord[0] + y := coord[1] - n.Coord[1] + z := coord[2] - n.Coord[2] + d := x*x + y*y + z*z + + if loc == nil || d < min { + loc = &n + min = d + } + } + + return loc +} diff --git a/common/loc/loc_test.go b/common/loc/loc_test.go new file mode 100644 index 0000000..5b68cc8 --- /dev/null +++ b/common/loc/loc_test.go @@ -0,0 +1,24 @@ +package loc + +import ( + "testing" +) + +func TestLoc(t *testing.T) { + dm3Loc := []byte(`-7040 -1856 -128 sng-mega +1536 -1664 -1408 ra-tunnel +11776 -7424 -192 ya +12160 3456 -704 rl +-5056 -5440 -128 sng-ra +4096 6144 1728 lifts`) + + loc, err := Parse(dm3Loc) + if err != nil { + t.Errorf("error when parsing loc data, %v", err) + } + + l := loc.Get([3]float32{1300, -700, -24}) + if l.Name != "ya" { + t.Errorf("expected to be at ya, got %v", l.Name) + } +} diff --git a/common/rand/rand.go b/common/rand/rand.go new file mode 100644 index 0000000..79f9efb --- /dev/null +++ b/common/rand/rand.go @@ -0,0 +1,7 @@ +package rand + +import "math/rand" + +func Uint16() uint16 { + return uint16(rand.Uint32() & 0xffff) +} diff --git a/common/sequencer/option.go b/common/sequencer/option.go new file mode 100644 index 0000000..1b3f613 --- /dev/null +++ b/common/sequencer/option.go @@ -0,0 +1,21 @@ +package sequencer + +type Option func(*Sequencer) + +func WithIncomingSeq(incomingSeq uint32) Option { + return func(s *Sequencer) { + s.incomingSeq = incomingSeq + } +} + +func WithOutgoingSeq(outgoingSeq uint32) Option { + return func(s *Sequencer) { + s.outgoingSeq = outgoingSeq + } +} + +func WithPing(ping int16) Option { + return func(s *Sequencer) { + s.ping = ping + } +} diff --git a/common/sequencer/sequencer.go b/common/sequencer/sequencer.go new file mode 100644 index 0000000..fe83580 --- /dev/null +++ b/common/sequencer/sequencer.go @@ -0,0 +1,145 @@ +package sequencer + +import ( + "errors" + "time" + + "github.com/osm/quake/packet/command" +) + +var ErrRateLimit = errors.New("rate limit reached") + +type State byte + +const ( + Disconnected State = 0 + Handshake State = 1 + Connecting State = 2 + Connected State = 3 +) + +type Sequencer struct { + ping int16 + lastWrite time.Time + + state State + + incomingSeq uint32 + incomingAck uint32 + lastReliableSeq uint32 + outgoingSeq uint32 + + isIncomingAckReliable bool + isOutgoingSeqReliable bool + isIncomingSeqReliable bool + + outgoingCommands []command.Command + outgoingCommandsBuf []command.Command +} + +func New(opts ...Option) *Sequencer { + s := Sequencer{ + ping: 999, + } + + for _, opt := range opts { + opt(&s) + } + + return &s +} + +func (s *Sequencer) SetState(state State) { s.state = state } +func (s *Sequencer) GetState() State { return s.state } +func (s *Sequencer) SetPing(ping int16) { s.ping = ping } +func (s *Sequencer) GetPing() int16 { return s.ping } + +func (s *Sequencer) Reset() { + s.incomingSeq = 0 + s.incomingAck = 0 + s.lastReliableSeq = 0 + s.outgoingSeq = 0 + + s.isIncomingAckReliable = false + s.isOutgoingSeqReliable = false + s.isIncomingSeqReliable = false + + s.outgoingCommands = []command.Command{} + s.outgoingCommandsBuf = []command.Command{} +} + +func (s *Sequencer) Process( + incomingSeq, incomingAck uint32, + cmds []command.Command, +) (uint32, uint32, []command.Command, error) { + s.incoming(incomingSeq, incomingAck) + return s.outgoing(cmds) +} + +func (s *Sequencer) incoming(incomingSeq, incomingAck uint32) { + isIncomingSeqReliable := incomingSeq>>31 == 1 + isIncomingAckReliable := incomingAck>>31 == 1 + + incomingSeq = incomingSeq & 0x7fffffff + incomingAck = incomingAck & 0x7fffffff + + if incomingSeq < s.incomingSeq { + return + } + + if isIncomingAckReliable == s.isOutgoingSeqReliable { + s.outgoingCommandsBuf = []command.Command{} + } + + if isIncomingSeqReliable { + s.isIncomingSeqReliable = !s.isIncomingSeqReliable + } + + s.incomingSeq = incomingSeq + s.incomingAck = incomingAck + s.isIncomingAckReliable = isIncomingAckReliable +} + +func (s *Sequencer) outgoing(cmds []command.Command) (uint32, uint32, []command.Command, error) { + s.outgoingCommands = append(s.outgoingCommands, cmds...) + + if s.state == Connected && time.Since(s.lastWrite).Milliseconds() < int64(s.ping) { + return 0, 0, nil, ErrRateLimit + } + + var isReliable bool + + if s.incomingAck > s.lastReliableSeq && + s.isIncomingAckReliable != s.isOutgoingSeqReliable { + isReliable = true + } + + if len(s.outgoingCommandsBuf) == 0 && len(s.outgoingCommands) > 0 { + s.outgoingCommandsBuf = s.outgoingCommands + s.isOutgoingSeqReliable = !s.isOutgoingSeqReliable + isReliable = true + s.outgoingCommands = []command.Command{} + } + + outgoingSeq := s.outgoingSeq + if isReliable { + outgoingSeq = s.outgoingSeq | (1 << 31) + } + + outgoingAck := s.incomingSeq + if s.isIncomingSeqReliable { + outgoingAck = s.incomingSeq | (1 << 31) + } + + outgoingCmds := []command.Command{} + + s.outgoingSeq++ + + if isReliable { + outgoingCmds = append(outgoingCmds, s.outgoingCommandsBuf...) + s.lastReliableSeq = s.outgoingSeq + } + + s.lastWrite = time.Now() + return outgoingSeq, outgoingAck, outgoingCmds, nil +} diff --git a/demo/dem/data.go b/demo/dem/data.go new file mode 100644 index 0000000..87ff90a --- /dev/null +++ b/demo/dem/data.go @@ -0,0 +1,54 @@ +package dem + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet" + "github.com/osm/quake/packet/svc" +) + +type Data struct { + Size uint32 + Angle [3]float32 + Packet packet.Packet +} + +func (d *Data) Bytes() []byte { + buf := buffer.New() + + buf.PutUint32(d.Size) + + for i := 0; i < 3; i++ { + buf.PutFloat32(d.Angle[i]) + } + + buf.PutBytes(d.Packet.Bytes()) + + return buf.Bytes() +} + +func parseData(ctx *context.Context, buf *buffer.Buffer) (*Data, error) { + var err error + var data Data + + if data.Size, err = buf.GetUint32(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if data.Angle[i], err = buf.GetFloat32(); err != nil { + return nil, err + } + } + + bytes, err := buf.GetBytes(int(data.Size)) + if err != nil { + return nil, err + } + + if data.Packet, err = svc.Parse(ctx, bytes); err != nil { + return nil, err + } + + return &data, nil +} diff --git a/demo/dem/parse.go b/demo/dem/parse.go new file mode 100644 index 0000000..203a711 --- /dev/null +++ b/demo/dem/parse.go @@ -0,0 +1,57 @@ +package dem + +import ( + "errors" + + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +var ErrUnknownType = errors.New("unknown type") + +type Demo struct { + CDTrack []byte + Data []*Data +} + +func (dem *Demo) Bytes() []byte { + buf := buffer.New() + + buf.PutBytes(dem.CDTrack) + + for _, d := range dem.Data { + buf.PutBytes(d.Bytes()) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, data []byte) (*Demo, error) { + var demo Demo + + buf := buffer.New(buffer.WithData(data)) + ctx.SetIsDem(true) + + for buf.Off() < buf.Len() { + b, err := buf.ReadByte() + if err != nil { + return nil, err + } + + demo.CDTrack = append(demo.CDTrack, b) + if b == '\n' { + break + } + } + + for buf.Off() < buf.Len() { + d, err := parseData(ctx, buf) + if err != nil { + return nil, err + } + + demo.Data = append(demo.Data, d) + } + + return &demo, nil +} diff --git a/demo/dem/parse_test.go b/demo/dem/parse_test.go new file mode 100644 index 0000000..d3d33cf --- /dev/null +++ b/demo/dem/parse_test.go @@ -0,0 +1,55 @@ +package dem + +import ( + "crypto/sha256" + "fmt" + "io/ioutil" + "testing" + + "github.com/osm/quake/common/context" +) + +type demTest struct { + filePath string + checksum string +} + +var demTests = []demTest{ + { + filePath: "testdata/demo1.dem", + checksum: "893e5279e3a84fed0416a1101dfc2b6bca1ebd28787323daa8e22cefe54883c1", + }, + { + filePath: "testdata/demo2.dem", + checksum: "150e6fc54c24a70a44012ba71473a5cbbec081e19204a9eae98249621fef00d7", + }, + { + filePath: "testdata/demo3.dem", + checksum: "7bee6edc47fe563cbf8f1e873d524d1e4cef7d919b523606e200c0d7c269bd83", + }, +} + +func TestParse(t *testing.T) { + for _, dt := range demTests { + t.Run(dt.filePath, func(t *testing.T) { + data, err := ioutil.ReadFile(dt.filePath) + if err != nil { + t.Errorf("unable to open demo file, %v", err) + } + + demo, err := Parse(context.New(), data) + if err != nil { + t.Errorf("unable to parse demo, %v", err) + } + + h := sha256.New() + h.Write(demo.Bytes()) + checksum := fmt.Sprintf("%x", h.Sum(nil)) + if checksum != dt.checksum { + t.Errorf("sha256 checksums didn't match") + t.Logf("output: %#v", checksum) + t.Logf("expected: %#v", dt.checksum) + } + }) + } +} diff --git a/demo/dem/testdata/demo1.dem b/demo/dem/testdata/demo1.dem Binary files differnew file mode 100644 index 0000000..2b821c3 --- /dev/null +++ b/demo/dem/testdata/demo1.dem diff --git a/demo/dem/testdata/demo2.dem b/demo/dem/testdata/demo2.dem Binary files differnew file mode 100644 index 0000000..9dd006e --- /dev/null +++ b/demo/dem/testdata/demo2.dem diff --git a/demo/dem/testdata/demo3.dem b/demo/dem/testdata/demo3.dem Binary files differnew file mode 100644 index 0000000..d25c402 --- /dev/null +++ b/demo/dem/testdata/demo3.dem diff --git a/demo/mvd/cmd.go b/demo/mvd/cmd.go new file mode 100644 index 0000000..d89f492 --- /dev/null +++ b/demo/mvd/cmd.go @@ -0,0 +1,93 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Cmd struct { + Msec byte + UserAngle [3]float32 + Forward uint16 + Side uint16 + Up uint16 + Buttons byte + Impulse byte + Padding [3]byte + Angle [3]float32 +} + +func (cmd *Cmd) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(cmd.Msec) + + for i := 0; i < 3; i++ { + buf.PutFloat32(cmd.UserAngle[i]) + } + + buf.PutUint16(cmd.Forward) + buf.PutUint16(cmd.Side) + buf.PutUint16(cmd.Up) + buf.PutByte(cmd.Buttons) + buf.PutByte(cmd.Impulse) + + for i := 0; i < 3; i++ { + buf.PutByte(cmd.Padding[i]) + } + + for i := 0; i < 3; i++ { + buf.PutFloat32(cmd.Angle[i]) + } + + return buf.Bytes() +} + +func parseCmd(ctx *context.Context, buf *buffer.Buffer) (*Cmd, error) { + var err error + var cmd Cmd + + if cmd.Msec, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.UserAngle[i], err = buf.GetFloat32(); err != nil { + return nil, err + } + } + + if cmd.Forward, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Side, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Up, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Buttons, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Impulse, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Padding[i], err = buf.ReadByte(); err != nil { + return nil, err + } + } + + for i := 0; i < 3; i++ { + if cmd.Angle[i], err = buf.GetFloat32(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/demo/mvd/damagedone.go b/demo/mvd/damagedone.go new file mode 100644 index 0000000..cf9012a --- /dev/null +++ b/demo/mvd/damagedone.go @@ -0,0 +1,63 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type DamageDone struct { + size uint32 + + Data []byte + DeathType uint16 + AttackerEnt uint16 + TargetEnt uint16 + Damage uint16 +} + +func (cmd *DamageDone) Bytes() []byte { + if cmd.size != 8 { + return cmd.Data + } + + buf := buffer.New() + buf.PutUint16(cmd.DeathType) + buf.PutUint16(cmd.AttackerEnt) + buf.PutUint16(cmd.TargetEnt) + buf.PutUint16(cmd.Damage) + + return buf.Bytes() +} + +func parseDamageDone(ctx *context.Context, buf *buffer.Buffer, size uint32) (*DamageDone, error) { + var err error + var cmd DamageDone + + cmd.size = size + if cmd.size != 8 { + if cmd.Data, err = buf.GetBytes(int(cmd.size)); err != nil { + return nil, err + } + + goto end + } + + if cmd.DeathType, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.AttackerEnt, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.TargetEnt, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Damage, err = buf.GetUint16(); err != nil { + return nil, err + } + +end: + return &cmd, nil +} diff --git a/demo/mvd/demoinfo.go b/demo/mvd/demoinfo.go new file mode 100644 index 0000000..c52a645 --- /dev/null +++ b/demo/mvd/demoinfo.go @@ -0,0 +1,35 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type DemoInfo struct { + BlockNumber uint16 + Data []byte +} + +func (cmd *DemoInfo) Bytes() []byte { + buf := buffer.New() + + buf.PutUint16(cmd.BlockNumber) + buf.PutBytes(cmd.Data) + + return buf.Bytes() +} + +func parseDemoInfo(ctx *context.Context, buf *buffer.Buffer, size uint32) (*DemoInfo, error) { + var err error + var cmd DemoInfo + + if cmd.BlockNumber, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Data, err = buf.GetBytes(int(size) - 2); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/demo/mvd/hidden.go b/demo/mvd/hidden.go new file mode 100644 index 0000000..f9b5ebf --- /dev/null +++ b/demo/mvd/hidden.go @@ -0,0 +1,71 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/mvd" +) + +type HiddenCommand struct { + Size uint32 + Type uint16 + Command command.Command +} + +func (cmd *HiddenCommand) Bytes() []byte { + buf := buffer.New() + + buf.PutUint32(cmd.Size) + buf.PutUint16(cmd.Type) + buf.PutBytes(cmd.Command.Bytes()) + + return buf.Bytes() +} + +func parseHiddenCommands(ctx *context.Context, data []byte) ([]*HiddenCommand, error) { + var err error + var cmds []*HiddenCommand + + buf := buffer.New(buffer.WithData(data)) + + for buf.Off() < buf.Len() { + var cmd HiddenCommand + + if cmd.Size, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.Type, err = buf.GetUint16(); err != nil { + return nil, err + } + + var c command.Command + switch protocol.CommandType(cmd.Type) { + case mvd.HiddenUserCommand: + c, err = parseUserCommand(ctx, buf, cmd.Size) + case mvd.HiddenUserCommandWeapon: + c, err = parseWeapon(ctx, buf, cmd.Size) + case mvd.HiddenDemoInfo: + c, err = parseDemoInfo(ctx, buf, cmd.Size) + case mvd.HiddenDamangeDone: + c, err = parseDamageDone(ctx, buf, cmd.Size) + case mvd.HiddenUserCommandWeaponServerSide: + c, err = parseWeaponServerSide(ctx, buf, cmd.Size) + case mvd.HiddenUserCommandWeaponInstruction: + c, err = parseWeaponInstruction(ctx, buf, cmd.Size) + default: + c, err = parseUnknown(ctx, buf, cmd.Size) + } + + if err != nil { + return cmds, err + } + cmd.Command = c + + cmds = append(cmds, &cmd) + } + + return cmds, nil +} diff --git a/demo/mvd/mutliple.go b/demo/mvd/mutliple.go new file mode 100644 index 0000000..6b66d37 --- /dev/null +++ b/demo/mvd/mutliple.go @@ -0,0 +1,58 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol/mvd" +) + +type Multiple struct { + LastTo uint32 + IsHiddenPacket bool + Size uint32 + HiddenCommands []*HiddenCommand +} + +func (cmd *Multiple) Bytes() []byte { + buf := buffer.New() + + buf.PutUint32(cmd.LastTo) + + if cmd.IsHiddenPacket { + buf.PutUint32(cmd.Size) + + for _, c := range cmd.HiddenCommands { + buf.PutBytes(c.Bytes()) + } + } + + return buf.Bytes() +} + +func parseMultiple(ctx *context.Context, buf *buffer.Buffer) (*Multiple, error) { + var err error + var cmd Multiple + + if cmd.LastTo, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.LastTo == 0 && ctx.GetMVDProtocolExtension()&mvd.ExtensionHiddenMessages != 0 { + cmd.IsHiddenPacket = true + + if cmd.Size, err = buf.GetUint32(); err != nil { + return nil, err + } + + bytes, err := buf.GetBytes(int(cmd.Size)) + if err != nil { + return nil, err + } + + if cmd.HiddenCommands, err = parseHiddenCommands(ctx, bytes); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/demo/mvd/parse.go b/demo/mvd/parse.go new file mode 100644 index 0000000..20ea811 --- /dev/null +++ b/demo/mvd/parse.go @@ -0,0 +1,133 @@ +package mvd + +import ( + "errors" + + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/mvd" +) + +var ErrUnknownType = errors.New("unknown type") + +type Demo struct { + Data []Data +} + +type Data struct { + Target uint32 + Timestamp byte + Command byte + Cmd *Cmd + Read *Read + Set *Set + Multiple *Multiple +} + +func (d *Demo) Bytes() []byte { + buf := buffer.New() + + for i := 0; i < len(d.Data); i++ { + buf.PutBytes(d.Data[i].Bytes()) + } + + return buf.Bytes() +} + +func (d *Data) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(d.Timestamp) + buf.PutByte(d.Command) + + switch d.Command & 0x7 { + case mvd.DemoMultiple: + buf.PutBytes(d.Multiple.Bytes()) + fallthrough + case mvd.DemoStats: + fallthrough + case mvd.DemoSingle: + fallthrough + case mvd.DemoAll: + fallthrough + case protocol.DemoRead: + buf.PutBytes(d.Read.Bytes()) + case protocol.DemoSet: + buf.PutBytes(d.Set.Bytes()) + case protocol.DemoCmd: + buf.PutBytes(d.Cmd.Bytes()) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, data []byte) (*Demo, error) { + var err error + var cmd Demo + + buf := buffer.New(buffer.WithData(data)) + ctx.SetIsMVD(true) + + for buf.Off() < buf.Len() { + var data Data + + process: + if data.Timestamp, err = buf.ReadByte(); err != nil { + return nil, err + } + + if data.Command, err = buf.ReadByte(); err != nil { + return nil, err + } + + switch data.Command & 0x7 { + case mvd.DemoMultiple: + if data.Multiple, err = parseMultiple(ctx, buf); err != nil { + return nil, err + } + data.Target = data.Multiple.LastTo + + if data.Multiple.IsHiddenPacket { + cmd.Data = append(cmd.Data, data) + + if buf.Off() == buf.Len() { + goto end + } + + goto process + } + + fallthrough + case mvd.DemoStats: + fallthrough + case mvd.DemoSingle: + // Target determines which client the data is intended + // to reach and can be used in conjunction with the + // updateuserinfo to determine who the client is. + data.Target = uint32(data.Command >> 3) + fallthrough + case mvd.DemoAll: + fallthrough + case protocol.DemoRead: + if data.Read, err = parseRead(ctx, buf); err != nil { + return nil, err + } + case protocol.DemoSet: + if data.Set, err = parseSet(ctx, buf); err != nil { + return nil, err + } + case protocol.DemoCmd: + if data.Cmd, err = parseCmd(ctx, buf); err != nil { + return nil, err + } + default: + return nil, ErrUnknownType + } + + cmd.Data = append(cmd.Data, data) + } + +end: + return &cmd, nil +} diff --git a/demo/mvd/parse_test.go b/demo/mvd/parse_test.go new file mode 100644 index 0000000..53a00a3 --- /dev/null +++ b/demo/mvd/parse_test.go @@ -0,0 +1,59 @@ +package mvd + +import ( + "crypto/sha256" + "fmt" + "io/ioutil" + "testing" + + "github.com/osm/quake/common/context" +) + +type mvdTest struct { + filePath string + checksum string +} + +var mvdTests = []mvdTest{ + { + filePath: "testdata/demo1.mvd", + checksum: "9489430c9513aed5b8e444df1b0d2040acb55de565c1b3e237dce51c8c103c57", + }, + { + filePath: "testdata/demo2.mvd", + checksum: "d40c3864dd3c052a8379e7e589cd045814ec462f6cf4432a2c94ec740843e30d", + }, + { + filePath: "testdata/demo3.mvd", + checksum: "3dd728aa90fdae100ade62d9b93220b7689f6ebdca6c986d3c2b1208b4e1e33c", + }, + { + filePath: "testdata/demo4.mvd", + checksum: "1f7b2c0ad77608f431028c8e68904400fe406150875a51f793f3395bf3784c90", + }, +} + +func TestParse(t *testing.T) { + for _, mt := range mvdTests { + t.Run(mt.filePath, func(t *testing.T) { + data, err := ioutil.ReadFile(mt.filePath) + if err != nil { + t.Errorf("unable to open demo file, %v", err) + } + + demo, err := Parse(context.New(), data) + if err != nil { + t.Errorf("unable to parse demo, %v", err) + } + + h := sha256.New() + h.Write(demo.Bytes()) + checksum := fmt.Sprintf("%x", h.Sum(nil)) + if checksum != mt.checksum { + t.Errorf("sha256 checksums didn't match") + t.Logf("output: %#v", checksum) + t.Logf("expected: %#v", mt.checksum) + } + }) + } +} diff --git a/demo/mvd/read.go b/demo/mvd/read.go new file mode 100644 index 0000000..20dae8a --- /dev/null +++ b/demo/mvd/read.go @@ -0,0 +1,46 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet" + "github.com/osm/quake/packet/svc" +) + +type Read struct { + Size uint32 + Packet packet.Packet +} + +func (cmd *Read) Bytes() []byte { + if cmd == nil || cmd.Packet == nil { + return []byte{} + } + + buf := buffer.New() + + buf.PutUint32(cmd.Size) + buf.PutBytes(cmd.Packet.Bytes()) + + return buf.Bytes() +} + +func parseRead(ctx *context.Context, buf *buffer.Buffer) (*Read, error) { + var err error + var cmd Read + + if cmd.Size, err = buf.GetUint32(); err != nil { + return nil, err + } + + bytes, err := buf.GetBytes(int(cmd.Size)) + if err != nil { + return nil, err + } + + if cmd.Packet, err = svc.Parse(ctx, bytes); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/demo/mvd/set.go b/demo/mvd/set.go new file mode 100644 index 0000000..8cc4c95 --- /dev/null +++ b/demo/mvd/set.go @@ -0,0 +1,35 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Set struct { + SeqOut uint32 + SeqIn uint32 +} + +func (cmd *Set) Bytes() []byte { + buf := buffer.New() + + buf.PutUint32(cmd.SeqOut) + buf.PutUint32(cmd.SeqIn) + + return buf.Bytes() +} + +func parseSet(ctx *context.Context, buf *buffer.Buffer) (*Set, error) { + var err error + var cmd Set + + if cmd.SeqOut, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.SeqIn, err = buf.GetUint32(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/demo/mvd/testdata/demo1.mvd b/demo/mvd/testdata/demo1.mvd Binary files differnew file mode 100644 index 0000000..052b266 --- /dev/null +++ b/demo/mvd/testdata/demo1.mvd diff --git a/demo/mvd/testdata/demo2.mvd b/demo/mvd/testdata/demo2.mvd Binary files differnew file mode 100644 index 0000000..c8f32e4 --- /dev/null +++ b/demo/mvd/testdata/demo2.mvd diff --git a/demo/mvd/testdata/demo3.mvd b/demo/mvd/testdata/demo3.mvd Binary files differnew file mode 100644 index 0000000..374af38 --- /dev/null +++ b/demo/mvd/testdata/demo3.mvd diff --git a/demo/mvd/testdata/demo4.mvd b/demo/mvd/testdata/demo4.mvd Binary files differnew file mode 100644 index 0000000..d2589ec --- /dev/null +++ b/demo/mvd/testdata/demo4.mvd diff --git a/demo/mvd/unknown.go b/demo/mvd/unknown.go new file mode 100644 index 0000000..ec0747a --- /dev/null +++ b/demo/mvd/unknown.go @@ -0,0 +1,25 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Unknown struct { + Data []byte +} + +func (cmd *Unknown) Bytes() []byte { + return cmd.Data +} + +func parseUnknown(ctx *context.Context, buf *buffer.Buffer, size uint32) (*Unknown, error) { + var err error + var cmd Unknown + + if cmd.Data, err = buf.GetBytes(int(size)); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/demo/mvd/usercommand.go b/demo/mvd/usercommand.go new file mode 100644 index 0000000..fc06b18 --- /dev/null +++ b/demo/mvd/usercommand.go @@ -0,0 +1,100 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type UserCommand struct { + size uint32 + + Data []byte + PlayerIndex byte + DropIndex byte + Msec byte + Angle [3]float32 + Forward uint16 + Side uint16 + Up uint16 + Buttons byte + Impulse byte +} + +func (cmd *UserCommand) Bytes() []byte { + if cmd.size != 23 { + return cmd.Data + } + + buf := buffer.New() + + buf.PutByte(cmd.PlayerIndex) + buf.PutByte(cmd.DropIndex) + buf.PutByte(cmd.Msec) + + for _, a := range cmd.Angle { + buf.PutFloat32(a) + } + + buf.PutUint16(cmd.Forward) + buf.PutUint16(cmd.Side) + buf.PutUint16(cmd.Up) + buf.PutByte(cmd.Buttons) + buf.PutByte(cmd.Impulse) + + return buf.Bytes() +} + +func parseUserCommand(ctx *context.Context, buf *buffer.Buffer, size uint32) (*UserCommand, error) { + var err error + var cmd UserCommand + + cmd.size = size + if cmd.size != 23 { + if cmd.Data, err = buf.GetBytes(int(cmd.size)); err != nil { + return nil, err + } + + goto end + } + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.DropIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Msec, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Angle[i], err = buf.GetFloat32(); err != nil { + return nil, err + } + } + + if cmd.Forward, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Side, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Up, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Buttons, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Impulse, err = buf.ReadByte(); err != nil { + return nil, err + } + +end: + return &cmd, nil +} diff --git a/demo/mvd/weapon.go b/demo/mvd/weapon.go new file mode 100644 index 0000000..d7b6bf0 --- /dev/null +++ b/demo/mvd/weapon.go @@ -0,0 +1,71 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Weapon struct { + PlayerIndex byte + Items uint32 + Shells byte + Nails byte + Rockets byte + Cells byte + Choice byte + String string +} + +func (cmd *Weapon) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(cmd.PlayerIndex) + buf.PutUint32(cmd.Items) + buf.PutByte(cmd.Shells) + buf.PutByte(cmd.Nails) + buf.PutByte(cmd.Rockets) + buf.PutByte(cmd.Cells) + buf.PutByte(cmd.Choice) + buf.PutString(cmd.String) + + return buf.Bytes() +} + +func parseWeapon(ctx *context.Context, buf *buffer.Buffer, size uint32) (*Weapon, error) { + var err error + var cmd Weapon + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Items, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.Shells, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Nails, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Rockets, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Cells, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Choice, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.String, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/demo/mvd/weaponinstruction.go b/demo/mvd/weaponinstruction.go new file mode 100644 index 0000000..d923d1b --- /dev/null +++ b/demo/mvd/weaponinstruction.go @@ -0,0 +1,57 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type WeaponInstruction struct { + PlayerIndex byte + Bits byte + Seq uint32 + Mode uint32 + WeaponList []byte +} + +func (cmd *WeaponInstruction) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(cmd.PlayerIndex) + buf.PutByte(cmd.Bits) + buf.PutUint32(cmd.Seq) + buf.PutUint32(cmd.Mode) + buf.PutBytes(cmd.WeaponList) + + return buf.Bytes() +} + +func parseWeaponInstruction( + ctx *context.Context, + buf *buffer.Buffer, + size uint32, +) (*WeaponInstruction, error) { + var err error + var cmd WeaponInstruction + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Bits, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Seq, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.Mode, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.WeaponList, err = buf.GetBytes(10); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/demo/mvd/weaponserverside.go b/demo/mvd/weaponserverside.go new file mode 100644 index 0000000..36840e9 --- /dev/null +++ b/demo/mvd/weaponserverside.go @@ -0,0 +1,75 @@ +package mvd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type WeaponServerSide struct { + PlayerIndex byte + Items uint32 + Shells byte + Nails byte + Rockets byte + Cells byte + Choice byte + String string +} + +func (cmd *WeaponServerSide) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(cmd.PlayerIndex) + buf.PutUint32(cmd.Items) + buf.PutByte(cmd.Shells) + buf.PutByte(cmd.Nails) + buf.PutByte(cmd.Rockets) + buf.PutByte(cmd.Cells) + buf.PutByte(cmd.Choice) + buf.PutString(cmd.String) + + return buf.Bytes() +} + +func parseWeaponServerSide( + ctx *context.Context, + buf *buffer.Buffer, + size uint32, +) (*WeaponServerSide, error) { + var err error + var cmd WeaponServerSide + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Items, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.Shells, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Nails, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Rockets, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Cells, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Choice, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.String, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/demo/qwd/cmd.go b/demo/qwd/cmd.go new file mode 100644 index 0000000..8bd4d15 --- /dev/null +++ b/demo/qwd/cmd.go @@ -0,0 +1,94 @@ +package qwd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Cmd struct { + Msec byte + UserAngle [3]float32 + Forward uint16 + Side uint16 + Up uint16 + Buttons byte + Impulse byte + Padding [3]byte + Angle [3]float32 +} + +func (cmd *Cmd) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(cmd.Msec) + + for i := 0; i < 3; i++ { + buf.PutFloat32(cmd.UserAngle[i]) + } + + buf.PutUint16(cmd.Forward) + buf.PutUint16(cmd.Side) + buf.PutUint16(cmd.Up) + + for i := 0; i < 3; i++ { + buf.PutByte(cmd.Padding[i]) + } + + buf.PutByte(cmd.Buttons) + buf.PutByte(cmd.Impulse) + + for i := 0; i < 3; i++ { + buf.PutFloat32(cmd.Angle[i]) + } + + return buf.Bytes() +} + +func parseCmd(ctx *context.Context, buf *buffer.Buffer) (*Cmd, error) { + var err error + var cmd Cmd + + if cmd.Msec, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.UserAngle[i], err = buf.GetFloat32(); err != nil { + return nil, err + } + } + + if cmd.Forward, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Side, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Up, err = buf.GetUint16(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Padding[i], err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Buttons, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Impulse, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Angle[i], err = buf.GetFloat32(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/demo/qwd/data.go b/demo/qwd/data.go new file mode 100644 index 0000000..cc00c23 --- /dev/null +++ b/demo/qwd/data.go @@ -0,0 +1,32 @@ +package qwd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/protocol" +) + +type Data struct { + Timestamp float32 + Command byte + Cmd *Cmd + Read *Read + Set *Set +} + +func (d *Data) Bytes() []byte { + buf := buffer.New() + + buf.PutFloat32(d.Timestamp) + buf.PutByte(d.Command) + + switch d.Command { + case protocol.DemoCmd: + buf.PutBytes(d.Cmd.Bytes()) + case protocol.DemoRead: + buf.PutBytes(d.Read.Bytes()) + case protocol.DemoSet: + buf.PutBytes(d.Set.Bytes()) + } + + return buf.Bytes() +} diff --git a/demo/qwd/parse.go b/demo/qwd/parse.go new file mode 100644 index 0000000..458f714 --- /dev/null +++ b/demo/qwd/parse.go @@ -0,0 +1,66 @@ +package qwd + +import ( + "errors" + + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +var ErrUnknownType = errors.New("unknown type") + +type Demo struct { + Data []*Data +} + +func (dem *Demo) Bytes() []byte { + buf := buffer.New() + + for _, d := range dem.Data { + buf.PutBytes(d.Bytes()) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, data []byte) (*Demo, error) { + var err error + var cmd Demo + + buf := buffer.New(buffer.WithData(data)) + ctx.SetIsQWD(true) + + for buf.Off() < buf.Len() { + var data Data + + if data.Timestamp, err = buf.GetFloat32(); err != nil { + return nil, err + } + + if data.Command, err = buf.ReadByte(); err != nil { + return nil, err + } + + switch data.Command { + case protocol.DemoCmd: + if data.Cmd, err = parseCmd(ctx, buf); err != nil { + return nil, err + } + case protocol.DemoRead: + if data.Read, err = parseRead(ctx, buf); err != nil { + return nil, err + } + case protocol.DemoSet: + if data.Set, err = parseSet(ctx, buf); err != nil { + return nil, err + } + default: + return nil, ErrUnknownType + } + + cmd.Data = append(cmd.Data, &data) + } + + return &cmd, nil +} diff --git a/demo/qwd/parse_test.go b/demo/qwd/parse_test.go new file mode 100644 index 0000000..97e39df --- /dev/null +++ b/demo/qwd/parse_test.go @@ -0,0 +1,67 @@ +package qwd + +import ( + "crypto/sha256" + "fmt" + "io/ioutil" + "testing" + + "github.com/osm/quake/common/context" +) + +type qwdTest struct { + filePath string + checksum string +} + +var qwdTests = []qwdTest{ + { + filePath: "testdata/demo1.qwd", + checksum: "849cf01cb3fe5c7161625dafdf03178536048d093eda885ca9b37fc673c3c72a", + }, + { + filePath: "testdata/demo2.qwd", + checksum: "c90f0a6c9ba79fc7f84d90b12d923a3ad2c0e54849d8df6fd913264f147931b2", + }, + { + filePath: "testdata/demo3.qwd", + checksum: "b816f366489d22ed70b84f105a19ce63810fee7295e3b3612fdf7e09c26fc9e9", + }, + { + filePath: "testdata/demo4.qwd", + checksum: "6b9088a13fc08609ca58e9887379e743beae0c0752939c001ac35d81a8f9c5af", + }, + { + filePath: "testdata/demo5.qwd", + checksum: "698b1961ee8ac009fff062255cece5ab6729c50df9f6ece2d1cf7f1c52406c13", + }, + { + filePath: "testdata/demo6.qwd", + checksum: "796357af178feb0c5fd5bff9cc6423914a36cc028f172b92c1336f1000e48d74", + }, +} + +func TestParse(t *testing.T) { + for _, qt := range qwdTests { + t.Run(qt.filePath, func(t *testing.T) { + data, err := ioutil.ReadFile(qt.filePath) + if err != nil { + t.Errorf("unable to open demo file, %v", err) + } + + demo, err := Parse(context.New(), data) + if err != nil { + t.Errorf("unable to parse demo, %v", err) + } + + h := sha256.New() + h.Write(demo.Bytes()) + checksum := fmt.Sprintf("%x", h.Sum(nil)) + if checksum != qt.checksum { + t.Errorf("sha256 checksums didn't match") + t.Logf("output: %#v", checksum) + t.Logf("expected: %#v", qt.checksum) + } + }) + } +} diff --git a/demo/qwd/read.go b/demo/qwd/read.go new file mode 100644 index 0000000..fdcee77 --- /dev/null +++ b/demo/qwd/read.go @@ -0,0 +1,42 @@ +package qwd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet" + "github.com/osm/quake/packet/svc" +) + +type Read struct { + Size uint32 + Packet packet.Packet +} + +func (cmd *Read) Bytes() []byte { + buf := buffer.New() + + buf.PutUint32(cmd.Size) + buf.PutBytes(cmd.Packet.Bytes()) + + return buf.Bytes() +} + +func parseRead(ctx *context.Context, buf *buffer.Buffer) (*Read, error) { + var err error + var cmd Read + + if cmd.Size, err = buf.GetUint32(); err != nil { + return nil, err + } + + bytes, err := buf.GetBytes(int(cmd.Size)) + if err != nil { + return nil, err + } + + if cmd.Packet, err = svc.Parse(ctx, bytes); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/demo/qwd/set.go b/demo/qwd/set.go new file mode 100644 index 0000000..d7cc48d --- /dev/null +++ b/demo/qwd/set.go @@ -0,0 +1,35 @@ +package qwd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Set struct { + SeqOut uint32 + SeqIn uint32 +} + +func (cmd *Set) Bytes() []byte { + buf := buffer.New() + + buf.PutUint32(cmd.SeqOut) + buf.PutUint32(cmd.SeqIn) + + return buf.Bytes() +} + +func parseSet(ctx *context.Context, buf *buffer.Buffer) (*Set, error) { + var err error + var cmd Set + + if cmd.SeqOut, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.SeqIn, err = buf.GetUint32(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/demo/qwd/testdata/demo1.qwd b/demo/qwd/testdata/demo1.qwd Binary files differnew file mode 100644 index 0000000..afc2c30 --- /dev/null +++ b/demo/qwd/testdata/demo1.qwd diff --git a/demo/qwd/testdata/demo2.qwd b/demo/qwd/testdata/demo2.qwd Binary files differnew file mode 100644 index 0000000..55f5306 --- /dev/null +++ b/demo/qwd/testdata/demo2.qwd diff --git a/demo/qwd/testdata/demo3.qwd b/demo/qwd/testdata/demo3.qwd Binary files differnew file mode 100644 index 0000000..e04d332 --- /dev/null +++ b/demo/qwd/testdata/demo3.qwd diff --git a/demo/qwd/testdata/demo4.qwd b/demo/qwd/testdata/demo4.qwd Binary files differnew file mode 100644 index 0000000..005a143 --- /dev/null +++ b/demo/qwd/testdata/demo4.qwd diff --git a/demo/qwd/testdata/demo5.qwd b/demo/qwd/testdata/demo5.qwd Binary files differnew file mode 100644 index 0000000..a78e779 --- /dev/null +++ b/demo/qwd/testdata/demo5.qwd diff --git a/demo/qwd/testdata/demo6.qwd b/demo/qwd/testdata/demo6.qwd Binary files differnew file mode 100644 index 0000000..78e6abc --- /dev/null +++ b/demo/qwd/testdata/demo6.qwd diff --git a/example/client/main.go b/example/client/main.go new file mode 100644 index 0000000..3d2e6d9 --- /dev/null +++ b/example/client/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "log" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/osm/quake/client" + "github.com/osm/quake/client/quake" + "github.com/osm/quake/common/ascii" + "github.com/osm/quake/packet" + "github.com/osm/quake/packet/command" + "github.com/osm/quake/packet/command/print" + "github.com/osm/quake/packet/command/stringcmd" + "github.com/osm/quake/packet/svc" +) + +func main() { + addrPort := flag.String("addr", "172.236.100.99:28501", "address and port to connect to") + name := flag.String("name", "player", "name") + team := flag.String("team", "red", "team") + flag.Parse() + + logger := log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime) + + if *addrPort == "" { + logger.Fatalf("-addr is required") + } + + var client client.Client + var err error + client, err = quake.New( + *name, + *team, + []quake.Option{ + quake.WithSpectator(true), + quake.WithLogger(logger), + }..., + ) + if err != nil { + logger.Fatalf("unable to create new client, %v", err) + } + + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-signalCh + client.Quit() + os.Exit(0) + }() + + go func() { + reader := bufio.NewReader(os.Stdin) + for { + input, _ := reader.ReadString('\n') + + client.Enqueue([]command.Command{ + &stringcmd.Command{String: strings.TrimSpace(input)}, + }) + } + }() + + client.HandleFunc(func(packet packet.Packet) []command.Command { + gameData, ok := packet.(*svc.GameData) + if !ok { + return []command.Command{} + } + + for _, cmd := range gameData.Commands { + switch c := cmd.(type) { + case *print.Command: + fmt.Printf("%s", ascii.Parse(c.String)) + } + } + + return nil + }) + + logger.Printf("connecting to %s", *addrPort) + if err := client.Connect(*addrPort); err != nil { + logger.Fatal(err) + } +} @@ -0,0 +1,3 @@ +module github.com/osm/quake + +go 1.23.0 diff --git a/packet/clc/connectionless.go b/packet/clc/connectionless.go new file mode 100644 index 0000000..96db112 --- /dev/null +++ b/packet/clc/connectionless.go @@ -0,0 +1,64 @@ +package clc + +import ( + "errors" + + "github.com/osm/quake/common/args" + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command" + "github.com/osm/quake/packet/command/connect" + "github.com/osm/quake/packet/command/getchallenge" + "github.com/osm/quake/packet/command/passthrough" +) + +type Connectionless struct { + Command command.Command +} + +func (cmd *Connectionless) Bytes() []byte { + buf := buffer.New() + + buf.PutInt32(-1) + buf.PutBytes(cmd.Command.Bytes()) + + return buf.Bytes() +} + +func parseConnectionless(ctx *context.Context, buf *buffer.Buffer) (*Connectionless, error) { + var err error + var pkg Connectionless + + if err := buf.Skip(4); err != nil { + return nil, err + } + + var str string + if str, err = buf.GetString(); err != nil { + return nil, err + } + + args := args.Parse(str) + if len(args) != 1 { + return nil, errors.New("unexpected length of parsed arguments") + } + + arg := args[0] + + var cmd command.Command + switch arg.Cmd { + case "connect": + cmd, err = connect.Parse(ctx, buf, arg) + case "getchallenge": + cmd, err = getchallenge.Parse(ctx, buf) + default: + cmd, err = passthrough.Parse(ctx, buf, str) + } + + if err != nil { + return nil, err + } + pkg.Command = cmd + + return &pkg, nil +} diff --git a/packet/clc/gamedata.go b/packet/clc/gamedata.go new file mode 100644 index 0000000..7ee5890 --- /dev/null +++ b/packet/clc/gamedata.go @@ -0,0 +1,101 @@ +package clc + +import ( + "errors" + + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command" + "github.com/osm/quake/packet/command/bad" + "github.com/osm/quake/packet/command/delta" + "github.com/osm/quake/packet/command/ftevoicechatc" + "github.com/osm/quake/packet/command/move" + "github.com/osm/quake/packet/command/mvdweapon" + "github.com/osm/quake/packet/command/nopc" + "github.com/osm/quake/packet/command/stringcmd" + "github.com/osm/quake/packet/command/tmove" + "github.com/osm/quake/packet/command/upload" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/fte" + "github.com/osm/quake/protocol/mvd" +) + +var ErrUnknownCommandType = errors.New("unknown command type") + +type GameData struct { + Seq uint32 + Ack uint32 + QPort uint16 + Commands []command.Command +} + +func (gd *GameData) Bytes() []byte { + buf := buffer.New() + + buf.PutUint32(gd.Seq) + buf.PutUint32(gd.Ack) + buf.PutUint16(gd.QPort) + + for _, c := range gd.Commands { + buf.PutBytes(c.Bytes()) + } + + return buf.Bytes() +} + +func parseGameData(ctx *context.Context, buf *buffer.Buffer) (*GameData, error) { + var err error + var pkg GameData + + if pkg.Seq, err = buf.GetUint32(); err != nil { + return nil, err + } + + if pkg.Ack, err = buf.GetUint32(); err != nil { + return nil, err + } + + if pkg.QPort, err = buf.GetUint16(); err != nil { + return nil, err + } + + var cmd command.Command + for buf.Off() < buf.Len() { + typ, err := buf.ReadByte() + if err != nil { + return nil, err + } + + switch protocol.CommandType(typ) { + case protocol.CLCBad: + cmd, err = bad.Parse(ctx, buf, protocol.CLCBad) + case protocol.CLCNOP: + cmd, err = nopc.Parse(ctx, buf) + case protocol.CLCDoubleMove: + case protocol.CLCMove: + cmd, err = move.Parse(ctx, buf) + case protocol.CLCStringCmd: + cmd, err = stringcmd.Parse(ctx, buf) + case protocol.CLCDelta: + cmd, err = delta.Parse(ctx, buf) + case protocol.CLCTMove: + cmd, err = tmove.Parse(ctx, buf) + case protocol.CLCUpload: + cmd, err = upload.Parse(ctx, buf) + case fte.CLCVoiceChat: + cmd, err = ftevoicechatc.Parse(ctx, buf) + case mvd.CLCWeapon: + cmd, err = mvdweapon.Parse(ctx, buf) + default: + return nil, ErrUnknownCommandType + } + + if err != nil { + return nil, err + } + + pkg.Commands = append(pkg.Commands, cmd) + } + + return &pkg, nil +} diff --git a/packet/clc/parse.go b/packet/clc/parse.go new file mode 100644 index 0000000..594a9dd --- /dev/null +++ b/packet/clc/parse.go @@ -0,0 +1,18 @@ +package clc + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet" +) + +func Parse(ctx *context.Context, data []byte) (packet.Packet, error) { + buf := buffer.New(buffer.WithData(data)) + + header, _ := buf.PeekInt32() + if header == -1 { + return parseConnectionless(ctx, buf) + } + + return parseGameData(ctx, buf) +} diff --git a/packet/command/a2aping/a2aping.go b/packet/command/a2aping/a2aping.go new file mode 100644 index 0000000..37929a3 --- /dev/null +++ b/packet/command/a2aping/a2aping.go @@ -0,0 +1,17 @@ +package a2aping + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.A2APing} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/a2cclientcommand/a2cclientcommand.go b/packet/command/a2cclientcommand/a2cclientcommand.go new file mode 100644 index 0000000..e2d8011 --- /dev/null +++ b/packet/command/a2cclientcommand/a2cclientcommand.go @@ -0,0 +1,37 @@ +package a2cclientcommand + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Command string + LocalID string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.A2CClientCommand) + buf.PutString(cmd.Command) + buf.PutString(cmd.LocalID) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Command, err = buf.GetString(); err != nil { + return nil, err + } + + if cmd.LocalID, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/a2cprint/a2cprint.go b/packet/command/a2cprint/a2cprint.go new file mode 100644 index 0000000..01c6051 --- /dev/null +++ b/packet/command/a2cprint/a2cprint.go @@ -0,0 +1,52 @@ +package a2cprint + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/ftedownload" + "github.com/osm/quake/protocol" +) + +type Command struct { + String string + + IsChunkedDownload bool + Download *ftedownload.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.A2CPrint) + + if cmd.IsChunkedDownload && cmd.Download != nil { + buf.PutBytes(cmd.Download.Bytes()) + } else { + buf.PutString(cmd.String) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + bytes, _ := buf.PeekBytes(6) + cmd.IsChunkedDownload = bytes != nil && string(bytes) == "\\chunk" + + if cmd.IsChunkedDownload { + cmd.IsChunkedDownload = true + + if cmd.Download, err = ftedownload.Parse(ctx, buf); err != nil { + return nil, err + } + + } + + if cmd.String, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/bad/bad.go b/packet/command/bad/bad.go new file mode 100644 index 0000000..dfab5de --- /dev/null +++ b/packet/command/bad/bad.go @@ -0,0 +1,19 @@ +package bad + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Type protocol.CommandType +} + +func (cmd *Command) Bytes() []byte { + return []byte{byte(cmd.Type)} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer, typ protocol.CommandType) (*Command, error) { + return &Command{Type: typ}, nil +} diff --git a/packet/command/baseline/baseline.go b/packet/command/baseline/baseline.go new file mode 100644 index 0000000..bc775bb --- /dev/null +++ b/packet/command/baseline/baseline.go @@ -0,0 +1,89 @@ +package baseline + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Command struct { + AngleSize uint8 + CoordSize uint8 + + ModelIndex byte + Frame byte + ColorMap byte + SkinNum byte + Coord [3]float32 + Angle [3]float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + writeAngle := buf.PutAngle8 + if cmd.AngleSize == 2 { + writeAngle = buf.PutAngle16 + } + + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 { + writeCoord = buf.PutCoord32 + } + + buf.PutByte(cmd.ModelIndex) + buf.PutByte(cmd.Frame) + buf.PutByte(cmd.ColorMap) + buf.PutByte(cmd.SkinNum) + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + writeAngle(cmd.Angle[i]) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.AngleSize = ctx.GetAngleSize() + readAngle := buf.GetAngle8 + if cmd.AngleSize == 2 { + readAngle = buf.GetAngle16 + } + + cmd.CoordSize = ctx.GetCoordSize() + readCoord := buf.GetCoord16 + if cmd.CoordSize == 4 { + readCoord = buf.GetCoord32 + } + + if cmd.ModelIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Frame, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.ColorMap, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.SkinNum, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + + if cmd.Angle[i], err = readAngle(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/bigkick/bigkick.go b/packet/command/bigkick/bigkick.go new file mode 100644 index 0000000..b5971b5 --- /dev/null +++ b/packet/command/bigkick/bigkick.go @@ -0,0 +1,17 @@ +package bigkick + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCBigKick} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/cdtrack/cdtrack.go b/packet/command/cdtrack/cdtrack.go new file mode 100644 index 0000000..55720bf --- /dev/null +++ b/packet/command/cdtrack/cdtrack.go @@ -0,0 +1,46 @@ +package cdtrack + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + IsNQ bool + + Track byte + Loop byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCCDTrack) + buf.PutByte(cmd.Track) + + if cmd.IsNQ { + buf.PutByte(cmd.Loop) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.IsNQ = ctx.GetIsNQ() + + if cmd.Track, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.IsNQ { + if cmd.Loop, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/centerprint/centerprint.go b/packet/command/centerprint/centerprint.go new file mode 100644 index 0000000..b4bfc8b --- /dev/null +++ b/packet/command/centerprint/centerprint.go @@ -0,0 +1,31 @@ +package centerprint + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + String string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCCenterPrint) + buf.PutString(cmd.String) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.String, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/chokecount/chokecount.go b/packet/command/chokecount/chokecount.go new file mode 100644 index 0000000..7c6d8f8 --- /dev/null +++ b/packet/command/chokecount/chokecount.go @@ -0,0 +1,31 @@ +package chokecount + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Count byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCChokeCount) + buf.PutByte(cmd.Count) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Count, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/clientdata/clientdata.go b/packet/command/clientdata/clientdata.go new file mode 100644 index 0000000..d032edd --- /dev/null +++ b/packet/command/clientdata/clientdata.go @@ -0,0 +1,164 @@ +package clientdata + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Bits uint16 + ViewHeight byte + IdealPitch byte + PunchAngle [3]byte + Velocity [3]byte + Items uint32 + WeaponFrame byte + Armor byte + Weapon byte + Health uint16 + ActiveAmmo byte + AmmoShells byte + AmmoNails byte + AmmoRockets byte + AmmoCells byte + ActiveWeapon byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCClientData) + buf.PutUint16(cmd.Bits) + + if cmd.Bits&protocol.SUViewHeight != 0 { + buf.PutByte(cmd.ViewHeight) + } + + if cmd.Bits&protocol.SUIdealPitch != 0 { + buf.PutByte(cmd.IdealPitch) + } + + for i := 0; i < 3; i++ { + if cmd.Bits&(protocol.SUPunch1<<i) != 0 { + buf.PutByte(cmd.PunchAngle[i]) + } + + if cmd.Bits&(protocol.SUVelocity1<<i) != 0 { + buf.PutByte(cmd.Velocity[i]) + } + } + + if cmd.Bits&protocol.SUItems != 0 { + buf.PutUint32(cmd.Items) + } + + if cmd.Bits&protocol.SUWeaponFrame != 0 { + buf.PutByte(cmd.WeaponFrame) + } + + if cmd.Bits&protocol.SUArmor != 0 { + buf.PutByte(cmd.Armor) + } + + if cmd.Bits&protocol.SUWeapon != 0 { + buf.PutByte(cmd.Weapon) + } + + buf.PutUint16(cmd.Health) + buf.PutByte(cmd.ActiveAmmo) + buf.PutByte(cmd.AmmoShells) + buf.PutByte(cmd.AmmoNails) + buf.PutByte(cmd.AmmoRockets) + buf.PutByte(cmd.AmmoCells) + buf.PutByte(cmd.ActiveWeapon) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Bits, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Bits&protocol.SUViewHeight != 0 { + if cmd.ViewHeight, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.SUIdealPitch != 0 { + if cmd.IdealPitch, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + for i := 0; i < 3; i++ { + if cmd.Bits&(protocol.SUPunch1<<i) != 0 { + if cmd.PunchAngle[i], err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&(protocol.SUVelocity1<<i) != 0 { + if cmd.Velocity[i], err = buf.ReadByte(); err != nil { + return nil, err + } + } + } + + if cmd.Items, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.Bits&protocol.SUWeaponFrame != 0 { + if cmd.WeaponFrame, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.SUArmor != 0 { + if cmd.Armor, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.SUWeapon != 0 { + if cmd.Weapon, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Health, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.ActiveAmmo, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.AmmoShells, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.AmmoNails, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.AmmoRockets, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.AmmoCells, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.ActiveWeapon, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/command.go b/packet/command/command.go new file mode 100644 index 0000000..3b5034f --- /dev/null +++ b/packet/command/command.go @@ -0,0 +1,5 @@ +package command + +type Command interface { + Bytes() []byte +} diff --git a/packet/command/connect/connect.go b/packet/command/connect/connect.go new file mode 100644 index 0000000..35527f1 --- /dev/null +++ b/packet/command/connect/connect.go @@ -0,0 +1,67 @@ +package connect + +import ( + "errors" + "fmt" + "strconv" + + "github.com/osm/quake/common/args" + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/common/infostring" + "github.com/osm/quake/protocol" +) + +type Command struct { + Command string + Version string + QPort uint16 + ChallengeID string + UserInfo *infostring.InfoString + Extensions []*protocol.Extension +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutBytes([]byte(cmd.Command + " ")) + buf.PutBytes([]byte(cmd.Version + " ")) + buf.PutBytes([]byte(fmt.Sprintf("%v ", cmd.QPort))) + buf.PutBytes([]byte(cmd.ChallengeID + " ")) + + if cmd.UserInfo != nil { + buf.PutBytes(cmd.UserInfo.Bytes()) + } + + buf.PutByte(0x0a) + + for _, ext := range cmd.Extensions { + if ext != nil { + buf.PutBytes(ext.Bytes()) + } + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer, arg args.Arg) (*Command, error) { + var cmd Command + + if len(arg.Args) < 4 { + return nil, errors.New("unexpected length of args") + } + + cmd.Command = arg.Cmd + cmd.Version = arg.Args[0] + + qPort, err := strconv.ParseUint(arg.Args[1], 10, 16) + if err != nil { + return nil, err + } + cmd.QPort = uint16(qPort) + + cmd.ChallengeID = arg.Args[2] + cmd.UserInfo = infostring.Parse(arg.Args[3]) + + return &cmd, nil +} diff --git a/packet/command/damage/damage.go b/packet/command/damage/damage.go new file mode 100644 index 0000000..93ddb1e --- /dev/null +++ b/packet/command/damage/damage.go @@ -0,0 +1,61 @@ +package damage + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + CoordSize uint8 + + Armor byte + Blood byte + Coord [3]float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 { + writeCoord = buf.PutCoord32 + } + + buf.PutByte(byte(protocol.SVCDamage)) + buf.PutByte(cmd.Armor) + buf.PutByte(cmd.Blood) + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.CoordSize = ctx.GetCoordSize() + readCoord := buf.GetCoord16 + if cmd.CoordSize == 4 { + readCoord = buf.GetCoord32 + } + + if cmd.Armor, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Blood, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/delta/delta.go b/packet/command/delta/delta.go new file mode 100644 index 0000000..90fdffe --- /dev/null +++ b/packet/command/delta/delta.go @@ -0,0 +1,26 @@ +package delta + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Seq byte +} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.CLCDelta, cmd.Seq} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Seq, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/deltapacketentities/deltapacketentities.go b/packet/command/deltapacketentities/deltapacketentities.go new file mode 100644 index 0000000..7a1d415 --- /dev/null +++ b/packet/command/deltapacketentities/deltapacketentities.go @@ -0,0 +1,43 @@ +package deltapacketentities + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/packetentity" + "github.com/osm/quake/protocol" +) + +type Command struct { + Index byte + Entities []*packetentity.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCDeltaPacketEntities) + buf.PutByte(cmd.Index) + + for i := 0; i < len(cmd.Entities); i++ { + buf.PutBytes(cmd.Entities[i].Bytes()) + } + + buf.PutUint16(0x0000) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Index, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Entities, err = packetentity.Parse(ctx, buf); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/deltausercommand/deltausercommand.go b/packet/command/deltausercommand/deltausercommand.go new file mode 100644 index 0000000..3a18371 --- /dev/null +++ b/packet/command/deltausercommand/deltausercommand.go @@ -0,0 +1,178 @@ +package deltausercommand + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + ProtocolVersion uint32 + + Bits byte + + CMAngle1 float32 + CMAngle2 float32 + CMAngle3 float32 + CMForward8 byte + CMForward16 uint16 + CMSide8 byte + CMSide16 uint16 + CMUp8 byte + CMUp16 uint16 + CMButtons byte + CMImpulse byte + CMMsec byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(cmd.Bits) + + if cmd.Bits&protocol.CMAngle1 != 0 { + buf.PutAngle16(cmd.CMAngle1) + } + + if cmd.Bits&protocol.CMAngle2 != 0 { + buf.PutAngle16(cmd.CMAngle2) + } + + if cmd.Bits&protocol.CMAngle3 != 0 { + buf.PutAngle16(cmd.CMAngle3) + } + + if cmd.ProtocolVersion <= 26 { + if cmd.Bits&protocol.CMForward != 0 { + buf.PutByte(cmd.CMForward8) + } + + if cmd.Bits&protocol.CMSide != 0 { + buf.PutByte(cmd.CMSide8) + } + + if cmd.Bits&protocol.CMUp != 0 { + buf.PutByte(cmd.CMUp8) + } + } else { + if cmd.Bits&protocol.CMForward != 0 { + buf.PutUint16(cmd.CMForward16) + } + + if cmd.Bits&protocol.CMSide != 0 { + buf.PutUint16(cmd.CMSide16) + } + + if cmd.Bits&protocol.CMUp != 0 { + buf.PutUint16(cmd.CMUp16) + } + } + + if cmd.Bits&protocol.CMButtons != 0 { + buf.PutByte(cmd.CMButtons) + } + + if cmd.Bits&protocol.CMImpulse != 0 { + buf.PutByte(cmd.CMImpulse) + } + + if cmd.ProtocolVersion <= 26 && cmd.Bits&protocol.CMMsec != 0 { + buf.PutByte(cmd.CMMsec) + } else if cmd.ProtocolVersion >= 27 { + buf.PutByte(cmd.CMMsec) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.ProtocolVersion = ctx.GetProtocolVersion() + + if cmd.Bits, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Bits&protocol.CMAngle1 != 0 { + if cmd.CMAngle1, err = buf.GetAngle16(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.CMAngle2 != 0 { + if cmd.CMAngle2, err = buf.GetAngle16(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.CMAngle3 != 0 { + if cmd.CMAngle3, err = buf.GetAngle16(); err != nil { + return nil, err + } + } + + if cmd.ProtocolVersion <= 26 { + if cmd.Bits&protocol.CMForward != 0 { + if cmd.CMForward8, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.CMSide != 0 { + if cmd.CMSide8, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.CMUp != 0 { + if cmd.CMUp8, err = buf.ReadByte(); err != nil { + return nil, err + } + } + } else { + if cmd.Bits&protocol.CMForward != 0 { + if cmd.CMForward16, err = buf.GetUint16(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.CMSide != 0 { + if cmd.CMSide16, err = buf.GetUint16(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.CMUp != 0 { + if cmd.CMUp16, err = buf.GetUint16(); err != nil { + return nil, err + } + } + + } + + if cmd.Bits&protocol.CMButtons != 0 { + if cmd.CMButtons, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.CMImpulse != 0 { + if cmd.CMImpulse, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.ProtocolVersion <= 26 && cmd.Bits&protocol.CMMsec != 0 { + if cmd.CMMsec, err = buf.ReadByte(); err != nil { + return nil, err + } + } else if cmd.ProtocolVersion >= 27 { + if cmd.CMMsec, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/disconnect/disconnect.go b/packet/command/disconnect/disconnect.go new file mode 100644 index 0000000..1045ee2 --- /dev/null +++ b/packet/command/disconnect/disconnect.go @@ -0,0 +1,42 @@ +package disconnect + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + IsMVD bool + IsQWD bool + + String string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCDisconnect) + + if cmd.IsMVD || cmd.IsQWD { + buf.PutString(cmd.String) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.IsQWD = ctx.GetIsQWD() + cmd.IsMVD = ctx.GetIsMVD() + + if cmd.IsQWD || cmd.IsMVD { + if cmd.String, err = buf.GetString(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/download/download.go b/packet/command/download/download.go new file mode 100644 index 0000000..0872f6f --- /dev/null +++ b/packet/command/download/download.go @@ -0,0 +1,90 @@ +package download + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/fte" +) + +type Command struct { + FTEProtocolExtension uint32 + + Size16 int16 + Size32 int32 + Percent byte + Number int32 + Name string + Data []byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCDownload) + + if cmd.FTEProtocolExtension&fte.ExtensionChunkedDownloads != 0 { + buf.PutInt32(cmd.Number) + + if cmd.Number == -1 { + buf.PutInt32(cmd.Size32) + buf.PutString(cmd.Name) + } else { + buf.PutBytes(cmd.Data) + } + } else { + buf.PutInt16(cmd.Size16) + buf.PutByte(cmd.Percent) + + if cmd.Size16 != -1 { + buf.PutBytes(cmd.Data) + } + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.FTEProtocolExtension = ctx.GetFTEProtocolExtension() + + if cmd.FTEProtocolExtension&fte.ExtensionChunkedDownloads != 0 { + if cmd.Number, err = buf.GetInt32(); err != nil { + return nil, err + } + + if cmd.Number < 0 { + if cmd.Size32, err = buf.GetInt32(); err != nil { + return nil, err + } + + if cmd.Name, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil + } + + if cmd.Data, err = buf.GetBytes(protocol.DownloadBlockSize); err != nil { + return nil, err + } + } else { + if cmd.Size16, err = buf.GetInt16(); err != nil { + return nil, err + } + + if cmd.Percent, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Size16 > 0 { + if cmd.Data, err = buf.GetBytes(int(cmd.Size16)); err != nil { + return nil, err + } + } + } + + return &cmd, nil +} diff --git a/packet/command/entgravity/entgravity.go b/packet/command/entgravity/entgravity.go new file mode 100644 index 0000000..c6567ce --- /dev/null +++ b/packet/command/entgravity/entgravity.go @@ -0,0 +1,31 @@ +package entgravity + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + EntGravity float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCEntGravity) + buf.PutFloat32(cmd.EntGravity) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.EntGravity, err = buf.GetFloat32(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/fastupdate/fastupdate.go b/packet/command/fastupdate/fastupdate.go new file mode 100644 index 0000000..e6e344c --- /dev/null +++ b/packet/command/fastupdate/fastupdate.go @@ -0,0 +1,182 @@ +package fastupdate + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Bits byte + MoreBits byte + Entity8 byte + Entity16 uint16 + Model byte + Frame byte + ColorMap byte + Skin byte + Effects byte + Origin [3]float32 + Angle [3]float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(cmd.Bits) + + bits16 := uint16(cmd.Bits) + bits16 &= 127 + + if bits16&protocol.NQUMoreBits != 0 { + buf.PutByte(cmd.MoreBits) + bits16 |= uint16(cmd.MoreBits) << 8 + } + + if bits16&protocol.NQULongEntity != 0 { + buf.PutUint16(cmd.Entity16) + } else { + buf.PutByte(cmd.Entity8) + } + + if bits16&protocol.NQUModel != 0 { + buf.PutByte(cmd.Model) + } + + if bits16&protocol.NQUFrame != 0 { + buf.PutByte(cmd.Frame) + } + + if bits16&protocol.NQUColorMap != 0 { + buf.PutByte(cmd.ColorMap) + } + + if bits16&protocol.NQUSkin != 0 { + buf.PutByte(cmd.Skin) + } + + if bits16&protocol.NQUEffects != 0 { + buf.PutByte(cmd.Effects) + } + + if bits16&protocol.NQUOrigin1 != 0 { + buf.PutCoord16(cmd.Origin[0]) + } + + if bits16&protocol.NQUAngle1 != 0 { + buf.PutAngle8(cmd.Angle[0]) + } + + if bits16&protocol.NQUOrigin2 != 0 { + buf.PutCoord16(cmd.Origin[1]) + } + + if bits16&protocol.NQUAngle2 != 0 { + buf.PutAngle8(cmd.Angle[1]) + } + + if bits16&protocol.NQUOrigin3 != 0 { + buf.PutCoord16(cmd.Origin[2]) + } + + if bits16&protocol.NQUAngle3 != 0 { + buf.PutAngle8(cmd.Angle[2]) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer, bits byte) (*Command, error) { + var err error + var cmd Command + + cmd.Bits = bits + + bits16 := uint16(bits) + bits16 &= 127 + + if bits16&protocol.NQUMoreBits != 0 { + if cmd.MoreBits, err = buf.ReadByte(); err != nil { + return nil, err + } + bits16 |= uint16(cmd.MoreBits) << 8 + } + + if bits16&protocol.NQULongEntity != 0 { + if cmd.Entity16, err = buf.GetUint16(); err != nil { + return nil, err + } + } else { + if cmd.Entity8, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUModel != 0 { + if cmd.Model, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUFrame != 0 { + if cmd.Frame, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUColorMap != 0 { + if cmd.ColorMap, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUSkin != 0 { + if cmd.Skin, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUEffects != 0 { + if cmd.Effects, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUOrigin1 != 0 { + if cmd.Origin[0], err = buf.GetCoord16(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUAngle1 != 0 { + if cmd.Angle[0], err = buf.GetAngle8(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUOrigin2 != 0 { + if cmd.Origin[1], err = buf.GetCoord16(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUAngle2 != 0 { + if cmd.Angle[1], err = buf.GetAngle8(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUOrigin3 != 0 { + if cmd.Origin[2], err = buf.GetCoord16(); err != nil { + return nil, err + } + } + + if bits16&protocol.NQUAngle3 != 0 { + if cmd.Angle[2], err = buf.GetAngle8(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/finale/finale.go b/packet/command/finale/finale.go new file mode 100644 index 0000000..c49ec6d --- /dev/null +++ b/packet/command/finale/finale.go @@ -0,0 +1,31 @@ +package finale + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + String string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCFinale) + buf.PutString(cmd.String) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.String, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/foundsecret/foundsecret.go b/packet/command/foundsecret/foundsecret.go new file mode 100644 index 0000000..b63517a --- /dev/null +++ b/packet/command/foundsecret/foundsecret.go @@ -0,0 +1,17 @@ +package foundsecret + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCFoundSecret} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/ftedownload/ftedownload.go b/packet/command/ftedownload/ftedownload.go new file mode 100644 index 0000000..f9b1ee6 --- /dev/null +++ b/packet/command/ftedownload/ftedownload.go @@ -0,0 +1,54 @@ +package ftedownload + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/download" + "github.com/osm/quake/protocol" +) + +type Command struct { + Number int32 + DownloadID int32 + Command byte + Chunk *download.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.A2CPrint) + buf.PutBytes([]byte("\\chunk")) + buf.PutInt32(cmd.DownloadID) + + if cmd.Chunk != nil { + buf.PutByte(cmd.Command) + buf.PutInt32(cmd.Number) + buf.PutBytes(cmd.Chunk.Data) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if err := buf.Skip(6); err != nil { + return nil, err + } + + if cmd.Number, err = buf.GetInt32(); err != nil { + return nil, err + } + + if cmd.Command, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Chunk, err = download.Parse(ctx, buf); err != nil { + return nil, err + } + + return nil, nil +} diff --git a/packet/command/ftemodellist/ftemodellist.go b/packet/command/ftemodellist/ftemodellist.go new file mode 100644 index 0000000..b9af505 --- /dev/null +++ b/packet/command/ftemodellist/ftemodellist.go @@ -0,0 +1,57 @@ +package ftemodellist + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol/fte" +) + +type Command struct { + NumModels uint16 + Models []string + More byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(fte.SVCModelListShort) + buf.PutUint16(cmd.NumModels) + + for i := 0; i < len(cmd.Models); i++ { + buf.PutString(cmd.Models[i]) + } + buf.PutByte(0x00) + + buf.PutByte(cmd.More) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.NumModels, err = buf.GetUint16(); err != nil { + return nil, err + } + + for { + var model string + if model, err = buf.GetString(); err != nil { + return nil, err + } + + if model == "" { + break + } + + cmd.Models = append(cmd.Models, model) + } + + if cmd.More, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/ftespawnbaseline/ftespawnbaseline.go b/packet/command/ftespawnbaseline/ftespawnbaseline.go new file mode 100644 index 0000000..39abd1e --- /dev/null +++ b/packet/command/ftespawnbaseline/ftespawnbaseline.go @@ -0,0 +1,41 @@ +package ftespawnbaseline + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/packetentitydelta" + "github.com/osm/quake/protocol/fte" +) + +type Command struct { + Index uint16 + Delta *packetentitydelta.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(fte.SVCSpawnBaseline) + buf.PutUint16(cmd.Index) + + if cmd.Delta != nil { + buf.PutBytes(cmd.Delta.Bytes()) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Index, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Delta, err = packetentitydelta.Parse(ctx, buf, cmd.Index); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/ftespawnstatic/ftespawnstatic.go b/packet/command/ftespawnstatic/ftespawnstatic.go new file mode 100644 index 0000000..b093424 --- /dev/null +++ b/packet/command/ftespawnstatic/ftespawnstatic.go @@ -0,0 +1,41 @@ +package ftespawnstatic + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/packetentitydelta" + "github.com/osm/quake/protocol/fte" +) + +type Command struct { + Bits uint16 + Delta *packetentitydelta.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(fte.SVCSpawnStatic) + buf.PutUint16(cmd.Bits) + + if cmd.Delta != nil { + buf.PutBytes(cmd.Delta.Bytes()) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Bits, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Delta, err = packetentitydelta.Parse(ctx, buf, cmd.Bits); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/ftevoicechatc/ftevoicechatc.go b/packet/command/ftevoicechatc/ftevoicechatc.go new file mode 100644 index 0000000..369fb12 --- /dev/null +++ b/packet/command/ftevoicechatc/ftevoicechatc.go @@ -0,0 +1,49 @@ +package ftevoicechatc + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol/fte" +) + +type Command struct { + Gen byte + Seq byte + Size uint16 + Data []byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(fte.CLCVoiceChat) + buf.PutByte(cmd.Gen) + buf.PutByte(cmd.Seq) + buf.PutUint16(cmd.Size) + buf.PutBytes(cmd.Data) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Gen, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Seq, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Size, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Data, err = buf.GetBytes(int(cmd.Size)); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/ftevoicechats/ftevoicechats.go b/packet/command/ftevoicechats/ftevoicechats.go new file mode 100644 index 0000000..be4109d --- /dev/null +++ b/packet/command/ftevoicechats/ftevoicechats.go @@ -0,0 +1,55 @@ +package ftevoicechats + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol/fte" +) + +type Command struct { + Sender byte + Gen byte + Seq byte + Size uint16 + Data []byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(fte.SVCVoiceChat) + buf.PutByte(cmd.Sender) + buf.PutByte(cmd.Gen) + buf.PutByte(cmd.Seq) + buf.PutUint16(cmd.Size) + buf.PutBytes(cmd.Data) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Sender, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Gen, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Seq, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Size, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Data, err = buf.GetBytes(int(cmd.Size)); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/getchallenge/getchallenge.go b/packet/command/getchallenge/getchallenge.go new file mode 100644 index 0000000..090f78d --- /dev/null +++ b/packet/command/getchallenge/getchallenge.go @@ -0,0 +1,16 @@ +package getchallenge + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte("getchallenge\n") +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/intermission/intermission.go b/packet/command/intermission/intermission.go new file mode 100644 index 0000000..41177ff --- /dev/null +++ b/packet/command/intermission/intermission.go @@ -0,0 +1,72 @@ +package intermission + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + AngleSize uint8 + CoordSize uint8 + + Coord [3]float32 + Angle [3]float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + writeAngle := buf.PutAngle8 + if cmd.AngleSize == 2 { + writeAngle = buf.PutAngle16 + } + + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 { + writeCoord = buf.PutCoord32 + } + + buf.PutByte(protocol.SVCIntermission) + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + + for i := 0; i < 3; i++ { + writeAngle(cmd.Angle[i]) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.AngleSize = ctx.GetAngleSize() + readAngle := buf.GetAngle8 + if cmd.AngleSize == 2 { + readAngle = buf.GetAngle16 + } + + cmd.CoordSize = ctx.GetCoordSize() + readCoord := buf.GetCoord16 + if cmd.CoordSize == 4 { + readCoord = buf.GetCoord32 + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + + for i := 0; i < 3; i++ { + if cmd.Angle[i], err = readAngle(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/ip/ip.go b/packet/command/ip/ip.go new file mode 100644 index 0000000..2d658a1 --- /dev/null +++ b/packet/command/ip/ip.go @@ -0,0 +1,22 @@ +package ip + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Command struct { + String string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutString(cmd.String) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/killedmonster/killedmonster.go b/packet/command/killedmonster/killedmonster.go new file mode 100644 index 0000000..1de4ad0 --- /dev/null +++ b/packet/command/killedmonster/killedmonster.go @@ -0,0 +1,17 @@ +package killedmonster + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCKilledMonster} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/lightstyle/lightstyle.go b/packet/command/lightstyle/lightstyle.go new file mode 100644 index 0000000..0b66c5c --- /dev/null +++ b/packet/command/lightstyle/lightstyle.go @@ -0,0 +1,37 @@ +package lightstyle + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Index byte + Command string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCLightStyle) + buf.PutByte(cmd.Index) + buf.PutString(cmd.Command) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Index, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Command, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/maxspeed/maxspeed.go b/packet/command/maxspeed/maxspeed.go new file mode 100644 index 0000000..ee11b79 --- /dev/null +++ b/packet/command/maxspeed/maxspeed.go @@ -0,0 +1,31 @@ +package maxspeed + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Command float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCMaxSpeed) + buf.PutFloat32(cmd.Command) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Command, err = buf.GetFloat32(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/modellist/modellist.go b/packet/command/modellist/modellist.go new file mode 100644 index 0000000..2df83b0 --- /dev/null +++ b/packet/command/modellist/modellist.go @@ -0,0 +1,83 @@ +package modellist + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + ProtocolVersion uint32 + NumModels byte + Models []string + Index byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCModelList) + + if cmd.ProtocolVersion >= 26 { + buf.PutByte(cmd.NumModels) + + for i := 0; i < len(cmd.Models); i++ { + buf.PutString(cmd.Models[i]) + } + buf.PutByte(0x00) + + buf.PutByte(cmd.Index) + } else { + for i := 0; i < len(cmd.Models); i++ { + buf.PutString(cmd.Models[i]) + } + buf.PutByte(0x00) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.ProtocolVersion = ctx.GetProtocolVersion() + + if cmd.ProtocolVersion >= 26 { + if cmd.NumModels, err = buf.ReadByte(); err != nil { + return nil, err + } + + for { + var model string + if model, err = buf.GetString(); err != nil { + return nil, err + } + + if model == "" { + break + } + + cmd.Models = append(cmd.Models, model) + } + + if cmd.Index, err = buf.ReadByte(); err != nil { + return nil, err + } + } else { + for { + var model string + if model, err = buf.GetString(); err != nil { + return nil, err + } + + if model == "" { + break + } + + cmd.Models = append(cmd.Models, model) + } + } + + return &cmd, nil +} diff --git a/packet/command/move/move.go b/packet/command/move/move.go new file mode 100644 index 0000000..d52bb27 --- /dev/null +++ b/packet/command/move/move.go @@ -0,0 +1,79 @@ +package move + +import ( + "slices" + + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/common/crc" + "github.com/osm/quake/packet/command/deltausercommand" + "github.com/osm/quake/protocol" +) + +type Command struct { + Checksum byte + Lossage byte + Null *deltausercommand.Command + Old *deltausercommand.Command + New *deltausercommand.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.CLCMove) + buf.PutByte(cmd.Checksum) + buf.PutByte(cmd.Lossage) + + if cmd.Null != nil { + buf.PutBytes(cmd.Null.Bytes()) + } + + if cmd.Old != nil { + buf.PutBytes(cmd.Old.Bytes()) + } + + if cmd.New != nil { + buf.PutBytes(cmd.New.Bytes()) + } + + return buf.Bytes() +} + +func (cmd *Command) GetChecksum(sequence uint32) byte { + b := slices.Concat( + []byte{cmd.Lossage}, + cmd.Null.Bytes(), + cmd.Old.Bytes(), + cmd.New.Bytes(), + ) + + return crc.Byte(b, int(sequence)) +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Checksum, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Lossage, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Null, err = deltausercommand.Parse(ctx, buf); err != nil { + return nil, err + } + + if cmd.Old, err = deltausercommand.Parse(ctx, buf); err != nil { + return nil, err + } + + if cmd.New, err = deltausercommand.Parse(ctx, buf); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/muzzleflash/muzzleflash.go b/packet/command/muzzleflash/muzzleflash.go new file mode 100644 index 0000000..9db50b2 --- /dev/null +++ b/packet/command/muzzleflash/muzzleflash.go @@ -0,0 +1,31 @@ +package muzzleflash + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex uint16 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCMuzzleFlash) + buf.PutUint16(cmd.PlayerIndex) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.GetUint16(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/mvdweapon/mvdweapon.go b/packet/command/mvdweapon/mvdweapon.go new file mode 100644 index 0000000..6e726f5 --- /dev/null +++ b/packet/command/mvdweapon/mvdweapon.go @@ -0,0 +1,54 @@ +package mvdweapon + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol/mvd" +) + +type Command struct { + Bits byte + Age byte + Weapons []byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(mvd.CLCWeapon) + buf.PutByte(cmd.Bits) + buf.PutByte(cmd.Age) + buf.PutBytes(cmd.Weapons) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Bits, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Bits&mvd.CLCWeaponForgetRanking != 0 { + if cmd.Age, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + for { + weapon, err := buf.ReadByte() + if err != nil { + return nil, err + } + + if weapon == 0 { + break + } + + cmd.Weapons = append(cmd.Weapons, weapon) + } + + return &cmd, nil +} diff --git a/packet/command/nails/nails.go b/packet/command/nails/nails.go new file mode 100644 index 0000000..a4a3987 --- /dev/null +++ b/packet/command/nails/nails.go @@ -0,0 +1,50 @@ +package nails + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Count byte + Command []Nail +} + +type Nail struct { + Bits []byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCNails) + buf.PutByte(cmd.Count) + + for i := 0; i < len(cmd.Command); i++ { + buf.PutBytes(cmd.Command[i].Bits) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Count, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < int(cmd.Count); i++ { + var nail Nail + + if nail.Bits, err = buf.GetBytes(6); err != nil { + return nil, err + } + + cmd.Command = append(cmd.Command, nail) + } + + return &cmd, nil +} diff --git a/packet/command/nails2/nails2.go b/packet/command/nails2/nails2.go new file mode 100644 index 0000000..9bf8038 --- /dev/null +++ b/packet/command/nails2/nails2.go @@ -0,0 +1,56 @@ +package nails2 + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Count byte + Nails []Nail2 +} + +type Nail2 struct { + Index byte + Bits []byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCNails2) + buf.PutByte(cmd.Count) + + for i := 0; i < len(cmd.Nails); i++ { + buf.PutByte(cmd.Nails[i].Index) + buf.PutBytes(cmd.Nails[i].Bits) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Count, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < int(cmd.Count); i++ { + var nail Nail2 + + if nail.Index, err = buf.ReadByte(); err != nil { + return nil, err + } + + if nail.Bits, err = buf.GetBytes(6); err != nil { + return nil, err + } + + cmd.Nails = append(cmd.Nails, nail) + } + + return &cmd, nil +} diff --git a/packet/command/nopc/nopc.go b/packet/command/nopc/nopc.go new file mode 100644 index 0000000..5610433 --- /dev/null +++ b/packet/command/nopc/nopc.go @@ -0,0 +1,17 @@ +package nopc + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.CLCNOP} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/nops/nops.go b/packet/command/nops/nops.go new file mode 100644 index 0000000..bcf354f --- /dev/null +++ b/packet/command/nops/nops.go @@ -0,0 +1,17 @@ +package nops + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCNOP} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/packetentities/packetentities.go b/packet/command/packetentities/packetentities.go new file mode 100644 index 0000000..5ce85ff --- /dev/null +++ b/packet/command/packetentities/packetentities.go @@ -0,0 +1,37 @@ +package packetentities + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/packetentity" + "github.com/osm/quake/protocol" +) + +type Command struct { + Entities []*packetentity.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCPacketEntities) + + for i := 0; i < len(cmd.Entities); i++ { + buf.PutBytes(cmd.Entities[i].Bytes()) + } + + buf.PutUint16(0x0000) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Entities, err = packetentity.Parse(ctx, buf); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/packetentity/packetentity.go b/packet/command/packetentity/packetentity.go new file mode 100644 index 0000000..6accec7 --- /dev/null +++ b/packet/command/packetentity/packetentity.go @@ -0,0 +1,90 @@ +package packetentity + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/packetentitydelta" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/fte" +) + +type Command struct { + FTEProtocolExtension uint32 + + Bits uint16 + MoreBits byte + EvenMoreBits byte + PacketEntityDelta *packetentitydelta.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutUint16(cmd.Bits) + + if cmd.Bits == 0 { + goto end + } + + if cmd.Bits&protocol.URemove != 0 { + if cmd.Bits&protocol.UMoreBits != 0 && + cmd.FTEProtocolExtension&fte.ExtensionEntityDbl != 0 { + buf.PutByte(cmd.MoreBits) + + if cmd.MoreBits&fte.UEvenMore != 0 { + buf.PutByte(cmd.EvenMoreBits) + } + } + goto end + } + + if cmd.PacketEntityDelta != nil { + buf.PutBytes(cmd.PacketEntityDelta.Bytes()) + } + +end: + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) ([]*Command, error) { + var err error + var cmds []*Command + + for { + var cmd Command + cmd.FTEProtocolExtension = ctx.GetFTEProtocolExtension() + + if cmd.Bits, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Bits == 0 { + break + } + + if cmd.Bits&protocol.URemove != 0 { + if cmd.Bits&protocol.UMoreBits != 0 && + cmd.FTEProtocolExtension&fte.ExtensionEntityDbl != 0 { + if cmd.MoreBits, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.MoreBits&fte.UEvenMore != 0 { + if cmd.EvenMoreBits, err = buf.ReadByte(); err != nil { + return nil, err + } + } + } + goto next + } + + cmd.PacketEntityDelta, err = packetentitydelta.Parse(ctx, buf, cmd.Bits) + if err != nil { + return nil, err + } + next: + cmds = append(cmds, &cmd) + } + + return cmds, nil +} diff --git a/packet/command/packetentitydelta/packetentitydelta.go b/packet/command/packetentitydelta/packetentitydelta.go new file mode 100644 index 0000000..d131020 --- /dev/null +++ b/packet/command/packetentitydelta/packetentitydelta.go @@ -0,0 +1,254 @@ +package packetentitydelta + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/fte" + "github.com/osm/quake/protocol/mvd" +) + +type Command struct { + AngleSize uint8 + CoordSize uint8 + FTEProtocolExtension uint32 + MVDProtocolExtension uint32 + + bits uint16 + Number uint16 + + MoreBits byte + EvenMoreBits byte + YetMoreBits byte + + ModelIndex byte + Frame byte + ColorMap byte + Skin byte + Effects byte + Coord [3]float32 + Angle [3]float32 + Trans byte + ColorMod [3]byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + writeAngle := buf.PutAngle8 + if cmd.AngleSize == 2 { + writeAngle = buf.PutAngle16 + } + + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 || cmd.MVDProtocolExtension&mvd.ExtensionFloatCoords != 0 { + writeCoord = buf.PutCoord32 + } + + bits := cmd.bits + bits &= ^uint16(511) + + if bits&protocol.UMoreBits != 0 { + buf.PutByte(cmd.MoreBits) + bits |= uint16(cmd.MoreBits) + } + + var moreBits uint16 + if bits&fte.UEvenMore != 0 && cmd.FTEProtocolExtension > 0 { + buf.PutByte(cmd.EvenMoreBits) + moreBits = uint16(cmd.EvenMoreBits) + + if cmd.EvenMoreBits&fte.UYetMore != 0 { + buf.PutByte(cmd.YetMoreBits) + moreBits |= uint16(cmd.YetMoreBits) << 8 + } + } + + if bits&protocol.UModel != 0 { + buf.PutByte(cmd.ModelIndex) + } + + if bits&protocol.UFrame != 0 { + buf.PutByte(cmd.Frame) + } + + if bits&protocol.UColorMap != 0 { + buf.PutByte(cmd.ColorMap) + } + + if bits&protocol.USkin != 0 { + buf.PutByte(cmd.Skin) + } + + if bits&protocol.UEffects != 0 { + buf.PutByte(cmd.Effects) + } + + if bits&protocol.UOrigin1 != 0 { + writeCoord(cmd.Coord[0]) + } + + if bits&protocol.UAngle1 != 0 { + writeAngle(cmd.Angle[0]) + } + + if bits&protocol.UOrigin2 != 0 { + writeCoord(cmd.Coord[1]) + } + + if bits&protocol.UAngle2 != 0 { + writeAngle(cmd.Angle[1]) + } + + if bits&protocol.UOrigin3 != 0 { + writeCoord(cmd.Coord[2]) + } + + if bits&protocol.UAngle3 != 0 { + writeAngle(cmd.Angle[2]) + } + + if moreBits&fte.UTrans != 0 && cmd.FTEProtocolExtension&fte.ExtensionTrans != 0 { + buf.PutByte(cmd.Trans) + } + + if moreBits&fte.UColorMod != 0 && cmd.FTEProtocolExtension&fte.ExtensionColorMod != 0 { + for i := 0; i < len(cmd.ColorMod); i++ { + buf.PutByte(cmd.ColorMod[i]) + } + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer, bits uint16) (*Command, error) { + var err error + var cmd Command + + cmd.FTEProtocolExtension = ctx.GetFTEProtocolExtension() + cmd.MVDProtocolExtension = ctx.GetMVDProtocolExtension() + + cmd.AngleSize = ctx.GetAngleSize() + readAngle := buf.GetAngle8 + if cmd.AngleSize == 2 { + readAngle = buf.GetAngle16 + } + + cmd.CoordSize = ctx.GetCoordSize() + readCoord := buf.GetCoord16 + if cmd.CoordSize == 4 || cmd.MVDProtocolExtension&mvd.ExtensionFloatCoords != 0 { + readCoord = buf.GetCoord32 + } + + cmd.Number = bits & 511 + cmd.bits = bits + + bits &= ^uint16(511) + + if bits&protocol.UMoreBits != 0 { + if cmd.MoreBits, err = buf.ReadByte(); err != nil { + return nil, err + } + + bits |= uint16(cmd.MoreBits) + } + + var moreBits uint16 + if bits&fte.UEvenMore != 0 && cmd.FTEProtocolExtension > 0 { + if cmd.EvenMoreBits, err = buf.ReadByte(); err != nil { + return nil, err + } + moreBits = uint16(cmd.EvenMoreBits) + + if cmd.EvenMoreBits&fte.UYetMore != 0 { + if cmd.YetMoreBits, err = buf.ReadByte(); err != nil { + return nil, err + } + yetMoreBits := uint16(cmd.YetMoreBits) + + moreBits |= yetMoreBits << 8 + } + } + + if bits&protocol.UModel != 0 { + if cmd.ModelIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits&protocol.UFrame != 0 { + if cmd.Frame, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits&protocol.UColorMap != 0 { + if cmd.ColorMap, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits&protocol.USkin != 0 { + if cmd.Skin, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits&protocol.UEffects != 0 { + if cmd.Effects, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits&protocol.UOrigin1 != 0 { + if cmd.Coord[0], err = readCoord(); err != nil { + return nil, err + } + } + + if bits&protocol.UAngle1 != 0 { + if cmd.Angle[0], err = readAngle(); err != nil { + return nil, err + } + } + + if bits&protocol.UOrigin2 != 0 { + if cmd.Coord[1], err = readCoord(); err != nil { + return nil, err + } + } + + if bits&protocol.UAngle2 != 0 { + if cmd.Angle[1], err = readAngle(); err != nil { + return nil, err + } + } + + if bits&protocol.UOrigin3 != 0 { + if cmd.Coord[2], err = readCoord(); err != nil { + return nil, err + } + } + + if bits&protocol.UAngle3 != 0 { + if cmd.Angle[2], err = readAngle(); err != nil { + return nil, err + } + } + + if moreBits&fte.UTrans != 0 && cmd.FTEProtocolExtension&fte.ExtensionTrans != 0 { + if cmd.Trans, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if moreBits&fte.UColorMod != 0 && cmd.FTEProtocolExtension&fte.ExtensionColorMod != 0 { + for i := 0; i < len(cmd.ColorMod); i++ { + if cmd.ColorMod[i], err = buf.ReadByte(); err != nil { + return nil, err + } + } + } + + return &cmd, nil +} diff --git a/packet/command/particle/particle.go b/packet/command/particle/particle.go new file mode 100644 index 0000000..b1f3942 --- /dev/null +++ b/packet/command/particle/particle.go @@ -0,0 +1,85 @@ +package particle + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + AngleSize uint8 + CoordSize uint8 + + Coord [3]float32 + Angle [3]float32 + Count byte + Color byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + writeAngle := buf.PutAngle8 + if cmd.AngleSize == 2 { + writeAngle = buf.PutAngle16 + } + + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 { + writeCoord = buf.PutCoord32 + } + + buf.PutByte(protocol.SVCParticle) + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + + for i := 0; i < 3; i++ { + writeAngle(cmd.Angle[i]) + } + + buf.PutByte(cmd.Count) + buf.PutByte(cmd.Color) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.AngleSize = ctx.GetAngleSize() + readAngle := buf.GetAngle8 + if cmd.AngleSize == 2 { + readAngle = buf.GetAngle16 + } + + cmd.CoordSize = ctx.GetCoordSize() + readCoord := buf.GetCoord16 + if cmd.CoordSize == 4 { + readCoord = buf.GetCoord32 + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + + for i := 0; i < 3; i++ { + if cmd.Angle[i], err = readAngle(); err != nil { + return nil, err + } + } + + if cmd.Count, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Color, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/passthrough/passthrough.go b/packet/command/passthrough/passthrough.go new file mode 100644 index 0000000..58c937c --- /dev/null +++ b/packet/command/passthrough/passthrough.go @@ -0,0 +1,38 @@ +package passthrough + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" +) + +type Command struct { + Data []byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutBytes(cmd.Data) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer, str string) (*Command, error) { + var cmd Command + + if len(str) > 0 { + cmd.Data = []byte(str) + } + + remaining := buf.Len() - buf.Off() + if remaining > 0 { + data, err := buf.GetBytes(remaining) + if err != nil { + return nil, err + } + + cmd.Data = append(cmd.Data, data...) + } + + return &cmd, nil +} diff --git a/packet/command/playerinfo/playerinfo.go b/packet/command/playerinfo/playerinfo.go new file mode 100644 index 0000000..962101b --- /dev/null +++ b/packet/command/playerinfo/playerinfo.go @@ -0,0 +1,337 @@ +package playerinfo + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/deltausercommand" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/fte" + "github.com/osm/quake/protocol/mvd" +) + +const fteExtensions uint32 = fte.ExtensionHullSize | + fte.ExtensionTrans | + fte.ExtensionScale | + fte.ExtensionFatness + +type Command struct { + IsMVD bool + + Index byte + Default *CommandDefault + MVD *CommandMVD +} + +type CommandDefault struct { + CoordSize uint8 + MVDProtocolExtension uint32 + FTEProtocolExtension uint32 + + Bits uint16 + ExtraBits byte + Coord [3]float32 + Frame byte + Msec byte + DeltaUserCommand *deltausercommand.Command + Velocity [3]uint16 + ModelIndex byte + SkinNum byte + Effects byte + WeaponFrame byte +} + +type CommandMVD struct { + CoordSize uint8 + + Bits uint16 + Frame byte + Coord [3]float32 + Angle [3]float32 + ModelIndex byte + SkinNum byte + Effects byte + WeaponFrame byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCPlayerInfo) + buf.PutByte(cmd.Index) + + if cmd.IsMVD && cmd.MVD != nil { + buf.PutBytes(cmd.MVD.Bytes()) + } else if cmd.Default != nil { + buf.PutBytes(cmd.Default.Bytes()) + } + + return buf.Bytes() +} + +func (cmd *CommandDefault) Bytes() []byte { + buf := buffer.New() + + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 || + cmd.MVDProtocolExtension&mvd.ExtensionFloatCoords != 0 || + cmd.FTEProtocolExtension&fteExtensions != 0 { + writeCoord = buf.PutCoord32 + } + + buf.PutUint16(cmd.Bits) + + if cmd.FTEProtocolExtension&fteExtensions != 0 && cmd.Bits&fte.UFarMore != 0 { + buf.PutByte(cmd.ExtraBits) + } + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + + buf.PutByte(cmd.Frame) + + if cmd.Bits&protocol.PFMsec != 0 { + buf.PutByte(cmd.Msec) + } + + if cmd.Bits&protocol.PFCommand != 0 { + buf.PutBytes(cmd.DeltaUserCommand.Bytes()) + } + + for i := 0; i < 3; i++ { + if cmd.Bits&(protocol.PFVelocity1<<i) != 0 { + buf.PutUint16(cmd.Velocity[i]) + } + } + + if cmd.Bits&protocol.PFModel != 0 { + buf.PutByte(cmd.ModelIndex) + } + + if cmd.Bits&protocol.PFSkinNum != 0 { + buf.PutByte(cmd.SkinNum) + } + + if cmd.Bits&protocol.PFEffects != 0 { + buf.PutByte(cmd.Effects) + } + + if cmd.Bits&protocol.PFWeaponFrame != 0 { + buf.PutByte(cmd.WeaponFrame) + } + + return buf.Bytes() +} + +func (cmd *CommandMVD) Bytes() []byte { + buf := buffer.New() + + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 { + writeCoord = buf.PutCoord32 + } + + buf.PutUint16(cmd.Bits) + buf.PutByte(cmd.Frame) + + for i := 0; i < 3; i++ { + if cmd.Bits&(protocol.DFOrigin<<i) != 0 { + writeCoord(cmd.Coord[i]) + } + } + + for i := 0; i < 3; i++ { + if cmd.Bits&(protocol.DFAngles<<i) != 0 { + buf.PutAngle16(cmd.Angle[i]) + } + } + + if cmd.Bits&protocol.DFModel != 0 { + buf.PutByte(cmd.ModelIndex) + } + + if cmd.Bits&protocol.DFSkinNum != 0 { + buf.PutByte(cmd.SkinNum) + } + + if cmd.Bits&protocol.DFEffects != 0 { + buf.PutByte(cmd.Effects) + } + + if cmd.Bits&protocol.DFWeaponFrame != 0 { + buf.PutByte(cmd.WeaponFrame) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.IsMVD = ctx.GetIsMVD() + + if cmd.Index, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.IsMVD { + if cmd.MVD, err = parseCommandMVD(ctx, buf); err != nil { + return nil, err + } + } else { + if cmd.Default, err = parseCommandDefault(ctx, buf); err != nil { + return nil, err + } + } + + return &cmd, nil +} + +func parseCommandDefault(ctx *context.Context, buf *buffer.Buffer) (*CommandDefault, error) { + var err error + var cmd CommandDefault + + cmd.FTEProtocolExtension = ctx.GetFTEProtocolExtension() + cmd.MVDProtocolExtension = ctx.GetMVDProtocolExtension() + cmd.CoordSize = ctx.GetCoordSize() + + readCoord := buf.GetCoord16 + + if cmd.CoordSize == 4 || + cmd.MVDProtocolExtension&mvd.ExtensionFloatCoords != 0 || + cmd.FTEProtocolExtension&fteExtensions != 0 { + readCoord = buf.GetCoord32 + } + + if cmd.Bits, err = buf.GetUint16(); err != nil { + return nil, err + } + + var bits uint32 = uint32(cmd.Bits) + + if cmd.FTEProtocolExtension&fteExtensions != 0 && bits&fte.UFarMore != 0 { + if cmd.ExtraBits, err = buf.ReadByte(); err != nil { + return nil, err + } + + bits = (uint32(cmd.ExtraBits) << 16) | uint32(cmd.Bits) + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + + if cmd.Frame, err = buf.ReadByte(); err != nil { + return nil, err + } + + if bits&protocol.PFMsec != 0 { + if cmd.Msec, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits&protocol.PFCommand != 0 { + if cmd.DeltaUserCommand, err = deltausercommand.Parse(ctx, buf); err != nil { + return nil, err + } + } + + for i := 0; i < 3; i++ { + if bits&(protocol.PFVelocity1<<i) != 0 { + if cmd.Velocity[i], err = buf.GetUint16(); err != nil { + return nil, err + } + } + } + + if bits&protocol.PFModel != 0 { + if cmd.ModelIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits&protocol.PFSkinNum != 0 { + if cmd.SkinNum, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits&protocol.PFEffects != 0 { + if cmd.Effects, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if bits&protocol.PFWeaponFrame != 0 { + if cmd.WeaponFrame, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + return &cmd, nil +} + +func parseCommandMVD(ctx *context.Context, buf *buffer.Buffer) (*CommandMVD, error) { + var err error + var cmd CommandMVD + + cmd.CoordSize = ctx.GetCoordSize() + readCoord := buf.GetCoord16 + if cmd.CoordSize == 4 { + readCoord = buf.GetCoord32 + } + + if cmd.Bits, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Frame, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Bits&(protocol.DFOrigin<<i) != 0 { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + } + + for i := 0; i < 3; i++ { + if cmd.Bits&(protocol.DFAngles<<i) != 0 { + if cmd.Angle[i], err = buf.GetAngle16(); err != nil { + return nil, err + } + } + } + + if cmd.Bits&protocol.DFModel != 0 { + if cmd.ModelIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.DFSkinNum != 0 { + if cmd.SkinNum, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.DFEffects != 0 { + if cmd.Effects, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.DFWeaponFrame != 0 { + if cmd.WeaponFrame, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/print/print.go b/packet/command/print/print.go new file mode 100644 index 0000000..5e6766b --- /dev/null +++ b/packet/command/print/print.go @@ -0,0 +1,47 @@ +package print + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + IsNQ bool + + ID byte + String string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCPrint) + + if !cmd.IsNQ { + buf.PutByte(cmd.ID) + } + + buf.PutString(cmd.String) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.IsNQ = ctx.GetIsNQ() + + if !cmd.IsNQ { + if cmd.ID, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.String, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/qizmovoice/qizmovoice.go b/packet/command/qizmovoice/qizmovoice.go new file mode 100644 index 0000000..4504ceb --- /dev/null +++ b/packet/command/qizmovoice/qizmovoice.go @@ -0,0 +1,29 @@ +package qizmovoice + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Data []byte +} + +func (cmd *Command) Bytes() []byte { + return append([]byte{ + protocol.SVCQizmoVoice}, + cmd.Data..., + ) +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Data, err = buf.GetBytes(34); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/qtvconnect/qtvconnect.go b/packet/command/qtvconnect/qtvconnect.go new file mode 100644 index 0000000..86adab3 --- /dev/null +++ b/packet/command/qtvconnect/qtvconnect.go @@ -0,0 +1,32 @@ +package qtvconnect + +import ( + "fmt" + + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/infostring" +) + +type Command struct { + Version byte + Extensions uint32 + Source string + UserInfo *infostring.InfoString +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutBytes([]byte("QTV\n")) + buf.PutBytes([]byte(fmt.Sprintf("VERSION: %d\n", cmd.Version))) + buf.PutBytes([]byte(fmt.Sprintf("QTV_EZQUAKE_EXT: %d\n", cmd.Extensions))) + buf.PutBytes([]byte(fmt.Sprintf("SOURCE: %s\n", cmd.Source))) + + if cmd.UserInfo != nil { + buf.PutBytes([]byte(fmt.Sprintf("USERINFO: %s\n", string(cmd.UserInfo.Bytes())))) + } + + buf.PutBytes([]byte("\n")) + + return buf.Bytes() +} diff --git a/packet/command/qtvstringcmd/qtvstringcmd.go b/packet/command/qtvstringcmd/qtvstringcmd.go new file mode 100644 index 0000000..c8cd49e --- /dev/null +++ b/packet/command/qtvstringcmd/qtvstringcmd.go @@ -0,0 +1,20 @@ +package qtvstringcmd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/protocol/qtv" +) + +type Command struct { + String string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutUint16(uint16(1+2+len(cmd.String)) + 1) + buf.PutByte(byte(qtv.CLCStringCmd)) + buf.PutString(cmd.String) + + return buf.Bytes() +} diff --git a/packet/command/s2cchallenge/s2cchallenge.go b/packet/command/s2cchallenge/s2cchallenge.go new file mode 100644 index 0000000..95cba70 --- /dev/null +++ b/packet/command/s2cchallenge/s2cchallenge.go @@ -0,0 +1,72 @@ +package s2cchallenge + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/fte" + "github.com/osm/quake/protocol/mvd" +) + +type Command struct { + ChallengeID string + Extensions []*protocol.Extension +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(byte(protocol.S2CChallenge)) + buf.PutString(cmd.ChallengeID) + + for _, ext := range cmd.Extensions { + buf.PutBytes(ext.Bytes()) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.ChallengeID, err = buf.GetString(); err != nil { + return nil, err + } + + for buf.Off() < buf.Len() { + version, err := buf.GetUint32() + if err != nil { + return nil, err + } + + extensions, err := buf.GetUint32() + if err != nil { + return nil, err + } + + if ctx.GetIsFTEEnabled() && version == fte.ProtocolVersion { + ctx.SetFTEProtocolExtension(extensions) + + if extensions&fte.ExtensionFloatCoords != 0 { + ctx.SetAngleSize(1) + ctx.SetCoordSize(2) + } + } + + if ctx.GetIsFTE2Enabled() && version == fte.ProtocolVersion2 { + ctx.SetFTE2ProtocolExtension(extensions) + } + + if ctx.GetIsMVDEnabled() && version == mvd.ProtocolVersion { + ctx.SetMVDProtocolExtension(extensions) + } + + cmd.Extensions = append(cmd.Extensions, &protocol.Extension{ + Version: version, + Extensions: extensions, + }) + } + + return &cmd, nil +} diff --git a/packet/command/s2cconnection/s2cconnection.go b/packet/command/s2cconnection/s2cconnection.go new file mode 100644 index 0000000..840f27c --- /dev/null +++ b/packet/command/s2cconnection/s2cconnection.go @@ -0,0 +1,17 @@ +package s2cconnection + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.S2CConnection} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/sellscreen/sellscreen.go b/packet/command/sellscreen/sellscreen.go new file mode 100644 index 0000000..3089ec3 --- /dev/null +++ b/packet/command/sellscreen/sellscreen.go @@ -0,0 +1,17 @@ +package sellscreen + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCSellScreen} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/serverdata/serverdata.go b/packet/command/serverdata/serverdata.go new file mode 100644 index 0000000..e5c6ab7 --- /dev/null +++ b/packet/command/serverdata/serverdata.go @@ -0,0 +1,285 @@ +package serverdata + +import ( + "errors" + + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/fte" + "github.com/osm/quake/protocol/mvd" +) + +var ( + ErrUnknownProtocolVersion = errors.New("unknown protocol version") +) + +type Command struct { + IsMVD bool + + ProtocolVersion uint32 + FTEProtocolExtension uint32 + FTE2ProtocolExtension uint32 + MVDProtocolExtension uint32 + + // NQ + MaxClients byte + GameType byte + SignOnMessage string + Models []string + Sounds []string + + // QW + ServerCount int32 + GameDirectory string + LastReceived float32 + PlayerNumber byte + Spectator bool + LevelName string + Gravity float32 + StopSpeed float32 + MaxSpeed float32 + SpectatorMaxSpeed float32 + Accelerate float32 + AirAccelerate float32 + WaterAccelerate float32 + Friction float32 + WaterFriction float32 + EntityGravity float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCServerData) + + if cmd.FTEProtocolExtension > 0 { + buf.PutUint32(fte.ProtocolVersion) + buf.PutUint32(cmd.FTEProtocolExtension) + } + + if cmd.FTE2ProtocolExtension > 0 { + buf.PutUint32(fte.ProtocolVersion2) + buf.PutUint32(cmd.FTE2ProtocolExtension) + } + + if cmd.MVDProtocolExtension > 0 { + buf.PutUint32(mvd.ProtocolVersion) + buf.PutUint32(cmd.MVDProtocolExtension) + } + + buf.PutUint32(uint32(cmd.ProtocolVersion)) + + if cmd.ProtocolVersion == protocol.VersionNQ { + buf.PutByte(cmd.MaxClients) + buf.PutByte(cmd.GameType) + buf.PutString(cmd.SignOnMessage) + + for i := 0; i < len(cmd.Models); i++ { + buf.PutString(cmd.Models[i]) + } + buf.PutByte(0x00) + + for i := 0; i < len(cmd.Sounds); i++ { + buf.PutString(cmd.Sounds[i]) + } + buf.PutByte(0x00) + } else { + buf.PutUint32(uint32(cmd.ServerCount)) + buf.PutString(cmd.GameDirectory) + + if cmd.IsMVD { + buf.PutFloat32(cmd.LastReceived) + } else { + buf.PutByte(cmd.PlayerNumber) + } + + buf.PutString(cmd.LevelName) + + if cmd.ProtocolVersion >= 25 { + buf.PutFloat32(cmd.Gravity) + buf.PutFloat32(cmd.StopSpeed) + buf.PutFloat32(cmd.MaxSpeed) + buf.PutFloat32(cmd.SpectatorMaxSpeed) + buf.PutFloat32(cmd.Accelerate) + buf.PutFloat32(cmd.AirAccelerate) + buf.PutFloat32(cmd.WaterAccelerate) + buf.PutFloat32(cmd.Friction) + buf.PutFloat32(cmd.WaterFriction) + buf.PutFloat32(cmd.EntityGravity) + } + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.IsMVD = ctx.GetIsMVD() + + for { + pv, err := buf.GetUint32() + if err != nil { + return nil, err + } + + if pv == fte.ProtocolVersion { + if cmd.FTEProtocolExtension, err = buf.GetUint32(); err != nil { + return nil, err + } + ctx.SetFTEProtocolExtension(cmd.FTEProtocolExtension) + if cmd.FTEProtocolExtension&fte.ExtensionFloatCoords != 0 { + ctx.SetAngleSize(2) + ctx.SetCoordSize(4) + } + continue + } + + if pv == fte.ProtocolVersion2 { + if cmd.FTE2ProtocolExtension, err = buf.GetUint32(); err != nil { + return nil, err + } + ctx.SetFTE2ProtocolExtension(cmd.FTE2ProtocolExtension) + continue + } + + if pv == mvd.ProtocolVersion { + if cmd.MVDProtocolExtension, err = buf.GetUint32(); err != nil { + return nil, err + } + ctx.SetMVDProtocolExtension(cmd.MVDProtocolExtension) + continue + } + + if pv == protocol.VersionNQ || pv == protocol.VersionQW || + pv == protocol.VersionQW210 || pv == protocol.VersionQW221 { + cmd.ProtocolVersion = pv + ctx.SetProtocolVersion(cmd.ProtocolVersion) + break + } + + return nil, ErrUnknownProtocolVersion + } + + if cmd.ProtocolVersion == protocol.VersionNQ { + if err = parseCommandNQ(ctx, buf, &cmd); err != nil { + return nil, err + } + } else { + if err = parseCommandQW(ctx, buf, &cmd); err != nil { + return nil, err + } + } + + return &cmd, nil +} + +func parseCommandNQ(ctx *context.Context, buf *buffer.Buffer, cmd *Command) error { + var err error + + if cmd.MaxClients, err = buf.ReadByte(); err != nil { + return err + } + + if cmd.GameType, err = buf.ReadByte(); err != nil { + return err + } + + if cmd.SignOnMessage, err = buf.GetString(); err != nil { + return err + } + + for { + var model string + if model, err = buf.GetString(); err != nil { + return err + } + + if model == "" { + break + } + + cmd.Models = append(cmd.Models, model) + } + + for { + var sound string + if sound, err = buf.GetString(); err != nil { + return err + } + + if sound == "" { + break + } + + cmd.Sounds = append(cmd.Sounds, sound) + } + + return nil +} + +func parseCommandQW(ctx *context.Context, buf *buffer.Buffer, cmd *Command) error { + var err error + + if cmd.ServerCount, err = buf.GetInt32(); err != nil { + return err + } + + if cmd.GameDirectory, err = buf.GetString(); err != nil { + return err + } + + if cmd.IsMVD { + if cmd.LastReceived, err = buf.GetFloat32(); err != nil { + return err + } + } else { + if cmd.PlayerNumber, err = buf.ReadByte(); err != nil { + return err + } + if cmd.PlayerNumber&128 != 0 { + cmd.Spectator = true + } + } + + if cmd.LevelName, err = buf.GetString(); err != nil { + return err + } + + if cmd.ProtocolVersion >= 25 { + if cmd.Gravity, err = buf.GetFloat32(); err != nil { + return err + } + if cmd.StopSpeed, err = buf.GetFloat32(); err != nil { + return err + } + if cmd.MaxSpeed, err = buf.GetFloat32(); err != nil { + return err + } + if cmd.SpectatorMaxSpeed, err = buf.GetFloat32(); err != nil { + return err + } + if cmd.Accelerate, err = buf.GetFloat32(); err != nil { + return err + } + if cmd.AirAccelerate, err = buf.GetFloat32(); err != nil { + return err + } + if cmd.WaterAccelerate, err = buf.GetFloat32(); err != nil { + return err + } + if cmd.Friction, err = buf.GetFloat32(); err != nil { + return err + } + if cmd.WaterFriction, err = buf.GetFloat32(); err != nil { + return err + } + if cmd.EntityGravity, err = buf.GetFloat32(); err != nil { + return err + } + } + + return nil +} diff --git a/packet/command/serverinfo/serverinfo.go b/packet/command/serverinfo/serverinfo.go new file mode 100644 index 0000000..f144d74 --- /dev/null +++ b/packet/command/serverinfo/serverinfo.go @@ -0,0 +1,37 @@ +package serverinfo + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Key string + Value string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCServerInfo) + buf.PutString(cmd.Key) + buf.PutString(cmd.Value) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Key, err = buf.GetString(); err != nil { + return nil, err + } + + if cmd.Value, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/setangle/setangle.go b/packet/command/setangle/setangle.go new file mode 100644 index 0000000..d9440da --- /dev/null +++ b/packet/command/setangle/setangle.go @@ -0,0 +1,66 @@ +package setangle + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/mvd" +) + +type Command struct { + AngleSize uint8 + MVDProtocolExtension uint32 + IsMVD bool + + MVDAngleIndex byte + Angle [3]float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + writeAngle := buf.PutAngle8 + if cmd.AngleSize == 2 { + writeAngle = buf.PutAngle16 + } + + buf.PutByte(protocol.SVCSetAngle) + + if cmd.IsMVD || cmd.MVDProtocolExtension&mvd.ExtensionHighLagTeleport != 0 { + buf.PutByte(cmd.MVDAngleIndex) + } + + for i := 0; i < 3; i++ { + writeAngle(cmd.Angle[i]) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.MVDProtocolExtension = ctx.GetMVDProtocolExtension() + cmd.IsMVD = ctx.GetIsMVD() + + cmd.AngleSize = ctx.GetAngleSize() + readAngle := buf.GetAngle8 + if cmd.AngleSize == 2 { + readAngle = buf.GetAngle16 + } + + if cmd.IsMVD || cmd.MVDProtocolExtension&mvd.ExtensionHighLagTeleport != 0 { + if cmd.MVDAngleIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + for i := 0; i < 3; i++ { + if cmd.Angle[i], err = readAngle(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/setinfo/setinfo.go b/packet/command/setinfo/setinfo.go new file mode 100644 index 0000000..6262ab8 --- /dev/null +++ b/packet/command/setinfo/setinfo.go @@ -0,0 +1,43 @@ +package setinfo + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex byte + Key string + Value string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCSetInfo) + buf.PutByte(cmd.PlayerIndex) + buf.PutString(cmd.Key) + buf.PutString(cmd.Value) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Key, err = buf.GetString(); err != nil { + return nil, err + } + + if cmd.Value, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/setpause/setpause.go b/packet/command/setpause/setpause.go new file mode 100644 index 0000000..5bf1679 --- /dev/null +++ b/packet/command/setpause/setpause.go @@ -0,0 +1,26 @@ +package setpause + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Paused byte +} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCSetPause, cmd.Paused} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Paused, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/setview/setview.go b/packet/command/setview/setview.go new file mode 100644 index 0000000..3b3f75c --- /dev/null +++ b/packet/command/setview/setview.go @@ -0,0 +1,31 @@ +package setview + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + ViewEntity uint16 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCSetView) + buf.PutUint16(cmd.ViewEntity) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.ViewEntity, err = buf.GetUint16(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/signonnum/signonnum.go b/packet/command/signonnum/signonnum.go new file mode 100644 index 0000000..95c487e --- /dev/null +++ b/packet/command/signonnum/signonnum.go @@ -0,0 +1,26 @@ +package signonnum + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex byte +} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCSignOnNum, cmd.PlayerIndex} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/smallkick/smallkick.go b/packet/command/smallkick/smallkick.go new file mode 100644 index 0000000..308d369 --- /dev/null +++ b/packet/command/smallkick/smallkick.go @@ -0,0 +1,17 @@ +package smallkick + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct{} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCSmallKick} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + return &Command{}, nil +} diff --git a/packet/command/sound/sound.go b/packet/command/sound/sound.go new file mode 100644 index 0000000..09c0149 --- /dev/null +++ b/packet/command/sound/sound.go @@ -0,0 +1,142 @@ +package sound + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + CoordSize uint8 + IsNQ bool + + Bits byte + SoundIndex byte + Channel uint16 + Volume byte + Attenuation byte + SoundNum byte + Coord [3]float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCSound) + + if cmd.IsNQ { + buf.PutByte(cmd.Bits) + + if cmd.Bits&protocol.NQSoundVolume != 0 { + buf.PutByte(cmd.Volume) + } + + if cmd.Bits&protocol.NQSoundAttenuation != 0 { + buf.PutByte(cmd.Attenuation) + } + + buf.PutUint16(cmd.Channel) + buf.PutByte(cmd.SoundIndex) + + for i := 0; i < 3; i++ { + buf.PutCoord16(cmd.Coord[i]) + } + } else { + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 { + writeCoord = buf.PutCoord32 + } + + buf.PutUint16(cmd.Channel) + + if cmd.Channel&protocol.SoundVolume != 0 { + buf.PutByte(cmd.Volume) + } + + if cmd.Channel&protocol.SoundAttenuation != 0 { + buf.PutByte(cmd.Attenuation) + } + + buf.PutByte(cmd.SoundNum) + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.IsNQ = ctx.GetIsNQ() + + if cmd.IsNQ { + if cmd.Bits, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Bits&protocol.NQSoundVolume != 0 { + if cmd.Volume, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Bits&protocol.NQSoundAttenuation != 0 { + if cmd.Attenuation, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Channel, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.SoundIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = buf.GetCoord16(); err != nil { + return nil, err + } + } + + } else { + cmd.CoordSize = ctx.GetCoordSize() + readCoord := buf.GetCoord16 + if cmd.CoordSize == 4 { + readCoord = buf.GetCoord32 + } + + if cmd.Channel, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Channel&protocol.SoundVolume != 0 { + if cmd.Volume, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.Channel&protocol.SoundAttenuation != 0 { + if cmd.Attenuation, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + if cmd.SoundNum, err = buf.ReadByte(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + } + + return &cmd, nil +} diff --git a/packet/command/soundlist/soundlist.go b/packet/command/soundlist/soundlist.go new file mode 100644 index 0000000..19ae09f --- /dev/null +++ b/packet/command/soundlist/soundlist.go @@ -0,0 +1,83 @@ +package soundlist + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + ProtocolVersion uint32 + NumSounds byte + Sounds []string + Index byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCSoundList) + + if cmd.ProtocolVersion >= 26 { + buf.PutByte(cmd.NumSounds) + + for i := 0; i < len(cmd.Sounds); i++ { + buf.PutString(cmd.Sounds[i]) + } + buf.PutByte(0x00) + + buf.PutByte(cmd.Index) + } else { + for i := 0; i < len(cmd.Sounds); i++ { + buf.PutString(cmd.Sounds[i]) + } + buf.PutByte(0x00) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.ProtocolVersion = ctx.GetProtocolVersion() + + if cmd.ProtocolVersion >= 26 { + if cmd.NumSounds, err = buf.ReadByte(); err != nil { + return nil, err + } + + for { + var sound string + if sound, err = buf.GetString(); err != nil { + return nil, err + } + + if sound == "" { + break + } + + cmd.Sounds = append(cmd.Sounds, sound) + } + + if cmd.Index, err = buf.ReadByte(); err != nil { + return nil, err + } + } else { + for { + var sound string + if sound, err = buf.GetString(); err != nil { + return nil, err + } + + if sound == "" { + break + } + + cmd.Sounds = append(cmd.Sounds, sound) + } + } + + return &cmd, nil +} diff --git a/packet/command/spawnbaseline/spawnbaseline.go b/packet/command/spawnbaseline/spawnbaseline.go new file mode 100644 index 0000000..55199c4 --- /dev/null +++ b/packet/command/spawnbaseline/spawnbaseline.go @@ -0,0 +1,41 @@ +package spawnbaseline + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/baseline" + "github.com/osm/quake/protocol" +) + +type Command struct { + Index uint16 + Baseline *baseline.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCSpawnBaseline) + buf.PutUint16(cmd.Index) + + if cmd.Baseline != nil { + buf.PutBytes(cmd.Baseline.Bytes()) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Index, err = buf.GetUint16(); err != nil { + return nil, err + } + + if cmd.Baseline, err = baseline.Parse(ctx, buf); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/spawnstatic/spawnstatic.go b/packet/command/spawnstatic/spawnstatic.go new file mode 100644 index 0000000..759d494 --- /dev/null +++ b/packet/command/spawnstatic/spawnstatic.go @@ -0,0 +1,35 @@ +package spawnstatic + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command/baseline" + "github.com/osm/quake/protocol" +) + +type Command struct { + Baseline *baseline.Command +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCSpawnStatic) + + if cmd.Baseline != nil { + buf.PutBytes(cmd.Baseline.Bytes()) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Baseline, err = baseline.Parse(ctx, buf); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/spawnstaticsound/spawnstaticsound.go b/packet/command/spawnstaticsound/spawnstaticsound.go new file mode 100644 index 0000000..435feab --- /dev/null +++ b/packet/command/spawnstaticsound/spawnstaticsound.go @@ -0,0 +1,68 @@ +package spawnstaticsound + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + CoordSize uint8 + + Coord [3]float32 + SoundIndex byte + Volume byte + Attenuation byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 { + writeCoord = buf.PutCoord32 + } + + buf.PutByte(protocol.SVCSpawnStaticSound) + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + + buf.PutByte(cmd.SoundIndex) + buf.PutByte(cmd.Volume) + buf.PutByte(cmd.Attenuation) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.CoordSize = ctx.GetCoordSize() + readCoord := buf.GetCoord16 + if cmd.CoordSize == 4 { + readCoord = buf.GetCoord32 + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + + if cmd.SoundIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Volume, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Attenuation, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/stopsound/stopsound.go b/packet/command/stopsound/stopsound.go new file mode 100644 index 0000000..96a4608 --- /dev/null +++ b/packet/command/stopsound/stopsound.go @@ -0,0 +1,31 @@ +package stopsound + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + SoundIndex uint16 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCStopSound) + buf.PutUint16(cmd.SoundIndex) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.SoundIndex, err = buf.GetUint16(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/stringcmd/stringcmd.go b/packet/command/stringcmd/stringcmd.go new file mode 100644 index 0000000..cc5f8dd --- /dev/null +++ b/packet/command/stringcmd/stringcmd.go @@ -0,0 +1,31 @@ +package stringcmd + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + String string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.CLCStringCmd) + buf.PutString(cmd.String) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.String, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/stufftext/stufftext.go b/packet/command/stufftext/stufftext.go new file mode 100644 index 0000000..0b323c1 --- /dev/null +++ b/packet/command/stufftext/stufftext.go @@ -0,0 +1,32 @@ +package stufftext + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + String string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCStuffText) + buf.PutString(cmd.String) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.String, err = buf.GetString() + if err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/tempentity/tempentity.go b/packet/command/tempentity/tempentity.go new file mode 100644 index 0000000..ebaf8c6 --- /dev/null +++ b/packet/command/tempentity/tempentity.go @@ -0,0 +1,176 @@ +package tempentity + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + IsNQ bool + CoordSize uint8 + + Type byte + + Coord [3]float32 + EndCoord [3]float32 + Entity uint16 + Count byte + ColorStart byte + ColorLength byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + writeCoord := buf.PutCoord16 + if cmd.CoordSize == 4 { + writeCoord = buf.PutCoord32 + } + + buf.PutByte(protocol.SVCTempEntity) + buf.PutByte(cmd.Type) + + switch cmd.Type { + case protocol.TELightning1: + fallthrough + case protocol.TELightning2: + fallthrough + case protocol.TELightning3: + buf.PutUint16(cmd.Entity) + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + + for i := 0; i < 3; i++ { + writeCoord(cmd.EndCoord[i]) + } + case protocol.TEGunshot: + fallthrough + case protocol.TEBlood: + if !cmd.IsNQ { + buf.PutByte(cmd.Count) + } + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + + if cmd.IsNQ && cmd.Type != protocol.TEGunshot { + buf.PutByte(cmd.ColorStart) + buf.PutByte(cmd.ColorLength) + } + case protocol.TELightningBlood: + if cmd.IsNQ { + buf.PutUint16(cmd.Entity) + } + + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + + if cmd.IsNQ { + for i := 0; i < 3; i++ { + writeCoord(cmd.EndCoord[i]) + } + } + default: + for i := 0; i < 3; i++ { + writeCoord(cmd.Coord[i]) + } + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.IsNQ = ctx.GetIsNQ() + + cmd.CoordSize = ctx.GetCoordSize() + readCoord := buf.GetCoord16 + if cmd.CoordSize == 4 { + readCoord = buf.GetCoord32 + } + + if cmd.Type, err = buf.ReadByte(); err != nil { + return nil, err + } + + switch cmd.Type { + case protocol.TELightning1: + fallthrough + case protocol.TELightning2: + fallthrough + case protocol.TELightning3: + if cmd.Entity, err = buf.GetUint16(); err != nil { + return nil, err + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + for i := 0; i < 3; i++ { + if cmd.EndCoord[i], err = readCoord(); err != nil { + return nil, err + } + } + case protocol.TEGunshot: + fallthrough + case protocol.TEBlood: + if !cmd.IsNQ { + if cmd.Count, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + + if cmd.IsNQ && cmd.Type != protocol.TEGunshot { + if cmd.ColorStart, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.ColorLength, err = buf.ReadByte(); err != nil { + return nil, err + } + } + case protocol.TELightningBlood: + if cmd.IsNQ { + if cmd.Entity, err = buf.GetUint16(); err != nil { + return nil, err + } + } + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + + if cmd.IsNQ { + for i := 0; i < 3; i++ { + if cmd.EndCoord[i], err = readCoord(); err != nil { + return nil, err + } + } + } + default: + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = readCoord(); err != nil { + return nil, err + } + } + } + + return &cmd, nil +} diff --git a/packet/command/time/time.go b/packet/command/time/time.go new file mode 100644 index 0000000..825d571 --- /dev/null +++ b/packet/command/time/time.go @@ -0,0 +1,31 @@ +package time + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Time float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCTime) + buf.PutFloat32(cmd.Time) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Time, err = buf.GetFloat32(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/tmove/tmove.go b/packet/command/tmove/tmove.go new file mode 100644 index 0000000..4733f2c --- /dev/null +++ b/packet/command/tmove/tmove.go @@ -0,0 +1,36 @@ +package tmove + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Coord [3]uint16 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.CLCTMove) + + for i := 0; i < 3; i++ { + buf.PutUint16(cmd.Coord[i]) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + for i := 0; i < 3; i++ { + if cmd.Coord[i], err = buf.GetUint16(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/updatecolors/updatecolors.go b/packet/command/updatecolors/updatecolors.go new file mode 100644 index 0000000..491ff72 --- /dev/null +++ b/packet/command/updatecolors/updatecolors.go @@ -0,0 +1,31 @@ +package updatecolors + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex byte + Color byte +} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCUpdateColors, cmd.PlayerIndex, cmd.Color} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Color, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/updateentertime/updateentertime.go b/packet/command/updateentertime/updateentertime.go new file mode 100644 index 0000000..da670e2 --- /dev/null +++ b/packet/command/updateentertime/updateentertime.go @@ -0,0 +1,37 @@ +package updateentertime + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex byte + EnterTime float32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCUpdateEnterTime) + buf.PutByte(cmd.PlayerIndex) + buf.PutFloat32(cmd.EnterTime) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.EnterTime, err = buf.GetFloat32(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/updatefrags/updatefrags.go b/packet/command/updatefrags/updatefrags.go new file mode 100644 index 0000000..5713bce --- /dev/null +++ b/packet/command/updatefrags/updatefrags.go @@ -0,0 +1,37 @@ +package updatefrags + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex byte + Frags int16 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCUpdateFrags) + buf.PutByte(cmd.PlayerIndex) + buf.PutInt16(cmd.Frags) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Frags, err = buf.GetInt16(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/updatename/updatename.go b/packet/command/updatename/updatename.go new file mode 100644 index 0000000..06e1bfd --- /dev/null +++ b/packet/command/updatename/updatename.go @@ -0,0 +1,37 @@ +package updatename + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex byte + Name string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCUpdateName) + buf.PutByte(cmd.PlayerIndex) + buf.PutString(cmd.Name) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Name, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/updateping/updateping.go b/packet/command/updateping/updateping.go new file mode 100644 index 0000000..412d9c5 --- /dev/null +++ b/packet/command/updateping/updateping.go @@ -0,0 +1,37 @@ +package updateping + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex byte + Ping int16 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCUpdatePing) + buf.PutByte(cmd.PlayerIndex) + buf.PutInt16(cmd.Ping) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Ping, err = buf.GetInt16(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/updatepl/updatepl.go b/packet/command/updatepl/updatepl.go new file mode 100644 index 0000000..782ed9a --- /dev/null +++ b/packet/command/updatepl/updatepl.go @@ -0,0 +1,31 @@ +package updatepl + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex byte + PL byte +} + +func (cmd *Command) Bytes() []byte { + return []byte{protocol.SVCUpdatePL, cmd.PlayerIndex, cmd.PL} +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.PL, err = buf.ReadByte(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/updatestat/updatestat.go b/packet/command/updatestat/updatestat.go new file mode 100644 index 0000000..0f4810a --- /dev/null +++ b/packet/command/updatestat/updatestat.go @@ -0,0 +1,53 @@ +package updatestat + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + IsNQ bool + + Stat byte + Value8 byte + Value32 uint32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCUpdateStat) + buf.PutByte(cmd.Stat) + + if cmd.IsNQ { + buf.PutUint32(cmd.Value32) + } else { + buf.PutByte(cmd.Value8) + } + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + cmd.IsNQ = ctx.GetIsNQ() + + if cmd.Stat, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.IsNQ { + if cmd.Value32, err = buf.GetUint32(); err != nil { + return nil, err + } + } else { + if cmd.Value8, err = buf.ReadByte(); err != nil { + return nil, err + } + } + + return &cmd, nil +} diff --git a/packet/command/updatestatlong/updatestatlong.go b/packet/command/updatestatlong/updatestatlong.go new file mode 100644 index 0000000..f45a0d9 --- /dev/null +++ b/packet/command/updatestatlong/updatestatlong.go @@ -0,0 +1,37 @@ +package updatestatlong + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Stat byte + Value int32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCUpdateStatLong) + buf.PutByte(cmd.Stat) + buf.PutInt32(cmd.Value) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Stat, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Value, err = buf.GetInt32(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/updateuserinfo/updateuserinfo.go b/packet/command/updateuserinfo/updateuserinfo.go new file mode 100644 index 0000000..ce08d4f --- /dev/null +++ b/packet/command/updateuserinfo/updateuserinfo.go @@ -0,0 +1,43 @@ +package updateuserinfo + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + PlayerIndex byte + UserID uint32 + UserInfo string +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCUpdateUserInfo) + buf.PutByte(cmd.PlayerIndex) + buf.PutUint32(cmd.UserID) + buf.PutString(cmd.UserInfo) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.PlayerIndex, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.UserID, err = buf.GetUint32(); err != nil { + return nil, err + } + + if cmd.UserInfo, err = buf.GetString(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/upload/upload.go b/packet/command/upload/upload.go new file mode 100644 index 0000000..a55d47f --- /dev/null +++ b/packet/command/upload/upload.go @@ -0,0 +1,43 @@ +package upload + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Size int16 + Percent byte + Data []byte +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.CLCUpload) + buf.PutInt16(cmd.Size) + buf.PutByte(cmd.Percent) + buf.PutBytes(cmd.Data) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Size, err = buf.GetInt16(); err != nil { + return nil, err + } + + if cmd.Percent, err = buf.ReadByte(); err != nil { + return nil, err + } + + if cmd.Data, err = buf.GetBytes(int(cmd.Size)); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/command/version/version.go b/packet/command/version/version.go new file mode 100644 index 0000000..60ee1b9 --- /dev/null +++ b/packet/command/version/version.go @@ -0,0 +1,31 @@ +package version + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/protocol" +) + +type Command struct { + Version uint32 +} + +func (cmd *Command) Bytes() []byte { + buf := buffer.New() + + buf.PutByte(protocol.SVCVersion) + buf.PutUint32(cmd.Version) + + return buf.Bytes() +} + +func Parse(ctx *context.Context, buf *buffer.Buffer) (*Command, error) { + var err error + var cmd Command + + if cmd.Version, err = buf.GetUint32(); err != nil { + return nil, err + } + + return &cmd, nil +} diff --git a/packet/packet.go b/packet/packet.go new file mode 100644 index 0000000..7b20c3f --- /dev/null +++ b/packet/packet.go @@ -0,0 +1,5 @@ +package packet + +type Packet interface { + Bytes() []byte +} diff --git a/packet/svc/connectionless.go b/packet/svc/connectionless.go new file mode 100644 index 0000000..6d42ae2 --- /dev/null +++ b/packet/svc/connectionless.go @@ -0,0 +1,67 @@ +package svc + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command" + "github.com/osm/quake/packet/command/a2aping" + "github.com/osm/quake/packet/command/a2cclientcommand" + "github.com/osm/quake/packet/command/a2cprint" + "github.com/osm/quake/packet/command/disconnect" + "github.com/osm/quake/packet/command/passthrough" + "github.com/osm/quake/packet/command/s2cchallenge" + "github.com/osm/quake/packet/command/s2cconnection" + "github.com/osm/quake/protocol" +) + +type Connectionless struct { + Command command.Command +} + +func (cmd *Connectionless) Bytes() []byte { + buf := buffer.New() + + buf.PutInt32(-1) + buf.PutBytes(cmd.Command.Bytes()) + + return buf.Bytes() +} + +func parseConnectionless(ctx *context.Context, buf *buffer.Buffer) (*Connectionless, error) { + var err error + var pkg Connectionless + + if err := buf.Skip(4); err != nil { + return nil, err + } + + typ, err := buf.ReadByte() + if err != nil { + return nil, err + } + + var cmd command.Command + switch protocol.CommandType(typ) { + case protocol.S2CConnection: + cmd, err = s2cconnection.Parse(ctx, buf) + case protocol.A2CClientCommand: + cmd, err = a2cclientcommand.Parse(ctx, buf) + case protocol.A2CPrint: + cmd, err = a2cprint.Parse(ctx, buf) + case protocol.A2APing: + cmd, err = a2aping.Parse(ctx, buf) + case protocol.S2CChallenge: + cmd, err = s2cchallenge.Parse(ctx, buf) + case protocol.SVCDisconnect: + cmd, err = disconnect.Parse(ctx, buf) + default: + cmd, err = passthrough.Parse(ctx, buf, "") + } + + if err != nil { + return nil, err + } + pkg.Command = cmd + + return &pkg, nil +} diff --git a/packet/svc/gamedata.go b/packet/svc/gamedata.go new file mode 100644 index 0000000..8d46cb0 --- /dev/null +++ b/packet/svc/gamedata.go @@ -0,0 +1,265 @@ +package svc + +import ( + "errors" + + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet/command" + "github.com/osm/quake/packet/command/bad" + "github.com/osm/quake/packet/command/bigkick" + "github.com/osm/quake/packet/command/cdtrack" + "github.com/osm/quake/packet/command/centerprint" + "github.com/osm/quake/packet/command/chokecount" + "github.com/osm/quake/packet/command/clientdata" + "github.com/osm/quake/packet/command/damage" + "github.com/osm/quake/packet/command/deltapacketentities" + "github.com/osm/quake/packet/command/disconnect" + "github.com/osm/quake/packet/command/download" + "github.com/osm/quake/packet/command/entgravity" + "github.com/osm/quake/packet/command/fastupdate" + "github.com/osm/quake/packet/command/finale" + "github.com/osm/quake/packet/command/foundsecret" + "github.com/osm/quake/packet/command/ftemodellist" + "github.com/osm/quake/packet/command/ftespawnbaseline" + "github.com/osm/quake/packet/command/ftespawnstatic" + "github.com/osm/quake/packet/command/ftevoicechats" + "github.com/osm/quake/packet/command/intermission" + "github.com/osm/quake/packet/command/killedmonster" + "github.com/osm/quake/packet/command/lightstyle" + "github.com/osm/quake/packet/command/maxspeed" + "github.com/osm/quake/packet/command/modellist" + "github.com/osm/quake/packet/command/muzzleflash" + "github.com/osm/quake/packet/command/nails" + "github.com/osm/quake/packet/command/nails2" + "github.com/osm/quake/packet/command/nops" + "github.com/osm/quake/packet/command/packetentities" + "github.com/osm/quake/packet/command/particle" + "github.com/osm/quake/packet/command/playerinfo" + "github.com/osm/quake/packet/command/print" + "github.com/osm/quake/packet/command/qizmovoice" + "github.com/osm/quake/packet/command/sellscreen" + "github.com/osm/quake/packet/command/serverdata" + "github.com/osm/quake/packet/command/serverinfo" + "github.com/osm/quake/packet/command/setangle" + "github.com/osm/quake/packet/command/setinfo" + "github.com/osm/quake/packet/command/setpause" + "github.com/osm/quake/packet/command/setview" + "github.com/osm/quake/packet/command/signonnum" + "github.com/osm/quake/packet/command/smallkick" + "github.com/osm/quake/packet/command/sound" + "github.com/osm/quake/packet/command/soundlist" + "github.com/osm/quake/packet/command/spawnbaseline" + "github.com/osm/quake/packet/command/spawnstatic" + "github.com/osm/quake/packet/command/spawnstaticsound" + "github.com/osm/quake/packet/command/stopsound" + "github.com/osm/quake/packet/command/stufftext" + "github.com/osm/quake/packet/command/tempentity" + "github.com/osm/quake/packet/command/time" + "github.com/osm/quake/packet/command/updatecolors" + "github.com/osm/quake/packet/command/updateentertime" + "github.com/osm/quake/packet/command/updatefrags" + "github.com/osm/quake/packet/command/updatename" + "github.com/osm/quake/packet/command/updateping" + "github.com/osm/quake/packet/command/updatepl" + "github.com/osm/quake/packet/command/updatestat" + "github.com/osm/quake/packet/command/updatestatlong" + "github.com/osm/quake/packet/command/updateuserinfo" + "github.com/osm/quake/packet/command/version" + "github.com/osm/quake/protocol" + "github.com/osm/quake/protocol/fte" +) + +var ErrUnknownCommandType = errors.New("unknown command type") + +type GameData struct { + IsMVD bool + IsNQ bool + + Seq uint32 + Ack uint32 + Commands []command.Command +} + +func (gd *GameData) Bytes() []byte { + buf := buffer.New() + + if gd.IsMVD || gd.IsNQ { + goto process + } + + buf.PutUint32(gd.Seq) + buf.PutUint32(gd.Ack) + +process: + for _, c := range gd.Commands { + buf.PutBytes(c.Bytes()) + } + + return buf.Bytes() +} + +func parseGameData(ctx *context.Context, buf *buffer.Buffer) (*GameData, error) { + var err error + var pkg GameData + + pkg.IsMVD = ctx.GetIsMVD() + pkg.IsNQ = ctx.GetIsNQ() + + if pkg.IsMVD || pkg.IsNQ { + goto process + } + + if pkg.Seq, err = buf.GetUint32(); err != nil { + return nil, err + } + + if pkg.Ack, err = buf.GetUint32(); err != nil { + return nil, err + } + +process: + var cmd command.Command + for buf.Off() < buf.Len() { + typ, err := buf.ReadByte() + if err != nil { + return nil, err + } + + if pkg.IsNQ && typ&128 != 0 { + cmd, err = fastupdate.Parse(ctx, buf, typ) + goto next + } + + switch protocol.CommandType(typ) { + case protocol.SVCBad: + cmd, err = bad.Parse(ctx, buf, protocol.SVCBad) + case protocol.SVCNOP: + cmd, err = nops.Parse(ctx, buf) + case protocol.SVCDisconnect: + cmd, err = disconnect.Parse(ctx, buf) + case protocol.SVCUpdateStat: + cmd, err = updatestat.Parse(ctx, buf) + case protocol.SVCVersion: + cmd, err = version.Parse(ctx, buf) + case protocol.SVCSetView: + cmd, err = setview.Parse(ctx, buf) + case protocol.SVCSound: + cmd, err = sound.Parse(ctx, buf) + case protocol.SVCTime: + cmd, err = time.Parse(ctx, buf) + case protocol.SVCPrint: + cmd, err = print.Parse(ctx, buf) + case protocol.SVCStuffText: + cmd, err = stufftext.Parse(ctx, buf) + case protocol.SVCSetAngle: + cmd, err = setangle.Parse(ctx, buf) + case protocol.SVCServerData: + cmd, err = serverdata.Parse(ctx, buf) + case protocol.SVCLightStyle: + cmd, err = lightstyle.Parse(ctx, buf) + case protocol.SVCUpdateName: + cmd, err = updatename.Parse(ctx, buf) + case protocol.SVCUpdateFrags: + cmd, err = updatefrags.Parse(ctx, buf) + case protocol.SVCClientData: + cmd, err = clientdata.Parse(ctx, buf) + case protocol.SVCStopSound: + cmd, err = stopsound.Parse(ctx, buf) + case protocol.SVCUpdateColors: + cmd, err = updatecolors.Parse(ctx, buf) + case protocol.SVCParticle: + cmd, err = particle.Parse(ctx, buf) + case protocol.SVCDamage: + cmd, err = damage.Parse(ctx, buf) + case protocol.SVCSpawnStatic: + cmd, err = spawnstatic.Parse(ctx, buf) + case protocol.SVCSpawnBaseline: + cmd, err = spawnbaseline.Parse(ctx, buf) + case protocol.SVCTempEntity: + cmd, err = tempentity.Parse(ctx, buf) + case protocol.SVCSetPause: + cmd, err = setpause.Parse(ctx, buf) + case protocol.SVCSignOnNum: + cmd, err = signonnum.Parse(ctx, buf) + case protocol.SVCCenterPrint: + cmd, err = centerprint.Parse(ctx, buf) + case protocol.SVCKilledMonster: + cmd, err = killedmonster.Parse(ctx, buf) + case protocol.SVCFoundSecret: + cmd, err = foundsecret.Parse(ctx, buf) + case protocol.SVCSpawnStaticSound: + cmd, err = spawnstaticsound.Parse(ctx, buf) + case protocol.SVCIntermission: + cmd, err = intermission.Parse(ctx, buf) + case protocol.SVCFinale: + cmd, err = finale.Parse(ctx, buf) + case protocol.SVCCDTrack: + cmd, err = cdtrack.Parse(ctx, buf) + case protocol.SVCSellScreen: + cmd, err = sellscreen.Parse(ctx, buf) + case protocol.SVCSmallKick: + cmd, err = smallkick.Parse(ctx, buf) + case protocol.SVCBigKick: + cmd, err = bigkick.Parse(ctx, buf) + case protocol.SVCUpdatePing: + cmd, err = updateping.Parse(ctx, buf) + case protocol.SVCUpdateEnterTime: + cmd, err = updateentertime.Parse(ctx, buf) + case protocol.SVCUpdateStatLong: + cmd, err = updatestatlong.Parse(ctx, buf) + case protocol.SVCMuzzleFlash: + cmd, err = muzzleflash.Parse(ctx, buf) + case protocol.SVCUpdateUserInfo: + cmd, err = updateuserinfo.Parse(ctx, buf) + case protocol.SVCDownload: + cmd, err = download.Parse(ctx, buf) + case protocol.SVCPlayerInfo: + cmd, err = playerinfo.Parse(ctx, buf) + case protocol.SVCNails: + cmd, err = nails.Parse(ctx, buf) + case protocol.SVCChokeCount: + cmd, err = chokecount.Parse(ctx, buf) + case protocol.SVCModelList: + cmd, err = modellist.Parse(ctx, buf) + case protocol.SVCSoundList: + cmd, err = soundlist.Parse(ctx, buf) + case protocol.SVCPacketEntities: + cmd, err = packetentities.Parse(ctx, buf) + case protocol.SVCDeltaPacketEntities: + cmd, err = deltapacketentities.Parse(ctx, buf) + case protocol.SVCMaxSpeed: + cmd, err = maxspeed.Parse(ctx, buf) + case protocol.SVCEntGravity: + cmd, err = entgravity.Parse(ctx, buf) + case protocol.SVCSetInfo: + cmd, err = setinfo.Parse(ctx, buf) + case protocol.SVCServerInfo: + cmd, err = serverinfo.Parse(ctx, buf) + case protocol.SVCUpdatePL: + cmd, err = updatepl.Parse(ctx, buf) + case protocol.SVCNails2: + cmd, err = nails2.Parse(ctx, buf) + case protocol.SVCQizmoVoice: + cmd, err = qizmovoice.Parse(ctx, buf) + case fte.SVCSpawnStatic: + cmd, err = ftespawnstatic.Parse(ctx, buf) + case fte.SVCModelListShort: + cmd, err = ftemodellist.Parse(ctx, buf) + case fte.SVCSpawnBaseline: + cmd, err = ftespawnbaseline.Parse(ctx, buf) + case fte.SVCVoiceChat: + cmd, err = ftevoicechats.Parse(ctx, buf) + default: + return nil, ErrUnknownCommandType + } + + next: + if err != nil { + return nil, err + } + pkg.Commands = append(pkg.Commands, cmd) + } + + return &pkg, nil +} diff --git a/packet/svc/parse.go b/packet/svc/parse.go new file mode 100644 index 0000000..a95a33a --- /dev/null +++ b/packet/svc/parse.go @@ -0,0 +1,18 @@ +package svc + +import ( + "github.com/osm/quake/common/buffer" + "github.com/osm/quake/common/context" + "github.com/osm/quake/packet" +) + +func Parse(ctx *context.Context, data []byte) (packet.Packet, error) { + buf := buffer.New(buffer.WithData(data)) + + header, _ := buf.PeekInt32() + if header == -1 { + return parseConnectionless(ctx, buf) + } + + return parseGameData(ctx, buf) +} diff --git a/protocol/extension.go b/protocol/extension.go new file mode 100644 index 0000000..5f17cff --- /dev/null +++ b/protocol/extension.go @@ -0,0 +1,22 @@ +package protocol + +import ( + "fmt" + + "github.com/osm/quake/common/buffer" +) + +type Extension struct { + Version uint32 + Extensions uint32 +} + +func (cmd *Extension) Bytes() []byte { + buf := buffer.New() + + buf.PutBytes([]byte(fmt.Sprintf("0x%x", cmd.Version))) + buf.PutBytes([]byte(fmt.Sprintf("0x%x", cmd.Extensions))) + buf.PutByte(0x0a) + + return buf.Bytes() +} diff --git a/protocol/fte/fte.go b/protocol/fte/fte.go new file mode 100644 index 0000000..39b8e2e --- /dev/null +++ b/protocol/fte/fte.go @@ -0,0 +1,82 @@ +package fte + +const ( + ProtocolVersion = ('F' << 0) + ('T' << 8) + ('E' << 16) + ('X' << 24) + ProtocolVersion2 = ('F' << 0) + ('T' << 8) + ('E' << 16) + ('2' << 24) +) + +const ( + ExtensionSetView = 0x00000001 + ExtensionScale = 0x00000002 + ExtensionLightStyleCol = 0x00000004 + ExtensionTrans = 0x00000008 + ExtensionView2 = 0x00000010 + ExtensionBulletEns = 0x00000020 + ExtensionAccurateTimings = 0x00000040 + ExtensionSoundDbl = 0x00000080 + ExtensionFatness = 0x00000100 + ExtensionHLBSP = 0x00000200 + ExtensionTEBullet = 0x00000400 + ExtensionHullSize = 0x00000800 + ExtensionModelDbl = 0x00001000 + ExtensionEntityDbl = 0x00002000 + ExtensionEntityDbl2 = 0x00004000 + ExtensionFloatCoords = 0x00008000 + ExtensionVWeap = 0x00010000 + ExtensionQ2BSP = 0x00020000 + ExtensionQ3BSP = 0x00040000 + ExtensionColorMod = 0x00080000 + ExtensionSplitScreen = 0x00100000 + ExtensionHexen2 = 0x00200000 + ExtensionSpawnStatic2 = 0x00400000 + ExtensionCustomTempEffects = 0x00800000 + Extension256PacketEntities = 0x01000000 + ExtensionNeverUsed1 = 0x02000000 + ExtensionShowPic = 0x04000000 + ExtensionSetAttachment = 0x08000000 + ExtensionNeverUsed2 = 0x10000000 + ExtensionChunkedDownloads = 0x20000000 + ExtensionCSQC = 0x40000000 + ExtensionDPFlags = 0x80000000 + + Extension2PrydonCursor = 0x00000001 + Extension2VoiceChat = 0x00000002 + Extension2SetAngleDelta = 0x00000004 + Extension2ReplacementDeltas = 0x00000008 + Extension2MaxPlayers = 0x00000010 + Extension2PredictionInfo = 0x00000020 + Extension2NewSizeEncoding = 0x00000040 + Extension2InfoBlobs = 0x00000080 + Extension2StunAware = 0x00000100 + Extension2VRInputs = 0x00000200 + Extension2LerpTime = 0x00000400 +) + +const ( + SVCSpawnStatic = 21 + SVCModelListShort = 60 + SVCSpawnBaseline = 66 + SVCVoiceChat = 84 +) + +const CLCVoiceChat = 83 + +const ( + UEvenMore = 1 << 7 + UScale = 1 << 0 + UTrans = 1 << 1 + UFatness = 1 << 2 + UModelDbl = 1 << 3 + UUnused1 = 1 << 4 + UEntityDbl = 1 << 5 + UEntityDbl2 = 1 << 6 + UYetMore = 1 << 7 + UDrawFlags = 1 << 8 + UAbsLight = 1 << 9 + UColorMod = 1 << 10 + UDPFlags = 1 << 11 + UTagInfo = 1 << 12 + ULight = 1 << 13 + UEffects16 = 1 << 14 + UFarMore = 1 << 15 +) diff --git a/protocol/mvd/mvd.go b/protocol/mvd/mvd.go new file mode 100644 index 0000000..7165bfd --- /dev/null +++ b/protocol/mvd/mvd.go @@ -0,0 +1,47 @@ +package mvd + +const ProtocolVersion = ('M' << 0) + ('V' << 8) + ('D' << 16) + ('1' << 24) + +const ( + ExtensionFloatCoords = 1 << 0 + ExtensionHighLagTeleport = 1 << 1 + ExtensionServerSideWeapon = 1 << 2 + ExtensionDebugWeapon = 1 << 3 + ExtensionDebugAntilag = 1 << 4 + ExtensionHiddenMessages = 1 << 5 + ExtensionServerSideWeapon2 = 1 << 6 + ExtensionIncludeInMVD = ExtensionHiddenMessages +) + +const ( + DemoMultiple = 3 + DemoSingle = 4 + DemoStats = 5 + DemoAll = 6 +) + +const ( + CLCWeapon = 200 + CLCWeaponModePresel = 1 << 0 + CLCWeaponModeIffiring = 1 << 1 + CLCWeaponForgetRanking = 1 << 2 + CLCWeaponHideAxe = 1 << 3 + CLCWeaponHideSg = 1 << 4 + CLCWeaponResetOnDeath = 1 << 5 + CLCWeaponSwitching = 1 << 6 + CLCWeaponFullImpulse = 1 << 7 +) + +const ( + HiddenAntilagPosition = 0 + HiddenUserCommand = 1 + HiddenUserCommandWeapon = 2 + HiddenDemoInfo = 3 + HiddenCommentaryTrack = 4 + HiddenCommentaryData = 5 + HiddenCommentaryTextSegment = 6 + HiddenDamangeDone = 7 + HiddenUserCommandWeaponServerSide = 8 + HiddenUserCommandWeaponInstruction = 9 + HiddenPausedDuration = 10 +) diff --git a/protocol/protocol.go b/protocol/protocol.go new file mode 100644 index 0000000..866ee35 --- /dev/null +++ b/protocol/protocol.go @@ -0,0 +1,343 @@ +package protocol + +const ( + VersionNQ uint32 = 15 + VersionQW210 uint32 = 25 + VersionQW221 uint32 = 26 + VersionQW uint32 = 28 +) + +type CommandType int + +const ( + S2CChallenge = 'c' + S2CConnection = 'j' + A2APing = 'k' + A2AAck = 'l' + A2ANack = 'm' + A2AEcho = 'e' + A2CPrint = 'n' + S2MHeartbeat = 'a' + A2CClientCommand = 'B' + S2MShutdown = 'C' +) + +const ( + SVCBad = 0 + SVCNOP = 1 + SVCDisconnect = 2 + SVCUpdateStat = 3 + SVCVersion = 4 + SVCSetView = 5 + SVCSound = 6 + SVCTime = 7 + SVCPrint = 8 + SVCStuffText = 9 + SVCSetAngle = 10 + SVCServerData = 11 + SVCLightStyle = 12 + SVCUpdateName = 13 + SVCUpdateFrags = 14 + SVCClientData = 15 + SVCStopSound = 16 + SVCUpdateColors = 17 + SVCParticle = 18 + SVCDamage = 19 + SVCSpawnStatic = 20 + SVCSpawnBaseline = 22 + SVCTempEntity = 23 + SVCSetPause = 24 + SVCSignOnNum = 25 + SVCCenterPrint = 26 + SVCKilledMonster = 27 + SVCFoundSecret = 28 + SVCSpawnStaticSound = 29 + SVCIntermission = 30 + SVCFinale = 31 + SVCCDTrack = 32 + SVCSellScreen = 33 + SVCSmallKick = 34 + SVCBigKick = 35 + SVCUpdatePing = 36 + SVCUpdateEnterTime = 37 + SVCUpdateStatLong = 38 + SVCMuzzleFlash = 39 + SVCUpdateUserInfo = 40 + SVCDownload = 41 + SVCPlayerInfo = 42 + SVCNails = 43 + SVCChokeCount = 44 + SVCModelList = 45 + SVCSoundList = 46 + SVCPacketEntities = 47 + SVCDeltaPacketEntities = 48 + SVCMaxSpeed = 49 + SVCEntGravity = 50 + SVCSetInfo = 51 + SVCServerInfo = 52 + SVCUpdatePL = 53 + SVCNails2 = 54 + SVCQizmoVoice = 83 +) + +const ( + CLCBad = 0 + CLCNOP = 1 + CLCDoubleMove = 2 + CLCMove = 3 + CLCStringCmd = 4 + CLCDelta = 5 + CLCTMove = 6 + CLCUpload = 7 +) + +const ( + PFMsec = 1 << 0 + PFCommand = 1 << 1 + PFVelocity1 = 1 << 2 + PFVelocity2 = 1 << 3 + PFVelocity3 = 1 << 4 + PFModel = 1 << 5 + PFSkinNum = 1 << 6 + PFEffects = 1 << 7 + PFWeaponFrame = 1 << 8 + PFDead = 1 << 9 + PFGib = 1 << 10 + PFNoGrav = 1 << 11 + PFPMCShift = 11 + PFPMCMask = 7 + PFOnground = 1 << 14 + PFSolid = 1 << 15 +) + +const ( + PMCNormal = 0 + PMCNormalJumpHeld = 1 + PMCOldSpectator = 2 + PMCSpectator = 3 + PMCFly = 4 + PMCNone = 5 + PMCLock = 6 + PMCExtra3 = 7 +) + +const ( + CMAngle1 = 1 << 0 + CMAngle3 = 1 << 1 + CMForward = 1 << 2 + CMSide = 1 << 3 + CMUp = 1 << 4 + CMButtons = 1 << 5 + CMImpulse = 1 << 6 + CMAngle2 = 1 << 7 + CMMsec = 1 << 7 +) + +const ( + DFOrigin = 1 + DFAngles = 1 << 3 + DFEffects = 1 << 6 + DFSkinNum = 1 << 7 + DFDead = 1 << 8 + DFGib = 1 << 9 + DFWeaponFrame = 1 << 10 + DFModel = 1 << 11 +) + +const ( + UOrigin1 = 1 << 9 + UOrigin2 = 1 << 10 + UOrigin3 = 1 << 11 + UAngle2 = 1 << 12 + UFrame = 1 << 13 + URemove = 1 << 14 + UMoreBits = 1 << 15 +) + +const ( + UAngle1 = 1 << 0 + UAngle3 = 1 << 1 + UModel = 1 << 2 + UColorMap = 1 << 3 + USkin = 1 << 4 + UEffects = 1 << 5 + USolid = 1 << 6 + UCheckMoreBits = (1 << 9) - 1 +) + +const ( + NQUMoreBits uint16 = 1 << 0 + NQUOrigin1 uint16 = 1 << 1 + NQUOrigin2 uint16 = 1 << 2 + NQUOrigin3 uint16 = 1 << 3 + NQUAngle2 uint16 = 1 << 4 + NQUNoLerp uint16 = 1 << 5 + NQUFrame uint16 = 1 << 6 + NQUSignal uint16 = 1 << 7 + NQUAngle1 uint16 = 1 << 8 + NQUAngle3 uint16 = 1 << 9 + NQUModel uint16 = 1 << 10 + NQUColorMap uint16 = 1 << 11 + NQUSkin uint16 = 1 << 12 + NQUEffects uint16 = 1 << 13 + NQULongEntity uint16 = 1 << 14 +) + +const ( + SoundAttenuation = 1 << 14 + SoundVolume = 1 << 15 + + NQSoundVolume = 1 << 0 + NQSoundAttenuation = 1 << 1 + NQSoundLooping = 1 << 2 +) + +const ( + PrintLow = 0 + PrintMedium = 1 + PrintHigh = 2 + PrintChat = 3 +) + +const ( + TESpike = 0 + TESuperSpike = 1 + TEGunshot = 2 + TEExplosion = 3 + TETarExplosion = 4 + TELightning1 = 5 + TELightning2 = 6 + TEWizSpike = 7 + TEKnightSpike = 8 + TELightning3 = 9 + TELavaSplash = 10 + TETEleport = 11 + TEBlood = 12 + TELightningBlood = 13 +) + +const ( + ButtonAttack = 1 << 0 + ButtonJump = 1 << 1 + ButtonUse = 1 << 2 + ButtonAttack2 = 1 << 3 +) + +const ( + DemoCmd = 0 + DemoRead = 1 + DemoSet = 2 +) + +const ( + MaxClStats = 32 + StatHealth = 0 + StatWeapon = 2 + StatAmmo = 3 + StatArmor = 4 + StatShells = 6 + StatNails = 7 + StatRockets = 8 + StatCells = 9 + StatActiveWeapon = 10 + StatTotalSecrets = 11 + StatTotalMonsters = 12 + StatSecrets = 13 + StatMonsters = 14 + StatItems = 15 +) + +const ( + ITShotgun = 1 << 0 + ITSuperShotgun = 1 << 1 + ITNailgun = 1 << 2 + ITSuperNailgun = 1 << 3 + ITGrenadeLauncher = 1 << 4 + ITRocketLauncher = 1 << 5 + ITLightning = 1 << 6 + ITSuperLightning = 1 << 7 + ITShells = 1 << 8 + ITNails = 1 << 9 + ITRockets = 1 << 10 + ITCells = 1 << 11 + ITAx = 1 << 12 + ITArmor1 = 1 << 13 + ITArmor2 = 1 << 14 + ITArmor3 = 1 << 15 + ITSuperhealth = 1 << 16 + ITKey1 = 1 << 17 + ITKey2 = 1 << 18 + ITInvisibility = 1 << 19 + ITInvulnerability = 1 << 20 + ITSuit = 1 << 21 + ITQuad = 1 << 22 + ITSigil1 = 1 << 28 + ITSigil2 = 1 << 29 + ITSigil3 = 1 << 30 + ITSigil4 = 1 << 31 +) + +const ( + SUViewHeight = 1 << 0 + SUIdealPitch = 1 << 1 + SUPunch1 = 1 << 2 + SUPunch2 = 1 << 3 + SUPunch3 = 1 << 4 + SUVelocity1 = 1 << 5 + SUVelocity2 = 1 << 6 + SUVelocity3 = 1 << 7 + SUItems = 1 << 9 + SUOnGround = 1 << 10 + SUInWater = 1 << 11 + SUWeaponFrame = 1 << 12 + SUArmor = 1 << 13 + SUWeapon = 1 << 14 +) + +const DownloadBlockSize = 1024 + +const ( + PlayerModel = 33168 + EyeModel = 6967 +) + +var MapChecksum = map[string]int{ + "dm1": 0xc5c7dab3, + "dm2": 0x65f63634, + "dm3": 0x15e20df8, + "dm4": 0x9c6fe4bf, + "dm5": 0xb02d48fd, + "dm6": 0x5208da2b, + "e1m1": 0xad07d882, + "e1m2": 0x67100127, + "e1m3": 0x3546324a, + "e1m4": 0xedda0675, + "e1m5": 0xa82c1c8a, + "e1m6": 0x2c0028e3, + "e1m7": 0x97d6fb1a, + "e1m8": 0x4b6e741, + "e2m1": 0xdcf57032, + "e2m2": 0xaf961d4d, + "e2m3": 0xfc992551, + "e2m4": 0xc3169bc9, + "e2m5": 0xbf028f3f, + "e2m6": 0x91a33b81, + "e2m7": 0x7a3fe018, + "e3m1": 0x90b20d21, + "e3m2": 0x9c6c7538, + "e3m3": 0xc3d05d18, + "e3m4": 0xb1790cb8, + "e3m5": 0x917a0631, + "e3m6": 0x2dc17df8, + "e3m7": 0x1039c1b1, + "e4m1": 0xbbf06350, + "e4m2": 0xfff8cb18, + "e4m3": 0x59bef08c, + "e4m4": 0x2d3b183f, + "e4m5": 0x699ce7f4, + "e4m6": 0x620ff98, + "e4m7": 0x9dec01ac, + "e4m8": 0x3cb46c57, + "end": 0xbbd4b4a5, + "start": 0x2a9a3763, +} diff --git a/protocol/qtv/qtv.go b/protocol/qtv/qtv.go new file mode 100644 index 0000000..30bd154 --- /dev/null +++ b/protocol/qtv/qtv.go @@ -0,0 +1,18 @@ +package qtv + +import "github.com/osm/quake/protocol" + +const ( + ProtocolVersion = "QTV_EZQUAKE_EXT" + Version = 1 +) + +const ( + ExtensionDownload = 1 << 0 + ExtensionSetInfo = 1 << 1 + ExtensionUserList = 1 << 2 +) + +const ( + CLCStringCmd protocol.CommandType = 1 +) diff --git a/protocol/zquake/zquake.go b/protocol/zquake/zquake.go new file mode 100644 index 0000000..b2f8b0d --- /dev/null +++ b/protocol/zquake/zquake.go @@ -0,0 +1,13 @@ +package zquake + +const ( + ExtensionPMType = 1 << 0 + ExtensionPMTypeNew = 1 << 1 + ExtensionViewHeight = 1 << 2 + ExtensionServerTime = 1 << 3 + ExtensionPitchLimits = 1 << 4 + ExtensionJoinObserver = 1 << 5 + ExtensionPFOnGround = 1 << 6 + ExtensionVWep = 1 << 7 + ExtensionPFSolid = 1 << 8 +) |
