aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar BanceDev 2026-02-16 16:31:54 -0500
committerGravatar BanceDev 2026-02-16 16:31:54 -0500
commitca90ebdfa8789654766c5d7969baa7afacd9ebd2 (patch)
tree9693e0c7a5af6713f4c5e39372dcf22d05844ec3
initial commitHEADmain
-rw-r--r--LICENSE.md21
-rw-r--r--README.md3
-rw-r--r--client/client.go13
-rw-r--r--client/quake/client.go107
-rw-r--r--client/quake/commands.go237
-rw-r--r--client/quake/move.go30
-rw-r--r--client/quake/net.go144
-rw-r--r--client/quake/option.go87
-rw-r--r--common/args/args.go79
-rw-r--r--common/args/args_test.go83
-rw-r--r--common/ascii/ascii.go75
-rw-r--r--common/buffer/buffer.go34
-rw-r--r--common/buffer/get.go189
-rw-r--r--common/buffer/get_test.go107
-rw-r--r--common/buffer/option.go9
-rw-r--r--common/buffer/put.go87
-rw-r--r--common/context/context.go236
-rw-r--r--common/context/option.go27
-rw-r--r--common/crc/crc.go106
-rw-r--r--common/crc/crc_test.go48
-rw-r--r--common/death/death.go132
-rw-r--r--common/death/parser.go55
-rw-r--r--common/death/template.go155
-rw-r--r--common/infostring/infostring.go72
-rw-r--r--common/infostring/infostring_test.go49
-rw-r--r--common/infostring/option.go9
-rw-r--r--common/loc/loc.go82
-rw-r--r--common/loc/loc_test.go24
-rw-r--r--common/rand/rand.go7
-rw-r--r--common/sequencer/option.go21
-rw-r--r--common/sequencer/sequencer.go145
-rw-r--r--demo/dem/data.go54
-rw-r--r--demo/dem/parse.go57
-rw-r--r--demo/dem/parse_test.go55
-rw-r--r--demo/dem/testdata/demo1.dembin0 -> 184471 bytes
-rw-r--r--demo/dem/testdata/demo2.dembin0 -> 152705 bytes
-rw-r--r--demo/dem/testdata/demo3.dembin0 -> 197679 bytes
-rw-r--r--demo/mvd/cmd.go93
-rw-r--r--demo/mvd/damagedone.go63
-rw-r--r--demo/mvd/demoinfo.go35
-rw-r--r--demo/mvd/hidden.go71
-rw-r--r--demo/mvd/mutliple.go58
-rw-r--r--demo/mvd/parse.go133
-rw-r--r--demo/mvd/parse_test.go59
-rw-r--r--demo/mvd/read.go46
-rw-r--r--demo/mvd/set.go35
-rw-r--r--demo/mvd/testdata/demo1.mvdbin0 -> 5535264 bytes
-rw-r--r--demo/mvd/testdata/demo2.mvdbin0 -> 816709 bytes
-rw-r--r--demo/mvd/testdata/demo3.mvdbin0 -> 4541251 bytes
-rw-r--r--demo/mvd/testdata/demo4.mvdbin0 -> 428923 bytes
-rw-r--r--demo/mvd/unknown.go25
-rw-r--r--demo/mvd/usercommand.go100
-rw-r--r--demo/mvd/weapon.go71
-rw-r--r--demo/mvd/weaponinstruction.go57
-rw-r--r--demo/mvd/weaponserverside.go75
-rw-r--r--demo/qwd/cmd.go94
-rw-r--r--demo/qwd/data.go32
-rw-r--r--demo/qwd/parse.go66
-rw-r--r--demo/qwd/parse_test.go67
-rw-r--r--demo/qwd/read.go42
-rw-r--r--demo/qwd/set.go35
-rw-r--r--demo/qwd/testdata/demo1.qwdbin0 -> 13326081 bytes
-rw-r--r--demo/qwd/testdata/demo2.qwdbin0 -> 14619553 bytes
-rw-r--r--demo/qwd/testdata/demo3.qwdbin0 -> 6568269 bytes
-rw-r--r--demo/qwd/testdata/demo4.qwdbin0 -> 3599960 bytes
-rw-r--r--demo/qwd/testdata/demo5.qwdbin0 -> 9282497 bytes
-rw-r--r--demo/qwd/testdata/demo6.qwdbin0 -> 13509417 bytes
-rw-r--r--example/client/main.go88
-rw-r--r--go.mod3
-rw-r--r--packet/clc/connectionless.go64
-rw-r--r--packet/clc/gamedata.go101
-rw-r--r--packet/clc/parse.go18
-rw-r--r--packet/command/a2aping/a2aping.go17
-rw-r--r--packet/command/a2cclientcommand/a2cclientcommand.go37
-rw-r--r--packet/command/a2cprint/a2cprint.go52
-rw-r--r--packet/command/bad/bad.go19
-rw-r--r--packet/command/baseline/baseline.go89
-rw-r--r--packet/command/bigkick/bigkick.go17
-rw-r--r--packet/command/cdtrack/cdtrack.go46
-rw-r--r--packet/command/centerprint/centerprint.go31
-rw-r--r--packet/command/chokecount/chokecount.go31
-rw-r--r--packet/command/clientdata/clientdata.go164
-rw-r--r--packet/command/command.go5
-rw-r--r--packet/command/connect/connect.go67
-rw-r--r--packet/command/damage/damage.go61
-rw-r--r--packet/command/delta/delta.go26
-rw-r--r--packet/command/deltapacketentities/deltapacketentities.go43
-rw-r--r--packet/command/deltausercommand/deltausercommand.go178
-rw-r--r--packet/command/disconnect/disconnect.go42
-rw-r--r--packet/command/download/download.go90
-rw-r--r--packet/command/entgravity/entgravity.go31
-rw-r--r--packet/command/fastupdate/fastupdate.go182
-rw-r--r--packet/command/finale/finale.go31
-rw-r--r--packet/command/foundsecret/foundsecret.go17
-rw-r--r--packet/command/ftedownload/ftedownload.go54
-rw-r--r--packet/command/ftemodellist/ftemodellist.go57
-rw-r--r--packet/command/ftespawnbaseline/ftespawnbaseline.go41
-rw-r--r--packet/command/ftespawnstatic/ftespawnstatic.go41
-rw-r--r--packet/command/ftevoicechatc/ftevoicechatc.go49
-rw-r--r--packet/command/ftevoicechats/ftevoicechats.go55
-rw-r--r--packet/command/getchallenge/getchallenge.go16
-rw-r--r--packet/command/intermission/intermission.go72
-rw-r--r--packet/command/ip/ip.go22
-rw-r--r--packet/command/killedmonster/killedmonster.go17
-rw-r--r--packet/command/lightstyle/lightstyle.go37
-rw-r--r--packet/command/maxspeed/maxspeed.go31
-rw-r--r--packet/command/modellist/modellist.go83
-rw-r--r--packet/command/move/move.go79
-rw-r--r--packet/command/muzzleflash/muzzleflash.go31
-rw-r--r--packet/command/mvdweapon/mvdweapon.go54
-rw-r--r--packet/command/nails/nails.go50
-rw-r--r--packet/command/nails2/nails2.go56
-rw-r--r--packet/command/nopc/nopc.go17
-rw-r--r--packet/command/nops/nops.go17
-rw-r--r--packet/command/packetentities/packetentities.go37
-rw-r--r--packet/command/packetentity/packetentity.go90
-rw-r--r--packet/command/packetentitydelta/packetentitydelta.go254
-rw-r--r--packet/command/particle/particle.go85
-rw-r--r--packet/command/passthrough/passthrough.go38
-rw-r--r--packet/command/playerinfo/playerinfo.go337
-rw-r--r--packet/command/print/print.go47
-rw-r--r--packet/command/qizmovoice/qizmovoice.go29
-rw-r--r--packet/command/qtvconnect/qtvconnect.go32
-rw-r--r--packet/command/qtvstringcmd/qtvstringcmd.go20
-rw-r--r--packet/command/s2cchallenge/s2cchallenge.go72
-rw-r--r--packet/command/s2cconnection/s2cconnection.go17
-rw-r--r--packet/command/sellscreen/sellscreen.go17
-rw-r--r--packet/command/serverdata/serverdata.go285
-rw-r--r--packet/command/serverinfo/serverinfo.go37
-rw-r--r--packet/command/setangle/setangle.go66
-rw-r--r--packet/command/setinfo/setinfo.go43
-rw-r--r--packet/command/setpause/setpause.go26
-rw-r--r--packet/command/setview/setview.go31
-rw-r--r--packet/command/signonnum/signonnum.go26
-rw-r--r--packet/command/smallkick/smallkick.go17
-rw-r--r--packet/command/sound/sound.go142
-rw-r--r--packet/command/soundlist/soundlist.go83
-rw-r--r--packet/command/spawnbaseline/spawnbaseline.go41
-rw-r--r--packet/command/spawnstatic/spawnstatic.go35
-rw-r--r--packet/command/spawnstaticsound/spawnstaticsound.go68
-rw-r--r--packet/command/stopsound/stopsound.go31
-rw-r--r--packet/command/stringcmd/stringcmd.go31
-rw-r--r--packet/command/stufftext/stufftext.go32
-rw-r--r--packet/command/tempentity/tempentity.go176
-rw-r--r--packet/command/time/time.go31
-rw-r--r--packet/command/tmove/tmove.go36
-rw-r--r--packet/command/updatecolors/updatecolors.go31
-rw-r--r--packet/command/updateentertime/updateentertime.go37
-rw-r--r--packet/command/updatefrags/updatefrags.go37
-rw-r--r--packet/command/updatename/updatename.go37
-rw-r--r--packet/command/updateping/updateping.go37
-rw-r--r--packet/command/updatepl/updatepl.go31
-rw-r--r--packet/command/updatestat/updatestat.go53
-rw-r--r--packet/command/updatestatlong/updatestatlong.go37
-rw-r--r--packet/command/updateuserinfo/updateuserinfo.go43
-rw-r--r--packet/command/upload/upload.go43
-rw-r--r--packet/command/version/version.go31
-rw-r--r--packet/packet.go5
-rw-r--r--packet/svc/connectionless.go67
-rw-r--r--packet/svc/gamedata.go265
-rw-r--r--packet/svc/parse.go18
-rw-r--r--protocol/extension.go22
-rw-r--r--protocol/fte/fte.go82
-rw-r--r--protocol/mvd/mvd.go47
-rw-r--r--protocol/protocol.go343
-rw-r--r--protocol/qtv/qtv.go18
-rw-r--r--protocol/zquake/zquake.go13
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
new file mode 100644
index 0000000..2b821c3
--- /dev/null
+++ b/demo/dem/testdata/demo1.dem
Binary files differ
diff --git a/demo/dem/testdata/demo2.dem b/demo/dem/testdata/demo2.dem
new file mode 100644
index 0000000..9dd006e
--- /dev/null
+++ b/demo/dem/testdata/demo2.dem
Binary files differ
diff --git a/demo/dem/testdata/demo3.dem b/demo/dem/testdata/demo3.dem
new file mode 100644
index 0000000..d25c402
--- /dev/null
+++ b/demo/dem/testdata/demo3.dem
Binary files differ
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
new file mode 100644
index 0000000..052b266
--- /dev/null
+++ b/demo/mvd/testdata/demo1.mvd
Binary files differ
diff --git a/demo/mvd/testdata/demo2.mvd b/demo/mvd/testdata/demo2.mvd
new file mode 100644
index 0000000..c8f32e4
--- /dev/null
+++ b/demo/mvd/testdata/demo2.mvd
Binary files differ
diff --git a/demo/mvd/testdata/demo3.mvd b/demo/mvd/testdata/demo3.mvd
new file mode 100644
index 0000000..374af38
--- /dev/null
+++ b/demo/mvd/testdata/demo3.mvd
Binary files differ
diff --git a/demo/mvd/testdata/demo4.mvd b/demo/mvd/testdata/demo4.mvd
new file mode 100644
index 0000000..d2589ec
--- /dev/null
+++ b/demo/mvd/testdata/demo4.mvd
Binary files differ
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
new file mode 100644
index 0000000..afc2c30
--- /dev/null
+++ b/demo/qwd/testdata/demo1.qwd
Binary files differ
diff --git a/demo/qwd/testdata/demo2.qwd b/demo/qwd/testdata/demo2.qwd
new file mode 100644
index 0000000..55f5306
--- /dev/null
+++ b/demo/qwd/testdata/demo2.qwd
Binary files differ
diff --git a/demo/qwd/testdata/demo3.qwd b/demo/qwd/testdata/demo3.qwd
new file mode 100644
index 0000000..e04d332
--- /dev/null
+++ b/demo/qwd/testdata/demo3.qwd
Binary files differ
diff --git a/demo/qwd/testdata/demo4.qwd b/demo/qwd/testdata/demo4.qwd
new file mode 100644
index 0000000..005a143
--- /dev/null
+++ b/demo/qwd/testdata/demo4.qwd
Binary files differ
diff --git a/demo/qwd/testdata/demo5.qwd b/demo/qwd/testdata/demo5.qwd
new file mode 100644
index 0000000..a78e779
--- /dev/null
+++ b/demo/qwd/testdata/demo5.qwd
Binary files differ
diff --git a/demo/qwd/testdata/demo6.qwd b/demo/qwd/testdata/demo6.qwd
new file mode 100644
index 0000000..78e6abc
--- /dev/null
+++ b/demo/qwd/testdata/demo6.qwd
Binary files differ
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)
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..8eee0f6
--- /dev/null
+++ b/go.mod
@@ -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
+)