aboutsummaryrefslogtreecommitdiffstats
path: root/common
diff options
context:
space:
mode:
Diffstat (limited to '')
-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
23 files changed, 1831 insertions, 0 deletions
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
+}